diff --git a/README.md b/README.md index e54308ef2bcf27d55825c834da5da147933c3b1a..0327e717c3ea54b968d20bbd0c2153a45b130982 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # admin-react-ec #### 项目介绍 -react 电商系统的搭建 +react 电商系统的搭建 #### 软件架构 @@ -16,9 +16,31 @@ react 电商系统的搭建 #### 使用说明 -1. xxxx -2. xxxx -3. xxxx +1. 整个网站的所有的东西 样式啊图片的都是可以根据配置来显示的 + 比如 一个网站title旁的小图标,favicon + 之前 接触的多是cms 系统的网站,用iis搭建的 title旁的小图标 + 只要改名为指定的图片名 就可以自动设置了, + 但是在react里要在webpack.config 里配置一下。 + # new HtmlWebpackPlugin({ + # template: './src/index.html' ,// 新建了一个模板 和模板的位置 + # favicon : './favicon.ico' //处理 网站title旁边的图片的 + # }), +2.接口请求问题 + 接口请求 用的是 promise 来处理的 + 但是提交的数据 有跨域问题 用的是webpack自带的devServer里的功能 + #proxy : { + # '/manage' :{ //这是接口所在的文件夹 url: '/manage/user/login.do', + # target: '跨域的链接', + # changeOrigin : true + # } + # } + + 这块对promise的使用 要求对异步回调函数的结构要非常的清楚 才能在层层嵌套里返回出自己想要的数据 + + +3.翻页模块的引用 + + #### 参与贡献 diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a8d3317951f2143b09f8782ddec1a1117a4295d5 Binary files /dev/null and b/favicon.ico differ diff --git a/package.json b/package.json index 54761cad80fcf4447fadc7f844e77664031780cd..8660b8f62ecfe99edc6124b46d3fa1206edc07a8 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "repository": "git@gitee.com:React_code/admin-react-ec.git", "author": "tom <820027697@qq.com>", "license": "MIT", - // 命令行里的一些快捷键的设置 "scripts": { - "dev" : "node_modules/.bin/webpack-dev-server", //运行服务启动项目 - "dist" : "node_modules/.bin/webpack -p" //线上环境的打包 + "dev": "node_modules/.bin/webpack-dev-server", + "dist": "node_modules/.bin/webpack -p", + "dist_win": "set WEBPACK_ENV=online&& node_modules/.bin/webpack -p" }, "devDependencies": { "babel-core": "6.26.0", @@ -26,9 +26,12 @@ "webpack-dev-server": "2.9.7" }, "dependencies": { - "font-awesome": "^4.7.0", "node-sass": "^4.9.2", + "prop-types": "15.6.0", + "rc-pagination": "^1.16.5", "react": "16.2.0", - "react-dom": "16.2.0" + "react-dom": "16.2.0", + "react-router-dom": "4.2.2", + "simditor": "^2.3.6" } } diff --git a/src/app.js b/src/app.js index cbb0b1680b0f08d2014fa81cd393f5b5a6e0467b..1ea8adf278e8acf81808752632e29fe1edc2d025 100644 --- a/src/app.js +++ b/src/app.js @@ -1,15 +1,59 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { BrowserRouter as Router, Route, Redirect, Switch, Link } + from "react-router-dom"; -import 'font-awesome/css/font-awesome.min.css'; -import './index.css'; -import './index.scss'; +// 这是页面 做集成用的 +import Layout from 'component/layout/index.js'; +import Home from 'page/Home/index.js'; +import ProductRouter from 'page/product/router.js'; + + +import Login from 'page/login/index.js'; +import UserList from 'page/user/index.js'; + +import ErrorPage from 'page/error/index.js'; + +class App extends React.Component{ + render(){ + let LayoutRouter=( + + + + + + + + {/* 实际链接为 user 跳转链接为 user/index */} + + + + + + + ); + + return( +
+ + + + {/* 这一块 原版是直接上首页 展示 但是添加了个登陆页 所以路由模块要有所改变 */} + {/* 这里了解了一下 route 的render 属性 这也算是在子路由里添加链接的方式了 */} + {/* 回头看 项目 的话 多多了解一下 路由的问题 */} + + LayoutRouter} /> + + + + +
+ ); + } +} ReactDOM.render( -
- -

Hello world

-
, -document.getElementById("app") -); \ No newline at end of file + , + document.getElementById("app") +); \ No newline at end of file diff --git a/src/component/layout/index.js b/src/component/layout/index.js new file mode 100644 index 0000000000000000000000000000000000000000..47935da62616a2242ab974b64b779bfcbf5966d7 --- /dev/null +++ b/src/component/layout/index.js @@ -0,0 +1,26 @@ +import React from 'react'; + + +import SideNav from 'component/sidenav/index.js'; +import TopNav from 'component/topnav/index.js'; +import './theme.css'; +import './index.scss'; + + + +class Layout extends React.Component{ + constructor(props){ + super(props); + } + render(){ + return( +
+ + + {this.props.children} +
+ ) + } +} + +export default Layout; \ No newline at end of file diff --git a/src/component/layout/index.scss b/src/component/layout/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..b1a3e6918ce9cc42d12abb4fe025e0e2c068e201 --- /dev/null +++ b/src/component/layout/index.scss @@ -0,0 +1,5 @@ +.page-header-right{ + position: absolute; + right:15px; + top:12px; +} \ No newline at end of file diff --git a/src/component/layout/theme.css b/src/component/layout/theme.css new file mode 100644 index 0000000000000000000000000000000000000000..2b06118ee3e368daa644374065d263f06e24fae6 --- /dev/null +++ b/src/component/layout/theme.css @@ -0,0 +1,622 @@ +/*---------------------------------------------- +Author : www.webthemez.com +License: Commons Attribution 3.0 +http://creativecommons.org/licenses/by/3.0/ +------------------------------------------------*/ + + +/*---------------------------------------------- + COMMON STYLES +------------------------------------------------*/ +body { + font-family: 'Open Sans', sans-serif; + background: #f3f3f3; +} + +#wrapper { + width: 100%; +} + +#page-wrapper { + margin: 0 0 0 260px; + padding: 15px 30px; +} + +.text-center { + text-align: center; +} + + +h1, .h1, h2, .h2, h3, .h3 { + margin-top: 7px; + margin-bottom: -5px; +} + +h2 { + color: #000; +} + +h4 { + padding-top: 10px; +} + +.square-btn-adjust { + border: 0px solid transparent; + -webkit-border-radius: 0px; + -moz-border-radius: 0px; + border-radius: 0px; +} + +p { + font-size: 16px; + line-height: 25px; +} + +.panel { + border-radius: 0px; +} + +.navbar-side .nav > li > a > i { + color: #B5B5B5; + padding: 8px; + width: 30px; + text-align: center; +} + +.top-navbar { + position: fixed; + width: 100%; + z-index: 300; + -webkit-box-shadow: 0 3px 3px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 3px 3px rgba(0, 0, 0, 0.05); + box-shadow: 0 3px 3px rgba(0, 0, 0, 0.05); +} + +.navbar-side { + z-index: 1; + position: fixed; + width: 260px; + top: 60px; + bottom: 0; + overflow-y : auto; + background: #2b2e33; +} + +#page-wrapper { + position: relative; + top: 55px; +} + +.top-navbar .nav > li > a:hover, .top-navbar .nav > li > a:focus { + text-decoration: none; + background-color: #2497BA; + color: #fff; +} + +.nav .open > a, .nav .open > a:hover, .nav .open > a:focus { + background-color: #2497BA; + border-color: #428bca; +} + +.breadcrumb { + padding: 0; + margin-bottom: 20px; + list-style: none; +/* background-color: #FAFAFA; */ + border-radius: 0; +} +/*---------------------------------------------- + DASHBOARD STYLES +------------------------------------------------*/ +.page-header { + padding-bottom: 9px; + margin: 10px 0 20px; + border-bottom: 1px solid transparent; +} + +.panel-left { + width: 35%; + height: 158px; + background: #5CB85C; +} + +.panel-left .fa-5x { + font-size: 11em; + color: rgba(255, 255, 255, 0.15); + padding: 40px 0; + margin-bottom: 30px; +} + +.panel-right { + width: 65%; + height: 158px; + background: transparent; + margin-bottom: 0; + color: #fff; +} + +.panel-right h3 { + font-size: 50px; + padding: 31px 10px 13px; + color: rgba(255, 255, 255, 0.96); +} + +.panel-back { + background-color: #fff; +} + +.panel-default { + border-color: #ECECEC; +} + +.panel-default > .panel-heading { + color: #000; + border-color: #FFF; + font-weight: bold; + background: #FFFFFF; + font-size: 16px; +} + +.panel-heading { + padding: 15px 15px 0px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} + +.jumbotron, .well { + background: #fff; +} + +.noti-box { + min-height: 100px; + padding: 20px; +} + +.noti-box .icon-box { + display: block; + float: left; + margin: 0 15px 10px 0; + width: 70px; + height: 70px; + line-height: 75px; + vertical-align: middle; + text-align: center; + font-size: 40px; +} + +.text-box p { + margin: 0 0 3px; +} + +.main-text { + font-size: 25px; + font-weight: 600; +} + +.set-icon { + -webkit-border-radius: 50px; + -moz-border-radius: 50px; + border-radius: 50px; +} + +.panel-primary { + display: inline-block; + margin-bottom: 30px; + width: 100%; +} + +.green { + background-color: #5cb85c; + color: #fff; +} + +.blue { + background-color: #4CB1CF; + color: #fff +} + +.red { + background-color: #F0433D; + color: #fff; +} + +.brown { + background-color: #f0ad4e; + color: #fff; +} + +.back-footer-red { + background-color: #F0433D; + color: #fff; + border-top: 0px solid #fff; +} + +.icon-box-right { + display: block; + float: right; + margin: 0 15px 10px 0; + width: 70px; + height: 70px; + line-height: 75px; + vertical-align: middle; + text-align: center; + font-size: 40px; +} + +.main-temp-back { + background: #8702A8; + color: #FFFFFF; + font-size: 16px; + font-weight: 300; + text-align: center; +} + +.main-temp-back .text-temp { + font-size: 40px; +} + +.back-dash { + padding: 20px; + font-size: 20px; + font-weight: 500; + -webkit-border-radius: 0px; + -moz-border-radius: 0px; + border-radius: 0px; + background-color: #2EA7EB; + color: #fff; +} + +.back-dash p { + padding-top: 16px; + font-size: 13px; + color: #fff; + line-height: 25px; + text-align: justify; +} + +.color-bottom-txt { + color: #000; + font-size: 16px; + line-height: 30px; +} + /*CHAT PANEL*/ +/*Charts*/ + +.main-chart { + background: #fff; +} + +.easypiechart-panel { + text-align: center; + padding: 1px 0; + margin-bottom: 20px; +} + +.placeholder h2 { + margin-bottom: 0px; +} + +.donut { + width: 100%; +} + +.easypiechart { + position: relative; + text-align: center; + width: 120px; + height: 120px; + margin: 20px auto 10px auto; +} + +.easypiechart .percent { + display: block; + position: absolute; + font-size: 26px; + top: 38px; + width: 120px; +} + +#easypiechart-blue .percent { + color: #30a5ff; +} + +#easypiechart-teal .percent { + color: #1ebfae; +} + +#easypiechart-orange .percent { + color: #ffb53e; +} + +#easypiechart-red .percent { + color: #ef4040; +} + +.chat-panel .panel-body { + height: 450px; + overflow-y: scroll; +} + +.chat-box { + margin: 0; + padding: 0; + list-style: none; +} + +.chat-box li { + margin-bottom: 15px; + padding-bottom: 5px; + border-bottom: 1px dotted #808080; +} + +.chat-box li.left .chat-body { + margin-left: 90px; +} + +.chat-box li .chat-body p { + margin: 0; + color: #8d8888; +} + +.chat-img>img { + margin-left: 20px; +} + +footer p { + font-size: 14px; +} +/*---------------------------------------------- + MENU STYLES +------------------------------------------------*/ + + +.user-image { + margin: 25px auto; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + max-height: 170px; + max-width: 170px; +} + +.top-navbar { + margin: 0px; +} + +.top-navbar .navbar-brand { + color: #fff; + width: 260px; + text-align: left; + height: 60px; + font-size: 30px; + font-weight: 700; + text-transform: uppercase; + line-height: 30px; + background: #32323a; +} + +.navbar-brand b { + margin-right:5px; + color: #2DAFCB; +} + +.top-navbar .nav > li { + position: relative; + display: inline-block; + margin: 0px; + padding: 0px 10px; +} + +.top-navbar .nav > li > a { + position: relative; + display: block; + padding: 20px; + color: #FFFFFF; + margin: 0px; +} + +.top-navbar .nav > li > a:hover, .top-navbar .nav > li > a:focus { + text-decoration: none; + color: #319DB5 !important; + background: transparent; +} + +.top-navbar .nav > li:hover .dropdown-menu{ + display: block; +} +.top-navbar .dropdown-menu { + min-width: 230px; + top: 98%; + border-radius: 0 0 4px 4px; +} +.dropdown-menu > li > a{ + cursor: pointer; +} +.top-navbar .dropdown-menu > li > a:hover, .top-navbar .dropdown-menu > li > a:focus { + color: #225081; + background: none; +} + +.dropdown-tasks { + width: 255px; +} + +.dropdown-tasks .progress { + height: 8px; + margin-bottom: 8px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 0px; +} + +.dropdown-tasks > li > a { + padding: 0px 15px; +} + +.dropdown-tasks p { + font-size: 13px; + line-height: 21px; + padding-top: 4px; +} + +.active-menu { + background-color: #2DAFCB !important; + color: #fff !important; +} + +.active-menu i { + color: #fff !important; +} + +.arrow { + float: right; + margin-top: 8px; +} + +.fa.arrow:before { + content: "\f104"; +} + +.active > a > .fa.arrow:before { + content: "\f107"; +} + +.nav-second-level li, +.nav-third-level li { + border-bottom: none !important; +} + +.nav-second-level li a { + padding-left: 37px; +} + +.nav-third-level li a { + padding-left: 55px; +} + +.sidebar-collapse , .sidebar-collapse .nav { + background: none; +} + +.sidebar-collapse .nav { + padding: 0; +} + +.sidebar-collapse .nav > li > a { + color: #B5B5B5; + background: transparent; + text-shadow: none; +} + +.sidebar-collapse > .nav > li > a { + padding: 12px 10px; +} + +.sidebar-collapse > .nav > li { + border-bottom: 1px solid rgba(107, 108, 109, 0.19); +} + +ul.nav.nav-second-level.collapse.in { + background: #17191B; +} + +.sidebar-collapse .nav > li > a:hover, +.sidebar-collapse .nav > li > a:focus { + outline: 0; +} + +.navbar-side { + border: none; +} + +.top-navbar { + background: #fff; + border-bottom: none; +} + +.top-navbar .nav > li > a > i { + margin-right: 2px; +} +.top-navbar .nav > li > a > span { + color: #666; + margin-right: 5px; +} +.top-navbar .navbar-brand:hover { + color: #2DAFCB; + background-color: rgb(43, 46, 51); +} + +.dropdown-user li { + margin: 8px 0; +} + +.navbar-default { + border: 0px solid black; +} + +.navbar-header { + background: transparent; + display: flex; + background-color: rgb(43, 46, 51); +} + +.navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus { + background-color: #B40101; +} + +.navbar-default .navbar-toggle { + border-color: #fff; +} + +.navbar-default .navbar-toggle .icon-bar { + background-color: #FFF; +} + +.nav > li > a > i { + margin-right: 10px; + color: #666; +} +/*---------------------------------------------- + UI ELEMENTS STYLES +------------------------------------------------*/ +.btn-circle { + width: 50px; + height: 50px; + padding: 6px 0; + -webkit-border-radius: 25px; + -moz-border-radius: 25px; + border-radius: 25px; + text-align: center; + font-size: 12px; + line-height: 1.428571429; +} + +/*---------------------------------------------- + MEDIA QUERIES +------------------------------------------------*/ + +@media(max-width:768px) { + + .top-navbar{ + position: relative; + } + .top-navbar .navbar-brand{ + width: 100%; + } + + + .navbar-side { + z-index: 1; + width: 100%; + position: relative; + top: 0; + } + #page-wrapper { + margin: 0; + padding: 15px 30px; + top: 0; + } +} + + diff --git a/src/component/page-title/index.js b/src/component/page-title/index.js new file mode 100644 index 0000000000000000000000000000000000000000..483072f4c57b4571584e9cc213073c8e50a8142d --- /dev/null +++ b/src/component/page-title/index.js @@ -0,0 +1,30 @@ +import React from 'react'; + + +class PageTitle extends React.Component{ + constructor(props){ + super(props) + } + //在这里 我们使用一下 生命周期 + componentWillMount(){ + document.title = this.props.title + '- HAPPY MMALL'; + } + render(){ + return( +
+
+

+ {this.props.title} +

+ {/* 下面这块的意思是 + 首页的右边可以有 一些其他的东西 + 我们用包含组件的形式来展示 + */} + {this.props.children} +
+
+ + ) + } +} +export default PageTitle; diff --git a/src/component/sidenav/index.js b/src/component/sidenav/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d0166c9aee21bd8ba114d49a3fd761f87b80ddf1 --- /dev/null +++ b/src/component/sidenav/index.js @@ -0,0 +1,69 @@ +import React from 'react'; +import { Link,NavLink} from 'react-router-dom'; + + +class SideNav extends React.Component{ + constructor(props){ + super(props); + } + render(){ + return( +
+
+ + +
+ +
+ ) + } +} + +export default SideNav; \ No newline at end of file diff --git a/src/component/topnav/index.js b/src/component/topnav/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5baa9305d0c466f020a03a28ec9eb57f34cad2f5 --- /dev/null +++ b/src/component/topnav/index.js @@ -0,0 +1,269 @@ +import React from 'react'; +import { Link} from 'react-router-dom'; + +import MUtil from 'util/mm.js'; +import User from 'servers/user-server.js'; + +const _user = new User(); +const _mm = new MUtil(); + +class TopNav extends React.Component{ + constructor(props){ + super(props); + this.state={ + username: _mm.getStorage('userInfo').username || '' + } + } + // 退出登录 + onLogout(){ + _user.logout().then(res=>{ + _mm.removeStorage('userInfo'); + // 因为topNav 这个组件 没有history对象 没有 跳转链接 + // so 直接用window.location.href 来跳转 + + // this.props.history.push('/login'); + window.location.href='/login'; + }, errMsg =>{ + _mm.errorTips(errMsg); + }); + } + render(){ + return( +
+
+ + HAPPYMMALL + +
+ + +
+ ) + } +} + +export default TopNav; \ No newline at end of file diff --git a/src/image/favicon.ico b/src/image/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a8d3317951f2143b09f8782ddec1a1117a4295d5 Binary files /dev/null and b/src/image/favicon.ico differ diff --git a/src/index.css b/src/index.css deleted file mode 100644 index c66425016bbaba6ce9e527c205f4810c2ca7a7bd..0000000000000000000000000000000000000000 --- a/src/index.css +++ /dev/null @@ -1,4 +0,0 @@ -#app{ - color:red; - background:url(./image/hc.jpg); -} diff --git a/src/index.html b/src/index.html index 5330fc65b64b6c81c2da66cd977ba7aff4f26264..3eed0c00960576dd6d1f86ca34b550888b4e8992 100644 --- a/src/index.html +++ b/src/index.html @@ -5,9 +5,14 @@ - happy mall admin + + + + + HAPPY MMALL -
case
+
+ \ No newline at end of file diff --git a/src/index.scss b/src/index.scss deleted file mode 100644 index 684bf96ea8534ae799c7bd212c989d4a7a6b31a2..0000000000000000000000000000000000000000 --- a/src/index.scss +++ /dev/null @@ -1,6 +0,0 @@ -body{ - background: #ccc; - #app{ - font-size:100px; - } -} \ No newline at end of file diff --git a/src/page/Home/index.js b/src/page/Home/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2fe0faafbff9ae403174a8ec603dbfb15dde451c --- /dev/null +++ b/src/page/Home/index.js @@ -0,0 +1,74 @@ +import React from 'react'; + +import MUtil from 'util/mm.js'; +import Statistic from 'servers/statistic-server.js'; + +const _statistic = new Statistic(); +const _mm = new MUtil(); + +import './index.scss'; +import PageTitle from 'component/page-title/index.js'; +import { Link } from 'react-router-dom'; +class Home extends React.Component{ + constructor(props){ + super(props); + this.state={ + userCount: '300', + productCount: '4000', + orderCount:'500' + } + } + componentDidMount(){ + this.loadCount(); + } + loadCount(){ + _statistic.getHomeCount().then(res=>{ + this.setState(res); + }, errMsg=>{ + _mm.errorTips(errMsg); + }); + } + render(){ + return ( +
+ {/* 因为在 PageTitle 组件页 + 我们用的是 this.props.children + 包含组件 所以 这里就要用双标签 + */} + +
+
+ +

{this.state.userCount}

+

+ + 用户数 +

+ +
+
+ + +

{this.state.productCount}

+

+ + 商品总数 +

+ +
+
+ +

{this.state.orderCount}

+

+ + 订单总数 +

+ +
+
+
+ ) + } +} + +export default Home; diff --git a/src/page/Home/index.scss b/src/page/Home/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..33c4718dedc962240a0618cfefbdb8de7c48bc6d --- /dev/null +++ b/src/page/Home/index.scss @@ -0,0 +1,27 @@ +.panel-primary{ + border:none; + height:160px; + opacity: .9; + transition:all 0.3s; + .count{ + margin-top: 10px; + font-size: 50px; + height:80px; + line-height:80px; + } + .desc{ + font-size:18px; + .fa{ + margin-right:5px; + } + } + &:hover{ + text-decoration:none; + opacity:1; + transform:scale(1.08); + } + &:focus{ + text-decoration:none; + } +} + diff --git a/src/page/error/index.js b/src/page/error/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5f494b24cef176f535ef27e479dc975f52551def --- /dev/null +++ b/src/page/error/index.js @@ -0,0 +1,22 @@ +import React from 'react'; +import {Link} from "react-router-dom"; + +import PageTitle from "component/page-title/index.js" + +class ErrorPage extends React.Component{ + render(){ + return ( +
+ +
+
+ 找不到该路径
+ 点我 返回首页 +
+
+
+ ) + } +} + +export default ErrorPage \ No newline at end of file diff --git a/src/page/login/index.js b/src/page/login/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e34512e5272641e61ef8121d8be5bfc812feb35f --- /dev/null +++ b/src/page/login/index.js @@ -0,0 +1,133 @@ +import React from 'react'; + +import MUtil from 'util/mm.js' ; +import User from 'servers/user-server.js'; + +const _user = new User(); +const _mm = new MUtil(); + +import './login.scss'; +class Login extends React.Component{ + constructor(props){ + super(props); + this.state={ + username:"", + password:"", + // redirect是我们在URL参数里取的一个东西 + redirect:_mm.getUrlParam('redirect') || '/' + // 然后需要我们 去在 _mm 定义一个取参数的工具 + } + } + componentWillMount(){ + document.title="登录 --HAPPY SUPER 系统"; + } + // 当用户名 发生 改变 + // onUsernameChange(e){ + // console.log(e.target.value); + // this.setState({ + // username: e.target.value + // }) + // } + // onPasswordChange(e){ + // console.log(e.target.value); + // this.setState({ + // password: e.target.value + // }) + // } +// 代码 重构 + + onInputChange(e){ + + let inputname=e.target.name, + inputvalue= e.target.value; + + this.setState({ + // 这里 很好的用到了同名 + [inputname]:inputvalue + }) + } + onInputKeyUp(e){ + // console.log('调用oninputkeyUp 事件'); + if(e.KeyCode === 13){ + console.log('onSubmit 事件'); + this.onSubmit(); + } + } + onSubmit(e){ + // __mm.request() 这里用的是一个函数 + // __mm.request({}) 用{} 来包含 函数里 的一些参数 要传送的参数 + let loginInfo={ + username : this.state.username, + password : this.state.password + } + let checkResult = _user.checkLonginInfo(loginInfo); + // 验证通过 + if (checkResult.status){ + _user.login(loginInfo).then((res) => { + //这块是本地存储的功能 在登录完成后 + // console.log("then里的res"); + // console.log(res); + // localStorage.setItem("userInfo",JSON.stringify(res)); + _mm.setStorage("userInfo",res) + + //请求成功的话 就要他返回到首页页面 + // console.log(this.state.redirect); + this.props.history.push(this.state.redirect) + // history 是reac t的提供的 然后 push 压入一个新的页面 这就是来时候的页面 + }, (errMsg) => { + // console.log("失败后的参数"); + // console.log(errMsg); + _mm.errorTips(errMsg); + }); + } + // 验证没通过 + else { + _mm.errorTips(checkResult.msg); + } + + } + render(){ + return ( +
+
+
+

欢迎登陆 --SUPER 管理系统

+
+
+
+
+ + { this.onInputKeyUp(e)}} + onChange={(e)=>{this.onInputChange(e)}} + /> +
+
+ + { this.onInputKeyUp(e) }} + onChange={(e)=>{this.onInputChange(e)}} + /> +
+ + +
+
+
+
+ ) + } +} + +export default Login; \ No newline at end of file diff --git a/src/page/login/login.scss b/src/page/login/login.scss new file mode 100644 index 0000000000000000000000000000000000000000..8d4a8ac7f87612c43701a1ab8cd74a07aa9340d6 --- /dev/null +++ b/src/page/login/login.scss @@ -0,0 +1,3 @@ +.login-panel{ + margin-top: 30%; +} \ No newline at end of file diff --git a/src/page/product/categort/index.js b/src/page/product/categort/index.js new file mode 100644 index 0000000000000000000000000000000000000000..eb2c3f2af1965d9a3f8acca795f8c52e665d9104 --- /dev/null +++ b/src/page/product/categort/index.js @@ -0,0 +1,21 @@ +// 这是一个router 组件 为防止代码堆积 而分离出来的router +import React from 'react'; + + +import { Link } from "react-router-dom"; + + + +class ProductCategort extends React.Component { + constructor(props) { + super(props); + } + render() { + return ( +
+ 5432624335 +
+ ); + } +} +export default ProductCategort \ No newline at end of file diff --git a/src/page/product/index/category-selector.js b/src/page/product/index/category-selector.js new file mode 100644 index 0000000000000000000000000000000000000000..49c467185088e6c48b56ddd043c1e6c4f51ec641 --- /dev/null +++ b/src/page/product/index/category-selector.js @@ -0,0 +1,123 @@ +import React from 'react'; +import './category-selector.scss'; + +// 调用商品的接口 把品类的请求接口也放到商品server里 +import MUtil from 'util/mm.js'; +import Product from 'servers/product-server.js'; + +const _product = new Product(); +const _mm = new MUtil(); + +// 品类选择器 +class CategorSelector extends React.Component { + constructor(props){ + super(props); + this.state={ + firstCategoryList :[], //一级品类的列表 + firstCategoryId :0, + secondCategoryList :[], + secondCategoryId :0 + + } + } + + componentDidMount(){ + this.loadFirstCategory(); + } + //加载一级分类 + loadFirstCategory(){ + + _product.getCategoryList().then(res =>{ + this.setState({ + firstCategoryList : res + }); + },errMsg =>{ + _mm.errorTips(errMsg); + }) + } + // 加载二级分类 + loadSecondCategory(){ + _product.getCategoryList(this.state.firstCategoryId).then(res =>{ + this.setState({ + secondCategoryList : res + }); + },errMsg =>{ + _mm.errorTips(errMsg); + }) + } + // 加载一级品类的变化 + // 选择一级品类 + onFirstCategoryChange(e){ + let newValue = e.target.value || 0 ; + // 记住 setState是异步操作 + this.setState({ + firstCategoryId : newValue, + secondCategoryId :0, + secondCategoryList:[] + },()=>{ + // 更新二级陪你品类 + this.loadSecondCategory(); + // 暴露一级品类 + this.onPropsCategoryChange() + }) + } + // 选着二级品类 + onSecondCategoryChange(e){ + let newValue = e.target.value || 0 ; + // 记住 setState是异步操作 + this.setState({ + secondCategoryId : newValue + },()=>{ + // 传给父组件选中的结果 + this.onPropsCategoryChange() + }) + } + // 传给父组件选中的结果 + onPropsCategoryChange(){ + // 判断 props 里的回掉函数存在 + let categoryChangable = typeof this.props.onCategoryChange === 'function' ; + // 判断是否有二级品类 + if(this.state.secondCategoryId){ + // 这是把二级品类的值传给父级组件 + categoryChangable && this.props.onCategoryChange(this.state.secondCategoryId,this.state.firstCategoryId); + }else{ + // 直接把一级品类的值传给父级组件 + categoryChangable && this.props.onCategoryChange(this.state.firstCategoryId,0); + } + } + render() { + return ( +
+
+ + { this.state.secondCategoryList.length ? + (): null + } +
+
+ ); + + + + + } +} +export default CategorSelector \ No newline at end of file diff --git a/src/page/product/index/category-selector.scss b/src/page/product/index/category-selector.scss new file mode 100644 index 0000000000000000000000000000000000000000..cc518a584a893aa82074622fe1beebac66f54bfa --- /dev/null +++ b/src/page/product/index/category-selector.scss @@ -0,0 +1,6 @@ +.cate-select{ + width:200px; + margin-right:15px; + display: inline-block; + +} \ No newline at end of file diff --git a/src/page/product/index/index-list-search.js b/src/page/product/index/index-list-search.js new file mode 100644 index 0000000000000000000000000000000000000000..d8c662128432b1dfe68732a0b5ec8cb5779ec1e6 --- /dev/null +++ b/src/page/product/index/index-list-search.js @@ -0,0 +1,58 @@ +import React from 'react'; + +class ListSearch extends React.Component{ + constructor(props){ + super(props); + this.state={ + searchType:'productId',//productId,productName + searchKeyword:'' + } + } + // 数据变化的时候 + onValueChange(e){ + let name = e.target.name, + value = e.target.value.trim(); + this.setState({ + [name]:value + }); + } + // 点击搜索按钮的时候 + onSearch(){ + //这里的onSearch 事件是父级的事件 这里用 this.props去掉用 + //然后传值用 this.state + this.props.onSearch(this.state.searchType,this.state.searchKeyword); + } + // 这里是输入关键字后 按回车 自动提交 + onSearchKeywordKeyUp(e){ + if(e.keyCode ===13){ + //调用事件 用this去找 这里的this 指的是这个组件 + this.onSearch(); + } + + } + render(){ + return( +
+
+
+ +
+
+ this.onSearchKeywordKeyUp(e)} + name="searchKeyword" + onChange={(e)=>this.onValueChange(e)} + placeholder="关键词" /> +
+ +
+
+ ) + } +} +export default ListSearch; \ No newline at end of file diff --git a/src/page/product/index/index.js b/src/page/product/index/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3556ac120b2d89794ae02dddce794fae78d9e062 --- /dev/null +++ b/src/page/product/index/index.js @@ -0,0 +1,169 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.scss'; + + +// import './index.scss'; +import PageTitle from 'component/page-title/index.js'; +import Pageination from 'util/pageination/index.js'; +import Tablist from 'util/table-list/index.js'; + +import ListSearch from './index-list-search'; + + +import MUtil from 'util/mm.js'; +import Product from 'servers/product-server.js'; + +const _product = new Product(); +const _mm = new MUtil(); + +import { Link } from 'react-router-dom'; +class ProductList extends React.Component { + constructor(props) { + super(props); + this.state = { + list: [], + pageNum: 1, + listType:'list' + + } + } + // 生命周期 渲染完成之后 + componentDidMount() { + this.loadProductList(); + } + loadProductList() { + let listParam = {}; + listParam.listType=this.state.listType; + listParam.pageNum = this.state.pageNum; + +//判断 如果是搜索的话 需要传入搜索的类型和关键字 + if(this.state.listType === 'search'){ + // 这里是把 输入的关键词 + listParam.searchType = this.state.searchType; + listParam.Keyword = this.state.searchKeyword; + console.log('查看点击搜索拿到的输入数据'+ listParam.searchType,listParam.Keyword); + } + // 请求接口 + _product.getProductList(listParam).then(res => { + // 注意这块 + // 在这里 我们传入的res res 是请求成功后 + // Promise 里的resolve方法里传过来的res.data + // 然后用res.data里的数据完全替换this.state里的数据 + + // 在加载用户信息的时候 添加了一个回调函数 + this.setState(res); + }, errMsg => { + this.setState({ + list: [] + }); + _mm.errorTips(errMsg); + }); + } + // 当页数发生变化的时候 + onPageNumChange(pageNum) { + // 这里需要注意 setState 是异步函数 它并不是同步的 + this.setState({ + pageNum: pageNum + + // 下面添加了一个回调 + // 当setState执行完成后也就是页数传递后 + // 执行了一个 loadUserList()来进行加载数据 + }, () => { + // 这里的loadUserList ()里什么都不用传 + this.loadProductList(); + }) + } + // 改变商品状态 用来判断上下架功能 + onSetProductStatus(e,productId, currentStatus){ + let newStatus = currentStatus ==1 ? 2 : 1, + confirmTips = currentStatus == 1 ?'确认要下架该商品吗?':'确认要上架该商品吗?'; + // 在操作之前 做一个操作提醒 防止误操作 + if (window.confirm(confirmTips)){ + _product.onSetProductStatus({ + productId:productId, + status: newStatus + }).then(res =>{ + _mm.successTips(res); + // 列表重新加载 + this.loadProductList(); + }, errMsg =>{ + _mm.errTips(res); + }); + } + } + // 搜索功能 + onSearch(searchType,searchKeyword){ + console.log(searchType,searchKeyword); + let listType = searchKeyword ==='' ? 'list':'search'; + this.setState({ + listType :listType, + pageNum :1, + searchType :searchType, + searchKeyword :searchKeyword, + + },//这里 又添加了一个回调 + ()=>{ + console.log('3'+searchType,searchKeyword); + this.loadProductList(); + }); + } + render() { + let tableHeads=[ + {name:'商品ID',width:'10%' }, + {name:"商品信息",width:"50%" }, + {name:"商品价格",width:"10%" }, + {name:"状态",width:"15%" }, + {name:"操作",width:"15%" }, + ] + return ( +
+ +
+ + + + 添加商品 + +
+
+ {this.onSearch(searchType,searchKeyword)}}/> + {/* 这里用了 重构 简洁代码 */} + + { + this.state.list.map((product, index) => { + return ( + + {product.id} + +

{product.name}

+

{product.subtitle}

+ + ${product.price} + +

+ {product.status == 1 ?"在售":"下架"} +

+ + + + 查看详情 + 编辑 + + + ) + }) + } +
+ {/* */} + { this.onPageNumChange(pageNum) }} + /> +
+ ) + } +} + +export default ProductList; diff --git a/src/page/product/index/index.scss b/src/page/product/index/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..656c9af2dd30a13a24396e0e8768183e00368cbe --- /dev/null +++ b/src/page/product/index/index.scss @@ -0,0 +1,10 @@ +.opear{ + margin-right: 5px; +} +.search-wrap{ + padding-left: 0px; + padding-bottom: 15px; + .form-group{ + padding-right:10px; + } +} \ No newline at end of file diff --git a/src/page/product/index/save.js b/src/page/product/index/save.js new file mode 100644 index 0000000000000000000000000000000000000000..11a9c0d03e44d88083eb2800f0d23dd1d300bd39 --- /dev/null +++ b/src/page/product/index/save.js @@ -0,0 +1,217 @@ + +import React from 'react'; +import PageTitle from 'component/page-title/index.js'; +import CategorSelector from './category-selector.js'; + +import FileUploader from 'util/file-upload/image-upload.js'; +import RichEditor from 'util/rich-editor/rich-editor.js'; + +import './save.scss'; + +import MUtil from 'util/mm.js'; +import Product from 'servers/product-server.js'; + +import { Link } from 'react-router-dom'; + +const _product = new Product(); +const _mm = new MUtil(); + +class ProductSave extends React.Component { + constructor(props){ + super(props); + this.state={ + name : '', + subtitle : '', + categoryId : 0, + parentCategoryId : 0, + subImages : [], + price : '', + stock : '', + detail : '', + status : 1 //商品状态1为在售 + + } + } + //简单的字段的改变,比如 商品 的描述 价格 库存 + onValueChange(e){ + let name = e.target.name, + value = e.target.value.trim(); + this.setState({ + [name]:value + }) + } + + // 获取子集组件品类的的选着 + onCategoryChange(categoryId, parentCategoryId){ + this.setState({ + categoryId : categoryId, + parentCategoryId : parentCategoryId + }); + + } + // 上传图片成功 + onUploadsuccess(res){ + let subImages = this.state.subImages; + subImages.push(res); + this.setState({ + subImages : subImages + }); + } + // 上传图片 失败 + onUploaderror(errMsg){ + _mm.errorTips(errMsg); + } + // 删除图片 + onImageDelect(e){ + // 首先要 先获取 index的值 好知道要删除那个图片 + let index = parseInt(e.target.getAttribute('index')), + subImages = this.state.subImages; + // 下面我们要删除subImages 下对应的图片 + + subImages.splice(index,1); + this.setState({ + subImages:subImages + }) + } + // 富文本编辑器的变化 + onDetailValueChange(value){ + console.log(value); + this.setState({ + detail:value + }) + } + getSubImagesString(){ + return this.state.subImages.map((image)=>image.uri).join(','); + } + // 提交表单 + onSubmint(){ + let product = { + name : this.state.name, + subtitle : this.state.subtitle, + categoryId : parseInt(this.state.categoryId), + subImages : this.getSubImagesString(), + detail : this.state.detail, + price : parseFloat(this.state.price), + stock : parseInt(this.state.stock), + status : this.state.status + + }, + productCheckResult = _product.checkProduct(product); + // 表单验证成功 + if(productCheckResult.status){ + _product.saveProduct(product).then((res)=>{ + _mm.successTips(res); + this.props.history.push('/product/index'); + },(errMsg)=>{ + _mm.errorTips(errMsg); + }); + }else{ + // 表单验证失败 + _mm.errorTips(productCheckResult.msg); + } + } + render() { + return ( +
+ +
+ + + 取消 + +
+
+
+
+ +
+ this.onValueChange(e)} + placeholder="商品名称"/> +
+
+
+ +
+ this.onValueChange(e)} + placeholder="商品描述"/> +
+
+ +
+ + this.onCategoryChange(categoryId,parentCategoryId) + }/> +
+ +
+ +
+
+ this.onValueChange(e)} + placeholder="商品价格"/> + $ +
+
+
+ +
+ +
+
+ this.onValueChange(e)} + placeholder="商品库存"/> + +
+
+
+ +
+ +
+ {/* 这里存放图片 */} + { + this.state.subImages.length ? this.state.subImages.map( + (image,index)=>( +
+ + this.onImageDelect(e)}> +
+ ) + + ):(
请上传图片
) + + } +
+
+ this.onUploadsuccess(res)} + onError={(errMsg)=>this.onUploaderror(errMsg)} + /> +
+
+ +
+ +
+ this.onDetailValueChange(value)}/> +
+
+
+
+ +
+
+
+
+ ); + } +} +export default ProductSave \ No newline at end of file diff --git a/src/page/product/index/save.scss b/src/page/product/index/save.scss new file mode 100644 index 0000000000000000000000000000000000000000..ca068ac6bd746e0f14bd51685866a46b27890467 --- /dev/null +++ b/src/page/product/index/save.scss @@ -0,0 +1,39 @@ +$sub-img-size:80px; +.img-con{ + position: relative; + width:$sub-img-size; + height:$sub-img-size; + float:left; + margin-left:10px; + border:1px solid #ccc; + position: relative; + .img{ + width:100%; + height:100%; + display: block; + } + .fa-close{ + position: absolute; + top:0; + bottom: 0; + left: 0; + right:0; + cursor:pointer; + display:none; + font-size:50px; + color:#ccc; + background: rgba(0,0,0,.2); + text-align: center; + height: $sub-img-size; + line-height: $sub-img-size; + } + &:hover{ + .fa-close{ + display: block; + } + } + +} +.margin-top{ + margin-top: 20px; +} \ No newline at end of file diff --git a/src/page/product/router.js b/src/page/product/router.js new file mode 100644 index 0000000000000000000000000000000000000000..5b5440a0749385473eef43a2922542492abfb322 --- /dev/null +++ b/src/page/product/router.js @@ -0,0 +1,30 @@ + + +import React from 'react'; + +import { BrowserRouter as Router, Route, Redirect, Switch, Link } + from "react-router-dom"; + +// 这是页面 做集成用的 + +import ProductList from 'page/product/index/index.js'; +// import ProductCategort from 'page/product/categort/index.js'; +import ProductSave from 'page/product/index/save.js'; + + +class ProductRouter extends React.Component { + render() { + return ( +
+ + + + + {/* */} + + +
+ ); + } +} +export default ProductRouter \ No newline at end of file diff --git a/src/page/user/index.js b/src/page/user/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6abaf4d25aa2f093e5e9633856d7d32e8ff28e61 --- /dev/null +++ b/src/page/user/index.js @@ -0,0 +1,92 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + + +// import './index.scss'; +import PageTitle from 'component/page-title/index.js'; +import Pageination from 'util/pageination/index.js'; +import TableList from 'util/table-list/index.js'; + + +import MUtil from 'util/mm.js'; +import User from 'servers/user-server.js'; + +const _user = new User(); +const _mm = new MUtil(); + +import { Link } from 'react-router-dom'; +class UserList extends React.Component { + constructor(props){ + super(props); + this.state={ + list :[], + pageNum :1 + } + } + // 生命周期 渲染完成之后 + componentDidMount(){ + this.loadUserList(); + } + loadUserList(){ + _user.getUserList(this.state.pageNum).then(res=>{ + // 注意这块 + // 在这里 我们传入的res res 是请求成功后 + // Promise 里的resolve方法里传过来的res.data + // 然后用res.data里的数据完全替换this.state里的数据 + + // 在加载用户信息的时候 添加了一个回调函数 + this.setState(res); + },errMsg=>{ + this.setState({ + list:[] + }); + _mm.errorTips(errMsg); + }); + } + // 当页数发生变化的时候 + onPageNumChange(pageNum){ + // 这里需要注意 setState 是异步函数 它并不是同步的 + this.setState({ + pageNum:pageNum + + // 下面添加了一个回调 + // 当setState执行完成后也就是页数传递后 + // 执行了一个 loadUserList()来进行加载数据 + },()=>{ + // 这里的loadUserList ()里什么都不用传 + this.loadUserList(); + }) + } + render() { + let ListBody = this.state.list.map((user, index) => { + return ( + + {user.id} + {user.username} + {user.email} + {user.phone} + {new Date(user.createTime).toLocaleString()} + + + ) + }); + return ( + +
+ + + + {ListBody} + + + { this.onPageNumChange(pageNum)}} + /> +
+ ) + } +} + +export default UserList; diff --git a/src/servers/product-server.js b/src/servers/product-server.js new file mode 100644 index 0000000000000000000000000000000000000000..6246e5c9f476582e91ac8044986848d06f99ea5b --- /dev/null +++ b/src/servers/product-server.js @@ -0,0 +1,109 @@ +import MUtil from 'util/mm.js'; +const _mm = new MUtil(); + +class Product { +// 获取用户列表 + getProductList(listParam){ + let url = '', + data = {}; + if(listParam.listType ==='list'){ + url = '/manage/product/list.do'; + data.pageNum =listParam.pageNum; + }else if(listParam.listType === 'search'){ + url = '/manage/product/search.do'; + data.pageNum =listParam.pageNum; + data[listParam.searchType] = listParam.Keyword; + console.log(data); + } + return _mm.request({ + type: 'post', + // url 的地址 有跨域问题 需要用 请求劫持 来做 + url : url, + data:data + }); + } + // 变更 商品的销售状态 上下价 + onSetProductStatus(productInfo){ + return _mm.request({ + type: 'post', + // url 的地址 有跨域问题 需要用 请求劫持 来做 + url: '/manage/product/set_sale_status.do', + data: productInfo + + + }); + } + + // 检查保存商品的表单数据 + checkProduct(product){ + let result ={ + status :true, + msg :'验证通过' + }; + console.log(product); + // 判断用户名不能为空 + if(typeof product.name !=='string' || product.name.length === 0){ + return { + status: false, + msg: '商品名称不能为空!' + } + } + // 判断描述不能为空 + if(typeof product.subtitle !=='string' || product.subtitle.length === 0){ + return { + status: false, + msg: '商品描述不能为空!' + } + } + // 判断品类Id + if(typeof product.categoryId !=='number' || !(product.categoryId >0)){ + return { + status: false, + msg: '请选择商品品类!' + } + } + // 判断价格为数字 切大于0 + if(typeof product.price !=='number' || !(product.price >= 0)){ + return { + status: false, + msg: '请输入正确 的商品价格' + } + } + // 判断库存为数字 切大于0 + if(typeof product.stock !=='number' || !(product.stock >= 0)){ + return { + status: false, + msg: '请输入正确 的库存数量' + } + } + return result; + } + + // 保存商品 + saveProduct(product){ + return _mm.request({ + type: 'post', + // url 的地址 有跨域问题 需要用 请求劫持 来做 + url: '/manage/product/save.do', + + data:product + + }); + } + + // 这里是用来 品类相关 + getCategoryList(parentCategoryId){ + return _mm.request({ + type: 'post', + // url 的地址 有跨域问题 需要用 请求劫持 来做 + url: '/manage/category/get_category.do', + + data: { + // 一级品类的时候parentCategoryId为空 所以 categoryId=0 + categoryId : parentCategoryId || 0 + } + + }); + } +} +export default Product; \ No newline at end of file diff --git a/src/servers/statistic-server.js b/src/servers/statistic-server.js new file mode 100644 index 0000000000000000000000000000000000000000..68ae20571472c0f8bf475d1a64c88455fff7641f --- /dev/null +++ b/src/servers/statistic-server.js @@ -0,0 +1,15 @@ +import MUtil from 'util/mm.js'; +const _mm = new MUtil(); + +class Statistic{ + + getHomeCount(){ + + return _mm.request({ + // url 的地址 有跨域问题 需要用 请求劫持 来做 + url: '/manage/statistic/base_count.do' + + }); + } +} +export default Statistic diff --git a/src/servers/user-server.js b/src/servers/user-server.js new file mode 100644 index 0000000000000000000000000000000000000000..13ad0cc5c8095a55021e52115cfa44a387bc906d --- /dev/null +++ b/src/servers/user-server.js @@ -0,0 +1,63 @@ +import MUtil from 'util/mm.js' ; +const _mm = new MUtil(); + +class User{ + // 用户登录 + login(loginInfo){ + // 这里 为了后面可以使用then方法 所以返回值必须是promise 结构所以直接把request整个返回 + return _mm.request({ + type: 'post', + // url 的地址 有跨域问题 需要用 请求劫持 来做 + url: '/manage/user/login.do', + data: loginInfo + + }) + } +// 检查登录接口的数据是不是合法 +// $.trim 用于去除字符串两端的空白字符 + checkLonginInfo(loginInfo){ + let username = $.trim(loginInfo.username), + password = $.trim(loginInfo.password); + //判断用户名为空 + if(typeof username !=='string' || username.length === 0){ + return { + status: false, + msg: '用户名不能为空' + } + } + // 判断密码为空 + if (typeof password !== 'string' || password.length === 0) { + return { + status: false, + msg: '用户密码不能为空' + } + } + return { + status:true, + msg:'验证通过' + } + } + // 退出登录 + logout(){ + return _mm.request({ + type: 'post', + // url 的地址 有跨域问题 需要用 请求劫持 来做 + url: '/user/logout.do' + + }) + } + getUserList(pageNum){ + return _mm.request({ + type: 'post', + // url 的地址 有跨域问题 需要用 请求劫持 来做 + url: '/manage/user/list.do', + data : { + pageNum:pageNum + } + + }) + } + + } + + export default User; \ No newline at end of file diff --git a/src/util/file-upload/image-upload.js b/src/util/file-upload/image-upload.js new file mode 100644 index 0000000000000000000000000000000000000000..0736c5675e7ff51be367c4920ef7227b8762f249 --- /dev/null +++ b/src/util/file-upload/image-upload.js @@ -0,0 +1,35 @@ +import React from 'react'; +import FileUpload from './react-fileupload.js'; + + +class FileUploader extends React.Component{ + render(){ + /*set properties*/ + const options={ + baseUrl:'/manage/product/upload.do', + // 指定组件的名称 + fileFieldName:'upload_file', + dataType :'json', + // 添加一个属性 当图片已选择 就上传 不需要点击上传 + chooseAndUpload :true, + // 成功和失败的函数要从外面传 + // 成功和失败 方法都是从这个组件外调用 + uploadSuccess:(res)=>{ + this.props.onSuccess(res.data); + }, + uploadError :(err)=>{ + this.props.onError(err.message||'上传图片除错啦'); + } + } + + /*Use FileUpload with options*/ + /*Set two dom with ref*/ + return ( + + + + ) + } +} + +export default FileUploader; \ No newline at end of file diff --git a/src/util/file-upload/react-fileupload.js b/src/util/file-upload/react-fileupload.js new file mode 100644 index 0000000000000000000000000000000000000000..034c03107a00d74b16e031a0806794a0084b2944 --- /dev/null +++ b/src/util/file-upload/react-fileupload.js @@ -0,0 +1,752 @@ +/** + * Created by cheesu on 2015/8/17. + */ + +/** + * React文件上传组件,兼容IE8+ + * 现代浏览器采用AJAX(XHR2+File API)上传。低版本浏览器使用form+iframe上传。 + * 使用到ES6,需要经babel转译 + */ + +/*eslint indent: 0 */ +import React from 'react'; + +import PT from 'prop-types'; +const emptyFunction = function() {} +/*当前IE上传组的id*/ +let currentIEID = 0 +/*存放当前IE上传组的可用情况*/ +const IEFormGroup = [true] +/*当前xhr的数组(仅有已开始上传之后的xhr)*/ +let xhrList = [] +let currentXHRID = 0 + + +class FileUpload extends React.Component{ + constructor(props){ + super(props); + this.state={ + chooseBtn: {}, //选择按钮。如果chooseAndUpload=true代表选择并上传。 + uploadBtn: {}, //上传按钮。如果chooseAndUpload=true则无效。 + before: [], //存放props.children中位于chooseBtn前的元素 + middle: [], //存放props.children中位于chooseBtn后,uploadBtn前的元素 + after: [] //存放props.children中位于uploadBtn后的元素, + } + } + + /*根据props更新组件*/ + _updateProps(props) { + this.isIE = !(this.checkIE() < 0 || this.checkIE() >= 10) + const options = props.options + this.baseUrl = options.baseUrl //域名 + this.param = options.param //get参数 + this.chooseAndUpload = options.chooseAndUpload || false //是否在用户选择了文件之后立刻上传 + this.paramAddToField = options.paramAddToField || undefined //需要添加到FormData的对象。不支持IE + /*upload success 返回resp的格式*/ + this.dataType = 'json' + options.dataType && (options.dataType.toLowerCase() == 'text') && (this.dataType = 'text') + this.wrapperDisplay = options.wrapperDisplay || 'inline-block' //包裹chooseBtn或uploadBtn的div的display + this.timeout = (typeof options.timeout == 'number' && options.timeout > 0) ? options.timeout : 0 //超时时间 + this.accept = options.accept || '' //限制文件后缀 + this.multiple = options.multiple || false + this.numberLimit = options.numberLimit || false //允许多文件上传时,选择文件数量的限制 + this.fileFieldName = options.fileFieldName || false //文件附加到formData上时的key,传入string指定一个file的属性名,值为其属性的值。不支持IE + this.withCredentials = options.withCredentials || false //跨域时是否使用认证信息 + this.requestHeaders = options.requestHeaders || false //要设置的请求头键值对 + + /*生命周期函数*/ + /** + * beforeChoose() : 用户选择之前执行,返回true继续,false阻止用户选择 + * @param null + * @return {boolean} 是否允许用户进行选择 + */ + this.beforeChoose = options.beforeChoose || emptyFunction + /** + * chooseFile(file) : 用户选择文件后的触发的回调函数 + * @param file {File | string} 现代浏览器返回File对象,IE返回文件名 + * @return + */ + this.chooseFile = options.chooseFile || emptyFunction + /** + * beforeUpload(file,mill) : 用户上传之前执行,返回true继续,false阻止用户选择 + * @param file {File | string} 现代浏览器返回File对象,IE返回文件名 + * @param mill {long} 毫秒数,如果File对象已有毫秒数则返回一样的 + * @return {boolean || object} 是否允许用户进行上传 (hack:如果是obj{ + * assign:boolean 默认true + * param:object + * }), 则对本次的param进行处理 + */ + this.beforeUpload = options.beforeUpload || emptyFunction + /** + * doUpload(file,mill) : 上传动作(xhr send | form submit)执行后调用 + * @param file {File | string} 现代浏览器返回File对象,IE返回文件名 + * @param mill {long} 毫秒数,如果File对象已有毫秒数则返回一样的 + * @return + */ + this.doUpload = options.doUpload || emptyFunction + /** + * uploading(progress) : 在文件上传中的时候,浏览器会不断触发此函数。IE中使用每200ms触发的假进度 + * @param progress {Progress} progress对象,里面存有例如上传进度loaded和文件大小total等属性 + * @return + */ + this.uploading = options.uploading || emptyFunction + /** + * uploadSuccess(resp) : 上传成功后执行的回调(针对AJAX而言) + * @param resp {json | string} 根据options.dataType指定返回数据的格式 + * @return + */ + this.uploadSuccess = options.uploadSuccess || emptyFunction + /** + * uploadError(err) : 上传错误后执行的回调(针对AJAX而言) + * @param err {Error | object} 如果返回catch到的error,其具有type和message属性 + * @return + */ + this.uploadError = options.uploadError || emptyFunction + /** + * uploadFail(resp) : 上传失败后执行的回调(针对AJAX而言) + * @param resp {string} 失败信息 + */ + this.uploadFail = options.uploadFail || emptyFunction + /** + * onabort(mill, xhrID) : 主动取消xhr进程的响应 + * @param mill {long} 毫秒数,本次上传时刻的时间 + * @param xhrID {int} 在doUpload时会返回的当次xhr代表ID + */ + this.onabort = options.onabort || emptyFunction + + this.files = options.files || this.files || false //保存需要上传的文件 + /*特殊内容*/ + + /*IE情况下,由于上传按钮被隐藏的input覆盖,不能进行disabled按钮处理。 + * 所以当disabledIEChoose为true(或者func返回值为true)时,禁止IE上传。 + */ + this.disabledIEChoose = options.disabledIEChoose || false + + this._withoutFileUpload = options._withoutFileUpload || false //不带文件上传,为了给秒传功能使用,不影响IE + this.filesToUpload = options.filesToUpload || [] //使用filesToUpload()方法代替 + this.textBeforeFiles = options.textBeforeFiles || false //make this true to add text fields before file data + /*使用filesToUpload()方法代替*/ + if (this.filesToUpload.length && !this.isIE) { + this.filesToUpload.forEach( file => { + this.files = [file] + this.commonUpload() + }) + } + + /*放置虚拟DOM*/ + let chooseBtn, uploadBtn, flag = 0 + const before = [], middle = [], after = [] + if (this.chooseAndUpload) { + React.Children.forEach(props.children, (child)=> { + if (child && child.ref == 'chooseAndUpload') { + chooseBtn = child + flag++ + } else { + flag == 0 ? before.push(child) : flag == 1 ? middle.push(child) : '' + } + }) + } else { + React.Children.forEach(props.children, (child)=> { + if (child && child.ref == 'chooseBtn') { + chooseBtn = child + flag++ + } else if (child && child.ref == 'uploadBtn') { + uploadBtn = child + flag++ + } else { + flag == 0 ? before.push(child) : flag == 1 ? middle.push(child) : after.push(child) + } + }) + } + this.setState({ + chooseBtn, + uploadBtn, + before, + middle, + after + }) + } + + /*触发隐藏的input框选择*/ + /*触发beforeChoose*/ + commonChooseFile() { + const jud = this.beforeChoose() + if (jud != true && jud != undefined) return + this.refs['ajax_upload_file_input'].click() + } + /*现代浏览器input change事件。File API保存文件*/ + /*触发chooseFile*/ + commonChange(e) { + let files + e.dataTransfer ? files = e.dataTransfer.files : + e.target ? files = e.target.files : '' + + /*如果限制了多文件上传时的数量*/ + const numberLimit = typeof this.numberLimit === 'function' ? this.numberLimit() : this.numberLimit + if(this.multiple && numberLimit && files.length > numberLimit) { + const newFiles = {} + for(let i = 0; i< numberLimit; i++) newFiles[i] = files[i] + newFiles.length = numberLimit + files = newFiles + } + this.files = files + this.chooseFile(files) + this.chooseAndUpload && this.commonUpload() + } + + /*执行上传*/ + commonUpload() { + /*mill参数是当前时刻毫秒数,file第一次进行上传时会添加为file的属性,也可在beforeUpload为其添加,之后同一文件的mill不会更改,作为文件的识别id*/ + const mill = (this.files.length && this.files[0].mill) || (new Date).getTime() + const jud = this.beforeUpload(this.files, mill) + if (jud != true && jud != undefined && typeof jud != 'object') { + /*清除input的值*/ + this.refs['ajax_upload_file_input'].value = '' + return + } + + + + if (!this.files) return + if (!this.baseUrl) throw new Error('baseUrl missing in options') + + /*用于存放当前作用域的东西*/ + const scope = {} + /*组装FormData*/ + let formData = new FormData() + /*If we need to add fields before file data append here*/ + if(this.textBeforeFiles){ + formData = this.appendFieldsToFormData(formData); + } + if (!this._withoutFileUpload) { + const fieldNameType = typeof this.fileFieldName + + /*判断是用什么方式作为formdata item 的 name*/ + Object.keys(this.files).forEach(key => { + if(key == 'length') return + + if(fieldNameType == 'function') { + const file = this.files[key] + const fileFieldName = this.fileFieldName(file) + formData.append(fileFieldName, file) + }else if(fieldNameType == 'string') { + const file = this.files[key] + formData.append(this.fileFieldName, file) + }else { + const file = this.files[key] + formData.append(file.name, file) + } + }) + + } + /*If we need to add fields after file data append here*/ + if(!this.textBeforeFiles){ + formData = this.appendFieldsToFormData(formData); + } + const baseUrl = this.baseUrl + + /*url参数*/ + /*如果param是一个函数*/ + const param = typeof this.param === 'function' ? this.param(this.files) : this.param + + let paramStr = '' + + if (param) { + const paramArr = [] + param['_'] = mill + Object.keys(param).forEach(key => + paramArr.push(`${key}=${param[key]}`) + ) + paramStr = '?' + paramArr.join('&') + } + const targeturl = baseUrl + paramStr + + /*AJAX上传部分*/ + const xhr = new XMLHttpRequest() + xhr.open('POST', targeturl, true) + + /*跨域是否开启验证信息*/ + xhr.withCredentials = this.withCredentials + /*是否需要设置请求头*/ + const rh = this.requestHeaders + rh && Object.keys(rh).forEach(key => + xhr.setRequestHeader(key, rh[key]) + ) + + /*处理超时。用定时器判断超时,不然xhr state=4 catch的错误无法判断是超时*/ + if(this.timeout) { + xhr.timeout = this.timeout + xhr.ontimeout = () => { + this.uploadError({type: 'TIMEOUTERROR', message: 'timeout'}) + scope.isTimeout = false + } + scope.isTimeout = false + setTimeout(()=>{ + scope.isTimeout = true + },this.timeout) + } + + xhr.onreadystatechange = () => { + /*xhr finish*/ + try { + if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status < 400) { + const resp = this.dataType == 'json' ? JSON.parse(xhr.responseText) : xhr.responseText + this.uploadSuccess(resp) + } else if (xhr.readyState == 4) { + /*xhr fail*/ + const resp = this.dataType == 'json' ? JSON.parse(xhr.responseText) : xhr.responseText + this.uploadFail(resp) + } + } catch (e) { + /*超时抛出不一样的错误,不在这里处理*/ + !scope.isTimeout && this.uploadError({type: 'FINISHERROR', message: e.message}) + } + } + /*xhr error*/ + xhr.onerror = () => { + try { + const resp = this.dataType == 'json' ? JSON.parse(xhr.responseText) : xhr.responseText + this.uploadError({type: 'XHRERROR', message: resp}) + } catch (e) { + this.uploadError({type: 'XHRERROR', message: e.message}) + } + } + /*这里部分浏览器实现不一致,而且IE没有这个方法*/ + xhr.onprogress = xhr.upload.onprogress = progress => { + this.uploading(progress, mill) + } + + /*不带文件上传,给秒传使用*/ + this._withoutFileUpload ? xhr.send(null) : xhr.send(formData) + + /*保存xhr id*/ + xhrList.push(xhr) + const cID = xhrList.length - 1 + currentXHRID = cID + + /*有响应abort的情况*/ + xhr.onabort = () => this.onabort(mill, cID) + + /*trigger执行上传的用户回调*/ + this.doUpload(this.files, mill, currentXHRID) + + /*清除input的值*/ + this.refs['ajax_upload_file_input'].value = '' + } + + /*组装自定义添加到FormData的对象*/ + appendFieldsToFormData(formData){ + const field = typeof this.paramAddToField == 'function' ? this.paramAddToField() : this.paramAddToField + field && + Object.keys(field).map(index=> + formData.append(index, field[index]) + ) + return formData + } + + /*iE选择前验证*/ + /*触发beforeChoose*/ + IEBeforeChoose(e) { + const jud = this.beforeChoose() + jud != true && jud != undefined && e.preventDefault() + } + /*IE需要用户真实点击上传按钮,所以使用透明按钮*/ + /*触发chooseFile*/ + IEChooseFile(e) { + this.fileName = e.target.value.substring(e.target.value.lastIndexOf('\\') + 1) + this.chooseFile(this.fileName) + /*先执行IEUpload,配置好action等参数,然后submit*/ + this.chooseAndUpload && (this.IEUpload() !== false) && + document.getElementById(`ajax_upload_file_form_${this.IETag}${currentIEID}`).submit() + e.target.blur() + } + /*IE处理上传函数*/ + /*触发beforeUpload doUpload*/ + IEUpload(e) { + const mill = (new Date).getTime() + const jud = this.beforeUpload(this.fileName, mill) + if(!this.fileName || (jud != true && jud != undefined) ) { + e && e.preventDefault() + return false + } + const that = this + + /*url参数*/ + const baseUrl = this.baseUrl + + const param = typeof this.param === 'function' ? this.param(this.fileName) : this.param + let paramStr = '' + + if (param) { + const paramArr = [] + param['_'] = mill + param['ie'] === undefined && (param['ie'] = 'true') + for (const key in param) { + if(param[key] != undefined) paramArr.push(`${key}=${param[key]}`) + } + paramStr = '?' + paramArr.join('&') + } + const targeturl = baseUrl + paramStr + + document.getElementById(`ajax_upload_file_form_${this.IETag}${currentIEID}`).setAttribute('action', targeturl) + /*IE假的上传进度*/ + const getFakeProgress = this.fakeProgress() + let loaded = 0, + count = 0 + + const progressInterval = setInterval(() => { + loaded = getFakeProgress(loaded) + this.uploading({ + loaded, + total: 100 + },mill) + /*防止永久执行,设定最大的次数。暂时为30秒(200*150)*/ + ++count >= 150 && clearInterval(progressInterval) + },200) + + + /*当前上传id*/ + const partIEID = currentIEID + /*回调函数*/ + window.attachEvent ? + document.getElementById(`ajax_upload_file_frame_${this.IETag}${partIEID}`).attachEvent('onload', handleOnLoad) : + document.getElementById(`ajax_upload_file_frame_${this.IETag}${partIEID}`).addEventListener('load', handleOnLoad) + + + function handleOnLoad() { + /*clear progress interval*/ + clearInterval(progressInterval) + try { + that.uploadSuccess(that.IECallback(that.dataType, partIEID)) + } catch (e) { + that.uploadError(e) + } finally { + /*清除输入框的值*/ + const oInput = document.getElementById(`ajax_upload_hidden_input_${that.IETag}${partIEID}`) + oInput.outerHTML = oInput.outerHTML + } + } + this.doUpload(this.fileName, mill) + /*置为非空闲*/ + IEFormGroup[currentIEID] = false + + } + /*IE回调函数*/ + //TODO 处理Timeout + IECallback(dataType, frameId) { + /*回复空闲状态*/ + IEFormGroup[frameId] = true + + const frame = document.getElementById(`ajax_upload_file_frame_${this.IETag}${frameId}`) + const resp = {} + const content = frame.contentWindow ? frame.contentWindow.document.body : frame.contentDocument.document.body + if(!content) throw new Error('Your browser does not support async upload') + try { + resp.responseText = content.innerHTML || 'null innerHTML' + resp.json = JSON ? JSON.parse(resp.responseText) : eval(`(${resp.responseText})`) + } catch (e) { + /*如果是包含了
*/
+            if (e.message && e.message.indexOf('Unexpected token') >= 0) {
+                /*包含返回的json*/
+                if (resp.responseText.indexOf('{') >= 0) {
+                    const msg = resp.responseText.substring(resp.responseText.indexOf('{'), resp.responseText.lastIndexOf('}') + 1)
+                    return JSON ? JSON.parse(msg) : eval(`(${msg})`)
+                }
+                return {type: 'FINISHERROR', message: e.message}
+            }
+            throw e
+        }
+        return dataType == 'json' ? resp.json : resp.responseText
+    }
+
+    /*外部调用方法,主动触发选择文件(等同于调用btn.click()), 仅支持现代浏览器*/
+    forwardChoose() {
+        if(this.isIE) return false
+        this.commonChooseFile()
+    }
+
+    /**
+     * 外部调用方法,当多文件上传时,用这个方法主动删除列表中某个文件
+     * TODO: 此方法应为可以任意操作文件数组
+     * @param func 用户调用时传入的函数,函数接收参数files(filesAPI 对象)
+     * @return Obj File API 对象
+     * File API Obj:
+     * {
+     *   0 : file,
+     *   1 : file,
+     *   length : 2
+     * }
+     */
+    fowardRemoveFile(func) {
+        this.files = func(this.files)
+    }
+
+    /*外部调用方法,传入files(File API)对象可以立刻执行上传动作,IE不支持。调用随后会触发beforeUpload*/
+    filesToUpload(files) {
+        if(this.isIE) return
+        this.files = files
+        this.commonUpload()
+    }
+
+    /*外部调用方法,取消一个正在进行的xhr,传入id指定xhr(doupload时返回)或者默认取消最近一个。*/
+    abort(id) {
+        id === undefined ? 
+            xhrList[currentXHRID].abort() :
+            xhrList[id].abort()
+    }
+
+    /*判断ie版本*/
+    checkIE() {
+        const userAgent = this.userAgent;
+        const version = userAgent.indexOf('MSIE')
+        if (version < 0) return -1
+
+        return parseFloat(userAgent.substring(version + 5, userAgent.indexOf(';', version)))
+    }
+
+    /*生成假的IE上传进度*/
+    fakeProgress() {
+        let add = 6
+        const decrease = 0.3,
+          end = 98,
+          min = 0.2
+        return (lastTime) => {
+            let start = lastTime
+            if (start >= end) return start
+
+            start += add
+            add = add - decrease
+            add < min && (add = min)
+
+            return start
+        }
+    }
+
+    getUserAgent() {
+        const userAgentString = this.props.options && this.props.options.userAgent;
+        const navigatorIsAvailable = typeof navigator !== 'undefined';        
+        if (!navigatorIsAvailable && !userAgentString) {
+            throw new Error('\`options.userAgent\` must be set rendering react-fileuploader in situations when \`navigator\` is not defined in the global namespace. (on the server, for example)');
+        }
+        return navigatorIsAvailable ? navigator.userAgent : userAgentString;
+    }
+
+
+
+    componentWillMount() {
+        this.userAgent = this.getUserAgent();
+        this.isIE = !(this.checkIE() < 0 || this.checkIE() >= 10)
+        /*因为IE每次要用到很多form组,如果在同一页面需要用到多个可以在options传入tag作为区分。并且不随后续props改变而改变*/
+        const tag = this.props.options && this.props.options.tag
+        this.IETag = tag ? tag+'_' : ''
+
+        this._updateProps(this.props)
+    }
+
+    componentDidMount() {
+    }
+
+    componentWillReceiveProps(newProps) {
+        this._updateProps(newProps)
+    }
+
+    render() {
+        return this._packRender()
+    }
+
+
+    /*打包render函数*/
+    _packRender() {
+        /*IE用iframe表单上传,其他用ajax Formdata*/
+        let render = ''
+        if (this.isIE) {
+            render = this._multiIEForm()
+        } else {
+            const restAttrs = {
+                accept: this.accept,
+                multiple: this.multiple
+            }
+
+            render = (
+                
+ {this.state.before} +
this.commonChooseFile(e)} + style={{overflow:'hidden',postion:'relative',display:this.wrapperDisplay}} + > + {this.state.chooseBtn} +
+ {this.state.middle} + +
this.commonUpload(e)} + style={{ + overflow: 'hidden', + postion: 'relative', + display: this.chooseAndUpload ? 'none' : this.wrapperDisplay + }} + > + {this.state.uploadBtn} +
+ {this.state.after} + this.commonChange(e)} + {...restAttrs} + /> +
+ ) + } + return render + } + + /*IE多文件同时上传,需要多个表单+多个form组合。根据currentIEID代表有多少个form。*/ + /*所有不在空闲(正在上传)的上传组都以display:none的形式插入,第一个空闲的上传组会display:block捕捉。*/ + _multiIEForm() { + const formArr = [] + let hasFree = false + + /* IE情况下,由于上传按钮被隐藏的input覆盖,不能进行disabled按钮处理。 + * 所以当disabledIEChoose为true(或者func返回值为true)时,禁止IE上传。 + */ + const isDisabled = + typeof this.disabledIEChoose === 'function' ? this.disabledIEChoose() : this.disabledIEChoose + + /*这里IEFormGroup的长度会变,所以不能存len*/ + for(let i = 0; i + {formArr} + + ) + + function _insertIEForm(formArr,i) { + /*如果已经push了空闲组而当前也是空闲组*/ + if(IEFormGroup[i] && hasFree) return + /*是否display*/ + const isShow = IEFormGroup[i] + /*Input内联样式*/ + const style = { + position:'absolute', + left:'-30px', + top:0, + zIndex:'50', + fontSize:'80px', + width:'200px', + opacity:0, + filter:'alpha(opacity=0)' + } + + /*是否限制了文件后缀,以及是否disabled*/ + const restAttrs = { + accept: this.accept, + disabled: isDisabled + } + + const input = + this.IEChooseFile(e)} onClick={(e)=>this.IEBeforeChoose(e)} + style={style} {...restAttrs} + /> + + i = `${this.IETag}${i}` + formArr.push(( +
this.IEUpload(e)} + style={{display:isShow? 'block':'none'}} + > + {this.state.before} +
+ {this.state.chooseBtn} + {/*input file 的name不能省略*/} + {input} +
+ {this.state.middle} +
+ {this.state.uploadBtn} + +
+ {this.state.after} +
+ )) + formArr.push(( + + )) + } + } + +} + +FileUpload.propTypes ={ + options: PT.shape({ + /*basics*/ + baseUrl: PT.string.isRequired, + param: PT.oneOfType([PT.object, PT.func]), + dataType: PT.string, + chooseAndUpload: PT.bool, + paramAddToField: PT.oneOfType([PT.object, PT.func]), + wrapperDisplay: PT.string, + timeout: PT.number, + accept: PT.string, + multiple: PT.bool, + numberLimit: PT.oneOfType([PT.number, PT.func]), + fileFieldName: PT.oneOfType([PT.string, PT.func]), + withCredentials: PT.bool, + requestHeaders: PT.object, + /*specials*/ + tag: PT.string, + userAgent: PT.string, + disabledIEChoose: PT.oneOfType([PT.bool, PT.func]), + _withoutFileUpload: PT.bool, + filesToUpload: PT.arrayOf(PT.object), + textBeforeFiles: PT.bool, + /*funcs*/ + beforeChoose: PT.func, + chooseFile: PT.func, + beforeUpload: PT.func, + doUpload: PT.func, + uploading: PT.func, + uploadSuccess: PT.func, + uploadError: PT.func, + uploadFail: PT.func, + onabort: PT.func + }).isRequired, + style: PT.object, + className: PT.string +}; +module.exports = FileUpload diff --git a/src/util/mm.js b/src/util/mm.js new file mode 100644 index 0000000000000000000000000000000000000000..868410bfb19c10e6514e3c4a7998747606c02572 --- /dev/null +++ b/src/util/mm.js @@ -0,0 +1,138 @@ +// 这块 是通用工具 这里我们把它 建成 类 + +// 类 和 对象 的区别是 +// 类可以做隔离的作用域 + +// 假如这块用对象的话 几个模块 同时使用这一个模块 +// 如果有一个模块把对象里的变量改了 其他模块就都会受影响 +// 使用 类 有 单独的作用域 每个模块的使用 重新的New 一下就好了、 + +class MUtil{ + + // 先写个 通用的请求接口的方法 + request(param){ + return new Promise((resolve,reject)=>{ + $.ajax({ + // 1 请求格式 自定义的请求格式 param.type 没有的话 就是get 默认的请求 + type: param.type || 'get', + // 2 请求地址 + url: param.url || '', + // 指定我们 传输的类型 + dataType: param.dataType || 'json', + // 这是我要传给后端的数据 + data: param.data || null, + + + //后面的是对数据传输后的回掉的处理 + success: res => { + // console.log("123"); + // console.log(res); + // console.log(param); + // console.log(param.data); + // 数据请求成功 + if(0 ===res.status){ + // console.log("8" + res); + // 这是Promise函数里的then的resolve方法 + // then函数是在login页面里 + // onSubmit()这个方法调用MUtil里的request这个方法 + // 而request里又有 Promise这个处理异步回调函数的方法 + // so 这个resolve方法将会在login页面里执行 + // 这里的参数就会传到login页面里执行 so login页面的res 是ren.data + // resolve(res.data,res.msg) 这句就是开始传输数据了 + typeof resolve === 'function' && resolve(res.data,res.msg); + // 这里用来 typeof 是用来判断resolve 是不是function + } + // 没有登录强制登录 + else if(10 ===res.status){ + this.doLogin(); + + } + else{ + typeof reject === 'function' && reject(res.msg || res.data); + } + }, + error:err=> { + typeof reject === 'function' && reject(err.statusText); + } + }); + }); + + + + // 在请求的时候 可以引用一些组件 jquery 的 Ajax 的请求 也可以的 + //在 后面会引用Ajax的插件 所以一定会引用jquery的 + // $.ajax({ + // // 1 请求格式 自定义的请求格式 param.type 没有的话 就是get 默认的请求 + // type : parm.type ||'get', + // // 2 请求地址 + // url : param.url || '', + // // 指定我们 传输的类型 + // dataType : param.dataType || 'json', + // // 这是我要传给后端的数据 + // data : param.data || null, + // //后面的是对数据传输后的回掉的处理 + // success(res){ + + // }, + // error(err){ + + // } + // }); + + } + // 跳转登录 + doLogin(){ + window.location.href= '/login?redirect='+ encodeURIComponent(window.location.pathname); + } + // 获取url参数的 + getUrlParam(name){ + //xxx.com?param=123¶m1=456 + let queryString = window.location.search.split('?')[1]||'', + reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"), + result = queryString.match(reg) ; + // result :['param=123','','123','&'] + // so 在result里我们想取到name对应的值 就要取下标为2的元素 + return result ? decodeURIComponent(result[2]) : null + } + // 成功提示 + successTips(successMsg){ + alert(successMsg || "请求成功~~~"); + } + // 错误信息 + errorTips(errMsg){ + alert(errMsg || "好像哪里不对~~~"); + } + // 本地存储 + setStorage(name,data){ + let dataType = typeof data; + if (dataType==='object'){ + // 这里给object 类型的json数据进行JSON的序列化处理 这样就保存为json字符串 + window.localStorage.setItem(name,JSON.stringify(data)); + } + // 如果是基础类型的是数据 + else if(['number','string','boolean'].indexOf('dataType') >= 0){ + // 基础类型的数据 就不用JSON序列化了 直接可以进行存储 + window.localStorage.setItem(name,data); + }else{ + alert('该类型 不能用于本地存储'); + } + } + // 本地数据 提取 + getStorage(name){ + let data =window.localStorage.getItem(name); + if(data){ + // parse的用法 + return JSON.parse(data); + } + else{ + return ''; + } + } + // 删除 本地数据 + removeStorage(name){ + window.localStorage.removeItem(name); + } + +} + +export default MUtil \ No newline at end of file diff --git a/src/util/pageination/index.js b/src/util/pageination/index.js new file mode 100644 index 0000000000000000000000000000000000000000..acf3a71ca8a0ea4aed0bfd390277f7174f6b6cea --- /dev/null +++ b/src/util/pageination/index.js @@ -0,0 +1,27 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import Pagination from 'rc-pagination'; +import 'rc-pagination/assets/index.css'; + + +class Pageination extends React.Component { + constructor(props){ + super(props); + } + render() { + return ( +
+
+ console.log(pageNum)} + /> +
+
+ ) + } +} + +export default Pageination \ No newline at end of file diff --git a/src/util/rich-editor/rich-editor.js b/src/util/rich-editor/rich-editor.js new file mode 100644 index 0000000000000000000000000000000000000000..e2dd26fe06eb417174cf06b92e492f5bd2f0f802 --- /dev/null +++ b/src/util/rich-editor/rich-editor.js @@ -0,0 +1,43 @@ +import React from 'react'; +import Simditor from 'simditor'; +import 'simditor/styles/simditor.scss'; + + +class RichEditor extends React.Component { + constructor(props){ + super(props); + } + componentDidMount(){ + this.loadEditor(); + } + loadEditor(){ + let elemnet = this.refs['textarea']; + this.simditor = new Simditor({ + textarea: $(elemnet), + defaultValue:this.props.placeholder||'请输入内容', + upload :{ + url:'/manage/product/richtext_img_upload.do', + defauleImage:'', + fileKey:'upload_file' + } + }) + // 初始化事件 + this.bindEditorEvent(); + } + // 初始化文本编辑器的事件 + bindEditorEvent(){ + this.simditor.on('valuechanged',e=>{ + // 这里用getvalue()方法 取出这个值传给父组件 + this.props.onValueChange(this.simditor.getValue()); + }) + } + render() { + return ( +
+ +
+ ) + } +} + +export default RichEditor \ No newline at end of file diff --git a/src/util/table-list/index.js b/src/util/table-list/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5d5187b530363f57039b6ddd0f4e44dde7c69029 --- /dev/null +++ b/src/util/table-list/index.js @@ -0,0 +1,66 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import Pagination from 'rc-pagination'; +import 'rc-pagination/assets/index.css'; + +// 通用的table-list +class Tablist extends React.Component { + constructor(props) { + super(props); + this.state={ + isFirstLoading:true + } + } + // 列表只有在第一次加载的时候 isFirstLoading为true 其他都为 fales + componentWillReceiveProps(){ + this.setState({ + isFirstLoading:false + }) + } + render() { + // 表头信息 + let tableHeader = this.props.tableHeads.map( + (tableHead,index)=>{ + if(typeof tableHead ==='object'){ + return {tableHead.name} + }else + if(typeof tableHead ==='string'){ + return {tableHead} + } + } + ); + // 列表内容 + let listBody = this.props.children; + + // 列表的信息 + let Listinfo = ( + + + {this.state.isFirstLoading ? '正在加载数据~~~~' : '没有找到相应的结果'} + + + ); + let tableBody = listBody.length > 0 ? listBody : Listinfo; + return ( +
+
+ + + + {tableHeader} + + + + {/* so 这里的state里有list的参数 */} + {listBody} + + +
+
+
+ ) + } +} + +export default Tablist \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index eb0d1448f7419738d7cd1216891d105e04d47093..6b1edd56521fd2bb27a4f7bce0d71f2872c518c5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,15 +6,31 @@ const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); +// process.env 这是在我们执行scripts里的程序时要传入一个环境的变量 + +let WEBPACK_ENV = process.env.WEBPACK_ENV || 'dev'; +console.log(WEBPACK_ENV); module.exports = { - entry: './src/app.js', //项目的入口文件路径 - output: { // 这里是输出文件 + entry: './src/app.js', //项目的入口文件路径 +// entry: './src/redux/index.js', + output: { // 这里是输出文件 path: path.resolve(__dirname, 'dist'), - publicPath:'/dist/', //配置访问文件的路径 + publicPath: WEBPACK_ENV ==='dev'? '/dist/': '/dist/123', //配置访问文件的路径 filename: 'js/app.js' }, + devtool: 'inline-source-map', + + resolve:{ + alias:{ + page : path.resolve(__dirname, 'src/page'), + component: path.resolve(__dirname, 'src/component'), + util : path.resolve(__dirname, 'src/util'), + servers : path.resolve(__dirname, 'src/servers') + + } + }, module: { rules: [ // react(jsx) 语法的处理 @@ -78,7 +94,8 @@ module.exports = { plugins: [ //这是plugins引入文件 // 这是处理HTML文件 new HtmlWebpackPlugin({ - template: './src/index.html' // 新建了一个模板 和模板的位置 + template: './src/index.html' ,// 新建了一个模板 和模板的位置 + favicon : './favicon.ico' //处理 网站title旁边的图片的 }), // 独立处理css文件 new ExtractTextPlugin("css/[name].css"), @@ -90,6 +107,19 @@ module.exports = { }) ], devServer: { - port:8086 //修改启动的端口号 + port:8086 , //修改启动的端口号 + historyApiFallback:{ + index:'/dist/index.html' + }, + proxy : { + '/manage' :{ + target: 'http://admintest.happymmall.com', + changeOrigin : true + }, + '/user/logout.do': { + target: 'http://admintest.happymmall.com', + changeOrigin: true + } + } } }; diff --git a/yarn.lock b/yarn.lock index 1643960eca830abcd9b3b50c75415b95eb6881dc..4be63941868cc9797608ee3020c151478438e68d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -768,7 +768,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@6.x, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -2167,10 +2167,6 @@ follow-redirects@^1.0.0: dependencies: debug "^3.1.0" -font-awesome@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" - for-in@^0.1.3: version "0.1.8" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" @@ -2475,6 +2471,16 @@ he@1.1.x: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" +history@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" + dependencies: + invariant "^2.2.1" + loose-envify "^1.2.0" + resolve-pathname "^2.2.0" + value-equal "^0.4.0" + warning "^3.0.0" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -2487,6 +2493,10 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hoist-non-react-statics@^2.5.0: + version "2.5.5" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -2694,7 +2704,7 @@ interpret@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" -invariant@^2.2.2: +invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" dependencies: @@ -2965,6 +2975,14 @@ isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" +jquery@2.x: + version "2.2.4" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.4.tgz#2c89d6889b5eac522a7eea32c14521559c6cbf02" + +jquery@~2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.1.4.tgz#228bde698a0c61431dc2630a6a154f15890d2317" + js-base64@^2.1.8, js-base64@^2.1.9: version "2.4.8" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.8.tgz#57a9b130888f956834aa40c5b165ba59c758f033" @@ -3174,7 +3192,7 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" dependencies: @@ -3835,6 +3853,12 @@ path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -4181,7 +4205,15 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.6.0: +prop-types@15.6.0: + version "15.6.0" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.3.1" + object-assign "^4.1.1" + +prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.6.0, prop-types@^15.6.1: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" dependencies: @@ -4290,6 +4322,13 @@ raw-body@2.3.2: iconv-lite "0.4.19" unpipe "1.0.0" +rc-pagination@^1.16.5: + version "1.17.1" + resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-1.17.1.tgz#76cfaa64dc8b301816a118a0aca8cde46bbbb32b" + dependencies: + babel-runtime "6.x" + prop-types "^15.5.7" + rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -4308,6 +4347,29 @@ react-dom@16.2.0: object-assign "^4.1.1" prop-types "^15.6.0" +react-router-dom@4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d" + dependencies: + history "^4.7.2" + invariant "^2.2.2" + loose-envify "^1.3.1" + prop-types "^15.5.4" + react-router "^4.2.0" + warning "^3.0.0" + +react-router@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" + dependencies: + history "^4.7.2" + hoist-non-react-statics "^2.5.0" + invariant "^2.2.4" + loose-envify "^1.3.1" + path-to-regexp "^1.7.0" + prop-types "^15.6.1" + warning "^4.0.1" + react@16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" @@ -4559,6 +4621,10 @@ resolve-from@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" +resolve-pathname@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -4765,6 +4831,35 @@ signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +simditor@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/simditor/-/simditor-2.3.6.tgz#ba71edc3161fc438466c336c9c10052743403947" + dependencies: + jquery "~2.1.4" + simple-hotkeys "~1.0.3" + simple-module "~2.0.6" + simple-uploader "~2.0.7" + +simple-hotkeys@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/simple-hotkeys/-/simple-hotkeys-1.0.3.tgz#47a5bc35362ca7339611bd09a84fa37818b0bbc6" + dependencies: + jquery "2.x" + simple-module "~2.0.5" + +simple-module@~2.0.5, simple-module@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/simple-module/-/simple-module-2.0.6.tgz#cec8250325f8c7f575f4aa75c919b38396a6037b" + dependencies: + jquery "2.x" + +simple-uploader@~2.0.7: + version "2.0.8" + resolved "https://registry.yarnpkg.com/simple-uploader/-/simple-uploader-2.0.8.tgz#5fc4106c6d968b0b353293e3b16094aafde957ba" + dependencies: + jquery "2.x" + simple-module "~2.0.5" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -5336,6 +5431,10 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +value-equal@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -5358,6 +5457,18 @@ vm-browserify@0.0.4: dependencies: indexof "0.0.1" +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + dependencies: + loose-envify "^1.0.0" + +warning@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.1.tgz#66ce376b7fbfe8a887c22bdf0e7349d73d397745" + dependencies: + loose-envify "^1.0.0" + watchpack@^1.4.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"