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