linyaostalker
5 years ago
31 changed files with 0 additions and 11726 deletions
-
7855backend/web/custom/sku_item.178ab99e60852816d81e.js
-
4backend/web/custom/sku_item.html
-
30backend/web/src/custom-menu/AddItem.js
-
124backend/web/src/custom-menu/Editor.js
-
176backend/web/src/custom-menu/MenuItem.js
-
247backend/web/src/custom-menu/index.js
-
7backend/web/src/custom-menu/index.scss
-
25backend/web/src/custom-menu/utils.js
-
149backend/web/src/dashboard/CircleCard.js
-
135backend/web/src/dashboard/LineChart.js
-
53backend/web/src/dashboard/OverviewCard.js
-
40backend/web/src/dashboard/PieChart.js
-
74backend/web/src/dashboard/RadioGroup.js
-
32backend/web/src/dashboard/Table.js
-
145backend/web/src/dashboard/index.js
-
65backend/web/src/mini-program-management/MainDescription.js
-
132backend/web/src/mini-program-management/ManageTriers.js
-
38backend/web/src/mini-program-management/StepBar.js
-
22backend/web/src/mini-program-management/common.js
-
146backend/web/src/mini-program-management/data.js
-
397backend/web/src/mini-program-management/index.js
-
127backend/web/src/order-detail/Card.js
-
77backend/web/src/order-detail/GoodsCard.js
-
23backend/web/src/order-detail/getOrderStatus.js
-
206backend/web/src/order-detail/index.js
-
171backend/web/src/sku-for-activity/index.js
-
185backend/web/src/spread/Movable/index.js
-
15backend/web/src/spread/Movable/index.scss
-
570backend/web/src/spread/index.js
-
354backend/web/src/spread/normalize.scss
-
102backend/web/src/spread/preview.scss
7855
backend/web/custom/sku_item.178ab99e60852816d81e.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,4 +0,0 @@ |
|||||
<div id="app"></div> |
|
||||
<script> |
|
||||
var csrfToken = "<?= Yii::$app->request->csrfToken ?>"; |
|
||||
</script><script type="text/javascript" src="/custom/sku_item.178ab99e60852816d81e.js"></script> |
|
@ -1,30 +0,0 @@ |
|||||
import React from 'react' |
|
||||
import {Icon} from "antd"; |
|
||||
import styled from "styled-components"; |
|
||||
|
|
||||
export default function AddItem({ subMenu = false, onClick }) { |
|
||||
return ( |
|
||||
<AddItemRoot className={subMenu ? 'sub-menu' : ''} onClick={onClick}> |
|
||||
<Icon type='plus' /> |
|
||||
</AddItemRoot> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
const AddItemRoot = styled.div`
|
|
||||
flex: 1; |
|
||||
display: inline-flex; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
width: 100%; |
|
||||
cursor: default; |
|
||||
&:active { |
|
||||
background: #eee; |
|
||||
} |
|
||||
&.sub-menu { |
|
||||
height: 40px; |
|
||||
border: none; |
|
||||
&:not(:last-child) { |
|
||||
border-bottom: 1px solid #eee; |
|
||||
} |
|
||||
} |
|
||||
`;
|
|
@ -1,124 +0,0 @@ |
|||||
import React, { createContext, useContext } from 'react' |
|
||||
import { Form, Input, Radio, Select } from "antd"; |
|
||||
import styled from "styled-components"; |
|
||||
import { produce } from "immer"; |
|
||||
|
|
||||
const FormItem = Form.Item; |
|
||||
const RadioGroup = Radio.Group; |
|
||||
const Option = Select.Option; |
|
||||
|
|
||||
const { customPageList } = window; |
|
||||
|
|
||||
const MenuContext = createContext(); |
|
||||
const MenuContextProvider = MenuContext.Provider; |
|
||||
|
|
||||
function InputItem({ label, name }) { |
|
||||
const { activeMenu, formItemLayout, onChange } = useContext(MenuContext); |
|
||||
const rawValue = activeMenu.content.value; |
|
||||
const value = name ? (rawValue ? rawValue[name] : '') : rawValue; |
|
||||
|
|
||||
function handleChange(e) { |
|
||||
const newValue = e.target.value; |
|
||||
onChange(name ? { |
|
||||
...rawValue, |
|
||||
[name]: newValue |
|
||||
} : newValue); |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<FormItem label={label} {...formItemLayout}> |
|
||||
<Input |
|
||||
value={value} |
|
||||
placeholder={'请输入' + label} |
|
||||
onChange={handleChange} |
|
||||
/> |
|
||||
</FormItem> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
export default function Editor({ activeMenu, onChange }) { |
|
||||
function changeType(e) { |
|
||||
onChange('content', produce(activeMenu.content, draft => { |
|
||||
draft.type = e.target.value; |
|
||||
draft.value = null; |
|
||||
})) |
|
||||
} |
|
||||
|
|
||||
function changeValue(newValue) { |
|
||||
onChange('content', { |
|
||||
type: activeMenu.content.type, |
|
||||
value: newValue |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
const formItemLayout = { |
|
||||
labelCol: {span: 10}, |
|
||||
wrapperCol: {span: 14}, |
|
||||
}; |
|
||||
|
|
||||
const typeTable = { |
|
||||
view: { |
|
||||
label: '跳转网页', |
|
||||
content: <InputItem label='网页链接' /> |
|
||||
}, |
|
||||
customPage: { |
|
||||
label: '自定义页面', |
|
||||
content: ( |
|
||||
<FormItem label='自定义页面' {...formItemLayout}> |
|
||||
<Select defaultValue={activeMenu.content.value} onChange={changeValue}> |
|
||||
{customPageList.map(({id, title}) => <Option key={id} value={id.toString()}>{title}</Option>)} |
|
||||
</Select> |
|
||||
</FormItem> |
|
||||
) |
|
||||
}, |
|
||||
miniprogram: { |
|
||||
label: '跳转小程序', |
|
||||
content: ( |
|
||||
<> |
|
||||
<InputItem label='小程序 AppId' name='appId' /> |
|
||||
<InputItem label='小程序页面链接' name='url' /> |
|
||||
<InputItem label='备用网页链接' name='spareWebUrl' /> |
|
||||
</> |
|
||||
) |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
return ( |
|
||||
<Root> |
|
||||
<Form> |
|
||||
<FormItem label='菜单名称' {...formItemLayout}> |
|
||||
<Input |
|
||||
value={activeMenu.title} |
|
||||
onInput={e => onChange('title', e.target.value)} |
|
||||
/> |
|
||||
</FormItem> |
|
||||
{(!activeMenu.children || activeMenu.children.length === 0) && ( |
|
||||
<ContentValue> |
|
||||
<FormItem label='菜单内容' {...formItemLayout}> |
|
||||
<RadioGroup value={activeMenu.content.type} onChange={changeType}> |
|
||||
{Object.keys(typeTable).map(name => ( |
|
||||
<Radio key={name} value={name}>{typeTable[name].label}</Radio> |
|
||||
))} |
|
||||
</RadioGroup> |
|
||||
</FormItem> |
|
||||
{activeMenu.content.type && ( |
|
||||
<MenuContextProvider value={{ activeMenu, onChange: changeValue, formItemLayout }}> |
|
||||
{typeTable[activeMenu.content.type].content} |
|
||||
</MenuContextProvider> |
|
||||
)} |
|
||||
</ContentValue> |
|
||||
)} |
|
||||
</Form> |
|
||||
</Root> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
const Root = styled.div`
|
|
||||
margin-left: 20px; |
|
||||
padding: 20px; |
|
||||
background: #fff; |
|
||||
`;
|
|
||||
|
|
||||
const ContentValue = styled.div`
|
|
||||
|
|
||||
`;
|
|
@ -1,176 +0,0 @@ |
|||||
import React, { useState } from 'react' |
|
||||
import { Button, Popover } from "antd"; |
|
||||
import styled from "styled-components"; |
|
||||
import ClassNames from 'classnames' |
|
||||
import AddItem from './AddItem' |
|
||||
import { DragDropContext, Draggable, Droppable} from "react-beautiful-dnd"; |
|
||||
|
|
||||
function BaseMenuItem({ title, active, onClick, onDelete, children, provided, snapshot }) { |
|
||||
const providedProps = provided ? { |
|
||||
ref: provided.innerRef, |
|
||||
...provided.draggableProps, |
|
||||
...provided.dragHandleProps |
|
||||
} : {}; |
|
||||
|
|
||||
return ( |
|
||||
<MenuItemRoot |
|
||||
{...providedProps} |
|
||||
className={ClassNames({ |
|
||||
active, |
|
||||
dragging: snapshot.isDragging |
|
||||
})} |
|
||||
onClick={onClick} |
|
||||
> |
|
||||
<MenuItemWrapper> |
|
||||
<MenuItemText>{title}</MenuItemText> |
|
||||
<DeleteButton |
|
||||
type='danger' |
|
||||
shape='circle' |
|
||||
size='small' |
|
||||
icon='close' |
|
||||
onClick={e => { |
|
||||
e.stopPropagation(); |
|
||||
onDelete(e); |
|
||||
}} |
|
||||
/> |
|
||||
{children} |
|
||||
</MenuItemWrapper> |
|
||||
</MenuItemRoot> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
export default function MenuItem( |
|
||||
{ |
|
||||
title, children, active, activeSubMenuId, showSubMenu, provided, snapshot, onActivate, |
|
||||
onAddSubMenu, onDeleteSubMenu, onReorderSubMenu, onDelete |
|
||||
}) { |
|
||||
const content = ( |
|
||||
<> |
|
||||
<DragDropContext |
|
||||
onDragStart={({ draggableId }) => onActivate(draggableId)} |
|
||||
onDragEnd={result => onReorderSubMenu(result)} |
|
||||
> |
|
||||
<Droppable droppableId="droppable"> |
|
||||
{provided => ( |
|
||||
<div |
|
||||
{...provided.droppableProps} |
|
||||
ref={provided.innerRef} |
|
||||
> |
|
||||
{children && children.map(({id, title}, subIndex) => ( |
|
||||
<Draggable key={id} draggableId={id} index={subIndex}> |
|
||||
{(provided, snapshot) => ( |
|
||||
<SubMenuItem |
|
||||
ref={provided.innerRef} |
|
||||
{...provided.draggableProps} |
|
||||
{...provided.dragHandleProps} |
|
||||
> |
|
||||
<BaseMenuItem |
|
||||
title={title} |
|
||||
active={activeSubMenuId === id} |
|
||||
snapshot={snapshot} |
|
||||
onClick={() => onActivate(id)} |
|
||||
onDelete={() => onDeleteSubMenu(subIndex)} |
|
||||
/> |
|
||||
</SubMenuItem> |
|
||||
)} |
|
||||
</Draggable> |
|
||||
))} |
|
||||
{provided.placeholder} |
|
||||
</div> |
|
||||
)} |
|
||||
</Droppable> |
|
||||
</DragDropContext> |
|
||||
{(!children || children.length < 5) && <AddItem subMenu onClick={onAddSubMenu}/>} |
|
||||
</> |
|
||||
); |
|
||||
|
|
||||
return ( |
|
||||
<Popover |
|
||||
trigger='click' |
|
||||
content={content} |
|
||||
overlayStyle={{width: '100px', padding: '0'}} |
|
||||
visible={showSubMenu} |
|
||||
> |
|
||||
<BaseMenuItem |
|
||||
provided={provided} |
|
||||
snapshot={snapshot} |
|
||||
title={title} |
|
||||
active={active} |
|
||||
onClick={() => onActivate()} |
|
||||
onDelete={onDelete} |
|
||||
> |
|
||||
{(children && children.length > 0) && <SpreadSymbol/>} |
|
||||
</BaseMenuItem> |
|
||||
</Popover> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
const DeleteButton = styled(Button)`
|
|
||||
position: absolute; |
|
||||
right: 0; |
|
||||
top: 0; |
|
||||
transform: translate(50%, -50%); |
|
||||
box-shadow: 0 0 10px 0 rgba(0 0 0 .1); |
|
||||
visibility: hidden; |
|
||||
z-index: 2000; |
|
||||
`;
|
|
||||
|
|
||||
const fontSizeRatio = 0.03; |
|
||||
const MenuItemText = styled.div`
|
|
||||
padding: 0 5px; |
|
||||
white-space: nowrap; |
|
||||
overflow: hidden; |
|
||||
text-overflow: ellipsis; |
|
||||
font-size: ${70 * fontSizeRatio}vh; |
|
||||
`;
|
|
||||
|
|
||||
const SubMenuItem = styled.div`
|
|
||||
display: flex; |
|
||||
width: 100%; |
|
||||
height: 40px; |
|
||||
border-bottom: 1px solid #eee; |
|
||||
`;
|
|
||||
|
|
||||
const MenuItemWrapper = styled.div`
|
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
`;
|
|
||||
|
|
||||
const triangleSize = 8; |
|
||||
const spreadOffset = 3; |
|
||||
const SpreadSymbol = styled.div`
|
|
||||
position: absolute; |
|
||||
right: ${spreadOffset}px; |
|
||||
bottom: ${spreadOffset}px; |
|
||||
width: 0; |
|
||||
height: 0; |
|
||||
border-top: ${triangleSize}px solid transparent; |
|
||||
border-right: ${triangleSize}px solid #ddd; |
|
||||
`;
|
|
||||
|
|
||||
const MenuItemRoot = styled.div`
|
|
||||
align-items: stretch; |
|
||||
position: relative; |
|
||||
flex: 1; |
|
||||
padding: 5px 0; |
|
||||
cursor: default; |
|
||||
background: #fff; |
|
||||
border: 2px solid transparent; |
|
||||
width: 0; |
|
||||
transition: box-shadow .1s; |
|
||||
&.active { |
|
||||
border-color: #09bb07; |
|
||||
} |
|
||||
&.dragging { |
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, .1); |
|
||||
} |
|
||||
&:not(:last-child) ${MenuItemWrapper} { |
|
||||
border-right: 1px solid #eee; |
|
||||
} |
|
||||
&:hover ${DeleteButton} { |
|
||||
visibility: visible; |
|
||||
} |
|
||||
`;
|
|
@ -1,247 +0,0 @@ |
|||||
import React, { useState } from 'react' |
|
||||
import ReactDOM from 'react-dom' |
|
||||
import styled from 'styled-components' |
|
||||
import { Button, Icon, message } from 'antd' |
|
||||
import { produce } from 'immer' |
|
||||
import axios from 'axios' |
|
||||
import { DragDropContext, Draggable, Droppable} from "react-beautiful-dnd"; |
|
||||
import './index.scss' |
|
||||
import Editor from './Editor' |
|
||||
import AddItem from './AddItem' |
|
||||
import MenuItem from './MenuItem' |
|
||||
import '../utils/ajax' |
|
||||
import { handleDragEnd } from './utils' |
|
||||
import uuid from 'uuid' |
|
||||
|
|
||||
const IconFont = Icon.createFromIconfontCN({ |
|
||||
scriptUrl: '//at.alicdn.com/t/font_827976_5ojdd8bmdsw.js', |
|
||||
}); |
|
||||
|
|
||||
const { data: initMenuList = [] } = window; |
|
||||
|
|
||||
function getActiveMenu(menuList, activeMenuInfo) { |
|
||||
const activeParent = menuList.find(menu => menu.id === activeMenuInfo.id); |
|
||||
return (activeParent && activeMenuInfo.subId !== null && activeParent.children.length > 0) ? |
|
||||
activeParent.children.find(child => child.id === activeMenuInfo.subId) : activeParent; |
|
||||
} |
|
||||
|
|
||||
function App() { |
|
||||
const [menuList, setMenuList] = useState(initMenuList); |
|
||||
const [activeMenuInfo, setActiveMenuInfo] = useState({ id: null, subId: null }); |
|
||||
|
|
||||
const activeMenu = getActiveMenu(menuList, activeMenuInfo); |
|
||||
|
|
||||
function addMenu() { |
|
||||
const id = uuid(); |
|
||||
|
|
||||
setMenuList([ |
|
||||
...menuList, |
|
||||
{ |
|
||||
id, |
|
||||
title: '菜单名称', |
|
||||
content: { |
|
||||
type: null, |
|
||||
value: null |
|
||||
}, |
|
||||
children: [] |
|
||||
} |
|
||||
]); |
|
||||
|
|
||||
setActiveMenuInfo({ |
|
||||
id, |
|
||||
subId: null |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
function deleteMenu(index) { |
|
||||
setMenuList(produce(menuList, (draft => { |
|
||||
draft.splice(index, 1); |
|
||||
}))); |
|
||||
} |
|
||||
|
|
||||
function deleteSubMenu(index, subIndex) { |
|
||||
setMenuList(produce(menuList, (draft => { |
|
||||
draft[index].children.splice(subIndex, 1); |
|
||||
}))); |
|
||||
} |
|
||||
|
|
||||
function addSubMenu(index) { |
|
||||
const id = uuid(); |
|
||||
|
|
||||
setMenuList(produce(menuList, (draft => { |
|
||||
draft[index].children.push({ |
|
||||
id, |
|
||||
title: '菜单名称', |
|
||||
content: { |
|
||||
type: null, |
|
||||
value: null |
|
||||
} |
|
||||
}) |
|
||||
}))); |
|
||||
setActiveMenuInfo({ |
|
||||
id: menuList[index].id, |
|
||||
subId: id |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
function changeForm(name, value) { |
|
||||
setMenuList(produce(menuList, (draft => { |
|
||||
const activeMenu = getActiveMenu(draft, activeMenuInfo); |
|
||||
activeMenu[name] = value; |
|
||||
}))); |
|
||||
} |
|
||||
|
|
||||
function checkData() { |
|
||||
return menuList.every(menu => { |
|
||||
if (menu.children && menu.children.length > 0) { |
|
||||
return menu.title && menu.children.every(item => { |
|
||||
if (item.content.type === 'weapp') { |
|
||||
const { appId, url, spareWebUrl } = item.content.value; |
|
||||
return item.title && appId && url && spareWebUrl |
|
||||
} else { |
|
||||
return item.title && item.content.type && item.content.value |
|
||||
} |
|
||||
}) |
|
||||
} else { |
|
||||
return menu.title && menu.content.type && menu.content.value |
|
||||
} |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
async function submit() { |
|
||||
if (checkData()) { |
|
||||
await axios.post('', { |
|
||||
data: menuList |
|
||||
}); |
|
||||
message.success('保存成功'); |
|
||||
} else { |
|
||||
message.error('数据填写不完整,请检查是否有数据漏填') |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function reorderSubMenu(index, result) { |
|
||||
handleDragEnd(menuList[index].children, result, newSubMenu => { |
|
||||
setMenuList(produce(menuList, draft => { |
|
||||
draft[index].children = newSubMenu; |
|
||||
})) |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<Root> |
|
||||
<Previewer> |
|
||||
<TopBar src='/img/wechat-top.png' /> |
|
||||
<BottomBar> |
|
||||
<KeyBoardIcon> |
|
||||
<IconFont |
|
||||
type='fa-keyboard' |
|
||||
style={{ |
|
||||
fontSize: '1.3em', |
|
||||
padding: '0 10px', |
|
||||
alignSelf: 'center', |
|
||||
borderRight: '1px solid #eee' |
|
||||
}} |
|
||||
/> |
|
||||
</KeyBoardIcon> |
|
||||
<MenuWrapper> |
|
||||
<DragDropContext |
|
||||
onDragStart={({ draggableId }) => setActiveMenuInfo({ id: draggableId, subId: null })} |
|
||||
onDragEnd={result => handleDragEnd(menuList, result, setMenuList)} |
|
||||
> |
|
||||
<Droppable droppableId='droppable' direction='horizontal'> |
|
||||
{provided => ( |
|
||||
<MenuGroup |
|
||||
{...provided.droppableProps} |
|
||||
ref={provided.innerRef} |
|
||||
length={menuList.length} |
|
||||
> |
|
||||
{menuList.map((menu, index) => ( |
|
||||
<Draggable key={menu.id} draggableId={menu.id} index={index}> |
|
||||
{(provided, snapshot) => ( |
|
||||
<MenuItem |
|
||||
key={menu.id} |
|
||||
{...menu} |
|
||||
provided={provided} |
|
||||
snapshot={snapshot} |
|
||||
showSubMenu={activeMenuInfo.id === menu.id} |
|
||||
active={activeMenuInfo.id === menu.id && activeMenuInfo.subId === null} |
|
||||
activeSubMenuId={activeMenuInfo.id === menu.id && activeMenuInfo.subId} |
|
||||
onAddSubMenu={() => addSubMenu(index)} |
|
||||
onActivate={(subId = null) => setActiveMenuInfo({ id: menu.id, subId })} |
|
||||
onDelete={() => deleteMenu(index)} |
|
||||
onDeleteSubMenu={(subIndex) => deleteSubMenu(index, subIndex)} |
|
||||
onReorderSubMenu={result => reorderSubMenu(index, result)} |
|
||||
/> |
|
||||
)} |
|
||||
</Draggable> |
|
||||
))} |
|
||||
{provided.placeholder} |
|
||||
</MenuGroup> |
|
||||
)} |
|
||||
</Droppable> |
|
||||
</DragDropContext> |
|
||||
{menuList.length < 3 && <AddItem onClick={addMenu} />} |
|
||||
</MenuWrapper> |
|
||||
</BottomBar> |
|
||||
</Previewer> |
|
||||
{activeMenu && <Editor activeMenu={activeMenu} onChange={changeForm} />} |
|
||||
<SubmitButton type='primary' shape='circle' icon='check' size='large' onClick={submit} /> |
|
||||
</Root> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
const Root = styled.div`
|
|
||||
display: flex; |
|
||||
`;
|
|
||||
|
|
||||
const TopBar = styled.img`
|
|
||||
width: 100%; |
|
||||
height: auto; |
|
||||
`;
|
|
||||
|
|
||||
const BottomBar = styled.div`
|
|
||||
display: flex; |
|
||||
width: 100%; |
|
||||
height: 35px; |
|
||||
background: #fff; |
|
||||
`;
|
|
||||
|
|
||||
const KeyBoardIcon = styled.div`
|
|
||||
padding: 10px 0; |
|
||||
`;
|
|
||||
|
|
||||
const height = 70; |
|
||||
const ratio = 16 / 9; |
|
||||
|
|
||||
const Previewer = styled.div`
|
|
||||
flex-shrink: 0; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: space-between; |
|
||||
width: ${height / ratio}vh; |
|
||||
height: ${height}vh; |
|
||||
background: #eee; |
|
||||
border: 1px solid #eee; |
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, .1); |
|
||||
`;
|
|
||||
|
|
||||
const SubmitButton = styled(Button)`
|
|
||||
position: fixed; |
|
||||
width: 60px!important; |
|
||||
height: 60px!important; |
|
||||
font-size: 1.8em!important; |
|
||||
bottom: 40px; |
|
||||
right: 40px; |
|
||||
`;
|
|
||||
|
|
||||
const MenuWrapper = styled.div`
|
|
||||
flex-grow: 1; |
|
||||
display: flex; |
|
||||
width: 0; |
|
||||
`;
|
|
||||
|
|
||||
const MenuGroup = styled(MenuWrapper)`
|
|
||||
flex-grow: ${({ length }) => length}; |
|
||||
`;
|
|
||||
|
|
||||
ReactDOM.render(<App />, document.getElementById('app')); |
|
@ -1,7 +0,0 @@ |
|||||
.ant-popover-inner-content { |
|
||||
padding: 0!important; |
|
||||
} |
|
||||
|
|
||||
.ant-popover-arrow { |
|
||||
display: none!important; |
|
||||
} |
|
@ -1,25 +0,0 @@ |
|||||
export const reorder = (list, startIndex, endIndex) => { |
|
||||
const result = Array.from(list); |
|
||||
const [removed] = result.splice(startIndex, 1); |
|
||||
result.splice(endIndex, 0, removed); |
|
||||
|
|
||||
return result; |
|
||||
}; |
|
||||
|
|
||||
export function handleDragEnd(list, result, setList) { |
|
||||
if (!result.destination) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (result.destination.index === result.source.index) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
const newList = reorder( |
|
||||
list, |
|
||||
result.source.index, |
|
||||
result.destination.index |
|
||||
); |
|
||||
|
|
||||
setList(newList); |
|
||||
} |
|
@ -1,149 +0,0 @@ |
|||||
import Circle from 'react-circle'; |
|
||||
import React from "react"; |
|
||||
import PropTypes from "prop-types"; |
|
||||
import styled from 'styled-components' |
|
||||
import ClassNames from 'classnames' |
|
||||
import {Icon} from 'antd' |
|
||||
import NumberEasing from "che-react-number-easing" |
|
||||
|
|
||||
export default class RadioGroup extends React.Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
this.state = { |
|
||||
percentForDisplay: 0, |
|
||||
growthForDisplay: 0, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static propTypes = { |
|
||||
title: PropTypes.string, |
|
||||
percent: PropTypes.number, |
|
||||
growth: PropTypes.number, |
|
||||
color: PropTypes.string, |
|
||||
animationDuration: PropTypes.number |
|
||||
}; |
|
||||
|
|
||||
static defaultProps = { |
|
||||
title: '', |
|
||||
percent: 0, |
|
||||
growth: 0, |
|
||||
color: '#312c50', |
|
||||
animationDuration: 1500 |
|
||||
}; |
|
||||
|
|
||||
componentDidMount() { |
|
||||
const {percent, growth} = this.props; |
|
||||
|
|
||||
function fixDigits(number, fractionDigits) { |
|
||||
return Number((number * 100).toFixed(fractionDigits)) |
|
||||
} |
|
||||
|
|
||||
this.setState({ |
|
||||
percentForDisplay: fixDigits(percent, 2), |
|
||||
growthForDisplay: fixDigits(growth, 2) |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const {percentForDisplay, growthForDisplay} = this.state; |
|
||||
const {title, percent, growth, color, animationDuration} = this.props; |
|
||||
|
|
||||
return ( |
|
||||
<Root> |
|
||||
<Title>{title}</Title> |
|
||||
<CircleWrapper> |
|
||||
<Circle |
|
||||
animate={true} |
|
||||
animationDuration={`${animationDuration / 1000}s`} |
|
||||
progress={percentForDisplay} |
|
||||
progressColor={color} |
|
||||
textColor={color} |
|
||||
size={120} |
|
||||
textStyle={{ |
|
||||
font: 'bold 7rem Helvetica, Arial, sans-serif' |
|
||||
}} |
|
||||
roundedStroke={true} |
|
||||
showPercentage={false} |
|
||||
/> |
|
||||
<Percentage> |
|
||||
{percent > 0 ? ( |
|
||||
<> |
|
||||
<NumberEasing |
|
||||
value={percentForDisplay} |
|
||||
speed={animationDuration} |
|
||||
precision={2} |
|
||||
/> |
|
||||
<span>%</span> |
|
||||
</> |
|
||||
) : <div>N/A</div>} |
|
||||
</Percentage> |
|
||||
</CircleWrapper> |
|
||||
{(growth !== 0 && growth > 0) && ( |
|
||||
<Growth |
|
||||
className={ClassNames({ |
|
||||
up: growth > 0, |
|
||||
zero: growth === 0, |
|
||||
down: growth < 0 |
|
||||
})} |
|
||||
title={`同比${growth > 0 ? '增长' : '下降'} ${Math.abs(growth.toFixed(4) * 100)}%`} |
|
||||
> |
|
||||
<Icon type={'arrow-' + (growth > 0 ? 'up' : 'down')} /> |
|
||||
<NumberEasing |
|
||||
value={Math.abs(growthForDisplay)} |
|
||||
speed={animationDuration} |
|
||||
precision={2} |
|
||||
/> |
|
||||
<span>%</span> |
|
||||
</Growth> |
|
||||
)} |
|
||||
</Root> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const Root = styled.div`
|
|
||||
height: 200px; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: space-around; |
|
||||
align-items: center; |
|
||||
border-radius: 10px; |
|
||||
padding: 20px; |
|
||||
background: #fff; |
|
||||
box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.1)!important; |
|
||||
`;
|
|
||||
|
|
||||
const Title = styled.div`
|
|
||||
font-weight: bold; |
|
||||
font-size: 1em; |
|
||||
`;
|
|
||||
|
|
||||
const Growth = styled.div`
|
|
||||
font-weight: bold; |
|
||||
&.up { |
|
||||
color: #3fbe67; |
|
||||
} |
|
||||
&.zero { |
|
||||
color: #666; |
|
||||
} |
|
||||
&.down { |
|
||||
color: #ee4d48; |
|
||||
} |
|
||||
.anticon { |
|
||||
margin-right: 5px; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
const CircleWrapper = styled.div`
|
|
||||
position: relative; |
|
||||
`;
|
|
||||
|
|
||||
const Percentage = styled.div`
|
|
||||
position: absolute; |
|
||||
left: 50%; |
|
||||
top: 50%; |
|
||||
transform: translate(-50%, -50%); |
|
||||
font-size: 1.5em; |
|
||||
font-weight: bold; |
|
||||
color: ${(props) => props.color}; |
|
||||
`;
|
|
@ -1,135 +0,0 @@ |
|||||
import React from "react"; |
|
||||
import PropTypes from "prop-types"; |
|
||||
import { Chart, Geom, Axis, Tooltip, Legend, Coord } from 'bizcharts'; |
|
||||
import styled from 'styled-components' |
|
||||
import RadioGroup from './RadioGroup' |
|
||||
import {setLightness} from 'polished' |
|
||||
|
|
||||
export default class LineChart extends React.Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
this.state = { |
|
||||
checkedIndex: 0 |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static propTypes = { |
|
||||
data: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, |
|
||||
label: PropTypes.string.isRequired, |
|
||||
color: PropTypes.string, |
|
||||
title: PropTypes.string, |
|
||||
labelY: PropTypes.string, |
|
||||
prefix: PropTypes.string, |
|
||||
unit: PropTypes.string, |
|
||||
dateCount: PropTypes.number, |
|
||||
type: PropTypes.oneOf(['integer']) |
|
||||
}; |
|
||||
|
|
||||
static defaultProps = { |
|
||||
data: [], |
|
||||
label: '', |
|
||||
color: '#5d58e9', |
|
||||
title: '', |
|
||||
labelY: '', |
|
||||
prefix: '', |
|
||||
unit: '', |
|
||||
dateCount: 7, |
|
||||
type: 'integer' |
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
const {checkedIndex} = this.state; |
|
||||
const {data, label, prefix, unit, title, color, type, dateCount} = this.props; |
|
||||
const currentData = data[checkedIndex]; |
|
||||
const position = `${currentData.key || 'date'}*value`; |
|
||||
const isLittle = !currentData.data.find(i => i.value > 5); |
|
||||
|
|
||||
// 定义度量
|
|
||||
const cols = { |
|
||||
[data[checkedIndex].key || 'date']: { |
|
||||
alias: '日期', |
|
||||
tickCount: dateCount |
|
||||
}, |
|
||||
value: { |
|
||||
alias: label, |
|
||||
type: 'linear', |
|
||||
formatter: value => `${prefix}${value}${unit}`, |
|
||||
tickInterval: type === 'integer' && isLittle ? 1 : undefined |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
return ( |
|
||||
<div className="line-chart"> |
|
||||
<Header> |
|
||||
<h1> |
|
||||
{title.replace(/<days>/g, data[checkedIndex].data.length) |
|
||||
.replace(/<label>/g, data[checkedIndex].label)} |
|
||||
</h1> |
|
||||
{data.length > 1 && ( |
|
||||
<RadioGroup |
|
||||
current={checkedIndex} |
|
||||
items={data.map(i => i.label)} |
|
||||
color={color} |
|
||||
onChange={(newValue) => { |
|
||||
this.setState({checkedIndex: newValue}) |
|
||||
}} |
|
||||
/> |
|
||||
)} |
|
||||
</Header> |
|
||||
<div className="content"> |
|
||||
<Chart |
|
||||
forceFit |
|
||||
height={300} |
|
||||
padding='auto' |
|
||||
data={data[checkedIndex].data} |
|
||||
scale={cols} |
|
||||
> |
|
||||
<Axis name="date" /> |
|
||||
<Axis name="value" /> |
|
||||
<Tooltip /> |
|
||||
<Geom |
|
||||
type="line" |
|
||||
shape="smooth" |
|
||||
position={position} |
|
||||
color={color} |
|
||||
animate={{ |
|
||||
appear: { |
|
||||
animation: 'clipIn', |
|
||||
easing: 'easePolyIn', |
|
||||
duration: 1500, |
|
||||
delay: 0 |
|
||||
} |
|
||||
}} |
|
||||
/> |
|
||||
<Geom |
|
||||
type="area" |
|
||||
shape="smooth" |
|
||||
position={position} |
|
||||
color={`l (90) 0:${setLightness(.85, color)} 0.7:${setLightness(.9, color)} 1:#ffffff`} |
|
||||
tooltip={false} |
|
||||
animate={{ |
|
||||
appear: { |
|
||||
animation: 'clipIn', |
|
||||
easing: 'easePolyIn', |
|
||||
duration: 1500, |
|
||||
delay: 0 |
|
||||
} |
|
||||
}} |
|
||||
/> |
|
||||
</Chart> |
|
||||
</div> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const Header = styled.div`
|
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
margin: 10px 0 20px 0; |
|
||||
h1 { |
|
||||
flex-grow: 1; |
|
||||
line-height: 1; |
|
||||
margin: 0; |
|
||||
} |
|
||||
`;
|
|
@ -1,53 +0,0 @@ |
|||||
import React from "react"; |
|
||||
import PropTypes from "prop-types"; |
|
||||
import NumberEasing from 'che-react-number-easing'; |
|
||||
|
|
||||
export default class OverviewCard extends React.Component{ |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
this.state = { |
|
||||
number: 0 |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static propTypes = { |
|
||||
title: PropTypes.string.isRequired, |
|
||||
number: PropTypes.oneOfType([ |
|
||||
PropTypes.number, |
|
||||
PropTypes.string |
|
||||
]).isRequired, |
|
||||
image: PropTypes.string, |
|
||||
isPrice: PropTypes.bool |
|
||||
}; |
|
||||
|
|
||||
static defaultProps = { |
|
||||
isPrice: false |
|
||||
}; |
|
||||
|
|
||||
|
|
||||
componentDidMount() { |
|
||||
this.setState({ |
|
||||
number: parseFloat(this.props.number).toFixed(2) |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const {number} = this.state; |
|
||||
const {image, title, isPrice} = this.props; |
|
||||
return ( |
|
||||
<div className='overview-card'> |
|
||||
<div className="main"> |
|
||||
<h1>{title}</h1> |
|
||||
<p className='number'>{isPrice && '¥'} |
|
||||
<NumberEasing |
|
||||
value={number} |
|
||||
speed={1500} |
|
||||
precision={isPrice ? 2 : 0} |
|
||||
/> |
|
||||
</p> |
|
||||
</div> |
|
||||
<img src={image} alt="text"/> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
} |
|
@ -1,40 +0,0 @@ |
|||||
// var myPieChart = new Chart(ctxTotalPie, {
|
|
||||
// type: 'pie',
|
|
||||
// data: {
|
|
||||
// datasets: [{
|
|
||||
// data: [
|
|
||||
// data.total.finish,
|
|
||||
// data.total.refund,
|
|
||||
// data.total.shipping,
|
|
||||
// data.total.unpaied,
|
|
||||
// data.total.unshipping
|
|
||||
// ],
|
|
||||
// backgroundColor: [
|
|
||||
// 'rgba(255, 87, 51, 1)',
|
|
||||
// 'rgba(95, 108, 160, 1)',
|
|
||||
// 'rgba(255, 206, 86, 1)',
|
|
||||
// 'rgba(75, 192, 192, 1)',
|
|
||||
// 'rgba(153, 102, 255, 1)',
|
|
||||
// 'rgba(255, 159, 64, 1)'
|
|
||||
// ],
|
|
||||
// // borderColor: [
|
|
||||
// // 'rgba(95, 108, 160, 1)',
|
|
||||
// // 'rgba(54, 162, 235, 1)',
|
|
||||
// // 'rgba(255, 206, 86, 1)',
|
|
||||
// // 'rgba(75, 192, 192, 1)',
|
|
||||
// // 'rgba(153, 102, 255, 1)',
|
|
||||
// // 'rgba(255, 159, 64, 1)'
|
|
||||
// // ]
|
|
||||
// }],
|
|
||||
//
|
|
||||
// // These labels appear in the legend and in the tooltips when hovering different arcs
|
|
||||
// labels: [
|
|
||||
// '已完成',
|
|
||||
// '申请退款中',
|
|
||||
// '待收货',
|
|
||||
// '未付款',
|
|
||||
// '待发货'
|
|
||||
// ]
|
|
||||
// },
|
|
||||
// options: {}
|
|
||||
// });
|
|
@ -1,74 +0,0 @@ |
|||||
import React from "react"; |
|
||||
import PropTypes from "prop-types"; |
|
||||
import styled from 'styled-components' |
|
||||
import {setLightness} from 'polished' |
|
||||
|
|
||||
export default class RadioGroup extends React.Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
this.state = { |
|
||||
number: 0 |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static propTypes = { |
|
||||
current: PropTypes.number, |
|
||||
items: PropTypes.arrayOf(PropTypes.string).isRequired, |
|
||||
color: PropTypes.string, |
|
||||
onChange: PropTypes.func |
|
||||
}; |
|
||||
|
|
||||
static defaultProps = { |
|
||||
current: '', |
|
||||
items: [], |
|
||||
onChange: () => { |
|
||||
}, |
|
||||
color: '#5d58e9' |
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
const {current, items, color, onChange} = this.props; |
|
||||
|
|
||||
return ( |
|
||||
<Root> |
|
||||
{items.map((item, index) => ( |
|
||||
<Item |
|
||||
key={index} |
|
||||
color={color} |
|
||||
className={current === index ? 'active' : ''} |
|
||||
onClick={() => { |
|
||||
current !== item && onChange(index) |
|
||||
}} |
|
||||
>{item}</Item> |
|
||||
))} |
|
||||
</Root> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const Root = styled.div`
|
|
||||
display: flex; |
|
||||
flex-wrap: wrap; |
|
||||
align-items: center; |
|
||||
`;
|
|
||||
|
|
||||
const Item = styled.div`
|
|
||||
margin: 5px; |
|
||||
padding: 2px 10px; |
|
||||
font-size: .8em; |
|
||||
border-radius: 40px; |
|
||||
border: 2px solid ${(props) => props.color}; |
|
||||
color: ${(props) => props.color}; |
|
||||
background: #fff; |
|
||||
transition: all .2s; |
|
||||
white-space: nowrap; |
|
||||
cursor: pointer; |
|
||||
&:hover { |
|
||||
background: ${(props) => setLightness(.9, props.color)}; |
|
||||
} |
|
||||
&.active { |
|
||||
background: ${(props) => props.color}; |
|
||||
color: #fff; |
|
||||
cursor: default; |
|
||||
} |
|
||||
`;
|
|
@ -1,32 +0,0 @@ |
|||||
import React from "react"; |
|
||||
import PropTypes from "prop-types"; |
|
||||
import styled from "styled-components"; |
|
||||
|
|
||||
export default class Table extends React.Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
this.state = { |
|
||||
|
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
static defaultProps = { |
|
||||
|
|
||||
}; |
|
||||
|
|
||||
static propTypes = { |
|
||||
|
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
return ( |
|
||||
<Root> |
|
||||
|
|
||||
</Root> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const Root = styled.div`
|
|
||||
|
|
||||
`;
|
|
@ -1,145 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
import ReactDOM from 'react-dom'; |
|
||||
import styled from 'styled-components' |
|
||||
import CircleCard from './CircleCard'; |
|
||||
import LineChart from './LineChart' |
|
||||
import OverviewCard from './OverviewCard.js' |
|
||||
import '../styles/dashboard.scss' |
|
||||
import 'rc-switch/assets/index.css' |
|
||||
|
|
||||
const {total, sales, monthlySales, users, topGoods, topOrders, DAU, |
|
||||
dailyConversionRate, monthlyConversionRate} = data; |
|
||||
const {salesAmount, salesCount, orderCount, goodsCount, userCount} = total; |
|
||||
const fixedSales = sales.map(i => ({ |
|
||||
...i, |
|
||||
value: Number.parseFloat(i.value).toFixed(2) |
|
||||
})); |
|
||||
|
|
||||
const fixedonthlySales = monthlySales.map(i => ({ |
|
||||
...i, |
|
||||
value: Number.parseFloat(i.value).toFixed(2) |
|
||||
})); |
|
||||
|
|
||||
const salesData = [ |
|
||||
{ |
|
||||
label: '30天', |
|
||||
data: fixedSales |
|
||||
}, |
|
||||
{ |
|
||||
label: '15天', |
|
||||
data: fixedSales.slice(15), |
|
||||
}, |
|
||||
{ |
|
||||
label: '12个月', |
|
||||
data: fixedonthlySales.reverse(), |
|
||||
key: 'month' |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
const topGoodsData = [ |
|
||||
{ |
|
||||
label: '销量最高', |
|
||||
highlightCol: 1, |
|
||||
data: topGoods.count.map(i => ({ |
|
||||
'商品名称': i.name, |
|
||||
'销量': i.count, |
|
||||
'销售额': i.sales |
|
||||
})) |
|
||||
}, |
|
||||
{ |
|
||||
label: '销售额最高', |
|
||||
highlightCol: 2, |
|
||||
data: data.topGoods.amount.map(i => ({ |
|
||||
'商品名称': i.name, |
|
||||
'销量': i.count, |
|
||||
'销售额': i.sales |
|
||||
})) |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
|
|
||||
const CirclePanel = styled.div`
|
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: space-between; |
|
||||
align-items: center; |
|
||||
height: 420px; |
|
||||
`;
|
|
||||
|
|
||||
const dashboard = ( |
|
||||
<div> |
|
||||
<div className="row"> |
|
||||
<div className="total-display"> |
|
||||
<OverviewCard image='/img/money.png' title='总营业额' number={salesAmount} isPrice /> |
|
||||
<OverviewCard image='/img/sales.png' title='总销量' number={salesCount} /> |
|
||||
<OverviewCard image='/img/order.png' title='订单数量' number={orderCount} /> |
|
||||
<OverviewCard image='/img/goods.png' title='商品数量' number={goodsCount} /> |
|
||||
<OverviewCard image='/img/user.png' title='用户数量' number={userCount} /> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div className="row"> |
|
||||
<LineChart |
|
||||
title='最近<label>营业额' |
|
||||
label='营业额' |
|
||||
prefix='¥' |
|
||||
data={salesData} |
|
||||
color='#ed862b' |
|
||||
/> |
|
||||
<CirclePanel> |
|
||||
<CircleCard |
|
||||
title='当日订单转化率' |
|
||||
percent={dailyConversionRate.rate} |
|
||||
growth={dailyConversionRate.growth} |
|
||||
color='#705fff' |
|
||||
/> |
|
||||
<CircleCard |
|
||||
title='当月订单转化率' |
|
||||
percent={monthlyConversionRate.rate} |
|
||||
growth={monthlyConversionRate.growth} |
|
||||
color='#eb607e' |
|
||||
/> |
|
||||
</CirclePanel> |
|
||||
{/*<Table*/} |
|
||||
{/*title='<label>的商品'*/} |
|
||||
{/*data={topGoodsData}*/} |
|
||||
{/*priceCol={2}*/} |
|
||||
{/*width='300px'*/} |
|
||||
{/*/>*/} |
|
||||
</div> |
|
||||
<div className="row"> |
|
||||
<LineChart |
|
||||
title='最近<days>天日活跃用户数' |
|
||||
label='日活量' |
|
||||
unit='人' |
|
||||
dateCount={4} |
|
||||
data={[{data: DAU}]} |
|
||||
color='#17e7ae' |
|
||||
/> |
|
||||
<LineChart |
|
||||
title='最近<days>天新增用户数' |
|
||||
label='新增用户数' |
|
||||
unit='人' |
|
||||
data={[{data: users}]} |
|
||||
color='#17e7ae' |
|
||||
/> |
|
||||
|
|
||||
{/*<Table*/} |
|
||||
{/*title='单笔最高'*/} |
|
||||
{/*data={[{*/} |
|
||||
{/*highlightCol: 1,*/} |
|
||||
{/*data: topOrders.map(i => ({*/} |
|
||||
{/*'名字': i.user.name,*/} |
|
||||
{/*'金额': i.info.pay_fee*/} |
|
||||
{/*}))*/} |
|
||||
{/*}]}*/} |
|
||||
{/*color='#ed862b'*/} |
|
||||
{/*priceCol={1}*/} |
|
||||
{/*/>*/} |
|
||||
</div> |
|
||||
</div> |
|
||||
); |
|
||||
|
|
||||
ReactDOM.render( |
|
||||
dashboard, |
|
||||
document.getElementById('react') |
|
||||
); |
|
@ -1,65 +0,0 @@ |
|||||
import ClassNames from "classnames"; |
|
||||
import {Icon} from "antd"; |
|
||||
import {Reason} from "./common"; |
|
||||
import React from "react"; |
|
||||
import styled from "styled-components"; |
|
||||
|
|
||||
export default function MainDescription({currentStep, auditStatusCode, currentStepInfo, currentStatusInfo, currentStepInfoWithStatus}) { |
|
||||
if (currentStepInfo) { |
|
||||
return ( |
|
||||
<Root |
|
||||
className={ClassNames({ |
|
||||
'error': currentStep === 2 && (auditStatusCode === 1 || auditStatusCode === 3), |
|
||||
'warning': currentStep === 2 && !currentStatusInfo |
|
||||
})} |
|
||||
> |
|
||||
<Icon |
|
||||
type={currentStepInfoWithStatus.icon} |
|
||||
style={{fontSize: '40px'}} |
|
||||
/> |
|
||||
<DescriptionText> |
|
||||
{currentStepInfoWithStatus.description || currentStepInfoWithStatus.title} |
|
||||
{(currentStep === 2 && auditStatusCode === 1) && <Reason>{auditStatus.reason}</Reason>} |
|
||||
</DescriptionText> |
|
||||
</Root> |
|
||||
) |
|
||||
} else { |
|
||||
return ( |
|
||||
<LatestTip> |
|
||||
<Icon type="safety-certificate" theme="filled" style={{fontSize: '120px'}}/> |
|
||||
<TipText>您的小程序线上版本已经是最新的</TipText> |
|
||||
</LatestTip> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
const LatestTip = styled.div`
|
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
align-items: center; |
|
||||
justify-content: space-around; |
|
||||
color: #33a968; |
|
||||
`;
|
|
||||
|
|
||||
const TipText = styled.div`
|
|
||||
margin-top: 30px; |
|
||||
font-size: 1.5em; |
|
||||
`;
|
|
||||
|
|
||||
const Root = styled.div`
|
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
color: #40a9ff; |
|
||||
&.error { |
|
||||
color: #f5222d; |
|
||||
} |
|
||||
&.warning { |
|
||||
color: #f49c00; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
const DescriptionText = styled.div`
|
|
||||
margin-left: 20px; |
|
||||
font-size: 1.2em; |
|
||||
`;
|
|
@ -1,132 +0,0 @@ |
|||||
import React from "react"; |
|
||||
import PropTypes from "prop-types"; |
|
||||
import styled from "styled-components"; |
|
||||
import {Input, Button, Tag, Alert, message} from "antd"; |
|
||||
import {LoadingBar, Reason} from "./common"; |
|
||||
import axios from "axios"; |
|
||||
|
|
||||
const colorList = ['magenta', 'red', 'volcano', 'orange', 'gold', 'lime', 'green', 'cyan', 'blue', 'geekblue', 'purple']; |
|
||||
|
|
||||
function getRandomInt(min, max) { |
|
||||
min = Math.ceil(min); |
|
||||
max = Math.floor(max); |
|
||||
return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive
|
|
||||
} |
|
||||
|
|
||||
export default class ManageTriers extends React.Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
this.state = { |
|
||||
trierList: null, |
|
||||
wechatId: '', |
|
||||
isAddingTrier: false |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
static propTypes = {}; |
|
||||
|
|
||||
static defaultProps = {}; |
|
||||
|
|
||||
componentWillMount() { |
|
||||
this.getTrierList(); |
|
||||
} |
|
||||
|
|
||||
getTrierList = () => { |
|
||||
axios.get('/agent/trier-list').then(data => { |
|
||||
this.setState({trierList: data.map(trier => ({ |
|
||||
...trier, |
|
||||
color: colorList[getRandomInt(0, colorList.length - 1)], |
|
||||
}))}) |
|
||||
}).catch((err) => this.setState({trierList: err})); |
|
||||
}; |
|
||||
|
|
||||
changeWechatId = (e) => { |
|
||||
this.setState({wechatId: e.target.value}) |
|
||||
}; |
|
||||
|
|
||||
addTrier = () => { |
|
||||
const {wechatId} = this.state; |
|
||||
|
|
||||
if (wechatId) { |
|
||||
this.setState({isAddingTrier: true}); |
|
||||
axios.post('/agent/set-trier', { |
|
||||
wechatId |
|
||||
}).then(() => { |
|
||||
message.success('设置体验者成功'); |
|
||||
this.setState({isAddingTrier: false, wechatId: ''}); |
|
||||
this.getTrierList(); |
|
||||
}).catch(() => { |
|
||||
this.setState({isAddingTrier: false}) |
|
||||
}); |
|
||||
} else { |
|
||||
message.error('请输入用户微信号') |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
const {trierList, wechatId, isAddingTrier} = this.state; |
|
||||
let trierListView; |
|
||||
|
|
||||
if (trierList && trierList.length > 0) { |
|
||||
trierListView = ( |
|
||||
<TrierList> |
|
||||
{trierList.map((trier, index) => ( |
|
||||
<Tag |
|
||||
key={index} |
|
||||
color={trier.color} |
|
||||
>{trier.wechatid}</Tag> |
|
||||
))} |
|
||||
</TrierList> |
|
||||
) |
|
||||
} else if (trierList && trierList.length === 0) { |
|
||||
trierListView = <Reason>当前没有体验者</Reason> |
|
||||
} else if (trierList && typeof trierList === 'string') { |
|
||||
trierListView = <Reason>{trierList}</Reason> |
|
||||
} else { |
|
||||
trierListView = <LoadingBar>加载体验者中</LoadingBar> |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<Root> |
|
||||
<Title>体验者微信号列表</Title> |
|
||||
<Alert message="由于微信限制,只有通过本系统设置的体验者才会在此处显示,在微信公众平台设置的不能在此处显示。" type="info" showIcon /> |
|
||||
<TrierArea>{trierListView}</TrierArea> |
|
||||
<Handler> |
|
||||
<Input |
|
||||
placeholder='请输入用户微信号' |
|
||||
value={wechatId} |
|
||||
allowClear |
|
||||
onChange={this.changeWechatId} |
|
||||
/> |
|
||||
<Button |
|
||||
type='primary' |
|
||||
loading={isAddingTrier} |
|
||||
style={{marginLeft: '10px'}} |
|
||||
onClick={this.addTrier} |
|
||||
>添加体验者</Button> |
|
||||
</Handler> |
|
||||
</Root> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const Root = styled.div`
|
|
||||
|
|
||||
`;
|
|
||||
|
|
||||
const Title = styled.div`
|
|
||||
font-size: 1.2em; |
|
||||
font-weight: bold; |
|
||||
margin-bottom: 20px; |
|
||||
`;
|
|
||||
|
|
||||
const TrierArea = styled.div`
|
|
||||
margin: 20px 0; |
|
||||
`;
|
|
||||
|
|
||||
const Handler = styled.div`
|
|
||||
display: flex; |
|
||||
`;
|
|
||||
|
|
||||
const TrierList = styled.div`
|
|
||||
`;
|
|
@ -1,38 +0,0 @@ |
|||||
import React from "react"; |
|
||||
import {Icon, Steps} from "antd"; |
|
||||
import {stepInfoList} from "./data"; |
|
||||
|
|
||||
const Step = Steps.Step; |
|
||||
|
|
||||
export default function StepBar({currentStep, auditStatusCode, currentStatusInfo}) { |
|
||||
if (currentStep <= 3) { |
|
||||
return ( |
|
||||
<Steps |
|
||||
current={currentStep} |
|
||||
status={currentStatusInfo && currentStatusInfo.stepStatus} |
|
||||
> |
|
||||
{stepInfoList.map(({title, icon, status}, index) => { |
|
||||
if (index === 2 && status && status[auditStatusCode]) { |
|
||||
return ( |
|
||||
<Step |
|
||||
key={index} |
|
||||
title={status[auditStatusCode].title} |
|
||||
icon={<Icon type={status[auditStatusCode].icon}/>} |
|
||||
/> |
|
||||
) |
|
||||
} else { |
|
||||
return ( |
|
||||
<Step |
|
||||
key={index} |
|
||||
title={title} |
|
||||
icon={<Icon type={icon}/>} |
|
||||
/> |
|
||||
) |
|
||||
} |
|
||||
})} |
|
||||
</Steps> |
|
||||
) |
|
||||
} else { |
|
||||
return <></>; |
|
||||
} |
|
||||
} |
|
@ -1,22 +0,0 @@ |
|||||
import React from "react"; |
|
||||
import styled from "styled-components"; |
|
||||
import {Icon, Spin} from "antd"; |
|
||||
const antIcon = <Icon type="loading" style={{fontSize: 24}} spin/>; |
|
||||
|
|
||||
const LoadingBarRoot = styled.div`
|
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
margin: 10px 0; |
|
||||
> span { |
|
||||
margin-left: 20px; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
export function LoadingBar({children}) { |
|
||||
return <LoadingBarRoot><Spin indicator={antIcon}/><span>{children}...</span></LoadingBarRoot> |
|
||||
} |
|
||||
|
|
||||
export const Reason = styled.div`
|
|
||||
color: #f5222d; |
|
||||
`;
|
|
@ -1,146 +0,0 @@ |
|||||
import compareVersions from 'compare-versions' |
|
||||
|
|
||||
if (ENV_DEV) { |
|
||||
window.auditStatus = { |
|
||||
auditid: 438893451, |
|
||||
errcode: 0, |
|
||||
errmsg: "ok", |
|
||||
reason: '你充的钱不够,请再充钱', |
|
||||
status: 1, // 0: 审核成功,1: 审核失败,2: 审核中,3: 已撤回
|
|
||||
}; |
|
||||
|
|
||||
window.versions = { |
|
||||
version: "1.4.0", // 第三方平台模板库的最新版本
|
|
||||
commit_version: "1.3.0", // 上传为体验版
|
|
||||
audit_version: "", // 最新提交了审核的版本
|
|
||||
audited_version: "1.2.0", // 审核通过的版本
|
|
||||
release_version: "", // 线上的版本
|
|
||||
}; |
|
||||
|
|
||||
window.categoriesList = [ |
|
||||
{"first_class": "商家自营", "second_class": "机械/电子器件", "first_id": 304, "second_id": 788}, |
|
||||
{"first_class": "商家自营2", "second_class": "机械/电子器件", "first_id": 304, "second_id": 788}, |
|
||||
{"first_class": "商家自营3", "second_class": "机械/电子器件", "first_id": 304, "second_id": 788}, |
|
||||
{"first_class": "商家自营4", "second_class": "机械/电子器件", "first_id": 304, "second_id": 788}, |
|
||||
{"first_class": "商家自营5", "second_class": "机械/电子器件", "first_id": 304, "second_id": 788}, |
|
||||
{"first_class": "商家自营6", "second_class": "机械/电子器件", "first_id": 304, "second_id": 788}, |
|
||||
]; |
|
||||
} |
|
||||
|
|
||||
const {auditStatus, versions, categoriesList} = window; |
|
||||
|
|
||||
const {version, commit_version, audit_version, audited_version, release_version} = versions; |
|
||||
|
|
||||
const versionsArray = [version, commit_version, audit_version, audited_version, release_version]; |
|
||||
|
|
||||
const stepInfoList = [ |
|
||||
{ |
|
||||
title: '上传', |
|
||||
buttonText: '上传小程序代码', |
|
||||
icon: 'cloud-upload', |
|
||||
url: 'commit', |
|
||||
description: '将代码上传到微信公众平台,使其成为体验版' |
|
||||
}, |
|
||||
{ |
|
||||
title: '审核', |
|
||||
buttonText: '提交审核', |
|
||||
icon: 'audit', |
|
||||
url: 'submit-audit', |
|
||||
action: 'submitAudit', |
|
||||
description: '微信小程序均需要审核后才能上线,一般情况下24小时内即可完成审核' |
|
||||
}, |
|
||||
{ |
|
||||
title: '审核结果未知', |
|
||||
icon: 'question-circle', |
|
||||
buttonIcon: 'audit', |
|
||||
url: 'submit-audit', |
|
||||
action: 'submitAudit', |
|
||||
status: { |
|
||||
0: { |
|
||||
title: '审核通过', |
|
||||
icon: 'check-circle', |
|
||||
}, |
|
||||
1: { |
|
||||
title: '审核不通过', |
|
||||
icon: 'close-circle', |
|
||||
stepStatus: 'error', |
|
||||
buttonText: '重新提交审核', |
|
||||
description: '审核不通过,原因:' |
|
||||
}, |
|
||||
2: { |
|
||||
title: '等待审核结果', |
|
||||
icon: 'sync', |
|
||||
description: '请耐心等待微信的审核结果,一般情况下 24 小时内即可完成审核,节假日审核可能需要一到三天' |
|
||||
}, |
|
||||
3: { |
|
||||
title: '审核已撤回', |
|
||||
icon: 'rollback', |
|
||||
stepStatus: 'error', |
|
||||
buttonText: '重新提交审核', |
|
||||
description: '有人手动操作撤回了本次审核,请重新提交审核' |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
title: '发布', |
|
||||
buttonText: '发布新版', |
|
||||
icon: 'export', |
|
||||
url: 'release', |
|
||||
description: '新版本的小程序需要等下一次冷启动,即退出超过一定时间(目前是5分钟)后才会应用上。' |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
const initVersionList = [ |
|
||||
{ |
|
||||
label: '版本库最新版本', |
|
||||
value: version |
|
||||
}, |
|
||||
{ |
|
||||
label: '体验版版本', |
|
||||
value: commit_version |
|
||||
}, |
|
||||
{ |
|
||||
label: '审核中版本', |
|
||||
value: auditStatus.status === 2 ? audit_version : '无' |
|
||||
}, |
|
||||
{ |
|
||||
label: '已审核版本', |
|
||||
value: audited_version |
|
||||
}, |
|
||||
{ |
|
||||
label: '线上版本', |
|
||||
value: release_version |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
let canRelease = false; |
|
||||
|
|
||||
// 查找第一个需要完成的步骤
|
|
||||
let _currentStep = versionsArray[0] ? 0 : 4; |
|
||||
|
|
||||
if (versionsArray[2] && auditStatus.status === 2) { |
|
||||
_currentStep = 2; |
|
||||
} else if (versionsArray[0]) { |
|
||||
_currentStep = versionsArray.findIndex((currentVersion, index) => { |
|
||||
const nextItem = versionsArray[index + 1]; |
|
||||
return !nextItem || compareVersions(currentVersion, nextItem) > 0 |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
let _auditStatusCode = auditStatus.status; |
|
||||
|
|
||||
if (_currentStep === -1) { |
|
||||
_currentStep = 4; |
|
||||
} |
|
||||
|
|
||||
if (_currentStep !== 2) { |
|
||||
_auditStatusCode = 2; |
|
||||
} |
|
||||
|
|
||||
const currentStep = _currentStep; |
|
||||
const auditStatusCode = _auditStatusCode; |
|
||||
const latestVersion = version; |
|
||||
const auditedVersion = audited_version; |
|
||||
|
|
||||
export {currentStep, auditStatusCode, initVersionList, stepInfoList, latestVersion, |
|
||||
auditedVersion, categoriesList} |
|
@ -1,397 +0,0 @@ |
|||||
import React, {useState} from "react"; |
|
||||
import styled from "styled-components"; |
|
||||
import ReactDOM from "react-dom"; |
|
||||
import {Icon, Button, Alert, Modal, message, LocaleProvider, Checkbox} from 'antd'; |
|
||||
import zhCN from 'antd/lib/locale-provider/zh_CN'; |
|
||||
import axios from 'axios' |
|
||||
import compareVersions from 'compare-versions' |
|
||||
import '../utils/ajax' |
|
||||
import {stepInfoList, currentStep, auditStatusCode, initVersionList, latestVersion, categoriesList, |
|
||||
auditedVersion, canRelease} from './data' |
|
||||
import {LoadingBar, Reason} from "./common"; |
|
||||
import ManageTriers from './ManageTriers' |
|
||||
import StepBar from './StepBar' |
|
||||
import MainDescription from './MainDescription' |
|
||||
import '../utils/ajax' |
|
||||
|
|
||||
const CheckboxGroup = Checkbox.Group; |
|
||||
|
|
||||
function getStepInfo(stepIndex, currentStepInfo, auditStatusCode) { |
|
||||
if (stepIndex === 2 && currentStepInfo.status && currentStepInfo.status[auditStatusCode]) { |
|
||||
return { |
|
||||
title: currentStepInfo.status[auditStatusCode].title, |
|
||||
icon: currentStepInfo.status[auditStatusCode].icon, |
|
||||
description: currentStepInfo.status[auditStatusCode].description |
|
||||
} |
|
||||
} else { |
|
||||
return { |
|
||||
title: currentStepInfo.title, |
|
||||
icon: currentStepInfo.icon, |
|
||||
description: currentStepInfo.description |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default class MiniProgramManagement extends React.Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
|
|
||||
this.state = { |
|
||||
currentStep, |
|
||||
versionList: initVersionList, |
|
||||
auditStatusCode, |
|
||||
qrCodeToTry: null, |
|
||||
isLoadingAction: false, |
|
||||
isShowModalForQrCode: false, |
|
||||
isShowModalForTrier: false, |
|
||||
isShowModalForAudit: false, |
|
||||
isShowReleaseAlert: canRelease, |
|
||||
checkedCategoryIndexList: [0] |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
static defaultProps = {}; |
|
||||
|
|
||||
static propTypes = {}; |
|
||||
|
|
||||
handleAction = () => { |
|
||||
const {currentStep} = this.state; |
|
||||
const {url, action} = stepInfoList[currentStep]; |
|
||||
|
|
||||
if (action === 'submitAudit') { |
|
||||
this.setState({isShowModalForAudit: true}) |
|
||||
} else { |
|
||||
this.commitAction(url); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
commitAction = (url, params = {}, independentAction) => { |
|
||||
const {currentStep} = this.state; |
|
||||
let {versionList} = this.state; |
|
||||
const {buttonText} = stepInfoList[currentStep]; |
|
||||
|
|
||||
independentAction || this.setState({isLoadingAction: true}); |
|
||||
axios.post(`/agent/${url}`, params).then(() => { |
|
||||
if (independentAction) { |
|
||||
message.success(independentAction + '成功'); |
|
||||
if (independentAction === '发布') { |
|
||||
// 更新线上版本
|
|
||||
versionList[4].value = versionList[3].value; |
|
||||
this.setState({isShowReleaseAlert: false}) |
|
||||
} |
|
||||
} else if (currentStep === 2) { |
|
||||
// 重新提交审核,不切换到下一个步骤
|
|
||||
this.setState({ |
|
||||
auditStatusCode: 2, |
|
||||
isLoadingAction: false |
|
||||
}); |
|
||||
message.success((buttonText || '重新提交审核') + '成功'); |
|
||||
} else { |
|
||||
// 切换到下一个步骤
|
|
||||
versionList[currentStep + 1].value = versionList[currentStep].value; |
|
||||
|
|
||||
this.setState({ |
|
||||
currentStep: currentStep + 1, |
|
||||
isLoadingAction: false, |
|
||||
versionList |
|
||||
}); |
|
||||
|
|
||||
message.success((buttonText || '重新提交审核') + '成功'); |
|
||||
|
|
||||
if (currentStep === 1) { |
|
||||
this.setState({ |
|
||||
auditStatusCode: 2 |
|
||||
}); |
|
||||
} else if (currentStep === 3) { |
|
||||
Modal.info({ |
|
||||
title: '提示', |
|
||||
content: stepInfoList[3].description, |
|
||||
cancelText: '', |
|
||||
}) |
|
||||
} |
|
||||
} |
|
||||
}).catch(() => { |
|
||||
this.setState({isLoadingAction: false}) |
|
||||
}) |
|
||||
}; |
|
||||
|
|
||||
hideModalFoeAudit = () => { |
|
||||
this.setState({isShowModalForAudit: false}) |
|
||||
}; |
|
||||
|
|
||||
submitAudit = () => { |
|
||||
const {currentStep, checkedCategoryIndexList} = this.state; |
|
||||
const {url} = stepInfoList[currentStep]; |
|
||||
|
|
||||
if (checkedCategoryIndexList && checkedCategoryIndexList.length > 0) { |
|
||||
this.hideModalFoeAudit(); |
|
||||
this.commitAction(url, {categories: checkedCategoryIndexList}); |
|
||||
this.setState({isLoadingAction: true}); |
|
||||
} else { |
|
||||
message.error('应至少选择一个分类') |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
showManagingTrier = () => { |
|
||||
this.setState({isShowModalForTrier: true, trierList: null}); |
|
||||
}; |
|
||||
|
|
||||
showModalForQrCode = () => { |
|
||||
this.setState({isShowModalForQrCode: true, qrCodeToTry: ''}); |
|
||||
axios.get('/agent/qr-code-to-try').then(data => { |
|
||||
this.setState({qrCodeToTry: data.image}) |
|
||||
}).catch((err) => this.setState({qrCodeToTry: `error: ${err}`})); |
|
||||
}; |
|
||||
|
|
||||
hideModalForQrCode = () => { |
|
||||
this.setState({isShowModalForQrCode: false}); |
|
||||
}; |
|
||||
|
|
||||
hideModalForTrier = () => { |
|
||||
this.setState({isShowModalForTrier: false}); |
|
||||
}; |
|
||||
|
|
||||
handleChangeCategory = (value) => { |
|
||||
this.setState({ |
|
||||
checkedCategoryIndexList: value |
|
||||
}) |
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
const {currentStep, versionList, isLoadingAction, auditStatusCode, isShowModalForQrCode, isShowModalForAudit, |
|
||||
isShowModalForTrier, qrCodeToTry, isAddingTrier, checkedCategoryIndexList} = this.state; |
|
||||
const currentStepInfo = stepInfoList[currentStep]; |
|
||||
const currentStatusInfo = currentStepInfo && currentStepInfo.status && currentStepInfo.status[auditStatusCode]; |
|
||||
const currentStepInfoWithStatus = currentStepInfo && getStepInfo(currentStep, currentStepInfo, auditStatusCode); |
|
||||
|
|
||||
const isShowReleaseAlert = currentStep !== 2 && currentStep !== 3 && versionList[3].value && |
|
||||
(!versionList[4].value || compareVersions(versionList[3].value, versionList[4].value) > 0); |
|
||||
|
|
||||
return ( |
|
||||
<Root> |
|
||||
<Container> |
|
||||
<StepBar currentStep={currentStep} auditStatusCode={auditStatusCode} currentStatusInfo={currentStatusInfo} /> |
|
||||
<Content> |
|
||||
<Description> |
|
||||
{currentStep === 0 && ( |
|
||||
<MessageAlert> |
|
||||
<Alert |
|
||||
message={( |
|
||||
<UpdateTip> |
|
||||
有新的版本<LatestVersion>{latestVersion}</LatestVersion>,建议进行更新。 |
|
||||
<GotoChangeLog href="/weapp/log">看看更新了什么</GotoChangeLog> |
|
||||
</UpdateTip> |
|
||||
)} |
|
||||
type="success" |
|
||||
showIcon |
|
||||
icon={<Icon type="bulb" theme="twoTone"/>} |
|
||||
/> |
|
||||
</MessageAlert> |
|
||||
)} |
|
||||
{isShowReleaseAlert && ( |
|
||||
<MessageAlert> |
|
||||
<Alert |
|
||||
message={( |
|
||||
<UpdateTip> |
|
||||
有旧的已审核版本<LatestVersion>{auditedVersion}</LatestVersion>,可以 |
|
||||
<GotoChangeLog |
|
||||
onClick={this.commitAction.bind(undefined, 'release', undefined, '发布')} |
|
||||
>发布此版本</GotoChangeLog> |
|
||||
</UpdateTip> |
|
||||
)} |
|
||||
type="info" |
|
||||
showIcon |
|
||||
closable |
|
||||
/> |
|
||||
</MessageAlert> |
|
||||
)} |
|
||||
<MainDescription |
|
||||
currentStep={currentStep} |
|
||||
auditStatusCode={auditStatusCode} |
|
||||
currentStatusInfo={currentStepInfo} |
|
||||
currentStepInfo={currentStepInfo} |
|
||||
currentStepInfoWithStatus={currentStepInfoWithStatus} |
|
||||
/> |
|
||||
</Description> |
|
||||
{(currentStepInfo && currentStepInfo.buttonText || currentStatusInfo && currentStatusInfo.buttonText) && ( |
|
||||
<Operation> |
|
||||
<Button |
|
||||
type='primary' |
|
||||
icon={ |
|
||||
stepInfoList[currentStep].buttonIcon || |
|
||||
stepInfoList[currentStep].icon |
|
||||
} |
|
||||
loading={isLoadingAction} |
|
||||
onClick={this.handleAction} |
|
||||
> |
|
||||
{currentStepInfo.buttonText || currentStatusInfo && currentStatusInfo.buttonText} |
|
||||
</Button> |
|
||||
</Operation> |
|
||||
)} |
|
||||
<Versions> |
|
||||
{versionList.map(({label, value}, index) => ( |
|
||||
<VersionItem key={index}> |
|
||||
<Label>{label}</Label> |
|
||||
<Value>{value || '无'}</Value> |
|
||||
</VersionItem> |
|
||||
))} |
|
||||
</Versions> |
|
||||
<Handler> |
|
||||
<Button type='primary' onClick={this.showModalForQrCode}>查看体验版二维码</Button> |
|
||||
<Button type='primary' onClick={this.showManagingTrier}>添加体验者</Button> |
|
||||
</Handler> |
|
||||
</Content> |
|
||||
</Container> |
|
||||
<Modal |
|
||||
visible={isShowModalForAudit} |
|
||||
title='提交审核' |
|
||||
onOk={this.submitAudit} |
|
||||
onCancel={this.hideModalFoeAudit} |
|
||||
> |
|
||||
<Alert |
|
||||
message="请选择小程序所属类目,至少选择1项,最多选择5项" |
|
||||
type="info" |
|
||||
showIcon |
|
||||
style={{ marginBottom: 20 }} |
|
||||
/> |
|
||||
{checkedCategoryIndexList.length >= 5 && ( |
|
||||
<Alert |
|
||||
message="已达到最大可选数量,不能再继续选择其它分类" |
|
||||
type="warning" |
|
||||
showIcon |
|
||||
style={{ marginBottom: 20 }} |
|
||||
/> |
|
||||
)} |
|
||||
<CheckboxGroup |
|
||||
value={checkedCategoryIndexList} |
|
||||
options={categoriesList.map(({first_class, second_class, third_class}, index) => ({ |
|
||||
label: ( |
|
||||
<CheckboxItem> |
|
||||
<CategoryName>{first_class}</CategoryName> |
|
||||
<span>></span> |
|
||||
<CategoryName>{second_class}</CategoryName> |
|
||||
{third_class && <><span>></span><CategoryName>{third_class}</CategoryName></>} |
|
||||
</CheckboxItem> |
|
||||
), |
|
||||
value: index, |
|
||||
disabled: checkedCategoryIndexList.length >= 5 && !checkedCategoryIndexList.includes(index) |
|
||||
}))} |
|
||||
onChange={this.handleChangeCategory} |
|
||||
/> |
|
||||
</Modal> |
|
||||
<Modal |
|
||||
visible={isShowModalForQrCode} |
|
||||
title='查看体验二维码' |
|
||||
cancelButtonProps={{style: {display: 'none'}}} |
|
||||
onOk={this.hideModalForQrCode} |
|
||||
onCancel={this.hideModalForQrCode} |
|
||||
> |
|
||||
{qrCodeToTry ? (qrCodeToTry.startsWith('error: ') ? <Reason>{qrCodeToTry.substr(6)}</Reason> : |
|
||||
<img src={qrCodeToTry} alt=""/>) : ( |
|
||||
<LoadingBar>加载中...</LoadingBar> |
|
||||
)} |
|
||||
</Modal> |
|
||||
<Modal |
|
||||
visible={isShowModalForTrier} |
|
||||
title='设置体验者' |
|
||||
confirmLoading={isAddingTrier} |
|
||||
cancelButtonProps={{style: {display: 'none'}}} |
|
||||
onOk={this.hideModalForTrier} |
|
||||
onCancel={this.hideModalForTrier} |
|
||||
> |
|
||||
<ManageTriers /> |
|
||||
</Modal> |
|
||||
</Root> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const Root = styled.div`
|
|
||||
`;
|
|
||||
|
|
||||
const Container = styled.div`
|
|
||||
max-width: 600px; |
|
||||
margin: auto; |
|
||||
`;
|
|
||||
|
|
||||
const Content = styled.div`
|
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: space-around; |
|
||||
align-items: center; |
|
||||
min-height: 300px; |
|
||||
padding: 20px; |
|
||||
`;
|
|
||||
|
|
||||
const Description = styled.div`
|
|
||||
max-width: 400px; |
|
||||
margin-bottom: 30px; |
|
||||
`;
|
|
||||
|
|
||||
const Operation = styled.div`
|
|
||||
margin-bottom: 20px; |
|
||||
`;
|
|
||||
|
|
||||
const Versions = styled.div`
|
|
||||
display: table-row-group; |
|
||||
`;
|
|
||||
|
|
||||
const VersionItem = styled.div`
|
|
||||
display: table-row; |
|
||||
`;
|
|
||||
|
|
||||
const Label = styled.div`
|
|
||||
display: table-cell; |
|
||||
padding: 5px 20px; |
|
||||
`;
|
|
||||
|
|
||||
const Value = styled.div`
|
|
||||
display: table-cell; |
|
||||
padding: 5px 10px; |
|
||||
text-align: center; |
|
||||
color: #1c7cff; |
|
||||
`;
|
|
||||
|
|
||||
const Handler = styled.div`
|
|
||||
display: flex; |
|
||||
margin-top: 20px; |
|
||||
> button { |
|
||||
margin: 0 10px; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
const MessageAlert = styled.div`
|
|
||||
margin-bottom: 20px; |
|
||||
`;
|
|
||||
|
|
||||
const UpdateTip = styled.div`
|
|
||||
|
|
||||
`
|
|
||||
|
|
||||
const LatestVersion = styled.span`
|
|
||||
margin: 0 5px; |
|
||||
color: #f49c00; |
|
||||
`;
|
|
||||
|
|
||||
const GotoChangeLog = styled.a`
|
|
||||
margin-left: 5px; |
|
||||
`;
|
|
||||
|
|
||||
const CheckboxItem = styled.div`
|
|
||||
display: inline; |
|
||||
color: #aaa; |
|
||||
line-height: 2; |
|
||||
`;
|
|
||||
|
|
||||
const CategoryName = styled.span`
|
|
||||
color: #000; |
|
||||
margin: 0 5px; |
|
||||
font-weight: normal; |
|
||||
`;
|
|
||||
|
|
||||
ReactDOM.render( |
|
||||
<LocaleProvider locale={zhCN}> |
|
||||
<MiniProgramManagement/> |
|
||||
</LocaleProvider>, |
|
||||
document.getElementById('mini-program-management') |
|
||||
); |
|
@ -1,127 +0,0 @@ |
|||||
import React from "react"; |
|
||||
import PropTypes from "prop-types"; |
|
||||
import styled from "styled-components"; |
|
||||
import {setLightness} from 'polished' |
|
||||
import dayjs from 'dayjs' |
|
||||
|
|
||||
export default class Card extends React.Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
this.state = {}; |
|
||||
} |
|
||||
|
|
||||
static propTypes = { |
|
||||
icon: PropTypes.string, |
|
||||
title: PropTypes.string, |
|
||||
color: PropTypes.string, |
|
||||
data: PropTypes.arrayOf(PropTypes.object) |
|
||||
}; |
|
||||
|
|
||||
static defaultProps = { |
|
||||
icon: '', |
|
||||
title: '', |
|
||||
color: '#5845e9', |
|
||||
data: [] |
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
const {icon, title, color, data} = this.props; |
|
||||
return ( |
|
||||
<Root> |
|
||||
<Header color={color}> |
|
||||
<Icon className={'fa ' + ('fa-' + icon)} color={color} /> |
|
||||
<Title>{title}</Title> |
|
||||
</Header> |
|
||||
<Body> |
|
||||
{data.map((item, index) => { |
|
||||
let value = item.value; |
|
||||
|
|
||||
if (item.type === 'time') { |
|
||||
value = dayjs(item.value * 1000).format('YYYY-MM-DD HH:mm:ss') |
|
||||
} else if (item.type === 'price') { |
|
||||
value = `${item.prefix || ''}¥${value}`; |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<Row key={index}> |
|
||||
<Label>{item.label}</Label> |
|
||||
<Value |
|
||||
className={'card-type__' + item.type} |
|
||||
> |
|
||||
{value || '无'} |
|
||||
{(item.link && item.link.url) && <a href={item.link.url}>{item.link.text}</a>} |
|
||||
</Value> |
|
||||
</Row> |
|
||||
) |
|
||||
})} |
|
||||
</Body> |
|
||||
</Root> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const iconSize = 50; |
|
||||
|
|
||||
const Root = styled.div`
|
|
||||
//width: 300px;
|
|
||||
flex-grow: 1; |
|
||||
margin: 10px; |
|
||||
border-radius: 10px; |
|
||||
background: #fff; |
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, .1); |
|
||||
`;
|
|
||||
|
|
||||
const Header = styled.div`
|
|
||||
position: relative; |
|
||||
padding: 10px; |
|
||||
border-radius: 10px 10px 0 0; |
|
||||
background: ${props => setLightness(.95, props.color)}; |
|
||||
color: ${props => setLightness(.2, props.color)}; |
|
||||
`;
|
|
||||
|
|
||||
const Icon = styled.div`
|
|
||||
position: absolute; |
|
||||
left: 10px; |
|
||||
bottom: -${iconSize / 2}px; |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
width: ${iconSize}px; |
|
||||
height: ${iconSize}px; |
|
||||
font-size: ${iconSize / 2}px!important; |
|
||||
border-radius: 100%; |
|
||||
background: ${props => props.color}; |
|
||||
color: #fff; |
|
||||
box-shadow: 0 2px 20px 3px rgba(0, 0, 0, .2); |
|
||||
`;
|
|
||||
|
|
||||
const Body = styled.div`
|
|
||||
padding: 10px 20px; |
|
||||
min-height: 100px; |
|
||||
margin-top: ${iconSize / 2}px; |
|
||||
`;
|
|
||||
|
|
||||
const Title = styled.div`
|
|
||||
margin-left: ${iconSize + 10}px; |
|
||||
font-weight: bold; |
|
||||
`;
|
|
||||
|
|
||||
const Row = styled.div`
|
|
||||
display: table-row; |
|
||||
`;
|
|
||||
|
|
||||
const Label = styled.div`
|
|
||||
display: table-cell; |
|
||||
font-weight: bold; |
|
||||
padding: 10px 20px 10px 0; |
|
||||
`;
|
|
||||
|
|
||||
const Value = styled.div`
|
|
||||
display: table-cell; |
|
||||
&.card-type__price { |
|
||||
color: #e83139; |
|
||||
} |
|
||||
a { |
|
||||
margin-left: 10px; |
|
||||
} |
|
||||
`;
|
|
@ -1,77 +0,0 @@ |
|||||
import React from "react"; |
|
||||
import PropTypes from "prop-types"; |
|
||||
import styled from "styled-components"; |
|
||||
|
|
||||
export default class GoodsCard extends React.Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
this.state = {}; |
|
||||
} |
|
||||
|
|
||||
static propTypes = { |
|
||||
goodsInfo: PropTypes.object.isRequired |
|
||||
}; |
|
||||
|
|
||||
static defaultProps = { |
|
||||
goodsInfo: {} |
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
const {goodsInfo} = this.props; |
|
||||
|
|
||||
return ( |
|
||||
<Root> |
|
||||
<Left><img src={`${ENV_DEV ? '//demo.kcshop.pro' : ''}${goodsInfo.goods_img}`} alt=""/></Left> |
|
||||
<Right> |
|
||||
<div>{goodsInfo.goods_name}</div> |
|
||||
{goodsInfo.sku_type && <Sku>规格:{goodsInfo.sku_type}</Sku>} |
|
||||
<Price>¥{goodsInfo.shop_price}</Price> |
|
||||
</Right> |
|
||||
<Other>x{goodsInfo.goods_number}</Other> |
|
||||
</Root> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const Root = styled.div`
|
|
||||
display: flex; |
|
||||
padding: 10px 20px; |
|
||||
margin: 10px 0; |
|
||||
&:not(:last-child) { |
|
||||
border-bottom: 1px solid #eee; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
const imageSize = 60; |
|
||||
const Left = styled.div`
|
|
||||
width: ${imageSize}px; |
|
||||
height: ${imageSize}px; |
|
||||
img { |
|
||||
max-width: 100%; |
|
||||
max-height: 100%; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
const Right = styled.div`
|
|
||||
flex-grow: 1; |
|
||||
margin-left: 10px; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: space-around; |
|
||||
`;
|
|
||||
|
|
||||
const Other = styled.div`
|
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
`;
|
|
||||
|
|
||||
const Sku = styled.div`
|
|
||||
color: #aaa; |
|
||||
font-size: .8em; |
|
||||
`;
|
|
||||
|
|
||||
const Price = styled.div`
|
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
color: #e83139; |
|
||||
`;
|
|
@ -1,23 +0,0 @@ |
|||||
export default function getStatus(order) { |
|
||||
if (!order) return '无'; |
|
||||
let orderStatus = parseInt(order.order_status); |
|
||||
let payStatus = parseInt(order.pay_status); |
|
||||
let shippingStatus = parseInt(order.shipping_status); |
|
||||
|
|
||||
if (orderStatus === 1 && payStatus === 1) { |
|
||||
return '付款确认中' |
|
||||
} else if (orderStatus === 1 && payStatus === 2) { |
|
||||
// 物流状态
|
|
||||
let shippingStatusText; |
|
||||
if (order.shipping_type === 1) { |
|
||||
shippingStatusText = ['待提货', '待提货', '已收货', '正在退货中']; |
|
||||
} else { |
|
||||
shippingStatusText = ['待发货', '待收货', '已收货', '正在退货中']; |
|
||||
} |
|
||||
return shippingStatusText[shippingStatus] || '已付款'; |
|
||||
} |
|
||||
|
|
||||
const orderStatusText = ['未确认', '待付款', '已取消', '申请退款中', '已关闭', '已退款', '已完成']; |
|
||||
|
|
||||
return orderStatusText[orderStatus] || '未知'; |
|
||||
} |
|
@ -1,206 +0,0 @@ |
|||||
import React from "react"; |
|
||||
import PropTypes from "prop-types"; |
|
||||
import styled from "styled-components"; |
|
||||
import ReactDOM from "react-dom"; |
|
||||
import GoodsCard from './GoodsCard' |
|
||||
import Card from './Card' |
|
||||
import getOrderStatus from './getOrderStatus' |
|
||||
import {Collapse} from 'antd'; |
|
||||
const Panel = Collapse.Panel; |
|
||||
|
|
||||
export default class DetailDisplay extends React.Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
this.state = {}; |
|
||||
} |
|
||||
|
|
||||
static defaultProps = {}; |
|
||||
|
|
||||
static propTypes = {}; |
|
||||
|
|
||||
render() { |
|
||||
const {id, order_sn, goodsList, consignee, tel, created_at, pay_at, |
|
||||
goods_fee, shipping_fee, pay_fee, discount_money, remark, |
|
||||
postscript, pay_id, pay_type, shipping_type, goods_amount, province, city, |
|
||||
district, address, shipping_sn, shipping_detail_link |
|
||||
} = order_info; |
|
||||
|
|
||||
let userInfo = [ |
|
||||
{ |
|
||||
label: '备注', |
|
||||
value: postscript, |
|
||||
}, |
|
||||
{ |
|
||||
label: '配送方式', |
|
||||
value: shipping_type, |
|
||||
}, |
|
||||
{ |
|
||||
label: '支付来源', |
|
||||
value: pay_id, |
|
||||
}, |
|
||||
{ |
|
||||
label: '支付方式', |
|
||||
value: pay_type, |
|
||||
}, |
|
||||
]; |
|
||||
|
|
||||
if (shipping_type === '快递配送') { |
|
||||
userInfo = [ |
|
||||
{ |
|
||||
label: '地区', |
|
||||
value: province + city + district, |
|
||||
}, |
|
||||
{ |
|
||||
label: '详细地址', |
|
||||
value: address, |
|
||||
}, |
|
||||
{ |
|
||||
label: '联系人', |
|
||||
value: consignee, |
|
||||
}, |
|
||||
{ |
|
||||
label: '联系电话', |
|
||||
value: tel, |
|
||||
}, |
|
||||
...userInfo |
|
||||
] |
|
||||
} else { |
|
||||
userInfo = [ |
|
||||
{ |
|
||||
label: '提货人', |
|
||||
value: consignee, |
|
||||
}, |
|
||||
{ |
|
||||
label: '联系电话', |
|
||||
value: tel, |
|
||||
}, |
|
||||
...userInfo |
|
||||
]; |
|
||||
} |
|
||||
return ( |
|
||||
<Root> |
|
||||
<CardList> |
|
||||
<Card |
|
||||
icon='info' |
|
||||
title='基本信息' |
|
||||
color='#55cbb8' |
|
||||
data={[ |
|
||||
{ |
|
||||
label: '订单状态', |
|
||||
value: getOrderStatus(order_info), |
|
||||
}, |
|
||||
{ |
|
||||
label: '订单号', |
|
||||
value: order_sn, |
|
||||
}, |
|
||||
{ |
|
||||
label: '发货单号', |
|
||||
value: shipping_sn, |
|
||||
link: { |
|
||||
url: shipping_detail_link, |
|
||||
text: '查看' |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
label: '数量', |
|
||||
value: goods_amount, |
|
||||
},{ |
|
||||
label: '下单时间', |
|
||||
value: created_at, |
|
||||
type: 'time' |
|
||||
}, |
|
||||
{ |
|
||||
label: '支付时间', |
|
||||
value: pay_at, |
|
||||
type: 'time' |
|
||||
}, |
|
||||
]} |
|
||||
/> |
|
||||
<Card |
|
||||
icon='contacts' |
|
||||
title='客户信息' |
|
||||
color='#6254a6' |
|
||||
data={userInfo} |
|
||||
/> |
|
||||
<Card |
|
||||
icon='money' |
|
||||
title='价格' |
|
||||
color='#ef5e5c' |
|
||||
data={[ |
|
||||
{ |
|
||||
label: '商品价格', |
|
||||
value: goods_fee, |
|
||||
type: 'price' |
|
||||
}, |
|
||||
{ |
|
||||
label: '运费', |
|
||||
value: shipping_fee, |
|
||||
type: 'price', |
|
||||
prefix: '+' |
|
||||
}, |
|
||||
{ |
|
||||
label: '优惠金额', |
|
||||
value: discount_money, |
|
||||
type: 'price', |
|
||||
prefix: '-' |
|
||||
}, |
|
||||
{ |
|
||||
label: '优惠内容', |
|
||||
value: remark, |
|
||||
}, |
|
||||
{ |
|
||||
label: '实付金额', |
|
||||
value: pay_fee, |
|
||||
type: 'price' |
|
||||
}, |
|
||||
]} |
|
||||
/> |
|
||||
</CardList> |
|
||||
<GoodsArea> |
|
||||
<Collapse> |
|
||||
<Panel header="商品列表" key="1"> |
|
||||
<GoodsList> |
|
||||
{goodsList.map(goodsInfo => ( |
|
||||
<GoodsCard key={goodsInfo.id} goodsInfo={goodsInfo} /> |
|
||||
))} |
|
||||
</GoodsList> |
|
||||
</Panel> |
|
||||
</Collapse> |
|
||||
</GoodsArea> |
|
||||
</Root> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const Root = styled.div`
|
|
||||
|
|
||||
`;
|
|
||||
|
|
||||
const GoodsList = styled.div`
|
|
||||
|
|
||||
`;
|
|
||||
|
|
||||
const CardList = styled.div`
|
|
||||
display: flex; |
|
||||
flex-wrap: wrap; |
|
||||
margin: 10px -10px; |
|
||||
`;
|
|
||||
|
|
||||
const GoodsArea = styled.div`
|
|
||||
max-width: 500px; |
|
||||
margin-bottom: 20px; |
|
||||
.ant-collapse { |
|
||||
border: none; |
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, .1); |
|
||||
} |
|
||||
.ant-collapse-header { |
|
||||
background: #fbf5e8; |
|
||||
color: #573e07!important; |
|
||||
border: none; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
ReactDOM.render( |
|
||||
<DetailDisplay />, |
|
||||
document.getElementById('detail-display') |
|
||||
); |
|
@ -1,171 +0,0 @@ |
|||||
import React, {useState} from 'react' |
|
||||
import ReactDOM from 'react-dom' |
|
||||
import {Table, Select, InputNumber, Button, message, LocaleProvider} from "antd"; |
|
||||
import styled from 'styled-components' |
|
||||
import produce from 'immer' |
|
||||
import axios from 'axios' |
|
||||
import zhCN from 'antd/lib/locale-provider/zh_CN'; |
|
||||
import queryString from 'qs'; |
|
||||
import '../utils/ajax' |
|
||||
|
|
||||
const {allSku, existingSku} = window.data; |
|
||||
|
|
||||
const Option = Select.Option; |
|
||||
|
|
||||
const Fragment = React.Fragment; |
|
||||
|
|
||||
const skuColumnsData = [ |
|
||||
{ |
|
||||
key: 'id', |
|
||||
title: '规格', |
|
||||
type: 'select' |
|
||||
}, |
|
||||
{ |
|
||||
key: 'price', |
|
||||
title: '价格', |
|
||||
type: 'input', |
|
||||
isPrice: true, |
|
||||
precision: 2, |
|
||||
min: 0 |
|
||||
}, |
|
||||
{ |
|
||||
key: 'stock', |
|
||||
title: '库存(-1 为不限库存)', |
|
||||
type: 'input', |
|
||||
precision: 0, |
|
||||
min: -1 |
|
||||
} |
|
||||
]; |
|
||||
|
|
||||
function SkuTable({ data, allSkuOptions, onChangeItem, onDeleteItem }) { |
|
||||
const skuColumns = skuColumnsData.map(({key, title, type, precision, min, isPrice = false}, index) => ({ |
|
||||
title, |
|
||||
dataIndex: index, |
|
||||
key, |
|
||||
align: 'center', |
|
||||
render: (value, _, rowIndex) => ( |
|
||||
<Fragment> |
|
||||
{type === 'select' ? ( |
|
||||
<Select |
|
||||
value={data[rowIndex][key]} |
|
||||
style={{ width: '100%', minWidth: '200px' }} |
|
||||
onChange={newValue => onChangeItem(rowIndex, key, Number(newValue))} |
|
||||
> |
|
||||
{allSkuOptions.filter(i => data.every((j, index) => index === rowIndex || i.id !== j.id)) |
|
||||
.map(({ id, name }) => <Option key={id} value={id}>{name}</Option>)} |
|
||||
</Select> |
|
||||
) : ( |
|
||||
<InputNumber |
|
||||
type='number' |
|
||||
value={data[rowIndex][key]} |
|
||||
style={{ width: '100%', minWidth: '50px' }} |
|
||||
precision={precision} |
|
||||
min={min} |
|
||||
onChange={value => onChangeItem(rowIndex, key, value)} |
|
||||
/> |
|
||||
)} |
|
||||
</Fragment> |
|
||||
) |
|
||||
})); |
|
||||
|
|
||||
const columns = [ |
|
||||
...skuColumns, |
|
||||
{ |
|
||||
key: 'delete', |
|
||||
title: '操作', |
|
||||
render: (_1, _2, index) => <a href="javascript:void(0);" onClick={() => onDeleteItem(index)}>删除</a> |
|
||||
} |
|
||||
]; |
|
||||
|
|
||||
const dataSource = data.map((item, index) => ({ key: index, ...item })); |
|
||||
|
|
||||
return ( |
|
||||
<Fragment> |
|
||||
<Table dataSource={dataSource} columns={columns} pagination={false} /> |
|
||||
</Fragment> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
const allSkuOptions = allSku.map(({ id, value}) => ({ id, name: value })); |
|
||||
|
|
||||
function App() { |
|
||||
const skuId = queryString.parse(location.search.substr(1)).id; |
|
||||
const [data, setData] = useState(existingSku.map(({id, value, ...others}) => ({ |
|
||||
id: allSku.find(i => i.value === value).id, |
|
||||
...others |
|
||||
}))); |
|
||||
|
|
||||
function setItem(skuIndex, key, value) { |
|
||||
setData(produce(data, draftState => { |
|
||||
draftState[skuIndex][key] = value; |
|
||||
|
|
||||
if (key === 'id') { |
|
||||
const {price, stock} = allSku.find(({id}) => id === value); |
|
||||
draftState[skuIndex].price = price; |
|
||||
draftState[skuIndex].stock = stock; |
|
||||
} |
|
||||
})); |
|
||||
} |
|
||||
|
|
||||
function deleteItem(index) { |
|
||||
setData(produce(data, draftState => { |
|
||||
draftState.splice(index, 1) |
|
||||
})); |
|
||||
} |
|
||||
|
|
||||
function addItem() { |
|
||||
setData([...data, { |
|
||||
id: null, |
|
||||
price: null, |
|
||||
stock: null |
|
||||
}]) |
|
||||
} |
|
||||
|
|
||||
async function save() { |
|
||||
let errorMessage = '请填写完整的信息'; |
|
||||
|
|
||||
const isAllDataCorrect = data.every(item => { |
|
||||
return Object.keys(item) |
|
||||
.every(key => item[key] !== undefined && item[key] !== null && item[key] !== ''); |
|
||||
}); |
|
||||
|
|
||||
if (isAllDataCorrect) { |
|
||||
await axios.post('update-sku', { id: skuId, data }); |
|
||||
message.success('保存成功'); |
|
||||
history.back(); |
|
||||
} else { |
|
||||
message.error(errorMessage) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<Wrapper> |
|
||||
<SkuTable |
|
||||
data={data} |
|
||||
allSkuOptions={allSkuOptions} |
|
||||
onChangeItem={setItem} |
|
||||
onDeleteItem={deleteItem} |
|
||||
/> |
|
||||
<ButtonGroup> |
|
||||
<Button type='primary' onClick={addItem}>新增一行</Button> |
|
||||
<Button type='primary' style={{ marginLeft: '20px' }} onClick={save}>保存</Button> |
|
||||
<Button style={{ marginLeft: '20px' }} onClick={() => history.back()}>返回</Button> |
|
||||
</ButtonGroup> |
|
||||
</Wrapper> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
const ButtonGroup = styled.div`
|
|
||||
margin-top: 20px; |
|
||||
`;
|
|
||||
|
|
||||
const Wrapper = styled.div`
|
|
||||
|
|
||||
`;
|
|
||||
|
|
||||
ReactDOM.render( |
|
||||
<LocaleProvider locale={zhCN}> |
|
||||
<App /> |
|
||||
</LocaleProvider>, |
|
||||
document.getElementById('app') |
|
||||
); |
|
@ -1,185 +0,0 @@ |
|||||
import React from "react"; |
|
||||
import PropTyps from 'prop-types' |
|
||||
import ClassNames from 'classnames' |
|
||||
import style from '!!to-string-loader!css-loader!resolve-url-loader!sass-loader?sourceMap!./index.scss' |
|
||||
|
|
||||
/** |
|
||||
* 计算对角线长度 |
|
||||
* @param x |
|
||||
* @param y |
|
||||
* @returns {number} |
|
||||
*/ |
|
||||
function getDiagonalLength(x, y) { |
|
||||
return Math.sqrt(x * x + y * y); |
|
||||
} |
|
||||
|
|
||||
export default class Movable extends React.Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
|
|
||||
this.refMovable = React.createRef(); |
|
||||
|
|
||||
this.isMouseDown = false; |
|
||||
this.isResizeHandleMouseDown = false; |
|
||||
this.recordedPosX = 0; |
|
||||
this.recordedPosY = 0; |
|
||||
this.state = { |
|
||||
active: false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static propTypes = { |
|
||||
posX: PropTyps.number, |
|
||||
posY: PropTyps.number, |
|
||||
width: PropTyps.number, |
|
||||
height: PropTyps.number, |
|
||||
scale: PropTyps.number, |
|
||||
canResize: PropTyps.bool, |
|
||||
keepAspectRatio: PropTyps.bool, |
|
||||
resizeHandlerSize: PropTyps.number, |
|
||||
onPosChange: PropTyps.func, |
|
||||
onSizeChange: PropTyps.func |
|
||||
}; |
|
||||
|
|
||||
static defaultProps = { |
|
||||
posX: 0, |
|
||||
posY: 0, |
|
||||
size: null, |
|
||||
scale: 1, |
|
||||
canResize: true, |
|
||||
keepAspectRatio: true, |
|
||||
resizeHandlerSize: 10, |
|
||||
onPosChange: () => {}, |
|
||||
onSizeChange: () => {} |
|
||||
}; |
|
||||
|
|
||||
componentWillMount() { |
|
||||
window.addEventListener('mousedown', (e) => { |
|
||||
this.setState({ |
|
||||
active: false |
|
||||
}); |
|
||||
}, true); |
|
||||
window.addEventListener('mouseup', (e) => { |
|
||||
this.handleMouseUp(e) |
|
||||
}); |
|
||||
window.addEventListener('mousemove', (e) => { |
|
||||
this.handleMouseMove(e) |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
savePosition = (e) => { |
|
||||
this.recordedPosX = e.clientX; |
|
||||
this.recordedPosY = e.clientY; |
|
||||
}; |
|
||||
|
|
||||
handleMouseMove = (e) => { |
|
||||
if (this.isResizeHandleMouseDown) { |
|
||||
const {width, height, scale, keepAspectRatio, onSizeChange} = this.props; |
|
||||
if (keepAspectRatio) { |
|
||||
// 保持横纵比缩放
|
|
||||
// 参考 Photoshop 等比缩放的交互,往左和上移动视为减小大小,往右和下移动视为增加大小
|
|
||||
|
|
||||
|
|
||||
// const {x, y} = this.refMovable.current.getBoundingClientRect();
|
|
||||
// const ratioX = e.clientX + this.recordedPosX / width;
|
|
||||
// const ratioY = e.clientY - height / this.recordedPosY - height;
|
|
||||
|
|
||||
// 具体实现:先计算 X 和 Y 方向的移动距离,屏幕左上角为原点,右下角方向为正
|
|
||||
const distanceX = (e.clientX - this.recordedPosX) * scale; |
|
||||
const distanceY = (e.clientY - this.recordedPosY) * scale; |
|
||||
|
|
||||
// 取X和Y两个方向距离的叠加距离,
|
|
||||
// 可能全为正(鼠标往右下角移动)、全为负(鼠标往左上角移动)、正负叠加抵消(鼠标往左下角或右上角移动)
|
|
||||
const compoundDistance = distanceX + distanceY; |
|
||||
|
|
||||
// 计算取X和Y两个方向距离的叠加距离的对角线距离
|
|
||||
const compoundDiagonalDistance = getDiagonalLength(compoundDistance, compoundDistance); |
|
||||
|
|
||||
// 鼠标本次对角线方向的直接移动距离
|
|
||||
const diagonalDistance = getDiagonalLength(distanceX, distanceY); |
|
||||
|
|
||||
// 取叠加距离和直接距离的最小值,并附上正负
|
|
||||
const diagonalResult = (compoundDistance >= 0 ? 1 : -1) * Math.min(compoundDiagonalDistance, diagonalDistance); |
|
||||
|
|
||||
// const originalDiagonal = getDiagonalLength(width, height);
|
|
||||
|
|
||||
// const ratio = diagonalResult / originalDiagonal;
|
|
||||
// const ratio = Math.min(ratioX, ratioY);
|
|
||||
// const ratio = getDiagonalLength(ratioX, ratioY);
|
|
||||
|
|
||||
onSizeChange(width + diagonalResult, height + diagonalResult); |
|
||||
} else { |
|
||||
// 不保持横纵比缩放
|
|
||||
const distanceX = (e.clientX - this.recordedPosX) * scale; |
|
||||
const distanceY = (e.clientY - this.recordedPosY) * scale; |
|
||||
|
|
||||
onSizeChange(width + distanceX, height + distanceY); |
|
||||
} |
|
||||
this.savePosition(e); |
|
||||
} if (this.isMouseDown) { |
|
||||
const {posX, posY, scale, onPosChange} = this.props; |
|
||||
const distanceX = posX + (e.clientX - this.recordedPosX) / scale; |
|
||||
const distanceY = posY + (e.clientY - this.recordedPosY) / scale; |
|
||||
onPosChange(distanceX, distanceY); |
|
||||
this.savePosition(e); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
handleMouseDown = (e) => { |
|
||||
this.isMouseDown = true; |
|
||||
this.isResizeHandleMouseDown = false; |
|
||||
this.savePosition(e); |
|
||||
this.setState({ |
|
||||
active: true |
|
||||
}); |
|
||||
}; |
|
||||
|
|
||||
handleResizeMouseDown = (e) => { |
|
||||
this.isMouseDown = false; |
|
||||
this.isResizeHandleMouseDown = true; |
|
||||
this.savePosition(e); |
|
||||
this.setState({ |
|
||||
active: true |
|
||||
}); |
|
||||
document.body.style.cursor = 'se-resize'; |
|
||||
|
|
||||
e.stopPropagation(); |
|
||||
e.preventDefault(); |
|
||||
}; |
|
||||
|
|
||||
handleMouseUp = () => { |
|
||||
this.isMouseDown = false; |
|
||||
if (this.isResizeHandleMouseDown) { |
|
||||
this.isResizeHandleMouseDown = false; |
|
||||
document.body.style.cursor = 'default'; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
const {active} = this.state; |
|
||||
const {posX, posY, width, height, canResize, resizeHandlerSize, children} = this.props; |
|
||||
|
|
||||
return ( |
|
||||
<div |
|
||||
className={ClassNames('movable-object', {active})} |
|
||||
style={{ |
|
||||
left: `${posX}px`, |
|
||||
top: `${posY}px`, |
|
||||
width: `${width}px`, |
|
||||
height: `${height}px`, |
|
||||
}} |
|
||||
draggable="false" |
|
||||
onMouseDown={this.handleMouseDown} |
|
||||
ref={this.refMovable} |
|
||||
> |
|
||||
<style>{style}</style> |
|
||||
<div |
|
||||
className="resize-handler" |
|
||||
style={{width: `${resizeHandlerSize}px`, height: `${resizeHandlerSize}px`}} |
|
||||
onMouseDown={this.handleResizeMouseDown} |
|
||||
/> |
|
||||
{children} |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
} |
|
@ -1,15 +0,0 @@ |
|||||
.movable-object { |
|
||||
position: absolute; |
|
||||
border: 2px solid transparent; |
|
||||
&.active { |
|
||||
border-color: #35c7ff; |
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, .3); |
|
||||
} |
|
||||
.resize-handler { |
|
||||
position: absolute; |
|
||||
right: 0; |
|
||||
bottom: 0; |
|
||||
cursor: se-resize; |
|
||||
user-select: none; |
|
||||
} |
|
||||
} |
|
@ -1,570 +0,0 @@ |
|||||
import React, {useState} from "react"; |
|
||||
import styled from "styled-components"; |
|
||||
import {LocaleProvider, Form, Radio, Input, Button, message, Icon, Popover, Modal} from 'antd'; |
|
||||
import ReactDOM from "react-dom"; |
|
||||
import zhCN from "antd/lib/locale-provider/zh_CN"; |
|
||||
import ClassNames from 'classnames' |
|
||||
import RcUpload from "rc-upload"; |
|
||||
import axios from 'axios' |
|
||||
import qs from 'qs'; |
|
||||
import '../utils/ajax' |
|
||||
import * as rasterizeHTML from 'rasterizehtml' |
|
||||
import previewStyle from '!!to-string-loader!css-loader!resolve-url-loader!sass-loader?sourceMap!./preview.scss' |
|
||||
import {ChromePicker} from 'react-color' |
|
||||
import Movable from './Movable' |
|
||||
import {getLuminance} from 'polished' |
|
||||
import { debounce } from 'throttle-debounce'; |
|
||||
|
|
||||
const RadioGroup = Radio.Group; |
|
||||
|
|
||||
const {data, appName, qrCodeForWeapp, qrCodeForH5, errorMsg, |
|
||||
dataOfDistribution, csrf, isDistributionPage} = window; |
|
||||
|
|
||||
const codeType = { |
|
||||
weapp: { |
|
||||
imageUrl: qrCodeForWeapp, |
|
||||
text: '小程序码' |
|
||||
}, |
|
||||
h5: { |
|
||||
imageUrl: qrCodeForH5, |
|
||||
text: '二维码' |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
function SizeChangeForm({defaultWidth, defaultHeight, onChange}) { |
|
||||
const [width, setWidth] = useState(defaultWidth); |
|
||||
const [height, setHeight] = useState(defaultHeight); |
|
||||
|
|
||||
const handleChange = debounce(300, onChange); |
|
||||
|
|
||||
return ( |
|
||||
<InputGroup> |
|
||||
<Input |
|
||||
value={width} |
|
||||
placeholder='宽' |
|
||||
onChange={e => { |
|
||||
const newValue = e.target.value; |
|
||||
setWidth(newValue); |
|
||||
handleChange(newValue, height); |
|
||||
}} |
|
||||
/> |
|
||||
X |
|
||||
<Input |
|
||||
value={height} |
|
||||
placeholder='高' |
|
||||
onChange={e => { |
|
||||
const newValue = e.target.value; |
|
||||
setHeight(newValue); |
|
||||
handleChange(width, newValue); |
|
||||
}} |
|
||||
/> |
|
||||
</InputGroup> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
class Spread extends React.Component { |
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
|
|
||||
this.previewRef = React.createRef(); |
|
||||
|
|
||||
const aspectRatio = 2; |
|
||||
|
|
||||
const refWidth = 300; |
|
||||
const refHeight = refWidth * aspectRatio; |
|
||||
|
|
||||
const exportWidth = 1080; |
|
||||
const exportHeight = exportWidth * aspectRatio; |
|
||||
|
|
||||
if (isDistributionPage) { |
|
||||
const forceData = { |
|
||||
refWidth, |
|
||||
refHeight, |
|
||||
exportWidth, |
|
||||
exportHeight |
|
||||
}; |
|
||||
|
|
||||
this.state = { |
|
||||
mode: 'custom', |
|
||||
type: 'weapp', |
|
||||
refWidth, |
|
||||
refHeight, |
|
||||
exportWidth, |
|
||||
exportHeight, |
|
||||
imageUrl: '/img/background-for-qrcode.png', |
|
||||
title: appName, |
|
||||
description: '这里放入描述文字', |
|
||||
qrCodePos: { |
|
||||
x: 96.25, |
|
||||
y: 361.25 |
|
||||
}, |
|
||||
qrCodeSize: 109.46274169979692, |
|
||||
backgroundColor: '#fff', |
|
||||
...forceData, |
|
||||
...dataOfDistribution, |
|
||||
}; |
|
||||
} else { |
|
||||
const match = document.title.match(/(.+)-页面推广/); |
|
||||
if (match && match[1]) { |
|
||||
this.pageTitle = match[1] |
|
||||
} |
|
||||
|
|
||||
this.state = { |
|
||||
mode: 'template', |
|
||||
type: 'weapp', |
|
||||
refWidth, |
|
||||
refHeight, |
|
||||
exportWidth, |
|
||||
exportHeight, |
|
||||
imageUrl: '/img/logo-kcshop.png', |
|
||||
title: appName, |
|
||||
description: '这里放入描述文字', |
|
||||
qrCodePos: { |
|
||||
x: 0, |
|
||||
y: 0 |
|
||||
}, |
|
||||
qrCodeSize: 100, |
|
||||
backgroundColor: '#fff', |
|
||||
...data |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
componentDidMount() { |
|
||||
if (isDistributionPage) { |
|
||||
this.showCustomPosterTip() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
changeType = (e) => { |
|
||||
this.setState({type: e.target.value}) |
|
||||
}; |
|
||||
|
|
||||
changeMode = (e) => { |
|
||||
const mode = e.target.value; |
|
||||
this.setState({mode}); |
|
||||
if (mode === 'custom') { |
|
||||
this.showCustomPosterTip(); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
showCustomPosterTip = () => { |
|
||||
if (localStorage.getItem('hasShowCustomPosterTip') !== '1') { |
|
||||
const {type} = this.state; |
|
||||
Modal.info({ |
|
||||
title: '自定义模式操作说明', |
|
||||
content: `拖动左侧海报内的${codeType[type].text}可以改变其位置,点击让其激活后拖动右下角区域可以改变其大小` |
|
||||
}); |
|
||||
localStorage.setItem('hasShowCustomPosterTip', '1'); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
handleChange = ({fileList}) => this.setState({fileList}); |
|
||||
|
|
||||
handleInputChange = (name, e) => { |
|
||||
this.setState({[name]: e.target.value}) |
|
||||
}; |
|
||||
|
|
||||
handleImageChange = (data) => { |
|
||||
if (data && (data.status)) { |
|
||||
this.setState({imageUrl: data.filename}) |
|
||||
} else if (data.info) { |
|
||||
message.error(`上传失败:${data.info}`) |
|
||||
} else if (!data.status) { |
|
||||
message.error(`上传失败,请稍后重试`) |
|
||||
} else { |
|
||||
message.error(`上传失败:${JSON.stringify(data)}`) |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
downloadFile = (url, filename = '文件') => { |
|
||||
const downloadElement = document.createElement('a'); |
|
||||
downloadElement.download = filename; |
|
||||
downloadElement.href = url; |
|
||||
downloadElement.click(); |
|
||||
}; |
|
||||
|
|
||||
downloadPoster = () => { |
|
||||
if (this.previewRef && this.previewRef.current) { |
|
||||
const {refWidth, refHeight, exportWidth, exportHeight} = this.state; |
|
||||
|
|
||||
const canvas = document.createElement('canvas'); |
|
||||
canvas.width = exportWidth; |
|
||||
canvas.height = exportHeight; |
|
||||
|
|
||||
const ctx = canvas.getContext('2d'); |
|
||||
ctx.scale(exportWidth / refWidth, exportHeight / refHeight); |
|
||||
|
|
||||
rasterizeHTML.drawHTML(this.previewRef.current.outerHTML, canvas).then(() => { |
|
||||
this.downloadFile(canvas.toDataURL('image/png'), `【${this.pageTitle}】推广海报`); |
|
||||
}) |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
downloadQRCode = () => { |
|
||||
const {type} = this.state; |
|
||||
this.downloadFile(codeType[type].imageUrl, `【${this.pageTitle}】${codeType[type].text}`) |
|
||||
}; |
|
||||
|
|
||||
savePoster = () => { |
|
||||
const url = isDistributionPage ? '' : '/page-layout/spread-setting'; |
|
||||
axios.post(url, { |
|
||||
'_csrf-api': csrf, |
|
||||
id: qs.parse(location.search.substr(1)).id, |
|
||||
content: this.state, |
|
||||
}).then(() => { |
|
||||
message.success('保存成功'); |
|
||||
}) |
|
||||
}; |
|
||||
|
|
||||
handlePosChange = (x, y) => { |
|
||||
this.setState({ |
|
||||
qrCodePos: {x, y} |
|
||||
}) |
|
||||
}; |
|
||||
|
|
||||
handleSizeChange = (size) => { |
|
||||
this.setState({ |
|
||||
qrCodeSize: size |
|
||||
}) |
|
||||
}; |
|
||||
|
|
||||
handleChangeColor = (color) => { |
|
||||
this.setState({ |
|
||||
backgroundColor: color.hex |
|
||||
}) |
|
||||
}; |
|
||||
|
|
||||
changePosterSize = (width, height) => { |
|
||||
const {refWidth, refHeight, qrCodePos} = this.state; |
|
||||
const aspectRatio = height / width; |
|
||||
const newRefHeight = refWidth * aspectRatio; |
|
||||
|
|
||||
this.setState({ |
|
||||
exportWidth: width, |
|
||||
exportHeight: height, |
|
||||
refHeight: newRefHeight, |
|
||||
qrCodePos: { |
|
||||
x: qrCodePos.x, |
|
||||
y: qrCodePos.y / refHeight * newRefHeight |
|
||||
} |
|
||||
}) |
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
const {mode, type, imageUrl, title, description, exportWidth, exportHeight, qrCodePos, refWidth, |
|
||||
qrCodeSize, backgroundColor} = this.state; |
|
||||
const formItemLayout = { |
|
||||
labelCol: {span: 4}, |
|
||||
wrapperCol: {span: 20}, |
|
||||
}; |
|
||||
const buttonItemLayout = { |
|
||||
wrapperCol: {span: 16, offset: 3}, |
|
||||
}; |
|
||||
|
|
||||
const aspectRatio = exportHeight / exportWidth; |
|
||||
const refHeight = refWidth * aspectRatio; |
|
||||
|
|
||||
return ( |
|
||||
<Root> |
|
||||
{errorMsg ? ( |
|
||||
<Error> |
|
||||
<Icon type="exclamation-circle" theme="filled" style={{fontSize: '6em'}}/> |
|
||||
<ErrorText>{errorMsg}</ErrorText> |
|
||||
</Error> |
|
||||
) : ( |
|
||||
<Container> |
|
||||
<PreviewWrapper style={{width: `${refWidth * .8 + 40}px`, height: `${refHeight * .8}px`}}> |
|
||||
<PreviewContainer style={{width: `${refWidth + 4}px`, height: `${refHeight + 4}px`}}> |
|
||||
<div |
|
||||
ref={this.previewRef} |
|
||||
className={ClassNames('preview', `mode-${mode}`)} |
|
||||
style={{ |
|
||||
width: `${refWidth}px`, |
|
||||
height: `${refHeight}px`, |
|
||||
backgroundColor |
|
||||
}} |
|
||||
> |
|
||||
<style>{previewStyle}</style> |
|
||||
{mode === 'template' ? ( |
|
||||
<> |
|
||||
<div className='preview__main'> |
|
||||
<div className="preview__image-wrapper"> |
|
||||
<img className='preview__image' src={imageUrl}/> |
|
||||
</div> |
|
||||
<div className='preview__info'> |
|
||||
<h1 className='preview__title'>{title}</h1> |
|
||||
<div className='preview__description'>{description}</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div className='preview__footer'> |
|
||||
<img className='preview__circle-image' src={imageUrl}/> |
|
||||
<div className='preview__footer-info'> |
|
||||
<h4 className='preview__app-name'>{appName}</h4> |
|
||||
<div className='preview__tip'>扫描或长按{codeType[type].text}</div> |
|
||||
</div> |
|
||||
<img className='preview__qr-code' src={codeType[type].imageUrl}/> |
|
||||
</div> |
|
||||
</> |
|
||||
) : ( |
|
||||
<> |
|
||||
<img |
|
||||
src={imageUrl} |
|
||||
className={ClassNames('background', { |
|
||||
'distribution': isDistributionPage |
|
||||
})} |
|
||||
/> |
|
||||
<Movable |
|
||||
posX={qrCodePos.x} |
|
||||
posY={qrCodePos.y} |
|
||||
width={qrCodeSize} |
|
||||
height={qrCodeSize} |
|
||||
scale={0.8} |
|
||||
keepAspectRatio |
|
||||
onPosChange={this.handlePosChange} |
|
||||
onSizeChange={this.handleSizeChange} |
|
||||
> |
|
||||
<img |
|
||||
className='preview__qr-code' |
|
||||
src={codeType[type].imageUrl} |
|
||||
draggable="false" |
|
||||
/> |
|
||||
</Movable> |
|
||||
</> |
|
||||
)} |
|
||||
</div> |
|
||||
</PreviewContainer> |
|
||||
</PreviewWrapper> |
|
||||
<Editor> |
|
||||
<Form layout='horizontal'> |
|
||||
{!isDistributionPage && ( |
|
||||
<Form.Item label='模式' {...formItemLayout}> |
|
||||
<RadioGroup onChange={this.changeMode} value={mode}> |
|
||||
<Radio value='template'>默认模板</Radio> |
|
||||
<Radio value='custom'>自定义</Radio> |
|
||||
</RadioGroup> |
|
||||
</Form.Item> |
|
||||
)} |
|
||||
<Form.Item label='平台' {...formItemLayout}> |
|
||||
<RadioGroup onChange={this.changeType} value={type}> |
|
||||
<Radio value='weapp'>小程序</Radio> |
|
||||
<Radio value='h5'>H5</Radio> |
|
||||
</RadioGroup> |
|
||||
</Form.Item> |
|
||||
<Form.Item label='输出尺寸' {...formItemLayout}> |
|
||||
<SizeChangeForm |
|
||||
defaultWidth={exportWidth} |
|
||||
defaultHeight={exportHeight} |
|
||||
onChange={this.changePosterSize} |
|
||||
/> |
|
||||
</Form.Item> |
|
||||
{mode === 'template' && ( |
|
||||
<> |
|
||||
<Form.Item label='标题' {...formItemLayout}> |
|
||||
<Input |
|
||||
value={title} |
|
||||
placeholder='请输入标题' |
|
||||
onChange={this.handleInputChange.bind(undefined, 'title')} |
|
||||
/> |
|
||||
</Form.Item> |
|
||||
<Form.Item label='描述' {...formItemLayout}> |
|
||||
<Input |
|
||||
value={description} |
|
||||
placeholder='请输入描述' |
|
||||
onChange={this.handleInputChange.bind(undefined, 'description')} |
|
||||
/> |
|
||||
</Form.Item> |
|
||||
</> |
|
||||
)} |
|
||||
<Form.Item label='图片' {...formItemLayout}> |
|
||||
<Thumbnail> |
|
||||
<RcUpload |
|
||||
action='/page-layout/image-upload' |
|
||||
accept='image/*' |
|
||||
data={{'_csrf-api': csrf}} |
|
||||
onStart={this.onStart} |
|
||||
onSuccess={this.handleImageChange} |
|
||||
> |
|
||||
<UploadedImage |
|
||||
src={imageUrl} |
|
||||
draggable="false" |
|
||||
/> |
|
||||
<ImageHandle className='image-handle'> |
|
||||
<Icon type='edit' onClick={this.handlePreview}/> |
|
||||
<div>更改图片</div> |
|
||||
</ImageHandle> |
|
||||
</RcUpload> |
|
||||
</Thumbnail> |
|
||||
<ImageTip>海报背景宽高比例 1:{aspectRatio.toFixed(2)}</ImageTip> |
|
||||
</Form.Item> |
|
||||
{!isDistributionPage && ( |
|
||||
<Form.Item label='背景颜色' {...formItemLayout}> |
|
||||
<Popover |
|
||||
content={( |
|
||||
<ColorPicker> |
|
||||
<ChromePicker |
|
||||
color={backgroundColor} |
|
||||
disableAlpha |
|
||||
style={{ |
|
||||
padding: 0, |
|
||||
boxShadow: 'none' |
|
||||
}} |
|
||||
onChange={this.handleChangeColor} |
|
||||
/> |
|
||||
</ColorPicker> |
|
||||
)} |
|
||||
trigger='click' |
|
||||
> |
|
||||
<Color |
|
||||
style={{ |
|
||||
backgroundColor, |
|
||||
color: getLuminance(backgroundColor) > 0.5 ? '#333' : '#eee' |
|
||||
}} |
|
||||
>点击选择颜色</Color> |
|
||||
</Popover> |
|
||||
</Form.Item> |
|
||||
)} |
|
||||
<Form.Item {...buttonItemLayout}> |
|
||||
{!isDistributionPage && ( |
|
||||
<> |
|
||||
<Button |
|
||||
type="primary" |
|
||||
onClick={this.downloadPoster} |
|
||||
>下载海报</Button> |
|
||||
<Button |
|
||||
type="primary" |
|
||||
onClick={this.downloadQRCode} |
|
||||
style={{margin: '20px'}} |
|
||||
>仅下载{codeType[type].text}</Button> |
|
||||
</> |
|
||||
)} |
|
||||
<Button |
|
||||
type="primary" |
|
||||
onClick={this.savePoster} |
|
||||
>保存海报布局</Button> |
|
||||
</Form.Item> |
|
||||
</Form> |
|
||||
</Editor> |
|
||||
</Container> |
|
||||
)} |
|
||||
</Root> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const Root = styled.div`
|
|
||||
`;
|
|
||||
|
|
||||
const Container = styled.div`
|
|
||||
display: flex; |
|
||||
align-items: flex-start; |
|
||||
`;
|
|
||||
|
|
||||
const Error = styled.div`
|
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
width: 100%; |
|
||||
min-height: 400px; |
|
||||
font-weight: bold; |
|
||||
color: #f5222d; |
|
||||
`;
|
|
||||
|
|
||||
const ErrorText = styled.div`
|
|
||||
margin-top: 40px; |
|
||||
font-weight: bold; |
|
||||
font-size: 1.5em; |
|
||||
`;
|
|
||||
|
|
||||
const Editor = styled.div`
|
|
||||
flex-grow: 1; |
|
||||
`;
|
|
||||
|
|
||||
const PreviewWrapper = styled.div`
|
|
||||
`;
|
|
||||
|
|
||||
const PreviewContainer = styled.div`
|
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
border: 2px solid #eee; |
|
||||
transform-origin: left top; |
|
||||
transform: scale(.8); |
|
||||
`;
|
|
||||
|
|
||||
const Thumbnail = styled.div`
|
|
||||
position: relative; |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
width: 150px; |
|
||||
height: 150px; |
|
||||
border: 2px solid #eee; |
|
||||
border-radius: 10px; |
|
||||
overflow: hidden; |
|
||||
`;
|
|
||||
|
|
||||
const UploadedImage = styled.img`
|
|
||||
max-width: 150px; |
|
||||
max-height: 150px; |
|
||||
`;
|
|
||||
|
|
||||
const ImageHandle = styled.div`
|
|
||||
position: absolute; |
|
||||
left: 0; |
|
||||
bottom: 0; |
|
||||
width: 100%; |
|
||||
height: 40px; |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
background: rgba(0, 0, 0, .5); |
|
||||
color: #fff; |
|
||||
.anticon { |
|
||||
margin-right: 10px; |
|
||||
font-size: 1.4em; |
|
||||
transition: transform .3s; |
|
||||
cursor: pointer; |
|
||||
&:active { |
|
||||
transform: scale(1.6); |
|
||||
} |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
const ImageTip = styled.div`
|
|
||||
`;
|
|
||||
|
|
||||
const ColorPicker = styled.div`
|
|
||||
.sketch-picker, .chrome-picker { |
|
||||
padding: 0!important; |
|
||||
box-shadow: none!important; |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
const Color = styled.div`
|
|
||||
width: 120px; |
|
||||
height: 40px; |
|
||||
border: 1px solid #eee; |
|
||||
border-radius: 5px; |
|
||||
text-align: center; |
|
||||
cursor: pointer; |
|
||||
`;
|
|
||||
|
|
||||
const InputGroup = styled.div`
|
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
input { |
|
||||
width: 80px!important; |
|
||||
margin: 0 10px; |
|
||||
&:first-child { |
|
||||
margin-left: 0; |
|
||||
} |
|
||||
} |
|
||||
`;
|
|
||||
|
|
||||
ReactDOM.render( |
|
||||
<LocaleProvider locale={zhCN}> |
|
||||
<Spread/> |
|
||||
</LocaleProvider>, |
|
||||
document.getElementById('spread') |
|
||||
); |
|
@ -1,354 +0,0 @@ |
|||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ |
|
||||
|
|
||||
/* Document |
|
||||
========================================================================== */ |
|
||||
|
|
||||
/** |
|
||||
* 1. Correct the line height in all browsers. |
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS. |
|
||||
*/ |
|
||||
|
|
||||
html { |
|
||||
line-height: 1.15; /* 1 */ |
|
||||
-webkit-text-size-adjust: 100%; /* 2 */ |
|
||||
} |
|
||||
|
|
||||
/* Sections |
|
||||
========================================================================== */ |
|
||||
|
|
||||
/** |
|
||||
* Remove the margin in all browsers. |
|
||||
*/ |
|
||||
|
|
||||
body { |
|
||||
margin: 0; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Render the `main` element consistently in IE. |
|
||||
*/ |
|
||||
|
|
||||
main { |
|
||||
display: block; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Correct the font size and margin on `h1` elements within `section` and |
|
||||
* `article` contexts in Chrome, Firefox, and Safari. |
|
||||
*/ |
|
||||
|
|
||||
h1 { |
|
||||
font-size: 2em; |
|
||||
margin: 0.67em 0; |
|
||||
} |
|
||||
|
|
||||
h4 { |
|
||||
font-size: 1.2em; |
|
||||
margin: 0.2em 0; |
|
||||
} |
|
||||
|
|
||||
/* Grouping content |
|
||||
========================================================================== */ |
|
||||
|
|
||||
/** |
|
||||
* 1. Add the correct box sizing in Firefox. |
|
||||
* 2. Show the overflow in Edge and IE. |
|
||||
*/ |
|
||||
|
|
||||
hr { |
|
||||
box-sizing: content-box; /* 1 */ |
|
||||
height: 0; /* 1 */ |
|
||||
overflow: visible; /* 2 */ |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers. |
|
||||
* 2. Correct the odd `em` font sizing in all browsers. |
|
||||
*/ |
|
||||
|
|
||||
pre { |
|
||||
font-family: monospace, monospace; /* 1 */ |
|
||||
font-size: 1em; /* 2 */ |
|
||||
} |
|
||||
|
|
||||
/* Text-level semantics |
|
||||
========================================================================== */ |
|
||||
|
|
||||
/** |
|
||||
* Remove the gray background on active links in IE 10. |
|
||||
*/ |
|
||||
|
|
||||
a { |
|
||||
background-color: transparent; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 1. Remove the bottom border in Chrome 57- |
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. |
|
||||
*/ |
|
||||
|
|
||||
abbr[title] { |
|
||||
border-bottom: none; /* 1 */ |
|
||||
text-decoration: underline; /* 2 */ |
|
||||
text-decoration: underline dotted; /* 2 */ |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Add the correct font weight in Chrome, Edge, and Safari. |
|
||||
*/ |
|
||||
|
|
||||
b, |
|
||||
strong { |
|
||||
font-weight: bolder; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers. |
|
||||
* 2. Correct the odd `em` font sizing in all browsers. |
|
||||
*/ |
|
||||
|
|
||||
code, |
|
||||
kbd, |
|
||||
samp { |
|
||||
font-family: monospace, monospace; /* 1 */ |
|
||||
font-size: 1em; /* 2 */ |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Add the correct font size in all browsers. |
|
||||
*/ |
|
||||
|
|
||||
small { |
|
||||
font-size: 80%; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in |
|
||||
* all browsers. |
|
||||
*/ |
|
||||
|
|
||||
sub, |
|
||||
sup { |
|
||||
font-size: 75%; |
|
||||
line-height: 0; |
|
||||
position: relative; |
|
||||
vertical-align: baseline; |
|
||||
} |
|
||||
|
|
||||
sub { |
|
||||
bottom: -0.25em; |
|
||||
} |
|
||||
|
|
||||
sup { |
|
||||
top: -0.5em; |
|
||||
} |
|
||||
|
|
||||
/* Embedded content |
|
||||
========================================================================== */ |
|
||||
|
|
||||
/** |
|
||||
* Remove the border on images inside links in IE 10. |
|
||||
*/ |
|
||||
|
|
||||
img { |
|
||||
border-style: none; |
|
||||
} |
|
||||
|
|
||||
/* Forms |
|
||||
========================================================================== */ |
|
||||
|
|
||||
/** |
|
||||
* 1. Change the font styles in all browsers. |
|
||||
* 2. Remove the margin in Firefox and Safari. |
|
||||
*/ |
|
||||
|
|
||||
button, |
|
||||
input, |
|
||||
optgroup, |
|
||||
select, |
|
||||
textarea { |
|
||||
font-family: inherit; /* 1 */ |
|
||||
font-size: 100%; /* 1 */ |
|
||||
line-height: 1.15; /* 1 */ |
|
||||
margin: 0; /* 2 */ |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Show the overflow in IE. |
|
||||
* 1. Show the overflow in Edge. |
|
||||
*/ |
|
||||
|
|
||||
button, |
|
||||
input { /* 1 */ |
|
||||
overflow: visible; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE. |
|
||||
* 1. Remove the inheritance of text transform in Firefox. |
|
||||
*/ |
|
||||
|
|
||||
button, |
|
||||
select { /* 1 */ |
|
||||
text-transform: none; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Correct the inability to style clickable types in iOS and Safari. |
|
||||
*/ |
|
||||
|
|
||||
button, |
|
||||
[type="button"], |
|
||||
[type="reset"], |
|
||||
[type="submit"] { |
|
||||
-webkit-appearance: button; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Remove the inner border and padding in Firefox. |
|
||||
*/ |
|
||||
|
|
||||
button::-moz-focus-inner, |
|
||||
[type="button"]::-moz-focus-inner, |
|
||||
[type="reset"]::-moz-focus-inner, |
|
||||
[type="submit"]::-moz-focus-inner { |
|
||||
border-style: none; |
|
||||
padding: 0; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Restore the focus styles unset by the previous rule. |
|
||||
*/ |
|
||||
|
|
||||
button:-moz-focusring, |
|
||||
[type="button"]:-moz-focusring, |
|
||||
[type="reset"]:-moz-focusring, |
|
||||
[type="submit"]:-moz-focusring { |
|
||||
outline: 1px dotted ButtonText; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Correct the padding in Firefox. |
|
||||
*/ |
|
||||
|
|
||||
fieldset { |
|
||||
padding: 0.35em 0.75em 0.625em; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 1. Correct the text wrapping in Edge and IE. |
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE. |
|
||||
* 3. Remove the padding so developers are not caught out when they zero out |
|
||||
* `fieldset` elements in all browsers. |
|
||||
*/ |
|
||||
|
|
||||
legend { |
|
||||
box-sizing: border-box; /* 1 */ |
|
||||
color: inherit; /* 2 */ |
|
||||
display: table; /* 1 */ |
|
||||
max-width: 100%; /* 1 */ |
|
||||
padding: 0; /* 3 */ |
|
||||
white-space: normal; /* 1 */ |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera. |
|
||||
*/ |
|
||||
|
|
||||
progress { |
|
||||
vertical-align: baseline; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Remove the default vertical scrollbar in IE 10+. |
|
||||
*/ |
|
||||
|
|
||||
textarea { |
|
||||
overflow: auto; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 1. Add the correct box sizing in IE 10. |
|
||||
* 2. Remove the padding in IE 10. |
|
||||
*/ |
|
||||
|
|
||||
[type="checkbox"], |
|
||||
[type="radio"] { |
|
||||
box-sizing: border-box; /* 1 */ |
|
||||
padding: 0; /* 2 */ |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome. |
|
||||
*/ |
|
||||
|
|
||||
[type="number"]::-webkit-inner-spin-button, |
|
||||
[type="number"]::-webkit-outer-spin-button { |
|
||||
height: auto; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 1. Correct the odd appearance in Chrome and Safari. |
|
||||
* 2. Correct the outline style in Safari. |
|
||||
*/ |
|
||||
|
|
||||
[type="search"] { |
|
||||
-webkit-appearance: textfield; /* 1 */ |
|
||||
outline-offset: -2px; /* 2 */ |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Remove the inner padding in Chrome and Safari on macOS. |
|
||||
*/ |
|
||||
|
|
||||
[type="search"]::-webkit-search-decoration { |
|
||||
-webkit-appearance: none; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari. |
|
||||
* 2. Change font properties to `inherit` in Safari. |
|
||||
*/ |
|
||||
|
|
||||
::-webkit-file-upload-button { |
|
||||
-webkit-appearance: button; /* 1 */ |
|
||||
font: inherit; /* 2 */ |
|
||||
} |
|
||||
|
|
||||
/* Interactive |
|
||||
========================================================================== */ |
|
||||
|
|
||||
/* |
|
||||
* Add the correct display in Edge, IE 10+, and Firefox. |
|
||||
*/ |
|
||||
|
|
||||
details { |
|
||||
display: block; |
|
||||
} |
|
||||
|
|
||||
/* |
|
||||
* Add the correct display in all browsers. |
|
||||
*/ |
|
||||
|
|
||||
summary { |
|
||||
display: list-item; |
|
||||
} |
|
||||
|
|
||||
/* Misc |
|
||||
========================================================================== */ |
|
||||
|
|
||||
/** |
|
||||
* Add the correct display in IE 10+. |
|
||||
*/ |
|
||||
|
|
||||
template { |
|
||||
display: none; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Add the correct display in IE 10. |
|
||||
*/ |
|
||||
|
|
||||
[hidden] { |
|
||||
display: none; |
|
||||
} |
|
@ -1,102 +0,0 @@ |
|||||
@import "normalize"; |
|
||||
|
|
||||
$poster-width: 300px; |
|
||||
$padding: 20px; |
|
||||
$image-size: $poster-width - $padding * 2; |
|
||||
|
|
||||
.preview { |
|
||||
overflow: hidden; |
|
||||
background: #fff; |
|
||||
&.mode-template { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: space-between; |
|
||||
align-items: center; |
|
||||
padding: 20px; |
|
||||
box-sizing: border-box; |
|
||||
.preview { |
|
||||
&__main { |
|
||||
padding: 20px; |
|
||||
background: #fff; |
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, .1); |
|
||||
} |
|
||||
&__image-wrapper { |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
} |
|
||||
&__image { |
|
||||
width: 100%; |
|
||||
height: auto; |
|
||||
} |
|
||||
&__info { |
|
||||
width: 100%; |
|
||||
margin-top: 10px; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
&__footer { |
|
||||
width: 100%; |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
align-items: center; |
|
||||
margin-top: 30px; |
|
||||
} |
|
||||
&__title { |
|
||||
|
|
||||
} |
|
||||
&__description { |
|
||||
width: 100%; |
|
||||
font-size: 14px; |
|
||||
line-height: 1.5; |
|
||||
color: #aaa; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
&__circle-image { |
|
||||
$circle-image-size: $poster-width * 0.2; |
|
||||
width: $circle-image-size; |
|
||||
height: $circle-image-size; |
|
||||
border-radius: 100%; |
|
||||
background: #fff; |
|
||||
} |
|
||||
&__footer-info { |
|
||||
border-radius: 100%; |
|
||||
margin: 0 10px; |
|
||||
} |
|
||||
&__app-name { |
|
||||
margin: 0; |
|
||||
margin-bottom: 5px; |
|
||||
} |
|
||||
&__tip { |
|
||||
font-size: 12px; |
|
||||
color: #aaa; |
|
||||
} |
|
||||
&__qr-code { |
|
||||
$size: $poster-width * 0.2; |
|
||||
width: $size; |
|
||||
height: $size; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
&.mode-custom { |
|
||||
position: relative; |
|
||||
background-repeat: no-repeat; |
|
||||
background-size: contain; |
|
||||
.background { |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
max-width: 100%; |
|
||||
max-height:100%; |
|
||||
&.distribution { |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
} |
|
||||
} |
|
||||
.preview { |
|
||||
&__qr-code { |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue