Merge remote-tracking branch 'origin/master'
commit
b384179610
|
@ -1,136 +0,0 @@
|
|||
English | [简体中文](./README.zh-CN.md) | [Русский](./README.ru-RU.md)
|
||||
|
||||
<h1 align="center">Ant Design Pro</h1>
|
||||
|
||||
<div align="center">
|
||||
|
||||
An out-of-box UI solution for enterprise applications as a React boilerplate.
|
||||
|
||||
[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/)
|
||||
[![Build Status](https://dev.azure.com/ant-design/ant-design-pro/_apis/build/status/ant-design.ant-design-pro?branchName=master)](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master)
|
||||
[![Dependencies](https://img.shields.io/david/ant-design/ant-design-pro.svg)](https://david-dm.org/ant-design/ant-design-pro)
|
||||
[![DevDependencies](https://img.shields.io/david/dev/ant-design/ant-design-pro.svg)](https://david-dm.org/ant-design/ant-design-pro?type=dev)
|
||||
[![Gitter](https://img.shields.io/gitter/room/ant-design/pro-english.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjEyMzUiIGhlaWdodD0iNjUwIiB2aWV3Qm94PSIwIDAgNzQxMCAzOTAwIj4NCjxyZWN0IHdpZHRoPSI3NDEwIiBoZWlnaHQ9IjM5MDAiIGZpbGw9IiNiMjIyMzQiLz4NCjxwYXRoIGQ9Ik0wLDQ1MEg3NDEwbTAsNjAwSDBtMCw2MDBINzQxMG0wLDYwMEgwbTAsNjAwSDc0MTBtMCw2MDBIMCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjMwMCIvPg0KPHJlY3Qgd2lkdGg9IjI5NjQiIGhlaWdodD0iMjEwMCIgZmlsbD0iIzNjM2I2ZSIvPg0KPGcgZmlsbD0iI2ZmZiI%2BDQo8ZyBpZD0iczE4Ij4NCjxnIGlkPSJzOSI%2BDQo8ZyBpZD0iczUiPg0KPGcgaWQ9InM0Ij4NCjxwYXRoIGlkPSJzIiBkPSJNMjQ3LDkwIDMxNy41MzQyMzAsMzA3LjA4MjAzOSAxMzIuODczMjE4LDE3Mi45MTc5NjFIMzYxLjEyNjc4MkwxNzYuNDY1NzcwLDMwNy4wODIwMzl6Ii8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB5PSI0MjAiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHk9Ijg0MCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgeT0iMTI2MCIvPg0KPC9nPg0KPHVzZSB4bGluazpocmVmPSIjcyIgeT0iMTY4MCIvPg0KPC9nPg0KPHVzZSB4bGluazpocmVmPSIjczQiIHg9IjI0NyIgeT0iMjEwIi8%2BDQo8L2c%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzOSIgeD0iNDk0Ii8%2BDQo8L2c%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzMTgiIHg9Ijk4OCIvPg0KPHVzZSB4bGluazpocmVmPSIjczkiIHg9IjE5NzYiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3M1IiB4PSIyNDcwIi8%2BDQo8L2c%2BDQo8L3N2Zz4%3D)](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[![Join the chat at https://gitter.im/ant-design/ant-design-pro](https://img.shields.io/gitter/room/ant-design/ant-design-pro.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjkwMCIgaGVpZ2h0PSI2MDAiIHZpZXdCb3g9IjAgMCAzMCAyMCI%2BDQo8ZGVmcz4NCjxwYXRoIGlkPSJzIiBkPSJNMCwtMSAwLjU4Nzc4NSwwLjgwOTAxNyAtMC45NTEwNTcsLTAuMzA5MDE3SDAuOTUxMDU3TC0wLjU4Nzc4NSwwLjgwOTAxN3oiIGZpbGw9IiNmZmRlMDAiLz4NCjwvZGVmcz4NCjxyZWN0IHdpZHRoPSIzMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2RlMjkxMCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw1KSBzY2FsZSgzKSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsMikgcm90YXRlKDIzLjAzNjI0MykiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyLDQpIHJvdGF0ZSg0NS44Njk4OTgpIi8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMiw3KSByb3RhdGUoNjkuOTQ1Mzk2KSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsOSkgcm90YXRlKDIwLjY1OTgwOCkiLz4NCjwvc3ZnPg%3D%3D)](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
![](https://user-images.githubusercontent.com/8186664/44953195-581e3d80-aec4-11e8-8dcb-54b9db38ec11.png)
|
||||
|
||||
</div>
|
||||
|
||||
- Preview: http://preview.pro.ant.design
|
||||
- Home Page: http://pro.ant.design
|
||||
- Documentation: http://pro.ant.design/docs/getting-started
|
||||
- ChangeLog: http://pro.ant.design/docs/changelog
|
||||
- FAQ: http://pro.ant.design/docs/faq
|
||||
- Mirror Site in China: http://ant-design-pro.gitee.io
|
||||
|
||||
## 2.0 Released Now! 🎉🎉🎉
|
||||
[Announcing Ant Design Pro 2.0.0](https://medium.com/ant-design/beautiful-and-powerful-ant-design-pro-2-0-release-51358da5af95)
|
||||
|
||||
## Translation Recruitment :loudspeaker:
|
||||
|
||||
We need your help: https://github.com/ant-design/ant-design-pro/issues/120
|
||||
|
||||
## Features
|
||||
|
||||
- :gem: **Neat Design**: Follow [Ant Design specification](http://ant.design/)
|
||||
- :triangular_ruler: **Common Templates**: Typical templates for enterprise applications
|
||||
- :rocket: **State of The Art Development**: Newest development stack of React/umi/dva/antd
|
||||
- :iphone: **Responsive**: Designed for variable screen sizes
|
||||
- :art: **Theming**: Customizable theme with simple config
|
||||
- :globe_with_meridians: **International**: Built-in i18n solution
|
||||
- :gear: **Best Practices**: Solid workflow to make your code healthy
|
||||
- :1234: **Mock development**: Easy to use mock development solution
|
||||
- :white_check_mark: **UI Test**: Fly safely with unit and e2e tests
|
||||
|
||||
## Templates
|
||||
|
||||
```
|
||||
- Dashboard
|
||||
- Analytic
|
||||
- Monitor
|
||||
- Workspace
|
||||
- Form
|
||||
- Basic Form
|
||||
- Step Form
|
||||
- Advanced From
|
||||
- List
|
||||
- Standard Table
|
||||
- Standard List
|
||||
- Card List
|
||||
- Search List (Project/Applications/Article)
|
||||
- Profile
|
||||
- Simple Profile
|
||||
- Advanced Profile
|
||||
- Account
|
||||
- Account Center
|
||||
- Account Settings
|
||||
- Result
|
||||
- Success
|
||||
- Failed
|
||||
- Exception
|
||||
- 403
|
||||
- 404
|
||||
- 500
|
||||
- User
|
||||
- Login
|
||||
- Register
|
||||
- Register Result
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Use bash
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/ant-design/ant-design-pro.git --depth=1
|
||||
$ cd ant-design-pro
|
||||
$ npm install
|
||||
$ npm start # visit http://localhost:8000
|
||||
```
|
||||
|
||||
### Use by docker
|
||||
|
||||
```bash
|
||||
# preview
|
||||
$ docker pull antdesign/ant-design-pro
|
||||
$ docker run -p 80:80 antdesign/ant-design-pro
|
||||
# open http://localhost
|
||||
|
||||
# dev
|
||||
$ npm run docker:dev
|
||||
|
||||
# build
|
||||
$ npm run docker:build
|
||||
|
||||
|
||||
# production dev
|
||||
$ npm run docker-prod:dev
|
||||
|
||||
# production build
|
||||
$ npm run docker-prod:build
|
||||
```
|
||||
|
||||
### Use Gitpod
|
||||
|
||||
Open the project in Gitpod (free online dev environment for GitHub) and start coding immediately.
|
||||
|
||||
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/ant-design/ant-design-pro)
|
||||
|
||||
More instructions at [documentation](http://pro.ant.design/docs/getting-started).
|
||||
|
||||
## Browsers support
|
||||
|
||||
Modern browsers and IE11.
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
|
||||
| --------- | --------- | --------- | --------- | --------- |
|
||||
| IE11, Edge| last 2 versions| last 2 versions| last 2 versions| last 2 versions
|
||||
|
||||
## Contributing
|
||||
|
||||
Any type of contribution is welcome, here are some examples of how you may contribute to this project:
|
||||
|
||||
- Use Ant Design Pro in your daily work.
|
||||
- Submit [issues](http://github.com/ant-design/ant-design-pro/issues) to report bugs or ask questions.
|
||||
- Propose [pull requests](http://github.com/ant-design/ant-design-pro/pulls) to improve our code.
|
|
@ -3,3 +3,32 @@
|
|||
> 采用 antd pro 快速开发
|
||||
|
||||
> TODO
|
||||
|
||||
## 命名规范
|
||||
|
||||
#### 1.文件夹命名
|
||||
|
||||
文件夹命名全部小写,单词之间以中划线隔离 例如: node-modules
|
||||
|
||||
|
||||
#### 2.文件命名
|
||||
|
||||
文件以小写开头,以驼峰格式连接单词 例如: dashBoard.js
|
||||
component目录下的文件 以大写开头
|
||||
route目录下的文件以大写开头
|
||||
model目录下的文件大写开头
|
||||
|
||||
|
||||
#### 3.标点符号
|
||||
|
||||
对于字符串统一用单引号 例如: 'hello world'
|
||||
|
||||
#### 4.语法规范
|
||||
|
||||
JS语法规范遵守ES6规范
|
||||
http://www.tuicool.com/articles/YrQ7j2a
|
||||
|
||||
#### 5.注释
|
||||
|
||||
1.route目录下的文件需要加上文件头部注释(写清楚文件是什么功能)
|
||||
2.component文件需要加上头部注释 (写清楚改控件的用处)
|
|
@ -1,103 +0,0 @@
|
|||
[English](./README.md) | [简体中文](./README.zh-CN.md) | Русский
|
||||
|
||||
<h1 align="center">Ant Design Pro</h1>
|
||||
|
||||
<div align="center">
|
||||
|
||||
UI-решение "из коробки" для корпоративных приложений как React boilerplate
|
||||
|
||||
[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/)
|
||||
[![Build Status](https://dev.azure.com/ant-design/ant-design-pro/_apis/build/status/ant-design.ant-design-pro?branchName=master)](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master)
|
||||
[![Dependencies](https://img.shields.io/david/ant-design/ant-design-pro.svg)](https://david-dm.org/ant-design/ant-design-pro)
|
||||
[![DevDependencies](https://img.shields.io/david/dev/ant-design/ant-design-pro.svg)](https://david-dm.org/ant-design/ant-design-pro?type=dev)
|
||||
[![Gitter](https://img.shields.io/gitter/room/ant-design/pro-english.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjEyMzUiIGhlaWdodD0iNjUwIiB2aWV3Qm94PSIwIDAgNzQxMCAzOTAwIj4NCjxyZWN0IHdpZHRoPSI3NDEwIiBoZWlnaHQ9IjM5MDAiIGZpbGw9IiNiMjIyMzQiLz4NCjxwYXRoIGQ9Ik0wLDQ1MEg3NDEwbTAsNjAwSDBtMCw2MDBINzQxMG0wLDYwMEgwbTAsNjAwSDc0MTBtMCw2MDBIMCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjMwMCIvPg0KPHJlY3Qgd2lkdGg9IjI5NjQiIGhlaWdodD0iMjEwMCIgZmlsbD0iIzNjM2I2ZSIvPg0KPGcgZmlsbD0iI2ZmZiI%2BDQo8ZyBpZD0iczE4Ij4NCjxnIGlkPSJzOSI%2BDQo8ZyBpZD0iczUiPg0KPGcgaWQ9InM0Ij4NCjxwYXRoIGlkPSJzIiBkPSJNMjQ3LDkwIDMxNy41MzQyMzAsMzA3LjA4MjAzOSAxMzIuODczMjE4LDE3Mi45MTc5NjFIMzYxLjEyNjc4MkwxNzYuNDY1NzcwLDMwNy4wODIwMzl6Ii8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB5PSI0MjAiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHk9Ijg0MCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgeT0iMTI2MCIvPg0KPC9nPg0KPHVzZSB4bGluazpocmVmPSIjcyIgeT0iMTY4MCIvPg0KPC9nPg0KPHVzZSB4bGluazpocmVmPSIjczQiIHg9IjI0NyIgeT0iMjEwIi8%2BDQo8L2c%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzOSIgeD0iNDk0Ii8%2BDQo8L2c%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzMTgiIHg9Ijk4OCIvPg0KPHVzZSB4bGluazpocmVmPSIjczkiIHg9IjE5NzYiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3M1IiB4PSIyNDcwIi8%2BDQo8L2c%2BDQo8L3N2Zz4%3D)](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[![Join the chat at https://gitter.im/ant-design/ant-design-pro](https://img.shields.io/gitter/room/ant-design/ant-design-pro.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjkwMCIgaGVpZ2h0PSI2MDAiIHZpZXdCb3g9IjAgMCAzMCAyMCI%2BDQo8ZGVmcz4NCjxwYXRoIGlkPSJzIiBkPSJNMCwtMSAwLjU4Nzc4NSwwLjgwOTAxNyAtMC45NTEwNTcsLTAuMzA5MDE3SDAuOTUxMDU3TC0wLjU4Nzc4NSwwLjgwOTAxN3oiIGZpbGw9IiNmZmRlMDAiLz4NCjwvZGVmcz4NCjxyZWN0IHdpZHRoPSIzMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2RlMjkxMCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw1KSBzY2FsZSgzKSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsMikgcm90YXRlKDIzLjAzNjI0MykiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyLDQpIHJvdGF0ZSg0NS44Njk4OTgpIi8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMiw3KSByb3RhdGUoNjkuOTQ1Mzk2KSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsOSkgcm90YXRlKDIwLjY1OTgwOCkiLz4NCjwvc3ZnPg%3D%3D)](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
![](https://user-images.githubusercontent.com/8186664/44953195-581e3d80-aec4-11e8-8dcb-54b9db38ec11.png)
|
||||
|
||||
</div>
|
||||
|
||||
- Демо: http://preview.pro.ant.design
|
||||
- Домашняя страница: http://pro.ant.design
|
||||
- Документация: http://pro.ant.design/docs/getting-started
|
||||
- История изменений: http://pro.ant.design/docs/changelog
|
||||
- FAQ: http://pro.ant.design/docs/faq
|
||||
- Китайское зеркало сайта: http://ant-design-pro.gitee.io
|
||||
|
||||
## Поиск переводчиков :loudspeaker:
|
||||
|
||||
Нам нужна ваша помощь: https://github.com/ant-design/ant-design-pro/issues/120
|
||||
|
||||
## Возможности
|
||||
|
||||
- :gem: **Аккуратный дизайн**: Посмотрите [спецификацию Ant Design](http://ant.design/)
|
||||
- :triangular_ruler: **Общие шаблоны**: Стандартные шаблоны для корпоративных приложений
|
||||
- :rocket: **Разработка, как искусство**: Новейший стек технологий React/umi/dva/antd
|
||||
- :iphone: **Отзывчивая верстка**: Создан для экранов разных размеров
|
||||
- :art: **Темизация**: Возможность изменения темы с помощью конфигурации
|
||||
- :globe_with_meridians: **Мультиязычность**: Встроенное i18n решение
|
||||
- :gear: **Лучшие практики**: Надежные процессы для хорошего кода
|
||||
- :1234: **Разработка по шаблону**: Простое в использовании решение для разработки
|
||||
- :white_check_mark: **UI тесты**: Разрабатывайте безопасно с юнит и e2e тестами
|
||||
|
||||
## Шаблоны
|
||||
|
||||
```
|
||||
- Dashboard
|
||||
- Analytic
|
||||
- Monitor
|
||||
- Workspace
|
||||
- Form
|
||||
- Basic Form
|
||||
- Step Form
|
||||
- Advanced From
|
||||
- List
|
||||
- Standard Table
|
||||
- Standard List
|
||||
- Card List
|
||||
- Search List (Project/Applications/Article)
|
||||
- Profile
|
||||
- Simple Profile
|
||||
- Advanced Profile
|
||||
- Account
|
||||
- Account Center
|
||||
- Account Settings
|
||||
- Result
|
||||
- Success
|
||||
- Failed
|
||||
- Exception
|
||||
- 403
|
||||
- 404
|
||||
- 500
|
||||
- User
|
||||
- Login
|
||||
- Register
|
||||
- Register Result
|
||||
```
|
||||
|
||||
## Использование
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/ant-design/ant-design-pro.git --depth=1
|
||||
$ cd ant-design-pro
|
||||
$ npm install
|
||||
$ npm start # visit http://localhost:8000
|
||||
```
|
||||
|
||||
Больше информации в [документации](http://pro.ant.design/docs/getting-started).
|
||||
|
||||
## Совместимость
|
||||
|
||||
Современные браузеры и IE11.
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
|
||||
| --------- | --------- | --------- | --------- | --------- |
|
||||
| IE11, Edge| last 2 versions| last 2 versions| last 2 versions| last 2 versions
|
||||
|
||||
## Распространение
|
||||
|
||||
Любые варианты распространения приветствуются! Вот несколько примеров того, как вы можете помочь распространению проекта:
|
||||
|
||||
- Использовать Ant Design Pro в ежедневной работе.
|
||||
- Создавать [задачи](http://github.com/ant-design/ant-design-pro/issues) заводить баги или отвечать на вопросы.
|
||||
- Делать [pull-реквесты](http://github.com/ant-design/ant-design-pro/pulls) для совершенствования нашего кода.
|
|
@ -1,121 +0,0 @@
|
|||
[English](./README.md) | 简体中文 | [Русский](./README.ru-RU.md)
|
||||
|
||||
<h1 align="center">Ant Design Pro</h1>
|
||||
|
||||
<div align="center">
|
||||
|
||||
开箱即用的中台前端/设计解决方案。
|
||||
|
||||
[![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/)
|
||||
[![Build Status](https://dev.azure.com/ant-design/ant-design-pro/_apis/build/status/ant-design.ant-design-pro?branchName=master)](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master)
|
||||
[![Dependencies](https://img.shields.io/david/ant-design/ant-design-pro.svg)](https://david-dm.org/ant-design/ant-design-pro)
|
||||
[![DevDependencies](https://img.shields.io/david/dev/ant-design/ant-design-pro.svg)](https://david-dm.org/ant-design/ant-design-pro?type=dev)
|
||||
[![Join the chat at https://gitter.im/ant-design/ant-design-pro](https://img.shields.io/gitter/room/ant-design/ant-design-pro.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjkwMCIgaGVpZ2h0PSI2MDAiIHZpZXdCb3g9IjAgMCAzMCAyMCI%2BDQo8ZGVmcz4NCjxwYXRoIGlkPSJzIiBkPSJNMCwtMSAwLjU4Nzc4NSwwLjgwOTAxNyAtMC45NTEwNTcsLTAuMzA5MDE3SDAuOTUxMDU3TC0wLjU4Nzc4NSwwLjgwOTAxN3oiIGZpbGw9IiNmZmRlMDAiLz4NCjwvZGVmcz4NCjxyZWN0IHdpZHRoPSIzMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2RlMjkxMCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw1KSBzY2FsZSgzKSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsMikgcm90YXRlKDIzLjAzNjI0MykiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyLDQpIHJvdGF0ZSg0NS44Njk4OTgpIi8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMiw3KSByb3RhdGUoNjkuOTQ1Mzk2KSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsOSkgcm90YXRlKDIwLjY1OTgwOCkiLz4NCjwvc3ZnPg%3D%3D)](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
![](https://user-images.githubusercontent.com/8186664/44953195-581e3d80-aec4-11e8-8dcb-54b9db38ec11.png)
|
||||
|
||||
</div>
|
||||
|
||||
- 预览:http://preview.pro.ant.design
|
||||
- 首页:http://pro.ant.design/index-cn
|
||||
- 使用文档:http://pro.ant.design/docs/getting-started-cn
|
||||
- 更新日志: http://pro.ant.design/docs/changelog-cn
|
||||
- 常见问题:http://pro.ant.design/docs/faq-cn
|
||||
- 国内镜像:http://ant-design-pro.gitee.io
|
||||
|
||||
## 特性
|
||||
|
||||
- :gem: **优雅美观**:基于 Ant Design 体系精心设计
|
||||
- :triangular_ruler: **常见设计模式**:提炼自中后台应用的典型页面和场景
|
||||
- :rocket: **最新技术栈**:使用 React/umi/dva/antd 等前端前沿技术开发
|
||||
- :iphone: **响应式**:针对不同屏幕大小设计
|
||||
- :art: **主题**:可配置的主题满足多样化的品牌诉求
|
||||
- :globe_with_meridians: **国际化**:内建业界通用的国际化方案
|
||||
- :gear: **最佳实践**:良好的工程实践助您持续产出高质量代码
|
||||
- :1234: **Mock 数据**:实用的本地数据调试方案
|
||||
- :white_check_mark: **UI 测试**:自动化测试保障前端产品质量
|
||||
|
||||
## 模板
|
||||
|
||||
```
|
||||
- Dashboard
|
||||
- 分析页
|
||||
- 监控页
|
||||
- 工作台
|
||||
- 表单页
|
||||
- 基础表单页
|
||||
- 分步表单页
|
||||
- 高级表单页
|
||||
- 列表页
|
||||
- 查询表格
|
||||
- 标准列表
|
||||
- 卡片列表
|
||||
- 搜索列表(项目/应用/文章)
|
||||
- 详情页
|
||||
- 基础详情页
|
||||
- 高级详情页
|
||||
- 用户
|
||||
- 用户中心页
|
||||
- 用户设置页
|
||||
- 结果
|
||||
- 成功页
|
||||
- 失败页
|
||||
- 异常
|
||||
- 403 无权限
|
||||
- 404 找不到
|
||||
- 500 服务器出错
|
||||
- 帐户
|
||||
- 登录
|
||||
- 注册
|
||||
- 注册成功
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
### 使用命令行
|
||||
```bash
|
||||
$ git clone https://github.com/ant-design/ant-design-pro.git --depth=1
|
||||
$ cd ant-design-pro
|
||||
$ npm install
|
||||
$ npm start # 访问 http://localhost:8000
|
||||
```
|
||||
|
||||
### 使用 docker
|
||||
|
||||
```bash
|
||||
# preview
|
||||
$ docker pull antdesign/ant-design-pro
|
||||
$ docker run -p 80:80 antdesign/ant-design-pro
|
||||
# open http://localhost
|
||||
|
||||
# dev
|
||||
$ npm run docker:dev
|
||||
|
||||
# build
|
||||
$ npm run docker:build
|
||||
|
||||
|
||||
# production dev
|
||||
$ npm run docker-prod:dev
|
||||
|
||||
// production build
|
||||
$ npm run docker-prod:build
|
||||
```
|
||||
|
||||
更多信息请参考 [使用文档](http://pro.ant.design/docs/getting-started)。
|
||||
|
||||
## 支持环境
|
||||
|
||||
现代浏览器及 IE11。
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
|
||||
| --------- | --------- | --------- | --------- | --------- |
|
||||
| IE11, Edge| last 2 versions| last 2 versions| last 2 versions| last 2 versions
|
||||
|
||||
## 参与贡献
|
||||
|
||||
我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :smiley::
|
||||
|
||||
- 在你的公司或个人项目中使用 Ant Design Pro。
|
||||
- 通过 [Issue](http://github.com/ant-design/ant-design-pro/issues) 报告 bug 或进行咨询。
|
||||
- 提交 [Pull Request](http://github.com/ant-design/ant-design-pro/pulls) 改进 Pro 的代码。
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
// 校验必须是英文或者数字
|
||||
export function checkTypeWithEnglishAndNumbers (rule, value, callback, text) {
|
||||
let char = /^[a-zA-Z0-9]+$/
|
||||
if (char.test(value)) {
|
||||
callback()
|
||||
} else {
|
||||
callback(text)
|
||||
}
|
||||
}
|
|
@ -49,10 +49,10 @@ function getDictionaryTree(req, res) {
|
|||
}
|
||||
|
||||
export default {
|
||||
'GET /admin-api/admins/admin/menu_resource_tree': getAdminMenu,
|
||||
'GET /admin-api/admins/admin/menu_resource_tree': getAdminMenuAll,
|
||||
'GET /admin-api/admins/admin/url_resource_list': getAdminUrls,
|
||||
'GET /admin-api/admins/resource/tree': getResourceTree,
|
||||
'GET /admin-api/admins/role/page': getQueryRole,
|
||||
'GET /admin-api/admins/admin/page': getQueryRole,
|
||||
// 'GET /admin-api/admins/admin/page': getQueryRole,
|
||||
'GET /admin-api/admins/data_dict/tree': getDictionaryTree,
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ export default class DictionaryText extends PureComponent {
|
|||
componentDidMount() {}
|
||||
|
||||
render() {
|
||||
debugger;
|
||||
const { dicKey, dicValue } = this.props;
|
||||
return (
|
||||
<DictionaryContext.Consumer>
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
import React, {PureComponent} from "react";
|
||||
import {Select} from "antd";
|
||||
|
||||
const Option = Select.Option;
|
||||
|
||||
export default class ProductAttrSelectFormItem extends PureComponent {
|
||||
|
||||
handleSelectAttr = (value, option) => {
|
||||
// console.log(value);
|
||||
// console.log(option);
|
||||
// debugger;
|
||||
const { dispatch, index } = this.props;
|
||||
// let attrIndex = option.key.substring(option.key.indexOf('option-attr-') + 'option-attr-'.length, option.key.lastIndexOf('-'));
|
||||
// console.log('attrIndex: ' + attrIndex);
|
||||
// debugger;
|
||||
dispatch({
|
||||
type: 'productSpuAddOrUpdate/selectAttr',
|
||||
payload: {
|
||||
attrIndex: index,
|
||||
attr: {
|
||||
id: option.props.value,
|
||||
name: option.props.children,
|
||||
values: []
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleSelectAttrValue = (values, options) => {
|
||||
let attrValues = [];
|
||||
const { dispatch, index } = this.props;
|
||||
// debugger;
|
||||
// console.log('x' + this.children[0]);
|
||||
// let firstOption = this.children[0];
|
||||
// let attrIndex = firstOption.key.substring(firstOption.key.indexOf('option-attr-value-') + 'option-attr-value-'.length, firstOption.key.lastIndexOf('-'));
|
||||
for (let i in options) {
|
||||
let option = options[i];
|
||||
attrValues.push({
|
||||
id: parseInt(option.props.value),
|
||||
name: option.props.children,
|
||||
});
|
||||
}
|
||||
dispatch({
|
||||
type: 'productSpuAddOrUpdate/selectAttrValues',
|
||||
payload: {
|
||||
attrIndex: index,
|
||||
attrValues: attrValues,
|
||||
},
|
||||
});
|
||||
// debugger;
|
||||
|
||||
// console.log(value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {attr, allAttrTree, selectedAttrIds, index} = this.props;
|
||||
// console.log('i: ' + i);
|
||||
// 1. 规格
|
||||
let attrOptions = [];
|
||||
// allAttrTree.unshift(attr);
|
||||
// debugger;
|
||||
for (let j in allAttrTree) {
|
||||
let allAttr = allAttrTree[j];
|
||||
if (selectedAttrIds.has(allAttr.id) && allAttr.id !== attr.id) {
|
||||
continue;
|
||||
}
|
||||
attrOptions.push(<Option key={`option-attr-${index}-${allAttr.id}`} value={allAttr.id}>{allAttr.name}</Option>);
|
||||
}
|
||||
// 2. 规格值
|
||||
let attrValueOptions = [];
|
||||
// debugger;
|
||||
if (attr.id) {
|
||||
// 2.1 先找到规格值的数组
|
||||
let attrValues = [];
|
||||
for (let j in allAttrTree) {
|
||||
let allAttr = allAttrTree[j];
|
||||
if (attr.id === allAttr.id) {
|
||||
attrValues = allAttr.values;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 2.2 生成规格值的 HTML
|
||||
for (let j in attrValues) {
|
||||
let attrValue = attrValues[j];
|
||||
attrValueOptions.push(<Option key={`option-attr-value-${index}-${attrValue.id}`}
|
||||
value={attrValue.id + ''}>{attrValue.name}</Option>); // + '' 的原因是,多选必须是字符串
|
||||
}
|
||||
}
|
||||
// 3. 拼装最终,添加到 attrTreeHTML 中
|
||||
return <div key={`div-attr-${index}`}>
|
||||
<Select key={`select-attr-${index}`} style={{width: 120}} placeholder='请选择规格' onChange={this.handleSelectAttr}>
|
||||
{attrOptions}
|
||||
</Select>
|
||||
<Select key={`select-attr-value-${index}`} mode={"tags"} style={{width: 260}} placeholder='请选择规格值'
|
||||
onChange={this.handleSelectAttrValue}>
|
||||
{attrValueOptions}
|
||||
</Select>
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import React, {PureComponent} from "react";
|
||||
import {InputNumber, Select, Table} from "antd";
|
||||
import Input from "antd/es/input";
|
||||
|
||||
const Option = Select.Option;
|
||||
|
||||
class SkuInputNumber extends PureComponent {
|
||||
|
||||
handleChange = value => {
|
||||
// debugger;
|
||||
const { dispatch, index, dataIndex } = this.props;
|
||||
if (dataIndex === 'price') {
|
||||
dispatch({
|
||||
type: 'productSpuAddOrUpdate/inputSkuPrice',
|
||||
payload: {
|
||||
index: index,
|
||||
price: value
|
||||
},
|
||||
});
|
||||
} else if (dataIndex === 'quantity') {
|
||||
dispatch({
|
||||
type: 'productSpuAddOrUpdate/inputSkuQuantity',
|
||||
payload: {
|
||||
index: index,
|
||||
quantity: value
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputNumber placeholder="请输入" onChange={this.handleChange} />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class ProductSkuAddOrUpdateTable extends PureComponent {
|
||||
|
||||
render() {
|
||||
let that = this;
|
||||
// debugger;
|
||||
// console.log('ProductSkuAddOrUpdateTable');
|
||||
const {attrTree, skus, dispatch} = this.props;
|
||||
let columns = [];
|
||||
for (let i in attrTree) {
|
||||
let attr = attrTree[i];
|
||||
columns.push({
|
||||
title: attr.name,
|
||||
dataIndex: 'attrs[i]',
|
||||
render(value, record) {
|
||||
return record.attrs[i].name;
|
||||
}
|
||||
})
|
||||
}
|
||||
columns.push({
|
||||
title: '价格',
|
||||
dataIndex: 'price',
|
||||
render(value, record, index) {
|
||||
let props = {
|
||||
record: record,
|
||||
index: index,
|
||||
dispatch: dispatch,
|
||||
dataIndex: 'price'
|
||||
};
|
||||
return <SkuInputNumber {...props} />;
|
||||
}
|
||||
});
|
||||
columns.push({
|
||||
title: '库存',
|
||||
dataIndex: 'quantity',
|
||||
render(value, record, index) {
|
||||
let props = {
|
||||
record: record,
|
||||
index: index,
|
||||
dispatch: dispatch,
|
||||
dataIndex: 'quantity'
|
||||
};
|
||||
return <SkuInputNumber {...props} />;
|
||||
}
|
||||
});
|
||||
return <Table columns={columns} dataSource={skus} rowKey="index" />;
|
||||
// return <div />;
|
||||
}
|
||||
|
||||
}
|
|
@ -44,4 +44,9 @@ export default {
|
|||
'menu.account.settings': '个人设置',
|
||||
'menu.account.trigger': '触发报错',
|
||||
'menu.account.logout': '退出登录',
|
||||
// 商品相关
|
||||
'menu.product': '商品管理',
|
||||
'menu.product.product-spu-list': '商品管理',
|
||||
'menu.product.product-spu-add': '商品添加',
|
||||
'menu.product.product-category-list': '商品分类',
|
||||
};
|
||||
|
|
|
@ -76,13 +76,13 @@ export default {
|
|||
},
|
||||
*query({ payload }, { call, put }) {
|
||||
const response = yield call(queryAdmin, payload);
|
||||
message.info('查询成功!');
|
||||
const { count, admins } = response.data;
|
||||
yield put({
|
||||
type: 'querySuccess',
|
||||
payload: {
|
||||
list: admins,
|
||||
count,
|
||||
pageNo: payload.pageNo + 1
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import { message } from 'antd';
|
||||
import { productCategoryTree, productCategoryAdd, productCategoryUpdate, productCategoryUpdateStatus, productCategoryDelete } from '../../services/product';
|
||||
import { productCategoryTree, productSpuAdd, productCategoryUpdate, productCategoryUpdateStatus, productCategoryDelete } from '../../services/product';
|
||||
|
||||
export default {
|
||||
namespace: 'productSpuAddOrUpdate',
|
||||
|
||||
state: {
|
||||
list: [],
|
||||
attrTree: [{
|
||||
|
||||
}
|
||||
attrTree: [
|
||||
// {
|
||||
// id: //
|
||||
// name: //
|
||||
|
@ -17,6 +15,16 @@ export default {
|
|||
// name: //
|
||||
// }]
|
||||
// }
|
||||
],
|
||||
skus: [
|
||||
// {
|
||||
// attrs: [{
|
||||
// id: // 规格值编号
|
||||
// name: // 规格值名
|
||||
// }],
|
||||
// price: // 价格
|
||||
// quantity: // 数量
|
||||
// }
|
||||
]
|
||||
},
|
||||
|
||||
|
@ -65,7 +73,7 @@ export default {
|
|||
*addAttr({ payload }, { call, put }) {
|
||||
// const { queryParams } = payload;
|
||||
// const response = yield call(productCategoryTree, queryParams);
|
||||
message.info('调试:添加规格成功!');
|
||||
// message.info('调试:添加规格成功!');
|
||||
yield put({
|
||||
type: 'addAttrSuccess',
|
||||
payload: {
|
||||
|
@ -73,17 +81,137 @@ export default {
|
|||
},
|
||||
});
|
||||
},
|
||||
*selectAttr({ payload }, { call, put }) {
|
||||
// const { queryParams } = payload;
|
||||
// const response = yield call(productCategoryTree, queryParams);
|
||||
// message.info('调试:选择规格成功!');
|
||||
yield put({
|
||||
type: 'selectAttrSuccess',
|
||||
payload: payload,
|
||||
});
|
||||
},
|
||||
*selectAttrValues({ payload }, { call, put }) {
|
||||
// const { queryParams } = payload;
|
||||
// const response = yield call(productCategoryTree, queryParams);
|
||||
// message.info('调试:选择规格值成功!');
|
||||
yield put({
|
||||
type: 'selectAttrValueSuccess',
|
||||
payload: payload,
|
||||
});
|
||||
},
|
||||
*inputSkuPrice({ payload }, { call, put }) {
|
||||
// debugger;
|
||||
yield put({
|
||||
type: 'inputSkuPriceSuccess',
|
||||
payload: payload,
|
||||
});
|
||||
},
|
||||
*inputSkuQuantity({ payload }, { call, put }) {
|
||||
// debugger;
|
||||
yield put({
|
||||
type: 'inputSkuQuantitySuccess',
|
||||
payload: payload,
|
||||
});
|
||||
},
|
||||
*add({ payload }, { call, put }) {
|
||||
const { callback, body } = payload;
|
||||
const response = yield call(productSpuAdd, body);
|
||||
if (callback) {
|
||||
callback(response);
|
||||
}
|
||||
// yield put({
|
||||
// type: 'tree',
|
||||
// payload: {},
|
||||
// });
|
||||
alert('添加成功!后续改成跳转到手机站的详情');
|
||||
},
|
||||
*update({ payload }, { call, put }) {
|
||||
const { callback, body } = payload;
|
||||
const response = yield call(productSpuAdd, body);
|
||||
if (callback) {
|
||||
callback(response);
|
||||
}
|
||||
// yield put({
|
||||
// type: 'tree',
|
||||
// payload: {},
|
||||
// });
|
||||
alert('修改成功!后续改成跳转到手机站的详情');
|
||||
},
|
||||
},
|
||||
|
||||
reducers: {
|
||||
addAttrSuccess(state, {payload}) {
|
||||
// debugger;
|
||||
console.log(state.attrTree);
|
||||
// console.log(state.attrTree);
|
||||
state.attrTree.push(payload.attrAdd);
|
||||
return {
|
||||
...state
|
||||
}
|
||||
},
|
||||
selectAttrSuccess(state, {payload}) {
|
||||
// debugger;
|
||||
// console.log(state.attrTree);
|
||||
state.attrTree[payload.attrIndex] = payload.attr;
|
||||
return {
|
||||
...state
|
||||
}
|
||||
},
|
||||
selectAttrValueSuccess(state, {payload}) {
|
||||
// debugger;
|
||||
// console.log(state);
|
||||
state.attrTree[payload.attrIndex].values = payload.attrValues;
|
||||
// 生成 skus 值
|
||||
let skus = [];
|
||||
let skuSize = 1;
|
||||
for (let i in state.attrTree) { // 先计算 sku 数量
|
||||
let attr = state.attrTree[i];
|
||||
skuSize = skuSize * attr.values.length;
|
||||
}
|
||||
// console.log('skuSize: ' + skuSize);
|
||||
for (let i = 0; i < skuSize; i++) { // 初始化 sku 格子
|
||||
skus.push({
|
||||
attrs: [],
|
||||
price: undefined,
|
||||
quantity: undefined,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < state.attrTree.length; i++) { // 初始化 sku 格子里的 attrs
|
||||
for (let j = 0; j < skuSize; j++) {
|
||||
// let values = state.attrTree[i].values;
|
||||
// let attr = values[j % values.length];
|
||||
// skus[i].attrs.push({
|
||||
// id: attr.id,
|
||||
// name: attr.name,
|
||||
// });
|
||||
let values = state.attrTree[i].values;
|
||||
let attr = values[j % values.length];
|
||||
skus[j].attrs.push({
|
||||
id: attr.id,
|
||||
name: attr.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
state.skus = skus;
|
||||
// debugger;
|
||||
// console.l og('skus: ' + skus);
|
||||
return {
|
||||
...state
|
||||
}
|
||||
},
|
||||
inputSkuPriceSuccess(state, {payload}) {
|
||||
// debugger;
|
||||
state.skus[payload.index].price = payload.price;
|
||||
return {
|
||||
...state
|
||||
}
|
||||
},
|
||||
inputSkuQuantitySuccess(state, {payload}) {
|
||||
// debugger;
|
||||
state.skus[payload.index].quantity = payload.quantity;
|
||||
return {
|
||||
...state
|
||||
}
|
||||
},
|
||||
treeSuccess(state, { payload }) {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { message } from 'antd';
|
||||
import { productSpuPage, productCategoryAdd, productCategoryUpdate, productCategoryUpdateStatus, productCategoryDelete } from '../../services/product';
|
||||
import {routerRedux} from "dva/router";
|
||||
|
||||
export default {
|
||||
namespace: 'productSpuList',
|
||||
|
@ -9,46 +10,50 @@ export default {
|
|||
},
|
||||
|
||||
effects: {
|
||||
*add({ payload }, { call, put }) {
|
||||
const { callback, body } = payload;
|
||||
const response = yield call(productCategoryAdd, body);
|
||||
if (callback) {
|
||||
callback(response);
|
||||
}
|
||||
yield put({
|
||||
type: 'tree',
|
||||
payload: {},
|
||||
});
|
||||
},
|
||||
*update({ payload }, { call, put }) {
|
||||
const { callback, body } = payload;
|
||||
const response = yield call(productCategoryUpdate, body);
|
||||
if (callback) {
|
||||
callback(response);
|
||||
}
|
||||
yield put({
|
||||
type: 'tree',
|
||||
payload: {},
|
||||
});
|
||||
},
|
||||
*updateStatus({ payload }, { call, put }) {
|
||||
const { callback, body } = payload;
|
||||
const response = yield call(productCategoryUpdateStatus, body);
|
||||
if (callback) {
|
||||
callback(response);
|
||||
}
|
||||
yield put({
|
||||
type: 'tree',
|
||||
payload: {},
|
||||
});
|
||||
},
|
||||
*delete({ payload }, { call, put }) {
|
||||
const response = yield call(productCategoryDelete, payload);
|
||||
message.info('删除成功!');
|
||||
yield put({
|
||||
type: 'tree',
|
||||
payload: {},
|
||||
});
|
||||
// *add({ payload }, { call, put }) {
|
||||
// const { callback, body } = payload;
|
||||
// const response = yield call(productCategoryAdd, body);
|
||||
// if (callback) {
|
||||
// callback(response);
|
||||
// }
|
||||
// yield put({
|
||||
// type: 'tree',
|
||||
// payload: {},
|
||||
// });
|
||||
// },
|
||||
// *update({ payload }, { call, put }) {
|
||||
// const { callback, body } = payload;
|
||||
// const response = yield call(productCategoryUpdate, body);
|
||||
// if (callback) {
|
||||
// callback(response);
|
||||
// }
|
||||
// yield put({
|
||||
// type: 'tree',
|
||||
// payload: {},
|
||||
// });
|
||||
// },
|
||||
// *updateStatus({ payload }, { call, put }) {
|
||||
// const { callback, body } = payload;
|
||||
// const response = yield call(productCategoryUpdateStatus, body);
|
||||
// if (callback) {
|
||||
// callback(response);
|
||||
// }
|
||||
// yield put({
|
||||
// type: 'tree',
|
||||
// payload: {},
|
||||
// });
|
||||
// },
|
||||
// *delete({ payload }, { call, put }) {
|
||||
// const response = yield call(productCategoryDelete, payload);
|
||||
// message.info('删除成功!');
|
||||
// yield put({
|
||||
// type: 'tree',
|
||||
// payload: {},
|
||||
// });
|
||||
// },
|
||||
*redirectToAdd({ payload }, { call, put }) {
|
||||
// const { callback, body } = payload;
|
||||
yield put(routerRedux.replace('/product/product-spu-add'));
|
||||
},
|
||||
*page({ payload }, { call, put }) {
|
||||
const { queryParams } = payload;
|
||||
|
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
import React, { PureComponent, Fragment } from 'react';
|
||||
import { connect } from 'dva';
|
||||
import { Card, Form, Input, Button, Modal, message, Table, Divider, Tree, Spin } from 'antd';
|
||||
import {Card, Form, Input, Button, Modal, message, Table, Divider, Tree, Spin, Row, Col, Select, Icon} from 'antd';
|
||||
import { checkTypeWithEnglishAndNumbers } from '../../../helpers/validator'
|
||||
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
|
||||
|
||||
import styles from './AdminList.less';
|
||||
import moment from "moment";
|
||||
import Pagination from "antd/es/pagination";
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const { TreeNode } = Tree;
|
||||
|
@ -31,31 +34,36 @@ const CreateForm = Form.create()(props => {
|
|||
width: 200,
|
||||
};
|
||||
|
||||
const title = modalType === 'add' ? '添加一个 Resource' : '更新一个 Resource';
|
||||
const okText = modalType === 'add' ? '添加' : '更新';
|
||||
const title = modalType === 'add' ? '新建管理员' : '更新管理员';
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose
|
||||
title={title}
|
||||
visible={modalVisible}
|
||||
onOk={okHandle}
|
||||
okText={okText}
|
||||
okText='保存'
|
||||
onCancel={() => handleModalVisible()}
|
||||
>
|
||||
<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="名称">
|
||||
<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="用户名">
|
||||
{form.getFieldDecorator('username', {
|
||||
rules: [{ required: true, message: '请输入名称!', min: 2 }],
|
||||
rules: [{ required: true, message: '请输入用户名!'},
|
||||
{max: 16, min:6, message: '长度为6-16位'},
|
||||
{ validator: (rule, value, callback) => checkTypeWithEnglishAndNumbers(rule, value, callback, '数字以及字母')}
|
||||
],
|
||||
initialValue: initValues.username,
|
||||
})(<Input placeholder="请输入" />)}
|
||||
</FormItem>
|
||||
<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="昵称">
|
||||
{form.getFieldDecorator('nickname', {
|
||||
rules: [{ required: true, message: '请输入昵称!', min: 2 }],
|
||||
rules: [{ required: true, message: '请输入昵称!'},
|
||||
{max: 10, message: '姓名最大长度为10'}],
|
||||
initialValue: initValues.nickname,
|
||||
})(<Input placeholder="请输入" />)}
|
||||
</FormItem>
|
||||
<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="密码">
|
||||
{form.getFieldDecorator('password', {
|
||||
rules: [{ required: modalType === 'add', message: '请填写密码'}, // 添加时,必须输入密码
|
||||
{max: 16, min: 6, message: '长度为6-18位'}],
|
||||
initialValue: initValues.password,
|
||||
})(<Input placeholder="请输入" type="password" />)}
|
||||
</FormItem>
|
||||
|
@ -304,9 +312,49 @@ class ResourceList extends PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
onPageChange = (page = {}) => {
|
||||
const { dispatch } = this.props;
|
||||
// debugger;
|
||||
dispatch({
|
||||
type: 'adminList/query',
|
||||
payload: {
|
||||
pageNo: page - 1,
|
||||
pageSize: 10,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderSimpleForm() { // TODO 芋艿,搜索功能未完成
|
||||
const {
|
||||
form: { getFieldDecorator },
|
||||
} = this.props;
|
||||
return (
|
||||
<Form onSubmit={this.handleSearch} layout="inline">
|
||||
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
|
||||
<Col md={8} sm={24}>
|
||||
<FormItem label="昵称">
|
||||
{getFieldDecorator('name')(<Input placeholder="请输入" />)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col md={8} sm={24}>
|
||||
<span className={styles.submitButtons}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
<Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>
|
||||
重置
|
||||
</Button>
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { list, data } = this.props;
|
||||
const { roleList, roleCheckedKeys, roleAssignLoading } = data;
|
||||
let that = this;
|
||||
const { list, data } = this.props;
|
||||
const { count, pageNo, pageSize, roleList, roleCheckedKeys, roleAssignLoading } = data;
|
||||
const {
|
||||
modalVisible,
|
||||
modalType,
|
||||
|
@ -323,15 +371,9 @@ class ResourceList extends PureComponent {
|
|||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
render: text => <strong>{text}</strong>,
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
render: text => <a>{text}</a>,
|
||||
dataIndex: 'username'
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
|
@ -341,17 +383,22 @@ class ResourceList extends PureComponent {
|
|||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
render(val) {
|
||||
return <span>{status[val]}</span>;
|
||||
return <span>{status[val]}</span>; // TODO 芋艿,此处要改
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
render: val => <span>{moment(val).format('YYYY-MM-DD HH:mm')}</span>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 300,
|
||||
width: 360,
|
||||
render: (text, record) => {
|
||||
const statusText = record.status === 1 ? '确认禁用' : '取消禁用';
|
||||
const statusText = record.status === 1 ? '禁用' : '禁用';
|
||||
return (
|
||||
<Fragment>
|
||||
<a onClick={() => this.handleModalVisible(true, 'update', record)}>更新</a>
|
||||
<a onClick={() => this.handleModalVisible(true, 'update', record)}>编辑</a>
|
||||
<Divider type="vertical" />
|
||||
<a onClick={() => this.handleRoleAssign(record)}>角色分配</a>
|
||||
<Divider type="vertical" />
|
||||
|
@ -369,16 +416,17 @@ class ResourceList extends PureComponent {
|
|||
];
|
||||
|
||||
return (
|
||||
<PageHeaderWrapper title="查询表格">
|
||||
<PageHeaderWrapper>
|
||||
<Card bordered={false}>
|
||||
<div className={styles.tableList}>
|
||||
<div className={styles.tableListForm}>{that.renderSimpleForm()}</div>
|
||||
<div className={styles.tableListOperator}>
|
||||
<Button
|
||||
icon="plus"
|
||||
type="primary"
|
||||
onClick={() => this.handleModalVisible(true, 'add', {})}
|
||||
>
|
||||
新建
|
||||
新建管理员
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -387,6 +435,12 @@ class ResourceList extends PureComponent {
|
|||
columns={columns}
|
||||
dataSource={list}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
current: pageNo,
|
||||
pageSize: pageSize,
|
||||
total: count,
|
||||
onChange: this.onPageChange
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<CreateForm {...parentMethods} modalVisible={modalVisible} />
|
||||
|
|
|
@ -13,3 +13,35 @@
|
|||
.tableDelete {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.tableListForm {
|
||||
:global {
|
||||
.ant-form-item {
|
||||
display: flex;
|
||||
margin-right: 0;
|
||||
margin-bottom: 24px;
|
||||
> .ant-form-item-label {
|
||||
width: auto;
|
||||
padding-right: 8px;
|
||||
line-height: 32px;
|
||||
}
|
||||
.ant-form-item-control {
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
.ant-form-item-control-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.submitButtons {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-lg) {
|
||||
.tableListForm :global(.ant-form-item) {
|
||||
margin-right: 24px;
|
||||
}
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React, { PureComponent, Fragment } from 'react';
|
||||
import React, {PureComponent, Fragment, Component} from 'react';
|
||||
import { connect } from 'dva';
|
||||
import moment from 'moment';
|
||||
import {Card, Form, Input, Radio, Button, Table, Select} from 'antd';
|
||||
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
|
||||
|
||||
import styles from './ProductSpuAddOrUpdate.less';
|
||||
import ProductAttrSelectFormItem from "../../components/Product/ProductAttrSelectFormItem";
|
||||
import ProductSkuAddOrUpdateTable from "../../components/Product/ProductSkuAddOrUpdateTable";
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const RadioGroup = Radio.Group;
|
||||
|
@ -16,12 +18,15 @@ const Option = Select.Option;
|
|||
@connect(({ productSpuList, productAttrList, productSpuAddOrUpdate, loading }) => ({
|
||||
// list: productSpuList.list.spus,
|
||||
// loading: loading.models.productSpuList,
|
||||
productAttrList,
|
||||
productSpuAddOrUpdate,
|
||||
allAttrTree: productAttrList.tree,
|
||||
attrTree: productSpuAddOrUpdate.attrTree
|
||||
attrTree: productSpuAddOrUpdate.attrTree,
|
||||
skus: productSpuAddOrUpdate.skus,
|
||||
}))
|
||||
|
||||
@Form.create()
|
||||
class ProductSpuAddOrUpdate extends PureComponent {
|
||||
class ProductSpuAddOrUpdate extends Component {
|
||||
state = {
|
||||
modalVisible: false,
|
||||
modalType: 'add', //add update
|
||||
|
@ -42,18 +47,18 @@ class ProductSpuAddOrUpdate extends PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
const { dispatch, form } = this.props;
|
||||
e.preventDefault();
|
||||
form.validateFieldsAndScroll((err, values) => {
|
||||
if (!err) {
|
||||
dispatch({
|
||||
type: 'form/submitRegularForm',
|
||||
payload: values,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// handleSubmit = e => {
|
||||
// const { dispatch, form } = this.props;
|
||||
// e.preventDefault();
|
||||
// form.validateFieldsAndScroll((err, values) => {
|
||||
// if (!err) {
|
||||
// dispatch({
|
||||
// type: 'form/submitRegularForm',
|
||||
// payload: values,
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
handleAddAttr = e => {
|
||||
// alert('你猜');
|
||||
|
@ -65,50 +70,171 @@ class ProductSpuAddOrUpdate extends PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
debugger;
|
||||
e.preventDefault();
|
||||
const { skus, dispatch } = this.props;
|
||||
// 生成 skuStr 格式
|
||||
let skuStr = []; // 因为提交是字符串格式
|
||||
for (let i in skus) {
|
||||
let sku = skus[i];
|
||||
if (!sku.price || !sku.quantity) {
|
||||
continue;
|
||||
}
|
||||
let newAttr = {
|
||||
attrs: [],
|
||||
price: sku.price,
|
||||
quantity: sku.quantity,
|
||||
}
|
||||
for (let j in sku.attrs) {
|
||||
newAttr.attrs.push(sku.attrs[j].id);
|
||||
}
|
||||
skuStr.push(newAttr);
|
||||
}
|
||||
if (skuStr.length === 0) {
|
||||
alert('请设置商品规格!');
|
||||
return;
|
||||
}
|
||||
this.props.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
dispatch({
|
||||
type: 'productSpuAddOrUpdate/add',
|
||||
payload: {
|
||||
body: {
|
||||
...values,
|
||||
skuStr: JSON.stringify(skuStr)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
// console.log(fields);
|
||||
}
|
||||
|
||||
// handleSelectAttr = (value, option) => {
|
||||
// // console.log(value);
|
||||
// // console.log(option);
|
||||
// // debugger;
|
||||
// const { dispatch } = this.props;
|
||||
// let attrIndex = option.key.substring(option.key.indexOf('option-attr-') + 'option-attr-'.length, option.key.lastIndexOf('-'));
|
||||
// // console.log('attrIndex: ' + attrIndex);
|
||||
// // debugger;
|
||||
// dispatch({
|
||||
// type: 'productSpuAddOrUpdate/selectAttr',
|
||||
// payload: {
|
||||
// attrIndex: attrIndex,
|
||||
// attr: {
|
||||
// id: option.props.value,
|
||||
// name: option.props.children,
|
||||
// values: []
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// handleSelectAttrValue = (values, options) => {
|
||||
// let attrValues = [];
|
||||
// const { dispatch } = this.props;
|
||||
// debugger;
|
||||
// // console.log('x' + this.children[0]);
|
||||
// let firstOption = this.children[0];
|
||||
// // let attrIndex = firstOption.key.substring(firstOption.key.indexOf('option-attr-value-') + 'option-attr-value-'.length, firstOption.key.lastIndexOf('-'));
|
||||
// let attrIndex = 0;
|
||||
// for (let i in options) {
|
||||
// let option = options[i];
|
||||
// attrValues.push({
|
||||
// id: parseInt(option.props.value),
|
||||
// name: option.props.children,
|
||||
// });
|
||||
// }
|
||||
// dispatch({
|
||||
// type: 'productSpuAddOrUpdate/selectAttrValues',
|
||||
// payload: {
|
||||
// attrIndex: attrIndex,
|
||||
// attrValues: attrValues,
|
||||
// },
|
||||
// });
|
||||
// // debugger;
|
||||
//
|
||||
// // console.log(value);
|
||||
// }
|
||||
|
||||
render() {
|
||||
// debugger;
|
||||
const { form, data, attrTree } = this.props;
|
||||
|
||||
// 规格明细
|
||||
const columns = [
|
||||
{
|
||||
title: '颜色',
|
||||
dataIndex: 'price'
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
dataIndex: 'price',
|
||||
render(val) {
|
||||
return <span>{status[val]}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '库存',
|
||||
dataIndex: 'quantity',
|
||||
}
|
||||
];
|
||||
const { form, skus, attrTree, allAttrTree, dispatch } = this.props;
|
||||
// const that = this;
|
||||
|
||||
// 添加规格
|
||||
// debugger;
|
||||
let attrTreeHTML = [];
|
||||
if (attrTree && attrTree.length > 0) {
|
||||
// 已选择的的规格集合
|
||||
let selectedAttrIds = new Set();
|
||||
for (let i in attrTree) {
|
||||
let attr = attrTree[i];
|
||||
attr = <div>
|
||||
<Select defaultValue="lucy" style={{ width: 120 }}>
|
||||
{
|
||||
|
||||
}
|
||||
<Option value="jack">Jack</Option>
|
||||
<Option value="lucy">Lucy</Option>
|
||||
<Option value="disabled" disabled>Disabled</Option>
|
||||
<Option value="Yiminghe">yiminghe</Option>
|
||||
</Select>
|
||||
</div>;
|
||||
attrTreeHTML.push(attr);
|
||||
// debugger;
|
||||
selectedAttrIds.add(attr.id);
|
||||
}
|
||||
// 创建每个规格下拉框的 HTML
|
||||
for (let i in attrTree) {
|
||||
let attr = attrTree[i];
|
||||
let itemProps = {
|
||||
attr: attr,
|
||||
allAttrTree: allAttrTree,
|
||||
dispatch: dispatch,
|
||||
selectedAttrIds: selectedAttrIds,
|
||||
index: i // 位置。不然无法正确修改 Model 指定位置的数据
|
||||
};
|
||||
attrTreeHTML.push(<ProductAttrSelectFormItem {...itemProps} />);
|
||||
}
|
||||
}
|
||||
// if (attrTree && attrTree.length > 0) {
|
||||
// for (let i in attrTree) {
|
||||
// let attr = attrTree[i];
|
||||
// // console.log('i: ' + i);
|
||||
// // 1. 规格
|
||||
// let attrOptions = [];
|
||||
// for (let j in allAttrTree) {
|
||||
// let attr = allAttrTree[j];
|
||||
// attrOptions.push(<Option key={`option-attr-${i}-${attr.id}`} value={attr.id}>{attr.name}</Option>);
|
||||
// }
|
||||
// // 2. 规格值
|
||||
// let attrValueOptions = [];
|
||||
// // debugger;
|
||||
// if (attr.id) {
|
||||
// // 2.1 先招到规格值的数组
|
||||
// let attrValues = [];
|
||||
// for (let j in allAttrTree) {
|
||||
// let allAttr = allAttrTree[j];
|
||||
// if (attr.id === allAttr.id) {
|
||||
// attrValues = allAttr.values;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// // 2.2 生成规格值的 HTML
|
||||
// for (let j in attrValues) {
|
||||
// let attrValue = attrValues[j];
|
||||
// attrValueOptions.push(<Option key={`option-attr-value-${i}-${attrValue.id}`} value={attrValue.id + ''}>{attrValue.name}</Option>); // + '' 的原因是,多选必须是字符串
|
||||
// }
|
||||
// }
|
||||
// // 3. 拼装最终,添加到 attrTreeHTML 中
|
||||
// attr = <div key={`div-attr-${i}`}>
|
||||
// <Select key={`select-attr-${i}`} style={{ width: 120 }} placeholder='请选择规格' onChange={that.handleSelectAttr}>
|
||||
// {attrOptions}
|
||||
// </Select>
|
||||
// <Select key={`select-attr-value-${i}`} mode={"tags"} style={{ width: 260 }} placeholder='请选择规格值' onChange={that.handleSelectAttrValue}>
|
||||
// {attrValueOptions}
|
||||
// </Select>
|
||||
// </div>;
|
||||
// attrTreeHTML.push(attr);
|
||||
// }
|
||||
// }
|
||||
// 规格明细
|
||||
let productSkuProps = {
|
||||
attrTree: attrTree,
|
||||
skus: skus,
|
||||
dispatch: dispatch,
|
||||
};
|
||||
// console.log(productSkuProps);
|
||||
|
||||
return (
|
||||
<PageHeaderWrapper title="">
|
||||
|
@ -163,13 +289,12 @@ class ProductSpuAddOrUpdate extends PureComponent {
|
|||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
{/*<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="规格明细">*/}
|
||||
{/*{form.getFieldDecorator('visible', {*/}
|
||||
{/*initialValue: 1, // TODO 修改*/}
|
||||
{/*})(*/}
|
||||
{/*<Table defaultExpandAllRows={true} columns={columns} rowKey="id" />*/}
|
||||
{/*)}*/}
|
||||
{/*</FormItem>*/}
|
||||
<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="规格明细">
|
||||
{/*<Table defaultExpandAllRows={true} columns={columns} rowKey="id" />*/}
|
||||
<ProductSkuAddOrUpdateTable {...productSkuProps} />
|
||||
|
||||
<Button type="primary" htmlType="submit" style={{ marginLeft: 8 }} onSubmit={this.handleSubmit}>保存</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Card>
|
||||
</PageHeaderWrapper>
|
||||
|
|
|
@ -77,52 +77,13 @@ class ProductSpuList extends PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
handleModalVisible = (flag, modalType, initValues) => {
|
||||
this.setState({
|
||||
modalVisible: !!flag,
|
||||
initValues: initValues || {},
|
||||
modalType: modalType || 'add',
|
||||
redirectToAdd = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'productSpuList/redirectToAdd',
|
||||
});
|
||||
};
|
||||
|
||||
handleAdd = ({ fields, modalType, initValues }) => {
|
||||
const { dispatch, data } = this.props;
|
||||
const queryParams = {
|
||||
pageNo: data.pageNo,
|
||||
pageSize: data.pageSize,
|
||||
};
|
||||
if (modalType === 'add') {
|
||||
dispatch({
|
||||
type: 'roleList/add',
|
||||
payload: {
|
||||
body: {
|
||||
...fields,
|
||||
},
|
||||
queryParams,
|
||||
callback: () => {
|
||||
message.success('添加成功');
|
||||
this.handleModalVisible();
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'roleList/update',
|
||||
payload: {
|
||||
body: {
|
||||
...initValues,
|
||||
...fields,
|
||||
},
|
||||
queryParams,
|
||||
callback: () => {
|
||||
message.success('更新成功');
|
||||
this.handleModalVisible();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
// debugger;
|
||||
const { list, data } = this.props;
|
||||
|
@ -199,7 +160,7 @@ class ProductSpuList extends PureComponent {
|
|||
<Button
|
||||
icon="plus"
|
||||
type="primary"
|
||||
onClick={() => this.handleModalVisible(true, 'add', {})}
|
||||
onClick={this.redirectToAdd}
|
||||
>
|
||||
发布商品
|
||||
</Button>
|
||||
|
|
|
@ -44,6 +44,13 @@ export async function productSpuPage(params) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function productSpuAdd(params) {
|
||||
return request(`/product-api/admins/spu/add?${stringify(params)}`, {
|
||||
method: 'POST',
|
||||
body: {},
|
||||
});
|
||||
}
|
||||
|
||||
// product attr + attr value
|
||||
|
||||
export async function productAttrTree(params) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package cn.iocoder.mall.admin.application.config;
|
||||
|
||||
import cn.iocoder.common.framework.config.GlobalExceptionHandler;
|
||||
import cn.iocoder.mall.admin.sdk.interceptor.AdminAccessLogInterceptor;
|
||||
import cn.iocoder.mall.admin.sdk.interceptor.AdminSecurityInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
@ -18,16 +19,17 @@ public class MVCConfiguration implements WebMvcConfigurer {
|
|||
|
||||
@Autowired
|
||||
private AdminSecurityInterceptor adminSecurityInterceptor;
|
||||
@Autowired
|
||||
private AdminAccessLogInterceptor adminAccessLogInterceptor;
|
||||
//
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// registry.addInterceptor(securityInterceptor).addPathPatterns("/user/**", "/admin/**"); // 只拦截我们定义的接口
|
||||
registry.addInterceptor(adminAccessLogInterceptor).addPathPatterns("/admins/**");
|
||||
registry.addInterceptor(adminSecurityInterceptor).addPathPatterns("/admins/**")
|
||||
.excludePathPatterns("/admins/passport/login"); // 排除登陆接口
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// 解决 swagger-ui.html 的访问,参考自 https://stackoverflow.com/questions/43545540/swagger-ui-no-mapping-found-for-http-request 解决
|
||||
|
|
|
@ -42,6 +42,8 @@ public class AdminController {
|
|||
|
||||
// =========== 当前管理员相关的资源 API ===========
|
||||
|
||||
// TODO 功能:当前管理员
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
@GetMapping("/menu_resource_tree")
|
||||
@ApiOperation(value = "获得当前登陆的管理员拥有的菜单权限", notes = "以树结构返回")
|
||||
|
@ -120,7 +122,7 @@ public class AdminController {
|
|||
public CommonResult<Boolean> update(@RequestParam("id") Integer id,
|
||||
@RequestParam("username") String username,
|
||||
@RequestParam("nickname") String nickname,
|
||||
@RequestParam("password") String password) {
|
||||
@RequestParam(value = "password", required = false) String password) {
|
||||
AdminUpdateDTO adminUpdateDTO = new AdminUpdateDTO().setId(id).setUsername(username).setNickname(nickname).setPassword(password);
|
||||
return adminService.updateAdmin(AdminSecurityContextHolder.getContext().getAdminId(), adminUpdateDTO);
|
||||
}
|
||||
|
|
|
@ -3,17 +3,17 @@ package cn.iocoder.mall.admin.application.controller.admins;
|
|||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.mall.admin.api.OAuth2Service;
|
||||
import cn.iocoder.mall.admin.api.bo.OAuth2AccessTokenBO;
|
||||
import cn.iocoder.mall.admin.application.convert.AdminConvert;
|
||||
import cn.iocoder.mall.admin.application.convert.PassportConvert;
|
||||
import cn.iocoder.mall.admin.application.vo.AdminInfoVO;
|
||||
import cn.iocoder.mall.admin.application.vo.PassportLoginVO;
|
||||
import cn.iocoder.mall.admin.sdk.context.AdminSecurityContextHolder;
|
||||
import com.alibaba.dubbo.config.annotation.Reference;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiImplicitParams;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("admins/passport")
|
||||
|
@ -35,10 +35,8 @@ public class PassportController {
|
|||
return PassportConvert.INSTANCE.convert(result);
|
||||
}
|
||||
|
||||
// TODO 艿艿:后续继续完善
|
||||
@GetMapping("/info")
|
||||
public CommonResult<AdminInfoVO> info() {
|
||||
return CommonResult.success(AdminConvert.INSTANCE.convert(AdminSecurityContextHolder.getContext()));
|
||||
}
|
||||
// TODO 功能 logout
|
||||
|
||||
// TODO 功能 refresh_token
|
||||
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package cn.iocoder.mall.admin.sdk.interceptor;
|
||||
|
||||
import cn.iocoder.common.framework.util.HttpUtil;
|
||||
import cn.iocoder.mall.admin.api.AdminAccessLogService;
|
||||
import cn.iocoder.mall.admin.api.dto.AdminAccessLogAddDTO;
|
||||
import com.alibaba.dubbo.config.annotation.Reference;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 访问日志拦截器
|
||||
*/
|
||||
@Component
|
||||
public class AdminAccessLogInterceptor extends HandlerInterceptorAdapter {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
private static final ThreadLocal<Date> START_TIME = new ThreadLocal<>();
|
||||
/**
|
||||
* 管理员编号
|
||||
*/
|
||||
private static final ThreadLocal<Integer> ADMIN_ID = new ThreadLocal<>();
|
||||
|
||||
@Reference
|
||||
private AdminAccessLogService adminAccessLogService;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
// 记录当前时间
|
||||
START_TIME.set(new Date());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||
AdminAccessLogAddDTO accessLog = new AdminAccessLogAddDTO();
|
||||
try {
|
||||
accessLog.setAdminId(ADMIN_ID.get());
|
||||
if (accessLog.getAdminId() == null) {
|
||||
accessLog.setAdminId(AdminAccessLogAddDTO.ADMIN_ID_NULL);
|
||||
}
|
||||
accessLog.setUri(request.getRequestURI()); // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。
|
||||
accessLog.setQueryString(HttpUtil.buildQueryString(request));
|
||||
accessLog.setMethod(request.getMethod());
|
||||
accessLog.setUserAgent(HttpUtil.getUserAgent(request));
|
||||
accessLog.setIp(HttpUtil.getIp(request));
|
||||
accessLog.setStartTime(START_TIME.get());
|
||||
accessLog.setResponseTime((int) (System.currentTimeMillis() - accessLog.getStartTime().getTime()));// 默认响应时间设为0
|
||||
adminAccessLogService.addAdminAccessLog(accessLog);
|
||||
// TODO 提升:暂时不考虑 ELK 的方案。而是基于 MySQL 存储。如果访问日志比较多,需要定期归档。
|
||||
} catch (Throwable th) {
|
||||
logger.error("[afterCompletion][插入管理员访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
|
||||
} finally {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static void setAdminId(Integer adminId) {
|
||||
ADMIN_ID.set(adminId);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
START_TIME.remove();
|
||||
ADMIN_ID.remove();
|
||||
}
|
||||
|
||||
}
|
|
@ -39,6 +39,13 @@ public class AdminSecurityInterceptor extends HandlerInterceptorAdapter {
|
|||
// 添加到 AdminSecurityContext
|
||||
AdminSecurityContext context = new AdminSecurityContext(authentication.getAdminId(), authentication.getRoleIds());
|
||||
AdminSecurityContextHolder.setContext(context);
|
||||
// 同时也记录管理员编号到 AdminAccessLogInterceptor 中。因为:
|
||||
// AdminAccessLogInterceptor 需要在 AdminSecurityInterceptor 之前执行,这样记录的访问日志才健全
|
||||
// AdminSecurityInterceptor 执行后,会移除 AdminSecurityContext 信息,这就导致 AdminAccessLogInterceptor 无法获得管理员编号
|
||||
// 因此,这里需要进行记录
|
||||
if (authentication.getAdminId() != null) {
|
||||
AdminAccessLogInterceptor.setAdminId(authentication.getAdminId());
|
||||
}
|
||||
} else {
|
||||
String url = request.getRequestURI();
|
||||
if (!url.equals("/admin/passport/login")) { // TODO 临时写死。非登陆接口,必须已经认证身份,不允许匿名访问
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package cn.iocoder.mall.admin.api;
|
||||
|
||||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.mall.admin.api.dto.AdminAccessLogAddDTO;
|
||||
|
||||
/**
|
||||
* 管理员访问日志 Service 接口
|
||||
*/
|
||||
public interface AdminAccessLogService {
|
||||
|
||||
CommonResult<Boolean> addAdminAccessLog(AdminAccessLogAddDTO adminAccessLogAddDTO);
|
||||
|
||||
}
|
|
@ -9,6 +9,9 @@ import cn.iocoder.mall.admin.api.dto.AdminUpdateDTO;
|
|||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 管理员 Service 接口
|
||||
*/
|
||||
public interface AdminService {
|
||||
|
||||
CommonResult<AdminPageBO> getAdminPage(AdminPageDTO adminPageDTO);
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
package cn.iocoder.mall.admin.api.dto;
|
||||
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 管理员访问日志添加 DTO
|
||||
*/
|
||||
public class AdminAccessLogAddDTO {
|
||||
|
||||
/**
|
||||
* 管理员编号 - 空
|
||||
*/
|
||||
public static final Integer ADMIN_ID_NULL = 0;
|
||||
|
||||
/**
|
||||
* 管理员编号.
|
||||
*
|
||||
* 当管理员为空时,该值为0
|
||||
*/
|
||||
@NotNull(message = "管理员编号不能为空")
|
||||
private Integer adminId;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
@NotNull(message = "访问地址不能为空")
|
||||
private String uri;
|
||||
/**
|
||||
* 参数
|
||||
*/
|
||||
@NotNull(message = "请求参数不能为空")
|
||||
private String queryString;
|
||||
/**
|
||||
* http 方法
|
||||
*/
|
||||
@NotNull(message = "http 请求方法不能为空")
|
||||
private String method;
|
||||
/**
|
||||
* User Agent
|
||||
*/
|
||||
@NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
/**
|
||||
* ip
|
||||
*/
|
||||
@NotNull(message = "ip 不能为空")
|
||||
private String ip;
|
||||
/**
|
||||
* 请求时间
|
||||
*/
|
||||
@NotNull(message = "请求时间不能为空")
|
||||
private Date startTime;
|
||||
/**
|
||||
* 响应时长 -- 毫秒级
|
||||
*/
|
||||
@NotNull(message = "响应时长不能为空")
|
||||
private Integer responseTime;
|
||||
|
||||
public Integer getAdminId() {
|
||||
return adminId;
|
||||
}
|
||||
|
||||
public AdminAccessLogAddDTO setAdminId(Integer adminId) {
|
||||
this.adminId = adminId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public AdminAccessLogAddDTO setUri(String uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getQueryString() {
|
||||
return queryString;
|
||||
}
|
||||
|
||||
public AdminAccessLogAddDTO setQueryString(String queryString) {
|
||||
this.queryString = queryString;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public AdminAccessLogAddDTO setMethod(String method) {
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
public AdminAccessLogAddDTO setUserAgent(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getIp() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
public AdminAccessLogAddDTO setIp(String ip) {
|
||||
this.ip = ip;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Date getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public AdminAccessLogAddDTO setStartTime(Date startTime) {
|
||||
this.startTime = startTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getResponseTime() {
|
||||
return responseTime;
|
||||
}
|
||||
|
||||
public AdminAccessLogAddDTO setResponseTime(Integer responseTime) {
|
||||
this.responseTime = responseTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package cn.iocoder.mall.admin.convert;
|
||||
|
||||
import cn.iocoder.mall.admin.api.dto.AdminAccessLogAddDTO;
|
||||
import cn.iocoder.mall.admin.dataobject.AdminAccessLogDO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mappings;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
@Mapper
|
||||
public interface AdminAccessLogConvert {
|
||||
|
||||
AdminAccessLogConvert INSTANCE = Mappers.getMapper(AdminAccessLogConvert.class);
|
||||
|
||||
@Mappings({})
|
||||
AdminAccessLogDO convert(AdminAccessLogAddDTO adminAccessLogAddDTO);
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package cn.iocoder.mall.admin.dao;
|
||||
|
||||
import cn.iocoder.mall.admin.dataobject.AdminAccessLogDO;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface AdminAccessLogMapper {
|
||||
|
||||
void insert(AdminAccessLogDO entity);
|
||||
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package cn.iocoder.mall.admin.dataobject;
|
||||
|
||||
import cn.iocoder.common.framework.dataobject.BaseDO;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 管理员访问日志 DO
|
||||
*/
|
||||
public class AdminAccessLogDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
private Integer id;
|
||||
/**
|
||||
* 管理员编号.
|
||||
*
|
||||
* 当管理员为空时,该值为0
|
||||
*/
|
||||
private Integer adminId;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
private String uri;
|
||||
/**
|
||||
* 参数
|
||||
*/
|
||||
private String queryString;
|
||||
/**
|
||||
* http 方法
|
||||
*/
|
||||
private String method;
|
||||
/**
|
||||
* userAgent
|
||||
*/
|
||||
private String userAgent;
|
||||
/**
|
||||
* ip
|
||||
*/
|
||||
private String ip;
|
||||
/**
|
||||
* 请求时间
|
||||
*/
|
||||
private Date startTime;
|
||||
/**
|
||||
* 响应时长 -- 毫秒级
|
||||
*/
|
||||
private Integer responseTime;
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public AdminAccessLogDO setId(Integer id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getAdminId() {
|
||||
return adminId;
|
||||
}
|
||||
|
||||
public AdminAccessLogDO setAdminId(Integer adminId) {
|
||||
this.adminId = adminId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public AdminAccessLogDO setUri(String uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getQueryString() {
|
||||
return queryString;
|
||||
}
|
||||
|
||||
public AdminAccessLogDO setQueryString(String queryString) {
|
||||
this.queryString = queryString;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public AdminAccessLogDO setMethod(String method) {
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
public AdminAccessLogDO setUserAgent(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getIp() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
public AdminAccessLogDO setIp(String ip) {
|
||||
this.ip = ip;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Date getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public AdminAccessLogDO setStartTime(Date startTime) {
|
||||
this.startTime = startTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getResponseTime() {
|
||||
return responseTime;
|
||||
}
|
||||
|
||||
public AdminAccessLogDO setResponseTime(Integer responseTime) {
|
||||
this.responseTime = responseTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package cn.iocoder.mall.admin.service;
|
||||
|
||||
import cn.iocoder.common.framework.util.StringUtil;
|
||||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.mall.admin.api.AdminAccessLogService;
|
||||
import cn.iocoder.mall.admin.api.dto.AdminAccessLogAddDTO;
|
||||
import cn.iocoder.mall.admin.convert.AdminAccessLogConvert;
|
||||
import cn.iocoder.mall.admin.dao.AdminAccessLogMapper;
|
||||
import cn.iocoder.mall.admin.dataobject.AdminAccessLogDO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
@com.alibaba.dubbo.config.annotation.Service(validation = "true")
|
||||
public class AdminAccessLogServiceImpl implements AdminAccessLogService {
|
||||
|
||||
/**
|
||||
* 请求参数最大长度。
|
||||
*/
|
||||
private static final Integer QUERY_STRING_MAX_LENGTH = 4096;
|
||||
/**
|
||||
* 请求地址最大长度。
|
||||
*/
|
||||
private static final Integer URI_MAX_LENGTH = 4096;
|
||||
/**
|
||||
* User-Agent 最大长度。
|
||||
*/
|
||||
private static final Integer USER_AGENT_MAX_LENGTH = 1024;
|
||||
|
||||
@Autowired
|
||||
private AdminAccessLogMapper adminAccessLogMapper;
|
||||
|
||||
@Override
|
||||
public CommonResult<Boolean> addAdminAccessLog(AdminAccessLogAddDTO adminAccessLogAddDTO) {
|
||||
// 创建 AdminAccessLogDO
|
||||
AdminAccessLogDO accessLog = AdminAccessLogConvert.INSTANCE.convert(adminAccessLogAddDTO);
|
||||
accessLog.setCreateTime(new Date());
|
||||
// 截取最大长度
|
||||
if (accessLog.getUri().length() > URI_MAX_LENGTH) {
|
||||
accessLog.setUri(StringUtil.substring(accessLog.getUri(), URI_MAX_LENGTH));
|
||||
}
|
||||
if (accessLog.getQueryString().length() > QUERY_STRING_MAX_LENGTH) {
|
||||
accessLog.setQueryString(StringUtil.substring(accessLog.getQueryString(), QUERY_STRING_MAX_LENGTH));
|
||||
}
|
||||
if (accessLog.getUserAgent().length() > USER_AGENT_MAX_LENGTH) {
|
||||
accessLog.setUserAgent(StringUtil.substring(accessLog.getUserAgent(), USER_AGENT_MAX_LENGTH));
|
||||
}
|
||||
// 插入
|
||||
adminAccessLogMapper.insert(accessLog);
|
||||
// 返回成功
|
||||
return CommonResult.success(true);
|
||||
}
|
||||
|
||||
}
|
|
@ -77,7 +77,6 @@ public class OAuth2ServiceImpl implements OAuth2Service {
|
|||
}
|
||||
// 获得管理员拥有的角色
|
||||
List<AdminRoleDO> adminRoleDOs = adminService.getAdminRoles(accessTokenDO.getAdminId());
|
||||
// TODO 芋艿,有个 bug ,要排除掉已经失效的角色
|
||||
return CommonResult.success(OAuth2Convert.INSTANCE.convertToAuthentication(accessTokenDO, adminRoleDOs));
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="cn.iocoder.mall.admin.dao.AdminAccessLogMapper">
|
||||
|
||||
<!--<sql id="FIELDS">-->
|
||||
<!--id, username, nickname, password, status,-->
|
||||
<!--create_time-->
|
||||
<!--</sql>-->
|
||||
|
||||
<insert id="insert" parameterType="AdminAccessLogDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
|
||||
INSERT INTO admin_access_log (
|
||||
admin_id, uri, query_string, method, user_agent,
|
||||
ip, start_time, response_time, create_time
|
||||
) VALUES (
|
||||
#{adminId}, #{uri}, #{queryString}, #{method}, #{userAgent},
|
||||
#{ip}, #{startTime}, #{responseTime}, #{createTime}
|
||||
)
|
||||
</insert>
|
||||
|
||||
</mapper>
|
|
@ -1,11 +1,49 @@
|
|||
package cn.iocoder.common.framework.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.Enumeration;
|
||||
|
||||
public class HttpUtil {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(HttpUtil.class);
|
||||
|
||||
/**
|
||||
* Standard Servlet 2.3+ spec request attributes for include URI and paths.
|
||||
* <p>If included via a RequestDispatcher, the current resource will see the
|
||||
* originating request. Its own URI and paths are exposed as request attributes.
|
||||
*/
|
||||
public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";
|
||||
public static final String INCLUDE_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.include.context_path";
|
||||
// public static final String INCLUDE_SERVLET_PATH_ATTRIBUTE = "javax.servlet.include.servlet_path";
|
||||
// public static final String INCLUDE_PATH_INFO_ATTRIBUTE = "javax.servlet.include.path_info";
|
||||
// public static final String INCLUDE_QUERY_STRING_ATTRIBUTE = "javax.servlet.include.query_string";
|
||||
//
|
||||
// /**
|
||||
// * Standard Servlet 2.4+ spec request attributes for forward URI and paths.
|
||||
// * <p>If forwarded to via a RequestDispatcher, the current resource will see its
|
||||
// * own URI and paths. The originating URI and paths are exposed as request attributes.
|
||||
// */
|
||||
// public static final String FORWARD_REQUEST_URI_ATTRIBUTE = "javax.servlet.forward.request_uri";
|
||||
// public static final String FORWARD_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.forward.context_path";
|
||||
// public static final String FORWARD_SERVLET_PATH_ATTRIBUTE = "javax.servlet.forward.servlet_path";
|
||||
// public static final String FORWARD_PATH_INFO_ATTRIBUTE = "javax.servlet.forward.path_info";
|
||||
// public static final String FORWARD_QUERY_STRING_ATTRIBUTE = "javax.servlet.forward.query_string";
|
||||
|
||||
/**
|
||||
* Default character encoding to use when <code>request.getCharacterEncoding</code>
|
||||
* returns <code>null</code>, according to the Servlet spec.
|
||||
*
|
||||
* @see javax.servlet.ServletRequest#getCharacterEncoding
|
||||
*/
|
||||
public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1";
|
||||
|
||||
public static String obtainAccess(HttpServletRequest request) {
|
||||
String authorization = request.getHeader("Authorization");
|
||||
if (!StringUtils.hasText(authorization)) {
|
||||
|
@ -39,4 +77,243 @@ public class HttpUtil {
|
|||
return request.getRemoteAddr();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param request 请求
|
||||
* @return ua
|
||||
*/
|
||||
public static String getUserAgent(HttpServletRequest request) {
|
||||
String ua = request.getHeader("User-Agent");
|
||||
return ua != null ? ua : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据request拼接queryString
|
||||
*
|
||||
* @return queryString
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static String buildQueryString(HttpServletRequest request) {
|
||||
Enumeration<String> es = request.getParameterNames();
|
||||
if (!es.hasMoreElements()) {
|
||||
return "";
|
||||
}
|
||||
String parameterName, parameterValue;
|
||||
StringBuilder params = new StringBuilder();
|
||||
while (es.hasMoreElements()) {
|
||||
parameterName = es.nextElement();
|
||||
parameterValue = request.getParameter(parameterName);
|
||||
params.append(parameterName).append("=").append(parameterValue).append("&");
|
||||
}
|
||||
return params.deleteCharAt(params.length() - 1).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path within the web application for the given request.
|
||||
* Detects include request URL if called within a RequestDispatcher include.
|
||||
* <p/>
|
||||
* For example, for a request to URL
|
||||
* <p/>
|
||||
* <code>http://www.somehost.com/myapp/my/url.jsp</code>,
|
||||
* <p/>
|
||||
* for an application deployed to <code>/mayapp</code> (the application's context path), this method would return
|
||||
* <p/>
|
||||
* <code>/my/url.jsp</code>.
|
||||
*
|
||||
* 该方法,是从 Shiro 源码中,扣出来。add by 芋艿
|
||||
*
|
||||
* @param request current HTTP request
|
||||
* @return the path within the web application
|
||||
*/
|
||||
public static String getPathWithinApplication(HttpServletRequest request) {
|
||||
String contextPath = getContextPath(request);
|
||||
String requestUri = getRequestUri(request);
|
||||
if (StringUtils.startsWithIgnoreCase(requestUri, contextPath)) {
|
||||
// Normal case: URI contains context path.
|
||||
String path = requestUri.substring(contextPath.length());
|
||||
return (StringUtils.hasText(path) ? path : "/");
|
||||
} else {
|
||||
// Special case: rather unusual.
|
||||
return requestUri;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the request URI for the given request, detecting an include request
|
||||
* URL if called within a RequestDispatcher include.
|
||||
* <p>As the value returned by <code>request.getRequestURI()</code> is <i>not</i>
|
||||
* decoded by the servlet container, this method will decode it.
|
||||
* <p>The URI that the web container resolves <i>should</i> be correct, but some
|
||||
* containers like JBoss/Jetty incorrectly include ";" strings like ";jsessionid"
|
||||
* in the URI. This method cuts off such incorrect appendices.
|
||||
*
|
||||
* @param request current HTTP request
|
||||
* @return the request URI
|
||||
*/
|
||||
public static String getRequestUri(HttpServletRequest request) {
|
||||
String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE);
|
||||
if (uri == null) {
|
||||
uri = request.getRequestURI();
|
||||
}
|
||||
return normalize(decodeAndCleanUriString(request, uri));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a relative URI path that may have relative values ("/./",
|
||||
* "/../", and so on ) it it. <strong>WARNING</strong> - This method is
|
||||
* useful only for normalizing application-generated paths. It does not
|
||||
* try to perform security checks for malicious input.
|
||||
* Normalize operations were was happily taken from org.apache.catalina.util.RequestUtil in
|
||||
* Tomcat trunk, r939305
|
||||
*
|
||||
* @param path Relative path to be normalized
|
||||
* @return normalized path
|
||||
*/
|
||||
public static String normalize(String path) {
|
||||
return normalize(path, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a relative URI path that may have relative values ("/./",
|
||||
* "/../", and so on ) it it. <strong>WARNING</strong> - This method is
|
||||
* useful only for normalizing application-generated paths. It does not
|
||||
* try to perform security checks for malicious input.
|
||||
* Normalize operations were was happily taken from org.apache.catalina.util.RequestUtil in
|
||||
* Tomcat trunk, r939305
|
||||
*
|
||||
* @param path Relative path to be normalized
|
||||
* @param replaceBackSlash Should '\\' be replaced with '/'
|
||||
* @return normalized path
|
||||
*/
|
||||
private static String normalize(String path, boolean replaceBackSlash) {
|
||||
|
||||
if (path == null)
|
||||
return null;
|
||||
|
||||
// Create a place for the normalized path
|
||||
String normalized = path;
|
||||
|
||||
if (replaceBackSlash && normalized.indexOf('\\') >= 0)
|
||||
normalized = normalized.replace('\\', '/');
|
||||
|
||||
if (normalized.equals("/."))
|
||||
return "/";
|
||||
|
||||
// Add a leading "/" if necessary
|
||||
if (!normalized.startsWith("/"))
|
||||
normalized = "/" + normalized;
|
||||
|
||||
// Resolve occurrences of "//" in the normalized path
|
||||
while (true) {
|
||||
int index = normalized.indexOf("//");
|
||||
if (index < 0)
|
||||
break;
|
||||
normalized = normalized.substring(0, index) +
|
||||
normalized.substring(index + 1);
|
||||
}
|
||||
|
||||
// Resolve occurrences of "/./" in the normalized path
|
||||
while (true) {
|
||||
int index = normalized.indexOf("/./");
|
||||
if (index < 0)
|
||||
break;
|
||||
normalized = normalized.substring(0, index) +
|
||||
normalized.substring(index + 2);
|
||||
}
|
||||
|
||||
// Resolve occurrences of "/../" in the normalized path
|
||||
while (true) {
|
||||
int index = normalized.indexOf("/../");
|
||||
if (index < 0)
|
||||
break;
|
||||
if (index == 0)
|
||||
return (null); // Trying to go outside our context
|
||||
int index2 = normalized.lastIndexOf('/', index - 1);
|
||||
normalized = normalized.substring(0, index2) +
|
||||
normalized.substring(index + 3);
|
||||
}
|
||||
|
||||
// Return the normalized path that we have completed
|
||||
return (normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the supplied URI string and strips any extraneous portion after a ';'.
|
||||
*
|
||||
* @param request the incoming HttpServletRequest
|
||||
* @param uri the application's URI string
|
||||
* @return the supplied URI string stripped of any extraneous portion after a ';'.
|
||||
*/
|
||||
private static String decodeAndCleanUriString(HttpServletRequest request, String uri) {
|
||||
uri = decodeRequestString(request, uri);
|
||||
int semicolonIndex = uri.indexOf(';');
|
||||
return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the context path for the given request, detecting an include request
|
||||
* URL if called within a RequestDispatcher include.
|
||||
* <p>As the value returned by <code>request.getContextPath()</code> is <i>not</i>
|
||||
* decoded by the servlet container, this method will decode it.
|
||||
*
|
||||
* @param request current HTTP request
|
||||
* @return the context path
|
||||
*/
|
||||
public static String getContextPath(HttpServletRequest request) {
|
||||
String contextPath = (String) request.getAttribute(INCLUDE_CONTEXT_PATH_ATTRIBUTE);
|
||||
if (contextPath == null) {
|
||||
contextPath = request.getContextPath();
|
||||
}
|
||||
if ("/".equals(contextPath)) {
|
||||
// Invalid case, but happens for includes on Jetty: silently adapt it.
|
||||
contextPath = "";
|
||||
}
|
||||
return decodeRequestString(request, contextPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the given source string with a URLDecoder. The encoding will be taken
|
||||
* from the request, falling back to the default "ISO-8859-1".
|
||||
* <p>The default implementation uses <code>URLDecoder.decode(input, enc)</code>.
|
||||
*
|
||||
* @param request current HTTP request
|
||||
* @param source the String to decode
|
||||
* @return the decoded String
|
||||
* @see #DEFAULT_CHARACTER_ENCODING
|
||||
* @see javax.servlet.ServletRequest#getCharacterEncoding
|
||||
* @see java.net.URLDecoder#decode(String, String)
|
||||
* @see java.net.URLDecoder#decode(String)
|
||||
*/
|
||||
@SuppressWarnings({"deprecation"})
|
||||
public static String decodeRequestString(HttpServletRequest request, String source) {
|
||||
String enc = determineEncoding(request);
|
||||
try {
|
||||
return URLDecoder.decode(source, enc);
|
||||
} catch (UnsupportedEncodingException ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Could not decode request string [" + source + "] with encoding '" + enc +
|
||||
"': falling back to platform default encoding; exception message: " + ex.getMessage());
|
||||
}
|
||||
return URLDecoder.decode(source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the encoding for the given request.
|
||||
* Can be overridden in subclasses.
|
||||
* <p>The default implementation checks the request's
|
||||
* {@link ServletRequest#getCharacterEncoding() character encoding}, and if that
|
||||
* <code>null</code>, falls back to the {@link #DEFAULT_CHARACTER_ENCODING}.
|
||||
*
|
||||
* @param request current HTTP request
|
||||
* @return the encoding for the request (never <code>null</code>)
|
||||
* @see javax.servlet.ServletRequest#getCharacterEncoding()
|
||||
*/
|
||||
protected static String determineEncoding(HttpServletRequest request) {
|
||||
String enc = request.getCharacterEncoding();
|
||||
if (enc == null) {
|
||||
enc = DEFAULT_CHARACTER_ENCODING;
|
||||
}
|
||||
return enc;
|
||||
}
|
||||
|
||||
}
|
|
@ -27,4 +27,8 @@ public class StringUtil {
|
|||
return array;
|
||||
}
|
||||
|
||||
public static String substring(String str, int start) {
|
||||
return org.apache.commons.lang3.StringUtils.substring(str, start);
|
||||
}
|
||||
|
||||
}
|
|
@ -52,7 +52,7 @@ public class AdminsProductSpuController {
|
|||
@RequestParam("sellPoint") String sellPoint,
|
||||
@RequestParam("description") String description,
|
||||
@RequestParam("cid") Integer cid,
|
||||
@RequestParam("picURLs") List<String> picUrls,
|
||||
@RequestParam("picUrls") List<String> picUrls,
|
||||
@RequestParam("visible") Boolean visible,
|
||||
@RequestParam("skuStr") String skuStr) { // TODO 芋艿,因为考虑不使用 json 接受参数,所以这里手动转。
|
||||
// 创建 ProductSpuAddDTO 对象
|
||||
|
|
|
@ -2,6 +2,7 @@ package cn.iocoder.mall.user.application.config;
|
|||
|
||||
import cn.iocoder.common.framework.config.GlobalExceptionHandler;
|
||||
import cn.iocoder.mall.admin.sdk.interceptor.AdminSecurityInterceptor;
|
||||
import cn.iocoder.mall.user.sdk.interceptor.UserAccessLogInterceptor;
|
||||
import cn.iocoder.mall.user.sdk.interceptor.UserSecurityInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
@ -18,13 +19,18 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||
public class MVCConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private UserSecurityInterceptor securityInterceptor;
|
||||
private UserSecurityInterceptor userSecurityInterceptor;
|
||||
@Autowired
|
||||
private UserAccessLogInterceptor userAccessLogInterceptor;
|
||||
@Autowired
|
||||
private AdminSecurityInterceptor adminSecurityInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(securityInterceptor).addPathPatterns("/users/**"); // 只拦截我们定义的接口
|
||||
// 用户
|
||||
registry.addInterceptor(userAccessLogInterceptor).addPathPatterns("/users/**");
|
||||
registry.addInterceptor(userSecurityInterceptor).addPathPatterns("/users/**"); // 只拦截我们定义的接口
|
||||
// 管理员
|
||||
registry.addInterceptor(adminSecurityInterceptor).addPathPatterns("/admins/**"); // 只拦截我们定义的接口
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
package cn.iocoder.mall.user.sdk.interceptor;
|
||||
|
||||
import cn.iocoder.common.framework.util.HttpUtil;
|
||||
import cn.iocoder.mall.user.service.api.UserAccessLogService;
|
||||
import cn.iocoder.mall.user.service.api.dto.UserAccessLogAddDTO;
|
||||
import com.alibaba.dubbo.config.annotation.Reference;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 访问日志拦截器
|
||||
*/
|
||||
@Component
|
||||
public class UserAccessLogInterceptor extends HandlerInterceptorAdapter {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
private static final ThreadLocal<Date> START_TIME = new ThreadLocal<>();
|
||||
/**
|
||||
* 管理员编号
|
||||
*/
|
||||
private static final ThreadLocal<Integer> USER_ID = new ThreadLocal<>();
|
||||
|
||||
@Reference
|
||||
private UserAccessLogService userAccessLogService;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
// 记录当前时间
|
||||
START_TIME.set(new Date());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||
UserAccessLogAddDTO accessLog = new UserAccessLogAddDTO();
|
||||
try {
|
||||
accessLog.setUserId(USER_ID.get());
|
||||
if (accessLog.getUserId() == null) {
|
||||
accessLog.setUserId(UserAccessLogAddDTO.USER_ID_NULL);
|
||||
}
|
||||
accessLog.setUri(request.getRequestURI()); // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。
|
||||
accessLog.setQueryString(HttpUtil.buildQueryString(request));
|
||||
accessLog.setMethod(request.getMethod());
|
||||
accessLog.setUserAgent(HttpUtil.getUserAgent(request));
|
||||
accessLog.setIp(HttpUtil.getIp(request));
|
||||
accessLog.setStartTime(START_TIME.get());
|
||||
accessLog.setResponseTime((int) (System.currentTimeMillis() - accessLog.getStartTime().getTime()));// 默认响应时间设为0
|
||||
userAccessLogService.addUserAccessLog(accessLog);
|
||||
// TODO 提升:暂时不考虑 ELK 的方案。而是基于 MySQL 存储。如果访问日志比较多,需要定期归档。
|
||||
} catch (Throwable th) {
|
||||
logger.error("[afterCompletion][插入管理员访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
|
||||
} finally {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static void setUserId(Integer userId) {
|
||||
USER_ID.set(userId);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
START_TIME.remove();
|
||||
USER_ID.remove();
|
||||
}
|
||||
|
||||
}
|
|
@ -40,6 +40,13 @@ public class UserSecurityInterceptor extends HandlerInterceptorAdapter {
|
|||
// 添加到 SecurityContext
|
||||
UserSecurityContext context = new UserSecurityContext(authentication.getUserId());
|
||||
UserSecurityContextHolder.setContext(context);
|
||||
// 同时也记录管理员编号到 AdminAccessLogInterceptor 中。因为:
|
||||
// AdminAccessLogInterceptor 需要在 AdminSecurityInterceptor 之前执行,这样记录的访问日志才健全
|
||||
// AdminSecurityInterceptor 执行后,会移除 AdminSecurityContext 信息,这就导致 AdminAccessLogInterceptor 无法获得管理员编号
|
||||
// 因此,这里需要进行记录
|
||||
if (authentication.getUserId() != null) {
|
||||
UserAccessLogInterceptor.setUserId(authentication.getUserId());
|
||||
}
|
||||
}
|
||||
// 校验是否需要已授权
|
||||
HandlerMethod method = (HandlerMethod) handler;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package cn.iocoder.mall.user.service.api;
|
||||
|
||||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.mall.user.service.api.dto.UserAccessLogAddDTO;
|
||||
|
||||
public interface UserAccessLogService {
|
||||
|
||||
CommonResult<Boolean> addUserAccessLog(UserAccessLogAddDTO userAccessLogAddDTO);
|
||||
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package cn.iocoder.mall.user.service.api.dto;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 用户访问日志添加 DTO
|
||||
*/
|
||||
public class UserAccessLogAddDTO {
|
||||
|
||||
/**
|
||||
* 用户编号 - 空
|
||||
*/
|
||||
public static final Integer USER_ID_NULL = 0;
|
||||
|
||||
/**
|
||||
* 用户编号.
|
||||
*
|
||||
* 当用户为空时,该值为0
|
||||
*/
|
||||
@NotNull(message = "用户编号不能为空")
|
||||
private Integer userId;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
@NotNull(message = "访问地址不能为空")
|
||||
private String uri;
|
||||
/**
|
||||
* 参数
|
||||
*/
|
||||
@NotNull(message = "请求参数不能为空")
|
||||
private String queryString;
|
||||
/**
|
||||
* http 方法
|
||||
*/
|
||||
@NotNull(message = "http 请求方法不能为空")
|
||||
private String method;
|
||||
/**
|
||||
* User Agent
|
||||
*/
|
||||
@NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
/**
|
||||
* ip
|
||||
*/
|
||||
@NotNull(message = "ip 不能为空")
|
||||
private String ip;
|
||||
/**
|
||||
* 请求时间
|
||||
*/
|
||||
@NotNull(message = "请求时间不能为空")
|
||||
private Date startTime;
|
||||
/**
|
||||
* 响应时长 -- 毫秒级
|
||||
*/
|
||||
@NotNull(message = "响应时长不能为空")
|
||||
private Integer responseTime;
|
||||
|
||||
public Integer getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public UserAccessLogAddDTO setUserId(Integer userId) {
|
||||
this.userId = userId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public UserAccessLogAddDTO setUri(String uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getQueryString() {
|
||||
return queryString;
|
||||
}
|
||||
|
||||
public UserAccessLogAddDTO setQueryString(String queryString) {
|
||||
this.queryString = queryString;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public UserAccessLogAddDTO setMethod(String method) {
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
public UserAccessLogAddDTO setUserAgent(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getIp() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
public UserAccessLogAddDTO setIp(String ip) {
|
||||
this.ip = ip;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Date getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public UserAccessLogAddDTO setStartTime(Date startTime) {
|
||||
this.startTime = startTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getResponseTime() {
|
||||
return responseTime;
|
||||
}
|
||||
|
||||
public UserAccessLogAddDTO setResponseTime(Integer responseTime) {
|
||||
this.responseTime = responseTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package cn.iocoder.mall.user.convert;
|
||||
|
||||
import cn.iocoder.mall.user.dataobject.UserAccessLogDO;
|
||||
import cn.iocoder.mall.user.service.api.dto.UserAccessLogAddDTO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mappings;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
@Mapper
|
||||
public interface UserAccessLogConvert {
|
||||
|
||||
UserAccessLogConvert INSTANCE = Mappers.getMapper(UserAccessLogConvert.class);
|
||||
|
||||
@Mappings({})
|
||||
UserAccessLogDO convert(UserAccessLogAddDTO adminAccessLogAddDTO);
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package cn.iocoder.mall.user.dao;
|
||||
|
||||
import cn.iocoder.mall.user.dataobject.UserAccessLogDO;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface UserAccessLogMapper {
|
||||
|
||||
void insert(UserAccessLogDO entity);
|
||||
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package cn.iocoder.mall.user.dataobject;
|
||||
|
||||
import cn.iocoder.common.framework.dataobject.BaseDO;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 用户访问日志 DO
|
||||
*/
|
||||
public class UserAccessLogDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
private Integer id;
|
||||
/**
|
||||
* 用户编号.
|
||||
*
|
||||
* 当用户编号为空时,该值为0
|
||||
*/
|
||||
private Integer userId;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
private String uri;
|
||||
/**
|
||||
* 参数
|
||||
*/
|
||||
private String queryString;
|
||||
/**
|
||||
* http 方法
|
||||
*/
|
||||
private String method;
|
||||
/**
|
||||
* userAgent
|
||||
*/
|
||||
private String userAgent;
|
||||
/**
|
||||
* ip
|
||||
*/
|
||||
private String ip;
|
||||
/**
|
||||
* 请求时间
|
||||
*/
|
||||
private Date startTime;
|
||||
/**
|
||||
* 响应时长 -- 毫秒级
|
||||
*/
|
||||
private Integer responseTime;
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public UserAccessLogDO setId(Integer id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public UserAccessLogDO setUserId(Integer userId) {
|
||||
this.userId = userId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public UserAccessLogDO setUri(String uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getQueryString() {
|
||||
return queryString;
|
||||
}
|
||||
|
||||
public UserAccessLogDO setQueryString(String queryString) {
|
||||
this.queryString = queryString;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public UserAccessLogDO setMethod(String method) {
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
public UserAccessLogDO setUserAgent(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getIp() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
public UserAccessLogDO setIp(String ip) {
|
||||
this.ip = ip;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Date getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public UserAccessLogDO setStartTime(Date startTime) {
|
||||
this.startTime = startTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getResponseTime() {
|
||||
return responseTime;
|
||||
}
|
||||
|
||||
public UserAccessLogDO setResponseTime(Integer responseTime) {
|
||||
this.responseTime = responseTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package cn.iocoder.mall.user.service;
|
||||
|
||||
import cn.iocoder.common.framework.util.StringUtil;
|
||||
import cn.iocoder.common.framework.vo.CommonResult;
|
||||
import cn.iocoder.mall.user.convert.UserAccessLogConvert;
|
||||
import cn.iocoder.mall.user.dao.UserAccessLogMapper;
|
||||
import cn.iocoder.mall.user.dataobject.UserAccessLogDO;
|
||||
import cn.iocoder.mall.user.service.api.UserAccessLogService;
|
||||
import cn.iocoder.mall.user.service.api.dto.UserAccessLogAddDTO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
@com.alibaba.dubbo.config.annotation.Service(validation = "true")
|
||||
public class UserAccessLogServiceImpl implements UserAccessLogService {
|
||||
|
||||
/**
|
||||
* 请求参数最大长度。
|
||||
*/
|
||||
private static final Integer QUERY_STRING_MAX_LENGTH = 4096;
|
||||
/**
|
||||
* 请求地址最大长度。
|
||||
*/
|
||||
private static final Integer URI_MAX_LENGTH = 4096;
|
||||
/**
|
||||
* User-Agent 最大长度。
|
||||
*/
|
||||
private static final Integer USER_AGENT_MAX_LENGTH = 1024;
|
||||
|
||||
@Autowired
|
||||
private UserAccessLogMapper userAccessLogMapper;
|
||||
|
||||
@Override
|
||||
public CommonResult<Boolean> addUserAccessLog(UserAccessLogAddDTO userAccessLogAddDTO) {
|
||||
// 创建 UserAccessLogDO
|
||||
UserAccessLogDO accessLog = UserAccessLogConvert.INSTANCE.convert(userAccessLogAddDTO);
|
||||
accessLog.setCreateTime(new Date());
|
||||
// 截取最大长度
|
||||
if (accessLog.getUri().length() > URI_MAX_LENGTH) {
|
||||
accessLog.setUri(StringUtil.substring(accessLog.getUri(), URI_MAX_LENGTH));
|
||||
}
|
||||
if (accessLog.getQueryString().length() > QUERY_STRING_MAX_LENGTH) {
|
||||
accessLog.setQueryString(StringUtil.substring(accessLog.getQueryString(), QUERY_STRING_MAX_LENGTH));
|
||||
}
|
||||
if (accessLog.getUserAgent().length() > USER_AGENT_MAX_LENGTH) {
|
||||
accessLog.setUserAgent(StringUtil.substring(accessLog.getUserAgent(), USER_AGENT_MAX_LENGTH));
|
||||
}
|
||||
// 插入
|
||||
userAccessLogMapper.insert(accessLog);
|
||||
// 返回成功
|
||||
return CommonResult.success(true);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="cn.iocoder.mall.user.dao.UserAccessLogMapper">
|
||||
|
||||
<!--<sql id="FIELDS">-->
|
||||
<!--id, username, nickname, password, status,-->
|
||||
<!--create_time-->
|
||||
<!--</sql>-->
|
||||
|
||||
<insert id="insert" parameterType="UserAccessLogDO" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
|
||||
INSERT INTO user_access_log (
|
||||
user_id, uri, query_string, method, user_agent,
|
||||
ip, start_time, response_time, create_time
|
||||
) VALUES (
|
||||
#{userId}, #{uri}, #{queryString}, #{method}, #{userAgent},
|
||||
#{ip}, #{startTime}, #{responseTime}, #{createTime}
|
||||
)
|
||||
</insert>
|
||||
|
||||
</mapper>
|
Loading…
Reference in New Issue