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 @@ + + + + Group 28 Copy 5 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + \ 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(); + }); + +});