├── .editorconfig
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── config
├── config.ts
├── postcssPlugins.ts
└── router.ts
├── images
└── index.png
├── package.json
├── src
├── app.tsx
├── assets
│ └── public
│ │ ├── fiveStars.png
│ │ └── upload.png
├── components
│ └── Loading
│ │ ├── index.less
│ │ └── index.tsx
├── layouts
│ └── index.tsx
├── pages
│ ├── document.ejs
│ └── index
│ │ ├── components
│ │ ├── ImageModal.tsx
│ │ └── index.less
│ │ ├── img.ts
│ │ ├── index.less
│ │ └── index.tsx
├── styles
│ ├── global.less
│ ├── index.less
│ └── reset.less
└── utils
│ ├── api.ts
│ ├── dev.ts
│ ├── file.ts
│ └── props.ts
├── tsconfig.json
└── typings.d.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: github pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-20.04
11 | steps:
12 | - uses: actions/checkout@v2
13 | with:
14 | submodules: recursive
15 | fetch-depth: 0
16 |
17 | - name: Build
18 | uses: actions/setup-node@master
19 | - run: npm install
20 | - run: npm run build
21 |
22 | - name: Deploy
23 | uses: peaceiris/actions-gh-pages@v3
24 | with:
25 | github_token: ${{ secrets.ACCESS_TOKEN }}
26 | publish_dir: './dist'
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /npm-debug.log*
6 | /yarn-error.log
7 | /yarn.lock
8 | /package-lock.json
9 |
10 | # production
11 | /dist
12 |
13 | # misc
14 | .DS_Store
15 |
16 | # umi
17 | /src/.umi
18 | /src/.umi-production
19 | /src/.umi-test
20 | /.env.local
21 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/*.md
2 | **/*.svg
3 | **/*.ejs
4 | **/*.html
5 | package.json
6 | .umi
7 | .umi-production
8 | .umi-test
9 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "printWidth": 120,
5 | "overrides": [
6 | {
7 | "files": ".prettierrc",
8 | "options": { "parser": "json" }
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 [Sharida]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 国庆专属 生成渐变红旗头像
2 |
3 | ### 特性
4 |
5 | - 纯前端实现
6 | - `React` + `TypeScript`
7 | - `canvas` 绘图
8 | - `cropperjs` 裁切头像
9 |
10 | ### 在线体验
11 |
12 | 在线体验:[渐变红旗头像生成](https://findmio.github.io/Five-star-red-flag-Avatar)
13 |
14 |
15 | ### 界面
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/config/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'umi';
2 | import routes from './router'; // 路由
3 | import extraPostCSSPlugins from './postcssPlugins'; // 额外的 postcss 配置
4 |
5 | const path = '/Five-star-red-flag-Avatar/';
6 |
7 | export default defineConfig({
8 | routes,
9 | extraPostCSSPlugins,
10 | alias: {
11 | config: '/config',
12 | },
13 |
14 | publicPath: process.env.NODE_ENV === 'production' ? path : '/',
15 | base: path,
16 |
17 | fastRefresh: {},
18 | extraBabelPlugins: [['import', { libraryName: 'antd-mobile', libraryDirectory: 'es/components', style: false }]],
19 | dynamicImport: {
20 | loading: '@/components/Loading',
21 | },
22 |
23 | targets: {
24 | ios: 10,
25 | chrome: 79,
26 | ie: false,
27 | firefox: false,
28 | safari: false,
29 | edge: false,
30 | },
31 |
32 | // 优化项
33 | mfsu: {},
34 | mock: false,
35 | antd: false,
36 | hash: true,
37 | ignoreMomentLocale: true,
38 | nodeModulesTransform: {
39 | type: 'none',
40 | exclude: [],
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/config/postcssPlugins.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | require('postcss-px-to-viewport')({
3 | viewportWidth: 375, // 视窗的宽度,对应的是我们设计稿的宽度,一般是375
4 | unitPrecision: 3, // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
5 | viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用vw
6 | selectorBlackList: [], // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
7 | minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
8 | mediaQuery: false, // 允许在媒体查询中转换`px`
9 | }),
10 | // require('autoprefixer'),
11 | ]
--------------------------------------------------------------------------------
/config/router.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | path: '/',
4 | component: '@/layouts',
5 | routes: [
6 | { path: '/', component: '@/pages/index', title: '渐变红旗头像生成' },
7 | ],
8 | },
9 | ];
10 |
--------------------------------------------------------------------------------
/images/index.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/findmio/Five-star-red-flag-Avatar/8bb49ea481c598d1e11343458f67907445ada4d1/images/index.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "start": "umi dev",
5 | "build": "umi build",
6 | "deploy": "npm run build && gh-pages -d dist",
7 | "postinstall": "umi generate tmp",
8 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
9 | "test": "umi-test",
10 | "test:coverage": "umi-test --coverage"
11 | },
12 | "gitHooks": {
13 | "pre-commit": "lint-staged"
14 | },
15 | "lint-staged": {
16 | "*.{js,jsx,less,md,json}": [
17 | "prettier --write"
18 | ],
19 | "*.ts?(x)": [
20 | "prettier --parser=typescript --write"
21 | ]
22 | },
23 | "dependencies": {
24 | "@ant-design/pro-layout": "^6.5.0",
25 | "ahooks": "^3.5.0",
26 | "antd-mobile": "^5.15.1",
27 | "axios": "^0.21.4",
28 | "cropperjs": "^1.5.12",
29 | "dayjs": "^1.10.7",
30 | "lodash": "^4.17.21",
31 | "qs": "^6.10.1",
32 | "react": "17.x",
33 | "react-dom": "17.x",
34 | "react-query": "^3.24.4",
35 | "umi": "^3.5.19"
36 | },
37 | "devDependencies": {
38 | "@types/lodash": "^4.14.175",
39 | "@types/qs": "^6.9.7",
40 | "@types/react": "^17.0.0",
41 | "@types/react-dom": "^17.0.0",
42 | "@umijs/preset-react": "1.x",
43 | "@umijs/test": "^3.5.19",
44 | "autoprefixer": "^10.3.5",
45 | "babel-plugin-import": "^1.13.3",
46 | "gh-pages": "^3.2.3",
47 | "lint-staged": "^10.0.7",
48 | "postcss-px-to-viewport": "^1.1.1",
49 | "prettier": "^2.2.0",
50 | "typescript": "^4.1.2",
51 | "yorkie": "^2.0.0"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { QueryClient, QueryClientProvider } from 'react-query';
3 | import 'antd-mobile/es/global';
4 | import '@/styles/index.less';
5 |
6 | export function onRouteChange(props: { matchedRoutes: any }) {
7 | const { matchedRoutes } = props;
8 | if (matchedRoutes.length) {
9 | }
10 | }
11 |
12 | const queryClient = new QueryClient({
13 | defaultOptions: {
14 | queries: {
15 | refetchOnWindowFocus: false, // 窗口获得焦点时重新获取数据
16 | },
17 | },
18 | });
19 |
20 | /**
21 | * react-dom 渲染时的根组件
22 | */
23 | export function rootContainer(container: any) {
24 | const App = () => {container};
25 | return React.createElement(App, null, container);
26 | }
27 |
--------------------------------------------------------------------------------
/src/assets/public/fiveStars.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/findmio/Five-star-red-flag-Avatar/8bb49ea481c598d1e11343458f67907445ada4d1/src/assets/public/fiveStars.png
--------------------------------------------------------------------------------
/src/assets/public/upload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/findmio/Five-star-red-flag-Avatar/8bb49ea481c598d1e11343458f67907445ada4d1/src/assets/public/upload.png
--------------------------------------------------------------------------------
/src/components/Loading/index.less:
--------------------------------------------------------------------------------
1 | .page {
2 | position: fixed;
3 | z-index: 999;
4 | background-color: #fff;
5 | width: 100vw;
6 | height: 100vh;
7 | overflow: hidden;
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | }
12 |
13 | .lds-spinner,
14 | .lds-spinner div,
15 | .lds-spinner div:after {
16 | box-sizing: border-box;
17 | }
18 |
19 | .lds-spinner {
20 | color: black;
21 | display: inline-block;
22 | position: relative;
23 | width: 80px;
24 | height: 80px;
25 | transform: scale(0.6);
26 | }
27 |
28 | .lds-spinner div {
29 | transform-origin: 40px 40px;
30 | animation: lds-spinner 1.2s linear infinite;
31 | }
32 |
33 | .lds-spinner div:after {
34 | content: ' ';
35 | display: block;
36 | position: absolute;
37 | top: 3.2px;
38 | left: 36.8px;
39 | width: 4.4px;
40 | height: 12.6px;
41 | border-radius: 20%;
42 | background: currentColor;
43 | }
44 |
45 | .lds-spinner div:nth-child(1) {
46 | transform: rotate(0deg);
47 | animation-delay: -1.1s;
48 | }
49 |
50 | .lds-spinner div:nth-child(2) {
51 | transform: rotate(30deg);
52 | animation-delay: -1s;
53 | }
54 |
55 | .lds-spinner div:nth-child(3) {
56 | transform: rotate(60deg);
57 | animation-delay: -0.9s;
58 | }
59 |
60 | .lds-spinner div:nth-child(4) {
61 | transform: rotate(90deg);
62 | animation-delay: -0.8s;
63 | }
64 |
65 | .lds-spinner div:nth-child(5) {
66 | transform: rotate(120deg);
67 | animation-delay: -0.7s;
68 | }
69 |
70 | .lds-spinner div:nth-child(6) {
71 | transform: rotate(150deg);
72 | animation-delay: -0.6s;
73 | }
74 |
75 | .lds-spinner div:nth-child(7) {
76 | transform: rotate(180deg);
77 | animation-delay: -0.5s;
78 | }
79 |
80 | .lds-spinner div:nth-child(8) {
81 | transform: rotate(210deg);
82 | animation-delay: -0.4s;
83 | }
84 |
85 | .lds-spinner div:nth-child(9) {
86 | transform: rotate(240deg);
87 | animation-delay: -0.3s;
88 | }
89 |
90 | .lds-spinner div:nth-child(10) {
91 | transform: rotate(270deg);
92 | animation-delay: -0.2s;
93 | }
94 |
95 | .lds-spinner div:nth-child(11) {
96 | transform: rotate(300deg);
97 | animation-delay: -0.1s;
98 | }
99 |
100 | .lds-spinner div:nth-child(12) {
101 | transform: rotate(330deg);
102 | animation-delay: 0s;
103 | }
104 |
105 | @keyframes lds-spinner {
106 | 0% {
107 | opacity: 1;
108 | }
109 |
110 | 100% {
111 | opacity: 0;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/Loading/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './index.less';
3 |
4 |
5 | const Loading = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | };
25 | export default Loading;
--------------------------------------------------------------------------------
/src/layouts/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Layout: React.FC = (props) => {
4 | const { children } = props;
5 |
6 | return <>{children}>;
7 | };
8 | export default Layout;
9 |
--------------------------------------------------------------------------------
/src/pages/document.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/pages/index/components/ImageModal.tsx:
--------------------------------------------------------------------------------
1 | import Cropper from 'cropperjs';
2 | import React, { useEffect, useRef } from 'react';
3 | import styles from './index.less';
4 | import { Dialog } from 'antd-mobile';
5 | import { getDataUrl } from '@/utils/file';
6 | import 'cropperjs/dist/cropper.css';
7 |
8 | interface Props {
9 | imageModalVisible: boolean;
10 | onClose(visible: boolean): void;
11 | onConfirm(data: string): void;
12 | imgData: string;
13 | }
14 |
15 | interface CProps {
16 | url: string;
17 | store: any;
18 | }
19 |
20 | const Content: React.FC = (props) => {
21 | const { url, store } = props;
22 | const imageRef = useRef(null);
23 |
24 | useEffect(() => {
25 | if (imageRef.current) {
26 | store.current = new Cropper(imageRef.current, {
27 | aspectRatio: 1,
28 | autoCropArea: 1,
29 | viewMode: 1,
30 | movable: false,
31 | zoomable: false,
32 | });
33 | }
34 | }, [url]);
35 |
36 | return (
37 |
38 |

39 |
40 | );
41 | };
42 |
43 | const ImageModal: React.FC = (props) => {
44 | const { imageModalVisible, onClose, imgData, onConfirm } = props;
45 | const cropperRef = useRef(null);
46 |
47 | const handleConfirm = () => {
48 | cropperRef.current.getCroppedCanvas().toBlob(async (blob: Blob) => {
49 | const data = await getDataUrl(blob);
50 | onConfirm(data);
51 | });
52 | };
53 |
54 | return (
55 |