├── .editorconfig
├── .env
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .umirc.ts
├── .vscode
└── setting.json
├── LICENSE
├── README.md
├── package.json
├── public
├── favicon.ico
├── less.min.js
├── logo.png
└── logo_white.png
├── src
├── components
│ ├── CodeBlock
│ │ └── index.tsx
│ └── Loader
│ │ ├── index.less
│ │ └── index.tsx
├── db
│ └── models
│ │ ├── Groups.ts
│ │ └── Qas.ts
├── global.less
├── layouts
│ ├── components
│ │ ├── Header
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ ├── Logo
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ └── Modal
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ ├── index.less
│ └── index.tsx
├── locales
│ ├── en-US.ts
│ └── zh-CN.ts
├── models
│ ├── app.ts
│ └── index.ts
├── pages
│ └── index
│ │ ├── components
│ │ ├── Filter
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ ├── Header
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ ├── Modal
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ └── Qas
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ └── index.tsx
├── services
│ ├── app.ts
│ └── index.ts
├── themes
│ ├── changeTheme.ts
│ ├── index.less
│ ├── index.ts
│ ├── preset
│ │ ├── atom.less
│ │ ├── classes.less
│ │ ├── helpers
│ │ │ ├── bezierEasing.less
│ │ │ ├── colorPalette.less
│ │ │ └── tinyColor.less
│ │ ├── index.less
│ │ ├── init.less
│ │ └── mixin.less
│ └── skins
│ │ └── default.less
└── utils
│ ├── helpers
│ └── formatCheckOK.ts
│ └── model
│ └── index.ts
├── tsconfig.json
└── typings.d.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 6
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | BROWSER=chrome
2 | HOST=0.0.0.0
3 | PORT=8000
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Deploy gh-pages
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | build-and-deploy:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@master
12 |
13 | - name: Build
14 | run: npm install && npm run build
15 |
16 | - name: Deploy
17 | uses: peaceiris/actions-gh-pages@v2
18 | env:
19 | ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}
20 | PUBLISH_BRANCH: gh-pages
21 | PUBLISH_DIR: ./dist
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /npm-debug.log*
6 | /yarn-error.log
7 | /yarn.lock
8 | /package-lock.json
9 |
10 | # production
11 | /dist
12 |
13 | # misc
14 | .DS_Store
15 |
16 | # umi
17 | /src/.umi
18 | /src/.umi-production
19 | /src/.umi-test
20 | /.env.local
21 |
--------------------------------------------------------------------------------
/.umirc.ts:
--------------------------------------------------------------------------------
1 | import path, { resolve } from 'path'
2 | import { defineConfig } from 'umi'
3 | import OfflinePlugin from 'offline-plugin'
4 | import WebpackPwaManifest from 'webpack-pwa-manifest'
5 | import AntDesignThemePlugin from 'antd-theme-webpack-plugin'
6 | import theme from './src/themes'
7 |
8 | export default defineConfig({
9 | antd: {},
10 | cssnano: {},
11 | theme: theme,
12 | base: '/testool/',
13 | favicon: 'favicon.ico',
14 | publicPath: '/testool/',
15 | dva: { immer: true, hmr: true },
16 | alias: { R: resolve(__dirname, './') },
17 | links: [ { rel: 'manifest', href: 'manifest.json' } ],
18 | dynamicImport: { loading: '@/components/Loader/index' },
19 | locale: { baseNavigator: false, default: 'en-US', antd: true },
20 | lessLoader: {
21 | javascriptEnabled: true
22 | },
23 | extraBabelPlugins: [
24 | [
25 | 'import',
26 | {
27 | libraryName: 'lodash',
28 | libraryDirectory: '',
29 | camel2DashComponentName: false
30 | }
31 | ]
32 | ],
33 | chainWebpack (memo) {
34 | memo.resolve.alias.set(
35 | 'moment$',
36 | path.resolve(__dirname, 'node_modules/moment/moment.js')
37 | )
38 |
39 | memo.plugin('antd-theme').use(AntDesignThemePlugin, [ webpack_plugin_antd_theme ])
40 | memo.plugin('offline-plugin').use(OfflinePlugin, [ webpack_plugin_offline ])
41 | memo.plugin('webpack-pwa-manifest').use(WebpackPwaManifest, [ webpack_plugin_pwa ])
42 | }
43 | })
44 |
45 | const webpack_plugin_offline: any = {
46 | safeToUseOptionalCaches: true,
47 | ServiceWorker: { events: true },
48 | AppCache: { events: true }
49 | }
50 |
51 | const webpack_plugin_pwa: any = {
52 | name: 'Testool',
53 | short_name: 'Testool',
54 | fingerprints: false,
55 | description: 'A artifact for the test/interview/exam.',
56 | background_color: '#ffffff',
57 | theme_color: '#f44336',
58 | crossorigin: 'use-credentials',
59 | icons: [
60 | {
61 | src: path.resolve(__dirname, 'public/logo.png'),
62 | sizes: [ 96, 128, 192, 256, 384, 512 ]
63 | },
64 | {
65 | src: path.resolve(__dirname, 'public/logo_white.png'),
66 | size: '512x512'
67 | }
68 | ]
69 | }
70 |
71 | const webpack_plugin_antd_theme: any = {
72 | antDir: path.join(__dirname, './node_modules/antd'),
73 | stylesDir: path.join(__dirname, './src/themes'),
74 | varFile: path.join(__dirname, './src/themes/skins/default.less'),
75 | mainLessFile: path.join(__dirname, './src/global.less'),
76 | themeVariables: [
77 | '@text-color',
78 | '@disabled-color',
79 | '@item-hover-bg',
80 | '@component-background',
81 | '@background-color-light',
82 | '@background-color-base',
83 | '@text-color-secondary',
84 | '@body-background',
85 | '@border-color-base',
86 | '@border-color-split'
87 | ],
88 | lessUrl: 'less.min.js',
89 | publicPath: '/testool'
90 | }
91 |
--------------------------------------------------------------------------------
/.vscode/setting.json:
--------------------------------------------------------------------------------
1 | {
2 | "path-autocomplete.pathMappings": {
3 | "@": "${folder}/src",
4 | "R": "${folder}"
5 | },
6 | "path-intellisense.mappings": {
7 | "@": "${workspaceRoot}/src",
8 | "R": "${workspaceRoot}"
9 | }
10 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Dark
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 | # testool
5 |
6 | _A awesome tool for interview & test & exam!
_
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ## Usage
15 |
16 | https://matrixage.github.io/testool/
17 |
18 | ## Preview
19 |
20 | 
21 |
22 | ## Accessibility
23 |
24 | Testool supports the modern browers which support indexeddb and pwa.
25 |
26 | ## License
27 |
28 | Testool is licensed under the MIT license. (http://opensource.org/licenses/MIT)
29 |
30 | ## Contributing
31 |
32 | Welcome advices and pr!
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "name": "testool",
4 | "author": "matrixage",
5 | "license": "MIT",
6 | "private": false,
7 | "main": "src/pages/index/index.tsx",
8 | "description": "A artifact for the test/interview/exam.",
9 | "keywords": [
10 | "test",
11 | "interview",
12 | "exam",
13 | "react",
14 | "js",
15 | "umi",
16 | "考试",
17 | "面试",
18 | "背题神器",
19 | "背题"
20 | ],
21 | "scripts": {
22 | "start": "umi dev",
23 | "build": "umi build",
24 | "test": "umi-test",
25 | "test:coverage": "umi-test --coverage"
26 | },
27 | "dependencies": {
28 | "@umijs/preset-react": "1.3.2",
29 | "@umijs/test": "^3.0.9",
30 | "dexie": "^2.0.4",
31 | "dva-model-extend": "^0.1.2",
32 | "is-mobile": "^2.2.1",
33 | "lodash": "^4.17.15",
34 | "moment": "^2.24.0",
35 | "qs": "^6.9.1",
36 | "react": "^16.12.0",
37 | "react-bottom-scroll-listener": "^3.0.0",
38 | "react-dom": "^16.12.0",
39 | "react-helmet": "^5.2.1",
40 | "react-markdown": "^4.3.1",
41 | "react-syntax-highlighter": "^12.2.1",
42 | "react-virtualized": "^9.21.2",
43 | "recharts": "^1.8.5",
44 | "store": "^2.0.12",
45 | "umi": "^3.0.9"
46 | },
47 | "devDependencies": {
48 | "@types/react-helmet": "^5.0.15",
49 | "@types/react-syntax-highlighter": "^11.0.4",
50 | "@types/react-virtualized": "^9.21.8",
51 | "@types/recharts": "^1.8.9",
52 | "@types/store": "^2.0.2",
53 | "antd-theme-webpack-plugin": "^1.3.1",
54 | "babel-plugin-import": "^1.13.0",
55 | "less-vars-to-js": "^1.3.0",
56 | "offline-plugin": "^5.0.7",
57 | "webpack-pwa-manifest": "^4.2.0"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatrixAges/testool/ba4c2abdc00bf077a8e3c04a9db6e731949b7b92/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatrixAges/testool/ba4c2abdc00bf077a8e3c04a9db6e731949b7b92/public/logo.png
--------------------------------------------------------------------------------
/public/logo_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatrixAges/testool/ba4c2abdc00bf077a8e3c04a9db6e731949b7b92/public/logo_white.png
--------------------------------------------------------------------------------
/src/components/CodeBlock/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { connect } from 'umi'
3 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
4 | import { prism, atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
5 |
6 | interface IProps {
7 | language?: string
8 | value: string
9 | theme: any
10 | }
11 |
12 | const Index = (props: IProps) => {
13 | const { language, value, theme } = props
14 |
15 | return (
16 |
17 | {value}
18 |
19 | )
20 | }
21 |
22 | export default memo(connect(({ app: { theme } }: any) => ({ theme }))(Index))
23 |
--------------------------------------------------------------------------------
/src/components/Loader/index.less:
--------------------------------------------------------------------------------
1 | @keyframes spinner {
2 | 0% {
3 | transform: rotate(0deg);
4 | }
5 |
6 | 100% {
7 | transform: rotate(360deg);
8 | }
9 | }
10 |
11 | ._local {
12 | position: absolute;
13 | top: 0;
14 | left: 0;
15 | z-index: 100000;
16 | background-color: rgba(255, 255, 255, 0.9);
17 |
18 | :global {
19 | .inner {
20 | width: 40px;
21 | height: 40px;
22 | margin-bottom: 10px;
23 | border-top: 1px solid rgba(0, 0, 0, 0.08);
24 | border-right: 1px solid rgba(0, 0, 0, 0.08);
25 | border-bottom: 1px solid rgba(0, 0, 0, 0.08);
26 | border-left: 1px solid rgba(0, 0, 0, 1);
27 | border-radius: 50%;
28 | z-index: 100001;
29 | :local{
30 | animation: spinner 600ms infinite linear;
31 | }
32 | }
33 |
34 | .text {
35 | width: 100px;
36 | height: 20px;
37 | text-align: center;
38 | font-size: 14px;
39 | letter-spacing: 2px;
40 | color: black;
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/components/Loader/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import styles from './index.less'
3 |
4 | const Index = () => {
5 | return (
6 |
12 | )
13 | }
14 |
15 | export default memo(Index)
16 |
--------------------------------------------------------------------------------
/src/db/models/Groups.ts:
--------------------------------------------------------------------------------
1 | import Dexie from 'dexie'
2 |
3 | export interface IGroups {
4 | id?: number
5 | name: string
6 | }
7 |
8 | export default class Groups extends Dexie {
9 | groups: Dexie.Table
10 |
11 | constructor () {
12 | super('Groups')
13 |
14 | this.version(1).stores({
15 | groups: '++id,name'
16 | })
17 |
18 | this.groups = this.table('groups')
19 | }
20 |
21 | async init () {
22 | return await this.open()
23 | }
24 |
25 | async addGroup (name: string): Promise {
26 | await this.init()
27 |
28 | await this.transaction('rw', this.groups, async () => {
29 | await this.groups.add({ name })
30 | })
31 | }
32 |
33 | async delGroup (name: string): Promise {
34 | let id: number
35 |
36 | const origin_groups = await this.getOriginGroups()
37 |
38 | origin_groups.map((item) => {
39 | if (item.name === name) {
40 | id = item.id
41 | }
42 | })
43 |
44 | await this.transaction('rw', this.groups, async () => {
45 | await this.groups.delete(id)
46 | })
47 | }
48 |
49 | async getOriginGroups (): Promise> {
50 | await this.init()
51 |
52 | return await this.groups.toArray()
53 | }
54 |
55 | async getGroups (): Promise> {
56 | const groups = []
57 | const origin_groups = await this.getOriginGroups()
58 |
59 | origin_groups.map((item) => {
60 | groups.push(item.name)
61 | })
62 |
63 | return groups
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/db/models/Qas.ts:
--------------------------------------------------------------------------------
1 | import Dexie from 'dexie'
2 |
3 | export interface IQas {
4 | id?: number
5 | question: string
6 | answer: string
7 | tags?: Array
8 | rates?: Array
9 | }
10 |
11 | export interface IRates {
12 | rate: number
13 | create_at: number
14 | }
15 |
16 | export interface IRate {
17 | id: number
18 | rate: number
19 | }
20 |
21 | export interface IQuery {
22 | query?: string
23 | times?: number
24 | rate?: number
25 | }
26 |
27 | export default class Qas extends Dexie {
28 | qas: Dexie.Table
29 |
30 | constructor (group_name: string) {
31 | super(group_name)
32 |
33 | this.version(1).stores({
34 | qas: '++id,question,answer,tags,rates'
35 | })
36 |
37 | this.qas = this.table('qas')
38 | }
39 |
40 | async init () {
41 | return await this.open()
42 | }
43 |
44 | async addQa ({ question, answer, tags }: IQas): Promise {
45 | await this.init()
46 |
47 | await this.transaction('rw', this.qas, async () => {
48 | await this.qas.add({ question, answer, tags, rates: [] })
49 | })
50 | }
51 |
52 | async bulkAddQa (array: Array): Promise {
53 | await this.init()
54 |
55 | const res = await this.transaction('rw', this.qas, async () => {
56 | try {
57 | await this.qas.bulkAdd(array)
58 |
59 | return true
60 | } catch (e) {
61 | return e.message
62 | }
63 | })
64 |
65 | return res
66 | }
67 |
68 | async delQa (id: number): Promise {
69 | await this.init()
70 |
71 | await this.transaction('rw', this.qas, async () => {
72 | await this.qas.delete(id)
73 | })
74 | }
75 |
76 | async putQa (id: number, { question, answer, tags }: IQas): Promise {
77 | await this.init()
78 |
79 | await this.transaction('rw', this.qas, async () => {
80 | await this.qas.update(id, { question, answer, tags })
81 | })
82 | }
83 |
84 | async query ({ query, times, rate }: IQuery): Promise> {
85 | await this.init()
86 |
87 | if (query) {
88 | return await this.qas
89 | .filter((item) => new RegExp(query).test(item.question))
90 | .toArray()
91 | } else {
92 | if (times && !rate) {
93 | return await this.qas.filter((item) => item.rates.length < times).toArray()
94 | }
95 |
96 | if (!times && rate) {
97 | return await this.qas
98 | .filter((item) => {
99 | const total: number = item.rates.reduce(
100 | (total: number, curr: IRates) => total + curr.rate,
101 | 0
102 | )
103 |
104 | return total / item.rates.length <= rate
105 | })
106 | .toArray()
107 | }
108 |
109 | if (times && rate) {
110 | return await this.qas
111 | .filter((item) => item.rates.length < times)
112 | .filter((item) => {
113 | const total: number = item.rates.reduce(
114 | (total: number, curr: IRates) => total + curr.rate,
115 | 0
116 | )
117 |
118 | return total / item.rates.length <= rate
119 | })
120 | .toArray()
121 | }
122 | }
123 | }
124 |
125 | async rate ({ id, rate }: IRate): Promise {
126 | await this.init()
127 |
128 | await this.transaction('rw', this.qas, async () => {
129 | await this.qas
130 | .where('id')
131 | .equals(id)
132 | .modify((item) =>
133 | item.rates.push({ rate, create_at: new Date().valueOf() })
134 | )
135 | })
136 | }
137 |
138 | async clearRateLog (id: number): Promise {
139 | await this.init()
140 |
141 | await this.transaction('rw', this.qas, async () => {
142 | await this.qas.update(id, { rates: [] })
143 | })
144 | }
145 |
146 | async getQas (page?: number, page_size?: number): Promise> {
147 | await this.init()
148 |
149 | const _page = page ? page - 1 : 0
150 | const _page_size = page_size ? page_size : 10
151 | const _offset = _page * _page_size
152 |
153 | return await this.qas.orderBy('id').offset(_offset).limit(_page_size).toArray()
154 | }
155 |
156 | async getTotal (): Promise {
157 | await this.init()
158 |
159 | const array = await this.qas.toArray()
160 |
161 | return array.length
162 | }
163 |
164 | async getTotalQas (): Promise> {
165 | await this.init()
166 |
167 | return await this.qas.toArray()
168 | }
169 |
170 | async getAverageRate (): Promise {
171 | await this.init()
172 |
173 | const array = await this.qas.toArray()
174 | const length_array = array.length
175 |
176 | let total_all: number = 0
177 |
178 | if (!length_array) return 0
179 |
180 | array.map((item) => {
181 | const length_rates = item.rates.length
182 |
183 | const total_rates: number = item.rates.reduce(
184 | (total: number, curr: IRates) => total + curr.rate,
185 | 0
186 | )
187 |
188 | if (length_rates) {
189 | total_all = total_all + total_rates / length_rates
190 | }
191 | })
192 |
193 | return parseFloat((total_all / length_array).toFixed(2))
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/global.less:
--------------------------------------------------------------------------------
1 | @import './themes/index.less';
--------------------------------------------------------------------------------
/src/layouts/components/Header/index.less:
--------------------------------------------------------------------------------
1 | ._local {
2 | height: 56px;
3 | padding: 0 10px;
4 | top: 0;
5 | z-index: 10;
6 | background: var(--primary-color);
7 | color: white;
8 |
9 | &.scrolled {
10 | box-shadow: 0 0 10px #666;
11 |
12 | &.dark {
13 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
14 | }
15 |
16 | @media screen and (max-width:420px) {
17 | box-shadow: none;
18 | }
19 | }
20 |
21 | &.dark {
22 | background-color: var(--color_background);
23 | border-bottom: 1px solid #333;
24 |
25 | :global {
26 | .ant-back-top {
27 |
28 | .ant-back-top-content {
29 | background-color: #333;
30 | }
31 | }
32 | }
33 | }
34 |
35 | :global {
36 | .input_search {
37 | width: 680px;
38 | border: none;
39 |
40 | @media screen and (max-width:840px) {
41 | width: 50vw;
42 | display: none;
43 | }
44 | }
45 |
46 | .left {
47 | left: 10px;
48 |
49 | @media screen and (max-width:840px) {
50 | left: 16px;
51 | }
52 | }
53 |
54 | .right {
55 | right: 10px;
56 |
57 | @media screen and (max-width:840px) {
58 | .btn_search {
59 | display: flex;
60 | }
61 | }
62 |
63 | .icon_wrap {
64 | width: 40px;
65 | height: 40px;
66 | border-radius: var(--radius_normal);
67 |
68 | &:hover {
69 | background-color: var(--light_bg_hover);
70 | }
71 | }
72 | }
73 |
74 | .ant-input-affix-wrapper {
75 | background-color: rgba(0, 0, 0, 0.22);
76 | transition: all ease 0.3s;
77 |
78 | &:hover {
79 | background-color: rgba(0, 0, 0, 0.4);
80 | }
81 |
82 | .ant-input-prefix {
83 | margin-left: 4px;
84 | margin-right: 10px;
85 | }
86 |
87 | .ant-input-suffix {
88 | svg {
89 | color: white;
90 | }
91 | }
92 |
93 | input::-webkit-input-placeholder {
94 | color: rgba(255, 255, 255, 0.8);
95 | }
96 |
97 | input.ant-input {
98 | background-color: transparent;
99 | color: white;
100 | }
101 | }
102 |
103 | .ant-back-top {
104 | @media screen and (max-width:840px) {
105 | width: 30px !important;
106 | height: 30px !important;
107 | bottom: 15px !important;
108 | right: calc(~'50vw - 15px') !important;
109 | }
110 |
111 | .ant-back-top-content {
112 | width: 30px !important;
113 | height: 30px !important;
114 | background-color: #ddd;
115 | margin-right: 0;
116 | border-radius: var(--radius_normal);
117 | display: flex;
118 | justify-content: center;
119 | align-items: center;
120 |
121 | &:hover {
122 | background-color: var(--primary-color);
123 | }
124 |
125 | .ant-back-top-icon {
126 | margin: 0;
127 | background-size: 100% 100%;
128 | }
129 | }
130 | }
131 | }
132 | }
133 |
134 | .placeholder {
135 | height: 56px;
136 | }
--------------------------------------------------------------------------------
/src/layouts/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, Fragment, useState, useEffect } from 'react'
2 | import { connect, useIntl } from 'umi'
3 | import { Input, BackTop } from 'antd'
4 | import { SearchOutlined, SettingOutlined, GithubOutlined } from '@ant-design/icons'
5 | import Logo from '../Logo'
6 | import Modal from '../Modal'
7 | import styles from './index.less'
8 |
9 | const Index = (props: any) => {
10 | const { dispatch, app: { groups, analysis_data, theme, current_group } } = props
11 | const [ state_scrolled, setStateScrolled ] = useState(false)
12 | const [ state_modal_visible, setStateModalVisible ] = useState(false)
13 | const [ state_search_display, setStateSearchDisplay ] = useState({})
14 | const lang = useIntl()
15 |
16 | useEffect(() => {
17 | const setScrolled = (e: any) => {
18 | if (e.srcElement['documentElement'].scrollTop) {
19 | setStateScrolled(true)
20 | } else {
21 | setStateScrolled(false)
22 | }
23 | }
24 |
25 | window.addEventListener('scroll', setScrolled)
26 |
27 | return () => {
28 | window.removeEventListener('scroll', setScrolled)
29 | }
30 | }, [])
31 |
32 | const onDeleteGroup = (group: string) => {
33 | dispatch({
34 | type: 'app/deleteGroup',
35 | payload: {
36 | group,
37 | message_success: lang.formatMessage({
38 | id: 'layout.modal.clear.message_success'
39 | }),
40 | message_failed: lang.formatMessage({
41 | id: 'layout.modal.clear.message_failed'
42 | })
43 | }
44 | })
45 |
46 | setStateModalVisible(false)
47 | }
48 |
49 | const searchQaByQuestion = (e: any) => {
50 | if (e.target.value) {
51 | dispatch({
52 | type: 'index/queryQa',
53 | payload: {
54 | current_group,
55 | params: {
56 | query: e.target.value
57 | }
58 | }
59 | })
60 |
61 | dispatch({
62 | type: 'index/updateState',
63 | payload: { querying: true }
64 | })
65 | } else {
66 | dispatch({
67 | type: 'index/query',
68 | payload: {
69 | current_group
70 | }
71 | })
72 |
73 | dispatch({
74 | type: 'index/updateState',
75 | payload: {
76 | no_more: false,
77 | querying: false
78 | }
79 | })
80 | }
81 | }
82 |
83 | return (
84 |
85 |
93 |
94 |
95 |
96 |
}
100 | style={{ ...state_search_display }}
101 | allowClear={true}
102 | maxLength={16}
103 | size='large'
104 | type='search'
105 | onChange={searchQaByQuestion}
106 | />
107 |
108 | {!state_search_display['display'] && (
109 |
{
112 | setStateSearchDisplay({ display: 'flex' })
113 | }}
114 | >
115 |
116 |
117 | )}
118 |
124 |
125 |
126 |
{
129 | setStateModalVisible(true)
130 | }}
131 | >
132 |
133 |
134 |
135 |
136 |
137 |
138 | {
145 | setStateModalVisible(false)
146 | }}
147 | onDeleteGroup={onDeleteGroup}
148 | setStateModalVisible={setStateModalVisible}
149 | />
150 |
151 | )
152 | }
153 |
154 | export default memo(connect(({ app }: any) => ({ app }))(Index))
155 |
--------------------------------------------------------------------------------
/src/layouts/components/Logo/index.less:
--------------------------------------------------------------------------------
1 | ._local {
2 | width: 40em;
3 | height: 30em;
4 | padding: 2em 0;
5 |
6 | &.main {
7 | background-color: var(--primary-color);
8 |
9 | :global {
10 | .left,.right {
11 | width: calc(~'(100% - 1em) / 2');
12 | }
13 |
14 | .line_item {
15 | width: 10em;
16 | height: 1.6em;
17 | background-color: white;
18 | }
19 |
20 | .line_item:nth-of-type(2) {
21 | margin: 5em;
22 | }
23 |
24 | .center {
25 | width: 1em;
26 | height: 100%;
27 | background-color: white;
28 | }
29 | }
30 | }
31 |
32 | &.white {
33 | background-color: white;
34 |
35 | :global {
36 | .left,.right {
37 | width: calc(~'(100% - 1em) / 2');
38 | }
39 |
40 | .line_item {
41 | width: 10em;
42 | height: 1.6em;
43 | background-color: var(--primary-color);
44 | }
45 |
46 | .line_item:nth-of-type(2) {
47 | margin: 5em;
48 | }
49 |
50 | .center {
51 | width: 1em;
52 | height: 100%;
53 | background-color: var(--primary-color);
54 | }
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/layouts/components/Logo/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import styles from './index.less'
3 |
4 | interface IProps {
5 | type?: 'main' | 'white'
6 | size?: number
7 | }
8 |
9 | const Index = (props: IProps) => {
10 | const { type = 'main', size = 1.6 } = props
11 |
12 | return (
13 |
29 | )
30 | }
31 |
32 | export default memo(Index)
33 |
--------------------------------------------------------------------------------
/src/layouts/components/Modal/index.less:
--------------------------------------------------------------------------------
1 | ._local {
2 | &.dark {
3 | :global {
4 | .settings_wrap {
5 | background-color: var(--color_background);
6 |
7 | .setting_item {
8 | border-top: 1px solid #242424;
9 | }
10 | }
11 | }
12 | }
13 |
14 | :global {
15 | .ant-modal-content {
16 | overflow: hidden;
17 | }
18 |
19 | .ant-modal-header {
20 | padding: 16px 20px;
21 | }
22 |
23 | .ant-modal-body {
24 | padding: 0;
25 | }
26 |
27 | .settings_wrap {
28 | background-color: white;
29 |
30 | .setting_item {
31 | padding: 12px 20px;
32 | border-top: 1px solid whitesmoke;
33 | }
34 |
35 | .option_items {
36 | .option_item {
37 | flex: 1;
38 | padding: 24px 0;
39 | padding-bottom: 20px;
40 | border-radius: var(--radius_normal);
41 |
42 | &:hover {
43 | background-color: var(--light_bg_hover);
44 | }
45 |
46 | .name {
47 | margin-top: 6px;
48 | }
49 | }
50 | }
51 | }
52 |
53 | .clear_wrap {
54 | padding: 20px;
55 | }
56 |
57 | .analysis_wrap {
58 | padding: 20px;
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/src/layouts/components/Modal/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useState, useEffect } from 'react'
2 | import { useIntl, getLocale, setLocale } from 'umi'
3 | import { Modal, Switch, Select, Empty, message } from 'antd'
4 | import { PieChartOutlined, ImportOutlined, ExportOutlined, ClearOutlined } from '@ant-design/icons'
5 | import { ResponsiveContainer, ComposedChart, XAxis, YAxis, Tooltip, Bar, Line } from 'recharts'
6 | import store from 'store'
7 | import { IGetAnalysisData } from '@/services/app'
8 | import changeTheme from '@/themes/changeTheme'
9 | import formatCheckOK from '@/utils/helpers/formatCheckOK'
10 | import styles from './index.less'
11 |
12 | const { Option } = Select
13 |
14 | interface IProps {
15 | theme: string
16 | groups: Array
17 | analysis_data: Array
18 | visible: boolean
19 | dispatch: (params: any) => void
20 | onCancel: () => void
21 | onDeleteGroup: (group: string) => void
22 | setStateModalVisible: (visible: boolean) => void
23 | }
24 |
25 | const Index = (props: IProps) => {
26 | const {
27 | theme,
28 | dispatch,
29 | groups,
30 | analysis_data,
31 | visible,
32 | onCancel,
33 | onDeleteGroup,
34 | setStateModalVisible
35 | } = props
36 | const lang = useIntl()
37 | const [ state_modal_type, setStateModalType ] = useState('settings')
38 | const [ state_group_selected, setStateGroupSelected ] = useState('')
39 |
40 | useEffect(
41 | () => {
42 | setStateModalType('settings')
43 | },
44 | [ visible ]
45 | )
46 |
47 | const props_modal = {
48 | visible,
49 | onCancel,
50 | centered: true,
51 | maskClosable: true,
52 | destroyOnClose: true,
53 | title: lang.formatMessage({ id: 'layout.modal.title' })
54 | }
55 |
56 | const onChangeLang = (v: boolean): void => {
57 | if (v) {
58 | setLocale('en-US', false)
59 | } else {
60 | setLocale('zh-CN', false)
61 | }
62 | }
63 |
64 | const onChangeTheme = (v: boolean): void => {
65 | let _theme: 'light' | 'dark' = v ? 'light' : 'dark'
66 |
67 | store.set('theme', _theme)
68 | changeTheme(_theme)
69 |
70 | dispatch({
71 | type: 'app/updateState',
72 | payload: { theme: _theme }
73 | })
74 | }
75 |
76 | const onChangeLoadway = (v: boolean): void => {
77 | let _loadway: string = v ? 'scroll' : 'page'
78 |
79 | store.set('loadway', _loadway)
80 |
81 | dispatch({
82 | type: 'app/updateState',
83 | payload: { loadway: _loadway }
84 | })
85 | }
86 |
87 | const onChangeModalType = (type: string): void => {
88 | if (type === 'analysis') {
89 | dispatch({ type: 'app/getAnalysisData' })
90 | }
91 |
92 | if (type === 'import') {
93 | const message_success = lang.formatMessage({
94 | id: 'layout.modal.import.message_success'
95 | })
96 | const message_failed = lang.formatMessage({
97 | id: 'layout.modal.import.message_failed'
98 | })
99 |
100 | const upload_anchor = document.getElementById('upload_anchor_of_data')
101 |
102 | upload_anchor.click()
103 | upload_anchor.addEventListener('change', function (){
104 | const _that: any = this
105 | const files = _that.files
106 |
107 | if (!files.length) return
108 |
109 | const data = files[0]
110 | const reader = new FileReader()
111 |
112 | reader.readAsText(data)
113 | reader.onload = (e) => {
114 | const result: any = e.target.result
115 |
116 | try {
117 | const target_data = JSON.parse(result)
118 |
119 | if (!formatCheckOK(target_data)) {
120 | message.error(message_failed)
121 | } else {
122 | dispatch({
123 | type: 'app/importData',
124 | payload: {
125 | data: target_data,
126 | message_success
127 | }
128 | })
129 |
130 | setStateModalVisible(false)
131 | }
132 | } catch (_) {
133 | message.error(message_failed)
134 | }
135 | }
136 | })
137 |
138 | return
139 | }
140 |
141 | if (type === 'export') {
142 | dispatch({ type: 'app/exportData' })
143 |
144 | return
145 | }
146 |
147 | setStateModalType(type)
148 | }
149 |
150 | if (state_modal_type === 'settings') {
151 | return (
152 |
157 |
158 |
159 |
onChangeModalType('analysis')}
162 | >
163 |
164 |
165 | {lang.formatMessage({
166 | id: 'layout.modal.name_analysis'
167 | })}
168 |
169 |
170 |
176 |
onChangeModalType('import')}
179 | >
180 |
181 |
182 | {lang.formatMessage({
183 | id: 'layout.modal.name_import'
184 | })}
185 |
186 |
187 |
188 |
onChangeModalType('export')}
191 | >
192 |
193 |
194 | {lang.formatMessage({
195 | id: 'layout.modal.name_export'
196 | })}
197 |
198 |
199 |
onChangeModalType('clear')}
202 | >
203 |
204 |
205 | {lang.formatMessage({
206 | id: 'layout.modal.name_clear'
207 | })}
208 |
209 |
210 |
211 |
212 |
213 |
214 | {lang.formatMessage({
215 | id: 'layout.modal.name_language'
216 | })}
217 |
218 |
224 |
225 |
226 |
227 | {lang.formatMessage({
228 | id: 'layout.modal.name_theme'
229 | })}
230 |
231 |
241 |
242 |
243 |
244 | {lang.formatMessage({
245 | id: 'layout.modal.name_loadway'
246 | })}
247 |
248 |
258 |
259 |
260 |
261 |
262 | )
263 | }
264 |
265 | if (state_modal_type === 'clear') {
266 | return (
267 | onDeleteGroup(state_group_selected)}
275 | >
276 |
277 |
292 |
293 |
294 | )
295 | }
296 |
297 | if (state_modal_type === 'analysis') {
298 | return (
299 |
307 |
308 | {analysis_data.length > 0 ? (
309 |
310 |
311 |
312 |
313 |
314 |
319 |
324 |
325 |
326 | ) : (
327 |
328 |
329 |
330 | )}
331 |
332 |
333 | )
334 | }
335 | }
336 |
337 | export default memo(Index)
338 |
--------------------------------------------------------------------------------
/src/layouts/index.less:
--------------------------------------------------------------------------------
1 | .container {
2 | width: 680px;
3 | padding: 30px 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | @media screen and (max-width:840px) {
8 | .container {
9 | width: 100%;
10 | padding: 30px 12px;
11 | padding-bottom: 60px;
12 | }
13 | }
14 |
15 | .dark {
16 | color: #ffffff;
17 | }
--------------------------------------------------------------------------------
/src/layouts/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, Fragment } from 'react'
2 | import { connect, useIntl } from 'umi'
3 | import { Helmet } from 'react-helmet'
4 | import Header from './components/Header'
5 | import changeTheme from '@/themes/changeTheme'
6 | import styles from './index.less'
7 |
8 | import * as OfflinePluginRuntime from 'offline-plugin/runtime'
9 | OfflinePluginRuntime.install()
10 |
11 | const Index = (props: any) => {
12 | const { children, theme } = props
13 | const lang = useIntl()
14 |
15 | if (process.env.NODE_ENV !== 'development') {
16 | changeTheme(theme)
17 | }
18 |
19 | return (
20 |
21 |
22 | {lang.formatMessage({ id: 'site.title' })}
23 |
24 |
25 |
33 |
34 | )
35 | }
36 |
37 | export default memo(connect(({ app: { theme } }: any) => ({ theme }))(Index))
38 |
--------------------------------------------------------------------------------
/src/locales/en-US.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'site.title': 'Testool',
3 | 'common.btn_ok': 'OK',
4 | 'common.btn_cancel': 'Cancel',
5 | 'common.btn_delete': 'Delete',
6 | 'common.confirm': 'Confirm',
7 | 'header.search.placeholder': 'Search in QA',
8 | 'layout.modal.title': 'Settings',
9 | 'layout.modal.name_language': 'Language',
10 | 'layout.modal.name_theme': 'Theme',
11 | 'layout.modal.theme_dark': 'dark',
12 | 'layout.modal.theme_light': 'light',
13 | 'layout.modal.name_loadway': 'Loading Way',
14 | 'layout.modal.loadway_scroll': 'scroll',
15 | 'layout.modal.loadway_page': 'page',
16 | 'layout.modal.name_analysis': 'Analysis',
17 | 'layout.modal.name_import': 'Import',
18 | 'layout.modal.name_export': 'Export',
19 | 'layout.modal.name_clear': 'Clear',
20 | 'layout.modal.clear.select.placeholder': 'Choose a group to delete',
21 | 'layout.modal.clear.message_success': 'delete success',
22 | 'layout.modal.clear.message_failed': 'delete failed',
23 | 'layout.modal.import.message_success': 'import success',
24 | 'layout.modal.import.message_failed': 'import failed,please check data format.',
25 | 'index.btn_pass': 'Pass',
26 | 'index.btn_add': 'Add Group',
27 | 'index.header.tooltip.group': 'toggle/add group',
28 | 'index.header.tooltip.filter': 'show the filter',
29 | 'index.header.tooltip.add_qa': 'add QA',
30 | 'index.filter.times.placeholder': 'choose pass times',
31 | 'index.filter.times': 'times',
32 | 'index.filter.rate.placeholder': 'choose rate value',
33 | 'index.filter.below': 'below',
34 | 'index.modal.title.add_group': 'Groups',
35 | 'index.modal.title.add_qa': 'Add QA',
36 | 'index.modal.title.edit_qa': 'Edit QA',
37 | 'index.modal.title.rate_log': 'Rate logs',
38 | 'index.modal.add_group.placeholder': 'toggle/add group',
39 | 'index.modal.add_group.success': 'add group success',
40 | 'index.modal.add_group.failed': 'add group failed',
41 | 'index.modal.add_group.repeat': 'group has exsited',
42 | 'index.modal.add_qa.question.placeholder': 'Please input the question',
43 | 'index.modal.add_qa.answer.placeholder': 'Please input the answer (supported markdown)',
44 | 'index.modal.add_qa.tags.placeholder': 'Please add tags (at least 2)',
45 | 'index.modal.add_qa.tags.count.warn': 'at most add 5 tags',
46 | 'index.modal.add_qa.tags.length.warn': 'tag at most length 12',
47 | 'index.modal.add_qa.success': 'add qa success',
48 | 'index.modal.add_qa.failed': 'add qa failed',
49 | 'index.modal.rate.success': 'rate success',
50 | 'index.modal.rate.failed': 'rate failed',
51 | 'index.modal.edit_qa.success': 'edit qa success',
52 | 'index.modal.edit_qa.failed': 'edit qa failed',
53 | 'index.modal.edit_qa.delete.confirm': 'confirm delete this QA?',
54 | 'index.modal.edit_qa.delete.success': 'delete qa success',
55 | 'index.modal.edit_qa.delete.failed': 'delete qa failed',
56 | 'index.modal.clear_log.confirm': 'confirm clear rate log of this QA?',
57 | 'index.modal.clear_log.success': 'clear qa rate log success',
58 | 'index.modal.clear_log.failed': 'clear qa rate log failed',
59 | 'index.no_more': 'no more'
60 | }
61 |
--------------------------------------------------------------------------------
/src/locales/zh-CN.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'site.title': '复习神器',
3 | 'common.btn_ok': '确定',
4 | 'common.btn_cancel': '取消',
5 | 'common.btn_delete': '删除',
6 | 'common.confirm': '确认',
7 | 'header.search.placeholder': '在QA中搜索',
8 | 'layout.modal.title': '设置',
9 | 'layout.modal.name_language': '语言',
10 | 'layout.modal.name_theme': '主题',
11 | 'layout.modal.theme_dark': '浅色',
12 | 'layout.modal.theme_light': '深色',
13 | 'layout.modal.name_loadway': '加载方式',
14 | 'layout.modal.loadway_scroll': '滚动',
15 | 'layout.modal.loadway_page': '分页',
16 | 'layout.modal.name_analysis': '分析',
17 | 'layout.modal.name_import': '导入',
18 | 'layout.modal.name_export': '导出',
19 | 'layout.modal.name_clear': '清除',
20 | 'layout.modal.clear.select.placeholder': '选择一个分组进行删除',
21 | 'layout.modal.clear.message_success': '删除成功',
22 | 'layout.modal.clear.message_failed': '删除失败',
23 | 'layout.modal.import.message_success': '导入成功',
24 | 'layout.modal.import.message_failed': '导入失败,请检查数据格式',
25 | 'index.btn_pass': '通过',
26 | 'index.btn_add': '添加分组',
27 | 'index.header.tooltip.group': '切换/添加 分组',
28 | 'index.header.tooltip.filter': '展示筛选条件',
29 | 'index.header.tooltip.add_qa': '添加 QA',
30 | 'index.filter.times.placeholder': '选择通过次数',
31 | 'index.filter.times': '次',
32 | 'index.filter.rate.placeholder': '选择评分',
33 | 'index.filter.below': '低于',
34 | 'index.modal.title.add_group': '分组',
35 | 'index.modal.title.add_qa': '添加 QA',
36 | 'index.modal.title.edit_qa': '编辑 QA',
37 | 'index.modal.title.rate_log': '评分记录',
38 | 'index.modal.add_group.placeholder': '切换/添加 分组',
39 | 'index.modal.add_group.success': '分组添加成功',
40 | 'index.modal.add_group.failed': '分组添加失败',
41 | 'index.modal.add_group.repeat': '分组已经存在',
42 | 'index.modal.add_qa.question.placeholder': '请输入问题',
43 | 'index.modal.add_qa.answer.placeholder': '请输入答案(支持markdown)',
44 | 'index.modal.add_qa.tags.placeholder': '请添加标签(至少两个)',
45 | 'index.modal.add_qa.tags.count.warn': '最多添加5个标签',
46 | 'index.modal.add_qa.tags.length.warn': '标签不超过12个字符',
47 | 'index.modal.add_qa.success': 'QA添加成功',
48 | 'index.modal.add_qa.failed': 'QA添加失败',
49 | 'index.modal.rate.success': '评分成功',
50 | 'index.modal.rate.failed': '评分失败',
51 | 'index.modal.edit_qa.success': '修改成功',
52 | 'index.modal.edit_qa.failed': '修改失败',
53 | 'index.modal.edit_qa.delete.confirm': '确认删除该条QA?',
54 | 'index.modal.edit_qa.delete.success': '删除成功',
55 | 'index.modal.edit_qa.delete.failed': '删除失败',
56 | 'index.modal.clear_log.confirm': '确认清除这条QA的评分记录?',
57 | 'index.modal.clear_log.success': '清除评分记录成功',
58 | 'index.modal.clear_log.failed': '清除评分记录失败',
59 | 'index.no_more': '没有更多了'
60 | }
61 |
--------------------------------------------------------------------------------
/src/models/app.ts:
--------------------------------------------------------------------------------
1 | import { message } from 'antd'
2 | import store from 'store'
3 | import {
4 | Service_addTableGroups,
5 | Service_getAllGroups,
6 | Service_deleteGroup,
7 | Service_getAnalysisData,
8 | Service_importData,
9 | Service_exportData
10 | } from '@/services/app'
11 |
12 | export default {
13 | namespace: 'app',
14 |
15 | state: {
16 | groups: [],
17 | current_group: '',
18 | theme: store.get('theme') ? store.get('theme') : 'light',
19 | loadway: store.get('loadway'),
20 | analysis_data: []
21 | },
22 |
23 | subscriptions: {
24 | setup ({ dispatch, history }) {
25 | history.listen((location: any) => {
26 | dispatch({
27 | type: 'query',
28 | payload: {
29 | ...location.query
30 | }
31 | })
32 | })
33 | }
34 | },
35 |
36 | effects: {
37 | *query ({ payload }, { call, put, select }) {
38 | const logs = yield call(Service_addTableGroups)
39 |
40 | if (!logs) return
41 |
42 | const groups = yield call(Service_getAllGroups)
43 |
44 | if (groups.length === 0) {
45 | store.set('current_group', null)
46 | }
47 |
48 | const c_group = store.get('current_group')
49 |
50 | yield put({
51 | type: 'updateState',
52 | payload: {
53 | groups,
54 | current_group:
55 | groups.indexOf(c_group) === -1 ||
56 | c_group === null ||
57 | c_group === undefined
58 | ? groups[0]
59 | : c_group
60 | }
61 | })
62 |
63 | const { current_group } = yield select(({ app }) => app)
64 |
65 | if (!current_group) return
66 |
67 | yield put({
68 | type: 'index/query',
69 | payload: { current_group, ...payload }
70 | })
71 | },
72 | *deleteGroup ({ payload }, { call, put }) {
73 | const { group, message_success, message_failed } = payload
74 | const res = yield call(Service_deleteGroup, group)
75 |
76 | if (res) {
77 | message.success(message_success)
78 | } else {
79 | message.error(message_failed)
80 | }
81 |
82 | yield put({ type: 'query' })
83 | },
84 | *getAnalysisData ({}, { call, put }) {
85 | const res = yield call(Service_getAnalysisData)
86 |
87 | yield put({ type: 'updateState', payload: { analysis_data: res } })
88 | },
89 | *importData ({ payload }, { call, put }) {
90 | const { data, message_success } = payload
91 |
92 | const res = yield call(Service_importData, data)
93 |
94 | if (res === true) {
95 | message.success(message_success)
96 |
97 | yield put({ type: 'query' })
98 | } else {
99 | message.error(res)
100 | }
101 | },
102 | *exportData ({}, { call }) {
103 | const res = yield call(Service_exportData)
104 |
105 | const data = `data:text/json;charset=utf-8,${encodeURIComponent(
106 | JSON.stringify(res)
107 | )}`
108 | const download_anchor = document.getElementById('download_anchor_of_data')
109 |
110 | download_anchor.setAttribute('href', data)
111 | download_anchor.setAttribute('download', 'testool_data.json')
112 |
113 | download_anchor.click()
114 | }
115 | },
116 |
117 | reducers: {
118 | updateState (state, { payload }) {
119 | return {
120 | ...state,
121 | ...payload
122 | }
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/models/index.ts:
--------------------------------------------------------------------------------
1 | import modelExtend from 'dva-model-extend'
2 | import pageModel from '@/utils/model'
3 | import { message } from 'antd'
4 | import cloneDeep from 'lodash/cloneDeep'
5 | import {
6 | Service_addGroup,
7 | Service_addQa,
8 | Service_delQa,
9 | Service_putQa,
10 | Service_query,
11 | Service_getQas,
12 | Service_getTotal,
13 | Service_rate,
14 | Service_clearRateLog
15 | } from '@/services/index'
16 |
17 | export default modelExtend(pageModel, {
18 | namespace: 'index',
19 |
20 | state: {
21 | modal_visible: false,
22 | modal_type: '',
23 | filter_visible: false,
24 | qas: [],
25 | total: 0,
26 | no_more: false,
27 | current_item: {},
28 | current_id: 0,
29 | current_index: 0,
30 | querying: false
31 | },
32 |
33 | subscriptions: {},
34 |
35 | effects: {
36 | *query ({ payload }, { call, put }) {
37 | const { current_group, page } = payload
38 |
39 | if (!current_group) return
40 |
41 | const qas = yield call(Service_getQas, current_group, page)
42 | const total = yield call(Service_getTotal, current_group)
43 |
44 | yield put({
45 | type: 'updateState',
46 | payload: { qas, total }
47 | })
48 | },
49 | *loadMore ({ payload }, { call, put, select }) {
50 | const { qas } = yield select(({ index }) => index)
51 | const { current_group, page, page_size } = payload
52 |
53 | const res = yield call(Service_getQas, current_group, page, page_size)
54 |
55 | if (res.length === 0) {
56 | yield put({
57 | type: 'updateState',
58 | payload: { no_more: true }
59 | })
60 |
61 | return
62 | }
63 |
64 | yield put({
65 | type: 'updateState',
66 | payload: { qas: qas.concat(res) }
67 | })
68 | },
69 | *addGroup ({ payload }, { call, put }) {
70 | const { name, message_success, message_failed } = payload
71 |
72 | const res = yield call(Service_addGroup, name)
73 |
74 | if (res) {
75 | message.success(message_success)
76 | } else {
77 | message.error(message_failed)
78 | }
79 |
80 | yield put({ type: 'app/query' })
81 |
82 | yield put({
83 | type: 'updateState',
84 | payload: { modal_visible: false }
85 | })
86 | },
87 | *addQa ({ payload }, { call, put }) {
88 | const { current_group, params, message_success, message_failed } = payload
89 |
90 | const res = yield call(Service_addQa, current_group, params)
91 |
92 | if (res) {
93 | message.success(message_success)
94 | } else {
95 | message.error(message_failed)
96 | }
97 |
98 | yield put({ type: 'app/query' })
99 |
100 | yield put({
101 | type: 'updateState',
102 | payload: { modal_visible: false }
103 | })
104 | },
105 | *delQa ({ payload }, { call, put, select }) {
106 | const { qas, current_id, current_index } = yield select(({ index }) => index)
107 | const { current_group, message_success, message_failed } = payload
108 |
109 | const res = yield call(Service_delQa, current_group, current_id)
110 |
111 | if (res) {
112 | message.success(message_success)
113 | } else {
114 | message.error(message_failed)
115 | }
116 |
117 | const _qas = cloneDeep(qas)
118 |
119 | _qas.splice(current_index, 1)
120 |
121 | yield put({
122 | type: 'updateState',
123 | payload: {
124 | modal_visible: false,
125 | qas: _qas
126 | }
127 | })
128 | },
129 | *putQa ({ payload }, { call, put, select }) {
130 | const { qas, current_id, current_index } = yield select(({ index }) => index)
131 | const { current_group, params, message_success, message_failed } = payload
132 |
133 | const res = yield call(Service_putQa, current_group, current_id, params)
134 |
135 | if (res) {
136 | message.success(message_success)
137 | } else {
138 | message.error(message_failed)
139 | }
140 |
141 | qas[current_index] = Object.assign(qas[current_index], params)
142 |
143 | yield put({
144 | type: 'updateState',
145 | payload: {
146 | modal_visible: false,
147 | qas: qas
148 | }
149 | })
150 | },
151 | *queryQa ({ payload }, { call, put }) {
152 | const { current_group, params: { query, times, rate } } = payload
153 |
154 | const qas = yield call(Service_query, current_group, { query, times, rate })
155 |
156 | yield put({
157 | type: 'updateState',
158 | payload: {
159 | modal_visible: false,
160 | qas: qas
161 | }
162 | })
163 | },
164 | *rate ({ payload }, { call, put, select }) {
165 | const { qas } = yield select(({ index }) => index)
166 | const {
167 | current_group,
168 | params: { id, rate, index },
169 | message_success,
170 | message_failed
171 | } = payload
172 | const params = { id, rate }
173 |
174 | const res = yield call(Service_rate, current_group, params)
175 |
176 | if (res) {
177 | message.success(message_success)
178 | } else {
179 | message.error(message_failed)
180 | }
181 |
182 | qas[index].rates.push({ rate, create_at: new Date().valueOf() })
183 |
184 | yield put({
185 | type: 'updateState',
186 | payload: { qas: qas }
187 | })
188 | },
189 | *clearRateLog ({ payload }, { call, put, select }) {
190 | const { qas } = yield select(({ index }) => index)
191 | const { current_group, id, index, message_success, message_failed } = payload
192 |
193 | const res = yield call(Service_clearRateLog, current_group, id)
194 |
195 | if (res) {
196 | message.success(message_success)
197 | } else {
198 | message.error(message_failed)
199 | }
200 |
201 | qas[index].rates = []
202 |
203 | yield put({
204 | type: 'updateState',
205 | payload: { qas: qas }
206 | })
207 | }
208 | }
209 | })
210 |
--------------------------------------------------------------------------------
/src/pages/index/components/Filter/index.less:
--------------------------------------------------------------------------------
1 | @keyframes show {
2 | from {
3 | max-height: 0;
4 | }
5 |
6 | to {
7 | max-height: 10vh;
8 | }
9 | }
10 |
11 | ._local {
12 | animation: show 0.3s ease;
13 | margin-bottom: 24px;
14 |
15 | :global {
16 | .ant-form-item {
17 | margin-bottom: 0;
18 | }
19 |
20 | .select_filter {
21 | width: 160px;
22 | }
23 |
24 | @media screen and (max-width:420px) {
25 | .select_filter {
26 | width: 30vw;
27 | }
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/pages/index/components/Filter/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { useIntl } from 'umi'
3 | import { Select, Button, Form } from 'antd'
4 | import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
5 | import styles from './index.less'
6 |
7 | const { Option } = Select
8 | const { useForm, Item } = Form
9 |
10 | interface IParams {
11 | times?: number
12 | rate?: number
13 | }
14 |
15 | interface IProps {
16 | queryQa: (params: IParams) => void
17 | reset: () => void
18 | }
19 |
20 | const Index = (props: IProps) => {
21 | const { queryQa, reset } = props
22 | const [ form ] = useForm()
23 | const { resetFields, submit } = form
24 | const lang = useIntl()
25 |
26 | const onFinish = ({ times, rate }) => {
27 | if (!times && !rate) return
28 |
29 | queryQa({ times, rate })
30 | }
31 |
32 | return (
33 |
102 | )
103 | }
104 |
105 | export default memo(Index)
106 |
--------------------------------------------------------------------------------
/src/pages/index/components/Header/index.less:
--------------------------------------------------------------------------------
1 | ._local {
2 | margin-bottom: 24px;
3 |
4 | :global {
5 | .name {
6 | font-size: 24px;
7 | line-height: 1;
8 | font-weight: bold;
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/pages/index/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { useIntl } from 'umi'
3 | import { Button, Tooltip } from 'antd'
4 | import { FileSyncOutlined, FilterOutlined, PlusOutlined } from '@ant-design/icons'
5 | import styles from './index.less'
6 |
7 | interface IProps {
8 | name: string
9 | onAddGroup: () => void
10 | onFilter: () => void
11 | onAddQa: () => void
12 | }
13 |
14 | const Index = (props: IProps) => {
15 | const { name, onAddGroup, onFilter, onAddQa } = props
16 | const lang = useIntl()
17 |
18 | return (
19 |
20 |
{name}
21 |
22 |
23 | }
26 | onClick={onAddGroup}
27 | />
28 |
29 |
30 | }
33 | onClick={onFilter}
34 | />
35 |
36 |
37 | } onClick={onAddQa} />
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | export default memo(Index)
45 |
--------------------------------------------------------------------------------
/src/pages/index/components/Modal/index.less:
--------------------------------------------------------------------------------
1 | ._local {
2 | &.dark {
3 | :global {
4 | .answer {
5 | .ant-input {
6 | background-color: transparent;
7 | }
8 | }
9 |
10 | .ant-tag {
11 | background-color: #333333;
12 | }
13 | }
14 | }
15 |
16 | :global {
17 | .answer {
18 | min-height: 50vh;
19 | max-height: 80vh;
20 | background-color: transparent;
21 |
22 | @media screen and (max-width:840px) {
23 | resize: none;
24 | }
25 |
26 | .ant-input {
27 | background-color: white;
28 | }
29 | }
30 |
31 | .ant-tag {
32 | background-color: white;
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/pages/index/components/Modal/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useState, useEffect } from 'react'
2 | import { useIntl } from 'umi'
3 | import { Modal, Select, Input, Button, Form, Tag, message } from 'antd'
4 | import { CheckOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons'
5 | import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip } from 'recharts'
6 | import { IQas } from '@/db/models/Qas'
7 | import styles from './index.less'
8 |
9 | const { Option } = Select
10 | const { TextArea } = Input
11 | const { confirm } = Modal
12 | const { useForm, Item } = Form
13 |
14 | interface IProps {
15 | theme: string
16 | title: string
17 | groups: Array
18 | current_group: string
19 | current_item: IQas
20 | visible: boolean
21 | modal_type: string
22 | onCancel: () => void
23 | onAddGroup: (name: string) => void
24 | onChangeCurrentGroup: (v: string) => void
25 | onAddQa: (params: IQas) => void
26 | onDelQa: () => void
27 | onPutQa: (params: IQas) => void
28 | }
29 |
30 | const Index = (props: IProps) => {
31 | const {
32 | theme,
33 | title,
34 | groups,
35 | current_group,
36 | current_item,
37 | visible,
38 | modal_type,
39 | onCancel,
40 | onAddGroup,
41 | onChangeCurrentGroup,
42 | onAddQa,
43 | onDelQa,
44 | onPutQa
45 | } = props
46 | const lang = useIntl()
47 | const [ form ] = useForm()
48 | const { validateFields, setFieldsValue } = form
49 |
50 | const [ state_tags, setStateTags ] = useState([])
51 | const [ state_tags_input, setStateTagsInput ] = useState('')
52 | const [ state_group_input, setStateGroupInput ] = useState('')
53 | const [ state_tags_input_visible, setStateTagsInputVisible ] = useState(false)
54 |
55 | useEffect(
56 | () => {
57 | if (modal_type === 'rate_log') return
58 |
59 | if (current_item.tags) {
60 | setStateTags(current_item.tags)
61 |
62 | setFieldsValue({
63 | question: current_item.question,
64 | answer: current_item.answer
65 | })
66 | }
67 | },
68 | [ current_item ]
69 | )
70 |
71 | useEffect(() => {
72 | if (modal_type) {
73 | setStateTags(current_item.tags)
74 | }
75 | }, [])
76 |
77 | const delTag = (tag: string) => {
78 | setStateTags(state_tags.filter((item) => item !== tag))
79 | }
80 |
81 | const onComfirmAddTag = () => {
82 | setStateTagsInput('')
83 | setStateTagsInputVisible(false)
84 |
85 | if (state_tags.length > 4) {
86 | message.warn(lang.formatMessage({ id: 'index.modal.add_qa.tags.count.warn' }))
87 |
88 | return
89 | }
90 |
91 | if (state_tags_input.length > 12) {
92 | message.warn(lang.formatMessage({ id: 'index.modal.add_qa.tags.length.warn' }))
93 |
94 | return
95 | }
96 |
97 | if (state_tags_input && state_tags.indexOf(state_tags_input) === -1) {
98 | setStateTags([ ...state_tags, state_tags_input ])
99 | }
100 | }
101 |
102 | const props_modal = {
103 | title,
104 | visible,
105 | centered: true,
106 | maskClosable: true,
107 | destroyOnClose: true,
108 | onCancel () {
109 | setStateTags([])
110 |
111 | if (modal_type === 'edit_qa') {
112 | setFieldsValue({
113 | question: null,
114 | answer: null
115 | })
116 | }
117 |
118 | onCancel()
119 | }
120 | }
121 |
122 | const onOk = async () => {
123 | const { question, answer } = await validateFields()
124 |
125 | if (modal_type === 'add_qa') {
126 | onAddQa({
127 | question,
128 | answer,
129 | tags: state_tags
130 | })
131 | }
132 |
133 | if (modal_type === 'edit_qa') {
134 | onPutQa({
135 | question,
136 | answer,
137 | tags: state_tags
138 | })
139 | }
140 | }
141 |
142 | const onDelete = () => {
143 | confirm({
144 | centered: true,
145 | title: lang.formatMessage({ id: 'common.confirm' }),
146 | content: lang.formatMessage({ id: 'index.modal.edit_qa.delete.confirm' }),
147 | onOk () {
148 | onDelQa()
149 | }
150 | })
151 | }
152 |
153 | let _footer: object = {}
154 |
155 | if (modal_type === 'edit_qa') {
156 | _footer = {
157 | footer: (
158 |
159 |
} onClick={onDelete}>
160 | {lang.formatMessage({ id: 'common.btn_delete' })}
161 |
162 |
163 |
166 |
169 |
170 |
171 | )
172 | }
173 | }
174 | if (modal_type === 'add_group') {
175 | return (
176 |
182 |
217 |
218 | )
219 | }
220 |
221 | if (modal_type === 'rate_log') {
222 | return (
223 |
228 |
229 |
230 | new Date(item.create_at).toISOString()}
233 | />
234 |
235 |
236 |
237 |
238 |
239 |
240 | )
241 | }
242 |
243 | if (modal_type === 'add_qa' || modal_type === 'edit_qa') {
244 | return (
245 |
252 |
321 |
322 | )
323 | }
324 | }
325 |
326 | export default memo(Index)
327 |
--------------------------------------------------------------------------------
/src/pages/index/components/Qas/index.less:
--------------------------------------------------------------------------------
1 | ._local {
2 | :global {
3 | .empty_wrap {
4 | padding: 120px 0;
5 | }
6 |
7 | .ReactVirtualized__List {
8 | outline: none;
9 | overflow: visible !important;
10 |
11 | .ReactVirtualized__Grid__innerScrollContainer {
12 | overflow: visible !important;
13 | }
14 | }
15 |
16 | .loading_more {
17 | .loading {
18 | color: var(--primary-color);
19 | }
20 | }
21 | }
22 | }
23 |
24 | @keyframes show {
25 | from {
26 | transform: scaleY(0);
27 | }
28 |
29 | to {
30 | transform: scaleY(1);
31 | }
32 | }
33 |
34 | .qa_item {
35 | box-shadow: var(--light_box_shadow_card);
36 | border-radius: var(--radius_normal);
37 | background-color: white;
38 | margin-bottom: 16px;
39 | overflow: hidden;
40 |
41 | &.qa_item_dark {
42 | background-color: var(--dark_color_block);
43 |
44 | :global {
45 | .question {
46 | .q_head {
47 | border-bottom: 1px solid #2f2f2f;
48 | }
49 |
50 | .q_foot {
51 | .ant-tag {
52 | background-color: #333;
53 | }
54 | }
55 | }
56 | }
57 | }
58 |
59 | @media screen and (max-width:420px) {
60 | box-shadow: none;
61 | }
62 |
63 | :global {
64 | .icon_wrap {
65 | width: 30px;
66 | height: 30px;
67 | border-radius: var(--radius_normal);
68 |
69 | &:hover {
70 | background-color: var(--light_bg_hover);
71 | }
72 |
73 | &.disabled {
74 | opacity: 0.1;
75 | }
76 | }
77 |
78 | .icon_edit_wrap,
79 | .icon_toggle_wrap {
80 | margin-right: -10px;
81 | }
82 |
83 | .question {
84 | .q_item {
85 | padding: 16px 20px;
86 | }
87 |
88 | .q_head {
89 | padding: 10px 20px;
90 | border-bottom: 1px solid whitesmoke;
91 |
92 | .left {
93 |
94 | .pass_times {
95 | width: 40px;
96 | }
97 |
98 | .avarage_rate_value {
99 | width: 40px;
100 | }
101 | }
102 |
103 | }
104 |
105 | .q_foot {
106 | padding-top: 0;
107 | padding-bottom: 10px;
108 |
109 | }
110 | }
111 | }
112 |
113 | .answer {
114 | .content {
115 | transform-origin: top;
116 | animation: show 0.3s ease;
117 | padding: 16px 20px;
118 | overflow-y: scroll;
119 | border-top: 1px solid whitesmoke;
120 |
121 | &.dark {
122 | border-top: 1px solid #2f2f2f;
123 | }
124 | }
125 |
126 | &.dark {
127 | :global {
128 | .a_foot {
129 | background-color: rgba(0, 0, 0, 0.1);
130 | }
131 | }
132 | }
133 |
134 | :global {
135 | .a_foot {
136 | padding: 16px 20px;
137 | background-color: whitesmoke;
138 |
139 | .ant-rate {
140 | color: var(--primary-color);
141 | }
142 | }
143 | }
144 | }
145 | }
146 |
147 | .markdown {
148 | color: #24292e;
149 |
150 | &.dark {
151 | color: white;
152 |
153 | :global {
154 | table {
155 |
156 | th,
157 | td {
158 | background-color: #333;
159 | }
160 |
161 | th {
162 | background-color: #111;
163 | }
164 | }
165 |
166 | h1,
167 | h2,
168 | h3,
169 | h4,
170 | h5,
171 | h6 {
172 | color: white;
173 | }
174 |
175 | h1,
176 | h2 {
177 | padding-bottom: .3em;
178 | border-bottom: 1px solid rgba(255, 255, 255, 0.05);
179 | }
180 |
181 | pre {
182 | background-color: #333;
183 | }
184 |
185 | p {
186 |
187 | code,
188 | tt {
189 | background-color: #333;
190 | }
191 | }
192 | }
193 | }
194 |
195 | :global {
196 | table {
197 | width: 100%;
198 | border-collapse: separate;
199 | border-spacing: 2px;
200 |
201 | th,
202 | td {
203 | padding: 6px 13px;
204 | background-color: whitesmoke;
205 | }
206 |
207 | th {
208 | background-color: #ddd;
209 | white-space: nowrap;
210 | }
211 | }
212 |
213 | h1,
214 | h2,
215 | h3,
216 | h4,
217 | h5,
218 | h6 {
219 | margin-top: 24px;
220 | margin-bottom: 16px;
221 | font-weight: 600;
222 | line-height: 1.25;
223 | }
224 |
225 | h1,
226 | h2 {
227 | padding-bottom: .3em;
228 | border-bottom: 1px solid #eaecef;
229 | }
230 |
231 | h1 {
232 | font-size: 2em;
233 | }
234 |
235 | ol,
236 | ul {
237 | padding-left: 2em;
238 | margin-top: 10px;
239 | }
240 |
241 | pre {
242 | padding: 16px;
243 | overflow: auto;
244 | font-size: 85%;
245 | line-height: 1.45;
246 | background-color: #f6f8fa;
247 | border-radius: 3px;
248 | }
249 |
250 | p {
251 | margin-top: 10px;
252 | margin-bottom: 16px;
253 | line-height: 1.8;
254 |
255 | img {
256 | max-width: 100%;
257 | }
258 |
259 | code,
260 | tt {
261 | padding: .2em .4em;
262 | background-color: rgba(27, 31, 35, .05);
263 | border-radius: 3px;
264 | }
265 | }
266 | }
267 | }
--------------------------------------------------------------------------------
/src/pages/index/components/Qas/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useState, useEffect } from 'react'
2 | import { useIntl } from 'umi'
3 | import { Tag, Rate, Button, Empty, Pagination, Modal } from 'antd'
4 | import {
5 | CheckOutlined,
6 | StarOutlined,
7 | EditOutlined,
8 | UpCircleOutlined,
9 | DownCircleOutlined,
10 | CheckCircleOutlined,
11 | PlusOutlined,
12 | LineChartOutlined,
13 | RedoOutlined
14 | } from '@ant-design/icons'
15 | import { List, AutoSizer, WindowScroller, CellMeasurer, CellMeasurerCache } from 'react-virtualized'
16 | import { useBottomScrollListener } from 'react-bottom-scroll-listener'
17 | import ReactMarkDown from 'react-markdown'
18 | import CodeBlock from '@/components/CodeBlock'
19 | import { IQas } from '@/db/models/Qas'
20 | import { IORate } from '../../index'
21 | import styles from './index.less'
22 |
23 | const { confirm } = Modal
24 |
25 | interface IQa extends IQas {
26 | theme: string
27 | index: number
28 | style?: any
29 | ref?: any
30 | measure?: () => any
31 | rate: (params: IORate) => void
32 | onEdit: (id: number, index: number) => void
33 | onChart: (id: number, index: number) => void
34 | onClear: (id: number, index: number) => void
35 | }
36 |
37 | const Qa = (props: IQa) => {
38 | const {
39 | theme,
40 | id,
41 | index,
42 | question,
43 | answer,
44 | tags,
45 | rates,
46 | style,
47 | ref,
48 | measure,
49 | rate,
50 | onEdit,
51 | onChart,
52 | onClear
53 | } = props
54 | const [ state_answer_visible, setStateAnswerVisible ] = useState(false)
55 | const [ state_rate, setStateRate ] = useState(5)
56 | const lang = useIntl()
57 |
58 | useEffect(
59 | () => {
60 | if (!measure) return
61 |
62 | measure()
63 | },
64 | [ state_answer_visible ]
65 | )
66 |
67 | let avarage_rate: number = 0
68 |
69 | rates.map((item) => {
70 | avarage_rate = avarage_rate + item.rate
71 | })
72 |
73 | avarage_rate = avarage_rate / rates.length
74 |
75 | return (
76 |
77 |
84 |
85 |
86 |
87 |
88 |
89 | {rates.length}
90 |
91 |
92 |
93 |
94 | {avarage_rate ? avarage_rate.toFixed(2) : 0}
95 |
96 |
97 |
98 |
99 |
{
103 | confirm({
104 | centered: true,
105 | title: lang.formatMessage({
106 | id: 'common.confirm'
107 | }),
108 | content: lang.formatMessage({
109 | id:
110 | 'index.modal.clear_log.confirm'
111 | }),
112 | onOk () {
113 | onClear(id, index)
114 | }
115 | })
116 | }}
117 | >
118 |
119 |
120 |
onChart(id, index)}
124 | >
125 |
126 |
127 |
onEdit(id, index)}
130 | >
131 |
132 |
133 |
134 |
135 |
136 | {question}
137 |
138 |
139 |
140 | {tags.map((item, index) => (
141 |
142 | {item}
143 |
144 | ))}
145 |
146 | {state_answer_visible ? (
147 |
{
150 | setStateAnswerVisible(false)
151 | }}
152 | >
153 |
154 |
155 | ) : (
156 |
{
159 | setStateAnswerVisible(true)
160 | }}
161 | >
162 |
163 |
164 | )}
165 |
166 |
167 | {state_answer_visible && (
168 |
175 |
182 |
190 |
191 |
192 |
193 | {
197 | setStateRate(v)
198 | }}
199 | />
200 |
201 |
202 | }
205 | onClick={() => {
206 | setStateAnswerVisible(false)
207 | }}
208 | />
209 | }
212 | onClick={() => {
213 | rate({ id, rate: state_rate, index })
214 | }}
215 | >
216 | {lang.formatMessage({ id: 'index.btn_pass' })}
217 |
218 |
219 |
220 |
221 | )}
222 |
223 |
224 | )
225 | }
226 |
227 | interface IProps {
228 | loading: boolean
229 | loadway: string
230 | querying: boolean
231 | theme: string
232 | no_more: boolean
233 | qas: Array
234 | total: number
235 | groups: Array
236 | current_group: string
237 | dispatch: (params: any) => void
238 | onAddGroup: () => void
239 | rate: (params: IORate) => void
240 | }
241 |
242 | const Index = (props: IProps) => {
243 | const {
244 | dispatch,
245 | querying,
246 | loading,
247 | loadway,
248 | theme,
249 | no_more,
250 | qas,
251 | total,
252 | groups,
253 | onAddGroup,
254 | current_group,
255 | rate
256 | } = props
257 | const [ state_page, setStatePage ] = useState(1)
258 | const lang = useIntl()
259 | const cache = new CellMeasurerCache()
260 |
261 | useEffect(
262 | () => {
263 | window.scrollTo({ top: 0 })
264 |
265 | setStatePage(1)
266 |
267 | dispatch({
268 | type: 'index/updateState',
269 | payload: { no_more: false, qas: [] }
270 | })
271 |
272 | dispatch({
273 | type: 'index/query',
274 | payload: { current_group, page: 1 }
275 | })
276 | },
277 | [ loadway ]
278 | )
279 |
280 | useEffect(
281 | () => {
282 | if (!no_more) setStatePage(1)
283 | },
284 | [ no_more ]
285 | )
286 |
287 | const onEdit = (id: number, index: number) => {
288 | dispatch({
289 | type: 'index/updateState',
290 | payload: {
291 | modal_visible: true,
292 | modal_type: 'edit_qa',
293 | current_item: qas[index],
294 | current_id: id,
295 | current_index: index
296 | }
297 | })
298 | }
299 |
300 | const onChart = (id: number, index: number) => {
301 | dispatch({
302 | type: 'index/updateState',
303 | payload: {
304 | modal_visible: true,
305 | modal_type: 'rate_log',
306 | current_item: qas[index],
307 | current_id: id,
308 | current_index: index
309 | }
310 | })
311 | }
312 |
313 | const onClear = (id: number, index: number) => {
314 | dispatch({
315 | type: 'index/clearRateLog',
316 | payload: {
317 | current_group,
318 | id,
319 | index,
320 | message_success: lang.formatMessage({
321 | id: 'index.modal.clear_log.success'
322 | }),
323 | message_failed: lang.formatMessage({
324 | id: 'index.modal.clear_log.failed'
325 | })
326 | }
327 | })
328 | }
329 |
330 | const rowRenderer = ({ index, parent, key, style }) => {
331 | const item = qas[index]
332 |
333 | return (
334 |
341 | {({ registerChild, measure }) => (
342 |
354 | )}
355 |
356 | )
357 | }
358 |
359 | const onBottom = () => {
360 | if (loadway === 'page' || no_more || loading) return
361 |
362 | dispatch({
363 | type: 'index/loadMore',
364 | payload: {
365 | current_group,
366 | page: state_page + 1
367 | }
368 | })
369 |
370 | setStatePage(state_page + 1)
371 | }
372 |
373 | useBottomScrollListener(onBottom, 1000, 0)
374 |
375 | const onChangePagination = (v: number) => {
376 | dispatch({
377 | type: 'index/query',
378 | payload: { current_group, page: v }
379 | })
380 |
381 | window.scrollTo({ top: 0 })
382 | }
383 |
384 | return (
385 |
386 | {groups.length > 0 ? (
387 |
388 | {loadway === 'page' ? (
389 |
390 | {qas.map((item, index) => (
391 |
401 | ))}
402 |
403 | ) : (
404 |
405 | {({ height, isScrolling, onChildScroll, scrollTop }) => (
406 |
407 | {({ width }) => (
408 |
420 | )}
421 |
422 | )}
423 |
424 | )}
425 | {loadway === 'page' &&
426 | total > 10 &&
427 | !querying && (
428 |
435 | )}
436 | {loadway !== 'page' &&
437 | no_more &&
438 | !querying && (
439 |
440 |
441 | {lang.formatMessage({ id: 'index.no_more' })}
442 |
443 |
444 | )}
445 |
446 | ) : (
447 |
448 |
449 | }
453 | onClick={onAddGroup}
454 | >
455 | {lang.formatMessage({ id: 'index.btn_add' })}
456 |
457 |
458 | )}
459 |
460 | )
461 | }
462 |
463 | export default memo(Index)
464 |
--------------------------------------------------------------------------------
/src/pages/index/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { connect, useIntl } from 'umi'
3 | import { message } from 'antd'
4 | import store from 'store'
5 | import Modal from './components/Modal'
6 | import Header from './components/Header'
7 | import Filter from './components/Filter'
8 | import Qas from './components/Qas'
9 | import { IQas, IRate } from '@/db/models/Qas'
10 |
11 | export interface IORate extends IRate {
12 | index: number
13 | }
14 |
15 | const Index = (props: any) => {
16 | const {
17 | loading,
18 | dispatch,
19 | app: { groups, current_group, theme, loadway },
20 | index: {
21 | modal_visible,
22 | modal_type,
23 | filter_visible,
24 | qas,
25 | total,
26 | no_more,
27 | current_item,
28 | querying
29 | }
30 | } = props
31 | const lang = useIntl()
32 |
33 | const enum_modal_title = {
34 | add_group: lang.formatMessage({ id: 'index.modal.title.add_group' }),
35 | add_qa: lang.formatMessage({ id: 'index.modal.title.add_qa' }),
36 | edit_qa: lang.formatMessage({ id: 'index.modal.title.edit_qa' }),
37 | rate_log: lang.formatMessage({ id: 'index.modal.title.rate_log' })
38 | }
39 |
40 | const props_modal = {
41 | theme,
42 | groups,
43 | current_group,
44 | current_item,
45 | modal_type,
46 | visible: modal_visible,
47 | title: enum_modal_title[modal_type],
48 | onCancel () {
49 | dispatch({
50 | type: 'index/updateState',
51 | payload: {
52 | modal_visible: false,
53 | current_item: {}
54 | }
55 | })
56 | },
57 | onAddGroup (name: string) {
58 | if (groups.indexOf(name) === -1) {
59 | dispatch({
60 | type: 'index/addGroup',
61 | payload: {
62 | name,
63 | message_success: lang.formatMessage({
64 | id: 'index.modal.add_group.success'
65 | }),
66 | message_failed: lang.formatMessage({
67 | id: 'index.modal.add_group.failed'
68 | })
69 | }
70 | })
71 | } else {
72 | message.warn(
73 | lang.formatMessage({
74 | id: 'index.modal.add_group.repeat'
75 | })
76 | )
77 | }
78 | },
79 | onChangeCurrentGroup (v: string) {
80 | dispatch({
81 | type: 'index/updateState',
82 | payload: { modal_visible: false }
83 | })
84 |
85 | dispatch({
86 | type: 'app/updateState',
87 | payload: { current_group: v }
88 | })
89 |
90 | dispatch({
91 | type: 'index/query',
92 | payload: { current_group: v }
93 | })
94 |
95 | store.set('current_group', v)
96 | },
97 | onAddQa (params: IQas) {
98 | dispatch({
99 | type: 'index/addQa',
100 | payload: {
101 | current_group,
102 | params,
103 | message_success: lang.formatMessage({
104 | id: 'index.modal.add_qa.success'
105 | }),
106 | message_failed: lang.formatMessage({
107 | id: 'index.modal.add_qa.failed'
108 | })
109 | }
110 | })
111 | },
112 | onDelQa () {
113 | dispatch({
114 | type: 'index/delQa',
115 | payload: {
116 | current_group,
117 | message_success: lang.formatMessage({
118 | id: 'index.modal.edit_qa.delete.success'
119 | }),
120 | message_failed: lang.formatMessage({
121 | id: 'index.modal.edit_qa.delete.failed'
122 | })
123 | }
124 | })
125 | },
126 | onPutQa (params: IQas) {
127 | dispatch({
128 | type: 'index/putQa',
129 | payload: {
130 | current_group,
131 | params,
132 | message_success: lang.formatMessage({
133 | id: 'index.modal.edit_qa.success'
134 | }),
135 | message_failed: lang.formatMessage({
136 | id: 'index.modal.edit_qa.failed'
137 | })
138 | }
139 | })
140 | }
141 | }
142 |
143 | const props_header = {
144 | name: current_group,
145 | onAddGroup () {
146 | dispatch({
147 | type: 'index/updateState',
148 | payload: {
149 | modal_visible: true,
150 | modal_type: 'add_group'
151 | }
152 | })
153 | },
154 | onFilter () {
155 | dispatch({
156 | type: 'index/updateState',
157 | payload: { filter_visible: !filter_visible }
158 | })
159 | },
160 | onAddQa () {
161 | dispatch({
162 | type: 'index/updateState',
163 | payload: {
164 | modal_visible: true,
165 | modal_type: 'add_qa'
166 | }
167 | })
168 | }
169 | }
170 |
171 | const props_filter = {
172 | queryQa ({ times, rate }) {
173 | dispatch({
174 | type: 'index/queryQa',
175 | payload: {
176 | current_group,
177 | params: {
178 | times,
179 | rate
180 | }
181 | }
182 | })
183 |
184 | dispatch({
185 | type: 'index/updateState',
186 | payload: { querying: true }
187 | })
188 | },
189 | reset () {
190 | dispatch({
191 | type: 'index/query',
192 | payload: {
193 | current_group
194 | }
195 | })
196 |
197 | dispatch({
198 | type: 'index/updateState',
199 | payload: {
200 | no_more: false,
201 | querying: false
202 | }
203 | })
204 | }
205 | }
206 |
207 | const props_qas = {
208 | dispatch,
209 | querying,
210 | theme,
211 | loadway,
212 | no_more,
213 | qas,
214 | total,
215 | groups,
216 | current_group,
217 | loading: loading.effects['index/loadMore'],
218 | onAddGroup () {
219 | dispatch({
220 | type: 'index/updateState',
221 | payload: {
222 | modal_visible: true,
223 | modal_type: 'add_group'
224 | }
225 | })
226 | },
227 | rate (params: IORate) {
228 | dispatch({
229 | type: 'index/rate',
230 | payload: {
231 | current_group,
232 | params,
233 | message_success: lang.formatMessage({
234 | id: 'index.modal.rate.success'
235 | }),
236 | message_failed: lang.formatMessage({
237 | id: 'index.modal.rate.failed'
238 | })
239 | }
240 | })
241 | }
242 | }
243 |
244 | return (
245 |
246 |
247 | {groups.length > 0 && }
248 | {filter_visible && }
249 |
250 |
251 | )
252 | }
253 |
254 | export default memo(connect(({ app, index, loading }: any) => ({ app, index, loading }))(Index))
255 |
--------------------------------------------------------------------------------
/src/services/app.ts:
--------------------------------------------------------------------------------
1 | import Groups from '@/db/models/Groups'
2 | import Qas, { IQas } from '@/db/models/Qas'
3 | import { Service_addGroup } from './index'
4 |
5 | export interface IGetAnalysisData {
6 | name: string
7 | total: number
8 | average_rate: number
9 | }
10 |
11 | export interface IExportData {
12 | name: string
13 | data: Array
14 | }
15 |
16 | export const Service_addTableGroups = async (): Promise => {
17 | const groups = new Groups()
18 |
19 | try {
20 | await groups.init()
21 | } catch (_) {
22 | return false
23 | }
24 |
25 | return true
26 | }
27 |
28 | export const Service_getAllGroups = async (): Promise> => {
29 | const groups = new Groups()
30 |
31 | return await groups.getGroups()
32 | }
33 |
34 | export const Service_deleteGroup = async (group: string): Promise => {
35 | const groups = new Groups()
36 |
37 | try {
38 | await groups.delGroup(group)
39 | } catch (_) {
40 | return false
41 | }
42 |
43 | return true
44 | }
45 |
46 | export const Service_getAnalysisData = async (): Promise> => {
47 | const groups = new Groups()
48 | const groups_array = await groups.getGroups()
49 | const analysis_data = []
50 |
51 | for (let i of groups_array) {
52 | const qa = new Qas(i)
53 | const total = await qa.getTotal()
54 | const average_rate = await qa.getAverageRate()
55 |
56 | analysis_data.push({
57 | name: i,
58 | total: total,
59 | average_rate: average_rate
60 | })
61 | }
62 |
63 | return analysis_data
64 | }
65 |
66 | export const Service_importData = async (data: Array): Promise => {
67 | for (let i of data) {
68 | await Service_addGroup(i.name)
69 |
70 | const qa = new Qas(i.name)
71 |
72 | const res = await qa.bulkAddQa(i.data)
73 |
74 | if (res !== true) return res
75 | }
76 |
77 | return true
78 | }
79 |
80 | export const Service_exportData = async (): Promise> => {
81 | const groups = new Groups()
82 | const groups_array = await groups.getGroups()
83 | const export_data = []
84 |
85 | for (let i of groups_array) {
86 | const qa = new Qas(i)
87 | const total_qas = await qa.getTotalQas()
88 |
89 | export_data.push({
90 | name: i,
91 | data: total_qas
92 | })
93 | }
94 |
95 | return export_data
96 | }
97 |
--------------------------------------------------------------------------------
/src/services/index.ts:
--------------------------------------------------------------------------------
1 | import Groups from '@/db/models/Groups'
2 | import Qas, { IQas, IRate, IQuery } from '@/db/models/Qas'
3 |
4 | export const Service_addGroup = async (group: string): Promise => {
5 | const groups = new Groups()
6 |
7 | try {
8 | await groups.addGroup(group)
9 | } catch (_) {
10 | return false
11 | }
12 |
13 | return true
14 | }
15 |
16 | export const Service_addQa = async (current_group: string, params: IQas): Promise => {
17 | const qa = new Qas(current_group)
18 |
19 | try {
20 | await qa.addQa(params)
21 | } catch (_) {
22 | return false
23 | }
24 |
25 | return true
26 | }
27 |
28 | export const Service_delQa = async (current_group: string, id: number): Promise => {
29 | const qa = new Qas(current_group)
30 |
31 | try {
32 | await qa.delQa(id)
33 | } catch (_) {
34 | return false
35 | }
36 |
37 | return true
38 | }
39 |
40 | export const Service_putQa = async (
41 | current_group: string,
42 | id: number,
43 | params: IQas
44 | ): Promise => {
45 | const qa = new Qas(current_group)
46 |
47 | try {
48 | await qa.putQa(id, params)
49 | } catch (_) {
50 | return false
51 | }
52 |
53 | return true
54 | }
55 |
56 | export const Service_query = async (
57 | current_group: string,
58 | { query, times, rate }: IQuery
59 | ): Promise> => {
60 | const qa = new Qas(current_group)
61 |
62 | return await qa.query({ query, times, rate })
63 | }
64 |
65 | export const Service_getQas = async (
66 | current_group: string,
67 | page?: number,
68 | page_size?: number
69 | ): Promise> => {
70 | const qa = new Qas(current_group)
71 |
72 | return await qa.getQas(page, page_size)
73 | }
74 |
75 | export const Service_getTotal = async (current_group: string): Promise => {
76 | const qa = new Qas(current_group)
77 |
78 | return await qa.getTotal()
79 | }
80 |
81 | export const Service_rate = async (current_group: string, params: IRate): Promise => {
82 | const qa = new Qas(current_group)
83 |
84 | try {
85 | await qa.rate(params)
86 | } catch (_) {
87 | return false
88 | }
89 |
90 | return true
91 | }
92 |
93 | export const Service_clearRateLog = async (current_group: string, id: number): Promise => {
94 | const qa = new Qas(current_group)
95 |
96 | try {
97 | await qa.clearRateLog(id)
98 | } catch (_) {
99 | return false
100 | }
101 |
102 | return true
103 | }
104 |
--------------------------------------------------------------------------------
/src/themes/changeTheme.ts:
--------------------------------------------------------------------------------
1 | export default (theme: 'light' | 'dark') => {
2 | if (theme === 'light') {
3 | window.less.modifyVars({})
4 | } else {
5 | window.less.modifyVars({
6 | '@text-color': '#ffffff',
7 | '@disabled-color': '#333333',
8 | '@item-hover-bg': '#333333',
9 | '@component-background': '#202124',
10 | '@background-color-light': '#333333',
11 | '@background-color-base': '#333333',
12 | '@text-color-secondary': '#333333',
13 | '@body-background': '#202124',
14 | '@border-color-base': '#292929',
15 | '@border-color-split': '#292929',
16 | })
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/themes/index.less:
--------------------------------------------------------------------------------
1 | @import './preset/init.less';
2 | @import './preset/atom.less';
3 | @import './preset/index.less';
4 | @import './preset/classes.less';
5 | @import './preset/mixin.less';
--------------------------------------------------------------------------------
/src/themes/index.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import lessToJs from 'less-vars-to-js'
4 |
5 | export default lessToJs(
6 | fs.readFileSync(
7 | path.join( __dirname, `./skins/default.less` ),
8 | 'utf8'
9 | )
10 | )
--------------------------------------------------------------------------------
/src/themes/preset/atom.less:
--------------------------------------------------------------------------------
1 | // https://github.com/MatrixAge/atom.css
2 |
3 | .border_box {
4 | -webkit-box-sizing: border-box;
5 | box-sizing: border-box
6 | }
7 |
8 | .content_box {
9 | -webkit-box-sizing: content-box;
10 | box-sizing: content-box
11 | }
12 |
13 | .flex {
14 | display: -webkit-box;
15 | display: -ms-flexbox;
16 | display: flex
17 | }
18 |
19 | .flex_row {
20 | -webkit-box-orient: horizontal;
21 | -webkit-box-direction: normal;
22 | -ms-flex-direction: row;
23 | flex-direction: row
24 | }
25 |
26 | .flex_row_reverse {
27 | -webkit-box-orient: horizontal;
28 | -webkit-box-direction: reverse;
29 | -ms-flex-direction: row-reverse;
30 | flex-direction: row-reverse
31 | }
32 |
33 | .flex_column {
34 | -webkit-box-orient: vertical;
35 | -webkit-box-direction: normal;
36 | -ms-flex-direction: column;
37 | flex-direction: column
38 | }
39 |
40 | .flex_column_reverse {
41 | -webkit-box-orient: vertical;
42 | -webkit-box-direction: reverse;
43 | -ms-flex-direction: column-reverse;
44 | flex-direction: column-reverse
45 | }
46 |
47 | .flex_wrap {
48 | -ms-flex-wrap: wrap;
49 | flex-wrap: wrap
50 | }
51 |
52 | .flex_nowrap {
53 | -ms-flex-wrap: nowrap;
54 | flex-wrap: nowrap
55 | }
56 |
57 | .flex_wrap_reverse {
58 | -ms-flex-wrap: wrap-reverse;
59 | flex-wrap: wrap-reverse
60 | }
61 |
62 | .justify_center {
63 | -webkit-box-pack: center;
64 | -ms-flex-pack: center;
65 | justify-content: center
66 | }
67 |
68 | .justify_start {
69 | -webkit-box-pack: start;
70 | -ms-flex-pack: start;
71 | justify-content: flex-start
72 | }
73 |
74 | .justify_end {
75 | -webkit-box-pack: end;
76 | -ms-flex-pack: end;
77 | justify-content: flex-end
78 | }
79 |
80 | .justify_between {
81 | -webkit-box-pack: justify;
82 | -ms-flex-pack: justify;
83 | justify-content: space-between
84 | }
85 |
86 | .justify_around {
87 | -ms-flex-pack: distribute;
88 | justify-content: space-around
89 | }
90 |
91 | .justify_evenly {
92 | -webkit-box-pack: space-evenly;
93 | -ms-flex-pack: space-evenly;
94 | justify-content: space-evenly
95 | }
96 |
97 | .justify_initial {
98 | -webkit-box-pack: initial;
99 | -ms-flex-pack: initial;
100 | justify-content: initial
101 | }
102 |
103 | .align_center {
104 | -webkit-box-align: center;
105 | -ms-flex-align: center;
106 | align-items: center
107 | }
108 |
109 | .align_start {
110 | -webkit-box-align: start;
111 | -ms-flex-align: start;
112 | align-items: flex-start
113 | }
114 |
115 | .align_end {
116 | -webkit-box-align: end;
117 | -ms-flex-align: end;
118 | align-items: flex-end
119 | }
120 |
121 | .align_stretch {
122 | -webkit-box-align: stretch;
123 | -ms-flex-align: stretch;
124 | align-items: stretch
125 | }
126 |
127 | .align_baseline {
128 | -webkit-box-align: baseline;
129 | -ms-flex-align: baseline;
130 | align-items: baseline
131 | }
132 |
133 | .align_initial {
134 | -webkit-box-align: initial;
135 | -ms-flex-align: initial;
136 | align-items: initial
137 | }
138 |
139 | .line_clamp_1 {
140 | overflow: hidden;
141 | -o-text-overflow: ellipsis;
142 | text-overflow: ellipsis;
143 | white-space: nowrap
144 | }
145 |
146 | .line_clamp_2 {
147 | display: -webkit-box;
148 | -webkit-box-orient: vertical;
149 | overflow: hidden;
150 | -webkit-line-clamp: 2
151 | }
152 |
153 | .line_clamp_3 {
154 | display: -webkit-box;
155 | -webkit-box-orient: vertical;
156 | overflow: hidden;
157 | -webkit-line-clamp: 3
158 | }
159 |
160 | .line_clamp_4 {
161 | display: -webkit-box;
162 | -webkit-box-orient: vertical;
163 | overflow: hidden;
164 | -webkit-line-clamp: 4
165 | }
166 |
167 | .line_clamp_5 {
168 | display: -webkit-box;
169 | -webkit-box-orient: vertical;
170 | overflow: hidden;
171 | -webkit-line-clamp: 5
172 | }
173 |
174 | .left {
175 | float: left
176 | }
177 |
178 | .right {
179 | float: right
180 | }
181 |
182 | .clearfix:after {
183 | visibility: hidden;
184 | display: block;
185 | font-size: 0;
186 | content: " ";
187 | clear: both;
188 | height: 0
189 | }
190 |
191 | .clearfix {
192 | display: inline-table
193 | }
194 |
195 | .clearfix {
196 | *zoom: 1
197 | }
198 |
199 | .margin_center {
200 | margin: auto
201 | }
202 |
203 | .margin_xcenter {
204 | margin: 0 auto
205 | }
206 |
207 | .margin_ycenter {
208 | margin: auto 0
209 | }
210 |
211 | .text_center {
212 | text-align: center
213 | }
214 |
215 | .text_left {
216 | text-align: left
217 | }
218 |
219 | .text_right {
220 | text-align: right
221 | }
222 |
223 | .text_justify {
224 | text-align: justify
225 | }
226 |
227 | .absolute {
228 | position: absolute
229 | }
230 |
231 | .relative {
232 | position: relative
233 | }
234 |
235 | .fixed {
236 | position: fixed
237 | }
238 |
239 | .static {
240 | position: static
241 | }
242 |
243 | .sticky {
244 | position: -webkit-sticky;
245 | position: sticky
246 | }
247 |
248 | .top_0 {
249 | top: 0
250 | }
251 |
252 | .left_0 {
253 | left: 0
254 | }
255 |
256 | .right_0 {
257 | right: 0
258 | }
259 |
260 | .bottom_0 {
261 | bottom: 0
262 | }
263 |
264 | .z_index_0 {
265 | z-index: 0
266 | }
267 |
268 | .z_index_10 {
269 | z-index: 10
270 | }
271 |
272 | .z_index_20 {
273 | z-index: 20
274 | }
275 |
276 | .z_index_100 {
277 | z-index: 100
278 | }
279 |
280 | .z_index_1000 {
281 | z-index: 1000
282 | }
283 |
284 | .none {
285 | display: none
286 | }
287 |
288 | .block {
289 | display: block
290 | }
291 |
292 | .inline {
293 | display: inline
294 | }
295 |
296 | .inline_block {
297 | display: inline-block
298 | }
299 |
300 | .list_item {
301 | display: list-item
302 | }
303 |
304 | .radius_0 {
305 | border-radius: 0
306 | }
307 |
308 | .radius_4 {
309 | border-radius: 4px
310 | }
311 |
312 | .radius_10 {
313 | border-radius: 10px
314 | }
315 |
316 | .radius_100 {
317 | border-radius: 100px
318 | }
319 |
320 | .border_none {
321 | border: 0
322 | }
323 |
324 | .overflow_visible {
325 | overflow: visible
326 | }
327 |
328 | .overflow_hidden {
329 | overflow: hidden
330 | }
331 |
332 | .overflow_xhidden {
333 | overflow-x: hidden
334 | }
335 |
336 | .overflow_yhidden {
337 | overflow-y: hidden
338 | }
339 |
340 | .outline_none {
341 | outline: 0
342 | }
343 |
344 | .transition_normal {
345 | -webkit-transition: all ease .3s;
346 | -o-transition: all ease .3s;
347 | transition: all ease .3s
348 | }
349 |
350 | .transition_slow {
351 | -webkit-transition: all ease .8s;
352 | -o-transition: all ease .8s;
353 | transition: all ease .8s
354 | }
355 |
356 | .disabled {
357 | pointer-events: none
358 | }
359 |
360 | .w_100 {
361 | width: 100%
362 | }
363 |
364 | .h_100 {
365 | height: 100%
366 | }
367 |
368 | .w_100vw {
369 | width: 100vw
370 | }
371 |
372 | .h_100vh {
373 | height: 100vh
374 | }
375 |
376 | .m_0 {
377 | margin: 0
378 | }
379 |
380 | .m_2 {
381 | margin: 2px
382 | }
383 |
384 | .m_4 {
385 | margin: 4px
386 | }
387 |
388 | .m_6 {
389 | margin: 6px
390 | }
391 |
392 | .m_8 {
393 | margin: 8px
394 | }
395 |
396 | .m_10 {
397 | margin: 10px
398 | }
399 |
400 | .m_12 {
401 | margin: 12px
402 | }
403 |
404 | .m_14 {
405 | margin: 14px
406 | }
407 |
408 | .m_16 {
409 | margin: 16px
410 | }
411 |
412 | .m_18 {
413 | margin: 18px
414 | }
415 |
416 | .m_20 {
417 | margin: 20px
418 | }
419 |
420 | .m_30 {
421 | margin: 30px
422 | }
423 |
424 | .mt_0 {
425 | margin-top: 0
426 | }
427 |
428 | .mt_2 {
429 | margin-top: 2px
430 | }
431 |
432 | .mt_4 {
433 | margin-top: 4px
434 | }
435 |
436 | .mt_6 {
437 | margin-top: 6px
438 | }
439 |
440 | .mt_8 {
441 | margin-top: 8px
442 | }
443 |
444 | .mt_10 {
445 | margin-top: 10px
446 | }
447 |
448 | .mt_12 {
449 | margin-top: 12px
450 | }
451 |
452 | .mt_14 {
453 | margin-top: 14px
454 | }
455 |
456 | .mt_16 {
457 | margin-top: 16px
458 | }
459 |
460 | .mt_18 {
461 | margin-top: 18px
462 | }
463 |
464 | .mt_20 {
465 | margin-top: 20px
466 | }
467 |
468 | .mt_30 {
469 | margin-top: 30px
470 | }
471 |
472 | .mb_0 {
473 | margin-bottom: 0
474 | }
475 |
476 | .mb_2 {
477 | margin-bottom: 2px
478 | }
479 |
480 | .mb_4 {
481 | margin-bottom: 4px
482 | }
483 |
484 | .mb_6 {
485 | margin-bottom: 6px
486 | }
487 |
488 | .mb_8 {
489 | margin-bottom: 8px
490 | }
491 |
492 | .mb_10 {
493 | margin-bottom: 10px
494 | }
495 |
496 | .mb_12 {
497 | margin-bottom: 12px
498 | }
499 |
500 | .mb_14 {
501 | margin-bottom: 14px
502 | }
503 |
504 | .mb_16 {
505 | margin-bottom: 16px
506 | }
507 |
508 | .mb_18 {
509 | margin-bottom: 18px
510 | }
511 |
512 | .mb_20 {
513 | margin-bottom: 20px
514 | }
515 |
516 | .mb_30 {
517 | margin-bottom: 30px
518 | }
519 |
520 | .ml_0 {
521 | margin-left: 0
522 | }
523 |
524 | .ml_2 {
525 | margin-left: 2px
526 | }
527 |
528 | .ml_4 {
529 | margin-left: 4px
530 | }
531 |
532 | .ml_6 {
533 | margin-left: 6px
534 | }
535 |
536 | .ml_8 {
537 | margin-left: 8px
538 | }
539 |
540 | .ml_10 {
541 | margin-left: 10px
542 | }
543 |
544 | .ml_12 {
545 | margin-left: 12px
546 | }
547 |
548 | .ml_14 {
549 | margin-left: 14px
550 | }
551 |
552 | .ml_16 {
553 | margin-left: 16px
554 | }
555 |
556 | .ml_18 {
557 | margin-left: 18px
558 | }
559 |
560 | .ml_20 {
561 | margin-left: 20px
562 | }
563 |
564 | .ml_30 {
565 | margin-left: 20px
566 | }
567 |
568 | .mr_0 {
569 | margin-right: 0
570 | }
571 |
572 | .mr_2 {
573 | margin-right: 2px
574 | }
575 |
576 | .mr_4 {
577 | margin-right: 4px
578 | }
579 |
580 | .mr_6 {
581 | margin-right: 6px
582 | }
583 |
584 | .mr_8 {
585 | margin-right: 8px
586 | }
587 |
588 | .mr_10 {
589 | margin-right: 10px
590 | }
591 |
592 | .mr_12 {
593 | margin-right: 12px
594 | }
595 |
596 | .mr_14 {
597 | margin-right: 14px
598 | }
599 |
600 | .mr_16 {
601 | margin-right: 16px
602 | }
603 |
604 | .mr_18 {
605 | margin-right: 18px
606 | }
607 |
608 | .mr_20 {
609 | margin-right: 20px
610 | }
611 |
612 | .mr_30 {
613 | margin-right: 30px
614 | }
615 |
616 | .p_0 {
617 | padding: 0
618 | }
619 |
620 | .p_2 {
621 | padding: 2px
622 | }
623 |
624 | .p_4 {
625 | padding: 4px
626 | }
627 |
628 | .p_6 {
629 | padding: 6px
630 | }
631 |
632 | .p_8 {
633 | padding: 8px
634 | }
635 |
636 | .p_10 {
637 | padding: 10px
638 | }
639 |
640 | .p_12 {
641 | padding: 12px
642 | }
643 |
644 | .p_14 {
645 | padding: 14px
646 | }
647 |
648 | .p_16 {
649 | padding: 16px
650 | }
651 |
652 | .p_18 {
653 | padding: 18px
654 | }
655 |
656 | .p_20 {
657 | padding: 20px
658 | }
659 |
660 | .p_30 {
661 | padding: 30px
662 | }
663 |
664 | .pt_0 {
665 | padding-top: 0
666 | }
667 |
668 | .pt_2 {
669 | padding-top: 2px
670 | }
671 |
672 | .pt_4 {
673 | padding-top: 4px
674 | }
675 |
676 | .pt_6 {
677 | padding-top: 6px
678 | }
679 |
680 | .pt_8 {
681 | padding-top: 8px
682 | }
683 |
684 | .pt_10 {
685 | padding-top: 10px
686 | }
687 |
688 | .pt_12 {
689 | padding-top: 12px
690 | }
691 |
692 | .pt_14 {
693 | padding-top: 14px
694 | }
695 |
696 | .pt_16 {
697 | padding-top: 16px
698 | }
699 |
700 | .pt_18 {
701 | padding-top: 18px
702 | }
703 |
704 | .pt_20 {
705 | padding-top: 20px
706 | }
707 |
708 | .pt_30 {
709 | padding-top: 30px
710 | }
711 |
712 | .pb_0 {
713 | padding-bottom: 0
714 | }
715 |
716 | .pb_2 {
717 | padding-bottom: 2px
718 | }
719 |
720 | .pb_4 {
721 | padding-bottom: 4px
722 | }
723 |
724 | .pb_6 {
725 | padding-bottom: 6px
726 | }
727 |
728 | .pb_8 {
729 | padding-bottom: 8px
730 | }
731 |
732 | .pb_10 {
733 | padding-bottom: 10px
734 | }
735 |
736 | .pb_12 {
737 | padding-bottom: 12px
738 | }
739 |
740 | .pb_14 {
741 | padding-bottom: 14px
742 | }
743 |
744 | .pb_16 {
745 | padding-bottom: 16px
746 | }
747 |
748 | .pb_18 {
749 | padding-bottom: 18px
750 | }
751 |
752 | .pb_20 {
753 | padding-bottom: 20px
754 | }
755 |
756 | .pb_30 {
757 | padding-bottom: 30px
758 | }
759 |
760 | .pl_0 {
761 | padding-left: 0
762 | }
763 |
764 | .pl_2 {
765 | padding-left: 2px
766 | }
767 |
768 | .pl_4 {
769 | padding-left: 4px
770 | }
771 |
772 | .pl_6 {
773 | padding-left: 6px
774 | }
775 |
776 | .pl_8 {
777 | padding-left: 8px
778 | }
779 |
780 | .pl_10 {
781 | padding-left: 10px
782 | }
783 |
784 | .pl_12 {
785 | padding-left: 12px
786 | }
787 |
788 | .pl_14 {
789 | padding-left: 14px
790 | }
791 |
792 | .pl_16 {
793 | padding-left: 16px
794 | }
795 |
796 | .pl_18 {
797 | padding-left: 18px
798 | }
799 |
800 | .pl_20 {
801 | padding-left: 20px
802 | }
803 |
804 | .pl_30 {
805 | padding-left: 30px
806 | }
807 |
808 | .pr_0 {
809 | padding-right: 0
810 | }
811 |
812 | .pr_2 {
813 | padding-right: 2px
814 | }
815 |
816 | .pr_4 {
817 | padding-right: 4px
818 | }
819 |
820 | .pr_6 {
821 | padding-right: 6px
822 | }
823 |
824 | .pr_8 {
825 | padding-right: 8px
826 | }
827 |
828 | .pr_10 {
829 | padding-right: 10px
830 | }
831 |
832 | .pr_12 {
833 | padding-right: 12px
834 | }
835 |
836 | .pr_14 {
837 | padding-right: 14px
838 | }
839 |
840 | .pr_16 {
841 | padding-right: 16px
842 | }
843 |
844 | .pr_18 {
845 | padding-right: 18px
846 | }
847 |
848 | .pr_20 {
849 | padding-right: 20px
850 | }
851 |
852 | .pr_30 {
853 | padding-right: 30px
854 | }
855 |
856 | .transparent {
857 | color: transparent
858 | }
859 |
860 | .white {
861 | color: white
862 | }
863 |
864 | .whitesmoke {
865 | color: whitesmoke
866 | }
867 |
868 | .black {
869 | color: black
870 | }
871 |
872 | .red {
873 | color: #f44336
874 | }
875 |
876 | .orange {
877 | color: #ff9800
878 | }
879 |
880 | .yellow {
881 | color: #ffd600
882 | }
883 |
884 | .green {
885 | color: #4caf50
886 | }
887 |
888 | .cyan {
889 | color: #00bcd4
890 | }
891 |
892 | .blue {
893 | color: #2196f3
894 | }
895 |
896 | .purple {
897 | color: #673ab7
898 | }
899 |
900 | .color_000 {
901 | color: #000
902 | }
903 |
904 | .color_111 {
905 | color: #111
906 | }
907 |
908 | .color_222 {
909 | color: #222
910 | }
911 |
912 | .color_333 {
913 | color: #333
914 | }
915 |
916 | .color_444 {
917 | color: #444
918 | }
919 |
920 | .color_555 {
921 | color: #555
922 | }
923 |
924 | .color_666 {
925 | color: #666
926 | }
927 |
928 | .color_777 {
929 | color: #777
930 | }
931 |
932 | .color_888 {
933 | color: #888
934 | }
935 |
936 | .color_999 {
937 | color: #999
938 | }
939 |
940 | .color_aaa {
941 | color: #aaa
942 | }
943 |
944 | .color_bbb {
945 | color: #bbb
946 | }
947 |
948 | .color_ccc {
949 | color: #ccc
950 | }
951 |
952 | .color_ddd {
953 | color: #ddd
954 | }
955 |
956 | .color_eee {
957 | color: #eee
958 | }
959 |
960 | .color_fff {
961 | color: #fff
962 | }
963 |
964 | .bg_transparent {
965 | background-color: transparent
966 | }
967 |
968 | .bg_white {
969 | background-color: white
970 | }
971 |
972 | .bg_whitesmoke {
973 | background-color: whitesmoke
974 | }
975 |
976 | .bg_black {
977 | background-color: black
978 | }
979 |
980 | .bg_red {
981 | background-color: #f44336
982 | }
983 |
984 | .bg_orange {
985 | background-color: #ff9800
986 | }
987 |
988 | .bg_yellow {
989 | background-color: #ffd600
990 | }
991 |
992 | .bg_green {
993 | background-color: #4caf50
994 | }
995 |
996 | .bg_cyan {
997 | background-color: #00bcd4
998 | }
999 |
1000 | .bg_blue {
1001 | background-color: #2196f3
1002 | }
1003 |
1004 | .bg_purple {
1005 | background-color: #673ab7
1006 | }
1007 |
1008 | .bg_color_000 {
1009 | background-color: #000
1010 | }
1011 |
1012 | .bg_color_111 {
1013 | background-color: #111
1014 | }
1015 |
1016 | .bg_color_222 {
1017 | background-color: #222
1018 | }
1019 |
1020 | .bg_color_333 {
1021 | background-color: #333
1022 | }
1023 |
1024 | .bg_color_444 {
1025 | background-color: #444
1026 | }
1027 |
1028 | .bg_color_555 {
1029 | background-color: #555
1030 | }
1031 |
1032 | .bg_color_666 {
1033 | background-color: #666
1034 | }
1035 |
1036 | .bg_color_777 {
1037 | background-color: #777
1038 | }
1039 |
1040 | .bg_color_888 {
1041 | background-color: #888
1042 | }
1043 |
1044 | .bg_color_999 {
1045 | background-color: #999
1046 | }
1047 |
1048 | .bg_color_aaa {
1049 | background-color: #aaa
1050 | }
1051 |
1052 | .bg_color_bbb {
1053 | background-color: #bbb
1054 | }
1055 |
1056 | .bg_color_ccc {
1057 | background-color: #ccc
1058 | }
1059 |
1060 | .bg_color_ddd {
1061 | background-color: #ddd
1062 | }
1063 |
1064 | .bg_color_eee {
1065 | background-color: #eee
1066 | }
1067 |
1068 | .bg_color_fff {
1069 | background-color: #fff
1070 | }
1071 |
1072 | .font_normal {
1073 | font-weight: normal
1074 | }
1075 |
1076 | .font_bold {
1077 | font-weight: bold
1078 | }
1079 |
1080 | .font_bolder {
1081 | font-weight: bolder
1082 | }
1083 |
1084 | .fontsize_8 {
1085 | font-size: 8px
1086 | }
1087 |
1088 | .fontsize_10 {
1089 | font-size: 12px
1090 | }
1091 |
1092 | .fontsize_12 {
1093 | font-size: 12px
1094 | }
1095 |
1096 | .fontsize_13 {
1097 | font-size: 13px
1098 | }
1099 |
1100 | .fontsize_14 {
1101 | font-size: 14px
1102 | }
1103 |
1104 | .fontsize_15 {
1105 | font-size: 14px
1106 | }
1107 |
1108 | .fontsize_16 {
1109 | font-size: 16px
1110 | }
1111 |
1112 | .fontsize_18 {
1113 | font-size: 18px
1114 | }
1115 |
1116 | .fontsize_20 {
1117 | font-size: 20px
1118 | }
1119 |
1120 | .fontsize_24 {
1121 | font-size: 24px
1122 | }
1123 |
1124 | .fontsize_32 {
1125 | font-size: 32px
1126 | }
1127 |
1128 | .fontsize_64 {
1129 | font-size: 72px
1130 | }
1131 |
1132 | .fontsize_128 {
1133 | font-size: 128px
1134 | }
1135 |
1136 | .fontfamily_simhei {
1137 | font-family: simhei
1138 | }
1139 |
1140 | .fontfamily_simsun {
1141 | font-family: simsun
1142 | }
1143 |
1144 | .fontfamily_nsimsun {
1145 | font-family: nsimsun
1146 | }
1147 |
1148 | .fontfamily_fangsong {
1149 | font-family: fangsong
1150 | }
1151 |
1152 | .fontfamily_kaiti {
1153 | font-family: kaiti
1154 | }
1155 |
1156 | .fontfamily_yahei {
1157 | font-family: microsoft yahei
1158 | }
1159 |
1160 | .fontfamily_lisu {
1161 | font-family: lisu
1162 | }
1163 |
1164 | .fontfamily_pingfang {
1165 | font-family: pingfang sc
1166 | }
1167 |
1168 | .fontfamily_stheiti {
1169 | font-family: stheiti
1170 | }
1171 |
1172 | .letter_spacing_1 {
1173 | letter-spacing: 1px
1174 | }
1175 |
1176 | .letter_spacing_2 {
1177 | letter-spacing: 1px
1178 | }
1179 |
1180 | .cursor_point {
1181 | cursor: pointer
1182 | }
--------------------------------------------------------------------------------
/src/themes/preset/classes.less:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatrixAges/testool/ba4c2abdc00bf077a8e3c04a9db6e731949b7b92/src/themes/preset/classes.less
--------------------------------------------------------------------------------
/src/themes/preset/helpers/bezierEasing.less:
--------------------------------------------------------------------------------
1 | /* stylelint-disable */
2 | .bezierEasingMixin() {
3 | @functions: ~`(function() {
4 | var NEWTON_ITERATIONS=4;
5 | var NEWTON_MIN_SLOPE=0.001;
6 | var SUBDIVISION_PRECISION=0.0000001;
7 | var SUBDIVISION_MAX_ITERATIONS=10;
8 |
9 | var kSplineTableSize=11;
10 | var kSampleStepSize=1.0 / (kSplineTableSize - 1.0);
11 |
12 | var float32ArraySupported=typeof Float32Array==='function';
13 |
14 | function A (aA1, aA2) {
15 | return 1.0 - 3.0 * aA2 + 3.0 * aA1;
16 | }
17 |
18 | function B (aA1, aA2) {
19 | return 3.0 * aA2 - 6.0 * aA1;
20 | }
21 |
22 | function C (aA1) {
23 | return 3.0 * aA1;
24 | }
25 |
26 | // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
27 | function calcBezier (aT, aA1, aA2) {
28 | return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT;
29 | }
30 |
31 | // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
32 | function getSlope (aT, aA1, aA2) {
33 | return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
34 | }
35 |
36 | function binarySubdivide (aX, aA, aB, mX1, mX2) {
37 | var currentX, currentT, i=0;
38 |
39 | do {
40 | currentT=aA + (aB - aA) / 2.0;
41 | currentX=calcBezier(currentT, mX1, mX2) - aX;
42 |
43 | if (currentX > 0.0) {
44 | aB=currentT;
45 | }
46 |
47 | else {
48 | aA=currentT;
49 | }
50 | }
51 |
52 | while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS);
53 | return currentT;
54 | }
55 |
56 | function newtonRaphsonIterate (aX, aGuessT, mX1, mX2) {
57 | for (var i=0; i < NEWTON_ITERATIONS; ++i) {
58 | var currentSlope=getSlope(aGuessT, mX1, mX2);
59 |
60 | if (currentSlope===0.0) {
61 | return aGuessT;
62 | }
63 |
64 | var currentX=calcBezier(aGuessT, mX1, mX2) - aX;
65 | aGuessT -=currentX / currentSlope;
66 | }
67 |
68 | return aGuessT;
69 | }
70 |
71 | var BezierEasing=function (mX1, mY1, mX2, mY2) {
72 | if ( !(0 <=mX1 && mX1 <=1 && 0 <=mX2 && mX2 <=1)) {
73 | throw new Error('bezier x values must be in [0, 1] range');
74 | }
75 |
76 | // Precompute samples table
77 | var sampleValues=float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize);
78 |
79 | if (mX1 !==mY1 || mX2 !==mY2) {
80 | for (var i=0; i < kSplineTableSize; ++i) {
81 | sampleValues[i]=calcBezier(i * kSampleStepSize, mX1, mX2);
82 | }
83 | }
84 |
85 | function getTForX (aX) {
86 | var intervalStart=0.0;
87 | var currentSample=1;
88 | var lastSample=kSplineTableSize - 1;
89 |
90 | for (; currentSample !==lastSample && sampleValues[currentSample] <=aX; ++currentSample) {
91 | intervalStart +=kSampleStepSize;
92 | }
93 |
94 | --currentSample;
95 |
96 | // Interpolate to provide an initial guess for t
97 | var dist=(aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]);
98 | var guessForT=intervalStart + dist * kSampleStepSize;
99 |
100 | var initialSlope=getSlope(guessForT, mX1, mX2);
101 |
102 | if (initialSlope >=NEWTON_MIN_SLOPE) {
103 | return newtonRaphsonIterate(aX, guessForT, mX1, mX2);
104 | }
105 |
106 | else if (initialSlope===0.0) {
107 | return guessForT;
108 | }
109 |
110 | else {
111 | return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2);
112 | }
113 | }
114 |
115 | return function BezierEasing (x) {
116 | if (mX1===mY1 && mX2===mY2) {
117 | return x; // linear
118 | }
119 |
120 | // Because JavaScript number are imprecise, we should guarantee the extremes are right.
121 | if (x===0) {
122 | return 0;
123 | }
124 |
125 | if (x===1) {
126 | return 1;
127 | }
128 |
129 | return calcBezier(getTForX(x), mY1, mY2);
130 | }
131 |
132 | ;
133 | }
134 |
135 | ;
136 |
137 | this.colorEasing=BezierEasing(0.26, 0.09, 0.37, 0.18);
138 | // less 3 requires a return
139 | return '';
140 | }
141 |
142 | )()`;
143 | }
144 |
145 | // It is hacky way to make this function will be compiled preferentially by less
146 | // resolve error: `ReferenceError: colorPalette is not defined`
147 | // https://github.com/ant-design/ant-motion/issues/44
148 | .bezierEasingMixin();
--------------------------------------------------------------------------------
/src/themes/preset/helpers/colorPalette.less:
--------------------------------------------------------------------------------
1 | /* stylelint-disable no-duplicate-selectors */
2 | @import "bezierEasing.less";
3 | @import "tinyColor.less";
4 |
5 | // We create a very complex algorithm which take the place of original tint/shade color system
6 | // to make sure no one can understand it 👻
7 | // and create an entire color palette magicly by inputing just a single primary color.
8 | // We are using bezier-curve easing function and some color manipulations like tint/shade/darken/spin
9 | .colorPaletteMixin() {
10 | @functions: ~`(function() {
11 | var hueStep=2;
12 | var saturationStep=16;
13 | var saturationStep2=5;
14 | var brightnessStep1=5;
15 | var brightnessStep2=15;
16 | var lightColorCount=5;
17 | var darkColorCount=4;
18 |
19 | var getHue=function(hsv, i, isLight) {
20 | var hue;
21 |
22 | if (hsv.h >=60 && hsv.h <=240) {
23 | hue=isLight ? hsv.h - hueStep * i : hsv.h + hueStep * i;
24 | }
25 |
26 | else {
27 | hue=isLight ? hsv.h + hueStep * i : hsv.h - hueStep * i;
28 | }
29 |
30 | if (hue < 0) {
31 | hue +=360;
32 | }
33 |
34 | else if (hue >=360) {
35 | hue -=360;
36 | }
37 |
38 | return Math.round(hue);
39 | }
40 |
41 | ;
42 |
43 | var getSaturation=function(hsv, i, isLight) {
44 | var saturation;
45 |
46 | if (isLight) {
47 | saturation=Math.round(hsv.s * 100) - saturationStep * i;
48 | }
49 |
50 | else if (i===darkColorCount) {
51 | saturation=Math.round(hsv.s * 100) + saturationStep;
52 | }
53 |
54 | else {
55 | saturation=Math.round(hsv.s * 100) + saturationStep2 * i;
56 | }
57 |
58 | if (saturation > 100) {
59 | saturation=100;
60 | }
61 |
62 | if (isLight && i===lightColorCount && saturation > 10) {
63 | saturation=10;
64 | }
65 |
66 | if (saturation < 6) {
67 | saturation=6;
68 | }
69 |
70 | return Math.round(saturation);
71 | }
72 |
73 | ;
74 |
75 | var getValue=function(hsv, i, isLight) {
76 | if (isLight) {
77 | return Math.round(hsv.v * 100) + brightnessStep1 * i;
78 | }
79 |
80 | return Math.round(hsv.v * 100) - brightnessStep2 * i;
81 | }
82 |
83 | ;
84 |
85 | this.colorPalette=function(color, index) {
86 | var isLight=index <=6;
87 | var hsv=tinycolor(color).toHsv();
88 | var i=isLight ? lightColorCount + 1 - index : index - lightColorCount - 1;
89 |
90 | return tinycolor( {
91 | h: getHue(hsv, i, isLight),
92 | s: getSaturation(hsv, i, isLight),
93 | v: getValue(hsv, i, isLight),
94 | }
95 |
96 | ).toHexString();
97 | }
98 |
99 | ;
100 | }
101 |
102 | )()`;
103 | }
104 |
105 | // It is hacky way to make this function will be compiled preferentially by less
106 | // resolve error: `ReferenceError: colorPalette is not defined`
107 | // https://github.com/ant-design/ant-motion/issues/44
108 | .colorPaletteMixin();
--------------------------------------------------------------------------------
/src/themes/preset/helpers/tinyColor.less:
--------------------------------------------------------------------------------
1 | /* stylelint-disable declaration-bang-space-before,no-duplicate-selectors,string-no-newline */
2 | .tinyColorMixin() {
3 | @functions: ~`(function() {
4 | // TinyColor v1.4.1
5 | // https://github.com/bgrins/TinyColor
6 | // 2016-07-07, Brian Grinstead, MIT License
7 | var trimLeft = /^\s+/,
8 | trimRight = /\s+$/,
9 | tinyCounter = 0,
10 | mathRound = Math.round,
11 | mathMin = Math.min,
12 | mathMax = Math.max,
13 | mathRandom = Math.random;
14 |
15 | function tinycolor (color, opts) {
16 |
17 | color = (color) ? color : '';
18 | opts = opts || { };
19 |
20 | // If input is already a tinycolor, return itself
21 | if (color instanceof tinycolor) {
22 | return color;
23 | }
24 | // If we are called as a function, call using new instead
25 | if (!(this instanceof tinycolor)) {
26 | return new tinycolor(color, opts);
27 | }
28 |
29 | var rgb = inputToRGB(color);
30 | this._originalInput = color,
31 | this._r = rgb.r,
32 | this._g = rgb.g,
33 | this._b = rgb.b,
34 | this._a = rgb.a,
35 | this._roundA = mathRound(100*this._a) / 100,
36 | this._format = opts.format || rgb.format;
37 | this._gradientType = opts.gradientType;
38 |
39 | // Don't let the range of [0,255] come back in [0,1].
40 | // Potentially lose a little bit of precision here, but will fix issues where
41 | // .5 gets interpreted as half of the total, instead of half of 1
42 | // If it was supposed to be 128, this was already taken care of by inputToRgb
43 | if (this._r < 1) { this._r = mathRound(this._r); }
44 | if (this._g < 1) { this._g = mathRound(this._g); }
45 | if (this._b < 1) { this._b = mathRound(this._b); }
46 |
47 | this._ok = rgb.ok;
48 | this._tc_id = tinyCounter++;
49 | }
50 |
51 | tinycolor.prototype = {
52 | isDark: function() {
53 | return this.getBrightness() < 128;
54 | },
55 | isLight: function() {
56 | return !this.isDark();
57 | },
58 | isValid: function() {
59 | return this._ok;
60 | },
61 | getOriginalInput: function() {
62 | return this._originalInput;
63 | },
64 | getFormat: function() {
65 | return this._format;
66 | },
67 | getAlpha: function() {
68 | return this._a;
69 | },
70 | getBrightness: function() {
71 | //http://www.w3.org/TR/AERT#color-contrast
72 | var rgb = this.toRgb();
73 | return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
74 | },
75 | getLuminance: function() {
76 | //http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
77 | var rgb = this.toRgb();
78 | var RsRGB, GsRGB, BsRGB, R, G, B;
79 | RsRGB = rgb.r/255;
80 | GsRGB = rgb.g/255;
81 | BsRGB = rgb.b/255;
82 |
83 | if (RsRGB <= 0.03928) {R = RsRGB / 12.92;} else {R = Math.pow(((RsRGB + 0.055) / 1.055), 2.4);}
84 | if (GsRGB <= 0.03928) {G = GsRGB / 12.92;} else {G = Math.pow(((GsRGB + 0.055) / 1.055), 2.4);}
85 | if (BsRGB <= 0.03928) {B = BsRGB / 12.92;} else {B = Math.pow(((BsRGB + 0.055) / 1.055), 2.4);}
86 | return (0.2126 * R) + (0.7152 * G) + (0.0722 * B);
87 | },
88 | setAlpha: function(value) {
89 | this._a = boundAlpha(value);
90 | this._roundA = mathRound(100*this._a) / 100;
91 | return this;
92 | },
93 | toHsv: function() {
94 | var hsv = rgbToHsv(this._r, this._g, this._b);
95 | return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this._a };
96 | },
97 | toHsvString: function() {
98 | var hsv = rgbToHsv(this._r, this._g, this._b);
99 | var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100);
100 | return (this._a == 1) ?
101 | "hsv(" + h + ", " + s + "%, " + v + "%)" :
102 | "hsva(" + h + ", " + s + "%, " + v + "%, "+ this._roundA + ")";
103 | },
104 | toHsl: function() {
105 | var hsl = rgbToHsl(this._r, this._g, this._b);
106 | return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this._a };
107 | },
108 | toHslString: function() {
109 | var hsl = rgbToHsl(this._r, this._g, this._b);
110 | var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100);
111 | return (this._a == 1) ?
112 | "hsl(" + h + ", " + s + "%, " + l + "%)" :
113 | "hsla(" + h + ", " + s + "%, " + l + "%, "+ this._roundA + ")";
114 | },
115 | toHex: function(allow3Char) {
116 | return rgbToHex(this._r, this._g, this._b, allow3Char);
117 | },
118 | toHexString: function(allow3Char) {
119 | return '#' + this.toHex(allow3Char);
120 | },
121 | toHex8: function(allow4Char) {
122 | return rgbaToHex(this._r, this._g, this._b, this._a, allow4Char);
123 | },
124 | toHex8String: function(allow4Char) {
125 | return '#' + this.toHex8(allow4Char);
126 | },
127 | toRgb: function() {
128 | return { r: mathRound(this._r), g: mathRound(this._g), b: mathRound(this._b), a: this._a };
129 | },
130 | toRgbString: function() {
131 | return (this._a == 1) ?
132 | "rgb(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ")" :
133 | "rgba(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ", " + this._roundA + ")";
134 | },
135 | toPercentageRgb: function() {
136 | return { r: mathRound(bound01(this._r, 255) * 100) + "%", g: mathRound(bound01(this._g, 255) * 100) + "%", b: mathRound(bound01(this._b, 255) * 100) + "%", a: this._a };
137 | },
138 | toPercentageRgbString: function() {
139 | return (this._a == 1) ?
140 | "rgb(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%)" :
141 | "rgba(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%, " + this._roundA + ")";
142 | },
143 | toName: function() {
144 | if (this._a === 0) {
145 | return "transparent";
146 | }
147 |
148 | if (this._a < 1) {
149 | return false;
150 | }
151 |
152 | return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false;
153 | },
154 | toFilter: function(secondColor) {
155 | var hex8String = '#' + rgbaToArgbHex(this._r, this._g, this._b, this._a);
156 | var secondHex8String = hex8String;
157 | var gradientType = this._gradientType ? "GradientType = 1, " : "";
158 |
159 | if (secondColor) {
160 | var s = tinycolor(secondColor);
161 | secondHex8String = '#' + rgbaToArgbHex(s._r, s._g, s._b, s._a);
162 | }
163 |
164 | return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr="+hex8String+",endColorstr="+secondHex8String+")";
165 | },
166 | toString: function(format) {
167 | var formatSet = !!format;
168 | format = format || this._format;
169 |
170 | var formattedString = false;
171 | var hasAlpha = this._a < 1 && this._a >= 0;
172 | var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "hex4" || format === "hex8" || format === "name");
173 |
174 | if (needsAlphaFormat) {
175 | // Special case for "transparent", all other non-alpha formats
176 | // will return rgba when there is transparency.
177 | if (format === "name" && this._a === 0) {
178 | return this.toName();
179 | }
180 | return this.toRgbString();
181 | }
182 | if (format === "rgb") {
183 | formattedString = this.toRgbString();
184 | }
185 | if (format === "prgb") {
186 | formattedString = this.toPercentageRgbString();
187 | }
188 | if (format === "hex" || format === "hex6") {
189 | formattedString = this.toHexString();
190 | }
191 | if (format === "hex3") {
192 | formattedString = this.toHexString(true);
193 | }
194 | if (format === "hex4") {
195 | formattedString = this.toHex8String(true);
196 | }
197 | if (format === "hex8") {
198 | formattedString = this.toHex8String();
199 | }
200 | if (format === "name") {
201 | formattedString = this.toName();
202 | }
203 | if (format === "hsl") {
204 | formattedString = this.toHslString();
205 | }
206 | if (format === "hsv") {
207 | formattedString = this.toHsvString();
208 | }
209 |
210 | return formattedString || this.toHexString();
211 | },
212 | clone: function() {
213 | return tinycolor(this.toString());
214 | },
215 |
216 | _applyModification: function(fn, args) {
217 | var color = fn.apply(null, [this].concat([].slice.call(args)));
218 | this._r = color._r;
219 | this._g = color._g;
220 | this._b = color._b;
221 | this.setAlpha(color._a);
222 | return this;
223 | },
224 | lighten: function() {
225 | return this._applyModification(lighten, arguments);
226 | },
227 | brighten: function() {
228 | return this._applyModification(brighten, arguments);
229 | },
230 | darken: function() {
231 | return this._applyModification(darken, arguments);
232 | },
233 | desaturate: function() {
234 | return this._applyModification(desaturate, arguments);
235 | },
236 | saturate: function() {
237 | return this._applyModification(saturate, arguments);
238 | },
239 | greyscale: function() {
240 | return this._applyModification(greyscale, arguments);
241 | },
242 | spin: function() {
243 | return this._applyModification(spin, arguments);
244 | },
245 |
246 | _applyCombination: function(fn, args) {
247 | return fn.apply(null, [this].concat([].slice.call(args)));
248 | },
249 | analogous: function() {
250 | return this._applyCombination(analogous, arguments);
251 | },
252 | complement: function() {
253 | return this._applyCombination(complement, arguments);
254 | },
255 | monochromatic: function() {
256 | return this._applyCombination(monochromatic, arguments);
257 | },
258 | splitcomplement: function() {
259 | return this._applyCombination(splitcomplement, arguments);
260 | },
261 | triad: function() {
262 | return this._applyCombination(triad, arguments);
263 | },
264 | tetrad: function() {
265 | return this._applyCombination(tetrad, arguments);
266 | }
267 | };
268 |
269 | // If input is an object, force 1 into "1.0" to handle ratios properly
270 | // String input requires "1.0" as input, so 1 will be treated as 1
271 | tinycolor.fromRatio = function(color, opts) {
272 | if (typeof color == "object") {
273 | var newColor = {};
274 | for (var i in color) {
275 | if (color.hasOwnProperty(i)) {
276 | if (i === "a") {
277 | newColor[i] = color[i];
278 | }
279 | else {
280 | newColor[i] = convertToPercentage(color[i]);
281 | }
282 | }
283 | }
284 | color = newColor;
285 | }
286 |
287 | return tinycolor(color, opts);
288 | };
289 |
290 | // Given a string or object, convert that input to RGB
291 | // Possible string inputs:
292 | //
293 | // "red"
294 | // "#f00" or "f00"
295 | // "#ff0000" or "ff0000"
296 | // "#ff000000" or "ff000000"
297 | // "rgb 255 0 0" or "rgb (255, 0, 0)"
298 | // "rgb 1.0 0 0" or "rgb (1, 0, 0)"
299 | // "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
300 | // "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
301 | // "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
302 | // "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
303 | // "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
304 | //
305 | function inputToRGB(color) {
306 |
307 | var rgb = { r: 0, g: 0, b: 0 };
308 | var a = 1;
309 | var s = null;
310 | var v = null;
311 | var l = null;
312 | var ok = false;
313 | var format = false;
314 |
315 | if (typeof color == "string") {
316 | color = stringInputToObject(color);
317 | }
318 |
319 | if (typeof color == "object") {
320 | if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
321 | rgb = rgbToRgb(color.r, color.g, color.b);
322 | ok = true;
323 | format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
324 | }
325 | else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
326 | s = convertToPercentage(color.s);
327 | v = convertToPercentage(color.v);
328 | rgb = hsvToRgb(color.h, s, v);
329 | ok = true;
330 | format = "hsv";
331 | }
332 | else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
333 | s = convertToPercentage(color.s);
334 | l = convertToPercentage(color.l);
335 | rgb = hslToRgb(color.h, s, l);
336 | ok = true;
337 | format = "hsl";
338 | }
339 |
340 | if (color.hasOwnProperty("a")) {
341 | a = color.a;
342 | }
343 | }
344 |
345 | a = boundAlpha(a);
346 |
347 | return {
348 | ok: ok,
349 | format: color.format || format,
350 | r: mathMin(255, mathMax(rgb.r, 0)),
351 | g: mathMin(255, mathMax(rgb.g, 0)),
352 | b: mathMin(255, mathMax(rgb.b, 0)),
353 | a: a
354 | };
355 | }
356 |
357 | // Conversion Functions
358 | // --------------------
359 |
360 | // rgbToHsl, rgbToHsv, hslToRgb, hsvToRgb modified from:
361 | //
362 |
363 | // rgbToRgb
364 | // Handle bounds / percentage checking to conform to CSS color spec
365 | //
366 | // *Assumes:* r, g, b in [0, 255] or [0, 1]
367 | // *Returns:* { r, g, b } in [0, 255]
368 | function rgbToRgb(r, g, b){
369 | return {
370 | r: bound01(r, 255) * 255,
371 | g: bound01(g, 255) * 255,
372 | b: bound01(b, 255) * 255
373 | };
374 | }
375 |
376 | // rgbToHsl
377 | // Converts an RGB color value to HSL.
378 | // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
379 | // *Returns:* { h, s, l } in [0,1]
380 | function rgbToHsl(r, g, b) {
381 |
382 | r = bound01(r, 255);
383 | g = bound01(g, 255);
384 | b = bound01(b, 255);
385 |
386 | var max = mathMax(r, g, b), min = mathMin(r, g, b);
387 | var h, s, l = (max + min) / 2;
388 |
389 | if(max == min) {
390 | h = s = 0; // achromatic
391 | }
392 | else {
393 | var d = max - min;
394 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
395 | switch(max) {
396 | case r: h = (g - b) / d + (g < b ? 6 : 0); break;
397 | case g: h = (b - r) / d + 2; break;
398 | case b: h = (r - g) / d + 4; break;
399 | }
400 |
401 | h /= 6;
402 | }
403 |
404 | return { h: h, s: s, l: l };
405 | }
406 |
407 | // hslToRgb
408 | // Converts an HSL color value to RGB.
409 | // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
410 | // *Returns:* { r, g, b } in the set [0, 255]
411 | function hslToRgb(h, s, l) {
412 | var r, g, b;
413 |
414 | h = bound01(h, 360);
415 | s = bound01(s, 100);
416 | l = bound01(l, 100);
417 |
418 | function hue2rgb(p, q, t) {
419 | if(t < 0) t += 1;
420 | if(t > 1) t -= 1;
421 | if(t < 1/6) return p + (q - p) * 6 * t;
422 | if(t < 1/2) return q;
423 | if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
424 | return p;
425 | }
426 |
427 | if(s === 0) {
428 | r = g = b = l; // achromatic
429 | }
430 | else {
431 | var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
432 | var p = 2 * l - q;
433 | r = hue2rgb(p, q, h + 1/3);
434 | g = hue2rgb(p, q, h);
435 | b = hue2rgb(p, q, h - 1/3);
436 | }
437 |
438 | return { r: r * 255, g: g * 255, b: b * 255 };
439 | }
440 |
441 | // rgbToHsv
442 | // Converts an RGB color value to HSV
443 | // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
444 | // *Returns:* { h, s, v } in [0,1]
445 | function rgbToHsv(r, g, b) {
446 |
447 | r = bound01(r, 255);
448 | g = bound01(g, 255);
449 | b = bound01(b, 255);
450 |
451 | var max = mathMax(r, g, b), min = mathMin(r, g, b);
452 | var h, s, v = max;
453 |
454 | var d = max - min;
455 | s = max === 0 ? 0 : d / max;
456 |
457 | if(max == min) {
458 | h = 0; // achromatic
459 | }
460 | else {
461 | switch(max) {
462 | case r: h = (g - b) / d + (g < b ? 6 : 0); break;
463 | case g: h = (b - r) / d + 2; break;
464 | case b: h = (r - g) / d + 4; break;
465 | }
466 | h /= 6;
467 | }
468 | return { h: h, s: s, v: v };
469 | }
470 |
471 | // hsvToRgb
472 | // Converts an HSV color value to RGB.
473 | // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
474 | // *Returns:* { r, g, b } in the set [0, 255]
475 | function hsvToRgb(h, s, v) {
476 |
477 | h = bound01(h, 360) * 6;
478 | s = bound01(s, 100);
479 | v = bound01(v, 100);
480 |
481 | var i = Math.floor(h),
482 | f = h - i,
483 | p = v * (1 - s),
484 | q = v * (1 - f * s),
485 | t = v * (1 - (1 - f) * s),
486 | mod = i % 6,
487 | r = [v, q, p, p, t, v][mod],
488 | g = [t, v, v, q, p, p][mod],
489 | b = [p, p, t, v, v, q][mod];
490 |
491 | return { r: r * 255, g: g * 255, b: b * 255 };
492 | }
493 |
494 | // rgbToHex
495 | // Converts an RGB color to hex
496 | // Assumes r, g, and b are contained in the set [0, 255]
497 | // Returns a 3 or 6 character hex
498 | function rgbToHex(r, g, b, allow3Char) {
499 |
500 | var hex = [
501 | pad2(mathRound(r).toString(16)),
502 | pad2(mathRound(g).toString(16)),
503 | pad2(mathRound(b).toString(16))
504 | ];
505 |
506 | // Return a 3 character hex if possible
507 | if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
508 | return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
509 | }
510 |
511 | return hex.join("");
512 | }
513 |
514 | // rgbaToHex
515 | // Converts an RGBA color plus alpha transparency to hex
516 | // Assumes r, g, b are contained in the set [0, 255] and
517 | // a in [0, 1]. Returns a 4 or 8 character rgba hex
518 | function rgbaToHex(r, g, b, a, allow4Char) {
519 |
520 | var hex = [
521 | pad2(mathRound(r).toString(16)),
522 | pad2(mathRound(g).toString(16)),
523 | pad2(mathRound(b).toString(16)),
524 | pad2(convertDecimalToHex(a))
525 | ];
526 |
527 | // Return a 4 character hex if possible
528 | if (allow4Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1) && hex[3].charAt(0) == hex[3].charAt(1)) {
529 | return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
530 | }
531 |
532 | return hex.join("");
533 | }
534 |
535 | // rgbaToArgbHex
536 | // Converts an RGBA color to an ARGB Hex8 string
537 | // Rarely used, but required for "toFilter()"
538 | function rgbaToArgbHex(r, g, b, a) {
539 |
540 | var hex = [
541 | pad2(convertDecimalToHex(a)),
542 | pad2(mathRound(r).toString(16)),
543 | pad2(mathRound(g).toString(16)),
544 | pad2(mathRound(b).toString(16))
545 | ];
546 |
547 | return hex.join("");
548 | }
549 |
550 | // equals
551 | // Can be called with any tinycolor input
552 | tinycolor.equals = function (color1, color2) {
553 | if (!color1 || !color2) { return false; }
554 | return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
555 | };
556 |
557 | tinycolor.random = function() {
558 | return tinycolor.fromRatio({
559 | r: mathRandom(),
560 | g: mathRandom(),
561 | b: mathRandom()
562 | });
563 | };
564 |
565 | // Modification Functions
566 | // ----------------------
567 | // Thanks to less.js for some of the basics here
568 | //
569 |
570 | function desaturate(color, amount) {
571 | amount = (amount === 0) ? 0 : (amount || 10);
572 | var hsl = tinycolor(color).toHsl();
573 | hsl.s -= amount / 100;
574 | hsl.s = clamp01(hsl.s);
575 | return tinycolor(hsl);
576 | }
577 |
578 | function saturate(color, amount) {
579 | amount = (amount === 0) ? 0 : (amount || 10);
580 | var hsl = tinycolor(color).toHsl();
581 | hsl.s += amount / 100;
582 | hsl.s = clamp01(hsl.s);
583 | return tinycolor(hsl);
584 | }
585 |
586 | function greyscale(color) {
587 | return tinycolor(color).desaturate(100);
588 | }
589 |
590 | function lighten (color, amount) {
591 | amount = (amount === 0) ? 0 : (amount || 10);
592 | var hsl = tinycolor(color).toHsl();
593 | hsl.l += amount / 100;
594 | hsl.l = clamp01(hsl.l);
595 | return tinycolor(hsl);
596 | }
597 |
598 | function brighten(color, amount) {
599 | amount = (amount === 0) ? 0 : (amount || 10);
600 | var rgb = tinycolor(color).toRgb();
601 | rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * - (amount / 100))));
602 | rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * - (amount / 100))));
603 | rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * - (amount / 100))));
604 | return tinycolor(rgb);
605 | }
606 |
607 | function darken (color, amount) {
608 | amount = (amount === 0) ? 0 : (amount || 10);
609 | var hsl = tinycolor(color).toHsl();
610 | hsl.l -= amount / 100;
611 | hsl.l = clamp01(hsl.l);
612 | return tinycolor(hsl);
613 | }
614 |
615 | // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
616 | // Values outside of this range will be wrapped into this range.
617 | function spin(color, amount) {
618 | var hsl = tinycolor(color).toHsl();
619 | var hue = (hsl.h + amount) % 360;
620 | hsl.h = hue < 0 ? 360 + hue : hue;
621 | return tinycolor(hsl);
622 | }
623 |
624 | // Combination Functions
625 | // ---------------------
626 | // Thanks to jQuery xColor for some of the ideas behind these
627 | //
628 |
629 | function complement(color) {
630 | var hsl = tinycolor(color).toHsl();
631 | hsl.h = (hsl.h + 180) % 360;
632 | return tinycolor(hsl);
633 | }
634 |
635 | function triad(color) {
636 | var hsl = tinycolor(color).toHsl();
637 | var h = hsl.h;
638 | return [
639 | tinycolor(color),
640 | tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
641 | tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l })
642 | ];
643 | }
644 |
645 | function tetrad(color) {
646 | var hsl = tinycolor(color).toHsl();
647 | var h = hsl.h;
648 | return [
649 | tinycolor(color),
650 | tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
651 | tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
652 | tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l })
653 | ];
654 | }
655 |
656 | function splitcomplement(color) {
657 | var hsl = tinycolor(color).toHsl();
658 | var h = hsl.h;
659 | return [
660 | tinycolor(color),
661 | tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}),
662 | tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l})
663 | ];
664 | }
665 |
666 | function analogous(color, results, slices) {
667 | results = results || 6;
668 | slices = slices || 30;
669 |
670 | var hsl = tinycolor(color).toHsl();
671 | var part = 360 / slices;
672 | var ret = [tinycolor(color)];
673 |
674 | for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) {
675 | hsl.h = (hsl.h + part) % 360;
676 | ret.push(tinycolor(hsl));
677 | }
678 | return ret;
679 | }
680 |
681 | function monochromatic(color, results) {
682 | results = results || 6;
683 | var hsv = tinycolor(color).toHsv();
684 | var h = hsv.h, s = hsv.s, v = hsv.v;
685 | var ret = [];
686 | var modification = 1 / results;
687 |
688 | while (results--) {
689 | ret.push(tinycolor({ h: h, s: s, v: v}));
690 | v = (v + modification) % 1;
691 | }
692 |
693 | return ret;
694 | }
695 |
696 | // Utility Functions
697 | // ---------------------
698 |
699 | tinycolor.mix = function(color1, color2, amount) {
700 | amount = (amount === 0) ? 0 : (amount || 50);
701 |
702 | var rgb1 = tinycolor(color1).toRgb();
703 | var rgb2 = tinycolor(color2).toRgb();
704 |
705 | var p = amount / 100;
706 |
707 | var rgba = {
708 | r: ((rgb2.r - rgb1.r) * p) + rgb1.r,
709 | g: ((rgb2.g - rgb1.g) * p) + rgb1.g,
710 | b: ((rgb2.b - rgb1.b) * p) + rgb1.b,
711 | a: ((rgb2.a - rgb1.a) * p) + rgb1.a
712 | };
713 |
714 | return tinycolor(rgba);
715 | };
716 |
717 | // Readability Functions
718 | // ---------------------
719 | // false
738 | // tinycolor.isReadable("#000", "#111",{level:"AA",size:"large"}) => false
739 | tinycolor.isReadable = function(color1, color2, wcag2) {
740 | var readability = tinycolor.readability(color1, color2);
741 | var wcag2Parms, out;
742 |
743 | out = false;
744 |
745 | wcag2Parms = validateWCAG2Parms(wcag2);
746 | switch (wcag2Parms.level + wcag2Parms.size) {
747 | case "AAsmall":
748 | case "AAAlarge":
749 | out = readability >= 4.5;
750 | break;
751 | case "AAlarge":
752 | out = readability >= 3;
753 | break;
754 | case "AAAsmall":
755 | out = readability >= 7;
756 | break;
757 | }
758 | return out;
759 |
760 | };
761 |
762 | // mostReadable
763 | // Given a base color and a list of possible foreground or background
764 | // colors for that base, returns the most readable color.
765 | // Optionally returns Black or White if the most readable color is unreadable.
766 | // *Example*
767 | // tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(); // "#112255"
768 | // tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString(); // "#ffffff"
769 | // tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString(); // "#faf3f3"
770 | // tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString(); // "#ffffff"
771 | tinycolor.mostReadable = function(baseColor, colorList, args) {
772 | var bestColor = null;
773 | var bestScore = 0;
774 | var readability;
775 | var includeFallbackColors, level, size ;
776 | args = args || {};
777 | includeFallbackColors = args.includeFallbackColors ;
778 | level = args.level;
779 | size = args.size;
780 |
781 | for (var i= 0; i < colorList.length ; i++) {
782 | readability = tinycolor.readability(baseColor, colorList[i]);
783 | if (readability > bestScore) {
784 | bestScore = readability;
785 | bestColor = tinycolor(colorList[i]);
786 | }
787 | }
788 |
789 | if (tinycolor.isReadable(baseColor, bestColor, {"level":level,"size":size}) || !includeFallbackColors) {
790 | return bestColor;
791 | }
792 | else {
793 | args.includeFallbackColors=false;
794 | return tinycolor.mostReadable(baseColor,["#fff", "#000"],args);
795 | }
796 | };
797 |
798 | // Big List of Colors
799 | // ------------------
800 | //
801 | var names = tinycolor.names = {
802 | aliceblue: "f0f8ff",
803 | antiquewhite: "faebd7",
804 | aqua: "0ff",
805 | aquamarine: "7fffd4",
806 | azure: "f0ffff",
807 | beige: "f5f5dc",
808 | bisque: "ffe4c4",
809 | black: "000",
810 | blanchedalmond: "ffebcd",
811 | blue: "00f",
812 | blueviolet: "8a2be2",
813 | brown: "a52a2a",
814 | burlywood: "deb887",
815 | burntsienna: "ea7e5d",
816 | cadetblue: "5f9ea0",
817 | chartreuse: "7fff00",
818 | chocolate: "d2691e",
819 | coral: "ff7f50",
820 | cornflowerblue: "6495ed",
821 | cornsilk: "fff8dc",
822 | crimson: "dc143c",
823 | cyan: "0ff",
824 | darkblue: "00008b",
825 | darkcyan: "008b8b",
826 | darkgoldenrod: "b8860b",
827 | darkgray: "a9a9a9",
828 | darkgreen: "006400",
829 | darkgrey: "a9a9a9",
830 | darkkhaki: "bdb76b",
831 | darkmagenta: "8b008b",
832 | darkolivegreen: "556b2f",
833 | darkorange: "ff8c00",
834 | darkorchid: "9932cc",
835 | darkred: "8b0000",
836 | darksalmon: "e9967a",
837 | darkseagreen: "8fbc8f",
838 | darkslateblue: "483d8b",
839 | darkslategray: "2f4f4f",
840 | darkslategrey: "2f4f4f",
841 | darkturquoise: "00ced1",
842 | darkviolet: "9400d3",
843 | deeppink: "ff1493",
844 | deepskyblue: "00bfff",
845 | dimgray: "696969",
846 | dimgrey: "696969",
847 | dodgerblue: "1e90ff",
848 | firebrick: "b22222",
849 | floralwhite: "fffaf0",
850 | forestgreen: "228b22",
851 | fuchsia: "f0f",
852 | gainsboro: "dcdcdc",
853 | ghostwhite: "f8f8ff",
854 | gold: "ffd700",
855 | goldenrod: "daa520",
856 | gray: "808080",
857 | green: "008000",
858 | greenyellow: "adff2f",
859 | grey: "808080",
860 | honeydew: "f0fff0",
861 | hotpink: "ff69b4",
862 | indianred: "cd5c5c",
863 | indigo: "4b0082",
864 | ivory: "fffff0",
865 | khaki: "f0e68c",
866 | lavender: "e6e6fa",
867 | lavenderblush: "fff0f5",
868 | lawngreen: "7cfc00",
869 | lemonchiffon: "fffacd",
870 | lightblue: "add8e6",
871 | lightcoral: "f08080",
872 | lightcyan: "e0ffff",
873 | lightgoldenrodyellow: "fafad2",
874 | lightgray: "d3d3d3",
875 | lightgreen: "90ee90",
876 | lightgrey: "d3d3d3",
877 | lightpink: "ffb6c1",
878 | lightsalmon: "ffa07a",
879 | lightseagreen: "20b2aa",
880 | lightskyblue: "87cefa",
881 | lightslategray: "789",
882 | lightslategrey: "789",
883 | lightsteelblue: "b0c4de",
884 | lightyellow: "ffffe0",
885 | lime: "0f0",
886 | limegreen: "32cd32",
887 | linen: "faf0e6",
888 | magenta: "f0f",
889 | maroon: "800000",
890 | mediumaquamarine: "66cdaa",
891 | mediumblue: "0000cd",
892 | mediumorchid: "ba55d3",
893 | mediumpurple: "9370db",
894 | mediumseagreen: "3cb371",
895 | mediumslateblue: "7b68ee",
896 | mediumspringgreen: "00fa9a",
897 | mediumturquoise: "48d1cc",
898 | mediumvioletred: "c71585",
899 | midnightblue: "191970",
900 | mintcream: "f5fffa",
901 | mistyrose: "ffe4e1",
902 | moccasin: "ffe4b5",
903 | navajowhite: "ffdead",
904 | navy: "000080",
905 | oldlace: "fdf5e6",
906 | olive: "808000",
907 | olivedrab: "6b8e23",
908 | orange: "ffa500",
909 | orangered: "ff4500",
910 | orchid: "da70d6",
911 | palegoldenrod: "eee8aa",
912 | palegreen: "98fb98",
913 | paleturquoise: "afeeee",
914 | palevioletred: "db7093",
915 | papayawhip: "ffefd5",
916 | peachpuff: "ffdab9",
917 | peru: "cd853f",
918 | pink: "ffc0cb",
919 | plum: "dda0dd",
920 | powderblue: "b0e0e6",
921 | purple: "800080",
922 | rebeccapurple: "663399",
923 | red: "f00",
924 | rosybrown: "bc8f8f",
925 | royalblue: "4169e1",
926 | saddlebrown: "8b4513",
927 | salmon: "fa8072",
928 | sandybrown: "f4a460",
929 | seagreen: "2e8b57",
930 | seashell: "fff5ee",
931 | sienna: "a0522d",
932 | silver: "c0c0c0",
933 | skyblue: "87ceeb",
934 | slateblue: "6a5acd",
935 | slategray: "708090",
936 | slategrey: "708090",
937 | snow: "fffafa",
938 | springgreen: "00ff7f",
939 | steelblue: "4682b4",
940 | tan: "d2b48c",
941 | teal: "008080",
942 | thistle: "d8bfd8",
943 | tomato: "ff6347",
944 | turquoise: "40e0d0",
945 | violet: "ee82ee",
946 | wheat: "f5deb3",
947 | white: "fff",
948 | whitesmoke: "f5f5f5",
949 | yellow: "ff0",
950 | yellowgreen: "9acd32"
951 | };
952 |
953 | // Make it easy to access colors via hexNames[hex]
954 | var hexNames = tinycolor.hexNames = flip(names);
955 |
956 | // Utilities
957 | // ---------
958 |
959 | // { 'name1': 'val1' } becomes { 'val1': 'name1' }
960 | function flip(o) {
961 | var flipped = { };
962 | for (var i in o) {
963 | if (o.hasOwnProperty(i)) {
964 | flipped[o[i]] = i;
965 | }
966 | }
967 | return flipped;
968 | }
969 |
970 | // Return a valid alpha value [0,1] with all invalid values being set to 1
971 | function boundAlpha(a) {
972 | a = parseFloat(a);
973 |
974 | if (isNaN(a) || a < 0 || a > 1) {
975 | a = 1;
976 | }
977 |
978 | return a;
979 | }
980 |
981 | // Take input from [0, n] and return it as [0, 1]
982 | function bound01(n, max) {
983 | if (isOnePointZero(n)) { n = "100%"; }
984 |
985 | var processPercent = isPercentage(n);
986 | n = mathMin(max, mathMax(0, parseFloat(n)));
987 |
988 | // Automatically convert percentage into number
989 | if (processPercent) {
990 | n = parseInt(n * max, 10) / 100;
991 | }
992 |
993 | // Handle floating point rounding errors
994 | if ((Math.abs(n - max) < 0.000001)) {
995 | return 1;
996 | }
997 |
998 | // Convert into [0, 1] range if it isn't already
999 | return (n % max) / parseFloat(max);
1000 | }
1001 |
1002 | // Force a number between 0 and 1
1003 | function clamp01(val) {
1004 | return mathMin(1, mathMax(0, val));
1005 | }
1006 |
1007 | // Parse a base-16 hex value into a base-10 integer
1008 | function parseIntFromHex(val) {
1009 | return parseInt(val, 16);
1010 | }
1011 |
1012 | // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
1013 | //
1014 | function isOnePointZero(n) {
1015 | return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
1016 | }
1017 |
1018 | // Check to see if string passed in is a percentage
1019 | function isPercentage(n) {
1020 | return typeof n === "string" && n.indexOf('%') != -1;
1021 | }
1022 |
1023 | // Force a hex value to have 2 characters
1024 | function pad2(c) {
1025 | return c.length == 1 ? '0' + c : '' + c;
1026 | }
1027 |
1028 | // Replace a decimal with it's percentage value
1029 | function convertToPercentage(n) {
1030 | if (n <= 1) {
1031 | n = (n * 100) + "%";
1032 | }
1033 |
1034 | return n;
1035 | }
1036 |
1037 | // Converts a decimal to a hex value
1038 | function convertDecimalToHex(d) {
1039 | return Math.round(parseFloat(d) * 255).toString(16);
1040 | }
1041 | // Converts a hex value to a decimal
1042 | function convertHexToDecimal(h) {
1043 | return (parseIntFromHex(h) / 255);
1044 | }
1045 |
1046 | var matchers = (function() {
1047 |
1048 | //
1049 | var CSS_INTEGER = "[-\\+]?\\d+%?";
1050 |
1051 | //
1052 | var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";
1053 |
1054 | // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
1055 | var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";
1056 |
1057 | // Actual matching.
1058 | // Parentheses and commas are optional, but not required.
1059 | // Whitespace can take the place of commas or opening paren
1060 | var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
1061 | var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
1062 |
1063 | return {
1064 | CSS_UNIT: new RegExp(CSS_UNIT),
1065 | rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
1066 | rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
1067 | hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
1068 | hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
1069 | hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
1070 | hsva: new RegExp("hsva" + PERMISSIVE_MATCH4),
1071 | hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1072 | hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
1073 | hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1074 | hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
1075 | };
1076 | })();
1077 |
1078 | // isValidCSSUnit
1079 | // Take in a single string / number and check to see if it looks like a CSS unit
1080 | // (see matchers above for definition).
1081 | function isValidCSSUnit(color) {
1082 | return !!matchers.CSS_UNIT.exec(color);
1083 | }
1084 |
1085 | // stringInputToObject
1086 | // Permissive string parsing. Take in a number of formats, and output an object
1087 | // based on detected format. Returns { r, g, b } or { h, s, l } or { h, s, v}
1088 | function stringInputToObject(color) {
1089 |
1090 | color = color.replace(trimLeft, '').replace(trimRight, '').toLowerCase();
1091 | var named = false;
1092 | if (names[color]) {
1093 | color = names[color];
1094 | named = true;
1095 | }
1096 | else if (color == 'transparent') {
1097 | return { r: 0, g: 0, b: 0, a: 0, format: "name" };
1098 | }
1099 |
1100 | // Try to match string input using regular expressions.
1101 | // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
1102 | // Just return an object and let the conversion functions handle that.
1103 | // This way the result will be the same whether the tinycolor is initialized with string or object.
1104 | var match;
1105 | if ((match = matchers.rgb.exec(color))) {
1106 | return { r: match[1], g: match[2], b: match[3] };
1107 | }
1108 | if ((match = matchers.rgba.exec(color))) {
1109 | return { r: match[1], g: match[2], b: match[3], a: match[4] };
1110 | }
1111 | if ((match = matchers.hsl.exec(color))) {
1112 | return { h: match[1], s: match[2], l: match[3] };
1113 | }
1114 | if ((match = matchers.hsla.exec(color))) {
1115 | return { h: match[1], s: match[2], l: match[3], a: match[4] };
1116 | }
1117 | if ((match = matchers.hsv.exec(color))) {
1118 | return { h: match[1], s: match[2], v: match[3] };
1119 | }
1120 | if ((match = matchers.hsva.exec(color))) {
1121 | return { h: match[1], s: match[2], v: match[3], a: match[4] };
1122 | }
1123 | if ((match = matchers.hex8.exec(color))) {
1124 | return {
1125 | r: parseIntFromHex(match[1]),
1126 | g: parseIntFromHex(match[2]),
1127 | b: parseIntFromHex(match[3]),
1128 | a: convertHexToDecimal(match[4]),
1129 | format: named ? "name" : "hex8"
1130 | };
1131 | }
1132 | if ((match = matchers.hex6.exec(color))) {
1133 | return {
1134 | r: parseIntFromHex(match[1]),
1135 | g: parseIntFromHex(match[2]),
1136 | b: parseIntFromHex(match[3]),
1137 | format: named ? "name" : "hex"
1138 | };
1139 | }
1140 | if ((match = matchers.hex4.exec(color))) {
1141 | return {
1142 | r: parseIntFromHex(match[1] + '' + match[1]),
1143 | g: parseIntFromHex(match[2] + '' + match[2]),
1144 | b: parseIntFromHex(match[3] + '' + match[3]),
1145 | a: convertHexToDecimal(match[4] + '' + match[4]),
1146 | format: named ? "name" : "hex8"
1147 | };
1148 | }
1149 | if ((match = matchers.hex3.exec(color))) {
1150 | return {
1151 | r: parseIntFromHex(match[1] + '' + match[1]),
1152 | g: parseIntFromHex(match[2] + '' + match[2]),
1153 | b: parseIntFromHex(match[3] + '' + match[3]),
1154 | format: named ? "name" : "hex"
1155 | };
1156 | }
1157 |
1158 | return false;
1159 | }
1160 |
1161 | function validateWCAG2Parms(parms) {
1162 | // return valid WCAG2 parms for isReadable.
1163 | // If input parms are invalid, return {"level":"AA", "size":"small"}
1164 | var level, size;
1165 | parms = parms || {"level":"AA", "size":"small"};
1166 | level = (parms.level || "AA").toUpperCase();
1167 | size = (parms.size || "small").toLowerCase();
1168 | if (level !== "AA" && level !== "AAA") {
1169 | level = "AA";
1170 | }
1171 | if (size !== "small" && size !== "large") {
1172 | size = "small";
1173 | }
1174 | return {"level":level, "size":size};
1175 | }
1176 |
1177 | this.tinycolor = tinycolor;
1178 |
1179 | })()`;
1180 | }
1181 | // It is hacky way to make this function will be compiled preferentially by less
1182 | // resolve error: `ReferenceError: colorPalette is not defined`
1183 | // https://github.com/ant-design/ant-motion/issues/44
1184 | .tinyColorMixin();
1185 |
--------------------------------------------------------------------------------
/src/themes/preset/index.less:
--------------------------------------------------------------------------------
1 | @import '../skins/default.less';
2 |
3 | body {
4 | height: 100%;
5 | background-color: @color_background;
6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | scroll-behavior: smooth;
10 | line-height: 1.5;
11 | }
12 |
13 | ::-webkit-scrollbar-thumb {
14 | background-color: #e6e6e6;
15 | }
16 |
17 | ::-webkit-scrollbar {
18 | width: 0px;
19 | height: 0px;
20 | }
21 |
22 | @media screen and (max-width:420px) {
23 | .ant-modal-wrap {
24 | display: flex;
25 | align-items: flex-end;
26 |
27 | .ant-modal {
28 | padding-bottom: 0;
29 | }
30 | }
31 | }
32 |
33 | .ant-select-dropdown {
34 | .ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
35 | background-color: transparent;
36 | }
37 | }
38 |
39 | .ant-spin-nested-loading>div>.ant-spin {
40 | position: fixed;
41 | top: 0;
42 | left: 180px;
43 | z-index: 999;
44 | display: flex;
45 | align-items: center;
46 | width: ~'calc(100vw - 180px)';
47 | height: 100vh;
48 | max-height: 100vh;
49 | box-sizing: border-box;
50 | background-color: rgba(255, 255, 255, 0.8);
51 | }
52 |
53 | .ant-modal-body {
54 | .ant-form-item-label {
55 | width: 100px;
56 | text-align: left;
57 | }
58 |
59 | .ant-form-item-control-wrapper {
60 | width: ~'calc(100% - 100px)';
61 | }
62 |
63 | .ant-form-item {
64 | margin-bottom: 12px;
65 | }
66 | }
67 |
68 | .ant-calendar {
69 |
70 | .ant-calendar-panel {
71 | background-color: transparent;
72 | }
73 |
74 | .ant-calendar-input {
75 | background-color: transparent;
76 | }
77 | }
78 |
79 | .ant-modal {
80 | .ant-modal-content {
81 | background-color: @color_background;
82 | box-sizing: border-box;
83 |
84 | .ant-modal-header {
85 | background-color: transparent;
86 |
87 | .ant-modal-title {
88 | color: @text-color;
89 | }
90 | }
91 |
92 | .ant-modal-footer {
93 | padding: 16px 24px;
94 | }
95 |
96 | .ant-menu-submenu-popup {
97 | background-color: transparent;
98 | }
99 |
100 | .ant-modal-confirm-title{
101 | color: @text-color;
102 | }
103 | }
104 | }
105 |
106 | .ant-modal-mask {
107 | background-color: rgba(55, 55, 55, 0.6) !important;
108 | }
109 |
110 | .ant-modal-content {
111 | box-shadow: none;
112 | }
113 |
114 | .ant-breadcrumb {
115 | font-size: 24px;
116 |
117 | .ant-breadcrumb-separator {
118 | color: black;
119 | }
120 |
121 | &>span {
122 | a {
123 | color: black;
124 | }
125 |
126 | &:last-child {
127 | font-weight: normal;
128 | }
129 | }
130 | }
131 |
132 | .ant-popover {
133 | background-color: transparent;
134 |
135 | .ant-popover-content {
136 | background-color: white;
137 | border-radius: @border-radius-base;
138 |
139 | .ant-popover-inner {
140 | background-color: transparent;
141 | box-shadow: 4px 4px 40px rgba(0, 0, 0, 0.05);
142 |
143 | .ant-popover-inner-content {
144 | background-color: transparent;
145 |
146 | .ant-list {
147 | background-color: transparent;
148 | }
149 | }
150 | }
151 | }
152 | }
153 |
154 | .ant-popover-placement-left>.ant-popover-content>.ant-popover-arrow,
155 | .ant-popover-placement-leftTop>.ant-popover-content>.ant-popover-arrow,
156 | .ant-popover-placement-leftBottom>.ant-popover-content>.ant-popover-arrow {
157 | border-top-color: @color_background;
158 | border-right-color: @color_background;
159 | }
160 |
161 | .ant-popover-placement-bottom,
162 | .ant-popover-placement-bottomLeft,
163 | .ant-popover-placement-bottomRight {
164 | padding-top: 0;
165 | }
166 |
167 | .ant-breadcrumb-link {
168 | .anticon+span {
169 | margin-left: 4px;
170 | }
171 | }
172 |
173 | .ant-table {
174 | background-color: transparent !important;
175 |
176 | .ant-table-container {
177 | table {
178 | border-collapse: separate;
179 | border-spacing: 0 4px;
180 | }
181 |
182 | .ant-table-fixed {
183 | background-color: transparent;
184 |
185 |
186 | }
187 |
188 | thead {
189 | tr {
190 | height: 48px !important;
191 |
192 | th {
193 | position: sticky;
194 | top: 72px;
195 | z-index: 10;
196 | height: auto;
197 | text-align: center;
198 | font-weight: normal;
199 | border-right: none;
200 | border-bottom: none;
201 | color: @color_unimportant_1;
202 | white-space: nowrap;
203 | padding-top: 10px;
204 | padding-bottom: 0px;
205 | background-color: @color_background;
206 | }
207 | }
208 | }
209 |
210 | thead>tr>th,
211 | tbody>tr>td {
212 | .ant-checkbox-inner {
213 | border: 1px solid #ccc;
214 | }
215 |
216 | .ant-radio-inner {
217 | border: 1px solid #ccc;
218 | }
219 | }
220 |
221 | tbody>tr>td {
222 | text-align: center;
223 | border-right: none;
224 | border-bottom: none;
225 | background-color: white;
226 | border: none;
227 | word-wrap: break-word;
228 | word-break: break-all;
229 | }
230 |
231 | tbody>tr>td:first-child {
232 | border-top-left-radius: @radius_normal;
233 | border-bottom-left-radius: @radius_normal;
234 | }
235 |
236 | tbody>tr>td:last-child {
237 | border-top-right-radius: @radius_normal;
238 | border-bottom-right-radius: @radius_normal;
239 | }
240 |
241 | tbody>tr {
242 | transition: all ease 0.3s;
243 |
244 | &:hover {
245 | box-shadow: @box_shadow_unit;
246 | border-radius: @radius_normal;
247 | }
248 | }
249 |
250 | &.ant-table-small {
251 | .ant-table-body>table {
252 | padding: 0;
253 | }
254 | }
255 | }
256 | }
257 |
258 | .ant-table-pagination {
259 | float: none !important;
260 | display: table;
261 | margin: 16px auto !important;
262 | }
263 |
264 | .ant-popover-inner {
265 | border: none;
266 | box-shadow: 0 0 20px rgba(100, 100, 100, 0.2);
267 | }
268 |
269 | .ant-form-item-control {
270 | vertical-align: middle;
271 | }
272 |
273 | .ant-select-dropdown-menu-item {
274 | padding: 12px 16px !important;
275 | }
276 |
277 | .margin-right {
278 | margin-right: 16px;
279 | }
280 |
281 | a:focus {
282 | text-decoration: none;
283 | }
284 |
285 | .drop-over-downward td {
286 | border-bottom: 2px dashed #1890ff !important;
287 | }
288 |
289 | .drop-over-upward td {
290 | border-top: 2px dashed #1890ff !important;
291 | }
--------------------------------------------------------------------------------
/src/themes/preset/init.less:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
--------------------------------------------------------------------------------
/src/themes/preset/mixin.less:
--------------------------------------------------------------------------------
1 | .backdrop_blur {
2 | background-color: white;
3 |
4 | @supports (backdrop-filter: blur(5px)) or (-webkit-backdrop-filter: blur(5px)) {
5 | background-color: rgba(255, 255, 255, 0.3) !important;
6 | -webkit-backdrop-filter: blur(5px) !important;
7 | backdrop-filter: blur(5px) !important;
8 | }
9 | }
--------------------------------------------------------------------------------
/src/themes/skins/default.less:
--------------------------------------------------------------------------------
1 | // https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less
2 | @import '../preset/helpers/colorPalette.less';
3 |
4 | //antd variables
5 | @primary-color: #f44336;
6 | @border-radius-base: 6px;
7 | @border-radius-sm: 6px;
8 | @font-size-base: 13px;
9 | @success-color: #52c41a;
10 | @warning-color: #faad14;
11 | @error-color: #f5222d;
12 | @hover-color: #eee;
13 |
14 | //custom variables
15 | @secondary-color: #cb2013;
16 | @color_background: @body-background;
17 | @color_border_light: #eeeeee;
18 | @color_unimportant_1: #aaa;
19 | @color_unimportant_2: #ddd;
20 | @color_danger: #F44336;
21 | @radius_normal: 6px;
22 | @box_shadow_section: 0 3px 8px 0 rgba(116, 129, 141, 0.1);
23 | @box_shadow_unit: 0px 0px 30px #ddd;
24 |
25 | //light theme variables
26 | @text-color: #000;
27 | @body-background: #f8f9fa;
28 | @component-background: #ffffff;
29 | @border-color-base: #f5f5f5;
30 | @border-color-split: #f5f5f5;
31 |
32 | //dark theme varialbles
33 | // @text-color: #ffffff;
34 | // @disabled-color: #333333;
35 | // @item-hover-bg: #333333;
36 | // @component-background: #202124;
37 | // @background-color-light: #333333;
38 | // @background-color-base: #333333;
39 | // @text-color-secondary: #333333;
40 | // @body-background: #202124;
41 | // @border-color-base: #292929;
42 | // @border-color-split: #292929;
43 |
44 | //global variables
45 | :root {
46 | --hover-color: @hover-color;
47 | --primary-color: @primary-color;
48 | --secondary-color: @secondary-color;
49 |
50 | --color_background: @color_background;
51 | --color_unimportant_2: @color_unimportant_2;
52 | --radius_normal: @radius_normal;
53 |
54 | --light_box_shadow_card: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1.5px -2px rgba(0, 0, 0, 0.1);
55 | --light_bg_hover: rgba(0, 0, 0, 0.22);
56 |
57 | //dark_mode
58 | --dark_color_block: #292a2d;
59 | }
--------------------------------------------------------------------------------
/src/utils/helpers/formatCheckOK.ts:
--------------------------------------------------------------------------------
1 | export default (data: any) => {
2 | if (!Array.isArray(data)) return false
3 |
4 | for (let item of data) {
5 | if (!item.hasOwnProperty('name') || !item.hasOwnProperty('data')) return false
6 | if (!Array.isArray(item.data)) return false
7 | }
8 |
9 | return true
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/model/index.ts:
--------------------------------------------------------------------------------
1 | import modelExtend from 'dva-model-extend'
2 |
3 | export const model = {
4 | reducers: {
5 | updateState(state, { payload }) {
6 | return {
7 | ...state,
8 | ...payload,
9 | }
10 | },
11 | },
12 | }
13 |
14 | const pageModel = modelExtend(model, {
15 | state: {
16 | list: [],
17 | pagination: {
18 | showSizeChanger: true,
19 | showQuickJumper: true,
20 | current: 1,
21 | total: 0,
22 | pageSize: 10,
23 | },
24 | },
25 |
26 | reducers: {
27 | querySuccess(state, { payload }) {
28 | const { list, pagination } = payload
29 |
30 | return {
31 | ...state,
32 | list,
33 | pagination: {
34 | ...state.pagination,
35 | ...pagination,
36 | },
37 | }
38 | },
39 | },
40 | })
41 |
42 | export default pageModel
43 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "importHelpers": true,
7 | "jsx": "react",
8 | "esModuleInterop": true,
9 | "sourceMap": true,
10 | "baseUrl": ".",
11 | "strict": false,
12 | "paths": {
13 | "R/*": [
14 | "./*"
15 | ],
16 | "@/*": [
17 | "./src/*"
18 | ],
19 | "@@/*": [
20 | "./src/.umi/*"
21 | ]
22 | },
23 | "allowSyntheticDefaultImports": true
24 | }
25 | }
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css'
2 | declare module '*.less'
3 | declare module '*.png'
4 |
5 | interface Window {
6 | less: any
7 | }
8 |
--------------------------------------------------------------------------------