diff --git a/app/web/src/assets/logo.svg b/app/web/src/assets/logo.svg
new file mode 100644
index 00000000..e9f8c2a9
--- /dev/null
+++ b/app/web/src/assets/logo.svg
@@ -0,0 +1,43 @@
+
+
\ No newline at end of file
diff --git a/app/web/src/defaultSettings.js b/app/web/src/defaultSettings.js
new file mode 100644
index 00000000..c254c74e
--- /dev/null
+++ b/app/web/src/defaultSettings.js
@@ -0,0 +1,9 @@
+module.exports = {
+ navTheme: 'dark', // theme for nav menu
+ primaryColor: '#1890FF', // primary color of ant design
+ layout: 'sidemenu', // nav menu position: sidemenu or topmenu
+ contentWidth: 'Fluid', // layout of content: Fluid or Fixed, only works when layout is topmenu
+ fixedHeader: false, // sticky header
+ autoHideHeader: false, // auto hide header
+ fixSiderbar: false, // sticky siderbar
+};
diff --git a/app/web/src/models/global.js b/app/web/src/models/global.js
new file mode 100644
index 00000000..62f5c6a2
--- /dev/null
+++ b/app/web/src/models/global.js
@@ -0,0 +1,67 @@
+import { queryNotices } from '@/services/api';
+
+export default {
+ namespace: 'global',
+
+ state: {
+ collapsed: false,
+ notices: [],
+ },
+
+ effects: {
+ *fetchNotices(_, { call, put }) {
+ const data = yield call(queryNotices);
+ yield put({
+ type: 'saveNotices',
+ payload: data,
+ });
+ yield put({
+ type: 'user/changeNotifyCount',
+ payload: data.length,
+ });
+ },
+ *clearNotices({ payload }, { put, select }) {
+ yield put({
+ type: 'saveClearedNotices',
+ payload,
+ });
+ const count = yield select(state => state.global.notices.length);
+ yield put({
+ type: 'user/changeNotifyCount',
+ payload: count,
+ });
+ },
+ },
+
+ reducers: {
+ changeLayoutCollapsed(state, { payload }) {
+ return {
+ ...state,
+ collapsed: payload,
+ };
+ },
+ saveNotices(state, { payload }) {
+ return {
+ ...state,
+ notices: payload,
+ };
+ },
+ saveClearedNotices(state, { payload }) {
+ return {
+ ...state,
+ notices: state.notices.filter(item => item.type !== payload),
+ };
+ },
+ },
+
+ subscriptions: {
+ setup({ history }) {
+ // Subscribe history(url) change, trigger `load` action if pathname is `/`
+ return history.listen(({ pathname, search }) => {
+ if (typeof window.ga !== 'undefined') {
+ window.ga('send', 'pageview', pathname + search);
+ }
+ });
+ },
+ },
+};
diff --git a/app/web/src/models/setting.js b/app/web/src/models/setting.js
new file mode 100644
index 00000000..171da48d
--- /dev/null
+++ b/app/web/src/models/setting.js
@@ -0,0 +1,123 @@
+import { message } from 'antd';
+import defaultSettings from '../defaultSettings';
+
+let lessNodesAppended;
+const updateTheme = primaryColor => {
+ // Don't compile less in production!
+ if (APP_TYPE !== 'site') {
+ return;
+ }
+ // Determine if the component is remounted
+ if (!primaryColor) {
+ return;
+ }
+ const hideMessage = message.loading('正在编译主题!', 0);
+ function buildIt() {
+ if (!window.less) {
+ return;
+ }
+ setTimeout(() => {
+ window.less
+ .modifyVars({
+ '@primary-color': primaryColor,
+ })
+ .then(() => {
+ hideMessage();
+ })
+ .catch(() => {
+ message.error('Failed to update theme');
+ hideMessage();
+ });
+ }, 200);
+ }
+ if (!lessNodesAppended) {
+ // insert less.js and color.less
+ const lessStyleNode = document.createElement('link');
+ const lessConfigNode = document.createElement('script');
+ const lessScriptNode = document.createElement('script');
+ lessStyleNode.setAttribute('rel', 'stylesheet/less');
+ lessStyleNode.setAttribute('href', '/color.less');
+ lessConfigNode.innerHTML = `
+ window.less = {
+ async: true,
+ env: 'production',
+ javascriptEnabled: true
+ };
+ `;
+ lessScriptNode.src = 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js';
+ lessScriptNode.async = true;
+ lessScriptNode.onload = () => {
+ buildIt();
+ lessScriptNode.onload = null;
+ };
+ document.body.appendChild(lessStyleNode);
+ document.body.appendChild(lessConfigNode);
+ document.body.appendChild(lessScriptNode);
+ lessNodesAppended = true;
+ } else {
+ buildIt();
+ }
+};
+
+const updateColorWeak = colorWeak => {
+ document.body.className = colorWeak ? 'colorWeak' : '';
+};
+
+export default {
+ namespace: 'setting',
+ state: defaultSettings,
+ reducers: {
+ getSetting(state) {
+ const setting = {};
+ const urlParams = new URL(window.location.href);
+ Object.keys(state).forEach(key => {
+ if (urlParams.searchParams.has(key)) {
+ const value = urlParams.searchParams.get(key);
+ setting[key] = value === '1' ? true : value;
+ }
+ });
+ const { primaryColor, colorWeak } = setting;
+ if (state.primaryColor !== primaryColor) {
+ updateTheme(primaryColor);
+ }
+ updateColorWeak(colorWeak);
+ return {
+ ...state,
+ ...setting,
+ };
+ },
+ changeSetting(state, { payload }) {
+ const urlParams = new URL(window.location.href);
+ Object.keys(defaultSettings).forEach(key => {
+ if (urlParams.searchParams.has(key)) {
+ urlParams.searchParams.delete(key);
+ }
+ });
+ Object.keys(payload).forEach(key => {
+ if (key === 'collapse') {
+ return;
+ }
+ let value = payload[key];
+ if (value === true) {
+ value = 1;
+ }
+ if (defaultSettings[key] !== value) {
+ urlParams.searchParams.set(key, value);
+ }
+ });
+ const { primaryColor, colorWeak, contentWidth } = payload;
+ if (state.primaryColor !== primaryColor) {
+ updateTheme(primaryColor);
+ }
+ if (state.contentWidth !== contentWidth && window.dispatchEvent) {
+ window.dispatchEvent(new Event('resize'));
+ }
+ updateColorWeak(colorWeak);
+ window.history.replaceState(null, 'setting', urlParams.href);
+ return {
+ ...state,
+ ...payload,
+ };
+ },
+ },
+};
diff --git a/app/web/src/models/user.js b/app/web/src/models/user.js
new file mode 100644
index 00000000..3ceb127c
--- /dev/null
+++ b/app/web/src/models/user.js
@@ -0,0 +1,51 @@
+import { query as queryUsers, queryCurrent } from '@/services/user';
+
+export default {
+ namespace: 'user',
+
+ state: {
+ list: [],
+ currentUser: {},
+ },
+
+ effects: {
+ *fetch(_, { call, put }) {
+ const response = yield call(queryUsers);
+ yield put({
+ type: 'save',
+ payload: response,
+ });
+ },
+ *fetchCurrent(_, { call, put }) {
+ const response = yield call(queryCurrent);
+ yield put({
+ type: 'saveCurrentUser',
+ payload: response,
+ });
+ },
+ },
+
+ reducers: {
+ save(state, action) {
+ return {
+ ...state,
+ list: action.payload,
+ };
+ },
+ saveCurrentUser(state, action) {
+ return {
+ ...state,
+ currentUser: action.payload || {},
+ };
+ },
+ changeNotifyCount(state, action) {
+ return {
+ ...state,
+ currentUser: {
+ ...state.currentUser,
+ notifyCount: action.payload,
+ },
+ };
+ },
+ },
+};
diff --git a/app/web/src/pages/Exception/403.js b/app/web/src/pages/Exception/403.js
new file mode 100644
index 00000000..0e564154
--- /dev/null
+++ b/app/web/src/pages/Exception/403.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import { formatMessage } from 'umi';
+import {Link} from 'umi';
+import Exception from '@/components/Exception';
+
+const Exception403 = () => (
+
+);
+
+export default Exception403;
diff --git a/app/web/src/pages/Exception/404.js b/app/web/src/pages/Exception/404.js
new file mode 100644
index 00000000..84c986c5
--- /dev/null
+++ b/app/web/src/pages/Exception/404.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import { formatMessage } from 'umi/locale';
+import Link from 'umi/link';
+import Exception from '@/components/Exception';
+
+const Exception404 = () => (
+
+);
+
+export default Exception404;
diff --git a/app/web/src/pages/Exception/500.js b/app/web/src/pages/Exception/500.js
new file mode 100644
index 00000000..9d96f212
--- /dev/null
+++ b/app/web/src/pages/Exception/500.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import { formatMessage } from 'umi/locale';
+import Link from 'umi/link';
+import Exception from '@/components/Exception';
+
+const Exception500 = () => (
+
+);
+
+export default Exception500;
diff --git a/app/web/src/pages/Exception/TriggerException.js b/app/web/src/pages/Exception/TriggerException.js
new file mode 100644
index 00000000..15ace510
--- /dev/null
+++ b/app/web/src/pages/Exception/TriggerException.js
@@ -0,0 +1,50 @@
+import React, { PureComponent } from 'react';
+import { Button, Spin, Card } from 'antd';
+import { connect } from 'dva';
+import styles from './style.less';
+
+@connect(state => ({
+ isloading: state.error.isloading,
+}))
+class TriggerException extends PureComponent {
+ state = {
+ isloading: false,
+ };
+
+ triggerError = code => {
+ this.setState({
+ isloading: true,
+ });
+ const { dispatch } = this.props;
+ dispatch({
+ type: 'error/query',
+ payload: {
+ code,
+ },
+ });
+ };
+
+ render() {
+ const { isloading } = this.state;
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default TriggerException;
diff --git a/app/web/src/pages/Exception/models/error.js b/app/web/src/pages/Exception/models/error.js
new file mode 100644
index 00000000..1bfd9392
--- /dev/null
+++ b/app/web/src/pages/Exception/models/error.js
@@ -0,0 +1,28 @@
+import queryError from '@/services/error';
+
+export default {
+ namespace: 'error',
+
+ state: {
+ error: '',
+ isloading: false,
+ },
+
+ effects: {
+ *query({ payload }, { call, put }) {
+ yield call(queryError, payload.code);
+ yield put({
+ type: 'trigger',
+ payload: payload.code,
+ });
+ },
+ },
+
+ reducers: {
+ trigger(state, action) {
+ return {
+ error: action.payload,
+ };
+ },
+ },
+};
diff --git a/app/web/src/pages/Exception/style.less b/app/web/src/pages/Exception/style.less
new file mode 100644
index 00000000..91ec7dcf
--- /dev/null
+++ b/app/web/src/pages/Exception/style.less
@@ -0,0 +1,7 @@
+.trigger {
+ background: 'red';
+ :global(.ant-btn) {
+ margin-right: 8px;
+ margin-bottom: 12px;
+ }
+}
diff --git a/app/web/src/pages/helloworld.js b/app/web/src/pages/helloworld.js
new file mode 100644
index 00000000..7a158170
--- /dev/null
+++ b/app/web/src/pages/helloworld.js
@@ -0,0 +1,22 @@
+import {Component} from 'react';
+import {Card} from 'antd';
+
+class Helloworld extends Component {
+ render() {
+ return (
+
+ }
+ title="Alipay"
+ description="在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。"
+ />
+
+ );
+ }
+}
+
+export default Helloworld;
diff --git a/app/web/src/services/api.js b/app/web/src/services/api.js
new file mode 100644
index 00000000..b85a0a63
--- /dev/null
+++ b/app/web/src/services/api.js
@@ -0,0 +1,126 @@
+//import { stringify } from 'qs';
+import request from '@/utils/request';
+
+export async function queryProjectNotice() {
+ return request('/api/project/notice');
+}
+
+export async function queryActivities() {
+ return request('/api/activities');
+}
+
+export async function queryRule(params) {
+ return request(`/api/rule?${stringify(params)}`);
+}
+
+export async function removeRule(params) {
+ return request('/api/rule', {
+ method: 'POST',
+ body: {
+ ...params,
+ method: 'delete',
+ },
+ });
+}
+
+export async function addRule(params) {
+ return request('/api/rule', {
+ method: 'POST',
+ body: {
+ ...params,
+ method: 'post',
+ },
+ });
+}
+
+export async function updateRule(params) {
+ return request('/api/rule', {
+ method: 'POST',
+ body: {
+ ...params,
+ method: 'update',
+ },
+ });
+}
+
+export async function fakeSubmitForm(params) {
+ return request('/api/forms', {
+ method: 'POST',
+ body: params,
+ });
+}
+
+export async function fakeChartData() {
+ return request('/api/fake_chart_data');
+}
+
+export async function queryTags() {
+ return request('/api/tags');
+}
+
+export async function queryBasicProfile() {
+ return request('/api/profile/basic');
+}
+
+export async function queryAdvancedProfile() {
+ return request('/api/profile/advanced');
+}
+
+export async function queryFakeList(params) {
+ return request(`/api/fake_list?${stringify(params)}`);
+}
+
+export async function removeFakeList(params) {
+ const { count = 5, ...restParams } = params;
+ return request(`/api/fake_list?count=${count}`, {
+ method: 'POST',
+ body: {
+ ...restParams,
+ method: 'delete',
+ },
+ });
+}
+
+export async function addFakeList(params) {
+ const { count = 5, ...restParams } = params;
+ return request(`/api/fake_list?count=${count}`, {
+ method: 'POST',
+ body: {
+ ...restParams,
+ method: 'post',
+ },
+ });
+}
+
+export async function updateFakeList(params) {
+ const { count = 5, ...restParams } = params;
+ return request(`/api/fake_list?count=${count}`, {
+ method: 'POST',
+ body: {
+ ...restParams,
+ method: 'update',
+ },
+ });
+}
+
+export async function fakeAccountLogin(params) {
+ return request('/api/login/account', {
+ method: 'POST',
+ body: params,
+ });
+}
+
+export async function fakeRegister(params) {
+ return request('/api/register', {
+ method: 'POST',
+ body: params,
+ });
+}
+
+export async function queryNotices() {
+ return request('/api/notices');
+}
+
+export async function getFakeCaptcha(mobile) {
+ return request(`/api/captcha?mobile=${mobile}`);
+}
diff --git a/app/web/src/services/error.js b/app/web/src/services/error.js
new file mode 100644
index 00000000..13f2e942
--- /dev/null
+++ b/app/web/src/services/error.js
@@ -0,0 +1,5 @@
+import request from '@/utils/request';
+
+export default async function queryError(code) {
+ return request(`/api/${code}`);
+}
diff --git a/app/web/src/services/user.js b/app/web/src/services/user.js
new file mode 100644
index 00000000..89e03c6f
--- /dev/null
+++ b/app/web/src/services/user.js
@@ -0,0 +1,9 @@
+import request from '@/utils/request';
+
+export async function query() {
+ return request('/api/users');
+}
+
+export async function queryCurrent() {
+ return request('/api/currentUser');
+}
diff --git a/app/web/src/utils/Authorized.js b/app/web/src/utils/Authorized.js
new file mode 100644
index 00000000..8c420cba
--- /dev/null
+++ b/app/web/src/utils/Authorized.js
@@ -0,0 +1,12 @@
+import RenderAuthorized from '@/components/Authorized';
+import { getAuthority } from './authority';
+
+let Authorized = RenderAuthorized(getAuthority()); // eslint-disable-line
+
+// Reload the rights component
+const reloadAuthorized = () => {
+ Authorized = RenderAuthorized(getAuthority());
+};
+
+export { reloadAuthorized };
+export default Authorized;
diff --git a/app/web/src/utils/Yuan.js b/app/web/src/utils/Yuan.js
new file mode 100644
index 00000000..434a57fb
--- /dev/null
+++ b/app/web/src/utils/Yuan.js
@@ -0,0 +1,31 @@
+import React from 'react';
+import { yuan } from '@/components/Charts';
+/**
+ * 减少使用 dangerouslySetInnerHTML
+ */
+export default class Yuan extends React.PureComponent {
+ componentDidMount() {
+ this.rendertoHtml();
+ }
+
+ componentDidUpdate() {
+ this.rendertoHtml();
+ }
+
+ rendertoHtml = () => {
+ const { children } = this.props;
+ if (this.main) {
+ this.main.innerHTML = yuan(children);
+ }
+ };
+
+ render() {
+ return (
+ {
+ this.main = ref;
+ }}
+ />
+ );
+ }
+}
diff --git a/app/web/src/utils/authority.js b/app/web/src/utils/authority.js
new file mode 100644
index 00000000..3d2e7b34
--- /dev/null
+++ b/app/web/src/utils/authority.js
@@ -0,0 +1,22 @@
+// use localStorage to store the authority info, which might be sent from server in actual project.
+export function getAuthority(str) {
+ // return localStorage.getItem('antd-pro-authority') || ['admin', 'user'];
+ const authorityString =
+ typeof str === 'undefined' ? localStorage.getItem('antd-pro-authority') : str;
+ // authorityString could be admin, "admin", ["admin"]
+ let authority;
+ try {
+ authority = JSON.parse(authorityString);
+ } catch (e) {
+ authority = authorityString;
+ }
+ if (typeof authority === 'string') {
+ return [authority];
+ }
+ return authority || ['admin'];
+}
+
+export function setAuthority(authority) {
+ const proAuthority = typeof authority === 'string' ? [authority] : authority;
+ return localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority));
+}
diff --git a/app/web/src/utils/authority.test.js b/app/web/src/utils/authority.test.js
new file mode 100644
index 00000000..8a6cd41f
--- /dev/null
+++ b/app/web/src/utils/authority.test.js
@@ -0,0 +1,19 @@
+import { getAuthority } from './authority';
+
+describe('getAuthority should be strong', () => {
+ it('empty', () => {
+ expect(getAuthority(null)).toEqual(['admin']); // default value
+ });
+ it('string', () => {
+ expect(getAuthority('admin')).toEqual(['admin']);
+ });
+ it('array with double quotes', () => {
+ expect(getAuthority('"admin"')).toEqual(['admin']);
+ });
+ it('array with single item', () => {
+ expect(getAuthority('["admin"]')).toEqual(['admin']);
+ });
+ it('array with multiple items', () => {
+ expect(getAuthority('["admin", "guest"]')).toEqual(['admin', 'guest']);
+ });
+});
diff --git a/app/web/src/utils/request.js b/app/web/src/utils/request.js
new file mode 100644
index 00000000..98d1319f
--- /dev/null
+++ b/app/web/src/utils/request.js
@@ -0,0 +1,158 @@
+import fetch from 'dva/fetch';
+import { notification } from 'antd';
+import {history} from 'umi';
+import hash from 'hash.js';
+import { isAntdPro } from './utils';
+
+const codeMessage = {
+ 200: '服务器成功返回请求的数据。',
+ 201: '新建或修改数据成功。',
+ 202: '一个请求已经进入后台排队(异步任务)。',
+ 204: '删除数据成功。',
+ 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
+ 401: '用户没有权限(令牌、用户名、密码错误)。',
+ 403: '用户得到授权,但是访问是被禁止的。',
+ 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
+ 406: '请求的格式不可得。',
+ 410: '请求的资源被永久删除,且不会再得到的。',
+ 422: '当创建一个对象时,发生一个验证错误。',
+ 500: '服务器发生错误,请检查服务器。',
+ 502: '网关错误。',
+ 503: '服务不可用,服务器暂时过载或维护。',
+ 504: '网关超时。',
+};
+
+const checkStatus = response => {
+ if (response.status >= 200 && response.status < 300) {
+ return response;
+ }
+ const errortext = codeMessage[response.status] || response.statusText;
+ notification.error({
+ message: `请求错误 ${response.status}: ${response.url}`,
+ description: errortext,
+ });
+ const error = new Error(errortext);
+ error.name = response.status;
+ error.response = response;
+ throw error;
+};
+
+const cachedSave = (response, hashcode) => {
+ /**
+ * Clone a response data and store it in sessionStorage
+ * Does not support data other than json, Cache only json
+ */
+ const contentType = response.headers.get('Content-Type');
+ if (contentType && contentType.match(/application\/json/i)) {
+ // All data is saved as text
+ response
+ .clone()
+ .text()
+ .then(content => {
+ sessionStorage.setItem(hashcode, content);
+ sessionStorage.setItem(`${hashcode}:timestamp`, Date.now());
+ });
+ }
+ return response;
+};
+
+/**
+ * Requests a URL, returning a promise.
+ *
+ * @param {string} url The URL we want to request
+ * @param {object} [option] The options we want to pass to "fetch"
+ * @return {object} An object containing either "data" or "err"
+ */
+export default function request(
+ url,
+ option,
+) {
+ const options = {
+ expirys: isAntdPro(),
+ ...option,
+ };
+ /**
+ * Produce fingerprints based on url and parameters
+ * Maybe url has the same parameters
+ */
+ const fingerprint = url + (options.body ? JSON.stringify(options.body) : '');
+ const hashcode = hash
+ .sha256()
+ .update(fingerprint)
+ .digest('hex');
+
+ const defaultOptions = {
+ credentials: 'include',
+ };
+ const newOptions = { ...defaultOptions, ...options };
+ if (
+ newOptions.method === 'POST' ||
+ newOptions.method === 'PUT' ||
+ newOptions.method === 'DELETE'
+ ) {
+ if (!(newOptions.body instanceof FormData)) {
+ newOptions.headers = {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json; charset=utf-8',
+ ...newOptions.headers,
+ };
+ newOptions.body = JSON.stringify(newOptions.body);
+ } else {
+ // newOptions.body is FormData
+ newOptions.headers = {
+ Accept: 'application/json',
+ ...newOptions.headers,
+ };
+ }
+ }
+
+ const expirys = options.expirys && 60;
+ // options.expirys !== false, return the cache,
+ if (options.expirys !== false) {
+ const cached = sessionStorage.getItem(hashcode);
+ const whenCached = sessionStorage.getItem(`${hashcode}:timestamp`);
+ if (cached !== null && whenCached !== null) {
+ const age = (Date.now() - whenCached) / 1000;
+ if (age < expirys) {
+ const response = new Response(new Blob([cached]));
+ return response.json();
+ }
+ sessionStorage.removeItem(hashcode);
+ sessionStorage.removeItem(`${hashcode}:timestamp`);
+ }
+ }
+ return fetch(url, newOptions)
+ .then(checkStatus)
+ .then(response => cachedSave(response, hashcode))
+ .then(response => {
+ // DELETE and 204 do not return data by default
+ // using .json will report an error.
+ if (newOptions.method === 'DELETE' || response.status === 204) {
+ return response.text();
+ }
+ return response.json();
+ })
+ .catch(e => {
+ const status = e.name;
+ if (status === 401) {
+ // @HACK
+ /* eslint-disable no-underscore-dangle */
+ window.g_app._store.dispatch({
+ type: 'login/logout',
+ });
+ return;
+ }
+ // environment should not be used
+ if (status === 403) {
+ history.push('/exception/403');
+ return;
+ }
+ if (status <= 504 && status >= 500) {
+ history.push('/exception/500');
+ return;
+ }
+ if (status >= 404 && status < 422) {
+ history.push('/exception/404');
+ }
+ });
+}
diff --git a/app/web/src/utils/utils.js b/app/web/src/utils/utils.js
new file mode 100644
index 00000000..3c795c22
--- /dev/null
+++ b/app/web/src/utils/utils.js
@@ -0,0 +1,183 @@
+import moment from 'moment';
+import React from 'react';
+import nzh from 'nzh/cn';
+import { parse, stringify } from 'qs';
+
+export function fixedZero(val) {
+ return val * 1 < 10 ? `0${val}` : val;
+}
+
+export function getTimeDistance(type) {
+ const now = new Date();
+ const oneDay = 1000 * 60 * 60 * 24;
+
+ if (type === 'today') {
+ now.setHours(0);
+ now.setMinutes(0);
+ now.setSeconds(0);
+ return [moment(now), moment(now.getTime() + (oneDay - 1000))];
+ }
+
+ if (type === 'week') {
+ let day = now.getDay();
+ now.setHours(0);
+ now.setMinutes(0);
+ now.setSeconds(0);
+
+ if (day === 0) {
+ day = 6;
+ } else {
+ day -= 1;
+ }
+
+ const beginTime = now.getTime() - day * oneDay;
+
+ return [moment(beginTime), moment(beginTime + (7 * oneDay - 1000))];
+ }
+
+ if (type === 'month') {
+ const year = now.getFullYear();
+ const month = now.getMonth();
+ const nextDate = moment(now).add(1, 'months');
+ const nextYear = nextDate.year();
+ const nextMonth = nextDate.month();
+
+ return [
+ moment(`${year}-${fixedZero(month + 1)}-01 00:00:00`),
+ moment(moment(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() - 1000),
+ ];
+ }
+
+ const year = now.getFullYear();
+ return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)];
+}
+
+export function getPlainNode(nodeList, parentPath = '') {
+ const arr = [];
+ nodeList.forEach(node => {
+ const item = node;
+ item.path = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/');
+ item.exact = true;
+ if (item.children && !item.component) {
+ arr.push(...getPlainNode(item.children, item.path));
+ } else {
+ if (item.children && item.component) {
+ item.exact = false;
+ }
+ arr.push(item);
+ }
+ });
+ return arr;
+}
+
+export function digitUppercase(n) {
+ return nzh.toMoney(n);
+}
+
+function getRelation(str1, str2) {
+ if (str1 === str2) {
+ console.warn('Two path are equal!'); // eslint-disable-line
+ }
+ const arr1 = str1.split('/');
+ const arr2 = str2.split('/');
+ if (arr2.every((item, index) => item === arr1[index])) {
+ return 1;
+ }
+ if (arr1.every((item, index) => item === arr2[index])) {
+ return 2;
+ }
+ return 3;
+}
+
+function getRenderArr(routes) {
+ let renderArr = [];
+ renderArr.push(routes[0]);
+ for (let i = 1; i < routes.length; i += 1) {
+ // 去重
+ renderArr = renderArr.filter(item => getRelation(item, routes[i]) !== 1);
+ // 是否包含
+ const isAdd = renderArr.every(item => getRelation(item, routes[i]) === 3);
+ if (isAdd) {
+ renderArr.push(routes[i]);
+ }
+ }
+ return renderArr;
+}
+
+/**
+ * Get router routing configuration
+ * { path:{name,...param}}=>Array<{name,path ...param}>
+ * @param {string} path
+ * @param {routerData} routerData
+ */
+export function getRoutes(path, routerData) {
+ let routes = Object.keys(routerData).filter(
+ routePath => routePath.indexOf(path) === 0 && routePath !== path
+ );
+ // Replace path to '' eg. path='user' /user/name => name
+ routes = routes.map(item => item.replace(path, ''));
+ // Get the route to be rendered to remove the deep rendering
+ const renderArr = getRenderArr(routes);
+ // Conversion and stitching parameters
+ const renderRoutes = renderArr.map(item => {
+ const exact = !routes.some(route => route !== item && getRelation(route, item) === 1);
+ return {
+ exact,
+ ...routerData[`${path}${item}`],
+ key: `${path}${item}`,
+ path: `${path}${item}`,
+ };
+ });
+ return renderRoutes;
+}
+
+export function getPageQuery() {
+ return parse(window.location.href.split('?')[1]);
+}
+
+export function getQueryPath(path = '', query = {}) {
+ const search = stringify(query);
+ if (search.length) {
+ return `${path}?${search}`;
+ }
+ return path;
+}
+
+/* eslint no-useless-escape:0 */
+const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
+
+export function isUrl(path) {
+ return reg.test(path);
+}
+
+export function formatWan(val) {
+ const v = val * 1;
+ if (!v || Number.isNaN(v)) return '';
+
+ let result = val;
+ if (val > 10000) {
+ result = Math.floor(val / 10000);
+ result = (
+
+ {result}
+
+ 万
+
+
+ );
+ }
+ return result;
+}
+
+export function isAntdPro() {
+ return window.location.hostname === 'preview.pro.ant.design';
+}
diff --git a/app/web/src/utils/utils.less b/app/web/src/utils/utils.less
new file mode 100644
index 00000000..72579225
--- /dev/null
+++ b/app/web/src/utils/utils.less
@@ -0,0 +1,50 @@
+.textOverflow() {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ white-space: nowrap;
+}
+
+.textOverflowMulti(@line: 3, @bg: #fff) {
+ overflow: hidden;
+ position: relative;
+ line-height: 1.5em;
+ max-height: @line * 1.5em;
+ text-align: justify;
+ margin-right: -1em;
+ padding-right: 1em;
+ &:before {
+ background: @bg;
+ content: '...';
+ padding: 0 1px;
+ position: absolute;
+ right: 14px;
+ bottom: 0;
+ }
+ &:after {
+ background: white;
+ content: '';
+ margin-top: 0.2em;
+ position: absolute;
+ right: 14px;
+ width: 1em;
+ height: 1em;
+ }
+}
+
+// mixins for clearfix
+// ------------------------
+.clearfix() {
+ zoom: 1;
+ &:before,
+ &:after {
+ content: ' ';
+ display: table;
+ }
+ &:after {
+ clear: both;
+ visibility: hidden;
+ font-size: 0;
+ height: 0;
+ }
+}
diff --git a/app/web/src/utils/utils.test.js b/app/web/src/utils/utils.test.js
new file mode 100644
index 00000000..282db154
--- /dev/null
+++ b/app/web/src/utils/utils.test.js
@@ -0,0 +1,64 @@
+import { fixedZero, isUrl } from './utils';
+
+describe('fixedZero tests', () => {
+ it('should not pad large numbers', () => {
+ expect(fixedZero(10)).toEqual(10);
+ expect(fixedZero(11)).toEqual(11);
+ expect(fixedZero(15)).toEqual(15);
+ expect(fixedZero(20)).toEqual(20);
+ expect(fixedZero(100)).toEqual(100);
+ expect(fixedZero(1000)).toEqual(1000);
+ });
+
+ it('should pad single digit numbers and return them as string', () => {
+ expect(fixedZero(0)).toEqual('00');
+ expect(fixedZero(1)).toEqual('01');
+ expect(fixedZero(2)).toEqual('02');
+ expect(fixedZero(3)).toEqual('03');
+ expect(fixedZero(4)).toEqual('04');
+ expect(fixedZero(5)).toEqual('05');
+ expect(fixedZero(6)).toEqual('06');
+ expect(fixedZero(7)).toEqual('07');
+ expect(fixedZero(8)).toEqual('08');
+ expect(fixedZero(9)).toEqual('09');
+ });
+
+});
+
+describe('isUrl tests', () => {
+ it('should return false for invalid and corner case inputs', () => {
+ expect(isUrl([])).toBeFalsy();
+ expect(isUrl({})).toBeFalsy();
+ expect(isUrl(false)).toBeFalsy();
+ expect(isUrl(true)).toBeFalsy();
+ expect(isUrl(NaN)).toBeFalsy();
+ expect(isUrl(null)).toBeFalsy();
+ expect(isUrl(undefined)).toBeFalsy();
+ expect(isUrl()).toBeFalsy();
+ expect(isUrl('')).toBeFalsy();
+ });
+
+ it('should return false for invalid URLs', () => {
+ expect(isUrl('foo')).toBeFalsy();
+ expect(isUrl('bar')).toBeFalsy();
+ expect(isUrl('bar/test')).toBeFalsy();
+ expect(isUrl('http:/example.com/')).toBeFalsy();
+ expect(isUrl('ttp://example.com/')).toBeFalsy();
+ });
+
+ it('should return true for valid URLs', () => {
+ expect(isUrl('http://example.com/')).toBeTruthy();
+ expect(isUrl('https://example.com/')).toBeTruthy();
+ expect(isUrl('http://example.com/test/123')).toBeTruthy();
+ expect(isUrl('https://example.com/test/123')).toBeTruthy();
+ expect(isUrl('http://example.com/test/123?foo=bar')).toBeTruthy();
+ expect(isUrl('https://example.com/test/123?foo=bar')).toBeTruthy();
+ expect(isUrl('http://www.example.com/')).toBeTruthy();
+ expect(isUrl('https://www.example.com/')).toBeTruthy();
+ expect(isUrl('http://www.example.com/test/123')).toBeTruthy();
+ expect(isUrl('https://www.example.com/test/123')).toBeTruthy();
+ expect(isUrl('http://www.example.com/test/123?foo=bar')).toBeTruthy();
+ expect(isUrl('https://www.example.com/test/123?foo=bar')).toBeTruthy();
+ });
+
+});