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