From 03bb3f4881d907fb70ebb7cd81e53f3f83aeadec Mon Sep 17 00:00:00 2001 From: lishuang Date: Fri, 5 Jun 2020 23:21:18 +0800 Subject: [PATCH] Finish the first version of tank from bamboo. --- .eslintignore | 6 + .eslintrc | 13 + config/setupProxy.js | 17 + package.json | 8 + src/App.less | 8 +- src/App.tsx | 16 +- src/assets/css/bootstrap/variables.less | 868 ++++++++++++++++++ .../css/global/animation.less} | 0 src/assets/css/global/border.less | 41 + src/assets/css/global/button.less | 37 + src/assets/css/global/color.less | 112 +++ src/assets/css/global/font.less | 38 + src/assets/css/global/layout.less | 143 +++ src/assets/css/global/miscellaneous.less | 108 +++ src/assets/css/global/mixin.less | 11 + src/assets/css/global/responsive.less | 43 + src/assets/css/global/variables.less | 15 + src/assets/css/index.less | 16 + src/assets/image/avatar.png | Bin 0 -> 2396 bytes src/assets/image/empty.svg | 1 + src/assets/image/error.png | Bin 0 -> 3498 bytes src/assets/image/logo.png | Bin 0 -> 35919 bytes src/common/component/BambooComponent.tsx | 37 + src/common/menu/MenuItem.ts | 23 + src/common/menu/MenuManager.ts | 75 ++ src/common/model/article/Article.ts | 62 ++ src/common/model/base/Base.ts | 140 +++ src/common/model/base/BaseEntity.ts | 227 +++++ src/common/model/base/HttpBase.ts | 262 ++++++ src/common/model/base/Pager.ts | 405 ++++++++ src/common/model/base/SortDirection.ts | 12 + src/common/model/base/ViewBase.ts | 56 ++ src/common/model/base/WebResultCode.ts | 42 + src/common/model/base/filter/CheckFilter.ts | 51 + src/common/model/base/filter/DateFilter.ts | 54 ++ .../model/base/filter/DateTimeFilter.ts | 54 ++ src/common/model/base/filter/Filter.ts | 55 ++ .../model/base/filter/HttpSelectionFilter.ts | 32 + src/common/model/base/filter/InputFilter.ts | 47 + .../model/base/filter/SelectionFilter.ts | 68 ++ .../model/base/filter/SelectionFilterType.ts | 11 + src/common/model/base/filter/SortFilter.ts | 69 ++ .../model/base/option/CodeSelectionOption.ts | 9 + src/common/model/base/option/Color.ts | 9 + .../model/base/option/ColorSelectionOption.ts | 9 + .../base/option/IconIndexSelectionOption.ts | 11 + .../model/base/option/IndexSelectionOption.ts | 10 + .../model/base/option/SelectionOption.ts | 8 + .../model/base/option/StyleSelectionOption.ts | 8 + src/common/model/base/types/index.d.ts | 57 ++ src/common/model/global/Moon.ts | 26 + src/common/model/global/Sun.ts | 43 + src/common/model/user/User.ts | 166 ++++ src/common/model/user/UserRole.ts | 38 + src/common/types/index.d.ts | 57 ++ src/common/util/AnimateUtil.ts | 75 ++ src/common/util/AntdUtil.ts | 46 + src/common/util/ArrayUtil.ts | 142 +++ src/common/util/BrowserUtil.ts | 113 +++ src/common/util/ClipboardUtil.ts | 39 + src/common/util/DateUtil.ts | 86 ++ src/common/util/DomUtil.ts | 20 + src/common/util/HttpUtil.ts | 92 ++ src/common/util/JsonUtil.ts | 62 ++ src/common/util/MessageBoxUtil.ts | 28 + src/common/util/NumberUtil.ts | 20 + src/common/util/ObjectUtil.ts | 114 +++ src/common/util/SafeUtil.ts | 35 + src/common/util/StringUtil.ts | 248 +++++ src/index.tsx | 1 - src/pages/Frame.less | 79 ++ src/pages/Frame.tsx | 173 ++++ src/pages/article/Detail.less | 11 + src/pages/article/Detail.tsx | 111 +++ src/pages/article/Edit.less | 5 + src/pages/article/Edit.tsx | 84 ++ src/pages/article/List.less | 8 + src/pages/article/List.tsx | 172 ++++ src/pages/index/Index.less | 5 + src/pages/index/Index.tsx | 53 ++ src/pages/user/ChangePasswordModal.less | 19 + src/pages/user/ChangePasswordModal.tsx | 93 ++ src/pages/user/Login.less | 28 + src/pages/user/Login.tsx | 86 ++ src/pages/user/Profile.less | 7 + src/pages/user/Profile.tsx | 72 ++ src/pages/widget/BambooTitle.less | 32 + src/pages/widget/BambooTitle.tsx | 50 + src/pages/widget/ChangeNumModal.less | 5 + src/pages/widget/ChangeNumModal.tsx | 80 ++ src/pages/widget/FrameLoading.tsx | 42 + src/pages/widget/InfoCell.less | 14 + src/pages/widget/InfoCell.tsx | 49 + src/pages/widget/StringsPicker.less | 6 + src/pages/widget/StringsPicker.tsx | 179 ++++ src/pages/widget/TableEmpty.less | 11 + src/pages/widget/TableEmpty.tsx | 69 ++ src/pages/widget/filter/CheckFilterBox.tsx | 73 ++ src/pages/widget/filter/DateFilterBox.tsx | 81 ++ src/pages/widget/filter/DateTimeFilterBox.tsx | 81 ++ src/pages/widget/filter/FilterPanel.less | 67 ++ src/pages/widget/filter/FilterPanel.tsx | 193 ++++ .../widget/filter/HttpSelectionFilterBox.tsx | 126 +++ src/pages/widget/filter/InputFilterBox.tsx | 65 ++ .../widget/filter/SelectionFilterBox.tsx | 100 ++ src/pages/widget/filter/SortFilterBox.tsx | 70 ++ 106 files changed, 7242 insertions(+), 12 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 config/setupProxy.js create mode 100644 src/assets/css/bootstrap/variables.less rename src/{index.css => assets/css/global/animation.less} (100%) create mode 100644 src/assets/css/global/border.less create mode 100644 src/assets/css/global/button.less create mode 100644 src/assets/css/global/color.less create mode 100644 src/assets/css/global/font.less create mode 100644 src/assets/css/global/layout.less create mode 100644 src/assets/css/global/miscellaneous.less create mode 100644 src/assets/css/global/mixin.less create mode 100644 src/assets/css/global/responsive.less create mode 100644 src/assets/css/global/variables.less create mode 100644 src/assets/css/index.less create mode 100644 src/assets/image/avatar.png create mode 100644 src/assets/image/empty.svg create mode 100644 src/assets/image/error.png create mode 100644 src/assets/image/logo.png create mode 100644 src/common/component/BambooComponent.tsx create mode 100644 src/common/menu/MenuItem.ts create mode 100644 src/common/menu/MenuManager.ts create mode 100644 src/common/model/article/Article.ts create mode 100644 src/common/model/base/Base.ts create mode 100644 src/common/model/base/BaseEntity.ts create mode 100644 src/common/model/base/HttpBase.ts create mode 100644 src/common/model/base/Pager.ts create mode 100644 src/common/model/base/SortDirection.ts create mode 100644 src/common/model/base/ViewBase.ts create mode 100644 src/common/model/base/WebResultCode.ts create mode 100644 src/common/model/base/filter/CheckFilter.ts create mode 100644 src/common/model/base/filter/DateFilter.ts create mode 100644 src/common/model/base/filter/DateTimeFilter.ts create mode 100644 src/common/model/base/filter/Filter.ts create mode 100644 src/common/model/base/filter/HttpSelectionFilter.ts create mode 100644 src/common/model/base/filter/InputFilter.ts create mode 100644 src/common/model/base/filter/SelectionFilter.ts create mode 100644 src/common/model/base/filter/SelectionFilterType.ts create mode 100644 src/common/model/base/filter/SortFilter.ts create mode 100644 src/common/model/base/option/CodeSelectionOption.ts create mode 100644 src/common/model/base/option/Color.ts create mode 100644 src/common/model/base/option/ColorSelectionOption.ts create mode 100644 src/common/model/base/option/IconIndexSelectionOption.ts create mode 100644 src/common/model/base/option/IndexSelectionOption.ts create mode 100644 src/common/model/base/option/SelectionOption.ts create mode 100644 src/common/model/base/option/StyleSelectionOption.ts create mode 100644 src/common/model/base/types/index.d.ts create mode 100644 src/common/model/global/Moon.ts create mode 100644 src/common/model/global/Sun.ts create mode 100644 src/common/model/user/User.ts create mode 100644 src/common/model/user/UserRole.ts create mode 100644 src/common/types/index.d.ts create mode 100644 src/common/util/AnimateUtil.ts create mode 100644 src/common/util/AntdUtil.ts create mode 100644 src/common/util/ArrayUtil.ts create mode 100644 src/common/util/BrowserUtil.ts create mode 100644 src/common/util/ClipboardUtil.ts create mode 100644 src/common/util/DateUtil.ts create mode 100644 src/common/util/DomUtil.ts create mode 100644 src/common/util/HttpUtil.ts create mode 100644 src/common/util/JsonUtil.ts create mode 100644 src/common/util/MessageBoxUtil.ts create mode 100644 src/common/util/NumberUtil.ts create mode 100644 src/common/util/ObjectUtil.ts create mode 100644 src/common/util/SafeUtil.ts create mode 100644 src/common/util/StringUtil.ts create mode 100644 src/pages/Frame.less create mode 100644 src/pages/Frame.tsx create mode 100644 src/pages/article/Detail.less create mode 100644 src/pages/article/Detail.tsx create mode 100644 src/pages/article/Edit.less create mode 100644 src/pages/article/Edit.tsx create mode 100644 src/pages/article/List.less create mode 100644 src/pages/article/List.tsx create mode 100644 src/pages/index/Index.less create mode 100644 src/pages/index/Index.tsx create mode 100644 src/pages/user/ChangePasswordModal.less create mode 100644 src/pages/user/ChangePasswordModal.tsx create mode 100644 src/pages/user/Login.less create mode 100644 src/pages/user/Login.tsx create mode 100644 src/pages/user/Profile.less create mode 100644 src/pages/user/Profile.tsx create mode 100644 src/pages/widget/BambooTitle.less create mode 100644 src/pages/widget/BambooTitle.tsx create mode 100644 src/pages/widget/ChangeNumModal.less create mode 100644 src/pages/widget/ChangeNumModal.tsx create mode 100644 src/pages/widget/FrameLoading.tsx create mode 100644 src/pages/widget/InfoCell.less create mode 100644 src/pages/widget/InfoCell.tsx create mode 100644 src/pages/widget/StringsPicker.less create mode 100644 src/pages/widget/StringsPicker.tsx create mode 100644 src/pages/widget/TableEmpty.less create mode 100644 src/pages/widget/TableEmpty.tsx create mode 100644 src/pages/widget/filter/CheckFilterBox.tsx create mode 100644 src/pages/widget/filter/DateFilterBox.tsx create mode 100644 src/pages/widget/filter/DateTimeFilterBox.tsx create mode 100644 src/pages/widget/filter/FilterPanel.less create mode 100644 src/pages/widget/filter/FilterPanel.tsx create mode 100644 src/pages/widget/filter/HttpSelectionFilterBox.tsx create mode 100644 src/pages/widget/filter/InputFilterBox.tsx create mode 100644 src/pages/widget/filter/SelectionFilterBox.tsx create mode 100644 src/pages/widget/filter/SortFilterBox.tsx 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 0000000000000000000000000000000000000000..222f0b4183f01306f8c1e90034f627a5c737b4d7
GIT binary patch
literal 2396
zcmV-i38VIjP)tdU_5`}sHM?2k36Coq6HnpbN}=q0aOow{*E
zEjWL@ld6@w+%B`)Wh=Ybr?rlmwNA^Gn>3fYv7+adrWZOoEp*aep^(3FQmqneEWWly
zaQc4KSZrU5p=d(?RG%d_bGFctqKX%E*Snly*6iyvvDmJ4tf2x<$tY{l__aR!Di&{
z(>}_ZKcD-iY&Yfts;;5nfgjcycgoMYdeJbvn
zoI`^sGJG^(0t3_Y5e_13S2}{ENVu6W(T2d$h7C*w+_e>Jz_>(OU9Cn~qPn&t7=a>K
zJAjc!026dIp{_S<&uES}H0B
zbf9Irdd;pj9+R70$8<<$yY4?D@>~$T8^POVf;ZB2i*{X8K|6(S33)5R-}CMWz&#;u
zcMV$in3{yTceEbC80E6hscR|<*wL(fP~UV4@7vG_y59>r_xzvj%701e`%tUp#`*QC
z-S6Bbw_Q4QQ@thk&D(T5fa(Vi7guE;9T-U7x
zs#b!l7H=x5X8Wmw!30zt7&{ZHzS-Q7nCeO>NVk0IFwy6*=IbY{
zYW)z4{$ue~Js9UnBzoiWM`Eh(f_<1{v4D}N>R2H9+XhCmuiq5bZ(Q}x1qPz3zWx0n
z0hxTapmFWje;NXc#uhwEr0Q-U)sc^=vd^y!V-x!S_mEK{RVvBR#K8uMRd-y`v5%^<
z&)O_L-|7?{BvMU*35XY{>J(;&ByWxn;Y)BNpt@+kB5=uquB!W_n_~Sc5OGw4CbNzf
zTgovd6EswJ{QQ1bqO`3V_fkAl%uCfU44<=MBrDQ+Xgmz$l0qi=c2WIu)$T8R68x*)
z;x4LsR6D)L$|9>eX7|DRsaos$#dtT6syi{$<_mW#ldqsUZNJCA^;X^T(gTq|RgfRB
zR@|h+M}iI#sru0CcHBc1Nf7)}SDhotfd^GpG(l3huqu{yu%GZ$h6RnoGbFfZu~SkN
zBy;F3rOv8?2`Gv?1yw5*=bbbm{3798DRNZTVDx7^pRDCi=
zMI&2D)xG%!CrXoV*HHRZkbiYYF(|qv4meJbDRq~is^ux?Up*gGHL?6FuIfYS*JSA+
z!$i4a8&Vx16ClhLdr$`2>XX&=1HR}2OF#=%#|}UJYU88+aJ^@s-2{sH>WedrhBrljMoY;RZ@MLy&E1TJ$PB)o2N4}F&
z4We$@{dC=3^)3gCirUk#=%)HCxo40+i&a1^x1v{b1*i7mdL1M4opy^Y{iQS(T)
z6Lo992$5zAWh*qxvK)C+kGA3(os6VfO-OAyNKt&^xNe{kEW3(xd8nfElHU
z?~P$5lqULD?U-CtFRB++J^O}wRJH7HY2jj32N$b4(6hm-h5vi2Zus*0YhUfI?q7wQ
z%$*q8s`Z?4cY`;b_V7^#vT8pa=dgI^?^>61&nTNfP^Jsf39RE6|HN7ajolFU#HuvkquRa@r$~YtRB3+^qy1`DzzKh
zs_-jb5mQTQPSvfF=e|~Lx#jOMt6@*4(G66<30g%9hLSTJ`OB39F9!q>3xLMfict3E<(?)hK3asiT7AAc7;
z3ce|lVjF6*8m_ZU{?x=tGx*OPDOw$HGFjp*o)2Yp5%)ebg?96Ohf|mSnt@@q$p>F^
z*lzrSd7(d%pa?gat~E#OCJV?9WOb1#e1I~L-7HyhJ@Kp_?m=2Akh>FQ*=s1Yy3U6E
z1Vf#)`1wFv4L`a6mH#b1E8;9$&uV*TtIJf!mm))u0fqw!sz)jLJJqGI=EQX!NsZ$E>?ALv8n?-pYdtI*nRb6b>_cK4=-#j_+U~1
O0000
\ 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 0000000000000000000000000000000000000000..97f79c9f32c7242f4ec93585eee40632f8ecd5a7
GIT binary patch
literal 3498
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
KdeYnjslNk$;AhkT

literal 0
HcmV?d00001

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} + + + + + + ); + } +} + +