├── src ├── pages │ ├── store │ │ ├── store │ │ │ ├── Info.style.less │ │ │ ├── c │ │ │ │ ├── StoreInfo.style.less │ │ │ │ └── StoreInfo.tsx │ │ │ └── Info.tsx │ │ └── notification │ │ │ ├── List.style.less │ │ │ └── List.tsx │ ├── goods │ │ ├── category │ │ │ ├── list.less │ │ │ └── List.tsx │ │ └── goods │ │ │ ├── c │ │ │ ├── Item.style.less │ │ │ ├── DragSelectContainer.style.less │ │ │ ├── GoodsForm.less │ │ │ ├── Item.tsx │ │ │ ├── DragSelectContainer.tsx │ │ │ └── GoodsForm.tsx │ │ │ ├── List.style.less │ │ │ └── List.tsx │ ├── user │ │ ├── login.less │ │ └── Login.tsx │ ├── _app.js │ ├── api.tsx │ ├── index.tsx │ └── _document.js ├── asserts │ ├── global.less │ └── iconfont.less ├── components │ ├── footer │ │ ├── footer.less │ │ └── Footer.tsx │ ├── header │ │ ├── header.less │ │ └── Header.tsx │ ├── toolbar │ │ ├── ToolBar.style.less │ │ └── ToolBar.tsx │ ├── modal │ │ └── EditModal.tsx │ └── Menu.tsx ├── api │ ├── axioxBuilder.ts │ ├── business │ │ ├── indexApi.ts │ │ ├── restfulAPI.ts │ │ ├── userAPI.ts │ │ └── goodsAPI.ts │ ├── constants.ts │ ├── interfaces │ │ └── iHttp.ts │ ├── iHttpImp.ts │ └── api.ts ├── class │ ├── Category.ts │ ├── api │ │ ├── user │ │ │ └── index.ts │ │ └── index.ts │ ├── Goods.ts │ ├── Query.ts │ ├── Store.ts │ └── goodsTypes.ts ├── model │ ├── index.ts │ ├── common.ts │ ├── router.ts │ ├── user.ts │ ├── api.ts │ └── goods.ts ├── constants │ └── className.ts ├── utils │ ├── localStorage.ts │ ├── with-redux-store.js │ └── store.js └── layouts │ └── Dashboard.tsx ├── screenShots ├── screen_shot_1.png ├── screen_shot_2.png └── screen_shot_3.png ├── .prettierrc ├── tslint.json ├── README.md ├── next.config.js ├── tsconfig.json ├── .babelrc ├── doc └── changlog.md ├── LICENSE ├── package.json ├── server └── server.js └── .gitignore /src/pages/store/store/Info.style.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/asserts/global.less: -------------------------------------------------------------------------------- 1 | @icon-logo: 60px; -------------------------------------------------------------------------------- /src/pages/store/notification/List.style.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/goods/category/list.less: -------------------------------------------------------------------------------- 1 | .table { 2 | margin-top: 16px; 3 | } -------------------------------------------------------------------------------- /src/components/footer/footer.less: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | padding: 0 20px; 4 | } -------------------------------------------------------------------------------- /src/api/axioxBuilder.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const axiosInstance = axios.create() 4 | 5 | export default axiosInstance 6 | -------------------------------------------------------------------------------- /screenShots/screen_shot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2oiswater/nextjs-dashboard-antdesign-typescriopt/HEAD/screenShots/screen_shot_1.png -------------------------------------------------------------------------------- /screenShots/screen_shot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2oiswater/nextjs-dashboard-antdesign-typescriopt/HEAD/screenShots/screen_shot_2.png -------------------------------------------------------------------------------- /screenShots/screen_shot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2oiswater/nextjs-dashboard-antdesign-typescriopt/HEAD/screenShots/screen_shot_3.png -------------------------------------------------------------------------------- /src/class/Category.ts: -------------------------------------------------------------------------------- 1 | export default interface Category { 2 | objectId?: string 3 | name: string 4 | sort: number 5 | pid?: string 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/goods/goods/c/Item.style.less: -------------------------------------------------------------------------------- 1 | .selected-bg{ 2 | background-color: rgb(116, 174, 230); 3 | } 4 | 5 | .goods-img { 6 | height: 200px; 7 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "eslintIntegration": true, 3 | "stylelintIntegration": true, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "semi": false 7 | } -------------------------------------------------------------------------------- /src/api/business/indexApi.ts: -------------------------------------------------------------------------------- 1 | import HttpClient from '../iHttpImp' 2 | 3 | export function getIndexData() { 4 | return HttpClient.get('/mock') 5 | } 6 | -------------------------------------------------------------------------------- /src/components/header/header.less: -------------------------------------------------------------------------------- 1 | .header { 2 | background-color: white; 3 | display: flex; 4 | padding: 0 20px; 5 | justify-content: flex-end; 6 | } -------------------------------------------------------------------------------- /src/class/api/user/index.ts: -------------------------------------------------------------------------------- 1 | import { AVUser } from 'src/class/Store' 2 | 3 | export namespace Rep { 4 | export interface LoginRep extends AVUser{ 5 | sessionToken: string 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/goods/goods/List.style.less: -------------------------------------------------------------------------------- 1 | .content-area { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .page-area { 7 | margin-top: 8px; 8 | align-self: flex-end; 9 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["tslint-plugin-prettier"], 3 | "extends": ["tslint-config-standard", "tslint-config-prettier"], 4 | "rules": { 5 | "prettier": true, 6 | "tabWidth": 2 7 | } 8 | } -------------------------------------------------------------------------------- /src/model/index.ts: -------------------------------------------------------------------------------- 1 | import common from './common' 2 | import router from './router' 3 | import user from './user' 4 | import goods from './goods' 5 | import api from './api' 6 | 7 | const model = [common, router, user, goods, api] 8 | 9 | export default model 10 | -------------------------------------------------------------------------------- /src/components/toolbar/ToolBar.style.less: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: white; 3 | padding: 16px; 4 | display: flex; 5 | align-items: center; 6 | } 7 | 8 | .button { 9 | margin-right: 16px; 10 | } 11 | 12 | .space { 13 | flex: 1; 14 | } 15 | 16 | .search { 17 | width: 250px; 18 | } -------------------------------------------------------------------------------- /src/pages/user/login.less: -------------------------------------------------------------------------------- 1 | .login-container { 2 | display: flex; 3 | height: 100vh; 4 | width: 100vw; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | .login-form { 10 | max-width: 300px; 11 | } 12 | .login-form-forgot { 13 | float: right; 14 | } 15 | .login-form-button { 16 | width: 100%; 17 | } -------------------------------------------------------------------------------- /src/api/constants.ts: -------------------------------------------------------------------------------- 1 | export const LEAN_API_ID = '' 2 | 3 | export const LEAN_API_KEY = '' 4 | 5 | export function getUploadUrl(name: string) { 6 | return `https://${LEAN_API_ID.substring( 7 | 0, 8 | 8 9 | )}.api.lncld.net/1.1/files/${name}` 10 | } 11 | 12 | export const BASE_API_URL = 'https://api.leancloud.cn/1.1' 13 | -------------------------------------------------------------------------------- /src/pages/goods/goods/c/DragSelectContainer.style.less: -------------------------------------------------------------------------------- 1 | .drag-select-container { 2 | background-color: white; 3 | padding: 16px; 4 | display: flex; 5 | flex-flow: wrap; 6 | margin-top: 16px; 7 | } 8 | 9 | .drag-area { 10 | position: absolute; 11 | background: rgba(187, 255, 255, 0.3); 12 | border: 1px solid cadetblue; 13 | } -------------------------------------------------------------------------------- /src/constants/className.ts: -------------------------------------------------------------------------------- 1 | export enum API_CLASS_NAME { 2 | STORE = 'Store', 3 | NOTIFICATION = 'Notification', 4 | CATEGORY = 'Category', 5 | GOODS = 'Goods', 6 | } 7 | 8 | export enum DB_GOODS { 9 | CATEGORY = 'category' 10 | } 11 | 12 | export enum DB_STORE { 13 | LOGO = 'logo' 14 | } 15 | 16 | export enum DB_NAME { 17 | User = '_User' 18 | } -------------------------------------------------------------------------------- /src/pages/goods/goods/c/GoodsForm.less: -------------------------------------------------------------------------------- 1 | .dynamic-delete-button { 2 | cursor: pointer; 3 | position: relative; 4 | top: 4px; 5 | font-size: 24px; 6 | color: #999; 7 | transition: all .3s; 8 | } 9 | 10 | .dynamic-delete-button:hover { 11 | color: #777; 12 | } 13 | .dynamic-delete-button[disabled] { 14 | cursor: not-allowed; 15 | opacity: 0.5; 16 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Next.js starter with TypeScript and AntDesign (base on Next.js 7) 2 | 3 | # It's a simple Dashboard. Less is More. 4 | 5 | ## How to using 6 | 7 | 1. fork or clone or download 8 | 2. npm i 9 | 3. npm run dev 10 | 11 | **replace LeanCloud API_KEY & APP_ID with yourself** 12 | 13 | ## What it looks like 14 | ![screen_shot_1](screenShots/screen_shot_2.png) 15 | ![screen_shot_1](screenShots/screen_shot_3.png) -------------------------------------------------------------------------------- /src/api/interfaces/iHttp.ts: -------------------------------------------------------------------------------- 1 | export default interface IHttp { 2 | request(url, params, config): Promise 3 | } 4 | 5 | export enum HTTP_METHODS { 6 | GET = 'GET', 7 | DELETE = 'DELETE', 8 | POST = 'POST', 9 | PUT = 'PUT' 10 | } 11 | 12 | export interface RequestConfig { 13 | // header 14 | headers?: any 15 | // 是否允许跨域 16 | allowCros?: boolean 17 | jsonp?: string 18 | method?: HTTP_METHODS 19 | } 20 | -------------------------------------------------------------------------------- /src/model/common.ts: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | currentStoreID: undefined as string 3 | } 4 | 5 | export type CommonState = typeof initialState 6 | 7 | const model = { 8 | namespace: 'common', 9 | state: initialState, 10 | reducers: { 11 | updateCurrentStoreID(state, { payload }): CommonState { 12 | return { ...state, currentStoreID: payload } 13 | } 14 | }, 15 | effects: {} 16 | } 17 | 18 | export default model 19 | -------------------------------------------------------------------------------- /src/components/footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Icon } from 'antd' 3 | import './footer.less' 4 | 5 | export default class Header extends React.Component { 6 | 7 | render() { 8 | return ( 9 |
10 | 11 | Github 12 |
13 | ) 14 | } 15 | } -------------------------------------------------------------------------------- /src/class/Goods.ts: -------------------------------------------------------------------------------- 1 | import { Spec, AVFile } from './goodsTypes' 2 | import Category from './Category'; 3 | export default interface Goods { 4 | objectId?: string 5 | // 描述 6 | desc: string 7 | // 分类 8 | category: Category 9 | // 是否上架 0 不上架 1 上架 10 | display: number 11 | // 商品图片 12 | images: Array 13 | // 价格 14 | price?: number 15 | // 名称 16 | title: string 17 | // 规格 18 | spec?: Array 19 | // 主图 20 | mainImage?: AVFile 21 | } 22 | -------------------------------------------------------------------------------- /src/class/Query.ts: -------------------------------------------------------------------------------- 1 | // export default interface Category { 2 | // class?: string 3 | // where?: string 4 | // limit?: number 5 | // skip?: number 6 | // order?: string 7 | // include?: string 8 | // keys?: string 9 | // count?: number 10 | // } 11 | 12 | export default interface Query { 13 | class?: string 14 | where?: any 15 | limit?: number 16 | skip?: number 17 | order?: string 18 | include?: string 19 | keys?: string 20 | count?: number 21 | } -------------------------------------------------------------------------------- /src/class/Store.ts: -------------------------------------------------------------------------------- 1 | import { AVFile } from './goodsTypes' 2 | export interface Store { 3 | objectId?: string 4 | name: string 5 | logo: AVFile 6 | owner: AVUser 7 | } 8 | 9 | export interface Notifaction { 10 | objectId?: string 11 | title: string 12 | cover: AVFile 13 | content: string 14 | } 15 | 16 | export interface AVUser { 17 | updatedAt: Date 18 | objectId: string 19 | username: string 20 | createdAt: Date 21 | emailVerified: boolean 22 | mobilePhoneVerified: boolean 23 | } 24 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const withLess = require('@zeit/next-less') 3 | const withTypescript = require("@zeit/next-typescript") 4 | 5 | // fix: prevents error when .less files are required by node 6 | if (typeof require !== 'undefined') { 7 | require.extensions['.less'] = (file) => {} 8 | } 9 | 10 | module.exports = withTypescript(withLess({ 11 | lessLoaderOptions: { 12 | javascriptEnabled: true, 13 | modifyVars: { 14 | 'primary-color': '#1DA57A', 15 | 'link-color': '#1DA57A', 16 | 'border-radius-base': '2px', 17 | }, 18 | } 19 | })) -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import App, {Container} from 'next/app' 2 | import React from 'react' 3 | import '../asserts/iconfont.less' 4 | 5 | export default class MyApp extends App { 6 | static async getInitialProps ({ Component, router, ctx }) { 7 | let pageProps = {} 8 | 9 | if (Component.getInitialProps) { 10 | pageProps = await Component.getInitialProps(ctx) 11 | } 12 | 13 | return {pageProps} 14 | } 15 | 16 | render () { 17 | const {Component, pageProps} = this.props 18 | return 19 | 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "jsx": "preserve", 7 | "allowJs": true, 8 | "moduleResolution": "node", 9 | "allowSyntheticDefaultImports": true, 10 | "experimentalDecorators": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "removeComments": false, 14 | "preserveConstEnums": true, 15 | "sourceMap": true, 16 | "skipLibCheck": true, 17 | "baseUrl": ".", 18 | "lib": ["dom", "es2016"], 19 | "outDir": "./src/.next" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/store/store/c/StoreInfo.style.less: -------------------------------------------------------------------------------- 1 | .store-container { 2 | display: flex; 3 | flex-direction: column; 4 | background: white; 5 | padding: 16px; 6 | margin-top: 16px; 7 | 8 | .div-info { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | 13 | .store-title { 14 | font-size: 16px; 15 | } 16 | 17 | .btn-default-store { 18 | margin-left: 32px; 19 | } 20 | } 21 | 22 | .store-save { 23 | margin-top: 16px; 24 | width: 120px; 25 | } 26 | 27 | .store-logo { 28 | width: 120px; 29 | height: 120px; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["next/babel"], 4 | ["@zeit/next-typescript/babel"] 5 | ], 6 | "plugins": [ 7 | ["module-resolver", { 8 | "root": ["./src"], 9 | "alias": { 10 | "@layouts": "./src/layouts/", 11 | "@components": "./src/components/", 12 | "@styles": "./src/styles/", 13 | "src": "./src/", 14 | "dva-utils": "./src/utils/", 15 | "dva": "dva-no-router" 16 | } 17 | }], 18 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 19 | [ 20 | "import", 21 | { 22 | "libraryName": "antd", 23 | "style": true 24 | } 25 | ] 26 | ] 27 | } -------------------------------------------------------------------------------- /src/pages/api.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import WithDva from 'dva-utils/store' 3 | import * as IndexAPI from '../api/business/indexApi' 4 | 5 | @WithDva(({ common }) => { 6 | return { common } 7 | }) 8 | export default class ApiPage extends React.Component { 9 | props: { 10 | stars: string 11 | common: any 12 | } 13 | 14 | static async getInitialProps({}) { 15 | const rep = await IndexAPI.getIndexData() 16 | return { stars: JSON.stringify(rep.data) } 17 | } 18 | 19 | render() { 20 | const { msg } = this.props.common 21 | return ( 22 |
23 |

{this.props.stars}

24 |

{msg}

25 |
26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/class/goodsTypes.ts: -------------------------------------------------------------------------------- 1 | import Category from './Category' 2 | import Goods from './Goods' 3 | 4 | export const SPEC_TYPE_BASE = 0 5 | export const SPEC_TYPE_MODIFY = 1 6 | 7 | export interface Spec { 8 | objectId?: string 9 | // 名称 10 | name?: string 11 | // 价格 12 | price?: number 13 | // 价格类型 0 基准价 1 修饰价 14 | type?: number 15 | // 子规格 16 | subSpecs?: Array 17 | } 18 | 19 | export type CategoryListRep = { 20 | result: Array 21 | count: number 22 | } 23 | 24 | export type GoodsListRep = { 25 | result: Array 26 | count: number 27 | } 28 | 29 | // leancloud 30 | export type AVFile = { 31 | objectId?: string 32 | createdAt?: string 33 | name?: string 34 | url?: string 35 | bucket?: string 36 | } -------------------------------------------------------------------------------- /src/utils/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { AVUser } from 'src/class/Store' 2 | 3 | export const keys = { 4 | KEY_TOKEN: 'token', 5 | KEY_CURRENT_USER: 'current_user', 6 | KEY_CURRENT_SOTRE_ID: 'current_store_id' 7 | } 8 | 9 | export function set(key: string, data: string) { 10 | localStorage.setItem(key, data) 11 | } 12 | 13 | export function get(key: string): string { 14 | return localStorage.getItem(key) 15 | } 16 | 17 | export function getUser(): AVUser | undefined { 18 | const userString = localStorage.getItem(keys.KEY_CURRENT_USER) 19 | if (userString) { 20 | return JSON.parse(userString) 21 | } 22 | return undefined 23 | } 24 | 25 | export function setUser(user: AVUser) { 26 | localStorage.setItem(keys.KEY_CURRENT_USER, JSON.stringify(user)) 27 | } 28 | -------------------------------------------------------------------------------- /src/api/iHttpImp.ts: -------------------------------------------------------------------------------- 1 | import IHttp, { RequestConfig, HTTP_METHODS } from './interfaces/iHttp' 2 | import axios from './axioxBuilder' 3 | 4 | class HttpClient implements IHttp { 5 | request( 6 | url: string, 7 | params?: object, 8 | config = { method: HTTP_METHODS.GET } as RequestConfig 9 | ): Promise { 10 | return this._request(url, params, config) 11 | } 12 | 13 | private _request( 14 | url: string, 15 | params: object, 16 | config?: RequestConfig 17 | ): Promise { 18 | return axios.request({ 19 | url, 20 | method: config.method, 21 | params, 22 | data: params, 23 | headers: config.headers 24 | }) 25 | } 26 | } 27 | 28 | const httpClient = new HttpClient() 29 | 30 | export default httpClient 31 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Dashboard from '../layouts/Dashboard' 3 | // @ts-ignore 4 | import WithDva from 'dva-utils/store' 5 | 6 | import { UserState } from 'src/model/user' 7 | 8 | type IndexPageProps = { dispatch: any; user: UserState } 9 | 10 | @WithDva(({ user }: { user: UserState }) => { 11 | return { user } 12 | }) 13 | export default class IndexPage extends React.Component { 14 | componentDidMount() { 15 | const { dispatch } = this.props 16 | dispatch({ 17 | type: 'user/me' 18 | }) 19 | } 20 | 21 | render() { 22 | const { user } = this.props.user 23 | 24 | return ( 25 | 26 |

{user ? user.username : ''}你好!

27 |
28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/_document.js: -------------------------------------------------------------------------------- 1 | // _document is only rendered on the server side and not on the client side 2 | // Event handlers like onClick can't be added to this file 3 | // ./pages/_document.js 4 | import Document, { Head, Main, NextScript } from 'next/document' 5 | 6 | export default class MyDocument extends Document { 7 | static async getInitialProps(ctx) { 8 | const initialProps = await Document.getInitialProps(ctx) 9 | return { ...initialProps } 10 | } 11 | 12 | render() { 13 | return ( 14 | 15 | 16 | XiaoYu Dashboard 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /doc/changlog.md: -------------------------------------------------------------------------------- 1 | ## change logs 2 | 11. 12-16 3 | - coding make me happy 4 | 10. 11-13 5 | - still working 6 | 9. 9-28 7 | - finish category function 8 | - prepare design a HOC for functions like category does 9 | 8. 9-27 10 | - use [leancloud](https://leancloud.cn) 11 | - add some api support 12 | - add category manager 13 | 9. 9-20 14 | - support next.js 7 15 | 10. 9-19 16 | - add some page 17 | 11. 9-18 18 | - add menu model 19 | 12. 9-16 20 | - add redux and it's friends 21 | - add dva 22 | - use dva 23 | - writing templates 24 | 13. 9-12 25 | - add antd 26 | 14. 9-11 27 | - add koa 28 | - add default _app,_document, _error 29 | - add some test layouts 30 | 15. 9-7 31 | - init nextjs repo 32 | - add ts,scss config 33 | - add axios and a simple wrapper 34 | -------------------------------------------------------------------------------- /src/class/api/index.ts: -------------------------------------------------------------------------------- 1 | export interface CreateStoreParams { 2 | name: string 3 | owner: Pointer 4 | logo: FilePointer 5 | } 6 | 7 | export interface Pointer { 8 | __type: 'Pointer' | 'File' 9 | className: string 10 | objectId: string 11 | } 12 | 13 | export interface FilePointer { 14 | __type: 'File' 15 | objectId: string 16 | } 17 | 18 | export function buildPointer(params: { 19 | className: string 20 | objectId: string 21 | }): Pointer { 22 | const pointer: Pointer = { 23 | className: params.className, 24 | objectId: params.objectId, 25 | __type: 'Pointer' 26 | } 27 | return pointer 28 | } 29 | 30 | export function buildFile(params: { objectId: string }): FilePointer { 31 | const pointer: FilePointer = { 32 | objectId: params.objectId, 33 | __type: 'File' 34 | } 35 | return pointer 36 | } 37 | -------------------------------------------------------------------------------- /src/api/business/restfulAPI.ts: -------------------------------------------------------------------------------- 1 | import { apiGet, apiPost, apiDelete, apiPut } from '../api' 2 | import Query from '../../class/Query' 3 | 4 | export function rstPost(data: any, className: string): Promise { 5 | return apiPost({ url: `/classes/${className}`, params: data }) 6 | } 7 | 8 | export function rstPut(data: any, className: string): Promise { 9 | return apiPut({ 10 | url: `/classes/${className}/${data.objectId}`, 11 | params: data 12 | }) 13 | } 14 | 15 | export function rstDelete(data: any, className: string): Promise { 16 | return apiDelete({ 17 | url: `/classes/${className}/${data.objectId}` 18 | }) 19 | } 20 | 21 | export function rstGet(data: Query, className: string): Promise { 22 | data.count = 1 23 | return apiGet({ 24 | url: `/classes/${className}`, 25 | params: data 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/modal/EditModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Form, Modal } from 'antd' 3 | 4 | interface EditModalProps { 5 | title: string 6 | visible: boolean 7 | onOk: any 8 | confirmLoading: boolean 9 | onCancel: any 10 | data: any // pass to form 11 | } 12 | 13 | export function create(SourceForm) { 14 | let WrappedForm = Form.create()(SourceForm) 15 | 16 | return class extends React.Component { 17 | props: EditModalProps 18 | 19 | render() { 20 | return ( 21 | 28 | 29 | 30 | ) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/api/business/userAPI.ts: -------------------------------------------------------------------------------- 1 | import Router from 'next/router' 2 | import { apiGet } from '../api' 3 | import { keys, set, setUser, getUser } from '../../utils/localStorage' 4 | import * as APIUserTypes from 'src/class/api/user' 5 | import { AVUser } from 'src/class/Store' 6 | 7 | export function login(params): Promise { 8 | return apiGet({ url: '/login', params }).then( 9 | (data: APIUserTypes.Rep.LoginRep) => { 10 | set(keys.KEY_TOKEN, data.sessionToken) 11 | setUser(data) 12 | Router.replace('/') 13 | return data 14 | } 15 | ) 16 | } 17 | 18 | export function me(): Promise { 19 | let currentUser = getUser() 20 | if (!currentUser) { 21 | Router.replace('/login') 22 | return 23 | } 24 | return apiGet({ url: '/users/'.concat(currentUser.objectId) }).then( 25 | (data: AVUser) => data 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/asserts/iconfont.less: -------------------------------------------------------------------------------- 1 | @import "./global.less"; 2 | 3 | @font-face { 4 | font-family: 'iconfont'; /* project id 843794 */ 5 | src: url('//at.alicdn.com/t/font_843794_ei7t908dh2g.eot'); 6 | src: url('//at.alicdn.com/t/font_843794_ei7t908dh2g.eot?#iefix') format('embedded-opentype'), 7 | url('//at.alicdn.com/t/font_843794_ei7t908dh2g.woff') format('woff'), 8 | url('//at.alicdn.com/t/font_843794_ei7t908dh2g.ttf') format('truetype'), 9 | url('//at.alicdn.com/t/font_843794_ei7t908dh2g.svg#iconfont') format('svg'); 10 | } 11 | 12 | .iconfont { 13 | font-family:"iconfont" !important; 14 | font-size: 20px; 15 | font-style:normal; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | 20 | .icon-size-logo { 21 | width: @icon-logo; 22 | height: @icon-logo; 23 | font-size: @icon-logo; 24 | } 25 | 26 | .icon-logo::before { content: "\e687";} -------------------------------------------------------------------------------- /src/model/router.ts: -------------------------------------------------------------------------------- 1 | import Router from 'next/router' 2 | 3 | const model = { 4 | namespace: 'router', 5 | state: { 6 | path: '/', 7 | key: ['1'] 8 | }, 9 | reducers: { 10 | setPath(state, { path }) { 11 | Router.replace(path) 12 | return { ...state, path } 13 | }, 14 | updateKey(state, { key }) { 15 | let newKey = [] 16 | newKey.push(key) 17 | return { ...state, key: newKey } 18 | } 19 | }, 20 | effects: { 21 | *updatePath({ path }, { select, put }) { 22 | // let needToken = false 23 | // for (let p in urlsWithoutAuth) { 24 | // if (urlsWithoutAuth[p] !== path) { 25 | // needToken = true 26 | // break 27 | // } 28 | // } 29 | // let token = yield select(state => state.user.token) 30 | 31 | // if (needToken && token) { 32 | yield put({ type: 'setPath', path }) 33 | // } else { 34 | // yield put({ type: 'setPath', path: '/' }) 35 | // } 36 | } 37 | } 38 | } 39 | 40 | export default model 41 | -------------------------------------------------------------------------------- /src/model/user.ts: -------------------------------------------------------------------------------- 1 | import * as userAPI from '../api/business/userAPI' 2 | import { AVUser } from 'src/class/Store' 3 | 4 | const inititalState = { 5 | token: undefined as string, 6 | user: undefined as AVUser 7 | } 8 | 9 | export type UserState = Readonly 10 | 11 | const model = { 12 | namespace: 'user', 13 | state: inititalState, 14 | reducers: { 15 | updateUser(state, { payload }) { 16 | return { ...state, user: payload } 17 | } 18 | }, 19 | effects: { 20 | *login({ payload }, { put, call }) { 21 | try { 22 | let result = yield call(userAPI.login, payload) 23 | } catch (error) { 24 | console.error(error) 25 | } 26 | }, 27 | *me({}, { put, call }) { 28 | try { 29 | let user: AVUser = yield call(userAPI.me) 30 | yield put({ 31 | type: 'updateUser', 32 | payload: user 33 | }) 34 | } catch (error) { 35 | console.error(error) 36 | } 37 | } 38 | } 39 | } 40 | 41 | export default model 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 龚江鹏 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 | -------------------------------------------------------------------------------- /src/layouts/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Layout, Divider } from 'antd' 3 | import Menu from '../components/Menu' 4 | import NovaFooter from '../components//footer/Footer' 5 | import NovaHeader from '../components/header/Header' 6 | 7 | const theme = 'light' 8 | 9 | const { Header, Footer, Sider, Content } = Layout 10 | 11 | export default class ApiPage extends React.Component { 12 | render () { 13 | return ( 14 |
15 | 16 | 19 |
20 | 21 |

I am a Logo

22 |
23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 | {this.props.children} 32 | 33 |
34 | 35 |
36 |
37 | 38 |
39 | ) 40 | } 41 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-ant-design-less", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "next src", 6 | "build": "next build", 7 | "start": "next start", 8 | "server": "node ./server/server.js" 9 | }, 10 | "dependencies": { 11 | "antd": "^3.11.6", 12 | "axios": "^0.18.1", 13 | "braft-editor": "^2.2.5", 14 | "dva-no-router": "^1.2.0", 15 | "koa": "^2.5.3", 16 | "koa-bodyparser": "^4.2.1", 17 | "koa-router": "^7.4.0", 18 | "koa2-cors": "^2.0.6", 19 | "less": "^3.8.1", 20 | "lodash": "^4.17.11", 21 | "md5": "^2.2.1", 22 | "next": "^7.0.2", 23 | "react": "^16.5.2", 24 | "react-dom": "^16.5.2", 25 | "react-ds": "^1.10.0", 26 | "react-selectable-fast": "^2.1.1" 27 | }, 28 | "license": "ISC", 29 | "devDependencies": { 30 | "@babel/plugin-proposal-decorators": "^7.1.0", 31 | "@babel/preset-stage-0": "^7.0.0", 32 | "@types/react": "^16.4.14", 33 | "@zeit/next-less": "^1.0.0", 34 | "@zeit/next-typescript": "^1.1.1", 35 | "babel-plugin-import": "^1.7.0", 36 | "babel-plugin-module-resolver": "^3.1.1", 37 | "prettier": "^1.14.2", 38 | "tslint": "^5.11.0", 39 | "tslint-config-prettier": "^1.15.0", 40 | "tslint-config-standard": "^8.0.1", 41 | "tslint-plugin-prettier": "^2.0.0", 42 | "typescript": "^3.0.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/pages/store/store/c/StoreInfo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Button } from 'antd' 4 | 5 | import './StoreInfo.style.less' 6 | import { Store } from 'src/class/Store' 7 | 8 | export interface StoreInfoProps { 9 | data: Store 10 | isCurrentSotre: boolean 11 | onDefaultButtonClicked(store: Store): void 12 | onModifyButtonClicked(store: Store): void 13 | } 14 | 15 | export default class StoreInfo extends React.Component { 16 | state = { 17 | refresh: false 18 | } 19 | 20 | public render() { 21 | const { 22 | data, 23 | isCurrentSotre, 24 | onDefaultButtonClicked, 25 | onModifyButtonClicked 26 | } = this.props 27 | return ( 28 |
29 |
30 |
店铺名称: {data.name}
31 | 40 |
41 | 42 | 43 | 44 | 53 |
54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/api/business/goodsAPI.ts: -------------------------------------------------------------------------------- 1 | import { apiGet, apiPost, apiDelete, apiPut } from '../api' 2 | // import { keys, set } from '../../utils/localStorage' 3 | import Category from '../../class/Category' 4 | import Goods from '../../class/Goods' 5 | import Query from '../../class/Query' 6 | import { CategoryListRep, GoodsListRep } from '../../class/goodsTypes' 7 | 8 | const CLASS_CATEGORY_NAME = 'Category' 9 | const CLASS_GOODS_NAME = 'Goods' 10 | 11 | export function goodsCreate(data: Goods): Promise { 12 | return apiPost({ url: `/classes/${CLASS_GOODS_NAME}`, params: data }) 13 | } 14 | 15 | export function categoryCreate(data: Category): Promise { 16 | return apiPost({ url: `/classes/${CLASS_CATEGORY_NAME}`, params: data }) 17 | } 18 | 19 | export function categoryUpdate(data: Category): Promise { 20 | return apiPut({ 21 | url: `/classes/${CLASS_CATEGORY_NAME}/${data.objectId}`, 22 | params: data 23 | }) 24 | } 25 | 26 | export function categoryDelete(data: Category): Promise { 27 | return apiDelete({ 28 | url: `/classes/${CLASS_CATEGORY_NAME}/${data.objectId}`, 29 | params: data 30 | }) 31 | } 32 | 33 | export function category(data: Query): Promise { 34 | data.class = CLASS_CATEGORY_NAME 35 | data.count = 1 36 | return apiGet({ 37 | url: `/classes/${CLASS_CATEGORY_NAME}`, 38 | params: data 39 | }) 40 | } 41 | 42 | export function goods(data: Query): Promise { 43 | data.class = CLASS_GOODS_NAME 44 | data.count = 1 45 | return apiGet({ 46 | url: `/classes/${CLASS_GOODS_NAME}`, 47 | params: data 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /src/components/toolbar/ToolBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Button, Input } from 'antd' 3 | const Search = Input.Search 4 | 5 | import './ToolBar.style.less' 6 | 7 | interface ToolBarButton { 8 | text: String 9 | id: number 10 | } 11 | 12 | interface ToolBarProps { 13 | showButtons: Array 14 | onButtonClicked: Function 15 | search: any 16 | } 17 | 18 | export default class ToolBar extends Component { 19 | state = { 20 | currentSearchKey: '' 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | {this.props.showButtons.map((item, index) => { 27 | return ( 28 | 37 | ) 38 | })} 39 |
40 | { 47 | this.setState({ 48 | currentSearchKey: e.currentTarget.value 49 | }) 50 | }} 51 | onSearch={value => { 52 | this.props.search(value) 53 | this.setState({ 54 | currentSearchKey: '' 55 | }) 56 | }} 57 | /> 58 |
59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/with-redux-store.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {initializeStore} from '../store/store' 3 | 4 | const isServer = typeof window === 'undefined' 5 | const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__' 6 | 7 | function getOrCreateStore(initialState) { 8 | // Always make a new store if server, otherwise state is shared between requests 9 | if (isServer) { 10 | return initializeStore(initialState) 11 | } 12 | 13 | // Create store if unavailable on the client and set it on the window object 14 | if (!window[__NEXT_REDUX_STORE__]) { 15 | window[__NEXT_REDUX_STORE__] = initializeStore(initialState) 16 | } 17 | return window[__NEXT_REDUX_STORE__] 18 | } 19 | 20 | export default (App) => { 21 | return class AppWithRedux extends React.Component { 22 | static async getInitialProps (appContext) { 23 | // Get or Create the store with `undefined` as initialState 24 | // This allows you to set a custom default initialState 25 | const reduxStore = getOrCreateStore() 26 | 27 | // Provide the store to getInitialProps of pages 28 | appContext.ctx.reduxStore = reduxStore 29 | 30 | let appProps = {} 31 | if (typeof App.getInitialProps === 'function') { 32 | appProps = await App.getInitialProps.call(App, appContext) 33 | } 34 | 35 | return { 36 | ...appProps, 37 | initialReduxState: reduxStore.getState() 38 | } 39 | } 40 | 41 | constructor(props) { 42 | super(props) 43 | this.reduxStore = getOrCreateStore(props.initialReduxState) 44 | } 45 | 46 | render() { 47 | return 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/components/header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Router from 'next/router' 3 | import { Icon, Tooltip, Avatar, Menu, Dropdown } from 'antd' 4 | import './header.less' 5 | 6 | import WithDva from 'dva-utils/store' 7 | 8 | const menu = ( 9 | 10 | 11 |

12 | 13 | 18 | 1st menu item 19 | 20 |

21 |
22 | 23 | 28 | 2nd menu item 29 | 30 | 31 | 32 | 33 | 3rd menu item 34 | 35 | 36 |
37 | ) 38 | 39 | @WithDva(({ router }) => { 40 | return { router } 41 | }) 42 | export default class Header extends React.Component { 43 | render() { 44 | return ( 45 |
46 | 47 | 53 | 54 | 55 | 56 | 57 |
58 | 59 | 王闪火 60 |
61 |
62 |
63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const next = require('next') 3 | const Router = require('koa-router') 4 | // const cors = require('koa2-cors') 5 | const bodyParser = require('koa-bodyparser') 6 | 7 | const port = parseInt(process.env.PORT, 10) || 3001 8 | const dev = process.env.NODE_ENV !== 'production' 9 | const app = next({ 10 | dev, 11 | dir: './src' 12 | }) 13 | const handle = app.getRequestHandler() 14 | 15 | app.prepare() 16 | .then(() => { 17 | const server = new Koa() 18 | const router = new Router() 19 | const apiRouter = new Router({ 20 | prefix: '/api' 21 | }) 22 | 23 | router.get('/login', async ctx => { 24 | await app.render(ctx.req, ctx.res, '/user/Login', ctx.query) 25 | ctx.respond = false 26 | }) 27 | 28 | router.get('*', async ctx => { 29 | await handle(ctx.req, ctx.res) 30 | ctx.respond = false 31 | }) 32 | 33 | server.use(async (ctx, next) => { 34 | ctx.res.statusCode = 200 35 | await next() 36 | }) 37 | 38 | // trust proxy 39 | server.proxy = true 40 | 41 | // body parser 42 | server.use(bodyParser()) 43 | 44 | // Require authentication for now 45 | server.use(function (ctx, next) { 46 | // if (ctx.isAuthenticated()) { 47 | return next() 48 | // } else { 49 | // // static resource not redirect 50 | // // api not redirect 51 | // let needAuth = (ctx.url.indexOf('/_next') === -1 && ctx.url.indexOf('/api') === -1) 52 | 53 | // if (needAuth) { 54 | // for (let p in noAuthUrls) { 55 | // if (noAuthUrls[p] === ctx.url) { 56 | // needAuth = false 57 | // break 58 | // } 59 | // } 60 | // } 61 | 62 | // if (needAuth) { 63 | // ctx.redirect('/login') 64 | // } else { 65 | // return next() 66 | // } 67 | // } 68 | }) 69 | 70 | server.use(router.routes()) 71 | server.use(apiRouter.routes()) 72 | server.listen(port, () => { 73 | console.log(`> Ready on http://localhost:${port}`) 74 | }) 75 | }) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### Node ### 33 | # Logs 34 | logs 35 | *.log 36 | npm-debug.log* 37 | yarn-debug.log* 38 | yarn-error.log* 39 | 40 | # Runtime data 41 | pids 42 | *.pid 43 | *.seed 44 | *.pid.lock 45 | 46 | # Directory for instrumented libs generated by jscoverage/JSCover 47 | lib-cov 48 | 49 | # Coverage directory used by tools like istanbul 50 | coverage 51 | 52 | # nyc test coverage 53 | .nyc_output 54 | 55 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 56 | .grunt 57 | 58 | # Bower dependency directory (https://bower.io/) 59 | bower_components 60 | 61 | # node-waf configuration 62 | .lock-wscript 63 | 64 | # Compiled binary addons (https://nodejs.org/api/addons.html) 65 | build/Release 66 | 67 | # Dependency directories 68 | node_modules/ 69 | jspm_packages/ 70 | 71 | # TypeScript v1 declaration files 72 | typings/ 73 | 74 | # Optional npm cache directory 75 | .npm 76 | 77 | # Optional eslint cache 78 | .eslintcache 79 | 80 | # Optional REPL history 81 | .node_repl_history 82 | 83 | # Output of 'npm pack' 84 | *.tgz 85 | 86 | # Yarn Integrity file 87 | .yarn-integrity 88 | 89 | # dotenv environment variables file 90 | .env 91 | 92 | # parcel-bundler cache (https://parceljs.org/) 93 | .cache 94 | 95 | # next.js build output 96 | .next 97 | 98 | # nuxt.js build output 99 | .nuxt 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # Serverless directories 105 | .serverless 106 | 107 | 108 | # End of https://www.gitignore.io/api/node,macos 109 | 110 | package-lock\.json 111 | -------------------------------------------------------------------------------- /src/api/api.ts: -------------------------------------------------------------------------------- 1 | import HttpClient from './iHttpImp' 2 | import md5 from 'md5' 3 | import { LEAN_API_KEY, LEAN_API_ID, BASE_API_URL } from './constants' 4 | import { keys, get } from '../utils/localStorage' 5 | import { HTTP_METHODS, RequestConfig } from './interfaces/iHttp' 6 | 7 | const isLog = false 8 | 9 | export function getHeaders() { 10 | let timeStamp = new Date().getTime() 11 | let token = get(keys.KEY_TOKEN) 12 | return { 13 | 'X-LC-Id': LEAN_API_ID, 14 | 'X-LC-Sign': md5(timeStamp + LEAN_API_KEY) + ',' + timeStamp, 15 | 'X-LC-Session': token 16 | } 17 | } 18 | interface APIParams { 19 | url: string 20 | params?: any 21 | extra?: any 22 | } 23 | 24 | function _buildRequestConfig({ 25 | method 26 | }: { 27 | method: HTTP_METHODS 28 | }): RequestConfig { 29 | return { 30 | headers: getHeaders(), 31 | method 32 | } 33 | } 34 | 35 | const errorHanding = err => { 36 | isLog && console.log('-------- error --------') 37 | isLog && console.error(err) 38 | } 39 | 40 | function _wrapperResponse(data: any): T { 41 | isLog && console.log('-------- response --------') 42 | isLog && console.log(data) 43 | return data as T 44 | } 45 | 46 | export async function apiGet(params: APIParams): Promise { 47 | return _api(params, HTTP_METHODS.GET) 48 | } 49 | 50 | export async function apiPost(params: APIParams): Promise { 51 | return _api(params, HTTP_METHODS.POST) 52 | } 53 | 54 | export async function apiDelete(params: APIParams): Promise { 55 | return _api(params, HTTP_METHODS.DELETE) 56 | } 57 | 58 | export async function apiPut(params: APIParams): Promise { 59 | return _api(params, HTTP_METHODS.PUT) 60 | } 61 | 62 | async function _api(params: APIParams, method: HTTP_METHODS) { 63 | isLog && console.log('-------- request start --------') 64 | isLog && console.log(params) 65 | isLog && console.log(method) 66 | isLog && console.log('-------- request end --------') 67 | try { 68 | let res = await HttpClient.request( 69 | BASE_API_URL.concat(params.url), 70 | params.params, 71 | _buildRequestConfig({ method }) 72 | ) 73 | return _wrapperResponse(res.data) 74 | } catch (error) { 75 | errorHanding(error) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/utils/store.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dva, { connect } from 'dva'; 3 | import { Provider } from 'react-redux'; 4 | import model from '../model/index'; 5 | 6 | const checkServer = () => Object.prototype.toString.call(global.process) === '[object process]'; 7 | 8 | // eslint-disable-next-line 9 | const __NEXT_DVA_STORE__ = '__NEXT_DVA_STORE__' 10 | 11 | function createDvaStore(initialState) { 12 | let app; 13 | if (initialState) { 14 | app = dva({ 15 | initialState, 16 | }); 17 | } else { 18 | app = dva({}); 19 | } 20 | const isArray = Array.isArray(model); 21 | if (isArray) { 22 | model.forEach((m) => { 23 | app.model(m); 24 | }); 25 | } else { 26 | app.model(model); 27 | } 28 | app.router(() => {}); 29 | app.start(); 30 | // console.log(app); 31 | // eslint-disable-next-line 32 | const store = app._store 33 | return store; 34 | } 35 | 36 | function getOrCreateStore(initialState) { 37 | const isServer = checkServer(); 38 | if (isServer) { // run in server 39 | // console.log('server'); 40 | return createDvaStore(initialState); 41 | } 42 | // eslint-disable-next-line 43 | if (!window[__NEXT_DVA_STORE__]) { 44 | // console.log('client'); 45 | // eslint-disable-next-line 46 | window[__NEXT_DVA_STORE__] = createDvaStore(initialState); 47 | } 48 | // eslint-disable-next-line 49 | return window[__NEXT_DVA_STORE__]; 50 | } 51 | 52 | export default function withDva(...args) { 53 | return function CreateNextPage(Component) { 54 | const ComponentWithDva = (props = {}) => { 55 | const { store, initialProps, initialState } = props; 56 | const ConnectedComponent = connect(...args)(Component); 57 | return React.createElement( 58 | Provider, 59 | // in client side, it will init store with the initial state tranfer from server side 60 | { store: store && store.dispatch ? store : getOrCreateStore(initialState) }, 61 | // transfer next.js's props to the page 62 | React.createElement(ConnectedComponent, initialProps), 63 | ); 64 | }; 65 | ComponentWithDva.getInitialProps = async (props = {}) => { 66 | // console.log('get......'); 67 | const isServer = checkServer(); 68 | const store = getOrCreateStore(props.req); 69 | // call children's getInitialProps 70 | // get initProps and transfer in to the page 71 | const initialProps = Component.getInitialProps 72 | ? await Component.getInitialProps({ ...props, isServer, store }) 73 | : {}; 74 | return { 75 | store, 76 | initialProps, 77 | initialState: store.getState(), 78 | }; 79 | }; 80 | return ComponentWithDva; 81 | }; 82 | } -------------------------------------------------------------------------------- /src/components/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Menu, Icon } from 'antd' 3 | 4 | import WithDva from 'dva-utils/store' 5 | 6 | const SubMenu = Menu.SubMenu 7 | 8 | let id = 0 9 | 10 | @WithDva(({ router }) => { 11 | return { router } 12 | }) 13 | export default class Sider extends React.Component { 14 | handleClick = e => { 15 | const { dispatch } = this.props 16 | switch (e.key) { 17 | case '1': 18 | dispatch({ 19 | type: 'router/updatePath', 20 | path: '/store/store/Info' 21 | }) 22 | break 23 | case '2': 24 | dispatch({ 25 | type: 'router/updatePath', 26 | path: '/store/notification/List' 27 | }) 28 | break 29 | case '3': 30 | dispatch({ 31 | type: 'router/updatePath', 32 | path: '/goods/category/List' 33 | }) 34 | break 35 | case '4': 36 | dispatch({ 37 | type: 'router/updatePath', 38 | path: '/goods/goods/List' 39 | }) 40 | break 41 | } 42 | dispatch({ 43 | type: 'router/updateKey', 44 | key: e.key 45 | }) 46 | } 47 | 48 | render() { 49 | const { key } = this.props.router 50 | return ( 51 | 58 | 62 | 63 | 店铺管理 64 | 65 | } 66 | > 67 | 店铺信息 68 | 公告管理 69 | 70 | 74 | 75 | 商品管理 76 | 77 | } 78 | > 79 | 分类管理 80 | 商品管理 81 | 82 | 86 | 87 | 订单管理 88 | 89 | } 90 | > 91 | 订单列表 92 | 93 | 97 | 98 | 供应链管理 99 | 100 | } 101 | > 102 | 骑手管理 103 | 104 | 105 | ) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/pages/user/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './login.less' 3 | import WithDva from 'dva-utils/store' 4 | import { Form, Icon, Input, Button, Checkbox } from 'antd' 5 | const FormItem = Form.Item 6 | 7 | @WithDva(({ common }) => { 8 | return { common } 9 | }) 10 | @Form.create() 11 | export default class NormalLoginForm extends React.Component { 12 | props: { 13 | dispatch: Function 14 | form: any 15 | } 16 | 17 | handleSubmit = e => { 18 | e.preventDefault() 19 | 20 | const { dispatch } = this.props 21 | 22 | this.props.form.validateFields((err, values) => { 23 | if (!err) { 24 | console.log('Received values of form: ', values) 25 | dispatch({ 26 | type: 'user/login', 27 | payload: { 28 | username: values.username, 29 | password: values.password 30 | } 31 | }) 32 | } 33 | }) 34 | } 35 | 36 | render() { 37 | const { getFieldDecorator } = this.props.form 38 | 39 | return ( 40 |
41 |
42 | 43 | {getFieldDecorator('username', { 44 | rules: [ 45 | { required: true, message: 'Please input your username!' } 46 | ] 47 | })( 48 | 51 | } 52 | placeholder="Username" 53 | /> 54 | )} 55 | 56 | 57 | {getFieldDecorator('password', { 58 | rules: [ 59 | { required: true, message: 'Please input your Password!' } 60 | ] 61 | })( 62 | 65 | } 66 | type="password" 67 | placeholder="Password" 68 | /> 69 | )} 70 | 71 | 72 | {getFieldDecorator('remember', { 73 | valuePropName: 'checked', 74 | initialValue: true 75 | })(Remember me)} 76 | 77 | Forgot password 78 | 79 | 86 | Or register now! 87 | 88 |
89 |
90 | ) 91 | } 92 | } 93 | 94 | // const WrappedNormalLoginForm = Form.create()(NormalLoginForm) 95 | 96 | // export default WrappedNormalLoginForm 97 | -------------------------------------------------------------------------------- /src/pages/goods/goods/c/Item.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Card, Icon, Popconfirm } from 'antd' 3 | const { Meta } = Card 4 | 5 | import './Item.style.less' 6 | import Goods from '../../../../class/Goods' 7 | import { SPEC_TYPE_BASE, SPEC_TYPE_MODIFY } from '../../../../class/goodsTypes' 8 | 9 | interface ItemProps { 10 | isSelected?: boolean 11 | data: Goods 12 | onEditClick(goods: Goods): void 13 | onDeleteClick(goods: Goods): void 14 | } 15 | 16 | const DEFAULT_IMG_URL = 17 | 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1539053797&di=6cf97cb3dbdb85da447b75c1f2aef921&imgtype=jpg&er=1&src=http%3A%2F%2Fpic40.photophoto.cn%2F20160807%2F1155115790822404_b.jpg' 18 | 19 | export default class Item extends Component { 20 | private getPrice = (): string => { 21 | const goods = this.props.data 22 | const { spec, price } = goods 23 | 24 | if (spec.length > 0) { 25 | let minPrice = undefined 26 | spec.map(item => { 27 | item.subSpecs.map(subItem => { 28 | if ( 29 | subItem.type === SPEC_TYPE_BASE && 30 | (!minPrice || (isNaN(minPrice) && subItem.price < minPrice)) 31 | ) { 32 | minPrice = subItem.price 33 | } 34 | }) 35 | }) 36 | let minFixPrice = undefined 37 | spec.map(item => { 38 | item.subSpecs.map(subItem => { 39 | if ( 40 | subItem.type === SPEC_TYPE_MODIFY && 41 | (!minFixPrice || 42 | (isNaN(minFixPrice) && subItem.price < minFixPrice)) 43 | ) { 44 | minFixPrice = subItem.price 45 | } 46 | }) 47 | }) 48 | 49 | if (!minPrice) minPrice = 0 50 | if (!minFixPrice) minFixPrice = 0 51 | return `${minPrice + minFixPrice}元起` 52 | } else { 53 | return `${price}元` 54 | } 55 | } 56 | 57 | render() { 58 | const { onEditClick, onDeleteClick } = this.props 59 | const goods = this.props.data 60 | 61 | return ( 62 |
63 | 72 | } 73 | actions={[ 74 | , 75 | onEditClick(goods)} />, 76 | {onDeleteClick(goods)}} 79 | okText="确定" 80 | cancelText="取消" 81 | > 82 | 83 | 84 | ]} 85 | > 86 | 87 | {goods.spec.map((item, index) => { 88 | let subSpecsText = '' 89 | item.subSpecs.map(subItem => { 90 | subSpecsText += subItem.name + '/' 91 | }) 92 | subSpecsText = subSpecsText.substring(0, subSpecsText.length - 1) 93 | return ( 94 | 95 | ) 96 | })} 97 | 98 | 99 |
100 | ) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/model/api.ts: -------------------------------------------------------------------------------- 1 | import * as restfulAPI from '../api/business/restfulAPI' 2 | import Query from '../class/Query' 3 | const MODEL_NAME = 'api' 4 | const initialState = { 5 | perPageSize: 10 6 | } 7 | 8 | export type GoodsState = Readonly 9 | 10 | const model = { 11 | namespace: MODEL_NAME, 12 | state: initialState, 13 | reducers: { 14 | update(state, data) { 15 | return { ...state, data } 16 | }, 17 | updateList(state, { payload }) { 18 | let result = { ...state } 19 | result[payload.className + 'List'] = payload.results 20 | result[payload.className + 'Count'] = payload.count 21 | result[payload.className + 'Page'] = payload.page 22 | return result 23 | }, 24 | updateCurrent(state, { payload }) { 25 | let result = { ...state } 26 | result[payload.className + 'Current'] = payload.params 27 | return result 28 | }, 29 | }, 30 | effects: { 31 | *getList( 32 | { 33 | payload 34 | }: { payload: { className: string; params: any; query?: Query } }, 35 | { select, call, put } 36 | ) { 37 | let page = yield select( 38 | state => state[MODEL_NAME][payload.className + 'Page'] 39 | ) 40 | 41 | if (!page) { 42 | page = 1 43 | } 44 | 45 | if (payload.params) { 46 | if (typeof payload.params === 'string' && payload.params === 'all') { 47 | page = -1 48 | } else { 49 | page = payload.params 50 | } 51 | } 52 | 53 | let pageSize = yield select(state => state[MODEL_NAME].perPageSize) 54 | 55 | let pQuery: Query 56 | if (payload.query) { 57 | pQuery = 58 | page !== -1 59 | ? { 60 | ...payload.query, 61 | limit: pageSize, 62 | skip: (page - 1) * pageSize 63 | } 64 | : { ...payload.query } 65 | } else { 66 | pQuery = 67 | page !== -1 68 | ? { 69 | limit: pageSize, 70 | skip: (page - 1) * pageSize 71 | } 72 | : {} 73 | } 74 | 75 | let result = yield call(restfulAPI.rstGet, pQuery, payload.className) 76 | 77 | yield put({ 78 | type: 'updateList', 79 | payload: { ...result, page, className: payload.className } 80 | }) 81 | }, 82 | *create( 83 | { payload }: { payload: { className: string; params: any } }, 84 | { select, call, put } 85 | ) { 86 | let currentData = yield select( 87 | state => state[MODEL_NAME][payload.className + 'Current'] 88 | ) 89 | if (currentData && currentData.objectId) { 90 | let data = { 91 | ...payload.params, 92 | objectId: currentData.objectId 93 | } 94 | yield call(restfulAPI.rstPut, data, payload.className) 95 | } else { 96 | yield call(restfulAPI.rstPost, payload.params, payload.className) 97 | } 98 | yield put({ 99 | type: 'updateCurrent', 100 | payload: { params: {}, className: payload.className } 101 | }) 102 | yield put({ type: 'getList', payload: { className: payload.className } }) 103 | }, 104 | *delete( 105 | { payload }: { payload: { className: string; params: any } }, 106 | { call, put } 107 | ) { 108 | yield call(restfulAPI.rstDelete, payload.params, payload.className) 109 | 110 | yield put({ type: 'getList', payload: { className: payload.className } }) 111 | } 112 | } 113 | } 114 | 115 | export type MODEL_API = Readonly 116 | 117 | export default model 118 | -------------------------------------------------------------------------------- /src/pages/goods/goods/c/DragSelectContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import './DragSelectContainer.style.less' 3 | 4 | interface DragSelectContainerProps { 5 | onSelected: Function 6 | enable: boolean 7 | } 8 | 9 | export default class DragSelectContainer extends Component< 10 | DragSelectContainerProps, 11 | any 12 | > { 13 | state = { 14 | startX: 0, 15 | startY: 0, 16 | currentX: 0, 17 | currentY: 0, 18 | isMoving: false, 19 | isMouseDown: false 20 | } 21 | _onMouseDown = e => { 22 | if (!this.props.enable) return 23 | this.setState({ 24 | startX: e.clientX, 25 | startY: e.clientY, 26 | isMouseDown: true 27 | }) 28 | } 29 | 30 | _onMouseMove = e => { 31 | if (!this.state.isMouseDown) return 32 | 33 | this.setState({ 34 | currentX: e.clientX, 35 | currentY: e.clientY, 36 | isMoving: true 37 | }) 38 | 39 | this._renderSelectionArae() 40 | 41 | this._updateCurrentState() 42 | } 43 | 44 | _onMouseUp = () => { 45 | this.setState({ 46 | isMoving: false, 47 | isMouseDown: false 48 | }) 49 | } 50 | 51 | _updateCurrentState = () => { 52 | let isInResult = [] 53 | let dragContainer: HTMLElement = this.refs.dragContainer 54 | if (dragContainer && dragContainer.children) { 55 | let children: HTMLCollection = dragContainer.children 56 | for (let i = 0; i < children.length; i++) { 57 | let ele = children.item(i) 58 | let rect = ele.getBoundingClientRect() 59 | let left = rect.left 60 | let right = left + rect.width 61 | let top = rect.top 62 | let bottom = rect.top + rect.height 63 | 64 | let mLeft 65 | let mRight 66 | let mTop 67 | let mBottom 68 | if (this.state.startX < this.state.currentX) { 69 | mLeft = this.state.startX 70 | mRight = this.state.currentX 71 | } else { 72 | mLeft = this.state.currentX 73 | mRight = this.state.startX 74 | } 75 | 76 | if (this.state.startY < this.state.currentY) { 77 | mTop = this.state.startY 78 | mBottom = this.state.currentY 79 | } else { 80 | mTop = this.state.currentY 81 | mBottom = this.state.startY 82 | } 83 | 84 | let isNotIn = 85 | mRight < left || mLeft > right || mBottom < top || mTop > bottom 86 | 87 | if (!isNotIn) { 88 | isInResult.push(i) 89 | } 90 | } 91 | isInResult.pop() 92 | this.props.onSelected(isInResult) 93 | } else { 94 | this.props.onSelected([]) 95 | } 96 | } 97 | 98 | _renderSelectionArae = () => { 99 | if (this.state.isMoving) { 100 | return ( 101 |
116 | ) 117 | } else { 118 | return null 119 | } 120 | } 121 | 122 | render() { 123 | return ( 124 |
135 | {this.props.children} 136 | {this._renderSelectionArae()} 137 |
138 | ) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/model/goods.ts: -------------------------------------------------------------------------------- 1 | import * as goodsAPI from '../api/business/goodsAPI' 2 | import Category from '../class/Category' 3 | import Query from '../class/Query' 4 | import Goods from '../class/Goods' 5 | import { CategoryListRep, GoodsListRep } from '../class/goodsTypes' 6 | 7 | const initialState = { 8 | goodsCount: 0, 9 | categoryCount: 0, 10 | currentCategory: {} as Category, 11 | currentGoods: {} as Goods, 12 | categoryList: [] as Array, 13 | goodsList: [] as Array, 14 | categoryPage: 1, 15 | goodsPage: 1, 16 | perPageSize: 10 17 | } 18 | 19 | export type GoodsState = Readonly 20 | 21 | const model = { 22 | namespace: 'goods', 23 | state: initialState, 24 | reducers: { 25 | updateLoading(state, loading) { 26 | return { ...state, loading } 27 | }, 28 | updateCategoryList(state, { payload }) { 29 | return { 30 | ...state, 31 | categoryList: payload.results, 32 | categoryCount: payload.count, 33 | categoryPage: payload.categoryPage 34 | } 35 | }, 36 | updateGoodsList(state, { payload }) { 37 | return { 38 | ...state, 39 | goodsList: payload.results, 40 | goodsCount: payload.count, 41 | goodsPage: payload.goodsPage 42 | } 43 | }, 44 | updateCurrentCategory(state, { payload }) { 45 | return { ...state, currentCategory: payload } 46 | }, 47 | updateCurrentGoods(state, { payload }) { 48 | return { ...state, currentGoods: payload } 49 | } 50 | }, 51 | effects: { 52 | *getCategoryList({ payload }, { select, call, put }) { 53 | let page = yield select(state => state.goods.categoryPage) 54 | 55 | if (payload) { 56 | if (typeof payload === 'string' && payload === 'all') { 57 | page = -1 58 | } else { 59 | page = payload 60 | } 61 | } 62 | 63 | let pageSize = yield select(state => state.goods.perPageSize) 64 | let query: Query = 65 | page !== -1 66 | ? { 67 | limit: pageSize, 68 | skip: (page - 1) * pageSize 69 | } 70 | : {} 71 | let result: CategoryListRep = yield call(goodsAPI.category, query) 72 | 73 | yield put({ 74 | type: 'updateCategoryList', 75 | payload: { ...result, categoryPage: page } 76 | }) 77 | }, 78 | 79 | *getGoodsList({ payload }, { select, call, put }) { 80 | let page = yield select(state => state.goods.goodsPage) 81 | 82 | if (payload) { 83 | if (typeof payload === 'string' && payload === 'all') { 84 | page = -1 85 | } else { 86 | page = payload 87 | } 88 | } 89 | 90 | let pageSize = yield select(state => state.goods.perPageSize) 91 | let query: Query = 92 | page !== -1 93 | ? { 94 | limit: pageSize, 95 | skip: (page - 1) * pageSize 96 | } 97 | : {} 98 | let result: GoodsListRep = yield call(goodsAPI.goods, query) 99 | 100 | yield put({ 101 | type: 'updateGoodsList', 102 | payload: { ...result, goodsPage: page } 103 | }) 104 | }, 105 | *createGoods(action: { payload: Goods }, { select, call, put }) { 106 | let currentGoods: Goods = yield select( 107 | state => state.goods.currentCategory 108 | ) 109 | if (currentGoods.objectId) { 110 | let goods = { 111 | ...action.payload, 112 | objectId: currentGoods.objectId 113 | } 114 | yield call(goodsAPI.goodsCreate, goods) 115 | } else { 116 | yield call(goodsAPI.goodsCreate, action.payload) 117 | } 118 | yield put({ type: 'updateCurrentGoods', payload: {} }) 119 | yield put({ type: 'getGoodsList' }) 120 | }, 121 | *createCategory(action: { payload: Category }, { select, call, put }) { 122 | let currentCategory = yield select(state => state.goods.currentCategory) 123 | if (currentCategory.objectId) { 124 | let category = { 125 | ...action.payload, 126 | objectId: currentCategory.objectId 127 | } 128 | yield call(goodsAPI.categoryUpdate, category) 129 | } else { 130 | yield call(goodsAPI.categoryCreate, action.payload) 131 | } 132 | yield put({ type: 'updateCurrentCategory', payload: {} }) 133 | yield put({ type: 'getCategoryList' }) 134 | }, 135 | *deleteCategory(action: { payload: Category }, { call, put }) { 136 | yield call(goodsAPI.categoryDelete, action.payload) 137 | 138 | yield put({ type: 'getCategoryList' }) 139 | } 140 | } 141 | } 142 | 143 | export default model 144 | -------------------------------------------------------------------------------- /src/pages/goods/category/List.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // @ts-ignore 3 | import Dashboard from '@layouts/Dashboard' 4 | // @ts-ignore 5 | import WithDva from 'dva-utils/store' 6 | import { Table, Button, Modal, Form, Input, Popconfirm } from 'antd' 7 | import { FormComponentProps } from 'antd/lib/form' 8 | import { ColumnProps } from 'antd/lib/table' 9 | import './list.less' 10 | import Category from '../../../class/Category' 11 | import { API_CLASS_NAME } from '../../../constants/className' 12 | const FormItem = Form.Item 13 | 14 | interface CollectionCreateFormProps extends FormComponentProps { 15 | visible: boolean 16 | onCancel: any 17 | onCreate: any 18 | currentCategory: Category 19 | } 20 | const CollectionCreateForm = Form.create()( 21 | class extends React.Component { 22 | render() { 23 | const { visible, onCancel, onCreate, form } = this.props 24 | const { getFieldDecorator } = form 25 | return ( 26 | 34 |
35 | 36 | {getFieldDecorator('name', { 37 | rules: [ 38 | { 39 | required: true, 40 | message: '名称为必填项目' 41 | } 42 | ] 43 | })()} 44 | 45 | 46 | {getFieldDecorator('sort')()} 47 | 48 |
49 |
50 | ) 51 | } 52 | } 53 | ) 54 | 55 | interface CategoryListPageProps { 56 | goods: any 57 | api: any 58 | dispatch: any 59 | } 60 | 61 | @WithDva(({ api }) => { 62 | return { api } 63 | }) 64 | export default class CategoryListPage extends React.Component< 65 | CategoryListPageProps, 66 | any 67 | > { 68 | state = { 69 | visible: false 70 | } 71 | 72 | formRef: Form 73 | 74 | constructor(props) { 75 | super(props) 76 | } 77 | 78 | componentDidMount() { 79 | const { dispatch } = this.props 80 | 81 | dispatch({ 82 | type: 'api/getList', 83 | payload: { 84 | className: API_CLASS_NAME.CATEGORY 85 | } 86 | }) 87 | } 88 | 89 | showModal = (item?: Category) => { 90 | const { dispatch } = this.props 91 | dispatch({ 92 | type: 'api/updateCurrent', 93 | payload: { params: item ? item : {}, className: API_CLASS_NAME.CATEGORY } 94 | }) 95 | // set value 96 | if (this.formRef) { 97 | this.formRef.props.form.setFieldsValue({ 98 | name: item ? item.name : '', 99 | sort: item ? item.sort : '' 100 | }) 101 | } 102 | 103 | this.setState({ visible: true }) 104 | } 105 | 106 | handleTableChange = (pagination, filters, sorter) => { 107 | const { dispatch } = this.props 108 | let current = pagination.current 109 | 110 | dispatch({ 111 | type: 'api/getList', 112 | payload: { 113 | className: API_CLASS_NAME.CATEGORY, 114 | params: current 115 | } 116 | }) 117 | } 118 | 119 | handleCancel = () => { 120 | this.setState({ visible: false }) 121 | } 122 | 123 | handleDelete = item => { 124 | const { dispatch } = this.props 125 | dispatch({ 126 | type: 'api/delete', 127 | payload: { params: item, className: API_CLASS_NAME.CATEGORY } 128 | }) 129 | } 130 | 131 | handleCreate = () => { 132 | const form = this.formRef.props.form 133 | form.validateFields((err, values) => { 134 | if (err) { 135 | return 136 | } 137 | 138 | const { dispatch } = this.props 139 | 140 | let category: Category = { 141 | name: values.name, 142 | sort: values.sort 143 | } 144 | 145 | dispatch({ 146 | type: 'api/create', 147 | payload: { 148 | params: category, 149 | className: API_CLASS_NAME.CATEGORY 150 | } 151 | }) 152 | 153 | console.log('Received values of form: ', values) 154 | 155 | form.resetFields() 156 | this.setState({ visible: false }) 157 | }) 158 | } 159 | 160 | saveFormRef = formRef => { 161 | this.formRef = formRef 162 | } 163 | 164 | columns = () : Array> => { 165 | return [ 166 | { title: 'ID', dataIndex: 'objectId', key: 'objectId' }, 167 | { title: '名称', dataIndex: 'name', key: 'name' }, 168 | { title: '排序', dataIndex: 'sort', key: 'sort' }, 169 | { 170 | title: '操作', 171 | key: 'x', 172 | render: item => ( 173 |

174 | { 176 | this.showModal(item) 177 | }} 178 | > 179 | 编辑 180 | 181 | { 184 | this.handleDelete(item) 185 | }} 186 | okText="确定" 187 | cancelText="取消" 188 | > 189 | 删除 190 | 191 |

192 | ) 193 | } 194 | ] 195 | } 196 | 197 | render() { 198 | let categoryList = this.props.api[API_CLASS_NAME.CATEGORY + 'List'] 199 | let categoryCount = this.props.api[API_CLASS_NAME.CATEGORY + 'Count'] 200 | let currentCategory = this.props.api[API_CLASS_NAME.CATEGORY + 'Current'] 201 | 202 | return ( 203 | 204 |
205 | 208 | { 216 | return item.objectId 217 | }} 218 | onChange={this.handleTableChange} 219 | /> 220 | 227 | 228 | 229 | ) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/pages/store/notification/List.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // @ts-ignore 3 | import Dashboard from '@layouts/Dashboard' 4 | // @ts-ignore 5 | import ToolBar from '@components/toolbar/ToolBar' 6 | // @ts-ignore 7 | import WithDva from 'dva-utils/store' 8 | import { Table, Button, Modal, Form, Input, Popconfirm } from 'antd' 9 | import { FormComponentProps } from 'antd/lib/form' 10 | 11 | import { ColumnProps } from 'antd/lib/table' 12 | import './List.style.less' 13 | import Category from '../../../class/Category' 14 | import { API_CLASS_NAME } from '../../../constants/className' 15 | const FormItem = Form.Item 16 | 17 | const showButtons = [ 18 | { 19 | id: 1, 20 | text: 'New' 21 | }, 22 | { 23 | id: 2, 24 | text: 'Edit' 25 | }, 26 | { 27 | id: 3, 28 | text: 'Delete' 29 | }, 30 | { 31 | id: 4, 32 | text: 'Unselect All' 33 | } 34 | ] 35 | 36 | interface CollectionCreateFormProps extends FormComponentProps { 37 | visible: boolean 38 | onCancel: any 39 | onCreate: any 40 | currentCategory: Category 41 | } 42 | const CollectionCreateForm = Form.create()( 43 | class extends React.Component { 44 | render() { 45 | const { visible, onCancel, onCreate, form } = this.props 46 | const { getFieldDecorator } = form 47 | return ( 48 | 56 |
57 | 58 | {getFieldDecorator('name', { 59 | rules: [ 60 | { 61 | required: true, 62 | message: '名称为必填项目' 63 | } 64 | ] 65 | })()} 66 | 67 | 68 | {getFieldDecorator('sort')()} 69 | 70 | 71 |
72 | ) 73 | } 74 | } 75 | ) 76 | 77 | interface CategoryListPageProps { 78 | goods: any 79 | api: any 80 | dispatch: any 81 | } 82 | 83 | @WithDva(({ api }) => { 84 | return { api } 85 | }) 86 | export default class CategoryListPage extends React.Component< 87 | CategoryListPageProps, 88 | any 89 | > { 90 | state = { 91 | visible: false 92 | } 93 | 94 | formRef: Form 95 | 96 | constructor(props) { 97 | super(props) 98 | } 99 | 100 | componentDidMount() { 101 | const { dispatch } = this.props 102 | 103 | dispatch({ 104 | type: 'api/getList', 105 | payload: { 106 | className: API_CLASS_NAME.CATEGORY 107 | } 108 | }) 109 | } 110 | 111 | showModal = (item?: Category) => { 112 | const { dispatch } = this.props 113 | dispatch({ 114 | type: 'api/updateCurrent', 115 | payload: { params: item ? item : {}, className: API_CLASS_NAME.CATEGORY } 116 | }) 117 | // set value 118 | if (this.formRef) { 119 | this.formRef.props.form.setFieldsValue({ 120 | name: item ? item.name : '', 121 | sort: item ? item.sort : '' 122 | }) 123 | } 124 | 125 | this.setState({ visible: true }) 126 | } 127 | 128 | handleTableChange = (pagination, filters, sorter) => { 129 | const { dispatch } = this.props 130 | let current = pagination.current 131 | 132 | dispatch({ 133 | type: 'api/getList', 134 | payload: { 135 | className: API_CLASS_NAME.CATEGORY, 136 | params: current 137 | } 138 | }) 139 | } 140 | 141 | handleCancel = () => { 142 | this.setState({ visible: false }) 143 | } 144 | 145 | handleDelete = item => { 146 | const { dispatch } = this.props 147 | dispatch({ 148 | type: 'api/delete', 149 | payload: { params: item, className: API_CLASS_NAME.CATEGORY } 150 | }) 151 | } 152 | 153 | handleCreate = () => { 154 | const form = this.formRef.props.form 155 | form.validateFields((err, values) => { 156 | if (err) { 157 | return 158 | } 159 | 160 | const { dispatch } = this.props 161 | 162 | let category: Category = { 163 | name: values.name, 164 | sort: values.sort 165 | } 166 | 167 | dispatch({ 168 | type: 'api/create', 169 | payload: { 170 | params: category, 171 | className: API_CLASS_NAME.CATEGORY 172 | } 173 | }) 174 | 175 | console.log('Received values of form: ', values) 176 | 177 | form.resetFields() 178 | this.setState({ visible: false }) 179 | }) 180 | } 181 | 182 | saveFormRef = formRef => { 183 | this.formRef = formRef 184 | } 185 | 186 | columns = (): Array> => { 187 | return [ 188 | { title: 'ID', dataIndex: 'objectId', key: 'objectId' }, 189 | { title: '名称', dataIndex: 'name', key: 'name' }, 190 | { title: '排序', dataIndex: 'sort', key: 'sort' }, 191 | { 192 | title: '操作', 193 | key: 'x', 194 | render: item => ( 195 |

196 | { 198 | this.showModal(item) 199 | }} 200 | > 201 | 编辑 202 | 203 | { 206 | this.handleDelete(item) 207 | }} 208 | okText="确定" 209 | cancelText="取消" 210 | > 211 | 删除 212 | 213 |

214 | ) 215 | } 216 | ] 217 | } 218 | 219 | render() { 220 | let categoryList = this.props.api[API_CLASS_NAME.CATEGORY + 'List'] 221 | let categoryCount = this.props.api[API_CLASS_NAME.CATEGORY + 'Count'] 222 | let currentCategory = this.props.api[API_CLASS_NAME.CATEGORY + 'Current'] 223 | 224 | return ( 225 | 226 |
227 | { 230 | switch (id) { 231 | case 1: 232 | this.setState({ 233 | isEditDialogShow: true 234 | }) 235 | break 236 | } 237 | }} 238 | search={key => alert(key)} 239 | /> 240 |
{ 248 | return item.objectId 249 | }} 250 | onChange={this.handleTableChange} 251 | /> 252 | 259 | 260 | 261 | ) 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/pages/goods/goods/List.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Form, Pagination } from 'antd' 3 | import _ from 'lodash' 4 | // @ts-ignore 5 | import Dashboard from '@layouts/Dashboard' 6 | // @ts-ignore 7 | import WithDva from 'dva-utils/store' 8 | import DragSelectContainer from './c/DragSelectContainer' 9 | // @ts-ignore 10 | import ToolBar from '@components/toolbar/ToolBar' 11 | import Item from './c/Item' 12 | import GoodsForm from './c/GoodsForm' 13 | 14 | import Goods from '../../../class/Goods' 15 | import { GoodsState } from '../../../model/goods' 16 | import { MODEL_API } from '../../../model/api' 17 | import { API_CLASS_NAME, DB_GOODS } from '../../../constants/className' 18 | import Category from '../../../class/Category' 19 | import Query from '../../../class/Query' 20 | 21 | import './List.style.less' 22 | 23 | const showButtons = [ 24 | { 25 | id: 1, 26 | text: 'New' 27 | }, 28 | { 29 | id: 2, 30 | text: 'Edit' 31 | }, 32 | { 33 | id: 3, 34 | text: 'Delete' 35 | }, 36 | { 37 | id: 4, 38 | text: 'Unselect All' 39 | } 40 | ] 41 | 42 | type GoodsListProps = { 43 | dispatch: any 44 | goods: GoodsState 45 | api: MODEL_API 46 | } 47 | 48 | type GoodsListStates = { 49 | selectedItems: Array 50 | isMutilSelect: boolean 51 | isEditDialogShow: boolean 52 | } 53 | 54 | const initialState = { 55 | selectedItems: [], 56 | isMutilSelect: false, 57 | isEditDialogShow: false 58 | } 59 | type State = Readonly 60 | 61 | @WithDva(({ api }: { api: MODEL_API }) => { 62 | return { api } 63 | }) 64 | export default class GoodsList extends React.Component< 65 | GoodsListProps, 66 | GoodsListStates 67 | > { 68 | readonly state: State = initialState 69 | 70 | formRef: Form 71 | saveFormRef = formRef => { 72 | this.formRef = formRef 73 | } 74 | 75 | componentDidMount() { 76 | const { dispatch } = this.props 77 | 78 | dispatch({ 79 | type: 'api/getList', 80 | payload: { 81 | className: API_CLASS_NAME.CATEGORY 82 | } 83 | }) 84 | 85 | dispatch({ 86 | type: 'api/getList', 87 | payload: { 88 | className: API_CLASS_NAME.GOODS, 89 | query: { 90 | include: DB_GOODS.CATEGORY 91 | } as Query 92 | } 93 | }) 94 | } 95 | 96 | render() { 97 | let categoryList = this.props.api[API_CLASS_NAME.CATEGORY + 'List'] 98 | let goodsList: Array = this.props.api[API_CLASS_NAME.GOODS + 'List'] 99 | let categoryCount = this.props.api[API_CLASS_NAME.GOODS + 'Count'] 100 | 101 | goodsList = goodsList ? goodsList : [] 102 | categoryList = categoryList ? categoryList : [] 103 | 104 | return ( 105 | 106 |
107 | { 110 | switch (id) { 111 | case 1: 112 | this.setState({ 113 | isEditDialogShow: true 114 | }) 115 | break 116 | } 117 | }} 118 | search={key => alert(key)} 119 | /> 120 | { 123 | this.setState({ 124 | selectedItems: result 125 | }) 126 | }} 127 | > 128 | {goodsList.map(item => ( 129 | 135 | ))} 136 | 137 | { 142 | const { dispatch } = this.props 143 | let current = page 144 | 145 | dispatch({ 146 | type: 'api/getList', 147 | payload: { 148 | className: API_CLASS_NAME.GOODS, 149 | params: current 150 | } 151 | }) 152 | }} 153 | /> 154 | 161 |
162 |
163 | ) 164 | } 165 | 166 | private handleDialogOnCancel = () => { 167 | const { dispatch } = this.props 168 | 169 | // 关闭弹窗 170 | this.setState(showEditDialog(false)) 171 | 172 | // 重置表单 173 | const form = this.formRef.props.form 174 | form.resetFields() 175 | 176 | // 重置state对象 177 | dispatch({ 178 | type: 'api/updateCurrent', 179 | payload: { params: {}, className: API_CLASS_NAME.GOODS } 180 | }) 181 | } 182 | 183 | private handleEdit = (goods: Goods) => { 184 | const newGoods = _.cloneDeep(goods) 185 | const form = this.formRef.props.form 186 | const { setFieldsValue } = form 187 | const { dispatch } = this.props 188 | 189 | const images = newGoods.images.map(item => ({ 190 | name: item.name, 191 | objectId: item.objectId, 192 | uid: item.objectId, 193 | url: item.url, 194 | status: 'done' 195 | })) 196 | 197 | const { title, desc, price, spec, display } = newGoods 198 | 199 | setFieldsValue({ 200 | title, 201 | desc, 202 | price, 203 | spec, 204 | display, 205 | category: goods.category.objectId, 206 | images 207 | }) 208 | 209 | this.setState(showEditDialog(true)) 210 | 211 | dispatch({ 212 | type: 'api/updateCurrent', 213 | payload: { params: goods, className: API_CLASS_NAME.GOODS } 214 | }) 215 | } 216 | 217 | private handleDelete = (goods: Goods) => { 218 | const { dispatch } = this.props 219 | dispatch({ 220 | type: 'api/delete', 221 | payload: { params: goods, className: API_CLASS_NAME.GOODS } 222 | }) 223 | } 224 | 225 | private handleCreate = (category: Category) => { 226 | const form = this.formRef.props.form 227 | form.validateFields((err, values) => { 228 | console.log('Received values of form: ', values) 229 | 230 | if (err) { 231 | return 232 | } 233 | 234 | const { dispatch } = this.props 235 | 236 | let goods: Goods = { 237 | ...values 238 | } 239 | 240 | let params = { 241 | ...goods, 242 | category: { 243 | __type: 'Pointer', 244 | className: 'Category', 245 | objectId: category.objectId 246 | }, 247 | mainImage: { 248 | id: values.images[0].objectId, 249 | __type: 'File' 250 | } 251 | } 252 | 253 | dispatch({ 254 | type: 'api/create', 255 | payload: { 256 | params: params, 257 | className: API_CLASS_NAME.GOODS 258 | } 259 | }) 260 | 261 | form.resetFields() 262 | 263 | this.handleDialogOnCancel() 264 | }) 265 | } 266 | } 267 | 268 | const showEditDialog = (isShow: boolean) => ({ 269 | isEditDialogShow: isShow 270 | }) 271 | -------------------------------------------------------------------------------- /src/pages/store/store/Info.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import _ from 'lodash' 3 | // @ts-ignore 4 | import Dashboard from '@layouts/Dashboard' 5 | // @ts-ignore 6 | import ToolBar from '@components/toolbar/ToolBar' 7 | // @ts-ignore 8 | import WithDva from 'dva-utils/store' 9 | import { Modal, Form, Input } from 'antd' 10 | import StoreInfo from './c/StoreInfo' 11 | import { FormComponentProps } from 'antd/lib/form' 12 | import { PicturesWall } from 'src/pages/goods/goods/c/GoodsForm' 13 | import './Info.style.less' 14 | import Category from '../../../class/Category' 15 | import { API_CLASS_NAME, DB_NAME, DB_STORE } from '../../../constants/className' 16 | import { Store } from 'src/class/Store' 17 | import * as APITypes from 'src/class/api' 18 | import { getUser, keys, get, set } from 'src/utils/localStorage' 19 | import Query from 'src/class/Query' 20 | import { CommonState } from 'src/model/common' 21 | const FormItem = Form.Item 22 | 23 | const showButtons = [ 24 | { 25 | id: 1, 26 | text: 'New' 27 | }, 28 | { 29 | id: 2, 30 | text: 'Edit' 31 | }, 32 | { 33 | id: 3, 34 | text: 'Delete' 35 | }, 36 | { 37 | id: 4, 38 | text: 'Unselect All' 39 | } 40 | ] 41 | 42 | interface CollectionCreateFormProps extends FormComponentProps { 43 | visible: boolean 44 | onCancel: any 45 | onConfirm(store: Store): void 46 | currentStore?: Store 47 | } 48 | const CollectionCreateForm = Form.create()( 49 | class extends React.Component { 50 | render() { 51 | const { visible, onCancel, onConfirm, form, currentStore } = this.props 52 | const { getFieldDecorator } = form 53 | return ( 54 | { 61 | onConfirm(currentStore) 62 | }} 63 | > 64 |
65 | 66 | {getFieldDecorator('name', { 67 | rules: [ 68 | { 69 | required: true, 70 | message: '名称为必填项目' 71 | } 72 | ] 73 | })()} 74 | 75 | 76 | {getFieldDecorator('images', { 77 | initialValue: [], 78 | rules: [ 79 | { 80 | required: true, 81 | message: '上传一张店铺Logo' 82 | } 83 | ] 84 | })()} 85 | 86 | 87 |
88 | ) 89 | } 90 | } 91 | ) 92 | 93 | interface CategoryListPageProps { 94 | goods: any 95 | api: any 96 | dispatch: any 97 | common: CommonState 98 | } 99 | 100 | @WithDva(({ api, common }) => { 101 | return { api, common } 102 | }) 103 | export default class CategoryListPage extends React.Component< 104 | CategoryListPageProps, 105 | any 106 | > { 107 | state = { 108 | isEditDialogShow: false, 109 | currentStoreID: undefined 110 | } 111 | 112 | formRef: Form 113 | 114 | constructor(props) { 115 | super(props) 116 | } 117 | 118 | componentDidMount() { 119 | const { dispatch } = this.props 120 | 121 | const user = getUser() 122 | 123 | if (!getUser) return 124 | 125 | dispatch({ 126 | type: 'api/getList', 127 | payload: { 128 | className: API_CLASS_NAME.STORE, 129 | query: { 130 | where: { 131 | owner: APITypes.buildPointer({ 132 | className: DB_NAME.User, 133 | objectId: user.objectId 134 | }) 135 | }, 136 | include: DB_STORE.LOGO 137 | } as Query 138 | } 139 | }) 140 | 141 | this.setState({ 142 | currentStoreID: get(keys.KEY_CURRENT_SOTRE_ID) 143 | }) 144 | } 145 | 146 | showModal = (item?: Category) => { 147 | const { dispatch } = this.props 148 | dispatch({ 149 | type: 'api/updateCurrent', 150 | payload: { params: item ? item : {}, className: API_CLASS_NAME.CATEGORY } 151 | }) 152 | // set value 153 | if (this.formRef) { 154 | this.formRef.props.form.setFieldsValue({ 155 | name: item ? item.name : '', 156 | sort: item ? item.sort : '' 157 | }) 158 | } 159 | 160 | this.setState({ visible: true }) 161 | } 162 | 163 | handleCancel = () => { 164 | this.setState({ visible: false }) 165 | } 166 | 167 | handleDelete = item => { 168 | const { dispatch } = this.props 169 | dispatch({ 170 | type: 'api/delete', 171 | payload: { params: item, className: API_CLASS_NAME.CATEGORY } 172 | }) 173 | } 174 | 175 | private handleEdit = (store: Store) => { 176 | const newStore: Store = _.cloneDeep(store) 177 | const form = this.formRef.props.form 178 | const { setFieldsValue } = form 179 | const { dispatch } = this.props 180 | 181 | const item = newStore.logo 182 | const images = [ 183 | { 184 | name: item.name, 185 | objectId: item.objectId, 186 | uid: item.objectId, 187 | url: item.url, 188 | status: 'done' 189 | } 190 | ] 191 | 192 | setFieldsValue({ 193 | name: newStore.name, 194 | images 195 | }) 196 | 197 | this.setState(showEditDialog(true)) 198 | 199 | dispatch({ 200 | type: 'api/updateCurrent', 201 | payload: { params: newStore, className: API_CLASS_NAME.STORE } 202 | }) 203 | } 204 | 205 | handleConfirm = () => { 206 | const form = this.formRef.props.form 207 | form.validateFields((err, values) => { 208 | console.log('Received values of form: ', values) 209 | 210 | if (err) { 211 | return 212 | } 213 | 214 | const { dispatch } = this.props 215 | 216 | const user = getUser() 217 | 218 | const params: APITypes.CreateStoreParams = { 219 | name: values.name, 220 | owner: APITypes.buildPointer({ 221 | className: DB_NAME.User, 222 | objectId: user.objectId 223 | }), 224 | logo: APITypes.buildFile({ 225 | objectId: values.images[0].objectId as string 226 | }) 227 | } 228 | 229 | dispatch({ 230 | type: 'api/create', 231 | payload: { 232 | params: params, 233 | className: API_CLASS_NAME.STORE 234 | } 235 | }) 236 | 237 | this.handleDialogOnCancel() 238 | }) 239 | } 240 | 241 | saveFormRef = formRef => { 242 | this.formRef = formRef 243 | } 244 | 245 | render() { 246 | let storeList = this.props.api[API_CLASS_NAME.STORE + 'List'] as Array< 247 | Store 248 | > 249 | let { currentStoreID } = this.props.common 250 | if (!currentStoreID) { 251 | currentStoreID = this.state.currentStoreID 252 | } 253 | 254 | storeList = storeList ? storeList : [] 255 | return ( 256 | 257 |
258 | { 261 | switch (id) { 262 | case 1: 263 | this.setState({ 264 | isEditDialogShow: true 265 | }) 266 | break 267 | } 268 | }} 269 | search={key => alert(key)} 270 | /> 271 | 272 | {storeList.map((item, index) => ( 273 | { 276 | const { dispatch } = this.props 277 | dispatch({ 278 | type: 'common/updateCurrentStoreID', 279 | payload: store.objectId 280 | }) 281 | set(keys.KEY_CURRENT_SOTRE_ID, store.objectId) 282 | }} 283 | isCurrentSotre={currentStoreID === item.objectId} 284 | key={index} 285 | data={item} 286 | /> 287 | ))} 288 |
289 | 290 | 296 |
297 | ) 298 | } 299 | 300 | private handleDialogOnCancel = () => { 301 | // 关闭弹窗 302 | this.setState(showEditDialog(false)) 303 | 304 | // 重置表单 305 | const form = this.formRef.props.form 306 | form.resetFields() 307 | } 308 | } 309 | 310 | const showEditDialog = (isShow: boolean) => ({ 311 | isEditDialogShow: isShow 312 | }) 313 | -------------------------------------------------------------------------------- /src/pages/goods/goods/c/GoodsForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | Form, 4 | Upload, 5 | Icon, 6 | Modal, 7 | Input, 8 | Select, 9 | Switch, 10 | Button, 11 | InputNumber, 12 | Row, 13 | Col 14 | } from 'antd' 15 | import { FormComponentProps } from 'antd/lib/form' 16 | import axios from 'axios' 17 | import { getUploadUrl } from '../../../../api/constants' 18 | import Category from '../../../../class/Category' 19 | import { 20 | Spec, 21 | SPEC_TYPE_BASE, 22 | SPEC_TYPE_MODIFY 23 | } from '../../../../class/goodsTypes' 24 | 25 | import './GoodsForm.less' 26 | 27 | const FormItem = Form.Item 28 | const Option = Select.Option 29 | 30 | const TAG = 'GoodsForm' 31 | 32 | type PicturesWallProps = { 33 | onChange?: Function 34 | } 35 | 36 | // note 37 | // 自定义或第三方的表单控件,也可以与 Form 组件一起使用。只要该组件遵循以下的约定: 38 | // 提供受控属性 value 或其它与 valuePropName 的值同名的属性。 39 | // 提供 onChange 事件或 trigger 的值同名的事件。 40 | // 不能是函数式组件。 41 | export class PicturesWall extends React.Component { 42 | static getDerivedStateFromProps(nextProps) { 43 | // Should be a controlled component. 44 | if ('value' in nextProps) { 45 | return { 46 | fileList: nextProps.value 47 | } 48 | } 49 | return null 50 | } 51 | 52 | constructor(props) { 53 | super(props) 54 | 55 | const value = props.value || [] 56 | 57 | this.state = { 58 | fileList: value, 59 | previewVisible: false, 60 | previewImage: '' 61 | } 62 | } 63 | 64 | handleCancel = () => this.setState({ previewVisible: false }) 65 | 66 | handlePreview = file => { 67 | this.setState({ 68 | previewImage: file.url || file.thumbUrl, 69 | previewVisible: true 70 | }) 71 | } 72 | 73 | handleChange = ({ fileList }) => { 74 | console.log(fileList) 75 | this.setState({ fileList }) 76 | this.props.onChange( 77 | fileList.map(item => { 78 | if (item.originFileObj && item.originFileObj.response) { 79 | return { 80 | name: item.originFileObj.response.data.name, 81 | objectId: item.originFileObj.response.data.objectId, 82 | uid: item.originFileObj.response.data.objectId, 83 | url: item.originFileObj.response.data.url, 84 | status: 'done' 85 | } 86 | } 87 | return item 88 | }) 89 | ) 90 | } 91 | 92 | render() { 93 | const { previewVisible, previewImage, fileList } = this.state 94 | const uploadButton = ( 95 |
96 | 97 |
Upload
98 |
99 | ) 100 | return ( 101 |
102 | { 108 | let formData = new FormData() 109 | formData.append('file', file) 110 | axios 111 | .post(getUploadUrl(file.name), formData, { 112 | headers: { 113 | 'X-LC-Id': 'DpnvHL3ttpjzk5UvHnSEedNo-gzGzoHsz', 114 | 'X-LC-Key': 'vGLWcKIk9nh1udRwF44o1AsS' 115 | } 116 | }) 117 | .then(data => { 118 | file.response = data 119 | onSuccess(null, file) 120 | }) 121 | .catch(() => onError()) 122 | }} 123 | onChange={this.handleChange} 124 | > 125 | {fileList.length >= 3 ? null : uploadButton} 126 | 127 | 132 | example 133 | 134 |
135 | ) 136 | } 137 | } 138 | 139 | interface GoodsFormProps extends FormComponentProps { 140 | categoryList: Array 141 | visible: boolean 142 | onCancel(): void 143 | onOK(category: Category): void 144 | } 145 | 146 | const VALIDATE_MSG = '规格中存在空的名称、价格或者存在没有子规格,请检查' 147 | const initialState = { 148 | specValidateStatus: true 149 | } 150 | 151 | type GoodsFormState = Readonly 152 | 153 | const CollectionCreateForm = Form.create()( 154 | class GoodsForm extends Component { 155 | state: GoodsFormState = initialState 156 | 157 | remove = (k: string) => { 158 | const { form } = this.props 159 | let spec = form.getFieldValue('spec') as Array 160 | spec = spec.filter(item => item.objectId !== k) 161 | form.setFieldsValue({ 162 | spec 163 | }) 164 | } 165 | 166 | add = () => { 167 | const { form } = this.props 168 | form.getFieldDecorator(`spec`, { initialValue: [] }) 169 | const spec = form.getFieldValue('spec') as Array 170 | let id = 0 171 | while (this.getID(id.toString(), true)) { 172 | id++ 173 | } 174 | spec.push({ 175 | objectId: id.toString() 176 | }) 177 | form.setFieldsValue({ 178 | spec 179 | }) 180 | } 181 | 182 | getID = (id: string, isMain: boolean): boolean => { 183 | const { form } = this.props 184 | 185 | form.getFieldDecorator(`spec`, { initialValue: [] }) 186 | const spec = form.getFieldValue('spec') as Array 187 | 188 | let hasExisted = false 189 | spec.map(item => { 190 | if (isMain) { 191 | // 检查父规格 192 | if (item.objectId === id) { 193 | hasExisted = true 194 | } 195 | } else { 196 | // 检查子规格 197 | if (item.subSpecs) { 198 | item.subSpecs.map(subItem => { 199 | if (subItem.objectId === id) { 200 | hasExisted = true 201 | } 202 | }) 203 | } 204 | } 205 | }) 206 | return hasExisted 207 | } 208 | 209 | addSubSpec = (k: string) => { 210 | const { form } = this.props 211 | const spec = form.getFieldValue(`spec`) as Array 212 | const thisSpec = _getSpec(spec, k) 213 | const subSpecs = thisSpec.subSpecs ? thisSpec.subSpecs : [] 214 | 215 | let subID = 0 216 | while (this.getID(subID.toString(), false)) { 217 | subID++ 218 | } 219 | 220 | subSpecs.push({ 221 | objectId: subID.toString(), 222 | type: SPEC_TYPE_MODIFY 223 | }) 224 | 225 | let nextSpec = spec.map(item => { 226 | if (item.objectId === k) { 227 | return { 228 | ...item, 229 | subSpecs 230 | } 231 | } 232 | return item 233 | }) 234 | 235 | form.setFieldsValue({ 236 | spec: nextSpec 237 | }) 238 | } 239 | 240 | removeSubSpec = (k: string, sk: string) => { 241 | const { form } = this.props 242 | const spec = form.getFieldValue(`spec`) as Array 243 | const thisSpec = _getSpec(spec, k) 244 | 245 | if (!thisSpec || !thisSpec.subSpecs) { 246 | return 247 | } 248 | 249 | let nextSpecs = spec.map(item => { 250 | if (item.objectId === k) { 251 | item.subSpecs = item.subSpecs.filter(item => item.objectId !== sk) 252 | } 253 | return item 254 | }) 255 | 256 | form.setFieldsValue({ 257 | spec: nextSpecs 258 | }) 259 | } 260 | 261 | getSubSpec = (k: string) => { 262 | const { form } = this.props 263 | const { getFieldValue } = form 264 | 265 | let spec = getFieldValue(`spec`) as Array 266 | const thisSpec = _getSpec(spec, k) 267 | 268 | if (!thisSpec.subSpecs) { 269 | return null 270 | } 271 | const subKeys = thisSpec.subSpecs 272 | const subFormItems = subKeys.map(sk => ( 273 | 274 |
275 | { 278 | this.handleValueChanged('name', e.target.value, k, sk.objectId) 279 | }} 280 | onBlur={this.handleBlurChanged} 281 | value={sk.name} 282 | /> 283 | 284 | 285 | { 289 | this.handleValueChanged('price', e, k, sk.objectId) 290 | }} 291 | onBlur={this.handleBlurChanged} 292 | value={sk.price} 293 | /> 294 | 295 | 296 | { 300 | this.handleValueChanged( 301 | 'type', 302 | e ? SPEC_TYPE_BASE : SPEC_TYPE_MODIFY, 303 | k, 304 | sk.objectId 305 | ) 306 | }} 307 | checked={sk.type === SPEC_TYPE_BASE} 308 | /> 309 | this.removeSubSpec(k, sk.objectId)} 314 | /> 315 | 316 | 317 | )) 318 | return subFormItems 319 | } 320 | 321 | handleValueChanged = ( 322 | key: string, 323 | value: any, 324 | id: string, 325 | subID?: string 326 | ) => { 327 | const { form } = this.props 328 | const { getFieldValue } = form 329 | const spec = getFieldValue('spec') as Array 330 | const nextSpec = spec.map(item => { 331 | if (item.objectId === id) { 332 | if (!subID) { 333 | // 更新父规格 334 | item[key] = value 335 | } else { 336 | // 更新子规格 337 | item.subSpecs = item.subSpecs.map(subItem => { 338 | if (subItem.objectId === subID) { 339 | subItem[key] = value 340 | } 341 | return subItem 342 | }) 343 | } 344 | } 345 | return item 346 | }) 347 | 348 | form.setFieldsValue({ 349 | spec: nextSpec 350 | }) 351 | } 352 | 353 | handleBlurChanged = () => { 354 | const { form } = this.props 355 | const { getFieldValue } = form 356 | 357 | const spec = getFieldValue('spec') as Array 358 | 359 | let passed = true 360 | spec.map(item => { 361 | if (item.subSpecs && item.subSpecs.length > 0) { 362 | item.subSpecs.map(subItem => { 363 | if (!subItem.name || isNaN(subItem.price)) { 364 | passed = false 365 | } 366 | }) 367 | } else { 368 | passed = false 369 | } 370 | if (!item.name) { 371 | passed = false 372 | } 373 | return item 374 | }) 375 | 376 | this.setState({ 377 | specValidateStatus: passed 378 | } as GoodsFormState) 379 | 380 | return passed 381 | } 382 | 383 | render() { 384 | const { categoryList, visible, onCancel, onOK, form } = this.props 385 | const { getFieldDecorator, getFieldValue } = form 386 | 387 | getFieldDecorator('spec', { initialValue: [] }) 388 | const spec = getFieldValue('spec') as Array 389 | 390 | const formItems = spec.map(k => { 391 | const subFormItem = this.getSubSpec(k.objectId) 392 | return ( 393 | 400 | 401 | 402 | { 405 | this.handleValueChanged('name', e.target.value, k.objectId) 406 | }} 407 | onBlur={this.handleBlurChanged} 408 | value={k.name} 409 | /> 410 | 411 | 412 | this.remove(k.objectId)} 416 | /> 417 | 424 | 425 | 426 | {subFormItem} 427 | 428 | ) 429 | }) 430 | 431 | return ( 432 | { 436 | if (this.handleBlurChanged()) { 437 | let category = this._rebuildFormValues() 438 | onOK(category) 439 | } 440 | }} 441 | > 442 |
443 | 444 | {getFieldDecorator('images', { 445 | initialValue: [], 446 | rules: [ 447 | { 448 | required: true, 449 | message: '至少上传一张商品图片' 450 | } 451 | ] 452 | })()} 453 | 454 | 455 | {getFieldDecorator('title', { 456 | rules: [ 457 | { 458 | required: true, 459 | message: '名称为必填项目' 460 | } 461 | ] 462 | })()} 463 | 464 | {formItems.length === 0 ? ( 465 | 466 | {getFieldDecorator('price', { 467 | validateTrigger: ['onChange', 'onBlur'], 468 | rules: [ 469 | { 470 | required: true, 471 | whitespace: true, 472 | message: '请输入价格' 473 | } 474 | ] 475 | })( 476 | 480 | )} 481 | 482 | ) : null} 483 | 484 | {formItems} 485 | 486 | 487 | 490 | 491 | 492 | 493 | {getFieldDecorator('desc', { 494 | rules: [ 495 | { 496 | required: true, 497 | message: '描述为必填项目' 498 | } 499 | ] 500 | })()} 501 | 502 | 503 | 504 | {getFieldDecorator('category', { 505 | rules: [{ required: true, message: '选商品分类啊' }] 506 | })( 507 | 518 | )} 519 | 520 | 521 | {getFieldDecorator('display', { 522 | initialValue: true 523 | })()} 524 | 525 | 526 |
527 | ) 528 | } 529 | 530 | private _rebuildFormValues = (): Category => { 531 | const { categoryList, form } = this.props 532 | const { getFieldValue } = form 533 | let cid = getFieldValue('category') 534 | let result = categoryList.filter(item => cid === item.objectId) 535 | if (result.length > 0) { 536 | return result[0] 537 | } 538 | return null 539 | } 540 | } 541 | ) 542 | 543 | function _getSpec(array: Array, id: string): Spec { 544 | let result = array.filter(item => item.objectId === id) 545 | return result.length > 0 ? result[0] : null 546 | } 547 | 548 | export default CollectionCreateForm 549 | --------------------------------------------------------------------------------