diff --git a/.gitignore b/.gitignore index 4d29575..488daa3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +env.json \ No newline at end of file diff --git a/README.md b/README.md index 1835f41..c6276b1 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ -#inMyHeartFrontEnd +# inMyHeartFrontEnd + +# 开发或编译前 +请根据`env.example.json`的格式按照自己的需求编写`env.json`,或者直接运行`cp env.example.json env.json`,否则无法通过编译! \ No newline at end of file diff --git a/package.json b/package.json index 4ea92a8..bc50585 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "0.1.0", "private": true, "dependencies": { + "axios": "^0.21.1", "cra-template": "1.1.2", + "dotenv": "^10.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-dropzone": "^11.3.4", diff --git a/src/App.css b/src/App.css index 57efb25..8212939 100644 --- a/src/App.css +++ b/src/App.css @@ -6,10 +6,19 @@ a.btn { border-radius: .5rem; padding: .375rem 1.75rem; font-size: 1.1rem; + cursor: pointer; + transition: filter .3s ease-out; +} +.btn:disabled { + cursor: not-allowed; + filter: grayscale(100%); } .btn:active { color: white !important; } +.btn:hover { + filter: brightness(1.2); +} button.btn { border: none; @@ -17,4 +26,18 @@ button.btn { .btn-primary { background-color: #318ffb; +} +.btn-ok { + background-color: #47c74c; +} +.btn-gray { + background-color: #d9d9d9; +} + +.btn-circle { + border-radius: 50%; + width: 40px; + height: 40px; + padding: 8px; + box-shadow: 1px 1px 4px 0 #0003; } \ No newline at end of file diff --git a/src/components/Spinner/Spinner.js b/src/components/Spinner/Spinner.js new file mode 100644 index 0000000..10c6ea7 --- /dev/null +++ b/src/components/Spinner/Spinner.js @@ -0,0 +1,17 @@ +import './spinner.css'; + +export default function Spinner(props) { + return ( +
+ + + + + + + + + +
+ ); +} \ No newline at end of file diff --git a/src/components/Spinner/spinner.css b/src/components/Spinner/spinner.css new file mode 100644 index 0000000..daab551 --- /dev/null +++ b/src/components/Spinner/spinner.css @@ -0,0 +1,19 @@ +.spinner { + width: 20px; + height: 20px; + margin: 0 auto; +} +.spinner > svg { + width: 100%; + height: 100%; + animation: spin 1s steps(8, start) infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src/components/UploadUnit/UploadUnit.js b/src/components/UploadUnit/UploadUnit.js index ca3cb2d..58eca4d 100644 --- a/src/components/UploadUnit/UploadUnit.js +++ b/src/components/UploadUnit/UploadUnit.js @@ -1,33 +1,62 @@ import './upload.css'; -import { Component } from "react"; -import { apis } from '../../resources.json'; - +import { Component, Fragment } from "react"; +import { apis } from '../../helper/apis'; +import Spinner from '../Spinner/Spinner'; export default class UploadUnit extends Component { - toBlobPromise = null; - constructor(props) { super(props); this.state = { - // 0: loading, 1: loaded, 2: compressing, 3: uploading, 4: uploaded - status: 0, - src: null + // 0: loading, 1: compressing, 2: compressed, 3: uploading, 4: uploaded + status: -1, + src: null, + file: null, + progress: 0, + width: 0 }; + this.upload = this.upload.bind(this); + this.handleCancel = this.handleCancel.bind(this); } - selected(file) { - if (file) { - if (!["image/jpeg", "image/png", "image/gif"].includes(file.type)) - return alert('请不要上传jpg、png、gif格式以外的文件!') - let reader = new FileReader(); + componentDidMount() { + setTimeout(() => { + this.readFile(this.props.file); + }, 100); + } + + componentDidUpdate() { + if (this.props.upload && this.state.status === 2) + this.upload(); + } + + handleCancel() { + this.setState({ status: -1 }); + setTimeout(() => { + this.props.onCancel(); + }, 300); + } + + readFile(file) { + if (!["image/jpeg", "image/png", "image/gif"].includes(file.type)) { + alert('请不要上传jpg、png、gif格式以外的文件!'); + this.handleCancel(); + return; + } + let reader = new FileReader(); + this.setState({ status: -2 }); + setTimeout(() => { + this.setState({ status: 0 }); reader.onload = () => { var image = new Image(); - image.onload = () => setTimeout(() => this.convert(image), 1000); + // 被注释掉的是用来应对ios巨大图片没来得及加载的问题 + // image.onload = () => setTimeout(() => this.convert(image), 1000); + image.onload = () => this.convert(image); image.src = reader.result; + this.setState({ status: 1 }); }; - reader.readAsDataURL(file); - } + reader.readAsDataURL(file); + }, 300); } convert(img) { @@ -45,21 +74,12 @@ export default class UploadUnit extends Component { canvas.width = width; canvas.height = height; canvas.getContext('2d').drawImage(img, 0, 0, width, height); - this.toBlobPromise = new Promise(res => { - canvas.toBlob(blob => { - res(blob); - }, 'image/jpeg', 0.8); - }) - this.converted = true; - this.setState({ img_base64: canvas.toDataURL('image/jpeg', 0.8) }); + canvas.toBlob(blob => { + this.setState({ src: canvas.toDataURL('image/jpeg', 0.8), status: 2, file: blob, width: width / height * 200 }); + }, 'image/jpeg', 0.8); } async upload() { - var el = document.getElementById('process-bubble'); - el.style.display = ''; - setTimeout(() => { - el.className = 'show'; - }, 100); var xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { if (xhr.readyState === 4) { @@ -70,49 +90,101 @@ export default class UploadUnit extends Component { this.props.onUpload(data.data.url); } else { - this.setState({ status: 1 }); + this.setState({ status: 2 }); this.props.onUploadError(data.msg); } } else { - this.setState({ status: 1 }); + this.setState({ status: 2 }); this.props.onUploadError('上传失败:服务器出错'); } } }; xhr.onerror = () => { - this.setState({ status: 1 }); + this.setState({ status: 2 }); this.props.onUploadError('请求失败,请检查网络'); } + xhr.onprogress = e => { + if (e.lengthComputable) { + var percent = Math.round(e.loaded * 100 / e.total); + this.setState({ progress: percent }); + } + }; xhr.open("POST", apis.uploadImage, true); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); var fd = new FormData(); - fd.append("image", await this.toBlobPromise, 'image.jpg'); + fd.append("image", this.state.file, 'image.jpg'); xhr.send(fd); + this.setState({ status: 3, progress: 0 }); } render() { + const angle = this.state.progress / 100 * Math.PI * 2; return ( -
- { - this.state.status !== 0 - ? {this.state.status +
+
+ {this.state.status >= 2 + ? 压缩后的图片 : null - } -
-
-
-
-
-
-
-
-
- -
-
- + } + {this.state.status >= 0 + ? ( + +
+ {this.state.status === 3 + ? ( +
+
+ + + +
+
+ 上传中 +
+
+ ) : null + } + {this.state.status <= 1 + ? ( +
+ +
+ {this.state.status === 0 ? '加载中' : '压缩中'} +
+
+ ) : null + } + {this.state.status === 4 + ? ( +
上传成功
+ ) : null} +
+
+ {this.state.status === 2 + ? ( + + ) : null + } + {this.state.status === 2 || this.state.status === 4 + ? ( + + ) : null + } + +
+
+ ) : null + }
); diff --git a/src/components/UploadUnit/upload.css b/src/components/UploadUnit/upload.css index e69de29..3a81628 100644 --- a/src/components/UploadUnit/upload.css +++ b/src/components/UploadUnit/upload.css @@ -0,0 +1,78 @@ +.upload-wrap { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} +.upload-unit { + width: calc(100%); + height: 200px; + min-width: 150px; + border: 2px dashed blue; + border-radius: 8px; + box-sizing: border-box; + transition: width .3s ease-out, height .3s ease-out, border .3s ease-out; + display: flex; + flex-direction: column; + align-items: center; + overflow: hidden; +} +.upload-unit.open { + width: 200px; + border-width: 0; +} +.upload-unit > img { + margin-bottom: -200px; + height: 200px; + animation: fade-in .3s ease-out; + border-radius: 8px; +} + +.upload-unit-mask { + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + transition: filter .3s ease-out; +} +.upload-unit-mask.hide { + filter: opacity(0); +} + +.upload-unit-progress { + width: 50px; + display: flex; + flex-direction: column; + align-items: center; +} +.progress-detail { + color: white; + font-size: 16px; + margin-top: 10px; +} +.progress-circle { + width: 50px; + height: 50px; +} + +.upload-btns { + display: flex; + justify-content: center; + margin-top: -60px; + z-index: 1; +} +.upload-btns > button { + margin: 0 5px; +} + +@keyframes fade-in { + from { + filter: opacity(0); + } + to { + filter: opacity(1); + } +} \ No newline at end of file diff --git a/src/env.example.json b/src/env.example.json new file mode 100644 index 0000000..4576918 --- /dev/null +++ b/src/env.example.json @@ -0,0 +1,3 @@ +{ + "backEndBaseURL": "localhost:9444" +} \ No newline at end of file diff --git a/src/helper/apis.js b/src/helper/apis.js new file mode 100644 index 0000000..1370c5f --- /dev/null +++ b/src/helper/apis.js @@ -0,0 +1,13 @@ +import { backEndBaseURL } from '../env.json'; +export const apis = { + uploadImage: "https://image.kieng.cn/upload.html?type=jd", + + login: backEndBaseURL + "/user/login", + getProfile: backEndBaseURL + "/user/me", + + submitMessage: backEndBaseURL + "/post/submit", + listEssence: backEndBaseURL + "/post/listPublished", + + listAll: backEndBaseURL + "/admin/list", + setStatus: backEndBaseURL + "/admin/setStatus" +}; \ No newline at end of file diff --git a/src/helper/axios.js b/src/helper/axios.js new file mode 100644 index 0000000..883fdb8 --- /dev/null +++ b/src/helper/axios.js @@ -0,0 +1,18 @@ +import axios from 'axios'; + +export function get(url) { + return axios.get(url, { + headers: { + Authorization: 'Bearer ' + localStorage.getItem('jwt'), + "Allow-Control-Allow-Origin": "*" + } + }); +} +export function post(url, data) { + return axios.post(url, data, { + headers: { + Authorization: 'Bearer ' + localStorage.getItem('jwt'), + "Allow-Control-Allow-Origin": "*" + } + }); +} \ No newline at end of file diff --git a/src/resources.json b/src/resources.json index 3f7e743..ac4e087 100644 --- a/src/resources.json +++ b/src/resources.json @@ -1,5 +1,5 @@ { "apis": { - "uploadImage": "https://image.kieng.cn/upload.html?type=jd" + } } \ No newline at end of file diff --git a/src/upload/upload.css b/src/upload/upload.css index 08a151c..99e557c 100644 --- a/src/upload/upload.css +++ b/src/upload/upload.css @@ -28,23 +28,40 @@ .message-box { width: 100%; height: 200px; + margin-top: 40px; } .message-box-textarea { width: 100%; resize: none; height: 100%; box-sizing: border-box; + border-radius: 8px; + font-size: 16px; + padding: 10px; +} +.message-box-textarea:focus { + outline: none; + border-color: #008cff; } .upload-box { width: 100%; height: 200px; + margin-top: 15px; } .upload-area { width: 100%; height: 100%; border: 2px dashed blue; border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + cursor: pointer; +} +.upload-area-text { + font-size: 16px; } .btn-upload { diff --git a/src/upload/upload.js b/src/upload/upload.js index 82bba48..b6dafd4 100644 --- a/src/upload/upload.js +++ b/src/upload/upload.js @@ -1,19 +1,48 @@ import { Component } from 'react'; import Dropzone from 'react-dropzone'; +import Spinner from '../components/Spinner/Spinner'; +import UploadUnit from '../components/UploadUnit/UploadUnit'; +import { post } from 'axios'; +import { apis } from '../helper/apis'; + import './upload.css'; export class UploadContainer extends Component { constructor(props) { super(props); this.state = { - files: [], - msg: "" + file: null, + msg: "", + url: "", + submitting: false }; + this.handleChange = this.handleChange.bind(this); + this.handleUploadError = this.handleUploadError.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); } - handleChange = files => { - console.log(files); - this.setState({ files }); + handleChange(files) { + if (files.length > 0) + this.setState({ file: files[0] }); + } + + handleUploadError(msg) { + this.setState({ submitting: false }); + } + + handleSubmit() { + if ((this.state.msg === "" && this.file === null) || this.state.submitting) return; + this.setState({ submitting: true }); + } + + componentDidUpdate() { + 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: "" }); + }); + } } render() { @@ -36,26 +65,38 @@ export class UploadContainer extends Component { placeholder="留下您的寄语" value={this.state.msg} onChange={e => this.setState({ msg: e.target.value })} + disabled={this.state.submitting} />
- - {({ getRootProps, getInputProps, isDragActive }) => ( -
- -

拖拽

-
- )} -
+ {this.state.file ? ( + this.setState({ url })} + onUploadError={this.handleUploadError} + onCancel={() => this.setState({ file: null, url: "" })} + upload={this.state.submitting} + /> + ) : ( + + {({ getRootProps, getInputProps, isDragActive }) => ( +
+ +
点击方框添加图片或者将图片拖入框内,支持jpg、png、gif,但均会被转换为jpg
+
+ )} +
+ )}
+ onClick={this.handleSubmit} + disabled={(this.state.msg === "" && this.state.file === null) || this.state.submitting} + >{this.state.submitting ? () : '提交'}
);