├── mock ├── .gitkeep ├── dayList.js └── homeList.js ├── public └── a.txt ├── src ├── pages │ ├── dayList │ │ ├── index.less │ │ ├── service.ts │ │ ├── model.ts │ │ ├── index.tsx │ │ └── components │ │ │ └── QueryDay │ │ │ └── index.tsx │ ├── home │ │ ├── service.ts │ │ ├── index.less │ │ ├── model.ts │ │ ├── components │ │ │ ├── TodayTable.tsx │ │ │ ├── TomorrowTable.tsx │ │ │ └── OutDateTable.tsx │ │ └── index.tsx │ ├── userInfo │ │ └── index.tsx │ ├── login │ │ └── index.tsx │ └── newSchedule │ │ └── index.tsx ├── layouts │ ├── index.less │ └── index.tsx ├── global.css ├── config │ ├── baseUrl.ts │ └── schedule.ts └── components │ ├── Editor │ ├── DynamicIndex.tsx │ └── index.tsx │ └── Menu │ └── index.tsx ├── .prettierignore ├── .prettierrc ├── test ├── index.test.js ├── home.model.test.js └── home.page.test.js ├── jest.config.js ├── typings.d.ts ├── .editorconfig ├── .gitignore ├── .umirc.ts ├── .eslintrc.js ├── tsconfig.json ├── serverHelper.js ├── package.json ├── server.js └── README.md /mock/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/a.txt: -------------------------------------------------------------------------------- 1 | 1111 -------------------------------------------------------------------------------- /src/pages/dayList/index.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layouts/index.less: -------------------------------------------------------------------------------- 1 | .layout { 2 | min-height: 100vh; 3 | } 4 | -------------------------------------------------------------------------------- /src/global.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-bg-color: #f6f7f9;; 3 | --main-color: #1d6efe; 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | .umi 7 | .umi-production 8 | .umi-test 9 | -------------------------------------------------------------------------------- /src/config/baseUrl.ts: -------------------------------------------------------------------------------- 1 | // 服务端请求无法走相对地址 这里配置请求的baseUrl 以及其他baseUrl 2 | 3 | const baseUrl = { 4 | requestUrl: 'http://localhost:8000', 5 | }; 6 | 7 | export default baseUrl; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | describe('jest单测启动', () => { 2 | test('jest单测启动', () => { 3 | // const wrapper = shallow(); 4 | // expect(wrapper.find('.title').length).toEqual(1); 5 | expect(1).toEqual(1); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // 测试配置说明 https://github.com/umijs/umi/issues/446 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | moduleNameMapper: { 7 | '^@/(.*)$': '/src/$1', 8 | '\\.(css|less|sass|scss)$': require.resolve('identity-obj-proxy'), 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | declare module '*.png'; 4 | declare module '*.svg' { 5 | export function ReactComponent( 6 | props: React.SVGProps, 7 | ): React.ReactElement; 8 | const url: string; 9 | export default url; 10 | } 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/components/Editor/DynamicIndex.tsx: -------------------------------------------------------------------------------- 1 | import { dynamic } from 'umi'; 2 | 3 | export default dynamic({ 4 | loader: async function () { 5 | // 这里的注释 webpackChunkName 可以指导 webpack 将该组件 HugeA 以这个名字单独拆出去 6 | const { default: Index } = await import( 7 | /* webpackChunkName: "external_A" */ './index' 8 | ); 9 | return Index; 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/pages/dayList/service.ts: -------------------------------------------------------------------------------- 1 | import request from 'umi-request'; 2 | import baseUrl from '@/config/baseUrl'; 3 | export interface todayParamsType { 4 | userId: string; 5 | } 6 | export async function getDayList(params: todayParamsType) { 7 | return request(`${baseUrl.requestUrl}/api/dayList`, { 8 | method: 'get', 9 | params: { id: 1 }, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/pages/home/service.ts: -------------------------------------------------------------------------------- 1 | import request from 'umi-request'; 2 | import baseUrl from '@/config/baseUrl'; 3 | export interface todayParamsType { 4 | userId: string; 5 | } 6 | export async function getTodayList(params: todayParamsType) { 7 | return request(`${baseUrl.requestUrl}/api/homeList`, { 8 | method: 'get', 9 | params: { id: 1 || params.userId }, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'umi'; 2 | 3 | export default defineConfig({ 4 | nodeModulesTransform: { 5 | type: 'none', 6 | }, 7 | // routes: [ // 走约定式路由 8 | // { path: '/', component: '@/pages/index' }, 9 | // ], 10 | fastRefresh: {}, 11 | ssr: { 12 | mode: 'stream', 13 | // forceInitial: true // 无论是首屏还是页面切换,都会触发 getInitialProps 14 | }, 15 | dva: {}, 16 | publicPath: '/dist/', 17 | }); 18 | -------------------------------------------------------------------------------- /src/pages/home/index.less: -------------------------------------------------------------------------------- 1 | .ScheduleIndex { 2 | display: flex; 3 | min-height: 100vh; 4 | .todaySchedule { 5 | width: 60%; 6 | // h3 { 7 | // text-align: center; 8 | // color: var(--main-color); 9 | // } 10 | } 11 | .otherSchedule { 12 | width: 40%; 13 | h3 { 14 | color: var(--main-color); 15 | text-align: center; 16 | } 17 | .outDateSchedule { 18 | // height: 50%; 19 | } 20 | .tomorrowSchedule { 21 | // height: 50%; 22 | } 23 | } 24 | .btnWrapper { 25 | display: flex; 26 | justify-content: space-between; 27 | text-align: center; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'umi'; 3 | import { Menu } from 'antd'; 4 | import { MENUS } from '@/config/schedule'; 5 | const menu = () => { 6 | const handleClick = () => { 7 | // console.log(111); 8 | }; 9 | return ( 10 | 18 | {MENUS.map((menu) => { 19 | return ( 20 | 21 | 22 | {menu.label} 23 | 24 | ); 25 | })} 26 | 27 | ); 28 | }; 29 | 30 | export default menu; 31 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | require.resolve('@umijs/fabric/dist/eslint'), 4 | 'plugin:import/typescript', 5 | 'plugin:prettier/recommended', 6 | ], 7 | plugins: ['import'], 8 | globals: { 9 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, 10 | page: true, 11 | REACT_APP_ENV: true, 12 | }, 13 | rules: { 14 | 'prettier/prettier': 'error', 15 | // 'import/order': [ 16 | // 'warn', 17 | // { 18 | // groups: ['index', 'sibling', 'parent', 'internal', 'external', 'builtin', 'object', 'type'], 19 | // }, 20 | // ], 21 | 'import/order': 'off', 22 | 'no-console': process.env.UMI_ENV === 'production' ? 'error' : 'off', 23 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "resolveJsonModule": true, 7 | "importHelpers": true, 8 | "jsx": "react-jsx", 9 | "esModuleInterop": true, 10 | "sourceMap": true, 11 | "baseUrl": "./", 12 | "strict": true, 13 | "paths": { 14 | "@/*": ["src/*"], 15 | "@@/*": ["src/.umi/*"] 16 | }, 17 | "allowSyntheticDefaultImports": true 18 | }, 19 | "include": [ 20 | "mock/**/*", 21 | "src/**/*", 22 | "config/**/*", 23 | ".umirc.ts", 24 | "typings.d.ts" 25 | ], 26 | "exclude": [ 27 | "node_modules", 28 | "lib", 29 | "es", 30 | "dist", 31 | "typings", 32 | "**/__test__", 33 | "test", 34 | "docs", 35 | "tests" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /serverHelper.js: -------------------------------------------------------------------------------- 1 | const parseCookie = (ctx) => { 2 | let cookies = ctx.get('cookie'); 3 | if (!cookies) { 4 | return []; 5 | } 6 | cookies = cookies.split(';'); 7 | const res = {}; 8 | for (const item of cookies) { 9 | const kv = item.split('='); 10 | if (kv && kv.length > 0) { 11 | res[kv[0].trim()] = decodeURIComponent(kv[1]); 12 | } 13 | } 14 | return res; 15 | }; 16 | 17 | const parseNavLang = (ctx) => { 18 | // 服务端无法获取navigator.language,所以只能通过Accept-Language来判断浏览器语言。 19 | let navigatorLang; 20 | const clientLang = ctx.get('Accept-Language'); 21 | if (clientLang.startsWith('zh')) { 22 | navigatorLang = 'zh-CN'; 23 | } else if (clientLang.startsWith('en')) { 24 | navigatorLang = 'en-US'; 25 | } 26 | return navigatorLang; 27 | }; 28 | 29 | module.exports = { 30 | parseCookie, 31 | parseNavLang, 32 | }; 33 | -------------------------------------------------------------------------------- /src/layouts/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { Layout, Card } from 'antd'; 3 | import Menu from '@/components/Menu'; 4 | import styles from './index.less'; 5 | const { Header, Footer, Sider, Content } = Layout; 6 | 7 | React.useLayoutEffect = 8 | typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; 9 | interface IProps { 10 | children: ReactNode; 11 | } 12 | 13 | const layout = (props: IProps) => { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | {/*
Header
*/} 21 | 22 | 23 | {props.children} 24 | 25 | 26 |
Footer
27 |
28 | 29 | ); 30 | }; 31 | 32 | export default layout; 33 | -------------------------------------------------------------------------------- /src/config/schedule.ts: -------------------------------------------------------------------------------- 1 | // 左侧导航 2 | export const MENUS = [ 3 | { 4 | label: '主页', 5 | link: '/home', 6 | key: 'home', 7 | }, 8 | { 9 | label: '日程管理', 10 | link: '/dayList', 11 | key: 'dayList', 12 | }, 13 | { 14 | label: '新建日程', 15 | link: '/newSchedule', 16 | key: 'newSchedule', 17 | }, 18 | ]; 19 | 20 | // 重复方式 21 | export const REPEATCONFIG = [ 22 | { 23 | label: '一次性活动', 24 | value: '1', 25 | }, 26 | { 27 | label: '每天', 28 | value: '2', 29 | }, 30 | { 31 | label: '每周', 32 | value: '3', 33 | }, 34 | { 35 | label: '每月', 36 | value: '4', 37 | }, 38 | { 39 | label: '每年', 40 | value: '5', 41 | }, 42 | ]; 43 | // 结束方式 暂时不用 目前只做结束时间 44 | export const STOPREPEATCONFIG = [ 45 | { 46 | label: '用不', 47 | value: '1', 48 | }, 49 | { 50 | label: '时间', 51 | value: '2', 52 | }, 53 | { 54 | label: '次数', 55 | value: '3', 56 | }, 57 | ]; 58 | -------------------------------------------------------------------------------- /test/home.model.test.js: -------------------------------------------------------------------------------- 1 | import { saga } from 'dva'; 2 | import * as service from '../src/pages/home/service'; 3 | import homeModel from '../src/pages/home/model'; 4 | 5 | const { effects } = saga; 6 | describe('home Model', () => { 7 | it('home Model loads', () => { 8 | expect(homeModel).toBeTruthy(); 9 | }); 10 | 11 | describe('effects', () => { 12 | it('home Model should work', () => { 13 | const { call, put } = effects; 14 | const getTodayListSaga = homeModel.effects.getTodayList; 15 | const generator = getTodayListSaga( 16 | { type: `getTodayList`, payload: {} }, 17 | { call, put }, 18 | ); 19 | // let next = generator.next(); 20 | // console.log('next.value===>', next.value); 21 | // expect(next.value).toEqual(call(service.getTodayList, 1000)); 22 | // next = generator.next({ 23 | // result: { 24 | // data: 1 25 | // } 26 | // }); 27 | // expect(next.value).toEqual(put({ type: `${homeModel.nameSpace}/setTodayList`, payload: '666' })); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/home.page.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import configureStore from 'redux-mock-store'; 4 | import HomePage from '@/pages/home'; 5 | 6 | describe('TestView', () => { 7 | const action = 'testView/query'; 8 | const initialState = { 9 | isLoading: true, 10 | queryResult: {}, 11 | }; 12 | const mockStore = configureStore(); 13 | let store; 14 | let container; 15 | beforeEach(() => { 16 | store = mockStore(initialState); 17 | container = shallow(); 18 | }); 19 | 20 | /* 测试 state 到 props 的映射是否正确 */ 21 | 22 | test('should pass state to props', () => { 23 | const props = container.props(); 24 | expect(props).toHaveProperty('isLoading', initialState.isLoading); 25 | expect(props).toHaveProperty('queryResult', initialState.queryResult); 26 | }); 27 | 28 | /* 测试 actions 到 props 的映射是否正确 */ 29 | 30 | test('should pass actions to props', () => { 31 | const props = container.props(); 32 | expect(props).toHaveProperty('queryList', expect.any(Function)); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/Editor/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Card } from 'antd'; 3 | import BraftEditor, { EditorState } from 'braft-editor'; 4 | 5 | import 'braft-editor/dist/index.css'; 6 | import 'codemirror/lib/codemirror.css'; 7 | import 'codemirror/theme/material.css'; 8 | import 'codemirror/mode/xml/xml'; 9 | import 'codemirror/mode/javascript/javascript'; 10 | 11 | interface EditProps { 12 | value?: any; 13 | onChange?: (value: any) => void; 14 | } 15 | 16 | const Editor: React.FC = ({ value = {}, onChange }) => { 17 | const defaultContent = BraftEditor.createEditorState(value); 18 | const [editorState, setEditorState] = useState({ 19 | content: defaultContent, 20 | html: '', 21 | json: '', 22 | }); 23 | 24 | const handleContentChange = (val: EditorState) => { 25 | setEditorState((state) => ({ 26 | ...state, 27 | content: val, 28 | })); 29 | onChange?.(value.toHTML()); 30 | }; 31 | return ( 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default Editor; 39 | -------------------------------------------------------------------------------- /src/pages/dayList/model.ts: -------------------------------------------------------------------------------- 1 | import { Effect, ImmerReducer, Reducer, Subscription } from 'umi'; 2 | import * as service from './service'; 3 | export interface DayListModelState { 4 | dayList: { total?: number; list?: any[] }; 5 | } 6 | export interface DayModelType { 7 | namespace: 'day'; 8 | state: DayListModelState; 9 | effects: { 10 | getDayList: Effect; 11 | }; 12 | reducers: { 13 | setDayList: Reducer; 14 | // 启用 immer 之后 15 | // save: ImmerReducer; 16 | }; 17 | // subscriptions: { setup: Subscription }; 18 | } 19 | 20 | const DayModel: DayModelType = { 21 | namespace: 'day', 22 | state: { 23 | dayList: {}, 24 | }, 25 | 26 | effects: { 27 | *getDayList({ payload }, { call, put }) { 28 | const result = yield call(service.getDayList, payload); 29 | yield put({ 30 | type: 'setDayList', 31 | payload: result.data, 32 | }); 33 | }, 34 | }, 35 | reducers: { 36 | setDayList(state, action) { 37 | return { 38 | ...state, 39 | dayList: { 40 | ...action.payload, 41 | }, 42 | }; 43 | }, 44 | }, 45 | }; 46 | 47 | export default DayModel; 48 | -------------------------------------------------------------------------------- /mock/dayList.js: -------------------------------------------------------------------------------- 1 | import { delay } from 'roadhog-api-doc'; 2 | 3 | const proxy = { 4 | '/api/dayList': { 5 | code: 0, 6 | data: { 7 | total: 6, 8 | list: [ 9 | { 10 | key: '1', 11 | name: 'John Brown', 12 | date: 32, 13 | remark: 'New York No. 1 Lake Park', 14 | }, 15 | { 16 | key: '2', 17 | name: 'Joe Black', 18 | date: 42, 19 | remark: 'London No. 1 Lake Park', 20 | }, 21 | { 22 | key: '3', 23 | name: 'Jim Green', 24 | date: 32, 25 | remark: 'Sidney No. 1 Lake Park', 26 | }, 27 | { 28 | key: '4', 29 | name: 'Jim Red', 30 | date: 32, 31 | remark: 'London No. 2 Lake Park', 32 | }, 33 | { 34 | key: '5', 35 | name: 'Jim Red xh', 36 | date: 32, 37 | remark: 'London No. 2 Lake Park', 38 | }, 39 | { 40 | key: '6', 41 | name: 'xim Red ', 42 | date: 32, 43 | remark: 'London No. 2 Lake Park', 44 | }, 45 | ], 46 | }, 47 | }, 48 | }; 49 | 50 | export default delay(proxy, 500); 51 | -------------------------------------------------------------------------------- /src/pages/home/model.ts: -------------------------------------------------------------------------------- 1 | import { Effect, Reducer } from 'umi'; 2 | import * as service from './service'; 3 | export interface IndexModelState { 4 | todayList: { total?: number; list?: any[] }; 5 | outDateList: { total?: number; list?: any[] }; 6 | tomorrowList: { total?: number; list?: any[] }; 7 | } 8 | export interface HomeModelType { 9 | namespace: 'home'; 10 | state: IndexModelState; 11 | effects: { 12 | getTodayList: Effect; 13 | }; 14 | reducers: { 15 | setTodayList: Reducer; 16 | // 启用 immer 之后 17 | // save: ImmerReducer; 18 | }; 19 | // subscriptions: { setup: Subscription }; 20 | } 21 | 22 | const HomeModel: HomeModelType = { 23 | namespace: 'home', 24 | state: { 25 | todayList: {}, 26 | outDateList: {}, 27 | tomorrowList: {}, 28 | }, 29 | 30 | effects: { 31 | *getTodayList({ payload }, { call, put }) { 32 | const result = yield call(service.getTodayList, payload); 33 | yield put({ 34 | type: 'setTodayList', 35 | payload: result.data, 36 | }); 37 | }, 38 | }, 39 | reducers: { 40 | setTodayList(state, action) { 41 | return { 42 | ...state, 43 | ...action.payload, 44 | }; 45 | }, 46 | }, 47 | }; 48 | 49 | export default HomeModel; 50 | -------------------------------------------------------------------------------- /src/pages/userInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Form, Input, Button, Checkbox, DatePicker, Select } from 'antd'; 3 | 4 | const userInfo = () => { 5 | const onFinish = (values: any) => { 6 | console.log('Success:', values); 7 | }; 8 | 9 | const onFinishFailed = (errorInfo: any) => { 10 | console.log('Failed:', errorInfo); 11 | }; 12 | 13 | return ( 14 |
23 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 43 | 44 |
45 | ); 46 | }; 47 | 48 | export default userInfo; 49 | -------------------------------------------------------------------------------- /src/pages/home/components/TodayTable.tsx: -------------------------------------------------------------------------------- 1 | // 大盘数据 2 | import styles from '../index.less'; 3 | import React from 'react'; 4 | import { Table, Button } from 'antd'; 5 | import type { IndexModelState } from '../model'; 6 | import type { ColumnsType } from 'antd/lib/table'; 7 | interface Iprops { 8 | todayList: IndexModelState['todayList'] | undefined; 9 | } 10 | const columns: ColumnsType = [ 11 | { 12 | title: '名称', 13 | dataIndex: 'name', 14 | key: 'name', 15 | width: '20%', 16 | }, 17 | { 18 | title: '日期', 19 | dataIndex: 'date', 20 | key: 'date', 21 | width: '20%', 22 | }, 23 | { 24 | title: '备注', 25 | dataIndex: 'remark', 26 | key: 'remark', 27 | width: '38%', 28 | }, 29 | { 30 | title: '操作', 31 | dataIndex: 'btn', 32 | key: 'btn', 33 | width: '22%', 34 | render: () => { 35 | return ( 36 |
37 | 38 | 41 | 42 |
43 | ); 44 | }, 45 | }, 46 | ]; 47 | function TodayTable(props: Iprops) { 48 | const { todayList } = props; 49 | return ; 50 | } 51 | 52 | export default TodayTable; 53 | -------------------------------------------------------------------------------- /src/pages/home/components/TomorrowTable.tsx: -------------------------------------------------------------------------------- 1 | // 大盘数据 2 | import React from 'react'; 3 | import { Table, Button } from 'antd'; 4 | import { ColumnsType } from 'antd/lib/table'; 5 | import { IndexModelState } from '../model'; 6 | import styles from '../index.less'; 7 | interface Iprops { 8 | tomorrowList: IndexModelState['tomorrowList'] | undefined; 9 | } 10 | const columns: ColumnsType = [ 11 | { 12 | title: '名称', 13 | dataIndex: 'name', 14 | key: 'name', 15 | width: '30%', 16 | }, 17 | { 18 | title: '日期', 19 | dataIndex: 'date', 20 | key: 'date', 21 | width: '30%', 22 | }, 23 | { 24 | title: '操作', 25 | dataIndex: 'btn', 26 | key: 'btn', 27 | width: '40%', 28 | render: () => { 29 | return ( 30 |
31 | 32 | 35 | 36 |
37 | ); 38 | }, 39 | }, 40 | ]; 41 | function todayTable(props: Iprops) { 42 | const { tomorrowList } = props; 43 | return ( 44 |
50 | ); 51 | } 52 | 53 | export default todayTable; 54 | -------------------------------------------------------------------------------- /src/pages/home/components/OutDateTable.tsx: -------------------------------------------------------------------------------- 1 | // 大盘数据 2 | import styles from '../index.less'; 3 | import React from 'react'; 4 | import { Table, Button } from 'antd'; 5 | import type { IndexModelState } from '../model'; 6 | import type { ColumnsType } from 'antd/lib/table'; 7 | interface Iprops { 8 | outDateList: IndexModelState['outDateList'] | undefined; 9 | } 10 | const columns: ColumnsType = [ 11 | { 12 | title: '名称', 13 | dataIndex: 'name', 14 | key: 'name', 15 | width: '30%', 16 | }, 17 | { 18 | title: '日期', 19 | dataIndex: 'date', 20 | key: 'date', 21 | width: '30%', 22 | }, 23 | { 24 | title: '操作', 25 | dataIndex: 'btn', 26 | key: 'btn', 27 | width: '40%', 28 | render: () => { 29 | return ( 30 |
31 | 32 | 35 | 36 |
37 | ); 38 | }, 39 | }, 40 | ]; 41 | function OutDateTable(props: Iprops) { 42 | const { outDateList } = props; 43 | return ( 44 |
50 | ); 51 | } 52 | 53 | export default OutDateTable; 54 | -------------------------------------------------------------------------------- /src/pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Input, Button, Checkbox } from 'antd'; 3 | 4 | const login = () => { 5 | const onFinish = (values: any) => { 6 | // eslint-disable-next-line no-console 7 | console.log('Success:', values); 8 | }; 9 | 10 | const onFinishFailed = (errorInfo: any) => { 11 | // eslint-disable-next-line no-console 12 | console.log('Failed:', errorInfo); 13 | }; 14 | 15 | return ( 16 | 25 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 46 | 记住密码 47 | 48 | 49 | 50 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default login; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "umi dev", 5 | "build": "umi build", 6 | "postinstall": "umi generate tmp", 7 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", 8 | "test": "umi-test", 9 | "test:coverage": "umi-test --coverage" 10 | }, 11 | "gitHooks": { 12 | "pre-commit": "lint-staged" 13 | }, 14 | "lint-staged": { 15 | "*.{js,jsx,less,md,json}": [ 16 | "prettier --write" 17 | ], 18 | "*.ts?(x)": [ 19 | "prettier --parser=typescript --write" 20 | ] 21 | }, 22 | "dependencies": { 23 | "@ant-design/pro-layout": "^6.5.0", 24 | "braft-editor": "^2.3.9", 25 | "codemirror": "^5.62.3", 26 | "enzyme": "^3.11.0", 27 | "enzyme-adapter-react-16": "^1.15.6", 28 | "koa": "^2.13.4", 29 | "koa-compress": "^5.1.0", 30 | "koa-mount": "^4.0.0", 31 | "koa-static": "^5.0.0", 32 | "mockjs": "^1.1.0", 33 | "react": "17.x", 34 | "react-codemirror2": "^7.2.1", 35 | "react-dom": "17.x", 36 | "react-highlight-words": "^0.17.0", 37 | "roadhog-api-doc": "^1.1.2", 38 | "umi": "^3.4.0", 39 | "umi-request": "^1.3.9", 40 | "umi-server": "^1.2.3" 41 | }, 42 | "devDependencies": { 43 | "@types/react": "^17.0.0", 44 | "@types/react-dom": "^17.0.0", 45 | "@types/react-highlight-words": "^0.16.3", 46 | "@umijs/preset-react": "^1.8.22", 47 | "@umijs/test": "^3.5.17", 48 | "eslint": "^7.32.0", 49 | "eslint-plugin-import": "^2.24.2", 50 | "eslint-plugin-prettier": "^4.0.0", 51 | "lint-staged": "^10.0.7", 52 | "prettier": "^2.2.0", 53 | "typescript": "^4.1.2", 54 | "yorkie": "^2.0.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/pages/home/index.tsx: -------------------------------------------------------------------------------- 1 | // 大盘数据 2 | import styles from './index.less'; 3 | import TodayTable from './components/TodayTable'; 4 | import OutDateTable from './components/OutDateTable'; 5 | import TomorrowTable from './components/TomorrowTable'; 6 | import React from 'react'; 7 | import { connect } from 'dva'; 8 | import { Card } from 'antd'; 9 | import type { IGetInitialProps } from 'umi'; 10 | import type { IndexModelState } from './model'; 11 | 12 | interface Iprops { 13 | home: IndexModelState; 14 | } 15 | function IndexPage(props: Iprops) { 16 | const { home: { todayList, outDateList, tomorrowList } = {} } = props; 17 | return ( 18 |
19 |
20 | 21 | 22 | 23 |
24 |
25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 |
37 | ); 38 | } 39 | 40 | // 页面组件上的 getInitialProps 静态方法,执行后将结果注入到该页面组件的 props 中 41 | IndexPage.getInitialProps = (async (ctx) => { 42 | const { store } = ctx; 43 | await store.dispatch({ 44 | type: 'home/getTodayList', 45 | }); 46 | return store.getState(); 47 | }) as IGetInitialProps; 48 | 49 | export default connect((rootState) => ({ 50 | rootState, 51 | }))(IndexPage); 52 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const compress = require('koa-compress'); 3 | const mount = require('koa-mount'); 4 | const { join, extname } = require('path'); 5 | const { parseCookie, parseNavLang } = require('./serverHelper'); 6 | 7 | const root = join(__dirname, 'dist'); 8 | 9 | const app = new Koa(); 10 | app.use( 11 | compress({ 12 | threshold: 2048, 13 | gzip: { 14 | flush: require('zlib').constants.Z_SYNC_FLUSH, 15 | }, 16 | deflate: { 17 | flush: require('zlib').constants.Z_SYNC_FLUSH, 18 | }, 19 | br: false, // 禁用br解决https gzip不生效加载缓慢问题 20 | }), 21 | ); 22 | 23 | let render; 24 | app.use(async (ctx, next) => { 25 | /** 26 | * 扩展global对象 27 | * 28 | * 这里是在服务端处理好cookie, 29 | * 会把所有cookie处理成{}形式 30 | * 赋值到global上面,方便客户端使用 31 | * 32 | * 同时获取浏览器的默认语言,处理好 33 | */ 34 | global._cookies = parseCookie(ctx); 35 | global._navigatorLang = parseNavLang(ctx); 36 | 37 | const ext = extname(ctx.request.path); 38 | // 符合要求的路由才进行服务端渲染,否则走静态文件逻辑 39 | if (!ext) { 40 | if (!render) { 41 | render = require('./dist/umi.server'); 42 | } 43 | // 这里默认是字符串渲染 44 | ctx.type = 'text/html'; 45 | ctx.status = 200; 46 | const { html, error } = await render({ 47 | path: ctx.request.url, 48 | }); 49 | if (error) { 50 | console.log('----------------服务端报错-------------------', error); 51 | ctx.throw(500, error); 52 | } 53 | ctx.body = html; 54 | } else { 55 | await next(); 56 | } 57 | }); 58 | 59 | /** 60 | * 注意这里的静态目录设置,需要和umi打包出来的目录是同一个 61 | * 这里最好是用nginx配置静态目录,如果是用cdn方式部署,这里可以忽略 62 | * 63 | */ 64 | app.use(mount('/dist', require('koa-static')(root))); 65 | 66 | app.listen(7001); 67 | console.log('http://localhost:7001'); 68 | 69 | module.exports = app.callback(); 70 | -------------------------------------------------------------------------------- /src/pages/dayList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'dva'; 3 | import { Table, Button } from 'antd'; 4 | import styles from './index.less'; 5 | import type { DayListModelState } from './model'; 6 | import type { IGetInitialProps } from 'umi'; 7 | import type { ColumnsType } from 'antd/lib/table'; 8 | 9 | import QueryDay from './components/QueryDay'; 10 | 11 | interface Iprops { 12 | day: DayListModelState; 13 | } 14 | const columns: ColumnsType = [ 15 | { 16 | title: '名称', 17 | dataIndex: 'name', 18 | key: 'name', 19 | width: '10%', 20 | }, 21 | { 22 | title: '日期', 23 | dataIndex: 'date', 24 | key: 'date', 25 | width: '10%', 26 | }, 27 | { 28 | title: '状态', 29 | dataIndex: 'status', 30 | key: 'status', 31 | width: '10%', 32 | }, 33 | { 34 | title: '备注', 35 | dataIndex: 'remark', 36 | key: 'remark', 37 | width: '38%', 38 | }, 39 | { 40 | title: '操作', 41 | dataIndex: 'btn', 42 | key: 'btn', 43 | width: '22%', 44 | render: () => { 45 | return ( 46 |
47 | 48 | 51 | 52 |
53 | ); 54 | }, 55 | }, 56 | ]; 57 | 58 | function DayList(props: Iprops) { 59 | const { day: { dayList } = {} } = props; 60 | return ( 61 | <> 62 | 63 |
; 64 | 65 | ); 66 | } 67 | 68 | DayList.getInitialProps = (async (ctx) => { 69 | const { store } = ctx; 70 | await store.dispatch({ 71 | type: 'day/getDayList', 72 | }); 73 | return store.getState(); 74 | }) as IGetInitialProps; 75 | 76 | export default connect((rootState) => ({ 77 | rootState, 78 | }))(DayList); 79 | -------------------------------------------------------------------------------- /src/pages/dayList/components/QueryDay/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Row, Button, Space, Input, Select, DatePicker } from 'antd'; 3 | import { PlusOutlined } from '@ant-design/icons'; 4 | const { RangePicker } = DatePicker; 5 | 6 | const fieldsList = [ 7 | { 8 | label: '日程名称', 9 | name: 'task_name', 10 | placeholder: '请输入任务名', 11 | disabledEdit: false, 12 | rules: [{ max: 100, message: '最长不可超过100个字符' }], 13 | }, 14 | { 15 | label: '日程创建时间', 16 | name: 'add_time', 17 | placeholder: '请选择日程创建时间', 18 | render: () => ( 19 | 20 | ), 21 | }, 22 | { 23 | label: '日程状态', 24 | name: 'task_status', 25 | placeholder: '任务状态', 26 | render: () => { 27 | return ( 28 | 46 | ); 47 | }, 48 | }, 49 | ]; 50 | 51 | function QueryDay() { 52 | const [formInstance] = Form.useForm(); 53 | return ( 54 |
55 | {fieldsList.map((filed) => { 56 | return ( 57 | 64 | {filed.render ? ( 65 | filed.render() 66 | ) : ( 67 | 68 | )} 69 | 70 | ); 71 | })} 72 | 73 | 74 | 75 | 76 | 80 | 81 | 82 | 83 | ); 84 | } 85 | 86 | export default React.memo(QueryDay); 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 一,基于umi3开发的日程管理系统 2 | > 包括新建日程 日程列表, 打通微信, 定时推送日程 3 | **喜欢的帮忙点个star** 4 | 5 | 6 | # 二,开发说明 7 | 1. clone 项目 8 | 2. cd daymanage 9 | 3. npm i 10 | 4. npm start 11 | 5. 访问 /home 路径 作为主路径 12 | 13 | ## 目录 14 | ``` 15 | . 16 | ├── README.md 17 | ├── dist // 打包后输出文件目录 18 | ├── jest.config.js // 单元测试配置 19 | ├── mock // mock数据文件 20 | ├── package.json 21 | ├── public // 静态文件目录 可以直接```/a.txt```这样访问 22 | │ └── a.txt 23 | ├── src 24 | │ ├── components // 公共组件 25 | │ ├── config // 常用配置 26 | │ ├── global.css // 全局css 文件 27 | │ └── pages // 页面 28 | ├── test // 测试文件目录 29 | │ └── index.test.js 30 | ├── tsconfig.json // ts配置 31 | └── typings.d.ts 32 | ``` 33 | ## 其他 34 | 1. css变量在src/global.css中定义 后续在全局中使用; 如果考虑css变量兼容问题,则可以在.umirc.ts中define定义变量。umi会在编译时自动将所有变量替换 35 | 2. 开启dva配置:https://umijs.org/zh-CN/plugins/plugin-dva 36 | 3. request请求库 https://github.com/umijs/umi-request/blob/master/README_zh-CN.md 37 | 4. mock数据 https://umijs.org/zh-CN/docs/mock#%E7%BC%96%E5%86%99-mock-%E6%96%87%E4%BB%B6 38 | 39 | 40 | 41 | 42 | # 三,服务端渲染 43 | https://umijs.org/zh-CN/docs/ssr 44 | 45 | ## 其他问题 46 | 1. 服务端请求必须使用绝对的 URL 路径; 47 | 48 | 开启了 SSR 之后,app.getInitialData 以及 Home.getInitialProps 都会在服务端下执行,服务端发请求必须用绝对路径不能用相对路径,因此这两个方法里如果出现异步请求,请务必使用绝对路径,或者正确设置 request.baseURL 49 | 50 | 2. ssr与dva结合: https://umijs.org/zh-CN/docs/ssr#%E4%B8%8E-dva-%E7%BB%93%E5%90%88%E4%BD%BF%E7%94%A8 51 | 52 | ## 服务端地址 53 | 54 | 55 | # 四,单测 56 | - https://github.com/umijs/umi/issues/446 57 | - 如果你以前没有过单测经验或者对单测了解较少 推荐你阅读下面这便文章: 58 | https://juejin.cn/post/6844903798406643725 59 | https://juejin.cn/post/6844903878119424008#heading-26 60 | - TODO: https://v2-pro.ant.design/docs/ui-test-cn 测试参考 61 | 62 | # 部署 63 | 64 | 本地模拟, 进入根目录 65 | ``` 66 | npm run build 67 | // 编译结束后 68 | 69 | node server.js 70 | 71 | // 浏览器中打开http://localhost:7001 72 | ``` 73 | 线上其实也是这样,无非加一些进程守护工具当服务挂掉后自动重启; 74 | # 五,TODO 75 | 1. 需要chrome插件 打通浏览器弹出消息 76 | 2. 打通手机日历 77 | - 小米 78 | - 华为 79 | - 苹果 80 | - 降级的兜底方案是发送约会邮件 81 | 82 | # 六,你可能会遇见的一些常见问题 83 | 1. localhost请求失败 --> 看看你的host 是否配置了 localhost, 需要配置 84 | 2. ssr中的getInitialProps没有执行, 查看你的页面是不是用react.memo包裹起来了,不可以包裹 85 | 3. [SSR] ReferenceError: window is not defined 86 | > 1, Umi 3 默认移除了 DOM/BOM 浏览器 API 在 Node.js 的 polyfill,如果应用确实需要 polyfill 一些浏览器对象,可以使用 beforeRenderServer 运行时事件 API 进行扩展 87 | > 2, 你可以使用dynamic异步加载组件的方式 88 | 4. seLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes 89 | >1, https://reactjs.org/link/uselayouteffect-ssr 这里详细记录了产生的原因及解决方案 90 | >2, antd中的某些组件会引起这个警告 https://github.com/react-component/overflow/issues/6 91 | >3, 很多第三方组件 如果一个一个的解决比较麻烦 有一种比较暴力的方法: React.useLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/pages/newSchedule/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Form, Input, Button, Checkbox, DatePicker, Select } from 'antd'; 3 | import Editor from '@/components/Editor/DynamicIndex'; 4 | import { REPEATCONFIG } from '@/config/schedule'; 5 | const initialValue = { 6 | repeat: '1', 7 | }; 8 | const NewSchedule = () => { 9 | const [formValues, setFormValues] = useState(initialValue); 10 | const [formInstance] = Form.useForm(); 11 | const onFinish = (values: any) => { 12 | console.log('Success:', values); 13 | }; 14 | 15 | const onFinishFailed = (errorInfo: any) => { 16 | console.log('Failed:', errorInfo); 17 | }; 18 | 19 | const onValuesChange = (changedValues: any, values: any) => { 20 | setFormValues(values); 21 | }; 22 | 23 | return ( 24 |
25 |
36 | 44 | 45 | 46 | 47 | 52 | 53 | 54 | 60 | 69 | 70 | {formValues?.repeat !== '1' ? ( 71 | 72 | 73 | 74 | ) : null} 75 | 76 | {/* 服务端渲染的兼容 */} 77 | {window ? : null} 78 | 79 | 80 | 85 | 日历提醒我 86 | 87 | 88 | 89 | 92 | 93 | 94 |
95 | ); 96 | }; 97 | 98 | export default NewSchedule; 99 | -------------------------------------------------------------------------------- /mock/homeList.js: -------------------------------------------------------------------------------- 1 | import { delay } from 'roadhog-api-doc'; 2 | 3 | const proxy = { 4 | '/api/homeList': { 5 | code: 0, 6 | data: { 7 | todayList: { 8 | total: 6, 9 | list: [ 10 | { 11 | key: '1', 12 | name: 'John Brown', 13 | date: 32, 14 | remark: 'New York No. 1 Lake Park', 15 | }, 16 | { 17 | key: '2', 18 | name: 'Joe Black', 19 | date: 42, 20 | remark: 'London No. 1 Lake Park', 21 | }, 22 | { 23 | key: '3', 24 | name: 'Jim Green', 25 | date: 32, 26 | remark: 'Sidney No. 1 Lake Park', 27 | }, 28 | { 29 | key: '4', 30 | name: 'Jim Red', 31 | date: 32, 32 | remark: 'London No. 2 Lake Park', 33 | }, 34 | { 35 | key: '5', 36 | name: 'Jim Red xh', 37 | date: 32, 38 | remark: 'London No. 2 Lake Park', 39 | }, 40 | { 41 | key: '6', 42 | name: 'xim Red ', 43 | date: 32, 44 | remark: 'London No. 2 Lake Park', 45 | }, 46 | ], 47 | }, 48 | outDateList: { 49 | total: 4, 50 | list: [ 51 | { 52 | key: '1', 53 | name: 'John Brown', 54 | date: 32, 55 | remark: 'New York No. 1 Lake Park', 56 | }, 57 | { 58 | key: '2', 59 | name: 'Joe Black', 60 | date: 42, 61 | remark: 'London No. 1 Lake Park', 62 | }, 63 | { 64 | key: '3', 65 | name: 'Jim Green', 66 | date: 32, 67 | remark: 'Sidney No. 1 Lake Park', 68 | }, 69 | { 70 | key: '4', 71 | name: 'Jim Red', 72 | date: 32, 73 | remark: 'London No. 2 Lake Park', 74 | }, 75 | ], 76 | }, 77 | tomorrowList: { 78 | total: 4, 79 | list: [ 80 | { 81 | key: '1', 82 | name: 'John Brown', 83 | date: 32, 84 | remark: 'New York No. 1 Lake Park', 85 | }, 86 | { 87 | key: '2', 88 | name: 'Joe Black', 89 | date: 42, 90 | remark: 'London No. 1 Lake Park', 91 | }, 92 | { 93 | key: '3', 94 | name: 'Jim Green', 95 | date: 32, 96 | remark: 'Sidney No. 1 Lake Park', 97 | }, 98 | { 99 | key: '4', 100 | name: 'John Brown', 101 | date: 32, 102 | remark: 'New York No. 1 Lake Park', 103 | }, 104 | { 105 | key: '5', 106 | name: 'Joe Black', 107 | date: 42, 108 | remark: 'London No. 1 Lake Park', 109 | }, 110 | { 111 | key: '6', 112 | name: 'Jim Green', 113 | date: 32, 114 | remark: 'Sidney No. 1 Lake Park', 115 | }, 116 | { 117 | key: '7', 118 | name: 'Joe Black', 119 | date: 42, 120 | remark: 'London No. 1 Lake Park', 121 | }, 122 | { 123 | key: '8', 124 | name: 'Jim Green', 125 | date: 32, 126 | remark: 'Sidney No. 1 Lake Park', 127 | }, 128 | ], 129 | }, 130 | }, 131 | }, 132 | }; 133 | 134 | export default delay(proxy, 200); 135 | --------------------------------------------------------------------------------