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