Feat: [前端]:修改个人信息功能完成

This commit is contained in:
fanxb 2019-11-17 17:30:35 +08:00
parent 0390308d44
commit df5fb48be0
19 changed files with 600 additions and 78 deletions

View File

@ -0,0 +1 @@
INSERT INTO `bookmark`.`url`(`method`, `url`, `type`) VALUES ('GET', '/baseInfo/verifyEmail', 0);

1
front/.gitignore vendored
View File

@ -24,3 +24,4 @@ yarn-error.log*
.vscode
yarn.lock
.idea
public/files

View File

@ -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 {
<Provider store={store}>
<div className="fullScreen" style={mainStyle}>
<Switch>
{/*书签管理页面*/}
<Redirect exact path="/" to="/manage/overview"/>
<Route exact path="/manage/overview" component={ManageOverview}/>
<Redirect exact path="/" to="/manage/overview" />
<Route exact path="/manage/overview" component={ManageOverview} />
{/*个人中心页面*/}
<Route exact path="/userSpace" component={UserSpace}/>
<Route exact path="/userSpace" component={UserSpace} />
{/* 公共页面 */}
<Route exact path="/public/login" component={Login}/>
<Route exact path="/public/register" component={RegisterOrReset}/>
<Route exact path="/public/resetPassword" component={RegisterOrReset}/>
<Route exact path="/404" component={NotFound}/>
<Route exact path="/public/login" component={Login} />
<Route exact path="/public/register" component={RegisterOrReset} />
<Route exact path="/public/resetPassword" component={RegisterOrReset} />
<Route exact path="/public/verifyEmail" component={EmailVerify} />
<Route exact path="/404" component={NotFound} />
{/* 当前面的路由都匹配不到时就会重定向到/404 */}
<Redirect path="/" to="/404"/>
<Redirect path="/" to="/404" />
</Switch>
</div>
</Provider>

View File

@ -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 (
<Modal title="密码校验" visible={visible} onOk={this.submit.bind(this)} onCancel={onClose}>
<Input.Password placeholder="输入旧密码" value={password} onChange={e => this.setState({ password: e.target.value })} />
</Modal>
);
}
}

View File

@ -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 (
<div className={styles.resultList}>
{resultList.map((item, index) => (
<div
className={`${styles.item} ${index === currentIndex ? styles.checked : ""}`}
key={item.bookmarkId}
onClick={() => window.open(item.url)}
>
<span style={{ fontWeight: 600 }}>{item.name}&emsp;</span>
<span style={{ fontSize: "0.8em", fontWeight: 400 }}>{item.url}</span>
</div>
))}
</div>
);
} else {
return <Empty className={styles.resultList} image={Empty.PRESENTED_IMAGE_SIMPLE} />;
}
}
render() {
const { content, resultList, currentIndex, options, currentOptionIndex } = this.state;
const { content, options, currentOptionIndex } = this.state;
const prefix = (
<Select value={options[currentOptionIndex]} onChange={this.valueIndexChange.bind(this)}>
{options.map((item, index) => (
@ -143,22 +171,9 @@ class Search extends React.Component {
onChange={this.contentChange.bind(this)}
onKeyDown={this.keyUp.bind(this)}
onFocus={() => this.setState({ isFocus: true })}
onBlur={() => this.setState({ isFocus: false })}
onBlur={() => setTimeout(() => this.setState({ isFocus: false }), 200)}
/>
{resultList.length > 0 ? (
<div className={styles.resultList}>
{resultList.map((item, index) => (
<div
className={`${styles.item} ${index === currentIndex ? styles.checked : ""}`}
key={item.bookmarkId}
onClick={() => window.open(item.url)}
>
<span style={{ fontWeight: 600 }}>{item.name}&emsp;</span>
<span style={{ fontSize: "0.8em", fontWeight: 400 }}>{item.url}</span>
</div>
))}
</div>
) : null}
{this.renderResults()}
</div>
);
}

View File

@ -7,8 +7,10 @@
background: white;
z-index: 1000;
width: 100%;
border: 1px solid black;
border: 1px solid #f2f4f5;
border-top: 0;
padding-top: 1em;
margin: 0;
.item {
cursor: pointer;
@ -19,19 +21,6 @@
vertical-align: baseline;
}
// .item > span {
// display: inline-block;
// padding-right: 0.5em;
// }
// .item:first-child {
// flex: 1;
// }
// .item:last-child {
// flex: 2;
// }
.item:hover {
background: #f2f4f5;
}

View File

@ -1,6 +1,7 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@ -13,6 +14,7 @@ html,
body {
width: 100%;
height: 100%;
background: #f6f6f6;
font-family: "lucida grande", "lucida sans unicode", lucida, helvetica, "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
}
.fullScreen {
@ -32,3 +34,7 @@ body {
.across-center {
align-items: center;
}
.pointer{
cursor: pointer;
}

View File

@ -20,32 +20,37 @@ function mapDispatchToProps(dispatch) {
class MainLayout extends React.Component {
constructor(props) {
super(props);
console.log(props);
this.state = {};
}
componentWillMount() {
httpUtil.get("/user/currentUserInfo").then(res => this.props.changeUserInfo(res));
async componentWillMount() {
if (!this.props.username) {
let res = await httpUtil.get("/user/currentUserInfo");
this.props.changeUserInfo(res);
}
}
renderUserArea() {
const { userInfo } = this.props;
const { username, icon } = this.props;
const menu = (
<Menu onClick={this.onClick}>
<Menu.Item key="personSpace">个人资料</Menu.Item>
<Menu.Item key="logout">退出登陆</Menu.Item>
</Menu>
);
if (userInfo !== null) {
if (username != null) {
return (
<Dropdown overlay={menu} placement="bottomCenter" trigger={["hover", "click"]}>
<span style={{ cursor: "pointer" }}>{userInfo.username}</span>
<span style={{ cursor: "pointer" }}>
<img className={styles.icon} src={icon} alt="icon" />
{username}
</span>
</Dropdown>
);
} else {
return (
<div>
<Link to="/public/login">登陆</Link>
<Link to="/public/login">登陆</Link>&emsp;
<Link to="/public/register">注册</Link>
</div>
);
@ -69,7 +74,7 @@ class MainLayout extends React.Component {
render() {
return (
<div className={"fullScreen " + styles.main}>
<div className={styles.main}>
<div className={styles.header}>
<a href="/">
<img style={{ width: "1.5rem" }} src="/img/bookmarkLogo.png" alt="logo" />
@ -77,15 +82,15 @@ class MainLayout extends React.Component {
{this.renderUserArea()}
</div>
<Divider style={{ margin: 0 }} />
<div className={styles.content}>{this.props.children}</div>
<div style={{ minHeight: `calc(${document.body.clientHeight}px - 1.45rem)` }} className={styles.content}>
{this.props.children}
</div>
<div className={styles.footer}>
开源地址<a href="https://github.com/FleyX/bookmark">github.com/FleyX/bookmark</a>
</div>
</div>
);
}
}
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps
)(MainLayout)
);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MainLayout));

View File

@ -8,7 +8,14 @@
justify-content: space-between;
align-items: center;
padding: 0.1rem 0.5rem;
height: 0.5rem;
background-color: @mainContentColor;
.icon {
width: 0.4rem;
height: 0.4rem;
border-radius: 50%;
}
}
.content {
@ -27,4 +34,13 @@
}
}
}
.footer {
text-align: center;
font-size: 1em;
margin-top: 0.1rem;
padding: 0.2rem;
height: 0.6rem;
background: @mainContentColor;
}
}

View File

@ -0,0 +1,42 @@
import React from "react";
import { Link } from "react-router-dom";
import MainLayout from "../../../layout/MainLayout/index";
import http from "../../../util/httpUtil";
import queryString from "query-string";
const style = {
"text-align": "center",
"padding-top": "200px"
};
class EmailVerify extends React.Component {
constructor(props) {
super(props);
this.state = {
message: "正在校验中,请稍候!"
};
}
async componentDidMount() {
let param = queryString.parseUrl(window.location.href);
try {
await http.get(`baseInfo/verifyEmail?secret=${param.query.key}`);
this.setState({ message: "校验成功,3s后跳转到首页" });
setTimeout(() => {
window.location.href = "/";
}, 3000);
} catch (e) {
this.setState({ message: "校验失败" });
}
}
render() {
return (
<MainLayout>
<div style={{ textAlign: "center" }}>{this.state.message}</div>
</MainLayout>
);
}
}
export default EmailVerify;

View File

@ -0,0 +1,64 @@
import React from "react";
import { message } from "antd";
import styles from "./index.module.less";
import MainLayout from "../../layout/MainLayout";
import { connect } from "react-redux";
import * as action from "../../redux/action/LoginInfoAction";
import httpUtil from "../../util/httpUtil";
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 = {};
}
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);
}
render() {
const { icon, username } = this.props;
return (
<MainLayout>
{/* 头像昵称 */}
<div className={styles.head}>
<div className={styles.icon}>
<img src={icon} alt="icon" className={styles.full} />
<label className={styles.full}>
<input type="file" style={{ display: "none" }} onChange={this.changeIcon.bind(this)} />
<div className={styles.full + " " + styles.changeIcon}>
<span>编辑</span>
</div>
</label>
</div>
<div>{username}</div>
</div>
{/* 个人资料 */}
<div></div>
</MainLayout>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(UserSpace);

View File

@ -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;
}
}
}

View File

@ -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 = (
<div>
{itemKey == "password" ? (
<div>
<Input.Password value={itemValue} placeholder="新密码" onChange={e => this.setState({ [itemKey]: e.target.value })} />
<Input.Password value={repeatPassword} placeholder="重复密码" onChange={e => this.setState({ repeatPassword: e.target.value })} />
</div>
) : (
<Input value={itemValue} onChange={e => this.setState({ [itemKey]: e.target.value })} />
)}
<div style={{ marginTop: "0.1rem" }}>
<Button type="primary" onClick={this.submit.bind(this, itemKey, isShowKey)}>
保存
</Button>
&nbsp;&nbsp;
<Button onClick={() => this.setState({ [isShowKey]: false })}>取消</Button>
</div>
</div>
);
} else {
block = (
<Tooltip title="点击编辑">
<div
className={itemKey === "username" ? styles.username : "" + " pointer " + styles.value}
onClick={() => this.setState({ [isShowKey]: true })}
>
{itemKey === "password" ? "********" : itemValue}
</div>
</Tooltip>
);
}
return (
<div className={styles.item + " flex"}>
{label.length > 0 ? <div className={styles.label}>{label}</div> : null}
{block}
</div>
);
}
render() {
const { isModelShow } = this.state;
const { icon, username } = this.props;
return (
<div className={styles.head}>
{/* 头像昵称 */}
<div className={styles.icon}>
<img src={icon} alt="icon" className={styles.full} />
<label className={styles.full}>
<input type="file" style={{ display: "none" }} onChange={this.changeIcon.bind(this)} />
<div className={styles.full + " " + styles.changeIcon}>
<span>编辑</span>
</div>
</label>
</div>
{/* 个人信息 */}
<div className={styles.userinfo}>
{this.renderItem("", "username", "isUsername")}
{this.renderItem("邮箱", "email", "isEmail")}
{this.renderItem("密码", "password", "isPassword")}
</div>
<PasswordCheck visible={isModelShow} onClose={() => this.setState({ isModelShow: false })} onChange={this.actionIdChange.bind(this)} />
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(UserInfo);

View File

@ -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;
}
}
}
}

View File

@ -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 (
<MainLayout>
<div className={styles.main}>你好</div>
<UserInfo />
</MainLayout>
);
}
}
export default UserSpace;
export default connect(mapStateToProps, mapDispatchToProps)(UserSpace);

View File

@ -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;
}
}
}

View File

@ -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";

View File

@ -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:

View File

@ -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) {