├── .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 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /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 | { 43 | this.element = el; 44 | }} 45 | style={{ 46 | marginTop: "50", 47 | border: "1px solid #999999" 48 | }} 49 | width="300" 50 | height="300" 51 | id="grid-background-target"> 52 | 53 | 54 | 55 | 56 | 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
7 | { 10 | 11 | window._czc && window._czc.push(["_trackEvent", "help", "click", '查看帮', 1, 'help_btn']); 12 | 13 | setVisible(true) 14 | }} 15 | /> 16 | { 19 | setVisible(false) 20 | }} 21 | style={{ 22 | height: "400px" 23 | }} 24 | > 25 |
28 |
29 | 30 | {/* 31 | 微信号(点击复制): { 35 | e.target.select(); 36 | document.execCommand('Copy'); 37 | Toast.info("复制成功") 38 | }} 39 | /> */} 40 |
41 |
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 | { 16 | this.setState({ visible: true }) 17 | }} 18 | xmlns="http://www.w3.org/2000/svg" 19 | style={{ 20 | margin: 2, 21 | border: "1px solid #999999" 22 | }} 23 | width={size} 24 | height={size} 25 | id="grid-background-target"> 26 | 27 | {type === "mi" ? 28 | 29 | 30 | : null} 31 | 32 | 33 | 34 | {children} 42 | 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 |
66 |
67 | 68 | 69 |
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 | --------------------------------------------------------------------------------