Browse Source

refactor:删除没用的react组件代码文件

wechat_public_accounts
linyaostalker 5 years ago
parent
commit
94064c819b
  1. 7855
      backend/web/custom/sku_item.178ab99e60852816d81e.js
  2. 4
      backend/web/custom/sku_item.html
  3. 30
      backend/web/src/custom-menu/AddItem.js
  4. 124
      backend/web/src/custom-menu/Editor.js
  5. 176
      backend/web/src/custom-menu/MenuItem.js
  6. 247
      backend/web/src/custom-menu/index.js
  7. 7
      backend/web/src/custom-menu/index.scss
  8. 25
      backend/web/src/custom-menu/utils.js
  9. 149
      backend/web/src/dashboard/CircleCard.js
  10. 135
      backend/web/src/dashboard/LineChart.js
  11. 53
      backend/web/src/dashboard/OverviewCard.js
  12. 40
      backend/web/src/dashboard/PieChart.js
  13. 74
      backend/web/src/dashboard/RadioGroup.js
  14. 32
      backend/web/src/dashboard/Table.js
  15. 145
      backend/web/src/dashboard/index.js
  16. 65
      backend/web/src/mini-program-management/MainDescription.js
  17. 132
      backend/web/src/mini-program-management/ManageTriers.js
  18. 38
      backend/web/src/mini-program-management/StepBar.js
  19. 22
      backend/web/src/mini-program-management/common.js
  20. 146
      backend/web/src/mini-program-management/data.js
  21. 397
      backend/web/src/mini-program-management/index.js
  22. 127
      backend/web/src/order-detail/Card.js
  23. 77
      backend/web/src/order-detail/GoodsCard.js
  24. 23
      backend/web/src/order-detail/getOrderStatus.js
  25. 206
      backend/web/src/order-detail/index.js
  26. 171
      backend/web/src/sku-for-activity/index.js
  27. 185
      backend/web/src/spread/Movable/index.js
  28. 15
      backend/web/src/spread/Movable/index.scss
  29. 570
      backend/web/src/spread/index.js
  30. 354
      backend/web/src/spread/normalize.scss
  31. 102
      backend/web/src/spread/preview.scss

7855
backend/web/custom/sku_item.178ab99e60852816d81e.js
File diff suppressed because it is too large
View File

4
backend/web/custom/sku_item.html

@ -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>

30
backend/web/src/custom-menu/AddItem.js

@ -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;
}
}
`;

124
backend/web/src/custom-menu/Editor.js

@ -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`
`;

176
backend/web/src/custom-menu/MenuItem.js

@ -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;
}
`;

247
backend/web/src/custom-menu/index.js

@ -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'));

7
backend/web/src/custom-menu/index.scss

@ -1,7 +0,0 @@
.ant-popover-inner-content {
padding: 0!important;
}
.ant-popover-arrow {
display: none!important;
}

25
backend/web/src/custom-menu/utils.js

@ -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);
}

149
backend/web/src/dashboard/CircleCard.js

@ -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};
`;

135
backend/web/src/dashboard/LineChart.js

@ -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;
}
`;

53
backend/web/src/dashboard/OverviewCard.js

@ -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>
)
}
}

40
backend/web/src/dashboard/PieChart.js

@ -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: {}
// });

74
backend/web/src/dashboard/RadioGroup.js

@ -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;
}
`;

32
backend/web/src/dashboard/Table.js

@ -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`
`;

145
backend/web/src/dashboard/index.js

@ -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')
);

65
backend/web/src/mini-program-management/MainDescription.js

@ -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;
`;

132
backend/web/src/mini-program-management/ManageTriers.js

@ -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`
`;

38
backend/web/src/mini-program-management/StepBar.js

@ -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 <></>;
}
}

22
backend/web/src/mini-program-management/common.js

@ -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;
`;

146
backend/web/src/mini-program-management/data.js

@ -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}

397
backend/web/src/mini-program-management/index.js

@ -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')
);

127
backend/web/src/order-detail/Card.js

@ -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;
}
`;

77
backend/web/src/order-detail/GoodsCard.js

@ -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;
`;

23
backend/web/src/order-detail/getOrderStatus.js

@ -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] || '未知';
}

206
backend/web/src/order-detail/index.js

@ -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')
);

171
backend/web/src/sku-for-activity/index.js

@ -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')
);

185
backend/web/src/spread/Movable/index.js

@ -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>
)
}
}

15
backend/web/src/spread/Movable/index.scss

@ -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;
}
}

570
backend/web/src/spread/index.js

@ -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')
);

354
backend/web/src/spread/normalize.scss

@ -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;
}

102
backend/web/src/spread/preview.scss

@ -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%;
}
}
}
}
Loading…
Cancel
Save