diff --git a/bookMarkService/web/src/main/resources/db/migration/V4__新增免验证url.sql b/bookMarkService/web/src/main/resources/db/migration/V4__新增免验证url.sql new file mode 100644 index 0000000..493d7e4 --- /dev/null +++ b/bookMarkService/web/src/main/resources/db/migration/V4__新增免验证url.sql @@ -0,0 +1 @@ +INSERT INTO `bookmark`.`url`(`method`, `url`, `type`) VALUES ('GET', '/baseInfo/verifyEmail', 0); \ No newline at end of file diff --git a/front/.gitignore b/front/.gitignore index 0f14cec..e36ebc4 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -24,3 +24,4 @@ yarn-error.log* .vscode yarn.lock .idea +public/files diff --git a/front/src/App.jsx b/front/src/App.jsx index 92ac89a..dc2980b 100644 --- a/front/src/App.jsx +++ b/front/src/App.jsx @@ -1,15 +1,16 @@ -import React, {Component} from "react"; -import {Route, Switch, Redirect} from "react-router-dom"; -import {message} from "antd"; -import {withRouter} from "react-router-dom"; -import {Provider} from "react-redux"; +import React, { Component } from "react"; +import { Route, Switch, Redirect } from "react-router-dom"; +import { message } from "antd"; +import { withRouter } from "react-router-dom"; +import { Provider } from "react-redux"; import ClipboardJS from "clipboard"; import store from "./redux"; import NotFound from "./pages/public/notFound/NotFound"; -import UserSpace from './pages/userSpace'; +import UserSpace from "./pages/userSpace"; import Login from "./pages/public/Login"; import RegisterOrReset from "./pages/public/RegisterOrReset"; +import EmailVerify from "./pages/public/EmailVerify"; import ManageOverview from "./pages/manage/OverView"; @@ -23,11 +24,11 @@ class App extends Component { componentDidMount() { //初始化clipboard let clipboard = new ClipboardJS(".copy-to-board", { - text: function (trigger) { + text: function(trigger) { return window.copyUrl; } }); - clipboard.on("success", function (e) { + clipboard.on("success", function(e) { message.success("复制成功"); e.clearSelection(); }); @@ -41,20 +42,20 @@ class App extends Component {
- {/*书签管理页面*/} - - + + {/*个人中心页面*/} - + {/* 公共页面 */} - - - - + + + + + {/* 当前面的路由都匹配不到时就会重定向到/404 */} - +
diff --git a/front/src/components/PasswordCheck.jsx b/front/src/components/PasswordCheck.jsx new file mode 100644 index 0000000..55f510e --- /dev/null +++ b/front/src/components/PasswordCheck.jsx @@ -0,0 +1,31 @@ +import React from "react"; +import { message, Modal, Input } from "antd"; +import http from "../util/httpUtil"; + +export default class PasswordCheck extends React.Component { + constructor(props) { + super(props); + this.state = { + password: "" + }; + } + + /** + * 提交 + */ + async submit() { + console.log(this.state); + let actionId = await http.post("/user/checkPassword", { password: this.state.password }); + this.props.onChange(actionId); + } + + render() { + const { visible, onClose } = this.props; + const { password } = this.state; + return ( + + this.setState({ password: e.target.value })} /> + + ); + } +} diff --git a/front/src/components/Search/index.jsx b/front/src/components/Search/index.jsx index 7e2e885..f4c7169 100644 --- a/front/src/components/Search/index.jsx +++ b/front/src/components/Search/index.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { Input, Select } from "antd"; +import { Input, Select, Empty } from "antd"; import styles from "./index.module.less"; import httpUtil from "../../util/httpUtil"; @@ -46,7 +46,7 @@ class Search extends React.Component { this.timer = setTimeout(() => { this.search(content); this.clearTimer(); - }, 300); + }, 200); } clearTimer() { @@ -119,8 +119,36 @@ class Search extends React.Component { } } + /** + * 渲染结果列表 + */ + renderResults() { + const { resultList, currentIndex, currentOptionIndex, isFocus } = this.state; + if (currentOptionIndex !== 0 || !isFocus) { + return; + } + if (resultList.length > 0) { + return ( +
+ {resultList.map((item, index) => ( +
window.open(item.url)} + > + {item.name}  + {item.url} +
+ ))} +
+ ); + } else { + return ; + } + } + render() { - const { content, resultList, currentIndex, options, currentOptionIndex } = this.state; + const { content, options, currentOptionIndex } = this.state; const prefix = ( +
+ 编辑 +
+ + +
{username}
+ + {/* 个人资料 */} +
+ + ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(UserSpace); diff --git a/front/src/pages/userSpace/components/UserInfo copy/index.module.less b/front/src/pages/userSpace/components/UserInfo copy/index.module.less new file mode 100644 index 0000000..73c023d --- /dev/null +++ b/front/src/pages/userSpace/components/UserInfo copy/index.module.less @@ -0,0 +1,41 @@ +@import "../../global.less"; +.head { + background: @mainContentColor; + padding: 0.1rem; + + display: flex; + + .icon { + position: relative; + border-radius: 5px; + @media (min-width: 768px) { + width: 150px; + height: 150px; + } + @media (max-width: 768px) { + width: 75px; + height: 75px; + } + + .full { + position: absolute; + width: 100%; + height: 100%; + } + + .changeIcon { + background-color: transparent; + color: rgba(173, 166, 166, 0); + display: flex; + justify-content: center; + align-items: center; + font-size: 2em; + } + + .changeIcon:hover { + // background-color: rbga(173, 166, 166, 1); + background-color: rgba(173, 166, 166, 0.8) !important; + color: white; + } + } +} diff --git a/front/src/pages/userSpace/components/UserInfo/index.jsx b/front/src/pages/userSpace/components/UserInfo/index.jsx new file mode 100644 index 0000000..06c3d0c --- /dev/null +++ b/front/src/pages/userSpace/components/UserInfo/index.jsx @@ -0,0 +1,181 @@ +import React from "react"; +import { message, Button, Tooltip, Input, Form } from "antd"; +import styles from "./index.module.less"; +import { connect } from "react-redux"; +import * as action from "../../../../redux/action/LoginInfoAction"; +import httpUtil from "../../../../util/httpUtil"; +import PasswordCheck from "../../../../components/PasswordCheck"; + +function mapStateToProps(state) { + return state[action.DATA_NAME]; +} + +function mapDispatchToProps(dispatch) { + return { + updateOne: (key, value) => dispatch(action.updateOne(key, value)) + }; +} + +class UserInfo extends React.Component { + constructor(props) { + super(props); + console.log(props); + this.state = { + email: null, + isEmail: false, + username: null, + isUsername: false, + password: "", + repeatPassword: "", + isPassword: false, + actionId: null, + isModelShow: false, + //当前正在提交的 + currentAction: null, + //当前正在提交的 + currentShowKey: null + }; + } + + /** + * 修改头像 + * @param {*} e + */ + async changeIcon(e) { + let file = e.target.files[0]; + if (!file || file.size > 500 * 1024) { + message.error("文件大小请勿超过500KB"); + return; + } + let formData = new FormData(); + formData.append("file", file); + let res = await httpUtil.post("/user/icon", formData, { + headers: { "Content-Type": "multipart/form-data" } + }); + console.log(res); + this.props.updateOne("icon", res); + } + + /** + * 更新 + * @param {*} itemKey 修改的字段 + * @param {*} isShowKey 是否修改字段 + */ + async submit(itemKey, isShowKey) { + if (this.state[itemKey] == null || this.state[itemKey] == "") { + message.error("请修改后重试"); + return; + } + const { password, repeatPassword, actionId, email, username } = this.state; + if (itemKey === "password" && password !== repeatPassword) { + message.error("两次密码不一致"); + return; + } + if ((itemKey === "password" || itemKey === "email") && actionId == null) { + message.warning("敏感操作,需校验密码"); + this.setState({ isModelShow: true, currentAction: itemKey, currentShowKey: isShowKey }); + return; + } + try { + if (itemKey === "password") { + await httpUtil.post("/baseInfo/password", { actionId, password }); + message.info("密码更新成功"); + } else if (itemKey === "email") { + await httpUtil.post("/baseInfo/email", { actionId, email }); + this.props.updateOne("newEmail", email); + message.info("新邮箱验证邮件已发送,请注意查收"); + } else { + await httpUtil.post("/baseInfo/username", { username }); + this.props.updateOne("username", username); + } + this.setState({ [isShowKey]: false }); + } finally { + this.setState({ actionId: null }); + } + } + + async actionIdChange(actionId) { + const { currentAction, currentShowKey } = this.state; + this.setState({ actionId, isModelShow: false, currentAction: null, currentShowKey: null }); + this.submit(currentAction, currentShowKey); + } + + /** + * 渲染用户名 + * @param {string} itemKey + * @param {string} isShowKey + */ + renderItem(label, itemKey, isShowKey) { + let repeatPassword = this.state.repeatPassword; + let itemValue = this.state[itemKey] === null ? this.props[itemKey] : this.state[itemKey]; + let isShow = this.state[isShowKey]; + let block; + if (isShow) { + block = ( +
+ {itemKey == "password" ? ( +
+ this.setState({ [itemKey]: e.target.value })} /> + this.setState({ repeatPassword: e.target.value })} /> +
+ ) : ( + this.setState({ [itemKey]: e.target.value })} /> + )} +
+ +    + +
+
+ ); + } else { + block = ( + +
this.setState({ [isShowKey]: true })} + > + {itemKey === "password" ? "********" : itemValue} +
+
+ ); + } + return ( +
+ {label.length > 0 ?
{label}
: null} + {block} +
+ ); + } + + render() { + const { isModelShow } = this.state; + const { icon, username } = this.props; + return ( +
+ {/* 头像昵称 */} +
+ icon + +
+ {/* 个人信息 */} +
+ {this.renderItem("", "username", "isUsername")} + {this.renderItem("邮箱", "email", "isEmail")} + {this.renderItem("密码", "password", "isPassword")} +
+ + this.setState({ isModelShow: false })} onChange={this.actionIdChange.bind(this)} /> +
+ ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(UserInfo); diff --git a/front/src/pages/userSpace/components/UserInfo/index.module.less b/front/src/pages/userSpace/components/UserInfo/index.module.less new file mode 100644 index 0000000..69a5ba2 --- /dev/null +++ b/front/src/pages/userSpace/components/UserInfo/index.module.less @@ -0,0 +1,68 @@ +@import "../../../../global.less"; +.head { + background: @mainContentColor; + padding: 0.1rem; + + display: flex; + + .icon { + position: relative; + border-radius: 5px; + margin-right: 1em; + @media (min-width: 768px) { + width: 150px; + height: 150px; + } + @media (max-width: 768px) { + width: 75px; + height: 75px; + } + + .full { + position: absolute; + width: 100%; + height: 100%; + } + + .changeIcon { + background-color: transparent; + color: rgba(173, 166, 166, 0); + display: flex; + justify-content: center; + align-items: center; + font-size: 2em; + } + + .changeIcon:hover { + // background-color: rbga(173, 166, 166, 1); + background-color: rgba(173, 166, 166, 0.8) !important; + color: white; + } + } + + .userinfo { + flex: 1; + + .username { + font-size: 0.4rem; + color: black; + font-weight: 600; + } + + .item { + font-size: 1.5em; + padding-top: 1em; + padding-bottom: 1em; + border-bottom: 1px solid #ebebeb; + + .label { + width: 5em; + color: black; + } + + .value { + min-width: 5em; + } + } + } +} diff --git a/front/src/pages/userSpace/index.jsx b/front/src/pages/userSpace/index.jsx index 02b1be3..38dd53e 100644 --- a/front/src/pages/userSpace/index.jsx +++ b/front/src/pages/userSpace/index.jsx @@ -1,20 +1,33 @@ import React from "react"; import styles from "./index.module.less"; import MainLayout from "../../layout/MainLayout"; +import { connect } from "react-redux"; +import * as action from "../../redux/action/LoginInfoAction"; +import UserInfo from "./components/UserInfo"; + +function mapStateToProps(state) { + return state[action.DATA_NAME]; +} + +function mapDispatchToProps(dispatch) { + return { + updateOne: (key, value) => dispatch(action.updateOne(key, value)) + }; +} class UserSpace extends React.Component { constructor(props) { super(props); this.state = {}; } - render() { + const { icon, username } = this.props; return ( -
你好
+
); } } -export default UserSpace; +export default connect(mapStateToProps, mapDispatchToProps)(UserSpace); diff --git a/front/src/pages/userSpace/index.module.less b/front/src/pages/userSpace/index.module.less index 2183220..73c023d 100644 --- a/front/src/pages/userSpace/index.module.less +++ b/front/src/pages/userSpace/index.module.less @@ -1,3 +1,41 @@ -.main{ - +@import "../../global.less"; +.head { + background: @mainContentColor; + padding: 0.1rem; + + display: flex; + + .icon { + position: relative; + border-radius: 5px; + @media (min-width: 768px) { + width: 150px; + height: 150px; + } + @media (max-width: 768px) { + width: 75px; + height: 75px; + } + + .full { + position: absolute; + width: 100%; + height: 100%; + } + + .changeIcon { + background-color: transparent; + color: rgba(173, 166, 166, 0); + display: flex; + justify-content: center; + align-items: center; + font-size: 2em; + } + + .changeIcon:hover { + // background-color: rbga(173, 166, 166, 1); + background-color: rgba(173, 166, 166, 0.8) !important; + color: white; + } + } } diff --git a/front/src/redux/action/LoginInfoAction.js b/front/src/redux/action/LoginInfoAction.js index fb73854..35afa14 100644 --- a/front/src/redux/action/LoginInfoAction.js +++ b/front/src/redux/action/LoginInfoAction.js @@ -5,8 +5,7 @@ export function getInitData() { let token = localStorage.getItem("token"); window.token = token; return { - token, - userInfo: null + token }; } @@ -30,12 +29,19 @@ export const CHANGE_USER_INFO = "changeUserInfo"; export const changeUserInfo = userInfo => { return { type: CHANGE_USER_INFO, - data: { - userInfo - } + data: Object.assign({}, userInfo) }; }; +//更新一个数据 +export const UPDATE_ONE = "updateOne"; + +export const updateOne = (key, value) => { + let data = {}; + data[key] = value; + return { type: UPDATE_ONE, data }; +}; + // 退出登录 export const LOGOUT = "logout"; diff --git a/front/src/redux/reducer/loginInfo.js b/front/src/redux/reducer/loginInfo.js index ad3d39f..2258e78 100644 --- a/front/src/redux/reducer/loginInfo.js +++ b/front/src/redux/reducer/loginInfo.js @@ -4,6 +4,7 @@ const LoginStatusReducer = (state = loginAction.getInitData(), action) => { switch (action.type) { case loginAction.CHANGE_TOKEN: case loginAction.CHANGE_USER_INFO: + case loginAction.UPDATE_ONE: case loginAction.LOGOUT: return { ...state, ...action.data }; default: diff --git a/front/src/util/httpUtil.js b/front/src/util/httpUtil.js index a2c563f..5ba57f4 100644 --- a/front/src/util/httpUtil.js +++ b/front/src/util/httpUtil.js @@ -4,7 +4,7 @@ import axios from "axios"; //定义http实例 const instance = axios.create({ baseURL: "/bookmark/api", - timeout: 15000 + timeout: 60000 }); //实例添加请求拦截器 @@ -28,7 +28,7 @@ instance.interceptors.response.use( message.error(data.message); return Promise.reject(data.message); } else { - showError(data); + showError(data, res.config.url.replace(res.config.baseURL, "")); return Promise.reject(data.message); } }, @@ -39,7 +39,10 @@ instance.interceptors.response.use( } ); -function showError(response) { +function showError(response, url) { + if (url === "/user/currentUserInfo") { + return; + } let description, message = "出问题啦"; if (response) {