Browse Source

feat:后台用到的react组件包

antshop
linyaostalker 5 years ago
parent
commit
9cfff4c84c
  1. 17
      backend/web/.babelrc
  2. 26
      backend/web/.eslintrc.json
  3. 11
      backend/web/.gitignore
  4. 89
      backend/web/package.json
  5. 30
      backend/web/src/custom-menu/AddItem.js
  6. 124
      backend/web/src/custom-menu/Editor.js
  7. 176
      backend/web/src/custom-menu/MenuItem.js
  8. 247
      backend/web/src/custom-menu/index.js
  9. 7
      backend/web/src/custom-menu/index.scss
  10. 25
      backend/web/src/custom-menu/utils.js
  11. 149
      backend/web/src/dashboard/CircleCard.js
  12. 135
      backend/web/src/dashboard/LineChart.js
  13. 53
      backend/web/src/dashboard/OverviewCard.js
  14. 40
      backend/web/src/dashboard/PieChart.js
  15. 74
      backend/web/src/dashboard/RadioGroup.js
  16. 32
      backend/web/src/dashboard/Table.js
  17. 145
      backend/web/src/dashboard/index.js
  18. 1
      backend/web/src/iconfont.html
  19. 4
      backend/web/src/import.html
  20. 36
      backend/web/src/js/amazing-creator/GridEditor.js
  21. 101
      backend/web/src/js/amazing-creator/Module.js
  22. 106
      backend/web/src/js/amazing-creator/Previewer.js
  23. 16
      backend/web/src/js/amazing-creator/PropsEditor.js
  24. 29
      backend/web/src/js/amazing-creator/index.js
  25. 67
      backend/web/src/js/amazing-creator/modules/GoodsList.js
  26. 14
      backend/web/src/js/amazing-creator/modules/Swiper.js
  27. 150
      backend/web/src/js/amazing-creator/old.js
  28. 68
      backend/web/src/js/amazing-creator/store.js
  29. 65
      backend/web/src/mini-program-management/MainDescription.js
  30. 132
      backend/web/src/mini-program-management/ManageTriers.js
  31. 38
      backend/web/src/mini-program-management/StepBar.js
  32. 22
      backend/web/src/mini-program-management/common.js
  33. 146
      backend/web/src/mini-program-management/data.js
  34. 397
      backend/web/src/mini-program-management/index.js
  35. 127
      backend/web/src/order-detail/Card.js
  36. 77
      backend/web/src/order-detail/GoodsCard.js
  37. 23
      backend/web/src/order-detail/getOrderStatus.js
  38. 206
      backend/web/src/order-detail/index.js
  39. 171
      backend/web/src/sku-for-activity/index.js
  40. 313
      backend/web/src/sku-item/index.js
  41. 74
      backend/web/src/sku/SelectCell.js
  42. 157
      backend/web/src/sku/index.js
  43. 185
      backend/web/src/spread/Movable/index.js
  44. 15
      backend/web/src/spread/Movable/index.scss
  45. 570
      backend/web/src/spread/index.js
  46. 354
      backend/web/src/spread/normalize.scss
  47. 102
      backend/web/src/spread/preview.scss
  48. 83
      backend/web/src/styles/amazing-creator.scss
  49. 90
      backend/web/src/styles/content-header.scss
  50. 180
      backend/web/src/styles/dashboard.scss
  51. 183
      backend/web/src/styles/global.scss
  52. 128
      backend/web/src/styles/header.scss
  53. 7
      backend/web/src/styles/index.scss
  54. 193
      backend/web/src/styles/login.scss
  55. 201
      backend/web/src/styles/sidebar.scss
  56. 30
      backend/web/src/styles/variables.scss
  57. 301
      backend/web/src/styles/widget.scss
  58. 76
      backend/web/src/utils/ajax.js
  59. 84
      backend/web/webpack.config.js

17
backend/web/.babelrc

@ -0,0 +1,17 @@
{
"presets": [
"@babel/preset-react",
"@babel/preset-env"
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/proposal-class-properties", { "loose": true }],
"@babel/plugin-transform-runtime",
"babel-plugin-styled-components",
["import", {
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css"
}]
]
}

26
backend/web/.eslintrc.json

@ -0,0 +1,26 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"react",
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}

11
backend/web/.gitignore

@ -0,0 +1,11 @@
/index.php
/index-test.php
/robots.txt
uploads
css/umeditor/php/upload/
node_modules/
yarn-error.log
yarn.lock
package-lock.json
ueditor

89
backend/web/package.json

@ -0,0 +1,89 @@
{
"scripts": {
"dev": "webpack --mode=development --watch",
"dev:dashboard": "webpack --module dashboard --mode=development --watch",
"build:dashboard": "webpack --module dashboard --mode=production",
"dev:mini-program-management": "webpack --module mini_program_management --mode=development --watch",
"build:mini-program-management": "webpack --module mini_program_management --mode=production",
"dev:spread": "webpack --module spread --mode=development --watch",
"build:spread": "webpack --module spread --mode=production",
"dev:sku": "webpack --module sku --mode=development --watch",
"build:sku": "webpack --module sku --mode=production",
"dev:style": "webpack --module style --mode=development --watch",
"build:style": "webpack --module style --mode=production",
"dev:order-detail": "webpack --module order_detail --mode=development --watch",
"build:order-detail": "webpack --module order_detail --mode=production",
"dev:sku-item": "webpack --module sku_item --mode=development --watch",
"build:sku-item": "webpack --module sku_item --mode=production",
"dev:custom-menu": "webpack --module custom-menu --mode=development --watch",
"build:custom-menu": "webpack --module custom-menu --mode=production",
"dev:sku-for-activity": "webpack --module sku_for_activity --mode=development --watch",
"build:sku-for-activity": "webpack --module sku_for_activity --mode=production",
"build": "webpack --mode=production"
},
"dependencies": {
"@babel/core": "^7.0.1",
"@babel/preset-react": "^7.0.0",
"antd": "^3.16.5",
"axios": "^0.18.0",
"babel-loader": "^8.0.2",
"bizcharts": "^3.4.3",
"che-react-number-easing": "^0.1.2",
"classnames": "^2.2.6",
"clone": "^2.1.2",
"compare-versions": "^3.4.0",
"dayjs": "^1.8.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"fast-deep-equal": "^2.0.1",
"file-loader": "^2.0.0",
"html2canvas": "^1.0.0-alpha.12",
"immer": "^2.1.5",
"mobx": "^5.6.0",
"mobx-react": "^5.3.6",
"polished": "^2.3.3",
"prop-types": "^15.6.2",
"qs": "^6.6.0",
"rasterizehtml": "^1.3.0",
"rc-switch": "^1.8.0",
"rc-upload": "^2.6.3",
"react": "^16.8.6",
"react-beautiful-dnd": "^11.0.2",
"react-chartjs-2": "^2.7.4",
"react-circle": "^1.1.1",
"react-color": "^2.17.0",
"react-dom": "^16.8.6",
"react-grid-layout": "^0.16.6",
"react-select": "^2.1.1",
"react-sortable-hoc": "^0.8.3",
"sass-resources-loader": "^1.3.3",
"styled-components": "^4.1.3",
"throttle-debounce": "^2.1.0",
"to-string-loader": "^1.1.5",
"url-loader": "^1.1.1",
"uuid": "^3.3.2",
"webpack": "^4.29.5",
"webpack-cli": "^3.2.3"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-proposal-decorators": "^7.1.2",
"@babel/plugin-transform-runtime": "^7.4.3",
"@babel/preset-env": "^7.1.0",
"babel-plugin-import": "^1.11.0",
"babel-plugin-styled-components": "^1.10.0",
"clean-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^4.6.0",
"css-loader": "^1.0.0",
"eslint": "^5.16.0",
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-react-hooks": "^1.6.0",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.5.0",
"mobx-react-devtools": "^6.0.3",
"node-sass": "^4.9.3",
"require-context": "^1.1.0",
"resolve-url-loader": "^2.3.1",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.0"
}
}

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

@ -0,0 +1,30 @@
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

@ -0,0 +1,124 @@
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

@ -0,0 +1,176 @@
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

@ -0,0 +1,247 @@
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

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

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

@ -0,0 +1,25 @@
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

@ -0,0 +1,149 @@
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

@ -0,0 +1,135 @@
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

@ -0,0 +1,53 @@
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

@ -0,0 +1,40 @@
// 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

@ -0,0 +1,74 @@
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

@ -0,0 +1,32 @@
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

@ -0,0 +1,145 @@
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
backend/web/src/iconfont.html

@ -0,0 +1 @@
<link rel="stylesheet" href="//at.alicdn.com/t/font_827976_jtnsfqxuiaf.css">

4
backend/web/src/import.html

@ -0,0 +1,4 @@
<div id="app"></div>
<script>
var csrfToken = "<?= Yii::$app->request->csrfToken ?>";
</script>

36
backend/web/src/js/amazing-creator/GridEditor.js

@ -0,0 +1,36 @@
import React, {Component} from "react";
import GridLayout from "react-grid-layout";
import styled from 'styled-components'
import "react-grid-layout/css/styles.css";
import "react-resizable/css/styles.css";
import {inject, observer} from 'mobx-react'
import Module from './Module'
@inject('store')
@observer
export default class GridEditor extends React.Component {
onLayoutChange = (layout) => {
const {changeLayout} = this.props.store;
changeLayout(layout);
};
render() {
const {layout, attrs, add} = this.props.store;
return (
<GridLayout
layout={layout}
cols={12}
rowHeight={30}
width={300}
onLayoutChange={this.onLayoutChange}
>
{layout.map(item => (
<div key={item.i}>
<Module attr={attrs[item.i]} />
</div>
))}
</GridLayout>
)
}
}

101
backend/web/src/js/amazing-creator/Module.js

@ -0,0 +1,101 @@
import React from "react";
import styled from "styled-components";
import PropTypes from 'prop-types'
import Swiper from './modules/Swiper'
import GoodsList from './modules/GoodsList'
export default class Module extends React.Component {
onLayoutChange = (layout) => {
const {changeLayout} = this.props.store;
changeLayout(layout);
};
static defaultProps = {
layout: {},
attr: {}
};
static propTypes = {
layout: PropTypes.object,
attr: PropTypes.object
};
render() {
const {attr} = this.props;
const moduleTable = {
Swiper: <Swiper />,
GoodsList: <GoodsList title={attr.title} type={attr.type} name={attr.data} />
};
if (moduleTable[attr.component]) {
return (
<Wrapper>
{moduleTable[attr.component]}
<HoverLayer>
<EditButton className='fa fa-edit' />
</HoverLayer>
</Wrapper>
)
} else {
return (
<ErrorTip>{attr.component}</ErrorTip>
)
}
}
}
const Wrapper = styled.div`
position: relative;
height: 100%;
background: #fff;
box-shadow: 0 0 40px 0 rgba(0, 0, 0, .1);
overflow: hidden;
`;
const ErrorTip = styled.div`
display: flex;
word-break: break-all;
height: 100%;
justify-content: center;
align-items: center;
background: #efefef;
`;
const HoverLayer = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
background: rgba(0, 0, 0, .6);
z-index: auto;
transition: all .3s;
&:hover {
opacity: 1;
z-index: 100;
}
`;
const buttonSize = 40;
const EditButton = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: ${buttonSize}px;
height: ${buttonSize}px;
border-radius: 100%;
background: #fff;
transition: all .1s;
font-size: ${buttonSize / 2}px;
font-weight: bold;
color: #000;
&:active {
background:#eee;
transform: scale(.8);
}
`;

106
backend/web/src/js/amazing-creator/Previewer.js

@ -0,0 +1,106 @@
import React, {Component} from "react";
import GridEditor from './GridEditor'
import styled from 'styled-components'
export default class Previewer extends React.Component {
state = {
time: `${new Date().getHours()}:${new Date().getMinutes()}`
};
componentWillMount() {
this.timer = setInterval(() => {
const now = new Date();
this.setState({
time: `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`
})
}, 1000)
}
componentWillUnmount() {
this.timer && clearInterval(this.timer);
}
render() {
const {time} = this.state;
// layout is an array of objects, see the demo for more complete usage
return (
<Wrapper>
<Header>
<StatusBar>
<span>{time}</span>
</StatusBar>
<TitleBar>
<span>首页</span>
</TitleBar>
</Header>
<Content>
<GridEditor />
</Content>
<Tabbar />
</Wrapper>
)
}
}
const ratio = 16 / 9;
const width = 300;
const Wrapper = styled.div`
display: flex;
flex-direction: column;
width: ${width}px;
height: ${width * ratio}px;
border: 1px solid #eee;
border-radius: 10px;
overflow-x: hidden;
background: #fff;
box-shadow: 0 0 40px 0 rgba(0, 0, 0, .1);
`;
const Header = styled.div`
flex-shrink: 0;
width: 100%;
background-size: cover;
background-repeat: no-repeat;
text-align: center;
font-size: 10px;
margin-bottom: 3px;
`;
const SimulationBar = styled.div`
width: 100%;
background-size: contain;
background-repeat: no-repeat;
`;
const StatusBar = styled(SimulationBar)`
background-image: url("/img/status-bar.png");
background-size: cover;
`;
const TitleBar = styled(SimulationBar)`
display: flex;
align-items: center;
height: 38px;
background-image: url("/img/weapp-status-bar.png");
span {
margin-left: 10px;
font-size: 1.4em;
}
`;
const Tabbar = styled(SimulationBar)`
flex-shrink: 0;
width: 100%;
height: 40px;
border-top: 1px solid #eee;
background-size: contain;
background-repeat: no-repeat;
background-image: url('/img/tabbar.png');
`;
const Content = styled.div`
flex-grow: 1;
overflow-y: auto;
`;

16
backend/web/src/js/amazing-creator/PropsEditor.js

@ -0,0 +1,16 @@
import React, {Component} from "react";
import styled from 'styled-components'
export default class PropsEditor extends React.Component {
render() {
return (
<Wrapper>
</Wrapper>
)
}
}
const Wrapper = styled.div`
flex-grow: 1;
`;

29
backend/web/src/js/amazing-creator/index.js

@ -0,0 +1,29 @@
import React, {Component} from 'react';
import {render} from "react-dom";
import axios from 'axios'
import styled from 'styled-components'
import Previewer from './Previewer'
import PropsEditor from './PropsEditor'
import {Provider} from 'mobx-react'
import store from './store'
import DevTools from 'mobx-react-devtools';
const Wrapper = styled.div`
display: flex;
`;
class AmazingCreator extends React.Component {
render() {
return (
<Provider store={store}>
<Wrapper>
<Previewer />
<PropsEditor />
<DevTools />
</Wrapper>
</Provider>
)
}
}
render(<AmazingCreator />, document.getElementById('edit-home'));

67
backend/web/src/js/amazing-creator/modules/GoodsList.js

@ -0,0 +1,67 @@
import React from "react";
import PropTypes from "prop-types";
import styled from 'styled-components'
export default class GoodsList extends React.Component {
static propTypes = {
name: PropTypes.string,
title: PropTypes.string,
type: PropTypes.string,
onChangeTitle: PropTypes.func,
onChangeType: PropTypes.func
};
render() {
const {name, title, type, onChangeTitle, onChangeType} = this.props;
const typeImageTable = {
grid: '/img/grid.png',
list: '/img/list.png',
horizScroll: '/img/scroll.png',
};
const options = [
{value: 1, label: '横排(右滚动)'},
{value: 2, label: '多列(三列)'},
{value: 3, label: '竖排(下滚动)'}
];
return (
<Wrapper>
<Header>
<Title>{title}</Title>
<More>查看更多></More>
</Header>
<Image src={typeImageTable[type]} draggable="false" />
</Wrapper>
)
}
}
const Wrapper = styled.div`
width: 100%;
`;
const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
`;
const Title = styled.div`
color: #e2314a;
font-weight: bold;
`;
const More = styled.div`
font-size: .8em;
color: #aaa;
cursor: pointer;
&:active {
color: #666;
}
`;
const Image = styled.img`
width: 100%;
`;

14
backend/web/src/js/amazing-creator/modules/Swiper.js

@ -0,0 +1,14 @@
import React from "react";
import styled from 'styled-components'
export default class Swiper extends React.Component {
render() {
return <Image src='/img/banner.png' draggable="false" />
}
}
const Image = styled.img`
width: 100%;
height: 100%;
user-select: none;
`;

150
backend/web/src/js/amazing-creator/old.js

@ -0,0 +1,150 @@
import React, {Component} from 'react';
import {
SortableContainer,
SortableElement,
SortableHandle,
arrayMove,
} from 'react-sortable-hoc';
import PropTypes from 'prop-types'
import Select from 'react-select';
import axios from 'axios'
const DragHandle = SortableHandle(() => (
<div className='drag-handle glyphicon glyphicon-menu-hamburger' />
)); // This can be any component you want
class Part extends React.Component {
static propTypes = {
name: PropTypes.string,
title: PropTypes.string,
onChangeTitle: PropTypes.func,
onChangeType: PropTypes.func
};
render() {
const {name, title, type, onChangeTitle, onChangeType} = this.props;
const typeImageTable = [
'/img/scroll.png',
'/img/grid.png',
'/img/list.png',
];
const options = [
{ value: 1, label: '横排(右滚动)' },
{ value: 2, label: '多列(三列)' },
{ value: 3, label: '竖排(下滚动)' }
];
return (
<div className="part">
<div className="part__handle">
<input type="text" value={title} onChange={onChangeTitle} />
<Select
value={options[type - 1]}
onChange={onChangeType}
options={options}
isSearchable={false}
/>
</div>
<div className="part__content">
<div className="part__header">
<div className="title">{title}</div>
<div className="more">查看更多></div>
</div>
<img src={typeImageTable[type - 1]} alt=""/>
</div>
</div>
)
}
}
const SortableItem = SortableElement(({value}) => {
return (
<li className='sortable-item'>
<DragHandle />
{value}
</li>
);
});
const SortableList = SortableContainer(({items}) => {
return (
<ul>
{items.map((value, index) => (
<SortableItem key={`item-${index}`} index={index} value={value} />
))}
</ul>
);
});
class SortableComponent extends Component {
state = {
items: data,
};
onSortEnd = ({oldIndex, newIndex}) => {
const {items} = this.state;
this.setState({
items: arrayMove(items, oldIndex, newIndex),
});
};
onChangeTitle = (index, event) => {
let {items} = this.state;
items[index].title = event.target.value;
this.setState({
items
})
};
onChangeType = (index, newType) => {
let {items} = this.state;
items[index].type = newType.value;
this.setState({
items
})
};
submit = () => {
const {items} = this.state;
axios.post(location.href, {
'_csrf-api': csrf,
data: items
}).then((res) => {
if (res.data === 1) {
this.back();
}
})
};
back = () => {
history.go(-1)
};
render() {
const {items} = this.state;
const itemsView = items.map((item, index) => (
<Part
key={index}
title={item.title}
type={item.type}
onChangeTitle={this.onChangeTitle.bind(undefined, index)}
onChangeType={this.onChangeType.bind(undefined, index)}
/>
));
return (
<div>
<SortableList
items={itemsView}
lockAxis='y'
onSortEnd={this.onSortEnd}
useDragHandle={true}
/>
<button type='button' className='btn btn-success' onClick={this.submit}>保存</button>
<button className='btn btn-info' onClick={this.back}>返回</button>
</div>
);
}
}

68
backend/web/src/js/amazing-creator/store.js

@ -0,0 +1,68 @@
import {observable, action, configure} from "mobx";
configure({
enforceActions: 'always'
});
class Store {
@observable currentId = 3;
@observable layout = [
{
i: '1',
x: 0,
y: 0,
w: 12,
h: 3
},
{
i: '2',
x: 1,
y: 2,
w: 12,
h: 3
},
{
i: '3',
x: 2,
y: 3,
w: 12,
h: 3
},
];
@observable attrs = {
1: {
component: 'Swiper',
},
2: {
component: 'GoodsList',
type: 'grid',
title: '新品推荐',
data: 'best'
},
3: {
component: 'GoodsList',
type: 'list',
title: '火爆热销',
data: 'hot'
}
};
@action.bound changeLayout(layout) {
this.layout = layout;
}
@action.bound add(attr) {
this.layout.push({
i: (++this.currentId).toString(),
x: 2,
y: 3,
w: 12,
h: 3
});
this.attrs[this.currentId] = attr;
}
}
export default new Store()

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

@ -0,0 +1,65 @@
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

@ -0,0 +1,132 @@
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

@ -0,0 +1,38 @@
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

@ -0,0 +1,22 @@
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

@ -0,0 +1,146 @@
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

@ -0,0 +1,397 @@
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

@ -0,0 +1,127 @@
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

@ -0,0 +1,77 @@
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

@ -0,0 +1,23 @@
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

@ -0,0 +1,206 @@
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

@ -0,0 +1,171 @@
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')
);

313
backend/web/src/sku-item/index.js

@ -0,0 +1,313 @@
import React, {useState, useEffect} from 'react'
import ReactDOM from 'react-dom'
import {Table, Select, Input, InputNumber, Button, message, Modal, Spin, 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 deepEqual from 'fast-deep-equal'
import deepClone from 'clone'
import '../utils/ajax'
const {sku: defaultSku, attributes: defaultAttributes} = window;
const Option = Select.Option;
const Fragment = React.Fragment;
const additionalCol = [
{
key: 'price',
title: '价格',
precision: 2,
min: 0
},
{
key: 'stock',
title: '库存(-1 为不限库存)',
precision: 0,
min: -1
},
{
key: 'weight',
title: '重量(kg)',
precision: 2,
min: 0
}
];
function buildInputCol(data, onChange) {
return data.map(({key, title, ...others}) => ({
title,
dataIndex: key,
key,
align: 'center',
render: (text, _, index) => (
<InputNumber
type='number'
value={text}
{...others}
onChange={value => onChange(index, key, value)}
/>
)
}))
}
function SkuTable({ skuData, attributes, type, onChangeItem, onDeleteItem }) {
function setSkuItem(skuIndex, skuItemIndex, skuItemValue) {
const newValue = Object.assign(skuData[skuIndex].value, {
[skuItemIndex]: skuItemValue
});
onChangeItem(skuIndex, 'value', newValue)
}
const dataSource = skuData.map(({ id, value, price, stock, weight }, index) => {
const attrList = type === 'select' ? value.reduce((accumulator, currentValue, currentIndex) => ({
...accumulator,
[currentIndex]: currentValue
}), {}) : { value };
return {
key: index,
...attrList,
price,
stock,
weight
}
});
const skuColumns = type === 'select' ? attributes.map(({id, name, attrValue}, index) => ({
title: name,
dataIndex: index,
key: id,
align: 'center',
render: (value, _, rowIndex) => (
<Select
value={value}
style={{ width: '100%', minWidth: '50px' }}
onChange={newValue => setSkuItem(rowIndex, index, newValue)}
>
{attrValue.map(({ id, attr_value }) => (
<Option key={id} value={id}>{attr_value}</Option>
))}
</Select>
)
})) : [{
title: '规格',
dataIndex: 'value',
key: 'value',
align: 'center',
render: (value, _, rowIndex) => (
<Input
value={value}
style={{ width: '100%', minWidth: '50px' }}
onChange={e => onChangeItem(rowIndex, 'value', e.target.value)}
/>
)
}];
const columns = [
...skuColumns,
...buildInputCol(additionalCol, onChangeItem),
{
key: 'delete',
title: '操作',
render: (_1, _2, index) => <a href="javascript:void(0);" onClick={() => onDeleteItem(index)}>删除</a>
}
];
return (
<Fragment>
<Table dataSource={dataSource} columns={columns} pagination={false} />
</Fragment>
)
}
function App() {
const goodsId = queryString.parse(location.search.substr(1)).id;
const defaultData = (Number(defaultSku.type) === 1 && defaultAttributes.length === 0) ? {
type: 2,
data: []
} : defaultSku;
const [data, setData] = useState(defaultData);
const [initData, setInitData] = useState(deepClone(defaultData));
const [attributes, setAttributes] = useState(defaultAttributes);
const [loading, setLoading] = useState(false);
const modeList = [
{
key: 'select',
title: '已选商品规格设置'
},
{
key: 'manual',
title: '手动输入'
},
];
function setItem(skuIndex, key, value) {
setData(produce(data, draftState => {
draftState.data[skuIndex] = {
...draftState.data[skuIndex],
[key]: value
};
}));
}
function deleteItem(index) {
setData(produce(data, draftState => {
draftState.data.splice(index, 1)
}));
}
const type = modeList[data.type - 1].key;
function selectMode(mode) {
async function switchMode() {
const modeTable = {
select: 1,
manual: 2
};
setLoading(true);
const {sku, attributes} = await axios.get('switch', {
params: {goodsId, type: modeTable[mode]}
});
setData(sku);
setInitData(deepClone(sku));
setAttributes(attributes);
setLoading(false);
}
if (mode === type) {
return null
} else if (mode === 'select' && attributes.length === 0) {
Modal.info({ content: '无已选规格,请到【规格管理】中添加规格并在【商品列表】>【修改】>【商品规格】中选择需要的规格' })
} else if (!deepEqual(initData, data)) {
Modal.confirm({
content: '修改的内容没有保存,是否放弃修改并切换?',
onOk: () => { switchMode() }
});
} else {
return switchMode();
}
}
function addItem() {
const attrValue = type === 'select' ? { value: [] } : { value: '' };
setData({
...data,
data: [
...data.data,
{
...attrValue,
id: -1,
price: null,
stock: -1,
weight: 0
}
]
})
}
async function save() {
let errorMessage = '请填写完整的信息';
const isAllDataCorrect = data.data.every(item => {
const isAllNotEmpty = Object.keys(item)
.every(key => item[key] !== undefined && item[key] !== null && item[key] !== '');
if (type === 'select') {
const isAllSkuCorrect = item.value && item.value.length === attributes.length;
if (!isAllSkuCorrect) errorMessage = '请选择所有的规格项';
return isAllNotEmpty && isAllSkuCorrect;
} else {
const isAllSkuCorrect = item.value && item.value.length > 0;
if (!isAllSkuCorrect) errorMessage = '请填写所有的规格项';
return isAllNotEmpty && isAllSkuCorrect;
}
});
if (isAllDataCorrect) {
await axios.post('add-sku', { goodsId, sku: data});
setInitData(deepClone(data));
message.success('保存成功');
history.back();
} else {
message.error(errorMessage)
}
}
return (
<Wrapper>
<Spin spinning={loading} tip='加载中...'>
<SelectMode>
{modeList.map(({ key, title }) => (
<ModeItem
key={key}
className={type === key ? 'active' : ''}
onClick={() => selectMode(key)}
>{title}</ModeItem>
))}
</SelectMode>
<SkuTable
skuData={data.data}
attributes={attributes}
type={type}
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>
</Spin>
</Wrapper>
)
}
const ButtonGroup = styled.div`
margin-top: 20px;
`;
const Wrapper = styled.div`
`;
const SelectMode = styled.div`
display: flex;
justify-content: center;
margin-bottom: 20px;
`;
const ModeItem = styled.div`
padding: 5px 15px;
margin: 10px;
cursor: pointer;
border-radius: 30px;
color: #999;
background: #eee;
border: 2px solid transparent;
transition: all .3s;
&.active {
background: #1c7cff;
color: #fff;
cursor: default;
}
&:hover {
border-color: #1c7cff;
}
`;
ReactDOM.render(
<LocaleProvider locale={zhCN}>
<App />
</LocaleProvider>,
document.getElementById('app')
);

74
backend/web/src/sku/SelectCell.js

@ -0,0 +1,74 @@
import React from "react";
import styled from "styled-components";
import { Select, Spin, Icon } from 'antd';
import PropTypes from 'prop-types'
const Option = Select.Option;
const antIcon = <Icon type="loading" style={{ fontSize: 24 }} spin />;
export default class SelectCell extends React.Component {
constructor(props) {
super(props);
this.state = {
skuList: null
};
}
static defaultProps = {
onChange: () => {}
};
static propTypes = {
items: PropTypes.array,
defaultValue: PropTypes.array,
value: PropTypes.array,
label: PropTypes.string,
onChange: PropTypes.func
};
render() {
const {items, label, value, defaultValue, onChange} = this.props;
return (
<Root>
<Label>{label}</Label>
{items ? (
<Select
mode="multiple"
style={{width: '100%'}}
placeholder="请选择要使用的规格"
defaultValue={defaultValue}
value={value}
onChange={onChange}
>
{items.map(item => (
<Option
key={item.id}
disabled={item.disabled}
title={item.name}
value={item.id}
>{item.name}</Option>
))}
</Select>
) : <Spin indicator={antIcon} />}
</Root>
)
}
}
const Root = styled.div`
display: flex;
align-items: center;
width: 100%;
margin: 10px 0;
input.ant-select-search__field:not([type="submit"]):not([type="button"]):not([type="reset"]) {
padding: 1px;
background: none!important;
}
`;
const Label = styled.div`
width: 100px;
margin-right: 20px;
`;

157
backend/web/src/sku/index.js

@ -0,0 +1,157 @@
import React from "react";
import styled from "styled-components";
import ReactDOM from "react-dom";
import { LocaleProvider, Alert } from 'antd';
import zhCN from 'antd/lib/locale-provider/zh_CN';
import SelectCell from './SelectCell'
const goodsId = window.goodsId;
const currentAttr = window.currentAttr.map(({id, name, value}) => ({id, name, value}));
const canNotDeleteAttr = window.canNotDeleteAttr;
class Sku extends React.Component {
constructor(props) {
super(props);
this.state = {
skuList: currentAttr,
allAttr: null,
};
}
componentDidMount() {
const select = $('#goods-cat_id');
const loadData = (id, shouldClearSelectedAttrs = false) => {
$.ajax({
cache: false,
url: "filter-attribute",
data: {catId: id, goodsId: goodsId},
dataType: "json",
success: data => {
const allAttr = data.map(({id, name, value}, index) => {
const canNotDeleteCurrent = canNotDeleteAttr.find(i => i.id === parseInt(id));
return {
id: parseInt(id),
name,
disabled: Boolean(canNotDeleteCurrent),
value: value.split(',').map(i => ({
id: i,
name: i,
disabled: Boolean(canNotDeleteCurrent) && canNotDeleteCurrent.value.some(j => j === i)
}))
}
});
if (shouldClearSelectedAttrs) {
this.setState({
skuList: [],
allAttr
})
} else {
this.setState({
allAttr
})
}
}
});
}
loadData(select.val())
select.change(function () {
loadData($(this).val(), true)
})
}
handleChangeAttr = (value) => {
let {skuList} = this.state;
this.setState({
skuList: value.map(id => {
let result = skuList.find(i => i.id === id);
if (!result) {
const attr = this.state.allAttr.find(i => i.id === id);
if (attr) {
const {id, name, value} = attr;
result = {id, name, value: value.map(i => i.id)};
}
}
return result;
})
})
};
handleChangeAttrItem = (index, value) => {
let {skuList} = this.state;
skuList[index].value = value;
this.setState({
skuList
})
};
render() {
const {skuList, allAttr} = this.state;
if (allAttr) {
return (
<Root>
{(canNotDeleteAttr && canNotDeleteAttr.length > 0) && (
<Header>
<Alert
message="提示"
description="一些规格已经被使用不能删除若想删除请先到商品列表找到该商品
点击右侧的添加 SKU按钮进入SKU 管理页面删除使用此规格的所有 SKU再回到这里进行删除"
type="info"
showIcon
/>
</Header>
)}
<SelectGroup>
<SelectCell
label='规格类型'
items={allAttr}
value={skuList.map(i => i.id)}
onChange={this.handleChangeAttr}
/>
{skuList && skuList.map((sku, index) => (
<SelectCell
key={sku.id}
label={sku.name}
items={allAttr.find(i => i.id === sku.id).value}
value={sku.value}
onChange={this.handleChangeAttrItem.bind(undefined, index)}
/>
))}
<input
type="hidden"
id="attribute"
name="attribute"
value={JSON.stringify(skuList.map(({id, value}) => ({id, value})))}
/>
</SelectGroup>
</Root>
)
} else {
return <></>
}
}
}
const Root = styled.div`
`;
const Header = styled.div`
margin-bottom: 30px;
`;
const SelectGroup = styled.div`
width: 100%;
`;
ReactDOM.render(
<LocaleProvider locale={zhCN}>
<Sku />
</LocaleProvider>,
document.getElementById('sku-choose')
);

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

@ -0,0 +1,185 @@
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

@ -0,0 +1,15 @@
.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

@ -0,0 +1,570 @@
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

@ -0,0 +1,354 @@
/*! 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

@ -0,0 +1,102 @@
@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%;
}
}
}
}

83
backend/web/src/styles/amazing-creator.scss

@ -0,0 +1,83 @@
.react-resizable-handle {
z-index: 200;
}
#edit-home {
width: 100%;
margin: auto;
//background: #eee;
ul {
padding: 0;
margin-bottom: 20px;
}
.btn {
margin-right: 10px;
}
}
.amazing-creator, .ac {
display: flex;
&__board {
flex-grow: 1;
}
}
.sortable-item {
display: flex;
position: relative;
list-style: none;
background: #fff;
margin: 10px 0;
padding: 10px;
border-radius: 10px;
box-shadow: 0 0 30px 0 rgba(0, 0, 0, .1);
.drag-handle {
display: flex;
align-items: center;
padding: 10px;
user-select: none;
cursor: row-resize;
color: #362c57;
font-size: 1.5em;
}
.part {
flex-grow: 1;
flex-shrink: 1;
display: flex;
justify-content: space-between;
&__handle {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 10px;
> * {
margin: 10px 0;
}
}
&__content {
flex-shrink: 0;
width: 250px;
img {
width: 100%;
}
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
color: #e2314a;
font-weight: bold;
margin-left: 15px;
}
.more {
font-size: .8em;
color: #aaa;
cursor: pointer;
&:active {
color: #666;
}
}
}
}
}

90
backend/web/src/styles/content-header.scss

@ -0,0 +1,90 @@
.content-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
padding: 0;
h1, .breadcrumb {
margin: 5px 0!important;
}
h1 {
color: $color-primary;
font-weight: bold;
padding-left: 10px;
border-left: 5px solid $color-primary;
}
}
.content-header>.breadcrumb {
$height: 25px;
$size: 10px;
display: flex;
flex-wrap: wrap;
position: static;
float: none!important;
height: $height;
background: none;
overflow: hidden;
padding: 0;
li {
display: flex;
align-items: center;
position: relative;
margin-right: $size + 3px;
background: $color-primary;
&:hover {
$background: darken($color-primary, 10%);
background: $background;
&::before {
border-top: $height / 2 solid $background;
border-bottom: $height / 2 solid $background;
}
&::after {
border-left: $size solid $background;
}
}
&::before {
content: '';
position: absolute;
top: 0;
left: -1 * $size;
width: 0;
height: 0;
border-top: $height / 2 solid $color-primary;
border-bottom: $height / 2 solid $color-primary;
border-left: $size solid transparent;
}
&::after {
content: '';
position: absolute;
top: 0;
right: -1 * $size;
width: 0;
height: 0;
border-top: $height / 2 solid transparent;
border-bottom: $height / 2 solid transparent;
border-left: $size solid $color-primary;
}
&.active {
$background: $color-primary-dim;
box-shadow: none;
background: $background;
color: $color-primary;
padding: 0 $size / 2 + 5px;
&::before {
border-top: $height / 2 solid $background;
border-bottom: $height / 2 solid $background;
}
&::after {
border-left: $size solid $background;
}
}
a {
padding: 5px 10px;
color: $color-primary-dim!important;
}
}
>li+li::before {
content: '';
}
}

180
backend/web/src/styles/dashboard.scss

@ -0,0 +1,180 @@
.content-header h1 {
margin: 0;
}
.box h1 {
font-size: 1.5em;
margin-top: 0;
margin-bottom: 20px;
}
.row {
display: flex;
flex-wrap: wrap;
margin: 0;
.row {
margin: 0;
}
> div {
margin: 10px;
}
}
.col {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.total {
width: 100%;
display: flex;
flex-wrap: wrap;
}
#trend-chart {
width: 80%;
}
#total-pie {
width: 20%;
}
.box {
margin: 20px;
}
#stock-tip {
width: 350px;
}
.overview-card, .line-chart, .data-table {
//@extend %shadow-soft;
box-shadow: 3px 6px 40px -6px rgba(125, 138, 179, 0.3);
}
.overview-card {
flex-grow: 1;
display: flex;
justify-content: space-around;
align-items: center;
width: 200px;
padding: 20px 15px;
margin: 10px;
background: $color-panel;
border-radius: 10px;
img {
$size: 50px;
width: $size;
height: $size;
}
h1 {
font-size: 1em;
}
p.number {
font-size: 1.6em;
font-weight: bold;
color: #ef9131;
margin: 0;
}
}
.total-display {
width: 100%;
margin: 0!important;
display: flex;
justify-content: space-around;
flex-wrap: wrap;
}
.line-chart {
flex-grow: 1;
flex-shrink: 1;
max-width: 100%;
padding: 10px 20px!important;
}
.line-chart, .data-table {
display: flex;
flex-direction: column;
padding: 20px;
background: $color-panel;
border-radius: 10px;
h1 {
font-size: 1.5em;
font-weight: bold;
}
.content {
display: flex;
flex-direction: column;
justify-content: center;
flex-grow: 1;
margin: 0;
padding: 0;
}
}
.data-table {
table {
thead {
tr {
font-weight: bold;
td {
white-space: nowrap;
}
}
}
td {
padding: 8px;
font-size: .9em;
//white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.line-number {
text-align: center;
}
&.highlight {
color: #ef9131;
}
}
}
.table-empty-text {
text-align: center;
font-size: 1.4em;
}
}
.fill-space {
flex-grow: 1;
}
@media (min-width: 1235px) {
.total-display {
flex-wrap: nowrap;
}
.row {
flex-wrap: nowrap;
width: 100%;
.line-chart {
//width: 60%;
flex: 1;
}
.data-table {
max-width: 400px;
flex-grow: 0;
flex-shrink: 0;
}
}
}
@media (max-width: 1235px) {
.row {
flex-wrap: wrap;
.line-chart {
flex-grow: 1;
width: 98%;
padding-right: 2%;
}
}
}

183
backend/web/src/styles/global.scss

@ -0,0 +1,183 @@
html, body {
color: $color-fore;
background: $color-background!important;
font-family: -apple-system, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Arial, sans-serif;
}
body .fa {
font-family: "fa", "FontAwesome" !important;
}
.content-wrapper {
margin-top: 70px;
margin-bottom: 10px;
}
.content-wrapper {
background: rgb(248, 250, 255)!important;
}
input, select {
border-radius: 3px!important;
}
button.btn-float, a.btn-float {
position: fixed;
display: block;
top: unset;
right: 50px;
bottom: 50px;
width: 60px;
height: 60px;
line-height: 60px;
padding: 0;
border-radius: 50%;
font-size: 1.6em;
box-shadow: 0 0 20px 5px rgba(0, 0, 0, .2);
z-index: 10000;
}
.btn-info.btn-float {
bottom: 120px;
}
fieldset {
transition: all .4s;
}
fieldset[disabled] {
opacity: 0;
cursor: auto!important;
}
#driver-popover-item, #driver-highlighted-element-stage {
position: fixed!important;
}
//.main-footer {
// position: absolute;
// bottom: 0;
// width: 100%;
//}
::-webkit-scrollbar{width:6px!important;height:6px!important;}
body::-webkit-scrollbar{width:6px!important;height:6px!important;}
::-webkit-scrollbar-track{background:rgba(200,200,200,0.22)!important;border-radius:8px!important;}
::-webkit-scrollbar-thumb{background-color: rgba(0, 0, 0, .1) !important;min-height:50px;border-radius:5px!important;}
@keyframes fadein {
0% {opacity: 0;}
100% {}
}
.content-wrapper {
$margin: 20px;
margin-left: $sidebar-width + $margin!important;
margin-right: $margin;
}
.main-footer {
margin-left: $sidebar-width!important;
a {
color: $color-primary-vivid;
}
}
.tab-content {
height: auto!important;
border: none!important;
padding: 0!important;
margin-top: 30px;
}
.login-logo {
font-weight: bold;
}
.category-list {
.category-list-item {
&.category__hidden {
background: #f8f8f8;
color: #aaa;
}
> div {
display: flex;
align-items: center;
border: none!important;
}
.hidden-tip {
margin-left: 20px;
color: #aaa;
}
}
a.btn {
padding: 10px !important;
}
.btn-group {
a.btn {
border-radius: 0;
&:first-of-type {
border-radius: 5px 0 0 5px;
}
&:last-child {
border-radius: 0 5px 5px 0;
}
}
&.btn-group__circle {
a.btn {
padding: 8px!important;
margin: 0 5px;
font-size: .8em;
border-radius: 100%!important;
}
}
}
a.collapse-list-btn {
color: $color-primary;
i {
transition: transform .3s;
}
&[aria-expanded="true"] i {
transform: rotate(180deg);
}
&.disabled {
color: $color-dim;
cursor: default;
}
}
.drag-btn {
margin-left: 20px;
color: $color-info;
}
}
.category-index {
min-height: 75vh;
}
.goods-update .props-main-box {
position: relative!important;
top: 0;
left: 0;
margin-top: 20px;
}
// 侧边栏折叠状态
.sidebar-mini.sidebar-collapse .content-wrapper,
.sidebar-mini.sidebar-collapse .right-side,
.sidebar-mini.sidebar-collapse .main-footer {
margin-left: 70px!important;
}
@media (max-width: 767px) {
body.sidebar-mini.sidebar-open {
.content-wrapper {
margin-left: 0!important;
}
}
.main-footer {
margin-left: 0!important;
}
}

128
backend/web/src/styles/header.scss

@ -0,0 +1,128 @@
//顶栏背景色
.logo, .navbar-static-top {
background: none!important;
}
.main-header {
position: fixed;
width: auto;
left: $sidebar-width;
right: 0;
display: flex;
//background-image: linear-gradient(45deg, $color-primary, adjust_hue($color-primary, 40));
background: $color-panel;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1)!important;
transition: all .3s ease-in-out;
z-index: 100 !important;
.logo-lg, .logo, .sidebar-toggle, .hidden-xs, .notifications-menu a.dropdown-toggle {
color: $color-thin !important;
}
.user-image {
background: lighten($color-primary, $lightenDegree);
}
.logo {
flex-grow: 1;
font-weight: bold;
}
.navbar-static-top {
margin-left: 0;
}
}
.user-header {
background: $color-primary!important;
}
.user-menu {
.dropdown-menu {
left: unset;
right: 0;
box-shadow: 0 4px 60px 10px rgba(0, 0, 0, 0.1)!important;
overflow: visible;
}
}
.sidebar-toggle {
&:active, &:hover {
background: $color-pale!important;
}
}
.notifications-menu {
.label-warning {
display: block;
padding: 3px 5px!important;
font-size: .8em!important;
background: #ff4e3d !important;
border-radius: 100%;
}
.dropdown-menu {
width: auto!important;
box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1)!important;
ul.menu {
max-height: 40vh!important;
li {
a {
display: flex!important;
align-items: center;
padding: 10px 20px!important;
span.number {
color: $color-danger;
margin: 0 10px;
font-weight: bold;
}
span.tip {
color: $color-danger;
margin-left: 10px;
font-weight: bold;
}
}
i {
width: auto!important;
font-size: 1.6em;
margin-right: 20px;
padding: 10px;
border-radius: 100%;
text-align: center;
&.blue {
$color: #5964ab;
background: $color;
color: lighten($color, 50%)!important;
}
&.yellow {
$color: #df8346;
background: $color;
color: lighten($color, 50%)!important;
}
&.red {
background: $color-danger;
color: lighten($color-danger, 50%)!important;
}
}
}
}
}
}
body.sidebar-collapse {
.main-header {
left: 50px;
}
}
@media (max-width: 767px) {
.main-header {
left: 0!important;
}
.logo {
padding: 0!important;
}
.main-header .logo, .main-header .navbar {
width: auto;
}
body.sidebar-mini.sidebar-open {
.main-header {
margin-left: 190px;
}
}
}

7
backend/web/src/styles/index.scss

@ -0,0 +1,7 @@
@import "./global";
@import "sidebar";
@import "widget";
@import "header";
@import "content-header";
@import "dashboard";
@import "amazing-creator";

193
backend/web/src/styles/login.scss

@ -0,0 +1,193 @@
html {
background: #292d34;
background-image: url("/img/login_background.png") !important;
background-size: cover !important;
font-family: -apple-system, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Arial, sans-serif;
}
body {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
//background: rgba(0, 0, 0, .7)!important;
background: none !important;
}
.login-box {
$color-primary: #2b457c;
$background: #e9f4ff;
&__body {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
height: 80vh;
box-shadow: 0 4px 30px 10px rgba(0, 0, 0, 0.2)!important;
//border-radius: 10px;
overflow: hidden;
}
&__bar {
position: absolute;
top: 40px;
width: 100%;
padding-right: 10px;
text-align: right;
border-left: 6px solid $background;
border-right: 6px solid $color-primary;
color: $color-primary;
font-weight: bold;
font-size: 1.5em;
}
&__left, &__right {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
}
&__left {
padding: 0 60px;
background: #1a1d25;
}
&__logo {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
img {
$size: 150px;
width: $size;
height: $size;
margin-bottom: 40px;
}
p {
font-size: 1.5em;
font-weight: bold;
color: #2ad0ff;
}
}
&__right {
flex-grow: 1;
background: $background;
.form-group.has-feedback {
position: relative;
.form-control {
padding-left: 40px;
border: none;
border-bottom: 2px solid $color-primary;
background: none !important;
&:-webkit-autofill {
-webkit-box-shadow: 0 0 0 1000px $background inset;
-webkit-text-fill-color: $color-primary;
}
}
.form-control-feedback {
position: absolute;
left: 0;
color: $color-primary;
}
}
.submit-area {
display: flex;
flex-direction: column;
align-content: center;
justify-content: space-around;
text-align: center;
.form-group.field-adminloginform-rememberme {
color: $color-primary;
label {
font-weight: bold!important;
}
}
.btn.btn-primary.btn-block.btn-flat {
display: inline-block;
padding: 10px 20px;
border-radius: 20px;
background: $color-primary;
box-shadow: 0 5px 10px 0 rgba(0, 0, 0, .2);
}
}
}
}
@media (min-width: 1200px) {
.login-box__body {
width: 60vw;
}
}
@media (max-width: 1200px) and (min-width: 800px) {
.login-box__body {
width: 700px;
}
}
@media (max-width: 800px) and (min-width: 400px) {
.login-box__body {
width: 87.5%;
.login-box__left {
padding: 0 30px;
}
}
}
@media (max-width: 600px) {
.login-box {
&__body {
flex-direction: column;
width: 100%;
height: 100%;
}
&__left {
padding: 20px 0;
background: #1a1d25;
}
&__logo {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
img {
$size: 50px;
width: $size;
height: $size;
margin-bottom: 0;
margin-right: 20px;
}
p {
margin-bottom: 0;
font-size: 1.5em;
font-weight: bold;
color: #6fd1ff;
}
}
&__bar {
top: 110px;
width: auto;
left: 50%;
padding: 0 10px 2px 10px;
transform: translateX(-50%);
border: none;
border-bottom: 4px solid $color-primary;
text-align: center;
}
}
}

201
backend/web/src/styles/sidebar.scss

@ -0,0 +1,201 @@
.main-sidebar {
position: fixed;
width: $sidebar-width;
height: 100%;
padding-top: 20px!important;
margin-bottom: 100px;
overflow-y: auto;
background: $color-sidebar!important;
//background-image: linear-gradient(135deg, #3a345d, #2d2a46);
.sidebar__header {
display: flex;
flex-direction: column;
align-items: center;
img {
$size: 60px;
width: $size;
height: $size;
border-radius: 50%;
}
p {
color: $color-sidebar-logo;
font-style: italic;
margin-top: 15px;
font-size: 1.2em;
font-weight: bold;
}
}
.user-panel .info {
a {
color: lighten($color-thin, 20%);
}
i {
font-size: 10px;
}
}
.sidebar-form {
border: none !important;
.input-group {
border-radius: 40px;
overflow: hidden;
input {
border: none !important;
}
}
input.form-control[type="text"], #search-btn {
background: lighten($color-primary-pale, 5%)!important;
color: $color-primary-dim !important;
}
}
ul.sidebar-menu {
margin-top: 10px;
> li.treeview {
// 一级菜单
list-style: none;
margin-left: 15px;
a {
background: $color-sidebar!important;
color: $color-sidebar-fore;
&:hover {
color: $color-sidebar-active-fore!important;
}
}
> a {
padding: 10px 15px!important;
font-weight: normal;
font-size: 1em;
.fa {
margin-right: 5px;
font-size: 1.2em;
}
}
ul.treeview-menu {
padding-left: 0;
padding-bottom: 10px;
background: $color-sidebar!important;
li {
// 二级菜单
padding: 5px 0;
//border-left: 4px solid transparent;
background: $color-sidebar!important;
&:hover {
background: $color-pale;
}
a {
padding: 0;
padding-left: 48px;
font-size: .9em;
background: $color-sidebar!important;
}
// 隐藏二级菜单前面的 O
.fa-circle-o {
display: none;
}
}
}
&.active {
// 当前页面所在的一级菜单
background: $color-sidebar-active!important;
border-radius: 20px 0 0 20px;
overflow: hidden;
> a {
//color: $color-primary-vivid !important;
background: $color-sidebar-active!important;
color: $color-sidebar-active-fore!important;
}
.treeview-menu {
background: $color-sidebar-active!important;
li {
background: $color-sidebar-active!important;
a {
background: $color-sidebar-active!important;
}
&.active {
// 选中的二级菜单
//border-left-color: $color-primary;
background: $color-sidebar-active!important;
a {
color: $color-sidebar-active-fore !important;
font-weight: normal;
}
}
}
}
}
&.menu-open {
// 展开的一级菜单
//color: $color-primary !important;
}
}
}
}
body.sidebar-collapse {
.main-sidebar {
padding-top: 10px!important;
overflow: visible;
}
.sidebar__header {
display: flex;
flex-direction: column;
align-items: center;
img {
$size: 25px;
width: $size;
height: $size;
border-radius: 50%;
transition: all .3s ease-in-out;
}
p {
display: none;
font-size: 12px;
margin-top: 5px;
margin-bottom: 0;
transition: all .3s ease-in-out;
}
}
ul.sidebar-menu {
> li.treeview {
margin-left: 0;
overflow: visible!important;
a {
border-radius: 50%;
}
//border-radius: 50%!important;
//a {
// padding: 10px!important;
// i {
// margin: 0!important;
// }
//}
a span {
width: 178px!important;
}
.pull-right-container {
margin-top: -1px!important;
padding: 8px 3px!important;
}
.treeview-menu {
left: 48px!important;
top: unset!important;
border: none!important;
}
}
}
}
.main-sidebar, .main-footer {
box-shadow: 0 4px 30px 0 rgba(223, 225, 230, 0.5) !important;
border: none !important;
}
.panel {
padding: 10px;
margin: 0;
}
@media (max-width: 767px) {
.content-wrapper {
margin-left: 30px!important;
}
}

30
backend/web/src/styles/variables.scss

@ -0,0 +1,30 @@
$color-primary: #2f4887;
$color-primary-vivid: hsl(hue($color-primary), saturation($color-primary), 50%);
$color-primary-dim: lighten($color-primary-vivid, 35%);
$color-primary-pale: lighten($color-primary-vivid, 42%);
$color-background: rgb(248, 250, 255);
$color-panel: #fff;
$color-pale: #f6f6f6;
$color-fore: #666;
$color-dim: #b4b6c5;
$color-thin: #515156;
$color-sidebar: #080e39;
$color-sidebar-active: #2a2e52;
$color-sidebar-fore: #dce0f2;
$color-sidebar-active-fore: #26caff;
$color-sidebar-logo: #26caff;
$color-success: #2ab57d;
$color-danger: #c60013;
$color-info: #2a92ec;
$lightenDegree: 70%;
$sidebar-width: 190px;
%shadow {
box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.1)!important;
}
%shadow-soft {
box-shadow: 0 4px 30px 0 rgba(223, 225, 230, 0.5) !important;
}

301
backend/web/src/styles/widget.scss

@ -0,0 +1,301 @@
.content-wrapper {
margin-left: 30px;
margin-right: 30px;
> section.content {
margin: 30px 0;
background: $color-panel!important;
box-shadow: 0 4px 30px 2px rgba(223, 225, 230, 0.5)!important;
border-radius: 10px;
}
}
.error-page .fa-warning {
font-size: 1.5em;
}
#treeView {
background: none;
}
body a:focus {
text-decoration: none;
}
.box, .panel {
padding: 0!important;
margin: 0!important;
border: none!important;
border-image-width: 0!important;
box-shadow: none!important;
//box-shadow: 0 4px 30px 2px rgba(223, 225, 230, 0.5)!important;
overflow: hidden;
.panel {
box-shadow: none!important;
}
}
.grid-view {
margin-top: 20px;
}
.box.box-default {
margin: 0;
}
.order-index {
.panel-default {
padding: 0;
padding-top: 20px;
margin: 0;
}
}
.panel {
margin: 20px 0;
.panel-heading {
padding: 0;
}
}
.btn {
border: none;
$btn-list: (name: default, color: $color-pale),
(name: primary, color: $color-primary),
(name: success, color: $color-success),
(name: info, color: $color-info),
(name: danger, color: $color-danger);
@each $btn in $btn-list {
&.btn-#{map_get($btn, name)} {
$color: map_get($btn, color);
background: $color;
@if (lightness($color) < 70%) {
color: lighten($color, $lightenDegree)!important;
} @else {
color: darken($color, $lightenDegree)!important;
}
&:hover {
background: darken(map_get($btn, color), 5%);
}
&:active {
background: darken(map_get($btn, color), 10%);
}
}
}
}
.dropdown-toggle {
box-shadow: none!important;
}
.select-data-box {
position: static!important;
}
.panel-heading, .panel-footer {
background: $color-panel!important;
}
.panel-heading {
border: none;
}
.nav-tabs {
display: flex;
border-bottom-color: $color-pale;
li {
float: none;
border-bottom: 3px solid transparent;
a {
color: $color-fore;
}
a, a:hover {
border: none!important;
}
i {
color: $color-primary-vivid!important;
}
&.active {
border-bottom-color: $color-primary;
box-shadow: none;
a {
margin-right: 0;
color: $color-primary;
font-weight: bold;
}
}
}
}
.kv-panel-before, .kv-panel-after, .panel-footer {
border: none;
}
table.kv-grid-table {
&, thead, th, td {
border: none!important;
}
thead {
background: lighten($color-primary-pale, 5%);
a {
color: $color-primary;
}
input.form-control[type][name], select {
background: lighten($color-primary-pale, 10%)!important;
}
}
>tbody {
>tr:nth-of-type(2n) {
background: lighten($color-primary-pale, 6%);
&:hover {
background: lighten($color-primary-pale, 6%)!important;
}
}
>tr:hover {
background: $color-panel!important;
}
}
td {
vertical-align: middle!important;
}
}
input:not([type="submit"]):not([type="button"]):not([type="reset"]):not([class^="ant-input"]), select {
width: 100%;
padding: 6px 12px;
background: lighten($color-primary-pale, 3%) !important;
//background: $color-pale!important;
//border: 1.5px solid lighten($color-primary-dim, 10%)!important;
border: none!important;
border-radius: 5px!important;
color: darken($color-primary, 10%);
box-shadow: none!important;
}
input.file-caption-name {
background: none!important;
}
.input-group-addon {
background: lighten($color-primary-pale, 5%)!important;
border: none!important;
&:hover {
background: $color-primary-pale!important;
}
}
.select2-selection.select2-selection--multiple,
.select2-dropdown.select2-dropdown--below {
border: none!important;
}
::-webkit-input-placeholder {
color: lighten($color-primary, 50%)!important;
//color: $color-dim!important;
}
// 复选框
.cbx-active {
border: 2px solid $color-primary-dim!important;
box-shadow: none!important;
border-radius: 0;
.cbx-icon {
background: lighten($color-primary-pale, 5%);
i {
color: $color-primary-vivid!important;
}
}
}
// Switch
label.btn.btn-default {
box-shadow: none!important;
&.active {
background: darken($color-primary-vivid, 10%);
color: $color-primary-pale;
}
}
.props-main-box, .props-detail-box {
position: static!important;
width: 100%!important;
}
.tab-content {
min-height: 350px;
}
.goods-index {
td:last-child {
font-size: 0;
.btn {
padding: 10px;
border-radius: 0;
}
}
}
ul.pagination {
li {
a, span {
border: none!important;
margin: 0 5px;
border-radius: 100%!important;
}
&.active {
a {
background: $color-primary-vivid;
//box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2)!important;
&:hover {
background: darken($color-primary-vivid, 10%);
}
}
}
}
}
.after-sale-index .panel, .panel-default {
position: relative;
min-height: 75vh;
}
$width: 95px;
.rc-switch {
width: $width!important;
margin-bottom: 10px;
&[aria-checked="false"] {
background: $color-info;
}
&[aria-checked="true"] {
background: $color-success;
}
}
.rc-switch-checked:after {
left: $width - 22px!important;
}
.empty {
position: absolute;
display: block;
width: 100%;
left: 50%;
top: 60%;
transform: translate(-50%, -50%);
text-align: center;
font-size: 2em;
}
.kv-page-summary-container td {
padding: 0!important;
}
table.table.table-striped.table-bordered.detail-view {
&, tbody, tr, th, td {
border: none!important;
}
th {
padding-right: 50px;
white-space: nowrap;
}
}

76
backend/web/src/utils/ajax.js

@ -0,0 +1,76 @@
import axios from 'axios'
import {message} from 'antd';
// 请求拦截器
axios.interceptors.request.use(config => ({
...config,
data: {
...config.data,
'_csrf-api': window.csrfToken
}
}));
// 响应拦截器
axios.interceptors.response.use(function (response) {
const rawData = response && response.data;
let data = null;
const errorText = '服务器返回的数据格式不正确';
if (rawData && typeof response.data === 'object') {
// 若返回的数据是对象或者数组
data = rawData;
} else if (typeof response.data === 'string') {
// 若返回的数据是字符串,尝试将其按 JSON 格式转为对象
try {
data = JSON.parse(rawData);
} catch (e) {
message.error(errorText);
return Promise.reject(errorText);
}
} else {
message.error(errorText);
return Promise.reject(errorText);
}
if (data) {
const {status, info} = response.data;
if (status === undefined || status === true || status === 1) {
return response.data;
} else if (info) {
const errorText = info;
message.error(errorText);
console.error(errorText);
return Promise.reject(errorText)
} else {
const errorText = `操作失败`;
message.error(errorText);
console.error(errorText);
return Promise.reject(errorText)
}
} else if (response) {
return response;
} else {
return Promise.reject(`服务器无响应`);
}
}, function (error) {
let errorText = '';
if (error && error.response && error.response.status === 500) {
if (error.response.data) {
errorText = error.response.data.message || error.response.data;
} else {
errorText = error.response.message
}
} else if (error.response && error.response.status) {
errorText = `请求出错:${error.response.statusText}${error.response.status}`
} else if (error.message) {
errorText = error.message
} else {
errorText = error
}
errorText && message.error(errorText);
error && console.error(error);
return Promise.reject(errorText);
});

84
backend/web/webpack.config.js

@ -0,0 +1,84 @@
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const entryList = {
dashboard: './src/dashboard/index.js',
style: './src/styles/index.scss',
login_style: './src/styles/login.scss',
order_detail: './src/detail-display/index.js',
sku: './src/sku/index.js',
mini_program_management: './src/mini-program-management/index.js',
spread: './src/spread/index.js',
sku_item: './src/sku-item/index.js',
'custom-menu': './src/custom-menu/index.js',
sku_for_activity: './src/sku-for-activity/index.js',
};
const needIconfontEntries = ['style', 'login_style'];
module.exports = function (env, argv) {
const pathToClean = argv.module ? `${argv.module}.*.js` : 'custom';
const entry = argv.module ? {[argv.module]: entryList[argv.module]} : entryList;
const htmlWebpackPluginList = Object.keys(entry).map(entryItem => new HtmlWebpackPlugin({
filename: `${entryItem}.html`,
chunks: [entryItem],
template: needIconfontEntries.includes(entryItem) ? 'src/iconfont.html' : 'src/import.html'
}));
return {
entry,
output: {
filename: '[name].[chunkhash].js',
path: path.resolve(__dirname, 'custom'),
publicPath: '/custom/'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.scss$/,
use: ["style-loader", "css-loader", "resolve-url-loader", "sass-loader?sourceMap",
{
loader: 'sass-resources-loader',
options: {
resources: './src/styles/variables.scss'
}
}]
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
},
{
test: /\.(png|jpg|gif|eot|svg|ttf|woff2?)(\?.*)?$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 10000
}
},
'file-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [pathToClean]
}),
new webpack.DefinePlugin({
ENV_DEV: JSON.stringify(argv.mode === 'development')
}),
...htmlWebpackPluginList,
]
};
};
Loading…
Cancel
Save