=> {
9 | try {
10 | let re;
11 | const filterText = compact(text.split(/\n/).map((str) => trim(str)));
12 | switch (model) {
13 | case 1:
14 | return filterText;
15 | case 3:
16 | re = /([,,])/;
17 | return compact(returnStr(filterText, re).map((str) => trim(str, ' 。.')));
18 | case 2:
19 | re = /([.。])/;
20 | return compact(returnStr(filterText, re).map((str) => trim(str, ' ,,')));
21 | }
22 | } catch (e) {
23 | console.log(e);
24 | }
25 | };
26 |
27 | const returnStr = (array: string[], re): string[] => {
28 | if (array.some((str) => str.match(re) === null)) {
29 | return array;
30 | } else
31 | return array
32 | .toString()
33 | .split(re)
34 | .filter((str) => str.match(re) === null);
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/Authorized/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import AuthorizedRoute, { authority } from './AuthorizedRoute';
3 | export type IReactComponent =
4 | | React.StatelessComponent
5 | | React.ComponentClass
6 | | React.ClassicComponentClass
;
7 |
8 | type Secured = (
9 | authority: authority,
10 | error?: React.ReactNode
11 | ) => (target: T) => T;
12 |
13 | type check = (
14 | authority: authority,
15 | target: T,
16 | Exception: S
17 | ) => T | S;
18 |
19 | export interface IAuthorizedProps {
20 | authority: authority;
21 | noMatch?: React.ReactNode;
22 | }
23 |
24 | export class Authorized extends React.Component {
25 | public static Secured: Secured;
26 | public static AuthorizedRoute: typeof AuthorizedRoute;
27 | public static check: check;
28 | }
29 |
30 | declare function renderAuthorize(currentAuthority: string): typeof Authorized;
31 |
32 | export default renderAuthorize;
33 |
--------------------------------------------------------------------------------
/src/components/Login/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Button from 'antd/lib/button';
3 | export interface LoginProps {
4 | defaultActiveKey?: string;
5 | onTabChange?: (key: string) => void;
6 | style?: React.CSSProperties;
7 | onSubmit?: (error: any, values: any) => void;
8 | }
9 |
10 | export interface TabProps {
11 | key?: string;
12 | tab?: React.ReactNode;
13 | }
14 | export class Tab extends React.Component {}
15 |
16 | export interface LoginItemProps {
17 | name?: string;
18 | rules?: any[];
19 | style?: React.CSSProperties;
20 | onGetCaptcha?: () => void;
21 | placeholder?: string;
22 | }
23 |
24 | export class LoginItem extends React.Component {}
25 |
26 | export default class Login extends React.Component {
27 | static Tab: typeof Tab;
28 | static UserName: typeof LoginItem;
29 | static Password: typeof LoginItem;
30 | static Mobile: typeof LoginItem;
31 | static Captcha: typeof LoginItem;
32 | static Submit: typeof Button;
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/persona.ts:
--------------------------------------------------------------------------------
1 | import { IDimGroup, TDimGroups } from '@/models/persona';
2 |
3 | export const extractKeyDimensions = (keyDimensions: TDimGroups) => {
4 | let dims = [];
5 | keyDimensions.forEach((keyDimension) => {
6 | keyDimension.dims.forEach((dim) => {
7 | dims.push(dim.labelKey);
8 | });
9 | });
10 | return dims;
11 | };
12 |
13 | export const generateTagId = (tagId: string, question: string): string => {
14 | return tagId === '' ? question : tagId;
15 | };
16 |
17 | /**
18 | * 在画布上根据 checkDims 的值过滤显示维度群组
19 | * @param dimGroups 维度群组
20 | * @param checkedDims 勾选的维度
21 | */
22 | export const displayDimGroups = (dimGroups: TDimGroups, checkedDims: string[]) => {
23 | const filterDimGroups = dimGroups.filter((dimGroup: IDimGroup) =>
24 | dimGroup.dims.some((dim) => checkedDims.some((key) => key === dim.labelKey))
25 | );
26 | return filterDimGroups.map((dimGroup) => ({
27 | ...dimGroup,
28 | dims: dimGroup.dims.filter((dim) => checkedDims.some((key) => key === dim.labelKey)),
29 | }));
30 | };
31 |
--------------------------------------------------------------------------------
/src/pages/Interview/components/RecordList/Editor/handlers/onEnter.ts:
--------------------------------------------------------------------------------
1 | import { Change } from 'slate';
2 |
3 | import Options from '../options';
4 | import { splitListItem, wrapInList } from '../changes/index';
5 | import { getCurrentItem } from '../utils/index';
6 |
7 | /**
8 | * User pressed Enter in an editor
9 | *
10 | * Enter in a list item should split the list item
11 | * Shift+Enter in a list item should make a new line
12 | */
13 | export default (event: any, change: Change, editor: any, opts: Options): void | any => {
14 | // Shift+Enter 另起一行,不分列
15 | if (event.shiftKey) {
16 | return undefined;
17 | }
18 | const { value } = change;
19 |
20 | const currentItem = getCurrentItem(opts, value);
21 |
22 | // Not in a list
23 | if (!currentItem) {
24 | return wrapInList(opts, change);
25 | }
26 | event.preventDefault();
27 |
28 | // If expanded, delete first.
29 | console.log(value.isExpanded);
30 | if (value.isExpanded) {
31 | change.delete();
32 | }
33 |
34 | return splitListItem(opts, change);
35 | };
36 |
--------------------------------------------------------------------------------
/src/pages/Interview/components/RecordList/PopupMenu.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount, shallow } from 'enzyme';
3 | import { spy, stub } from 'sinon';
4 | import App, { IPopupMenuProps } from './PopupMenu';
5 |
6 | const setup = () => {
7 | const dispatch = spy();
8 | const props: IPopupMenuProps = {
9 | menuRef: '',
10 | onChange: spy(),
11 | value: { activeMarks: [], change: spy() },
12 | };
13 | const wrapper = mount();
14 | return { props, wrapper, dispatch };
15 | };
16 |
17 | const { wrapper, dispatch, props } = setup();
18 |
19 | afterEach(() => {
20 | dispatch.resetHistory();
21 | });
22 | it('render', () => {
23 | expect(wrapper.find('.button-text').length).toEqual(1);
24 | });
25 |
26 | describe('response', () => {
27 | it('onClickMark should run when click menu', () => {
28 | const stubs = stub(wrapper.instance() as App, 'onClickMark');
29 | wrapper.find('.button-container').simulate('mousedown');
30 | expect(stubs.callCount).toEqual(1);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/data/quesData.ts:
--------------------------------------------------------------------------------
1 | import { generateKey } from '@/utils';
2 | import { TQuesData } from '@/models/data';
3 | export const quesData: TQuesData = [
4 | {
5 | records: [
6 | {
7 | key: generateKey(),
8 | question: '你的名字是?',
9 | answer: { text: 'A.小A', order: 0 },
10 | },
11 | {
12 | key: generateKey(),
13 | question: '你的性别是?',
14 | answer: { text: 'B.男', order: 1 },
15 | },
16 | {
17 | key: generateKey(),
18 | question: '你住在?',
19 | answer: { text: 'A.A', order: 0 },
20 | },
21 | ],
22 | },
23 | {
24 | records: [
25 | {
26 | key: generateKey(),
27 | question: '你的名字是?',
28 | answer: { text: 'B.小B', order: 1 },
29 | },
30 | {
31 | key: generateKey(),
32 | question: '你的性别是?',
33 | answer: { text: 'A.女', order: 0 },
34 | },
35 | {
36 | key: generateKey(),
37 | question: '你住在?',
38 | answer: { text: 'B.B', order: 1 },
39 | },
40 | ],
41 | },
42 | ];
43 |
--------------------------------------------------------------------------------
/src/styles/mixins/motion.less:
--------------------------------------------------------------------------------
1 | @import '../themes/default';
2 |
3 | .motion-common(@duration: @animation-duration-base) {
4 | animation-duration: @duration;
5 | animation-fill-mode: both;
6 | }
7 |
8 | .motion-common-leave(@duration: @animation-duration-base) {
9 | animation-duration: @duration;
10 | animation-fill-mode: both;
11 | }
12 |
13 | .make-motion(@className, @keyframeName, @duration: @animation-duration-base) {
14 | .@{className}-enter,
15 | .@{className}-appear {
16 | .motion-common(@duration);
17 | animation-play-state: paused;
18 | }
19 | .@{className}-leave {
20 | .motion-common-leave(@duration);
21 | animation-play-state: paused;
22 | }
23 | .@{className}-enter.@{className}-enter-active,
24 | .@{className}-appear.@{className}-appear-active {
25 | animation-name: ~"@{keyframeName}In";
26 | animation-play-state: running;
27 | }
28 | .@{className}-leave.@{className}-leave-active {
29 | animation-name: ~"@{keyframeName}Out";
30 | animation-play-state: running;
31 | pointer-events: none;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/menu.ts:
--------------------------------------------------------------------------------
1 | import { isUrl } from '@/utils';
2 | import { menuData } from '@/common';
3 |
4 | const formatter = (data, parentPath = '', parentAuthority?) => {
5 | return data.map((item) => {
6 | let { path } = item;
7 | if (!isUrl(path)) {
8 | path = parentPath + item.path;
9 | }
10 | const result = {
11 | ...item,
12 | path,
13 | authority: item.authority || parentAuthority,
14 | };
15 | if (item.children) {
16 | result.children = formatter(item.children, `${parentPath}${item.path}/`, item.authority);
17 | }
18 | return result;
19 | });
20 | };
21 | export const getMenuData = () => formatter(menuData);
22 |
23 | const redirectData: any[] = [];
24 | const getRedirect = (item) => {
25 | if (item && item.children) {
26 | if (item.children[0] && item.children[0].path) {
27 | redirectData.push({
28 | from: `/${item.path}`,
29 | to: `/${item.children[0].path}`,
30 | });
31 | }
32 | }
33 | };
34 |
35 | export const redirectMenuData = getMenuData().forEach(getRedirect);
36 |
--------------------------------------------------------------------------------
/src/utils/utils.less:
--------------------------------------------------------------------------------
1 | .textOverflow() {
2 | overflow: hidden;
3 | text-overflow: ellipsis;
4 | word-break: break-all;
5 | white-space: nowrap;
6 | }
7 |
8 | .textOverflowMulti(@line: 3, @bg: #fff) {
9 | overflow: hidden;
10 | position: relative;
11 | line-height: 1.5em;
12 | max-height: @line * 1.5em;
13 | text-align: justify;
14 | margin-right: -1em;
15 | padding-right: 1em;
16 | &:before {
17 | background: @bg;
18 | content: '...';
19 | padding: 0 1px;
20 | position: absolute;
21 | right: 14px;
22 | bottom: 0;
23 | }
24 | &:after {
25 | background: white;
26 | content: '';
27 | margin-top: 0.2em;
28 | position: absolute;
29 | right: 14px;
30 | width: 1em;
31 | height: 1em;
32 | }
33 | }
34 |
35 | // mixins for clearfix
36 | // ------------------------
37 | .clearfix() {
38 | zoom: 1;
39 | &:before,
40 | &:after {
41 | content: " ";
42 | display: table;
43 | }
44 | &:after {
45 | clear: both;
46 | visibility: hidden;
47 | font-size: 0;
48 | height: 0;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard",
3 | "rules": {
4 | "selector-pseudo-class-no-unknown": null,
5 | "shorthand-property-no-redundant-values": null,
6 | "at-rule-empty-line-before": null,
7 | "at-rule-name-space-after": null,
8 | "comment-empty-line-before": null,
9 | "declaration-bang-space-before": null,
10 | "declaration-empty-line-before": null,
11 | "function-comma-newline-after": null,
12 | "function-name-case": null,
13 | "function-parentheses-newline-inside": null,
14 | "function-max-empty-lines": null,
15 | "function-whitespace-after": null,
16 | "number-leading-zero": null,
17 | "number-no-trailing-zeros": null,
18 | "rule-empty-line-before": null,
19 | "selector-combinator-space-after": null,
20 | "selector-list-comma-newline-after": null,
21 | "selector-pseudo-element-colon-notation": null,
22 | "unit-no-unknown": null,
23 | "no-descending-specificity": null,
24 | "value-list-max-empty-lines": null,
25 | "font-family-no-missing-generic-family-keyword": null
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/Charts/MiniProgress/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Tooltip } from 'antd';
3 |
4 | import styles from './index.less';
5 |
6 | export interface MiniProgressProps {
7 | target?: number;
8 | color?: string;
9 | strokeWidth?: number;
10 | percent?: number;
11 | style?: React.CSSProperties;
12 | }
13 |
14 | export default class MiniProgress extends PureComponent {
15 | render() {
16 | const { color = '#439afc', strokeWidth, percent } = this.props;
17 |
18 | return (
19 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/Login/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/lib/style/themes/default.less';
2 |
3 | .login {
4 | .tabs {
5 | padding: 0 2px;
6 | margin: 0 -2px;
7 | :global {
8 | .ant-tabs-tab {
9 | font-size: 16px;
10 | line-height: 24px;
11 | }
12 | .ant-input-affix-wrapper {
13 | .ant-input:not(:first-child) {
14 | padding-left: 34px;
15 | }
16 | .ant-input {
17 | width: 100%;
18 | }
19 | }
20 | }
21 | }
22 |
23 | :global {
24 | .ant-tabs .ant-tabs-bar {
25 | border-bottom: 0;
26 | margin-bottom: 24px;
27 | text-align: center;
28 | }
29 | .ant-form-item {
30 | margin-bottom: 24px;
31 | }
32 | }
33 | .prefixIcon {
34 | font-size: @font-size-base;
35 | color: @disabled-color;
36 | }
37 |
38 | .getCaptcha {
39 | display: block;
40 | width: 100%;
41 | }
42 |
43 | .submit {
44 | width: 100%;
45 | margin-top: 24px;
46 | }
47 | }
48 |
49 | :global {
50 |
51 | .ant-checkbox-wrapper {
52 | user-select: none;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/data/records.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | object: 'value',
3 | document: {
4 | object: 'document',
5 | data: {},
6 | nodes: [
7 | {
8 | object: 'block',
9 | type: 'ul_list',
10 | isVoid: false,
11 | data: {},
12 | nodes: [
13 | {
14 | object: 'block',
15 | type: 'list_item',
16 | isVoid: false,
17 | data: {},
18 | nodes: [
19 | {
20 | object: 'block',
21 | type: 'paragraph',
22 | isVoid: false,
23 | data: {},
24 | nodes: [
25 | {
26 | object: 'text',
27 | leaves: [
28 | {
29 | object: 'leaf',
30 | text: '',
31 | marks: [],
32 | },
33 | ],
34 | },
35 | ],
36 | },
37 | ],
38 | },
39 | ],
40 | },
41 | ],
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/src/components/Header/index.less:
--------------------------------------------------------------------------------
1 | @import '~styles/themes/default';
2 |
3 | .header {
4 | background: @base-white;
5 | padding: 0 16px;
6 | color: @text-color;
7 | border-bottom: 1px solid @white-3;
8 | width: 100%;
9 | display: flex;
10 | justify-content: space-between;
11 | z-index: 100;
12 |
13 | :global {
14 | .ant-tabs-bar {
15 | border-bottom: 0;
16 | //border-image: linear-gradient(#439afc, #99f5ff) 1 1;
17 | margin: 0;
18 | }
19 | .ant-tabs-ink-bar {
20 | height: 3px;
21 | background: linear-gradient(to right, #439afc, #99f5ff);
22 | }
23 | }
24 | }
25 |
26 | .tool-container {
27 | display: flex;
28 | align-items: center;
29 | .icon {
30 | height: 24px;
31 | font-size: 18px;
32 | margin: 0 8px;
33 | display: table;
34 | cursor: pointer;
35 | animation: @animation-duration-base;
36 | &:hover {
37 | color: @primary-5;
38 | }
39 | &:active {
40 | color: @primary-7;
41 | }
42 | &::before {
43 | display: table-cell;
44 | vertical-align: middle;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/layouts/UserLayout.less:
--------------------------------------------------------------------------------
1 | @import '~styles/themes/default.less';
2 |
3 | .container {
4 | display: flex;
5 | flex-direction: column;
6 | min-height: 100%;
7 | background: #f0f2f5;
8 | align-items: center;
9 | }
10 |
11 | .content {
12 | padding-top: 64px;
13 | flex: 1;
14 | }
15 |
16 | @media (min-width: @screen-xs-min) {
17 | .container {
18 | background-image: url('./../assets/loginBg.png');
19 | background-size: cover;
20 | }
21 | }
22 |
23 | .top {
24 | text-align: center;
25 | }
26 |
27 | .header {
28 | height: 44px;
29 | line-height: 44px;
30 | a {
31 | text-decoration: none;
32 | }
33 | }
34 |
35 | .logo {
36 | height: 44px;
37 | vertical-align: top;
38 | margin-right: 16px;
39 | }
40 |
41 | .title {
42 | font-size: 33px;
43 | color: @heading-color;
44 | font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
45 | font-weight: 600;
46 | position: relative;
47 | top: 2px;
48 | }
49 |
50 | .desc {
51 | font-size: @font-size-base;
52 | color: @text-color-secondary;
53 | margin-top: 12px;
54 | margin-bottom: 40px;
55 | }
56 |
--------------------------------------------------------------------------------
/src/styles/mixins/iconfont.less:
--------------------------------------------------------------------------------
1 | .iconfont-mixin() {
2 | display: inline-block;
3 | font-style: normal;
4 | vertical-align: baseline;
5 | text-align: center;
6 | text-transform: none;
7 | line-height: 1;
8 | text-rendering: optimizeLegibility;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 | &:before {
12 | display: block;
13 | font-family: "anticon" !important;
14 | }
15 | }
16 |
17 | .iconfont-font(@content) {
18 | font-family: 'anticon';
19 | text-rendering: optimizeLegibility;
20 | -webkit-font-smoothing: antialiased;
21 | -moz-osx-font-smoothing: grayscale;
22 | content: @content;
23 | }
24 |
25 | // for iconfont font size
26 | // fix chrome 12px bug, support ie
27 | .iconfont-size-under-12px(@size, @rotate: 0deg) {
28 | display: inline-block;
29 | @font-scale: unit(@size / 12px);
30 | font-size: 12px;
31 | // IE9
32 | font-size: ~"@{size} \9"; // lesshint duplicateProperty: false
33 | transform: scale(@font-scale) rotate(@rotate);
34 | :root & {
35 | font-size: @font-size-sm; // reset IE9 and above
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/pages/Data/analysis.less:
--------------------------------------------------------------------------------
1 | @import '../../styles/themes/default';
2 |
3 | .base-card {
4 | background: @base-white;
5 | box-shadow: @shadow-1;
6 | border-radius: 4px;
7 | }
8 |
9 | .container {
10 | padding: 24px;
11 | display: flex;
12 | justify-content: space-between;
13 | flex-wrap: wrap;
14 | width: 100%;
15 | }
16 | .left {
17 | width: 25%;
18 | display: flex;
19 | flex-direction: column;
20 | justify-content: space-between;
21 | }
22 |
23 | .right {
24 | display: flex;
25 | flex-wrap: wrap;
26 | width: 75%;
27 | }
28 |
29 | .questionnares {
30 | .base-card;
31 | display: flex;
32 | font-size: @font-size-lg;
33 | justify-content: center;
34 | align-items: center;
35 | margin-top: 12px;
36 | margin-bottom: 24px;
37 | height: 20%;
38 | }
39 | .validation {
40 | .base-card;
41 | font-size: @font-size-lg;
42 | display: flex;
43 | flex-direction: column;
44 | align-items: center;
45 | height: 40%;
46 | margin-bottom: 24px;
47 | padding: 24px;
48 | }
49 | .dims {
50 | .base-card;
51 | height: 40%;
52 | margin-bottom: 12px;
53 | padding: 24px;
54 | }
55 |
--------------------------------------------------------------------------------
/src/layouts/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import App, { ILayoutEntryProps } from '.';
4 | import { spy } from 'sinon';
5 |
6 | const setup = () => {
7 | const dispatch = spy();
8 | const props: ILayoutEntryProps = {
9 | location: {
10 | hash: '',
11 | key: 'ohadrr',
12 | pathname: '/',
13 | search: '',
14 | state: undefined,
15 | },
16 | history: {},
17 | };
18 | //@ts-ignore TODO: WrappedComponent 如何 typing
19 | const wrapper = shallow(dsa} />);
20 | return { props, wrapper, dispatch };
21 | };
22 | const { wrapper, dispatch, props } = setup();
23 |
24 | it('render BasicLayout', () => {
25 | expect(wrapper.find('#BasicLayout').length).toEqual(1);
26 | expect(wrapper.find('#UserLayout').length).toEqual(0);
27 | });
28 |
29 | it('render UserLayout', () => {
30 | wrapper.setProps({
31 | location: {
32 | pathname: '/user/login',
33 | },
34 | });
35 | expect(wrapper.find('#BasicLayout').length).toEqual(0);
36 | expect(wrapper.find('#UserLayout').length).toEqual(1);
37 | });
38 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | stages:
2 | - test
3 | - build
4 | - release
5 | - deploy
6 |
7 | cache:
8 | paths:
9 | - node_modules/
10 |
11 | test:
12 | stage: test
13 | image: node:8-alpine
14 | before_script:
15 | - npm install
16 | script:
17 | - npm run cov
18 | lint:
19 | stage: test
20 | image: node:8-alpine
21 | before_script:
22 | - npm install
23 | script:
24 | - npm run lint:style
25 |
26 | build:
27 | stage: build
28 | script:
29 | - echo 'deployd!'
30 | #- npm run build
31 |
32 | release:
33 | stage: release
34 | image: docker:latest
35 | only:
36 | - tag
37 | services:
38 | - docker:dind
39 | script:
40 | - echo 'release!'
41 | # before_script:
42 | # - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
43 | # script:
44 | # - docker build -t ${CI_REGISTRY}/${CI_PROJECT_PATH}:$CI_COMMIT_TAG --pull .
45 | # - docker push ${CI_REGISTRY}/${CI_PROJECT_PATH}:$CI_COMMIT_TAG
46 | # after_script:
47 | # - docker logout ${CI_REGISTRY}
48 |
49 | deploy:
50 | stage: deploy
51 | script:
52 | - echo 'deployd!'
53 | only:
54 | - master
55 |
--------------------------------------------------------------------------------
/src/components/Charts/Radar/index.less:
--------------------------------------------------------------------------------
1 | @import "~antd/lib/style/themes/default.less";
2 |
3 | .radar {
4 | .legend {
5 | margin-top: 16px;
6 | .legendItem {
7 | position: relative;
8 | text-align: center;
9 | cursor: pointer;
10 | color: @text-color-secondary;
11 | line-height: 22px;
12 | p {
13 | margin: 0;
14 | }
15 | h6 {
16 | color: @heading-color;
17 | padding-left: 16px;
18 | font-size: 24px;
19 | line-height: 32px;
20 | margin-top: 4px;
21 | margin-bottom: 0;
22 | }
23 | &:after {
24 | background-color: @border-color-split;
25 | position: absolute;
26 | top: 8px;
27 | right: 0;
28 | height: 40px;
29 | width: 1px;
30 | content: '';
31 | }
32 | }
33 | > :last-child .legendItem:after {
34 | display: none;
35 | }
36 | .dot {
37 | border-radius: 6px;
38 | display: inline-block;
39 | margin-right: 6px;
40 | position: relative;
41 | top: -1px;
42 | height: 6px;
43 | width: 6px;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/layouts/BasicLayout.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import App, { IBasicLayoutProps } from './BasicLayout';
4 | import { spy } from 'sinon';
5 |
6 | const setup = () => {
7 | const dispatch = spy();
8 | const props: IBasicLayoutProps = {
9 | collapsed: false,
10 | showMenu: true,
11 | location: {
12 | hash: '',
13 | key: 'ohadrr',
14 | pathname: '/',
15 | query: {},
16 | search: '',
17 | state: undefined,
18 | },
19 | };
20 | //@ts-ignore TODO: WrappedComponent 如何 typing
21 | const wrapper = shallow();
22 | return { props, wrapper, dispatch };
23 | };
24 |
25 | const { wrapper, dispatch, props } = setup();
26 |
27 | it('render', () => {
28 | expect(wrapper.find('SiderMenu').length).toEqual(1);
29 | expect(wrapper.find('.layout').length).toEqual(1);
30 | });
31 |
32 | describe('response', () => {
33 | it('should collapse menu', () => {
34 | const SiderMenu = wrapper.find('SiderMenu');
35 | SiderMenu.simulate('collapse');
36 | expect(dispatch.callCount).toEqual(1);
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/components/GlobalFooter/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import classNames from 'classnames';
3 | import styles from './index.less';
4 | export interface GlobalFooterProps {
5 | links?: Array<{
6 | title: React.ReactNode;
7 | href: string;
8 | key: string;
9 | blankTarget?: boolean;
10 | }>;
11 | copyright?: React.ReactNode;
12 | style?: React.CSSProperties;
13 | className?: string;
14 | }
15 |
16 | export default class GlobalFooter extends Component {
17 | render() {
18 | const { className, links, copyright } = this.props;
19 | const clsString = classNames(styles.globalFooter, className);
20 | return (
21 |
22 | {links && (
23 |
30 | )}
31 | {copyright &&
{copyright}
}
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/pages/Data/components/LabelSelector/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { ILabel } from '@/models/label';
3 | import { Tag } from 'antd';
4 | import styles from './index.less';
5 | const { CheckableTag } = Tag;
6 |
7 | interface ILabelSelectorProps {
8 | labels: ILabel[];
9 | selectedLabels: string[];
10 | handleSelect: Function;
11 | }
12 | export default class LabelSelector extends Component {
13 | static defaultProps: ILabelSelectorProps = {
14 | labels: [],
15 | selectedLabels: [],
16 | handleSelect: () => {},
17 | };
18 |
19 | render() {
20 | const { labels, selectedLabels, handleSelect } = this.props;
21 | return (
22 |
23 | {labels.map((label: ILabel) => {
24 | const { key, text } = label;
25 | return (
26 | -1}
29 | onChange={(e) => handleSelect(e, key)}
30 | >
31 | {text}
32 |
33 | );
34 | })}
35 |
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/pages/Interview/components/TagGroup/index.less:
--------------------------------------------------------------------------------
1 | @import '~styles/themes/default';
2 |
3 | .ungroup {
4 | margin-top: 32px;
5 | margin-bottom: 20px;
6 | }
7 |
8 | .group {
9 | :global {
10 | .ant-collapse-item {
11 | border-bottom: none;
12 | }
13 | .ant-input {
14 | padding-left: 0;
15 | width: 35%;
16 | }
17 | .arrow {
18 | margin-top: 6px;
19 | }
20 | }
21 | .header {
22 | display: flex;
23 | justify-content: space-between;
24 | user-select: none;
25 |
26 | .title {
27 | cursor: default;
28 | height: 32px;
29 | display: flex;
30 | align-items: center;
31 | color: @text-color;
32 | }
33 | .close {
34 | color: @white-5;
35 | opacity: 0;
36 | cursor: pointer;
37 | transition: @animation-duration-base;
38 | &:active {
39 | color: @dark-2;
40 | transform: scale(0.4);
41 | }
42 | &:hover {
43 | color: @white-7;
44 | transform: scale(1.1);
45 | }
46 | }
47 | &:hover .close {
48 | opacity: 1;
49 | }
50 | }
51 | }
52 | .add-group {
53 | //background: red;
54 | padding-left: 40px !important;
55 | }
56 |
--------------------------------------------------------------------------------
/src/pages/Interview/components/RecordList/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import App, { IRecordListProps } from './index';
4 | import Plain from 'slate-plain-serializer';
5 | import { spy } from 'sinon';
6 | import { largeLabel } from '@/data/labels';
7 |
8 | const setup = () => {
9 | const dispatch = spy();
10 |
11 | const props: IRecordListProps = {
12 | records: Plain.deserialize('12345'),
13 | labels: largeLabel,
14 | };
15 | const wrapper = shallow();
16 | return {
17 | props,
18 | wrapper,
19 | dispatch,
20 | };
21 | };
22 | const { wrapper, props, dispatch } = setup();
23 | afterEach(() => {
24 | dispatch.resetHistory();
25 | });
26 |
27 | describe('RecordList 正常渲染样式', () => {
28 | it('RecordList Component should be render', () => {
29 | expect(wrapper.find('ListEditor').length).toEqual(1);
30 | // expect(wrapper).toMatchSnapshot();
31 | });
32 | });
33 |
34 | describe('function', () => {
35 | const instance = wrapper.instance() as App;
36 | it('setMenuRef', () => {
37 | instance.setMenuRef('2');
38 | expect(instance.menu).toEqual('2');
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/src/pages/Data/components/Plots.tsx:
--------------------------------------------------------------------------------
1 | import { Chart, Geom, Axis, Tooltip, Coord, Label, Legend, View, Guide, Shape } from 'bizcharts';
2 | import React, { Component } from 'react';
3 | interface IPlotsProps {
4 | data: number[];
5 | }
6 | const cols = {
7 | value: {
8 | min: 0,
9 | },
10 | n: {
11 | alias: '成分',
12 | range: [0, 1],
13 | },
14 | };
15 | export default class Plots extends Component {
16 | static defaultProps: IPlotsProps = {
17 | data: [],
18 | };
19 |
20 | render() {
21 | const { data } = this.props;
22 | const chartData = data.map((eigenValue, index) => ({
23 | n: index + 1,
24 | value: eigenValue,
25 | }));
26 | return (
27 |
28 |
29 | {
33 | return Number(val).toFixed(1);
34 | },
35 | }}
36 | />
37 |
38 |
39 |
40 |
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/pages/Persona/export.less:
--------------------------------------------------------------------------------
1 | @import '../../styles/themes/default';
2 | .base-card {
3 | background: @base-white;
4 | box-shadow: @shadow-1;
5 | border-radius: 4px;
6 | }
7 |
8 | .container {
9 | display: flex;
10 | flex-grow: 1;
11 | flex-direction: column;
12 | padding: 60px;
13 | width: 736px;
14 | margin: 12px 16px;
15 | min-height: 600px;
16 | .base-card;
17 | .preview-img {
18 | max-width: 100%;
19 | height: auto;
20 | }
21 | .option-container {
22 | margin-top: 16px;
23 | }
24 | .radio {
25 | display: block;
26 | height: 30px;
27 | line-height: 30px;
28 | }
29 | &:before {
30 | content: '';
31 | display: inline-block;
32 | height: 100%;
33 | vertical-align: middle;
34 | width: 0;
35 | }
36 | :global {
37 | .ant-modal {
38 | display: inline-block;
39 | vertical-align: middle;
40 | top: 0;
41 | text-align: left;
42 | }
43 | .ant-modal-title {
44 | text-align: center;
45 | }
46 | .ant-modal-header {
47 | border-bottom: 0;
48 | }
49 | .ant-modal-body {
50 | padding: 0 16px;
51 | }
52 | .ant-modal-footer {
53 | border-top: 0;
54 | padding: 16px;
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/services/api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const { get, post } = axios;
4 | export function saveDocument(params) {
5 | post('/api/documents', params);
6 | // request('/api/documents', {
7 | // method: 'POST',
8 | // 'Content-Type': 'application/json; charset=utf-8',
9 | // body: params,
10 | // });
11 | }
12 | export async function saveTagGroups(params) {
13 | post('/api/tag-groups', params);
14 |
15 | // request('/api/tag-groups', {
16 | // method: 'POST',
17 | // 'Content-Type': 'application/json; charset=utf-8',
18 | // body: params,
19 | // });
20 | }
21 |
22 | export async function uploadDocument(params) {
23 | console.log(params);
24 | return post('/api/upload', params);
25 | // return request('/api/upload', {
26 | // method: 'POST',
27 | // body: params,
28 | // });
29 | }
30 |
31 | export async function queryDocument() {
32 | return get('/api/documents');
33 | }
34 |
35 | export async function fakeAccountLogin(params) {
36 | return post('/api/login/account', params);
37 | }
38 |
39 | export async function fakeRegister(params) {
40 | return post('/api/register', params);
41 | }
42 |
43 | export async function queryNotices() {
44 | return get('/api/notices');
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/Charts/demo/radar.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 7
3 | title: 雷达图
4 | ---
5 |
6 | ````jsx
7 | import { Radar, ChartCard } from 'ant-design-pro/lib/Charts';
8 |
9 | const radarOriginData = [
10 | {
11 | name: '个人',
12 | ref: 10,
13 | koubei: 8,
14 | output: 4,
15 | contribute: 5,
16 | hot: 7,
17 | },
18 | {
19 | name: '团队',
20 | ref: 3,
21 | koubei: 9,
22 | output: 6,
23 | contribute: 3,
24 | hot: 1,
25 | },
26 | {
27 | name: '部门',
28 | ref: 4,
29 | koubei: 1,
30 | output: 6,
31 | contribute: 5,
32 | hot: 7,
33 | },
34 | ];
35 | const radarData = [];
36 | const radarTitleMap = {
37 | ref: '引用',
38 | koubei: '口碑',
39 | output: '产量',
40 | contribute: '贡献',
41 | hot: '热度',
42 | };
43 | radarOriginData.forEach((item) => {
44 | Object.keys(item).forEach((key) => {
45 | if (key !== 'name') {
46 | radarData.push({
47 | name: item.name,
48 | label: radarTitleMap[key],
49 | value: item[key],
50 | });
51 | }
52 | });
53 | });
54 |
55 | ReactDOM.render(
56 |
57 |
62 |
63 | , mountNode);
64 | ````
65 |
--------------------------------------------------------------------------------
/src/models/interview.ts:
--------------------------------------------------------------------------------
1 | import { DvaModel } from '@/typings/dva';
2 | import { generateKey, initRecords } from '@/utils';
3 |
4 | export interface IInterview {
5 | title: string;
6 | records: object;
7 | id: string;
8 | uploadVisible: boolean;
9 | tagVisible: boolean;
10 | }
11 |
12 | const interview: DvaModel = {
13 | state: {
14 | title: '',
15 | records: initRecords(''),
16 | id: generateKey(),
17 | uploadVisible: true,
18 | tagVisible: true,
19 | },
20 | reducers: {
21 | changeUploadVisible(state, action) {
22 | return {
23 | ...state,
24 | uploadVisible: !state.uploadVisible,
25 | };
26 | },
27 | changeTagVisible(state, action) {
28 | return {
29 | ...state,
30 | tagVisible: !state.tagVisible,
31 | };
32 | },
33 | changeTitle(state, { payload: title }) {
34 | return { ...state, title };
35 | },
36 |
37 | querryDocument(state, { payload }) {
38 | const { title, records, docId: id } = payload;
39 | return { ...state, records, title, id };
40 | },
41 |
42 | changeRecords(state, { payload: records }) {
43 | return { ...state, records };
44 | },
45 | },
46 | };
47 |
48 | export default interview;
49 |
--------------------------------------------------------------------------------
/src/utils/record.ts:
--------------------------------------------------------------------------------
1 | import { Value } from 'slate';
2 |
3 | export const initRecords = (text) => ({
4 | object: 'value',
5 | document: {
6 | object: 'document',
7 | data: {},
8 | nodes: [
9 | {
10 | object: 'block',
11 | type: 'ul_list',
12 | isVoid: false,
13 | data: {},
14 | nodes: [
15 | {
16 | object: 'block',
17 | type: 'list_item',
18 | isVoid: false,
19 | data: {},
20 | nodes: [
21 | {
22 | object: 'block',
23 | type: 'paragraph',
24 | isVoid: false,
25 | data: {},
26 | nodes: [
27 | {
28 | object: 'text',
29 | leaves: [
30 | {
31 | object: 'leaf',
32 | text,
33 | marks: [],
34 | },
35 | ],
36 | },
37 | ],
38 | },
39 | ],
40 | },
41 | ],
42 | },
43 | ],
44 | },
45 | });
46 |
47 | export const initRecordsAsValue = (text) => Value.fromJSON(initRecords(text));
48 |
--------------------------------------------------------------------------------
/src/pages/Interview/components/TagSelector/index.less:
--------------------------------------------------------------------------------
1 | @import '~styles/themes/default';
2 |
3 | .container {
4 | padding: @padding-lg @padding-md @padding-md @padding-md;
5 | :global {
6 | .ant-input {
7 | border-color: transparent;
8 | box-shadow: none;
9 | &:hover {
10 | border: solid 1px @white-5;
11 | box-shadow: none;
12 | }
13 | &:focus {
14 | border: solid 1px @white-7;
15 | color: @text-color;
16 | box-shadow: none;
17 | }
18 | }
19 | }
20 | }
21 |
22 | .dimension-container {
23 | display: flex;
24 | min-height: 32px;
25 | align-items: center;
26 | &:not(:last-child) {
27 | margin-bottom: @padding-md;
28 | }
29 | }
30 |
31 | .tag-container {
32 | display: inline-flex;
33 | margin-left: @padding-md;
34 | //max-width: 480px;
35 | flex-wrap: wrap;
36 | }
37 |
38 | .add-key {
39 | display: inline-flex;
40 | justify-content: flex-end;
41 | width: 136px;
42 | text-align: right;
43 | color: @text-color-secondary;
44 | padding-right: 11px;
45 | background: none;
46 | border-radius: 4px;
47 | &:hover {
48 | background-color: @base-white;
49 | }
50 | &:focus {
51 | color: @text-color;
52 | background-color: @base-white;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/pages/Data/components/VarianceExplain.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow, mount } from 'enzyme';
3 | import { spy } from 'sinon';
4 | import App, { IVarianceExplainProps } from './VarianceExplain';
5 | import { pca } from '@/data/ml';
6 | import { getAccumulation } from '@/utils';
7 | const { eigenValues, percent } = pca;
8 | const vEData = eigenValues.map((i, index) => ({
9 | key: index,
10 | eigenValue: i.toFixed(3),
11 | percent: (percent[index] * 100).toFixed(1) + '%',
12 | acc: (getAccumulation(percent)[index] * 100).toFixed(1) + '%',
13 | dims: index + 1,
14 | }));
15 | const setup = () => {
16 | const dispatch = spy();
17 | const props: IVarianceExplainProps = {
18 | rotation: false,
19 | data: vEData,
20 | };
21 | const wrapper = mount();
22 | return { props, wrapper, dispatch };
23 | };
24 |
25 | const { wrapper, dispatch, props } = setup();
26 | afterEach(() => {
27 | dispatch.resetHistory();
28 | });
29 |
30 | describe('render', () => {
31 | it('should not render rotation', () => {
32 | expect(wrapper.find('th').length).toEqual(4);
33 | });
34 | it('should render rotation', () => {
35 | wrapper.setProps({ rotation: true });
36 | expect(wrapper.find('th').length).toEqual(9);
37 | wrapper.setProps({ rotation: false });
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/src/pages/Persona/components/DraggableList.less:
--------------------------------------------------------------------------------
1 | @import '~styles/themes/default';
2 |
3 | .board {
4 | display: flex;
5 | //height: 150px;
6 | background: @white-1;
7 | margin: @padding-xs 0;
8 | padding: @padding-md;
9 | border: 2px solid @white-2;
10 | border-radius: 4px;
11 | flex: auto;
12 | flex-wrap: nowrap;
13 | user-select: none;
14 | transition: all @animation-duration-base;
15 | &:first-child {
16 | margin-top: 0;
17 | }
18 | &:last-child {
19 | margin-bottom: 0;
20 | }
21 |
22 | .dims {
23 | display: flex;
24 | align-items: center;
25 | margin-right: @padding-md;
26 | width: 80px;
27 | }
28 | }
29 |
30 | .board-dragging {
31 | .board;
32 | background: @blue-1;
33 | border: 2px dashed @blue-3;
34 | }
35 | .tag-container {
36 | display: flex;
37 | flex-wrap: wrap;
38 | }
39 | .tag {
40 | display: flex;
41 | align-items: center;
42 | background: @base-white;
43 | box-shadow: @shadow-0;
44 | padding: @padding-xs @padding-md;
45 | border-radius: 4px;
46 | margin-bottom: @padding-sm;
47 | margin-right: @padding-sm;
48 | user-select: none;
49 | cursor: pointer;
50 | transition: all @animation-duration-base;
51 | &:hover {
52 | box-shadow: @shadow-1;
53 | }
54 | &:active {
55 | background: @white-2;
56 | box-shadow: @shadow-0;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/common/persona.ts:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs';
2 | import { females, males } from '../assets/photos';
3 |
4 | const photo = [...females, ...males];
5 | const { Random } = Mock;
6 | export const dimGroups = [
7 | {
8 | text: '基本信息',
9 | key: 'basicInfo',
10 | dims: [],
11 | },
12 | {
13 | text: '动机',
14 | key: 'motivations',
15 | dims: [],
16 | },
17 | {
18 | text: '目标',
19 | key: 'goals',
20 | dims: [],
21 | },
22 | {
23 | text: '痛点',
24 | key: 'frustrations',
25 | dims: [],
26 | },
27 | {
28 | text: '行为变量',
29 | key: 'behavior',
30 | dims: [],
31 | },
32 | {
33 | text: '态度',
34 | key: 'attitude',
35 | dims: [],
36 | },
37 | {
38 | text: '设备偏好',
39 | key: 'device',
40 | dims: [],
41 | },
42 | {
43 | text: '相关决策者',
44 | key: 'influencers',
45 | dims: [],
46 | },
47 | {
48 | text: '技能',
49 | key: 'skill',
50 | dims: [],
51 | },
52 | {
53 | text: '活动',
54 | key: 'activities',
55 | dims: [],
56 | },
57 | ];
58 |
59 | export const basicInfo = () => ({
60 | keywords: '',
61 | name: Random.cname(),
62 | bios: '',
63 | career: Mock.mock({
64 | 'data|1': ['医生', '设计师', '制图员', '摄影师', '建筑师', '工业工程师', '画家'],
65 | }).data,
66 | photo: {
67 | text: 'photo1',
68 | value: photo,
69 | },
70 | });
71 |
--------------------------------------------------------------------------------
/src/pages/Interview/components/TagContent/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { spy } from 'sinon';
4 | import App, { ITagContentProps } from './index';
5 | import { smallLabel as labels } from '@/data/labels';
6 |
7 | const setup = () => {
8 | const dispatch = spy();
9 | const props: ITagContentProps = { labels };
10 | const wrapper = mount();
11 | return { props, wrapper, dispatch };
12 | };
13 |
14 | const { wrapper, dispatch, props } = setup();
15 | afterEach(() => {
16 | dispatch.resetHistory();
17 | });
18 |
19 | it('render', () => {
20 | expect(wrapper.find('.container').length).toEqual(1);
21 | });
22 |
23 | describe('response', () => {
24 | it('deleteTag should run when onConfirm', () => {
25 | //TODO: Propconfirm 不存在 simulate 事件
26 | // wrapper.find('Popconfirm').at(0);
27 | // .simulate('confirm'); // 问题: Propconfirm 不存在 simulate 事件
28 | // expect(dispatch.callCount).toEqual(1);
29 | expect(wrapper.find('Popconfirm').at(0).length).toEqual(1);
30 | });
31 |
32 | describe('点击标签目录筛选对应的记录', () => {
33 | it('filterRecord should run when click tag', () => {
34 | wrapper
35 | .find('#label')
36 | .at(0)
37 | .simulate('click');
38 | expect(dispatch.callCount).toEqual(1);
39 | });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/components/Charts/MiniBar/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Chart, Tooltip, Geom } from 'bizcharts';
3 | import autoHeight from '../autoHeight';
4 | import styles from '../index.less';
5 |
6 | @autoHeight()
7 | export default class MiniBar extends React.Component {
8 | render() {
9 | const { height, forceFit = true, color = '#1890FF', data = [] } = this.props;
10 |
11 | const scale = {
12 | x: {
13 | type: 'cat',
14 | },
15 | y: {
16 | min: 0,
17 | },
18 | };
19 |
20 | const padding = [36, 5, 30, 5];
21 |
22 | const tooltip = [
23 | 'x*y',
24 | (x, y) => ({
25 | name: x,
26 | value: y,
27 | }),
28 | ];
29 |
30 | // for tooltip not to be hide
31 | const chartHeight = height + 54;
32 |
33 | return (
34 |
35 |
36 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/utils/charts.ts:
--------------------------------------------------------------------------------
1 | import { getCountAndPercent } from './index';
2 | import { DataView } from '@antv/data-set';
3 | import { TQuesData, IKeyDimension } from '@/models/data';
4 |
5 | /**
6 | * 从选择的维度获得所需的图表数据
7 | * @param quesData:问卷数据
8 | * @param key:label Key
9 | * @param keyDimension:选择维度
10 | */
11 | export const getChartsDataSets = (
12 | quesData: TQuesData,
13 | key: string,
14 | keyDimension: IKeyDimension
15 | ) => {
16 | const answersOrders = quesData.map((quesRecord) => {
17 | if (quesRecord.records.length > 0) {
18 | const index = quesRecord.records.findIndex((i) => i.labelKey === key);
19 | return index < 0 ? [] : quesRecord.records[index].answer.order;
20 | } else return [];
21 | });
22 | const percent = getCountAndPercent(answersOrders);
23 | return percent.map((result, index) => {
24 | return {
25 | count: result.count,
26 | item: keyDimension.answers[index].text,
27 | };
28 | });
29 | };
30 |
31 | export const initDataSets = (data) => {
32 | const dv = new DataView();
33 | dv.source(data).transform({
34 | type: 'percent',
35 | field: 'count',
36 | dimension: 'item',
37 | as: 'percent',
38 | });
39 | const cols = {
40 | percent: {
41 | formatter: (val) => {
42 | val = Math.floor(val * 100) + '%';
43 | return val;
44 | },
45 | },
46 | };
47 | return { dv, cols };
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/Login/map.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Input, Icon } from 'antd';
3 | import styles from './index.less';
4 |
5 | const map = {
6 | UserName: {
7 | component: Input,
8 | props: {
9 | size: 'large',
10 | prefix: ,
11 | placeholder: 'admin',
12 | },
13 | rules: [{
14 | required: true, message: '请输入账户名!',
15 | }],
16 | },
17 | Password: {
18 | component: Input,
19 | props: {
20 | size: 'large',
21 | prefix: ,
22 | type: 'password',
23 | placeholder: '888888',
24 | },
25 | rules: [{
26 | required: true, message: '请输入密码!',
27 | }],
28 | },
29 | Mobile: {
30 | component: Input,
31 | props: {
32 | size: 'large',
33 | prefix: ,
34 | placeholder: '手机号',
35 | },
36 | rules: [{
37 | required: true, message: '请输入手机号!',
38 | }, {
39 | pattern: /^1\d{10}$/, message: '手机号格式错误!',
40 | }],
41 | },
42 | Captcha: {
43 | component: Input,
44 | props: {
45 | size: 'large',
46 | prefix: ,
47 | placeholder: '验证码',
48 | },
49 | rules: [{
50 | required: true, message: '请输入验证码!',
51 | }],
52 | },
53 | };
54 |
55 | export default map;
56 |
--------------------------------------------------------------------------------
/.umirc.js:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path';
2 |
3 | export default {
4 | plugins: [
5 | [
6 | 'umi-plugin-react',
7 | {
8 | dva: true,
9 | antd: true, // antd 默认不开启,如有使用需自行配置
10 | routes: {
11 | exclude: [
12 | /model\.[jt]sx?$/,
13 | /\.test\.[jt]sx?$/,
14 | /service\.[jt]sx?$/,
15 | /models\//,
16 | /components/,
17 | /services\//,
18 | ],
19 | },
20 | dynamicImport: {
21 | webpackChunkName: true,
22 | loadingComponent: './components/PageLoading',
23 | },
24 | dll: {
25 | include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch', 'antd/es'],
26 | },
27 | },
28 | ],
29 | ],
30 | theme: './src/styles/themes/theme.js',
31 | alias: {
32 | '@/components': resolve(__dirname, './src/components'),
33 | '@/utils': resolve(__dirname, './src/utils'),
34 | '@/services': resolve(__dirname, './src/services'),
35 | '@/common': resolve(__dirname, './src/common'),
36 | '@/assets': resolve(__dirname, './src/assets'),
37 | '@/typings': resolve(__dirname, './typings'),
38 | '@/mock': resolve(__dirname, './mock'),
39 | '@/data': resolve(__dirname, './data'),
40 | '@/src': resolve(__dirname, './src'),
41 | styles: resolve(__dirname, './src/styles'), // less 全局样式文件
42 | },
43 | proxy: {
44 | '/api/v1': 'http://127.0.0.1:7001/api/v1/',
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/mock/persona.js:
--------------------------------------------------------------------------------
1 | import shortid from 'shortid';
2 |
3 | const generateKey = () => {
4 | shortid.characters('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ%@');
5 | return shortid.generate();
6 | };
7 | const quesData = [
8 | {
9 | records: [
10 | {
11 | key: generateKey(),
12 | question: '你的名字是?',
13 | answer: { text: 'A.小A', order: 0 },
14 | },
15 | {
16 | key: generateKey(),
17 | question: '你的性别是?',
18 | answer: { text: 'B.男', order: 1 },
19 | },
20 | {
21 | key: generateKey(),
22 | question: '你住在?',
23 | answer: { text: 'A.A', order: 0 },
24 | },
25 | ],
26 | },
27 | {
28 | records: [
29 | {
30 | key: generateKey(),
31 | question: '你的名字是?',
32 | answer: { text: 'B.小B', order: 1 },
33 | },
34 | {
35 | key: generateKey(),
36 | question: '你的性别是?',
37 | answer: { text: 'A.女', order: 0 },
38 | },
39 | {
40 | key: generateKey(),
41 | question: '你住在?',
42 | answer: { text: 'B.B', order: 1 },
43 | },
44 | ],
45 | },
46 | ];
47 |
48 | import Mock from 'mockjs';
49 | const Random = Mock.Random;
50 |
51 | const userModels = quesData.map((item) => ({
52 | ...item,
53 | type: Random.natural(1, 5),
54 | typeName: Random.cword(),
55 | percent: Random.natural(1, 40),
56 | }));
57 |
58 | export default {
59 | 'get /api/v1/project/123/personas': userModels,
60 | };
61 |
--------------------------------------------------------------------------------
/src/components/SiderMenu/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { spy } from 'sinon';
4 | import App, { getMeunMatcheys } from './index';
5 | //
6 | //
7 | // const setup = () => {
8 | // const dispatch = spy();
9 | // const dispatch = spy();
10 | // const props = {
11 | // pathname: '/data/reduction',
12 | // tabStage: '1',
13 | // diagrams: [],
14 | // analysisStage: 5,
15 | // };
16 | // const wrapper = shallow();
17 | // return { props, wrapper, dispatch };
18 | // };
19 | //
20 | // const { wrapper, dispatch, props } = setup();
21 | // afterEach(() => {
22 | // dispatch.resetHistory();
23 | // });
24 |
25 | const meun = ['/dashboard', '/interview', '/data', '/userinfo/:id', '/userinfo/:key/info'];
26 |
27 | describe('test meun match', () => {
28 | it('simple path', () => {
29 | expect(getMeunMatcheys(meun, '/dashboard')).toEqual(['/dashboard']);
30 | });
31 | it('error path', () => {
32 | expect(getMeunMatcheys(meun, '/interview')).toEqual(['/interview']);
33 | });
34 |
35 | it('Secondary path', () => {
36 | expect(getMeunMatcheys(meun, '/dashboard/name')).toEqual([]);
37 | });
38 |
39 | it('Parameter path', () => {
40 | expect(getMeunMatcheys(meun, '/userinfo/2144')).toEqual(['/userinfo/:id']);
41 | });
42 |
43 | it('three parameter path', () => {
44 | expect(getMeunMatcheys(meun, '/userinfo/2144/info')).toEqual(['/userinfo/:key/info']);
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/src/pages/Data/components/DataPanel/LabelMatch.less:
--------------------------------------------------------------------------------
1 | @import '~styles/themes/default';
2 |
3 | .tag-container {
4 | padding-bottom: 24px;
5 | user-select: none;
6 | :global {
7 | .ant-tag {
8 | border-color: @transBlack-4;
9 | margin: 6px 8px;
10 | font-size: @font-size-base;
11 | height: 24px;
12 | padding: 1px 8px;
13 | background: @base-white;
14 | &:hover {
15 | color: @blue-6;
16 | }
17 | }
18 | }
19 | }
20 | .container {
21 | :global {
22 | .ant-list-footer {
23 | padding-right: 12px;
24 | border-top: 1px solid @white-3;
25 | }
26 | }
27 | }
28 | .list {
29 | :global {
30 | .ant-spin-nested-loading {
31 | height: 240px;
32 | overflow: auto;
33 | }
34 | .ant-list-item {
35 | cursor: pointer;
36 | padding-right: 12px;
37 | &:hover {
38 | background: @white-1;
39 | }
40 | }
41 | .ant-list-item-content {
42 | display: flex;
43 | align-items: center;
44 | justify-content: space-between;
45 | }
46 | }
47 | .list-active {
48 | background: @blue-1;
49 | }
50 | }
51 |
52 | .match-tag-base {
53 | padding: 4px 10px;
54 | height: 30px;
55 | font-size: 14px;
56 | cursor: pointer;
57 | margin-right: 0;
58 | }
59 |
60 | .match-tag-default {
61 | .match-tag-base;
62 | background: none;
63 | }
64 | .match-tag-active {
65 | .match-tag-base;
66 | border: none;
67 | color: @transWhite-1;
68 | background: @blue-6;
69 | }
70 |
--------------------------------------------------------------------------------
/src/pages/Interview/components/StarPic/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ReactEcharts from 'echarts-for-react';
3 |
4 | interface IStarPicProps {
5 | data: Array