feat(all): 前端初始化
|
@ -0,0 +1 @@
|
||||||
|
VITE_API_BASE_URL= 'http://localhost:8080'
|
|
@ -0,0 +1,5 @@
|
||||||
|
/*.json
|
||||||
|
/src/**/*.json
|
||||||
|
dist
|
||||||
|
postcss.config.js
|
||||||
|
*.md
|
|
@ -0,0 +1,87 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: 'vue-eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
// Parser that checks the content of the <script> tag
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
'browser': true,
|
||||||
|
'node': true,
|
||||||
|
'vue/setup-compiler-macros': true,
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
extends: [
|
||||||
|
// Airbnb JavaScript Style Guide https://github.com/airbnb/javascript
|
||||||
|
'airbnb-base',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:import/recommended',
|
||||||
|
'plugin:import/typescript',
|
||||||
|
'plugin:vue/vue3-recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
project: path.resolve(__dirname, './tsconfig.json'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': 1,
|
||||||
|
// Vue: Recommended rules to be closed or modify
|
||||||
|
'vue/require-default-prop': 0,
|
||||||
|
'vue/singleline-html-element-content-newline': 0,
|
||||||
|
'vue/max-attributes-per-line': 0,
|
||||||
|
// Vue: Add extra rules
|
||||||
|
'vue/custom-event-name-casing': [2, 'camelCase'],
|
||||||
|
'vue/no-v-text': 1,
|
||||||
|
'vue/padding-line-between-blocks': 1,
|
||||||
|
'vue/require-direct-export': 1,
|
||||||
|
'vue/multi-word-component-names': 0,
|
||||||
|
// Allow @ts-ignore comment
|
||||||
|
'@typescript-eslint/ban-ts-comment': 0,
|
||||||
|
'@typescript-eslint/no-unused-vars': 1,
|
||||||
|
'@typescript-eslint/no-empty-function': 1,
|
||||||
|
'@typescript-eslint/no-explicit-any': 0,
|
||||||
|
'consistent-return': 'off',
|
||||||
|
'import/extensions': [
|
||||||
|
2,
|
||||||
|
'ignorePackages',
|
||||||
|
{
|
||||||
|
js: 'never',
|
||||||
|
jsx: 'never',
|
||||||
|
ts: 'never',
|
||||||
|
tsx: 'never',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||||
|
'no-param-reassign': 0,
|
||||||
|
'prefer-regex-literals': 0,
|
||||||
|
'import/no-extraneous-dependencies': 0,
|
||||||
|
'import/no-cycle': 'off',
|
||||||
|
'import/order': 'off',
|
||||||
|
'class-methods-use-this': 'off',
|
||||||
|
'global-require': 0,
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
},
|
||||||
|
// 对特定文件进行配置
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['src/enums/**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'no-shadow': 'off', // eslint会报错提示重复声明,暂未找到问题原因,先关闭
|
||||||
|
// 可以在这里添加更多的规则禁用
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
.history
|
||||||
|
coverage
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
pnpm commitlint --edit $1
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npm run lint-staged
|
|
@ -0,0 +1,7 @@
|
||||||
|
/dist/*
|
||||||
|
.local
|
||||||
|
.output.js
|
||||||
|
/node_modules/**
|
||||||
|
|
||||||
|
**/*.svg
|
||||||
|
**/*.sh
|
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: [require('prettier-plugin-tailwindcss')],
|
||||||
|
tabWidth: 2,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
quoteProps: 'consistent',
|
||||||
|
htmlWhitespaceSensitivity: 'strict',
|
||||||
|
vueIndentScriptAndStyle: true,
|
||||||
|
useTabs: false,
|
||||||
|
trailingComma: 'es5',
|
||||||
|
printWidth: 120,
|
||||||
|
arrowParens: 'always',
|
||||||
|
endOfLine: 'auto',
|
||||||
|
};
|
|
@ -0,0 +1,123 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'stylelint-config-standard',
|
||||||
|
'stylelint-config-prettier',
|
||||||
|
'stylelint-config-html/vue',
|
||||||
|
'stylelint-config-recommended-less',
|
||||||
|
],
|
||||||
|
plugins: ['stylelint-less', 'stylelint-order'],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.vue'],
|
||||||
|
customSyntax: 'postcss-html',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
customSyntax: 'postcss-less',
|
||||||
|
ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts', '**/*.json', 'node_modules/**/*'],
|
||||||
|
rules: {
|
||||||
|
'indentation': 2,
|
||||||
|
'selector-pseudo-element-no-unknown': [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
ignorePseudoElements: ['v-deep', ':deep'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'number-leading-zero': 'always',
|
||||||
|
'no-descending-specificity': null,
|
||||||
|
'function-url-quotes': 'always',
|
||||||
|
'string-quotes': 'single',
|
||||||
|
'unit-case': null,
|
||||||
|
'color-hex-case': 'lower',
|
||||||
|
'color-hex-length': 'long',
|
||||||
|
'rule-empty-line-before': 'never',
|
||||||
|
'font-family-no-missing-generic-family-keyword': null,
|
||||||
|
'selector-type-no-unknown': null,
|
||||||
|
'block-opening-brace-space-before': 'always',
|
||||||
|
'at-rule-no-unknown': null,
|
||||||
|
'no-duplicate-selectors': null,
|
||||||
|
'property-no-unknown': null,
|
||||||
|
'no-empty-source': null,
|
||||||
|
'selector-class-pattern': null,
|
||||||
|
'keyframes-name-pattern': null,
|
||||||
|
'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['global', 'deep'] }],
|
||||||
|
'function-no-unknown': null,
|
||||||
|
'order/properties-order': [
|
||||||
|
'position',
|
||||||
|
'top',
|
||||||
|
'right',
|
||||||
|
'bottom',
|
||||||
|
'left',
|
||||||
|
'z-index',
|
||||||
|
'display',
|
||||||
|
'justify-content',
|
||||||
|
'align-items',
|
||||||
|
'float',
|
||||||
|
'clear',
|
||||||
|
'overflow',
|
||||||
|
'overflow-x',
|
||||||
|
'overflow-y',
|
||||||
|
'margin',
|
||||||
|
'margin-top',
|
||||||
|
'margin-right',
|
||||||
|
'margin-bottom',
|
||||||
|
'margin-left',
|
||||||
|
'padding',
|
||||||
|
'padding-top',
|
||||||
|
'padding-right',
|
||||||
|
'padding-bottom',
|
||||||
|
'padding-left',
|
||||||
|
'width',
|
||||||
|
'min-width',
|
||||||
|
'max-width',
|
||||||
|
'height',
|
||||||
|
'min-height',
|
||||||
|
'max-height',
|
||||||
|
'font-size',
|
||||||
|
'font-family',
|
||||||
|
'font-weight',
|
||||||
|
'border',
|
||||||
|
'border-style',
|
||||||
|
'border-width',
|
||||||
|
'border-color',
|
||||||
|
'border-top',
|
||||||
|
'border-top-style',
|
||||||
|
'border-top-width',
|
||||||
|
'border-top-color',
|
||||||
|
'border-right',
|
||||||
|
'border-right-style',
|
||||||
|
'border-right-width',
|
||||||
|
'border-right-color',
|
||||||
|
'border-bottom',
|
||||||
|
'border-bottom-style',
|
||||||
|
'border-bottom-width',
|
||||||
|
'border-bottom-color',
|
||||||
|
'border-left',
|
||||||
|
'border-left-style',
|
||||||
|
'border-left-width',
|
||||||
|
'border-left-color',
|
||||||
|
'border-radius',
|
||||||
|
'text-align',
|
||||||
|
'text-justify',
|
||||||
|
'text-indent',
|
||||||
|
'text-overflow',
|
||||||
|
'text-decoration',
|
||||||
|
'white-space',
|
||||||
|
'color',
|
||||||
|
'background',
|
||||||
|
'background-position',
|
||||||
|
'background-repeat',
|
||||||
|
'background-size',
|
||||||
|
'background-color',
|
||||||
|
'background-clip',
|
||||||
|
'opacity',
|
||||||
|
'filter',
|
||||||
|
'list-style',
|
||||||
|
'outline',
|
||||||
|
'visibility',
|
||||||
|
'box-shadow',
|
||||||
|
'text-shadow',
|
||||||
|
'resize',
|
||||||
|
'transition',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true,
|
||||||
|
"source.fixAll.stylelint": true, // 开启stylelint自动修复
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"html",
|
||||||
|
"vue",
|
||||||
|
"markdown",
|
||||||
|
"json",
|
||||||
|
"jsonc"
|
||||||
|
],
|
||||||
|
// 配置stylelint检查的文件类型范围
|
||||||
|
"stylelint.validate": [
|
||||||
|
"css",
|
||||||
|
"less",
|
||||||
|
"postcss",
|
||||||
|
"scss",
|
||||||
|
"sass",
|
||||||
|
"vue"
|
||||||
|
],
|
||||||
|
"stylelint.enable": true,
|
||||||
|
"css.validate": false,
|
||||||
|
"less.validate": false,
|
||||||
|
"scss.validate": false,
|
||||||
|
}
|
|
@ -0,0 +1,805 @@
|
||||||
|
## 提交规范
|
||||||
|
|
||||||
|
### 1. 提交消息格式
|
||||||
|
|
||||||
|
```bin
|
||||||
|
<Type>[optional Scope]: <Description>
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Type 提交的类型
|
||||||
|
|
||||||
|
> 表示提交的目的或影响的范围
|
||||||
|
|
||||||
|
- feat: 新功能(A new feature)
|
||||||
|
- fix: 修复 bug(A bug fix)
|
||||||
|
- docs: 文档变更(Documentation changes)
|
||||||
|
- style: 代码样式调整(Code style changes)
|
||||||
|
- refactor: 代码重构(Code refactoring)
|
||||||
|
- test: 测试相关的变更(Test-related changes)
|
||||||
|
- chore: 构建过程或工具变动(Build process or tooling changes)
|
||||||
|
- perf: 性能优化(Performance optimization)
|
||||||
|
- ci: CI/CD 相关的变动(Changes to the CI/CD configuration or scripts)
|
||||||
|
- revert: 撤销之前的提交(Revert a previous commit)
|
||||||
|
|
||||||
|
#### Scope 作用域
|
||||||
|
|
||||||
|
提交的作用域,表示本次提交影响的部分代码或模块,可以根据项目的需要选择性地添加。
|
||||||
|
|
||||||
|
#### Description 描述
|
||||||
|
|
||||||
|
简明扼要地描述本次提交的内容
|
||||||
|
|
||||||
|
#### body 正文
|
||||||
|
|
||||||
|
可选的详细描述,可以包含更多的信息和上下文
|
||||||
|
|
||||||
|
#### footer 脚注
|
||||||
|
|
||||||
|
可选的脚注,通常用于引用相关的问题编号或关闭问题。
|
||||||
|
|
||||||
|
## 示例提交消息:
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(user): add login functionality
|
||||||
|
|
||||||
|
- Add login form and authentication logic
|
||||||
|
- Implement user authentication API endpoints
|
||||||
|
|
||||||
|
Closes #123
|
||||||
|
```
|
||||||
|
|
||||||
|
在这个示例中,提交类型为 feat(新功能),作用域为 user,描述了添加登录功能的内容。正文部分提供了更详细的说明,并引用了问题编号。
|
||||||
|
|
||||||
|
## 架构总览
|
||||||
|
|
||||||
|
```
|
||||||
|
├── babel.config.js // babel配置,支持JSX
|
||||||
|
├── commitlint.config.js // commitlint配置,校验commit信息
|
||||||
|
├── components.d.ts // 组件注册TS声明
|
||||||
|
├── config // 项目构建配置
|
||||||
|
│ ├── plugin // 构建插件
|
||||||
|
│ ├── utils // 构建工具方法
|
||||||
|
│ ├── vite.config.base.ts // vite基础配置
|
||||||
|
│ ├── vite.config.dev.ts // vite开发环境配置
|
||||||
|
│ └── vite.config.prod.ts // vite 生产配置
|
||||||
|
├── index.html // 单页面html模板
|
||||||
|
├── src
|
||||||
|
│ ├── App.vue // 应用入口vue
|
||||||
|
│ ├── api // 项目请求api封装
|
||||||
|
│ │ ├── http // axios封装
|
||||||
|
│ │ ├── modules // 各业务模块的请求方法
|
||||||
|
│ │ ├── requrls // 按业务模块划分的接口地址
|
||||||
|
│ ├── assets // 全局静态资源
|
||||||
|
│ │ ├── images
|
||||||
|
- svg
|
||||||
|
│ │ ├── logo.svg
|
||||||
|
│ │ ├── style
|
||||||
|
│ ├── components // 组件
|
||||||
|
│ ├── config // 全局配置,常量类、JSON
|
||||||
|
│ ├── directive // 自定义指令集
|
||||||
|
│ ├── enums // 全局枚举定义
|
||||||
|
│ ├── hooks // 全局hooks集
|
||||||
|
│ ├── layout // 应用布局组件
|
||||||
|
│ ├── locale // 国际化配置
|
||||||
|
│ ├── main.ts // 项目主入口
|
||||||
|
│ ├── mock // mock数据配置
|
||||||
|
│ ├── models // 全局数据模型定义
|
||||||
|
│ ├── router // 路由
|
||||||
|
│ ├── store // pinia状态库
|
||||||
|
│ ├── types // 全局TS声明
|
||||||
|
│ ├── utils // 公共工具方法
|
||||||
|
│ └── views
|
||||||
|
│ ├── modules // 页面模块
|
||||||
|
│ └── base // 公共页面,403、404等
|
||||||
|
│ ├── env.d.ts // 环境信息TS类型声明
|
||||||
|
└── .env.development // 开发环境变量声明
|
||||||
|
└── .env.production // 生产环境变量声明
|
||||||
|
└── .eslintrc.js // eslint配置
|
||||||
|
└── .prettierrc.js // prettier配置
|
||||||
|
└── tsconfig.json // 全局TS配置
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="dd14f832"></a>
|
||||||
|
|
||||||
|
## -状态管理模块设计
|
||||||
|
|
||||||
|
Vue3 状态管理方案采用的是`pinia`,API 风格与`Redux`类的状态管理库类似,也是通过模块化的方式注册`模块store`,每个`store`中包含数据仓库`state`、数据包装过滤`getter`、同步/异步操作`action`。与`Vuex`相比,`pinia`提供了`compasition-API`、完整支持 TS 以及可扩展的`Plugin`功能。<br />整体模块划分为业务模块`modules/*`、注册入口`index`以及插件`plugins/*`。<br />首先,在`store/index.ts`中声明注册`pinia`并引入自定义的插件:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||||
|
import { debouncePlugin } from './plugins';
|
||||||
|
import useXXStore from './modules/xx';
|
||||||
|
|
||||||
|
const pinia = createPinia();
|
||||||
|
// 插件会在实例创建后应用,因此插件不能在pinia实例创建前使用
|
||||||
|
pinia.use(debouncePlugin).use(piniaPluginPersistedstate);
|
||||||
|
export { useXXStore }; // 导出模块store
|
||||||
|
export default pinia;
|
||||||
|
```
|
||||||
|
|
||||||
|
然后,在项目入口`mian.ts`中使用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
import store from '@/store';
|
||||||
|
import app from '@/App.vue';
|
||||||
|
|
||||||
|
createApp(app).use(store).mount('#app');
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="piniaPlugins.ts"></a>
|
||||||
|
|
||||||
|
### plugins
|
||||||
|
|
||||||
|
在此文件内编写`pinia`插件,并在注册的时候引入使用即可:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { debounce, isObject } from 'lodash-es';
|
||||||
|
// 首先得声明插件使用到的额外属性,因为pinia的TS类型声明中,每个store只有三个原生属性state、getters、actions,若没有使用到额外属性则无需声明
|
||||||
|
declare module 'pinia' {
|
||||||
|
export interface DefineStoreOptionsBase<S, Store> {
|
||||||
|
cache?: Partial<CacheType<S, Store>>; // 缓存配置
|
||||||
|
debounce?: Partial<Record<keyof StoreActions<Store>, number>>; // 节流配置
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 基于lodash的防抖函数封装,读取store中cache配置的属性,针对已配置的属性更改操作进行防抖处理
|
||||||
|
export const debouncePlugin = ({ options, store }: PiniaPluginContext) => {
|
||||||
|
if (options.debounce) {
|
||||||
|
return Object.keys(options.debounce).reduce((debounceActions: debounceAction, action) => {
|
||||||
|
debounceActions[action] = debounce(store[action], options.debounce![action]);
|
||||||
|
return debounceActions;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
上述自定义插件,在 store 中如下配置:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const useStore = defineStore('demo', {
|
||||||
|
state: () => ({
|
||||||
|
testDebounce: '',
|
||||||
|
}),
|
||||||
|
getters: {},
|
||||||
|
actions: {},
|
||||||
|
debounce: {
|
||||||
|
// 防抖设置
|
||||||
|
testDebounce: 500, // 值为防抖缓冲时间
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="520bb47a"></a>
|
||||||
|
|
||||||
|
## -网络模块设计
|
||||||
|
|
||||||
|
网络模块包含:请求 url 封装`api/requrls/*`、请求方法封装`api/modules/*`、请求工具封装`api/http/*`。
|
||||||
|
<a name="slaQE"></a>
|
||||||
|
|
||||||
|
### requrls
|
||||||
|
|
||||||
|
将项目接口地址收敛至此文件夹下管理,避免出现一个项目多个重复接口 url、方便接口地址复用且方便统一处理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const LoginUrl = '/api/user/login';
|
||||||
|
export const LogoutUrl = '/api/user/logout';
|
||||||
|
export const GetUserInfoUrl = '/api/user/info';
|
||||||
|
export const GetMenuListUrl = '/api/user/menu';
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="y561q"></a>
|
||||||
|
|
||||||
|
### modules
|
||||||
|
|
||||||
|
将项目实际请求方法按业务模块划分,统一管理实际业务请求,将请求方法与接口地址解耦
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import axios from 'axios';
|
||||||
|
import { LoginUrl, LogoutUrl, GetUserInfoUrl, GetMenuListUrl } from '@/api/requrls/user';
|
||||||
|
import type { RouteRecordNormalized } from 'vue-router';
|
||||||
|
import type { UserState } from '@/store/modules/user/types';
|
||||||
|
import type { LoginData, LoginRes } from '@/models/user';
|
||||||
|
|
||||||
|
export function login(data: LoginData) {
|
||||||
|
return axios.post<LoginRes>(LoginUrl, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
return axios.post<LoginRes>(LogoutUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserInfo() {
|
||||||
|
return axios.post<UserState>(GetUserInfoUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMenuList() {
|
||||||
|
return axios.post<RouteRecordNormalized[]>(GetMenuListUrl);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
最终通过`index.ts`将请求方法暴露出去
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export * from './modules/user';
|
||||||
|
export * from './modules/dashboard';
|
||||||
|
export * from './modules/message';
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="0f0a9627"></a>
|
||||||
|
|
||||||
|
#### 请求工具封装
|
||||||
|
|
||||||
|
基于`axios`封装请求方法,提供`form-data/json/urlencoded`格式的数据处理、自定义头部处理、响应拦截错误处理、分级提示(modal、message、none)、get 请求防缓存。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 分级错误信息提示,none为静默模式即不提示、modal为对话框提示、message为tips消息提示
|
||||||
|
// ErrorMessageMode默认为message模式,通过传入请求方法的options.errorMessageMode入参判断
|
||||||
|
export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined;
|
||||||
|
|
||||||
|
// 三种入参数据格式
|
||||||
|
export enum ContentTypeEnum {
|
||||||
|
// json
|
||||||
|
JSON = 'application/json;charset=UTF-8',
|
||||||
|
// form-data qs序列化处理
|
||||||
|
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||||
|
// form-data upload文件流处理
|
||||||
|
FORM_DATA = 'multipart/form-data;charset=UTF-8',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对上传文件请求入参格式额外定义
|
||||||
|
export interface UploadFileParams {
|
||||||
|
// 除文件流外的参数
|
||||||
|
data?: Recordable;
|
||||||
|
// 文件流字段名
|
||||||
|
name?: string;
|
||||||
|
// 文件内容
|
||||||
|
file: File | Blob;
|
||||||
|
// 文件名
|
||||||
|
filename?: string;
|
||||||
|
// 其他parmas
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基准返回数据格式
|
||||||
|
export interface Result<T = any> {
|
||||||
|
code: number;
|
||||||
|
type: 'success' | 'error' | 'warning';
|
||||||
|
message: string;
|
||||||
|
result: T;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="c53518d5"></a>
|
||||||
|
|
||||||
|
## -directive 指令集
|
||||||
|
|
||||||
|
指令入口`index.ts`导入并注册定义的全部指令
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { App } from 'vue';
|
||||||
|
import permission from './permission';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(Vue: App) {
|
||||||
|
Vue.directive('permission', permission);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
权限指令(或其他自定义指令)在同级目录下创建,以指令名称命名文件夹,下面是示例:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DirectiveBinding } from 'vue';
|
||||||
|
import { useUserStore } from '@/store';
|
||||||
|
|
||||||
|
function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
|
const { value } = binding;
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { role } = userStore;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length > 0) {
|
||||||
|
const permissionValues = value;
|
||||||
|
const hasPermission = permissionValues.includes(role);
|
||||||
|
if (!hasPermission && el.parentNode) {
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`need roles! Like v-permission="['admin','user']"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
|
checkPermission(el, binding);
|
||||||
|
},
|
||||||
|
updated(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
|
checkPermission(el, binding);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="Kw0kR"></a>
|
||||||
|
|
||||||
|
## -hooks
|
||||||
|
|
||||||
|
全局抽象钩子集(与 Vue2 的 mixins 类似),这里只写业务逻辑的钩子!!!通过`@vueuse/core`我们已经可以得到非常多实用的钩子函数,没必要重复造轮子,在编写钩子功能前先去[Function List](https://vueuse.org/functions.html)查看是否已经有相同功能的钩子了,没有再自己写。导出钩子`export default function useXxx`,以权限钩子示例:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
|
||||||
|
import { useUserStore } from '@/store';
|
||||||
|
|
||||||
|
export default function usePermission() {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
return {
|
||||||
|
accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
|
||||||
|
// do something
|
||||||
|
},
|
||||||
|
findFirstPermissionRoute(_routers: any, role = 'admin') {
|
||||||
|
// do something
|
||||||
|
},
|
||||||
|
// You can add any rules you want
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="In07K"></a>
|
||||||
|
|
||||||
|
## -layout
|
||||||
|
|
||||||
|
项目布局设计模块,将最上层布局组件放置此文件夹内,统一管理项目各类上层布局(若存在,例如左侧菜单-右侧内容布局、顶层菜单-中间内容-底部页脚布局等等),示例:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- router-view内为实际路由切换变化的内容,视为路由页面容器 -->
|
||||||
|
<router-view v-slot="{ Component, route }">
|
||||||
|
<!-- transition为页面录音切换时提供进出动画,平滑过渡页面渲染 -->
|
||||||
|
<transition name="fade" mode="out-in" appear>
|
||||||
|
<component :is="Component" v-if="route.meta.ignoreCache" :key="route.fullPath" />
|
||||||
|
<!-- keep-alive提供组件状态缓存,以便快速渲染组件内容 -->
|
||||||
|
<keep-alive v-else :include="cacheList">
|
||||||
|
<component :is="Component" :key="route.fullPath" />
|
||||||
|
</keep-alive>
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useTabBarStore } from '@/store';
|
||||||
|
|
||||||
|
const tabBarStore = useTabBarStore();
|
||||||
|
const cacheList = computed(() => tabBarStore.getCacheList);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="KZkaZ"></a>
|
||||||
|
|
||||||
|
## -models
|
||||||
|
|
||||||
|
全局数据模型,将涉及请求、组件 props、状态库的公共属性抽象为数据模型,在此文件夹内声明,保证全局数据的类型统一、方便维护。下面是请求模型示例:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface HttpResponse<T = unknown> {
|
||||||
|
status: number;
|
||||||
|
msg: string;
|
||||||
|
code: number;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以是业务相关模型:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface LoginData {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRes {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="d3Gdu"></a>
|
||||||
|
|
||||||
|
## -mock
|
||||||
|
|
||||||
|
提供全局接口数据 mock 功能,避免因为调试而修改请求代码导致出现问题,按功能模块划分文件,通过`index.ts`暴露,示例如下:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import Mock from 'mockjs';
|
||||||
|
import './user';
|
||||||
|
import '@/views/dashboard/workplace/mock';
|
||||||
|
|
||||||
|
Mock.setup({
|
||||||
|
timeout: '600-1000',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import Mock from 'mockjs';
|
||||||
|
import setupMock, { successResponseWrap, failResponseWrap } from '@/utils/setup-mock';
|
||||||
|
import { isLogin } from '@/utils/auth';
|
||||||
|
|
||||||
|
import { MockParams } from '@/types/mock';
|
||||||
|
|
||||||
|
setupMock({
|
||||||
|
setup() {
|
||||||
|
// 用户信息
|
||||||
|
Mock.mock(new RegExp('/api/user/info'), () => {
|
||||||
|
if (isLogin()) {
|
||||||
|
const role = window.localStorage.getItem('userRole') || 'admin';
|
||||||
|
return successResponseWrap({
|
||||||
|
name: '王立群',
|
||||||
|
avatar: '//lf1-xgcdn-tos.pstatp.com/obj/vcloud/vadmin/start.8e0e4855ee346a46ccff8ff3e24db27b.png',
|
||||||
|
email: 'wangliqun@email.com',
|
||||||
|
job: 'frontend',
|
||||||
|
jobName: '前端艺术家',
|
||||||
|
organization: 'Frontend',
|
||||||
|
organizationName: '前端',
|
||||||
|
location: 'beijing',
|
||||||
|
locationName: '北京',
|
||||||
|
introduction: '人潇洒,性温存',
|
||||||
|
personalWebsite: 'https://www.arco.design',
|
||||||
|
phone: '150****0000',
|
||||||
|
registrationDate: '2013-05-10 12:10:00',
|
||||||
|
accountId: '15012312300',
|
||||||
|
certification: 1,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return failResponseWrap(null, '未登录', 50008);
|
||||||
|
});
|
||||||
|
// 登出
|
||||||
|
Mock.mock(new RegExp('/api/user/logout'), () => {
|
||||||
|
return successResponseWrap(null);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="QgxDQ"></a>
|
||||||
|
|
||||||
|
## -router
|
||||||
|
|
||||||
|
项目路由管理模块,入口文件`index.ts`注册并暴露全部路由,以模块命名文件夹划分模块路由放置在`routes/*`下;`guard/*`下放置路由导航控制,包含权限、登录重定向等;`app-menus/index.ts`为菜单相关的路由信息;`constants.ts`定义路由常量,包括路由白名单、重定向路由名、默认主页路由信息等
|
||||||
|
<a name="YZWcg"></a>
|
||||||
|
|
||||||
|
## -enums
|
||||||
|
|
||||||
|
全局的枚举类型声明模块,将可枚举的类型统一管理,以`类型+Enum`命名,方便维护。示例如下:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* @description: Request result set
|
||||||
|
*/
|
||||||
|
export enum ResultEnum {
|
||||||
|
SUCCESS = 0,
|
||||||
|
ERROR = 1,
|
||||||
|
TIMEOUT = 401,
|
||||||
|
TYPE = 'success',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: request method
|
||||||
|
*/
|
||||||
|
export enum RequestEnum {
|
||||||
|
GET = 'GET',
|
||||||
|
POST = 'POST',
|
||||||
|
PUT = 'PUT',
|
||||||
|
DELETE = 'DELETE',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: contentTyp
|
||||||
|
*/
|
||||||
|
export enum ContentTypeEnum {
|
||||||
|
// json
|
||||||
|
JSON = 'application/json;charset=UTF-8',
|
||||||
|
// form-data qs
|
||||||
|
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||||
|
// form-data upload
|
||||||
|
FORM_DATA = 'multipart/form-data;charset=UTF-8',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="I0KEf"></a>
|
||||||
|
|
||||||
|
## -locale
|
||||||
|
|
||||||
|
国际化模块,存放项目声明的国际化配置,按语种划分模块。模块入口文件为`index.ts`,负责定义菜单、导航栏等公共的国际化配置,其他的按系统功能声明并导入`index.ts`,还有项目内页面组件的国际化配置也需要导入至`index.ts`,页面组件的国际化配置在页面的目录下声明,如:`views/dashboard/workplace/locale`。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
import en from './en-US';
|
||||||
|
import cn from './zh-CN';
|
||||||
|
|
||||||
|
export const LOCALE_OPTIONS = [
|
||||||
|
{ label: '中文', value: 'zh-CN' },
|
||||||
|
{ label: 'English', value: 'en-US' },
|
||||||
|
];
|
||||||
|
const defaultLocale = localStorage.getItem('MS-locale') || 'zh-CN';
|
||||||
|
const i18n = createI18n({
|
||||||
|
locale: defaultLocale,
|
||||||
|
fallbackLocale: 'en-US',
|
||||||
|
legacy: false,
|
||||||
|
allowComposition: true,
|
||||||
|
messages: {
|
||||||
|
'en-US': en,
|
||||||
|
'zh-CN': cn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="fZfbw"></a>
|
||||||
|
|
||||||
|
## -types
|
||||||
|
|
||||||
|
项目级别的类型声明,与业务无关的类型声明,与`models`、`enums`不同的是,这里声明的是项目模块级别的类型,或者工具模块的类型声明,例如:`axios`的配置声明、第三方插件不提供 TS 支持但需要我们自定义的声明等
|
||||||
|
<a name="hvu5O"></a>
|
||||||
|
|
||||||
|
## -utils
|
||||||
|
|
||||||
|
公共方法、工具,按功能类型命名文件,单个文件内工具方法的功能要对应命名
|
||||||
|
<a name="FFTqz"></a>
|
||||||
|
|
||||||
|
## -views
|
||||||
|
|
||||||
|
页面模块,按功能模块划分,公共模块有`login`、`base`,其中`base`模块内包含 404、403 页面等
|
||||||
|
<a name="RwVcu"></a>
|
||||||
|
|
||||||
|
## -theme 主题配置
|
||||||
|
1. 去Desing Lab 创建主题 https://arco.design/themes/home
|
||||||
|
2. 以`ms-theme-` 命名为开头
|
||||||
|
3. 点击页面的配置主题
|
||||||
|
**“CSS 变量” + “Tailwind 配置变量” + “基于 css 变量自行计算混合色覆盖 arco-theme 变量”**
|
||||||
|
|
||||||
|
## -.env.\*环境变量配置
|
||||||
|
|
||||||
|
在`vite`中内置了环境变量配置功能,只需要在项目根目录下创建以`.env.*`开头的文件,在内部写入配置的变量即可,默认`.env`为生产环境、`.env.development`为开发环境、`.env.XXX`为自定义`XXX`环境(注意:自定义环境需要在`package.json`的项目运行命令后加入`--mode XXX`),各类环境变量配置示例如下( ⚠️ 注意!!环境变量文件内使用注释要用`#`,不能使用`//`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env.production 为生产环境配置
|
||||||
|
NODE_ENV=production # 代码中通过import.meta.env.NODE_ENV访问
|
||||||
|
VITE_STG=1 # 自定义变量必须用VITE_开头,代码中通过import.meta.env.VITE_XXX访问
|
||||||
|
|
||||||
|
# .env.development 为开发环境配置
|
||||||
|
NODE_ENV=development
|
||||||
|
VITE_APP_ENV = dev
|
||||||
|
VITE_APP_TITLE = 我是标题
|
||||||
|
|
||||||
|
# .env.XXX 为自定义环境配置
|
||||||
|
VITE_MYENV = 1 # 代码中通过import.meta.env.VITE_MYENV访问
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ 上述环境变量在代码中正常都可以用`import.meta.env.VITE_XXX`访问,但是在`vite.config.ts`配置文件中无法使用此方法访问,原因是此访问链是 vite 在初始化后通过读取本地`.env.XXX`文件并注入到`import.meta`中的,但是在`vite.config.ts`中 vite 此时还未初始化,所以无法通过上述方法访问,应通过下面方法访问:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig, loadEnv } from 'vite'; // 导入loadEnv方法
|
||||||
|
loadEnv(mode, process.cwd()).VITE_XXX; // 在需要访问env里变量的地方使用此方法访问即可
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="c61428f2"></a>
|
||||||
|
|
||||||
|
## -TailwindCSS 配置
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
content: ['./index.html', './src/**/*.{html,js,vue}', './src/*.{html,js,vue}'], // 需要解析的文件路径
|
||||||
|
theme: {
|
||||||
|
// 自定义主题配置
|
||||||
|
backgroundColor: {
|
||||||
|
// 自定义背景色
|
||||||
|
menuHover: '#272D39',
|
||||||
|
headerBg: '#191E29',
|
||||||
|
},
|
||||||
|
textColor: (theme) => ({
|
||||||
|
// 自定义字体颜色
|
||||||
|
...theme('colors'), // 这里必须解构原本有的颜色,不然会导致在页面style中使用@apply应用内部字体颜色类的时候报错找不到内部字体颜色类
|
||||||
|
'40Gray': 'rgba(255,255,255,0.40)',
|
||||||
|
'65Gray': 'rgba(255,255,255,0.65)',
|
||||||
|
}),
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="c7baab5c"></a>
|
||||||
|
|
||||||
|
## -TS 配置-tsconfig.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue",
|
||||||
|
"src/components.d.ts",
|
||||||
|
"auto-imports.d.ts"
|
||||||
|
], // TS解析路径配置
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true, // 允许编译器编译JS,JSX文件
|
||||||
|
"noEmit": true,
|
||||||
|
"target": "esnext", // 使用es最新语法
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"allowSyntheticDefaultImports": true, // 允许异步导入模块,配合自动导入插件使用
|
||||||
|
"module": "esnext", // 使用ES模块语法
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": true, // 严格模式
|
||||||
|
"jsx": "preserve",
|
||||||
|
"sourceMap": true, // 代码映射
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["esnext", "dom"],
|
||||||
|
"skipLibCheck": true, // 跳过node依赖包语法检查
|
||||||
|
"types": [
|
||||||
|
// "vitest/globals",
|
||||||
|
// "vite-plugin-svg-icons/client"
|
||||||
|
], // 手动导入TS类型声明文件
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
// 路径映射
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"#/*": ["types/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="50a6c7f6"></a>
|
||||||
|
|
||||||
|
## -vite 配置-插件详解
|
||||||
|
|
||||||
|
`vite`构建生产资源使用`rollup`打包,所以可以在`vite`中使用`rollup`支持的所有插件。
|
||||||
|
|
||||||
|
- 组件自动导入,使用`unplugin-auto-import/vite`插件实现自动导入组件、`unplugin-vue-components/vite`插件按需导入自定义组件,具体用法如下:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { ArcoResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
import { vitePluginForArco } from '@arco-plugins/vite-vue'
|
||||||
|
|
||||||
|
export default () => defineConfig({
|
||||||
|
plugins: [
|
||||||
|
// 自定义组件自动引入
|
||||||
|
AutoImport({å
|
||||||
|
dts: 'src/auto-import.d.ts', // 输出声明文件地址(在使用typescript时必须配置,不然会导致页面使用未导入的组件时报错)
|
||||||
|
resolvers: [ElementPlusResolver()], // ElementPlus自动导入
|
||||||
|
}),
|
||||||
|
// 自定义组件按需引入
|
||||||
|
Components({
|
||||||
|
dts: 'src/components.d.ts', // 输出声明文件地址(在使用typescript时必须配置,不然会导致页面使用未导入的组件时报错)
|
||||||
|
dirs: ['src/components'], // 按需加载的文件夹
|
||||||
|
resolvers: [
|
||||||
|
ArcoResolver(), // ArcoDesignVue按需加载
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
// 样式自动导入
|
||||||
|
vitePluginForArco({})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- 使用`vite-plugin-svg-icons`插件实现自动加载 svg 图片,并通过封装 svg 组件的方式,一行代码使用 svg,配置如下:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||||
|
|
||||||
|
export default () =>
|
||||||
|
defineConfig({
|
||||||
|
plugins: [
|
||||||
|
createSvgIconsPlugin({
|
||||||
|
// 指定svg读取的文件夹
|
||||||
|
iconDirs: [resolve(process.cwd(), 'src/assets/icons/svg')],
|
||||||
|
// 指定icon的读取名字,使用svg文件名为icon名字
|
||||||
|
symbolId: 'icon-[dir]-[name]',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- 使用`vite`自带插件`loadEnv`读取环境信息,可作环境判断,使用`rollup-plugin-visualizer`插件可分析打包后的文件体积,配置如下:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
|
import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
|
|
||||||
|
// 这里的mode参数为package.json文件配置的环境参数,使用`--mode XXX`,如"report: rimraf dist && vite build --mode analyze"
|
||||||
|
export default ({ mode }) =>
|
||||||
|
defineConfig({
|
||||||
|
plugins: [
|
||||||
|
// 这里通过--mode analyze配置为分析模式,使用visualizer插件分析方法,输出report.html分析报告
|
||||||
|
loadEnv(mode, process.cwd()).VITE_ANALYZE === 'Y'
|
||||||
|
? visualizer({ open: true, brotliSize: true, filename: 'report.html' })
|
||||||
|
: null,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="0c0978d0"></a>
|
||||||
|
|
||||||
|
## -vite 配置-build 详解
|
||||||
|
|
||||||
|
上面讲述了插件应用,下面讲解除了插件外,在`vite`中的 build 构建生产资源时,`rollupOptions`的配置:
|
||||||
|
|
||||||
|
- `output`为输出产物配置,我们可以通过`manualChunks`去配置分包策略(与`webpack`分包机制类似),配置如下:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { mergeConfig } from 'vite';
|
||||||
|
import baseConfig from './vite.config.base';
|
||||||
|
import configCompressPlugin from './plugin/compress';
|
||||||
|
import configVisualizerPlugin from './plugin/visualizer';
|
||||||
|
import configArcoResolverPlugin from './plugin/arcoResolver';
|
||||||
|
import configImageminPlugin from './plugin/imagemin';
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
{
|
||||||
|
mode: 'production',
|
||||||
|
plugins: [
|
||||||
|
configCompressPlugin('gzip'),
|
||||||
|
configVisualizerPlugin(),
|
||||||
|
configArcoResolverPlugin(),
|
||||||
|
configImageminPlugin(),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
arco: ['@arco-design/web-vue'],
|
||||||
|
chart: ['echarts', 'vue-echarts'],
|
||||||
|
vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chunkSizeWarningLimit: 2000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
baseConfig
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 单元测试
|
||||||
|
|
||||||
|
测试框架:vitest
|
||||||
|
|
||||||
|
单元测试实用工具库: @vue/test-utils
|
||||||
|
|
||||||
|
测试环境: js-dom
|
||||||
|
|
||||||
|
报告: @vitest/coverage-c8
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add -D vitest @vue/test-utils js-dom @vitest/coverage-c8
|
||||||
|
```
|
||||||
|
|
||||||
|
配置
|
||||||
|
|
||||||
|
根目录新建 vitest.config.ts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { mergeConfig } from 'vite';
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import viteConfig from './config/vite.config.dev';
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
viteConfig,
|
||||||
|
defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: ['@vue/babel-plugin-jsx'],
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
import '@vue/runtime-core'
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare module '@vue/runtime-core' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* If you use the template method for development, you can use the unplugin-vue-components plugin to enable on-demand loading support.
|
||||||
|
* 按需引入
|
||||||
|
* https://github.com/antfu/unplugin-vue-components
|
||||||
|
* https://arco.design/vue/docs/start
|
||||||
|
* Although the Pro project is full of imported components, this plugin will be used by default.
|
||||||
|
* 虽然Pro项目中是全量引入组件,但此插件会默认使用。
|
||||||
|
*/
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import { ArcoResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
|
||||||
|
export default function configArcoResolverPlugin() {
|
||||||
|
const arcoResolverPlugin = Components({
|
||||||
|
dirs: [], // Avoid parsing src/components. 避免解析到src/components
|
||||||
|
deep: false,
|
||||||
|
resolvers: [ArcoResolver()],
|
||||||
|
});
|
||||||
|
return arcoResolverPlugin;
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Theme import
|
||||||
|
* 样式按需引入
|
||||||
|
* https://github.com/arco-design/arco-plugins/blob/main/packages/plugin-vite-vue/README.md
|
||||||
|
* https://arco.design/vue/docs/start
|
||||||
|
*/
|
||||||
|
import { vitePluginForArco } from '@arco-plugins/vite-vue';
|
||||||
|
|
||||||
|
export default function configArcoStyleImportPlugin() {
|
||||||
|
// 按需加载主题样式
|
||||||
|
const arcoResolverPlugin = vitePluginForArco({
|
||||||
|
theme: '@arco-themes/vue-ms-theme',
|
||||||
|
});
|
||||||
|
// const arcoResolverPlugin = vitePluginForArco({});
|
||||||
|
return arcoResolverPlugin;
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
|
||||||
|
* gzip压缩
|
||||||
|
* https://github.com/anncwb/vite-plugin-compression
|
||||||
|
*/
|
||||||
|
import type { Plugin } from 'vite';
|
||||||
|
import compressPlugin from 'vite-plugin-compression';
|
||||||
|
|
||||||
|
export default function configCompressPlugin(compress: 'gzip' | 'brotli', deleteOriginFile = false): Plugin | Plugin[] {
|
||||||
|
const plugins: Plugin[] = [];
|
||||||
|
|
||||||
|
if (compress === 'gzip') {
|
||||||
|
plugins.push(
|
||||||
|
compressPlugin({
|
||||||
|
ext: '.gz',
|
||||||
|
deleteOriginFile,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compress === 'brotli') {
|
||||||
|
plugins.push(
|
||||||
|
compressPlugin({
|
||||||
|
ext: '.br',
|
||||||
|
algorithm: 'brotliCompress',
|
||||||
|
deleteOriginFile,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return plugins;
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* Image resource files used to compress the output of the production environment
|
||||||
|
* 图片压缩
|
||||||
|
* https://github.com/anncwb/vite-plugin-imagemin
|
||||||
|
*/
|
||||||
|
import viteImagemin from 'vite-plugin-imagemin';
|
||||||
|
|
||||||
|
export default function configImageminPlugin() {
|
||||||
|
const imageminPlugin = viteImagemin({
|
||||||
|
gifsicle: {
|
||||||
|
optimizationLevel: 7,
|
||||||
|
interlaced: false,
|
||||||
|
},
|
||||||
|
optipng: {
|
||||||
|
optimizationLevel: 7,
|
||||||
|
},
|
||||||
|
mozjpeg: {
|
||||||
|
quality: 20,
|
||||||
|
},
|
||||||
|
pngquant: {
|
||||||
|
quality: [0.8, 0.9],
|
||||||
|
speed: 4,
|
||||||
|
},
|
||||||
|
svgo: {
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: 'removeViewBox',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'removeEmptyAttrs',
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return imageminPlugin;
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* Generation packaging analysis
|
||||||
|
* 生成打包分析
|
||||||
|
*/
|
||||||
|
import visualizer from 'rollup-plugin-visualizer';
|
||||||
|
import { isReportMode } from '../utils';
|
||||||
|
|
||||||
|
export default function configVisualizerPlugin() {
|
||||||
|
if (isReportMode()) {
|
||||||
|
return visualizer({
|
||||||
|
filename: './node_modules/.cache/visualizer/stats.html',
|
||||||
|
open: true,
|
||||||
|
gzipSize: true,
|
||||||
|
brotliSize: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Whether to generate package preview
|
||||||
|
* 是否生成打包报告
|
||||||
|
*/
|
||||||
|
export default {};
|
||||||
|
|
||||||
|
export function isReportMode(): boolean {
|
||||||
|
return process.env.REPORT === 'true';
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
|
import svgLoader from 'vite-svg-loader';
|
||||||
|
import configArcoStyleImportPlugin from './plugin/arcoStyleImport';
|
||||||
|
import configArcoResolverPlugin from './plugin/arcoResolver';
|
||||||
|
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
vueJsx(),
|
||||||
|
svgLoader({ svgoConfig: {} }),
|
||||||
|
configArcoResolverPlugin(),
|
||||||
|
configArcoStyleImportPlugin(),
|
||||||
|
createSvgIconsPlugin({
|
||||||
|
// 指定需要缓存的图标文件夹
|
||||||
|
iconDirs: [resolve(process.cwd(), 'src/assets/svg')], // 与本地储存地址一致
|
||||||
|
// 指定symbolId格式
|
||||||
|
symbolId: 'icon-[name]',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: [
|
||||||
|
{
|
||||||
|
find: '@',
|
||||||
|
replacement: resolve(__dirname, '../src'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: 'assets',
|
||||||
|
replacement: resolve(__dirname, '../src/assets'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: 'vue-i18n',
|
||||||
|
replacement: 'vue-i18n/dist/vue-i18n.cjs.js', // Resolve the i18n warning issue
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: 'vue',
|
||||||
|
replacement: 'vue/dist/vue.esm-bundler.js', // compile template
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extensions: ['.ts', '.js'],
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
'process.env': {},
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
less: {
|
||||||
|
modifyVars: {
|
||||||
|
hack: `true; @import (reference) "${resolve('src/assets/style/breakpoint.less')}";`,
|
||||||
|
},
|
||||||
|
javascriptEnabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,24 @@
|
||||||
|
/// <reference types="vitest" />
|
||||||
|
import { mergeConfig } from 'vite';
|
||||||
|
import eslint from 'vite-plugin-eslint';
|
||||||
|
import baseConfig from './vite.config.base';
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
{
|
||||||
|
mode: 'development',
|
||||||
|
server: {
|
||||||
|
open: true,
|
||||||
|
fs: {
|
||||||
|
strict: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
eslint({
|
||||||
|
cache: false,
|
||||||
|
include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.vue'],
|
||||||
|
exclude: ['node_modules'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
baseConfig
|
||||||
|
);
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { mergeConfig } from 'vite';
|
||||||
|
import baseConfig from './vite.config.base';
|
||||||
|
import configCompressPlugin from './plugin/compress';
|
||||||
|
import configVisualizerPlugin from './plugin/visualizer';
|
||||||
|
import configImageminPlugin from './plugin/imagemin';
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
{
|
||||||
|
mode: 'production',
|
||||||
|
plugins: [configCompressPlugin('gzip'), configVisualizerPlugin(), configImageminPlugin()],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
arco: ['@arco-design/web-vue'],
|
||||||
|
chart: ['echarts', 'vue-echarts'],
|
||||||
|
vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chunkSizeWarningLimit: 2000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
baseConfig
|
||||||
|
);
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link
|
||||||
|
rel="shortcut icon"
|
||||||
|
type="image/x-icon"
|
||||||
|
href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico"
|
||||||
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>MeterSphere</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,136 @@
|
||||||
|
{
|
||||||
|
"name": "meter-sphere",
|
||||||
|
"description": "MeterSphere FrontEnd",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"private": true,
|
||||||
|
"author": "MeterSphere Team",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --config ./config/vite.config.dev.ts",
|
||||||
|
"build": "vue-tsc --noEmit && vite build --config ./config/vite.config.prod.ts",
|
||||||
|
"report": "cross-env REPORT=true npm run build",
|
||||||
|
"preview": "npm run build && vite preview --host",
|
||||||
|
"type:check": "vue-tsc --noEmit --skipLibCheck",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
|
"lint:styles": "stylelint 'src/**/*.{vue,html,css,scss,less}' --fix",
|
||||||
|
"lint-staged": "npx lint-staged",
|
||||||
|
"prepare": "husky install",
|
||||||
|
"test": "vitest",
|
||||||
|
"coverage": "vitest run --coverage"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,ts,jsx,tsx}": [
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix"
|
||||||
|
],
|
||||||
|
"*.vue": [
|
||||||
|
"stylelint --fix",
|
||||||
|
"prettier --write",
|
||||||
|
"eslint --fix"
|
||||||
|
],
|
||||||
|
"*.{less,css,scss,sass}": [
|
||||||
|
"stylelint --fix --custom-syntax postcss-less",
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@7polo/kity": "2.0.8",
|
||||||
|
"@7polo/kityminder-core": "1.4.53",
|
||||||
|
"@arco-design/web-vue": "^2.46.0",
|
||||||
|
"@arco-themes/vue-ms-theme": "^0.0.1",
|
||||||
|
"@form-create/arco-design": "^3.1.21",
|
||||||
|
"@vueuse/core": "^9.13.0",
|
||||||
|
"ace-builds": "^1.21.1",
|
||||||
|
"axios": "^0.24.0",
|
||||||
|
"dayjs": "^1.11.7",
|
||||||
|
"echarts": "^5.4.2",
|
||||||
|
"hotbox-minder": "1.0.15",
|
||||||
|
"jsonpath-picker-vanilla": "^1.2.4",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"mitt": "^3.0.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"pinia": "^2.0.36",
|
||||||
|
"pinia-plugin-persistedstate": "^3.1.0",
|
||||||
|
"query-string": "^8.1.0",
|
||||||
|
"sortablejs": "^1.15.0",
|
||||||
|
"vue": "^3.3.2",
|
||||||
|
"vue-echarts": "^6.5.5",
|
||||||
|
"vue-i18n": "^9.2.2",
|
||||||
|
"vue-router": "^4.2.0",
|
||||||
|
"vue3-ace-editor": "^2.2.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@arco-plugins/vite-vue": "^1.4.5",
|
||||||
|
"@commitlint/cli": "^17.6.3",
|
||||||
|
"@commitlint/config-conventional": "^17.6.3",
|
||||||
|
"@types/lodash": "^4.14.194",
|
||||||
|
"@types/lodash-es": "^4.17.7",
|
||||||
|
"@types/mockjs": "^1.0.7",
|
||||||
|
"@types/nprogress": "^0.2.0",
|
||||||
|
"@types/sortablejs": "^1.15.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
||||||
|
"@typescript-eslint/parser": "^5.59.5",
|
||||||
|
"@vitejs/plugin-vue": "^3.2.0",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^2.1.1",
|
||||||
|
"@vitest/coverage-c8": "^0.31.0",
|
||||||
|
"@vue/babel-plugin-jsx": "^1.1.1",
|
||||||
|
"@vue/test-utils": "^2.3.2",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"consola": "^2.15.3",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"eslint": "^8.40.0",
|
||||||
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.5.5",
|
||||||
|
"eslint-plugin-import": "^2.27.5",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-vue": "^9.12.0",
|
||||||
|
"fast-glob": "^3.2.12",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"jsdom": "^22.0.0",
|
||||||
|
"less": "^4.1.3",
|
||||||
|
"less-loader": "^11.1.0",
|
||||||
|
"lint-staged": "^13.2.2",
|
||||||
|
"mockjs": "^1.1.0",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
|
"postcss-html": "^1.5.0",
|
||||||
|
"postcss-less": "^6.0.0",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||||
|
"rollup": "^2.79.1",
|
||||||
|
"rollup-plugin-visualizer": "^5.9.0",
|
||||||
|
"sass": "^1.62.1",
|
||||||
|
"stylelint": "^14.6.0",
|
||||||
|
"stylelint-config-html": "^1.0.0",
|
||||||
|
"stylelint-config-prettier": "^9.0.3",
|
||||||
|
"stylelint-config-rational-order": "^0.1.2",
|
||||||
|
"stylelint-config-recommended": "^7.0.0",
|
||||||
|
"stylelint-config-recommended-less": "^1.0.4",
|
||||||
|
"stylelint-config-recommended-scss": "^7.0.0",
|
||||||
|
"stylelint-config-recommended-vue": "^1.4.0",
|
||||||
|
"stylelint-config-standard": "^25.0.0",
|
||||||
|
"stylelint-config-standard-scss": "^4.0.0",
|
||||||
|
"stylelint-less": "^1.0.5",
|
||||||
|
"stylelint-order": "^5.0.0",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"unplugin-vue-components": "^0.24.1",
|
||||||
|
"vite": "^3.2.6",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vite-plugin-imagemin": "^0.6.1",
|
||||||
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
|
"vite-svg-loader": "^3.6.0",
|
||||||
|
"vitest": "^0.31.0",
|
||||||
|
"vue-tsc": "^1.6.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"bin-wrapper": "npm:bin-wrapper-china",
|
||||||
|
"rollup": "^2.79.1",
|
||||||
|
"gifsicle": "5.2.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: [require('tailwindcss'), require('autoprefixer')],
|
||||||
|
};
|
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<a-config-provider :locale="locale">
|
||||||
|
<router-view />
|
||||||
|
<global-setting />
|
||||||
|
<ThemeBox />
|
||||||
|
</a-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import enUS from '@arco-design/web-vue/es/locale/lang/en-us';
|
||||||
|
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
|
||||||
|
import GlobalSetting from '@/components/global-setting/index.vue';
|
||||||
|
import useLocale from '@/locale/useLocale';
|
||||||
|
import ThemeBox from '@/components/theme-box/index.vue';
|
||||||
|
|
||||||
|
const { currentLocale } = useLocale();
|
||||||
|
const locale = computed(() => {
|
||||||
|
switch (currentLocale.value) {
|
||||||
|
case 'zh-CN':
|
||||||
|
return zhCN;
|
||||||
|
case 'en-US':
|
||||||
|
return enUS;
|
||||||
|
default:
|
||||||
|
return zhCN;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,177 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { AxiosCanceler } from './axiosCancel';
|
||||||
|
import { isFunction } from '@/utils/is';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
import { ContentTypeEnum } from '@/enums/httpEnum';
|
||||||
|
|
||||||
|
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError } from 'axios';
|
||||||
|
import type { RequestOptions, Result, UploadFileParams } from '#/axios';
|
||||||
|
import type { CreateAxiosOptions } from './axiosTransform';
|
||||||
|
|
||||||
|
export * from './axiosTransform';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 封装axios请求,返回重新封装的数据格式
|
||||||
|
*/
|
||||||
|
export class MSAxios {
|
||||||
|
private axiosInstance: AxiosInstance;
|
||||||
|
|
||||||
|
private readonly options: CreateAxiosOptions;
|
||||||
|
|
||||||
|
constructor(options: CreateAxiosOptions) {
|
||||||
|
this.options = options;
|
||||||
|
this.axiosInstance = axios.create(options);
|
||||||
|
this.setupInterceptors();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTransform() {
|
||||||
|
const { transform } = this.options;
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 拦截器配置
|
||||||
|
*/
|
||||||
|
private setupInterceptors() {
|
||||||
|
const transform = this.getTransform();
|
||||||
|
if (!transform) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { requestInterceptors, responseInterceptors, responseInterceptorsCatch } = transform;
|
||||||
|
|
||||||
|
const axiosCanceler = new AxiosCanceler();
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
|
||||||
|
// 如果ignoreCancelToken为true,则不添加到pending中
|
||||||
|
const {
|
||||||
|
// @ts-ignore
|
||||||
|
headers: { ignoreCancelToken },
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
const ignoreCancel =
|
||||||
|
ignoreCancelToken !== undefined ? ignoreCancelToken : this.options.requestOptions?.ignoreCancelToken;
|
||||||
|
|
||||||
|
if (!ignoreCancel) {
|
||||||
|
axiosCanceler.addPending(config);
|
||||||
|
}
|
||||||
|
if (requestInterceptors && isFunction(requestInterceptors)) {
|
||||||
|
config = requestInterceptors(config, this.options);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}, undefined);
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
|
||||||
|
if (res) {
|
||||||
|
axiosCanceler.removePending(res.config);
|
||||||
|
}
|
||||||
|
if (responseInterceptors && isFunction(responseInterceptors)) {
|
||||||
|
res = responseInterceptors(res);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}, undefined);
|
||||||
|
|
||||||
|
// 响应错误处理
|
||||||
|
if (responseInterceptorsCatch && isFunction(responseInterceptorsCatch)) {
|
||||||
|
this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 文件上传
|
||||||
|
*/
|
||||||
|
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
|
||||||
|
const formData = new window.FormData();
|
||||||
|
const customFilename = params.name || 'file';
|
||||||
|
|
||||||
|
if (params.filename) {
|
||||||
|
formData.append(customFilename, params.file, params.filename);
|
||||||
|
} else {
|
||||||
|
formData.append(customFilename, params.file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.data) {
|
||||||
|
Object.keys(params.data).forEach((key) => {
|
||||||
|
const value = params.data[key];
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((item) => {
|
||||||
|
formData.append(`${key}[]`, item);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append(key, params.data[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.axiosInstance.request<T>({
|
||||||
|
...config,
|
||||||
|
method: 'POST',
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-type': ContentTypeEnum.FORM_DATA,
|
||||||
|
// @ts-ignore
|
||||||
|
'ignoreCancelToken': true, // 文件上传请求不需要添加到pending中
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||||
|
return this.request({ ...config, method: 'GET' }, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||||
|
return this.request({ ...config, method: 'POST' }, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||||
|
return this.request({ ...config, method: 'PUT' }, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||||
|
return this.request({ ...config, method: 'DELETE' }, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||||
|
let conf: CreateAxiosOptions = cloneDeep(config);
|
||||||
|
const transform = this.getTransform();
|
||||||
|
|
||||||
|
const { requestOptions } = this.options;
|
||||||
|
|
||||||
|
const opt = { ...requestOptions, ...options };
|
||||||
|
|
||||||
|
const { beforeRequestHook, transformRequestHook } = transform || {};
|
||||||
|
// 请求之前处理config
|
||||||
|
if (beforeRequestHook && isFunction(beforeRequestHook)) {
|
||||||
|
conf = beforeRequestHook(conf, opt);
|
||||||
|
}
|
||||||
|
conf.requestOptions = opt;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.axiosInstance
|
||||||
|
.request<any, AxiosResponse<Result>>(conf)
|
||||||
|
.then((res: AxiosResponse<Result>) => {
|
||||||
|
// 请求成功后的处理
|
||||||
|
if (transformRequestHook && isFunction(transformRequestHook)) {
|
||||||
|
try {
|
||||||
|
const ret = transformRequestHook(res, opt);
|
||||||
|
resolve(ret);
|
||||||
|
} catch (err) {
|
||||||
|
reject(err || new Error('request error!'));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(res as unknown as Promise<T>);
|
||||||
|
})
|
||||||
|
.catch((e: Error | AxiosError) => {
|
||||||
|
if (axios.isAxiosError(e)) {
|
||||||
|
// 在这可重写axios错误消息
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import type { AxiosRequestConfig, Canceler } from 'axios';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { isFunction } from '@/utils/is';
|
||||||
|
|
||||||
|
let pendingMap = new Map<string, Canceler>();
|
||||||
|
|
||||||
|
export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&');
|
||||||
|
|
||||||
|
export class AxiosCanceler {
|
||||||
|
/**
|
||||||
|
* 添加请求
|
||||||
|
* @param {Object} config
|
||||||
|
*/
|
||||||
|
addPending(config: AxiosRequestConfig) {
|
||||||
|
this.removePending(config);
|
||||||
|
const url = getPendingUrl(config);
|
||||||
|
config.cancelToken =
|
||||||
|
config.cancelToken ||
|
||||||
|
new axios.CancelToken((cancel) => {
|
||||||
|
if (!pendingMap.has(url)) {
|
||||||
|
// 非重复请求,存入pending中
|
||||||
|
pendingMap.set(url, cancel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 清理全部pending中的请求
|
||||||
|
*/
|
||||||
|
removeAllPending() {
|
||||||
|
pendingMap.forEach((cancel) => {
|
||||||
|
if (cancel && isFunction(cancel)) {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pendingMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消并移除指定请求
|
||||||
|
* @param {Object} config
|
||||||
|
*/
|
||||||
|
removePending(config: AxiosRequestConfig) {
|
||||||
|
const url = getPendingUrl(config);
|
||||||
|
|
||||||
|
if (pendingMap.has(url)) {
|
||||||
|
// 根据标识找到pending中对应的请求并取消
|
||||||
|
const cancel = pendingMap.get(url);
|
||||||
|
if (cancel && isFunction(cancel)) {
|
||||||
|
cancel(url);
|
||||||
|
}
|
||||||
|
pendingMap.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 重置pending列表
|
||||||
|
*/
|
||||||
|
static reset(): void {
|
||||||
|
pendingMap = new Map<string, Canceler>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* Data processing class, can be configured according to the project
|
||||||
|
*/
|
||||||
|
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
|
import type { RequestOptions, Result } from '#/axios';
|
||||||
|
|
||||||
|
export abstract class AxiosTransform {
|
||||||
|
/**
|
||||||
|
* @description: 请求之前处理配置
|
||||||
|
*/
|
||||||
|
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 处理请求数据。如果数据不是预期格式,可直接抛出错
|
||||||
|
*/
|
||||||
|
transformRequestHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 请求之前的拦截器
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
requestInterceptors?: (config: AxiosRequestConfig, options: CreateAxiosOptions) => AxiosRequestConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 请求之后的拦截器
|
||||||
|
*/
|
||||||
|
responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 请求之后的拦截器错误处理
|
||||||
|
*/
|
||||||
|
responseInterceptorsCatch?: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateAxiosOptions extends AxiosRequestConfig {
|
||||||
|
authenticationScheme?: string;
|
||||||
|
transform?: AxiosTransform;
|
||||||
|
requestOptions?: RequestOptions;
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { Message, Modal } from '@arco-design/web-vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useUser from '@/store/modules/user';
|
||||||
|
|
||||||
|
import type { ErrorMessageMode } from '#/axios';
|
||||||
|
|
||||||
|
export default function checkStatus(status: number, msg: string, errorMessageMode: ErrorMessageMode = 'message'): void {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const userStore = useUser();
|
||||||
|
let errMessage = '';
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 400:
|
||||||
|
errMessage = `${msg}`;
|
||||||
|
break;
|
||||||
|
// 401: Not logged in
|
||||||
|
case 401:
|
||||||
|
errMessage = msg || t('api.errMsg401');
|
||||||
|
userStore.logout();
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
errMessage = t('api.errMsg403');
|
||||||
|
break;
|
||||||
|
// 404请求不存在
|
||||||
|
case 404:
|
||||||
|
errMessage = t('api.errMsg404');
|
||||||
|
break;
|
||||||
|
case 405:
|
||||||
|
errMessage = t('api.errMsg405');
|
||||||
|
break;
|
||||||
|
case 408:
|
||||||
|
errMessage = t('api.errMsg408');
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
errMessage = t('api.errMsg500');
|
||||||
|
break;
|
||||||
|
case 501:
|
||||||
|
errMessage = t('api.errMsg501');
|
||||||
|
break;
|
||||||
|
case 502:
|
||||||
|
errMessage = t('api.errMsg502');
|
||||||
|
break;
|
||||||
|
case 503:
|
||||||
|
errMessage = t('api.errMsg503');
|
||||||
|
break;
|
||||||
|
case 504:
|
||||||
|
errMessage = t('api.errMsg504');
|
||||||
|
break;
|
||||||
|
case 505:
|
||||||
|
errMessage = t('api.errMsg505');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errMessage) {
|
||||||
|
if (errorMessageMode === 'modal') {
|
||||||
|
Modal.error({ title: t('api.errorTip'), content: errMessage });
|
||||||
|
} else if (errorMessageMode === 'message') {
|
||||||
|
Message.error(errMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
export function joinTimestamp<T extends boolean>(join: boolean, restful: T): T extends true ? string : object;
|
||||||
|
|
||||||
|
export function joinTimestamp(join: boolean, restful = false): string | object {
|
||||||
|
if (!join) {
|
||||||
|
return restful ? '' : {};
|
||||||
|
}
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (restful) {
|
||||||
|
return `?_t=${now}`;
|
||||||
|
}
|
||||||
|
return { _t: now };
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
import { MSAxios } from './Axios';
|
||||||
|
import checkStatus from './checkStatus';
|
||||||
|
import { Message, Modal } from '@arco-design/web-vue';
|
||||||
|
import { RequestEnum, ContentTypeEnum, ResultEnum } from '@/enums/httpEnum';
|
||||||
|
import { isString } from '@/utils/is';
|
||||||
|
import { getToken } from '@/utils/auth';
|
||||||
|
import { setObjToUrlParams, deepMerge } from '@/utils';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { joinTimestamp } from './helper';
|
||||||
|
|
||||||
|
import type { AxiosResponse } from 'axios';
|
||||||
|
import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform';
|
||||||
|
import type { Recordable } from '#/global';
|
||||||
|
import type { RequestOptions, Result } from '#/axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 数据处理,方便区分多种处理方式
|
||||||
|
*/
|
||||||
|
const transform: AxiosTransform = {
|
||||||
|
/**
|
||||||
|
* @description 请求之前处理config
|
||||||
|
*/
|
||||||
|
beforeRequestHook: (config, options) => {
|
||||||
|
const { joinParamsToUrl, joinTime = true } = options;
|
||||||
|
|
||||||
|
const params = config.params || {};
|
||||||
|
const data = config.data || false;
|
||||||
|
if (config.method?.toUpperCase() === RequestEnum.GET) {
|
||||||
|
if (!isString(params)) {
|
||||||
|
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
|
||||||
|
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
|
||||||
|
} else {
|
||||||
|
// 兼容restful风格
|
||||||
|
config.url = `${config.url}${params}${joinTimestamp(joinTime, true)}`;
|
||||||
|
config.params = undefined;
|
||||||
|
}
|
||||||
|
} else if (isString(params)) {
|
||||||
|
// 兼容restful风格
|
||||||
|
config.url += params;
|
||||||
|
config.params = undefined;
|
||||||
|
} else {
|
||||||
|
if (Reflect.has(config, 'data') && config.data && Object.keys(config.data).length > 0) {
|
||||||
|
config.data = data;
|
||||||
|
config.params = params;
|
||||||
|
} else {
|
||||||
|
// 非GET请求如果没有提供data,则将params视为data
|
||||||
|
config.data = { ...params };
|
||||||
|
config.params = undefined;
|
||||||
|
}
|
||||||
|
if (joinParamsToUrl) {
|
||||||
|
config.url = setObjToUrlParams(config.url as string, { ...config.params, ...config.data });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 处理请求数据。如果数据不是预期格式,可直接抛出错误
|
||||||
|
*/
|
||||||
|
transformRequestHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { isTransformResponse, isReturnNativeResponse } = options;
|
||||||
|
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||||
|
if (isReturnNativeResponse) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
// 不进行任何处理,直接返回
|
||||||
|
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
|
||||||
|
if (!isTransformResponse) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
// 错误的时候返回
|
||||||
|
|
||||||
|
const { data } = res;
|
||||||
|
if (!data) {
|
||||||
|
throw new Error(t('api.apiRequestFailed'));
|
||||||
|
}
|
||||||
|
// 这里 code,result,message为 后台统一的字段
|
||||||
|
const { code, result, message } = data;
|
||||||
|
|
||||||
|
// TODO:定义完成功code后需要重写
|
||||||
|
const hasSuccess = data && Reflect.has(data, 'code') && Number(code) === ResultEnum.SUCCESS;
|
||||||
|
if (hasSuccess) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在此处根据自己项目的实际情况对不同的code执行不同的操作
|
||||||
|
// 如果不希望中断当前请求,请return数据,否则直接抛出异常即可
|
||||||
|
let timeoutMsg = '';
|
||||||
|
if (Number(code) === ResultEnum.TIMEOUT) {
|
||||||
|
timeoutMsg = t('api.timeoutMessage');
|
||||||
|
} else if (message) {
|
||||||
|
timeoutMsg = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorMessageMode=‘modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
|
||||||
|
// errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
|
||||||
|
if (options.errorMessageMode === 'modal') {
|
||||||
|
Modal.error({ title: t('api.errorTip'), content: timeoutMsg });
|
||||||
|
} else if (options.errorMessageMode === 'message') {
|
||||||
|
Message.error(timeoutMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(timeoutMsg || t('api.apiRequestFailed'));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 请求拦截器处理
|
||||||
|
*/
|
||||||
|
requestInterceptors: (config, options) => {
|
||||||
|
// 请求之前处理config
|
||||||
|
const token = getToken();
|
||||||
|
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
|
||||||
|
// jwt token
|
||||||
|
(config as Recordable).headers.Authorization = options.authenticationScheme
|
||||||
|
? `${options.authenticationScheme} ${token}`
|
||||||
|
: token;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 响应拦截器处理
|
||||||
|
*/
|
||||||
|
responseInterceptors: (res: AxiosResponse<any>) => {
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 响应错误处理
|
||||||
|
*/
|
||||||
|
responseInterceptorsCatch: (error: any) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { response, code, message, config } = error || {};
|
||||||
|
const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none';
|
||||||
|
const msg: string = response?.data?.error?.message ?? '';
|
||||||
|
const err: string = error?.toString?.() ?? '';
|
||||||
|
let errMessage = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
|
||||||
|
errMessage = t('api.apiTimeoutMessage');
|
||||||
|
}
|
||||||
|
if (err?.includes('Network Error')) {
|
||||||
|
errMessage = t('api.networkExceptionMsg');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errMessage) {
|
||||||
|
if (errorMessageMode === 'modal') {
|
||||||
|
Modal.error({ title: t('api.errorTip'), content: errMessage });
|
||||||
|
} else if (errorMessageMode === 'message') {
|
||||||
|
Message.error(errMessage);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e as unknown as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStatus(error?.response?.status, msg, errorMessageMode);
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createAxios(opt?: Partial<CreateAxiosOptions>) {
|
||||||
|
return new MSAxios(
|
||||||
|
deepMerge(
|
||||||
|
{
|
||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
|
||||||
|
// authentication schemes,e.g: Bearer
|
||||||
|
// authenticationScheme: 'Bearer',
|
||||||
|
authenticationScheme: '',
|
||||||
|
timeout: 30 * 1000,
|
||||||
|
|
||||||
|
headers: { 'Content-Type': ContentTypeEnum.JSON },
|
||||||
|
// 如果是form-data格式
|
||||||
|
// headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
|
||||||
|
// 数据处理方式
|
||||||
|
transform,
|
||||||
|
// 配置项,下面的选项都可以在独立的接口请求中覆盖
|
||||||
|
requestOptions: {
|
||||||
|
// 默认将prefix 添加到url
|
||||||
|
joinPrefix: true,
|
||||||
|
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||||
|
isReturnNativeResponse: false,
|
||||||
|
// 需要对返回数据进行处理
|
||||||
|
isTransformResponse: true,
|
||||||
|
// post请求的时候添加参数到url
|
||||||
|
joinParamsToUrl: false,
|
||||||
|
// 格式化提交参数时间
|
||||||
|
formatDate: true,
|
||||||
|
// 消息提示类型
|
||||||
|
errorMessageMode: 'message',
|
||||||
|
// 是否加入时间戳
|
||||||
|
joinTime: true,
|
||||||
|
// 忽略重复请求
|
||||||
|
ignoreCancelToken: true,
|
||||||
|
// 是否携带token
|
||||||
|
withToken: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
opt || {}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const MSR = createAxios();
|
||||||
|
export default MSR;
|
|
@ -0,0 +1,22 @@
|
||||||
|
import MSR from '@/api/http/index';
|
||||||
|
import type { TableData } from '@arco-design/web-vue/es/table/interface';
|
||||||
|
|
||||||
|
export interface ContentDataRecord {
|
||||||
|
x: string;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryContentData() {
|
||||||
|
return MSR.get<ContentDataRecord[]>({ url: '/api/content-data' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PopularRecord {
|
||||||
|
key: number;
|
||||||
|
clickNumber: string;
|
||||||
|
title: string;
|
||||||
|
increases: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryPopularList(params: { type: string }) {
|
||||||
|
return MSR.get<TableData[]>({ url: '/api/popular/list', params });
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import MSR from '@/api/http/index';
|
||||||
|
|
||||||
|
export interface MessageRecord {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
subTitle: string;
|
||||||
|
avatar?: string;
|
||||||
|
content: string;
|
||||||
|
time: string;
|
||||||
|
status: 0 | 1;
|
||||||
|
messageType?: number;
|
||||||
|
}
|
||||||
|
export type MessageListType = MessageRecord[];
|
||||||
|
|
||||||
|
export function queryMessageList() {
|
||||||
|
return MSR.post<MessageListType>({ url: '/api/message/list' });
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageStatus {
|
||||||
|
ids: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMessageStatus(data: MessageStatus) {
|
||||||
|
return MSR.post<MessageListType>({ url: '/api/message/read', data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatRecord {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
content: string;
|
||||||
|
time: string;
|
||||||
|
isCollect: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryChatList() {
|
||||||
|
return MSR.post<ChatRecord[]>({ url: '/api/chat/list' });
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import MSR from '@/api/http/index';
|
||||||
|
import { LoginUrl, LogoutUrl, GetUserInfoUrl, GetMenuListUrl } from '@/api/requrls/user';
|
||||||
|
import type { RouteRecordNormalized } from 'vue-router';
|
||||||
|
import type { LoginData, LoginRes } from '@/models/user';
|
||||||
|
import type { UserState } from '@/store/modules/user/types';
|
||||||
|
|
||||||
|
export function login(data: LoginData) {
|
||||||
|
return MSR.post<LoginRes>({ url: LoginUrl, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
return MSR.post<LoginRes>({ url: LogoutUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserInfo() {
|
||||||
|
return MSR.post<UserState>({ url: GetUserInfoUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMenuList() {
|
||||||
|
return MSR.post<RouteRecordNormalized[]>({ url: GetMenuListUrl });
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const LoginUrl = '/api/user/login';
|
||||||
|
export const LogoutUrl = '/api/user/logout';
|
||||||
|
export const GetUserInfoUrl = '/api/user/info';
|
||||||
|
export const GetMenuListUrl = '/api/user/menu';
|
After Width: | Height: | Size: 169 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 6.9 KiB |
|
@ -0,0 +1,17 @@
|
||||||
|
// Extra small screen / phone
|
||||||
|
@screen-xs: 480px;
|
||||||
|
|
||||||
|
/* Small screen / tablet */
|
||||||
|
@screen-sm: 576px;
|
||||||
|
|
||||||
|
/* Medium screen / desktop */
|
||||||
|
@screen-md: 768px;
|
||||||
|
|
||||||
|
/* Large screen / wide desktop */
|
||||||
|
@screen-lg: 992px;
|
||||||
|
|
||||||
|
/* Extra large screen / full hd */
|
||||||
|
@screen-xl: 1200px;
|
||||||
|
|
||||||
|
/* Extra extra large screen / large desktop */
|
||||||
|
@screen-xxl: 1600px;
|
|
@ -0,0 +1,89 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: var(--color-bg-1);
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
.echarts-tooltip-diy {
|
||||||
|
border: none !important;
|
||||||
|
|
||||||
|
/* Note: backdrop-filter has minimal browser support */
|
||||||
|
|
||||||
|
border-radius: 6px !important;
|
||||||
|
background: linear-gradient(304.17deg, rgb(253 254 255 / 60%) -6.04%, rgb(244 247 252 / 60%) 85.2%) !important;
|
||||||
|
backdrop-filter: blur(10px) !important;
|
||||||
|
.content-panel {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding: 0 9px;
|
||||||
|
width: 164px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgb(255 255 255 / 80%);
|
||||||
|
box-shadow: 6px 0 20px rgb(34 87 188 / 10%);
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
.tooltip-title {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.tooltip-title,
|
||||||
|
.tooltip-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
color: #1d2129;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
.tooltip-item-icon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.general-card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
& > .arco-card-header {
|
||||||
|
padding: 20px;
|
||||||
|
height: auto;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
& > .arco-card-body {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.split-line {
|
||||||
|
border-color: rgb(var(--gray-2));
|
||||||
|
}
|
||||||
|
.arco-table-cell {
|
||||||
|
.circle {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 4px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgb(var(--blue-6));
|
||||||
|
&.pass {
|
||||||
|
background-color: rgb(var(--green-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="947.2 183.1 174 35" style="enable-background:new 947.2 183.1 174 35;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#783887;}
|
||||||
|
.st1{fill:#622870;}
|
||||||
|
.st2{fill:#8B489B;}
|
||||||
|
.st3{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<g id="XMLID_803_">
|
||||||
|
<path id="XMLID_829_" class="st0" d="M998.3,209.6l-1.1-11.2l-3.1,11.2h-3.6l-3-11.2l-1.1,11.2l-5,0l2.4-16.5h6.3l2.2,7.8l2.2-7.8
|
||||||
|
h6.2l2.5,16.5H998.3z"/>
|
||||||
|
<path id="XMLID_826_" class="st0" d="M1015.2,202.7v2.1h-6.5l0,0.4c0,0.8,0.3,1.3,0.8,1.4c0.4,0.1,0.8,0.2,1.3,0.2h0.3
|
||||||
|
c1.3,0,3.1-0.3,4.5-0.6h0l-0.5,2.9c0,0-0.6,0.3-1.8,0.5c-0.8,0.1-1.6,0.1-2.6,0.1c-2.6,0-4.2-0.9-4.9-2.6
|
||||||
|
c-0.3-0.7-0.5-1.4-0.5-2.2v-2.8c0-2.1,1-3.4,2.7-4.1c0.8-0.3,1.6-0.4,2.4-0.4c2.4,0,4,0.8,4.7,2.4
|
||||||
|
C1015.1,200.7,1015.2,201.6,1015.2,202.7z M1011.8,202.1c0-0.8-0.2-1.4-0.6-1.5c-0.3-0.2-0.7-0.2-1-0.2c-1,0-1.4,0.5-1.4,1.4
|
||||||
|
l-0.1,0.6h3.1V202.1z"/>
|
||||||
|
<path id="XMLID_823_" class="st0" d="M1037.3,202.7v2.1h-6.5l0,0.4c0,0.8,0.3,1.3,0.8,1.4c0.4,0.1,0.8,0.2,1.3,0.2h0.3
|
||||||
|
c1.3,0,3.1-0.3,4.5-0.6h0l-0.5,2.9c0,0-0.6,0.3-1.8,0.5c-0.8,0.1-1.6,0.1-2.6,0.1c-2.6,0-4.2-0.9-4.9-2.6
|
||||||
|
c-0.3-0.7-0.5-1.4-0.5-2.2v-2.8c0-2.1,1-3.4,2.7-4.1c0.8-0.3,1.6-0.4,2.4-0.4c2.4,0,4,0.8,4.7,2.4
|
||||||
|
C1037.2,200.7,1037.3,201.6,1037.3,202.7z M1033.9,202.1c0-0.8-0.2-1.4-0.6-1.5c-0.3-0.2-0.7-0.2-1-0.2c-1,0-1.4,0.5-1.4,1.4
|
||||||
|
l-0.1,0.6h3.1V202.1L1033.9,202.1z"/>
|
||||||
|
<path id="XMLID_820_" class="st0" d="M1098.2,202.7v2.1h-6.5l0,0.4c0,0.8,0.3,1.3,0.8,1.4c0.4,0.1,0.8,0.2,1.3,0.2h0.3
|
||||||
|
c1.3,0,3.1-0.3,4.5-0.6h0l-0.5,2.9c0,0-0.6,0.3-1.8,0.5c-0.8,0.1-1.6,0.1-2.6,0.1c-2.6,0-4.2-0.9-4.9-2.6
|
||||||
|
c-0.3-0.7-0.5-1.4-0.5-2.2v-2.8c0-2.1,1-3.4,2.7-4.1c0.8-0.3,1.6-0.4,2.4-0.4c2.4,0,4,0.8,4.7,2.4
|
||||||
|
C1098.1,200.7,1098.2,201.6,1098.2,202.7z M1094.8,202.1c0-0.8-0.2-1.4-0.6-1.5c-0.3-0.2-0.7-0.2-1-0.2c-1,0-1.4,0.5-1.4,1.4
|
||||||
|
l-0.1,0.6h3.1V202.1z"/>
|
||||||
|
<path id="XMLID_817_" class="st0" d="M1120.3,202.7v2.1h-6.5l0,0.4c0,0.8,0.3,1.3,0.8,1.4c0.4,0.1,0.8,0.2,1.3,0.2h0.3
|
||||||
|
c1.3,0,3.1-0.3,4.5-0.6h0l-0.5,2.9c0,0-0.6,0.3-1.8,0.5c-0.8,0.1-1.6,0.1-2.6,0.1c-2.6,0-4.2-0.9-4.9-2.6
|
||||||
|
c-0.3-0.7-0.5-1.4-0.5-2.2v-2.8c0-2.1,1-3.4,2.7-4.1c0.8-0.3,1.6-0.4,2.4-0.4c2.4,0,4,0.8,4.7,2.4
|
||||||
|
C1120.2,200.7,1120.3,201.6,1120.3,202.7z M1116.9,202.1c0-0.8-0.2-1.4-0.6-1.5c-0.3-0.2-0.7-0.2-1-0.2c-1,0-1.4,0.5-1.4,1.4
|
||||||
|
l-0.1,0.6h3.1V202.1z"/>
|
||||||
|
<path id="XMLID_815_" class="st0" d="M1025.7,209.6h-2.1c-0.4,0-0.7,0-0.9,0c-0.7-0.1-1.3-0.2-1.7-0.4c-0.5-0.3-1-0.7-1.2-1.2
|
||||||
|
c-0.3-0.6-0.5-1.4-0.5-2.5l0-4.6h-1.7v-3h1.7v-2.3l1.7,0l1.8-0.2v2.6h2.7l-0.4,3h-2.3v4.4c0,0.1,0,0.2,0,0.3c0,0.2,0,0.3,0.1,0.5
|
||||||
|
c0.1,0.3,0.3,0.5,0.7,0.6c0.3,0.1,1.9,0.1,1.9,0.1L1025.7,209.6z"/>
|
||||||
|
<path id="XMLID_813_" class="st0" d="M1047.7,197.9l-0.1,2.8c-0.3,0-1.2,0-2.1,0.2c-0.8,0.2-1.7,0.6-2.1,0.8v8h-3.5v-11.8h3.3
|
||||||
|
c0,0-0.1,0.6,0,0.9l0.8-0.3c0.5-0.2,1-0.3,1.7-0.4C1046.7,197.8,1047.3,197.9,1047.7,197.9z"/>
|
||||||
|
<path id="XMLID_811_" class="st0" d="M1108.9,197.9l-0.1,2.8c-0.3,0-1.2,0-2.1,0.2c-0.8,0.2-1.7,0.6-2.1,0.8v8h-3.5v-11.8h3.3
|
||||||
|
c0,0-0.1,0.6,0,0.9l0.8-0.3c0.5-0.2,1-0.3,1.7-0.4C1107.9,197.8,1108.5,197.9,1108.9,197.9z"/>
|
||||||
|
<path id="XMLID_809_" class="st0" d="M1054.6,210.1c-0.8,0-1.8-0.1-2.7-0.2c-0.4,0-0.8-0.1-1.2-0.2l-1.6-0.2l0-4.1l2.4,0.4
|
||||||
|
c0,0,1,0.1,1.4,0.2c0.5,0.1,1,0.1,1.4,0.1c1.5,0,2.4-0.4,2.7-1.1c0.1-0.3,0.1-0.6,0-0.9c-0.2-0.3-0.5-0.6-0.9-0.8
|
||||||
|
c-0.2-0.1-0.5-0.2-0.8-0.3l-0.9-0.3c-0.4-0.1-0.8-0.2-1.1-0.3c-0.4-0.1-0.8-0.3-1.1-0.4c-0.8-0.4-1.5-0.8-2-1.3
|
||||||
|
c-0.4-0.5-0.7-1.1-0.9-1.8c-0.1-0.5-0.1-1.1,0-1.7c0.2-1.8,1-3.1,2.3-3.8c0.5-0.3,1.1-0.5,1.8-0.6c0.3,0,0.6-0.1,0.9-0.1l0.9-0.1
|
||||||
|
h0.6c0.7,0,1.5,0,2.3,0.1l1,0.1l1.2,0.2l0,3.6c-0.5-0.1-1-0.2-1.7-0.3c-0.9-0.1-1.7-0.2-2.4-0.2c-1.6,0-2.2,0.3-2.5,1.1
|
||||||
|
c-0.1,0.3-0.1,0.6-0.1,0.9c0,0.4,0,0.5,0.7,0.8c0.2,0.1,0.5,0.2,0.8,0.3l2.1,0.6c0.4,0.1,0.8,0.3,1.1,0.4c0.8,0.3,1.4,0.7,1.8,1.1
|
||||||
|
c0.5,0.5,0.8,1.1,0.9,1.8c0.1,0.5,0.1,1.1,0,1.7c-0.2,1.8-1.1,3.4-2.4,4.1c-0.5,0.3-1.1,0.5-1.8,0.6c-0.3,0-0.6,0.1-0.9,0.1
|
||||||
|
l-0.8,0.1L1054.6,210.1L1054.6,210.1z"/>
|
||||||
|
<path id="XMLID_806_" class="st0" d="M1063.6,197.8h3.2l-0.1,0.8c0.3-0.2,0.6-0.3,1-0.4c0.4-0.2,0.8-0.3,1.1-0.3
|
||||||
|
c0.9-0.2,1.8-0.2,2.7,0.1c0.5,0.2,1,0.5,1.4,1c0.2,0.2,0.3,0.4,0.4,0.6l0.1,0.2c0,0.1,0.1,0.1,0.1,0.2c0.2,0.4,0.3,0.9,0.4,1.4
|
||||||
|
c0.1,0.6,0.2,1.3,0.2,2.2v0.7c0,1.8-0.1,2.7-0.4,3.4c-0.5,1.1-1.6,1.6-3.7,1.7c-0.3,0-0.7,0-1.2,0l-0.8,0c-0.3,0-0.5,0-0.7,0v5.1
|
||||||
|
l-3.6,0.2V197.8L1063.6,197.8z M1067.2,206.1c0.7,0.1,1.1,0.1,1.7,0.1l0.6-0.1c0.7-0.2,1-0.6,1-1.3v-2.3c0-0.3,0-0.6-0.1-0.7
|
||||||
|
c-0.2-0.7-0.8-1-1.8-0.9l-0.4,0.1l-0.7,0.2l-0.3,0.1L1067.2,206.1L1067.2,206.1z"/>
|
||||||
|
<path id="XMLID_804_" class="st0" d="M1086.5,203.2v6.4h-3.6v-7c0-0.4-0.3-1.1-0.8-1.4c-0.4-0.2-1-0.2-1.2-0.1
|
||||||
|
c-0.5,0.1-1,0.4-1.3,0.6v7.9h-3.6v-17l3.6,0v6c0.4-0.2,0.9-0.3,1.4-0.4c0.4-0.1,0.8-0.1,1.2-0.1c2.1,0,3.2,0.6,3.7,1.7
|
||||||
|
C1086.2,200.2,1086.5,201.4,1086.5,203.2z"/>
|
||||||
|
</g>
|
||||||
|
<g id="XMLID_799_">
|
||||||
|
<path id="XMLID_800_" class="st0" d="M961.7,184.4l13.7,8v16.1l-13.7,8l-13.7-8v-16.1L961.7,184.4 M961.7,183.9l-14.1,8.3v16.5
|
||||||
|
l14.1,8.3l14.1-8.3v-16.5L961.7,183.9L961.7,183.9z"/>
|
||||||
|
</g>
|
||||||
|
<g id="XMLID_796_">
|
||||||
|
<polygon id="XMLID_64_" class="st1" points="961.9,200.6 973.8,193.6 973.8,207.5 961.9,214.5 "/>
|
||||||
|
</g>
|
||||||
|
<polygon id="XMLID_795_" class="st2" points="949.8,193.2 961.7,186.3 973.6,193.2 961.7,200.2 "/>
|
||||||
|
<g id="XMLID_792_">
|
||||||
|
<polygon id="XMLID_65_" class="st0" points="949.6,207.5 949.6,193.6 961.5,200.6 961.5,214.5 "/>
|
||||||
|
</g>
|
||||||
|
<g id="XMLID_789_">
|
||||||
|
<path id="XMLID_790_" class="st3" d="M957.8,209.4v-4.9l-1.8,3.9l-1.5-0.9l-0.8-5.4l-0.9,4.3l-2.1-1.3l1.7-6.2l2.6,1.6l0.6,3.8
|
||||||
|
l1.2-2.7l2.6,1.5l0.4,7.5L957.8,209.4z"/>
|
||||||
|
</g>
|
||||||
|
<g id="XMLID_786_">
|
||||||
|
<path id="XMLID_787_" class="st3" d="M968,209.1c-0.4,0.2-0.9,0.3-1.4,0.5c-0.2,0.1-0.4,0.1-0.6,0.2l-0.8,0.2l0-2l1.2-0.3
|
||||||
|
c0,0,0.5-0.1,0.7-0.2c0.3-0.1,0.5-0.2,0.7-0.3c0.8-0.3,1.2-0.7,1.3-1.1c0.1-0.2,0.1-0.3,0-0.4c-0.1-0.1-0.2-0.2-0.5-0.2
|
||||||
|
c-0.1,0-0.2,0-0.4,0l-0.5,0c-0.2,0-0.4,0.1-0.5,0.1c-0.2,0-0.4,0-0.6,0c-0.4,0-0.8-0.1-1-0.2c-0.2-0.1-0.4-0.4-0.5-0.7
|
||||||
|
c-0.1-0.2-0.1-0.5,0-0.8c0.1-0.9,0.4-1.7,1.1-2.3c0.3-0.2,0.5-0.5,0.9-0.7c0.1-0.1,0.3-0.2,0.5-0.2l0.5-0.2l0.3-0.1
|
||||||
|
c0.4-0.1,0.7-0.3,1.2-0.4l0.5-0.2l0.6-0.2l0.1,1.8c-0.2,0-0.5,0.1-0.9,0.2c-0.4,0.1-0.9,0.3-1.2,0.4c-0.8,0.3-1.1,0.6-1.2,1
|
||||||
|
c0,0.2,0,0.3,0,0.5c0,0.2,0,0.2,0.4,0.3c0.1,0,0.3,0,0.4,0l1.1-0.1c0.2,0,0.4,0,0.5,0c0.4,0,0.7,0.1,0.9,0.2
|
||||||
|
c0.2,0.1,0.4,0.4,0.5,0.7c0.1,0.2,0.1,0.5,0,0.8c-0.1,0.9-0.5,1.9-1.1,2.5c-0.2,0.2-0.5,0.5-0.9,0.7c-0.1,0.1-0.3,0.2-0.5,0.2
|
||||||
|
l-0.4,0.2L968,209.1z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 8.3 KiB |
|
@ -0,0 +1,111 @@
|
||||||
|
<template>
|
||||||
|
<v-ace-editor
|
||||||
|
:value="currentValue"
|
||||||
|
:lang="props.lang"
|
||||||
|
:theme="props.theme"
|
||||||
|
:style="{ height: props.height, width: props.width }"
|
||||||
|
@init="editorInit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, PropType, ref, onUnmounted, watch } from 'vue';
|
||||||
|
import { VAceEditor } from 'vue3-ace-editor';
|
||||||
|
import ace from 'ace-builds';
|
||||||
|
import workerJavascriptUrl from 'ace-builds/src-min-noconflict/worker-javascript?url';
|
||||||
|
import workerJsonUrl from 'ace-builds/src-min-noconflict/worker-json?url';
|
||||||
|
import workerHtmlUrl from 'ace-builds/src-min-noconflict/worker-html?url';
|
||||||
|
import workerXmlUrl from 'ace-builds/src-min-noconflict/worker-xml?url';
|
||||||
|
import 'ace-builds/src-min-noconflict/ext-language_tools';
|
||||||
|
import 'ace-builds/src-min-noconflict/ext-beautify';
|
||||||
|
import 'ace-builds/src-min-noconflict/theme-github_dark';
|
||||||
|
import 'ace-builds/src-min-noconflict/theme-github';
|
||||||
|
import 'ace-builds/src-min-noconflict/theme-chrome';
|
||||||
|
import 'ace-builds/src-min-noconflict/mode-javascript';
|
||||||
|
import 'ace-builds/src-min-noconflict/mode-html';
|
||||||
|
import 'ace-builds/src-min-noconflict/mode-xml';
|
||||||
|
import 'ace-builds/src-min-noconflict/mode-json';
|
||||||
|
import 'ace-builds/src-min-noconflict/mode-java';
|
||||||
|
|
||||||
|
export type LangType = 'javascript' | 'html' | 'xml' | 'json' | 'java' | 'text';
|
||||||
|
export type ThemeType = 'github_dark' | 'github' | 'chrome';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
init: {
|
||||||
|
type: Function,
|
||||||
|
},
|
||||||
|
lang: {
|
||||||
|
type: String as PropType<LangType>,
|
||||||
|
default: 'javascript',
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String as PropType<ThemeType>,
|
||||||
|
default: 'github_dark',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '500px',
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: '100%',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:content']);
|
||||||
|
const editorInstance = ref<any>(null);
|
||||||
|
|
||||||
|
const currentValue = computed({
|
||||||
|
get() {
|
||||||
|
return props.content;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
emit('update:content', val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onLangChange = () => {
|
||||||
|
let useWorker = true;
|
||||||
|
if (props.lang === 'javascript') {
|
||||||
|
ace.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl);
|
||||||
|
} else if (props.lang === 'json') {
|
||||||
|
ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl);
|
||||||
|
} else if (props.lang === 'xml') {
|
||||||
|
ace.config.setModuleUrl('ace/mode/xml_worker', workerXmlUrl);
|
||||||
|
} else if (props.lang === 'html') {
|
||||||
|
ace.config.setModuleUrl('ace/mode/html_worker', workerHtmlUrl);
|
||||||
|
} else {
|
||||||
|
useWorker = false;
|
||||||
|
}
|
||||||
|
return useWorker;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editorInit = (editor: any) => {
|
||||||
|
if (props.init) props.init(editor);
|
||||||
|
editorInstance.value = editor;
|
||||||
|
const useWorker = onLangChange();
|
||||||
|
editor.setOptions({
|
||||||
|
enableBasicAutocompletion: true,
|
||||||
|
enableSnippets: false,
|
||||||
|
enableLiveAutocompletion: true,
|
||||||
|
wrap: true,
|
||||||
|
useWorker,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.lang,
|
||||||
|
() => {
|
||||||
|
onLangChange();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
editorInstance.value.destroy();
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<a-breadcrumb class="container-breadcrumb">
|
||||||
|
<a-breadcrumb-item>
|
||||||
|
<icon-apps />
|
||||||
|
</a-breadcrumb-item>
|
||||||
|
<a-breadcrumb-item v-for="item in items" :key="item">
|
||||||
|
{{ $t(item) }}
|
||||||
|
</a-breadcrumb-item>
|
||||||
|
</a-breadcrumb>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.container-breadcrumb {
|
||||||
|
margin: 16px 0;
|
||||||
|
:deep(.arco-breadcrumb-item) {
|
||||||
|
color: rgb(var(--gray-6));
|
||||||
|
&:last-child {
|
||||||
|
color: rgb(var(--gray-8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<VCharts v-if="renderChart" :option="options" :autoresize="autoResize" :style="{ width, height }" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, nextTick } from 'vue';
|
||||||
|
import VCharts from 'vue-echarts';
|
||||||
|
// import { useAppStore } from '@/store';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
autoResize: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: '100%',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '100%',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// const appStore = useAppStore();
|
||||||
|
// const theme = computed(() => {
|
||||||
|
// if (appStore.theme === 'dark') return 'dark';
|
||||||
|
// return '';
|
||||||
|
// });
|
||||||
|
const renderChart = ref(false);
|
||||||
|
// wait container expand
|
||||||
|
nextTick(() => {
|
||||||
|
renderChart.value = true;
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<form-create v-model="value" v-model:api="fApi" :rule="rule" :option="option"></form-create>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive } from 'vue';
|
||||||
|
|
||||||
|
const value = ref({});
|
||||||
|
const fApi = ref({});
|
||||||
|
const rule = reactive([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
field: 'goods_name',
|
||||||
|
title: '商品名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'datePicker',
|
||||||
|
field: 'created_at',
|
||||||
|
title: '创建时间',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const option = {
|
||||||
|
onSubmit(formData: any) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(JSON.stringify(formData));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import Footer from './index.vue';
|
||||||
|
|
||||||
|
describe('Footer', () => {
|
||||||
|
test('mount @vue/test-utils', () => {
|
||||||
|
const wrapper = shallowMount(Footer, {
|
||||||
|
slots: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(wrapper.text()).toBe('');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<a-layout-footer class="footer">MeterSphere</a-layout-footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<div class="block">
|
||||||
|
<h5 class="title">{{ title }}</h5>
|
||||||
|
<div v-for="option in options" :key="option.name" class="switch-wrapper">
|
||||||
|
<span>{{ $t(option.name) }}</span>
|
||||||
|
<form-wrapper
|
||||||
|
:type="option.type || 'switch'"
|
||||||
|
:name="option.key"
|
||||||
|
:default-value="option.defaultVal"
|
||||||
|
@input-change="handleChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
import FormWrapper from './form-wrapper.vue';
|
||||||
|
|
||||||
|
interface OptionsProps {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
type?: string;
|
||||||
|
defaultVal?: boolean | string | number;
|
||||||
|
}
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array as PropType<OptionsProps[]>,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const handleChange = async ({ key, value }: { key: string; value: unknown }) => {
|
||||||
|
if (key === 'colorWeak') {
|
||||||
|
document.body.style.filter = value ? 'invert(80%)' : 'none';
|
||||||
|
}
|
||||||
|
if (key === 'topMenu') {
|
||||||
|
appStore.updateSettings({
|
||||||
|
menuCollapse: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
appStore.updateSettings({ [key]: value });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.block {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.switch-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<a-input-number
|
||||||
|
v-if="type === 'number'"
|
||||||
|
:style="{ width: '80px' }"
|
||||||
|
size="small"
|
||||||
|
:default-value="(defaultValue as number)"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
<a-switch v-else :default-checked="(defaultValue as boolean)" size="small" @change="handleChange" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const props = defineProps({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
type: [String, Boolean, Number],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['inputChange']);
|
||||||
|
const handleChange = (value: unknown) => {
|
||||||
|
emit('inputChange', {
|
||||||
|
value,
|
||||||
|
key: props.name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,93 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="!appStore.navbar" class="fixed-settings" @click="setVisible">
|
||||||
|
<a-button type="primary">
|
||||||
|
<template #icon>
|
||||||
|
<icon-settings />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-drawer
|
||||||
|
:width="300"
|
||||||
|
unmount-on-close
|
||||||
|
:visible="visible"
|
||||||
|
:cancel-text="$t('settings.close')"
|
||||||
|
:ok-text="$t('settings.copySettings')"
|
||||||
|
@ok="copySettings"
|
||||||
|
@cancel="cancel"
|
||||||
|
>
|
||||||
|
<template #title> {{ $t('settings.title') }} </template>
|
||||||
|
<Block :options="contentOpts" :title="$t('settings.content')" />
|
||||||
|
<Block :options="othersOpts" :title="$t('settings.otherSettings')" />
|
||||||
|
<a-alert>{{ $t('settings.alertContent') }}</a-alert>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { useClipboard } from '@vueuse/core';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
import Block from './block.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['cancel']);
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const { copy } = useClipboard();
|
||||||
|
const visible = computed(() => appStore.globalSettings);
|
||||||
|
const contentOpts = computed(() => [
|
||||||
|
{ name: 'settings.navbar', key: 'navbar', defaultVal: appStore.navbar },
|
||||||
|
{
|
||||||
|
name: 'settings.menu',
|
||||||
|
key: 'menu',
|
||||||
|
defaultVal: appStore.menu,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'settings.topMenu',
|
||||||
|
key: 'topMenu',
|
||||||
|
defaultVal: appStore.topMenu,
|
||||||
|
},
|
||||||
|
{ name: 'settings.footer', key: 'footer', defaultVal: appStore.footer },
|
||||||
|
{ name: 'settings.tabBar', key: 'tabBar', defaultVal: appStore.tabBar },
|
||||||
|
{
|
||||||
|
name: 'settings.menuWidth',
|
||||||
|
key: 'menuWidth',
|
||||||
|
defaultVal: appStore.menuWidth,
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const othersOpts = computed(() => [
|
||||||
|
{
|
||||||
|
name: 'settings.colorWeak',
|
||||||
|
key: 'colorWeak',
|
||||||
|
defaultVal: appStore.colorWeak,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
appStore.updateSettings({ globalSettings: false });
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
const copySettings = async () => {
|
||||||
|
const text = JSON.stringify(appStore.$state, null, 2);
|
||||||
|
await copy(text);
|
||||||
|
Message.success(t('settings.copySettings.message'));
|
||||||
|
};
|
||||||
|
const setVisible = () => {
|
||||||
|
appStore.updateSettings({ globalSettings: true });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.fixed-settings {
|
||||||
|
position: fixed;
|
||||||
|
top: 280px;
|
||||||
|
right: 0;
|
||||||
|
svg {
|
||||||
|
font-size: 18px;
|
||||||
|
vertical-align: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { App } from 'vue';
|
||||||
|
import { use } from 'echarts/core';
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts';
|
||||||
|
import {
|
||||||
|
GridComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
GraphicComponent,
|
||||||
|
} from 'echarts/components';
|
||||||
|
import Chart from './chart/index.vue';
|
||||||
|
import Breadcrumb from './breadcrumb/index.vue';
|
||||||
|
|
||||||
|
// Manually introduce ECharts modules to reduce packing size
|
||||||
|
|
||||||
|
use([
|
||||||
|
CanvasRenderer,
|
||||||
|
BarChart,
|
||||||
|
LineChart,
|
||||||
|
PieChart,
|
||||||
|
RadarChart,
|
||||||
|
GridComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
GraphicComponent,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(Vue: App) {
|
||||||
|
Vue.component('Chart', Chart);
|
||||||
|
Vue.component('Breadcrumb', Breadcrumb);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,58 @@
|
||||||
|
<template>
|
||||||
|
<pre ref="jr" :class="props.class" @click="pickPath"></pre>
|
||||||
|
<input ref="ip" :value="jsonPath" class="path" type="hidden" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, onUnmounted, Ref } from 'vue';
|
||||||
|
import JPPicker from 'jsonpath-picker-vanilla';
|
||||||
|
import { Recordable } from '#/global';
|
||||||
|
|
||||||
|
const jr: Ref<HTMLElement | null> = ref(null);
|
||||||
|
const ip: Ref<HTMLInputElement | null> = ref(null);
|
||||||
|
const jsonPath = ref('');
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
data: object;
|
||||||
|
opt?: Recordable<any>;
|
||||||
|
class?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
data: () => ({
|
||||||
|
name1: 'val1',
|
||||||
|
name2: {
|
||||||
|
name3: 'val2',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
opt: () => ({}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'pick', path: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
JPPicker.jsonPathPicker(jr.value, props.data, [ip.value], props.opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
function pickPath(ev: any) {
|
||||||
|
if (ev.target && ev.target.classList.contains('pick-path')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ip.value) {
|
||||||
|
jsonPath.value = ip.value.value;
|
||||||
|
emit('pick', jsonPath.value);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
JPPicker.clearJsonPathPicker(jr.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css">
|
||||||
|
@import url('jsonpath-picker-vanilla/lib/jsonpath-picker.css');
|
||||||
|
</style>
|
|
@ -0,0 +1,149 @@
|
||||||
|
<script lang="tsx">
|
||||||
|
import { defineComponent, ref, h, compile, computed } from 'vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { useRoute, useRouter, RouteRecordRaw } from 'vue-router';
|
||||||
|
import type { RouteMeta } from 'vue-router';
|
||||||
|
import { useAppStore } from '@/store';
|
||||||
|
import { listenerRouteChange } from '@/utils/route-listener';
|
||||||
|
import { openWindow, regexUrl } from '@/utils';
|
||||||
|
import useMenuTree from './use-menu-tree';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emit: ['collapse'],
|
||||||
|
setup() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const { menuTree } = useMenuTree();
|
||||||
|
const collapsed = computed({
|
||||||
|
get() {
|
||||||
|
if (appStore.device === 'desktop') return appStore.menuCollapse;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
set(value: boolean) {
|
||||||
|
appStore.updateSettings({ menuCollapse: value });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const topMenu = computed(() => appStore.topMenu);
|
||||||
|
const openKeys = ref<string[]>([]);
|
||||||
|
const selectedKey = ref<string[]>([]);
|
||||||
|
|
||||||
|
const goto = (item: RouteRecordRaw) => {
|
||||||
|
// Open external link
|
||||||
|
if (regexUrl.test(item.path)) {
|
||||||
|
openWindow(item.path);
|
||||||
|
selectedKey.value = [item.name as string];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Eliminate external link side effects
|
||||||
|
const { hideInMenu, activeMenu } = item.meta as RouteMeta;
|
||||||
|
if (route.name === item.name && !hideInMenu && !activeMenu) {
|
||||||
|
selectedKey.value = [item.name as string];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Trigger router change
|
||||||
|
router.push({
|
||||||
|
name: item.name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const findMenuOpenKeys = (target: string) => {
|
||||||
|
const result: string[] = [];
|
||||||
|
let isFind = false;
|
||||||
|
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
|
||||||
|
if (item.name === target) {
|
||||||
|
isFind = true;
|
||||||
|
result.push(...keys);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.children?.length) {
|
||||||
|
item.children.forEach((el) => {
|
||||||
|
backtrack(el, [...keys, el.name as string]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
menuTree.value.forEach((el: RouteRecordRaw) => {
|
||||||
|
if (isFind) return; // Performance optimization
|
||||||
|
backtrack(el, [el.name as string]);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
listenerRouteChange((newRoute) => {
|
||||||
|
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
|
||||||
|
if (requiresAuth !== false && (!hideInMenu || activeMenu)) {
|
||||||
|
const menuOpenKeys = findMenuOpenKeys((activeMenu || newRoute.name) as string);
|
||||||
|
|
||||||
|
const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
|
||||||
|
openKeys.value = [...keySet];
|
||||||
|
|
||||||
|
selectedKey.value = [activeMenu || menuOpenKeys[menuOpenKeys.length - 1]];
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
const setCollapse = (val: boolean) => {
|
||||||
|
if (appStore.device === 'desktop') appStore.updateSettings({ menuCollapse: val });
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSubMenu = () => {
|
||||||
|
function travel(_route: RouteRecordRaw[], nodes = []) {
|
||||||
|
if (_route) {
|
||||||
|
_route.forEach((element) => {
|
||||||
|
// This is demo, modify nodes as needed
|
||||||
|
const icon = element?.meta?.icon ? () => h(compile(`<${element?.meta?.icon}/>`)) : null;
|
||||||
|
const node =
|
||||||
|
element?.children && element?.children.length !== 0 ? (
|
||||||
|
<a-sub-menu
|
||||||
|
key={element?.name}
|
||||||
|
v-slots={{
|
||||||
|
icon,
|
||||||
|
title: () => h(compile(t(element?.meta?.locale || ''))),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{travel(element?.children)}
|
||||||
|
</a-sub-menu>
|
||||||
|
) : (
|
||||||
|
<a-menu-item key={element?.name} v-slots={{ icon }} onClick={() => goto(element)}>
|
||||||
|
{t(element?.meta?.locale || '')}
|
||||||
|
</a-menu-item>
|
||||||
|
);
|
||||||
|
nodes.push(node as never);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
return travel(menuTree.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<a-menu
|
||||||
|
mode={topMenu.value ? 'horizontal' : 'vertical'}
|
||||||
|
v-model:collapsed={collapsed.value}
|
||||||
|
v-model:open-keys={openKeys.value}
|
||||||
|
show-collapse-button={appStore.device !== 'mobile'}
|
||||||
|
auto-open={false}
|
||||||
|
selected-keys={selectedKey.value}
|
||||||
|
auto-open-selected={true}
|
||||||
|
level-indent={34}
|
||||||
|
style="height: 100%;width:100%;"
|
||||||
|
onCollapse={setCollapse}
|
||||||
|
>
|
||||||
|
{renderSubMenu()}
|
||||||
|
</a-menu>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
:deep(.arco-menu-inner) {
|
||||||
|
.arco-menu-inline-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.arco-icon {
|
||||||
|
&:not(.arco-icon-down) {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { RouteRecordRaw, RouteRecordNormalized } from 'vue-router';
|
||||||
|
import usePermission from '@/hooks/usePermission';
|
||||||
|
import appClientMenus from '@/router/app-menus';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
|
export default function useMenuTree() {
|
||||||
|
const permission = usePermission();
|
||||||
|
const menuTree = computed(() => {
|
||||||
|
const copyRouter = cloneDeep(appClientMenus) as RouteRecordNormalized[];
|
||||||
|
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
|
||||||
|
return (a.meta.order || 0) - (b.meta.order || 0);
|
||||||
|
});
|
||||||
|
function travel(_routes: RouteRecordRaw[], layer: number) {
|
||||||
|
if (!_routes) return null;
|
||||||
|
|
||||||
|
const collector: any = _routes.map((element) => {
|
||||||
|
// 权限校验不通过
|
||||||
|
if (!permission.accessRouter(element)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 叶子菜单
|
||||||
|
if (element.meta?.hideChildrenInMenu || !element.children) {
|
||||||
|
element.children = [];
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤隐藏的菜单
|
||||||
|
element.children = element.children.filter((x) => x.meta?.hideInMenu !== true);
|
||||||
|
|
||||||
|
// 解析子菜单
|
||||||
|
const subItem = travel(element.children, layer + 1);
|
||||||
|
|
||||||
|
if (subItem.length) {
|
||||||
|
element.children = subItem;
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
// the else logic
|
||||||
|
if (layer > 1) {
|
||||||
|
element.children = subItem;
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.meta?.hideInMenu === false) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return collector.filter(Boolean);
|
||||||
|
}
|
||||||
|
return travel(copyRouter, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
menuTree,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
<template>
|
||||||
|
<a-spin style="display: block" :loading="loading">
|
||||||
|
<a-tabs v-model:activeKey="messageType" type="rounded" destroy-on-hide>
|
||||||
|
<a-tab-pane v-for="item in tabList" :key="item.key">
|
||||||
|
<template #title>
|
||||||
|
<span> {{ item.title }}{{ formatUnreadLength(item.key) }} </span>
|
||||||
|
</template>
|
||||||
|
<a-result v-if="!renderList.length" status="404">
|
||||||
|
<template #subtitle> {{ $t('messageBox.noContent') }} </template>
|
||||||
|
</a-result>
|
||||||
|
<List :render-list="renderList" :unread-count="unreadCount" @item-click="handleItemClick" />
|
||||||
|
</a-tab-pane>
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="text" @click="emptyList">
|
||||||
|
{{ $t('messageBox.tab.button') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</a-tabs>
|
||||||
|
</a-spin>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, toRefs, computed } from 'vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { queryMessageList, setMessageStatus, MessageRecord, MessageListType } from '@/api/modules/message';
|
||||||
|
import useLoading from '@/hooks/useLoading';
|
||||||
|
import List from './list.vue';
|
||||||
|
|
||||||
|
interface TabItem {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
const { loading, setLoading } = useLoading(true);
|
||||||
|
const messageType = ref('message');
|
||||||
|
const { t } = useI18n();
|
||||||
|
const messageData = reactive<{
|
||||||
|
renderList: MessageRecord[];
|
||||||
|
messageList: MessageRecord[];
|
||||||
|
}>({
|
||||||
|
renderList: [],
|
||||||
|
messageList: [],
|
||||||
|
});
|
||||||
|
toRefs(messageData);
|
||||||
|
const tabList: TabItem[] = [
|
||||||
|
{
|
||||||
|
key: 'message',
|
||||||
|
title: t('messageBox.tab.title.message'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'notice',
|
||||||
|
title: t('messageBox.tab.title.notice'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'todo',
|
||||||
|
title: t('messageBox.tab.title.todo'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
async function fetchSourceData() {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await queryMessageList();
|
||||||
|
messageData.messageList = data;
|
||||||
|
} catch (err) {
|
||||||
|
// you can report use errorHandler or other
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function readMessage(data: MessageListType) {
|
||||||
|
const ids = data.map((item) => item.id);
|
||||||
|
await setMessageStatus({ ids });
|
||||||
|
fetchSourceData();
|
||||||
|
}
|
||||||
|
const renderList = computed(() => {
|
||||||
|
return messageData.messageList.filter((item) => messageType.value === item.type);
|
||||||
|
});
|
||||||
|
const unreadCount = computed(() => {
|
||||||
|
return renderList.value.filter((item) => !item.status).length;
|
||||||
|
});
|
||||||
|
const getUnreadList = (type: string) => {
|
||||||
|
const list = messageData.messageList.filter((item) => item.type === type && !item.status);
|
||||||
|
return list;
|
||||||
|
};
|
||||||
|
const formatUnreadLength = (type: string) => {
|
||||||
|
const list = getUnreadList(type);
|
||||||
|
return list.length ? `(${list.length})` : ``;
|
||||||
|
};
|
||||||
|
const handleItemClick = (items: MessageListType) => {
|
||||||
|
if (renderList.value.length) readMessage([...items]);
|
||||||
|
};
|
||||||
|
const emptyList = () => {
|
||||||
|
messageData.messageList = [];
|
||||||
|
};
|
||||||
|
fetchSourceData();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
:deep(.arco-popover-popup-content) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
:deep(.arco-list-item-meta) {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
:deep(.arco-tabs-nav) {
|
||||||
|
padding: 14px 0 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--color-neutral-3);
|
||||||
|
}
|
||||||
|
:deep(.arco-tabs-content) {
|
||||||
|
padding-top: 0;
|
||||||
|
.arco-result-subtitle {
|
||||||
|
color: rgb(var(--gray-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,142 @@
|
||||||
|
<template>
|
||||||
|
<a-list :bordered="false">
|
||||||
|
<a-list-item
|
||||||
|
v-for="item in renderList"
|
||||||
|
:key="item.id"
|
||||||
|
action-layout="vertical"
|
||||||
|
:style="{
|
||||||
|
opacity: item.status ? 0.5 : 1,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<a-tag v-if="item.messageType === 0" color="gray">未开始</a-tag>
|
||||||
|
<a-tag v-else-if="item.messageType === 1" color="green">已开通</a-tag>
|
||||||
|
<a-tag v-else-if="item.messageType === 2" color="blue">进行中</a-tag>
|
||||||
|
<a-tag v-else-if="item.messageType === 3" color="red">即将到期</a-tag>
|
||||||
|
</template>
|
||||||
|
<div class="item-wrap" @click="onItemClick(item)">
|
||||||
|
<a-list-item-meta>
|
||||||
|
<template v-if="item.avatar" #avatar>
|
||||||
|
<a-avatar shape="circle">
|
||||||
|
<img v-if="item.avatar" :src="item.avatar" alt="avatar" />
|
||||||
|
<icon-desktop v-else />
|
||||||
|
</a-avatar>
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
<a-space :size="4">
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<a-typography-text type="secondary">
|
||||||
|
{{ item.subTitle }}
|
||||||
|
</a-typography-text>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<div>
|
||||||
|
<a-typography-paragraph
|
||||||
|
:ellipsis="{
|
||||||
|
rows: 1,
|
||||||
|
}"
|
||||||
|
>{{ item.content }}</a-typography-paragraph
|
||||||
|
>
|
||||||
|
<a-typography-text v-if="item.type === 'message'" class="time-text">
|
||||||
|
{{ item.time }}
|
||||||
|
</a-typography-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-list-item-meta>
|
||||||
|
</div>
|
||||||
|
</a-list-item>
|
||||||
|
<template #footer>
|
||||||
|
<a-space fill :size="0" :class="{ 'add-border-top': renderList.length < showMax }">
|
||||||
|
<div class="footer-wrap">
|
||||||
|
<a-link @click="allRead">{{ $t('messageBox.allRead') }}</a-link>
|
||||||
|
</div>
|
||||||
|
<div class="footer-wrap">
|
||||||
|
<a-link>{{ $t('messageBox.viewMore') }}</a-link>
|
||||||
|
</div>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="renderList.length && renderList.length < 3"
|
||||||
|
:style="{ height: (showMax - renderList.length) * 86 + 'px' }"
|
||||||
|
></div>
|
||||||
|
</a-list>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import { MessageRecord, MessageListType } from '@/api/modules/message';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
renderList: {
|
||||||
|
type: Array as PropType<MessageListType>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
unreadCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['itemClick']);
|
||||||
|
const allRead = () => {
|
||||||
|
emit('itemClick', [...props.renderList]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onItemClick = (item: MessageRecord) => {
|
||||||
|
if (!item.status) {
|
||||||
|
emit('itemClick', [item]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const showMax = 3;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
:deep(.arco-list) {
|
||||||
|
.arco-list-item {
|
||||||
|
min-height: 86px;
|
||||||
|
border-bottom: 1px solid rgb(var(--gray-3));
|
||||||
|
}
|
||||||
|
.arco-list-item-extra {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
.arco-list-item-meta-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.item-wrap {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.time-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgb(var(--gray-6));
|
||||||
|
}
|
||||||
|
.arco-empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.arco-list-footer {
|
||||||
|
padding: 0;
|
||||||
|
height: 50px;
|
||||||
|
border-top: none;
|
||||||
|
line-height: 50px;
|
||||||
|
.arco-space-item {
|
||||||
|
width: 100%;
|
||||||
|
border-right: 1px solid rgb(var(--gray-3));
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.add-border-top {
|
||||||
|
border-top: 1px solid rgb(var(--gray-3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.footer-wrap {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.arco-typography {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.add-border {
|
||||||
|
border-top: 1px solid rgb(var(--gray-3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,13 @@
|
||||||
|
export default {
|
||||||
|
'messageBox.tab.title.message': 'Message',
|
||||||
|
'messageBox.tab.title.notice': 'Notice',
|
||||||
|
'messageBox.tab.title.todo': 'Todo',
|
||||||
|
'messageBox.tab.button': 'empty',
|
||||||
|
'messageBox.allRead': 'All Read',
|
||||||
|
'messageBox.viewMore': 'View More',
|
||||||
|
'messageBox.noContent': 'No Content',
|
||||||
|
'messageBox.switchRoles': 'Switch Roles',
|
||||||
|
'messageBox.userCenter': 'User Center',
|
||||||
|
'messageBox.userSettings': 'User Settings',
|
||||||
|
'messageBox.logout': 'Logout',
|
||||||
|
};
|
|
@ -0,0 +1,13 @@
|
||||||
|
export default {
|
||||||
|
'messageBox.tab.title.message': '消息',
|
||||||
|
'messageBox.tab.title.notice': '通知',
|
||||||
|
'messageBox.tab.title.todo': '待办',
|
||||||
|
'messageBox.tab.button': '清空',
|
||||||
|
'messageBox.allRead': '全部已读',
|
||||||
|
'messageBox.viewMore': '查看更多',
|
||||||
|
'messageBox.noContent': '暂无内容',
|
||||||
|
'messageBox.switchRoles': '切换角色',
|
||||||
|
'messageBox.userCenter': '用户中心',
|
||||||
|
'messageBox.userSettings': '用户设置',
|
||||||
|
'messageBox.logout': '登出登录',
|
||||||
|
};
|
|
@ -0,0 +1,93 @@
|
||||||
|
export default {
|
||||||
|
minder: {
|
||||||
|
commons: {
|
||||||
|
confirm: 'Confirm',
|
||||||
|
clear: 'Clear',
|
||||||
|
export: 'Export',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
edit: 'Edit',
|
||||||
|
delete: 'Delete',
|
||||||
|
remove: 'Remove',
|
||||||
|
return: 'Return',
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
expand: {
|
||||||
|
expand: 'Expand',
|
||||||
|
folding: 'Folding',
|
||||||
|
expand_one: 'Expand one level',
|
||||||
|
expand_tow: 'Expand tow level',
|
||||||
|
expand_three: 'Expand three level',
|
||||||
|
expand_four: 'Expand four level',
|
||||||
|
expand_five: 'Expand five level',
|
||||||
|
expand_six: 'Expand six level',
|
||||||
|
},
|
||||||
|
insert: {
|
||||||
|
down: 'Subordinate',
|
||||||
|
up: 'Superior',
|
||||||
|
same: 'Same',
|
||||||
|
_same: 'Same level',
|
||||||
|
_down: 'Subordinate level',
|
||||||
|
_up: 'Superior level',
|
||||||
|
},
|
||||||
|
move: {
|
||||||
|
up: 'Up',
|
||||||
|
down: 'Down',
|
||||||
|
forward: 'Forward',
|
||||||
|
backward: 'Backward',
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
progress: 'Progress',
|
||||||
|
remove_progress: 'Remove progress',
|
||||||
|
prepare: 'Prepare',
|
||||||
|
complete_all: 'Complete all',
|
||||||
|
complete: 'Complete',
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
all: 'Select all',
|
||||||
|
invert: 'Select invert',
|
||||||
|
sibling: 'Select sibling node',
|
||||||
|
same: 'Select same node',
|
||||||
|
path: 'Select path',
|
||||||
|
subtree: 'Select subtree',
|
||||||
|
},
|
||||||
|
arrange: {
|
||||||
|
arrange_layout: 'Arrange layout',
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
font: 'Font',
|
||||||
|
size: 'Font size',
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
clear: 'Clear style',
|
||||||
|
copy: 'Copy style',
|
||||||
|
paste: 'Paste style',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
header: {
|
||||||
|
minder: 'Minder',
|
||||||
|
style: 'Appearance style',
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
save: 'Save',
|
||||||
|
},
|
||||||
|
navigator: {
|
||||||
|
amplification: 'Amplification',
|
||||||
|
narrow: 'Narrow',
|
||||||
|
drag: 'Drag',
|
||||||
|
locating_root: 'Locating root node',
|
||||||
|
navigator: 'Navigator',
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
undo: 'Undo',
|
||||||
|
redo: 'Redo',
|
||||||
|
},
|
||||||
|
subject: {
|
||||||
|
central: 'Central subject',
|
||||||
|
branch: 'Subject',
|
||||||
|
},
|
||||||
|
priority: 'Priority',
|
||||||
|
tag: 'Tag',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,93 @@
|
||||||
|
export default {
|
||||||
|
minder: {
|
||||||
|
commons: {
|
||||||
|
confirm: '确定',
|
||||||
|
clear: '清空',
|
||||||
|
export: '导出',
|
||||||
|
cancel: '取消',
|
||||||
|
edit: '编辑',
|
||||||
|
delete: '删除',
|
||||||
|
remove: '移除',
|
||||||
|
return: '返回',
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
expand: {
|
||||||
|
expand: '展开',
|
||||||
|
folding: '收起',
|
||||||
|
expand_one: '展开到一级节点',
|
||||||
|
expand_tow: '展开到二级节点',
|
||||||
|
expand_three: '展开到三级节点',
|
||||||
|
expand_four: '展开到四级节点',
|
||||||
|
expand_five: '展开到五级节点',
|
||||||
|
expand_six: '展开到六级节点',
|
||||||
|
},
|
||||||
|
insert: {
|
||||||
|
down: '插入下级主题',
|
||||||
|
up: '插入上级主题',
|
||||||
|
same: '插入同级主题',
|
||||||
|
_same: '同级',
|
||||||
|
_down: '下级',
|
||||||
|
_up: '上级',
|
||||||
|
},
|
||||||
|
move: {
|
||||||
|
up: '上移',
|
||||||
|
down: '下移',
|
||||||
|
forward: '前移',
|
||||||
|
backward: '后移',
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
progress: '进度',
|
||||||
|
remove_progress: '移除进度',
|
||||||
|
prepare: '未开始',
|
||||||
|
complete_all: '全部完成',
|
||||||
|
complete: '完成',
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
all: '全选',
|
||||||
|
invert: '反选',
|
||||||
|
sibling: '选择兄弟节点',
|
||||||
|
same: '选择同级节点',
|
||||||
|
path: '选择路径',
|
||||||
|
subtree: '选择子树',
|
||||||
|
},
|
||||||
|
arrange: {
|
||||||
|
arrange_layout: '整理布局',
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
font: '字体',
|
||||||
|
size: '字号',
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
clear: '清除样式',
|
||||||
|
copy: '复制样式',
|
||||||
|
paste: '粘贴样式',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
header: {
|
||||||
|
minder: '思维导图',
|
||||||
|
style: '外观样式',
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
save: '保存',
|
||||||
|
},
|
||||||
|
navigator: {
|
||||||
|
amplification: '放大',
|
||||||
|
narrow: '缩小',
|
||||||
|
drag: '拖拽',
|
||||||
|
locating_root: '定位根节点',
|
||||||
|
navigator: '导航器',
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
undo: '撤销',
|
||||||
|
redo: '重做',
|
||||||
|
},
|
||||||
|
subject: {
|
||||||
|
central: '中心主题',
|
||||||
|
branch: '分支主题',
|
||||||
|
},
|
||||||
|
priority: '优先级',
|
||||||
|
tag: '标签',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,111 @@
|
||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<a-tabs v-model="activeName" class="mind_tab-content">
|
||||||
|
<a-tab-pane key="editMenu" :title="t('minder.main.header.minder')">
|
||||||
|
<div class="mind-tab-panel">
|
||||||
|
<edit-menu
|
||||||
|
:minder="minder"
|
||||||
|
:move-enable="props.moveEnable"
|
||||||
|
:sequence-enable="props.sequenceEnable"
|
||||||
|
:tag-enable="props.tagEnable"
|
||||||
|
:progress-enable="props.progressEnable"
|
||||||
|
:priority-count="props.priorityCount"
|
||||||
|
:priority-prefix="props.priorityPrefix"
|
||||||
|
:tag-edit-check="props.tagEditCheck"
|
||||||
|
:tag-disable-check="props.tagDisableCheck"
|
||||||
|
:priority-disable-check="props.priorityDisableCheck"
|
||||||
|
:priority-start-with-zero="props.priorityStartWithZero"
|
||||||
|
:tags="props.tags"
|
||||||
|
:distinct-tags="props.distinctTags"
|
||||||
|
:del-confirm="props.delConfirm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="viewMenu" :title="t('minder.main.header.style')">
|
||||||
|
<div class="mind-tab-panel">
|
||||||
|
<view-menu :minder="minder" :default-mold="props.defaultMold" @mold-change="handleMoldChange" />
|
||||||
|
</div>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="headerVue" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import editMenu from '../menu/edit/editMenu.vue';
|
||||||
|
import viewMenu from '../menu/view/viewMenu.vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { editMenuProps, moleProps, priorityProps, tagProps, delProps } from '../props';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
...editMenuProps,
|
||||||
|
...moleProps,
|
||||||
|
...priorityProps,
|
||||||
|
...tagProps,
|
||||||
|
...delProps,
|
||||||
|
minder: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'moldChange', data: number): void;
|
||||||
|
}>();
|
||||||
|
const activeName = ref('editMenu');
|
||||||
|
|
||||||
|
function handleMoldChange(data: number) {
|
||||||
|
emit('moldChange', data);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@import '../style/header';
|
||||||
|
.mind_tab-content {
|
||||||
|
.tab-icons {
|
||||||
|
background-image: url('@/assets/images/minder/icons.png');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
header {
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
& > ul {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 30px;
|
||||||
|
background-color: #e1e1e1;
|
||||||
|
li {
|
||||||
|
line-height: 30px;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 80px;
|
||||||
|
height: 100%;
|
||||||
|
list-style: none;
|
||||||
|
a {
|
||||||
|
width: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #337ab7;
|
||||||
|
}
|
||||||
|
a:hover,
|
||||||
|
a:focus {
|
||||||
|
color: #23527c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li.selected {
|
||||||
|
background: #ffffff;
|
||||||
|
a {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.arco-tabs-content {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,141 @@
|
||||||
|
<template>
|
||||||
|
<div ref="mec" class="minder-container" :style="{ height: `${props.height}px` }">
|
||||||
|
<a-button type="primary" :disabled="props.disabled" class="save-btn" @click="save">{{
|
||||||
|
t('minder.main.main.save')
|
||||||
|
}}</a-button>
|
||||||
|
<navigator />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="minderContainer" setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import Navigator from './navigator.vue';
|
||||||
|
import { markChangeNode, markDeleteNode } from '../script/tool/utils';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { editMenuProps, mainEditorProps, priorityProps, tagProps } from '../props';
|
||||||
|
import Editor from '../script/editor';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const props = defineProps({ ...editMenuProps, ...mainEditorProps, ...tagProps, ...priorityProps });
|
||||||
|
|
||||||
|
const emit = defineEmits({
|
||||||
|
afterMount: () => ({}),
|
||||||
|
save: (json) => json,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mec: Ref<HTMLDivElement | null> = ref(null);
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
emit('save', window.minder.exportJson());
|
||||||
|
}
|
||||||
|
function handlePriorityButton() {
|
||||||
|
const { priorityPrefix } = props;
|
||||||
|
const { priorityStartWithZero } = props;
|
||||||
|
let start = priorityStartWithZero ? 0 : 1;
|
||||||
|
let res = '';
|
||||||
|
for (let i = 0; i < props.priorityCount; i++) {
|
||||||
|
res += start++;
|
||||||
|
}
|
||||||
|
const priority = window.minder.hotbox.state('priority');
|
||||||
|
res.replace(/./g, (p) => {
|
||||||
|
priority.button({
|
||||||
|
position: 'ring',
|
||||||
|
label: priorityPrefix + p,
|
||||||
|
key: p,
|
||||||
|
action() {
|
||||||
|
const pVal = parseInt(p, 10);
|
||||||
|
window.minder.execCommand('Priority', priorityStartWithZero ? pVal + 1 : pVal);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// 需要返回字符串
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function handleTagButton() {
|
||||||
|
const tag = window.minder.hotbox.state('tag');
|
||||||
|
props.tags?.forEach((item) => {
|
||||||
|
tag.button({
|
||||||
|
position: 'ring',
|
||||||
|
label: item,
|
||||||
|
key: item,
|
||||||
|
action() {
|
||||||
|
window.minder.execCommand('resource', item);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
window.editor = new Editor(mec.value, {
|
||||||
|
sequenceEnable: props.sequenceEnable,
|
||||||
|
tagEnable: props.tagEnable,
|
||||||
|
progressEnable: props.progressEnable,
|
||||||
|
moveEnable: props.moveEnable,
|
||||||
|
});
|
||||||
|
const { editor } = window;
|
||||||
|
if (Object.keys(props.importJson || {}).length > 0) {
|
||||||
|
editor.minder.importJson(props.importJson);
|
||||||
|
}
|
||||||
|
window.km = editor.minder;
|
||||||
|
window.minder = window.km;
|
||||||
|
window.minderEditor = editor;
|
||||||
|
window.minder.moveEnable = props.moveEnable;
|
||||||
|
|
||||||
|
window.minder.forceRemoveNode = () => {
|
||||||
|
markDeleteNode(window.minder);
|
||||||
|
window.minder.execCommand('RemoveNode');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.minder.on('preExecCommand', (env: any) => {
|
||||||
|
const selectNodes = env.minder.getSelectedNodes();
|
||||||
|
const notChangeCommands = new Set([
|
||||||
|
'camera',
|
||||||
|
'copy',
|
||||||
|
'expand',
|
||||||
|
'expandToLevel',
|
||||||
|
'hand',
|
||||||
|
'layout',
|
||||||
|
'template',
|
||||||
|
'theme',
|
||||||
|
'zoom',
|
||||||
|
'zoomIn',
|
||||||
|
'zoomOut',
|
||||||
|
'append',
|
||||||
|
'appendchildnode',
|
||||||
|
'appendsiblingnode',
|
||||||
|
]);
|
||||||
|
if (selectNodes && !notChangeCommands.has(env.commandName.toLocaleLowerCase())) {
|
||||||
|
selectNodes.forEach((node: any) => {
|
||||||
|
markChangeNode(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (env.commandName === 'movetoparent') {
|
||||||
|
setTimeout(() => {
|
||||||
|
const targetNode = window.minder.getSelectedNode();
|
||||||
|
targetNode.parent.renderTree();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePriorityButton();
|
||||||
|
handleTagButton();
|
||||||
|
emit('afterMount');
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@import '../style/editor.less';
|
||||||
|
.save-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 30px;
|
||||||
|
bottom: 30px;
|
||||||
|
}
|
||||||
|
.minder-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,316 @@
|
||||||
|
<template>
|
||||||
|
<div class="navigator">
|
||||||
|
<div class="nav-bar">
|
||||||
|
<div
|
||||||
|
class="nav-btn zoom-in"
|
||||||
|
:title="t('minder.main.navigator.amplification')"
|
||||||
|
:class="{ active: zoomRadioIn }"
|
||||||
|
@click="zoomIn"
|
||||||
|
>
|
||||||
|
<div class="icon" />
|
||||||
|
</div>
|
||||||
|
<div ref="zoomPan" class="zoom-pan">
|
||||||
|
<div class="origin" :style="{ transform: 'translate(0, ' + getHeight(100) + 'px)' }" @click="RestoreSize" />
|
||||||
|
<div
|
||||||
|
class="indicator"
|
||||||
|
:style="{
|
||||||
|
transform: 'translate(0, ' + getHeight(zoom) + 'px)',
|
||||||
|
transition: 'transform 200ms',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-btn zoom-out"
|
||||||
|
:title="t('minder.main.navigator.narrow')"
|
||||||
|
:class="{ active: zoomRadioOut }"
|
||||||
|
@click="zoomOut"
|
||||||
|
>
|
||||||
|
<div class="icon" />
|
||||||
|
</div>
|
||||||
|
<div class="nav-btn hand" :title="t('minder.main.navigator.drag')" :class="{ active: enableHand }" @click="hand">
|
||||||
|
<div class="icon" />
|
||||||
|
</div>
|
||||||
|
<div class="nav-btn camera" :title="t('minder.main.navigator.locating_root')" @click="locateToOrigin">
|
||||||
|
<div class="icon" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nav-btn nav-trigger"
|
||||||
|
:class="{ active: isNavOpen }"
|
||||||
|
:title="t('minder.main.navigator.navigator')"
|
||||||
|
@click="toggleNavOpen"
|
||||||
|
>
|
||||||
|
<div class="icon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="isNavOpen" ref="navPreviewer" class="nav-previewer" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="navigator" setup>
|
||||||
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { getLocalStorage, setLocalStorage } from '../script/store';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const zoomPan: Ref<HTMLDivElement | null> = ref(null);
|
||||||
|
const navPreviewer: Ref<HTMLDivElement | null> = ref(null);
|
||||||
|
|
||||||
|
const zoom = ref(100);
|
||||||
|
const isNavOpen = ref(true);
|
||||||
|
const previewNavigator: Ref<HTMLDivElement | null> = ref(null);
|
||||||
|
const contentView = ref('');
|
||||||
|
|
||||||
|
let visibleView = reactive<any>({});
|
||||||
|
let visibleRect = reactive<any>({});
|
||||||
|
let nodeThumb = reactive<any>({});
|
||||||
|
let connectionThumb = reactive<any>({});
|
||||||
|
let paper = reactive<any>({});
|
||||||
|
let minder = reactive<any>({});
|
||||||
|
|
||||||
|
const config = reactive({
|
||||||
|
// 右侧面板最小宽度
|
||||||
|
ctrlPanelMin: 250,
|
||||||
|
// 右侧面板宽度
|
||||||
|
ctrlPanelWidth: parseInt(window.localStorage.getItem('__dev_minder_ctrlPanelWidth') || '', 10) || 250,
|
||||||
|
// 分割线宽度
|
||||||
|
dividerWidth: 3,
|
||||||
|
// 默认语言
|
||||||
|
defaultLang: 'zh-cn',
|
||||||
|
// 放大缩小比例
|
||||||
|
zoom: [10, 20, 30, 50, 80, 100, 120, 150, 200],
|
||||||
|
});
|
||||||
|
|
||||||
|
const enableHand = ref(minder && minder.queryCommandState && minder.queryCommandState('hand') === 1);
|
||||||
|
// 避免缓存
|
||||||
|
function getNavOpenState() {
|
||||||
|
return getLocalStorage('navigator-hidden');
|
||||||
|
}
|
||||||
|
function zoomIn() {
|
||||||
|
if (minder && minder.execCommand) {
|
||||||
|
minder.execCommand('zoomIn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function RestoreSize() {
|
||||||
|
if (minder && minder.execCommand) {
|
||||||
|
minder.execCommand('zoom', 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function zoomOut() {
|
||||||
|
if (minder && minder.execCommand) {
|
||||||
|
minder.execCommand('zoomOut');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function hand() {
|
||||||
|
if (minder && minder.execCommand) {
|
||||||
|
minder.execCommand('hand');
|
||||||
|
enableHand.value = minder.queryCommandState && minder.queryCommandState('hand') === 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getZoomRadio(value: number) {
|
||||||
|
try {
|
||||||
|
if (!minder) return 2;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const zoomStack = minder && minder.getOption && minder.getOption('zoom');
|
||||||
|
if (!zoomStack) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const minValue = zoomStack[0];
|
||||||
|
const maxValue = zoomStack[zoomStack.length - 1];
|
||||||
|
const valueRange = maxValue - minValue;
|
||||||
|
|
||||||
|
return 1 - (value - minValue) / valueRange;
|
||||||
|
}
|
||||||
|
const zoomRadioIn = computed(() => getZoomRadio(zoom.value) === 0);
|
||||||
|
const zoomRadioOut = computed(() => getZoomRadio(zoom.value) === 1);
|
||||||
|
|
||||||
|
function getHeight(value: number) {
|
||||||
|
const totalHeight = Number(zoomPan.value?.style.height);
|
||||||
|
return getZoomRadio(value) * totalHeight;
|
||||||
|
}
|
||||||
|
function locateToOrigin() {
|
||||||
|
if (minder && minder.execCommand && minder.getRoot) {
|
||||||
|
minder.execCommand('camera', minder.getRoot(), 600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPathHandler(theme?: string) {
|
||||||
|
switch (theme) {
|
||||||
|
case 'tianpan':
|
||||||
|
case 'tianpan-compact':
|
||||||
|
return (nodePathData: any[], x: number, y: number, width: number) => {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
const r = width >> 1;
|
||||||
|
nodePathData.push('M', x, y + r, 'a', r, r, 0, 1, 1, 0, 0.01, 'z');
|
||||||
|
};
|
||||||
|
default: {
|
||||||
|
return (nodePathData: any[], x: number, y: number, width: number, height: number) => {
|
||||||
|
nodePathData.push('M', x, y, 'h', width, 'v', height, 'h', -width, 'z');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pathHandler = getPathHandler(minder.getTheme ? minder.getTheme() : '');
|
||||||
|
|
||||||
|
function updateVisibleView() {
|
||||||
|
if (minder.getViewDragger && visibleRect.setBox) {
|
||||||
|
visibleView = minder.getViewDragger().getView();
|
||||||
|
visibleRect.setBox(visibleView.intersect && visibleView.intersect(contentView.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateContentView() {
|
||||||
|
if (!minder.getRenderContainer || !paper.setViewBox || !minder.getRoot) return;
|
||||||
|
const view = minder.getRenderContainer().getBoundaryBox();
|
||||||
|
contentView.value = view;
|
||||||
|
const padding = 30;
|
||||||
|
paper.setViewBox(
|
||||||
|
view.x - padding - 0.5,
|
||||||
|
view.y - padding - 0.5,
|
||||||
|
view.width + padding * 2 + 1,
|
||||||
|
view.height + padding * 2 + 1
|
||||||
|
);
|
||||||
|
const nodePathData: any[] = [];
|
||||||
|
const connectionThumbData: any[] = [];
|
||||||
|
minder.getRoot().traverse((node: any) => {
|
||||||
|
const box = node.getLayoutBox();
|
||||||
|
pathHandler(nodePathData, box.x, box.y, box.width, box.height);
|
||||||
|
if (node.getConnection() && node.parent && node.parent.isExpanded()) {
|
||||||
|
connectionThumbData.push(node.getConnection().getPathData());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!paper.setStyle || !minder.getStyle || !nodeThumb.fill || !nodeThumb.setPathData) return;
|
||||||
|
paper.setStyle('background', minder.getStyle('background'));
|
||||||
|
if (nodePathData.length) {
|
||||||
|
nodeThumb.fill(minder.getStyle('root-background')).setPathData(nodePathData);
|
||||||
|
} else {
|
||||||
|
nodeThumb.setPathData(null);
|
||||||
|
}
|
||||||
|
if (connectionThumbData.length && connectionThumb.stroke) {
|
||||||
|
connectionThumb.stroke(minder.getStyle('connect-color'), '0.5%').setPathData(connectionThumbData);
|
||||||
|
} else if (connectionThumb.setPathData) {
|
||||||
|
connectionThumb.setPathData(null);
|
||||||
|
}
|
||||||
|
updateVisibleView();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind() {
|
||||||
|
if (minder && minder.on) {
|
||||||
|
minder.on('layout layoutallfinish', updateContentView);
|
||||||
|
minder.on('viewchange', updateVisibleView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbind() {
|
||||||
|
if (minder && minder.off) {
|
||||||
|
minder.off('layout layoutallfinish', updateContentView);
|
||||||
|
minder.off('viewchange', updateVisibleView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNavOpen() {
|
||||||
|
let isNavOpenState = false;
|
||||||
|
isNavOpenState = !JSON.parse(getNavOpenState());
|
||||||
|
isNavOpen.value = isNavOpenState;
|
||||||
|
setLocalStorage('navigator-hidden', isNavOpen.value);
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (isNavOpenState) {
|
||||||
|
bind();
|
||||||
|
updateContentView();
|
||||||
|
updateVisibleView();
|
||||||
|
} else {
|
||||||
|
unbind();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function navigate() {
|
||||||
|
function moveView(center: Record<string, any>, duration?: number) {
|
||||||
|
if (!minder.getPaper || !visibleView.width || !visibleView.height) return;
|
||||||
|
let box = visibleView;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
center.x = -center.x;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
center.y = -center.y;
|
||||||
|
const viewMatrix = minder.getPaper().getViewPortMatrix();
|
||||||
|
box = viewMatrix.transformBox(box);
|
||||||
|
if (!box.width || !box.height) return;
|
||||||
|
const targetPosition = center.offset(box.width / 2, box.height / 2);
|
||||||
|
if (minder.getViewDragger) minder.getViewDragger().moveTo(targetPosition, duration);
|
||||||
|
}
|
||||||
|
if (!paper.on) return;
|
||||||
|
let dragging = false;
|
||||||
|
paper.on('mousedown', (e: any) => {
|
||||||
|
dragging = true;
|
||||||
|
moveView(e.getPosition('top'), 200);
|
||||||
|
previewNavigator.value?.classList.add('grab');
|
||||||
|
});
|
||||||
|
paper.on('mousemove', (e: any) => {
|
||||||
|
if (dragging) {
|
||||||
|
moveView(e.getPosition('top'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
paper.on('mouseup', () => {
|
||||||
|
dragging = false;
|
||||||
|
previewNavigator.value?.classList.remove('grab');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
minder = window.minder;
|
||||||
|
const { kity } = window;
|
||||||
|
// 以下部分是缩略图导航器
|
||||||
|
previewNavigator.value = navPreviewer.value;
|
||||||
|
|
||||||
|
// 画布,渲染缩略图
|
||||||
|
paper = new kity.Paper(previewNavigator.value);
|
||||||
|
|
||||||
|
// 用两个路径来绘制节点和连线的缩略图
|
||||||
|
if (!paper.put || !minder || !minder.on) return;
|
||||||
|
nodeThumb = paper.put(new kity.Path());
|
||||||
|
connectionThumb = paper.put(new kity.Path());
|
||||||
|
// 表示可视区域的矩形
|
||||||
|
visibleRect = paper.put(new kity.Rect(100, 100).stroke('red', '1%'));
|
||||||
|
|
||||||
|
contentView.value = new kity.Box();
|
||||||
|
visibleView = new kity.Box();
|
||||||
|
|
||||||
|
pathHandler = getPathHandler(minder.getTheme ? minder.getTheme() : '');
|
||||||
|
if (minder.setDefaultOptions) {
|
||||||
|
minder.setDefaultOptions({
|
||||||
|
zoom: config.zoom,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
minder.on('zoom', (e: any) => {
|
||||||
|
zoom.value = e.zoom;
|
||||||
|
});
|
||||||
|
if (isNavOpen.value) {
|
||||||
|
bind();
|
||||||
|
updateContentView();
|
||||||
|
updateVisibleView();
|
||||||
|
} else {
|
||||||
|
unbind();
|
||||||
|
}
|
||||||
|
// 主题切换事件
|
||||||
|
minder.on('themechange', (e: any) => {
|
||||||
|
pathHandler = getPathHandler(e.theme);
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nav-btn .icon {
|
||||||
|
background: url('@/assets/images/minder/icons.png');
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,83 @@
|
||||||
|
<template>
|
||||||
|
<div class="edit-del-group">
|
||||||
|
<div class="edit menu-btn" :disabled="textDisabled" @click="edit">
|
||||||
|
<i class="tab-icons" />
|
||||||
|
<span>
|
||||||
|
{{ t('minder.commons.edit') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="del menu-btn" :disabled="removeNodeDisabled" @click="del">
|
||||||
|
<i class="tab-icons" />
|
||||||
|
<span>
|
||||||
|
{{ t('minder.commons.delete') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="edit_del" setup>
|
||||||
|
import { onMounted, reactive, nextTick, ref } from 'vue';
|
||||||
|
import { isDeleteDisableNode, isDisableNode } from '../../script/tool/utils';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { delProps } from '../../props';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps(delProps);
|
||||||
|
|
||||||
|
let minder = reactive<any>({});
|
||||||
|
const textDisabled = ref(true);
|
||||||
|
const removeNodeDisabled = ref(true);
|
||||||
|
|
||||||
|
function checkDisabled() {
|
||||||
|
try {
|
||||||
|
if (!minder) return false;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const node = minder.getSelectedNode();
|
||||||
|
removeNodeDisabled.value = !node || !!isDeleteDisableNode(minder) || node.parent === null;
|
||||||
|
textDisabled.value = !node || !!isDisableNode(minder);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
minder = window.minder;
|
||||||
|
minder.on('selectionchange', () => {
|
||||||
|
checkDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function editNode() {
|
||||||
|
if (!minder.queryCommandValue) return;
|
||||||
|
const editor = window.minderEditor;
|
||||||
|
const receiverElement = editor.receiver.element;
|
||||||
|
const { fsm } = editor;
|
||||||
|
const { receiver } = editor;
|
||||||
|
|
||||||
|
receiverElement.innerText = minder.queryCommandValue('text');
|
||||||
|
fsm.jump('input', 'input-request');
|
||||||
|
receiver.selectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit() {
|
||||||
|
if (textDisabled.value || !minder.queryCommandState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (minder.queryCommandState('text') !== -1) {
|
||||||
|
editNode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function del() {
|
||||||
|
if (removeNodeDisabled.value || !minder.queryCommandState || !minder.execCommand) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props.delConfirm) {
|
||||||
|
props.delConfirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
minder.forceRemoveNode();
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<div class="menu-container">
|
||||||
|
<expand />
|
||||||
|
<selection />
|
||||||
|
<insert-box />
|
||||||
|
<move-box :move-enable="props.moveEnable" />
|
||||||
|
<edit-del :del-confirm="props.delConfirm" />
|
||||||
|
<sequence-box
|
||||||
|
v-if="props.sequenceEnable"
|
||||||
|
:priority-prefix="props.priorityPrefix"
|
||||||
|
:priority-count="props.priorityCount"
|
||||||
|
:priority-disable-check="props.priorityDisableCheck"
|
||||||
|
:priority-start-with-zero="props.priorityStartWithZero"
|
||||||
|
/>
|
||||||
|
<progress-box v-if="props.progressEnable" />
|
||||||
|
<tag-box
|
||||||
|
v-if="props.tagEnable"
|
||||||
|
:tags="props.tags"
|
||||||
|
:tag-disable-check="props.tagDisableCheck"
|
||||||
|
:tag-edit-check="props.tagEditCheck"
|
||||||
|
:distinct-tags="props.distinctTags"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="editMenu" setup>
|
||||||
|
import insertBox from './insertBox.vue';
|
||||||
|
import moveBox from './moveBox.vue';
|
||||||
|
import editDel from './editDel.vue';
|
||||||
|
import sequenceBox from './sequenceBox.vue';
|
||||||
|
import progressBox from './progressBox.vue';
|
||||||
|
import expand from './expand.vue';
|
||||||
|
import selection from './selection.vue';
|
||||||
|
import TagBox from './tagBox.vue';
|
||||||
|
import { editMenuProps, priorityProps, tagProps, delProps } from '../../props';
|
||||||
|
|
||||||
|
const props = defineProps({ ...editMenuProps, ...priorityProps, ...tagProps, ...delProps });
|
||||||
|
</script>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<div class="expand-group">
|
||||||
|
<a-button class="tab-icons expand" type="text" @click="expandAll" />
|
||||||
|
<a-dropdown :popup-max-height="false" @select="handleCommand">
|
||||||
|
<span class="dropdown-link">
|
||||||
|
{{ t('minder.menu.expand.expand') }}
|
||||||
|
<icon-caret-down />
|
||||||
|
</span>
|
||||||
|
<template #content>
|
||||||
|
<a-doption value="1">{{ t('minder.menu.expand.expand_one') }}</a-doption>
|
||||||
|
<a-doption value="2">{{ t('minder.menu.expand.expand_tow') }}</a-doption>
|
||||||
|
<a-doption value="3">{{ t('minder.menu.expand.expand_three') }}</a-doption>
|
||||||
|
<a-doption value="4">{{ t('minder.menu.expand.expand_four') }}</a-doption>
|
||||||
|
<a-doption value="5">{{ t('minder.menu.expand.expand_five') }}</a-doption>
|
||||||
|
<a-doption value="6">{{ t('minder.menu.expand.expand_six') }}</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="expand" setup>
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
function handleCommand(command: string | number | Record<string, any> | undefined) {
|
||||||
|
window.minder?.execCommand('ExpandToLevel', command);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandAll() {
|
||||||
|
window.minder?.execCommand('ExpandToLevel', 9999);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.dropdown-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<div class="insert-group">
|
||||||
|
<div class="insert-child-box menu-btn" :disabled="appendChildNodeDisabled" @click="execCommand('AppendChildNode')">
|
||||||
|
<i class="tab-icons" />
|
||||||
|
<span>{{ t('minder.menu.insert.down') }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="insert-parent-box menu-btn"
|
||||||
|
:disabled="appendParentNodeDisabled"
|
||||||
|
@click="execCommand('AppendParentNode')"
|
||||||
|
>
|
||||||
|
<i class="tab-icons" />
|
||||||
|
<span>{{ t('minder.menu.insert.up') }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="insert-sibling-box menu-btn"
|
||||||
|
:disabled="appendSiblingNodeDisabled"
|
||||||
|
@click="execCommand('AppendSiblingNode')"
|
||||||
|
>
|
||||||
|
<i class="tab-icons" />
|
||||||
|
<span>{{ t('minder.menu.insert.same') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="insertBox" setup>
|
||||||
|
import { nextTick, onMounted, ref } from 'vue';
|
||||||
|
import { isDisableNode } from '../../script/tool/utils';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const minder = ref<any>({});
|
||||||
|
const appendChildNodeDisabled = ref(false);
|
||||||
|
const appendParentNodeDisabled = ref(false);
|
||||||
|
const appendSiblingNodeDisabled = ref(false);
|
||||||
|
|
||||||
|
function checkDisabled() {
|
||||||
|
try {
|
||||||
|
if (Object.keys(minder.value).length === 0) return false;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
appendChildNodeDisabled.value = minder.value.queryCommandState('AppendChildNode') === -1;
|
||||||
|
const node = minder.value.getSelectedNode();
|
||||||
|
appendSiblingNodeDisabled.value = !node || !!isDisableNode(minder.value) || node.parent === null;
|
||||||
|
appendParentNodeDisabled.value = !node || node.parent === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
minder.value = window.minder;
|
||||||
|
minder.value.on('selectionchange', () => {
|
||||||
|
checkDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function execCommand(command: string) {
|
||||||
|
if (minder.value.queryCommandState(command) !== -1) {
|
||||||
|
minder.value.execCommand(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<div class="move-group">
|
||||||
|
<div class="move-up menu-btn" :disabled="arrangeUpDisabled" @click="execCommand('ArrangeUp')">
|
||||||
|
<i class="tab-icons" />
|
||||||
|
<span>{{ t('minder.menu.move.up') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="move-down menu-btn" :disabled="arrangeDownDisabled" @click="execCommand('ArrangeDown')">
|
||||||
|
<i class="tab-icons" />
|
||||||
|
<span>{{ t('minder.menu.move.down') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="moveBox" setup>
|
||||||
|
import { nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
|
import { isDisableNode } from '../../script/tool/utils';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
moveEnable: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let minder = reactive<any>({});
|
||||||
|
const arrangeUpDisabled = ref(true);
|
||||||
|
const arrangeDownDisabled = ref(true);
|
||||||
|
|
||||||
|
function checkDisabled() {
|
||||||
|
try {
|
||||||
|
if (Object.keys(minder).length === 0) return false;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const node = minder.getSelectedNode();
|
||||||
|
if (!props.moveEnable || !node || node.parent === null || isDisableNode(minder)) {
|
||||||
|
arrangeUpDisabled.value = true;
|
||||||
|
arrangeDownDisabled.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (window.minder.queryCommandState) {
|
||||||
|
arrangeUpDisabled.value = window.minder.queryCommandState('ArrangeUp') === -1;
|
||||||
|
arrangeDownDisabled.value = window.minder.queryCommandState('ArrangeDown') === -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
minder = window.minder;
|
||||||
|
minder.on('selectionchange', () => {
|
||||||
|
checkDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function execCommand(command: string) {
|
||||||
|
if (window.minder.queryCommandState(command) !== -1) window.minder.execCommand(command);
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div class="progress-group">
|
||||||
|
<ul>
|
||||||
|
<ul :disabled="commandDisabled">
|
||||||
|
<li
|
||||||
|
v-for="(item, index) in items"
|
||||||
|
:key="item.text"
|
||||||
|
class="menu-btn"
|
||||||
|
:class="classArray(index)"
|
||||||
|
:title="title(index)"
|
||||||
|
@click="execCommand(index)"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="progressBox" setup>
|
||||||
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
|
import { isDisableNode } from '../../script/tool/utils';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
let minder = reactive<any>({});
|
||||||
|
const commandValue = ref('');
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ text: '0' },
|
||||||
|
{ text: '1' },
|
||||||
|
{ text: '2' },
|
||||||
|
{ text: '3' },
|
||||||
|
{ text: '4' },
|
||||||
|
{ text: '5' },
|
||||||
|
{ text: '6' },
|
||||||
|
{ text: '7' },
|
||||||
|
{ text: '8' },
|
||||||
|
{ text: '9' },
|
||||||
|
];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
minder = window.minder;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const commandDisabled = computed(() => {
|
||||||
|
if (Object.keys(minder).length === 0 || !minder.on) return true;
|
||||||
|
minder.on('interactchange', () => {
|
||||||
|
commandValue.value = minder.queryCommandValue && minder.queryCommandValue('progress');
|
||||||
|
});
|
||||||
|
if (isDisableNode(minder)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return minder.queryCommandState && minder.queryCommandState('progress') === -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
function execCommand(index: number) {
|
||||||
|
if (!commandDisabled.value && minder.execCommand) {
|
||||||
|
minder.execCommand('progress', index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function classArray(index: number) {
|
||||||
|
const isActive = minder.queryCommandValue && minder.queryCommandValue('progress') === index;
|
||||||
|
const sequence = `progress-${index}`;
|
||||||
|
|
||||||
|
// 用数组返回多个class
|
||||||
|
const arr = [
|
||||||
|
{
|
||||||
|
active: isActive,
|
||||||
|
},
|
||||||
|
sequence,
|
||||||
|
];
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
function title(index: number) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
return t('minder.menu.progress.remove_progress');
|
||||||
|
case 1:
|
||||||
|
return t('minder.menu.progress.prepare');
|
||||||
|
case 9:
|
||||||
|
return t('minder.menu.progress.complete_all');
|
||||||
|
default:
|
||||||
|
return `${t('minder.menu.progress.complete') + (index - 1)}/8`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.progress-group li {
|
||||||
|
background-image: url('@/assets/images/minder/iconprogress.png');
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,123 @@
|
||||||
|
<template>
|
||||||
|
<div class="selection-group">
|
||||||
|
<a-button type="text" class="tab-icons selection" @click="selectAll" />
|
||||||
|
<a-dropdown :popup-max-height="false" @select="handleCommand">
|
||||||
|
<span class="dropdown-link">
|
||||||
|
{{ t('minder.menu.selection.all') }}
|
||||||
|
<icon-caret-down />
|
||||||
|
</span>
|
||||||
|
<template #content>
|
||||||
|
<a-doption value="1">{{ t('minder.menu.selection.invert') }}</a-doption>
|
||||||
|
<a-doption value="2">{{ t('minder.menu.selection.sibling') }}</a-doption>
|
||||||
|
<a-doption value="3">{{ t('minder.menu.selection.same') }}</a-doption>
|
||||||
|
<a-doption value="4">{{ t('minder.menu.selection.path') }}</a-doption>
|
||||||
|
<a-doption value="5">{{ t('minder.menu.selection.subtree') }}</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="selection" setup>
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
function selectAll() {
|
||||||
|
const selection: any[] = [];
|
||||||
|
window.minder.getRoot().traverse((node: any) => {
|
||||||
|
selection.push(node);
|
||||||
|
});
|
||||||
|
window.minder.select(selection, true);
|
||||||
|
window.minder.fire('receiverfocus');
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRevert() {
|
||||||
|
const selected = window.minder.getSelectedNodes();
|
||||||
|
const selection: any[] = [];
|
||||||
|
window.minder.getRoot().traverse((node: any) => {
|
||||||
|
if (selected.indexOf(node) === -1) {
|
||||||
|
selection.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.minder.select(selection, true);
|
||||||
|
window.minder.fire('receiverfocus');
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSiblings() {
|
||||||
|
const selected = window.minder.getSelectedNodes();
|
||||||
|
const selection: any[] = [];
|
||||||
|
selected.forEach((node: any) => {
|
||||||
|
if (!node.parent) return;
|
||||||
|
node.parent.children.forEach((sibling: string) => {
|
||||||
|
if (selection.indexOf(sibling) === -1) selection.push(sibling);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.minder.select(selection, true);
|
||||||
|
window.minder.fire('receiverfocus');
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectLevel() {
|
||||||
|
const selectedLevel = window.minder.getSelectedNodes().map((node: any) => node.getLevel());
|
||||||
|
const selection: any[] = [];
|
||||||
|
window.minder.getRoot().traverse((node: any) => {
|
||||||
|
if (selectedLevel.indexOf(node.getLevel()) !== -1) {
|
||||||
|
selection.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.minder.select(selection, true);
|
||||||
|
window.minder.fire('receiverfocus');
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPath() {
|
||||||
|
const selected = window.minder.getSelectedNodes();
|
||||||
|
const selection: any[] = [];
|
||||||
|
selected.forEach((node: any) => {
|
||||||
|
let tempNode = node;
|
||||||
|
while (tempNode && selection.indexOf(tempNode) === -1) {
|
||||||
|
selection.push(tempNode);
|
||||||
|
tempNode = node.parent;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.minder.select(selection, true);
|
||||||
|
window.minder.fire('receiverfocus');
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTree() {
|
||||||
|
const selected = window.minder.getSelectedNodes();
|
||||||
|
const selection: any[] = [];
|
||||||
|
selected.forEach((parent: any) => {
|
||||||
|
parent.traverse((node: any) => {
|
||||||
|
if (selection.indexOf(node) === -1) selection.push(node);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.minder.select(selection, true);
|
||||||
|
window.minder.fire('receiverfocus');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCommand(value: string | number | Record<string, any> | undefined) {
|
||||||
|
switch (value) {
|
||||||
|
case 1:
|
||||||
|
selectRevert();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
selectSiblings();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
selectLevel();
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
selectPath();
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
selectTree();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.dropdown-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,161 @@
|
||||||
|
<template>
|
||||||
|
<div :disabled="commandDisabled">
|
||||||
|
<a-button class="delete-btn" shape="circle" @click="execCommand()">
|
||||||
|
<template #icon>
|
||||||
|
<icon-delete />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
<template v-for="(item, pIndex) in priorityCount + 1">
|
||||||
|
<a-button
|
||||||
|
v-if="pIndex != 0"
|
||||||
|
:key="item"
|
||||||
|
class="priority-btn"
|
||||||
|
:class="'priority-btn_' + pIndex"
|
||||||
|
size="small"
|
||||||
|
@click="execCommand(pIndex)"
|
||||||
|
>
|
||||||
|
{{ priorityPrefix }}{{ priorityStartWithZero ? pIndex - 1 : pIndex }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="sequenceBox" setup>
|
||||||
|
import { onMounted, reactive, nextTick, ref } from 'vue';
|
||||||
|
import { isDisableNode, setPriorityView } from '../../script/tool/utils';
|
||||||
|
import { priorityProps } from '../../props';
|
||||||
|
|
||||||
|
const props = defineProps(priorityProps);
|
||||||
|
|
||||||
|
const commandValue = ref('');
|
||||||
|
const commandDisabled = ref(true);
|
||||||
|
let minder = reactive<any>({});
|
||||||
|
|
||||||
|
const isDisable = (): boolean => {
|
||||||
|
if (Object.keys(minder).length === 0) return true;
|
||||||
|
nextTick(() => {
|
||||||
|
setPriorityView(props.priorityStartWithZero, props.priorityPrefix);
|
||||||
|
});
|
||||||
|
if (minder.on) {
|
||||||
|
minder.on('interactchange', () => {
|
||||||
|
commandValue.value = minder.queryCommandValue && minder.queryCommandValue('priority');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const node = minder.getSelectedNode();
|
||||||
|
if (isDisableNode(minder) || !node || node.parent === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (props.priorityDisableCheck) {
|
||||||
|
return props.priorityDisableCheck();
|
||||||
|
}
|
||||||
|
return !!minder.queryCommandState && minder.queryCommandState('priority') === -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
minder = window.minder;
|
||||||
|
const freshFuc = setPriorityView;
|
||||||
|
if (minder.on) {
|
||||||
|
minder.on('contentchange', () => {
|
||||||
|
// 异步执行,否则执行完,还会被重置
|
||||||
|
setTimeout(() => {
|
||||||
|
freshFuc(props.priorityStartWithZero, props.priorityPrefix);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
minder.on('selectionchange', () => {
|
||||||
|
commandDisabled.value = isDisable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function execCommand(index?: number) {
|
||||||
|
if (index && minder.execCommand) {
|
||||||
|
if (!commandDisabled.value) {
|
||||||
|
minder.execCommand('priority', index);
|
||||||
|
}
|
||||||
|
setPriorityView(props.priorityStartWithZero, props.priorityPrefix);
|
||||||
|
} else if (minder.execCommand && !commandDisabled.value) {
|
||||||
|
minder.execCommand('priority');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.delete-btn {
|
||||||
|
margin: 0 4px;
|
||||||
|
padding: 2px !important;
|
||||||
|
width: 23px;
|
||||||
|
height: 23px;
|
||||||
|
i {
|
||||||
|
width: 1em !important;
|
||||||
|
height: 1em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.priority-btn {
|
||||||
|
margin-right: 4px;
|
||||||
|
padding: 0;
|
||||||
|
padding-right: 5px;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.priority-btn_1 {
|
||||||
|
border-bottom: 3px solid #840023;
|
||||||
|
background-color: #ff1200;
|
||||||
|
}
|
||||||
|
.priority-btn_1:hover {
|
||||||
|
border-bottom: 3px solid #840023;
|
||||||
|
color: white;
|
||||||
|
background-color: #ff1200;
|
||||||
|
}
|
||||||
|
.priority-btn_2 {
|
||||||
|
border-bottom: 3px solid #01467f;
|
||||||
|
background-color: #0074ff;
|
||||||
|
}
|
||||||
|
.priority-btn_2:hover {
|
||||||
|
border-bottom: 3px solid #01467f;
|
||||||
|
color: white;
|
||||||
|
background-color: #0074ff;
|
||||||
|
}
|
||||||
|
.priority-btn_3 {
|
||||||
|
border-bottom: 3px solid #006300;
|
||||||
|
background-color: #00af00;
|
||||||
|
}
|
||||||
|
.priority-btn_3:hover {
|
||||||
|
border-bottom: 3px solid #006300;
|
||||||
|
color: white;
|
||||||
|
background-color: #00af00;
|
||||||
|
}
|
||||||
|
.priority-btn_4 {
|
||||||
|
border-bottom: 3px solid #b25000;
|
||||||
|
background-color: #ff962e;
|
||||||
|
}
|
||||||
|
.priority-btn_4:hover {
|
||||||
|
border-bottom: 3px solid #b25000;
|
||||||
|
color: white;
|
||||||
|
background-color: #ff962e;
|
||||||
|
}
|
||||||
|
.priority-btn_5 {
|
||||||
|
border-bottom: 3px solid #4720c4;
|
||||||
|
background-color: #a464ff;
|
||||||
|
}
|
||||||
|
.priority-btn_5:hover {
|
||||||
|
border-bottom: 3px solid #4720c4;
|
||||||
|
color: white;
|
||||||
|
background-color: #a464ff;
|
||||||
|
}
|
||||||
|
.priority-btn_6 {
|
||||||
|
border-bottom: 3px solid #515151;
|
||||||
|
background-color: #a3a3a3;
|
||||||
|
}
|
||||||
|
.priority-btn_6:hover {
|
||||||
|
border-bottom: 3px solid #515151;
|
||||||
|
color: white;
|
||||||
|
background-color: #a3a3a3;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,100 @@
|
||||||
|
<template>
|
||||||
|
<div :disabled="commandDisabled">
|
||||||
|
<a-tag
|
||||||
|
v-for="item in props.tags"
|
||||||
|
:key="item"
|
||||||
|
size="small"
|
||||||
|
:color="getResourceColor(item)"
|
||||||
|
@click="editResource(item)"
|
||||||
|
>{{ item }}</a-tag
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="TagBox" setup>
|
||||||
|
import { nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
|
import { isDisableNode, isTagEnable } from '../../script/tool/utils';
|
||||||
|
import { tagProps } from '../../props';
|
||||||
|
|
||||||
|
const props = defineProps(tagProps);
|
||||||
|
|
||||||
|
let minder = reactive<any>({});
|
||||||
|
const commandDisabled = ref(true);
|
||||||
|
|
||||||
|
const isDisable = (): boolean => {
|
||||||
|
if (Object.keys(minder).length === 0 || !minder.on) return true;
|
||||||
|
if (isDisableNode(minder) && !isTagEnable(minder)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (props.tagDisableCheck) {
|
||||||
|
return props.tagDisableCheck();
|
||||||
|
}
|
||||||
|
return !!minder.queryCommandState && minder.queryCommandState('resource') === -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
minder = window.minder;
|
||||||
|
minder.on('selectionchange', () => {
|
||||||
|
commandDisabled.value = isDisable();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getResourceColor(resource: string) {
|
||||||
|
if (minder.getResourceColor) {
|
||||||
|
return minder.getResourceColor(resource).toHEX();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editResource(resourceName: string) {
|
||||||
|
if (commandDisabled.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props.tagEditCheck) {
|
||||||
|
if (!props.tagEditCheck(resourceName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!resourceName || !/\S/.test(resourceName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const origin = window.minder.queryCommandValue('resource');
|
||||||
|
const index = origin.indexOf(resourceName);
|
||||||
|
// 先删除排他的标签
|
||||||
|
if (props.distinctTags.indexOf(resourceName) > -1) {
|
||||||
|
for (let i = 0; i < origin.length; i++) {
|
||||||
|
if (props.distinctTags.indexOf(origin[i]) > -1) {
|
||||||
|
origin.splice(i, 1);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index !== -1) {
|
||||||
|
origin.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
origin.push(resourceName);
|
||||||
|
}
|
||||||
|
window.minder.execCommand('resource', origin);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.arco-tag {
|
||||||
|
margin-right: 4px;
|
||||||
|
border: 0;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.arco-tag:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.arco-tag:first-child {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
.add-btn {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 36px;
|
||||||
|
height: 24px;
|
||||||
|
border-style: dashed !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<template>
|
||||||
|
<div class="arrange-group">
|
||||||
|
<div class="arrange menu-btn" :disabled="disabled" @click="resetlayout">
|
||||||
|
<span class="tab-icons" />
|
||||||
|
<span class="label">
|
||||||
|
{{ t('minder.menu.arrange.arrange_layout') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="Arrange" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const disabled = computed(() => {
|
||||||
|
try {
|
||||||
|
if (!window.minder) return false;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return window.minder.queryCommandState && window.minder.queryCommandState('resetlayout') === -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetlayout() {
|
||||||
|
if (window.minder.queryCommandState('resetlayout') !== -1) {
|
||||||
|
window.minder.execCommand('resetlayout');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,269 @@
|
||||||
|
<template>
|
||||||
|
<div class="font-group">
|
||||||
|
<a-select
|
||||||
|
v-model="fontFamilyDefaultValue"
|
||||||
|
:placeholder="t('minder.menu.font.font')"
|
||||||
|
class="font-family-select"
|
||||||
|
:disabled="disabledFont"
|
||||||
|
size="mini"
|
||||||
|
@change="execCommandFontFamily"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="item in fontFamilys"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.value"
|
||||||
|
:style="{ 'font-family': item.value }"
|
||||||
|
/>
|
||||||
|
</a-select>
|
||||||
|
<a-select
|
||||||
|
v-model="fontSizeDefaultValue"
|
||||||
|
:placeholder="t('minder.menu.font.size')"
|
||||||
|
class="font-size-select"
|
||||||
|
:disabled="disabledFontSize"
|
||||||
|
size="mini"
|
||||||
|
@change="execCommandFontSize"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="item in fontSizes"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.label.toString()"
|
||||||
|
:value="item.value"
|
||||||
|
:style="{
|
||||||
|
'font-size': item.value + 'px',
|
||||||
|
'height': 2 * item.value + 'px',
|
||||||
|
'line-height': 2 * item.value + 'px',
|
||||||
|
'padding': 0,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</a-select>
|
||||||
|
<span class="font-btn">
|
||||||
|
<span
|
||||||
|
class="menu-btn tab-icons font-bold"
|
||||||
|
:class="{ selected: boldSelected }"
|
||||||
|
:disabled="disabledBold"
|
||||||
|
@click="execCommandFontStyle('bold')"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="font-italic menu-btn tab-icons"
|
||||||
|
:class="{ selected: italicSelected }"
|
||||||
|
:disabled="disabledItalic"
|
||||||
|
@click="execCommandFontStyle('italic')"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="StyleOpreation" setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const fontFamilys = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
value: '宋体,SimSun',
|
||||||
|
name: '宋体',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
value: '微软雅黑,Microsoft YaHei',
|
||||||
|
name: '微软雅黑',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
value: '楷体,楷体_GB2312,SimKai',
|
||||||
|
name: '楷体',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
value: '黑体, SimHei',
|
||||||
|
name: '黑体',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
value: '隶书, SimLi',
|
||||||
|
name: '隶书',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
value: 'andale mono',
|
||||||
|
name: 'Andale Mono',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
value: 'arial,helvetica,sans-serif',
|
||||||
|
name: 'Arial',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
value: 'arial black,avant garde',
|
||||||
|
name: 'arialBlack',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
value: 'comic sans ms',
|
||||||
|
name: 'comic Sans Ms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
value: 'impact,chicago',
|
||||||
|
name: 'Impact',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
value: 'times new roman',
|
||||||
|
name: 'times New Roman',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
value: 'sans-serif',
|
||||||
|
name: 'Sans-Serif',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const fontSizes = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
value: 10,
|
||||||
|
label: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
value: 12,
|
||||||
|
label: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
value: 16,
|
||||||
|
label: 16,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
value: 18,
|
||||||
|
label: 18,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
value: 24,
|
||||||
|
label: 24,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
value: 32,
|
||||||
|
label: 32,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
value: 48,
|
||||||
|
label: 48,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fontFamilyDefaultValue = ref('');
|
||||||
|
const fontSizeDefaultValue = ref('');
|
||||||
|
|
||||||
|
const disabledFont = computed(() => {
|
||||||
|
try {
|
||||||
|
if (!window.minder) return false;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFontFamily = window.minder.queryCommandValue('fontfamily');
|
||||||
|
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||||
|
fontFamilyDefaultValue.value = currentFontFamily || t('minder.menu.font.font');
|
||||||
|
return window.minder.queryCommandState('fontfamily') === -1;
|
||||||
|
});
|
||||||
|
const disabledFontSize = computed(() => {
|
||||||
|
try {
|
||||||
|
if (!window.minder) return false;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||||
|
fontSizeDefaultValue.value = window.minder.queryCommandValue('fontsize') || t('minder.menu.font.size');
|
||||||
|
return window.minder.queryCommandState('fontsize') === -1;
|
||||||
|
});
|
||||||
|
const disabledBold = computed(() => {
|
||||||
|
try {
|
||||||
|
if (!window.minder) return false;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return window.minder.queryCommandState('bold') === -1;
|
||||||
|
});
|
||||||
|
const disabledItalic = computed(() => {
|
||||||
|
try {
|
||||||
|
if (!window.minder) return false;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return window.minder.queryCommandState('italic') === -1;
|
||||||
|
});
|
||||||
|
const boldSelected = computed(() => {
|
||||||
|
try {
|
||||||
|
if (!window.minder) return false;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return window.minder.queryCommandState('bold') === -1;
|
||||||
|
});
|
||||||
|
const italicSelected = computed(() => {
|
||||||
|
try {
|
||||||
|
if (!window.minder) return false;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return window.minder.queryCommandState('italic') === -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
function execCommandFontFamily(
|
||||||
|
value: string | number | Record<string, any> | (string | number | Record<string, any>)[]
|
||||||
|
) {
|
||||||
|
if (value === t('minder.menu.font.font')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.minder.execCommand('fontfamily', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function execCommandFontSize(
|
||||||
|
value: string | number | Record<string, any> | (string | number | Record<string, any>)[]
|
||||||
|
) {
|
||||||
|
if (typeof value !== 'number') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.minder.execCommand('fontsize', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function execCommandFontStyle(style: string) {
|
||||||
|
switch (style) {
|
||||||
|
case 'bold':
|
||||||
|
if (window.minder.queryCommandState('bold') !== -1) {
|
||||||
|
window.minder.execCommand('bold');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'italic':
|
||||||
|
if (window.minder.queryCommandState('italic') !== -1) {
|
||||||
|
window.minder.execCommand('italic');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.font-group {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.font-btn {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,105 @@
|
||||||
|
<template>
|
||||||
|
<div class="mold-group" :disabled="disabled">
|
||||||
|
<a-dropdown class="toggle" @select="handleCommand">
|
||||||
|
<div>
|
||||||
|
<span class="dropdown-toggle mold-icons menu-btn" :class="'mold-' + (moldIndex + 1)" />
|
||||||
|
<span class="dropdown-link">
|
||||||
|
<icon-caret-down />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<template #content>
|
||||||
|
<a-doption class="dropdown-item mold-icons mold-1" :value="1" />
|
||||||
|
<a-doption class="dropdown-item mold-icons mold-2" :value="2" />
|
||||||
|
<a-doption class="dropdown-item mold-icons mold-3" :value="3" />
|
||||||
|
<a-doption class="dropdown-item mold-icons mold-4" :value="4" />
|
||||||
|
<a-doption class="dropdown-item mold-icons mold-5" :value="5" />
|
||||||
|
<a-doption class="dropdown-item mold-icons mold-6" :value="6" />
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="Mold" setup>
|
||||||
|
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||||
|
import { moleProps } from '../../props';
|
||||||
|
|
||||||
|
const props = defineProps(moleProps);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'moldChange', data: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const moldIndex = ref(0);
|
||||||
|
|
||||||
|
const disabled = computed(() => {
|
||||||
|
try {
|
||||||
|
if (!window.minder) return false;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return window.minder.queryCommandState('template') === -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const templateList = computed(() => window.kityminder.Minder.getTemplateList());
|
||||||
|
|
||||||
|
function handleCommand(value: string | number | Record<string, any> | undefined) {
|
||||||
|
moldIndex.value = value as number;
|
||||||
|
window.minder.execCommand('template', Object.keys(templateList.value)[value as number]);
|
||||||
|
emit('moldChange', value as number);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => handleCommand(props.defaultMold));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.toggle {
|
||||||
|
.arco-dropdown-list {
|
||||||
|
display: grid;
|
||||||
|
padding: 5px;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.dropdown-toggle .mold-icons,
|
||||||
|
.mold-icons {
|
||||||
|
background-image: url('@/assets/images/minder/mold.png');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.mold-group {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 80px;
|
||||||
|
.dropdown-toggle {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 5px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dropdown-link {
|
||||||
|
position: absolute;
|
||||||
|
right: 3px;
|
||||||
|
bottom: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.mold-loop(@i) when (@i > 0) {
|
||||||
|
.mold-@{i} {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 5px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background-position: (1 - @i) * 50px 0;
|
||||||
|
}
|
||||||
|
.mold-loop(@i - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mold-loop(6);
|
||||||
|
</style>
|
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<div class="style-group">
|
||||||
|
<div class="clear-style-btn menu-btn" :disabled="disabled" @click="clearstyle">
|
||||||
|
<span class="tab-icons" />
|
||||||
|
<span class="label">
|
||||||
|
{{ t('minder.menu.style.clear') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="copy-paste-panel" @click="copystyle">
|
||||||
|
<div class="copy-style menu-btn" :disabled="disabled">
|
||||||
|
<span class="tab-icons" />
|
||||||
|
<span class="label">
|
||||||
|
{{ t('minder.menu.style.copy') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="paste-style menu-btn" :disabled="disabled" @click="pastestyle">
|
||||||
|
<span class="tab-icons" />
|
||||||
|
<span class="label">
|
||||||
|
{{ t('minder.menu.style.paste') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="StyleOpreation" setup>
|
||||||
|
import { nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
let minder = reactive<any>({});
|
||||||
|
const disabled = ref(true);
|
||||||
|
|
||||||
|
function checkDisabled() {
|
||||||
|
try {
|
||||||
|
if (Object.keys(minder).length === 0) return false;
|
||||||
|
} catch (e) {
|
||||||
|
// 如果window的还没挂载minder,先捕捉undefined异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const nodes = minder.getSelectedNodes && minder.getSelectedNodes();
|
||||||
|
disabled.value = nodes === null || nodes.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
minder = window.minder;
|
||||||
|
minder.on('selectionchange', () => {
|
||||||
|
checkDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function clearstyle() {
|
||||||
|
if (minder.queryCommandState && minder.execCommand && minder.queryCommandState('clearstyle') !== -1) {
|
||||||
|
minder.execCommand('clearstyle');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function copystyle() {
|
||||||
|
if (minder.queryCommandState && minder.execCommand && minder.queryCommandState('copystyle') !== -1) {
|
||||||
|
minder.execCommand('copystyle');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function pastestyle() {
|
||||||
|
if (minder.queryCommandState && minder.execCommand && minder.queryCommandState('pastestyle') !== -1) {
|
||||||
|
minder.execCommand('pastestyle');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mold-dropdown-list .mold-icons,
|
||||||
|
.mold-icons {
|
||||||
|
background-image: url('@/assets/images/minder/mold.png');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<div class="menu-container">
|
||||||
|
<mold :default-mold="props.defaultMold" @mold-change="handleMoldChange" />
|
||||||
|
<arrange />
|
||||||
|
<style-operation />
|
||||||
|
<font-operation />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="viewMenu" setup>
|
||||||
|
import mold from './mold.vue';
|
||||||
|
import arrange from './arrange.vue';
|
||||||
|
import styleOperation from './styleOperation.vue';
|
||||||
|
import fontOperation from './fontOperation.vue';
|
||||||
|
import { moleProps } from '../../props';
|
||||||
|
|
||||||
|
const props = defineProps(moleProps);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'moldChange', data: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function handleMoldChange(data: number) {
|
||||||
|
emit('moldChange', data);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.menu-container {
|
||||||
|
height: 60px;
|
||||||
|
i {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,72 @@
|
||||||
|
<template>
|
||||||
|
<div class="main-container">
|
||||||
|
<header-menu
|
||||||
|
:sequence-enable="props.sequenceEnable"
|
||||||
|
:tag-enable="props.tagEnable"
|
||||||
|
:progress-enable="props.progressEnable"
|
||||||
|
:priority-count="props.priorityCount"
|
||||||
|
:priority-prefix="props.priorityPrefix"
|
||||||
|
:priority-start-with-zero="props.priorityStartWithZero"
|
||||||
|
:tags="props.tags"
|
||||||
|
:move-enable="props.moveEnable"
|
||||||
|
:tag-edit-check="props.tagEditCheck"
|
||||||
|
:tag-disable-check="props.tagDisableCheck"
|
||||||
|
:priority-disable-check="props.priorityDisableCheck"
|
||||||
|
:distinct-tags="props.distinctTags"
|
||||||
|
:default-mold="props.defaultMold"
|
||||||
|
:del-confirm="props.delConfirm"
|
||||||
|
@mold-change="handleMoldChange"
|
||||||
|
/>
|
||||||
|
<main-editor
|
||||||
|
:disabled="props.disabled"
|
||||||
|
:sequence-enable="props.sequenceEnable"
|
||||||
|
:tag-enable="props.tagEnable"
|
||||||
|
:move-enable="props.moveEnable"
|
||||||
|
:progress-enable="props.progressEnable"
|
||||||
|
:import-json="props.importJson"
|
||||||
|
:height="props.height"
|
||||||
|
:tags="props.tags"
|
||||||
|
:distinct-tags="props.distinctTags"
|
||||||
|
:tag-edit-check="props.tagEditCheck"
|
||||||
|
:tag-disable-check="props.tagDisableCheck"
|
||||||
|
:priority-count="props.priorityCount"
|
||||||
|
:priority-prefix="props.priorityPrefix"
|
||||||
|
:priority-start-with-zero="props.priorityStartWithZero"
|
||||||
|
@after-mount="emit('afterMount')"
|
||||||
|
@save="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" name="minderEditor" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import headerMenu from './main/header.vue';
|
||||||
|
import mainEditor from './main/mainEditor.vue';
|
||||||
|
import { editMenuProps, mainEditorProps, moleProps, priorityProps, tagProps, delProps } from './props';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'moldChange', data: number): void;
|
||||||
|
(e: 'save', data: Record<string, any>): void;
|
||||||
|
(e: 'afterMount'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
...editMenuProps,
|
||||||
|
...mainEditorProps,
|
||||||
|
...moleProps,
|
||||||
|
...priorityProps,
|
||||||
|
...tagProps,
|
||||||
|
...delProps,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
window.minderProps = props;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleMoldChange(data: number) {
|
||||||
|
emit('moldChange', data);
|
||||||
|
}
|
||||||
|
function save(data: Record<string, any>) {
|
||||||
|
emit('save', data);
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,113 @@
|
||||||
|
/**
|
||||||
|
* Api 列表
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const mainEditorProps = {
|
||||||
|
importJson: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {
|
||||||
|
root: {
|
||||||
|
data: {
|
||||||
|
text: 'test111',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
text: '地图',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
text: '百科',
|
||||||
|
expandState: 'collapse',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
template: 'default',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 500,
|
||||||
|
},
|
||||||
|
disabled: Boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const priorityProps = {
|
||||||
|
priorityCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 4,
|
||||||
|
validator: (value: number) => {
|
||||||
|
// 优先级最多支持 9 个级别
|
||||||
|
return value <= 9;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
priorityStartWithZero: {
|
||||||
|
// 优先级是否从0开始
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
priorityPrefix: {
|
||||||
|
// 优先级显示的前缀
|
||||||
|
type: String,
|
||||||
|
default: 'P',
|
||||||
|
},
|
||||||
|
priorityDisableCheck: Function,
|
||||||
|
operators: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tagProps = {
|
||||||
|
tags: {
|
||||||
|
// 自定义标签
|
||||||
|
type: Array<string>,
|
||||||
|
default() {
|
||||||
|
return [] as string[];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
distinctTags: {
|
||||||
|
// 个别标签二选一
|
||||||
|
type: Array<string>,
|
||||||
|
default() {
|
||||||
|
return [] as string[];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tagDisableCheck: Function,
|
||||||
|
tagEditCheck: Function,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editMenuProps = {
|
||||||
|
sequenceEnable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
tagEnable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
progressEnable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
moveEnable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moleProps = {
|
||||||
|
// 默认样式
|
||||||
|
defaultMold: {
|
||||||
|
type: Number,
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const delProps = {
|
||||||
|
delConfirm: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,92 @@
|
||||||
|
import '@7polo/kity/dist/kity';
|
||||||
|
import 'hotbox-minder/hotbox';
|
||||||
|
import '@7polo/kityminder-core';
|
||||||
|
import container from './runtime/container';
|
||||||
|
import fsm from './runtime/fsm';
|
||||||
|
import minder from './runtime/minder';
|
||||||
|
import receiver from './runtime/receiver';
|
||||||
|
import hotbox from './runtime/hotbox';
|
||||||
|
import input from './runtime/input';
|
||||||
|
import clipboardMimetype from './runtime/clipboard-mimetype';
|
||||||
|
import clipboard from './runtime/clipboard';
|
||||||
|
import drag from './runtime/drag';
|
||||||
|
import node from './runtime/node';
|
||||||
|
import history from './runtime/history';
|
||||||
|
import jumping from './runtime/jumping';
|
||||||
|
import priority from './runtime/priority';
|
||||||
|
import progress from './runtime/progress';
|
||||||
|
import exportsRuntime from './runtime/exports';
|
||||||
|
import tag from './runtime/tag';
|
||||||
|
|
||||||
|
type EditMenuProps = {
|
||||||
|
sequenceEnable: boolean;
|
||||||
|
tagEnable: boolean;
|
||||||
|
progressEnable: boolean;
|
||||||
|
moveEnable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Runtime = {
|
||||||
|
name: string;
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
call: (thisArg: KMEditor, editor: KMEditor) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const runtimes: Runtime[] = [];
|
||||||
|
|
||||||
|
function assemble(runtime: Runtime) {
|
||||||
|
runtimes.push(runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEnable(editMenuProps: EditMenuProps, runtime: Runtime) {
|
||||||
|
switch (runtime.name) {
|
||||||
|
case 'PriorityRuntime':
|
||||||
|
return editMenuProps.sequenceEnable === true;
|
||||||
|
case 'TagRuntime':
|
||||||
|
return editMenuProps.tagEnable === true;
|
||||||
|
case 'ProgressRuntime':
|
||||||
|
return editMenuProps.progressEnable === true;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KMEditor {
|
||||||
|
public selector: HTMLDivElement | null | string;
|
||||||
|
|
||||||
|
public editMenuProps: EditMenuProps;
|
||||||
|
|
||||||
|
constructor(selector: HTMLDivElement | null | string, editMenuPropsC: EditMenuProps) {
|
||||||
|
this.selector = selector;
|
||||||
|
this.editMenuProps = editMenuPropsC;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
for (let i = 0; i < runtimes.length; i++) {
|
||||||
|
if (typeof runtimes[i].call === 'function' && isEnable(this.editMenuProps, runtimes[i])) {
|
||||||
|
runtimes[i].call(this, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assemble(container);
|
||||||
|
assemble(fsm);
|
||||||
|
assemble(minder);
|
||||||
|
assemble(receiver);
|
||||||
|
assemble(hotbox);
|
||||||
|
assemble(input);
|
||||||
|
assemble(clipboardMimetype);
|
||||||
|
assemble(clipboard);
|
||||||
|
assemble(drag);
|
||||||
|
assemble(node);
|
||||||
|
assemble(history);
|
||||||
|
assemble(jumping);
|
||||||
|
assemble(priority);
|
||||||
|
assemble(progress);
|
||||||
|
assemble(exportsRuntime);
|
||||||
|
assemble(tag);
|
||||||
|
|
||||||
|
window.kityminder.Editor = KMEditor;
|
||||||
|
|
||||||
|
export default KMEditor;
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Editor from './editor';
|
||||||
|
|
||||||
|
export default window.kityminder.Editor = Editor;
|
|
@ -0,0 +1,70 @@
|
||||||
|
const priorities = [
|
||||||
|
{ jp: 1, mp: 'full-1' },
|
||||||
|
{ jp: 2, mp: 'full-2' },
|
||||||
|
{ jp: 3, mp: 'full-3' },
|
||||||
|
{ jp: 4, mp: 'full-4' },
|
||||||
|
{ jp: 5, mp: 'full-5' },
|
||||||
|
{ jp: 6, mp: 'full-6' },
|
||||||
|
{ jp: 7, mp: 'full-7' },
|
||||||
|
{ jp: 8, mp: 'full-8' },
|
||||||
|
];
|
||||||
|
const mmVersion = '<map version="1.0.1">\n';
|
||||||
|
const iconTextPrefix = '<icon BUILTIN="';
|
||||||
|
const iconTextSuffix = '"/>\n';
|
||||||
|
const nodeCreated = '<node CREATED="';
|
||||||
|
const nodeId = '" ID="';
|
||||||
|
const nodeText = '" TEXT="';
|
||||||
|
const nodeSuffix = '">\n';
|
||||||
|
const entityNode = '</node>\n';
|
||||||
|
const entityMap = '</map>';
|
||||||
|
|
||||||
|
function concatNodes(node: any) {
|
||||||
|
let result = '';
|
||||||
|
const datas = node.data;
|
||||||
|
result += nodeCreated + datas.created + nodeId + datas.id + nodeText + datas.text + nodeSuffix;
|
||||||
|
if (datas.priority) {
|
||||||
|
const mapped = priorities.find((d) => {
|
||||||
|
return d.jp === datas.priority;
|
||||||
|
});
|
||||||
|
if (mapped) {
|
||||||
|
result += iconTextPrefix + mapped.mp + iconTextSuffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function traverseJson(node: any) {
|
||||||
|
let result = '';
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result += concatNodes(node);
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const element of node.children) {
|
||||||
|
result += traverseJson(element);
|
||||||
|
result += entityNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportFreeMind(minder: any) {
|
||||||
|
const minds = minder.exportJson();
|
||||||
|
const mmContent = mmVersion + traverseJson(minds.root) + entityNode + entityMap;
|
||||||
|
try {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const blob = new Blob([`\ufeff${mmContent}`], {
|
||||||
|
type: 'text/xml',
|
||||||
|
});
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
link.download = `${minds.root.data.text}.mm`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-alert, no-console
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { exportFreeMind };
|
|
@ -0,0 +1,19 @@
|
||||||
|
function exportJson(minder: any) {
|
||||||
|
const minds = minder.exportJson();
|
||||||
|
try {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const blob = new Blob([`\ufeff${JSON.stringify(minds)}`], {
|
||||||
|
type: 'text/json',
|
||||||
|
});
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
link.download = `${minds.root.data.text}.json`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { exportJson };
|
|
@ -0,0 +1,66 @@
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
const EMPTY_LINE = '';
|
||||||
|
const NOTE_MARK_START = '<!--Note-->';
|
||||||
|
const NOTE_MARK_CLOSE = '<!--/Note-->';
|
||||||
|
|
||||||
|
function _generateHeaderSharp(level: number) {
|
||||||
|
let sharps = '';
|
||||||
|
while (level--) sharps += '#';
|
||||||
|
return sharps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _build(node: any, level: number) {
|
||||||
|
let lines: string[] = [];
|
||||||
|
|
||||||
|
level = level || 1;
|
||||||
|
|
||||||
|
const sharps = _generateHeaderSharp(level);
|
||||||
|
lines.push(`${sharps} ${node.data.text}`);
|
||||||
|
lines.push(EMPTY_LINE);
|
||||||
|
|
||||||
|
let { note } = node.data;
|
||||||
|
if (note) {
|
||||||
|
const hasSharp = /^#/.test(note);
|
||||||
|
if (hasSharp) {
|
||||||
|
lines.push(NOTE_MARK_START);
|
||||||
|
note = note.replace(/^#+/gm, ($0: string) => {
|
||||||
|
return sharps + $0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
lines.push(note);
|
||||||
|
if (hasSharp) {
|
||||||
|
lines.push(NOTE_MARK_CLOSE);
|
||||||
|
}
|
||||||
|
lines.push(EMPTY_LINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children)
|
||||||
|
node.children.forEach((child: any) => {
|
||||||
|
lines = lines.concat(_build(child, level + 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode(json: any) {
|
||||||
|
return _build(json, 1).join('\n');
|
||||||
|
}
|
||||||
|
function exportMarkdown(minder: any) {
|
||||||
|
const minds = minder.exportJson();
|
||||||
|
try {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const blob = new Blob([`\ufeff${encode(minds.root)}`], {
|
||||||
|
type: 'markdown',
|
||||||
|
});
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
link.download = `${minds.root.data.text}.md`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { exportMarkdown };
|
|
@ -0,0 +1,41 @@
|
||||||
|
const LINE_ENDING = '\r';
|
||||||
|
const TAB_CHAR = '\t';
|
||||||
|
|
||||||
|
function repeat(s: string, n: number) {
|
||||||
|
let result = '';
|
||||||
|
while (n--) result += s;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode(json: any, level: number) {
|
||||||
|
let local = '';
|
||||||
|
level = level || 0;
|
||||||
|
local += repeat(TAB_CHAR, level);
|
||||||
|
local += json.data.text + LINE_ENDING;
|
||||||
|
if (json.children) {
|
||||||
|
json.children.forEach((child: any) => {
|
||||||
|
local += encode(child, level + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return local;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportTextTree(minder: any) {
|
||||||
|
const minds = minder.exportJson();
|
||||||
|
try {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const blob = new Blob([`\ufeff${encode(minds.root, 0)}`], {
|
||||||
|
type: 'text/plain',
|
||||||
|
});
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
link.download = `${minds.root.data.text}.txt`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { exportTextTree };
|
|
@ -0,0 +1,167 @@
|
||||||
|
/* eslint-disable prefer-const */
|
||||||
|
const DOMURL = window.URL || window.webkitURL || window;
|
||||||
|
|
||||||
|
function downloadImage(fileURI: string, fileName: string) {
|
||||||
|
try {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = fileURI;
|
||||||
|
link.download = `${fileName}.png`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadImage(url: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const image = document.createElement('img');
|
||||||
|
image.onload = function () {
|
||||||
|
resolve(this);
|
||||||
|
};
|
||||||
|
image.onerror = (err) => {
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
image.crossOrigin = '';
|
||||||
|
image.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSVGInfo(minder: any) {
|
||||||
|
const paper = minder.getPaper();
|
||||||
|
let svgXml;
|
||||||
|
let $svg;
|
||||||
|
const renderContainer = minder.getRenderContainer();
|
||||||
|
const renderBox = renderContainer.getRenderBox();
|
||||||
|
const width = renderBox.width + 1;
|
||||||
|
const height = renderBox.height + 1;
|
||||||
|
let blob;
|
||||||
|
let svgUrl;
|
||||||
|
// 保存原始变换,并且移动到合适的位置
|
||||||
|
const paperTransform = paper.shapeNode.getAttribute('transform');
|
||||||
|
paper.shapeNode.setAttribute('transform', 'translate(0.5, 0.5)');
|
||||||
|
renderContainer.translate(-renderBox.x, -renderBox.y);
|
||||||
|
|
||||||
|
// 获取当前的 XML 代码
|
||||||
|
svgXml = paper.container.innerHTML;
|
||||||
|
|
||||||
|
// 回复原始变换及位置
|
||||||
|
renderContainer.translate(renderBox.x, renderBox.y);
|
||||||
|
paper.shapeNode.setAttribute('transform', paperTransform);
|
||||||
|
|
||||||
|
// 过滤内容
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.innerHTML = svgXml;
|
||||||
|
$svg = el.getElementsByTagName('svg');
|
||||||
|
|
||||||
|
const index = $svg.length - 1;
|
||||||
|
|
||||||
|
$svg[index].setAttribute('width', renderBox.width + 1);
|
||||||
|
$svg[index].setAttribute('height', renderBox.height + 1);
|
||||||
|
$svg[index].setAttribute('style', 'font-family: Arial, "Microsoft Yahei","Heiti SC";');
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.appendChild($svg[index]);
|
||||||
|
svgXml = div.innerHTML;
|
||||||
|
|
||||||
|
// Dummy IE
|
||||||
|
svgXml = svgXml.replace(
|
||||||
|
' xmlns="http://www.w3.org/2000/svg" xmlns:NS1="" NS1:ns1:xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:NS2="" NS2:xmlns:ns1=""',
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
// svg 含有 符号导出报错 Entity 'nbsp' not defined
|
||||||
|
svgXml = svgXml.replace(/ /g, ' ');
|
||||||
|
|
||||||
|
blob = new Blob([svgXml], {
|
||||||
|
type: 'image/svg+xml',
|
||||||
|
});
|
||||||
|
|
||||||
|
svgUrl = DOMURL.createObjectURL(blob);
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
dataUrl: svgUrl,
|
||||||
|
xml: svgXml,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillBackground(ctx: any, style: CanvasPattern | null | undefined, canvas: any) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = style;
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawImage(ctx: any, image: any, x: number, y: number) {
|
||||||
|
ctx.drawImage(image, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDataUrl(canvas: any) {
|
||||||
|
try {
|
||||||
|
const url = canvas.toDataURL('png');
|
||||||
|
return url;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('当前浏览器版本不支持导出 PNG 功能,请尝试升级到最新版本!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportPNGImage(minder: any) {
|
||||||
|
/* 绘制 PNG 的画布及上下文 */
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
/* 尝试获取背景图片 URL 或背景颜色 */
|
||||||
|
const bgDeclare = minder.getStyle('background').toString();
|
||||||
|
const bgUrl = /url\((.+)\)/.exec(bgDeclare);
|
||||||
|
const bgColor = window.kity.Color.parse(bgDeclare);
|
||||||
|
|
||||||
|
/* 获取 SVG 文件内容 */
|
||||||
|
const svgInfo = getSVGInfo(minder);
|
||||||
|
const { width } = svgInfo;
|
||||||
|
const { height } = svgInfo;
|
||||||
|
const svgDataUrl = svgInfo.dataUrl;
|
||||||
|
|
||||||
|
/* 画布的填充大小 */
|
||||||
|
const padding = 20;
|
||||||
|
|
||||||
|
canvas.width = width + padding * 2;
|
||||||
|
canvas.height = height + padding * 2;
|
||||||
|
|
||||||
|
function drawSVG() {
|
||||||
|
const mind = window.editor.minder.exportJson();
|
||||||
|
if (typeof window.canvg !== 'undefined') {
|
||||||
|
return window.canvg(canvas, svgInfo.xml, {
|
||||||
|
ignoreMouse: true,
|
||||||
|
ignoreAnimation: true,
|
||||||
|
ignoreDimensions: true,
|
||||||
|
ignoreClear: true,
|
||||||
|
offsetX: padding,
|
||||||
|
offsetY: padding,
|
||||||
|
renderCallback() {
|
||||||
|
downloadImage(generateDataUrl(canvas), mind.root.data.text);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return loadImage(svgDataUrl).then((svgImage) => {
|
||||||
|
drawImage(ctx, svgImage, padding, padding);
|
||||||
|
DOMURL.revokeObjectURL(svgDataUrl);
|
||||||
|
downloadImage(generateDataUrl(canvas), mind.root.data.text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bgUrl) {
|
||||||
|
loadImage(bgUrl[1]).then((image) => {
|
||||||
|
fillBackground(ctx, ctx?.createPattern(image as CanvasImageSource, 'repeat'), canvas);
|
||||||
|
drawSVG();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fillBackground(ctx, bgColor.toString(), canvas);
|
||||||
|
drawSVG();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { exportPNGImage };
|
|
@ -0,0 +1,73 @@
|
||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
/* eslint-disable prefer-const */
|
||||||
|
|
||||||
|
function downloadSVG(fileURI: string, fileName: string) {
|
||||||
|
try {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = fileURI;
|
||||||
|
link.download = `${fileName}.svg`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportSVG(minder: any) {
|
||||||
|
const paper = minder.getPaper();
|
||||||
|
const paperTransform = paper.shapeNode.getAttribute('transform');
|
||||||
|
let svgXml;
|
||||||
|
let $svg;
|
||||||
|
|
||||||
|
const renderContainer = minder.getRenderContainer();
|
||||||
|
const renderBox = renderContainer.getRenderBox();
|
||||||
|
const { width, height } = renderBox;
|
||||||
|
const padding = 20;
|
||||||
|
|
||||||
|
paper.shapeNode.setAttribute('transform', 'translate(0.5, 0.5)');
|
||||||
|
svgXml = paper.container.innerHTML;
|
||||||
|
paper.shapeNode.setAttribute('transform', paperTransform);
|
||||||
|
|
||||||
|
const { document } = window;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.innerHTML = svgXml;
|
||||||
|
$svg = el.getElementsByTagName('svg');
|
||||||
|
|
||||||
|
const index = $svg.length - 1;
|
||||||
|
|
||||||
|
$svg[index].setAttribute('width', width + padding * 2 || 0);
|
||||||
|
$svg[index].setAttribute('height', height + padding * 2 || 0);
|
||||||
|
$svg[index].setAttribute(
|
||||||
|
'style',
|
||||||
|
`font-family: Arial, "Microsoft Yahei", "Heiti SC"; background: ${minder.getStyle('background')}`
|
||||||
|
);
|
||||||
|
|
||||||
|
$svg[index].setAttribute(
|
||||||
|
'viewBox',
|
||||||
|
[
|
||||||
|
(renderBox.x - padding) | 0,
|
||||||
|
(renderBox.y - padding) | 0,
|
||||||
|
(width + padding * 2) | 0,
|
||||||
|
(height + padding * 2) | 0,
|
||||||
|
].join(' ')
|
||||||
|
);
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.appendChild($svg[index]);
|
||||||
|
svgXml = div.innerHTML;
|
||||||
|
svgXml = svgXml.replace(/ /g, ' ');
|
||||||
|
|
||||||
|
const blob = new Blob([svgXml], {
|
||||||
|
type: 'image/svg+xml',
|
||||||
|
});
|
||||||
|
|
||||||
|
const DOMURL = window.URL || window.webkitURL || window;
|
||||||
|
const svgUrl = DOMURL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const mind = window.editor.minder.exportJson();
|
||||||
|
downloadSVG(svgUrl, mind.root.data.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { exportSVG };
|
|
@ -0,0 +1,98 @@
|
||||||
|
interface MimeTypes {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Signatures {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MimeType = () => {
|
||||||
|
const SPLITOR = '\uFEFF';
|
||||||
|
const MIMETYPE: MimeTypes = {
|
||||||
|
'application/km': '\uFFFF',
|
||||||
|
};
|
||||||
|
const SIGN: Signatures = {
|
||||||
|
'\uFEFF': 'SPLITOR',
|
||||||
|
'\uFFFF': 'application/km',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSpitor(): string {
|
||||||
|
return SPLITOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPureText(text: string): boolean {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
return !~text.indexOf(getSpitor());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPureText(text: string): string {
|
||||||
|
if (isPureText(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return text.split(getSpitor())[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMimeType(sign?: string): MimeTypes | string | null {
|
||||||
|
if (sign !== undefined) {
|
||||||
|
return SIGN[sign] || null;
|
||||||
|
}
|
||||||
|
return MIMETYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function whichMimeType(text: string): MimeTypes | string | null {
|
||||||
|
if (isPureText(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getMimeType(text.split(getSpitor())[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function process(mimetype: string | false, text: string): string {
|
||||||
|
if (!isPureText(text)) {
|
||||||
|
const _mimetype = whichMimeType(text);
|
||||||
|
if (!_mimetype) {
|
||||||
|
throw new Error('unknown mimetype!');
|
||||||
|
}
|
||||||
|
text = getPureText(text);
|
||||||
|
}
|
||||||
|
if (mimetype === false) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return mimetype + SPLITOR + text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registMimeTypeProtocol(type: string, sign: string): void {
|
||||||
|
if (sign && SIGN[sign]) {
|
||||||
|
throw new Error('sign has registered!');
|
||||||
|
}
|
||||||
|
if (type && !!MIMETYPE[type]) {
|
||||||
|
throw new Error('mimetype has registered!');
|
||||||
|
}
|
||||||
|
SIGN[sign] = type;
|
||||||
|
MIMETYPE[type] = sign;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMimeTypeProtocol(type: string, text?: string): any {
|
||||||
|
const mimetype = MIMETYPE[type] || false;
|
||||||
|
|
||||||
|
if (text === undefined) {
|
||||||
|
return process.bind(null, mimetype);
|
||||||
|
}
|
||||||
|
|
||||||
|
return process(mimetype, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
registMimeTypeProtocol,
|
||||||
|
getMimeTypeProtocol,
|
||||||
|
getSpitor,
|
||||||
|
getMimeType,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const MimeTypeRuntime = function r(this: any) {
|
||||||
|
if (this.minder.supportClipboardEvent && !window.kity.Browser.gecko) {
|
||||||
|
this.MimeType = MimeType();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MimeTypeRuntime;
|
|
@ -0,0 +1,199 @@
|
||||||
|
import { markDeleteNode, resetNodes } from '../tool/utils';
|
||||||
|
|
||||||
|
interface INode {
|
||||||
|
getLevel(): number;
|
||||||
|
isAncestorOf(node: INode): boolean;
|
||||||
|
appendChild(node: INode): INode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IData {
|
||||||
|
getRegisterProtocol(protocol: string): {
|
||||||
|
encode: (nodes: Array<INode>) => Array<INode>;
|
||||||
|
decode: (nodes: Array<INode>) => Array<INode>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICliboardEvent extends ClipboardEvent {
|
||||||
|
clipboardData: DataTransfer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClipboardRuntime(this: any) {
|
||||||
|
const { minder } = this;
|
||||||
|
const { receiver } = this;
|
||||||
|
const Data: IData = window.kityminder.data;
|
||||||
|
|
||||||
|
if (!minder.supportClipboardEvent || window.kity.Browser.gecko) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kmencode = this.MimeType.getMimeTypeProtocol('application/km');
|
||||||
|
const { decode } = Data.getRegisterProtocol('json');
|
||||||
|
let _selectedNodes: Array<INode> = [];
|
||||||
|
|
||||||
|
function encode(nodes: Array<INode>): string {
|
||||||
|
const _nodes = [];
|
||||||
|
for (let i = 0, l = nodes.length; i < l; i++) {
|
||||||
|
_nodes.push(minder.exportNode(nodes[i]));
|
||||||
|
}
|
||||||
|
return kmencode(Data.getRegisterProtocol('json').encode(_nodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeCopy = (e: ICliboardEvent) => {
|
||||||
|
if (document.activeElement === receiver.element) {
|
||||||
|
const clipBoardEvent = e;
|
||||||
|
const state = this.fsm.state();
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'input': {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'normal': {
|
||||||
|
const nodes = [...minder.getSelectedNodes()];
|
||||||
|
if (nodes.length) {
|
||||||
|
if (nodes.length > 1) {
|
||||||
|
let targetLevel;
|
||||||
|
nodes.sort((a: any, b: any) => {
|
||||||
|
return a.getLevel() - b.getLevel();
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
targetLevel = nodes[0].getLevel();
|
||||||
|
if (targetLevel !== nodes[nodes.length - 1].getLevel()) {
|
||||||
|
let pnode;
|
||||||
|
let idx = 0;
|
||||||
|
const l = nodes.length;
|
||||||
|
let pidx = l - 1;
|
||||||
|
|
||||||
|
pnode = nodes[pidx];
|
||||||
|
|
||||||
|
while (pnode.getLevel() !== targetLevel) {
|
||||||
|
idx = 0;
|
||||||
|
while (idx < l && nodes[idx].getLevel() === targetLevel) {
|
||||||
|
if (nodes[idx].isAncestorOf(pnode)) {
|
||||||
|
nodes.splice(pidx, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
pidx--;
|
||||||
|
pnode = nodes[pidx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const str = encode(nodes);
|
||||||
|
clipBoardEvent.clipboardData.setData('text/plain', str);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeCut = (e: ClipboardEvent) => {
|
||||||
|
const { activeElement } = document;
|
||||||
|
if (activeElement === receiver.element) {
|
||||||
|
if (minder.getStatus() !== 'normal') {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipBoardEvent = e;
|
||||||
|
const state = this.fsm.state();
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'input': {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'normal': {
|
||||||
|
markDeleteNode(minder);
|
||||||
|
const nodes = minder.getSelectedNodes();
|
||||||
|
if (nodes.length) {
|
||||||
|
clipBoardEvent.clipboardData?.setData('text/plain', encode(nodes));
|
||||||
|
minder.execCommand('removenode');
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforePaste = (e: ClipboardEvent) => {
|
||||||
|
if (document.activeElement === receiver.element) {
|
||||||
|
if (minder.getStatus() !== 'normal') {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipBoardEvent = e;
|
||||||
|
const state = this.fsm.state();
|
||||||
|
const textData = clipBoardEvent.clipboardData?.getData('text/plain');
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'input': {
|
||||||
|
// input状态下如果格式为application/km则不进行paste操作
|
||||||
|
if (!this.MimeType.isPureText(textData)) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'normal': {
|
||||||
|
/*
|
||||||
|
* 针对normal状态下通过对选中节点粘贴导入子节点文本进行单独处理
|
||||||
|
*/
|
||||||
|
const sNodes = minder.getSelectedNodes();
|
||||||
|
|
||||||
|
if (this.MimeType.whichMimeType(textData) === 'application/km') {
|
||||||
|
const nodes = decode(this.MimeType.getPureText(textData));
|
||||||
|
resetNodes(nodes);
|
||||||
|
let _node;
|
||||||
|
sNodes.forEach((node: INode) => {
|
||||||
|
// 由于粘贴逻辑中为了排除子节点重新排序导致逆序,因此复制的时候倒过来
|
||||||
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||||
|
_node = minder.createNode(null, node);
|
||||||
|
minder.importNode(_node, nodes[i]);
|
||||||
|
_selectedNodes.push(_node);
|
||||||
|
node.appendChild(_node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
minder.select(_selectedNodes, true);
|
||||||
|
_selectedNodes = [];
|
||||||
|
|
||||||
|
minder.refresh();
|
||||||
|
} else if (clipBoardEvent.clipboardData && clipBoardEvent.clipboardData.items[0].type.indexOf('image') > -1) {
|
||||||
|
const imageFile = clipBoardEvent.clipboardData.items[0].getAsFile();
|
||||||
|
const serverService = window.angular.element(document.body).injector().get('server');
|
||||||
|
|
||||||
|
return serverService.uploadImage(imageFile).then((json: Record<string, any>) => {
|
||||||
|
const resp = json.data;
|
||||||
|
if (resp.errno === 0) {
|
||||||
|
minder.execCommand('image', resp.data.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sNodes.forEach((node: INode) => {
|
||||||
|
minder.Text2Children(node, textData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
// 触发命令监听
|
||||||
|
minder.execCommand('paste');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 由editor的receiver统一处理全部事件,包括clipboard事件
|
||||||
|
* @Editor: Naixor
|
||||||
|
* @Date: 2015.9.24
|
||||||
|
*/
|
||||||
|
document.addEventListener('copy', () => beforeCopy);
|
||||||
|
document.addEventListener('cut', () => beforeCut);
|
||||||
|
document.addEventListener('paste', () => beforePaste);
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
function ContainerRuntime(this: { selector: string; container?: HTMLElement }) {
|
||||||
|
let container: HTMLElement | null;
|
||||||
|
|
||||||
|
if (typeof this.selector === 'string') {
|
||||||
|
container = document.querySelector(this.selector);
|
||||||
|
} else {
|
||||||
|
container = this.selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!container) throw new Error(`Invalid selector: ${this.selector}`);
|
||||||
|
|
||||||
|
// 这个类名用于给编辑器添加样式
|
||||||
|
container.classList.add('km-editor');
|
||||||
|
|
||||||
|
// 暴露容器给其他运行时使用
|
||||||
|
this.container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContainerRuntime;
|
|
@ -0,0 +1,180 @@
|
||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
/**
|
||||||
|
* @fileOverview
|
||||||
|
*
|
||||||
|
* 用于拖拽节点时屏蔽键盘事件
|
||||||
|
*
|
||||||
|
* @author: techird
|
||||||
|
* @copyright: Baidu FEX, 2014
|
||||||
|
*/
|
||||||
|
interface DragRuntimeOptions {
|
||||||
|
fsm: any;
|
||||||
|
minder: any;
|
||||||
|
hotbox: any;
|
||||||
|
receiver: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDragRuntime(this: DragRuntimeOptions) {
|
||||||
|
const { fsm, minder, hotbox } = this;
|
||||||
|
|
||||||
|
// listen the fsm changes, make action.
|
||||||
|
function setupFsm() {
|
||||||
|
// when jumped to drag mode, enter
|
||||||
|
fsm.when('* -> drag', () => {
|
||||||
|
// now is drag mode
|
||||||
|
});
|
||||||
|
|
||||||
|
fsm.when('drag -> *', (exit: any, enter: any, reason: string) => {
|
||||||
|
if (reason === 'drag-finish') {
|
||||||
|
// now exit drag mode
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup everything to go
|
||||||
|
setupFsm();
|
||||||
|
|
||||||
|
let downX: number;
|
||||||
|
let downY: number;
|
||||||
|
const MOUSE_HAS_DOWN = 0;
|
||||||
|
const MOUSE_HAS_UP = 1;
|
||||||
|
const BOUND_CHECK = 20;
|
||||||
|
let flag = MOUSE_HAS_UP;
|
||||||
|
let maxX: number;
|
||||||
|
let maxY: number;
|
||||||
|
let containerY: number;
|
||||||
|
let freeHorizen = false;
|
||||||
|
let freeVirtical = false;
|
||||||
|
let frame: number | null = null;
|
||||||
|
|
||||||
|
function move(direction: 'left' | 'top' | 'right' | 'bottom' | false, speed?: number) {
|
||||||
|
if (!direction) {
|
||||||
|
freeHorizen = false;
|
||||||
|
freeVirtical = false;
|
||||||
|
if (frame) {
|
||||||
|
cancelAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
frame = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!frame) {
|
||||||
|
frame = requestAnimationFrame(
|
||||||
|
((directionF: 'left' | 'top' | 'right' | 'bottom', minderF: any, speedF = 0): any => {
|
||||||
|
return () => {
|
||||||
|
switch (directionF) {
|
||||||
|
case 'left':
|
||||||
|
minderF._viewDragger.move(
|
||||||
|
{
|
||||||
|
x: -speedF,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'top':
|
||||||
|
minderF._viewDragger.move(
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: -speedF,
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
minderF._viewDragger.move(
|
||||||
|
{
|
||||||
|
x: speedF,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'bottom':
|
||||||
|
minderF._viewDragger.move(
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: speedF,
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (frame) {
|
||||||
|
cancelAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(direction, minder, speed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minder.on('mousedown', (e: any) => {
|
||||||
|
flag = MOUSE_HAS_DOWN;
|
||||||
|
const rect = minder.getPaper().container.getBoundingClientRect();
|
||||||
|
downX = e.originEvent.clientX;
|
||||||
|
downY = e.originEvent.clientY;
|
||||||
|
containerY = rect.top;
|
||||||
|
maxX = rect.width;
|
||||||
|
maxY = rect.height;
|
||||||
|
});
|
||||||
|
|
||||||
|
minder.on('mousemove', (e: any) => {
|
||||||
|
if (
|
||||||
|
fsm.state() === 'drag' &&
|
||||||
|
flag === MOUSE_HAS_DOWN &&
|
||||||
|
minder.getSelectedNode() &&
|
||||||
|
(Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK || Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK)
|
||||||
|
) {
|
||||||
|
const osx = e.originEvent.clientX;
|
||||||
|
const osy = e.originEvent.clientY - containerY;
|
||||||
|
|
||||||
|
if (osx < BOUND_CHECK) {
|
||||||
|
move('right', BOUND_CHECK - osx);
|
||||||
|
} else if (osx > maxX - BOUND_CHECK) {
|
||||||
|
move('left', BOUND_CHECK + osx - maxX);
|
||||||
|
} else {
|
||||||
|
freeHorizen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (osy < BOUND_CHECK) {
|
||||||
|
move('bottom', osy);
|
||||||
|
} else if (osy > maxY - BOUND_CHECK) {
|
||||||
|
move('top', BOUND_CHECK + osy - maxY);
|
||||||
|
} else {
|
||||||
|
freeVirtical = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (freeHorizen && freeVirtical) {
|
||||||
|
move(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
fsm.state() !== 'drag' &&
|
||||||
|
flag === MOUSE_HAS_DOWN &&
|
||||||
|
minder.getSelectedNode() &&
|
||||||
|
(Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK || Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK)
|
||||||
|
) {
|
||||||
|
if (fsm.state() === 'hotbox') {
|
||||||
|
hotbox.active(window.HotBox.STATE_IDLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
fsm.jump('drag', 'user-drag');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
'mouseup',
|
||||||
|
() => {
|
||||||
|
flag = MOUSE_HAS_UP;
|
||||||
|
if (fsm.state() === 'drag') {
|
||||||
|
move(false);
|
||||||
|
fsm.jump('normal', 'drag-finish');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default createDragRuntime;
|
|
@ -0,0 +1,85 @@
|
||||||
|
import png from '../protocol/png';
|
||||||
|
import svg from '../protocol/svg';
|
||||||
|
import json from '../protocol/json';
|
||||||
|
import plain from '../protocol/plain';
|
||||||
|
import md from '../protocol/markdown';
|
||||||
|
import mm from '../protocol/freemind';
|
||||||
|
import useLocaleNotVue from '../tool/useLocaleNotVue';
|
||||||
|
|
||||||
|
const tran = useLocaleNotVue;
|
||||||
|
|
||||||
|
export default function ExportRuntime(this: any) {
|
||||||
|
const { minder, hotbox } = this;
|
||||||
|
|
||||||
|
function canExp() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportJson() {
|
||||||
|
json.exportJson(minder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportImage() {
|
||||||
|
png.exportPNGImage(minder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportSVG() {
|
||||||
|
svg.exportSVG(minder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportTextTree() {
|
||||||
|
plain.exportTextTree(minder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportMarkdown() {
|
||||||
|
md.exportMarkdown(minder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportFreeMind() {
|
||||||
|
mm.exportFreeMind(minder);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exps = [
|
||||||
|
{ label: '.json', key: 'j', cmd: exportJson },
|
||||||
|
{ label: '.png', key: 'p', cmd: exportImage },
|
||||||
|
{ label: '.svg', key: 's', cmd: exportSVG },
|
||||||
|
{ label: '.txt', key: 't', cmd: exportTextTree },
|
||||||
|
{ label: '.md', key: 'm', cmd: exportMarkdown },
|
||||||
|
{ label: '.mm', key: 'f', cmd: exportFreeMind },
|
||||||
|
];
|
||||||
|
|
||||||
|
const main = hotbox.state('main');
|
||||||
|
main.button({
|
||||||
|
position: 'top',
|
||||||
|
label: tran('minder.commons.export'),
|
||||||
|
key: 'E',
|
||||||
|
enable: canExp,
|
||||||
|
beforeShow() {
|
||||||
|
this.$button.children[0].innerHTML = tran('minder.commons.export');
|
||||||
|
},
|
||||||
|
next: 'exp',
|
||||||
|
});
|
||||||
|
|
||||||
|
const exp = hotbox.state('exp');
|
||||||
|
exps.forEach((item) => {
|
||||||
|
exp.button({
|
||||||
|
position: 'ring',
|
||||||
|
label: item.label,
|
||||||
|
key: null,
|
||||||
|
action: item.cmd,
|
||||||
|
beforeShow() {
|
||||||
|
this.$button.children[0].innerHTML = tran(item.label);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
exp.button({
|
||||||
|
position: 'center',
|
||||||
|
label: tran('minder.commons.cancel'),
|
||||||
|
key: 'esc',
|
||||||
|
beforeShow() {
|
||||||
|
this.$button.children[0].innerHTML = tran('minder.commons.cancel');
|
||||||
|
},
|
||||||
|
next: 'back',
|
||||||
|
});
|
||||||
|
}
|