From 9cfff4c84c5759d151fd53a15130e05880f09631 Mon Sep 17 00:00:00 2001 From: linyaostalker <602604991@qq.com> Date: Wed, 18 Dec 2019 09:26:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=90=8E=E5=8F=B0=E7=94=A8=E5=88=B0?= =?UTF-8?q?=E7=9A=84react=E7=BB=84=E4=BB=B6=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/web/.babelrc | 17 + backend/web/.eslintrc.json | 26 + backend/web/.gitignore | 11 + backend/web/package.json | 89 +++ backend/web/src/custom-menu/AddItem.js | 30 + backend/web/src/custom-menu/Editor.js | 124 ++++ backend/web/src/custom-menu/MenuItem.js | 176 ++++++ backend/web/src/custom-menu/index.js | 247 ++++++++ backend/web/src/custom-menu/index.scss | 7 + backend/web/src/custom-menu/utils.js | 25 + backend/web/src/dashboard/CircleCard.js | 149 +++++ backend/web/src/dashboard/LineChart.js | 135 +++++ backend/web/src/dashboard/OverviewCard.js | 53 ++ backend/web/src/dashboard/PieChart.js | 40 ++ backend/web/src/dashboard/RadioGroup.js | 74 +++ backend/web/src/dashboard/Table.js | 32 + backend/web/src/dashboard/index.js | 145 +++++ backend/web/src/iconfont.html | 1 + backend/web/src/import.html | 4 + .../web/src/js/amazing-creator/GridEditor.js | 36 ++ backend/web/src/js/amazing-creator/Module.js | 101 ++++ .../web/src/js/amazing-creator/Previewer.js | 106 ++++ .../web/src/js/amazing-creator/PropsEditor.js | 16 + backend/web/src/js/amazing-creator/index.js | 29 + .../js/amazing-creator/modules/GoodsList.js | 67 ++ .../src/js/amazing-creator/modules/Swiper.js | 14 + backend/web/src/js/amazing-creator/old.js | 150 +++++ backend/web/src/js/amazing-creator/store.js | 68 +++ .../MainDescription.js | 65 ++ .../mini-program-management/ManageTriers.js | 132 ++++ .../src/mini-program-management/StepBar.js | 38 ++ .../web/src/mini-program-management/common.js | 22 + .../web/src/mini-program-management/data.js | 146 +++++ .../web/src/mini-program-management/index.js | 397 ++++++++++++ backend/web/src/order-detail/Card.js | 127 ++++ backend/web/src/order-detail/GoodsCard.js | 77 +++ .../web/src/order-detail/getOrderStatus.js | 23 + backend/web/src/order-detail/index.js | 206 +++++++ backend/web/src/sku-for-activity/index.js | 171 ++++++ backend/web/src/sku-item/index.js | 313 ++++++++++ backend/web/src/sku/SelectCell.js | 74 +++ backend/web/src/sku/index.js | 157 +++++ backend/web/src/spread/Movable/index.js | 185 ++++++ backend/web/src/spread/Movable/index.scss | 15 + backend/web/src/spread/index.js | 570 ++++++++++++++++++ backend/web/src/spread/normalize.scss | 354 +++++++++++ backend/web/src/spread/preview.scss | 102 ++++ backend/web/src/styles/amazing-creator.scss | 83 +++ backend/web/src/styles/content-header.scss | 90 +++ backend/web/src/styles/dashboard.scss | 180 ++++++ backend/web/src/styles/global.scss | 183 ++++++ backend/web/src/styles/header.scss | 128 ++++ backend/web/src/styles/index.scss | 7 + backend/web/src/styles/login.scss | 193 ++++++ backend/web/src/styles/sidebar.scss | 201 ++++++ backend/web/src/styles/variables.scss | 30 + backend/web/src/styles/widget.scss | 301 +++++++++ backend/web/src/utils/ajax.js | 76 +++ backend/web/webpack.config.js | 84 +++ 59 files changed, 6702 insertions(+) create mode 100644 backend/web/.babelrc create mode 100644 backend/web/.eslintrc.json create mode 100644 backend/web/.gitignore create mode 100644 backend/web/package.json create mode 100644 backend/web/src/custom-menu/AddItem.js create mode 100644 backend/web/src/custom-menu/Editor.js create mode 100644 backend/web/src/custom-menu/MenuItem.js create mode 100644 backend/web/src/custom-menu/index.js create mode 100644 backend/web/src/custom-menu/index.scss create mode 100644 backend/web/src/custom-menu/utils.js create mode 100644 backend/web/src/dashboard/CircleCard.js create mode 100644 backend/web/src/dashboard/LineChart.js create mode 100644 backend/web/src/dashboard/OverviewCard.js create mode 100644 backend/web/src/dashboard/PieChart.js create mode 100644 backend/web/src/dashboard/RadioGroup.js create mode 100644 backend/web/src/dashboard/Table.js create mode 100644 backend/web/src/dashboard/index.js create mode 100644 backend/web/src/iconfont.html create mode 100644 backend/web/src/import.html create mode 100644 backend/web/src/js/amazing-creator/GridEditor.js create mode 100644 backend/web/src/js/amazing-creator/Module.js create mode 100644 backend/web/src/js/amazing-creator/Previewer.js create mode 100644 backend/web/src/js/amazing-creator/PropsEditor.js create mode 100644 backend/web/src/js/amazing-creator/index.js create mode 100644 backend/web/src/js/amazing-creator/modules/GoodsList.js create mode 100644 backend/web/src/js/amazing-creator/modules/Swiper.js create mode 100644 backend/web/src/js/amazing-creator/old.js create mode 100644 backend/web/src/js/amazing-creator/store.js create mode 100644 backend/web/src/mini-program-management/MainDescription.js create mode 100644 backend/web/src/mini-program-management/ManageTriers.js create mode 100644 backend/web/src/mini-program-management/StepBar.js create mode 100644 backend/web/src/mini-program-management/common.js create mode 100644 backend/web/src/mini-program-management/data.js create mode 100644 backend/web/src/mini-program-management/index.js create mode 100644 backend/web/src/order-detail/Card.js create mode 100644 backend/web/src/order-detail/GoodsCard.js create mode 100644 backend/web/src/order-detail/getOrderStatus.js create mode 100644 backend/web/src/order-detail/index.js create mode 100644 backend/web/src/sku-for-activity/index.js create mode 100644 backend/web/src/sku-item/index.js create mode 100644 backend/web/src/sku/SelectCell.js create mode 100644 backend/web/src/sku/index.js create mode 100644 backend/web/src/spread/Movable/index.js create mode 100644 backend/web/src/spread/Movable/index.scss create mode 100644 backend/web/src/spread/index.js create mode 100644 backend/web/src/spread/normalize.scss create mode 100644 backend/web/src/spread/preview.scss create mode 100644 backend/web/src/styles/amazing-creator.scss create mode 100644 backend/web/src/styles/content-header.scss create mode 100644 backend/web/src/styles/dashboard.scss create mode 100644 backend/web/src/styles/global.scss create mode 100644 backend/web/src/styles/header.scss create mode 100644 backend/web/src/styles/index.scss create mode 100644 backend/web/src/styles/login.scss create mode 100644 backend/web/src/styles/sidebar.scss create mode 100644 backend/web/src/styles/variables.scss create mode 100644 backend/web/src/styles/widget.scss create mode 100644 backend/web/src/utils/ajax.js create mode 100644 backend/web/webpack.config.js diff --git a/backend/web/.babelrc b/backend/web/.babelrc new file mode 100644 index 0000000..0b6f443 --- /dev/null +++ b/backend/web/.babelrc @@ -0,0 +1,17 @@ +{ + "presets": [ + "@babel/preset-react", + "@babel/preset-env" + ], + "plugins": [ + ["@babel/plugin-proposal-decorators", { "legacy": true }], + ["@babel/proposal-class-properties", { "loose": true }], + "@babel/plugin-transform-runtime", + "babel-plugin-styled-components", + ["import", { + "libraryName": "antd", + "libraryDirectory": "es", + "style": "css" + }] + ] +} \ No newline at end of file diff --git a/backend/web/.eslintrc.json b/backend/web/.eslintrc.json new file mode 100644 index 0000000..a30c45b --- /dev/null +++ b/backend/web/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "extends": "eslint:recommended", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 2018, + "sourceType": "module" + }, + "plugins": [ + "react", + "react-hooks" + ], + "rules": { + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn" + } +} \ No newline at end of file diff --git a/backend/web/.gitignore b/backend/web/.gitignore new file mode 100644 index 0000000..cb7f43e --- /dev/null +++ b/backend/web/.gitignore @@ -0,0 +1,11 @@ +/index.php +/index-test.php +/robots.txt +uploads +css/umeditor/php/upload/ + +node_modules/ +yarn-error.log +yarn.lock +package-lock.json +ueditor diff --git a/backend/web/package.json b/backend/web/package.json new file mode 100644 index 0000000..a2b9588 --- /dev/null +++ b/backend/web/package.json @@ -0,0 +1,89 @@ +{ + "scripts": { + "dev": "webpack --mode=development --watch", + "dev:dashboard": "webpack --module dashboard --mode=development --watch", + "build:dashboard": "webpack --module dashboard --mode=production", + "dev:mini-program-management": "webpack --module mini_program_management --mode=development --watch", + "build:mini-program-management": "webpack --module mini_program_management --mode=production", + "dev:spread": "webpack --module spread --mode=development --watch", + "build:spread": "webpack --module spread --mode=production", + "dev:sku": "webpack --module sku --mode=development --watch", + "build:sku": "webpack --module sku --mode=production", + "dev:style": "webpack --module style --mode=development --watch", + "build:style": "webpack --module style --mode=production", + "dev:order-detail": "webpack --module order_detail --mode=development --watch", + "build:order-detail": "webpack --module order_detail --mode=production", + "dev:sku-item": "webpack --module sku_item --mode=development --watch", + "build:sku-item": "webpack --module sku_item --mode=production", + "dev:custom-menu": "webpack --module custom-menu --mode=development --watch", + "build:custom-menu": "webpack --module custom-menu --mode=production", + "dev:sku-for-activity": "webpack --module sku_for_activity --mode=development --watch", + "build:sku-for-activity": "webpack --module sku_for_activity --mode=production", + "build": "webpack --mode=production" + }, + "dependencies": { + "@babel/core": "^7.0.1", + "@babel/preset-react": "^7.0.0", + "antd": "^3.16.5", + "axios": "^0.18.0", + "babel-loader": "^8.0.2", + "bizcharts": "^3.4.3", + "che-react-number-easing": "^0.1.2", + "classnames": "^2.2.6", + "clone": "^2.1.2", + "compare-versions": "^3.4.0", + "dayjs": "^1.8.0", + "extract-text-webpack-plugin": "^4.0.0-beta.0", + "fast-deep-equal": "^2.0.1", + "file-loader": "^2.0.0", + "html2canvas": "^1.0.0-alpha.12", + "immer": "^2.1.5", + "mobx": "^5.6.0", + "mobx-react": "^5.3.6", + "polished": "^2.3.3", + "prop-types": "^15.6.2", + "qs": "^6.6.0", + "rasterizehtml": "^1.3.0", + "rc-switch": "^1.8.0", + "rc-upload": "^2.6.3", + "react": "^16.8.6", + "react-beautiful-dnd": "^11.0.2", + "react-chartjs-2": "^2.7.4", + "react-circle": "^1.1.1", + "react-color": "^2.17.0", + "react-dom": "^16.8.6", + "react-grid-layout": "^0.16.6", + "react-select": "^2.1.1", + "react-sortable-hoc": "^0.8.3", + "sass-resources-loader": "^1.3.3", + "styled-components": "^4.1.3", + "throttle-debounce": "^2.1.0", + "to-string-loader": "^1.1.5", + "url-loader": "^1.1.1", + "uuid": "^3.3.2", + "webpack": "^4.29.5", + "webpack-cli": "^3.2.3" + }, + "devDependencies": { + "@babel/plugin-proposal-class-properties": "^7.1.0", + "@babel/plugin-proposal-decorators": "^7.1.2", + "@babel/plugin-transform-runtime": "^7.4.3", + "@babel/preset-env": "^7.1.0", + "babel-plugin-import": "^1.11.0", + "babel-plugin-styled-components": "^1.10.0", + "clean-webpack-plugin": "^2.0.0", + "copy-webpack-plugin": "^4.6.0", + "css-loader": "^1.0.0", + "eslint": "^5.16.0", + "eslint-plugin-react": "^7.12.4", + "eslint-plugin-react-hooks": "^1.6.0", + "html-webpack-plugin": "^3.2.0", + "mini-css-extract-plugin": "^0.5.0", + "mobx-react-devtools": "^6.0.3", + "node-sass": "^4.9.3", + "require-context": "^1.1.0", + "resolve-url-loader": "^2.3.1", + "sass-loader": "^7.1.0", + "style-loader": "^0.23.0" + } +} diff --git a/backend/web/src/custom-menu/AddItem.js b/backend/web/src/custom-menu/AddItem.js new file mode 100644 index 0000000..ecbc659 --- /dev/null +++ b/backend/web/src/custom-menu/AddItem.js @@ -0,0 +1,30 @@ +import React from 'react' +import {Icon} from "antd"; +import styled from "styled-components"; + +export default function AddItem({ subMenu = false, onClick }) { + return ( + + + + ) +} + +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; + } + } +`; \ No newline at end of file diff --git a/backend/web/src/custom-menu/Editor.js b/backend/web/src/custom-menu/Editor.js new file mode 100644 index 0000000..f7c56c0 --- /dev/null +++ b/backend/web/src/custom-menu/Editor.js @@ -0,0 +1,124 @@ +import React, { createContext, useContext } from 'react' +import { Form, Input, Radio, Select } from "antd"; +import styled from "styled-components"; +import { produce } from "immer"; + +const FormItem = Form.Item; +const RadioGroup = Radio.Group; +const Option = Select.Option; + +const { customPageList } = window; + +const MenuContext = createContext(); +const MenuContextProvider = MenuContext.Provider; + +function InputItem({ label, name }) { + const { activeMenu, formItemLayout, onChange } = useContext(MenuContext); + const rawValue = activeMenu.content.value; + const value = name ? (rawValue ? rawValue[name] : '') : rawValue; + + function handleChange(e) { + const newValue = e.target.value; + onChange(name ? { + ...rawValue, + [name]: newValue + } : newValue); + } + + return ( + + + + ) +} + +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: + }, + customPage: { + label: '自定义页面', + content: ( + + + + ) + }, + miniprogram: { + label: '跳转小程序', + content: ( + <> + + + + + ) + } + }; + + return ( + +
+ + onChange('title', e.target.value)} + /> + + {(!activeMenu.children || activeMenu.children.length === 0) && ( + + + + {Object.keys(typeTable).map(name => ( + {typeTable[name].label} + ))} + + + {activeMenu.content.type && ( + + {typeTable[activeMenu.content.type].content} + + )} + + )} +
+
+ ) +} + +const Root = styled.div` + margin-left: 20px; + padding: 20px; + background: #fff; +`; + +const ContentValue = styled.div` + +`; \ No newline at end of file diff --git a/backend/web/src/custom-menu/MenuItem.js b/backend/web/src/custom-menu/MenuItem.js new file mode 100644 index 0000000..eaaad1e --- /dev/null +++ b/backend/web/src/custom-menu/MenuItem.js @@ -0,0 +1,176 @@ +import React, { useState } from 'react' +import { Button, Popover } from "antd"; +import styled from "styled-components"; +import ClassNames from 'classnames' +import AddItem from './AddItem' +import { DragDropContext, Draggable, Droppable} from "react-beautiful-dnd"; + +function BaseMenuItem({ title, active, onClick, onDelete, children, provided, snapshot }) { + const providedProps = provided ? { + ref: provided.innerRef, + ...provided.draggableProps, + ...provided.dragHandleProps + } : {}; + + return ( + + + {title} + { + e.stopPropagation(); + onDelete(e); + }} + /> + {children} + + + ) +} + +export default function MenuItem( + { + title, children, active, activeSubMenuId, showSubMenu, provided, snapshot, onActivate, + onAddSubMenu, onDeleteSubMenu, onReorderSubMenu, onDelete + }) { + const content = ( + <> + onActivate(draggableId)} + onDragEnd={result => onReorderSubMenu(result)} + > + + {provided => ( +
+ {children && children.map(({id, title}, subIndex) => ( + + {(provided, snapshot) => ( + + onActivate(id)} + onDelete={() => onDeleteSubMenu(subIndex)} + /> + + )} + + ))} + {provided.placeholder} +
+ )} +
+
+ {(!children || children.length < 5) && } + + ); + + return ( + + onActivate()} + onDelete={onDelete} + > + {(children && children.length > 0) && } + + + ) +} + +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; + } +`; \ No newline at end of file diff --git a/backend/web/src/custom-menu/index.js b/backend/web/src/custom-menu/index.js new file mode 100644 index 0000000..a969cdd --- /dev/null +++ b/backend/web/src/custom-menu/index.js @@ -0,0 +1,247 @@ +import React, { useState } from 'react' +import ReactDOM from 'react-dom' +import styled from 'styled-components' +import { Button, Icon, message } from 'antd' +import { produce } from 'immer' +import axios from 'axios' +import { DragDropContext, Draggable, Droppable} from "react-beautiful-dnd"; +import './index.scss' +import Editor from './Editor' +import AddItem from './AddItem' +import MenuItem from './MenuItem' +import '../utils/ajax' +import { handleDragEnd } from './utils' +import uuid from 'uuid' + +const IconFont = Icon.createFromIconfontCN({ + scriptUrl: '//at.alicdn.com/t/font_827976_5ojdd8bmdsw.js', +}); + +const { data: initMenuList = [] } = window; + +function getActiveMenu(menuList, activeMenuInfo) { + const activeParent = menuList.find(menu => menu.id === activeMenuInfo.id); + return (activeParent && activeMenuInfo.subId !== null && activeParent.children.length > 0) ? + activeParent.children.find(child => child.id === activeMenuInfo.subId) : activeParent; +} + +function App() { + const [menuList, setMenuList] = useState(initMenuList); + const [activeMenuInfo, setActiveMenuInfo] = useState({ id: null, subId: null }); + + const activeMenu = getActiveMenu(menuList, activeMenuInfo); + + function addMenu() { + const id = uuid(); + + setMenuList([ + ...menuList, + { + id, + title: '菜单名称', + content: { + type: null, + value: null + }, + children: [] + } + ]); + + setActiveMenuInfo({ + id, + subId: null + }) + } + + function deleteMenu(index) { + setMenuList(produce(menuList, (draft => { + draft.splice(index, 1); + }))); + } + + function deleteSubMenu(index, subIndex) { + setMenuList(produce(menuList, (draft => { + draft[index].children.splice(subIndex, 1); + }))); + } + + function addSubMenu(index) { + const id = uuid(); + + setMenuList(produce(menuList, (draft => { + draft[index].children.push({ + id, + title: '菜单名称', + content: { + type: null, + value: null + } + }) + }))); + setActiveMenuInfo({ + id: menuList[index].id, + subId: id + }) + } + + function changeForm(name, value) { + setMenuList(produce(menuList, (draft => { + const activeMenu = getActiveMenu(draft, activeMenuInfo); + activeMenu[name] = value; + }))); + } + + function checkData() { + return menuList.every(menu => { + if (menu.children && menu.children.length > 0) { + return menu.title && menu.children.every(item => { + if (item.content.type === 'weapp') { + const { appId, url, spareWebUrl } = item.content.value; + return item.title && appId && url && spareWebUrl + } else { + return item.title && item.content.type && item.content.value + } + }) + } else { + return menu.title && menu.content.type && menu.content.value + } + }) + } + + async function submit() { + if (checkData()) { + await axios.post('', { + data: menuList + }); + message.success('保存成功'); + } else { + message.error('数据填写不完整,请检查是否有数据漏填') + } + } + + function reorderSubMenu(index, result) { + handleDragEnd(menuList[index].children, result, newSubMenu => { + setMenuList(produce(menuList, draft => { + draft[index].children = newSubMenu; + })) + }) + } + + return ( + + + + + + + + + setActiveMenuInfo({ id: draggableId, subId: null })} + onDragEnd={result => handleDragEnd(menuList, result, setMenuList)} + > + + {provided => ( + + {menuList.map((menu, index) => ( + + {(provided, snapshot) => ( + addSubMenu(index)} + onActivate={(subId = null) => setActiveMenuInfo({ id: menu.id, subId })} + onDelete={() => deleteMenu(index)} + onDeleteSubMenu={(subIndex) => deleteSubMenu(index, subIndex)} + onReorderSubMenu={result => reorderSubMenu(index, result)} + /> + )} + + ))} + {provided.placeholder} + + )} + + + {menuList.length < 3 && } + + + + {activeMenu && } + + + ) +} + +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(, document.getElementById('app')); \ No newline at end of file diff --git a/backend/web/src/custom-menu/index.scss b/backend/web/src/custom-menu/index.scss new file mode 100644 index 0000000..87e9898 --- /dev/null +++ b/backend/web/src/custom-menu/index.scss @@ -0,0 +1,7 @@ +.ant-popover-inner-content { + padding: 0!important; +} + +.ant-popover-arrow { + display: none!important; +} \ No newline at end of file diff --git a/backend/web/src/custom-menu/utils.js b/backend/web/src/custom-menu/utils.js new file mode 100644 index 0000000..115db70 --- /dev/null +++ b/backend/web/src/custom-menu/utils.js @@ -0,0 +1,25 @@ +export const reorder = (list, startIndex, endIndex) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result; +}; + +export function handleDragEnd(list, result, setList) { + if (!result.destination) { + return; + } + + if (result.destination.index === result.source.index) { + return; + } + + const newList = reorder( + list, + result.source.index, + result.destination.index + ); + + setList(newList); +} \ No newline at end of file diff --git a/backend/web/src/dashboard/CircleCard.js b/backend/web/src/dashboard/CircleCard.js new file mode 100644 index 0000000..e54905c --- /dev/null +++ b/backend/web/src/dashboard/CircleCard.js @@ -0,0 +1,149 @@ +import Circle from 'react-circle'; +import React from "react"; +import PropTypes from "prop-types"; +import styled from 'styled-components' +import ClassNames from 'classnames' +import {Icon} from 'antd' +import NumberEasing from "che-react-number-easing" + +export default class RadioGroup extends React.Component { + constructor(props) { + super(props); + this.state = { + percentForDisplay: 0, + growthForDisplay: 0, + } + } + + static propTypes = { + title: PropTypes.string, + percent: PropTypes.number, + growth: PropTypes.number, + color: PropTypes.string, + animationDuration: PropTypes.number + }; + + static defaultProps = { + title: '', + percent: 0, + growth: 0, + color: '#312c50', + animationDuration: 1500 + }; + + componentDidMount() { + const {percent, growth} = this.props; + + function fixDigits(number, fractionDigits) { + return Number((number * 100).toFixed(fractionDigits)) + } + + this.setState({ + percentForDisplay: fixDigits(percent, 2), + growthForDisplay: fixDigits(growth, 2) + }) + } + + render() { + const {percentForDisplay, growthForDisplay} = this.state; + const {title, percent, growth, color, animationDuration} = this.props; + + return ( + + {title} + + + + {percent > 0 ? ( + <> + + % + + ) :
N/A
} +
+
+ {(growth !== 0 && growth > 0) && ( + 0, + zero: growth === 0, + down: growth < 0 + })} + title={`同比${growth > 0 ? '增长' : '下降'} ${Math.abs(growth.toFixed(4) * 100)}%`} + > + 0 ? 'up' : 'down')} /> + + % + + )} +
+ ) + } +} + +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}; +`; \ No newline at end of file diff --git a/backend/web/src/dashboard/LineChart.js b/backend/web/src/dashboard/LineChart.js new file mode 100644 index 0000000..0247746 --- /dev/null +++ b/backend/web/src/dashboard/LineChart.js @@ -0,0 +1,135 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Chart, Geom, Axis, Tooltip, Legend, Coord } from 'bizcharts'; +import styled from 'styled-components' +import RadioGroup from './RadioGroup' +import {setLightness} from 'polished' + +export default class LineChart extends React.Component { + constructor(props) { + super(props); + this.state = { + checkedIndex: 0 + } + } + + static propTypes = { + data: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, + label: PropTypes.string.isRequired, + color: PropTypes.string, + title: PropTypes.string, + labelY: PropTypes.string, + prefix: PropTypes.string, + unit: PropTypes.string, + dateCount: PropTypes.number, + type: PropTypes.oneOf(['integer']) + }; + + static defaultProps = { + data: [], + label: '', + color: '#5d58e9', + title: '', + labelY: '', + prefix: '', + unit: '', + dateCount: 7, + type: 'integer' + }; + + render() { + const {checkedIndex} = this.state; + const {data, label, prefix, unit, title, color, type, dateCount} = this.props; + const currentData = data[checkedIndex]; + const position = `${currentData.key || 'date'}*value`; + const isLittle = !currentData.data.find(i => i.value > 5); + + // 定义度量 + const cols = { + [data[checkedIndex].key || 'date']: { + alias: '日期', + tickCount: dateCount + }, + value: { + alias: label, + type: 'linear', + formatter: value => `${prefix}${value}${unit}`, + tickInterval: type === 'integer' && isLittle ? 1 : undefined + } + }; + + return ( +
+
+

+ {title.replace(//g, data[checkedIndex].data.length) + .replace(/

+ {data.length > 1 && ( + i.label)} + color={color} + onChange={(newValue) => { + this.setState({checkedIndex: newValue}) + }} + /> + )} +
+
+ + + + + + + +
+
+ ); + } +} + +const Header = styled.div` + display: flex; + align-items: center; + margin: 10px 0 20px 0; + h1 { + flex-grow: 1; + line-height: 1; + margin: 0; + } +`; \ No newline at end of file diff --git a/backend/web/src/dashboard/OverviewCard.js b/backend/web/src/dashboard/OverviewCard.js new file mode 100644 index 0000000..4ab887f --- /dev/null +++ b/backend/web/src/dashboard/OverviewCard.js @@ -0,0 +1,53 @@ +import React from "react"; +import PropTypes from "prop-types"; +import NumberEasing from 'che-react-number-easing'; + +export default class OverviewCard extends React.Component{ + constructor(props) { + super(props); + this.state = { + number: 0 + } + } + + static propTypes = { + title: PropTypes.string.isRequired, + number: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string + ]).isRequired, + image: PropTypes.string, + isPrice: PropTypes.bool + }; + + static defaultProps = { + isPrice: false + }; + + + componentDidMount() { + this.setState({ + number: parseFloat(this.props.number).toFixed(2) + }) + } + + render() { + const {number} = this.state; + const {image, title, isPrice} = this.props; + return ( +
+
+

{title}

+

{isPrice && '¥'} + +

+
+ text +
+ ) + } +} \ No newline at end of file diff --git a/backend/web/src/dashboard/PieChart.js b/backend/web/src/dashboard/PieChart.js new file mode 100644 index 0000000..3eba6f4 --- /dev/null +++ b/backend/web/src/dashboard/PieChart.js @@ -0,0 +1,40 @@ +// var myPieChart = new Chart(ctxTotalPie, { +// type: 'pie', +// data: { +// datasets: [{ +// data: [ +// data.total.finish, +// data.total.refund, +// data.total.shipping, +// data.total.unpaied, +// data.total.unshipping +// ], +// backgroundColor: [ +// 'rgba(255, 87, 51, 1)', +// 'rgba(95, 108, 160, 1)', +// 'rgba(255, 206, 86, 1)', +// 'rgba(75, 192, 192, 1)', +// 'rgba(153, 102, 255, 1)', +// 'rgba(255, 159, 64, 1)' +// ], +// // borderColor: [ +// // 'rgba(95, 108, 160, 1)', +// // 'rgba(54, 162, 235, 1)', +// // 'rgba(255, 206, 86, 1)', +// // 'rgba(75, 192, 192, 1)', +// // 'rgba(153, 102, 255, 1)', +// // 'rgba(255, 159, 64, 1)' +// // ] +// }], +// +// // These labels appear in the legend and in the tooltips when hovering different arcs +// labels: [ +// '已完成', +// '申请退款中', +// '待收货', +// '未付款', +// '待发货' +// ] +// }, +// options: {} +// }); \ No newline at end of file diff --git a/backend/web/src/dashboard/RadioGroup.js b/backend/web/src/dashboard/RadioGroup.js new file mode 100644 index 0000000..f34f707 --- /dev/null +++ b/backend/web/src/dashboard/RadioGroup.js @@ -0,0 +1,74 @@ +import React from "react"; +import PropTypes from "prop-types"; +import styled from 'styled-components' +import {setLightness} from 'polished' + +export default class RadioGroup extends React.Component { + constructor(props) { + super(props); + this.state = { + number: 0 + } + } + + static propTypes = { + current: PropTypes.number, + items: PropTypes.arrayOf(PropTypes.string).isRequired, + color: PropTypes.string, + onChange: PropTypes.func + }; + + static defaultProps = { + current: '', + items: [], + onChange: () => { + }, + color: '#5d58e9' + }; + + render() { + const {current, items, color, onChange} = this.props; + + return ( + + {items.map((item, index) => ( + { + current !== item && onChange(index) + }} + >{item} + ))} + + ) + } +} + +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; + } +`; \ No newline at end of file diff --git a/backend/web/src/dashboard/Table.js b/backend/web/src/dashboard/Table.js new file mode 100644 index 0000000..b61d1c0 --- /dev/null +++ b/backend/web/src/dashboard/Table.js @@ -0,0 +1,32 @@ +import React from "react"; +import PropTypes from "prop-types"; +import styled from "styled-components"; + +export default class Table extends React.Component { + constructor(props) { + super(props); + this.state = { + + }; + } + + static defaultProps = { + + }; + + static propTypes = { + + }; + + render() { + return ( + + + + ) + } +} + +const Root = styled.div` + +`; \ No newline at end of file diff --git a/backend/web/src/dashboard/index.js b/backend/web/src/dashboard/index.js new file mode 100644 index 0000000..a7d6e5e --- /dev/null +++ b/backend/web/src/dashboard/index.js @@ -0,0 +1,145 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import styled from 'styled-components' +import CircleCard from './CircleCard'; +import LineChart from './LineChart' +import OverviewCard from './OverviewCard.js' +import '../styles/dashboard.scss' +import 'rc-switch/assets/index.css' + +const {total, sales, monthlySales, users, topGoods, topOrders, DAU, + dailyConversionRate, monthlyConversionRate} = data; +const {salesAmount, salesCount, orderCount, goodsCount, userCount} = total; +const fixedSales = sales.map(i => ({ + ...i, + value: Number.parseFloat(i.value).toFixed(2) +})); + +const fixedonthlySales = monthlySales.map(i => ({ + ...i, + value: Number.parseFloat(i.value).toFixed(2) +})); + +const salesData = [ + { + label: '30天', + data: fixedSales + }, + { + label: '15天', + data: fixedSales.slice(15), + }, + { + label: '12个月', + data: fixedonthlySales.reverse(), + key: 'month' + }, +]; + +const topGoodsData = [ + { + label: '销量最高', + highlightCol: 1, + data: topGoods.count.map(i => ({ + '商品名称': i.name, + '销量': i.count, + '销售额': i.sales + })) + }, + { + label: '销售额最高', + highlightCol: 2, + data: data.topGoods.amount.map(i => ({ + '商品名称': i.name, + '销量': i.count, + '销售额': i.sales + })) + }, +]; + + +const CirclePanel = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + height: 420px; +`; + +const dashboard = ( +
+
+
+ + + + + +
+
+
+ + + + + + {/**/} +
+
+ + + + {/* ({*/} + {/*'名字': i.user.name,*/} + {/*'金额': i.info.pay_fee*/} + {/*}))*/} + {/*}]}*/} + {/*color='#ed862b'*/} + {/*priceCol={1}*/} + {/*/>*/} +
+
+); + +ReactDOM.render( + dashboard, + document.getElementById('react') +); \ No newline at end of file diff --git a/backend/web/src/iconfont.html b/backend/web/src/iconfont.html new file mode 100644 index 0000000..7679640 --- /dev/null +++ b/backend/web/src/iconfont.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/web/src/import.html b/backend/web/src/import.html new file mode 100644 index 0000000..db8c706 --- /dev/null +++ b/backend/web/src/import.html @@ -0,0 +1,4 @@ +
+ \ No newline at end of file diff --git a/backend/web/src/js/amazing-creator/GridEditor.js b/backend/web/src/js/amazing-creator/GridEditor.js new file mode 100644 index 0000000..aa3c959 --- /dev/null +++ b/backend/web/src/js/amazing-creator/GridEditor.js @@ -0,0 +1,36 @@ +import React, {Component} from "react"; +import GridLayout from "react-grid-layout"; +import styled from 'styled-components' +import "react-grid-layout/css/styles.css"; +import "react-resizable/css/styles.css"; +import {inject, observer} from 'mobx-react' +import Module from './Module' + +@inject('store') +@observer +export default class GridEditor extends React.Component { + onLayoutChange = (layout) => { + const {changeLayout} = this.props.store; + changeLayout(layout); + }; + + render() { + const {layout, attrs, add} = this.props.store; + + return ( + + {layout.map(item => ( +
+ +
+ ))} +
+ ) + } +} \ No newline at end of file diff --git a/backend/web/src/js/amazing-creator/Module.js b/backend/web/src/js/amazing-creator/Module.js new file mode 100644 index 0000000..8562f50 --- /dev/null +++ b/backend/web/src/js/amazing-creator/Module.js @@ -0,0 +1,101 @@ +import React from "react"; +import styled from "styled-components"; +import PropTypes from 'prop-types' +import Swiper from './modules/Swiper' +import GoodsList from './modules/GoodsList' + +export default class Module extends React.Component { + onLayoutChange = (layout) => { + const {changeLayout} = this.props.store; + changeLayout(layout); + }; + + static defaultProps = { + layout: {}, + attr: {} + }; + + static propTypes = { + layout: PropTypes.object, + attr: PropTypes.object + }; + + render() { + const {attr} = this.props; + + const moduleTable = { + Swiper: , + GoodsList: + }; + + if (moduleTable[attr.component]) { + return ( + + {moduleTable[attr.component]} + + + + + ) + } else { + return ( + {attr.component} + ) + } + } +} + +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); + } +`; \ No newline at end of file diff --git a/backend/web/src/js/amazing-creator/Previewer.js b/backend/web/src/js/amazing-creator/Previewer.js new file mode 100644 index 0000000..64f2dc3 --- /dev/null +++ b/backend/web/src/js/amazing-creator/Previewer.js @@ -0,0 +1,106 @@ +import React, {Component} from "react"; +import GridEditor from './GridEditor' +import styled from 'styled-components' + +export default class Previewer extends React.Component { + state = { + time: `${new Date().getHours()}:${new Date().getMinutes()}` + }; + + componentWillMount() { + this.timer = setInterval(() => { + const now = new Date(); + this.setState({ + time: `${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}` + }) + }, 1000) + } + + componentWillUnmount() { + this.timer && clearInterval(this.timer); + } + + render() { + const {time} = this.state; + // layout is an array of objects, see the demo for more complete usage + + return ( + +
+ + {time} + + + 首页 + +
+ + + + +
+ ) + } +} + +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; +`; diff --git a/backend/web/src/js/amazing-creator/PropsEditor.js b/backend/web/src/js/amazing-creator/PropsEditor.js new file mode 100644 index 0000000..2474ffb --- /dev/null +++ b/backend/web/src/js/amazing-creator/PropsEditor.js @@ -0,0 +1,16 @@ +import React, {Component} from "react"; +import styled from 'styled-components' + +export default class PropsEditor extends React.Component { + render() { + return ( + + + + ) + } +} + +const Wrapper = styled.div` + flex-grow: 1; +`; \ No newline at end of file diff --git a/backend/web/src/js/amazing-creator/index.js b/backend/web/src/js/amazing-creator/index.js new file mode 100644 index 0000000..23c0637 --- /dev/null +++ b/backend/web/src/js/amazing-creator/index.js @@ -0,0 +1,29 @@ +import React, {Component} from 'react'; +import {render} from "react-dom"; +import axios from 'axios' +import styled from 'styled-components' +import Previewer from './Previewer' +import PropsEditor from './PropsEditor' +import {Provider} from 'mobx-react' +import store from './store' +import DevTools from 'mobx-react-devtools'; + +const Wrapper = styled.div` + display: flex; +`; + +class AmazingCreator extends React.Component { + render() { + return ( + + + + + + + + ) + } +} + +render(, document.getElementById('edit-home')); \ No newline at end of file diff --git a/backend/web/src/js/amazing-creator/modules/GoodsList.js b/backend/web/src/js/amazing-creator/modules/GoodsList.js new file mode 100644 index 0000000..e4e68e1 --- /dev/null +++ b/backend/web/src/js/amazing-creator/modules/GoodsList.js @@ -0,0 +1,67 @@ +import React from "react"; +import PropTypes from "prop-types"; +import styled from 'styled-components' + +export default class GoodsList extends React.Component { + static propTypes = { + name: PropTypes.string, + title: PropTypes.string, + type: PropTypes.string, + onChangeTitle: PropTypes.func, + onChangeType: PropTypes.func + }; + + render() { + const {name, title, type, onChangeTitle, onChangeType} = this.props; + const typeImageTable = { + grid: '/img/grid.png', + list: '/img/list.png', + horizScroll: '/img/scroll.png', + }; + + const options = [ + {value: 1, label: '横排(右滚动)'}, + {value: 2, label: '多列(三列)'}, + {value: 3, label: '竖排(下滚动)'} + ]; + + return ( + +
+ {title} + 查看更多> +
+ +
+ ) + } +} + +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%; +`; \ No newline at end of file diff --git a/backend/web/src/js/amazing-creator/modules/Swiper.js b/backend/web/src/js/amazing-creator/modules/Swiper.js new file mode 100644 index 0000000..d464a1f --- /dev/null +++ b/backend/web/src/js/amazing-creator/modules/Swiper.js @@ -0,0 +1,14 @@ +import React from "react"; +import styled from 'styled-components' + +export default class Swiper extends React.Component { + render() { + return + } +} + +const Image = styled.img` + width: 100%; + height: 100%; + user-select: none; +`; \ No newline at end of file diff --git a/backend/web/src/js/amazing-creator/old.js b/backend/web/src/js/amazing-creator/old.js new file mode 100644 index 0000000..43a77be --- /dev/null +++ b/backend/web/src/js/amazing-creator/old.js @@ -0,0 +1,150 @@ +import React, {Component} from 'react'; +import { + SortableContainer, + SortableElement, + SortableHandle, + arrayMove, +} from 'react-sortable-hoc'; +import PropTypes from 'prop-types' +import Select from 'react-select'; +import axios from 'axios' + +const DragHandle = SortableHandle(() => ( +
+)); // 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 ( +
+
+ + + + + + ) + } +} + +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` +`; \ No newline at end of file diff --git a/backend/web/src/mini-program-management/StepBar.js b/backend/web/src/mini-program-management/StepBar.js new file mode 100644 index 0000000..10d9aaf --- /dev/null +++ b/backend/web/src/mini-program-management/StepBar.js @@ -0,0 +1,38 @@ +import React from "react"; +import {Icon, Steps} from "antd"; +import {stepInfoList} from "./data"; + +const Step = Steps.Step; + +export default function StepBar({currentStep, auditStatusCode, currentStatusInfo}) { + if (currentStep <= 3) { + return ( + + {stepInfoList.map(({title, icon, status}, index) => { + if (index === 2 && status && status[auditStatusCode]) { + return ( + } + /> + ) + } else { + return ( + } + /> + ) + } + })} + + ) + } else { + return <>; + } +} \ No newline at end of file diff --git a/backend/web/src/mini-program-management/common.js b/backend/web/src/mini-program-management/common.js new file mode 100644 index 0000000..37d7c8c --- /dev/null +++ b/backend/web/src/mini-program-management/common.js @@ -0,0 +1,22 @@ +import React from "react"; +import styled from "styled-components"; +import {Icon, Spin} from "antd"; +const antIcon = ; + +const LoadingBarRoot = styled.div` + display: flex; + justify-content: center; + align-items: center; + margin: 10px 0; + > span { + margin-left: 20px; + } +`; + +export function LoadingBar({children}) { + return {children}... +} + +export const Reason = styled.div` + color: #f5222d; +`; \ No newline at end of file diff --git a/backend/web/src/mini-program-management/data.js b/backend/web/src/mini-program-management/data.js new file mode 100644 index 0000000..a53be2b --- /dev/null +++ b/backend/web/src/mini-program-management/data.js @@ -0,0 +1,146 @@ +import compareVersions from 'compare-versions' + +if (ENV_DEV) { + window.auditStatus = { + auditid: 438893451, + errcode: 0, + errmsg: "ok", + reason: '你充的钱不够,请再充钱', + status: 1, // 0: 审核成功,1: 审核失败,2: 审核中,3: 已撤回 + }; + + window.versions = { + version: "1.4.0", // 第三方平台模板库的最新版本 + commit_version: "1.3.0", // 上传为体验版 + audit_version: "", // 最新提交了审核的版本 + audited_version: "1.2.0", // 审核通过的版本 + release_version: "", // 线上的版本 + }; + + window.categoriesList = [ + {"first_class": "商家自营", "second_class": "机械/电子器件", "first_id": 304, "second_id": 788}, + {"first_class": "商家自营2", "second_class": "机械/电子器件", "first_id": 304, "second_id": 788}, + {"first_class": "商家自营3", "second_class": "机械/电子器件", "first_id": 304, "second_id": 788}, + {"first_class": "商家自营4", "second_class": "机械/电子器件", "first_id": 304, "second_id": 788}, + {"first_class": "商家自营5", "second_class": "机械/电子器件", "first_id": 304, "second_id": 788}, + {"first_class": "商家自营6", "second_class": "机械/电子器件", "first_id": 304, "second_id": 788}, + ]; +} + +const {auditStatus, versions, categoriesList} = window; + +const {version, commit_version, audit_version, audited_version, release_version} = versions; + +const versionsArray = [version, commit_version, audit_version, audited_version, release_version]; + +const stepInfoList = [ + { + title: '上传', + buttonText: '上传小程序代码', + icon: 'cloud-upload', + url: 'commit', + description: '将代码上传到微信公众平台,使其成为体验版' + }, + { + title: '审核', + buttonText: '提交审核', + icon: 'audit', + url: 'submit-audit', + action: 'submitAudit', + description: '微信小程序均需要审核后才能上线,一般情况下24小时内即可完成审核' + }, + { + title: '审核结果未知', + icon: 'question-circle', + buttonIcon: 'audit', + url: 'submit-audit', + action: 'submitAudit', + status: { + 0: { + title: '审核通过', + icon: 'check-circle', + }, + 1: { + title: '审核不通过', + icon: 'close-circle', + stepStatus: 'error', + buttonText: '重新提交审核', + description: '审核不通过,原因:' + }, + 2: { + title: '等待审核结果', + icon: 'sync', + description: '请耐心等待微信的审核结果,一般情况下 24 小时内即可完成审核,节假日审核可能需要一到三天' + }, + 3: { + title: '审核已撤回', + icon: 'rollback', + stepStatus: 'error', + buttonText: '重新提交审核', + description: '有人手动操作撤回了本次审核,请重新提交审核' + } + } + }, + { + title: '发布', + buttonText: '发布新版', + icon: 'export', + url: 'release', + description: '新版本的小程序需要等下一次冷启动,即退出超过一定时间(目前是5分钟)后才会应用上。' + }, +]; + +const initVersionList = [ + { + label: '版本库最新版本', + value: version + }, + { + label: '体验版版本', + value: commit_version + }, + { + label: '审核中版本', + value: auditStatus.status === 2 ? audit_version : '无' + }, + { + label: '已审核版本', + value: audited_version + }, + { + label: '线上版本', + value: release_version + }, +]; + +let canRelease = false; + +// 查找第一个需要完成的步骤 +let _currentStep = versionsArray[0] ? 0 : 4; + +if (versionsArray[2] && auditStatus.status === 2) { + _currentStep = 2; +} else if (versionsArray[0]) { + _currentStep = versionsArray.findIndex((currentVersion, index) => { + const nextItem = versionsArray[index + 1]; + return !nextItem || compareVersions(currentVersion, nextItem) > 0 + }); +} + +let _auditStatusCode = auditStatus.status; + +if (_currentStep === -1) { + _currentStep = 4; +} + +if (_currentStep !== 2) { + _auditStatusCode = 2; +} + +const currentStep = _currentStep; +const auditStatusCode = _auditStatusCode; +const latestVersion = version; +const auditedVersion = audited_version; + +export {currentStep, auditStatusCode, initVersionList, stepInfoList, latestVersion, + auditedVersion, categoriesList} \ No newline at end of file diff --git a/backend/web/src/mini-program-management/index.js b/backend/web/src/mini-program-management/index.js new file mode 100644 index 0000000..d2e189b --- /dev/null +++ b/backend/web/src/mini-program-management/index.js @@ -0,0 +1,397 @@ +import React, {useState} from "react"; +import styled from "styled-components"; +import ReactDOM from "react-dom"; +import {Icon, Button, Alert, Modal, message, LocaleProvider, Checkbox} from 'antd'; +import zhCN from 'antd/lib/locale-provider/zh_CN'; +import axios from 'axios' +import compareVersions from 'compare-versions' +import '../utils/ajax' +import {stepInfoList, currentStep, auditStatusCode, initVersionList, latestVersion, categoriesList, + auditedVersion, canRelease} from './data' +import {LoadingBar, Reason} from "./common"; +import ManageTriers from './ManageTriers' +import StepBar from './StepBar' +import MainDescription from './MainDescription' +import '../utils/ajax' + +const CheckboxGroup = Checkbox.Group; + +function getStepInfo(stepIndex, currentStepInfo, auditStatusCode) { + if (stepIndex === 2 && currentStepInfo.status && currentStepInfo.status[auditStatusCode]) { + return { + title: currentStepInfo.status[auditStatusCode].title, + icon: currentStepInfo.status[auditStatusCode].icon, + description: currentStepInfo.status[auditStatusCode].description + } + } else { + return { + title: currentStepInfo.title, + icon: currentStepInfo.icon, + description: currentStepInfo.description + } + } +} + +export default class MiniProgramManagement extends React.Component { + constructor(props) { + super(props); + + this.state = { + currentStep, + versionList: initVersionList, + auditStatusCode, + qrCodeToTry: null, + isLoadingAction: false, + isShowModalForQrCode: false, + isShowModalForTrier: false, + isShowModalForAudit: false, + isShowReleaseAlert: canRelease, + checkedCategoryIndexList: [0] + }; + } + + static defaultProps = {}; + + static propTypes = {}; + + handleAction = () => { + const {currentStep} = this.state; + const {url, action} = stepInfoList[currentStep]; + + if (action === 'submitAudit') { + this.setState({isShowModalForAudit: true}) + } else { + this.commitAction(url); + } + }; + + commitAction = (url, params = {}, independentAction) => { + const {currentStep} = this.state; + let {versionList} = this.state; + const {buttonText} = stepInfoList[currentStep]; + + independentAction || this.setState({isLoadingAction: true}); + axios.post(`/agent/${url}`, params).then(() => { + if (independentAction) { + message.success(independentAction + '成功'); + if (independentAction === '发布') { + // 更新线上版本 + versionList[4].value = versionList[3].value; + this.setState({isShowReleaseAlert: false}) + } + } else if (currentStep === 2) { + // 重新提交审核,不切换到下一个步骤 + this.setState({ + auditStatusCode: 2, + isLoadingAction: false + }); + message.success((buttonText || '重新提交审核') + '成功'); + } else { + // 切换到下一个步骤 + versionList[currentStep + 1].value = versionList[currentStep].value; + + this.setState({ + currentStep: currentStep + 1, + isLoadingAction: false, + versionList + }); + + message.success((buttonText || '重新提交审核') + '成功'); + + if (currentStep === 1) { + this.setState({ + auditStatusCode: 2 + }); + } else if (currentStep === 3) { + Modal.info({ + title: '提示', + content: stepInfoList[3].description, + cancelText: '', + }) + } + } + }).catch(() => { + this.setState({isLoadingAction: false}) + }) + }; + + hideModalFoeAudit = () => { + this.setState({isShowModalForAudit: false}) + }; + + submitAudit = () => { + const {currentStep, checkedCategoryIndexList} = this.state; + const {url} = stepInfoList[currentStep]; + + if (checkedCategoryIndexList && checkedCategoryIndexList.length > 0) { + this.hideModalFoeAudit(); + this.commitAction(url, {categories: checkedCategoryIndexList}); + this.setState({isLoadingAction: true}); + } else { + message.error('应至少选择一个分类') + } + }; + + showManagingTrier = () => { + this.setState({isShowModalForTrier: true, trierList: null}); + }; + + showModalForQrCode = () => { + this.setState({isShowModalForQrCode: true, qrCodeToTry: ''}); + axios.get('/agent/qr-code-to-try').then(data => { + this.setState({qrCodeToTry: data.image}) + }).catch((err) => this.setState({qrCodeToTry: `error: ${err}`})); + }; + + hideModalForQrCode = () => { + this.setState({isShowModalForQrCode: false}); + }; + + hideModalForTrier = () => { + this.setState({isShowModalForTrier: false}); + }; + + handleChangeCategory = (value) => { + this.setState({ + checkedCategoryIndexList: value + }) + }; + + render() { + const {currentStep, versionList, isLoadingAction, auditStatusCode, isShowModalForQrCode, isShowModalForAudit, + isShowModalForTrier, qrCodeToTry, isAddingTrier, checkedCategoryIndexList} = this.state; + const currentStepInfo = stepInfoList[currentStep]; + const currentStatusInfo = currentStepInfo && currentStepInfo.status && currentStepInfo.status[auditStatusCode]; + const currentStepInfoWithStatus = currentStepInfo && getStepInfo(currentStep, currentStepInfo, auditStatusCode); + + const isShowReleaseAlert = currentStep !== 2 && currentStep !== 3 && versionList[3].value && + (!versionList[4].value || compareVersions(versionList[3].value, versionList[4].value) > 0); + + return ( + + + + + + {currentStep === 0 && ( + + + 有新的版本{latestVersion},建议进行更新。 + 看看更新了什么 + + )} + type="success" + showIcon + icon={} + /> + + )} + {isShowReleaseAlert && ( + + + 有旧的已审核版本{auditedVersion},可以 + 发布此版本 + + )} + type="info" + showIcon + closable + /> + + )} + + + {(currentStepInfo && currentStepInfo.buttonText || currentStatusInfo && currentStatusInfo.buttonText) && ( + + + + )} + + {versionList.map(({label, value}, index) => ( + + + {value || '无'} + + ))} + + + + + + + + + + {checkedCategoryIndexList.length >= 5 && ( + + )} + ({ + label: ( + + {first_class} + + {second_class} + {third_class && <>{third_class}} + + ), + value: index, + disabled: checkedCategoryIndexList.length >= 5 && !checkedCategoryIndexList.includes(index) + }))} + onChange={this.handleChangeCategory} + /> + + + {qrCodeToTry ? (qrCodeToTry.startsWith('error: ') ? {qrCodeToTry.substr(6)} : + ) : ( + 加载中... + )} + + + + + + ) + } +} + +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( + + + , + document.getElementById('mini-program-management') +); \ No newline at end of file diff --git a/backend/web/src/order-detail/Card.js b/backend/web/src/order-detail/Card.js new file mode 100644 index 0000000..e40cfda --- /dev/null +++ b/backend/web/src/order-detail/Card.js @@ -0,0 +1,127 @@ +import React from "react"; +import PropTypes from "prop-types"; +import styled from "styled-components"; +import {setLightness} from 'polished' +import dayjs from 'dayjs' + +export default class Card extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + static propTypes = { + icon: PropTypes.string, + title: PropTypes.string, + color: PropTypes.string, + data: PropTypes.arrayOf(PropTypes.object) + }; + + static defaultProps = { + icon: '', + title: '', + color: '#5845e9', + data: [] + }; + + render() { + const {icon, title, color, data} = this.props; + return ( + +
+ + {title} +
+ + {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 ( + + + + {value || '无'} + {(item.link && item.link.url) && {item.link.text}} + + + ) + })} + +
+ ) + } +} + +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; + } +`; \ No newline at end of file diff --git a/backend/web/src/order-detail/GoodsCard.js b/backend/web/src/order-detail/GoodsCard.js new file mode 100644 index 0000000..bee4dcb --- /dev/null +++ b/backend/web/src/order-detail/GoodsCard.js @@ -0,0 +1,77 @@ +import React from "react"; +import PropTypes from "prop-types"; +import styled from "styled-components"; + +export default class GoodsCard extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + static propTypes = { + goodsInfo: PropTypes.object.isRequired + }; + + static defaultProps = { + goodsInfo: {} + }; + + render() { + const {goodsInfo} = this.props; + + return ( + + + +
{goodsInfo.goods_name}
+ {goodsInfo.sku_type && 规格:{goodsInfo.sku_type}} + ¥{goodsInfo.shop_price} +
+ x{goodsInfo.goods_number} +
+ ) + } +} + +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; +`; \ No newline at end of file diff --git a/backend/web/src/order-detail/getOrderStatus.js b/backend/web/src/order-detail/getOrderStatus.js new file mode 100644 index 0000000..4283d82 --- /dev/null +++ b/backend/web/src/order-detail/getOrderStatus.js @@ -0,0 +1,23 @@ +export default function getStatus(order) { + if (!order) return '无'; + let orderStatus = parseInt(order.order_status); + let payStatus = parseInt(order.pay_status); + let shippingStatus = parseInt(order.shipping_status); + + if (orderStatus === 1 && payStatus === 1) { + return '付款确认中' + } else if (orderStatus === 1 && payStatus === 2) { + // 物流状态 + let shippingStatusText; + if (order.shipping_type === 1) { + shippingStatusText = ['待提货', '待提货', '已收货', '正在退货中']; + } else { + shippingStatusText = ['待发货', '待收货', '已收货', '正在退货中']; + } + return shippingStatusText[shippingStatus] || '已付款'; + } + + const orderStatusText = ['未确认', '待付款', '已取消', '申请退款中', '已关闭', '已退款', '已完成']; + + return orderStatusText[orderStatus] || '未知'; +} diff --git a/backend/web/src/order-detail/index.js b/backend/web/src/order-detail/index.js new file mode 100644 index 0000000..4b89df9 --- /dev/null +++ b/backend/web/src/order-detail/index.js @@ -0,0 +1,206 @@ +import React from "react"; +import PropTypes from "prop-types"; +import styled from "styled-components"; +import ReactDOM from "react-dom"; +import GoodsCard from './GoodsCard' +import Card from './Card' +import getOrderStatus from './getOrderStatus' +import {Collapse} from 'antd'; +const Panel = Collapse.Panel; + +export default class DetailDisplay extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + static defaultProps = {}; + + static propTypes = {}; + + render() { + const {id, order_sn, goodsList, consignee, tel, created_at, pay_at, + goods_fee, shipping_fee, pay_fee, discount_money, remark, + postscript, pay_id, pay_type, shipping_type, goods_amount, province, city, + district, address, shipping_sn, shipping_detail_link + } = order_info; + + let userInfo = [ + { + label: '备注', + value: postscript, + }, + { + label: '配送方式', + value: shipping_type, + }, + { + label: '支付来源', + value: pay_id, + }, + { + label: '支付方式', + value: pay_type, + }, + ]; + + if (shipping_type === '快递配送') { + userInfo = [ + { + label: '地区', + value: province + city + district, + }, + { + label: '详细地址', + value: address, + }, + { + label: '联系人', + value: consignee, + }, + { + label: '联系电话', + value: tel, + }, + ...userInfo + ] + } else { + userInfo = [ + { + label: '提货人', + value: consignee, + }, + { + label: '联系电话', + value: tel, + }, + ...userInfo + ]; + } + return ( + + + + + + + + + + + {goodsList.map(goodsInfo => ( + + ))} + + + + + + ) + } +} + +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( + , + document.getElementById('detail-display') +); \ No newline at end of file diff --git a/backend/web/src/sku-for-activity/index.js b/backend/web/src/sku-for-activity/index.js new file mode 100644 index 0000000..9f5307d --- /dev/null +++ b/backend/web/src/sku-for-activity/index.js @@ -0,0 +1,171 @@ +import React, {useState} from 'react' +import ReactDOM from 'react-dom' +import {Table, Select, InputNumber, Button, message, LocaleProvider} from "antd"; +import styled from 'styled-components' +import produce from 'immer' +import axios from 'axios' +import zhCN from 'antd/lib/locale-provider/zh_CN'; +import queryString from 'qs'; +import '../utils/ajax' + +const {allSku, existingSku} = window.data; + +const Option = Select.Option; + +const Fragment = React.Fragment; + +const skuColumnsData = [ + { + key: 'id', + title: '规格', + type: 'select' + }, + { + key: 'price', + title: '价格', + type: 'input', + isPrice: true, + precision: 2, + min: 0 + }, + { + key: 'stock', + title: '库存(-1 为不限库存)', + type: 'input', + precision: 0, + min: -1 + } +]; + +function SkuTable({ data, allSkuOptions, onChangeItem, onDeleteItem }) { + const skuColumns = skuColumnsData.map(({key, title, type, precision, min, isPrice = false}, index) => ({ + title, + dataIndex: index, + key, + align: 'center', + render: (value, _, rowIndex) => ( + + {type === 'select' ? ( + + ) : ( + onChangeItem(rowIndex, key, value)} + /> + )} + + ) + })); + + const columns = [ + ...skuColumns, + { + key: 'delete', + title: '操作', + render: (_1, _2, index) => onDeleteItem(index)}>删除 + } + ]; + + const dataSource = data.map((item, index) => ({ key: index, ...item })); + + return ( + + + + ) +} + +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 ( + + + + + + + + + ) +} + +const ButtonGroup = styled.div` + margin-top: 20px; +`; + +const Wrapper = styled.div` + +`; + +ReactDOM.render( + + + , + document.getElementById('app') +); \ No newline at end of file diff --git a/backend/web/src/sku-item/index.js b/backend/web/src/sku-item/index.js new file mode 100644 index 0000000..acb0188 --- /dev/null +++ b/backend/web/src/sku-item/index.js @@ -0,0 +1,313 @@ +import React, {useState, useEffect} from 'react' +import ReactDOM from 'react-dom' +import {Table, Select, Input, InputNumber, Button, message, Modal, Spin, LocaleProvider} from "antd"; +import styled from 'styled-components' +import produce from 'immer' +import axios from 'axios' +import zhCN from 'antd/lib/locale-provider/zh_CN'; +import queryString from 'qs'; +import deepEqual from 'fast-deep-equal' +import deepClone from 'clone' +import '../utils/ajax' + +const {sku: defaultSku, attributes: defaultAttributes} = window; + +const Option = Select.Option; + +const Fragment = React.Fragment; + +const additionalCol = [ + { + key: 'price', + title: '价格', + precision: 2, + min: 0 + }, + { + key: 'stock', + title: '库存(-1 为不限库存)', + precision: 0, + min: -1 + }, + { + key: 'weight', + title: '重量(kg)', + precision: 2, + min: 0 + } +]; + +function buildInputCol(data, onChange) { + return data.map(({key, title, ...others}) => ({ + title, + dataIndex: key, + key, + align: 'center', + render: (text, _, index) => ( + 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) => ( + + ) + })) : [{ + title: '规格', + dataIndex: 'value', + key: 'value', + align: 'center', + render: (value, _, rowIndex) => ( + onChangeItem(rowIndex, 'value', e.target.value)} + /> + ) + }]; + + const columns = [ + ...skuColumns, + ...buildInputCol(additionalCol, onChangeItem), + { + key: 'delete', + title: '操作', + render: (_1, _2, index) => onDeleteItem(index)}>删除 + } + ]; + + return ( + +
+ + ) +} + +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 ( + + + + {modeList.map(({ key, title }) => ( + selectMode(key)} + >{title} + ))} + + + + + + + + + + ) +} + +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( + + + , + document.getElementById('app') +); \ No newline at end of file diff --git a/backend/web/src/sku/SelectCell.js b/backend/web/src/sku/SelectCell.js new file mode 100644 index 0000000..62fae7e --- /dev/null +++ b/backend/web/src/sku/SelectCell.js @@ -0,0 +1,74 @@ +import React from "react"; +import styled from "styled-components"; +import { Select, Spin, Icon } from 'antd'; +import PropTypes from 'prop-types' + +const Option = Select.Option; + +const antIcon = ; + +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 ( + + + {items ? ( + + ) : } + + ) + } +} + +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; +`; \ No newline at end of file diff --git a/backend/web/src/sku/index.js b/backend/web/src/sku/index.js new file mode 100644 index 0000000..42c96ef --- /dev/null +++ b/backend/web/src/sku/index.js @@ -0,0 +1,157 @@ +import React from "react"; +import styled from "styled-components"; +import ReactDOM from "react-dom"; +import { LocaleProvider, Alert } from 'antd'; +import zhCN from 'antd/lib/locale-provider/zh_CN'; +import SelectCell from './SelectCell' + +const goodsId = window.goodsId; +const currentAttr = window.currentAttr.map(({id, name, value}) => ({id, name, value})); +const canNotDeleteAttr = window.canNotDeleteAttr; + +class Sku extends React.Component { + constructor(props) { + super(props); + + this.state = { + skuList: currentAttr, + allAttr: null, + }; + } + + componentDidMount() { + const select = $('#goods-cat_id'); + const loadData = (id, shouldClearSelectedAttrs = false) => { + $.ajax({ + cache: false, + url: "filter-attribute", + data: {catId: id, goodsId: goodsId}, + dataType: "json", + success: data => { + const allAttr = data.map(({id, name, value}, index) => { + const canNotDeleteCurrent = canNotDeleteAttr.find(i => i.id === parseInt(id)); + + return { + id: parseInt(id), + name, + disabled: Boolean(canNotDeleteCurrent), + value: value.split(',').map(i => ({ + id: i, + name: i, + disabled: Boolean(canNotDeleteCurrent) && canNotDeleteCurrent.value.some(j => j === i) + })) + } + }); + if (shouldClearSelectedAttrs) { + this.setState({ + skuList: [], + allAttr + }) + } else { + this.setState({ + allAttr + }) + } + } + }); + } + + loadData(select.val()) + select.change(function () { + loadData($(this).val(), true) + }) + } + + handleChangeAttr = (value) => { + let {skuList} = this.state; + + this.setState({ + skuList: value.map(id => { + let result = skuList.find(i => i.id === id); + + if (!result) { + const attr = this.state.allAttr.find(i => i.id === id); + if (attr) { + const {id, name, value} = attr; + result = {id, name, value: value.map(i => i.id)}; + } + } + + return result; + }) + }) + }; + + handleChangeAttrItem = (index, value) => { + let {skuList} = this.state; + skuList[index].value = value; + this.setState({ + skuList + }) + }; + + render() { + const {skuList, allAttr} = this.state; + if (allAttr) { + return ( + + {(canNotDeleteAttr && canNotDeleteAttr.length > 0) && ( +
+ +
+ )} + + i.id)} + onChange={this.handleChangeAttr} + /> + {skuList && skuList.map((sku, index) => ( + i.id === sku.id).value} + value={sku.value} + onChange={this.handleChangeAttrItem.bind(undefined, index)} + /> + ))} + ({id, value})))} + /> + +
+ ) + } else { + return <> + } + } +} + +const Root = styled.div` + +`; + +const Header = styled.div` + margin-bottom: 30px; +`; + +const SelectGroup = styled.div` + width: 100%; +`; + +ReactDOM.render( + + + , + document.getElementById('sku-choose') +); \ No newline at end of file diff --git a/backend/web/src/spread/Movable/index.js b/backend/web/src/spread/Movable/index.js new file mode 100644 index 0000000..80d5e9d --- /dev/null +++ b/backend/web/src/spread/Movable/index.js @@ -0,0 +1,185 @@ +import React from "react"; +import PropTyps from 'prop-types' +import ClassNames from 'classnames' +import style from '!!to-string-loader!css-loader!resolve-url-loader!sass-loader?sourceMap!./index.scss' + +/** + * 计算对角线长度 + * @param x + * @param y + * @returns {number} + */ +function getDiagonalLength(x, y) { + return Math.sqrt(x * x + y * y); +} + +export default class Movable extends React.Component { + constructor(props) { + super(props); + + this.refMovable = React.createRef(); + + this.isMouseDown = false; + this.isResizeHandleMouseDown = false; + this.recordedPosX = 0; + this.recordedPosY = 0; + this.state = { + active: false + } + } + + static propTypes = { + posX: PropTyps.number, + posY: PropTyps.number, + width: PropTyps.number, + height: PropTyps.number, + scale: PropTyps.number, + canResize: PropTyps.bool, + keepAspectRatio: PropTyps.bool, + resizeHandlerSize: PropTyps.number, + onPosChange: PropTyps.func, + onSizeChange: PropTyps.func + }; + + static defaultProps = { + posX: 0, + posY: 0, + size: null, + scale: 1, + canResize: true, + keepAspectRatio: true, + resizeHandlerSize: 10, + onPosChange: () => {}, + onSizeChange: () => {} + }; + + componentWillMount() { + window.addEventListener('mousedown', (e) => { + this.setState({ + active: false + }); + }, true); + window.addEventListener('mouseup', (e) => { + this.handleMouseUp(e) + }); + window.addEventListener('mousemove', (e) => { + this.handleMouseMove(e) + }); + } + + savePosition = (e) => { + this.recordedPosX = e.clientX; + this.recordedPosY = e.clientY; + }; + + handleMouseMove = (e) => { + if (this.isResizeHandleMouseDown) { + const {width, height, scale, keepAspectRatio, onSizeChange} = this.props; + if (keepAspectRatio) { + // 保持横纵比缩放 + // 参考 Photoshop 等比缩放的交互,往左和上移动视为减小大小,往右和下移动视为增加大小 + + + // const {x, y} = this.refMovable.current.getBoundingClientRect(); + // const ratioX = e.clientX + this.recordedPosX / width; + // const ratioY = e.clientY - height / this.recordedPosY - height; + + // 具体实现:先计算 X 和 Y 方向的移动距离,屏幕左上角为原点,右下角方向为正 + const distanceX = (e.clientX - this.recordedPosX) * scale; + const distanceY = (e.clientY - this.recordedPosY) * scale; + + // 取X和Y两个方向距离的叠加距离, + // 可能全为正(鼠标往右下角移动)、全为负(鼠标往左上角移动)、正负叠加抵消(鼠标往左下角或右上角移动) + const compoundDistance = distanceX + distanceY; + + // 计算取X和Y两个方向距离的叠加距离的对角线距离 + const compoundDiagonalDistance = getDiagonalLength(compoundDistance, compoundDistance); + + // 鼠标本次对角线方向的直接移动距离 + const diagonalDistance = getDiagonalLength(distanceX, distanceY); + + // 取叠加距离和直接距离的最小值,并附上正负 + const diagonalResult = (compoundDistance >= 0 ? 1 : -1) * Math.min(compoundDiagonalDistance, diagonalDistance); + + // const originalDiagonal = getDiagonalLength(width, height); + + // const ratio = diagonalResult / originalDiagonal; + // const ratio = Math.min(ratioX, ratioY); + // const ratio = getDiagonalLength(ratioX, ratioY); + + onSizeChange(width + diagonalResult, height + diagonalResult); + } else { + // 不保持横纵比缩放 + const distanceX = (e.clientX - this.recordedPosX) * scale; + const distanceY = (e.clientY - this.recordedPosY) * scale; + + onSizeChange(width + distanceX, height + distanceY); + } + this.savePosition(e); + } if (this.isMouseDown) { + const {posX, posY, scale, onPosChange} = this.props; + const distanceX = posX + (e.clientX - this.recordedPosX) / scale; + const distanceY = posY + (e.clientY - this.recordedPosY) / scale; + onPosChange(distanceX, distanceY); + this.savePosition(e); + } + }; + + handleMouseDown = (e) => { + this.isMouseDown = true; + this.isResizeHandleMouseDown = false; + this.savePosition(e); + this.setState({ + active: true + }); + }; + + handleResizeMouseDown = (e) => { + this.isMouseDown = false; + this.isResizeHandleMouseDown = true; + this.savePosition(e); + this.setState({ + active: true + }); + document.body.style.cursor = 'se-resize'; + + e.stopPropagation(); + e.preventDefault(); + }; + + handleMouseUp = () => { + this.isMouseDown = false; + if (this.isResizeHandleMouseDown) { + this.isResizeHandleMouseDown = false; + document.body.style.cursor = 'default'; + } + }; + + render() { + const {active} = this.state; + const {posX, posY, width, height, canResize, resizeHandlerSize, children} = this.props; + + return ( +
+ +
+ {children} +
+ ) + } +} \ No newline at end of file diff --git a/backend/web/src/spread/Movable/index.scss b/backend/web/src/spread/Movable/index.scss new file mode 100644 index 0000000..17f6b91 --- /dev/null +++ b/backend/web/src/spread/Movable/index.scss @@ -0,0 +1,15 @@ +.movable-object { + position: absolute; + border: 2px solid transparent; + &.active { + border-color: #35c7ff; + box-shadow: 0 0 20px rgba(0, 0, 0, .3); + } + .resize-handler { + position: absolute; + right: 0; + bottom: 0; + cursor: se-resize; + user-select: none; + } +} \ No newline at end of file diff --git a/backend/web/src/spread/index.js b/backend/web/src/spread/index.js new file mode 100644 index 0000000..4df4c40 --- /dev/null +++ b/backend/web/src/spread/index.js @@ -0,0 +1,570 @@ +import React, {useState} from "react"; +import styled from "styled-components"; +import {LocaleProvider, Form, Radio, Input, Button, message, Icon, Popover, Modal} from 'antd'; +import ReactDOM from "react-dom"; +import zhCN from "antd/lib/locale-provider/zh_CN"; +import ClassNames from 'classnames' +import RcUpload from "rc-upload"; +import axios from 'axios' +import qs from 'qs'; +import '../utils/ajax' +import * as rasterizeHTML from 'rasterizehtml' +import previewStyle from '!!to-string-loader!css-loader!resolve-url-loader!sass-loader?sourceMap!./preview.scss' +import {ChromePicker} from 'react-color' +import Movable from './Movable' +import {getLuminance} from 'polished' +import { debounce } from 'throttle-debounce'; + +const RadioGroup = Radio.Group; + +const {data, appName, qrCodeForWeapp, qrCodeForH5, errorMsg, + dataOfDistribution, csrf, isDistributionPage} = window; + +const codeType = { + weapp: { + imageUrl: qrCodeForWeapp, + text: '小程序码' + }, + h5: { + imageUrl: qrCodeForH5, + text: '二维码' + } +}; + +function SizeChangeForm({defaultWidth, defaultHeight, onChange}) { + const [width, setWidth] = useState(defaultWidth); + const [height, setHeight] = useState(defaultHeight); + + const handleChange = debounce(300, onChange); + + return ( + + { + const newValue = e.target.value; + setWidth(newValue); + handleChange(newValue, height); + }} + /> + X + { + const newValue = e.target.value; + setHeight(newValue); + handleChange(width, newValue); + }} + /> + + ) +} + +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 ( + + {errorMsg ? ( + + + {errorMsg} + + ) : ( + + + +
+ + {mode === 'template' ? ( + <> +
+
+ +
+
+

{title}

+
{description}
+
+
+
+ +
+

{appName}

+
扫描或长按{codeType[type].text}
+
+ +
+ + ) : ( + <> + + + + + + )} +
+
+
+ +
+ {!isDistributionPage && ( + + + 默认模板 + 自定义 + + + )} + + + 小程序 + H5 + + + + + + {mode === 'template' && ( + <> + + + + + + + + )} + + + + + + +
更改图片
+
+
+
+ 海报背景宽高比例 1:{aspectRatio.toFixed(2)} +
+ {!isDistributionPage && ( + + + + + )} + trigger='click' + > + 0.5 ? '#333' : '#eee' + }} + >点击选择颜色 + + + )} + + {!isDistributionPage && ( + <> + + + + )} + + + +
+
+ )} +
+ ) + } +} + +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( + + + , + document.getElementById('spread') +); \ No newline at end of file diff --git a/backend/web/src/spread/normalize.scss b/backend/web/src/spread/normalize.scss new file mode 100644 index 0000000..00a9983 --- /dev/null +++ b/backend/web/src/spread/normalize.scss @@ -0,0 +1,354 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h4 { + font-size: 1.2em; + margin: 0.2em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} diff --git a/backend/web/src/spread/preview.scss b/backend/web/src/spread/preview.scss new file mode 100644 index 0000000..6a9a302 --- /dev/null +++ b/backend/web/src/spread/preview.scss @@ -0,0 +1,102 @@ +@import "normalize"; + +$poster-width: 300px; +$padding: 20px; +$image-size: $poster-width - $padding * 2; + +.preview { + overflow: hidden; + background: #fff; + &.mode-template { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + padding: 20px; + box-sizing: border-box; + .preview { + &__main { + padding: 20px; + background: #fff; + box-shadow: 0 0 30px rgba(0, 0, 0, .1); + } + &__image-wrapper { + display: flex; + justify-content: center; + align-items: center; + } + &__image { + width: 100%; + height: auto; + } + &__info { + width: 100%; + margin-top: 10px; + overflow: hidden; + } + &__footer { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 30px; + } + &__title { + + } + &__description { + width: 100%; + font-size: 14px; + line-height: 1.5; + color: #aaa; + overflow: hidden; + } + &__circle-image { + $circle-image-size: $poster-width * 0.2; + width: $circle-image-size; + height: $circle-image-size; + border-radius: 100%; + background: #fff; + } + &__footer-info { + border-radius: 100%; + margin: 0 10px; + } + &__app-name { + margin: 0; + margin-bottom: 5px; + } + &__tip { + font-size: 12px; + color: #aaa; + } + &__qr-code { + $size: $poster-width * 0.2; + width: $size; + height: $size; + } + } + } + &.mode-custom { + position: relative; + background-repeat: no-repeat; + background-size: contain; + .background { + position: absolute; + top: 0; + left: 0; + max-width: 100%; + max-height:100%; + &.distribution { + width: 100%; + height: 100%; + } + } + .preview { + &__qr-code { + width: 100%; + height: 100%; + } + } + } +} \ No newline at end of file diff --git a/backend/web/src/styles/amazing-creator.scss b/backend/web/src/styles/amazing-creator.scss new file mode 100644 index 0000000..65402ae --- /dev/null +++ b/backend/web/src/styles/amazing-creator.scss @@ -0,0 +1,83 @@ +.react-resizable-handle { + z-index: 200; +} + +#edit-home { + width: 100%; + margin: auto; + //background: #eee; + ul { + padding: 0; + margin-bottom: 20px; + } + .btn { + margin-right: 10px; + } +} + +.amazing-creator, .ac { + display: flex; + &__board { + flex-grow: 1; + } +} + +.sortable-item { + display: flex; + position: relative; + list-style: none; + background: #fff; + margin: 10px 0; + padding: 10px; + border-radius: 10px; + box-shadow: 0 0 30px 0 rgba(0, 0, 0, .1); + .drag-handle { + display: flex; + align-items: center; + padding: 10px; + user-select: none; + cursor: row-resize; + color: #362c57; + font-size: 1.5em; + } + .part { + flex-grow: 1; + flex-shrink: 1; + display: flex; + justify-content: space-between; + &__handle { + display: flex; + flex-direction: column; + justify-content: center; + margin-left: 10px; + > * { + margin: 10px 0; + } + } + &__content { + flex-shrink: 0; + width: 250px; + img { + width: 100%; + } + } + &__header { + display: flex; + justify-content: space-between; + align-items: center; + .title { + color: #e2314a; + font-weight: bold; + margin-left: 15px; + } + .more { + font-size: .8em; + color: #aaa; + cursor: pointer; + &:active { + color: #666; + } + } + } + } +} \ No newline at end of file diff --git a/backend/web/src/styles/content-header.scss b/backend/web/src/styles/content-header.scss new file mode 100644 index 0000000..332f11b --- /dev/null +++ b/backend/web/src/styles/content-header.scss @@ -0,0 +1,90 @@ +.content-header { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + padding: 0; + h1, .breadcrumb { + margin: 5px 0!important; + } + h1 { + color: $color-primary; + font-weight: bold; + padding-left: 10px; + border-left: 5px solid $color-primary; + } +} + +.content-header>.breadcrumb { + $height: 25px; + $size: 10px; + display: flex; + flex-wrap: wrap; + position: static; + float: none!important; + height: $height; + background: none; + overflow: hidden; + padding: 0; + li { + display: flex; + align-items: center; + position: relative; + margin-right: $size + 3px; + background: $color-primary; + &:hover { + $background: darken($color-primary, 10%); + background: $background; + &::before { + border-top: $height / 2 solid $background; + border-bottom: $height / 2 solid $background; + } + &::after { + border-left: $size solid $background; + } + } + &::before { + content: ''; + position: absolute; + top: 0; + left: -1 * $size; + width: 0; + height: 0; + border-top: $height / 2 solid $color-primary; + border-bottom: $height / 2 solid $color-primary; + border-left: $size solid transparent; + } + &::after { + content: ''; + position: absolute; + top: 0; + right: -1 * $size; + width: 0; + height: 0; + border-top: $height / 2 solid transparent; + border-bottom: $height / 2 solid transparent; + border-left: $size solid $color-primary; + } + &.active { + $background: $color-primary-dim; + box-shadow: none; + background: $background; + color: $color-primary; + padding: 0 $size / 2 + 5px; + &::before { + border-top: $height / 2 solid $background; + border-bottom: $height / 2 solid $background; + } + &::after { + border-left: $size solid $background; + } + } + a { + padding: 5px 10px; + color: $color-primary-dim!important; + } + } + >li+li::before { + content: ''; + } +} \ No newline at end of file diff --git a/backend/web/src/styles/dashboard.scss b/backend/web/src/styles/dashboard.scss new file mode 100644 index 0000000..bcc6722 --- /dev/null +++ b/backend/web/src/styles/dashboard.scss @@ -0,0 +1,180 @@ +.content-header h1 { + margin: 0; +} + +.box h1 { + font-size: 1.5em; + margin-top: 0; + margin-bottom: 20px; +} + +.row { + display: flex; + flex-wrap: wrap; + margin: 0; + .row { + margin: 0; + } + > div { + margin: 10px; + } +} + +.col { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.total { + width: 100%; + display: flex; + flex-wrap: wrap; +} + +#trend-chart { + width: 80%; +} + +#total-pie { + width: 20%; +} + +.box { + margin: 20px; +} + +#stock-tip { + width: 350px; +} + +.overview-card, .line-chart, .data-table { + //@extend %shadow-soft; + box-shadow: 3px 6px 40px -6px rgba(125, 138, 179, 0.3); +} + +.overview-card { + flex-grow: 1; + display: flex; + justify-content: space-around; + align-items: center; + width: 200px; + padding: 20px 15px; + margin: 10px; + background: $color-panel; + border-radius: 10px; + img { + $size: 50px; + width: $size; + height: $size; + } + h1 { + font-size: 1em; + } + p.number { + font-size: 1.6em; + font-weight: bold; + color: #ef9131; + margin: 0; + } +} + +.total-display { + width: 100%; + margin: 0!important; + display: flex; + justify-content: space-around; + flex-wrap: wrap; +} + +.line-chart { + flex-grow: 1; + flex-shrink: 1; + max-width: 100%; + padding: 10px 20px!important; +} + +.line-chart, .data-table { + display: flex; + flex-direction: column; + padding: 20px; + background: $color-panel; + border-radius: 10px; + h1 { + font-size: 1.5em; + font-weight: bold; + } + .content { + display: flex; + flex-direction: column; + justify-content: center; + flex-grow: 1; + margin: 0; + padding: 0; + } +} + +.data-table { + table { + thead { + tr { + font-weight: bold; + td { + white-space: nowrap; + } + } + } + td { + padding: 8px; + font-size: .9em; + //white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + &.line-number { + text-align: center; + } + &.highlight { + color: #ef9131; + } + } + } + .table-empty-text { + text-align: center; + font-size: 1.4em; + } +} + +.fill-space { + flex-grow: 1; +} + +@media (min-width: 1235px) { + .total-display { + flex-wrap: nowrap; + } + .row { + flex-wrap: nowrap; + width: 100%; + .line-chart { + //width: 60%; + flex: 1; + } + .data-table { + max-width: 400px; + flex-grow: 0; + flex-shrink: 0; + } + } +} + +@media (max-width: 1235px) { + .row { + flex-wrap: wrap; + .line-chart { + flex-grow: 1; + width: 98%; + padding-right: 2%; + } + } +} \ No newline at end of file diff --git a/backend/web/src/styles/global.scss b/backend/web/src/styles/global.scss new file mode 100644 index 0000000..f0db4c2 --- /dev/null +++ b/backend/web/src/styles/global.scss @@ -0,0 +1,183 @@ +html, body { + color: $color-fore; + background: $color-background!important; + font-family: -apple-system, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Arial, sans-serif; +} + +body .fa { + font-family: "fa", "FontAwesome" !important; +} + +.content-wrapper { + margin-top: 70px; + margin-bottom: 10px; +} +.content-wrapper { + background: rgb(248, 250, 255)!important; +} + +input, select { + border-radius: 3px!important; +} + +button.btn-float, a.btn-float { + position: fixed; + display: block; + top: unset; + right: 50px; + bottom: 50px; + width: 60px; + height: 60px; + line-height: 60px; + padding: 0; + border-radius: 50%; + font-size: 1.6em; + box-shadow: 0 0 20px 5px rgba(0, 0, 0, .2); + z-index: 10000; +} + +.btn-info.btn-float { + bottom: 120px; +} + +fieldset { + transition: all .4s; +} +fieldset[disabled] { + opacity: 0; + cursor: auto!important; +} + +#driver-popover-item, #driver-highlighted-element-stage { + position: fixed!important; +} + +//.main-footer { +// position: absolute; +// bottom: 0; +// width: 100%; +//} + +::-webkit-scrollbar{width:6px!important;height:6px!important;} +body::-webkit-scrollbar{width:6px!important;height:6px!important;} + +::-webkit-scrollbar-track{background:rgba(200,200,200,0.22)!important;border-radius:8px!important;} + + +::-webkit-scrollbar-thumb{background-color: rgba(0, 0, 0, .1) !important;min-height:50px;border-radius:5px!important;} + + +@keyframes fadein { + 0% {opacity: 0;} + 100% {} +} + +.content-wrapper { + $margin: 20px; + margin-left: $sidebar-width + $margin!important; + margin-right: $margin; +} + +.main-footer { + margin-left: $sidebar-width!important; + a { + color: $color-primary-vivid; + } +} + +.tab-content { + height: auto!important; + border: none!important; + padding: 0!important; + margin-top: 30px; +} + +.login-logo { + font-weight: bold; +} + +.category-list { + .category-list-item { + &.category__hidden { + background: #f8f8f8; + color: #aaa; + } + > div { + display: flex; + align-items: center; + border: none!important; + } + .hidden-tip { + margin-left: 20px; + color: #aaa; + } + } + a.btn { + padding: 10px !important; + } + .btn-group { + a.btn { + border-radius: 0; + &:first-of-type { + border-radius: 5px 0 0 5px; + } + &:last-child { + border-radius: 0 5px 5px 0; + } + } + &.btn-group__circle { + a.btn { + padding: 8px!important; + margin: 0 5px; + font-size: .8em; + border-radius: 100%!important; + } + } + } + a.collapse-list-btn { + color: $color-primary; + i { + transition: transform .3s; + } + &[aria-expanded="true"] i { + transform: rotate(180deg); + } + &.disabled { + color: $color-dim; + cursor: default; + } + } + .drag-btn { + margin-left: 20px; + color: $color-info; + } +} + +.category-index { + min-height: 75vh; +} + +.goods-update .props-main-box { + position: relative!important; + top: 0; + left: 0; + margin-top: 20px; +} + +// 侧边栏折叠状态 +.sidebar-mini.sidebar-collapse .content-wrapper, +.sidebar-mini.sidebar-collapse .right-side, +.sidebar-mini.sidebar-collapse .main-footer { + margin-left: 70px!important; +} + +@media (max-width: 767px) { + body.sidebar-mini.sidebar-open { + .content-wrapper { + margin-left: 0!important; + } + } + .main-footer { + margin-left: 0!important; + } +} \ No newline at end of file diff --git a/backend/web/src/styles/header.scss b/backend/web/src/styles/header.scss new file mode 100644 index 0000000..7059b4b --- /dev/null +++ b/backend/web/src/styles/header.scss @@ -0,0 +1,128 @@ + +//顶栏背景色 +.logo, .navbar-static-top { + background: none!important; +} + +.main-header { + position: fixed; + width: auto; + left: $sidebar-width; + right: 0; + display: flex; + //background-image: linear-gradient(45deg, $color-primary, adjust_hue($color-primary, 40)); + background: $color-panel; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1)!important; + transition: all .3s ease-in-out; + z-index: 100 !important; + .logo-lg, .logo, .sidebar-toggle, .hidden-xs, .notifications-menu a.dropdown-toggle { + color: $color-thin !important; + } + .user-image { + background: lighten($color-primary, $lightenDegree); + } + .logo { + flex-grow: 1; + font-weight: bold; + } + .navbar-static-top { + margin-left: 0; + } +} + +.user-header { + background: $color-primary!important; +} + +.user-menu { + .dropdown-menu { + left: unset; + right: 0; + box-shadow: 0 4px 60px 10px rgba(0, 0, 0, 0.1)!important; + overflow: visible; + } +} + +.sidebar-toggle { + &:active, &:hover { + background: $color-pale!important; + } +} +.notifications-menu { + .label-warning { + display: block; + padding: 3px 5px!important; + font-size: .8em!important; + background: #ff4e3d !important; + border-radius: 100%; + } + .dropdown-menu { + width: auto!important; + box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1)!important; + ul.menu { + max-height: 40vh!important; + li { + a { + display: flex!important; + align-items: center; + padding: 10px 20px!important; + span.number { + color: $color-danger; + margin: 0 10px; + font-weight: bold; + } + span.tip { + color: $color-danger; + margin-left: 10px; + font-weight: bold; + } + } + i { + width: auto!important; + font-size: 1.6em; + margin-right: 20px; + padding: 10px; + border-radius: 100%; + text-align: center; + &.blue { + $color: #5964ab; + background: $color; + color: lighten($color, 50%)!important; + } + &.yellow { + $color: #df8346; + background: $color; + color: lighten($color, 50%)!important; + } + &.red { + background: $color-danger; + color: lighten($color-danger, 50%)!important; + } + } + } + } + } +} + +body.sidebar-collapse { + .main-header { + left: 50px; + } +} + +@media (max-width: 767px) { + .main-header { + left: 0!important; + } + .logo { + padding: 0!important; + } + .main-header .logo, .main-header .navbar { + width: auto; + } + body.sidebar-mini.sidebar-open { + .main-header { + margin-left: 190px; + } + } +} \ No newline at end of file diff --git a/backend/web/src/styles/index.scss b/backend/web/src/styles/index.scss new file mode 100644 index 0000000..d705bb4 --- /dev/null +++ b/backend/web/src/styles/index.scss @@ -0,0 +1,7 @@ +@import "./global"; +@import "sidebar"; +@import "widget"; +@import "header"; +@import "content-header"; +@import "dashboard"; +@import "amazing-creator"; \ No newline at end of file diff --git a/backend/web/src/styles/login.scss b/backend/web/src/styles/login.scss new file mode 100644 index 0000000..19a0439 --- /dev/null +++ b/backend/web/src/styles/login.scss @@ -0,0 +1,193 @@ + +html { + background: #292d34; + background-image: url("/img/login_background.png") !important; + background-size: cover !important; + font-family: -apple-system, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Arial, sans-serif; +} + +body { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + //background: rgba(0, 0, 0, .7)!important; + background: none !important; +} + +.login-box { + $color-primary: #2b457c; + $background: #e9f4ff; + + &__body { + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + display: flex; + height: 80vh; + box-shadow: 0 4px 30px 10px rgba(0, 0, 0, 0.2)!important; + //border-radius: 10px; + overflow: hidden; + } + &__bar { + position: absolute; + top: 40px; + width: 100%; + padding-right: 10px; + text-align: right; + border-left: 6px solid $background; + border-right: 6px solid $color-primary; + color: $color-primary; + font-weight: bold; + font-size: 1.5em; + } + &__left, &__right { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 20px; + } + + &__left { + padding: 0 60px; + background: #1a1d25; + } + + &__logo { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-around; + + img { + $size: 150px; + width: $size; + height: $size; + margin-bottom: 40px; + } + + p { + font-size: 1.5em; + font-weight: bold; + color: #2ad0ff; + } + } + + &__right { + flex-grow: 1; + background: $background; + + .form-group.has-feedback { + position: relative; + + .form-control { + padding-left: 40px; + border: none; + border-bottom: 2px solid $color-primary; + background: none !important; + + &:-webkit-autofill { + -webkit-box-shadow: 0 0 0 1000px $background inset; + -webkit-text-fill-color: $color-primary; + } + } + + .form-control-feedback { + position: absolute; + left: 0; + color: $color-primary; + } + } + + .submit-area { + display: flex; + flex-direction: column; + align-content: center; + justify-content: space-around; + text-align: center; + .form-group.field-adminloginform-rememberme { + color: $color-primary; + + label { + font-weight: bold!important; + } + } + .btn.btn-primary.btn-block.btn-flat { + display: inline-block; + padding: 10px 20px; + border-radius: 20px; + background: $color-primary; + box-shadow: 0 5px 10px 0 rgba(0, 0, 0, .2); + } + } + } +} + +@media (min-width: 1200px) { + .login-box__body { + width: 60vw; + } +} + +@media (max-width: 1200px) and (min-width: 800px) { + .login-box__body { + width: 700px; + } +} + +@media (max-width: 800px) and (min-width: 400px) { + .login-box__body { + width: 87.5%; + .login-box__left { + padding: 0 30px; + } + } +} + +@media (max-width: 600px) { + .login-box { + &__body { + flex-direction: column; + width: 100%; + height: 100%; + } + &__left { + padding: 20px 0; + background: #1a1d25; + } + &__logo { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-around; + + img { + $size: 50px; + width: $size; + height: $size; + margin-bottom: 0; + margin-right: 20px; + } + + p { + margin-bottom: 0; + font-size: 1.5em; + font-weight: bold; + color: #6fd1ff; + } + } + &__bar { + top: 110px; + width: auto; + left: 50%; + padding: 0 10px 2px 10px; + transform: translateX(-50%); + border: none; + border-bottom: 4px solid $color-primary; + text-align: center; + } + } +} \ No newline at end of file diff --git a/backend/web/src/styles/sidebar.scss b/backend/web/src/styles/sidebar.scss new file mode 100644 index 0000000..dad50c1 --- /dev/null +++ b/backend/web/src/styles/sidebar.scss @@ -0,0 +1,201 @@ +.main-sidebar { + position: fixed; + width: $sidebar-width; + height: 100%; + padding-top: 20px!important; + margin-bottom: 100px; + overflow-y: auto; + background: $color-sidebar!important; + //background-image: linear-gradient(135deg, #3a345d, #2d2a46); + .sidebar__header { + display: flex; + flex-direction: column; + align-items: center; + img { + $size: 60px; + width: $size; + height: $size; + border-radius: 50%; + } + p { + color: $color-sidebar-logo; + font-style: italic; + margin-top: 15px; + font-size: 1.2em; + font-weight: bold; + } + } + .user-panel .info { + a { + color: lighten($color-thin, 20%); + } + i { + font-size: 10px; + } + } + .sidebar-form { + border: none !important; + .input-group { + border-radius: 40px; + overflow: hidden; + input { + border: none !important; + } + } + input.form-control[type="text"], #search-btn { + background: lighten($color-primary-pale, 5%)!important; + color: $color-primary-dim !important; + } + } + ul.sidebar-menu { + margin-top: 10px; + > li.treeview { + // 一级菜单 + list-style: none; + margin-left: 15px; + a { + background: $color-sidebar!important; + color: $color-sidebar-fore; + &:hover { + color: $color-sidebar-active-fore!important; + } + } + > a { + padding: 10px 15px!important; + font-weight: normal; + font-size: 1em; + .fa { + margin-right: 5px; + font-size: 1.2em; + } + } + ul.treeview-menu { + padding-left: 0; + padding-bottom: 10px; + background: $color-sidebar!important; + li { + // 二级菜单 + padding: 5px 0; + //border-left: 4px solid transparent; + background: $color-sidebar!important; + &:hover { + background: $color-pale; + } + a { + padding: 0; + padding-left: 48px; + font-size: .9em; + background: $color-sidebar!important; + } + // 隐藏二级菜单前面的 O + .fa-circle-o { + display: none; + } + } + } + &.active { + // 当前页面所在的一级菜单 + background: $color-sidebar-active!important; + border-radius: 20px 0 0 20px; + overflow: hidden; + > a { + //color: $color-primary-vivid !important; + background: $color-sidebar-active!important; + color: $color-sidebar-active-fore!important; + } + .treeview-menu { + background: $color-sidebar-active!important; + li { + background: $color-sidebar-active!important; + a { + background: $color-sidebar-active!important; + } + &.active { + // 选中的二级菜单 + //border-left-color: $color-primary; + background: $color-sidebar-active!important; + a { + color: $color-sidebar-active-fore !important; + font-weight: normal; + } + } + } + } + } + &.menu-open { + // 展开的一级菜单 + //color: $color-primary !important; + } + } + } +} + +body.sidebar-collapse { + .main-sidebar { + padding-top: 10px!important; + overflow: visible; + } + .sidebar__header { + display: flex; + flex-direction: column; + align-items: center; + img { + $size: 25px; + width: $size; + height: $size; + border-radius: 50%; + transition: all .3s ease-in-out; + } + p { + display: none; + font-size: 12px; + margin-top: 5px; + margin-bottom: 0; + transition: all .3s ease-in-out; + } + } + ul.sidebar-menu { + > li.treeview { + margin-left: 0; + overflow: visible!important; + a { + border-radius: 50%; + } + //border-radius: 50%!important; + //a { + // padding: 10px!important; + // i { + // margin: 0!important; + // } + //} + a span { + width: 178px!important; + } + .pull-right-container { + margin-top: -1px!important; + padding: 8px 3px!important; + } + .treeview-menu { + left: 48px!important; + top: unset!important; + border: none!important; + } + } + } +} + +.main-sidebar, .main-footer { + box-shadow: 0 4px 30px 0 rgba(223, 225, 230, 0.5) !important; + border: none !important; +} + +.panel { + padding: 10px; + margin: 0; +} + +@media (max-width: 767px) { + .content-wrapper { + margin-left: 30px!important; + } +} \ No newline at end of file diff --git a/backend/web/src/styles/variables.scss b/backend/web/src/styles/variables.scss new file mode 100644 index 0000000..b5ff942 --- /dev/null +++ b/backend/web/src/styles/variables.scss @@ -0,0 +1,30 @@ +$color-primary: #2f4887; +$color-primary-vivid: hsl(hue($color-primary), saturation($color-primary), 50%); +$color-primary-dim: lighten($color-primary-vivid, 35%); +$color-primary-pale: lighten($color-primary-vivid, 42%); +$color-background: rgb(248, 250, 255); +$color-panel: #fff; +$color-pale: #f6f6f6; +$color-fore: #666; +$color-dim: #b4b6c5; +$color-thin: #515156; +$color-sidebar: #080e39; +$color-sidebar-active: #2a2e52; +$color-sidebar-fore: #dce0f2; +$color-sidebar-active-fore: #26caff; +$color-sidebar-logo: #26caff; + +$color-success: #2ab57d; +$color-danger: #c60013; +$color-info: #2a92ec; + +$lightenDegree: 70%; +$sidebar-width: 190px; + +%shadow { + box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.1)!important; +} + +%shadow-soft { + box-shadow: 0 4px 30px 0 rgba(223, 225, 230, 0.5) !important; +} \ No newline at end of file diff --git a/backend/web/src/styles/widget.scss b/backend/web/src/styles/widget.scss new file mode 100644 index 0000000..b90af99 --- /dev/null +++ b/backend/web/src/styles/widget.scss @@ -0,0 +1,301 @@ +.content-wrapper { + margin-left: 30px; + margin-right: 30px; + > section.content { + margin: 30px 0; + background: $color-panel!important; + box-shadow: 0 4px 30px 2px rgba(223, 225, 230, 0.5)!important; + border-radius: 10px; + } + } + +.error-page .fa-warning { + font-size: 1.5em; +} + +#treeView { + background: none; +} + +body a:focus { + text-decoration: none; +} + +.box, .panel { + padding: 0!important; + margin: 0!important; + border: none!important; + border-image-width: 0!important; + box-shadow: none!important; + //box-shadow: 0 4px 30px 2px rgba(223, 225, 230, 0.5)!important; + overflow: hidden; + .panel { + box-shadow: none!important; + } +} + +.grid-view { + margin-top: 20px; +} + +.box.box-default { + margin: 0; +} + +.order-index { + .panel-default { + padding: 0; + padding-top: 20px; + margin: 0; + } +} + +.panel { + margin: 20px 0; + .panel-heading { + padding: 0; + } +} + +.btn { + border: none; + $btn-list: (name: default, color: $color-pale), + (name: primary, color: $color-primary), + (name: success, color: $color-success), + (name: info, color: $color-info), + (name: danger, color: $color-danger); + + @each $btn in $btn-list { + &.btn-#{map_get($btn, name)} { + $color: map_get($btn, color); + background: $color; + @if (lightness($color) < 70%) { + color: lighten($color, $lightenDegree)!important; + } @else { + color: darken($color, $lightenDegree)!important; + } + &:hover { + background: darken(map_get($btn, color), 5%); + } + &:active { + background: darken(map_get($btn, color), 10%); + } + } + } +} + +.dropdown-toggle { + box-shadow: none!important; +} +.select-data-box { + position: static!important; +} + +.panel-heading, .panel-footer { + background: $color-panel!important; +} + +.panel-heading { + border: none; +} + +.nav-tabs { + display: flex; + border-bottom-color: $color-pale; + li { + float: none; + border-bottom: 3px solid transparent; + a { + color: $color-fore; + } + a, a:hover { + border: none!important; + } + i { + color: $color-primary-vivid!important; + } + &.active { + border-bottom-color: $color-primary; + box-shadow: none; + a { + margin-right: 0; + color: $color-primary; + font-weight: bold; + } + } + } +} + +.kv-panel-before, .kv-panel-after, .panel-footer { + border: none; +} + +table.kv-grid-table { + &, thead, th, td { + border: none!important; + } + thead { + background: lighten($color-primary-pale, 5%); + a { + color: $color-primary; + } + input.form-control[type][name], select { + background: lighten($color-primary-pale, 10%)!important; + } + } + >tbody { + >tr:nth-of-type(2n) { + background: lighten($color-primary-pale, 6%); + &:hover { + background: lighten($color-primary-pale, 6%)!important; + } + } + >tr:hover { + background: $color-panel!important; + } + } + td { + vertical-align: middle!important; + } +} + +input:not([type="submit"]):not([type="button"]):not([type="reset"]):not([class^="ant-input"]), select { + width: 100%; + padding: 6px 12px; + background: lighten($color-primary-pale, 3%) !important; + //background: $color-pale!important; + //border: 1.5px solid lighten($color-primary-dim, 10%)!important; + border: none!important; + border-radius: 5px!important; + color: darken($color-primary, 10%); + box-shadow: none!important; +} + +input.file-caption-name { + background: none!important; +} + +.input-group-addon { + background: lighten($color-primary-pale, 5%)!important; + border: none!important; + &:hover { + background: $color-primary-pale!important; + } +} + +.select2-selection.select2-selection--multiple, +.select2-dropdown.select2-dropdown--below { + border: none!important; +} + +::-webkit-input-placeholder { + color: lighten($color-primary, 50%)!important; + //color: $color-dim!important; +} + +// 复选框 +.cbx-active { + border: 2px solid $color-primary-dim!important; + box-shadow: none!important; + border-radius: 0; + .cbx-icon { + background: lighten($color-primary-pale, 5%); + i { + color: $color-primary-vivid!important; + } + } +} + +// Switch +label.btn.btn-default { + box-shadow: none!important; + &.active { + background: darken($color-primary-vivid, 10%); + color: $color-primary-pale; + } +} + +.props-main-box, .props-detail-box { + position: static!important; + width: 100%!important; +} + +.tab-content { + min-height: 350px; +} + +.goods-index { + td:last-child { + font-size: 0; + .btn { + padding: 10px; + border-radius: 0; + } + } +} + +ul.pagination { + li { + a, span { + border: none!important; + margin: 0 5px; + border-radius: 100%!important; + } + &.active { + a { + background: $color-primary-vivid; + //box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2)!important; + &:hover { + background: darken($color-primary-vivid, 10%); + } + } + } + } +} + +.after-sale-index .panel, .panel-default { + position: relative; + min-height: 75vh; +} + +$width: 95px; + +.rc-switch { + width: $width!important; + margin-bottom: 10px; + &[aria-checked="false"] { + background: $color-info; + } + &[aria-checked="true"] { + background: $color-success; + } +} + +.rc-switch-checked:after { + left: $width - 22px!important; +} + +.empty { + position: absolute; + display: block; + width: 100%; + left: 50%; + top: 60%; + transform: translate(-50%, -50%); + text-align: center; + font-size: 2em; +} + +.kv-page-summary-container td { + padding: 0!important; +} + +table.table.table-striped.table-bordered.detail-view { + &, tbody, tr, th, td { + border: none!important; + } + th { + padding-right: 50px; + white-space: nowrap; + } +} \ No newline at end of file diff --git a/backend/web/src/utils/ajax.js b/backend/web/src/utils/ajax.js new file mode 100644 index 0000000..cf62be7 --- /dev/null +++ b/backend/web/src/utils/ajax.js @@ -0,0 +1,76 @@ +import axios from 'axios' +import {message} from 'antd'; + +// 请求拦截器 +axios.interceptors.request.use(config => ({ + ...config, + data: { + ...config.data, + '_csrf-api': window.csrfToken + } +})); + +// 响应拦截器 +axios.interceptors.response.use(function (response) { + const rawData = response && response.data; + let data = null; + const errorText = '服务器返回的数据格式不正确'; + + if (rawData && typeof response.data === 'object') { + // 若返回的数据是对象或者数组 + data = rawData; + } else if (typeof response.data === 'string') { + // 若返回的数据是字符串,尝试将其按 JSON 格式转为对象 + try { + data = JSON.parse(rawData); + } catch (e) { + message.error(errorText); + return Promise.reject(errorText); + } + } else { + message.error(errorText); + return Promise.reject(errorText); + } + + if (data) { + const {status, info} = response.data; + + if (status === undefined || status === true || status === 1) { + return response.data; + } else if (info) { + const errorText = info; + message.error(errorText); + console.error(errorText); + return Promise.reject(errorText) + } else { + const errorText = `操作失败`; + message.error(errorText); + console.error(errorText); + return Promise.reject(errorText) + } + } else if (response) { + return response; + } else { + return Promise.reject(`服务器无响应`); + } +}, function (error) { + let errorText = ''; + + if (error && error.response && error.response.status === 500) { + if (error.response.data) { + errorText = error.response.data.message || error.response.data; + } else { + errorText = error.response.message + } + } else if (error.response && error.response.status) { + errorText = `请求出错:${error.response.statusText}(${error.response.status})` + } else if (error.message) { + errorText = error.message + } else { + errorText = error + } + + errorText && message.error(errorText); + error && console.error(error); + return Promise.reject(errorText); +}); \ No newline at end of file diff --git a/backend/web/webpack.config.js b/backend/web/webpack.config.js new file mode 100644 index 0000000..8538926 --- /dev/null +++ b/backend/web/webpack.config.js @@ -0,0 +1,84 @@ +const path = require('path'); +const webpack = require('webpack'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const CleanWebpackPlugin = require('clean-webpack-plugin'); + +const entryList = { + dashboard: './src/dashboard/index.js', + style: './src/styles/index.scss', + login_style: './src/styles/login.scss', + order_detail: './src/detail-display/index.js', + sku: './src/sku/index.js', + mini_program_management: './src/mini-program-management/index.js', + spread: './src/spread/index.js', + sku_item: './src/sku-item/index.js', + 'custom-menu': './src/custom-menu/index.js', + sku_for_activity: './src/sku-for-activity/index.js', +}; + +const needIconfontEntries = ['style', 'login_style']; + +module.exports = function (env, argv) { + const pathToClean = argv.module ? `${argv.module}.*.js` : 'custom'; + const entry = argv.module ? {[argv.module]: entryList[argv.module]} : entryList; + const htmlWebpackPluginList = Object.keys(entry).map(entryItem => new HtmlWebpackPlugin({ + filename: `${entryItem}.html`, + chunks: [entryItem], + template: needIconfontEntries.includes(entryItem) ? 'src/iconfont.html' : 'src/import.html' + })); + + return { + entry, + output: { + filename: '[name].[chunkhash].js', + path: path.resolve(__dirname, 'custom'), + publicPath: '/custom/' + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /(node_modules|bower_components)/, + use: { + loader: 'babel-loader' + } + }, + { + test: /\.scss$/, + use: ["style-loader", "css-loader", "resolve-url-loader", "sass-loader?sourceMap", + { + loader: 'sass-resources-loader', + options: { + resources: './src/styles/variables.scss' + } + }] + }, + { + test: /\.css$/, + use: ["style-loader", "css-loader"] + }, + { + test: /\.(png|jpg|gif|eot|svg|ttf|woff2?)(\?.*)?$/i, + use: [ + { + loader: 'url-loader', + options: { + limit: 10000 + } + }, + 'file-loader' + ] + } + ] + }, + plugins: [ + new CleanWebpackPlugin({ + cleanOnceBeforeBuildPatterns: [pathToClean] + }), + new webpack.DefinePlugin({ + ENV_DEV: JSON.stringify(argv.mode === 'development') + }), + ...htmlWebpackPluginList, + ] + }; +}; \ No newline at end of file