diff --git a/README.md b/README.md index c6276b1..147aa02 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# inMyHeartFrontEnd +# 新双心 - 前端 -# 开发或编译前 -请根据`env.example.json`的格式按照自己的需求编写`env.json`,或者直接运行`cp env.example.json env.json`,否则无法通过编译! \ No newline at end of file +## 维护者必读 + +### 开发或编译前 +请根据`env.example.json`的格式按照自己的需求编写`env.json`,或者直接运行`cp env.example.json env.json`,**否则无法正确处理网络请求!** + +### 网络请求的用法 +引入./src/helper/axios.js(请以相对路径引入),尽情使用其中的post、get方法,它们会返回一个Promise(或可被视作Promise的AsyncFunction),且不会以抛出异常的方式通知网络错误,它会在服务器返回内容中插入属性`networkStatus`,**当值为200时才可以视作正确的服务器响应**,当值为-1时说明这是用户的网络错误,**你不需要处理200以外的情况,代码能够发出弹窗并建议用户重试** \ No newline at end of file diff --git a/package.json b/package.json index bc50585..0c717a6 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-dropzone": "^11.3.4", - "react-router-dom": "^5.2.0", - "react-scripts": "4.0.3" + "react-scripts": "4.0.3", + "sweetalert2": "^11.0.20" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.js b/src/App.js index 9b8c300..cbf1861 100644 --- a/src/App.js +++ b/src/App.js @@ -1,18 +1,19 @@ import './App.css'; -import { HashRouter, Route } from 'react-router-dom'; +import { Route, SingleRouter } from './components/SingleRouter/SingleRouter'; import { AppContainer } from './index/index'; import { UploadContainer } from './upload/upload'; import { LogInContainer } from './login/login'; import { ReviewContainer } from './review/review'; +import History from './helper/history'; function App() { return ( - - + + - + ); } diff --git a/src/components/SingleRouter/Frame.css b/src/components/SingleRouter/Frame.css new file mode 100644 index 0000000..57df597 --- /dev/null +++ b/src/components/SingleRouter/Frame.css @@ -0,0 +1,9 @@ +.full-window { + width: 100%; + height: 100%; +} + +.full-window > div { + background-size: cover !important; + background-position: center !important; +} \ No newline at end of file diff --git a/src/components/SingleRouter/SingleRouter.js b/src/components/SingleRouter/SingleRouter.js new file mode 100644 index 0000000..08a8fcf --- /dev/null +++ b/src/components/SingleRouter/SingleRouter.js @@ -0,0 +1,63 @@ +import './Frame.css'; +import { Component } from 'react'; +import History from '../../helper/history'; + +export class SingleRouter extends Component { + historyRemover = null; + constructor(props) { + super(props); + this.state = { + page: null + }; + } + + componentDidMount() { + this.historyRemover = History.init(() => { + this.setState({ + page: this.getPage() + }); + }); + this.setState({ + page: this.getPage() + }); + } + + componentWillUnmount() { + this.historyRemover?.(); + } + + getPage() { + const path = History.getHref(); + return this.props.children.filter(component => + component && (!component.props.path || + path === component.props.path || + path.replace(/[\d]*$/, ':id') === component.props.path) + ); + } + + render() { + return ( +
+ {this.state.page} +
+ ); + } +} + +export function Route(props) { + return ( +
+ {props.children ?? } +
+ ); +} + +export function Redirect(props) { + if (History.getHref().match(props.from)) + History.force(props.to); + return null; +} + +export function Link(props) { + return ({props.children}); +} \ No newline at end of file diff --git a/src/helper/alert.js b/src/helper/alert.js new file mode 100644 index 0000000..5a5a2e9 --- /dev/null +++ b/src/helper/alert.js @@ -0,0 +1,45 @@ +import Swal from 'sweetalert2'; + +const SelfSwal = Swal.mixin({ + customClass: { + confirmButton: 'btn-round-full-single', + cancelButton: 'btn-round-full-single', + }, + buttonsStyling: false, + heightAuto: false +}); + +// ajax +export const success = content => { + return SelfSwal.fire({ + html: content, + showConfirmButton: false, + timer: 1000, + icon: 'success' + }); +} +export const failed = (content, afterRetry) => { + return afterRetry ? + SelfSwal.fire({ + html: content, + showCancelButton: true, + confirmButtonText: '重试', + cancelButtonText: '放弃' + }) + .then(result => result.isConfirmed ? afterRetry() : null) : + SelfSwal.fire({ + html: content, + showConfirmButton: false, + timer: 1000, + icon: 'error' + }) + .then(() => null); +} + +export const alert = (content) => { + return SelfSwal.fire({ + html: content, + confirmButtonText: '了解', + icon: 'warning' + }); +} diff --git a/src/helper/axios.js b/src/helper/axios.js index 883fdb8..919867d 100644 --- a/src/helper/axios.js +++ b/src/helper/axios.js @@ -1,18 +1,62 @@ import axios from 'axios'; +import { failed } from './alert'; export function get(url) { - return axios.get(url, { - headers: { - Authorization: 'Bearer ' + localStorage.getItem('jwt'), - "Allow-Control-Allow-Origin": "*" - } - }); + return send( + axios.get(url, { + headers: { + Authorization: 'Bearer ' + localStorage.getItem('jwt'), + "Allow-Control-Allow-Origin": "*" + } + }), + () => get(url) + ); } export function post(url, data) { - return axios.post(url, data, { - headers: { - Authorization: 'Bearer ' + localStorage.getItem('jwt'), - "Allow-Control-Allow-Origin": "*" - } - }); + return send( + axios.post(url, data, { + headers: { + Authorization: 'Bearer ' + localStorage.getItem('jwt'), + "Allow-Control-Allow-Origin": "*" + } + }), + () => post(url, data) + ); +} + +const waitToSend = []; + +async function send(xhr, retryFunc) { + if (waitToSend.length) { + waitToSend.push(retryFunc); + return; + } + try { + const { data } = await xhr; + return { + ...data, + networkStatus: 200 + }; + } + catch (err) { + const failData = { + networkStatus: err?.response?.status ?? -1, + status: false + }; + waitToSend.push(retryFunc); + if (err.message === 'Network Error') + return await failed('您的设备似乎断网了,请检查网络后重试或刷新', flushWaitList) || failData; + else if (err?.response?.status === 401) + window.history.replaceState({}, '', '#/login'); + else if (err?.response?.status === 504) + return await failed('请求超时,请耐心等待几秒后重试或刷新', flushWaitList) || failData; + else + return await failed('服务器出现问题,请稍后重试或刷新', flushWaitList) || failData; + return failData; + } +} + +function flushWaitList() { + waitToSend.forEach(retryFunc => retryFunc()); + waitToSend.splice(0, waitToSend.length); } \ No newline at end of file diff --git a/src/helper/history.js b/src/helper/history.js new file mode 100644 index 0000000..ef919d3 --- /dev/null +++ b/src/helper/history.js @@ -0,0 +1,98 @@ +const listeners = []; +const statics = { + nowDepth: null, + forceURL: null +}; + +export default class History { + static push(url, state) { + // console.log('href: push', url, 'over', this.getHref()); + if (url === this.getHref() || this.getHref() === '/forceback') { + // console.log('href: push ignored.'); + return; + } + window.history.pushState({state, depth: ++statics.nowDepth}, null, '#' + url); + listeners.forEach(fn => fn()); + } + static replace(url, state) { + // console.log('href: substitute', url, 'for', this.getHref()); + if (url === this.getHref() || this.getHref() === '/forceback') { + // console.log('href: replace ignored.'); + return; + } + window.history.replaceState({state, depth: statics.nowDepth}, null, '#' + url); + listeners.forEach(fn => fn()); + } + static force(url) { + // console.log('href: force', this.getHref(), 'to', url); + if (url === this.getHref()) return; + const depth = window.history.state?.depth || 0; + if (depth) { + this.go(-depth); + statics.forceURL = url; + } + else + this.replace(url); + } + + static listen(listener) { + listeners.push(listener); + return () => { + for (let i in listeners) + if (listeners[i] === listener) + listeners.splice(i, 1); + }; + } + static go(d) { + window.history.go(d); + // console.log("go", d); + } + static back() { + if (statics.nowDepth === 0) + this.replace('/'); + else + window.history.back(); + // console.log("goback") + } + static forward() { + window.history.forward(); + // console.log("goforward") + } + + static getLocation() { + return document.location; + } + static getHref() { + return document.location.href.match(/#[^#]*$/)?.[0]?.replace('#', '') ?? '/'; + } + static getId() { + return this.getHref().match(/[\d]*$/)?.[0] ?? null; + } + + static init(pageChangeHandler) { + if (statics.nowDepth != null) return; + statics.nowDepth = window.history.state?.depth || 0; + window.addEventListener('hashchange', () => { + // console.log('href: hash changed to:', this.getHref()); + listeners.forEach(fn => fn()); + }); + return this.listen(() => { + const depth = window.history.state?.depth || 0; + if (depth > statics.nowDepth) { + this.go(statics.nowDepth - depth); + return; + } + else + statics.nowDepth = depth; + if (statics.forceURL) { + const replaceURL = statics.forceURL; + statics.forceURL = null; + this.replace(replaceURL); + } + else { + // 触发换页 + pageChangeHandler() + } + }); + } +}; \ No newline at end of file diff --git a/src/upload/upload.js b/src/upload/upload.js index c5df44e..a29fe21 100644 --- a/src/upload/upload.js +++ b/src/upload/upload.js @@ -4,6 +4,7 @@ import Spinner from '../components/Spinner/Spinner'; import UploadUnit from '../components/UploadUnit/UploadUnit'; import { post } from '../helper/axios'; import { apis } from '../helper/apis'; +import { alert } from '../helper/alert'; import './upload.css'; @@ -39,8 +40,13 @@ export class UploadContainer extends Component { if (this.state.submitting && (!this.state.file || this.state.url !== "")) { // upload using axios post(apis.submitMessage, { content: this.state.msg, image: this.state.url }) - .then(res => { - this.setState({ submitting: false, msg: "", url: "" }); + .then(({ data, status, networkStatus }) => { + if (networkStatus !== 200) return; + if (!status) return alert('提交内容失败:' + data); + this.setState({ submitting: false, msg: "", url: "", file: null }); + alert('内容提交成功啦').then(({ isConfirmed }) => { + if (isConfirmed) window.close(); + }); }); } }