├── .DS_Store
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .vscode
└── settings.json
├── .yarnrc
├── Dockerfile
├── LICENSE
├── README.md
├── _config.yml
├── config-overrides.js
├── default.conf
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo.png
├── logo.psd
├── logo.svg
├── logo192.png
├── logo256.png
├── logo512.png
├── manifest.json
├── robots.txt
└── root.txt
├── src
├── App.css
├── App.test.js
├── App.tsx
├── async-validator.d.ts
├── components
│ ├── hanzi-writer
│ │ └── index.tsx
│ ├── help
│ │ ├── help.css
│ │ └── index.js
│ ├── icons
│ │ ├── index.css
│ │ └── index.tsx
│ ├── matts
│ │ └── index.tsx
│ └── print
│ │ ├── index.css
│ │ └── index.js
├── global.d.ts
├── index.js
├── index.scss
├── layout
│ └── index.tsx
├── pages
│ ├── hanzi
│ │ ├── index.scss
│ │ └── index.tsx
│ ├── inspect
│ │ └── index.tsx
│ ├── setting
│ │ ├── font-list.js
│ │ ├── font.scss
│ │ ├── index.css
│ │ └── index.js
│ ├── task-clock
│ │ ├── components
│ │ │ ├── clock
│ │ │ │ ├── clock.ts
│ │ │ │ ├── index.scss
│ │ │ │ ├── index.tsx
│ │ │ │ └── utils.ts
│ │ │ └── list
│ │ │ │ └── index.tsx
│ │ ├── index.scss
│ │ ├── index.tsx
│ │ └── typing.d.ts
│ └── task-list
│ │ ├── components
│ │ └── task-form
│ │ │ ├── index.tsx
│ │ │ ├── task-date-rang-item.tsx
│ │ │ ├── task-form.tsx
│ │ │ ├── task-repeat-day-item.tsx
│ │ │ ├── task-repeat-type-item.tsx
│ │ │ ├── task-time-rang-item.tsx
│ │ │ └── typing.d.ts
│ │ ├── index.tsx
│ │ ├── service.ts
│ │ ├── storage.ts
│ │ ├── typing.d.ts
│ │ └── uitls.ts
├── rc-form.d.ts
├── react-app-env.d.ts
├── resource
│ ├── .DS_Store
│ ├── ads
│ │ └── 字帖-2020-02-25.xls
│ ├── fonts
│ │ ├── FZKTJW.TTF
│ │ ├── FZSJ-DQYBKSJW.TTF
│ │ ├── FZXKTJW.TTF
│ │ ├── FZYBKSJW.TTF
│ │ ├── FZZJ-FYJW.TTF
│ │ ├── PZHGBZTJW.TTF
│ │ ├── STFWXZKJW.TTF
│ │ └── TYZKSJW.TTF
│ └── images
│ │ └── IMG_7308.JPG
├── serviceWorker.js
└── setupTests.js
├── tsconfig.json
└── yarn.lock
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/.DS_Store
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: GitHub Actions Build and Deploy Demo
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 and Deploy
14 | uses: JamesIves/github-pages-deploy-action@master
15 | env:
16 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
17 | BRANCH: gh-pages
18 | FOLDER: build
19 | BUILD_SCRIPT: npm install && npm run build
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | #构建生产文件
10 | static/
11 | # asset-manifest.json
12 | # precache-manifest.*.js
13 | # service-worker.js
14 | # index.html
15 |
16 | # Diagnostic reports (https://nodejs.org/api/report.html)
17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
18 |
19 | # Runtime data
20 | pids
21 | *.pid
22 | *.seed
23 | *.pid.lock
24 | # Directory for instrumented libs generated by jscoverage/JSCover
25 | lib-cov
26 | build
27 |
28 | # Coverage directory used by tools like istanbul
29 | coverage
30 | *.lcov
31 |
32 | # nyc test coverage
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 | .grunt
37 |
38 | # Bower dependency directory (https://bower.io/)
39 | bower_components
40 |
41 | # node-waf configuration
42 | .lock-wscript
43 |
44 | # Compiled binary addons (https://nodejs.org/api/addons.html)
45 | build/Release
46 |
47 | # Dependency directories
48 | node_modules/
49 | jspm_packages/
50 |
51 | # TypeScript v1 declaration files
52 | typings/
53 |
54 | # TypeScript cache
55 | *.tsbuildinfo
56 |
57 | # Optional npm cache directory
58 | .npm
59 |
60 | # Optional eslint cache
61 | .eslintcache
62 |
63 | # Microbundle cache
64 | .rpt2_cache/
65 | .rts2_cache_cjs/
66 | .rts2_cache_es/
67 | .rts2_cache_umd/
68 |
69 | # Optional REPL history
70 | .node_repl_history
71 |
72 | # Output of 'npm pack'
73 | *.tgz
74 |
75 | # Yarn Integrity file
76 | .yarn-integrity
77 |
78 | # dotenv environment variables file
79 | .env
80 | .env.test
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 |
85 | # Next.js build output
86 | .next
87 |
88 | # Nuxt.js build / generate output
89 | .nuxt
90 | dist
91 |
92 | # Gatsby files
93 | .cache/
94 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
95 | # https://nextjs.org/blog/next-9-1#public-directory-support
96 | # public
97 |
98 | # vuepress build output
99 | .vuepress/dist
100 |
101 | # Serverless directories
102 | .serverless/
103 |
104 | # FuseBox cache
105 | .fusebox/
106 |
107 | # DynamoDB Local files
108 | .dynamodb/
109 |
110 | # TernJS port file
111 | .tern-port
112 |
113 |
114 | # ignore friday config
115 | .vscode/friday.json
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "Hanzi"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | registry "https://registry.npm.taobao.org"
2 | sass_binary_site "https://npm.taobao.com/mirrors/node-sass/"
3 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:latest
2 |
3 | ADD default.conf /etc/nginx/conf.d/
4 | ADD build/. /usr/share/nginx/html/
5 | RUN chmod -R 777 /usr/share/nginx/html/
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 leoyin
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 | 方正字体:https://www.foundertype.com/
3 |
4 | 自动绘制功能:https://hanziwriter.org
5 |
6 | ttf2svg : https://www.npmjs.com/package/ttf2svg
7 | ## Available Scripts
8 |
9 |
10 | docker stop copybook ; docker rm copybook ; docker rmi copybook ; docker build ./ -t copybook && docker run --name copybook -d -p 80:80 copybook
11 |
12 | In the project directory, you can run:
13 |
14 | ### `yarn start`
15 |
16 | Runs the app in the development mode.
17 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
18 |
19 | The page will reload if you make edits.
20 | You will also see any lint errors in the console.
21 |
22 | ### `yarn test`
23 |
24 | Launches the test runner in the interactive watch mode.
25 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
26 |
27 | ### `yarn build`
28 |
29 | Builds the app for production to the `build` folder.
30 | It correctly bundles React in production mode and optimizes the build for the best performance.
31 |
32 | The build is minified and the filenames include the hashes.
33 | Your app is ready to be deployed!
34 |
35 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
36 |
37 | ### `yarn eject`
38 |
39 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
40 |
41 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
42 |
43 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
44 |
45 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
46 |
47 | ## Learn More
48 |
49 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
50 |
51 | To learn React, check out the [React documentation](https://reactjs.org/).
52 |
53 | ### Code Splitting
54 |
55 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
56 |
57 | ### Analyzing the Bundle Size
58 |
59 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
60 |
61 | ### Making a Progressive Web App
62 |
63 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
64 |
65 | ### Advanced Configuration
66 |
67 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
68 |
69 | ### Deployment
70 |
71 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
72 |
73 | ### `yarn build` fails to minify
74 |
75 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
76 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-merlot
--------------------------------------------------------------------------------
/config-overrides.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = function override(config, env) {
3 | // do stuff with the webpack config...
4 | return config
5 | };
6 |
7 |
8 | // const { override, fixBabelImports } = require('customize-cra');
9 | // module.exports = override(
10 | // fixBabelImports('import', {
11 | // libraryName: 'antd-mobile',
12 | // style: 'css',
13 | // }),
14 | // );
--------------------------------------------------------------------------------
/default.conf:
--------------------------------------------------------------------------------
1 |
2 | server {
3 | listen 80;
4 | listen [::]:80;
5 | server_name localhost;
6 |
7 | #access_log /var/log/nginx/host.access.log main;
8 |
9 | location / {
10 | root /usr/share/nginx/html;
11 | index index.html index.htm;
12 | }
13 |
14 | #error_page 404 /404.html;
15 |
16 | # redirect server error pages to the static page /50x.html
17 | #
18 | error_page 500 502 503 504 /50x.html;
19 | location = /50x.html {
20 | root /usr/share/nginx/html;
21 | }
22 |
23 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80
24 | #
25 | #location ~ \.php$ {
26 | # proxy_pass http://127.0.0.1;
27 | #}
28 |
29 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
30 | #
31 | #location ~ \.php$ {
32 | # root html;
33 | # fastcgi_pass 127.0.0.1:9000;
34 | # fastcgi_index index.php;
35 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
36 | # include fastcgi_params;
37 | #}
38 |
39 | # deny access to .htaccess files, if Apache's document root
40 | # concurs with nginx's one
41 | #
42 | #location ~ /\.ht {
43 | # deny all;
44 | #}
45 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": "./",
6 | "dependencies": {
7 | "@testing-library/jest-dom": "^4.2.4",
8 | "@testing-library/react": "^9.3.2",
9 | "@testing-library/user-event": "^7.1.2",
10 | "@types/jest": "^25.1.3",
11 | "@types/node": "^13.7.7",
12 | "@types/react": "^16.9.23",
13 | "@types/react-dom": "^16.9.5",
14 | "@types/react-router-dom": "^5.1.3",
15 | "alife-logger": "^1.8.6",
16 | "antd-mobile": "^2.3.1",
17 | "async-validator": "^3.3.0",
18 | "canvas2image": "^1.0.5",
19 | "dayjs": "^1.8.29",
20 | "hanzi-writer": "^2.2.0",
21 | "html2canvas": "^1.0.0-rc.7",
22 | "rc-form": "^2.4.11",
23 | "react": "^16.12.0",
24 | "react-dom": "^16.12.0",
25 | "react-router-dom": "^5.1.2",
26 | "react-scripts": "3.4.0",
27 | "typescript": "^3.8.3"
28 | },
29 | "scripts": {
30 | "start": "react-app-rewired start",
31 | "build": "react-app-rewired build",
32 | "test": "react-app-rewired test --env=jsdom",
33 | "eject": "react-app-rewired eject"
34 | },
35 | "eslintConfig": {
36 | "extends": "react-app"
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | },
50 | "devDependencies": {
51 | "babel-plugin-import": "^1.13.0",
52 | "customize-cra": "^0.9.1",
53 | "react-app-rewired": "^2.1.5",
54 | "sass": "^1.53.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
37 |
46 | 学习辅助工具
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/public/logo.png
--------------------------------------------------------------------------------
/public/logo.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/public/logo.psd
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/public/logo192.png
--------------------------------------------------------------------------------
/public/logo256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/public/logo256.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "学习辅助",
3 | "name": "学习辅助工具",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo256.png",
17 | "type": "image/png",
18 | "sizes": "256x256"
19 | },
20 | {
21 | "src": "logo512.png",
22 | "type": "image/png",
23 | "sizes": "512x512"
24 | }
25 | ],
26 | "start_url": "./index.html",
27 | "display": "standalone",
28 | "theme_color": "#108ee9",
29 | "background_color": "#ffffff"
30 | }
31 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/root.txt:
--------------------------------------------------------------------------------
1 | 69071f8048391c52cc811abefe16b88d
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | /* max-width: 900px; */
4 | margin: auto;
5 | background-color: #fffbf2;
6 | }
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { HashRouter, Route, Switch, Redirect } from "react-router-dom";
3 |
4 | // import logo from './logo.svg';
5 | import "antd-mobile/dist/antd-mobile.css"; // or 'antd-mobile/dist/antd-mobile.less'
6 | import "./App.css";
7 | import Hanzi from "./pages/hanzi/index";
8 | import Setting from "./pages/setting";
9 | import Layout from "./layout/index";
10 | import Inspect from "./pages/inspect";
11 | import Clock from "./pages/task-clock";
12 | import TaskList from "./pages/task-list/index";
13 | // declare var global: any;
14 | declare var window: any;
15 |
16 | // global = window;
17 | window._czc = window._czc || [];
18 |
19 | function App() {
20 | return (
21 |
22 |
23 |
24 | {/*
25 |
26 | */}
27 |
28 | (
38 |
39 |
40 |
41 |
42 |
43 |
44 | {/* */}
45 |
46 | )}
47 | />
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export default App;
55 |
--------------------------------------------------------------------------------
/src/async-validator.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace AsyncValidator {
2 | type ValidateTrigger = "onBlur"
3 | interface RuleRange {
4 | min: number
5 | max: number
6 | }
7 |
8 | interface RuleString {
9 | type?: "string"
10 | pattern?: RegExp
11 | /**字符串长度范围 */
12 | range?: RuleRange
13 | /**字符串长度 */
14 | len?: number
15 | /**是否忽略空格 */
16 | whitespace?: boolean
17 | }
18 |
19 | interface RuleBoolean {
20 | type?: "boolean"
21 | }
22 | interface RuleMethod {
23 | type?: "method"
24 | }
25 | interface RuleRegexp {
26 | type?: "regexp"
27 | pattern: RegExp
28 | }
29 | interface RuleNumber {
30 | type?: "number"
31 | /**数组值大小范围(包含)*/
32 | range?: RuleRange
33 | /**小数精度 */
34 | len?: number
35 | }
36 | interface RuleInteger {
37 | type?: "interger"
38 | /**数组值大小范围(包含)*/
39 | range?: RuleRange
40 |
41 | }
42 | interface RuleFloat extends RuleNumber {
43 | type?: "float"
44 | }
45 | interface RuleArray {
46 | type?: "array"
47 | range?: RuleRange
48 | len?: number
49 | }
50 | interface RuleObject {
51 | type?: "object"
52 | /**Object 类型深度校验 */
53 | fields?: {
54 | [field: string]: Rule | Rule[]
55 | }
56 | }
57 | interface RuleEnum {
58 | type?: "enum"
59 | enum: string[]
60 | }
61 | interface RuleDate {
62 | type?: "date"
63 | }
64 | interface RuleUrl {
65 | type?: "url"
66 | }
67 | interface RuleHex {
68 | type?: "hex"
69 | }
70 | interface RuleEmail {
71 | type?: "email"
72 | }
73 | interface RuleAny {
74 | type?: "any"
75 | }
76 |
77 |
78 | type ValidateResult = boolean | Error | Error[]
79 | interface ValidateFunction {
80 | (
81 | rule: Rule | Rule[],
82 | value: any,
83 | callback?: (errors: ValidateResult) => void,
84 | source?: any,
85 | options?: { message: string }
86 | ): TReturn
87 | }
88 | type Rule = (RuleString) & {
89 | required?: boolean
90 | /**校验前数据清洗 */
91 | transform?: (value: any) => any
92 | /**自定义校验 */
93 | validator?: ValidateFunction
94 | /**异步校验 */
95 | asyncValidator?: ValidateFunction>
96 | /**校验异常文案,使用Function 方式,用来支如文案动态计算等支出 */
97 | message?: string | { (): any }
98 | }
99 | type RuleType = ValidateFunction | Rule[]
100 | }
101 | declare module "async-validator" {
102 | export class AsyncValidator{
103 | constructor(descriptor: {
104 | [field: keyof ISource]: AsyncValidator.RuleType
105 | })
106 | validate: {
107 | (
108 | source: {
109 | [field: keyof ISource]: any
110 | },
111 | options?: {
112 | suppressWarning?: boolean
113 | first?: boolean
114 | firstFields?: boolean | (keyof ISource)[]
115 | },
116 | callback: (errors: any, fields: any) => void
117 | )
118 | : Promise
119 | }
120 | messages: {
121 | (any): void
122 | }
123 | }
124 | export default AsyncValidator
125 | }
126 | // export
--------------------------------------------------------------------------------
/src/components/hanzi-writer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Modal } from 'antd-mobile';
3 | const HanziWriter = require('hanzi-writer');
4 | export default class Writer extends React.Component<{ visible: boolean, onClose: () => void }> {
5 | element?: Element | null
6 | writer?: any
7 | componentDidMount() {
8 | if (this.element) {
9 | this.write()
10 | }
11 | }
12 | componentDidUpdate() {
13 | if (this.element) {
14 | this.write();
15 | }
16 | }
17 | write() {
18 | const { children: word } = this.props;
19 | this.writer = HanziWriter.create(this.element, word, {
20 | width: 300,
21 | height: 300,
22 | padding: 30,
23 | showOutline: true,
24 |
25 | // strokeColor:"#c43b3b",
26 | // radicalColor:"#333333"
27 | });
28 | this.writer.animateCharacter();
29 | }
30 | render() {
31 | const { visible, children, ...props } = this.props;
32 | if (!visible || !children) { return null }
33 | return
40 |
57 |
61 | {`
62 | 笔画顺序为楷体简体
63 | 版权信息@https://hanziwriter.org
64 | `}
65 |
66 |
67 | }
68 | }
--------------------------------------------------------------------------------
/src/components/help/help.css:
--------------------------------------------------------------------------------
1 | .icon-help {
2 | font-size: 28px;
3 | margin-right: 8px;
4 | color: #dddddd;
5 | }
6 | .help-weixi-code {
7 | background-image: url(../../resource/images/IMG_7308.JPG);
8 | width: 300px;
9 | height: 300px;
10 | background-size: contain;
11 | background-repeat: no-repeat;
12 | background-position: center;
13 | margin: auto;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/help/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Modal } from 'antd-mobile';
3 | import './help.css';
4 | export default () => {
5 | const [visible, setVisible] = useState(false)
6 | return
42 | }
--------------------------------------------------------------------------------
/src/components/icons/index.css:
--------------------------------------------------------------------------------
1 | @import "//at.alicdn.com/t/font_1656641_i54wuos68no.css";
2 | .iconfont {
3 | }
4 | .icon-middle{
5 | font-size: 16px;
6 | }
7 | .icon-large{
8 | font-size: 20;
9 | }
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/icons/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './index.css';
3 | type IconType = "scan" | "zhong-o" | "zhong" | "setting"
4 | interface IconProps {
5 | type: IconType
6 | size?: "middle" | "large" | "small"
7 | className?: string
8 | }
9 | export default (
10 | { size = "middle", type, className = "" }: IconProps
11 | ) => ();
14 |
--------------------------------------------------------------------------------
/src/components/matts/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HanziWriter from '../hanzi-writer';
3 |
4 | export default class Copybook extends React.Component<{ size: number, font: string, type: string, children: string }> {
5 | state = {
6 | visible: false
7 | }
8 | render() {
9 | const { size, type, font: fontFamily, children } = this.props
10 | const { visible } = this.state;
11 | return
14 |
43 | {
46 | this.setState({ visible: false })
47 | }}
48 | >{children}
49 |
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/print/index.css:
--------------------------------------------------------------------------------
1 | .icon-help {
2 | font-size: 28px;
3 | margin-right: 8px;
4 | color: #dddddd;
5 | }
6 | .help-weixi-code {
7 | background-image: url(../../resource/images/IMG_7308.JPG);
8 | width: 300px;
9 | height: 300px;
10 | background-size: contain;
11 | background-repeat: no-repeat;
12 | background-position: center;
13 | margin: auto;
14 | }
15 |
16 | .copybook-print{
17 | /* width: 1240px;
18 | height: 1757px; */
19 | /* padding: 40px; */
20 | display: none;
21 | }
22 |
23 | @media print{
24 | .copybook-print{
25 | display: block;
26 | }
27 | .am-tabs-tab-bar-wrap{
28 | display: none;
29 | }
30 | .App{
31 | display: none;
32 | }
33 | }
--------------------------------------------------------------------------------
/src/components/print/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Modal,Button } from 'antd-mobile';
3 | // import html2canvas from 'html2canvas';
4 | // import {saveAsPNG} from 'canvas2image';
5 | import './index.css';
6 | export default (props) => {
7 | const [visible, setVisible] = useState(false)
8 | // const [canvas,updateCanvas] = useState(null);
9 | return
10 |
27 | {/*
{
30 | window._czc && window._czc.push(["_trackEvent", "help", "click", '打印', 1, 'print_btn']);
31 | window.print();
32 | setVisible(true)
33 | }}
34 | /> */}
35 | {
38 | setVisible(false)
39 | props.onCancel();
40 | }}
41 | style={{
42 | // height: "600px"
43 | }}
44 | > {
46 | setVisible(false)
47 | props.onCancel();
48 | }}
49 | >
50 |
51 |
52 | }
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/global.d.ts
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.scss';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.register();
13 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | @import './pages/setting/font.scss';
2 |
3 | body {
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
7 | sans-serif;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | }
11 |
12 | code {
13 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
14 | monospace;
15 | }
16 |
--------------------------------------------------------------------------------
/src/layout/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TabBar, } from 'antd-mobile';
3 | import Icon from '../components/icons/index';
4 | interface LayoutProps {
5 | children: any;
6 | history: any;
7 | location: any;
8 | }
9 | export default ({ children, history, location, ...props }: LayoutProps) => {
10 | const pathname = location.pathname
11 | // const history = useHistory();
12 | return (
13 |
14 |
15 |
19 |
24 | }
28 | selectedIcon={}
29 | selected={pathname.indexOf("task") === 1}
30 | onPress={() => {
31 | history.push("/task-clock")
32 | }}>
33 | {pathname.indexOf("task") === 1 ? children : null}
34 |
35 | }
39 | selectedIcon={}
40 | selected={pathname === "/" || pathname === '/hanzi'}
41 | onPress={() => {
42 | history.push("/hanzi")
43 | // browserHistory.push('/questions')
44 | }}
45 | >
46 | {pathname === '/hanzi' ? children : null}
47 |
48 | }
52 | selectedIcon={}
53 | selected={pathname === '/setting'}
54 | onPress={() => {
55 | history.push("/setting")
56 | }}
57 | >
58 | {pathname === '/setting' ? children : null}
59 |
60 |
61 |
62 |
63 |
64 | )
65 | }
--------------------------------------------------------------------------------
/src/pages/hanzi/index.scss:
--------------------------------------------------------------------------------
1 | .words-input {
2 | height: 30px;
3 | width: 95%;
4 | font-size: 20px;
5 | border: 1px solid #ff6600;
6 | padding: 4px 20px;
7 | font-family: cursive;
8 | border-radius: 10px;
9 | box-shadow: 0px 0px 4px #ff6600;
10 | }
11 |
12 | .copybook-page {
13 | border: 1px solid red;
14 | padding: 4px;
15 | margin: 8px;
16 | }
17 | .copybook-page-box {
18 | display: flex;
19 | flex-wrap: wrap;
20 | padding: 4px;
21 | width: 100%;
22 | border: 2px solid red;
23 | }
24 |
25 | .my-radio {
26 | padding: 2.5px;
27 | border: 1px solid #ccc;
28 | border-radius: 50%;
29 | margin-right: 5px;
30 | }
31 |
--------------------------------------------------------------------------------
/src/pages/hanzi/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { NavBar, TextareaItem, } from 'antd-mobile';
3 | import 'antd-mobile/dist/antd-mobile.css'; // or 'antd-mobile/dist/antd-mobile.less'
4 | import '.'
5 | import './index.scss';
6 | import Matts from '../../components/matts';
7 | import Print from '../../components/print/index';
8 | declare var window: any;
9 | window._czc = window._czc || [];
10 | function App() {
11 | const [str, setWords] = useState(window.localStorage.getItem("current.words") || "");
12 | const size = parseInt(window.localStorage.getItem("setting.size"), 10) || 160
13 | const type = window.localStorage.getItem("setting.type") || "tian";
14 | const font = window.localStorage.getItem("setting.font-family") || "FZKTJW";
15 | // const words = str.split("");
16 | const printWords = str.split("\n").map((item: string) => {
17 | const lent = item.length % 12;
18 |
19 | if (lent === 0 && item) {
20 | return item;
21 | }
22 | const ret = item + " ".slice(1, 12 - lent+1);
23 | console.log(ret,item.length,16-lent,ret.length)
24 | return ret;
25 | }).join("").split("");
26 |
27 | const words =printWords;
28 | const [isPrint, setIsPrint] = useState(false);
29 | return (
30 |
31 | {isPrint ?
32 |
33 |
34 | {printWords.map((word: string, i: number) => {word})}
35 |
36 |
37 | : null}
38 |
39 |
40 |
41 |
42 |
{
53 | window.localStorage.setItem("current.words", v);
54 | setWords(v)
55 | }}
56 | autoHeight
57 | />
58 |
59 |
60 | {words.map((word: string, i: number) => {word})}
61 |
62 |
63 |
68 | 字体版权: 方正字体库(https://www.foundertype.com/)
69 |
70 |
71 |
72 |
setIsPrint(true)} onCancel={() => setIsPrint(false)} />}
79 | >汉字字帖
80 |
81 |
82 | );
83 | }
84 |
85 | export default App;
86 |
--------------------------------------------------------------------------------
/src/pages/inspect/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { NavBar, InputItem } from 'antd-mobile';
3 |
4 | export default () => {
5 | let canvasElement: HTMLCanvasElement | null;
6 | let videoElement: HTMLVideoElement | null;
7 |
8 | if (!navigator.mediaDevices) {
9 | return
10 | 请使用Chrome 等浏览器获取更好的体验效果
11 |
12 | }
13 | const [word, setWord] = useState("我");
14 | const videoPort = 375;
15 | const [size] = useState(200);
16 | const fontFamily = localStorage.getItem("setting.font-family");
17 | useEffect(() => {
18 | let videoInput: MediaStream | null;
19 | if (videoElement) {
20 | navigator.mediaDevices.enumerateDevices()
21 | .then(devices => devices.filter(({ kind }) => kind === "videoinput")[0])
22 | .then(({ deviceId }) => {
23 | return navigator.mediaDevices.getUserMedia({
24 | audio: false,
25 | video: {
26 | deviceId,
27 | width: 500,
28 | height: 500
29 | }
30 | })
31 | }).then(stream => {
32 | videoInput = stream
33 | if (videoElement && canvasElement) {
34 | videoElement.srcObject = stream;
35 | const ctx = canvasElement.getContext("2d");
36 | if (ctx) {
37 | const drawFrame = () => {
38 | // if (videoElement) {
39 | ctx.clearRect(0, 0, videoPort, videoPort);
40 | // ctx.drawImage(videoElement, 0, 0, videoPort, videoPort)
41 | // }
42 | const start = (videoPort - size) / 2;
43 | const center = videoPort / 2;
44 | const end = videoPort - start;
45 | ctx.fillStyle = "rgba(40, 40, 40, 0.6)";
46 | ctx.fillRect(0, 0, videoPort, videoPort);
47 | ctx.clearRect(start, start, size - 2, size - 2);
48 | ctx.strokeStyle = "#666666";
49 | ctx.strokeRect(start, start, size - 2, size - 2);
50 |
51 | ctx.strokeStyle = "#666666";
52 | ctx.setLineDash([size / 40, size / 40])
53 | // ctx.strokeRect(size / 4, size / 4, size / 2, size / 2);
54 | // 横中线
55 | ctx.moveTo(center, start);
56 | ctx.lineTo(center, end);
57 | // 竖中线
58 | ctx.moveTo(start, center);
59 | ctx.lineTo(end, center);
60 | ctx.stroke();
61 |
62 | ctx.strokeStyle = "#999999";
63 | // ctx.setLineDash([size / 40, size / 40])
64 | // 右斜线
65 | ctx.moveTo(start, start);
66 | ctx.lineTo(end, end);
67 | // 左斜线
68 | ctx.moveTo(start, end);
69 | ctx.lineTo(end, start);
70 | ctx.stroke();
71 |
72 | ctx.setLineDash([0, 0]);
73 | ctx.textAlign = "center"
74 | ctx.textBaseline = "middle"
75 | // ctx.fillStyle = "text-shadow: 1px 1px #000,-1px -1px #000, 1px -1px #000, -1px 1px #000; "
76 | ctx.font = size * 0.7 + "px " + fontFamily;
77 | ctx.strokeStyle = "red";
78 | ctx.strokeText(word, videoPort / 2, videoPort / 2 * 1.09);
79 | requestAnimationFrame(drawFrame);
80 | }
81 | drawFrame();
82 | }
83 | }
84 | });
85 | }
86 | return () => {
87 | if (videoInput) {
88 | videoInput.getVideoTracks()[0].stop();
89 | }
90 | }
91 | })
92 | return (
93 |
实用性功能,有问题请微信群反馈
94 |
setWord(value ? value[0] : "")}
97 | />
98 |
101 |
122 |
123 | )
124 | }
--------------------------------------------------------------------------------
/src/pages/setting/font-list.js:
--------------------------------------------------------------------------------
1 | // 字体来源:https://www.foundertype.com/
2 | export default [{
3 | label: "方正楷体简体",
4 | value: "FZKTJW",
5 | offset: { x: 1, y: 1.09 }
6 | // }, {
7 | // label: "方正新楷体简体",
8 | // value: "FZXKTJW"
9 | // }, {
10 | // label: "田英章楷书",
11 | // value: "TYZKSJW"
12 | // }, {
13 | // label: "方正硬笔楷书简体",
14 | // value: "FZYBKSJW",
15 | // }, {
16 | // label: "庞中华钢笔字体",
17 | // value: "PZHGBZTJW",
18 | // }, {
19 | // label: "方正字迹-仿颜简体 ",
20 | // value: "FZZJ-FYJW"
21 | // }, {
22 | // label: "书体坊王羲之楷",
23 | // value: "STFWXZKJW"
24 | },
25 | // {
26 | // label: "方正手迹-丁谦硬笔楷书",
27 | // value: "FZSJ-DQYBKSJW"
28 | // }
29 | ]
--------------------------------------------------------------------------------
/src/pages/setting/font.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "FZKTJW";
3 | src: url("../../resource/fonts/FZKTJW.TTF") format("truetype"); /* iOS 4.1- */
4 | font-style: normal;
5 | font-weight: normal;
6 | }
7 |
8 | // @font-face {
9 | // font-family: "FZXKTJW";
10 | // /* src: url("../../resource/fonts/FZXKTJW.TTF") format("truetype"); iOS 4.1- */
11 | // font-style: normal;
12 | // font-weight: normal;
13 | // }
14 |
15 |
16 | // @font-face {
17 | // font-family: "STFWXZKJW";
18 | // /* src: url("../../resource/fonts/STFWXZKJW.TTF") format("truetype"); iOS 4.1- */
19 | // font-style: normal;
20 | // font-weight: normal;
21 | // }
22 |
23 | // @font-face {
24 | // font-family: "FZZJ-FYJW";
25 | // /* src: url("../../resource/fonts/FZZJ-FYJW.TTF") format("truetype"); iOS 4.1- */
26 | // font-style: normal;
27 | // font-weight: normal;
28 | // }
29 | // @font-face {
30 | // font-family: "FZSJ-DQYBKSJW";
31 | // /* src: url("../../resource/fonts/FZSJ-DQYBKSJW.TTF") format("truetype"); iOS 4.1- */
32 | // font-style: normal;
33 | // font-weight: normal;
34 | // }
35 | // @font-face{
36 | // font-family: "TYZKSJW";
37 | // /* src: url("../../resource/fonts/TYZKSJW.TTF") format("truetype"); iOS 4.1- */
38 | // font-style: normal;
39 | // font-weight: normal;
40 | // }
41 |
42 |
43 | // @font-face{
44 | // font-family: "FZYBKSJW";
45 | // /* src: url("../../resource/fonts/FZYBKSJW.TTF") format("truetype"); iOS 4.1- */
46 | // font-style: normal;
47 | // font-weight: normal;
48 | // }
49 |
50 | // @font-face{
51 | // font-family: "PZHGBZTJW";
52 | // /* src: url("../../resource/fonts/PZHGBZTJW.TTF") format("truetype"); iOS 4.1- */
53 | // font-style: normal;
54 | // font-weight: normal;
55 | // }
56 |
--------------------------------------------------------------------------------
/src/pages/setting/index.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/pages/setting/index.css
--------------------------------------------------------------------------------
/src/pages/setting/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | // import logo from './logo.svg';
3 | import { Picker, Stepper, Button, SegmentedControl, WhiteSpace, WingBlank, NavBar } from 'antd-mobile';
4 | import 'antd-mobile/dist/antd-mobile.css'; // or 'antd-mobile/dist/antd-mobile.less'
5 |
6 | import fontList from './font-list';
7 | // import './App.css';
8 | import './index.css';
9 |
10 | window._czc = window._czc || [];
11 | let sizeTimer;
12 | export default () => {
13 | const [size, setSize] = useState(parseInt(window.localStorage.getItem("setting.size"), 10) || 60);
14 | const [type, setType] = useState(window.localStorage.getItem("setting.type") || "tian");
15 | const [font, setFont] = useState(window.localStorage.getItem("setting.font-family") || "FZKTJW");
16 | const fonts = fontList.map(({ label, value }) => ({
17 | label:label,
18 | labelText: label,
19 | value
20 | }))
21 | return (
22 |
23 |
设置
24 |
25 |
26 |
27 | {
36 | window.localStorage.setItem("setting.size", v);
37 | clearTimeout(sizeTimer);
38 | sizeTimer = setTimeout(() => {
39 | window._czc && window._czc.push(["_trackEvent", "setting", "size", '字体大小', v, 'stepper']);
40 | }, 3000)
41 | setSize(v)
42 | }}
43 | />
44 |
45 |
46 |
47 | {
51 | const { selectedSegmentIndex: v, value: label } = e.nativeEvent;
52 | const type = v === 1 ? "mi" : "tian";
53 | setType(type)
54 | window.localStorage.setItem("setting.type", type)
55 | window._czc && window._czc.push(["_trackEvent", "setting", "type", label, 1, 'bt_' + type]);
56 | }}
57 | />
58 |
59 |
60 |
61 | {
66 | const value = v[0];
67 | window.localStorage.setItem("setting.font-family",value)
68 | const label = fonts.filter(({ value }) => value === v[0]).map(({ labelText }) => labelText)[0]
69 | window._czc && window._czc.push(["_trackEvent", "setting", "font", label, 1, 'font_select']);
70 | setFont(value)
71 | }}
72 | >
73 |
77 |
78 |
79 |
80 |
81 | );
82 | }
--------------------------------------------------------------------------------
/src/pages/task-clock/components/clock/clock.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export */
2 |
3 | interface IdrawCalibrationProps {
4 | parts: 12 | 24 | 60 | 120
5 | size: [number, number, number?]
6 | textOffset?: number
7 | }
8 |
9 | interface IdrawClockOnePointerProps {
10 | parts: number
11 | postion: number
12 | offset?: {
13 | start?: number
14 | end?: number
15 | }
16 | lineWidth?: number
17 | }
18 |
19 | export default class Clock implements IClock {
20 | constructor(ctx: CanvasRenderingContext2D, options: { radius: number }) {
21 | this.ctx = ctx
22 | this.options = options
23 | }
24 | ctx: CanvasRenderingContext2D
25 | options: {
26 | radius: number
27 | }
28 | public drawClockBlank() {
29 | this.drawClockCalibration({
30 | parts: 24,
31 | size: [-18, -12, 3],
32 | textOffset: -20
33 | })
34 | this.drawClockCalibration({
35 | parts: 60,
36 | size: [-4, 0],
37 | textOffset: 20
38 | })
39 | };
40 | public drawClockPointer() {
41 | const ctx = this.ctx
42 | const { radius } = this.options
43 | //绘制表芯
44 | ctx.beginPath();
45 | ctx.arc(0, 0, 5, 0, 2 * Math.PI)
46 | ctx.fillStyle = "red"
47 | ctx.closePath();
48 | ctx.fill()
49 | //时针
50 | const now = new Date();
51 | const hours = new Date().getHours();
52 | const minutes = now.getMinutes()
53 | const seconds = now.getSeconds()
54 | this.drawClockOnePointer({
55 | parts: 24,
56 | postion: hours + minutes / 60 + seconds / (60 * 60),
57 | offset: {
58 | start: -radius * 1.03,
59 | end: -radius * 0.4
60 | },
61 | lineWidth: 2
62 | })
63 | this.drawClockOnePointer({
64 | parts: 60,
65 | postion: minutes + seconds / 60,
66 | offset: {
67 | start: -radius * 1.05,
68 | end: -radius * 0.2
69 | },
70 | lineWidth: 1
71 | })
72 | this.drawClockOnePointer({
73 | parts: 60,
74 | postion: seconds,
75 | offset: {
76 | start: -radius * 1.1,
77 | end: radius * .1
78 | },
79 | lineWidth: 0.2
80 | })
81 | };
82 | private getRadian = (parts: number, count: number,) => -(2 * Math.PI / 360) * ((360 / parts * count + 180) % 360);
83 | private drawClockCalibration = (props: IdrawCalibrationProps) => {
84 | const ctx = this.ctx
85 | const { radius } = this.options
86 | const { parts, size: [inner, outer, size = 1], textOffset } = props
87 | // 绘制时刻度
88 | let x: number;
89 | let y: number;
90 | ctx.beginPath();
91 | for (let i = 0; i < parts; i++) {
92 | const radian = this.getRadian(parts, i)
93 | x = Math.sin(radian) * (radius + inner);
94 | y = Math.cos(radian) * (radius + inner);
95 | ctx.moveTo(x, y);
96 | x = Math.sin(radian) * (radius + outer);
97 | y = Math.cos(radian) * (radius + outer);
98 | ctx.lineTo(x, y);
99 | if (size) {
100 | ctx.lineWidth = size
101 | }
102 | if (textOffset) {
103 | x = Math.sin(radian) * (radius + outer + textOffset);
104 | y = Math.cos(radian) * (radius + outer + textOffset);
105 | ctx.fillText((i).toString(), x, y)
106 | }
107 | }
108 | ctx.stroke();
109 | }
110 | private drawClockOnePointer(props: IdrawClockOnePointerProps) {
111 | const ctx = this.ctx
112 | const { radius } = this.options
113 | const { parts, postion, offset = {}, lineWidth = 1 } = props
114 | const { start = 0, end = 0 } = offset
115 | let x, y;
116 | ctx.beginPath();
117 | const radian = this.getRadian(parts, postion)
118 | x = Math.sin(radian) * (radius + start);
119 | y = Math.cos(radian) * (radius + start);
120 | ctx.moveTo(x, y);
121 | x = Math.sin(radian) * (radius + end);
122 | y = Math.cos(radian) * (radius + end);
123 | ctx.lineTo(x, y);
124 | ctx.lineWidth = lineWidth
125 | ctx.stroke()
126 | }
127 | }
128 |
129 |
--------------------------------------------------------------------------------
/src/pages/task-clock/components/clock/index.scss:
--------------------------------------------------------------------------------
1 | .clock{
2 | margin: auto;
3 | }
4 | .clock-canvas{
5 | display: block;
6 | margin:auto;
7 | }
--------------------------------------------------------------------------------
/src/pages/task-clock/components/clock/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import 'antd-mobile/dist/antd-mobile.css'; // or 'antd-mobile/dist/antd-mobile.less'
3 | import '.'
4 | import './index.scss';
5 | import { drawClockBlank, drawClockPointer } from './utils';
6 | declare var window: any;
7 | interface IProps {
8 | data: Clock.Task[]
9 | }
10 | const Clock: React.FC = (props) => {
11 | const { data } = props
12 | useEffect(() => {
13 | const canvas: HTMLCanvasElement = document.querySelector(".clock-canvas") || document.createElement("canvas")
14 | const ctx = canvas.getContext("2d");
15 | canvas.width = Math.min(window.innerWidth, window.innerHeight)-20
16 | canvas.height = Math.min(window.innerWidth, window.innerHeight)-20
17 | let requestID: number;
18 | if (ctx) {
19 | const w = ctx.canvas.width;
20 | const h = ctx.canvas.height;
21 | const zX = w / 2;
22 | const zY = h / 2;
23 | ctx.translate(zX, zY);
24 | const LayerBack = document.createElement("canvas");
25 | LayerBack.width = w;
26 | LayerBack.height = h;
27 | const ctxLayerBack = LayerBack.getContext("2d");
28 | const radius = Math.min(w, h) / 2 * .9 - 20;
29 | if (ctxLayerBack) {
30 | ctxLayerBack.translate(zX, zY)
31 | drawClockBlank(ctxLayerBack, { radius });
32 | }
33 |
34 | const LayerPointer = document.createElement("canvas");
35 | LayerPointer.width = w;
36 | LayerPointer.height = h;
37 | const ctxLayerPointer = LayerPointer.getContext("2d");
38 | if (ctxLayerPointer) {
39 | ctxLayerPointer.translate(zX, zY)
40 | }
41 | const drawShand = () => {
42 | if (ctx) {
43 | ctx.clearRect(-zX, -zY, 2 * zX, 2 * zY);
44 | if (ctxLayerPointer) {
45 | ctxLayerPointer.clearRect(-zX, -zY, 2 * zX, 2 * zY);
46 | drawClockPointer(ctxLayerPointer, {
47 | radius,
48 | taskList:data
49 | });
50 | }
51 | ctx.globalCompositeOperation = "destination-over"
52 | ctx.drawImage(LayerPointer, -zX, -zY)
53 | ctx.drawImage(LayerBack, -zX, -zY)
54 | }
55 | requestID = requestAnimationFrame(drawShand)
56 | }
57 | drawShand()
58 | }
59 | return () => {
60 | cancelAnimationFrame(requestID)
61 | // clearImmediate(rafHandler)
62 | }
63 | }, [data])
64 | return (
65 |
70 |
71 | );
72 | }
73 |
74 | export default Clock;
75 |
--------------------------------------------------------------------------------
/src/pages/task-clock/components/clock/utils.ts:
--------------------------------------------------------------------------------
1 |
2 | interface IdrawCalibrationProps {
3 | radius: number
4 | parts: 12 | 24 | 60 | 120
5 | size: [number, number, number?]
6 | textOffset?: number
7 | }
8 | const getRadian = (parts: number, count: number,) => -(2 * Math.PI / 360) * ((360 / parts * count + 180) % 360)
9 |
10 | const drawClockCalibration = (ctx: CanvasRenderingContext2D, props: IdrawCalibrationProps) => {
11 | const { radius, parts, size: [inner, outer, size = 1], textOffset } = props
12 | // 绘制时刻度
13 | let x: number;
14 | let y: number;
15 | ctx.beginPath();
16 | for (let i = 0; i < parts; i++) {
17 | const radian = getRadian(parts, i)
18 | x = Math.sin(radian) * (radius + inner);
19 | y = Math.cos(radian) * (radius + inner);
20 | ctx.moveTo(Math.floor(x), Math.floor(y))
21 | x = Math.sin(radian) * (radius + outer);
22 | y = Math.cos(radian) * (radius + outer);
23 | ctx.lineTo(Math.floor(x), Math.floor(y))
24 | if (size) {
25 | ctx.lineWidth = size
26 | }
27 | if (textOffset) {
28 | x = Math.sin(radian) * (radius + outer + textOffset);
29 | y = Math.cos(radian) * (radius + outer + textOffset);
30 | if (parts === 60) {
31 | if (i % 5 === 0) {
32 | ctx.fillText((i || parts).toString(), Math.floor(x), Math.floor(y))
33 | }
34 | } else {
35 | ctx.fillText((i || parts).toString(), Math.floor(x), Math.floor(y))
36 | }
37 | }
38 | }
39 | ctx.stroke();
40 | }
41 | export const drawClockBlank = (ctx: CanvasRenderingContext2D, props: { radius: number }) => {
42 | drawClockCalibration(ctx, {
43 | radius: props.radius,
44 | parts: 24,
45 | size: [-18, -12, 3],
46 | textOffset: -20
47 | })
48 | drawClockCalibration(ctx, {
49 | radius: props.radius,
50 | parts: 60,
51 | size: [-4, 0],
52 | textOffset: 20
53 | })
54 | }
55 | interface IdrawClockOnePointerProps {
56 | radius: number
57 | parts: number
58 | postion: number
59 | offset?: {
60 | start?: number
61 | end?: number
62 | }
63 | lineWidth?: number
64 | }
65 | const drawClockOnePointer = (ctx: CanvasRenderingContext2D, props: IdrawClockOnePointerProps) => {
66 |
67 | const { radius, parts, postion, offset = {}, lineWidth = 1 } = props
68 | const { start = 0, end = 0 } = offset
69 | let x, y;
70 | ctx.beginPath();
71 | const radian = getRadian(parts, postion)
72 | x = Math.sin(radian) * (radius + start);
73 | y = Math.cos(radian) * (radius + start);
74 | ctx.moveTo(Math.floor(x), Math.floor(y))
75 | x = Math.sin(radian) * (radius + end);
76 | y = Math.cos(radian) * (radius + end);
77 | ctx.lineTo(Math.floor(x), Math.floor(y))
78 | ctx.lineWidth = lineWidth
79 | ctx.stroke()
80 | }
81 | export const drawSector = (ctx: CanvasRenderingContext2D, props: {
82 | radius: number
83 | start: number
84 | end: number,
85 | imgUrl?: string,
86 | color?: string,
87 | label?: string
88 | }) => {
89 | const { radius } = props
90 | ctx.beginPath()
91 | ctx.fillStyle = ""
92 |
93 | if (props.color) {
94 | ctx.fillStyle = props.color
95 | }
96 |
97 |
98 | ctx.scale(1, 1)
99 | ctx.moveTo(0, 0);
100 | const start = (props.start ? Math.PI * 2 / props.start : 0) - (Math.PI * 2 / 360 * 90)
101 | const end = (props.end ? Math.PI * 2 / props.end : 0) - Math.PI * 2 / 360 * 90
102 | ctx.arc(0, 0, radius, start, end, false)
103 | ctx.closePath();
104 | ctx.fill()
105 |
106 | if (props.imgUrl) {
107 | var img = document.getElementById(props.imgUrl) as HTMLImageElement;
108 | if (!img) {
109 | img = document.createElement("img");
110 | img.src = props.imgUrl
111 | img.id = props.imgUrl || "";
112 | (document.getElementById("img-container") as HTMLElement).appendChild(img);
113 | }
114 | // ctx.drawImage(img, 0, 0, 50, 50)
115 | }
116 | }
117 | interface ITask {
118 | label: string,
119 | start: number,
120 | end: number
121 | color?: string
122 | imgUrl?: string
123 | }
124 | export const drawClockPointer = (ctx: CanvasRenderingContext2D, props: {
125 | radius: number,
126 | taskList: any[]
127 | }) => {
128 | const { radius } = props;
129 | const taskList: ITask[] = props.taskList.map((item: any) => {
130 | const startTime = item.start;
131 | const endTime = item.end
132 | return {
133 | color: item.color,
134 | label: item.name,
135 | imgUrl: item.imgUrl,
136 | start: 24 / (startTime.getHours() + startTime.getMinutes() / 60),
137 | end: 24 / (endTime.getHours() + endTime.getMinutes() / 60)
138 | }
139 | })
140 | taskList.forEach(item => {
141 | // ctx.fillStyle = "#000000"
142 | drawSector(ctx, {
143 | radius: radius * 0.7,
144 | start: item.start,
145 | end: item.end,
146 | imgUrl: item.imgUrl,
147 | color: item.color
148 | })
149 | })
150 | //绘制表芯
151 | ctx.beginPath();
152 | ctx.arc(0, 0, 5, 0, 2 * Math.PI)
153 | // ctx.fillStyle = "red"
154 | ctx.closePath();
155 | ctx.fill()
156 | //时针
157 | const now = new Date();
158 | const seconds = now.getSeconds() //+ now.getTime() % 1000 / 1000
159 | const minutes = now.getMinutes() + seconds / 60
160 | const hours = now.getHours() + minutes / 60;
161 | drawClockOnePointer(ctx, {
162 | radius,
163 | parts: 24,
164 | postion: hours,
165 | offset: {
166 | start: -radius * 1.03,
167 | end: -radius * 0.4
168 | },
169 | lineWidth: 2
170 | })
171 | // drawSector(ctx, {
172 | // radius:radius*0.6,
173 | // start: 0,
174 | // end: (12 / (hours%12))
175 | // })
176 | drawClockOnePointer(ctx, {
177 | radius,
178 | parts: 60,
179 | postion: minutes,
180 | offset: {
181 | start: -radius * 1.05,
182 | end: -radius * 0.2
183 | },
184 | lineWidth: 1
185 | })
186 | drawClockOnePointer(ctx, {
187 | radius,
188 | parts: 60,
189 | postion: seconds,
190 | offset: {
191 | start: -radius * 1.1,
192 | end: radius * .1
193 | },
194 | lineWidth: 0.2
195 | })
196 |
197 |
198 | }
--------------------------------------------------------------------------------
/src/pages/task-clock/components/list/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List } from 'antd-mobile';
3 | import { timeFormat } from '../../../task-list/uitls';
4 |
5 |
6 | interface IProps {
7 | data: Array
8 | }
9 |
10 | const TaskList: React.FC = (props) => {
11 | const { data } = props
12 | return
13 | {data.map(task => {
14 | return
15 | {task.name}
16 |
17 | {timeFormat(task.startTime)} - {timeFormat(task.endTime)}
18 |
19 |
20 | })}
21 |
22 | }
23 | export default TaskList;
--------------------------------------------------------------------------------
/src/pages/task-clock/index.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/pages/task-clock/index.scss
--------------------------------------------------------------------------------
/src/pages/task-clock/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import 'antd-mobile/dist/antd-mobile.css'; // or 'antd-mobile/dist/antd-mobile.less'
3 | import '.'
4 | import './index.scss';
5 | import { NavBar, Popover, Icon } from 'antd-mobile';
6 | import { RouteProps, useHistory } from 'react-router-dom';
7 | import { queryByDate } from '../task-list/service';
8 | import { buildDateTime, str2color } from '../task-list/uitls';
9 | import TaskList from './components/list';
10 | import Clock from './components/clock';
11 | declare var window: any;
12 | window._czc = window._czc || [];
13 | function App(props: RouteProps) {
14 | // props.
15 | const [clockTasks, updateClockTasks] = useState>([])
16 | const [taskList, updateTaskList] = useState>([])
17 | const history = useHistory();
18 | useEffect(() => {
19 | (async () => {
20 | const tasks = await (await queryByDate()).map(item => ({
21 | "color": str2color(item.name, { to: 0x999999 }),
22 | ...item
23 | }));
24 | updateTaskList(tasks);
25 | updateClockTasks(tasks.map(item => {
26 | const task: Clock.Task = {
27 | name: item.name,
28 | "color": item.color,
29 | "imgUrl": "",
30 | "start": buildDateTime(item.startDate, item.startTime),
31 | "end": buildDateTime(item.endDate, item.endTime),
32 | }
33 | return task
34 | }));
35 | })()
36 | }, [])
37 | return (
38 |
39 |
{
43 | history.push(firstItem)
44 | }}
45 | overlay={[
46 |
50 | 计划管理
51 |
52 | ]}
53 | >
54 |
55 |
56 | }
57 | >今日计划
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | export default App;
65 |
--------------------------------------------------------------------------------
/src/pages/task-clock/typing.d.ts:
--------------------------------------------------------------------------------
1 |
2 | declare namespace Clock {
3 | interface Task {
4 | name: string
5 | color: string
6 | start: Date
7 | end: Date
8 | imgUrl: string
9 | }
10 | }
11 |
12 |
13 | // interface Clock {
14 | // // drawClockBlank: () => void
15 | // // drawClockPointer: () => void
16 | // // }
17 | // interface ClockConstructor {
18 | // new(ctx: CanvasRenderingContext2D, options: {
19 | // radius: number
20 | // }): ClockInterface
21 | // }
22 | // interface ClockInterface {
23 | // drawClockBlank: () => void
24 | // }
25 | declare class IClock {
26 | constructor(ctx: CanvasRenderingContext2D, options: {
27 | radius: number
28 | })
29 | ctx: CanvasRenderingContext2D
30 | options: {
31 | radius: number
32 | }
33 | // private getRadian: (parts: number, postion: number) => number
34 | // private drawClockCalibration: (props: {
35 | // parts: 12 | 24 | 60 | 120
36 | // size: [number, number, number?]
37 | // textOffset?: number
38 | // }) => void
39 | public drawClockBlank: () => void
40 | // private drawClockOnePointer: (props: {
41 | // parts: number
42 | // postion: number
43 | // offset?: {
44 | // start?: number
45 | // end?: number
46 | // }
47 | // lineWidth?: number
48 | // }) => void
49 | public drawClockPointer: () => void
50 | }
51 |
--------------------------------------------------------------------------------
/src/pages/task-list/components/task-form/index.tsx:
--------------------------------------------------------------------------------
1 | import TaskForm from './task-form';
2 | export default TaskForm
--------------------------------------------------------------------------------
/src/pages/task-list/components/task-form/task-date-rang-item.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { List, Calendar } from 'antd-mobile';
3 | import { CalendarProps } from 'antd-mobile/lib/calendar/PropsType';
4 | import dayjs from 'dayjs';
5 | const TaskDateRangItem: React.FC void }> = (props) => {
6 | const { value, onChange } = props;
7 | const [visibleCalendar, updateVisibleCalendar] = useState(false)
8 |
9 | return (
10 |
11 |
15 | {dayjs(value[0]).format("YYYY-MM-DD")}
16 | {value[1] && value[1] > value[0] ?
17 |
18 | {dayjs(value[1]).format("YYYY-MM-DD")}
19 |
20 | : null}
21 |
22 | }
23 | onClick={() => {
24 | updateVisibleCalendar(true)
25 | }}
26 | >
27 | 日期
28 |
29 | {
32 | updateVisibleCalendar(false)
33 | }}
34 | onConfirm={(startDate, endDate) => {
35 | updateVisibleCalendar(false)
36 | onChange([dayjs(startDate).format("YYYYMMDD"), dayjs(endDate).format("YYYYMMDD")])
37 | }}
38 | showShortcut
39 | type={"range"}
40 | // defaultDate={new Date(value[0]) || new Date()}
41 | defaultValue={[dayjs(value[0]).toDate(), dayjs(value[1]).toDate()] as any}
42 | />
43 |
44 |
45 | )
46 | }
47 | export default TaskDateRangItem
48 |
--------------------------------------------------------------------------------
/src/pages/task-list/components/task-form/task-form.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, InputItem, List, Toast } from 'antd-mobile';
3 | import { createForm } from "rc-form";
4 | import TaskRepeatTypeItem from './task-repeat-type-item';
5 | import TaskRepeatDayItem from './task-repeat-day-item';
6 | import { Week } from './typing.d'
7 | import TaskTimeRangItem from './task-time-rang-item';
8 | import TaskDateRangItem from './task-date-rang-item';
9 |
10 | interface ISource extends Task { }
11 | interface IProps {
12 | task: ISource
13 | onSubmit: (data: ISource) => void
14 | onCancel: () => void
15 | }
16 | const TaskForm: RCForm.FC = (props) => {
17 | const { form, task } = props;
18 | const { getFieldProps, getFieldsValue, getFieldError, setFieldsValue, validateFields } = form
19 |
20 | getFieldProps("startTime", { initialValue: task.startTime })
21 | getFieldProps("endTime", { initialValue: task.endTime })
22 | getFieldProps("startDate", {
23 | initialValue: task.startDate
24 | })
25 | getFieldProps("endDate", {
26 | initialValue: task.endDate
27 | })
28 | const values = getFieldsValue(["name", "repeatType", "repeatDays", "startDate", "endDate", "startTime", "endTime"]);
29 | return (
30 |
39 |
44 | {
56 | Toast.info(getFieldError("name"))
57 | }}
58 | >任务名称
59 |
89 |
97 | {
100 | setFieldsValue({
101 | startDate: range[0],
102 | endDate: range[1]
103 | })
104 | }}
105 | />
106 | {
109 | setFieldsValue({
110 | startTime: range[0],
111 | endTime: range[1]
112 | })
113 | }}
114 | />
115 |
116 |
117 |
118 |
119 |
136 |
137 | )
138 | }
139 |
140 | export default createForm()(TaskForm)
141 |
--------------------------------------------------------------------------------
/src/pages/task-list/components/task-form/task-repeat-day-item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List, Checkbox } from 'antd-mobile';
3 | // import { Week } from './typing.d';
4 | import { Week } from './typing.d'
5 | import { InputProps } from 'antd-mobile/lib/input-item/Input';
6 | const list: Array<{
7 | value: Week,
8 | label: string
9 | }> = [
10 | { value: Week.Monday, label: "周一" },
11 | { value: Week.Tuesday, label: "周二" },
12 | { value: Week.Wednesday, label: "周三" },
13 | { value: Week.Thursday, label: "周四" },
14 | { value: Week.Friday, label: "周五" },
15 | { value: Week.Saturday, label: "周六" },
16 | { value: Week.Sunday, label: "周日" }
17 | ]
18 | const TaskRepeatDayItem: React.FC = (props) => {
19 | const { value = [] } = props;
20 | // console.log(value)
21 | return (
22 | {
24 | const target = e.target as HTMLInputElement
25 | if (target.type === "checkbox") {
26 | target.value = target.name
27 | if (props.repeatType === "other") {
28 | const { name, checked }: any = target
29 | const index = value.indexOf(name);
30 | let ret = [...value]
31 | if (checked && index === -1) {
32 | ret.push(name)
33 | }
34 | if (!checked && index > -1) {
35 | ret.splice(index, 1)
36 | }
37 | target.value = ret as any;
38 | props.onChange && props.onChange(ret)
39 | }
40 | }
41 | }}
42 | >
43 |
44 | {
45 | list.map(item => -1}
48 | name={item.value as any}
49 | >{item.label})
50 | }
51 |
52 |
53 |
54 | )
55 | }
56 | export default TaskRepeatDayItem
57 |
--------------------------------------------------------------------------------
/src/pages/task-list/components/task-form/task-repeat-type-item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List, Checkbox } from 'antd-mobile';
3 | import { InputProps } from 'antd-mobile/lib/input-item/Input';
4 | const list:Array<{value:Task.RepeatType,label:string}> = [
5 | { value: "none", label: "无" },
6 | { value: "day", label: "每日" },
7 | { value: "weekday", label: "工作日" },
8 | { value: "weekend", label: "周末" },
9 | { value: "other", label: "其他" }
10 | ]
11 | const TaskRepeatTypeItem: React.FC = (props) => {
12 | // const [value,updateValue] =
13 | // console.log(props)
14 | return (
15 | {
17 | const target = e.target as HTMLInputElement
18 | if (target.type === "checkbox") {
19 | target.value = target.name
20 | props.onChange && props.onChange(e as any)
21 | }
22 | // e.stopPropagation()
23 | return false
24 | }}
25 | >
26 | 循环任务
27 |
29 | {
30 | list.map(item => {item.label})
35 | }
36 |
37 |
38 |
39 | )
40 | }
41 | export default TaskRepeatTypeItem
42 |
--------------------------------------------------------------------------------
/src/pages/task-list/components/task-form/task-time-rang-item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List, Range } from 'antd-mobile';
3 | // import { Week } from './typing.d';
4 | import { InputProps } from 'antd-mobile/lib/input-item/Input';
5 | import { timeFormat } from '../../uitls';
6 | const TaskTimeRangItem: React.FC void }> = (props) => {
7 | const { value = [9 * 60, 10 * 60] } = props;
8 | // console.log(value)
9 | return (
10 |
11 |
14 | {timeFormat(value[0])} - {timeFormat(value[1])}
15 |
16 | }
17 | >
18 | 时间段
19 |
20 |
21 |
22 |
23 | props.onChange(range)}
39 | />
40 |
41 |
42 |
43 |
44 | )
45 | }
46 | export default TaskTimeRangItem
47 |
--------------------------------------------------------------------------------
/src/pages/task-list/components/task-form/typing.d.ts:
--------------------------------------------------------------------------------
1 |
2 | // declare module {
3 | export enum Week {
4 | Sunday,
5 | Monday,
6 | Tuesday,
7 | Wednesday,
8 | Thursday,
9 | Friday,
10 | Saturday,
11 | }
--------------------------------------------------------------------------------
/src/pages/task-list/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | // import { Button } from 'antd-mobile';
3 | import TaskForm from './components//task-form';
4 | import { Drawer, Button, List, NavBar, Icon, SwipeAction, Tag } from 'antd-mobile';
5 | import dayjs from 'dayjs';
6 | import * as service from './service';
7 | import { useHistory } from 'react-router-dom';
8 | import { timeFormat } from './uitls';
9 |
10 | const newTask: any = {
11 | name: "",
12 | repeatDays: [],
13 | repeatType: "none",
14 | startTime: 9 * 60,
15 | endTime: 10 * 60,
16 | startDate: dayjs(new Date()).format("YYYYMMDD"),
17 | endDate: dayjs(new Date()).format("YYYYMMDD"),
18 | }
19 | const TaskList: React.FC = (props) => {
20 | const history = useHistory()
21 | const [visible, updateVisible] = useState(false);
22 | const [list, updateList] = useState([]);
23 | const [count, updateCount] = useState(0);
24 | const [editTask, updateEditTask] = useState(newTask)
25 | useEffect(() => {
26 | service.load().then(res => {
27 | updateList(res)
28 | })
29 | }, [count, visible])
30 | return
31 |
updateVisible(!visible)}
37 | sidebar={
38 | visible ?
39 | {
42 | service.save(data)
43 | updateVisible(false)
44 | }}
45 | onCancel={() => updateVisible(false)}
46 | /> : null}
47 | >
48 | {
51 | history.goBack()
52 | }}
53 | />}
54 | >计划管理
55 |
56 |
60 |
61 | {list.map(task =>
62 |
92 |
96 | {task.name}
97 |
98 | {dayjs(task.startDate).format("YYYY-MM-DD")}
99 | {task.repeatType !== "none" ? ` - ${dayjs(task.endDate).format("YYYY-MM-DD")}` : null}
100 |
101 | {`${timeFormat(task.startTime)}-${timeFormat(task.endTime)}`}
102 |
103 |
104 | {task.repeatType === "day" ? 每天 : null}
105 | {task.repeatType === "weekday" ? 工作日 : null}
106 | {task.repeatType === "weekend" ? 周末 : null}
107 | {task.repeatType === "other" ? task.repeatDays.map(v => {v}) : null}
108 |
109 |
110 |
111 | )}
112 |
113 | {props.children}
114 |
115 |
116 |
117 | }
118 | export default TaskList
--------------------------------------------------------------------------------
/src/pages/task-list/service.ts:
--------------------------------------------------------------------------------
1 | import TaskStorage from "./storage";
2 | import dayjs from "dayjs";
3 |
4 | const task = new TaskStorage()
5 | export const load = () => {
6 | return task.load()
7 | }
8 | export const queryByDate=(date:number=parseInt(dayjs(new Date()).format("YYYYMMDD")))=>{
9 | return task.query({startDate:date,endDate:date})
10 | }
11 | export const save = (data: Task) => {
12 | if(data.id){
13 | return task.update(data);
14 | }
15 | return task.add(data)
16 | }
17 | export const remove=(id: string) => {
18 | return task.remove(id);
19 | }
--------------------------------------------------------------------------------
/src/pages/task-list/storage.ts:
--------------------------------------------------------------------------------
1 |
2 | interface ITask extends Task {
3 | // id: string
4 | }
5 |
6 | export default class TaskStorage {
7 | constructor() {
8 | this.db = new Promise((resolve, reject) => {
9 | var request = window.indexedDB.open("study");
10 | request.onsuccess = (event: any) => {
11 | const db = event.target.result;
12 | resolve(db)
13 | }
14 | request.onupgradeneeded = (event: any) => {
15 | const db = event.target.result;
16 | const objectStore: IDBObjectStore = db.createObjectStore('task-list', { keyPath: 'id', autoIncrement: true });
17 | objectStore.createIndex('name', 'name', { unique: false });
18 | objectStore.createIndex('startDate', 'startDate', { unique: false });
19 | objectStore.createIndex('endDate', 'endDate', { unique: false });
20 | resolve(db)
21 | }
22 | request.onerror = (event) => {
23 | reject(event)
24 | }
25 | })
26 | }
27 | private db: Promise
28 | private get store() {
29 | return this.db.then(db => db.transaction(["task-list"], "readwrite").objectStore("task-list"))
30 | }
31 | async load(): Promise {
32 | const store = await this.store;
33 | const request = store.getAll() as IDBRequest
34 | return await new Promise((resolve, reject) => {
35 | request.onsuccess = () => resolve(request.result)
36 | request.onerror = reject
37 | })
38 | }
39 | async add(task: Partial) {
40 | const store = await this.store;
41 | const request = store.add(task)
42 | return await new Promise((resolve, reject) => {
43 | request.onsuccess = (e) => {
44 | resolve(true)
45 | }
46 | request.onerror = (e) => {
47 | console.log(e);
48 | reject(e)
49 |
50 | }
51 | })
52 | }
53 | async update(task: ITask) {
54 | const store = await this.store;
55 | const request = store.put(task);
56 | return await new Promise((resolve, reject) => {
57 | request.onsuccess = (e) => {
58 | resolve(request.result)
59 | }
60 | request.onerror = reject
61 | })
62 | }
63 | async remove(id: string) {
64 | const store = await this.store;
65 | store.delete(id)
66 | }
67 | async query({ startDate, endDate = startDate }: { startDate: number, endDate?: number }): Promise {
68 | const store = await this.store;
69 | const request = store.getAll() as IDBRequest
70 | return await new Promise((resolve, reject) => {
71 | request.onsuccess = (e) => {
72 | resolve((request.result).filter((task) => {
73 | if (startDate >= task.startDate && endDate <= task.endDate) {
74 | return true
75 | }
76 | return false
77 | }).sort((a, b) => {
78 | return a.startTime - b.startTime
79 | }))
80 | }
81 | request.onerror = reject
82 | })
83 | }
84 | }
--------------------------------------------------------------------------------
/src/pages/task-list/typing.d.ts:
--------------------------------------------------------------------------------
1 |
2 | declare namespace Task {
3 | // export enum Week {
4 | // Sunday = "Sun",
5 | // Monday = "Mon",
6 | // Tuesday = "Tue",
7 | // Wednesday = "Wed",
8 | // Thursday = "Thu",
9 | // Friday = "Fri",
10 | // Saturday = "Sat",
11 | // }
12 | enum Week {
13 | Sunday = "Sun",
14 | Monday = "Mon",
15 | Tuesday = "Tue",
16 | Wednesday = "Wed",
17 | Thursday = "Thu",
18 | Friday = "Fri",
19 | Saturday = "Sat",
20 | }
21 | type RepeatType = "day" | 'weekday' | "weekend" | "other" | "none"
22 | }
23 | interface Task {
24 | id: string,
25 | name: string
26 | repeatType: Task.RepeatType
27 | repeatDays: Array
28 | startTime: number
29 | endTime: number
30 | startDate: number
31 | endDate: number
32 | }
33 | // export = Task;
34 | // export as namespace Task;
35 | // declare enum Week {
36 | // Sunday = "Sun",
37 | // Monday = "Mon",
38 | // Tuesday = "Tue",
39 | // Wednesday = "Wed",
40 | // Thursday = "Thu",
41 | // Friday = "Fri",
42 | // Saturday = "Sat",
43 | // }
44 | // export default Task
45 | // export Week
46 | // export declare let Week: Task.Week
--------------------------------------------------------------------------------
/src/pages/task-list/uitls.ts:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 |
3 | export const timeFormat = (time: number = 9 * 60) => {
4 | return [Math.floor(time as number / 60), (time as number) % 60].map(val => val < 10 ? ("0" + val) : val).join(":");
5 | }
6 | export const buildDateTime = (date: number, time: number) => {
7 | const ret = dayjs(date).toDate();
8 | ret.setMinutes(time);
9 | return ret;
10 | }
11 | export const str2color = (
12 | str: string,
13 | scope: {
14 | from?: number;
15 | to?: number;
16 | } = {},
17 | ): string => {
18 | const { from = 0x333333, to = 0xcccccc } = { from: 0x333333, to: 0xcccccc, ...scope };
19 | const str2code = str.replace(/[^\d]/g, $1 => $1.charCodeAt(0).toString());
20 | const mod = parseFloat(str2code) % to;
21 | const value = mod < from ? mod + from : mod;
22 | const rgbValue = `000000${value.toString(16)}`.match(/\0*([\dA-Fa-f]{6,6})$/);
23 | // .match(/\0*([\dA-Fa-f]{6,6})$/)[0]
24 | return `#${rgbValue ? rgbValue[0] : value.toString(16)}`;
25 | };
26 |
--------------------------------------------------------------------------------
/src/rc-form.d.ts:
--------------------------------------------------------------------------------
1 |
2 | // import * as V from 'async-validator '
3 | // export = RCForm
4 | // export as namespace RCForm
5 | declare namespace RCForm {
6 | type PickOne = T[P]
7 | type ValidateTrigger = "onBlur"
8 |
9 | interface RCFormOptions {
10 |
11 | }
12 | interface FieldOptions {
13 | /**字段名 */
14 | valuePropName?: string
15 | rules?: AsyncValidator.RuleType
16 | validateFirst?: boolean
17 | validate?: {
18 | [n: string | number]: {
19 | trigger: ValidateTrigger
20 | rules: AsyncValidator.RuleType
21 | }
22 | }
23 | getValueProps?: any
24 | /**当表单组件变化时用户计算Filed的值(多用于自定义组件) */
25 | getValueFromEvent?: {
26 | (event: React.SyntheticEvent): void
27 | }
28 | onChange?: {
29 | (event: React.SyntheticEvent): void
30 | }
31 | /**初始化值 */
32 | initialValue?: PickOne
33 | normalize?: any
34 | trigger?: any
35 | CalidateTrigger?: ValidateTrigger
36 |
37 | hidden?: any
38 | preserve?: any
39 |
40 | }
41 | interface CreateFromOptions {
42 | validateMessages?: any
43 | onFieldsChange?: any
44 | onValuesChange?: any
45 | mapProps?: any
46 | mapPropsToFields?: any
47 | fieldNameProp?: any
48 | fieldMetaProp?: any
49 | fieldDataProp?: any
50 | withRef?: any
51 | }
52 | interface FormInstance {
53 | /**设置初始化数据 */
54 | setFieldsInitialValue: (values: Partial) => void
55 | // getFieldDecorator
56 | setFieldsValue: (values: Partial) => void
57 | validateFields: {
58 | (
59 | callback: {
60 | (
61 | error: {
62 | error: {
63 | [TField in TName]: {
64 | errors: Array<{
65 | message: string,
66 | field: TField
67 | }>
68 | }
69 | },
70 | fields: TSource
71 | },
72 | values: TSource
73 | ): void
74 | }
75 | ): void
76 | }
77 | /**组件修饰器,一般有做处理自定义组件 */
78 | getFieldDecorator: {
79 | (
80 | name: TName,
81 | options?: FieldOptions
82 | ): {
83 | (node: React.ReactNode): React.ReactNode
84 | }
85 | }
86 | getFieldProps: {
87 | (
88 | name: TName,
89 | options?: FieldOptions
90 | ): RCFormOptions
91 | }
92 | getFieldsValue: {
93 | (names: TName[]): {
94 | [TField in TName]: PickOne
95 | }
96 | }
97 | getFieldError: (name: keyof TSource) => string[] | null
98 | getAllValues: () => TSource
99 | }
100 | type FormProps = T & {
101 | /**RCForm 表单 */
102 | form: RCForm.FormInstance
103 | }
104 |
105 | interface FC extends React.FC> {
106 | }
107 | interface ComponentType extends React.ComponentType> {
108 | }
109 | }
110 |
111 | declare module "rc-form" {
112 | export function createForm(option?: RCForm.CreateFromOptions): (c: RCForm.ComponentType) => React.ComponentType
113 | }
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/resource/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/resource/.DS_Store
--------------------------------------------------------------------------------
/src/resource/ads/字帖-2020-02-25.xls:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/resource/ads/字帖-2020-02-25.xls
--------------------------------------------------------------------------------
/src/resource/fonts/FZKTJW.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/resource/fonts/FZKTJW.TTF
--------------------------------------------------------------------------------
/src/resource/fonts/FZSJ-DQYBKSJW.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/resource/fonts/FZSJ-DQYBKSJW.TTF
--------------------------------------------------------------------------------
/src/resource/fonts/FZXKTJW.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/resource/fonts/FZXKTJW.TTF
--------------------------------------------------------------------------------
/src/resource/fonts/FZYBKSJW.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/resource/fonts/FZYBKSJW.TTF
--------------------------------------------------------------------------------
/src/resource/fonts/FZZJ-FYJW.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/resource/fonts/FZZJ-FYJW.TTF
--------------------------------------------------------------------------------
/src/resource/fonts/PZHGBZTJW.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/resource/fonts/PZHGBZTJW.TTF
--------------------------------------------------------------------------------
/src/resource/fonts/STFWXZKJW.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/resource/fonts/STFWXZKJW.TTF
--------------------------------------------------------------------------------
/src/resource/fonts/TYZKSJW.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/resource/fonts/TYZKSJW.TTF
--------------------------------------------------------------------------------
/src/resource/images/IMG_7308.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hulk-yin/copybook/48af120cd7fa39e148a91a7cc6f29381e1c991b0/src/resource/images/IMG_7308.JPG
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.update();
62 | registration.onupdatefound = () => {
63 | const installingWorker = registration.installing;
64 | if (installingWorker == null) {
65 | return;
66 | }
67 | installingWorker.onstatechange = () => {
68 | if (installingWorker.state === 'installed') {
69 | if (navigator.serviceWorker.controller) {
70 | // At this point, the updated precached content has been fetched,
71 | // but the previous service worker will still serve the older
72 | // content until all client tabs are closed.
73 | console.log(
74 | 'New content is available and will be used when all ' +
75 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
76 | );
77 |
78 | // Execute callback
79 | if (config && config.onUpdate) {
80 | config.onUpdate(registration);
81 | }
82 | } else {
83 | // At this point, everything has been precached.
84 | // It's the perfect time to display a
85 | // "Content is cached for offline use." message.
86 | console.log('Content is cached for offline use.');
87 |
88 | // Execute callback
89 | if (config && config.onSuccess) {
90 | config.onSuccess(registration);
91 | }
92 | }
93 | }
94 | };
95 | };
96 | })
97 | .catch(error => {
98 | console.error('Error during service worker registration:', error);
99 | });
100 | }
101 |
102 | function checkValidServiceWorker(swUrl, config) {
103 | // Check if the service worker can be found. If it can't reload the page.
104 | fetch(swUrl, {
105 | headers: { 'Service-Worker': 'script' }
106 | })
107 | .then(response => {
108 | // Ensure service worker exists, and that we really are getting a JS file.
109 | const contentType = response.headers.get('content-type');
110 | if (
111 | response.status === 404 ||
112 | (contentType != null && contentType.indexOf('javascript') === -1)
113 | ) {
114 | // No service worker found. Probably a different app. Reload the page.
115 | navigator.serviceWorker.ready.then(registration => {
116 | registration.unregister().then(() => {
117 | window.location.reload();
118 | });
119 | });
120 | } else {
121 | // Service worker found. Proceed as normal.
122 | registerValidSW(swUrl, config);
123 | }
124 | })
125 | .catch(() => {
126 | console.log(
127 | 'No internet connection found. App is running in offline mode.'
128 | );
129 | });
130 | }
131 |
132 | export function unregister() {
133 | if ('serviceWorker' in navigator) {
134 | navigator.serviceWorker.ready
135 | .then(registration => {
136 | registration.unregister();
137 | })
138 | .catch(error => {
139 | console.error(error.message);
140 | });
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "experimentalDecorators":true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------