diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..d326a47 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +node_modules +/dist +/www +/_dist +/_package +/mock diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..fcc70a1 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,13 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "rules": { + "@typescript-eslint/no-unused-vars": 0 + } +} diff --git a/config/setupProxy.js b/config/setupProxy.js new file mode 100644 index 0000000..50b4f76 --- /dev/null +++ b/config/setupProxy.js @@ -0,0 +1,17 @@ +const proxy = require('http-proxy-middleware'); + +module.exports = function (app) { + + //为了解决前端开发时跨域问题,配置后台接口代理。 + app.use(proxy( + '/api', + { + target: 'http://localhost:6020', + changeOrigin: true, + pathRewrite: { + '^/api': '/api' + } + } + )); + +}; diff --git a/package.json b/package.json index 28767e7..39e944b 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,13 @@ "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "@types/node": "^12.0.0", + "@types/qs": "^6.9.3", "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", "@typescript-eslint/eslint-plugin": "^2.10.0", "@typescript-eslint/parser": "^2.10.0", "antd": "^4.3.1", + "axios": "^0.19.2", "babel-eslint": "10.1.0", "babel-loader": "8.1.0", "babel-plugin-named-asset-import": "^0.3.6", @@ -44,10 +46,13 @@ "postcss-normalize": "8.0.1", "postcss-preset-env": "6.7.0", "postcss-safe-parser": "4.0.1", + "qs": "^6.9.4", "react": "^16.13.1", "react-app-polyfill": "^1.0.6", "react-dev-utils": "^10.2.1", "react-dom": "^16.13.1", + "react-router": "^5.2.0", + "react-router-dom": "^5.2.0", "resolve": "1.15.0", "resolve-url-loader": "3.1.1", "sass-loader": "8.0.2", @@ -86,5 +91,8 @@ "presets": [ "react-app" ] + }, + "devDependencies": { + "@types/react-router-dom": "^5.1.5" } } diff --git a/src/App.less b/src/App.less index bcf900a..79914d3 100644 --- a/src/App.less +++ b/src/App.less @@ -1,6 +1,4 @@ +//import antd style. @import '~antd/dist/antd.css'; - - -.good { - color: red; -} +//import global style. +@import './assets/css/index'; diff --git a/src/App.tsx b/src/App.tsx index 7d0307a..db2c4a4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,20 @@ import React from 'react'; import './App.less'; import {Button} from "antd"; +import {BrowserRouter as Router} from "react-router-dom"; +import {ConfigProvider} from 'antd'; +import Frame from "./pages/Frame"; +import zhCN from 'antd/lib/locale-provider/zh_CN'; + function App() { return ( -
+ + + + - - -
good
- -
+ ); } diff --git a/src/assets/css/bootstrap/variables.less b/src/assets/css/bootstrap/variables.less new file mode 100644 index 0000000..f7f9dff --- /dev/null +++ b/src/assets/css/bootstrap/variables.less @@ -0,0 +1,868 @@ +// +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +@gray-base: #000; +@gray-darker: lighten(@gray-base, 13.5%); // #222 +@gray-dark: lighten(@gray-base, 20%); // #333 +@gray: lighten(@gray-base, 33.5%); // #555 +@gray-light: lighten(@gray-base, 46.7%); // #777 +@gray-lighter: lighten(@gray-base, 93.5%); // #eee + +@brand-primary: darken(#428bca, 6.5%); // #337ab7 +@brand-success: #5cb85c; +@brand-info: #5bc0de; +@brand-warning: #f0ad4e; +@brand-danger: #d9534f; + + +//== Scaffolding +// +//## Settings for some of the most global styles. + +//** Background color for ``. +@body-bg: #fff; +//** Global text color on ``. +@text-color: @gray-dark; + +//** Global textual link color. +@link-color: @brand-primary; +//** Link hover color set via `darken()` function. +@link-hover-color: darken(@link-color, 15%); +//** Link hover decoration. +@link-hover-decoration: underline; + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; +@font-family-serif: Georgia, "Times New Roman", Times, serif; +//** Default monospace fonts for ``, ``, and `
`.
+@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
+@font-family-base:        @font-family-sans-serif;
+
+@font-size-base:          14px;
+@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
+@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
+
+@font-size-h1:            floor((@font-size-base * 2.6)); // ~36px
+@font-size-h2:            floor((@font-size-base * 2.15)); // ~30px
+@font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px
+@font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px
+@font-size-h5:            @font-size-base;
+@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
+
+//** Unit-less `line-height` for use in components like buttons.
+@line-height-base:        1.428571429; // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
+
+//** By default, this inherits from the ``.
+@headings-font-family:    inherit;
+@headings-font-weight:    500;
+@headings-line-height:    1.1;
+@headings-color:          inherit;
+
+
+//== Iconography
+//
+//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
+
+//** Load fonts from this directory.
+@icon-font-path:          "../fonts/";
+//** File name for all font files.
+@icon-font-name:          "glyphicons-halflings-regular";
+//** Element ID within SVG icon file.
+@icon-font-svg-id:        "glyphicons_halflingsregular";
+
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+@padding-base-vertical:     6px;
+@padding-base-horizontal:   12px;
+
+@padding-large-vertical:    10px;
+@padding-large-horizontal:  16px;
+
+@padding-small-vertical:    5px;
+@padding-small-horizontal:  10px;
+
+@padding-xs-vertical:       1px;
+@padding-xs-horizontal:     5px;
+
+@line-height-large:         1.3333333; // extra decimals for Win 8.1 Chrome
+@line-height-small:         1.5;
+
+@border-radius-base:        4px;
+@border-radius-large:       6px;
+@border-radius-small:       3px;
+
+//** Global color for active pendants (e.g., navs or dropdowns).
+@component-active-color:    #fff;
+//** Global background color for active pendants (e.g., navs or dropdowns).
+@component-active-bg:       @brand-primary;
+
+//** Width of the `border` for generating carets that indicate dropdowns.
+@caret-width-base:          4px;
+//** Carets increase slightly in size for larger components.
+@caret-width-large:         5px;
+
+
+//== Tables
+//
+//## Customizes the `.table` component with basic values, each used across all table variations.
+
+//** Padding for ``s and ``s.
+@table-cell-padding:            8px;
+//** Padding for cells in `.table-condensed`.
+@table-condensed-cell-padding:  5px;
+
+//** Default background color used for all tables.
+@table-bg:                      transparent;
+//** Background color used for `.table-striped`.
+@table-bg-accent:               #f9f9f9;
+//** Background color used for `.table-hover`.
+@table-bg-hover:                #f5f5f5;
+@table-bg-active:               @table-bg-hover;
+
+//** Border color for table and cell borders.
+@table-border-color:            #ddd;
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+@btn-font-weight:                normal;
+
+@btn-default-color:              #333;
+@btn-default-bg:                 #fff;
+@btn-default-border:             #ccc;
+
+@btn-primary-color:              #fff;
+@btn-primary-bg:                 @brand-primary;
+@btn-primary-border:             darken(@btn-primary-bg, 5%);
+
+@btn-success-color:              #fff;
+@btn-success-bg:                 @brand-success;
+@btn-success-border:             darken(@btn-success-bg, 5%);
+
+@btn-info-color:                 #fff;
+@btn-info-bg:                    @brand-info;
+@btn-info-border:                darken(@btn-info-bg, 5%);
+
+@btn-warning-color:              #fff;
+@btn-warning-bg:                 @brand-warning;
+@btn-warning-border:             darken(@btn-warning-bg, 5%);
+
+@btn-danger-color:               #fff;
+@btn-danger-bg:                  @brand-danger;
+@btn-danger-border:              darken(@btn-danger-bg, 5%);
+
+@btn-link-disabled-color:        @gray-light;
+
+// Allows for customizing button radius independently from global border radius
+@btn-border-radius-base:         @border-radius-base;
+@btn-border-radius-large:        @border-radius-large;
+@btn-border-radius-small:        @border-radius-small;
+
+
+//== Forms
+//
+//##
+
+//** `` background color
+@input-bg:                       #fff;
+//** `` background color
+@input-bg-disabled:              @gray-lighter;
+
+//** Text color for ``s
+@input-color:                    @gray;
+//** `` border color
+@input-border:                   #ccc;
+
+//** Default `.form-control` border radius
+// This has no effect on ``s in CSS.
+@input-border-radius:            @border-radius-base;
+//** Large `.form-control` border radius
+@input-border-radius-large:      @border-radius-large;
+//** Small `.form-control` border radius
+@input-border-radius-small:      @border-radius-small;
+
+//** Border color for inputs on focus
+@input-border-focus:             #66afe9;
+
+//** Placeholder text color
+@input-color-placeholder:        #999;
+
+//** Default `.form-control` height
+@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
+//** Large `.form-control` height
+@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
+//** Small `.form-control` height
+@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
+
+//** `.form-group` margin
+@form-group-margin-bottom:       15px;
+
+@legend-color:                   @gray-dark;
+@legend-border-color:            #e5e5e5;
+
+//** Background color for textual input addons
+@input-group-addon-bg:           @gray-lighter;
+//** Border color for textual input addons
+@input-group-addon-border-color: @input-border;
+
+//** Disabled cursor for form controls and buttons.
+@cursor-disabled:                not-allowed;
+
+
+//== Dropdowns
+//
+//## Dropdown menu container and contents.
+
+//** Background for the dropdown menu.
+@dropdown-bg:                    #fff;
+//** Dropdown menu `border-color`.
+@dropdown-border:                rgba(0,0,0,.15);
+//** Dropdown menu `border-color` **for IE8**.
+@dropdown-fallback-border:       #ccc;
+//** Divider color for between dropdown pendants.
+@dropdown-divider-bg:            #e5e5e5;
+
+//** Dropdown link text color.
+@dropdown-link-color:            @gray-dark;
+//** Hover color for dropdown links.
+@dropdown-link-hover-color:      darken(@gray-dark, 5%);
+//** Hover background for dropdown links.
+@dropdown-link-hover-bg:         #f5f5f5;
+
+//** Active dropdown menu item text color.
+@dropdown-link-active-color:     @component-active-color;
+//** Active dropdown menu item background color.
+@dropdown-link-active-bg:        @component-active-bg;
+
+//** Disabled dropdown menu item background color.
+@dropdown-link-disabled-color:   @gray-light;
+
+//** Text color for headers within dropdown menus.
+@dropdown-header-color:          @gray-light;
+
+//** Deprecated `@dropdown-caret-color` as of v3.1.0
+@dropdown-caret-color:           #000;
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+@zindex-navbar:            1000;
+@zindex-dropdown:          1000;
+@zindex-popover:           1060;
+@zindex-tooltip:           1070;
+@zindex-navbar-fixed:      1030;
+@zindex-modal-background:  1040;
+@zindex-modal:             1050;
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+//** Deprecated `@screen-xs` as of v3.0.1
+@screen-xs:                  480px;
+//** Deprecated `@screen-xs-min` as of v3.2.0
+@screen-xs-min:              @screen-xs;
+//** Deprecated `@screen-phone` as of v3.0.1
+@screen-phone:               @screen-xs-min;
+
+// Small screen / tablet
+//** Deprecated `@screen-sm` as of v3.0.1
+@screen-sm:                  768px;
+@screen-sm-min:              @screen-sm;
+//** Deprecated `@screen-tablet` as of v3.0.1
+@screen-tablet:              @screen-sm-min;
+
+// Medium screen / desktop
+//** Deprecated `@screen-md` as of v3.0.1
+@screen-md:                  992px;
+@screen-md-min:              @screen-md;
+//** Deprecated `@screen-desktop` as of v3.0.1
+@screen-desktop:             @screen-md-min;
+
+// Large screen / wide desktop
+//** Deprecated `@screen-lg` as of v3.0.1
+@screen-lg:                  1200px;
+@screen-lg-min:              @screen-lg;
+//** Deprecated `@screen-lg-desktop` as of v3.0.1
+@screen-lg-desktop:          @screen-lg-min;
+
+// So media queries don't overlap when required, provide a maximum
+@screen-xs-max:              (@screen-sm-min - 1);
+@screen-sm-max:              (@screen-md-min - 1);
+@screen-md-max:              (@screen-lg-min - 1);
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+@grid-columns:              12;
+//** Padding between columns. Gets divided in half for the left and right.
+@grid-gutter-width:         30px;
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+@grid-float-breakpoint:     @screen-sm-min;
+//** Point at which the navbar begins collapsing.
+@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
+
+
+//== Container sizes
+//
+//## Define the maximum width of `.container` for different screen sizes.
+
+// Small screen / tablet
+@container-tablet:             (720px + @grid-gutter-width);
+//** For `@screen-sm-min` and up.
+@container-sm:                 @container-tablet;
+
+// Medium screen / desktop
+@container-desktop:            (940px + @grid-gutter-width);
+//** For `@screen-md-min` and up.
+@container-md:                 @container-desktop;
+
+// Large screen / wide desktop
+@container-large-desktop:      (1140px + @grid-gutter-width);
+//** For `@screen-lg-min` and up.
+@container-lg:                 @container-large-desktop;
+
+
+//== Navbar
+//
+//##
+
+// Basics of a navbar
+@navbar-height:                    50px;
+@navbar-margin-bottom:             @line-height-computed;
+@navbar-border-radius:             @border-radius-base;
+@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
+@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
+@navbar-collapse-max-height:       340px;
+
+@navbar-default-color:             #777;
+@navbar-default-bg:                #f8f8f8;
+@navbar-default-border:            darken(@navbar-default-bg, 6.5%);
+
+// Navbar links
+@navbar-default-link-color:                #777;
+@navbar-default-link-hover-color:          #333;
+@navbar-default-link-hover-bg:             transparent;
+@navbar-default-link-active-color:         #555;
+@navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);
+@navbar-default-link-disabled-color:       #ccc;
+@navbar-default-link-disabled-bg:          transparent;
+
+// Navbar brand label
+@navbar-default-brand-color:               @navbar-default-link-color;
+@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);
+@navbar-default-brand-hover-bg:            transparent;
+
+// Navbar toggle
+@navbar-default-toggle-hover-bg:           #ddd;
+@navbar-default-toggle-icon-bar-bg:        #888;
+@navbar-default-toggle-border-color:       #ddd;
+
+
+//=== Inverted navbar
+// Reset inverted navbar basics
+@navbar-inverse-color:                      lighten(@gray-light, 15%);
+@navbar-inverse-bg:                         #222;
+@navbar-inverse-border:                     darken(@navbar-inverse-bg, 10%);
+
+// Inverted navbar links
+@navbar-inverse-link-color:                 lighten(@gray-light, 15%);
+@navbar-inverse-link-hover-color:           #fff;
+@navbar-inverse-link-hover-bg:              transparent;
+@navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;
+@navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 10%);
+@navbar-inverse-link-disabled-color:        #444;
+@navbar-inverse-link-disabled-bg:           transparent;
+
+// Inverted navbar brand label
+@navbar-inverse-brand-color:                @navbar-inverse-link-color;
+@navbar-inverse-brand-hover-color:          #fff;
+@navbar-inverse-brand-hover-bg:             transparent;
+
+// Inverted navbar toggle
+@navbar-inverse-toggle-hover-bg:            #333;
+@navbar-inverse-toggle-icon-bar-bg:         #fff;
+@navbar-inverse-toggle-border-color:        #333;
+
+
+//== Navs
+//
+//##
+
+//=== Shared nav styles
+@nav-link-padding:                          10px 15px;
+@nav-link-hover-bg:                         @gray-lighter;
+
+@nav-disabled-link-color:                   @gray-light;
+@nav-disabled-link-hover-color:             @gray-light;
+
+//== Tabs
+@nav-tabs-border-color:                     #ddd;
+
+@nav-tabs-link-hover-border-color:          @gray-lighter;
+
+@nav-tabs-active-link-hover-bg:             @body-bg;
+@nav-tabs-active-link-hover-color:          @gray;
+@nav-tabs-active-link-hover-border-color:   #ddd;
+
+@nav-tabs-justified-link-border-color:            #ddd;
+@nav-tabs-justified-active-link-border-color:     @body-bg;
+
+//== Pills
+@nav-pills-border-radius:                   @border-radius-base;
+@nav-pills-active-link-hover-bg:            @component-active-bg;
+@nav-pills-active-link-hover-color:         @component-active-color;
+
+
+//== Pagination
+//
+//##
+
+@pagination-color:                     @link-color;
+@pagination-bg:                        #fff;
+@pagination-border:                    #ddd;
+
+@pagination-hover-color:               @link-hover-color;
+@pagination-hover-bg:                  @gray-lighter;
+@pagination-hover-border:              #ddd;
+
+@pagination-active-color:              #fff;
+@pagination-active-bg:                 @brand-primary;
+@pagination-active-border:             @brand-primary;
+
+@pagination-disabled-color:            @gray-light;
+@pagination-disabled-bg:               #fff;
+@pagination-disabled-border:           #ddd;
+
+
+//== Pager
+//
+//##
+
+@pager-bg:                             @pagination-bg;
+@pager-border:                         @pagination-border;
+@pager-border-radius:                  15px;
+
+@pager-hover-bg:                       @pagination-hover-bg;
+
+@pager-active-bg:                      @pagination-active-bg;
+@pager-active-color:                   @pagination-active-color;
+
+@pager-disabled-color:                 @pagination-disabled-color;
+
+
+//== Jumbotron
+//
+//##
+
+@jumbotron-padding:              30px;
+@jumbotron-color:                inherit;
+@jumbotron-bg:                   @gray-lighter;
+@jumbotron-heading-color:        inherit;
+@jumbotron-font-size:            ceil((@font-size-base * 1.5));
+@jumbotron-heading-font-size:    ceil((@font-size-base * 4.5));
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+@state-success-text:             #3c763d;
+@state-success-bg:               #dff0d8;
+@state-success-border:           darken(spin(@state-success-bg, -10), 5%);
+
+@state-info-text:                #31708f;
+@state-info-bg:                  #d9edf7;
+@state-info-border:              darken(spin(@state-info-bg, -10), 7%);
+
+@state-warning-text:             #8a6d3b;
+@state-warning-bg:               #fcf8e3;
+@state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);
+
+@state-danger-text:              #a94442;
+@state-danger-bg:                #f2dede;
+@state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);
+
+
+//== Tooltips
+//
+//##
+
+//** Tooltip max width
+@tooltip-max-width:           200px;
+//** Tooltip text color
+@tooltip-color:               #fff;
+//** Tooltip background color
+@tooltip-bg:                  #000;
+@tooltip-opacity:             .9;
+
+//** Tooltip arrow width
+@tooltip-arrow-width:         5px;
+//** Tooltip arrow color
+@tooltip-arrow-color:         @tooltip-bg;
+
+
+//== Popovers
+//
+//##
+
+//** Popover body background color
+@popover-bg:                          #fff;
+//** Popover maximum width
+@popover-max-width:                   276px;
+//** Popover border color
+@popover-border-color:                rgba(0,0,0,.2);
+//** Popover fallback border color
+@popover-fallback-border-color:       #ccc;
+
+//** Popover title background color
+@popover-title-bg:                    darken(@popover-bg, 3%);
+
+//** Popover arrow width
+@popover-arrow-width:                 10px;
+//** Popover arrow color
+@popover-arrow-color:                 @popover-bg;
+
+//** Popover outer arrow width
+@popover-arrow-outer-width:           (@popover-arrow-width + 1);
+//** Popover outer arrow color
+@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);
+//** Popover outer arrow fallback color
+@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+@label-default-bg:            @gray-light;
+//** Primary label background color
+@label-primary-bg:            @brand-primary;
+//** Success label background color
+@label-success-bg:            @brand-success;
+//** Info label background color
+@label-info-bg:               @brand-info;
+//** Warning label background color
+@label-warning-bg:            @brand-warning;
+//** Danger label background color
+@label-danger-bg:             @brand-danger;
+
+//** Default label text color
+@label-color:                 #fff;
+//** Default text color of a linked label
+@label-link-hover-color:      #fff;
+
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+@modal-inner-padding:         15px;
+
+//** Padding applied to the modal title
+@modal-title-padding:         15px;
+//** Modal title line-height
+@modal-title-line-height:     @line-height-base;
+
+//** Background color of modal content area
+@modal-content-bg:                             #fff;
+//** Modal content border color
+@modal-content-border-color:                   rgba(0,0,0,.2);
+//** Modal content border color **for IE8**
+@modal-content-fallback-border-color:          #999;
+
+//** Modal backdrop background color
+@modal-backdrop-bg:           #000;
+//** Modal backdrop opacity
+@modal-backdrop-opacity:      .5;
+//** Modal header border color
+@modal-header-border-color:   #e5e5e5;
+//** Modal footer border color
+@modal-footer-border-color:   @modal-header-border-color;
+
+@modal-lg:                    900px;
+@modal-md:                    600px;
+@modal-sm:                    300px;
+
+
+//== Alerts
+//
+//## Define alert colors, border radius, and padding.
+
+@alert-padding:               15px;
+@alert-border-radius:         @border-radius-base;
+@alert-link-font-weight:      bold;
+
+@alert-success-bg:            @state-success-bg;
+@alert-success-text:          @state-success-text;
+@alert-success-border:        @state-success-border;
+
+@alert-info-bg:               @state-info-bg;
+@alert-info-text:             @state-info-text;
+@alert-info-border:           @state-info-border;
+
+@alert-warning-bg:            @state-warning-bg;
+@alert-warning-text:          @state-warning-text;
+@alert-warning-border:        @state-warning-border;
+
+@alert-danger-bg:             @state-danger-bg;
+@alert-danger-text:           @state-danger-text;
+@alert-danger-border:         @state-danger-border;
+
+
+//== Progress bars
+//
+//##
+
+//** Background color of the whole progress component
+@progress-bg:                 #f5f5f5;
+//** Progress bar text color
+@progress-bar-color:          #fff;
+//** Variable for setting rounded corners on progress bar.
+@progress-border-radius:      @border-radius-base;
+
+//** Default progress bar color
+@progress-bar-bg:             @brand-primary;
+//** Success progress bar color
+@progress-bar-success-bg:     @brand-success;
+//** Warning progress bar color
+@progress-bar-warning-bg:     @brand-warning;
+//** Danger progress bar color
+@progress-bar-danger-bg:      @brand-danger;
+//** Info progress bar color
+@progress-bar-info-bg:        @brand-info;
+
+
+//== List group
+//
+//##
+
+//** Background color on `.list-group-item`
+@list-group-bg:                 #fff;
+//** `.list-group-item` border color
+@list-group-border:             #ddd;
+//** List group border radius
+@list-group-border-radius:      @border-radius-base;
+
+//** Background color of single list pendants on hover
+@list-group-hover-bg:           #f5f5f5;
+//** Text color of active list pendants
+@list-group-active-color:       @component-active-color;
+//** Background color of active list pendants
+@list-group-active-bg:          @component-active-bg;
+//** Border color of active list elements
+@list-group-active-border:      @list-group-active-bg;
+//** Text color for content within active list pendants
+@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
+
+//** Text color of disabled list pendants
+@list-group-disabled-color:      @gray-light;
+//** Background color of disabled list pendants
+@list-group-disabled-bg:         @gray-lighter;
+//** Text color for content within disabled list pendants
+@list-group-disabled-text-color: @list-group-disabled-color;
+
+@list-group-link-color:         #555;
+@list-group-link-hover-color:   @list-group-link-color;
+@list-group-link-heading-color: #333;
+
+
+//== Panels
+//
+//##
+
+@panel-bg:                    #fff;
+@panel-body-padding:          15px;
+@panel-heading-padding:       10px 15px;
+@panel-footer-padding:        @panel-heading-padding;
+@panel-border-radius:         @border-radius-base;
+
+//** Border color for elements within panels
+@panel-inner-border:          #ddd;
+@panel-footer-bg:             #f5f5f5;
+
+@panel-default-text:          @gray-dark;
+@panel-default-border:        #ddd;
+@panel-default-heading-bg:    #f5f5f5;
+
+@panel-primary-text:          #fff;
+@panel-primary-border:        @brand-primary;
+@panel-primary-heading-bg:    @brand-primary;
+
+@panel-success-text:          @state-success-text;
+@panel-success-border:        @state-success-border;
+@panel-success-heading-bg:    @state-success-bg;
+
+@panel-info-text:             @state-info-text;
+@panel-info-border:           @state-info-border;
+@panel-info-heading-bg:       @state-info-bg;
+
+@panel-warning-text:          @state-warning-text;
+@panel-warning-border:        @state-warning-border;
+@panel-warning-heading-bg:    @state-warning-bg;
+
+@panel-danger-text:           @state-danger-text;
+@panel-danger-border:         @state-danger-border;
+@panel-danger-heading-bg:     @state-danger-bg;
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+@thumbnail-padding:           4px;
+//** Thumbnail background color
+@thumbnail-bg:                @body-bg;
+//** Thumbnail border color
+@thumbnail-border:            #ddd;
+//** Thumbnail border radius
+@thumbnail-border-radius:     @border-radius-base;
+
+//** Custom text color for thumbnail captions
+@thumbnail-caption-color:     @text-color;
+//** Padding around the thumbnail caption
+@thumbnail-caption-padding:   9px;
+
+
+//== Wells
+//
+//##
+
+@well-bg:                     #f5f5f5;
+@well-border:                 darken(@well-bg, 7%);
+
+
+//== Badges
+//
+//##
+
+@badge-color:                 #fff;
+//** Linked badge text color on hover
+@badge-link-hover-color:      #fff;
+@badge-bg:                    @gray-light;
+
+//** Badge text color in active nav link
+@badge-active-color:          @link-color;
+//** Badge background color in active nav link
+@badge-active-bg:             #fff;
+
+@badge-font-weight:           bold;
+@badge-line-height:           1;
+@badge-border-radius:         10px;
+
+
+//== Breadcrumbs
+//
+//##
+
+@breadcrumb-padding-vertical:   8px;
+@breadcrumb-padding-horizontal: 15px;
+//** Breadcrumb background color
+@breadcrumb-bg:                 #f5f5f5;
+//** Breadcrumb text color
+@breadcrumb-color:              #ccc;
+//** Text color of current page in the breadcrumb
+@breadcrumb-active-color:       @gray-light;
+//** Textual separator for between breadcrumb elements
+@breadcrumb-separator:          "/";
+
+
+//== Carousel
+//
+//##
+
+@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
+
+@carousel-control-color:                      #fff;
+@carousel-control-width:                      15%;
+@carousel-control-opacity:                    .5;
+@carousel-control-font-size:                  20px;
+
+@carousel-indicator-active-bg:                #fff;
+@carousel-indicator-border-color:             #fff;
+
+@carousel-caption-color:                      #fff;
+
+
+//== Close
+//
+//##
+
+@close-font-weight:           bold;
+@close-color:                 #000;
+@close-text-shadow:           0 1px 0 #fff;
+
+
+//== Code
+//
+//##
+
+@code-color:                  #c7254e;
+@code-bg:                     #f9f2f4;
+
+@kbd-color:                   #fff;
+@kbd-bg:                      #333;
+
+@pre-bg:                      #f5f5f5;
+@pre-color:                   @gray-dark;
+@pre-border-color:            #ccc;
+@pre-scrollable-max-height:   340px;
+
+
+//== Type
+//
+//##
+
+//** Horizontal offset for forms and lists.
+@component-offset-horizontal: 180px;
+//** Text muted color
+@text-muted:                  @gray-light;
+//** Abbreviations and acronyms border color
+@abbr-border-color:           @gray-light;
+//** Headings small color
+@headings-small-color:        @gray-light;
+//** Blockquote small color
+@blockquote-small-color:      @gray-light;
+//** Blockquote font size
+@blockquote-font-size:        (@font-size-base * 1.25);
+//** Blockquote border color
+@blockquote-border-color:     @gray-lighter;
+//** Page header border color
+@page-header-border-color:    @gray-lighter;
+//** Width of horizontal description list titles
+@dl-horizontal-offset:        @component-offset-horizontal;
+//** Point at which .dl-horizontal becomes horizontal
+@dl-horizontal-breakpoint:    @grid-float-breakpoint;
+//** Horizontal line color.
+@hr-border:                   @gray-lighter;
diff --git a/src/index.css b/src/assets/css/global/animation.less
similarity index 100%
rename from src/index.css
rename to src/assets/css/global/animation.less
diff --git a/src/assets/css/global/border.less b/src/assets/css/global/border.less
new file mode 100644
index 0000000..81c3cc2
--- /dev/null
+++ b/src/assets/css/global/border.less
@@ -0,0 +1,41 @@
+@import "variables";
+
+#radius {
+  .border-radius(@from,@end,@step) {
+    .mX(@f,@e,@s) when (@e >= @f) {
+      .border-radius-@{e} {
+        -webkit-border-radius: @e*1px;
+        -moz-border-radius: @e*1px;
+        border-radius: @e*1px;
+      }
+      .br@{e} {
+        -webkit-border-radius: @e*1px;
+        -moz-border-radius: @e*1px;
+        border-radius: @e*1px;
+      }
+
+      .mX(@f, @e - @s, @s);
+    }
+    .mX(@from, @end, @step);
+  }
+
+}
+
+#radius > .border-radius(1, 10, 1);
+
+.border-dash {
+  border: 1px dashed #ccc;
+}
+
+.border {
+  border: 1px solid #eee;
+}
+
+.border-danger {
+  border: 1px solid @brand-danger;
+}
+
+.border-bottom {
+  border-bottom: 1px solid #F9F9F9;
+}
+
diff --git a/src/assets/css/global/button.less b/src/assets/css/global/button.less
new file mode 100644
index 0000000..344cd7e
--- /dev/null
+++ b/src/assets/css/global/button.less
@@ -0,0 +1,37 @@
+@import "variables";
+
+.btn-action {
+    margin: 0 3px;
+    display: inline-block;
+    opacity: 0.85;
+    -webkit-transition: all 0.1s;
+    -o-transition: all 0.1s;
+    transition: all 0.1s;
+    cursor: pointer;
+    outline: none;
+
+    &:hover {
+        text-decoration: none;
+        opacity: 1;
+        -moz-transform: scale(1.2);
+        -webkit-transform: scale(1.2);
+        -o-transform: scale(1.2);
+        -ms-transform: scale(1.2);
+        transform: scale(1.2);
+    }
+
+    &:focus {
+        outline: none;
+    }
+}
+
+//编辑区域按钮,鼠标一上去按钮变大
+.action-buttons {
+    a {
+        .btn-action;
+    }
+}
+
+.cursor {
+    cursor: pointer;
+}
\ No newline at end of file
diff --git a/src/assets/css/global/color.less b/src/assets/css/global/color.less
new file mode 100644
index 0000000..eb1c53d
--- /dev/null
+++ b/src/assets/css/global/color.less
@@ -0,0 +1,112 @@
+@import "./variables.less";
+
+.bg-primary{ background-color: @brand-primary; color: white;}
+.bg-success{ background-color: @brand-success; color: white; }
+.bg-info{ background-color: @brand-info; color: white; }
+.bg-warning{ background-color: @brand-warning; color: white; }
+.bg-danger{ background-color: @brand-danger; color: white; }
+.bg-gray{ background-color: @brand-gray; color: white; }
+.bg-laxative{ background-color: @brand-laxative; color: white; }
+
+
+.text-primary{ color: @brand-primary;}
+.text-success{ color: @brand-success; }
+.text-info{ color: @brand-info; }
+.text-warning{ color: @brand-warning; }
+.text-danger{ color: @brand-danger; }
+.text-gray{ color: @brand-gray; }
+.text-laxative{ color: @brand-laxative; }
+
+.bg-navy {  background-color: #001F3F;}
+.bg-blue {  background-color: #0074D9;}
+.bg-aqua {  background-color: #7FDBFF;}
+.bg-aliceblue {  background-color: aliceblue;}
+.bg-pink {  background-color: pink;}
+.bg-azure {  background-color: azure;}
+.bg-teal {  background-color: #39CCCC;}
+.bg-olive {  background-color: #3D9970;}
+.bg-green {  background-color: #2ECC40;}
+.bg-lime {  background-color: #01FF70;}
+.bg-yellow {  background-color: #FFDC00;}
+.bg-pink { color: pink; }
+.bg-orange {  background-color: #FF851B;}
+.bg-red {  background-color: #FF4136;}
+.bg-fuchsia {  background-color: #F012BE;}
+.bg-purple {  background-color: #B10DC9;}
+.bg-maroon {  background-color: #85144B;}
+.bg-white {  background-color: #FFFFFF;}
+.bg-gray {  background-color: #AAAAAA;}
+.bg-silver {  background-color: #DDDDDD;}
+.bg-silver-white {
+  background-color: #EEEEEE;
+}
+.bg-black {  background-color: #111111;}
+
+.bg-111{ background-color:#111 }
+.bg-222{ background-color:#222 }
+.bg-333{ background-color:#333 }
+.bg-444{ background-color:#444 }
+.bg-555{ background-color:#555 }
+.bg-666{ background-color:#666 }
+.bg-777{ background-color:#777 }
+.bg-888{ background-color:#888 }
+.bg-999{ background-color:#999 }
+.bg-aaa{ background-color:#aaa }
+.bg-bbb{ background-color:#bbb }
+.bg-ccc{ background-color:#ccc }
+.bg-ddd{ background-color:#ddd }
+.bg-eee{ background-color:#eee }
+
+
+
+/* Colors */
+.navy {  color: #001F3F;}
+.blue {  color: #0074D9;}
+.aqua {  color: #7FDBFF;}
+.teal {  color: #39CCCC;}
+.olive {  color: #3D9970;}
+.green {  color: #2ECC40;}
+.lime {  color: #01FF70;}
+.yellow {  color: #FFDC00;}
+.pink { color: pink; }
+.orange {  color: #FF851B;}
+.red {  color: #FF4136;}
+.fuchsia {  color: #F012BE;}
+.purple {  color: #B10DC9;}
+.maroon {  color: #85144B;}
+.white {  color: #FFFFFF;}
+.silver {  color: #DDDDDD;}
+.gray {  color: #AAAAAA;}
+.black {  color: #111111;}
+
+
+.color-111{color:#111}
+.color-222{color:#222}
+.color-333{color:#333}
+.color-444{color:#444}
+.color-555{color:#555}
+.color-666{color:#666}
+.color-777{color:#777}
+.color-888{color:#888}
+.color-999{color:#999}
+.color-aaa{color:#aaa}
+.color-bbb{color:#bbb}
+.color-ccc{color:#ccc}
+.color-ddd{color:#ddd}
+.color-eee{color:#eee}
+
+
+.color-text {  color: #660E7A;}
+.color-doc {  color: #295496;}
+.color-xls {  color: #1E6C41;}
+.color-ppt {  color: #D04324;}
+.color-pdf {  color: #E40B0B;}
+.color-audio {  color: #5bc0de;}
+.color-video {  color: #5cb85c;}
+.color-image {  color: #0074D9;}
+.color-archive {  color: #4437f2;}
+
+.color-light-active{ color: #ffc60c;}
+.color-light-inactive{ color:#ccc;}
+
+
diff --git a/src/assets/css/global/font.less b/src/assets/css/global/font.less
new file mode 100644
index 0000000..114ce52
--- /dev/null
+++ b/src/assets/css/global/font.less
@@ -0,0 +1,38 @@
+#font{
+  .f(@from,@end,@step){
+    .mX(@f,@e,@s) when (@e >= @f){
+      .f@{e}{
+        font-size:@e*1px!important;
+      }
+      .mX(@f,@e - @s,@s);
+    }
+    .mX(@from,@end,@step);
+  }
+
+}
+
+#font > .f(10,80,1);
+
+#font{
+  .ln(@from,@end,@step){
+    .mX(@f,@e,@s) when (@e >= @f){
+      .ln@{e}{
+        line-height:@e*1px!important;
+      }
+      .mX(@f,@e - @s,@s);
+    }
+    .mX(@from,@end,@step);
+  }
+
+}
+
+#font > .ln(10,100,1);
+
+
+
+.bold{
+  font-weight:bold;
+}
+.italic{
+  font-style:italic;
+}
diff --git a/src/assets/css/global/layout.less b/src/assets/css/global/layout.less
new file mode 100644
index 0000000..83dafed
--- /dev/null
+++ b/src/assets/css/global/layout.less
@@ -0,0 +1,143 @@
+/**
+全局常用样式定义
+使用ml、pt等缩写表示常用的布局方式,数字表示像素值
+比如ml10表示margin-left:10px;
+ */
+#layout {
+  .margin(@from,@end,@step) {
+    .mX(@f,@e,@s) when (@e >= @f) {
+      .m@{e} {
+        margin: @e*1px;
+      }
+      .mt@{e} {
+        margin-top: @e*1px;
+      }
+      .mr@{e} {
+        margin-right: @e*1px;
+      }
+      .mb@{e} {
+        margin-bottom: @e*1px;
+      }
+      .ml@{e} {
+        margin-left: @e*1px;
+      }
+      .mv@{e} {
+        margin-top: @e*1px;
+        margin-bottom: @e*1px;
+      }
+      .mh@{e} {
+        margin-left: @e*1px;
+        margin-right: @e*1px*1px;
+      }
+      .mX(@f, @e - @s, @s);
+    }
+    .mX(@from, @end, @step);
+  }
+  .padding(@from,@end,@step) {
+    .pX(@f,@e,@s) when (@e >= @f) {
+      .p@{e} {
+        padding: @e*1px;
+      }
+      .pt@{e} {
+        padding-top: @e*1px;
+      }
+      .pr@{e} {
+        padding-right: @e*1px;
+      }
+      .pb@{e} {
+        padding-bottom: @e*1px;
+      }
+      .pl@{e} {
+        padding-left: @e*1px;
+      }
+      .pv@{e} {
+        padding-top: @e*1px;
+        padding-bottom: @e*1px;
+      }
+      .ph@{e} {
+        padding-left: @e*1px;
+        padding-right: @e*1px;
+      }
+      .pX(@f, @e - @s, @s);
+    }
+    .pX(@from, @end, @step);
+  }
+  .width(@from,@end,@step) {
+    .wX(@f,@e,@s) when (@e >= @f) {
+      .w@{e} {
+        width: @e*1px;
+      }
+      .w@{e}m {
+        max-width: @e*1px;
+      }
+      .w@{e}n {
+        min-width: @e*1px;
+      }
+      .w@{e}i {
+        width: @e*1px !important;
+      }
+      .wX(@f, @e - @s, @s);
+    }
+    .wX(@from, @end, @step);
+  }
+  .height(@from,@end,@step) {
+    .hX(@f,@e,@s) when (@e >= @f) {
+      .h@{e} {
+        height: @e*1px;
+      }
+      .lh@{e} {
+        line-height: @e*1px;
+      }
+      .h@{e}m {
+        max-height: @e*1px;
+      }
+      .h@{e}n {
+        min-height: @e*1px;
+      }
+      .h@{e}i {
+        height: @e*1px !important;
+      }
+      .hX(@f, @e - @s, @s);
+    }
+    .hX(@from, @end, @step);
+  }
+}
+
+.wp20 {
+  width: 20%;
+}
+
+.wp25 {
+  width: 25%;
+}
+
+.wp33 {
+  width: 33%;
+}
+
+.wp100 {
+  width: 100%;
+}
+
+.wp50 {
+  width: 50%;
+}
+
+.hp100 {
+  height: 100%;
+}
+
+.hp50 {
+  height: 50%;
+}
+
+#layout > .margin(20, 200, 5);
+#layout > .margin(0, 19, 1);
+#layout > .padding(20, 200, 5);
+#layout > .padding(0, 19, 1);
+#layout > .width(20, 400, 5);
+#layout > .width(0, 19, 1);
+#layout > .height(20, 400, 5);
+#layout > .height(0, 19, 1);
+
+
diff --git a/src/assets/css/global/miscellaneous.less b/src/assets/css/global/miscellaneous.less
new file mode 100644
index 0000000..7022c67
--- /dev/null
+++ b/src/assets/css/global/miscellaneous.less
@@ -0,0 +1,108 @@
+@import "variables";
+
+.compulsory {
+  &:before {
+    content: "*";
+    color: red;
+  }
+}
+
+.limit-hints {
+  font-size: 10px;
+  color: #aaaaaa;
+  float: right;
+}
+
+.hover-underline {
+
+  cursor: pointer;
+
+  &:hover {
+    text-decoration: underline;
+  }
+
+}
+
+
+
+//超出三行使用点点点
+.list-text-restriction {
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  position: relative;
+  line-height: 17px;
+  max-height: 51px;
+  -webkit-line-clamp: 3;
+  text-overflow: ellipsis;
+}
+
+.one-line {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.inline-block {
+  display: inline-block;
+}
+
+.overflow-hidden {
+  overflow: hidden;
+}
+
+//讨厌的outline
+a {
+  outline: none;
+
+  &:focus, &:hover, &:active {
+    outline: none;
+  }
+}
+
+//制造一个手机容器
+.mobile-container {
+  max-width: 400px;
+  border: 1px solid #eee;
+  padding: 10px;
+  margin: 10px auto;
+  display: block;
+  background-color: white;
+}
+
+
+//按照字母来wrap,不然有些老长老长
+.wrap-word {
+  word-wrap: break-word;
+}
+
+.text-left {
+  text-align: left;
+}
+
+.text-center {
+  text-align: center;
+}
+
+.text-right {
+  text-align: right;
+}
+
+
+.display-none {
+  display: none !important;
+}
+
+
+
+
+//超链接导航样式
+.link {
+  color: #1890ff;
+  text-decoration: none;
+  background-color: transparent;
+  outline: none;
+  cursor: pointer;
+  transition: color 0.3s;
+  text-decoration-skip: objects;
+}
diff --git a/src/assets/css/global/mixin.less b/src/assets/css/global/mixin.less
new file mode 100644
index 0000000..d78b3fb
--- /dev/null
+++ b/src/assets/css/global/mixin.less
@@ -0,0 +1,11 @@
+//超出三行使用点点点
+.line-ellipsis(@rows,@line-height) {
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  position: relative;
+  line-height: @line-height;
+  max-height: @line-height * @rows;
+  -webkit-line-clamp: @rows;
+  text-overflow: ellipsis;
+}
diff --git a/src/assets/css/global/responsive.less b/src/assets/css/global/responsive.less
new file mode 100644
index 0000000..83685f9
--- /dev/null
+++ b/src/assets/css/global/responsive.less
@@ -0,0 +1,43 @@
+@media (min-width: 992px) {
+  .visible-mobile {
+    display: none !important;
+  }
+
+  .visible-pc {
+    display: block !important;
+  }
+}
+
+@media (max-width: 992px) {
+  .visible-mobile {
+    display: block !important;
+  }
+
+  .visible-pc {
+    display: none !important;
+  }
+}
+
+//浏览器滚动条样式
+
+//滚动条样式
+::-webkit-scrollbar {
+  width: 10px;
+  height: 10px
+}
+
+::-webkit-scrollbar-thumb {
+  transition: all .3s ease;
+  border-color: transparent;
+  background-color: rgba(0, 0, 0, .1);
+  z-index: 40
+}
+
+::-webkit-scrollbar-thumb:hover {
+  transition: all .3s ease;
+  background-color: rgba(0, 0, 0, .15)
+}
+
+::-webkit-scrollbar-corner {
+  background-color: #eaeaeb
+}
diff --git a/src/assets/css/global/variables.less b/src/assets/css/global/variables.less
new file mode 100644
index 0000000..628340e
--- /dev/null
+++ b/src/assets/css/global/variables.less
@@ -0,0 +1,15 @@
+@import "../bootstrap/variables.less";
+//custom basic color
+@brand-primary: #006699;
+@brand-success: #1ab394;
+@brand-info: #5EC1C5;
+@brand-warning: #FEC62E;
+@brand-danger: #FE8768;
+@brand-gray: #c2c2c2;
+@brand-laxative: #B3EE3A;
+
+@theme-color: #1b2a9d;
+@theme-background: #fafafa;
+
+//左侧的主题图标导航栏宽度。
+@subject-icon-bar-width: 50px;
diff --git a/src/assets/css/index.less b/src/assets/css/index.less
new file mode 100644
index 0000000..9758963
--- /dev/null
+++ b/src/assets/css/index.less
@@ -0,0 +1,16 @@
+//使用自定义的主题
+@import "global/animation";
+@import "global/border";
+@import "global/button";
+@import "global/color";
+@import "global/font";
+@import "global/layout";
+@import "global/miscellaneous";
+@import "global/responsive";
+
+html, body {
+  font-family: "微软雅黑", Microsoft YaHei, "Helvetica Neue", Helvetica, Arial, sans-serif, "open sans";
+  font-size: 13px;
+  color: @text-color;
+
+}
diff --git a/src/assets/image/avatar.png b/src/assets/image/avatar.png
new file mode 100644
index 0000000..222f0b4
Binary files /dev/null and b/src/assets/image/avatar.png differ
diff --git a/src/assets/image/empty.svg b/src/assets/image/empty.svg
new file mode 100644
index 0000000..78ee45c
--- /dev/null
+++ b/src/assets/image/empty.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/image/error.png b/src/assets/image/error.png
new file mode 100644
index 0000000..97f79c9
Binary files /dev/null and b/src/assets/image/error.png differ
diff --git a/src/assets/image/logo.png b/src/assets/image/logo.png
new file mode 100644
index 0000000..85388ea
Binary files /dev/null and b/src/assets/image/logo.png differ
diff --git a/src/common/component/BambooComponent.tsx b/src/common/component/BambooComponent.tsx
new file mode 100644
index 0000000..8a8517c
--- /dev/null
+++ b/src/common/component/BambooComponent.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+
+
+/**
+ * 带有挂载状态的自定义组件
+ */
+export default class BambooComponent extends React.Component  {
+
+  //是否处于挂载的状态。
+  mounted: boolean = false
+
+  componentWillUnmount() {
+    let that = this
+    that.mounted = false
+  }
+
+  componentWillMount() {
+    let that = this
+    that.mounted = true
+  }
+
+  //更新当前视图。
+  updateUI() {
+    let that = this
+    if (that.mounted) {
+      that.setState({})
+    } else {
+      console.info(this.constructor.name + "已经脱离挂载,不再刷新视图。")
+    }
+  }
+
+  //获取当前组件的唯一标识
+  getIdentifier() {
+    return this.constructor.name;
+  }
+}
+
diff --git a/src/common/menu/MenuItem.ts b/src/common/menu/MenuItem.ts
new file mode 100644
index 0000000..8c76d03
--- /dev/null
+++ b/src/common/menu/MenuItem.ts
@@ -0,0 +1,23 @@
+import {ToolItem} from "../types";
+
+export default class MenuItem implements ToolItem {
+  name: string;
+  url: string;
+  iconType: string;
+  active: boolean = false;
+
+
+  constructor(name: string, url: string, iconType: string) {
+    this.name = name;
+    this.url = url;
+    this.iconType = iconType;
+
+    //当前的url完全一致,那么就高亮显示
+    if (url == "/user/login") {
+      this.active = window.location.pathname === url || window.location.pathname === "/user/register"
+    } else {
+      this.active = window.location.pathname === url
+    }
+
+  }
+}
diff --git a/src/common/menu/MenuManager.ts b/src/common/menu/MenuManager.ts
new file mode 100644
index 0000000..6708a7e
--- /dev/null
+++ b/src/common/menu/MenuManager.ts
@@ -0,0 +1,75 @@
+/**
+ * 管理当前所有的菜单
+ */
+import MenuItem from './MenuItem';
+import Moon from '../model/global/Moon';
+import User from '../model/user/User';
+import { UserRole } from '../model/user/UserRole';
+
+export default class MenuManager {
+
+  //单例模式
+  private static singleton: MenuManager;
+
+
+  constructor() {
+
+
+  }
+
+  static getSingleton(): MenuManager {
+    if (!MenuManager.singleton) {
+      //初始化一个mainLand.
+      MenuManager.singleton = new MenuManager();
+
+    }
+    return MenuManager.singleton;
+  }
+
+  /**
+   * 获取到当前高亮的菜单
+   */
+  getSelectedKeys(): string[] {
+
+    let keys: string[] = this.getMenuItems()
+      .filter((menuItem: MenuItem, index: number) => {
+        return menuItem.active;
+      }).map((menuItem: MenuItem, index: number) => {
+        return menuItem.url;
+      });
+
+    return keys;
+  }
+
+  /**
+   * 高亮某个菜单
+   */
+  selectMenu(url: string) {
+
+    this.getMenuItems().forEach((menuItem: MenuItem, index: number) => {
+      menuItem.active = menuItem.url === url;
+    });
+
+  }
+
+  getMenuItems(): MenuItem[] {
+    let user: User = Moon.getSingleton().user;
+
+    let menuItems: MenuItem[] = [];
+
+    if (user.role === UserRole.GUEST) {
+      menuItems = [
+        new MenuItem('登录', '/user/login', 'user'),
+      ];
+    } else {
+      menuItems = [
+        new MenuItem('文章', '/article/list', 'bar-chart'),
+        new MenuItem('退出', '/user/logout', 'poweroff'),
+      ];
+    }
+
+    return menuItems;
+
+  }
+
+}
diff --git a/src/common/model/article/Article.ts b/src/common/model/article/Article.ts
new file mode 100644
index 0000000..63ea7a6
--- /dev/null
+++ b/src/common/model/article/Article.ts
@@ -0,0 +1,62 @@
+import BaseEntity from '../base/BaseEntity';
+import Filter from '../base/filter/Filter';
+import SortFilter from '../base/filter/SortFilter';
+import InputFilter from '../base/filter/InputFilter';
+
+
+export default class Article extends BaseEntity {
+
+  userUuid: string | null = null;
+  title: string | null = null;
+  path: string | null = null;
+  tags: string | null = null;
+  posterTankUuid: string | null = null;
+  posterUrl: string | null = null;
+  author: string | null = null;
+  digest: string | null = null;
+  isMarkdown: boolean = true;
+  html: string | null = null;
+  privacy: boolean = false;
+  top: boolean = false;
+  agree: number = 0;
+  words: number = 0;
+  hit: number = 0;
+  commentNum: number = 0;
+
+  constructor(reactComponent?: React.Component) {
+
+    super(reactComponent);
+
+  }
+
+  assign(obj: any) {
+    super.assign(obj);
+
+
+  }
+
+  getForm(): any {
+    return {
+      title: this.title,
+      path: this.path,
+      author: this.author,
+      html: this.html,
+      uuid: this.uuid ? this.uuid : null,
+    };
+  }
+
+  getFilters(): Filter[] {
+    return [
+      ...super.getFilters(),
+      new InputFilter('标题', 'title'),
+      new InputFilter('路径', 'path'),
+      new InputFilter('作者', 'author'),
+    ];
+  }
+
+
+}
+
+
+
+
diff --git a/src/common/model/base/Base.ts b/src/common/model/base/Base.ts
new file mode 100644
index 0000000..3a89700
--- /dev/null
+++ b/src/common/model/base/Base.ts
@@ -0,0 +1,140 @@
+import ObjectUtil from "../../util/ObjectUtil";
+import JsonUtil from "../../util/JsonUtil";
+import DateUtil from "../../util/DateUtil";
+
+/**
+ *
+ * 基类。使我们前端的所有自定义类的基类。
+ * 在这个类中可以统计建立的实体个数。
+ */
+export default class Base {
+
+  //id自增长值
+  private static AUTO_INCREMENT_ID = 0
+
+  //当前对象的ID,这个字段让我们可以很轻松的统计出共创建了多少个实体类。
+  autoId: number = 0
+
+
+  //我们认为每个实体都会存放于某个react组件中,当然可以不传入。
+  constructor() {
+
+    this.autoId = Base.generateAutoId()
+  }
+
+  static generateAutoId() {
+    Base.AUTO_INCREMENT_ID++
+    return Base.AUTO_INCREMENT_ID
+  }
+
+
+  //把obj中的属性,赋值到this中来。采用深拷贝。
+  assign(obj: any) {
+    ObjectUtil.extend(this, obj)
+  }
+
+  /**
+   *
+   * @param field 字段名
+   * @param Clazz 类型名
+   */
+  assignList(field: string, Clazz: any) {
+
+    ObjectUtil.assignList(this, field, Clazz)
+  }
+
+
+  /**
+   * 根据一个类型,渲染出对应的数组。
+   * @param json 字符串或者数组对象。
+   * @param Clazz 需要渲染的目标对象
+   * @returns {*}
+   */
+  static renderList(json: any, Clazz: any) {
+
+    let target: any = []
+
+
+    let arr: any = []
+
+    if (json instanceof String || typeof json === "string") {
+
+      arr = JsonUtil.parseList(json)
+
+    } else if (json instanceof Array) {
+      arr = json
+    } else {
+
+      console.error("源必须为字符或者数组", json, typeof json)
+      return target
+    }
+
+    //如果我们要转换成字符串的数组形式,那么this[field]应该是一个字符串才对。
+    if (Clazz === String) {
+      return arr
+    }
+
+    if (!Clazz || !(Clazz.prototype instanceof Base)) {
+      console.error("指定的类型必须是 Base的子类 ")
+      return target
+    }
+
+    for (let i = 0; i < arr.length; i++) {
+      let bean = arr[i]
+
+      let clazz = new Clazz()
+
+      clazz.assign(bean)
+
+      target.push(clazz)
+    }
+
+    return target
+  }
+
+  //直接render出一个Entity. field字段名,Clazz类名。
+  assignEntity(field: any, Clazz: any) {
+
+    let thisObj: any = this
+
+    let obj: any = thisObj[field]
+    if (!obj) {
+      if (Clazz) {
+        let EntityClazz: any = this.constructor
+        obj = (new EntityClazz())[field]
+
+      } else {
+        return
+      }
+    }
+
+    if (Clazz === Date) {
+
+      thisObj[field] = DateUtil.str2Date(obj)
+
+    } else if (Clazz.prototype instanceof Base) {
+
+      //可能此处的该项属性做了特殊处理的。
+      //1024*1024 以及 "图片尺寸不超过1M"用let bean = new Clazz(); 就无法反映出来。因为父类assign的时候已经将avatar给变成了Object.
+      let bean = (new thisObj.constructor())[field]
+      if (!bean) {
+        bean = new Clazz()
+      }
+
+      if (obj !== null) {
+        bean.assign(obj)
+        thisObj[field] = bean
+      }
+
+    } else {
+      console.error('调用错误!')
+    }
+
+  }
+
+
+}
+
+
+
+
diff --git a/src/common/model/base/BaseEntity.ts b/src/common/model/base/BaseEntity.ts
new file mode 100644
index 0000000..89d8fec
--- /dev/null
+++ b/src/common/model/base/BaseEntity.ts
@@ -0,0 +1,227 @@
+import StringUtil from '../../util/StringUtil';
+import SafeUtil from '../../util/SafeUtil';
+import SortFilter from './filter/SortFilter';
+import Filter from './filter/Filter';
+import HttpBase from './HttpBase';
+
+
+/**
+ * 实体基类
+ * 继承这个类的表示在数据库中有对应的表。
+ */
+export default class BaseEntity extends HttpBase {
+
+  /**
+   * 唯一标识
+   */
+  uuid: string | null = null;
+
+  /**
+   * 排序值
+   */
+  sort: number = 0;
+
+  /**
+   * 创建时间
+   */
+  createTime: Date | null = null;
+
+  /**
+   * 修改时间
+   */
+  updateTime: Date | null = null;
+
+
+  //************ 前端辅助字段 ****************/
+  //加载详情的指示
+  detailLoading: boolean = false;
+
+
+  //我们认为每个实体都会存放于某个react组件中,当然可以不传入。
+  constructor(reactComponent?: React.Component | null) {
+
+    super(reactComponent);
+
+  }
+
+  //把obj中的属性,赋值到this中来。采用深拷贝。
+  assign(obj: any) {
+    super.assign(obj);
+
+    this.assignEntity('createTime', Date);
+    this.assignEntity('updateTime', Date);
+
+  }
+
+  //获取过滤器,必须每次动态生成,否则会造成filter逻辑混乱。
+  getFilters(): Filter[] {
+    return [
+      new SortFilter('修改时间排序', 'orderCreateTime'),
+      new SortFilter('创建时间排序', 'orderUpdateTime'),
+    ];
+  };
+
+
+  //提交之前对自己进行验证。返回错误信息,null表示没有错误。
+  validate() {
+
+    return null;
+  }
+
+  //提交的表单
+  getForm(): any {
+    console.error('getForm: you should override this base method.');
+  }
+
+  //获取到当前类的单数标签。比如 Project便得到 project
+  getTAG(): string {
+
+    let className = this.constructor.name;
+
+    //IE无法直接通过this.constructor.name获取到相应名称
+    if (!className) {
+      className = StringUtil.functionName(this.constructor);
+    }
+
+    return StringUtil.lowerCamel(className);
+  }
+
+
+  //获取到当前实体的url前缀。比如Notification获取到 /api/fs/notification
+  getUrlPrefix(): string {
+    return '/api' + StringUtil.lowerSlash(this.getTAG());
+  }
+
+
+  getUrlCreate(): string {
+    let prefix = this.getUrlPrefix();
+
+    return prefix + '/create';
+  }
+
+  getUrlDel(): string {
+    let prefix = this.getUrlPrefix();
+
+    return prefix + '/delete';
+
+  }
+
+  getUrlEdit(): string {
+    let prefix = this.getUrlPrefix();
+
+    return prefix + '/edit';
+  }
+
+  getUrlDetail(): string {
+    let prefix = this.getUrlPrefix();
+
+    return prefix + '/detail';
+  }
+
+  getUrlList(): string {
+    let prefix = this.getUrlPrefix();
+
+    return prefix + '/list';
+  }
+
+  getUrlSort(): string {
+    let prefix = this.getUrlPrefix();
+
+    return prefix + '/sort';
+  }
+
+
+  //新增或者修改
+  httpSave(successCallback?: any, errorCallback?: any, finallyCallback?: any) {
+
+    let that = this;
+
+    let url = this.getUrlCreate();
+    if (this.uuid) {
+      url = this.getUrlEdit();
+    }
+
+
+    this.errorMessage = this.validate();
+    if (this.errorMessage) {
+      that.defaultErrorHandler(this.errorMessage, errorCallback);
+      return;
+    }
+
+    this.httpPost(url, this.getForm(), function(response: any) {
+
+      that.assign(response.data.data);
+
+      SafeUtil.safeCallback(successCallback)(response);
+
+    }, errorCallback, finallyCallback);
+
+  }
+
+
+  //common http detail methods.
+  httpDetail(successCallback?: any, errorCallback?: any, finallyCallback?: any) {
+
+    let that = this;
+    if (!this.uuid) {
+
+      this.errorMessage = 'id未指定,无法获取到详情!';
+
+      this.defaultErrorHandler(this.errorMessage, errorCallback);
+
+      return;
+    }
+
+    let url = this.getUrlDetail() + '?uuid=' + this.uuid;
+
+    this.detailLoading = true;
+
+    this.httpGet(url, {}, function(response: any) {
+      that.detailLoading = false;
+
+      that.assign(response.data.data);
+
+      SafeUtil.safeCallback(successCallback)(response);
+
+    }, function(response: any) {
+
+      that.detailLoading = false;
+
+      if (typeof errorCallback === 'function') {
+        errorCallback(that.getErrorMessage(response), response);
+      } else {
+        //没有传入错误处理的方法就采用默认处理方法:toast弹出该错误信息。
+        that.defaultErrorHandler(response);
+      }
+    }, finallyCallback);
+
+  }
+
+
+  httpDel(successCallback?: any, errorCallback?: any, finallyCallback?: any) {
+
+    let that = this;
+    if (!this.uuid) {
+
+      this.errorMessage = '没有id,无法删除!';
+      that.defaultErrorHandler(this.errorMessage, errorCallback);
+
+      return;
+    }
+
+    let url = this.getUrlDel() + '?uuid=' + this.uuid;
+
+    this.httpPost(url, {}, function(response: any) {
+
+      SafeUtil.safeCallback(successCallback)(response);
+
+    }, errorCallback, finallyCallback);
+
+  }
+
+
+}
+
+
+
+
diff --git a/src/common/model/base/HttpBase.ts b/src/common/model/base/HttpBase.ts
new file mode 100644
index 0000000..d421eec
--- /dev/null
+++ b/src/common/model/base/HttpBase.ts
@@ -0,0 +1,262 @@
+import HttpUtil from "../../util/HttpUtil";
+import SafeUtil from "../../util/SafeUtil";
+import qs from "qs"
+import {message as MessageBox} from 'antd';
+import React from "react";
+import ViewBase from "./ViewBase";
+import Sun from "../global/Sun";
+import {WebResultCode} from "./WebResultCode";
+
+/**
+ * 基类。带有网络请求能力的基类
+ * 继承了该类就表示具有了去服务器请求的能力。
+ */
+export default class HttpBase extends ViewBase {
+
+  static lastLoginErrorTimestamp = 0
+
+  //是否需要自动刷新State
+  needReactComponentUpdate: boolean = true
+
+  //当前是否正在进行http请求
+  loading: boolean = false
+
+  //请求http的时候是否有错误
+  errorMessage: string | null = null
+
+  //我们认为每个实体都会存放于某个react组件中,当然可以不传入。
+  constructor(reactComponent?: React.Component | null) {
+
+    super()
+
+    if (reactComponent) {
+      this.reactComponent = reactComponent
+    }
+
+  }
+
+
+  //更新当前的视图,只在需要更新的情况下才更新。
+  updateUI() {
+    if (this.needReactComponentUpdate && this.reactComponent) {
+      ViewBase.updateComponentUI(this.reactComponent, this)
+    }
+  }
+
+
+  /**
+   * 转跳到ACL页面
+   */
+  jumpLogin() {
+
+    Sun.navigateTo("/user/login")
+
+    console.error("这里是需要跳转的")
+
+  }
+
+  //从一个返回中获取出其错误信息。适配各种错误的类型。
+  getErrorMessage(response: any) {
+
+    console.error("getErrorMessage", response)
+    let msg = '服务器出错,请稍后再试!'
+
+    if (!response) {
+      msg = '出错啦,请稍后重试!'
+    } else if (typeof response === 'string') {
+      msg = response
+    } else if (response['msg']) {
+      msg = response['msg']
+    } else if (response['message']) {
+      msg = response['message']
+    } else {
+      let temp = response['data']
+      if (temp !== null && typeof temp === 'object') {
+        if (temp['message']) {
+          msg = temp['message']
+        } else if (temp['msg']) {
+          msg = temp['msg']
+        } else {
+          if (temp['error'] && temp['error']['message']) {
+            msg = temp['error']['message']
+          }
+        }
+      }
+    }
+    this.errorMessage = msg
+    return msg
+  }
+
+
+  //提供全局的默认处理方式,可以自定义错误处理
+  defaultErrorHandler(response: any, errorCallback?: any) {
+
+    let msg = this.getErrorMessage(response)
+
+    console.error("请求出错了", typeof msg, msg)
+
+    if (typeof errorCallback === 'function') {
+      errorCallback(msg, response)
+    } else {
+      MessageBox.error(msg)
+    }
+
+  }
+
+
+  //专门捕捉没有登录这种错误。return true -> 有错误(已经处理掉了)  false -> 没错误 (什么都没干)
+  specialErrorHandler(response: any) {
+
+    if (!response || !response.data) {
+      return false
+    }
+
+    //1.判断是不是登录错误
+    if (response.data["code"] === WebResultCode.LOGIN) {
+
+      //这个问题不能报的太频繁,比如一个页面请求了两个接口,两个接口都报没有登录。
+      if ((new Date().getTime()) - HttpBase.lastLoginErrorTimestamp < 3000) {
+        return true
+      } else {
+        HttpBase.lastLoginErrorTimestamp = (new Date().getTime());
+      }
+
+      MessageBox.error("您尚未登录,请登录后访问!")
+
+      //立即进行登录跳转。
+      this.jumpLogin()
+
+      return true;
+    }
+
+
+    return false
+
+  }
+
+
+  /**
+   * 基类中的http请求会去统一处理错误情况。
+   * 1.登录过期
+   *
+   */
+  httpGet(url: any, params = {}, successCallback?: any, errorCallback?: any, finallyCallback?: any, opts?: any) {
+
+    let that = this
+
+    if (!opts) {
+      opts = {}
+    }
+
+
+    that.loading = true
+
+    //更新react控件的状态
+    that.updateUI()
+
+    HttpUtil.httpGet(url, params, function (response: any) {
+      //有可能正常接口回来的数据也是错误的。交给错误处理器处理。
+      if (that.specialErrorHandler(response)) {
+
+        SafeUtil.safeCallback(errorCallback)(response)
+        return
+      }
+
+
+      SafeUtil.safeCallback(successCallback)(response)
+
+    }, function (err: any) {
+
+      let response = err.response
+      console.error("请求出错啦", response)
+
+      //特殊错误情况的通用处理方式
+      if (that.specialErrorHandler(response)) {
+        return
+      }
+
+
+      that.defaultErrorHandler(response, errorCallback)
+
+    }, function (res: any) {
+
+      that.loading = false
+
+      //更新react控件的状态
+      that.updateUI()
+
+      SafeUtil.safeCallback(finallyCallback)(res)
+
+    }, opts);
+  }
+
+
+  /**
+   * 基类中的http请求会去统一处理错误情况。
+   * 1.登录过期
+   *
+   */
+  httpPost(url: any, params = {}, successCallback?: any, errorCallback?: any, finallyCallback?: any, opts?: any) {
+
+    let that = this
+
+    if (!opts) {
+      opts = {}
+    }
+
+    that.loading = true
+
+    //更新react控件的状态
+    that.updateUI()
+
+
+    let formData = qs.stringify(params);
+
+    if (!opts["headers"]) {
+      opts["headers"] = {}
+    }
+    opts["headers"]['Content-Type'] = 'application/x-www-form-urlencoded'
+
+
+    HttpUtil.httpPost(url, formData, function (response: any) {
+
+      //有可能正常接口回来的数据也是错误的。交给错误处理器处理。
+      if (that.specialErrorHandler(response)) {
+
+        SafeUtil.safeCallback(errorCallback)(response)
+        return
+      }
+
+      SafeUtil.safeCallback(successCallback)(response)
+
+    }, function (err: any) {
+
+      let response = err.response
+
+      console.error("请求出错啦", response ? response : err)
+
+      //特殊错误情况的通用处理方式
+      if (that.specialErrorHandler(response)) {
+        return
+      }
+
+      that.defaultErrorHandler(response, errorCallback)
+
+    }, function (res: any) {
+
+      that.loading = false
+
+      //更新react控件的状态
+      that.updateUI()
+
+      SafeUtil.safeCallback(finallyCallback)(res)
+
+    }, opts);
+  }
+
+
+}
+
+
+
+
diff --git a/src/common/model/base/Pager.ts b/src/common/model/base/Pager.ts
new file mode 100644
index 0000000..25dc36d
--- /dev/null
+++ b/src/common/model/base/Pager.ts
@@ -0,0 +1,405 @@
+import BaseEntity from './BaseEntity';
+import NumberUtil from '../../util/NumberUtil';
+import SafeUtil from '../../util/SafeUtil';
+import BrowserUtil from '../../util/BrowserUtil';
+import StringUtil from '../../util/StringUtil';
+import ObjectUtil from '../../util/ObjectUtil';
+import Filter from './filter/Filter';
+import SortFilter from './filter/SortFilter';
+import HttpBase from './HttpBase';
+
+/**
+ * 一个分页帮助器,可以去后台请求数据,也可以渲染需要数据源。
+ * 这个类具有非常神奇的能力,可以说是整个项目含金量最高的一个类!
+ */
+export default class Pager extends HttpBase {
+
+  static MAX_PAGE_SIZE = 500;
+
+  /**
+   * 当前分页大小 0基
+   */
+  page: number = 0;
+  /**
+   * 每一页的大小
+   */
+  pageSize: number = 10;
+  /**
+   * 总的条目数量
+   */
+  totalItems: number = 0;
+  /**
+   * 总的页数
+   */
+  totalPages: number = 0;
+
+  /**
+   * 返回的数据,类型为泛型
+   */
+  data: T[] = [];
+
+  /**
+   * 类。这个很特殊。
+   */
+  Clazz: any = null;
+
+  /**
+   * 分页的url链接地址
+   */
+  urlPage: string | null = null;
+
+  /**
+   * 过滤筛选器
+   */
+  filters: Filter[] = [];
+
+  /**
+   * 是否要求在浏览器中保存参数
+   */
+  history: boolean = false;
+
+
+  constructor(reactComponent: React.Component | null, Clazz: any, pageSize = 20) {
+
+    super(reactComponent);
+
+    this.pageSize = pageSize;
+
+    //这里的处理利用了js的原型调用链,比较魔法。
+    if (Clazz && (Clazz.prototype instanceof BaseEntity)) {
+      this.Clazz = Clazz;
+
+      let urlPage = Clazz.prototype.getUrlList();
+      if (urlPage) {
+        this.urlPage = urlPage;
+      } else {
+        console.error(Clazz + '必须定义分页url');
+      }
+
+      if (Clazz.prototype.getFilters) {
+
+        //直接获取该类的过滤器。
+        this.filters = Clazz.prototype.getFilters();
+
+      } else {
+
+        console.error('The Clazz MUST define a prototype method named \'getFilters\'');
+
+      }
+
+    } else {
+      console.error('You MUST specify a Clazz extended BaseEntity');
+    }
+  }
+
+  //把obj中的属性,赋值到this中来。采用深拷贝。
+  assign(obj: any) {
+
+    super.assign(obj);
+
+    this.assignList('data', this.Clazz);
+
+  }
+
+
+  //该方法是在地址栏添加上query参数,参数就是filters中的key和value.
+  //同时地址栏上有的参数也会自动读取到filters中去
+  //因此,启用该方法后返回时可以停留在之前的页码中。
+  enableHistory() {
+    this.history = true;
+
+    let queryPageNum: string | null = BrowserUtil.getParameterByName('page');
+    let queryPageSize: string | null = BrowserUtil.getParameterByName('pageSize');
+
+    if (queryPageNum !== null && queryPageNum !== '') {
+      this.page = parseInt(queryPageNum);
+    }
+
+    if (queryPageSize !== null && queryPageSize !== '') {
+      this.pageSize = parseInt(queryPageSize);
+    }
+
+    if (!NumberUtil.isInteger(this.page)) {
+      this.page = 0;
+    }
+    if (!NumberUtil.isInteger(this.pageSize)) {
+      this.pageSize = 10;
+    }
+
+
+    //从请求参数中传值。
+    for (let i = 0; i < this.filters.length; i++) {
+
+      let filter: Filter = this.filters[i];
+
+      let queryValue = BrowserUtil.getParameterByName(filter.key);
+
+      if (queryValue !== null && queryValue !== '') {
+
+        filter.putValue(queryValue);
+
+      }
+    }
+  }
+
+
+  //重置所有过滤器
+  resetFilter() {
+    for (let i = 0; i < this.filters.length; i++) {
+      let filter = this.filters[i];
+      filter.reset();
+    }
+  };
+
+  //重置排序过滤器
+  resetSortFilters() {
+    for (let i = 0; i < this.filters.length; i++) {
+      let filter = this.filters[i];
+      if (filter instanceof SortFilter) {
+        filter.reset();
+      }
+    }
+  };
+
+
+  //手动设置过滤器的值
+  setFilterValue(key: string, value: any) {
+    if (!this.filters || !this.filters.length) {
+      return;
+    }
+
+    for (let i = 0; i < this.filters.length; i++) {
+
+      let filter = this.filters[i];
+
+      if (filter.key === key) {
+        filter.putValue(value);
+      }
+
+    }
+  };
+
+
+  //根据key来删除某个Filter
+  removeFilter(key: string) {
+    if (!this.filters || !this.filters.length) {
+      return;
+    }
+    for (let i = 0; i < this.filters.length; i++) {
+      let filter = this.filters[i];
+      if (filter.key === key) {
+        this.filters.splice(i, 1);
+        break;
+      }
+    }
+  };
+
+
+  //隐藏某个Filter,实际上我们可以根据这个filter来筛选,只不过不出现在NbFilter中而已。
+  showFilter(key: string, visible = true) {
+    if (!this.filters || !this.filters.length) {
+      return;
+    }
+    for (let i = 0; i < this.filters.length; i++) {
+      let filter = this.filters[i];
+      if (filter.key === key) {
+        filter.visible = visible;
+        break;
+      }
+    }
+  };
+
+  showAllFilter(visible = true) {
+    if (!this.filters || !this.filters.length) {
+      return;
+    }
+    for (let i = 0; i < this.filters.length; i++) {
+      let filter = this.filters[i];
+      filter.visible = visible;
+    }
+  }
+
+  //根据一个key来获取某个filter
+  getFilter(key: string): Filter | null {
+    if (!this.filters || !this.filters.length) {
+      return null;
+    }
+
+    for (let i = 0; i < this.filters.length; i++) {
+      let filter = this.filters[i];
+      if (filter.key === key) {
+        return filter;
+      }
+    }
+
+    return null;
+  };
+
+
+  /**
+   * 获取当前进行sort的那个filter
+   * 我们认为一次只有一个排序值
+   */
+  getCurrentSortFilter(): Filter | null {
+
+    if (!this.filters || !this.filters.length) {
+      return null;
+    }
+
+    for (let i = 0; i < this.filters.length; i++) {
+      let filter = this.filters[i];
+      if (filter instanceof SortFilter) {
+        if (!filter.isEmpty()) {
+          return filter;
+        }
+      }
+
+    }
+    return null;
+  }
+
+
+  //获取所有的filter参数,键值对形式
+  getParams(): { [s: string]: string | number } {
+
+    let params: { [s: string]: string | number } = {
+      page: this.page,
+      pageSize: this.pageSize,
+    };
+
+    if (!this.filters || !this.filters.length) {
+      return params;
+    }
+
+    for (let i = 0; i < this.filters.length; i++) {
+      let filter = this.filters[i];
+
+      if (!filter.isEmpty()) {
+        params[filter.key] = filter.getValueString();
+      }
+    }
+
+    return params;
+  };
+
+
+  //元素是否为空
+  isEmpty(): boolean {
+    return !this.data || !this.data.length;
+  }
+
+  //去服务器端进行请求
+  httpList(successCallback?: any, errorCallback?: any, finalCallback?: any) {
+
+    let that = this;
+
+    let params: { [s: string]: string | number } = this.getParams();
+
+
+    if (this.history) {
+      window.history.replaceState({}, '', window.location.pathname + '?' + ObjectUtil.param(params));
+    }
+
+
+    //准备去请求,所有错误置为空
+    this.errorMessage = null;
+
+    this.httpGet(this.urlPage, params, function(response: any) {
+      // handle success
+
+      that.assign(response.data.data);
+
+      SafeUtil.safeCallback(successCallback)(response);
+
+    }, function(errorMessage: any, response: any) {
+
+      //失败了就清空
+      that.data = [];
+
+      SafeUtil.safeCallback(errorCallback)(errorMessage, response);
+
+    }, finalCallback);
+
+  }
+
+
+  //从pager中获取当前的分页情况,在table的分页器中显示 PaginationConfig
+  getPagination(): any | false {
+    let that = this;
+
+    if (this.totalPages > 1) {
+      return {
+        current: that.page + 1,
+        pageSize: that.pageSize,
+        total: that.totalItems,
+        showTotal: (totalNum: number) => '共' + totalNum + '条',
+        showSizeChanger: true,
+      };
+    } else {
+      return false;
+    }
+
+
+  }
+
+  //获取默认的排序顺序,提供给table使用
+  getDefaultSortOrder(columnKey: string): any | boolean {
+
+    //将变化的这个情况更新。
+    let filterKey = 'order' + StringUtil.capitalize(columnKey);
+
+    //直接放进排序值即可,底层会自动兼容
+    let sortFilter = this.getFilter(filterKey);
+
+    if (sortFilter && (sortFilter instanceof SortFilter)) {
+
+      let antdSortValue: any | null = sortFilter.getAntdValue();
+      if (antdSortValue) {
+        return antdSortValue;
+      } else {
+        return false;
+      }
+
+    } else {
+      return false;
+    }
+
+  }
+
+
+  //在table的分页器 发生变化调用 TODO: PaginationConfig SorterResult TableCurrentDataSource
+  tableOnChange(pagination: any, filters: any, sorter: any, extra: any) {
+    let that = this;
+
+    if (pagination.current !== undefined) {
+      that.page = pagination.current - 1;
+    }
+
+    if (pagination.pageSize !== undefined) {
+
+      that.pageSize = pagination.pageSize;
+    }
+
+
+    //重置所有的sort
+    that.resetSortFilters();
+    if (!StringUtil.isEmptyObject(sorter)) {
+
+      //将变化的这个情况更新。
+      let filterKey = 'order' + StringUtil.capitalize(StringUtil.underScoreToCamel(sorter.field));
+
+
+      //直接放进排序值即可,底层会自动兼容
+      that.setFilterValue(filterKey, sorter.order);
+
+    }
+
+    //直接去刷新
+    that.httpList();
+  }
+
+}
+
+
+
+
diff --git a/src/common/model/base/SortDirection.ts b/src/common/model/base/SortDirection.ts
new file mode 100644
index 0000000..2f85a78
--- /dev/null
+++ b/src/common/model/base/SortDirection.ts
@@ -0,0 +1,12 @@
+enum SortDirection {
+  //这两个是后台通用的标准
+  ASC = 'ASC',
+  DESC = 'DESC',
+
+  //这两项是antd的标准
+  DESCEND = 'descend',
+  ASCEND = 'ascend'
+}
+
+
+export default SortDirection
diff --git a/src/common/model/base/ViewBase.ts b/src/common/model/base/ViewBase.ts
new file mode 100644
index 0000000..91fdcb7
--- /dev/null
+++ b/src/common/model/base/ViewBase.ts
@@ -0,0 +1,56 @@
+/**
+ *
+ * 基类。
+ *
+ * 这个类附带了一个视图,通过对这个类的操作可以对对应的视图进行更新等操作。
+ * Castle中的类,大多数直接继承这个类。
+ */
+import Base from "./Base";
+import React from "react";
+
+export default class ViewBase extends Base {
+
+  //该类对应的React视图。
+  reactComponent: React.Component | null = null
+
+  //我们认为每个实体都会存放于某个react组件中,当然可以不传入。
+  constructor() {
+
+    super()
+
+  }
+
+
+  //更新某一个组件对应的UI
+  static updateComponentUI(component: React.Component | null, entity?: any) {
+    if (component) {
+      //_version在这里是充当fake的。
+      component.setState({_version: new Date().getTime()})
+    } else {
+      console.warn(`${entity && entity.constructor ? entity.constructor.name : '未知对象'} 的 reactComponent 不存在,无法更新其对应的UI`)
+    }
+  }
+
+  //获取ReactComponent
+  getReactComponent(): React.ReactElement | null {
+
+    console.error(`你必须在子类 ${this.constructor.name} 中 override getReactComponent 方法`)
+
+    return React.createElement(
+      'div',
+    );
+  }
+
+  //更新当前的视图
+  updateUI() {
+
+    ViewBase.updateComponentUI(this.reactComponent, this)
+
+  }
+
+
+}
+
+
+
+
diff --git a/src/common/model/base/WebResultCode.ts b/src/common/model/base/WebResultCode.ts
new file mode 100644
index 0000000..85f8aac
--- /dev/null
+++ b/src/common/model/base/WebResultCode.ts
@@ -0,0 +1,42 @@
+import SelectionOption from "../base/option/SelectionOption";
+
+
+enum WebResultCode {
+  SUCCESS = "SUCCESS",
+  BAD_REQUEST = "BAD_REQUEST",
+  LOGIN = "LOGIN",
+  ERROR = "ERROR",
+}
+
+let WebResultCodes: WebResultCode[] = Object.keys(WebResultCode).map(k => k as WebResultCode)
+
+let WebResultCodeMap: { [key in keyof typeof WebResultCode]: SelectionOption } = {
+  SUCCESS: {
+    "name": "成功",
+    "value": "SUCCESS",
+  },
+  BAD_REQUEST: {
+    "name": "请求错误",
+    "value": "BAD_REQUEST",
+  },
+  LOGIN: {
+    "name": "未登录",
+    "value": "LOGIN",
+  },
+  ERROR: {
+    "name": "未知错误",
+    "value": "ERROR",
+  },
+}
+
+let WebResultCodeList: SelectionOption[] = []
+WebResultCodes.forEach((type: WebResultCode, index: number) => {
+  WebResultCodeList.push(WebResultCodeMap[type])
+})
+
+
+export {WebResultCode, WebResultCodes, WebResultCodeMap, WebResultCodeList}
+
+
+
+
diff --git a/src/common/model/base/filter/CheckFilter.ts b/src/common/model/base/filter/CheckFilter.ts
new file mode 100644
index 0000000..0ab2da4
--- /dev/null
+++ b/src/common/model/base/filter/CheckFilter.ts
@@ -0,0 +1,51 @@
+import Filter from "./Filter";
+
+
+/**
+ *
+ * Checkbox筛选框过滤器
+ *
+ */
+export default class CheckFilter extends Filter {
+
+  //值,最终填写的内容。null表示这个值不设置
+  value: boolean | null = null
+
+  constructor(name: string, code: string, visible?: boolean) {
+    super(name, code, visible)
+  }
+
+  //获取值字符串,[null,true,false] => ["","true","false"]
+  getValueString(): string {
+    let stringValue = ""
+    if (this.value === true) {
+      stringValue = "true"
+    } else if (this.value === false) {
+      stringValue = "false"
+    } else {
+      stringValue = ""
+    }
+    return stringValue
+  }
+
+  //通过一个字符串来设置值
+  putValue(value: any) {
+
+    if (value === "true" || value === true) {
+      this.value = true
+    } else if (value === "false" || value === false) {
+      this.value = false
+    } else {
+      this.value = null
+    }
+  }
+
+  reset() {
+    this.value = null
+  }
+
+  isEmpty(): boolean {
+    return this.value === null;
+  }
+
+}
diff --git a/src/common/model/base/filter/DateFilter.ts b/src/common/model/base/filter/DateFilter.ts
new file mode 100644
index 0000000..b0b8a79
--- /dev/null
+++ b/src/common/model/base/filter/DateFilter.ts
@@ -0,0 +1,54 @@
+import Filter from "./Filter";
+import DateUtil from "../../../util/DateUtil";
+
+
+/**
+ *
+ * 时间筛选器
+ *
+ */
+export default class DateFilter extends Filter {
+
+  //时间筛选的格式
+  format: string
+
+  //值,最终填写的内容。null表示这个值不设置
+  value: Date | null = null
+
+  constructor(name: string, code: string, format?: string, visible?: boolean) {
+    super(name, code, visible)
+    if (format) {
+      this.format = format
+    } else {
+      this.format = DateUtil.DATE_FORMAT
+    }
+  }
+
+  //获取值字符串
+  getValueString(): string {
+    if (this.value) {
+      return DateUtil.format(this.value, this.format)
+    } else {
+      return ""
+    }
+  }
+
+
+  //通过一个字符串来设置值
+  putValue(value: string) {
+
+    this.value = DateUtil.parse(value)
+  }
+
+
+  reset() {
+    this.value = null
+  }
+
+  isEmpty(): boolean {
+    return this.value === null;
+  }
+
+
+
+}
diff --git a/src/common/model/base/filter/DateTimeFilter.ts b/src/common/model/base/filter/DateTimeFilter.ts
new file mode 100644
index 0000000..c3a1581
--- /dev/null
+++ b/src/common/model/base/filter/DateTimeFilter.ts
@@ -0,0 +1,54 @@
+import Filter from "./Filter";
+import DateUtil from "../../../util/DateUtil";
+
+
+/**
+ *
+ * 时间筛选器
+ *
+ */
+export default class DateTimeFilter extends Filter {
+
+  //时间筛选的格式
+  format: string
+
+  //值,最终填写的内容。null表示这个值不设置
+  value: Date | null = null
+
+  constructor(name: string, code: string, format?: string, visible?: boolean) {
+    super(name, code, visible)
+    if (format) {
+      this.format = format
+    } else {
+      this.format = DateUtil.DEFAULT_FORMAT
+    }
+  }
+
+  //获取值字符串
+  getValueString(): string {
+    if (this.value) {
+      return DateUtil.format(this.value, this.format)
+    } else {
+      return ""
+    }
+  }
+
+
+  //通过一个字符串来设置值
+  putValue(value: string) {
+
+    this.value = DateUtil.parse(value)
+  }
+
+
+  reset() {
+    this.value = null
+  }
+
+  isEmpty(): boolean {
+    return this.value === null;
+  }
+
+
+
+}
diff --git a/src/common/model/base/filter/Filter.ts b/src/common/model/base/filter/Filter.ts
new file mode 100644
index 0000000..8142cbd
--- /dev/null
+++ b/src/common/model/base/filter/Filter.ts
@@ -0,0 +1,55 @@
+/**
+ * 这个是列表筛选器的基类。
+ */
+export default class Filter {
+
+  //中文名
+  name: string
+  //提交时候的键值,英文
+  key: string
+  //是否可见
+  visible: boolean = true
+
+  constructor(name: string, key: string, visible?: boolean) {
+    this.name = name
+    this.key = key
+
+    //没有传默认为true.
+    if (visible === undefined) {
+      this.visible = true
+    } else {
+      this.visible = visible
+    }
+  }
+
+  /**
+   * 获取到一个字符串的值
+   */
+  getValueString(): string {
+    console.error(`${this.constructor.name} 的 getValue 不存在,请开发者及时配置。`)
+    return ""
+  }
+
+  /**
+   * 把一个字符串放置到value中去,因为有可能从请求参数中回填值
+   */
+  putValue(value: string) {
+    console.error(`${this.constructor.name} 的 putValue 不存在,请开发者及时配置。`)
+  }
+
+  /**
+   * 将过滤器的值清空
+   */
+  reset() {
+    console.error(`${this.constructor.name} 的 reset 不存在,请开发者及时配置。`)
+  }
+
+  /**
+   * 当前过滤器是否为空
+   */
+  isEmpty():boolean {
+    console.error(`${this.constructor.name} 的 isEmpty 不存在,请开发者及时配置。`)
+    return false
+  }
+
+}
diff --git a/src/common/model/base/filter/HttpSelectionFilter.ts b/src/common/model/base/filter/HttpSelectionFilter.ts
new file mode 100644
index 0000000..82017e3
--- /dev/null
+++ b/src/common/model/base/filter/HttpSelectionFilter.ts
@@ -0,0 +1,32 @@
+import SelectionFilter from "./SelectionFilter";
+import SelectionFilterType from "./SelectionFilterType";
+
+
+/**
+ *
+ * 从远程拉取一个筛选项的过滤器
+ *
+ */
+export default class HttpSelectionFilter extends SelectionFilter {
+
+  //远程的访问链接。
+  url: string
+
+  constructor(name: string, code: string, url: string, selectionType?: SelectionFilterType, visible?: boolean) {
+    super(name, code, [], selectionType, visible)
+    this.url = url
+  }
+
+  //通过一个字符串来设置值
+  //远程调用的值必须要宽容,因为值可能还没有从远程取回来呢。
+  putValue(value: string) {
+    this.value = value
+  }
+
+  //严格的回填,采用父类的策略
+  //这个方法会在http请求完成了之后调用。HttpSelectionFilter中会调用
+  strictPutValue(value: string) {
+    super.putValue(value)
+  }
+
+}
diff --git a/src/common/model/base/filter/InputFilter.ts b/src/common/model/base/filter/InputFilter.ts
new file mode 100644
index 0000000..ffc5b99
--- /dev/null
+++ b/src/common/model/base/filter/InputFilter.ts
@@ -0,0 +1,47 @@
+import Filter from "./Filter";
+
+/**
+ * 文本输入框筛选器
+ * 这种一般是模糊搜索。
+ */
+export default class InputFilter extends Filter {
+
+  //值,最终填写的内容
+  value: string = ""
+
+  //占位符
+  placeholder: string
+
+  constructor(name: string, code: string, placeholder?: string | null, visible?: boolean) {
+    super(name, code, visible)
+
+    if (placeholder) {
+      this.placeholder = placeholder
+    } else {
+      this.placeholder = name
+    }
+  }
+
+
+  //获取值字符串
+  getValueString(): string {
+    return this.value
+  }
+
+  //通过一个字符串来设置值
+  putValue(value: string) {
+
+    this.value = value
+
+  }
+
+
+  reset() {
+    this.value = ""
+  }
+
+  isEmpty(): boolean {
+    return this.value === "";
+  }
+
+}
diff --git a/src/common/model/base/filter/SelectionFilter.ts b/src/common/model/base/filter/SelectionFilter.ts
new file mode 100644
index 0000000..c644531
--- /dev/null
+++ b/src/common/model/base/filter/SelectionFilter.ts
@@ -0,0 +1,68 @@
+import Filter from "./Filter";
+import SelectionOption from "../option/SelectionOption";
+import SelectionFilterType from "./SelectionFilterType";
+
+
+/**
+ *
+ * 单项选择过滤器
+ *
+ */
+export default class SelectionFilter extends Filter {
+
+  //候选项,要求必须有name和value。
+  options: SelectionOption[]
+
+  //值,最终填写的内容。null表示这个值不设置
+  value: string = ""
+
+  //采用的样式
+  selectionType: SelectionFilterType
+
+  constructor(name: string, code: string, options: SelectionOption[],selectionType?: SelectionFilterType, visible?: boolean) {
+    super(name, code, visible)
+
+    this.options = options
+    if (selectionType === undefined) {
+      this.selectionType = SelectionFilterType.COMBOBOX
+    } else {
+      this.selectionType = selectionType
+    }
+  }
+
+  //获取值字符串
+  getValueString(): string {
+    return this.value
+  }
+
+  //通过一个字符串来设置值
+  putValue(value: string) {
+
+    if (value === "") {
+      this.value = ""
+    } else {
+
+      //必须是options中的值才接纳。
+      for (let j = 0; j < this.options.length; j++) {
+        let opt = this.options[j]
+        if (opt.value === value) {
+          this.value = value
+          return
+        }
+      }
+
+      this.value = ""
+    }
+
+  }
+
+
+  reset() {
+    this.value = ""
+  }
+
+  isEmpty(): boolean {
+    return this.value === "";
+  }
+
+}
diff --git a/src/common/model/base/filter/SelectionFilterType.ts b/src/common/model/base/filter/SelectionFilterType.ts
new file mode 100644
index 0000000..1919e59
--- /dev/null
+++ b/src/common/model/base/filter/SelectionFilterType.ts
@@ -0,0 +1,11 @@
+/**
+ * 过滤器中的选择样式
+ */
+enum SelectionFilterType {
+  //下拉框的样式
+  COMBOBOX = "COMBOBOX",
+  //按钮的样式
+  BUTTON = "BUTTON",
+}
+
+export default SelectionFilterType
diff --git a/src/common/model/base/filter/SortFilter.ts b/src/common/model/base/filter/SortFilter.ts
new file mode 100644
index 0000000..30cbee8
--- /dev/null
+++ b/src/common/model/base/filter/SortFilter.ts
@@ -0,0 +1,69 @@
+import Filter from "./Filter";
+import SortDirection from "../SortDirection";
+
+
+/**
+ *
+ * 排序过滤器
+ *
+ */
+export default class SortFilter extends Filter {
+
+  //值,最终填写的内容。null表示这个值不设置
+  value: SortDirection | null = null
+
+  constructor(name: string, code: string, visible?: boolean) {
+    super(name, code, visible)
+    //过滤排序器,more是不显示的。
+    //没有传默认为true.
+    if (visible === undefined) {
+      this.visible = false
+    } else {
+      this.visible = visible
+    }
+  }
+
+  //获取值字符串,[null,true,false] => ["","true","false"]
+  getValueString(): string {
+    if (this.value) {
+      return this.value
+    } else {
+      return ""
+    }
+  }
+
+  //通过一个字符串来设置值
+  putValue(value: string) {
+    if (value === SortDirection.DESC || value === SortDirection.DESCEND) {
+      this.value = SortDirection.DESC
+    } else if (value === SortDirection.ASC || value === SortDirection.ASCEND) {
+      this.value = SortDirection.ASC
+    } else {
+      this.value = null
+    }
+  }
+
+
+  //获取到antd需要使用的值 TODO: SortOrder
+  getAntdValue(): any | null {
+    if (this.value === SortDirection.DESC || this.value === SortDirection.DESCEND) {
+      return SortDirection.DESCEND
+    } else if (this.value === SortDirection.ASC || this.value === SortDirection.ASCEND) {
+      return SortDirection.ASCEND
+    } else {
+      return null
+    }
+  }
+
+
+  reset() {
+    this.value = null
+  }
+
+
+  isEmpty(): boolean {
+    return this.value === null;
+  }
+
+
+}
diff --git a/src/common/model/base/option/CodeSelectionOption.ts b/src/common/model/base/option/CodeSelectionOption.ts
new file mode 100644
index 0000000..b87998d
--- /dev/null
+++ b/src/common/model/base/option/CodeSelectionOption.ts
@@ -0,0 +1,9 @@
+/**
+ * 带有Code的一个selection
+ */
+import SelectionOption from "./SelectionOption";
+
+export default interface CodeSelectionOption extends SelectionOption {
+  code: string,
+  type: string,
+}
diff --git a/src/common/model/base/option/Color.ts b/src/common/model/base/option/Color.ts
new file mode 100644
index 0000000..d718bd2
--- /dev/null
+++ b/src/common/model/base/option/Color.ts
@@ -0,0 +1,9 @@
+enum Color {
+  PRIMARY = "#108EE9",
+  INFO = "#2DB7F5",
+  SUCCESS = "#87D068",
+  WARNING = "#FEC62E",
+  DANGER = "#F50",
+}
+
+export default Color
diff --git a/src/common/model/base/option/ColorSelectionOption.ts b/src/common/model/base/option/ColorSelectionOption.ts
new file mode 100644
index 0000000..34da7b9
--- /dev/null
+++ b/src/common/model/base/option/ColorSelectionOption.ts
@@ -0,0 +1,9 @@
+/**
+ * 下拉筛选框的选项,统一使用该接口,同时这个作为枚举的复杂类型。
+ */
+import SelectionOption from "./SelectionOption";
+
+export default interface ColorSelectionOption extends SelectionOption {
+  index: number,
+  color: string,
+}
diff --git a/src/common/model/base/option/IconIndexSelectionOption.ts b/src/common/model/base/option/IconIndexSelectionOption.ts
new file mode 100644
index 0000000..452e20d
--- /dev/null
+++ b/src/common/model/base/option/IconIndexSelectionOption.ts
@@ -0,0 +1,11 @@
+/**
+ * 包含图标和 index序列的选项,在ProteinSqlPendant中有用到
+ */
+import SelectionOption from "./SelectionOption";
+
+export default interface IconIndexSelectionOption extends SelectionOption {
+  index: number,
+  icon: string,
+}
+
+
diff --git a/src/common/model/base/option/IndexSelectionOption.ts b/src/common/model/base/option/IndexSelectionOption.ts
new file mode 100644
index 0000000..f564d3f
--- /dev/null
+++ b/src/common/model/base/option/IndexSelectionOption.ts
@@ -0,0 +1,10 @@
+/**
+ * 下拉筛选框的选项,统一使用该接口,同时这个作为枚举的复杂类型。
+ */
+import SelectionOption from "./SelectionOption";
+
+export default interface IndexSelectionOption extends SelectionOption {
+  index: number,
+  name: string,
+  value: string
+}
diff --git a/src/common/model/base/option/SelectionOption.ts b/src/common/model/base/option/SelectionOption.ts
new file mode 100644
index 0000000..73115ff
--- /dev/null
+++ b/src/common/model/base/option/SelectionOption.ts
@@ -0,0 +1,8 @@
+/**
+ * 下拉筛选框的选项,统一使用该接口,同时这个作为枚举的复杂类型。
+ */
+export default interface SelectionOption {
+  name: string,
+  value: string
+}
+
diff --git a/src/common/model/base/option/StyleSelectionOption.ts b/src/common/model/base/option/StyleSelectionOption.ts
new file mode 100644
index 0000000..ce53dd1
--- /dev/null
+++ b/src/common/model/base/option/StyleSelectionOption.ts
@@ -0,0 +1,8 @@
+/**
+ * 下拉筛选框的选项,统一使用该接口,同时这个作为枚举的复杂类型。
+ */
+import SelectionOption from "./SelectionOption";
+
+export default interface StyleSelectionOption extends SelectionOption {
+  style: string,
+}
diff --git a/src/common/model/base/types/index.d.ts b/src/common/model/base/types/index.d.ts
new file mode 100644
index 0000000..8f714cd
--- /dev/null
+++ b/src/common/model/base/types/index.d.ts
@@ -0,0 +1,57 @@
+/**
+ * 定义了一个图标行为的对象,这个可以辅助一些组件。
+ */
+export interface IconActionItem {
+  //图片的资源链接(优先级高于iconType)
+  icon?: string
+  //antd的图标类型
+  iconType?: string
+  //名称
+  name?: string
+  //是否可见。默认可见
+  visible?: boolean
+  //点击图标后的行为
+  onClick?: () => void
+}
+
+
+/**
+ * 工具箱的条目,图标采用antd的图标。
+ * 图标不带有高亮样式
+ */
+export interface ToolItem {
+
+  name: string
+
+  //当前是否处于展开状态
+  active: boolean
+
+  //antd的图标样式
+  iconType: string
+
+}
+
+
+/**
+ * 该接口可以像文件树一样展示。
+ * 同时图标带有高亮样式
+ */
+export interface SkeletonItem {
+
+  name: string
+
+  //当前是否处于展开状态
+  active: boolean
+
+  //高亮的图标 资源链接
+  activeIcon: string
+
+  //一般状态的图标 资源链接
+  icon: string
+
+}
+
+/**
+ * 请求的Map,键值对
+ */
+export type RequestParamMap = { [key: string]: string }
diff --git a/src/common/model/global/Moon.ts b/src/common/model/global/Moon.ts
new file mode 100644
index 0000000..d22e4c4
--- /dev/null
+++ b/src/common/model/global/Moon.ts
@@ -0,0 +1,26 @@
+/**
+ * 全局性的对象,主要和实体类相关的
+ * 这个类只包含Base的子类,比如User, Preference.
+ * 和Sun形成姊妹篇
+ */
+import User from "../user/User";
+
+export default class Moon {
+
+  //全局具有唯一的用户,即当前登录的用户.
+  user: User = new User()
+
+  //全局的一个store对象
+  private static singleton: Moon | null = null
+
+  //使用懒加载模式。
+  static getSingleton(): Moon {
+    if (Moon.singleton == null) {
+      Moon.singleton = new Moon();
+    }
+
+    return Moon.singleton
+  }
+
+
+}
diff --git a/src/common/model/global/Sun.ts b/src/common/model/global/Sun.ts
new file mode 100644
index 0000000..adaffdd
--- /dev/null
+++ b/src/common/model/global/Sun.ts
@@ -0,0 +1,43 @@
+/**
+ * 全局性的对象,主要和整个框架相关的
+ * 这个类不包含Base的子类,比如User, Preference.
+ * 和Moon形成姊妹篇
+ */
+export default class Sun {
+
+  //全局的一个store对象
+  static singleton: Sun | null = null
+
+  //持有全局的react-router对象,方便我们在非jsx环境中控制路由跳转
+  reactRouter: any = null;
+
+  //由于这个类采用单例模式,因此所有属性都是独一份的。
+  constructor() {
+
+  }
+
+  //使用懒加载模式。
+  static getSingleton(): Sun {
+    if (Sun.singleton == null) {
+      Sun.singleton = new Sun();
+    }
+    return Sun.singleton
+  }
+
+
+  ////////////和路由相关的方法 开始////////////
+
+  /**
+   * 跳转到某个页面去
+   */
+  static navigateTo(path: any) {
+    if (Sun.getSingleton().reactRouter) {
+      Sun.getSingleton().reactRouter.push(path)
+    } else {
+      console.error("全局的 reactRouter 未定义,请检查代码!")
+    }
+  }
+
+  ////////////和路由相关的方法 结束////////////
+
+}
diff --git a/src/common/model/user/User.ts b/src/common/model/user/User.ts
new file mode 100644
index 0000000..7c69014
--- /dev/null
+++ b/src/common/model/user/User.ts
@@ -0,0 +1,166 @@
+import SafeUtil from '../../util/SafeUtil';
+import BaseEntity from '../base/BaseEntity';
+import { UserRole } from './UserRole';
+
+
+export default class User extends BaseEntity {
+
+  //获取当前登录者的信息
+  static URL_INFO = '/api/user/info';
+
+  //用户登录
+  static URL_LOGIN = '/api/user/login';
+
+  //用户注册
+  static URL_REGISTER = '/api/user/register';
+
+  //退出登录
+  static URL_LOGOUT = '/api/user/logout';
+
+  //修改密码
+  static URL_CHANGE_PASSWORD = '/api/user/change/password';
+
+  //用户角色
+  role: UserRole = UserRole.GUEST;
+  //用户名
+  username: string | null = null;
+  //密码
+  password: string | null = null;
+  //头像
+  avatarUrl: string | null = null;
+  //上次登录ip
+  lastIp: string | null = null;
+  //上次登录时间
+  lastTime: string | null = null;
+  //状态
+  status: string | null = null;
+
+  //是否已经登录
+  isLogin: boolean = false;
+
+  constructor(reactComponent?: React.Component) {
+
+    super(reactComponent);
+
+  }
+
+
+  assign(obj: any) {
+    super.assign(obj);
+
+  }
+
+  getForm(): any {
+    return {
+      username: this.username,
+      password: this.password,
+      uuid: this.uuid ? this.uuid : null,
+    };
+  }
+
+  //登录
+  httpInfo(successCallback?: any, errorCallback?: any, finalCallback?: any) {
+
+    let that = this;
+
+    let form = {};
+
+    this.httpGet(User.URL_INFO, form, function(response: any) {
+
+      that.assign(response.data.data);
+
+      SafeUtil.safeCallback(successCallback)(response);
+
+    }, errorCallback, finalCallback);
+
+  }
+
+
+  //登录
+  httpLogin(username: string, password: string, successCallback?: any, errorCallback?: any, finalCallback?: any) {
+
+    let that = this;
+
+    let form = {
+      username,
+      password,
+    };
+
+    this.httpGet(User.URL_LOGIN, form, function(response: any) {
+
+      that.assign(response.data.data);
+
+      SafeUtil.safeCallback(successCallback)(response);
+
+    }, errorCallback, finalCallback);
+
+  }
+
+
+  //注册
+  httpRegister(username: string, password: string, successCallback?: any, errorCallback?: any, finalCallback?: any) {
+
+    let that = this;
+
+    let form = {
+      username,
+      password,
+    };
+
+    this.httpGet(User.URL_REGISTER, form, function(response: any) {
+
+      that.assign(response.data.data);
+
+      SafeUtil.safeCallback(successCallback)(response);
+
+    }, errorCallback, finalCallback);
+
+  }
+
+
+  //注册
+  httpLogout(successCallback?: any, errorCallback?: any, finalCallback?: any) {
+
+    let that = this;
+
+    let form = {};
+
+    this.httpGet(User.URL_LOGOUT, form, function(response: any) {
+
+      console.info('退出成功!');
+
+      that.role = UserRole.GUEST;
+
+      SafeUtil.safeCallback(successCallback)(response);
+
+    }, errorCallback, finalCallback);
+
+  }
+
+
+  //修改密码
+  httpChangePassword(oldPassword: string, newPassword: string, successCallback?: any, errorCallback?: any, finalCallback?: any) {
+
+    let that = this;
+
+    let form = {
+      oldPassword,
+      newPassword,
+    };
+
+    this.httpPost(User.URL_CHANGE_PASSWORD, form, function(response: any) {
+
+      console.info('修改密码成功!');
+
+      SafeUtil.safeCallback(successCallback)(response);
+
+    }, errorCallback, finalCallback);
+
+  }
+
+
+}
+
+
+
+
diff --git a/src/common/model/user/UserRole.ts b/src/common/model/user/UserRole.ts
new file mode 100644
index 0000000..b4d64be
--- /dev/null
+++ b/src/common/model/user/UserRole.ts
@@ -0,0 +1,38 @@
+import SelectionOption from '../base/option/SelectionOption';
+
+
+enum UserRole {
+  GUEST = 'GUEST',
+  USER = 'USER',
+  ADMINISTRATOR = 'ADMINISTRATOR',
+}
+
+let UserRoles: UserRole[] = Object.keys(UserRole).map(k => k as UserRole);
+
+let UserRoleMap: { [key in keyof typeof UserRole]: SelectionOption } = {
+  GUEST: {
+    'name': '游客',
+    'value': 'GUEST',
+  },
+  USER: {
+    'name': '普通用户',
+    'value': 'USER',
+  },
+  ADMINISTRATOR: {
+    'name': '管理员',
+    'value': 'ADMINISTRATOR',
+  },
+
+};
+
+let UserRoleList: SelectionOption[] = [];
+UserRoles.forEach((type: UserRole, index: number) => {
+  UserRoleList.push(UserRoleMap[type]);
+});
+
+
+export { UserRole, UserRoles, UserRoleMap, UserRoleList };
+
+
+
+
diff --git a/src/common/types/index.d.ts b/src/common/types/index.d.ts
new file mode 100644
index 0000000..8f714cd
--- /dev/null
+++ b/src/common/types/index.d.ts
@@ -0,0 +1,57 @@
+/**
+ * 定义了一个图标行为的对象,这个可以辅助一些组件。
+ */
+export interface IconActionItem {
+  //图片的资源链接(优先级高于iconType)
+  icon?: string
+  //antd的图标类型
+  iconType?: string
+  //名称
+  name?: string
+  //是否可见。默认可见
+  visible?: boolean
+  //点击图标后的行为
+  onClick?: () => void
+}
+
+
+/**
+ * 工具箱的条目,图标采用antd的图标。
+ * 图标不带有高亮样式
+ */
+export interface ToolItem {
+
+  name: string
+
+  //当前是否处于展开状态
+  active: boolean
+
+  //antd的图标样式
+  iconType: string
+
+}
+
+
+/**
+ * 该接口可以像文件树一样展示。
+ * 同时图标带有高亮样式
+ */
+export interface SkeletonItem {
+
+  name: string
+
+  //当前是否处于展开状态
+  active: boolean
+
+  //高亮的图标 资源链接
+  activeIcon: string
+
+  //一般状态的图标 资源链接
+  icon: string
+
+}
+
+/**
+ * 请求的Map,键值对
+ */
+export type RequestParamMap = { [key: string]: string }
diff --git a/src/common/util/AnimateUtil.ts b/src/common/util/AnimateUtil.ts
new file mode 100644
index 0000000..3b03cd9
--- /dev/null
+++ b/src/common/util/AnimateUtil.ts
@@ -0,0 +1,75 @@
+import SafeUtil from "./SafeUtil";
+
+export default class AnimateUtil {
+
+  /**
+   * 动画函数
+   * @param startValue 开始的值
+   * @param endValue 结束的值
+   * @param duration 动画持续时间,单位ms 如果开始值和结束值一样,那么会提前结束。
+   * @param processHandler 过程中处理的回调函数. 该函数能保证第一个值是startValue,结束的值是endValue
+   * @param startHandler 开始的回调函数
+   * @param endHandler 结束的回调函数
+   * @param standard 每隔多少毫秒进行一次处理。
+   *
+   */
+  static animate(
+    startValue: number,
+    endValue: number,
+    duration: number,
+    processHandler: (value: number) => void,
+    startHandler?: (value: number) => void,
+    endHandler?: (value: number) => void,
+    standard?: number
+  ): void {
+
+
+    //规则校验。
+    if (duration <= 0) {
+      console.error("duration值只能是正整数")
+      return
+    }
+
+    if (standard == undefined) {
+      standard = 20
+    } else if (standard <= 0) {
+      console.error("standard值只能是正整数")
+      return
+    }
+
+    let totalStep: number = endValue - startValue
+
+    //从startValue到endValue需要触发几次。
+    let num = Math.ceil(duration / standard)
+
+    //每一步的长度
+    let delta: number = totalStep
+    if (num != 0) {
+      delta = Math.ceil(totalStep / num)
+    }
+
+    //定时执行
+    let sum = 0;
+    SafeUtil.safeCallback(startHandler)(startValue)
+    processHandler(startValue)
+    let intervalHandler = setInterval(() => {
+
+      sum = sum + delta
+      if (Math.abs(sum) >= Math.abs(totalStep)) {
+        //该停止了
+        processHandler(endValue)
+        SafeUtil.safeCallback(endHandler)(endValue)
+
+        //关闭定时器。
+        clearInterval(intervalHandler)
+
+      } else {
+        //更新当前进度
+        processHandler(startValue + sum)
+      }
+
+    }, standard)
+
+  }
+
+}
diff --git a/src/common/util/AntdUtil.ts b/src/common/util/AntdUtil.ts
new file mode 100644
index 0000000..5788d7c
--- /dev/null
+++ b/src/common/util/AntdUtil.ts
@@ -0,0 +1,46 @@
+import DateUtil from "./DateUtil";
+
+/**
+ * 这个类是专门为antd服务的
+ */
+export default class AntdUtil {
+
+
+  /**
+   * 从一个数组中,获取到antd table想要的那种dataSource格式
+   */
+  static getDataSource(columns :any, data :any) {
+
+    let dataSource  :any= []
+    if (!((columns instanceof Array) && (data instanceof Array))) {
+
+      console.error("columns和data必须为数组")
+      return dataSource
+    }
+
+    data.forEach((item, index) => {
+
+      let obj :any = {key: index, id: item.id}
+
+
+      columns.forEach((column, i) => {
+
+        let value = item[column.key]
+
+        //遇到时间自动转成 yyyy-MM-dd HH:mm:ss
+        if (value instanceof Date) {
+          value = DateUtil.simpleDateTime(value)
+        }
+
+        obj[column.key] = value
+      })
+
+      dataSource.push(obj)
+    })
+
+
+    return dataSource
+
+  }
+
+}
diff --git a/src/common/util/ArrayUtil.ts b/src/common/util/ArrayUtil.ts
new file mode 100644
index 0000000..0390a3d
--- /dev/null
+++ b/src/common/util/ArrayUtil.ts
@@ -0,0 +1,142 @@
+export default class ArrayUtil {
+
+
+  //从srcArray中减去childArray的元素
+  static substract(srcArray: [], childArray: any) {
+    if (!srcArray) {
+      return [];
+    }
+
+    if (!childArray || childArray.length === 0) {
+      return srcArray;
+    }
+
+    let newArray: [] = []
+
+    for (let i = 0; i < srcArray.length; i++) {
+
+      //在childArray的那些元素就不要了。
+      if (childArray.indexOf(srcArray[i]) === -1) {
+        newArray.push(srcArray[i])
+      }
+
+    }
+    return newArray
+
+  }
+
+  //不重复添加
+  static uniqueAdd(srcArray: any, childArray: any) {
+    if (!srcArray) {
+      srcArray = []
+    }
+
+    if (!childArray || childArray.length === 0) {
+      return srcArray;
+    }
+
+
+    for (let i = 0; i < childArray.length; i++) {
+
+      //在childArray的那些元素就不要了。
+      if (srcArray.indexOf(childArray[i]) === -1) {
+        srcArray.push(childArray[i])
+      }
+
+    }
+    return srcArray
+  }
+
+  //不重复添加
+  static uniqueAddOne(srcArray: any[], child: any) {
+    if (!srcArray) {
+      srcArray = []
+    }
+
+    if (child === undefined || child === null) {
+      return srcArray;
+    }
+
+    //在childArray的那些元素就不要了。
+    if (srcArray.indexOf(child) === -1) {
+      srcArray.push(child)
+    }
+
+    return srcArray
+  }
+
+  //数组中查找特定元素并返回所有该元素的索引
+  static findAll(a: any, x: any) {
+    let results: number[] = [],
+      len = a.length,
+      pos: number = 0;
+    while (pos < len) {
+      pos = a.indexOf(x, pos);
+      if (pos === -1) {//未找到就退出循环完成搜索
+        break;
+      }
+      results.push(pos);//找到就存储索引
+      pos += 1;//并从下个位置开始搜索
+    }
+    return results;
+  }
+
+
+  //调整元素位置。将dragIndex的元素放到hoverIndex的前面,其余顺移。在拖拽排序中用到。
+  static insertSort(arr: any, dragIndex: any, hoverIndex: any) {
+
+
+    if (!(arr instanceof Array)) {
+      return
+    }
+
+    if (dragIndex < 0 || dragIndex >= arr.length) {
+      console.error("dragIndex = " + dragIndex + " index越界 length = " + arr.length)
+      return
+    }
+
+    if (hoverIndex < 0 || hoverIndex >= arr.length) {
+      console.error("hoverIndex = " + hoverIndex + " index越界 length = " + arr.length)
+      return
+    }
+
+    if (dragIndex === hoverIndex) {
+      return
+    }
+
+    if (dragIndex > hoverIndex) {
+
+      let temp = arr[dragIndex]
+
+      for (let i = dragIndex; i >= hoverIndex + 1; i--) {
+        arr[i] = arr[i - 1]
+      }
+      arr[hoverIndex] = temp
+
+
+    } else {
+
+      let temp = arr[dragIndex]
+
+      for (let i = dragIndex; i <= hoverIndex - 1; i++) {
+        arr[i] = arr[i + 1]
+      }
+      arr[hoverIndex] = temp
+
+    }
+
+
+  }
+
+  //数组去重
+  static unique(arr: string[]) {
+    let newArr: string[] = [];
+    for (let i = 0, item; item = arr[i++];) {
+      if (newArr.indexOf(item) === -1) {
+        newArr.push(item);
+      }
+    }
+    return newArr;
+  };
+
+}
diff --git a/src/common/util/BrowserUtil.ts b/src/common/util/BrowserUtil.ts
new file mode 100644
index 0000000..420984d
--- /dev/null
+++ b/src/common/util/BrowserUtil.ts
@@ -0,0 +1,113 @@
+export default class BrowserUtil {
+
+  //根据cookie键,读取cookie值
+  static readCookie(name: any) {
+    let nameEQ = name + "=";
+    let ca = document.cookie.split(';');
+    for (let i = 0; i < ca.length; i++) {
+      let c = ca[i];
+      while (c.charAt(0) === ' ') c = c.substring(1, c.length);
+      if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
+    }
+    return null;
+  }
+
+
+  /**
+   *
+   * 获取url地址栏的参数
+   * // query string: ?foo=lorem&bar=&baz
+   * var foo = getParameterByName('foo'); // "lorem"
+   * var bar = getParameterByName('bar'); // "" (present with empty value)
+   * var baz = getParameterByName('baz'); // "" (present with no value)
+   * var qux = getParameterByName('qux'); // null (absent)
+   * @param name
+   * @param url
+   * @returns {*}
+   */
+  static getParameterByName(name: string, url?: string): string | null {
+
+    if (!url) {
+      url = window.location.href
+    }
+
+    name = name.replace(/[\[\]]/g, "\\$&");
+
+    let regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)")
+    let results = regex.exec(url);
+
+    if (!results) {
+      return null;
+    }
+
+    if (!results[2]) {
+      return "";
+    }
+
+    return decodeURIComponent(results[2].replace(/\+/g, " "));
+  }
+
+
+  /**
+   * 半颜的方法。注释待补充
+   * @param name
+   * @returns {*}
+   */
+  static getQueryString(name: any) {
+    let reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
+    let r = window.location.search.substr(1).match(reg);
+    if (r != null) return unescape(r[2]);
+    return null;
+  }
+
+
+  static isLocalStorageNameSupported() {
+    let testKey = 'test';
+    let storage = window.localStorage;
+    try {
+      storage.setItem(testKey, '1');
+      storage.removeItem(testKey);
+      return true;
+    } catch (error) {
+      return false;
+    }
+  }
+
+
+  static readLocalStorage(key: any) {
+    if (BrowserUtil.isLocalStorageNameSupported()) {
+      return window.localStorage[key];
+    } else {
+      console.error("not support localStorage.");
+      return null;
+    }
+  }
+
+  static saveToLocalStorage(key: any, content: any) {
+    if (BrowserUtil.isLocalStorageNameSupported()) {
+      window.localStorage[key] = content;
+    } else {
+      console.error("not support localStorage.");
+    }
+  }
+
+
+  static removeLocalStorage(key: any) {
+    if (BrowserUtil.isLocalStorageNameSupported()) {
+      window.localStorage.removeItem(key);
+    } else {
+      console.error("not support localStorage.");
+    }
+  }
+
+
+  /**
+   * 获取完整的host
+   * eg:
+   * https://bamboo.eyeblue.cn
+   */
+  static fullHost() {
+    return window.location.protocol + "//" + window.location.host
+  }
+
+}
diff --git a/src/common/util/ClipboardUtil.ts b/src/common/util/ClipboardUtil.ts
new file mode 100644
index 0000000..91fe97d
--- /dev/null
+++ b/src/common/util/ClipboardUtil.ts
@@ -0,0 +1,39 @@
+import SafeUtil from "./SafeUtil";
+
+export default class ClipboardUtil {
+
+
+  /**
+   * 复制一段文字到粘贴板。
+   *
+   */
+  static copy(text: string, successCallback: () => void, errorCallback?: () => void) {
+
+    let className = "clipboard-textarea-util"
+
+    let textAreaElement: HTMLTextAreaElement | null = document.querySelector("." + className);
+
+    if (!textAreaElement) {
+      textAreaElement = document.createElement("textarea")
+      textAreaElement.className = className
+      textAreaElement.style.cssText = "position:fixed;opacity:0;z-index:0;width:5px;height:5px;"
+      document.body.appendChild(textAreaElement)
+    }
+
+    textAreaElement.value = text
+    textAreaElement.select();
+
+    try {
+      let successful = document.execCommand('copy');
+      if (successful) {
+        successCallback()
+      } else {
+        SafeUtil.safeCallback(errorCallback)()
+      }
+    } catch (err) {
+      console.error('复制失败', err);
+      SafeUtil.safeCallback(errorCallback)()
+    }
+  }
+
+}
diff --git a/src/common/util/DateUtil.ts b/src/common/util/DateUtil.ts
new file mode 100644
index 0000000..80d388f
--- /dev/null
+++ b/src/common/util/DateUtil.ts
@@ -0,0 +1,86 @@
+import moment from "moment"
+
+export default class DateUtil {
+
+  static DEFAULT_FORMAT = "YYYY-MM-DD HH:mm:ss";
+  static SLASH_DATE_FORMAT = "YYYY/MM/DD";
+  static TIME_FORMAT = "HH:mm:ss";
+  static DATE_FORMAT = "YYYY-MM-DD";
+  //紧凑型的时间格式
+  static COMPACT_DATE_FORMAT = "YYYYMMDD";
+
+  static simpleDateTime(date: Date | null): string {
+    if (date == null) {
+      return ""
+    } else {
+      return moment(date).format(DateUtil.DEFAULT_FORMAT)
+    }
+  }
+
+  static simpleDate(date: Date | null): string {
+
+    if (date == null) {
+      return ""
+    } else {
+      return moment(date).format(DateUtil.DATE_FORMAT)
+    }
+  }
+
+  /**
+   * 某个日期的前一天
+   */
+  static lastDay(date: Date | null, format?: string): string {
+
+    if (format === undefined) {
+      format = DateUtil.DEFAULT_FORMAT
+    }
+
+    if (date == null) {
+      return ""
+    } else {
+      return moment(date).add(-1, 'days').format(format)
+    }
+  }
+
+
+  /**
+   * 按照指定格式进行格式化
+   */
+  static format(date: Date | null, formatString: string): string {
+    if (date == null) {
+      return ""
+    } else {
+      return moment(date).format(formatString)
+    }
+  }
+
+
+  /**
+   * 将字符串,按照指定的格式反序列化成为时间对象
+   * @param str
+   */
+  static parse(str: string): Date | null {
+
+    let valid = moment(str).isValid();
+    if (valid) {
+      return moment(str).toDate()
+    } else {
+      return null
+    }
+  }
+
+
+  //将时间字符串转化成js date
+  //deprecated,使用 parse方法替代。
+  static str2Date(str: any): Date {
+
+    let valid = moment(str).isValid();
+    if (valid) {
+      return moment(str).toDate()
+    } else {
+      //console.warn("不能转换成时间对象:", str)
+      return new Date()
+    }
+  }
+
+}
diff --git a/src/common/util/DomUtil.ts b/src/common/util/DomUtil.ts
new file mode 100644
index 0000000..12e9c29
--- /dev/null
+++ b/src/common/util/DomUtil.ts
@@ -0,0 +1,20 @@
+export default class DomUtil {
+
+  static addEvent(el: Node, event: string, handler: (evt: Event) => void): void {
+    if (el.addEventListener) {
+      el.addEventListener(event, handler, true);
+    } else {
+      console.error("不支持事件监听!", el)
+    }
+  }
+
+  static removeEvent(el: Node, event: string, handler: (evt: Event) => void): void {
+
+    if (el.removeEventListener) {
+      el.removeEventListener(event, handler, true);
+    } else {
+      console.error("不支持事件监听!", el)
+    }
+  }
+
+}
diff --git a/src/common/util/HttpUtil.ts b/src/common/util/HttpUtil.ts
new file mode 100644
index 0000000..1a4a364
--- /dev/null
+++ b/src/common/util/HttpUtil.ts
@@ -0,0 +1,92 @@
+import axios, {AxiosRequestConfig} from "axios";
+import SafeUtil from "./SafeUtil";
+
+
+//http请求全部收口在这个工具类中,可以快速切换http框架。
+export default class HttpUtil {
+
+
+  static httpGet(url: any, params = {}, successCallback?: any, errorCallback?: any, finallyCallback?: any, opts?: any) {
+
+    axios
+      .get(url, {
+        params: params
+      })
+      .then(function (response) {
+        SafeUtil.safeCallback(successCallback)(response)
+
+      })
+      .catch(function (error) {
+
+        SafeUtil.safeCallback(errorCallback)(error)
+
+      })
+      .then(function (res) {
+
+        SafeUtil.safeCallback(finallyCallback)(res)
+
+      });
+  }
+
+
+  static httpPost(url: any, params = {}, successCallback?: any, errorCallback?: any, finallyCallback?: any, opts?: any) {
+
+
+    axios
+      .post(url, params, opts)
+      .then(function (response) {
+
+        SafeUtil.safeCallback(successCallback)(response)
+
+      })
+      .catch(function (error) {
+
+        SafeUtil.safeCallback(errorCallback)(error)
+
+      })
+      .then(function (res) {
+
+        SafeUtil.safeCallback(finallyCallback)(res)
+
+      });
+  }
+
+  /**
+   * 上传文件的请求
+   */
+  static httpPostFile(url: string,
+                      formData: FormData,
+                      successCallback?: any,
+                      errorCallback?: any,
+                      finallyCallback?: any,
+                      processCallback?: (progressEvent: any) => void,
+                      opts?: any) {
+
+    const config: AxiosRequestConfig = {
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      },
+      onUploadProgress: processCallback
+    }
+
+    axios
+      .post(url, formData, config)
+      .then(function (response) {
+
+        SafeUtil.safeCallback(successCallback)(response)
+
+      })
+      .catch(function (error) {
+
+        SafeUtil.safeCallback(errorCallback)(error)
+
+      })
+      .then(function (res) {
+
+        SafeUtil.safeCallback(finallyCallback)(res)
+
+      });
+  }
+
+}
+
diff --git a/src/common/util/JsonUtil.ts b/src/common/util/JsonUtil.ts
new file mode 100644
index 0000000..d2bd334
--- /dev/null
+++ b/src/common/util/JsonUtil.ts
@@ -0,0 +1,62 @@
+export default class JsonUtil {
+
+
+  //将一个json字符串转换成 json 数组
+  static parseList(str: any) {
+    if (!str) {
+      return []
+    }
+    if (str instanceof Array) {
+      return str;
+    }
+    try {
+      let list = JSON.parse(str);
+      if (list instanceof Array) {
+        return list;
+      } else {
+        console.error("不能将" + str + "转换成数组");
+        return [];
+      }
+    } catch (e) {
+      console.error("不能将" + str + "转换成JSON");
+      return [];
+    }
+  }
+
+
+  //将一个字符串转成js对象
+  static toObj(str: any) {
+
+    if (!str) {
+      return {}
+    }
+
+    try {
+      return JSON.parse(str)
+    } catch (e) {
+      console.error("不能将json字符串" + str + "转换成对象");
+      return {};
+    }
+  }
+
+  //将一个对象转成json字符串
+  static toJson(obj: any): string {
+
+    if (!obj) {
+      obj = {}
+    }
+
+    return JSON.stringify(obj)
+  }
+
+  //将JSON进行格式化。
+  static prettyJson(json: any) {
+
+    if (typeof json === 'string') {
+      json = JSON.parse(json);
+    }
+
+    return JSON.stringify(json, null, 2)
+  };
+
+}
diff --git a/src/common/util/MessageBoxUtil.ts b/src/common/util/MessageBoxUtil.ts
new file mode 100644
index 0000000..57bbf53
--- /dev/null
+++ b/src/common/util/MessageBoxUtil.ts
@@ -0,0 +1,28 @@
+import {message} from 'antd';
+
+/**
+ * 消息弹出框,统一进行收口
+ */
+export default class MessageBoxUtil {
+
+  static success(content: string) {
+    message.success(content)
+  }
+
+  static info(content: string) {
+    message.info(content)
+  }
+
+  static error(content: string) {
+    message.error(content)
+  }
+
+  static warn(content: string) {
+    message.warn(content)
+  }
+
+  static warning(content: string) {
+    message.warning(content)
+  }
+
+}
diff --git a/src/common/util/NumberUtil.ts b/src/common/util/NumberUtil.ts
new file mode 100644
index 0000000..af40b65
--- /dev/null
+++ b/src/common/util/NumberUtil.ts
@@ -0,0 +1,20 @@
+export default class NumberUtil {
+
+
+  static isInteger(obj: any) {
+    return typeof obj === 'number' && obj % 1 === 0
+  }
+
+  //转换成整型
+  static parseInt(obj: any): number {
+
+    try {
+      return parseInt(obj)
+    } catch (e) {
+      console.error("无法转换成整数", obj)
+      return 0
+    }
+
+  }
+
+}
diff --git a/src/common/util/ObjectUtil.ts b/src/common/util/ObjectUtil.ts
new file mode 100644
index 0000000..fcf6803
--- /dev/null
+++ b/src/common/util/ObjectUtil.ts
@@ -0,0 +1,114 @@
+import DateUtil from "./DateUtil";
+import JsonUtil from "./JsonUtil";
+
+export default class ObjectUtil {
+  //将 extraObj 中的属性全部赋值给standardObj
+  static extend(standardObj: any, extraObj: any) {
+    for (let key in extraObj) {
+      if (extraObj.hasOwnProperty(key)) {
+        if (standardObj.hasOwnProperty(key)) {
+          standardObj[key] = extraObj[key];
+        }
+      }
+    }
+  }
+
+  // 深拷贝,保留standardObj第一层全部属性
+  static deepExtend(standardObj: any, extraObj: any, checkKey: boolean = true) {
+    for (let key in extraObj) {
+      if (checkKey) {
+        if (standardObj.hasOwnProperty(key)) {
+          standardObj[key] =
+            extraObj[key] instanceof Object
+              ? ObjectUtil.deepExtend({}, extraObj[key], false)
+              : extraObj[key];
+        }
+      } else {
+        standardObj[key] =
+            extraObj[key] instanceof Object
+              ? ObjectUtil.deepExtend({}, extraObj[key], false)
+              : extraObj[key];
+      }
+    }
+    return standardObj;
+  }
+
+  //将standardObj的field属性规整成时间格式。
+  static assignDate(standardObj: any, field: any) {
+    if (standardObj.hasOwnProperty(field)) {
+      standardObj[field] = DateUtil.str2Date(standardObj[field]);
+    }
+  }
+
+  //吧standardObj的field属性归整成Clazz类型
+  static assignList(standardObj: any, field: any, Clazz: any) {
+    if (standardObj.hasOwnProperty(field)) {
+      //如果我们要转换成字符串的数组形式,那么this[field]应该是一个字符串才对。
+      if (Clazz === String) {
+        standardObj[field] = JsonUtil.parseList(standardObj[field]);
+
+        return;
+      }
+
+      //下面就是转换实体数组了。
+      let beans = standardObj[field];
+      if (!beans) {
+        //服务器返回这个字段为空 维持构造函数中的默认值(一般而言是一个[])
+        standardObj[field] = new standardObj.constructor()[field];
+        return;
+      }
+
+      standardObj[field] = [];
+
+      if (!Clazz) {
+        return;
+      }
+
+      for (let i = 0; i < beans.length; i++) {
+        let bean = beans[i];
+        let clazz = new Clazz();
+
+        if (clazz.assign) {
+          clazz.assign(bean);
+        } else {
+          console.error(clazz);
+          console.error("没有定义assign方法");
+        }
+
+        standardObj[field].push(clazz);
+      }
+    }
+  }
+
+  //check whether an obj is empty
+  static isEmptyObject(obj: object) {
+    if (typeof obj !== "object") {
+      console.error("判定的不是obj对象");
+      return true;
+    }
+
+    for (let key in obj) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * 将键值对转换成  age=18&name=xxx 的形式
+   */
+  static param(map: any) {
+    let arr: string[] = [];
+    for (let key in map) {
+      if (map.hasOwnProperty(key)) {
+        let value = map[key];
+        arr[arr.length] =
+          encodeURIComponent(key) +
+          "=" +
+          encodeURIComponent(value == null ? "" : value);
+      }
+    }
+
+    return arr.join("&");
+  }
+}
diff --git a/src/common/util/SafeUtil.ts b/src/common/util/SafeUtil.ts
new file mode 100644
index 0000000..f6fd3b0
--- /dev/null
+++ b/src/common/util/SafeUtil.ts
@@ -0,0 +1,35 @@
+export default class SafeUtil {
+
+  //安全的调用某个函数,函数不存在,创建一个空函数。
+  static safeCallback(callback: any) {
+    if (typeof callback === "function") {
+      return callback
+    } else {
+      return function () {
+      }
+    }
+  }
+
+
+  //空函数
+  static noop = () => {
+  };
+
+
+  //停止事件冒泡
+  static stopPropagation(e: any) {
+    if (!e) {
+      return
+    }
+
+    if (e.stopPropagation) {
+      //系统的点击事件
+      e.stopPropagation()
+    } else if (e.domEvent && e.domEvent.stopPropagation) {
+      //antd的事件
+      e.domEvent.stopPropagation()
+
+    }
+  }
+
+}
diff --git a/src/common/util/StringUtil.ts b/src/common/util/StringUtil.ts
new file mode 100644
index 0000000..6337f88
--- /dev/null
+++ b/src/common/util/StringUtil.ts
@@ -0,0 +1,248 @@
+export default class StringUtil {
+
+  //大写字母
+  static UPPER_CASES = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+  //下划线转驼峰
+  static underScoreToCamel(str: string) {
+
+    if (!str) {
+      console.error('不能转换空的驼峰字符串。')
+      return str
+    }
+
+    const regex = /_[a-z]/gm;
+
+    return str.replace(regex, function (letter: any, index: any) {
+      console.log("letter", letter)
+      return letter.substr(1).toUpperCase()
+    })
+  }
+
+  //转换成首字母小写的驼峰法
+  static lowerCamel(str: any) {
+
+    if (!str) {
+      console.error('不能转换空的驼峰字符串。')
+      return str
+    }
+
+    return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter: any, index: any) {
+      return index === 0 ? letter.toLowerCase() : letter.toUpperCase()
+    }).replace(/\s+/g, '')
+  }
+
+
+  //转换成全部小写的使用 /分隔的字符串
+  static lowerSlash(str: any) {
+    return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter: any, index: any) {
+      return '/' + letter.toLowerCase()
+    }).replace(/\s+/g, '')
+  }
+
+
+  //将首字母大写
+  static capitalize(str: any) {
+
+    if (!str) {
+      return str;
+    }
+
+    str = str.replace(/^\w/, (c: any) => c.toUpperCase());
+
+    return str
+  }
+
+  //将首字母小写
+  static lower(str: any) {
+
+    if (!str) {
+      return str;
+    }
+
+    str = str.replace(/^\w/, (c: any) => c.toLowerCase());
+
+    return str
+  }
+
+  //获取一个function的名字
+  static functionName(func: any) {
+    // Match:
+    // - ^          the beginning of the string
+    // - function   the word 'function'
+    // - \s+        at least some white space
+    // - ([\w\$]+)  capture one or more valid JavaScript identifier characters
+    // - \s*        optionally followed by white space (in theory there won't be any here,
+    //              so if performance is an issue this can be omitted[1]
+    // - \(         followed by an opening brace
+    //
+    let result = /^function\s+([\w\$]+)\s*\(/.exec(func.toString())
+
+    return result ? result[1] : '' // for an anonymous function there won't be a match
+  }
+
+  //check whether an obj is empty
+  static isEmptyObject(obj: any) {
+
+    if (!obj) {
+      return true;
+    }
+
+    for (let key in obj) {
+      return false;
+    }
+    return true
+  }
+
+
+  /**
+   * 获取到上num级路径
+   * 例子: /bamboo/setting/notification -> /bamboo/setting
+   * @param path 原路径
+   * @param num 上几级,默认1级
+   */
+  static prePath(path: string, num: number = 1): string {
+    if (!path) {
+      return path
+    }
+
+    //去除掉最后的/符号。
+    let parts: string[] = path.split("/");
+    //去除所有的空
+    parts = parts.filter((item: string, index: number) => {
+      return item !== ""
+    })
+
+    //前面几层就是删掉前面几个元素。如果超出了,会全部删掉。
+    parts.splice(parts.length - num)
+
+    return "/" + parts.join("/")
+  }
+
+
+  static startWith(str: any, prefix: any) {
+    if (typeof prefix === 'undefined' || prefix === null || prefix === '' || typeof str === 'undefined' || str === null || str.length === 0 || prefix.length > str.length) {
+      return false
+    }
+
+    return str.substr(0, prefix.length) === prefix
+  }
+
+  static endWith(str: any, suffix: any) {
+    if (suffix === null || suffix === '' || str === null || str.length === 0 || suffix.length > str.length) {
+      return false
+    }
+
+    return str.substring(str.length - suffix.length) === suffix
+  }
+
+
+  /**
+   * 去除掉开头的前缀
+   * @param str 待处理的字符串
+   * @param prefix 前缀
+   */
+  static trimPrefix(str: string | null, prefix: string): string {
+
+    if (!str) {
+      return ""
+    } else {
+      if (str.substr(0, prefix.length) === prefix) {
+        return str.substr(prefix.length)
+      } else {
+        return str
+      }
+    }
+
+  }
+
+  /**
+   * 去除掉后缀
+   * @param str 待处理的字符串
+   * @param suffix 前缀
+   */
+  static trimSuffix(str: string | null, suffix: string): string {
+
+    if (!str) {
+      return ""
+    } else {
+      if (str.substring(str.length - suffix.length) === suffix) {
+        return str.substr(0, str.length - suffix.length)
+      } else {
+        return str
+      }
+    }
+
+  }
+
+
+  //在字符串a后面追加字符串b
+  static append(a: any, b: any, separator = "") {
+
+    if (a === null || a === "" || typeof a !== "string") {
+      return b;
+    } else {
+      return a + separator + b;
+    }
+  }
+
+
+  static isBlank(text: string | null | undefined): boolean {
+
+    if (text === null || text == undefined) {
+      return true
+    }
+
+    return text.trim() === "";
+
+  }
+
+  static isNotBlank(text: string | null | undefined): boolean {
+
+    return !StringUtil.isBlank(text)
+
+  }
+
+
+  //将时间戳转换成62进制
+  static generateUniqueCode(num?: number): string {
+    //获取时间戳
+
+    if (num === undefined) {
+      num = new Date().getTime()
+    }
+
+    let standardString = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+    let result;
+    let list: number[] = [];
+    let len = standardString.length;
+    let level;
+    for (level = 0; Math.floor(num / len) > 0; level++) {
+      result = num % len;
+      list.push(result)
+      num = (num - result) / len
+    }
+    list.push(num);
+    let code = '';
+    list.forEach((item) => {
+      code = standardString[item] + code;
+    })
+    return code
+  }
+
+  //字符串转换成十进制
+  static parseUniqueCode(str: string): number {
+
+    let exchangeString = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+    let strNew = str.split("");
+    let list: number[] = [];
+    strNew.map((item: string) => {
+      list.push(exchangeString.indexOf(item));
+    });
+    let num = 0;
+    for (let i = 0; i < list.length; i++) {
+      num += list[i] * Math.pow(exchangeString.length, list.length - i - 1);
+    }
+    return num;
+  }
+}
diff --git a/src/index.tsx b/src/index.tsx
index f5185c1..4146d1d 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,6 +1,5 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
-import './index.css';
 import App from './App';
 import * as serviceWorker from './serviceWorker';
 
diff --git a/src/pages/Frame.less b/src/pages/Frame.less
new file mode 100644
index 0000000..ee2a1eb
--- /dev/null
+++ b/src/pages/Frame.less
@@ -0,0 +1,79 @@
+
+.pages-frame {
+
+
+  position: fixed;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 10;
+
+
+  .ant-layout {
+
+    @header-height: 50px;
+    height: 100%;
+
+    .ant-layout-sider {
+      .username {
+        padding: 30px 20px;
+        text-align: center;
+        color: #bbb;
+      }
+    }
+
+    .ant-layout-header {
+
+      padding: 0 20px;
+      line-height: @header-height;
+      height: @header-height;
+      background: white;
+      border-bottom: 1px solid #eee;
+
+      .logo-title-area {
+
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+
+        cursor: pointer;
+
+        .header-logo {
+          width: 30px;
+        }
+
+        .header-title {
+          font-size: 20px;
+          font-weight: bold;
+          padding-left: 10px;
+        }
+      }
+
+
+    }
+
+    .ant-layout-content {
+
+      padding: 0;
+
+      .pages-content {
+        width: 100%;
+        height: 100%;
+        overflow: auto;
+        padding: 10px;
+      }
+
+    }
+
+    .ant-layout-footer {
+
+      background: white;
+      border-top: 1px solid #eee;
+      padding: 10px;
+
+    }
+
+  }
+
+}
diff --git a/src/pages/Frame.tsx b/src/pages/Frame.tsx
new file mode 100644
index 0000000..a1379c6
--- /dev/null
+++ b/src/pages/Frame.tsx
@@ -0,0 +1,173 @@
+import React from 'react';
+import { Link, Redirect, Route, RouteComponentProps, withRouter } from 'react-router-dom';
+
+import './Frame.less';
+import BambooComponent from '../common/component/BambooComponent';
+import UserLogin from './user/Login';
+
+import UserProfile from './user/Profile';
+import ArticleDetail from './article/Detail';
+import ArticleList from './article/List';
+import ArticleEdit from './article/Edit';
+
+import { Layout, Menu } from 'antd';
+import MenuManager from '../common/menu/MenuManager';
+import MenuItem from '../common/menu/MenuItem';
+import { SelectParam } from 'antd/lib/menu';
+import LogoSvg from '../assets/image/logo.png';
+import Index from './index/Index';
+import User from '../common/model/user/User';
+import Moon from '../common/model/global/Moon';
+import Sun from '../common/model/global/Sun';
+import { UserRole } from '../common/model/user/UserRole';
+
+const { Header, Content, Footer, Sider } = Layout;
+const { SubMenu } = Menu;
+
+
+interface IProps extends RouteComponentProps<{}> {
+
+}
+
+interface IState {
+  collapsed: boolean
+
+}
+
+
+class RawFrame extends BambooComponent {
+
+  user: User = Moon.getSingleton().user;
+
+  constructor(props: IProps) {
+    super(props);
+
+
+    this.state = {
+      collapsed: false,
+    };
+  }
+
+
+  componentDidMount() {
+
+    //装载全局的路由
+    Sun.getSingleton().reactRouter = this.props.history;
+
+    this.fetchInfo();
+
+  }
+
+  //获取当前登录者的信息
+  fetchInfo() {
+    let that = this;
+
+    let whitePaths = ['/user/login', '/user/register'];
+    //如果当前本身是登录界面,那么不去获取。
+    if (whitePaths.indexOf(this.props.location.pathname) == -1) {
+      this.user.httpInfo(function() {
+        that.updateUI();
+      });
+    }
+
+  }
+
+
+  onCollapse(collapsed: boolean) {
+    console.log(collapsed);
+    this.setState({ collapsed });
+  };
+
+  onSelect(param: SelectParam) {
+    let that = this;
+
+    let menuManager: MenuManager = MenuManager.getSingleton();
+    menuManager.selectMenu(param.key);
+
+    //打到对应的页面中。
+    if (param.key == '/user/logout') {
+      this.props.history.push('/user/login');
+    } else {
+      this.props.history.push(param.key);
+    }
+
+    this.updateUI();
+  }
+
+  goHome() {
+    this.props.history.push('/');
+  }
+
+  render() {
+
+    let that = this;
+
+    let menuManager: MenuManager = MenuManager.getSingleton();
+
+    let menuItems: MenuItem[] = menuManager.getMenuItems();
+
+    let user: User = Moon.getSingleton().user;
+
+    return (
+
+      
+ + +
+ {user.role === UserRole.GUEST ? + '未登录' : + {user.username} + } +
+ + { + menuItems.map((menuItem: MenuItem, index: number) => { + return ( + + {menuItem.name} + + ); + }) + } + +
+ +
+
+ logo + bamboo +
+
+ + +
+ + + }/> + + + + + + + }/> + + + +
+
+
Eyeblue ©2020 Copyright
+
+
+
+ ); + } +} + + +const Frame = withRouter>(RawFrame); +export default Frame; diff --git a/src/pages/article/Detail.less b/src/pages/article/Detail.less new file mode 100644 index 0000000..15a920f --- /dev/null +++ b/src/pages/article/Detail.less @@ -0,0 +1,11 @@ + +.article-detail { + padding: 20px; + overflow: auto; + background: white; + + +} +.article { + width: 100%; +} diff --git a/src/pages/article/Detail.tsx b/src/pages/article/Detail.tsx new file mode 100644 index 0000000..a9f2ba6 --- /dev/null +++ b/src/pages/article/Detail.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import './Detail.less'; +import BambooComponent from '../../common/component/BambooComponent'; +import Article from '../../common/model/article/Article'; +import { Button, Col, Row, Spin } from 'antd'; +import InfoCell from '../widget/InfoCell'; +import StringUtil from '../../common/util/StringUtil'; +import BambooTitle from '../widget/BambooTitle'; + +interface RouteParam { + uuid: string +} + +interface IProps extends RouteComponentProps { + +} + +interface IState { +} + +export default class Detail extends BambooComponent { + + article: Article = new Article(this); + + constructor(props: IProps) { + super(props); + + + this.state = {}; + + //article的id设置晚了就来不及了 + let match = this.props.match; + if (match.params.uuid) { + this.article.uuid = match.params.uuid; + } + + } + + + componentDidMount() { + //刷新一下列表 + let that = this; + + let match = this.props.match; + let article = that.article; + + article.httpDetail(function() { + + }); + + } + + + render() { + + let that = this; + let article: Article = that.article; + //router中传入的路由相关对象 + let match = this.props.match; + + + return ( +
+ + + + + + + + + +
+ + {/**/} + {/* */} + {/* */} + {/* {article.title}*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* {article.author}*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* {article.path}*/} + {/* */} + {/* */} + + + {/**/} + +
+ +
+ + + + +
+ ); + } +} + + diff --git a/src/pages/article/Edit.less b/src/pages/article/Edit.less new file mode 100644 index 0000000..8a72844 --- /dev/null +++ b/src/pages/article/Edit.less @@ -0,0 +1,5 @@ + +.article-edit { + padding: 20px; + background: white; +} diff --git a/src/pages/article/Edit.tsx b/src/pages/article/Edit.tsx new file mode 100644 index 0000000..fac47c4 --- /dev/null +++ b/src/pages/article/Edit.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Col, DatePicker, Input, message as MessageBox, Row, Spin } from 'antd'; +import BambooComponent from '../../common/component/BambooComponent'; +import Article from '../../common/model/article/Article'; +import StringUtil from '../../common/util/StringUtil'; +import './Edit.less'; +import BambooTitle from '../widget/BambooTitle'; + +const { MonthPicker, RangePicker } = DatePicker; + + +interface RouteParam { + uuid: string +} + + +interface IProps { + +} + +interface IState { +} + +class RawEdit extends BambooComponent { + + createMode: boolean = true; + article: Article = new Article(this); + + constructor(props: IProps) { + super(props); + this.state = { + + }; + } + + componentDidMount() { + + //刷新一下列表 + + let article = this.article; + + + } + + + //获取上一级目录 + getPrePath() { + + return "" + + } + + //返回到列表页面 + goToIndex() { + + } + + render() { + + let that = this; + + //router中传入的路由相关对象 + let article = this.article; + + + + + return ( +
+ + + + + +
+ ); + } +} + +export default RawEdit; + + + diff --git a/src/pages/article/List.less b/src/pages/article/List.less new file mode 100644 index 0000000..2222f42 --- /dev/null +++ b/src/pages/article/List.less @@ -0,0 +1,8 @@ + +.article-list { + padding: 5px; + + .ant-table { + background-color: white; + } +} diff --git a/src/pages/article/List.tsx b/src/pages/article/List.tsx new file mode 100644 index 0000000..5567f64 --- /dev/null +++ b/src/pages/article/List.tsx @@ -0,0 +1,172 @@ +import React from 'react'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import './List.less'; +import BambooComponent from '../../common/component/BambooComponent'; +import SortDirection from '../../common/model/base/SortDirection'; +import Pager from '../../common/model/base/Pager'; +import Article from '../../common/model/article/Article'; +import Table, { ColumnProps } from 'antd/lib/table'; +import StringUtil from '../../common/util/StringUtil'; +import { Button, message as MessageBox, Popconfirm } from 'antd'; +import DateUtil from '../../common/util/DateUtil'; +import FilterPanel from '../widget/filter/FilterPanel'; +import TableEmpty from '../widget/TableEmpty'; +import Sun from '../../common/model/global/Sun'; +import BambooTitle from '../widget/BambooTitle'; + + +interface IProps extends RouteComponentProps { + +} + +interface IState { + +} + + +export default class List extends BambooComponent { + + //获取分页的一个帮助器 + pager: Pager
= new Pager
(this, Article, 10); + + constructor(props: IProps) { + super(props); + + + this.state = {}; + } + + + componentDidMount() { + + //刷新一下列表 + let that = this; + + that.pager.enableHistory(); + + that.refresh(); + } + + search() { + let that = this; + that.pager.page = 0; + that.refresh(); + } + + refresh() { + + let that = this; + + //如果没有任何的排序,默认使用时间倒序 + if (!that.pager.getCurrentSortFilter()) { + that.pager.setFilterValue('orderCreateTime', SortDirection.DESC); + } + + that.pager.httpList(); + } + + createArticle() { + + let that = this; + + + //router中传入的路由相关对象 + let match = this.props.match; + + + let url: string = StringUtil.prePath(match.path) + `/create`; + + Sun.navigateTo(url); + } + + render() { + + let that = this; + + //router中传入的路由相关对象 + let match = this.props.match; + + //pager对象 + let pager = this.pager; + + const columns: ColumnProps
[] = [{ + title: '文章名称', + dataIndex: 'title', + render: (text: any, record: Article, index: number): React.ReactNode => ( + {record.title} + ), + }, { + title: '路径', + dataIndex: 'path', + }, { + title: '作者', + dataIndex: 'author', + }, { + title: '创建时间', + dataIndex: 'createTime', + sorter: true, + sortOrder: pager.getDefaultSortOrder('createTime'), + sortDirections: [SortDirection.DESCEND, SortDirection.ASCEND], + render: (text: any, record: Article, index: number): React.ReactNode => ( + DateUtil.simpleDateTime(text) + ), + }, { + title: '修改时间', + dataIndex: 'updateTime', + sorter: true, + sortOrder: pager.getDefaultSortOrder('updateTime'), + sortDirections: [SortDirection.DESCEND, SortDirection.ASCEND], + render: (text: any, record: Article, index: number): React.ReactNode => ( + DateUtil.simpleDateTime(text) + ), + }, { + title: '操作', + dataIndex: 'action', + render: (text: any, record: Article) => ( + + + + 编辑 + + + { + record.httpDel(function() { + MessageBox.success('删除成功!'); + that.refresh(); + }); + }} okText="确认" cancelText="取消"> + 编辑 + + + ), + }]; + + return ( +
+ + + + + + +
+ +
+ ) }} + /> + + ); + } +} + + diff --git a/src/pages/index/Index.less b/src/pages/index/Index.less new file mode 100644 index 0000000..13016d9 --- /dev/null +++ b/src/pages/index/Index.less @@ -0,0 +1,5 @@ + +.index-page { + + padding: 20px; +} diff --git a/src/pages/index/Index.tsx b/src/pages/index/Index.tsx new file mode 100644 index 0000000..71732be --- /dev/null +++ b/src/pages/index/Index.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import './Index.less'; +import BambooComponent from '../../common/component/BambooComponent'; +import Moon from '../../common/model/global/Moon'; +import User from '../../common/model/user/User'; + +interface IProps extends RouteComponentProps { + +} + +interface IState { + +} + + +export default class Index extends BambooComponent { + + user: User = Moon.getSingleton().user; + + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + + componentDidMount() { + let that = this; + + //假装获取一下详情 + this.user.httpInfo(function() { + that.updateUI(); + }); + + } + + + render() { + + let that = this; + + return ( +
+ +

欢迎来到蓝眼博客,这里是首页,内容正在开发中。

+ +
+ ); + } +} + + diff --git a/src/pages/user/ChangePasswordModal.less b/src/pages/user/ChangePasswordModal.less new file mode 100644 index 0000000..904adc9 --- /dev/null +++ b/src/pages/user/ChangePasswordModal.less @@ -0,0 +1,19 @@ + +.change-password-modal { + + .title { + text-align: center; + font-size: 16px; + margin-bottom: 15px; + } + + .btn-area { + text-align: center; + + .login-form-button { + margin: 15px; + } + } + + +} diff --git a/src/pages/user/ChangePasswordModal.tsx b/src/pages/user/ChangePasswordModal.tsx new file mode 100644 index 0000000..0b9a922 --- /dev/null +++ b/src/pages/user/ChangePasswordModal.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import "./ChangePasswordModal.less" +import BambooComponent from "../../common/component/BambooComponent"; +import {Col, Modal, Row} from 'antd'; +import User from "../../common/model/user/User"; +import Moon from "../../common/model/global/Moon"; + +interface IProps { + + onSuccess: () => void + onClose: () => void +} + +interface IState { + +} + +export default class ChangePasswordModal extends BambooComponent { + + user: User = Moon.getSingleton().user + + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + + static open(onSuccess: () => void) { + + let modal = Modal.success({ + okCancel: false, + okButtonProps: { + className: "display-none" + }, + icon: null, + content: { + + onSuccess() + modal.destroy() + }} + onClose={() => { + modal.destroy() + }}/>, + }) + + } + + componentDidMount() { + + + } + + handleSubmit(e: any) { + e.preventDefault(); + + let that = this + + let user = that.user + + + user.httpChangePassword("", "", function () { + + that.props.onSuccess() + + }) + }; + + render() { + + let that = this + + return ( +
+ + +
+ +
+ 修改密码 +
+ + + + + + ); + } +} + + + diff --git a/src/pages/user/Login.less b/src/pages/user/Login.less new file mode 100644 index 0000000..c0001d9 --- /dev/null +++ b/src/pages/user/Login.less @@ -0,0 +1,28 @@ + +.user-login { + + + padding-top: 150px; + + .welcome { + text-align: center; + font-size: 18px; + font-weight: bold; + padding-bottom: 40px; + } + + .login-form { + + + .ant-input-prefix { + .anticon { + color: rgba(0, 0, 0, .25); + } + } + } + + .login-form-button { + width: 100%; + } + +} diff --git a/src/pages/user/Login.tsx b/src/pages/user/Login.tsx new file mode 100644 index 0000000..dcf241d --- /dev/null +++ b/src/pages/user/Login.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import {RouteComponentProps} from "react-router-dom"; +import "./Login.less" +import BambooComponent from "../../common/component/BambooComponent"; +import {Col, message as MessageBox, Row} from 'antd'; +import User from "../../common/model/user/User"; +import Moon from "../../common/model/global/Moon"; + +interface IProps extends RouteComponentProps { + +} + +interface IState { + +} + +class RawLogin extends BambooComponent { + + user: User = Moon.getSingleton().user + + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + componentDidMount() { + + + //立即退出 + this.logout() + } + + //退出登录 + logout() { + + this.user.httpLogout() + + } + + + handleSubmit(e: any) { + e.preventDefault(); + + let that = this + + let user = that.user + + user.httpLogin("xx", "xx", function () { + MessageBox.success("登录成功!") + + that.props.history.push('/') + + }) + }; + + render() { + + let that = this + + //router中传入的路由相关对象 + let match = this.props.match; + let location = this.props.location; + let history = this.props.history; + + return ( +
+ + +
+ +
+ 欢迎登录 +
+ + + + + + + ); + } +} + + +export default RawLogin; diff --git a/src/pages/user/Profile.less b/src/pages/user/Profile.less new file mode 100644 index 0000000..8915e9b --- /dev/null +++ b/src/pages/user/Profile.less @@ -0,0 +1,7 @@ +.page-profile { + + background-color: white; + + padding: 15px; + +} diff --git a/src/pages/user/Profile.tsx b/src/pages/user/Profile.tsx new file mode 100644 index 0000000..a3990b9 --- /dev/null +++ b/src/pages/user/Profile.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import "./Profile.less" +import BambooComponent from "../../common/component/BambooComponent"; +import {Button} from 'antd'; +import InfoCell from "../widget/InfoCell"; +import User from "../../common/model/user/User"; +import Moon from "../../common/model/global/Moon"; +import DateUtil from "../../common/util/DateUtil"; +import ChangePasswordModal from "./ChangePasswordModal"; +import MessageBoxUtil from "../../common/util/MessageBoxUtil"; +import BambooTitle from '../widget/BambooTitle'; + +interface IProps { + name: string + firstSpan?: number +} + +interface IState { + +} + +export default class Profile extends BambooComponent { + + constructor(props: IProps) { + super(props) + this.state = {} + } + + + componentDidMount() { + + } + + changePassword() { + + ChangePasswordModal.open(() => { + MessageBoxUtil.success("修改密码成功!") + }) + } + + render() { + + let that = this + let name: string = this.props.name + let user: User = Moon.getSingleton().user + + return ( + +
+ + + + + +
+ + {user.username} + + + + {DateUtil.simpleDateTime(user.createTime)} + + +
+ +
+ ) + } +} + diff --git a/src/pages/widget/BambooTitle.less b/src/pages/widget/BambooTitle.less new file mode 100644 index 0000000..426da05 --- /dev/null +++ b/src/pages/widget/BambooTitle.less @@ -0,0 +1,32 @@ +//页面的导航样式 +.widget-bamboo-title { + margin-top: 10px; + margin-bottom: 10px; + border-bottom: 1px solid #E6E6E6; + display: flex; + justify-content: space-between; + align-items: flex-end; + + .item { + font-size: 18px; + color: #778195; + display: inline-block; + box-sizing: content-box; + line-height: 30px; + + &:hover, &.active { + color: #333; + border-bottom: 2px solid #333; + } + } + + .tool { + flex: 1; + + padding-bottom: 5px; + display: flex; + justify-content: flex-end; + align-items: flex-end; + + } +} diff --git a/src/pages/widget/BambooTitle.tsx b/src/pages/widget/BambooTitle.tsx new file mode 100644 index 0000000..2753f8c --- /dev/null +++ b/src/pages/widget/BambooTitle.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import "./BambooTitle.less" +import BambooComponent from '../../common/component/BambooComponent'; + +interface IProps { + name: React.ReactNode +} + +interface IState { + +} + +/** + * 标题导航快捷插件 + */ +export default class BambooTitle extends BambooComponent { + + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + componentDidMount() { + + //刷新一下列表 + let that = this + + } + + componentWillReceiveProps(nextProps: Readonly, nextContext: any): void { + let that = this + } + + render() { + + let that = this + + return ( +
+ + {that.props.name} + + + {that.props.children} + +
+ ); + } +} diff --git a/src/pages/widget/ChangeNumModal.less b/src/pages/widget/ChangeNumModal.less new file mode 100644 index 0000000..077b8f3 --- /dev/null +++ b/src/pages/widget/ChangeNumModal.less @@ -0,0 +1,5 @@ +.widget-change-num-modal { + + background-color: aliceblue; + +} diff --git a/src/pages/widget/ChangeNumModal.tsx b/src/pages/widget/ChangeNumModal.tsx new file mode 100644 index 0000000..5030164 --- /dev/null +++ b/src/pages/widget/ChangeNumModal.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import Button from 'antd/lib/button'; +import {message as MessageBox, Input} from "antd" + +interface IProps { + value: number + title?: string + onSuccess: (val: number) => void + onClose: () => void +} + +interface IState { + + innerValue: number +} + +export default class ChangeNumModal extends React.Component { + + constructor(props: IProps) { + super(props); + this.state = { + innerValue: this.props.value + }; + } + + + componentDidMount() { + + } + + onSubmitClick() { + if (this.state.innerValue) { + this.props.onSuccess(this.state.innerValue) + } else { + MessageBox.error("没有填写值,提交失败!") + } + } + + render() { + + let that = this + + return ( +
+ +
+

+ {that.props.title} +

+
+ + { + that.setState({ + innerValue: parseInt(e.target.value) + }) + + }}/> + +
+ + + + +
+ + +
+ ); + } +} + + + + diff --git a/src/pages/widget/FrameLoading.tsx b/src/pages/widget/FrameLoading.tsx new file mode 100644 index 0000000..58bc706 --- /dev/null +++ b/src/pages/widget/FrameLoading.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import "./TableEmpty.less" +import BambooComponent from "../../common/component/BambooComponent"; +import LogoBluePng from "../../assets/image/logo-blue.png"; + +interface IProps { +} + +interface IState { +} + +/** + * 主站加载控件。 + * 在主站加载过程中或者在工作台加载过程中使用 + * 该控件采用fixed布局,会占据全屏 + */ +export default class FrameLoading extends BambooComponent { + + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + render() { + + let that = this + + return ( +
+
+
+ 加载按钮 +
+
+ 加载中... +
+
+
+ ) + } +} diff --git a/src/pages/widget/InfoCell.less b/src/pages/widget/InfoCell.less new file mode 100644 index 0000000..47ffdfa --- /dev/null +++ b/src/pages/widget/InfoCell.less @@ -0,0 +1,14 @@ +.castle-widget-info-cell { + + margin-bottom: 10px; + .info-cell-name { + color: #99a9bf; + font-size: 14px; + font-weight: bold; + } + + .info-cell-content { + font-size: 15px; + } + +} diff --git a/src/pages/widget/InfoCell.tsx b/src/pages/widget/InfoCell.tsx new file mode 100644 index 0000000..4236298 --- /dev/null +++ b/src/pages/widget/InfoCell.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import "./InfoCell.less" +import {Col, Row} from "antd" + +interface IProps { + name: string + firstSpan?: number +} + +interface IState { + +} + +export default class InfoCell extends React.Component { + + constructor(props: IProps) { + super(props) + this.state = {} + } + + + + componentDidMount() { + + } + + + render() { + + let that = this + let name: string = this.props.name + + let firstSpan: number = 8 + if (this.props.firstSpan !== undefined) { + firstSpan = this.props.firstSpan + } + let secondSpan: number = 24 - firstSpan + + return ( + +
{name} + + {that.props.children} + + + ) + } +} + diff --git a/src/pages/widget/StringsPicker.less b/src/pages/widget/StringsPicker.less new file mode 100644 index 0000000..3430139 --- /dev/null +++ b/src/pages/widget/StringsPicker.less @@ -0,0 +1,6 @@ + +.widget-strings-picker { + + width: 100%; + +} diff --git a/src/pages/widget/StringsPicker.tsx b/src/pages/widget/StringsPicker.tsx new file mode 100644 index 0000000..d8e696f --- /dev/null +++ b/src/pages/widget/StringsPicker.tsx @@ -0,0 +1,179 @@ +import React from 'react'; +import "./StringsPicker.less" +import {Input, Tag, Tooltip} from 'antd'; +import BambooComponent from "../../common/component/BambooComponent"; +import SafeUtil from "../../common/util/SafeUtil"; + +interface IProps { + value?: string | null, + onChange?: (value: string | null) => void, + editable?: boolean +} + +interface IState { + tags: string[], + inputVisible: boolean, + inputValue: string +} + +/** + * 这个控件可以填充一组字符串,同时具备回填的能力。 + * 格式为 json list 的字符串形式 + */ +export default class StringsPicker extends BambooComponent { + + innerValue: string | null = null + + inputDom: any = null + + constructor(props: IProps) { + super(props); + + this.state = { + tags: [], + inputVisible: false, + inputValue: "" + }; + } + + componentDidMount() { + + //刷新一下列表 + let that = this + + this.fillBack() + } + + + componentWillReceiveProps(nextProps: Readonly, nextContext: any): void { + let that = this + + //每次刷新都尝试去回填。 + if (nextProps.value != this.innerValue) { + + that.fillBack(nextProps.value) + } + + } + + //回填 + fillBack(selectedValue?: string | null) { + let that = this + + if (!selectedValue) { + selectedValue = this.props.value + } + + let arr: string[] = [] + if (selectedValue) { + try { + arr = JSON.parse(selectedValue); + } catch (e) { + arr = [] + } + that.setState({tags: arr}) + } + + this.setState({ + tags: arr + }) + + } + + + handleClose(removedTag: string) { + const tags = this.state.tags.filter(tag => tag !== removedTag); + console.log(tags); + + this.setState({tags}); + + + this.innerValue = JSON.stringify(tags) + //往外通知 + SafeUtil.safeCallback(this.props.onChange)(this.innerValue) + }; + + showInput = () => { + this.setState({inputVisible: true}, () => this.inputDom.focus()); + }; + + handleInputChange(e: any) { + + this.setState({inputValue: e.target.value}); + + }; + + handleInputConfirm = () => { + const {inputValue} = this.state; + let {tags} = this.state; + if (inputValue && tags.indexOf(inputValue) === -1) { + tags = [...tags, inputValue]; + } + console.log(tags); + this.setState({ + tags, + inputVisible: false, + inputValue: '', + }); + + this.innerValue = JSON.stringify(tags) + + //往外通知 + SafeUtil.safeCallback(this.props.onChange)(this.innerValue) + + }; + + /** + * 由于Select控件具有内部自动搜索的能力,因此value要用nickname,realname,no的拼接 + */ + render() { + let tags: string[] = this.state.tags + let inputVisible: boolean = this.state.inputVisible + let inputValue: string = this.state.inputValue + + let editable: boolean = true + if (this.props.editable !== undefined) { + editable = this.props.editable + } + + + return ( +
+ {tags.map((tag, index) => { + const isLongTag = tag.length > 20; + const tagElem = ( + this.handleClose(tag)}> + {isLongTag ? `${tag.slice(0, 20)}...` : tag} + + ); + return isLongTag ? ( + + {tagElem} + + ) : ( + tagElem + ); + })} + {inputVisible && editable && ( + { + this.inputDom = dom + }} + type="text" + size="small" + style={{width: 120}} + value={inputValue} + onChange={this.handleInputChange.bind(this)} + onBlur={this.handleInputConfirm} + onPressEnter={this.handleInputConfirm} + /> + )} + {!inputVisible && editable && ( + + 添加 + + )} +
+ ); + } +} diff --git a/src/pages/widget/TableEmpty.less b/src/pages/widget/TableEmpty.less new file mode 100644 index 0000000..60624e2 --- /dev/null +++ b/src/pages/widget/TableEmpty.less @@ -0,0 +1,11 @@ + +.widget-table-empty { + .empty-content { + + } + + .error-content { + img { + } + } +} diff --git a/src/pages/widget/TableEmpty.tsx b/src/pages/widget/TableEmpty.tsx new file mode 100644 index 0000000..34a7f3c --- /dev/null +++ b/src/pages/widget/TableEmpty.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import {Empty} from 'antd'; +import EmptyImage from "../../assets/image/empty.svg" +import ErrorImage from "../../assets/image/error.png" +import SafeUtil from "../../common/util/SafeUtil"; +import "./TableEmpty.less" +import Pager from "../../common/model/base/Pager"; + +interface IProps { + pager: Pager + onRefresh?: () => void +} + +interface IState { +} + +class TableEmpty extends React.Component { + + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + render() { + + let that = this + + let pager = this.props.pager + + + let emptyContent = ( +
+ + { + SafeUtil.safeCallback(that.props.onRefresh)() + } + }>点击重试 + +
+ ) + let errorContent = ( +
+ + { + SafeUtil.safeCallback(that.props.onRefresh)() + } + }>点击重试 + +
+ ) + + return ( +
+ {pager.errorMessage ? errorContent : (pager.data.length ? "" : emptyContent)} +
+ + ) + ; + } +} + +export default TableEmpty; diff --git a/src/pages/widget/filter/CheckFilterBox.tsx b/src/pages/widget/filter/CheckFilterBox.tsx new file mode 100644 index 0000000..85dd210 --- /dev/null +++ b/src/pages/widget/filter/CheckFilterBox.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import {Select} from 'antd'; +import SafeUtil from "../../../common/util/SafeUtil"; +import CheckFilter from "../../../common/model/base/filter/CheckFilter"; + +const Option = Select.Option; + +interface IProps { + checkFilter: CheckFilter + onChange?: (value: boolean | null) => void +} + +interface IState { +} + +export default class CheckFilterBox extends React.Component { + + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + + //改变某个filter的值 + onValueChange = (value: string) => { + + let that = this + + let checkFilter = this.props.checkFilter + + checkFilter.putValue(value) + + //通知外面变化了。 + SafeUtil.safeCallback(that.props.onChange)(checkFilter.value) + + //更新UI + that.setState({}) + + } + + + render() { + + let that = this + + let checkFilter = this.props.checkFilter + + //用于回填的值 + let backValue = checkFilter.getValueString() + + return ( + + + + + {checkFilter.name} + + + + + + ); + } +} + + diff --git a/src/pages/widget/filter/DateFilterBox.tsx b/src/pages/widget/filter/DateFilterBox.tsx new file mode 100644 index 0000000..be81bdc --- /dev/null +++ b/src/pages/widget/filter/DateFilterBox.tsx @@ -0,0 +1,81 @@ +import React from 'react'; + +import {DatePicker} from 'antd'; +import moment from "moment"; +import {default as DateFilter} from "../../../common/model/base/filter/DateFilter"; +import SafeUtil from "../../../common/util/SafeUtil"; + +interface IProps { + dateFilter: DateFilter + onChange?: (value: Date | null) => void +} + +interface IState { +} + + +class DateFilterBox extends React.Component { + + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + //改变某个filter的值 + onValueChange(momentTime: moment.Moment | null, dateString: string) { + + let that = this + + let filter = this.props.dateFilter + + if (momentTime) { + + filter.value = momentTime.toDate() + + } else { + filter.value = null + } + + //通知外面变化了。 + SafeUtil.safeCallback(that.props.onChange)(filter.value) + + //更新UI + that.setState({}) + } + + render() { + + let that = this + + let dateFilter = this.props.dateFilter + + let value: moment.Moment | undefined = undefined + if (dateFilter.value) { + value = moment(dateFilter.value) + } + + return ( + + + + {dateFilter.name} + + + + { + that.onValueChange(date, dateString) + }}/> + + + + ); + } +} + +export default DateFilterBox; diff --git a/src/pages/widget/filter/DateTimeFilterBox.tsx b/src/pages/widget/filter/DateTimeFilterBox.tsx new file mode 100644 index 0000000..7937c4d --- /dev/null +++ b/src/pages/widget/filter/DateTimeFilterBox.tsx @@ -0,0 +1,81 @@ +import React from 'react'; + +import {DatePicker} from 'antd'; +import moment from "moment"; +import DateTimeFilter from "../../../common/model/base/filter/DateTimeFilter"; +import SafeUtil from "../../../common/util/SafeUtil"; + +interface IProps { + dateTimeFilter: DateTimeFilter + onChange?: (value: Date | null) => void +} + +interface IState { +} + + +class DateTimeFilterBox extends React.Component { + + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + //改变某个filter的值 + onValueChange(momentTime: moment.Moment | null, dateString: string) { + + let that = this + + let filter = this.props.dateTimeFilter + + if (momentTime) { + + filter.value = momentTime.toDate() + + } else { + filter.value = null + } + + //通知外面变化了。 + SafeUtil.safeCallback(that.props.onChange)(filter.value) + + //更新UI + that.setState({}) + } + + render() { + + let that = this + + let dateTimeFilter = this.props.dateTimeFilter + + let value: moment.Moment | undefined = undefined + if (dateTimeFilter.value) { + value = moment(dateTimeFilter.value) + } + + return ( + + + + {dateTimeFilter.name} + + + + { + that.onValueChange(date, dateString) + }}/> + + + + ); + } +} + +export default DateTimeFilterBox; diff --git a/src/pages/widget/filter/FilterPanel.less b/src/pages/widget/filter/FilterPanel.less new file mode 100644 index 0000000..43a5a7c --- /dev/null +++ b/src/pages/widget/filter/FilterPanel.less @@ -0,0 +1,67 @@ +.widget-filter-panel { + .filter-block { + display: inline-block; + + .filter-cell { + display: inline-block; + margin-right: 15px; + margin-bottom: 10px; + + .filter-name { + font-weight: bold; + margin-right: 10px; + } + + .filter-body { + display: inline-block; + } + } + + } + + &.selection-button-loose { + .selection-button-filter-box, .http-selection-combobox-filter-box, .http-selection-button-filter-box { + display: block; + + .filter-cell { + margin: 0; + + .ant-radio-button-wrapper { + // border: none; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 4px; + } + + .ant-radio-button-wrapper-checked { + color: #fff; + background-color: #1890ff; + border-color: #1890ff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, .12); + box-shadow: 0 2px 0 rgba(0, 0, 0, .045); + margin-right: 10px + + } + + .ant-select { + margin-right: 10px; + margin-bottom: 10px; + } + } + } + } + + + .operation-area { + .ant-btn { + margin-right: 10px; + margin-bottom: 10px; + } + } + + .user-filter-box { + .filter-body { + min-width: 200px; + } + } +} diff --git a/src/pages/widget/filter/FilterPanel.tsx b/src/pages/widget/filter/FilterPanel.tsx new file mode 100644 index 0000000..aacbb9d --- /dev/null +++ b/src/pages/widget/filter/FilterPanel.tsx @@ -0,0 +1,193 @@ +import React from 'react'; +import './FilterPanel.less'; +import SafeUtil from '../../../common/util/SafeUtil'; +import Filter from '../../../common/model/base/filter/Filter'; +import InputFilter from '../../../common/model/base/filter/InputFilter'; +import InputFilterBox from './InputFilterBox'; +import DateTimeFilter from '../../../common/model/base/filter/DateTimeFilter'; +import DateTimeFilterBox from './DateTimeFilterBox'; +import CheckFilter from '../../../common/model/base/filter/CheckFilter'; +import CheckFilterBox from './CheckFilterBox'; +import SelectionFilter from '../../../common/model/base/filter/SelectionFilter'; +import SelectionFilterBox from './SelectionFilterBox'; +import SortFilter from '../../../common/model/base/filter/SortFilter'; +import SortFilterBox from './SortFilterBox'; +import HttpSelectionFilter from '../../../common/model/base/filter/HttpSelectionFilter'; +import HttpSelectionFilterBox from './HttpSelectionFilterBox'; +import { Button } from 'antd'; + +import DateFilterBox from './DateFilterBox'; +import DateFilter from '../../../common/model/base/filter/DateFilter'; + + +interface IProps { + onChange?: (value: any, key: any) => void + + filters: Filter[] + + //Selection 筛选宽松模式 会使用Button,单独占一行的模式。 + selectionButtonLoose?: boolean + + //重置按钮触发 + onReset?: () => void + + //懒搜索模式,点击按钮时才触发搜索的动作,当传递了内容,就会出现搜索按钮。 + onSearch?: () => void + +} + +interface IState { +} + + +/** + * 这里是筛选面板。 + * 如果需要调整筛选面板的样式。 + * 请在自己的类中,通过css覆盖的方式来修改样式。 + */ +export default class FilterPanel extends React.Component { + + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + + //当改变某个filter的值 + onChildChange(value: any, key: any) { + + let that = this; + + //通知外面变化了。 + SafeUtil.safeCallback(that.props.onChange)(value, key); + + //更新UI + that.setState({}); + + } + + //当改变某个filter的值 + onReset() { + + let that = this; + let filters = that.props.filters; + + for (let i = 0; i < filters.length; i++) { + let filter = filters[i]; + filter.reset(); + } + + + //通知外面变化了。 + SafeUtil.safeCallback(that.props.onReset)(); + + //更新UI + that.setState({}); + } + + + //当改变某个filter的值 + onSearch() { + + let that = this; + + //通知外面变化了。 + SafeUtil.safeCallback(that.props.onSearch)(); + + //更新UI + that.setState({}); + + } + + + render() { + + let that = this; + //router中传入的路由相关对象 + let filters = this.props.filters; + + let needSearchButton: boolean = !!this.props.onSearch; + + let needResetButton: boolean = !!this.props.onReset; + + return ( +
+ { + filters.map(((filter: Filter, index: number) => { + + if ((filter instanceof InputFilter) && filter.visible) { + return ( + { + that.onChildChange(val, filter.key); + }}/> + ); + } else if ((filter instanceof DateTimeFilter) && filter.visible) { + return ( + { + that.onChildChange(val, filter.key); + }}/> + ); + } else if ((filter instanceof DateFilter) && filter.visible) { + return ( + { + that.onChildChange(val, filter.key); + }}/> + ); + } else if ((filter instanceof CheckFilter) && filter.visible) { + return ( + { + that.onChildChange(val, filter.key); + }}/> + ); + } else if ((filter instanceof HttpSelectionFilter) && filter.visible) { + //注意,子类放前面,父类放后面 + return ( + { + that.onChildChange(val, filter.key); + }}/> + ); + } else if ((filter instanceof SelectionFilter) && filter.visible) { + return ( + { + that.onChildChange(val, filter.key); + }}/> + ); + } else if ((filter instanceof SortFilter) && filter.visible) { + return ( + { + that.onChildChange(val, filter.key); + }}/> + ); + } else { + return null; + } + + })) + } + +
+ + + + +
+ +
+ ); + } +} + diff --git a/src/pages/widget/filter/HttpSelectionFilterBox.tsx b/src/pages/widget/filter/HttpSelectionFilterBox.tsx new file mode 100644 index 0000000..4a05c00 --- /dev/null +++ b/src/pages/widget/filter/HttpSelectionFilterBox.tsx @@ -0,0 +1,126 @@ +import React from 'react'; + +import {Radio, Select} from "antd" +import HttpUtil from "../../../common/util/HttpUtil"; +import SafeUtil from "../../../common/util/SafeUtil"; +import SelectionOption from "../../../common/model/base/option/SelectionOption"; +import HttpSelectionFilter from "../../../common/model/base/filter/HttpSelectionFilter"; +import SelectionFilterType from "../../../common/model/base/filter/SelectionFilterType"; +import BambooComponent from "../../../common/component/BambooComponent"; + + +const Option = Select.Option; + +interface IProps { + httpSelectionFilter: HttpSelectionFilter + onChange?: (value: string) => void +} + +interface IState { + +} + + +export default class HttpSelectionFilterBox extends BambooComponent { + + constructor(props: IProps) { + super(props); + this.state = {}; + } + + componentDidMount() { + + this.refresh() + } + + refresh() { + //刷新一下列表 + let that = this + + let filter = this.props.httpSelectionFilter + + let url = filter.url + + HttpUtil.httpGet(url, {}, function (response: any) { + + //尝试一次回填。因为这个时候才知道之前那个值是否有效。 + filter.options = response.data.data + filter.strictPutValue(filter.value) + + that.updateUI() + }) + + } + + //改变某个filter的值 + onValueChange = (value: any) => { + let that = this + + let filter = this.props.httpSelectionFilter + + filter.value = value + + //通知外面变化了。 + SafeUtil.safeCallback(that.props.onChange)(filter.value) + + //更新UI + that.updateUI() + } + + render() { + + let that = this + + let filter = this.props.httpSelectionFilter + + let selectionOptions = filter.options + + if (filter.selectionType === SelectionFilterType.COMBOBOX) { + return ( + + + + {filter.name} + + + + ); + } else { + return ( + + + + + {filter.name} + + { + this.onValueChange(e.target.value) + }} + //不希望对"全部"进行回填。 + value={filter.value === '' ? undefined : filter.value} + > + 全部 + { + selectionOptions.map((item: SelectionOption, index: number) => { + return {item.name} + }) + } + + + + + ); + } + + + } +} + diff --git a/src/pages/widget/filter/InputFilterBox.tsx b/src/pages/widget/filter/InputFilterBox.tsx new file mode 100644 index 0000000..c37f532 --- /dev/null +++ b/src/pages/widget/filter/InputFilterBox.tsx @@ -0,0 +1,65 @@ +import React from 'react'; + +import {Input} from 'antd'; +import InputFilter from "../../../common/model/base/filter/InputFilter"; +import SafeUtil from "../../../common/util/SafeUtil"; + + +interface IProps { + inputFilter: InputFilter + onChange?: (value: string | null) => void +} + +interface IState { + +} + +export default class InputFilterBox extends React.Component { + + constructor(props: IProps) { + super(props); + + this.state = {}; + + } + + //改变某个filter的值 + onValueChange(event: any) { + + let that = this + + let inputFilter: InputFilter = this.props.inputFilter + + inputFilter.value = event.target.value + + //通知外面变化了。 + SafeUtil.safeCallback(that.props.onChange)(inputFilter.value) + + //更新UI + that.setState({}) + } + + + render() { + + let that = this + + let inputFilter: InputFilter = this.props.inputFilter + + return ( + + + + {inputFilter.name} + + + + + + + ); + } +} diff --git a/src/pages/widget/filter/SelectionFilterBox.tsx b/src/pages/widget/filter/SelectionFilterBox.tsx new file mode 100644 index 0000000..1a8055a --- /dev/null +++ b/src/pages/widget/filter/SelectionFilterBox.tsx @@ -0,0 +1,100 @@ +import React from 'react'; + +import {Radio, Select} from 'antd'; +import SelectionFilter from "../../../common/model/base/filter/SelectionFilter"; +import SafeUtil from "../../../common/util/SafeUtil"; +import SelectionOption from "../../../common/model/base/option/SelectionOption"; +import SelectionFilterType from "../../../common/model/base/filter/SelectionFilterType"; + + +const Option = Select.Option; + +interface IProps { + selectionFilter: SelectionFilter + onChange?: (value: string) => void +} + +interface IState { +} + +export default class SelectionFilterBox extends React.Component { + + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + + //改变某个filter的值 + onValueChange = (value: any) => { + + let that = this + + let selectionFilter = this.props.selectionFilter + + selectionFilter.value = value + + //通知外面变化了。 + SafeUtil.safeCallback(that.props.onChange)() + + //更新UI + that.setState({}) + + } + + render() { + + let that = this + + let selectionFilter = this.props.selectionFilter + + if (selectionFilter.selectionType === SelectionFilterType.COMBOBOX) { + return ( + + + + {selectionFilter.name} + + + + + ); + } else { + return ( + + + + {selectionFilter.name} + + { + this.onValueChange(e.target.value) + }} + //不希望对"全部"进行回填。 + value={selectionFilter.value === '' ? undefined : selectionFilter.value} + > + 全部 + {selectionFilter.options ? + selectionFilter.options.map((item: SelectionOption, index: number) => { + return {item.name} + }) + : null} + + + + + ); + } + + } +} + + diff --git a/src/pages/widget/filter/SortFilterBox.tsx b/src/pages/widget/filter/SortFilterBox.tsx new file mode 100644 index 0000000..6efb338 --- /dev/null +++ b/src/pages/widget/filter/SortFilterBox.tsx @@ -0,0 +1,70 @@ +import React from 'react'; + +import {Select} from 'antd'; +import SafeUtil from "../../../common/util/SafeUtil"; +import SortFilter from "../../../common/model/base/filter/SortFilter"; +import SortDirection from "../../../common/model/base/SortDirection"; + +const Option = Select.Option; + +interface IProps { + sortFilter: SortFilter + onChange?: (value: SortDirection | null) => void +} + +interface IState { +} + +export default class SortFilterBox extends React.Component { + + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + //改变某个filter的值 + onValueChange = (value: string) => { + + let that = this + + let sortFilter = this.props.sortFilter + + sortFilter.putValue(value) + + //通知外面变化了。 + SafeUtil.safeCallback(that.props.onChange)(sortFilter.value) + + //更新UI + that.setState({}) + + } + + + render() { + + let that = this + + let sortFilter = this.props.sortFilter + + return ( + + + + + {sortFilter.name} + + + + + + ); + } +} + +