Finish the first version of tank from bamboo.

This commit is contained in:
lishuang
2020-06-05 23:21:18 +08:00
parent 397cc7af58
commit 03bb3f4881
106 changed files with 7242 additions and 12 deletions
+6
View File
@@ -0,0 +1,6 @@
node_modules
/dist
/www
/_dist
/_package
/mock
+13
View File
@@ -0,0 +1,13 @@
{
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"rules": {
"@typescript-eslint/no-unused-vars": 0
}
}
+17
View File
@@ -0,0 +1,17 @@
const proxy = require('http-proxy-middleware');
module.exports = function (app) {
//为了解决前端开发时跨域问题,配置后台接口代理。
app.use(proxy(
'/api',
{
target: 'http://localhost:6020',
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
}
));
};
+8
View File
@@ -8,11 +8,13 @@
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/node": "^12.0.0",
"@types/qs": "^6.9.3",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
"antd": "^4.3.1",
"axios": "^0.19.2",
"babel-eslint": "10.1.0",
"babel-loader": "8.1.0",
"babel-plugin-named-asset-import": "^0.3.6",
@@ -44,10 +46,13 @@
"postcss-normalize": "8.0.1",
"postcss-preset-env": "6.7.0",
"postcss-safe-parser": "4.0.1",
"qs": "^6.9.4",
"react": "^16.13.1",
"react-app-polyfill": "^1.0.6",
"react-dev-utils": "^10.2.1",
"react-dom": "^16.13.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"resolve": "1.15.0",
"resolve-url-loader": "3.1.1",
"sass-loader": "8.0.2",
@@ -86,5 +91,8 @@
"presets": [
"react-app"
]
},
"devDependencies": {
"@types/react-router-dom": "^5.1.5"
}
}
+3 -5
View File
@@ -1,6 +1,4 @@
//import antd style.
@import '~antd/dist/antd.css';
.good {
color: red;
}
//import global style.
@import './assets/css/index';
+10 -6
View File
@@ -1,16 +1,20 @@
import React from 'react';
import './App.less';
import {Button} from "antd";
import {BrowserRouter as Router} from "react-router-dom";
import {ConfigProvider} from 'antd';
import Frame from "./pages/Frame";
import zhCN from 'antd/lib/locale-provider/zh_CN';
function App() {
return (
<div className="App">
<Router>
<ConfigProvider locale={zhCN}>
<Frame/>
</ConfigProvider>
<Button></Button>
<div className="good">good</div>
</div>
</Router>
);
}
+868
View File
@@ -0,0 +1,868 @@
//
// Variables
// --------------------------------------------------
//== Colors
//
//## Gray and brand colors for use across Bootstrap.
@gray-base: #000;
@gray-darker: lighten(@gray-base, 13.5%); // #222
@gray-dark: lighten(@gray-base, 20%); // #333
@gray: lighten(@gray-base, 33.5%); // #555
@gray-light: lighten(@gray-base, 46.7%); // #777
@gray-lighter: lighten(@gray-base, 93.5%); // #eee
@brand-primary: darken(#428bca, 6.5%); // #337ab7
@brand-success: #5cb85c;
@brand-info: #5bc0de;
@brand-warning: #f0ad4e;
@brand-danger: #d9534f;
//== Scaffolding
//
//## Settings for some of the most global styles.
//** Background color for `<body>`.
@body-bg: #fff;
//** Global text color on `<body>`.
@text-color: @gray-dark;
//** Global textual link color.
@link-color: @brand-primary;
//** Link hover color set via `darken()` function.
@link-hover-color: darken(@link-color, 15%);
//** Link hover decoration.
@link-hover-decoration: underline;
//== Typography
//
//## Font, line-height, and color for body text, headings, and more.
@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif;
@font-family-serif: Georgia, "Times New Roman", Times, serif;
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
@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 `<body>`.
@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 `<th>`s and `<td>`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
//
//##
//** `<input>` background color
@input-bg: #fff;
//** `<input disabled>` background color
@input-bg-disabled: @gray-lighter;
//** Text color for `<input>`s
@input-color: @gray;
//** `<input>` border color
@input-border: #ccc;
//** Default `.form-control` border radius
// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`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;
+41
View File
@@ -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;
}
+37
View File
@@ -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;
}
+112
View File
@@ -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;}
+38
View File
@@ -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;
}
+143
View File
@@ -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);
+108
View File
@@ -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;
}
+11
View File
@@ -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;
}
+43
View File
@@ -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
}
+15
View File
@@ -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;
+16
View File
@@ -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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+37
View File
@@ -0,0 +1,37 @@
import React from 'react';
/**
* 带有挂载状态的自定义组件
*/
export default class BambooComponent<P, S> extends React.Component <P, S> {
//是否处于挂载的状态。
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;
}
}
+23
View File
@@ -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
}
}
}
+75
View File
@@ -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;
}
}
+62
View File
@@ -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'),
];
}
}
+140
View File
@@ -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<F extends keyof this>(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('调用错误!')
}
}
}
+227
View File
@@ -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);
}
}
+262
View File
@@ -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);
}
}
+405
View File
@@ -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<T> 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();
}
}
+12
View File
@@ -0,0 +1,12 @@
enum SortDirection {
//这两个是后台通用的标准
ASC = 'ASC',
DESC = 'DESC',
//这两项是antd的标准
DESCEND = 'descend',
ASCEND = 'ascend'
}
export default SortDirection
+56
View File
@@ -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)
}
}
+42
View File
@@ -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}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
+55
View File
@@ -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
}
}
@@ -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)
}
}
@@ -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 === "";
}
}
@@ -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 === "";
}
}
@@ -0,0 +1,11 @@
/**
* 过滤器中的选择样式
*/
enum SelectionFilterType {
//下拉框的样式
COMBOBOX = "COMBOBOX",
//按钮的样式
BUTTON = "BUTTON",
}
export default SelectionFilterType
@@ -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;
}
}
@@ -0,0 +1,9 @@
/**
* 带有Code的一个selection
*/
import SelectionOption from "./SelectionOption";
export default interface CodeSelectionOption extends SelectionOption {
code: string,
type: string,
}
+9
View File
@@ -0,0 +1,9 @@
enum Color {
PRIMARY = "#108EE9",
INFO = "#2DB7F5",
SUCCESS = "#87D068",
WARNING = "#FEC62E",
DANGER = "#F50",
}
export default Color
@@ -0,0 +1,9 @@
/**
* 下拉筛选框的选项,统一使用该接口,同时这个作为枚举的复杂类型。
*/
import SelectionOption from "./SelectionOption";
export default interface ColorSelectionOption extends SelectionOption {
index: number,
color: string,
}
@@ -0,0 +1,11 @@
/**
* 包含图标和 index序列的选项,在ProteinSqlPendant中有用到
*/
import SelectionOption from "./SelectionOption";
export default interface IconIndexSelectionOption extends SelectionOption {
index: number,
icon: string,
}
@@ -0,0 +1,10 @@
/**
* 下拉筛选框的选项,统一使用该接口,同时这个作为枚举的复杂类型。
*/
import SelectionOption from "./SelectionOption";
export default interface IndexSelectionOption extends SelectionOption {
index: number,
name: string,
value: string
}
@@ -0,0 +1,8 @@
/**
* 下拉筛选框的选项,统一使用该接口,同时这个作为枚举的复杂类型。
*/
export default interface SelectionOption {
name: string,
value: string
}
@@ -0,0 +1,8 @@
/**
* 下拉筛选框的选项,统一使用该接口,同时这个作为枚举的复杂类型。
*/
import SelectionOption from "./SelectionOption";
export default interface StyleSelectionOption extends SelectionOption {
style: string,
}
+57
View File
@@ -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 }
+26
View File
@@ -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
}
}
+43
View File
@@ -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 未定义,请检查代码!")
}
}
////////////和路由相关的方法 结束////////////
}
+166
View File
@@ -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);
}
}
+38
View File
@@ -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 };
+57
View File
@@ -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 }
+75
View File
@@ -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)
}
}
+46
View File
@@ -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
}
}
+142
View File
@@ -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;
};
}
+113
View File
@@ -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
}
}
+39
View File
@@ -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)()
}
}
}
+86
View File
@@ -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()
}
}
}
+20
View File
@@ -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)
}
}
}
+92
View File
@@ -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)
});
}
}
+62
View File
@@ -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)
};
}
+28
View File
@@ -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)
}
}
+20
View File
@@ -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
}
}
}
+114
View File
@@ -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("&");
}
}
+35
View File
@@ -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()
}
}
}
+248
View File
@@ -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;
}
}
-1
View File
@@ -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';
+79
View File
@@ -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;
}
}
}
+173
View File
@@ -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<IProps, IState> {
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 (
<div className="pages-frame">
<Layout style={{ minHeight: '100vh' }}>
<Sider>
<div className="username">
{user.role === UserRole.GUEST ?
'未登录' :
<Link className="username-text" to="/user/profile">{user.username}</Link>
}
</div>
<Menu
theme="dark"
selectedKeys={menuManager.getSelectedKeys()}
onSelect={this.onSelect.bind(this)}
mode="inline">
{
menuItems.map((menuItem: MenuItem, index: number) => {
return (
<Menu.Item key={menuItem.url}>
<span>{menuItem.name}</span>
</Menu.Item>
);
})
}
</Menu>
</Sider>
<Layout>
<Header>
<div className="logo-title-area" onClick={this.goHome.bind(this)}>
<img className="header-logo" src={LogoSvg} alt="logo"/>
<span className="header-title">bamboo</span>
</div>
</Header>
<Content>
<div className="pages-content">
<Route exact path="/" render={() =>
<Redirect to="/article/list"/>
}/>
<Route path="/index" component={Index}/>
<Route path="/user/login" component={UserLogin}/>
<Route path="/user/profile" component={UserProfile}/>
<Route path="/article/detail/:uuid" component={ArticleDetail}/>
<Route exact path="/article" render={() =>
<Redirect to="/article/list"/>
}/>
<Route path="/article/list" component={ArticleList}/>
<Route path="/article/create" component={ArticleEdit}/>
<Route path="/article/edit/:uuid" component={ArticleEdit}/>
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>Eyeblue ©2020 Copyright</Footer>
</Layout>
</Layout>
</div>
);
}
}
const Frame = withRouter<IProps, React.ComponentType<IProps>>(RawFrame);
export default Frame;
+11
View File
@@ -0,0 +1,11 @@
.article-detail {
padding: 20px;
overflow: auto;
background: white;
}
.article {
width: 100%;
}
+111
View File
@@ -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<RouteParam> {
}
interface IState {
}
export default class Detail extends BambooComponent<IProps, IState> {
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 (
<div className="article-detail">
<BambooTitle name={'文章详情'}>
<Link title="编辑"
to={StringUtil.prePath(match.path, 2) + '/edit/' + article.uuid}>
<Button className="mh10" type="primary" icon="edit">
</Button>
</Link>
</BambooTitle>
<Spin tip="加载中" spinning={article.detailLoading}>
<div className="info">
{/*<Row>*/}
{/* <Col span={12}>*/}
{/* <InfoCell name="文章名称">*/}
{/* {article.title}*/}
{/* </InfoCell>*/}
{/* </Col>*/}
{/* <Col span={12}>*/}
{/* <InfoCell name="作者">*/}
{/* {article.author}*/}
{/* </InfoCell>*/}
{/* </Col>*/}
{/* <Col span={12}>*/}
{/* <InfoCell name="路径">*/}
{/* {article.path}*/}
{/* </InfoCell>*/}
{/* </Col>*/}
{/*</Row>*/}
<div className="article" dangerouslySetInnerHTML={{__html: ''+ article.html}} />
</div>
</Spin>
</div>
);
}
}
+5
View File
@@ -0,0 +1,5 @@
.article-edit {
padding: 20px;
background: white;
}
+84
View File
@@ -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<IProps, IState> {
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 (
<div className="article-edit">
<BambooTitle name={this.createMode ? '创建文章' : '编辑文章'}>
</BambooTitle>
</div>
);
}
}
export default RawEdit;
+8
View File
@@ -0,0 +1,8 @@
.article-list {
padding: 5px;
.ant-table {
background-color: white;
}
}
+172
View File
@@ -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<IProps, IState> {
//获取分页的一个帮助器
pager: Pager<Article> = new Pager<Article>(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<Article>[] = [{
title: '文章名称',
dataIndex: 'title',
render: (text: any, record: Article, index: number): React.ReactNode => (
<Link to={StringUtil.prePath(match.path) + '/detail/' + record.uuid}>{record.title}</Link>
),
}, {
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) => (
<span>
<Link title="编辑"
to={StringUtil.prePath(match.path) + '/edit/' + record.uuid}>
</Link>
<Popconfirm title="确认删除该文章,删除后不可恢复?" onConfirm={(e: any) => {
record.httpDel(function() {
MessageBox.success('删除成功!');
that.refresh();
});
}} okText="确认" cancelText="取消">
<span></span>
</Popconfirm>
</span>
),
}];
return (
<div className="article-list">
<BambooTitle name={'文章管理'}>
<Button type="primary" onClick={this.createArticle.bind(this)}>
</Button>
</BambooTitle>
<div>
<FilterPanel filters={pager.filters} onChange={this.search.bind(this)}/>
</div>
<Table
rowKey="id"
loading={pager.loading}
dataSource={pager.data}
columns={columns}
pagination={pager.getPagination()}
onChange={pager.tableOnChange.bind(pager)}
locale={{ emptyText: (<TableEmpty pager={pager} onRefresh={this.refresh.bind(this)}/>) }}
/>
</div>
);
}
}
+5
View File
@@ -0,0 +1,5 @@
.index-page {
padding: 20px;
}
+53
View File
@@ -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<IProps, IState> {
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 (
<div className="index-page">
<h1></h1>
</div>
);
}
}
+19
View File
@@ -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;
}
}
}
+93
View File
@@ -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<IProps, IState> {
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: <ChangePasswordModal
onSuccess={() => {
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 (
<div className="change-password-modal">
<Row>
<Col>
<div className="title">
</div>
</Col>
</Row>
</div>
);
}
}
+28
View File
@@ -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%;
}
}
+86
View File
@@ -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<IProps, IState> {
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 (
<div className="user-login">
<Row>
<Col span={8} offset={8}>
<div className="welcome">
</div>
</Col>
</Row>
</div>
);
}
}
export default RawLogin;
+7
View File
@@ -0,0 +1,7 @@
.page-profile {
background-color: white;
padding: 15px;
}
+72
View File
@@ -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 <IProps, IState> {
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 (
<div className="page-profile">
<BambooTitle name={'个人资料'}>
<Button type="primary" icon="plus" onClick={this.changePassword.bind(this)}>
</Button>
</BambooTitle>
<div>
<InfoCell name="用户名">
{user.username}
</InfoCell>
<InfoCell name="创建时间">
{DateUtil.simpleDateTime(user.createTime)}
</InfoCell>
</div>
</div>
)
}
}
+32
View File
@@ -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;
}
}
+50
View File
@@ -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<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
componentDidMount() {
//刷新一下列表
let that = this
}
componentWillReceiveProps(nextProps: Readonly<IProps>, nextContext: any): void {
let that = this
}
render() {
let that = this
return (
<div className="widget-bamboo-title">
<span className="item active">
{that.props.name}
</span>
<span className="tool">
{that.props.children}
</span>
</div>
);
}
}
+5
View File
@@ -0,0 +1,5 @@
.widget-change-num-modal {
background-color: aliceblue;
}
+80
View File
@@ -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<IProps, IState> {
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 (
<div className="widget-change-num-modal">
<div className="text-center">
<h2>
{that.props.title}
</h2>
</div>
<Input value={this.state.innerValue} onChange={(e) => {
that.setState({
innerValue: parseInt(e.target.value)
})
}}/>
<div className="text-center mt20">
<Button className="ml20" type="default" onClick={() => {
this.props.onClose()
}}></Button>
<Button className="ml20"
type="primary"
onClick={() => {
this.onSubmitClick()
}}></Button>
</div>
</div>
);
}
}
+42
View File
@@ -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<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
render() {
let that = this
return (
<div className="app-frame-loading">
<div className="loading-box">
<div>
<img alt="加载按钮" className="loading-logo" src={LogoBluePng}/>
</div>
<div>
...
</div>
</div>
</div>
)
}
}
+14
View File
@@ -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;
}
}
+49
View File
@@ -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 <IProps, IState> {
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 (
<Row className="castle-widget-info-cell">
<Col span={firstSpan} className="info-cell-name">{name}</Col>
<Col span={secondSpan} className="info-cell-content">
{that.props.children}
</Col>
</Row>
)
}
}
+6
View File
@@ -0,0 +1,6 @@
.widget-strings-picker {
width: 100%;
}
+179
View File
@@ -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<IProps, IState> {
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<IProps>, 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 (
<div>
{tags.map((tag, index) => {
const isLongTag = tag.length > 20;
const tagElem = (
<Tag key={tag} closable={editable} onClose={() => this.handleClose(tag)}>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</Tag>
);
return isLongTag ? (
<Tooltip title={tag} key={tag}>
{tagElem}
</Tooltip>
) : (
tagElem
);
})}
{inputVisible && editable && (
<Input
ref={(dom) => {
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 && (
<Tag onClick={this.showInput} style={{background: '#fff', borderStyle: 'dashed'}}>
</Tag>
)}
</div>
);
}
}
+11
View File
@@ -0,0 +1,11 @@
.widget-table-empty {
.empty-content {
}
.error-content {
img {
}
}
}
+69
View File
@@ -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<any>
onRefresh?: () => void
}
interface IState {
}
class TableEmpty extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {};
}
render() {
let that = this
let pager = this.props.pager
let emptyContent = (
<div className="empty-content">
<Empty
image={EmptyImage}
description="暂无数据"
>
<span className="link" onClick={event => {
SafeUtil.safeCallback(that.props.onRefresh)()
}
}></span>
</Empty>
</div>
)
let errorContent = (
<div className="error-content">
<Empty
image={ErrorImage}
description={pager.errorMessage}
>
<span className="link" onClick={event => {
SafeUtil.safeCallback(that.props.onRefresh)()
}
}></span>
</Empty>
</div>
)
return (
<div className="widget-table-empty">
{pager.errorMessage ? errorContent : (pager.data.length ? "" : emptyContent)}
</div>
)
;
}
}
export default TableEmpty;
@@ -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 <IProps, IState> {
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 (
<span className="filter-block check-filter-box">
<span className="filter-cell">
<span className="filter-name">
{checkFilter.name}
</span>
<Select value={backValue} style={{minWidth: 120}}
onChange={this.onValueChange}>
<Option key={0} value={""}></Option>
<Option key={1} value={"true"}></Option>
<Option key={2} value={"false"}></Option>
</Select>
</span>
</span>
);
}
}
+81
View File
@@ -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<IProps, IState> {
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 (
<span className="filter-block date-time-filter-box">
<span className="filter-cell">
<span className="filter-name">
{dateFilter.name}
</span>
<span className="filter-body">
<DatePicker
showTime
format={dateFilter.format}
value={value}
placeholder="请选择日期"
onChange={(date: moment.Moment | null, dateString: string) => {
that.onValueChange(date, dateString)
}}/>
</span>
</span>
</span>
);
}
}
export default DateFilterBox;
@@ -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<IProps, IState> {
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 (
<span className="filter-block date-time-filter-box">
<span className="filter-cell">
<span className="filter-name">
{dateTimeFilter.name}
</span>
<span className="filter-body">
<DatePicker
showTime
format={dateTimeFilter.format}
value={value}
placeholder="请选择日期时间"
onChange={(date: moment.Moment | null, dateString: string) => {
that.onValueChange(date, dateString)
}}/>
</span>
</span>
</span>
);
}
}
export default DateTimeFilterBox;

Some files were not shown because too many files have changed in this diff Show More