59 changed files with 6702 additions and 0 deletions
-
17backend/web/.babelrc
-
26backend/web/.eslintrc.json
-
11backend/web/.gitignore
-
89backend/web/package.json
-
30backend/web/src/custom-menu/AddItem.js
-
124backend/web/src/custom-menu/Editor.js
-
176backend/web/src/custom-menu/MenuItem.js
-
247backend/web/src/custom-menu/index.js
-
7backend/web/src/custom-menu/index.scss
-
25backend/web/src/custom-menu/utils.js
-
149backend/web/src/dashboard/CircleCard.js
-
135backend/web/src/dashboard/LineChart.js
-
53backend/web/src/dashboard/OverviewCard.js
-
40backend/web/src/dashboard/PieChart.js
-
74backend/web/src/dashboard/RadioGroup.js
-
32backend/web/src/dashboard/Table.js
-
145backend/web/src/dashboard/index.js
-
1backend/web/src/iconfont.html
-
4backend/web/src/import.html
-
36backend/web/src/js/amazing-creator/GridEditor.js
-
101backend/web/src/js/amazing-creator/Module.js
-
106backend/web/src/js/amazing-creator/Previewer.js
-
16backend/web/src/js/amazing-creator/PropsEditor.js
-
29backend/web/src/js/amazing-creator/index.js
-
67backend/web/src/js/amazing-creator/modules/GoodsList.js
-
14backend/web/src/js/amazing-creator/modules/Swiper.js
-
150backend/web/src/js/amazing-creator/old.js
-
68backend/web/src/js/amazing-creator/store.js
-
65backend/web/src/mini-program-management/MainDescription.js
-
132backend/web/src/mini-program-management/ManageTriers.js
-
38backend/web/src/mini-program-management/StepBar.js
-
22backend/web/src/mini-program-management/common.js
-
146backend/web/src/mini-program-management/data.js
-
397backend/web/src/mini-program-management/index.js
-
127backend/web/src/order-detail/Card.js
-
77backend/web/src/order-detail/GoodsCard.js
-
23backend/web/src/order-detail/getOrderStatus.js
-
206backend/web/src/order-detail/index.js
-
171backend/web/src/sku-for-activity/index.js
-
313backend/web/src/sku-item/index.js
-
74backend/web/src/sku/SelectCell.js
-
157backend/web/src/sku/index.js
-
185backend/web/src/spread/Movable/index.js
-
15backend/web/src/spread/Movable/index.scss
-
570backend/web/src/spread/index.js
-
354backend/web/src/spread/normalize.scss
-
102backend/web/src/spread/preview.scss
-
83backend/web/src/styles/amazing-creator.scss
-
90backend/web/src/styles/content-header.scss
-
180backend/web/src/styles/dashboard.scss
-
183backend/web/src/styles/global.scss
-
128backend/web/src/styles/header.scss
-
7backend/web/src/styles/index.scss
-
193backend/web/src/styles/login.scss
-
201backend/web/src/styles/sidebar.scss
-
30backend/web/src/styles/variables.scss
-
301backend/web/src/styles/widget.scss
-
76backend/web/src/utils/ajax.js
-
84backend/web/webpack.config.js
@ -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" |
|||
}] |
|||
] |
|||
} |
@ -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" |
|||
} |
|||
} |
@ -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 |
@ -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" |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
|||
`;
|
@ -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`
|
|||
|
|||
`;
|
@ -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; |
|||
} |
|||
`;
|
@ -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')); |
@ -0,0 +1,7 @@ |
|||
.ant-popover-inner-content { |
|||
padding: 0!important; |
|||
} |
|||
|
|||
.ant-popover-arrow { |
|||
display: none!important; |
|||
} |
@ -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); |
|||
} |
@ -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}; |
|||
`;
|
@ -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; |
|||
} |
|||
`;
|
@ -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> |
|||
) |
|||
} |
|||
} |
@ -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: {}
|
|||
// });
|
@ -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; |
|||
} |
|||
`;
|
@ -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`
|
|||
|
|||
`;
|
@ -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') |
|||
); |
@ -0,0 +1 @@ |
|||
<link rel="stylesheet" href="//at.alicdn.com/t/font_827976_jtnsfqxuiaf.css"> |
@ -0,0 +1,4 @@ |
|||
<div id="app"></div> |
|||
<script> |
|||
var csrfToken = "<?= Yii::$app->request->csrfToken ?>"; |
|||
</script> |
@ -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> |
|||
) |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
`;
|
@ -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; |
|||
`;
|
@ -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; |
|||
`;
|
@ -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')); |
@ -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%; |
|||
`;
|
@ -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; |
|||
`;
|
@ -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> |
|||
); |
|||
} |
|||
} |
@ -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() |
@ -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; |
|||
`;
|
@ -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`
|
|||
`;
|
@ -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 <></>; |
|||
} |
|||
} |
@ -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; |
|||
`;
|
@ -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} |
@ -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') |
|||
); |
@ -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; |
|||
} |
|||
`;
|
@ -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; |
|||
`;
|
@ -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] || '未知'; |
|||
} |
@ -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') |
|||
); |
@ -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') |
|||
); |
@ -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') |
|||
); |
@ -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; |
|||
`;
|
@ -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') |
|||
); |
@ -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> |
|||
) |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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') |
|||
); |
@ -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; |
|||
} |
@ -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%; |
|||
} |
|||
} |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -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: ''; |
|||
} |
|||
} |
@ -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%; |
|||
} |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,7 @@ |
|||
@import "./global"; |
|||
@import "sidebar"; |
|||
@import "widget"; |
|||
@import "header"; |
|||
@import "content-header"; |
|||
@import "dashboard"; |
|||
@import "amazing-creator"; |
@ -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; |
|||
} |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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; |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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); |
|||
}); |
@ -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, |
|||
] |
|||
}; |
|||
}; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue