Commit 9f684bbb authored by wenmo's avatar wenmo

系统配置界面

parent 28c5e425
......@@ -7,12 +7,7 @@ import com.dlink.service.SysConfigService;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
......@@ -82,6 +77,14 @@ public class SysConfigController {
return Result.succeed(sysConfig,"获取成功");
}
/**
* 获取所有配置
*/
@GetMapping("/getAll")
public Result getAll() {
return Result.succeed(sysConfigService.getAll(),"获取成功");
}
/**
* 批量更新配置
*/
......
......@@ -4,6 +4,8 @@ import com.dlink.db.service.ISuperService;
import com.dlink.model.SysConfig;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Map;
/**
* SysConfig
*
......@@ -12,6 +14,8 @@ import com.fasterxml.jackson.databind.JsonNode;
**/
public interface SysConfigService extends ISuperService<SysConfig> {
Map<String,String> getAll();
void initSysConfig();
void updateSysConfigByJson(JsonNode node);
......
......@@ -29,16 +29,19 @@ public class SysConfigServiceImpl extends SuperServiceImpl<SysConfigMapper, SysC
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public void initSysConfig() {
List<SysConfig> sysConfigs = list();
if(sysConfigs.size()==0){
return;
}
public Map<String, String> getAll() {
Map<String,String> map = new HashMap<>();
List<SysConfig> sysConfigs = list();
for(SysConfig item : sysConfigs){
map.put(item.getName(),item.getValue());
}
SystemConfiguration.getInstances().setConfiguration(mapper.valueToTree(map));
SystemConfiguration.getInstances().addConfiguration(map);
return map;
}
@Override
public void initSysConfig() {
SystemConfiguration.getInstances().setConfiguration(mapper.valueToTree(getAll()));
}
@Override
......
......@@ -2,6 +2,8 @@ package com.dlink.model;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Map;
/**
* SystemConfiguration
*
......@@ -49,6 +51,18 @@ public class SystemConfiguration {
}
}
public void addConfiguration(Map<String,String> map){
if(!map.containsKey("sqlSubmitJarPath")){
map.put("sqlSubmitJarPath",sqlSubmitJarPath.getValue().toString());
}
if(!map.containsKey("sqlSubmitJarParas")){
map.put("sqlSubmitJarParas",sqlSubmitJarParas.getValue().toString());
}
if(!map.containsKey("sqlSubmitJarMainAppClass")){
map.put("sqlSubmitJarMainAppClass",sqlSubmitJarMainAppClass.getValue().toString());
}
}
public String getSqlSubmitJarParas() {
return sqlSubmitJarParas.getValue().toString();
}
......
......@@ -31,38 +31,39 @@ export default [
path: '/taskcenter',
name: 'taskcenter',
icon: 'partition',
routes:[
routes: [
/*{
path: '/taskcenter/task',
name: 'task',
icon: 'task',
component: './Task',
},*/
path: '/taskcenter/task',
name: 'task',
icon: 'task',
component: './Task',
},*/
{
path: '/taskcenter/jar',
name: 'jar',
icon: 'code-sandbox',
component: './Jar',
}
]
},
],
},
{
path: '/clusters',
name: 'clusters',
icon: 'cluster',
routes:[
routes: [
{
path: '/clusters/cluster',
name: 'cluster',
icon: 'cluster',
component: './Cluster',
},{
},
{
path: '/clusters/clusterConfiguration',
name: 'clusterConfiguration',
icon: 'setting',
component: './ClusterConfiguration',
}
]
},
],
},
{
path: '/database',
......@@ -76,105 +77,16 @@ export default [
icon: 'container',
component: './Document',
},
/*{
path: '/dev',
name: 'dev',
icon: 'crown',
routes: [
{
path: '/dev/flink',
name: 'flink',
icon: 'github',
routes: [
{
path: 'https://ci.apache.org/projects/flink/flink-docs-release-1.13/',
name: 'docs',
},
],
},
{
path: '/dev/ant',
name: 'ant-design',
icon: 'antDesign',
routes: [
{
path: 'https://ant.design/components/icon-cn/',
name: 'docs',
},
{
path:
'https://preview.pro.ant.design/dashboard/analysis?primaryColor=%231890ff&fixSiderbar=true&colorWeak=false&pwa=false',
name: 'preview',
},
],
},
],
},*/
/*{
path: '/admin',
name: 'admin',
icon: 'crown',
access: 'canAdmin',
component: './Admin',
routes: [
{
path: '/admin/sub-page',
name: 'sub-page',
icon: 'smile',
component: './Welcome',
},
],
},*/
/*{
path: '/demo',
name: 'demo',
icon: 'crown',
//access: 'canAdmin',
routes: [
{
name: 'list',
icon: 'table',
path: '/demo/list',
routes: [
{
name: 'table-list',
icon: 'table',
path: '/demo/list/listtablelist',
component: './Demo/ListTableList',
},
{
name: 'basic-list',
icon: 'smile',
path: '/demo/list/listbasiclist',
component: './Demo/ListBasicList',
},
],
},
{
name: 'form',
icon: 'smile',
path: '/demo/form',
routes: [
{
name: 'advanced-form',
icon: 'smile',
path: '/demo/form/formadvancedform',
component: './Demo/FormAdvancedForm',
},
{
name: '分步表单',
icon: 'smile',
path: '/demo/form/formstepform',
component: './Demo/FormStepForm',
},
],
},
],
},*/
{
path: '/',
redirect: '/welcome',
},
{
name: 'settings',
icon: 'setting',
path: '/settings',
component: './Settings',
},
{
component: './404',
},
......
......@@ -15,6 +15,7 @@ import {
listSession, showCluster, showDataBase, getFillAllByVersion,
showClusterConfiguration
} from "@/components/Studio/StudioEvent/DDL";
import {loadSettings} from "@/pages/Settings/function";
type StudioProps = {
rightClickMenu:StateType['rightClickMenu'];
......@@ -25,6 +26,7 @@ const Studio: React.FC<StudioProps> = (props) => {
const {rightClickMenu,dispatch} = props;
const [form] = Form.useForm();
loadSettings(dispatch);
getFillAllByVersion('',dispatch);
showCluster(dispatch);
showClusterConfiguration(dispatch);
......
......@@ -60,7 +60,7 @@ export default {
'menu.taskcenter.task': '作业管理',
'menu.taskcenter.jar': 'Jar 管理',
'menu.document': '文档中心',
'menu.dev': 'Dev 开发者工具',
'menu.settings': '系统设置',
'menu.dev.flink': 'Flink 计算框架',
'menu.dev.flink.docs': '官方文档',
'menu.dev.ant-design': 'Ant Design UI框架',
......
// eslint-disable-next-line import/no-extraneous-dependencies
import type { Request, Response } from 'express';
export default {
'POST /api/forms': (_: Request, res: Response) => {
res.send({ message: 'Ok' });
},
};
import { PlusOutlined } from '@ant-design/icons';
import { Button, Divider, Input, Popconfirm, Table, message } from 'antd';
import type { FC } from 'react';
import React, { useState } from 'react';
import styles from '../style.less';
type TableFormDateType = {
key: string;
workId?: string;
name?: string;
department?: string;
isNew?: boolean;
editable?: boolean;
};
type TableFormProps = {
value?: TableFormDateType[];
onChange?: (value: TableFormDateType[]) => void;
};
const TableForm: FC<TableFormProps> = ({ value, onChange }) => {
const [clickedCancel, setClickedCancel] = useState(false);
const [loading, setLoading] = useState(false);
const [index, setIndex] = useState(0);
const [cacheOriginData, setCacheOriginData] = useState({});
const [data, setData] = useState(value);
const getRowByKey = (key: string, newData?: TableFormDateType[]) =>
(newData || data)?.filter((item) => item.key === key)[0];
const toggleEditable = (e: React.MouseEvent | React.KeyboardEvent, key: string) => {
e.preventDefault();
const newData = data?.map((item) => ({ ...item }));
const target = getRowByKey(key, newData);
if (target) {
// 进入编辑状态时保存原始数据
if (!target.editable) {
cacheOriginData[key] = { ...target };
setCacheOriginData(cacheOriginData);
}
target.editable = !target.editable;
setData(newData);
}
};
const newMember = () => {
const newData = data?.map((item) => ({ ...item })) || [];
newData.push({
key: `NEW_TEMP_ID_${index}`,
workId: '',
name: '',
department: '',
editable: true,
isNew: true,
});
setIndex(index + 1);
setData(newData);
};
const remove = (key: string) => {
const newData = data?.filter((item) => item.key !== key) as TableFormDateType[];
setData(newData);
if (onChange) {
onChange(newData);
}
};
const handleFieldChange = (
e: React.ChangeEvent<HTMLInputElement>,
fieldName: string,
key: string,
) => {
const newData = [...(data as TableFormDateType[])];
const target = getRowByKey(key, newData);
if (target) {
target[fieldName] = e.target.value;
setData(newData);
}
};
const saveRow = (e: React.MouseEvent | React.KeyboardEvent, key: string) => {
e.persist();
setLoading(true);
setTimeout(() => {
if (clickedCancel) {
setClickedCancel(false);
return;
}
const target = getRowByKey(key) || ({} as any);
if (!target.workId || !target.name || !target.department) {
message.error('请填写完整成员信息。');
(e.target as HTMLInputElement).focus();
setLoading(false);
return;
}
delete target.isNew;
toggleEditable(e, key);
if (onChange) {
onChange(data as TableFormDateType[]);
}
setLoading(false);
}, 500);
};
const handleKeyPress = (e: React.KeyboardEvent, key: string) => {
if (e.key === 'Enter') {
saveRow(e, key);
}
};
const cancel = (e: React.MouseEvent, key: string) => {
setClickedCancel(true);
e.preventDefault();
const newData = [...(data as TableFormDateType[])];
// 编辑前的原始数据
let cacheData = [];
cacheData = newData.map((item) => {
if (item.key === key) {
if (cacheOriginData[key]) {
const originItem = {
...item,
...cacheOriginData[key],
editable: false,
};
delete cacheOriginData[key];
setCacheOriginData(cacheOriginData);
return originItem;
}
}
return item;
});
setData(cacheData);
setClickedCancel(false);
};
const columns = [
{
title: '成员姓名',
dataIndex: 'name',
key: 'name',
width: '20%',
render: (text: string, record: TableFormDateType) => {
if (record.editable) {
return (
<Input
value={text}
autoFocus
onChange={(e) => handleFieldChange(e, 'name', record.key)}
onKeyPress={(e) => handleKeyPress(e, record.key)}
placeholder="成员姓名"
/>
);
}
return text;
},
},
{
title: '工号',
dataIndex: 'workId',
key: 'workId',
width: '20%',
render: (text: string, record: TableFormDateType) => {
if (record.editable) {
return (
<Input
value={text}
onChange={(e) => handleFieldChange(e, 'workId', record.key)}
onKeyPress={(e) => handleKeyPress(e, record.key)}
placeholder="工号"
/>
);
}
return text;
},
},
{
title: '所属部门',
dataIndex: 'department',
key: 'department',
width: '40%',
render: (text: string, record: TableFormDateType) => {
if (record.editable) {
return (
<Input
value={text}
onChange={(e) => handleFieldChange(e, 'department', record.key)}
onKeyPress={(e) => handleKeyPress(e, record.key)}
placeholder="所属部门"
/>
);
}
return text;
},
},
{
title: '操作',
key: 'action',
render: (text: string, record: TableFormDateType) => {
if (!!record.editable && loading) {
return null;
}
if (record.editable) {
if (record.isNew) {
return (
<span>
<a onClick={(e) => saveRow(e, record.key)}>添加</a>
<Divider type="vertical" />
<Popconfirm title="是否要删除此行?" onConfirm={() => remove(record.key)}>
<a>删除</a>
</Popconfirm>
</span>
);
}
return (
<span>
<a onClick={(e) => saveRow(e, record.key)}>保存</a>
<Divider type="vertical" />
<a onClick={(e) => cancel(e, record.key)}>取消</a>
</span>
);
}
return (
<span>
<a onClick={(e) => toggleEditable(e, record.key)}>编辑</a>
<Divider type="vertical" />
<Popconfirm title="是否要删除此行?" onConfirm={() => remove(record.key)}>
<a>删除</a>
</Popconfirm>
</span>
);
},
},
];
return (
<>
<Table<TableFormDateType>
loading={loading}
columns={columns}
dataSource={data}
pagination={false}
rowClassName={(record) => (record.editable ? styles.editable : '')}
/>
<Button
style={{ width: '100%', marginTop: 16, marginBottom: 8 }}
type="dashed"
onClick={newMember}
>
<PlusOutlined />
新增成员
</Button>
</>
);
};
export default TableForm;
import { CloseCircleOutlined } from '@ant-design/icons';
import { Button, Card, Col, DatePicker, Form, Input, Popover, Row, Select, TimePicker } from 'antd';
import type { FC } from 'react';
import React, { useState } from 'react';
import { PageContainer, FooterToolbar } from '@ant-design/pro-layout';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import TableForm from './components/TableForm';
import styles from './style.less';
type InternalNamePath = (string | number)[];
const { Option } = Select;
const { RangePicker } = DatePicker;
const fieldLabels = {
name: '仓库名',
url: '仓库域名',
owner: '仓库管理员',
approver: '审批人',
dateRange: '生效日期',
type: '仓库类型',
name2: '任务名',
url2: '任务描述',
owner2: '执行人',
approver2: '责任人',
dateRange2: '生效日期',
type2: '任务类型',
};
const tableData = [
{
key: '1',
workId: '00001',
name: 'John Brown',
department: 'New York No. 1 Lake Park',
},
{
key: '2',
workId: '00002',
name: 'Jim Green',
department: 'London No. 1 Lake Park',
},
{
key: '3',
workId: '00003',
name: 'Joe Black',
department: 'Sidney No. 1 Lake Park',
},
];
type FormAdvancedFormProps = {
dispatch: Dispatch;
submitting: boolean;
};
type ErrorField = {
name: InternalNamePath;
errors: string[];
};
const FormAdvancedForm: FC<FormAdvancedFormProps> = ({
submitting,
dispatch,
}) => {
const [form] = Form.useForm();
const [error, setError] = useState<ErrorField[]>([]);
const getErrorInfo = (errors: ErrorField[]) => {
const errorCount = errors.filter((item) => item.errors.length > 0).length;
if (!errors || errorCount === 0) {
return null;
}
const scrollToField = (fieldKey: string) => {
const labelNode = document.querySelector(`label[for="${fieldKey}"]`);
if (labelNode) {
labelNode.scrollIntoView(true);
}
};
const errorList = errors.map((err) => {
if (!err || err.errors.length === 0) {
return null;
}
const key = err.name[0] as string;
return (
<li key={key} className={styles.errorListItem} onClick={() => scrollToField(key)}>
<CloseCircleOutlined className={styles.errorIcon} />
<div className={styles.errorMessage}>{err.errors[0]}</div>
<div className={styles.errorField}>{fieldLabels[key]}</div>
</li>
);
});
return (
<span className={styles.errorIcon}>
<Popover
title="表单校验信息"
content={errorList}
overlayClassName={styles.errorPopover}
trigger="click"
getPopupContainer={(trigger: HTMLElement) => {
if (trigger && trigger.parentNode) {
return trigger.parentNode as HTMLElement;
}
return trigger;
}}
>
<CloseCircleOutlined />
</Popover>
{errorCount}
</span>
);
};
const onFinish = (values: Record<string, any>) => {
setError([]);
dispatch({
type: 'formAdvancedForm/submitAdvancedForm',
payload: values,
});
};
const onFinishFailed = (errorInfo: any) => {
setError(errorInfo.errorFields);
};
return (
<Form
form={form}
layout="vertical"
hideRequiredMark
initialValues={{ members: tableData }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<PageContainer content="高级表单常见于一次性输入和提交大批量数据的场景。">
<Card title="仓库管理" className={styles.card} bordered={false}>
<Row gutter={16}>
<Col lg={6} md={12} sm={24}>
<Form.Item
label={fieldLabels.name}
name="name"
rules={[{ required: true, message: '请输入仓库名称' }]}
>
<Input placeholder="请输入仓库名称" />
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
<Form.Item
label={fieldLabels.url}
name="url"
rules={[{ required: true, message: '请选择' }]}
>
<Input
style={{ width: '100%' }}
addonBefore="http://"
addonAfter=".com"
placeholder="请输入"
/>
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
<Form.Item
label={fieldLabels.owner}
name="owner"
rules={[{ required: true, message: '请选择管理员' }]}
>
<Select placeholder="请选择管理员">
<Option value="xiao">付晓晓</Option>
<Option value="mao">周毛毛</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col lg={6} md={12} sm={24}>
<Form.Item
label={fieldLabels.approver}
name="approver"
rules={[{ required: true, message: '请选择审批员' }]}
>
<Select placeholder="请选择审批员">
<Option value="xiao">付晓晓</Option>
<Option value="mao">周毛毛</Option>
</Select>
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
<Form.Item
label={fieldLabels.dateRange}
name="dateRange"
rules={[{ required: true, message: '请选择生效日期' }]}
>
<RangePicker placeholder={['开始日期', '结束日期']} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
<Form.Item
label={fieldLabels.type}
name="type"
rules={[{ required: true, message: '请选择仓库类型' }]}
>
<Select placeholder="请选择仓库类型">
<Option value="private">私密</Option>
<Option value="public">公开</Option>
</Select>
</Form.Item>
</Col>
</Row>
</Card>
<Card title="任务管理" className={styles.card} bordered={false}>
<Row gutter={16}>
<Col lg={6} md={12} sm={24}>
<Form.Item
label={fieldLabels.name2}
name="name2"
rules={[{ required: true, message: '请输入' }]}
>
<Input placeholder="请输入" />
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
<Form.Item
label={fieldLabels.url2}
name="url2"
rules={[{ required: true, message: '请选择' }]}
>
<Input placeholder="请输入" />
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
<Form.Item
label={fieldLabels.owner2}
name="owner2"
rules={[{ required: true, message: '请选择管理员' }]}
>
<Select placeholder="请选择管理员">
<Option value="xiao">付晓晓</Option>
<Option value="mao">周毛毛</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col lg={6} md={12} sm={24}>
<Form.Item
label={fieldLabels.approver2}
name="approver2"
rules={[{ required: true, message: '请选择审批员' }]}
>
<Select placeholder="请选择审批员">
<Option value="xiao">付晓晓</Option>
<Option value="mao">周毛毛</Option>
</Select>
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
<Form.Item
label={fieldLabels.dateRange2}
name="dateRange2"
rules={[{ required: true, message: '请输入' }]}
>
<TimePicker
placeholder="提醒时间"
style={{ width: '100%' }}
getPopupContainer={(trigger) => {
if (trigger && trigger.parentNode) {
return trigger.parentNode as HTMLElement;
}
return trigger;
}}
/>
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
<Form.Item
label={fieldLabels.type2}
name="type2"
rules={[{ required: true, message: '请选择仓库类型' }]}
>
<Select placeholder="请选择仓库类型">
<Option value="private">私密</Option>
<Option value="public">公开</Option>
</Select>
</Form.Item>
</Col>
</Row>
</Card>
<Card title="成员管理" bordered={false}>
<Form.Item name="members">
<TableForm />
</Form.Item>
</Card>
</PageContainer>
<FooterToolbar>
{getErrorInfo(error)}
<Button type="primary" onClick={() => form?.submit()} loading={submitting}>
提交
</Button>
</FooterToolbar>
</Form>
);
};
export default connect(({ loading }: { loading: { effects: Record<string, boolean> } }) => ({
submitting: loading.effects['formAdvancedForm/submitAdvancedForm'],
}))(FormAdvancedForm);
import type { Effect } from 'umi';
import { message } from 'antd';
import { fakeSubmitForm } from './service';
export type ModelType = {
namespace: string;
state: {};
effects: {
submitAdvancedForm: Effect;
};
};
const Model: ModelType = {
namespace: 'formAdvancedForm',
state: {},
effects: {
*submitAdvancedForm({ payload }, { call }) {
yield call(fakeSubmitForm, payload);
message.success('提交成功');
},
},
};
export default Model;
import request from 'umi-request';
export async function fakeSubmitForm(params: any) {
return request('/api/forms', {
method: 'POST',
data: params,
});
}
@import '~antd/es/style/themes/default.less';
.card {
margin-bottom: 24px;
:global {
.ant-legacy-form-item .ant-legacy-form-item-control-wrapper {
width: 100%;
}
}
}
.errorIcon {
margin-right: 24px;
color: @error-color;
cursor: pointer;
span.anticon {
margin-right: 4px;
}
}
.errorPopover {
:global {
.ant-popover-inner-content {
min-width: 256px;
max-height: 290px;
padding: 0;
overflow: auto;
}
}
}
.errorListItem {
padding: 8px 16px;
list-style: none;
border-bottom: 1px solid @border-color-split;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: @item-active-bg;
}
&:last-child {
border: 0;
}
.errorIcon {
float: left;
margin-top: 4px;
margin-right: 12px;
padding-bottom: 22px;
color: @error-color;
}
.errorField {
margin-top: 2px;
color: @text-color-secondary;
font-size: 12px;
}
}
.editable {
td {
padding-top: 13px !important;
padding-bottom: 12.5px !important;
}
}
// eslint-disable-next-line import/no-extraneous-dependencies
import type { Request, Response } from 'express';
export default {
'POST /api/forms': (_: Request, res: Response) => {
res.send({ message: 'Ok' });
},
};
@import '~antd/es/style/themes/default.less';
.stepForm {
max-width: 500px;
margin: 40px auto 0;
}
.stepFormText {
margin-bottom: 24px;
:global {
.ant-form-item-label,
.ant-form-item-control {
line-height: 22px;
}
}
}
.result {
max-width: 560px;
margin: 0 auto;
padding: 24px 0 8px;
}
.desc {
padding: 0 56px;
color: @text-color-secondary;
h3 {
margin: 0 0 12px 0;
color: @text-color-secondary;
font-size: 16px;
line-height: 32px;
}
h4 {
margin: 0 0 4px 0;
color: @text-color-secondary;
font-size: 14px;
line-height: 22px;
}
p {
margin-top: 0;
margin-bottom: 12px;
line-height: 22px;
}
}
@media screen and (max-width: @screen-md) {
.desc {
padding: 0;
}
}
.information {
line-height: 22px;
:global {
.ant-row:not(:last-child) {
margin-bottom: 24px;
}
}
.label {
padding-right: 8px;
color: @heading-color;
text-align: right;
@media screen and (max-width: @screen-sm) {
text-align: left;
}
}
}
.money {
font-weight: 500;
font-size: 20px;
font-family: 'Helvetica Neue', sans-serif;
line-height: 14px;
}
.uppercase {
font-size: 12px;
}
import React from 'react';
import { Form, Button, Divider, Input, Select } from 'antd';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../../model';
import styles from './index.less';
const { Option } = Select;
const formItemLayout = {
labelCol: {
span: 5,
},
wrapperCol: {
span: 19,
},
};
type Step1Props = {
data?: StateType['step'];
dispatch?: Dispatch;
};
const Step1: React.FC<Step1Props> = (props) => {
const { dispatch, data } = props;
const [form] = Form.useForm();
if (!data) {
return null;
}
const { validateFields } = form;
const onValidateForm = async () => {
const values = await validateFields();
if (dispatch) {
dispatch({
type: 'demoAndFormStepForm/saveStepFormData',
payload: values,
});
dispatch({
type: 'demoAndFormStepForm/saveCurrentStep',
payload: 'confirm',
});
}
};
return (
<>
<Form
{...formItemLayout}
form={form}
layout="horizontal"
className={styles.stepForm}
hideRequiredMark
initialValues={data}
>
<Form.Item
label="付款账户"
name="payAccount"
rules={[{ required: true, message: '请选择付款账户' }]}
>
<Select placeholder="test@example.com">
<Option value="ant-design@alipay.com">ant-design@alipay.com</Option>
</Select>
</Form.Item>
<Form.Item label="收款账户">
<Input.Group compact>
<Select defaultValue="alipay" style={{ width: 100 }}>
<Option value="alipay">支付宝</Option>
<Option value="bank">银行账户</Option>
</Select>
<Form.Item
noStyle
name="receiverAccount"
rules={[
{ required: true, message: '请输入收款人账户' },
{ type: 'email', message: '账户名应为邮箱格式' },
]}
>
<Input style={{ width: 'calc(100% - 100px)' }} placeholder="test@example.com" />
</Form.Item>
</Input.Group>
</Form.Item>
<Form.Item
label="收款人姓名"
name="receiverName"
rules={[{ required: true, message: '请输入收款人姓名' }]}
>
<Input placeholder="请输入收款人姓名" />
</Form.Item>
<Form.Item
label="转账金额"
name="amount"
rules={[
{ required: true, message: '请输入转账金额' },
{
pattern: /^(\d+)((?:\.\d+)?)$/,
message: '请输入合法金额数字',
},
]}
>
<Input prefix="¥" placeholder="请输入金额" />
</Form.Item>
<Form.Item
wrapperCol={{
xs: { span: 24, offset: 0 },
sm: {
span: formItemLayout.wrapperCol.span,
offset: formItemLayout.labelCol.span,
},
}}
>
<Button type="primary" onClick={onValidateForm}>
下一步
</Button>
</Form.Item>
</Form>
<Divider style={{ margin: '40px 0 24px' }} />
<div className={styles.desc}>
<h3>说明</h3>
<h4>转账到支付宝账户</h4>
<p>
如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。
</p>
<h4>转账到银行卡</h4>
<p>
如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。
</p>
</div>
</>
);
};
export default connect(({ demoAndFormStepForm }: { demoAndFormStepForm: StateType }) => ({
data: demoAndFormStepForm.step,
}))(Step1);
@import '~antd/es/style/themes/default.less';
.stepForm {
max-width: 500px;
margin: 40px auto 0;
}
.stepFormText {
margin-bottom: 24px;
:global {
.ant-form-item-label,
.ant-form-item-control {
line-height: 22px;
}
}
}
.result {
max-width: 560px;
margin: 0 auto;
padding: 24px 0 8px;
}
.desc {
padding: 0 56px;
color: @text-color-secondary;
h3 {
margin: 0 0 12px 0;
color: @text-color-secondary;
font-size: 16px;
line-height: 32px;
}
h4 {
margin: 0 0 4px 0;
color: @text-color-secondary;
font-size: 14px;
line-height: 22px;
}
p {
margin-top: 0;
margin-bottom: 12px;
line-height: 22px;
}
}
@media screen and (max-width: @screen-md) {
.desc {
padding: 0;
}
}
.information {
line-height: 22px;
:global {
.ant-row:not(:last-child) {
margin-bottom: 24px;
}
}
.label {
padding-right: 8px;
color: @heading-color;
text-align: right;
@media screen and (max-width: @screen-sm) {
text-align: left;
}
}
}
.money {
font-weight: 500;
font-size: 20px;
font-family: 'Helvetica Neue', sans-serif;
line-height: 14px;
}
.uppercase {
font-size: 12px;
}
import React from 'react';
import { Form, Alert, Button, Descriptions, Divider, Statistic, Input } from 'antd';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../../model';
import styles from './index.less';
const formItemLayout = {
labelCol: {
span: 5,
},
wrapperCol: {
span: 19,
},
};
type Step2Props = {
data?: StateType['step'];
dispatch?: Dispatch;
submitting?: boolean;
};
const Step2: React.FC<Step2Props> = (props) => {
const [form] = Form.useForm();
const { data, dispatch, submitting } = props;
if (!data) {
return null;
}
const { validateFields, getFieldsValue } = form;
const onPrev = () => {
if (dispatch) {
const values = getFieldsValue();
dispatch({
type: 'demoAndFormStepForm/saveStepFormData',
payload: {
...data,
...values,
},
});
dispatch({
type: 'demoAndFormStepForm/saveCurrentStep',
payload: 'info',
});
}
};
const onValidateForm = async () => {
const values = await validateFields();
if (dispatch) {
dispatch({
type: 'demoAndFormStepForm/submitStepForm',
payload: {
...data,
...values,
},
});
}
};
const { payAccount, receiverAccount, receiverName, amount } = data;
return (
<Form
{...formItemLayout}
form={form}
layout="horizontal"
className={styles.stepForm}
initialValues={{ password: '123456' }}
>
<Alert
closable
showIcon
message="确认转账后,资金将直接打入对方账户,无法退回。"
style={{ marginBottom: 24 }}
/>
<Descriptions column={1}>
<Descriptions.Item label="付款账户"> {payAccount}</Descriptions.Item>
<Descriptions.Item label="收款账户"> {receiverAccount}</Descriptions.Item>
<Descriptions.Item label="收款人姓名"> {receiverName}</Descriptions.Item>
<Descriptions.Item label="转账金额">
<Statistic value={amount} suffix="元" />
</Descriptions.Item>
</Descriptions>
<Divider style={{ margin: '24px 0' }} />
<Form.Item
label="支付密码"
name="password"
required={false}
rules={[{ required: true, message: '需要支付密码才能进行支付' }]}
>
<Input type="password" autoComplete="off" style={{ width: '80%' }} />
</Form.Item>
<Form.Item
style={{ marginBottom: 8 }}
wrapperCol={{
xs: { span: 24, offset: 0 },
sm: {
span: formItemLayout.wrapperCol.span,
offset: formItemLayout.labelCol.span,
},
}}
>
<Button type="primary" onClick={onValidateForm} loading={submitting}>
提交
</Button>
<Button onClick={onPrev} style={{ marginLeft: 8 }}>
上一步
</Button>
</Form.Item>
</Form>
);
};
export default connect(
({
demoAndFormStepForm,
loading,
}: {
demoAndFormStepForm: StateType;
loading: {
effects: Record<string, boolean>;
};
}) => ({
submitting: loading.effects['demoAndFormStepForm/submitStepForm'],
data: demoAndFormStepForm.step,
}),
)(Step2);
@import '~antd/es/style/themes/default.less';
.stepForm {
max-width: 500px;
margin: 40px auto 0;
}
.stepFormText {
margin-bottom: 24px;
:global {
.ant-form-item-label,
.ant-form-item-control {
line-height: 22px;
}
}
}
.result {
max-width: 560px;
margin: 0 auto;
padding: 24px 0 8px;
}
.desc {
padding: 0 56px;
color: @text-color-secondary;
h3 {
margin: 0 0 12px 0;
color: @text-color-secondary;
font-size: 16px;
line-height: 32px;
}
h4 {
margin: 0 0 4px 0;
color: @text-color-secondary;
font-size: 14px;
line-height: 22px;
}
p {
margin-top: 0;
margin-bottom: 12px;
line-height: 22px;
}
}
@media screen and (max-width: @screen-md) {
.desc {
padding: 0;
}
}
.information {
line-height: 22px;
:global {
.ant-row:not(:last-child) {
margin-bottom: 24px;
}
}
.label {
padding-right: 8px;
color: @heading-color;
text-align: right;
@media screen and (max-width: @screen-sm) {
text-align: left;
}
}
}
.uppercase {
font-size: 12px;
}
import { Button, Result, Descriptions, Statistic } from 'antd';
import React from 'react';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../../model';
import styles from './index.less';
type Step3Props = {
data?: StateType['step'];
dispatch?: Dispatch;
};
const Step3: React.FC<Step3Props> = (props) => {
const { data, dispatch } = props;
if (!data) {
return null;
}
const { payAccount, receiverAccount, receiverName, amount } = data;
const onFinish = () => {
if (dispatch) {
dispatch({
type: 'demoAndFormStepForm/saveCurrentStep',
payload: 'info',
});
}
};
const information = (
<div className={styles.information}>
<Descriptions column={1}>
<Descriptions.Item label="付款账户"> {payAccount}</Descriptions.Item>
<Descriptions.Item label="收款账户"> {receiverAccount}</Descriptions.Item>
<Descriptions.Item label="收款人姓名"> {receiverName}</Descriptions.Item>
<Descriptions.Item label="转账金额">
<Statistic value={amount} suffix="元" />
</Descriptions.Item>
</Descriptions>
</div>
);
const extra = (
<>
<Button type="primary" onClick={onFinish}>
再转一笔
</Button>
<Button>查看账单</Button>
</>
);
return (
<Result
status="success"
title="操作成功"
subTitle="预计两小时内到账"
extra={extra}
className={styles.result}
>
{information}
</Result>
);
};
export default connect(({ demoAndFormStepForm }: { demoAndFormStepForm: StateType }) => ({
data: demoAndFormStepForm.step,
}))(Step3);
import React, { useState, useEffect } from 'react';
import { Card, Steps } from 'antd';
import { PageContainer } from '@ant-design/pro-layout';
import { connect } from 'umi';
import type { StateType } from './model';
import Step1 from './components/Step1';
import Step2 from './components/Step2';
import Step3 from './components/Step3';
import styles from './style.less';
const { Step } = Steps;
type FormStepFormProps = {
current: StateType['current'];
};
const getCurrentStepAndComponent = (current?: string) => {
switch (current) {
case 'confirm':
return { step: 1, component: <Step2 /> };
case 'result':
return { step: 2, component: <Step3 /> };
case 'info':
default:
return { step: 0, component: <Step1 /> };
}
};
const FormStepForm: React.FC<FormStepFormProps> = ({ current }) => {
const [stepComponent, setStepComponent] = useState<React.ReactNode>(<Step1 />);
const [currentStep, setCurrentStep] = useState<number>(0);
useEffect(() => {
const { step, component } = getCurrentStepAndComponent(current);
setCurrentStep(step);
setStepComponent(component);
}, [current]);
return (
<PageContainer content="将一个冗长或用户不熟悉的表单任务分成多个步骤,指导用户完成。">
<Card bordered={false}>
<>
<Steps current={currentStep} className={styles.steps}>
<Step title="填写转账信息" />
<Step title="确认转账信息" />
<Step title="完成" />
</Steps>
{stepComponent}
</>
</Card>
</PageContainer>
);
};
export default connect(({ demoAndFormStepForm }: { demoAndFormStepForm: StateType }) => ({
current: demoAndFormStepForm.current,
}))(FormStepForm);
export default {
'demoandformstepform.basic.title': 'Basic form',
'demoandformstepform.basic.description':
'Form pages are used to collect or verify information to users, and basic forms are common in scenarios where there are fewer data items.',
'demoandformstepform.email.required': 'Please enter your email!',
'demoandformstepform.email.wrong-format': 'The email address is in the wrong format!',
'demoandformstepform.userName.required': 'Please enter your userName!',
'demoandformstepform.password.required': 'Please enter your password!',
'demoandformstepform.password.twice': 'The passwords entered twice do not match!',
'demoandformstepform.strength.msg':
"Please enter at least 6 characters and don't use passwords that are easy to guess.",
'demoandformstepform.strength.strong': 'Strength: strong',
'demoandformstepform.strength.medium': 'Strength: medium',
'demoandformstepform.strength.short': 'Strength: too short',
'demoandformstepform.confirm-password.required': 'Please confirm your password!',
'demoandformstepform.phone-number.required': 'Please enter your phone number!',
'demoandformstepform.phone-number.wrong-format': 'Malformed phone number!',
'demoandformstepform.verification-code.required': 'Please enter the verification code!',
'demoandformstepform.title.required': 'Please enter a title',
'demoandformstepform.date.required': 'Please select the start and end date',
'demoandformstepform.goal.required': 'Please enter a description of the goal',
'demoandformstepform.standard.required': 'Please enter a metric',
'demoandformstepform.form.get-captcha': 'Get Captcha',
'demoandformstepform.captcha.second': 'sec',
'demoandformstepform.form.optional': ' (optional) ',
'demoandformstepform.form.submit': 'Submit',
'demoandformstepform.form.save': 'Save',
'demoandformstepform.email.placeholder': 'Email',
'demoandformstepform.password.placeholder': 'Password',
'demoandformstepform.confirm-password.placeholder': 'Confirm password',
'demoandformstepform.phone-number.placeholder': 'Phone number',
'demoandformstepform.verification-code.placeholder': 'Verification code',
'demoandformstepform.title.label': 'Title',
'demoandformstepform.title.placeholder': 'Give the target a name',
'demoandformstepform.date.label': 'Start and end date',
'demoandformstepform.placeholder.start': 'Start date',
'demoandformstepform.placeholder.end': 'End date',
'demoandformstepform.goal.label': 'Goal description',
'demoandformstepform.goal.placeholder': 'Please enter your work goals',
'demoandformstepform.standard.label': 'Metrics',
'demoandformstepform.standard.placeholder': 'Please enter a metric',
'demoandformstepform.client.label': 'Client',
'demoandformstepform.label.tooltip': 'Target service object',
'demoandformstepform.client.placeholder':
'Please describe your customer service, internal customers directly @ Name / job number',
'demoandformstepform.invites.label': 'Inviting critics',
'demoandformstepform.invites.placeholder':
'Please direct @ Name / job number, you can invite up to 5 people',
'demoandformstepform.weight.label': 'Weight',
'demoandformstepform.weight.placeholder': 'Please enter weight',
'demoandformstepform.public.label': 'Target disclosure',
'demoandformstepform.label.help': 'Customers and invitees are shared by default',
'demoandformstepform.radio.public': 'Public',
'demoandformstepform.radio.partially-public': 'Partially public',
'demoandformstepform.radio.private': 'Private',
'demoandformstepform.publicUsers.placeholder': 'Open to',
'demoandformstepform.option.A': 'Colleague A',
'demoandformstepform.option.B': 'Colleague B',
'demoandformstepform.option.C': 'Colleague C',
};
export default {
'demoandformstepform.basic.title': 'Basic form',
'demoandformstepform.basic.description':
'Form pages are used to collect or verify information to users, and basic forms are common in scenarios where there are fewer data items.',
'demoandformstepform.email.required': 'Por favor insira seu email!',
'demoandformstepform.email.wrong-format': 'O email está errado!',
'demoandformstepform.userName.required': 'Por favor insira nome de usuário!',
'demoandformstepform.password.required': 'Por favor insira sua senha!',
'demoandformstepform.password.twice': 'As senhas não estão iguais!',
'demoandformstepform.strength.msg':
'Por favor insira pelo menos 6 caracteres e não use senhas fáceis de adivinhar.',
'demoandformstepform.strength.strong': 'Força: forte',
'demoandformstepform.strength.medium': 'Força: média',
'demoandformstepform.strength.short': 'Força: curta',
'demoandformstepform.confirm-password.required': 'Por favor confirme sua senha!',
'demoandformstepform.phone-number.required': 'Por favor insira seu telefone!',
'demoandformstepform.phone-number.wrong-format': 'Formato de telefone errado!',
'demoandformstepform.verification-code.required': 'Por favor insira seu código de verificação!',
'demoandformstepform.form.get-captcha': 'Get Captcha',
'demoandformstepform.captcha.second': 'sec',
'demoandformstepform.email.placeholder': 'Email',
'demoandformstepform.password.placeholder': 'Senha',
'demoandformstepform.confirm-password.placeholder': 'Confirme a senha',
'demoandformstepform.phone-number.placeholder': 'Telefone',
'demoandformstepform.verification-code.placeholder': 'Código de verificação',
'demoandformstepform.form.optional': ' (optional) ',
'demoandformstepform.form.submit': 'Submit',
'demoandformstepform.form.save': 'Save',
'demoandformstepform.title.label': 'Title',
'demoandformstepform.title.placeholder': 'Give the target a name',
'demoandformstepform.date.label': 'Start and end date',
'demoandformstepform.placeholder.start': 'Start date',
'demoandformstepform.placeholder.end': 'End date',
'demoandformstepform.goal.label': 'Goal description',
'demoandformstepform.goal.placeholder': 'Please enter your work goals',
'demoandformstepform.standard.label': 'Metrics',
'demoandformstepform.standard.placeholder': 'Please enter a metric',
'demoandformstepform.client.label': 'Client',
'demoandformstepform.label.tooltip': 'Target service object',
'demoandformstepform.client.placeholder':
'Please describe your customer service, internal customers directly @ Name / job number',
'demoandformstepform.invites.label': 'Inviting critics',
'demoandformstepform.invites.placeholder':
'Please direct @ Name / job number, you can invite up to 5 people',
'demoandformstepform.weight.label': 'Weight',
'demoandformstepform.weight.placeholder': 'Please enter weight',
'demoandformstepform.public.label': 'Target disclosure',
'demoandformstepform.label.help': 'Customers and invitees are shared by default',
'demoandformstepform.radio.public': 'Public',
'demoandformstepform.radio.partially-public': 'Partially public',
'demoandformstepform.radio.private': 'Private',
'demoandformstepform.publicUsers.placeholder': 'Open to',
'demoandformstepform.option.A': 'Colleague A',
'demoandformstepform.option.B': 'Colleague B',
'demoandformstepform.option.C': 'Colleague C',
};
export default {
'demoandformstepform.basic.title': '基础表单',
'demoandformstepform.basic.description':
'表单页用于向用户收集或验证信息,基础表单常见于数据项较少的表单场景。',
'demoandformstepform.email.required': '请输入邮箱地址!',
'demoandformstepform.email.wrong-format': '邮箱地址格式错误!',
'demoandformstepform.userName.required': '请输入用户名!',
'demoandformstepform.password.required': '请输入密码!',
'demoandformstepform.password.twice': '两次输入的密码不匹配!',
'demoandformstepform.strength.msg': '请至少输入 6 个字符。请不要使用容易被猜到的密码。',
'demoandformstepform.strength.strong': '强度:强',
'demoandformstepform.strength.medium': '强度:中',
'demoandformstepform.strength.short': '强度:太短',
'demoandformstepform.confirm-password.required': '请确认密码!',
'demoandformstepform.phone-number.required': '请输入手机号!',
'demoandformstepform.phone-number.wrong-format': '手机号格式错误!',
'demoandformstepform.verification-code.required': '请输入验证码!',
'demoandformstepform.title.required': '请输入标题',
'demoandformstepform.date.required': '请选择起止日期',
'demoandformstepform.goal.required': '请输入目标描述',
'demoandformstepform.standard.required': '请输入衡量标准',
'demoandformstepform.form.get-captcha': '获取验证码',
'demoandformstepform.captcha.second': '秒',
'demoandformstepform.form.optional': '(选填)',
'demoandformstepform.form.submit': '提交',
'demoandformstepform.form.save': '保存',
'demoandformstepform.email.placeholder': '邮箱',
'demoandformstepform.password.placeholder': '至少6位密码,区分大小写',
'demoandformstepform.confirm-password.placeholder': '确认密码',
'demoandformstepform.phone-number.placeholder': '手机号',
'demoandformstepform.verification-code.placeholder': '验证码',
'demoandformstepform.title.label': '标题',
'demoandformstepform.title.placeholder': '给目标起个名字',
'demoandformstepform.date.label': '起止日期',
'demoandformstepform.placeholder.start': '开始日期',
'demoandformstepform.placeholder.end': '结束日期',
'demoandformstepform.goal.label': '目标描述',
'demoandformstepform.goal.placeholder': '请输入你的阶段性工作目标',
'demoandformstepform.standard.label': '衡量标准',
'demoandformstepform.standard.placeholder': '请输入衡量标准',
'demoandformstepform.client.label': '客户',
'demoandformstepform.label.tooltip': '目标的服务对象',
'demoandformstepform.client.placeholder': '请描述你服务的客户,内部客户直接 @姓名/工号',
'demoandformstepform.invites.label': '邀评人',
'demoandformstepform.invites.placeholder': '请直接 @姓名/工号,最多可邀请 5 人',
'demoandformstepform.weight.label': '权重',
'demoandformstepform.weight.placeholder': '请输入',
'demoandformstepform.public.label': '目标公开',
'demoandformstepform.label.help': '客户、邀评人默认被分享',
'demoandformstepform.radio.public': '公开',
'demoandformstepform.radio.partially-public': '部分公开',
'demoandformstepform.radio.private': '不公开',
'demoandformstepform.publicUsers.placeholder': '公开给',
'demoandformstepform.option.A': '同事甲',
'demoandformstepform.option.B': '同事乙',
'demoandformstepform.option.C': '同事丙',
};
export default {
'demoandformstepform.basic.title': '基礎表單',
'demoandformstepform.basic.description':
'表單頁用於向用戶收集或驗證信息,基礎表單常見於數據項較少的表單場景。',
'demoandformstepform.email.required': '請輸入郵箱地址!',
'demoandformstepform.email.wrong-format': '郵箱地址格式錯誤!',
'demoandformstepform.userName.required': '請輸入賬戶!',
'demoandformstepform.password.required': '請輸入密碼!',
'demoandformstepform.password.twice': '兩次輸入的密碼不匹配!',
'demoandformstepform.strength.msg': '請至少輸入 6 個字符。請不要使用容易被猜到的密碼。',
'demoandformstepform.strength.strong': '強度:強',
'demoandformstepform.strength.medium': '強度:中',
'demoandformstepform.strength.short': '強度:太短',
'demoandformstepform.confirm-password.required': '請確認密碼!',
'demoandformstepform.phone-number.required': '請輸入手機號!',
'demoandformstepform.phone-number.wrong-format': '手機號格式錯誤!',
'demoandformstepform.verification-code.required': '請輸入驗證碼!',
'demoandformstepform.title.required': '請輸入標題',
'demoandformstepform.date.required': '請選擇起止日期',
'demoandformstepform.goal.required': '請輸入目標描述',
'demoandformstepform.standard.required': '請輸入衡量標淮',
'demoandformstepform.form.get-captcha': '獲取驗證碼',
'demoandformstepform.captcha.second': '秒',
'demoandformstepform.form.optional': '(選填)',
'demoandformstepform.form.submit': '提交',
'demoandformstepform.form.save': '保存',
'demoandformstepform.email.placeholder': '郵箱',
'demoandformstepform.password.placeholder': '至少6位密碼,區分大小寫',
'demoandformstepform.confirm-password.placeholder': '確認密碼',
'demoandformstepform.phone-number.placeholder': '手機號',
'demoandformstepform.verification-code.placeholder': '驗證碼',
'demoandformstepform.title.label': '標題',
'demoandformstepform.title.placeholder': '給目標起個名字',
'demoandformstepform.date.label': '起止日期',
'demoandformstepform.placeholder.start': '開始日期',
'demoandformstepform.placeholder.end': '結束日期',
'demoandformstepform.goal.label': '目標描述',
'demoandformstepform.goal.placeholder': '請輸入妳的階段性工作目標',
'demoandformstepform.standard.label': '衡量標淮',
'demoandformstepform.standard.placeholder': '請輸入衡量標淮',
'demoandformstepform.client.label': '客戶',
'demoandformstepform.label.tooltip': '目標的服務對象',
'demoandformstepform.client.placeholder': '請描述妳服務的客戶,內部客戶直接 @姓名/工號',
'demoandformstepform.invites.label': '邀評人',
'demoandformstepform.invites.placeholder': '請直接 @姓名/工號,最多可邀請 5 人',
'demoandformstepform.weight.label': '權重',
'demoandformstepform.weight.placeholder': '請輸入',
'demoandformstepform.public.label': '目標公開',
'demoandformstepform.label.help': '客戶、邀評人默認被分享',
'demoandformstepform.radio.public': '公開',
'demoandformstepform.radio.partially-public': '部分公開',
'demoandformstepform.radio.private': '不公開',
'demoandformstepform.publicUsers.placeholder': '公開給',
'demoandformstepform.option.A': '同事甲',
'demoandformstepform.option.B': '同事乙',
'demoandformstepform.option.C': '同事丙',
};
import type { Effect, Reducer } from 'umi';
import { fakeSubmitForm } from './service';
export type StateType = {
current?: string;
step?: {
payAccount: string;
receiverAccount: string;
receiverName: string;
amount: string;
};
};
export type ModelType = {
namespace: string;
state: StateType;
effects: {
submitStepForm: Effect;
};
reducers: {
saveStepFormData: Reducer<StateType>;
saveCurrentStep: Reducer<StateType>;
};
};
const Model: ModelType = {
namespace: 'demoAndFormStepForm',
state: {
current: 'info',
step: {
payAccount: 'ant-design@alipay.com',
receiverAccount: 'test@example.com',
receiverName: 'Alex',
amount: '500',
},
},
effects: {
*submitStepForm({ payload }, { call, put }) {
yield call(fakeSubmitForm, payload);
yield put({
type: 'saveStepFormData',
payload,
});
yield put({
type: 'saveCurrentStep',
payload: 'result',
});
},
},
reducers: {
saveCurrentStep(state, { payload }) {
return {
...state,
current: payload,
};
},
saveStepFormData(state, { payload }) {
return {
...state,
step: {
...(state as StateType).step,
...payload,
},
};
},
},
};
export default Model;
import request from 'umi-request';
export async function fakeSubmitForm(params: any) {
return request('/api/forms', {
method: 'POST',
data: params,
});
}
@import '~antd/es/style/themes/default.less';
.card {
margin-bottom: 24px;
}
.heading {
margin: 0 0 16px 0;
font-size: 14px;
line-height: 22px;
}
.steps:global(.ant-steps) {
max-width: 750px;
margin: 16px auto;
}
.errorIcon {
margin-right: 24px;
color: @error-color;
cursor: pointer;
span.anticon {
margin-right: 4px;
}
}
.errorPopover {
:global {
.ant-popover-inner-content {
min-width: 256px;
max-height: 290px;
padding: 0;
overflow: auto;
}
}
}
.errorListItem {
padding: 8px 16px;
list-style: none;
border-bottom: 1px solid @border-color-split;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: @item-active-bg;
}
&:last-child {
border: 0;
}
.errorIcon {
float: left;
margin-top: 4px;
margin-right: 12px;
padding-bottom: 22px;
color: @error-color;
}
.errorField {
margin-top: 2px;
color: @text-color-secondary;
font-size: 12px;
}
}
.editable {
td {
padding-top: 13px !important;
padding-bottom: 12.5px !important;
}
}
// custom footer for fixed footer toolbar
.advancedForm + div {
padding-bottom: 64px;
}
.advancedForm {
:global {
.ant-form .ant-row:last-child .ant-form-item {
margin-bottom: 24px;
}
.ant-table td {
transition: none !important;
}
}
}
.optional {
color: @text-color-secondary;
font-style: normal;
}
// eslint-disable-next-line import/no-extraneous-dependencies
import type { Request, Response } from 'express';
import type { BasicListItemDataType } from './data.d';
const titles = [
'Alipay',
'Angular',
'Ant Design',
'Ant Design Pro',
'Bootstrap',
'React',
'Vue',
'Webpack',
];
const avatars = [
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
];
const covers = [
'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png',
'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png',
'https://gw.alipayobjects.com/zos/rmsportal/iXjVmWVHbCJAyqvDxdtx.png',
'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
];
const desc = [
'那是一种内在的东西, 他们到达不了,也无法触及的',
'希望是一个好东西,也许是最好的,好东西是不会消亡的',
'生命就像一盒巧克力,结果往往出人意料',
'城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
'那时候我只会想自己想要什么,从不想自己拥有什么',
];
const user = [
'付小小',
'曲丽丽',
'林东东',
'周星星',
'吴加好',
'朱偏右',
'鱼酱',
'乐哥',
'谭小仪',
'仲尼',
];
function fakeList(count: number): BasicListItemDataType[] {
const list = [];
for (let i = 0; i < count; i += 1) {
list.push({
id: `fake-list-${i}`,
owner: user[i % 10],
title: titles[i % 8],
avatar: avatars[i % 8],
cover: parseInt(`${i / 4}`, 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)],
status: ['active', 'exception', 'normal'][i % 3] as
| 'normal'
| 'exception'
| 'active'
| 'success',
percent: Math.ceil(Math.random() * 50) + 50,
logo: avatars[i % 8],
href: 'https://ant.design',
updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i).getTime(),
createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i).getTime(),
subDescription: desc[i % 5],
description:
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
activeUser: Math.ceil(Math.random() * 100000) + 100000,
newUser: Math.ceil(Math.random() * 1000) + 1000,
star: Math.ceil(Math.random() * 100) + 100,
like: Math.ceil(Math.random() * 100) + 100,
message: Math.ceil(Math.random() * 10) + 10,
content:
'段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
members: [
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
name: '曲丽丽',
id: 'member1',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
name: '王昭君',
id: 'member2',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
name: '董娜娜',
id: 'member3',
},
],
});
}
return list;
}
let sourceData: BasicListItemDataType[] = [];
function getFakeList(req: Request, res: Response) {
const params = req.query as any
const count = params.count * 1 || 20;
const result = fakeList(count);
sourceData = result;
return res.json(result);
}
function postFakeList(req: Request, res: Response) {
const { /* url = '', */ body } = req;
// const params = getUrlParams(url);
const { method, id } = body;
// const count = (params.count * 1) || 20;
let result = sourceData || [];
switch (method) {
case 'delete':
result = result.filter((item) => item.id !== id);
break;
case 'update':
result.forEach((item, i) => {
if (item.id === id) {
result[i] = { ...item, ...body };
}
});
break;
case 'post':
result.unshift({
...body,
id: `fake-list-${result.length}`,
createdAt: new Date().getTime(),
});
break;
default:
break;
}
return res.json(result);
}
export default {
'GET /api/fake_list': getFakeList,
'POST /api/fake_list': postFakeList,
};
import type { FC } from 'react';
import React, { useEffect } from 'react';
import moment from 'moment';
import { Modal, Result, Button, Form, DatePicker, Input, Select } from 'antd';
import type { UserTableListItem } from '../data.d';
import styles from '../style.less';
type OperationModalProps = {
done: boolean;
visible: boolean;
current: Partial<UserTableListItem> | undefined;
onDone: () => void;
onSubmit: (values: UserTableListItem) => void;
onCancel: () => void;
};
const { TextArea } = Input;
const formLayout = {
labelCol: { span: 7 },
wrapperCol: { span: 13 },
};
const OperationModal: FC<OperationModalProps> = (props) => {
const [form] = Form.useForm();
const { done, visible, current, onDone, onCancel, onSubmit } = props;
useEffect(() => {
if (form && !visible) {
form.resetFields();
}
}, [props.visible]);
useEffect(() => {
if (current) {
form.setFieldsValue({
...current,
createdAt: current.createdAt ? moment(current.createdAt) : null,
});
}
}, [props.current]);
const handleSubmit = () => {
if (!form) return;
form.submit();
};
const handleFinish = (values: Record<string, any>) => {
if (onSubmit) {
onSubmit(values as UserTableListItem);
}
};
const modalFooter = done
? { footer: null, onCancel: onDone }
: { okText: '保存', onOk: handleSubmit, onCancel };
const getModalContent = () => {
if (done) {
return (
<Result
status="success"
title="操作成功"
subTitle="一系列的信息描述,很短同样也可以带标点。"
extra={
<Button type="primary" onClick={onDone}>
知道了
</Button>
}
className={styles.formResult}
/>
);
}
return (
<Form {...formLayout} form={form} onFinish={handleFinish}>
<Form.Item
name="title"
label="任务名称"
rules={[{ required: true, message: '请输入任务名称' }]}
>
<Input placeholder="请输入" />
</Form.Item>
<Form.Item
name="createdAt"
label="开始时间"
rules={[{ required: true, message: '请选择开始时间' }]}
>
<DatePicker
showTime
placeholder="请选择"
format="YYYY-MM-DD HH:mm:ss"
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item
name="owner"
label="任务负责人"
rules={[{ required: true, message: '请选择任务负责人' }]}
>
<Select placeholder="请选择">
<Select.Option value="付晓晓">付晓晓</Select.Option>
<Select.Option value="周毛毛">周毛毛</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="subDescription"
label="产品描述"
rules={[{ message: '请输入至少五个字符的产品描述!', min: 5 }]}
>
<TextArea rows={4} placeholder="请输入至少五个字符" />
</Form.Item>
</Form>
);
};
return (
<Modal
title={done ? null : `任务${current ? '编辑' : '添加'}`}
className={styles.standardListForm}
width={640}
bodyStyle={done ? { padding: '72px 0' } : { padding: '28px 0 0' }}
destroyOnClose
visible={visible}
{...modalFooter}
>
{getModalContent()}
</Modal>
);
};
export default OperationModal;
export type Member = {
avatar: string;
name: string;
id: string;
};
export type BasicListItemDataType = {
id: string;
owner: string;
title: string;
avatar: string;
cover: string;
status: 'normal' | 'exception' | 'active' | 'success';
percent: number;
logo: string;
href: string;
body?: any;
updatedAt: number;
createdAt: number;
subDescription: string;
description: string;
activeUser: number;
newUser: number;
star: number;
like: number;
message: number;
content: string;
members: Member[];
};
import type { FC } from 'react';
import React, { useRef, useState, useEffect } from 'react';
import { DownOutlined, PlusOutlined } from '@ant-design/icons';
import {
Avatar,
Button,
Card,
Col,
Dropdown,
Input,
List,
Menu,
Modal,
Progress,
Radio,
Row,
} from 'antd';
import { findDOMNode } from 'react-dom';
import { PageContainer } from '@ant-design/pro-layout';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import moment from 'moment';
import OperationModal from './components/OperationModal';
import type { StateType } from './model';
import type { BasicListItemDataType } from './data.d';
import styles from './style.less';
const RadioButton = Radio.Button;
const RadioGroup = Radio.Group;
const { Search } = Input;
type ListBasicListProps = {
listBasicList: StateType;
dispatch: Dispatch;
loading: boolean;
};
const Info: FC<{
title: React.ReactNode;
value: React.ReactNode;
bordered?: boolean;
}> = ({ title, value, bordered }) => (
<div className={styles.headerInfo}>
<span>{title}</span>
<p>{value}</p>
{bordered && <em />}
</div>
);
const ListContent = ({
data: { owner, createdAt, percent, status },
}: {
data: BasicListItemDataType;
}) => (
<div className={styles.listContent}>
<div className={styles.listContentItem}>
<span>Owner</span>
<p>{owner}</p>
</div>
<div className={styles.listContentItem}>
<span>开始时间</span>
<p>{moment(createdAt).format('YYYY-MM-DD HH:mm')}</p>
</div>
<div className={styles.listContentItem}>
<Progress percent={percent} status={status} strokeWidth={6} style={{ width: 180 }} />
</div>
</div>
);
export const ListBasicList: FC<ListBasicListProps> = (props) => {
const addBtn = useRef(null);
const {
loading,
dispatch,
listBasicList: { list },
} = props;
const [done, setDone] = useState<boolean>(false);
const [visible, setVisible] = useState<boolean>(false);
const [current, setCurrent] = useState<Partial<BasicListItemDataType> | undefined>(undefined);
useEffect(() => {
dispatch({
type: 'listBasicList/fetch',
payload: {
count: 5,
},
});
}, [1]);
const paginationProps = {
showSizeChanger: true,
showQuickJumper: true,
pageSize: 5,
total: 50,
};
const showModal = () => {
setVisible(true);
setCurrent(undefined);
};
const showEditModal = (item: BasicListItemDataType) => {
setVisible(true);
setCurrent(item);
};
const deleteItem = (id: string) => {
dispatch({
type: 'listBasicList/submit',
payload: { id },
});
};
const editAndDelete = (key: string | number, currentItem: BasicListItemDataType) => {
if (key === 'edit') showEditModal(currentItem);
else if (key === 'delete') {
Modal.confirm({
title: '删除任务',
content: '确定删除该任务吗?',
okText: '确认',
cancelText: '取消',
onOk: () => deleteItem(currentItem.id),
});
}
};
const extraContent = (
<div className={styles.extraContent}>
<RadioGroup defaultValue="all">
<RadioButton value="all">全部</RadioButton>
<RadioButton value="progress">进行中</RadioButton>
<RadioButton value="waiting">等待中</RadioButton>
</RadioGroup>
<Search className={styles.extraContentSearch} placeholder="请输入" onSearch={() => ({})} />
</div>
);
const MoreBtn: React.FC<{
item: BasicListItemDataType;
}> = ({ item }) => (
<Dropdown
overlay={
<Menu onClick={({ key }) => editAndDelete(key, item)}>
<Menu.Item key="edit">编辑</Menu.Item>
<Menu.Item key="delete">删除</Menu.Item>
</Menu>
}
>
<a>
更多 <DownOutlined />
</a>
</Dropdown>
);
const setAddBtnblur = () => {
if (addBtn.current) {
// eslint-disable-next-line react/no-find-dom-node
const addBtnDom = findDOMNode(addBtn.current) as HTMLButtonElement;
setTimeout(() => addBtnDom.blur(), 0);
}
};
const handleDone = () => {
setAddBtnblur();
setDone(false);
setVisible(false);
};
const handleCancel = () => {
setAddBtnblur();
setVisible(false);
};
const handleSubmit = (values: BasicListItemDataType) => {
setAddBtnblur();
setDone(true);
dispatch({
type: 'listBasicList/submit',
payload: values,
});
};
return (
<div>
<PageContainer>
<div className={styles.standardList}>
<Card bordered={false}>
<Row>
<Col sm={8} xs={24}>
<Info title="我的待办" value="8个任务" bordered />
</Col>
<Col sm={8} xs={24}>
<Info title="本周任务平均处理时间" value="32分钟" bordered />
</Col>
<Col sm={8} xs={24}>
<Info title="本周完成任务数" value="24个任务" />
</Col>
</Row>
</Card>
<Card
className={styles.listCard}
bordered={false}
title="基本列表"
style={{ marginTop: 24 }}
bodyStyle={{ padding: '0 32px 40px 32px' }}
extra={extraContent}
>
<Button
type="dashed"
style={{ width: '100%', marginBottom: 8 }}
onClick={showModal}
ref={addBtn}
>
<PlusOutlined />
添加
</Button>
<List
size="large"
rowKey="id"
loading={loading}
pagination={paginationProps}
dataSource={list}
renderItem={(item) => (
<List.Item
actions={[
<a
key="edit"
onClick={(e) => {
e.preventDefault();
showEditModal(item);
}}
>
编辑
</a>,
<MoreBtn key="more" item={item} />,
]}
>
<List.Item.Meta
avatar={<Avatar src={item.logo} shape="square" size="large" />}
title={<a href={item.href}>{item.title}</a>}
description={item.subDescription}
/>
<ListContent data={item} />
</List.Item>
)}
/>
</Card>
</div>
</PageContainer>
<OperationModal
done={done}
current={current}
visible={visible}
onDone={handleDone}
onCancel={handleCancel}
onSubmit={handleSubmit}
/>
</div>
);
};
export default connect(
({
listBasicList,
loading,
}: {
listBasicList: StateType;
loading: {
models: Record<string, boolean>;
};
}) => ({
listBasicList,
loading: loading.models.listBasicList,
}),
)(ListBasicList);
import type { Effect, Reducer } from 'umi';
import { addFakeList, queryFakeList, removeFakeList, updateFakeList } from './service';
import type { BasicListItemDataType } from './data.d';
export type StateType = {
list: BasicListItemDataType[];
};
export type ModelType = {
namespace: string;
state: StateType;
effects: {
fetch: Effect;
appendFetch: Effect;
submit: Effect;
};
reducers: {
queryList: Reducer<StateType>;
appendList: Reducer<StateType>;
};
};
const Model: ModelType = {
namespace: 'listBasicList',
state: {
list: [],
},
effects: {
*fetch({ payload }, { call, put }) {
const response = yield call(queryFakeList, payload);
yield put({
type: 'queryList',
payload: Array.isArray(response) ? response : [],
});
},
*appendFetch({ payload }, { call, put }) {
const response = yield call(queryFakeList, payload);
yield put({
type: 'appendList',
payload: Array.isArray(response) ? response : [],
});
},
*submit({ payload }, { call, put }) {
let callback;
if (payload.id) {
callback = Object.keys(payload).length === 1 ? removeFakeList : updateFakeList;
} else {
callback = addFakeList;
}
const response = yield call(callback, payload); // post
yield put({
type: 'queryList',
payload: response,
});
},
},
reducers: {
queryList(state, action) {
return {
...state,
list: action.payload,
};
},
appendList(state = { list: [] }, action) {
return {
...state,
list: state.list.concat(action.payload),
};
},
},
};
export default Model;
import request from 'umi-request';
import type { BasicListItemDataType } from './data.d';
type ParamsType = {
count?: number;
} & Partial<BasicListItemDataType>;
export async function queryFakeList(params: ParamsType) {
return request('/api/fake_list', {
params,
});
}
export async function removeFakeList(params: ParamsType) {
const { count = 5, ...restParams } = params;
return request('/api/fake_list', {
method: 'POST',
params: {
count,
},
data: {
...restParams,
method: 'delete',
},
});
}
export async function addFakeList(params: ParamsType) {
const { count = 5, ...restParams } = params;
return request('/api/fake_list', {
method: 'POST',
params: {
count,
},
data: {
...restParams,
method: 'post',
},
});
}
export async function updateFakeList(params: ParamsType) {
const { count = 5, ...restParams } = params;
return request('/api/fake_list', {
method: 'POST',
params: {
count,
},
data: {
...restParams,
method: 'update',
},
});
}
@import '~antd/es/style/themes/default.less';
@import './utils/utils.less';
.standardList {
:global {
.ant-card-head {
border-bottom: none;
}
.ant-card-head-title {
padding: 24px 0;
line-height: 32px;
}
.ant-card-extra {
padding: 24px 0;
}
.ant-list-pagination {
margin-top: 24px;
text-align: right;
}
.ant-avatar-lg {
width: 48px;
height: 48px;
line-height: 48px;
}
}
.headerInfo {
position: relative;
text-align: center;
& > span {
display: inline-block;
margin-bottom: 4px;
color: @text-color-secondary;
font-size: @font-size-base;
line-height: 22px;
}
& > p {
margin: 0;
color: @heading-color;
font-size: 24px;
line-height: 32px;
}
& > em {
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 56px;
background-color: @border-color-split;
}
}
.listContent {
font-size: 0;
.listContentItem {
display: inline-block;
margin-left: 40px;
color: @text-color-secondary;
font-size: @font-size-base;
vertical-align: middle;
> span {
line-height: 20px;
}
> p {
margin-top: 4px;
margin-bottom: 0;
line-height: 22px;
}
}
}
.extraContentSearch {
width: 272px;
margin-left: 16px;
}
}
@media screen and (max-width: @screen-xs) {
.standardList {
:global {
.ant-list-item-content {
display: block;
flex: none;
width: 100%;
}
.ant-list-item-action {
margin-left: 0;
}
}
.listContent {
margin-left: 0;
& > div {
margin-left: 0;
}
}
.listCard {
:global {
.ant-card-head-title {
overflow: visible;
}
}
}
}
}
@media screen and (max-width: @screen-sm) {
.standardList {
.extraContentSearch {
width: 100%;
margin-left: 0;
}
.headerInfo {
margin-bottom: 16px;
& > em {
display: none;
}
}
}
}
@media screen and (max-width: @screen-md) {
.standardList {
.listContent {
& > div {
display: block;
}
& > div:last-child {
top: 0;
width: 100%;
}
}
}
.listCard {
:global {
.ant-radio-group {
display: block;
margin-bottom: 8px;
}
}
}
}
@media screen and (max-width: @screen-lg) and (min-width: @screen-md) {
.standardList {
.listContent {
& > div {
display: block;
}
& > div:last-child {
top: 0;
width: 100%;
}
}
}
}
@media screen and (max-width: @screen-xl) {
.standardList {
.listContent {
& > div {
margin-left: 24px;
}
& > div:last-child {
top: 0;
}
}
}
}
@media screen and (max-width: 1400px) {
.standardList {
.listContent {
text-align: right;
& > div:last-child {
top: 0;
}
}
}
}
.standardListForm {
:global {
.ant-form-item {
margin-bottom: 12px;
&:last-child {
margin-bottom: 32px;
padding-top: 4px;
}
}
}
}
.formResult {
width: 100%;
[class^='title'] {
margin-bottom: 8px;
}
}
.textOverflow() {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
word-break: break-all;
}
.textOverflowMulti(@line: 3, @bg: #fff) {
position: relative;
max-height: @line * 1.5em;
margin-right: -1em;
padding-right: 1em;
overflow: hidden;
line-height: 1.5em;
text-align: justify;
&::before {
position: absolute;
right: 14px;
bottom: 0;
padding: 0 1px;
background: @bg;
content: '...';
}
&::after {
position: absolute;
right: 14px;
width: 1em;
height: 1em;
margin-top: 0.2em;
background: white;
content: '';
}
}
// mixins for clearfix
// ------------------------
.clearfix() {
zoom: 1;
&::before,
&::after {
display: table;
content: ' ';
}
&::after {
clear: both;
height: 0;
font-size: 0;
visibility: hidden;
}
}
// eslint-disable-next-line import/no-extraneous-dependencies
import type { Request, Response } from 'express';
import { parse } from 'url';
import type { TableListItem, TableListParams } from './data.d';
// mock tableListDataSource
const genList = (current: number, pageSize: number) => {
const tableListDataSource: TableListItem[] = [];
for (let i = 0; i < pageSize; i += 1) {
const index = (current - 1) * 10 + i;
tableListDataSource.push({
key: index,
disabled: i % 6 === 0,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name: `TradeCode ${index}`,
owner: '曲丽丽',
desc: '这是一段描述',
callNo: Math.floor(Math.random() * 1000),
status: (Math.floor(Math.random() * 10) % 4).toString(),
updatedAt: new Date(),
createdAt: new Date(),
progress: Math.ceil(Math.random() * 100),
});
}
tableListDataSource.reverse();
return tableListDataSource;
};
let tableListDataSource = genList(1, 100);
function getRule(req: Request, res: Response, u: string) {
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const { current = 1, pageSize = 10 } = req.query;
const params = (parse(realUrl, true).query as unknown) as TableListParams;
let dataSource = [...tableListDataSource].slice(
((current as number) - 1) * (pageSize as number),
(current as number) * (pageSize as number),
);
const sorter = JSON.parse(params.sorter as any);
if (sorter) {
dataSource = dataSource.sort((prev, next) => {
let sortNumber = 0;
Object.keys(sorter).forEach((key) => {
if (sorter[key] === 'descend') {
if (prev[key] - next[key] > 0) {
sortNumber += -1;
} else {
sortNumber += 1;
}
return;
}
if (prev[key] - next[key] > 0) {
sortNumber += 1;
} else {
sortNumber += -1;
}
});
return sortNumber;
});
}
if (params.filter) {
const filter = JSON.parse(params.filter as any) as Record<string, string[]>;
if (Object.keys(filter).length > 0) {
dataSource = dataSource.filter((item) => {
return Object.keys(filter).some((key) => {
if (!filter[key]) {
return true;
}
if (filter[key].includes(`${item[key]}`)) {
return true;
}
return false;
});
});
}
}
if (params.name) {
dataSource = dataSource.filter((data) => data.name.includes(params.name || ''));
}
const result = {
data: dataSource,
total: tableListDataSource.length,
success: true,
pageSize,
current: parseInt(`${params.currentPage}`, 10) || 1,
};
return res.json(result);
}
function postRule(req: Request, res: Response, u: string, b: Request) {
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const body = (b && b.body) || req.body;
const { method, name, desc, key } = body;
switch (method) {
/* eslint no-case-declarations:0 */
case 'delete':
tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1);
break;
case 'post':
(() => {
const i = Math.ceil(Math.random() * 10000);
const newRule = {
key: tableListDataSource.length,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name,
owner: '曲丽丽',
desc,
callNo: Math.floor(Math.random() * 1000),
status: (Math.floor(Math.random() * 10) % 2).toString(),
updatedAt: new Date(),
createdAt: new Date(),
progress: Math.ceil(Math.random() * 100),
};
tableListDataSource.unshift(newRule);
return res.json(newRule);
})();
return;
case 'update':
(() => {
let newRule = {};
tableListDataSource = tableListDataSource.map((item) => {
if (item.key === key) {
newRule = { ...item, desc, name };
return { ...item, desc, name };
}
return item;
});
return res.json(newRule);
})();
return;
default:
break;
}
const result = {
list: tableListDataSource,
pagination: {
total: tableListDataSource.length,
},
};
res.json(result);
}
export default {
'GET /api/rule': getRule,
'POST /api/rule': postRule,
};
import React from 'react';
import { Modal } from 'antd';
type CreateFormProps = {
modalVisible: boolean;
onCancel: () => void;
};
const CreateForm: React.FC<CreateFormProps> = (props) => {
const { modalVisible, onCancel } = props;
return (
<Modal
destroyOnClose
title="新建规则"
visible={modalVisible}
onCancel={() => onCancel()}
footer={null}
>
{props.children}
</Modal>
);
};
export default CreateForm;
import React, { useState } from 'react';
import { Form, Button, DatePicker, Input, Modal, Radio, Select, Steps } from 'antd';
import type { TableListItem } from '../data.d';
export type FormValueType = {
target?: string;
template?: string;
type?: string;
time?: string;
frequency?: string;
} & Partial<TableListItem>;
export type UpdateFormProps = {
onCancel: (flag?: boolean, formVals?: FormValueType) => void;
onSubmit: (values: FormValueType) => void;
updateModalVisible: boolean;
values: Partial<TableListItem>;
};
const FormItem = Form.Item;
const { Step } = Steps;
const { TextArea } = Input;
const { Option } = Select;
const RadioGroup = Radio.Group;
export type UpdateFormState = {
formVals: FormValueType;
currentStep: number;
};
const formLayout = {
labelCol: { span: 7 },
wrapperCol: { span: 13 },
};
const UpdateForm: React.FC<UpdateFormProps> = (props) => {
const [formVals, setFormVals] = useState<FormValueType>({
name: props.values.name,
desc: props.values.desc,
key: props.values.key,
target: '0',
template: '0',
type: '1',
time: '',
frequency: 'month',
});
const [currentStep, setCurrentStep] = useState<number>(0);
const [form] = Form.useForm();
const {
onSubmit: handleUpdate,
onCancel: handleUpdateModalVisible,
updateModalVisible,
values,
} = props;
const forward = () => setCurrentStep(currentStep + 1);
const backward = () => setCurrentStep(currentStep - 1);
const handleNext = async () => {
const fieldsValue = await form.validateFields();
setFormVals({ ...formVals, ...fieldsValue });
if (currentStep < 2) {
forward();
} else {
handleUpdate({ ...formVals, ...fieldsValue });
}
};
const renderContent = () => {
if (currentStep === 1) {
return (
<>
<FormItem name="target" label="监控对象">
<Select style={{ width: '100%' }}>
<Option value="0">表一</Option>
<Option value="1">表二</Option>
</Select>
</FormItem>
<FormItem name="template" label="规则模板">
<Select style={{ width: '100%' }}>
<Option value="0">规则模板一</Option>
<Option value="1">规则模板二</Option>
</Select>
</FormItem>
<FormItem name="type" label="规则类型">
<RadioGroup>
<Radio value="0"></Radio>
<Radio value="1"></Radio>
</RadioGroup>
</FormItem>
</>
);
}
if (currentStep === 2) {
return (
<>
<FormItem
name="time"
label="开始时间"
rules={[{ required: true, message: '请选择开始时间!' }]}
>
<DatePicker
style={{ width: '100%' }}
showTime
format="YYYY-MM-DD HH:mm:ss"
placeholder="选择开始时间"
/>
</FormItem>
<FormItem name="frequency" label="调度周期">
<Select style={{ width: '100%' }}>
<Option value="month"></Option>
<Option value="week"></Option>
</Select>
</FormItem>
</>
);
}
return (
<>
<FormItem
name="name"
label="规则名称"
rules={[{ required: true, message: '请输入规则名称!' }]}
>
<Input placeholder="请输入" />
</FormItem>
<FormItem
name="desc"
label="规则描述"
rules={[{ required: true, message: '请输入至少五个字符的规则描述!', min: 5 }]}
>
<TextArea rows={4} placeholder="请输入至少五个字符" />
</FormItem>
</>
);
};
const renderFooter = () => {
if (currentStep === 1) {
return (
<>
<Button style={{ float: 'left' }} onClick={backward}>
上一步
</Button>
<Button onClick={() => handleUpdateModalVisible(false, values)}>取消</Button>
<Button type="primary" onClick={() => handleNext()}>
下一步
</Button>
</>
);
}
if (currentStep === 2) {
return (
<>
<Button style={{ float: 'left' }} onClick={backward}>
上一步
</Button>
<Button onClick={() => handleUpdateModalVisible(false, values)}>取消</Button>
<Button type="primary" onClick={() => handleNext()}>
完成
</Button>
</>
);
}
return (
<>
<Button onClick={() => handleUpdateModalVisible(false, values)}>取消</Button>
<Button type="primary" onClick={() => handleNext()}>
下一步
</Button>
</>
);
};
return (
<Modal
width={640}
bodyStyle={{ padding: '32px 40px 48px' }}
destroyOnClose
title="规则配置"
visible={updateModalVisible}
footer={renderFooter()}
onCancel={() => handleUpdateModalVisible()}
>
<Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
<Step title="基本信息" />
<Step title="配置规则属性" />
<Step title="设定调度周期" />
</Steps>
<Form
{...formLayout}
form={form}
initialValues={{
target: formVals.target,
template: formVals.template,
type: formVals.type,
frequency: formVals.frequency,
name: formVals.name,
desc: formVals.desc,
}}
>
{renderContent()}
</Form>
</Modal>
);
};
export default UpdateForm;
export type TableListItem = {
key: number;
disabled?: boolean;
href: string;
avatar: string;
name: string;
owner: string;
desc: string;
callNo: number;
status: string;
updatedAt: Date;
createdAt: Date;
progress: number;
};
export type TableListPagination = {
total: number;
pageSize: number;
current: number;
};
export type TableListData = {
list: TableListItem[];
pagination: Partial<TableListPagination>;
};
export type TableListParams = {
status?: string;
name?: string;
desc?: string;
key?: number;
pageSize?: number;
currentPage?: number;
filter?: Record<string, any[]>;
sorter?: Record<string, any>;
};
import { PlusOutlined } from '@ant-design/icons';
import { Button, Divider, message, Input, Drawer } from 'antd';
import React, { useState, useRef } from 'react';
import { PageContainer, FooterToolbar } from '@ant-design/pro-layout';
import type { ProColumns, ActionType } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import ProDescriptions from '@ant-design/pro-descriptions';
import CreateForm from './components/CreateForm';
import type { FormValueType } from './components/UpdateForm';
import UpdateForm from './components/UpdateForm';
import type { TableListItem } from './data.d';
import { queryRule, updateRule, addRule, removeRule } from './service';
/**
* 添加节点
*
* @param fields
*/
const handleAdd = async (fields: TableListItem) => {
const hide = message.loading('正在添加');
try {
await addRule({ ...fields });
hide();
message.success('添加成功');
return true;
} catch (error) {
hide();
message.error('添加失败请重试!');
return false;
}
};
/**
* 更新节点
*
* @param fields
*/
const handleUpdate = async (fields: FormValueType) => {
const hide = message.loading('正在配置');
try {
await updateRule({
name: fields.name,
desc: fields.desc,
key: fields.key,
});
hide();
message.success('配置成功');
return true;
} catch (error) {
hide();
message.error('配置失败请重试!');
return false;
}
};
/**
* 删除节点
*
* @param selectedRows
*/
const handleRemove = async (selectedRows: TableListItem[]) => {
const hide = message.loading('正在删除');
if (!selectedRows) return true;
try {
await removeRule({
key: selectedRows.map((row) => row.key),
});
hide();
message.success('删除成功,即将刷新');
return true;
} catch (error) {
hide();
message.error('删除失败,请重试');
return false;
}
};
const TableList: React.FC<{}> = () => {
const [createModalVisible, handleModalVisible] = useState<boolean>(false);
const [updateModalVisible, handleUpdateModalVisible] = useState<boolean>(false);
const [stepFormValues, setStepFormValues] = useState({});
const actionRef = useRef<ActionType>();
const [row, setRow] = useState<TableListItem>();
const [selectedRowsState, setSelectedRows] = useState<TableListItem[]>([]);
const columns: ProColumns<TableListItem>[] = [
{
title: '规则名称',
dataIndex: 'name',
tip: '规则名称是唯一的 key',
formItemProps: {
rules: [
{
required: true,
message: '规则名称为必填项',
},
],
},
render: (dom, entity) => {
return <a onClick={() => setRow(entity)}>{dom}</a>;
},
},
{
title: '描述',
dataIndex: 'desc',
valueType: 'textarea',
},
{
title: '服务调用次数',
dataIndex: 'callNo',
sorter: true,
hideInForm: true,
renderText: (val: string) => `${val} 万`,
},
{
title: '状态',
dataIndex: 'status',
hideInForm: true,
valueEnum: {
0: { text: '关闭', status: 'Default' },
1: { text: '运行中', status: 'Processing' },
2: { text: '已上线', status: 'Success' },
3: { text: '异常', status: 'Error' },
},
},
{
title: '上次调度时间',
dataIndex: 'updatedAt',
sorter: true,
valueType: 'dateTime',
hideInForm: true,
renderFormItem: (item, { defaultRender, ...rest }, form) => {
const status = form.getFieldValue('status');
if (`${status}` === '0') {
return false;
}
if (`${status}` === '3') {
return <Input {...rest} placeholder="请输入异常原因!" />;
}
return defaultRender(item);
},
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
render: (_, record) => [
<a
onClick={() => {
handleUpdateModalVisible(true);
setStepFormValues(record);
}}
>
配置
</a>,
<Divider type="vertical" />,
<a href="">订阅警报</a>,
],
},
];
return (
<PageContainer>
<ProTable<TableListItem>
headerTitle="查询表格"
actionRef={actionRef}
rowKey="key"
search={{
labelWidth: 120,
}}
toolBarRender={() => [
<Button type="primary" onClick={() => handleModalVisible(true)}>
<PlusOutlined /> 新建
</Button>,
]}
request={(params, sorter, filter) => queryRule({ ...params, sorter, filter })}
columns={columns}
rowSelection={{
onChange: (_, selectedRows) => setSelectedRows(selectedRows),
}}
/>
{selectedRowsState?.length > 0 && (
<FooterToolbar
extra={
<div>
已选择 <a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>&nbsp;&nbsp;
<span>
服务调用次数总计 {selectedRowsState.reduce((pre, item) => pre + item.callNo, 0)}
</span>
</div>
}
>
<Button
onClick={async () => {
await handleRemove(selectedRowsState);
setSelectedRows([]);
actionRef.current?.reloadAndRest?.();
}}
>
批量删除
</Button>
<Button type="primary">批量审批</Button>
</FooterToolbar>
)}
<CreateForm onCancel={() => handleModalVisible(false)} modalVisible={createModalVisible}>
<ProTable<TableListItem, TableListItem>
onSubmit={async (value) => {
const success = await handleAdd(value);
if (success) {
handleModalVisible(false);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
rowKey="key"
type="form"
columns={columns}
/>
</CreateForm>
{stepFormValues && Object.keys(stepFormValues).length ? (
<UpdateForm
onSubmit={async (value) => {
const success = await handleUpdate(value);
if (success) {
handleUpdateModalVisible(false);
setStepFormValues({});
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
onCancel={() => {
handleUpdateModalVisible(false);
setStepFormValues({});
}}
updateModalVisible={updateModalVisible}
values={stepFormValues}
/>
) : null}
<Drawer
width={600}
visible={!!row}
onClose={() => {
setRow(undefined);
}}
closable={false}
>
{row?.name && (
<ProDescriptions<TableListItem>
column={2}
title={row?.name}
request={async () => ({
data: row || {},
})}
params={{
id: row?.name,
}}
columns={columns}
/>
)}
</Drawer>
</PageContainer>
);
};
export default TableList;
import request from 'umi-request';
import type { TableListParams } from './data.d';
export async function queryRule(params?: TableListParams) {
return request('/api/rule', {
params,
});
}
export async function removeRule(params: { key: number[] }) {
return request('/api/rule', {
method: 'POST',
data: {
...params,
method: 'delete',
},
});
}
export async function addRule(params: TableListParams) {
return request('/api/rule', {
method: 'POST',
data: {
...params,
method: 'post',
},
});
}
export async function updateRule(params: TableListParams) {
return request('/api/rule', {
method: 'POST',
data: {
...params,
method: 'update',
},
});
}
import React, {useState} from 'react';
import {List,Input} from 'antd';
import {connect} from "umi";
import {SettingsStateType} from "@/pages/Settings/model";
import {saveSettings} from "@/pages/Settings/function";
type FlinkConfigProps = {
sqlSubmitJarPath: SettingsStateType['sqlSubmitJarPath'];
sqlSubmitJarParas: SettingsStateType['sqlSubmitJarParas'];
sqlSubmitJarMainAppClass: SettingsStateType['sqlSubmitJarMainAppClass'];
dispatch: any;
};
const FlinkConfigView: React.FC<FlinkConfigProps> = (props) => {
const {sqlSubmitJarPath, sqlSubmitJarParas, sqlSubmitJarMainAppClass, dispatch} = props;
const [editName, setEditName] = useState<string>('');
const [formValues, setFormValues] = useState(props);
const getData = () => [
{
title: '提交FlinkSQL的Jar文件路径',
description: (
editName!='sqlSubmitJarPath'?
(sqlSubmitJarPath?sqlSubmitJarPath:'未设置'):(<Input
id='sqlSubmitJarPath'
defaultValue={sqlSubmitJarPath}
onChange={onChange}
placeholder="hdfs:///dlink/jar/dlink-app.jar" />)),
actions: editName!='sqlSubmitJarPath'?[<a onClick={({}) => handleEditClick('sqlSubmitJarPath')}>修改</a>]:
[<a onClick={({}) => handleSaveClick('sqlSubmitJarPath')}>保存</a>,
<a onClick={({}) => handleCancelClick()}>取消</a>],
},
{
title: '提交FlinkSQL的Jar的主类入参',
description: (
editName!='sqlSubmitJarParas'?
(sqlSubmitJarParas?sqlSubmitJarParas:'未设置'):(<Input
id='sqlSubmitJarParas'
defaultValue={sqlSubmitJarParas}
onChange={onChange}
placeholder="" />)),
actions: editName!='sqlSubmitJarParas'?[<a onClick={({}) => handleEditClick('sqlSubmitJarParas')}>修改</a>]:
[<a onClick={({}) => handleSaveClick('sqlSubmitJarParas')}>保存</a>,
<a onClick={({}) => handleCancelClick()}>取消</a>],
},
{
title: '提交FlinkSQL的Jar的主类',
description: (
editName!='sqlSubmitJarMainAppClass'?
(sqlSubmitJarMainAppClass?sqlSubmitJarMainAppClass:'未设置'):(<Input
id='sqlSubmitJarMainAppClass'
defaultValue={sqlSubmitJarMainAppClass}
onChange={onChange}
placeholder="com.dlink.app.MainApp" />)),
actions: editName!='sqlSubmitJarMainAppClass'?[<a onClick={({}) => handleEditClick('sqlSubmitJarMainAppClass')}>修改</a>]:
[<a onClick={({}) => handleSaveClick('sqlSubmitJarMainAppClass')}>保存</a>,
<a onClick={({}) => handleCancelClick()}>取消</a>],
},
];
const onChange = e => {
let values = {};
values[e.target.id]=e.target.value;
setFormValues({...formValues,...values});
};
const handleEditClick = (name:string)=>{
setEditName(name);
};
const handleSaveClick = (name:string)=>{
if(formValues[name]!=props[name]) {
let values = {};
values[name] = formValues[name];
saveSettings(values, dispatch);
}
setEditName('');
};
const handleCancelClick = ()=>{
setFormValues(props);
setEditName('');
};
const data = getData();
return (
<>
<List
itemLayout="horizontal"
dataSource={data}
renderItem={(item) => (
<List.Item actions={item.actions}>
<List.Item.Meta title={item.title} description={item.description}/>
</List.Item>
)}
/>
</>
);
};
export default connect(({Settings}: { Settings: SettingsStateType }) => ({
sqlSubmitJarPath: Settings.sqlSubmitJarPath,
sqlSubmitJarParas: Settings.sqlSubmitJarParas,
sqlSubmitJarMainAppClass: Settings.sqlSubmitJarMainAppClass,
}))(FlinkConfigView);
import {postAll, getData} from "@/components/Common/crud";
import {message} from "antd";
export function loadSettings(dispatch: any) {
const res = getData('api/sysConfig/getAll');
res.then((result) => {
result.datas && dispatch && dispatch({
type: "Settings/saveSettings",
payload: result.datas,
});
});
}
export function saveSettings(values:{},dispatch: any) {
const res = postAll("api/sysConfig/updateSysConfigByJson",values);
res.then((result) => {
message.success(`修改配置成功!`);
dispatch && dispatch({
type: "Settings/saveSettings",
payload: values,
});
});
}
import React, { useState, useRef, useLayoutEffect } from 'react';
import { GridContent } from '@ant-design/pro-layout';
import { Menu } from 'antd';
import FlinkConfigView from './components/flinkConfig';
import styles from './style.less';
import {loadSettings} from "@/pages/Settings/function";
import {SettingsStateType} from "@/pages/Settings/model";
import {connect} from "umi";
const { Item } = Menu;
type SettingsStateKeys = 'flinkConfig' | 'sysConfig';
type SettingsState = {
mode: 'inline' | 'horizontal';
selectKey: SettingsStateKeys;
};
type SettingsProps = {
dispatch:any;
};
const Settings: React.FC<SettingsProps> = (props) => {
const menuMap: Record<string, React.ReactNode> = {
flinkConfig: 'Flink 设置',
};
const {dispatch} = props;
const [initConfig, setInitConfig] = useState<SettingsState>({
mode: 'inline',
selectKey: 'flinkConfig',
});
const dom = useRef<HTMLDivElement>();
loadSettings(dispatch);
const resize = () => {
requestAnimationFrame(() => {
if (!dom.current) {
return;
}
let mode: 'inline' | 'horizontal' = 'inline';
const { offsetWidth } = dom.current;
if (dom.current.offsetWidth < 641 && offsetWidth > 400) {
mode = 'horizontal';
}
if (window.innerWidth < 768 && offsetWidth > 400) {
mode = 'horizontal';
}
setInitConfig({ ...initConfig, mode: mode as SettingsState['mode'] });
});
};
useLayoutEffect(() => {
if (dom.current) {
window.addEventListener('resize', resize);
resize();
}
return () => {
window.removeEventListener('resize', resize);
};
}, [dom.current]);
const getMenu = () => {
return Object.keys(menuMap).map((item) => <Item key={item}>{menuMap[item]}</Item>);
};
const renderChildren = () => {
const { selectKey } = initConfig;
switch (selectKey) {
case 'flinkConfig':
return <FlinkConfigView />;
default:
return null;
}
};
return (
<GridContent>
<div
className={styles.main}
ref={(ref) => {
if (ref) {
dom.current = ref;
}
}}
>
<div className={styles.leftMenu}>
<Menu
mode={initConfig.mode}
selectedKeys={[initConfig.selectKey]}
onClick={({ key }) => {
setInitConfig({
...initConfig,
selectKey: key as SettingsStateKeys,
});
}}
>
{getMenu()}
</Menu>
</div>
<div className={styles.right}>
<div className={styles.title}>{menuMap[initConfig.selectKey]}</div>
{renderChildren()}
</div>
</div>
</GridContent>
);
};
export default connect(({Settings}: { Settings: SettingsStateType }) => ({
sqlSubmitJarPath: Settings.sqlSubmitJarPath,
sqlSubmitJarParas: Settings.sqlSubmitJarParas,
sqlSubmitJarMainAppClass: Settings.sqlSubmitJarMainAppClass,
}))(Settings);
import {Effect, Reducer} from "umi";
export type SettingsStateType = {
sqlSubmitJarPath:string,
sqlSubmitJarParas:string,
sqlSubmitJarMainAppClass:string,
};
export type ModelType = {
namespace: string;
state: SettingsStateType;
effects: {
};
reducers: {
saveSettings: Reducer<SettingsStateType>;
};
};
const SettingsModel: ModelType = {
namespace: 'Settings',
state: {
sqlSubmitJarPath:'',
sqlSubmitJarParas:'',
sqlSubmitJarMainAppClass:'',
},
effects: {
},
reducers: {
saveSettings(state, {payload}) {
return {
...state,
...payload,
};
},
},
};
export default SettingsModel;
@import '~antd/es/style/themes/default.less';
.main {
display: flex;
width: 100%;
height: 100%;
padding-top: 16px;
padding-bottom: 16px;
background-color: @menu-bg;
.leftMenu {
width: 224px;
border-right: @border-width-base @border-style-base @border-color-split;
:global {
.ant-menu-inline {
border: none;
}
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
font-weight: bold;
}
}
}
.right {
flex: 1;
padding: 8px 40px;
.title {
margin-bottom: 12px;
color: @heading-color;
font-weight: 500;
font-size: 20px;
line-height: 28px;
}
}
:global {
.ant-list-split .ant-list-item:last-child {
border-bottom: 1px solid @border-color-split;
}
.ant-list-item {
padding-top: 14px;
padding-bottom: 14px;
}
}
}
:global {
.ant-list-item-meta {
// 账号绑定图标
.taobao {
display: block;
color: #ff4000;
font-size: 48px;
line-height: 48px;
border-radius: @border-radius-base;
}
.dingding {
margin: 2px;
padding: 6px;
color: #fff;
font-size: 32px;
line-height: 32px;
background-color: #2eabff;
border-radius: @border-radius-base;
}
.alipay {
color: #2eabff;
font-size: 48px;
line-height: 48px;
border-radius: @border-radius-base;
}
}
// 密码强度
font.strong {
color: @success-color;
}
font.medium {
color: @warning-color;
}
font.weak {
color: @error-color;
}
}
@media screen and (max-width: @screen-md) {
.main {
flex-direction: column;
.leftMenu {
width: 100%;
border: none;
}
.right {
padding: 40px;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment