diff --git a/.env.example b/.env.example
index bab7dcc..baca2d7 100644
--- a/.env.example
+++ b/.env.example
@@ -5,5 +5,7 @@ NODE_ENV = 'example'
VUE_APP_BASE_API = 'http://x.x.x.x:80'
# github login client id
-# get it in page : https://github.com/settings/developers
-VUE_APP_GITHUB_CLIENTID='xxxxxxxxxxxxxxxxxxx'
+# get it in page : https://github.com/settings/developers
+VUE_APP_GITHUB_CLIENTID = 'xxxxxxxxxxxxxxxxxxx'
+
+VUE_APP_ICP_ID = ''
diff --git a/.gitignore b/.gitignore
index bfad2f7..f0f30bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
/context/
/log/
node_modules/
+node_modules*
dist/
tmp/
npm-debug.log*
diff --git a/README.md b/README.md
index f0bfed8..8685115 100644
--- a/README.md
+++ b/README.md
@@ -22,3 +22,8 @@ npm run lint
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
+
+
+### Todo list
+[] add admin's page
+[] how to display draft and private articles
diff --git a/package.json b/package.json
index c2abb33..b29555a 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"axios": "^0.26.1",
"babel-plugin-transform-remove-console": "^6.9.4",
"core-js": "^3.8.3",
+ "echarts": "^5.4.1",
"element-plus": "^2.2.21",
"express": "^4.18.2",
"highlight.js": "^11.5.0",
diff --git a/src/components/PagiNation/index.vue b/src/components/PagiNation/index.vue
new file mode 100644
index 0000000..d0d0f2a
--- /dev/null
+++ b/src/components/PagiNation/index.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
diff --git a/src/components/TinyMce/components/EditorImage.vue b/src/components/TinyMce/components/EditorImage.vue
new file mode 100644
index 0000000..989d4d1
--- /dev/null
+++ b/src/components/TinyMce/components/EditorImage.vue
@@ -0,0 +1,114 @@
+
+
+
+ upload
+
+
+
+
+ Click upload
+
+
+
+ Cancel
+
+
+ Confirm
+
+
+
+
+
+
+
+
diff --git a/src/components/TinyMce/dynamicLoadScript.js b/src/components/TinyMce/dynamicLoadScript.js
new file mode 100644
index 0000000..185f58d
--- /dev/null
+++ b/src/components/TinyMce/dynamicLoadScript.js
@@ -0,0 +1,59 @@
+let callbacks = []
+
+function loadedTinymce() {
+ // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144
+ // check is successfully downloaded script
+ return window.tinymce
+}
+
+const dynamicLoadScript = (src, callback) => {
+ const existingScript = document.getElementById(src)
+ const cb = callback || function() {}
+
+ if (!existingScript) {
+ const script = document.createElement('script')
+ script.src = src // src url for the third-party library being loaded.
+ script.id = src
+ document.body.appendChild(script)
+ callbacks.push(cb)
+ const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd
+ onEnd(script)
+ }
+
+ if (existingScript && cb) {
+ if (loadedTinymce()) {
+ cb(null, existingScript)
+ } else {
+ callbacks.push(cb)
+ }
+ }
+
+ function stdOnEnd(script) {
+ script.onload = function() {
+ // this.onload = null here is necessary
+ // because even IE9 works not like others
+ this.onerror = this.onload = null
+ for (const cb of callbacks) {
+ cb(null, script)
+ }
+ callbacks = null
+ }
+ script.onerror = function() {
+ this.onerror = this.onload = null
+ cb(new Error('Failed to load ' + src), script)
+ }
+ }
+
+ function ieOnEnd(script) {
+ script.onreadystatechange = function() {
+ if (this.readyState !== 'complete' && this.readyState !== 'loaded') return
+ this.onreadystatechange = null
+ for (const cb of callbacks) {
+ cb(null, script) // there is no way to catch loading errors in IE8
+ }
+ callbacks = null
+ }
+ }
+}
+
+export default dynamicLoadScript
diff --git a/src/components/TinyMce/index.vue b/src/components/TinyMce/index.vue
new file mode 100644
index 0000000..678bf37
--- /dev/null
+++ b/src/components/TinyMce/index.vue
@@ -0,0 +1,255 @@
+
+
+
+
+
+
+
diff --git a/src/components/TinyMce/plugins.js b/src/components/TinyMce/plugins.js
new file mode 100644
index 0000000..89176c5
--- /dev/null
+++ b/src/components/TinyMce/plugins.js
@@ -0,0 +1,13 @@
+// Any plugins you want to use has to be imported
+// Detail plugins list see https://www.tinymce.com/docs/plugins/
+// Custom builds see https://www.tinymce.com/download/custom-builds/
+
+// const plugins = 'advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality' +
+// ' emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste ' +
+// 'preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks ' +
+// 'visualchars wordcount'
+const plugins='wordcount anchor code table lists advlist link autolink autosave charmap emoticons fullscreen image ' +
+ 'insertdatetime nonbreaking pagebreak preview save searchreplace visualchars codesample'
+// const plugins = 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table help wordcount'
+
+export default plugins
diff --git a/src/components/TinyMce/toolbar.js b/src/components/TinyMce/toolbar.js
new file mode 100644
index 0000000..e0696e0
--- /dev/null
+++ b/src/components/TinyMce/toolbar.js
@@ -0,0 +1,14 @@
+// Here is a list of the toolbar
+// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
+
+// const toolbar = ['blocks toc searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
+// const toolbar =['blocks | bold italic underline strikethrough | image quote bullist link codesample']
+// 不需要 copy cut
+// 付费支持 advcode advtable casechange AutosaveChecklist editimage
+// 待确认 language Dropdown list with languages to apply to the selection. This item requires the content_langs option.
+// 待确认 language subscript superscript visualaid typography list autosave charmap visualblocks_ quickbars
+const toolbar= ['undo redo blocks bold italic underline strikethrough fontfamily lineheight fontsize hr align ' +
+'backcolor print removeformat selectall code table numlist bullist anchor charmap emoticons fullscreen image ' +
+'insertdatetime nonbreaking pagebreak preview save searchreplace visualchars codesample' ]
+
+export default toolbar
diff --git a/src/layout/components/MainFooter.vue b/src/layout/components/MainFooter.vue
index adb27ed..6768625 100644
--- a/src/layout/components/MainFooter.vue
+++ b/src/layout/components/MainFooter.vue
@@ -2,13 +2,23 @@
Copyright@pycoder404
- powered by pycoder404
+ {{ ICP_ID }}
+ {{ POL_ICP_ID }}
+
diff --git a/src/layout/components/MainHeader.vue b/src/layout/components/MainHeader.vue
index 4aa2160..f653e6c 100644
--- a/src/layout/components/MainHeader.vue
+++ b/src/layout/components/MainHeader.vue
@@ -4,34 +4,40 @@
-
+
@@ -45,7 +51,7 @@
import {mapGetters} from 'vuex'
// import Breadcrumb from '@/components/Breadcrumb'
// import Hamburger from '@/components/Hamburger'
- import {ref} from 'vue'
+ // import {ref} from 'vue'
import SvgIcon from '@/components/SvgIcon/index'
export default {
@@ -53,20 +59,23 @@
SvgIcon
},
data() {
- return {
- }
+ return {}
},
computed: {
...mapGetters([
- 'avatar'
+ 'avatar',
+ 'isAdmin',
])
},
methods: {
async logout() {
await this.$store.dispatch('user/logout')
+ // fixme if push to this.$route.fullPath, because the route path is not change,
+ // fixme so the page is not reloaded
+ // this.$router.push(this.$route.fullPath)
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
}
@@ -75,18 +84,19 @@
diff --git a/src/store/getters.js b/src/store/getters.js
index 46757bb..c5f60ae 100644
--- a/src/store/getters.js
+++ b/src/store/getters.js
@@ -7,5 +7,7 @@ const getters = {
username: state => state.user.username,
introduction: state => state.user.introduction,
roles: state => state.user.roles,
+ isLogin: state => state.user.roles.length > 0,
+ isAdmin: state => state.user.roles.indexOf('admin') > -1,
}
export default getters
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 96095ef..26e0c7a 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -142,10 +142,10 @@ aside {
line-height: 50px;
position: relative;
width: 100%;
- text-align: right;
+ text-align: left;
padding-right: 20px;
transition: 600ms ease position;
- background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
+ //background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
.subtitle {
font-size: 20px;
@@ -155,7 +155,9 @@ aside {
&.draft {
background: #d0d0d0;
}
-
+ &.published {
+ background: rgba(208, 208, 208, 0.38);
+ }
&.deleted {
background: #d0d0d0;
}
diff --git a/src/utils/scroll-to.js b/src/utils/scroll-to.js
new file mode 100644
index 0000000..8df5c34
--- /dev/null
+++ b/src/utils/scroll-to.js
@@ -0,0 +1,58 @@
+Math.easeInOutQuad = function(t, b, c, d) {
+ t /= d / 2
+ if (t < 1) {
+ return c / 2 * t * t + b
+ }
+ t--
+ return -c / 2 * (t * (t - 2) - 1) + b
+}
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+var requestAnimFrame = (function() {
+ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
+})()
+
+/**
+ * Because it's so fucking difficult to detect the scrolling element, just move them all
+ * @param {number} amount
+ */
+function move(amount) {
+ document.documentElement.scrollTop = amount
+ document.body.parentNode.scrollTop = amount
+ document.body.scrollTop = amount
+}
+
+function position() {
+ return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
+}
+
+/**
+ * @param {number} to
+ * @param {number} duration
+ * @param {Function} callback
+ */
+export function scrollTo(to, duration, callback) {
+ const start = position()
+ const change = to - start
+ const increment = 20
+ let currentTime = 0
+ duration = (typeof (duration) === 'undefined') ? 500 : duration
+ var animateScroll = function() {
+ // increment the time
+ currentTime += increment
+ // find the value with the quadratic in-out easing function
+ var val = Math.easeInOutQuad(currentTime, start, change, duration)
+ // move the document.body
+ move(val)
+ // do the animation unless its over
+ if (currentTime < duration) {
+ requestAnimFrame(animateScroll)
+ } else {
+ if (callback && typeof (callback) === 'function') {
+ // the animation is done so lets callback
+ callback()
+ }
+ }
+ }
+ animateScroll()
+}
diff --git a/src/views/article/components/ArticleComment.vue b/src/views/article/components/ArticleComment.vue
index a02d9e5..296e0ab 100644
--- a/src/views/article/components/ArticleComment.vue
+++ b/src/views/article/components/ArticleComment.vue
@@ -31,13 +31,22 @@
-
+
+
-
+
{{category['title']}}
- 新建分类
-
+
+ 新建分类
+
-
-
-
-
-
-
-
-
+
- 新建标签
+
+ 新建标签
+
+
+
+
+
+
+
+
+
-
+
+
-
- Submit
+
+ Publish
+
+
+ Save Draft
@@ -88,8 +108,9 @@
import StickyNav from '@/components/StickyNav' // 粘性header组件
// import { validURL } from '@/utils/validate'
import {getArticleDetail, createArticle, updateArticle} from '@/api/article'
- import {getTagList,createTag} from '@/api/tag'
- import {getCategoryList,createCategory} from '@/api/category'
+ import {getTagList, createTag} from '@/api/tag'
+ import {getCategoryList, createCategory} from '@/api/category'
+ // import TinyMce from '@/components/TinyMce'
// import { CommentDropdown, PlatformDropdown, SourceUrlDropdown } from './Dropdown'
// import axios from 'axios'
import {uploadFile} from "@/api/files";
@@ -100,6 +121,7 @@
components: {
// Upload,
StickyNav,
+ // TinyMce,
// CommentDropdown,
// PlatformDropdown,
// SourceUrlDropdown
@@ -114,10 +136,10 @@
const validateRequire = (rule, value, callback) => {
if (value === '') {
this.$message({
- message: rule.field + '为必传项',
+ message: rule.field + '必填项,请认真填写',
type: 'error'
})
- callback(new Error(rule.field + '为必传项'))
+ callback(new Error(rule.field + '必填项,请认真填写'))
} else {
callback()
}
@@ -134,9 +156,9 @@
},
loading: false,
- addCategoryDialogVisible:false,
- addTagDialogVisible:false,
- newCategory:'',
+ addCategoryDialogVisible: false,
+ addTagDialogVisible: false,
+ newCategory: '',
newTag: '',
rules: {
title: [{validator: validateRequire}],
@@ -161,9 +183,6 @@
this.postForm.display_time = new Date(val)
}
}
- // articleCategory:{
- // return this.articleCategories
- // }
},
created() {
// fixme 如果有异常了,不应该进入编辑界面,防止对原有文章的破坏
@@ -227,13 +246,13 @@
const formData = new FormData();
formData.append('upload_file', $file)
const that = this;
- uploadFile(formData).then((res)=>{
+ uploadFile(formData).then((res) => {
const img_url = res.data.url
that.$refs.editor.$img2Url(pos, img_url)
}).catch(err => {
- console.log(err)
- })
+ console.log(err)
+ })
// const formData = new FormData()
// formData.append('upload_file', blobInfo.blob(), blobInfo.filename())
// axios({
@@ -273,13 +292,14 @@
const title = 'Edit Article'
document.title = `${title} - ${this.postForm.id}`
},
- submitForm() {
+ submitForm(status = 'draft') {
console.log(this.postForm)
this.$refs.postFormRef.validate(valid => {
if (valid) {
this.loading = true
if (this.isEdit) {
const articleId = this.$route.params && this.$route.params.id
+ this.postForm['status'] = status
updateArticle(articleId, this.postForm).then((res) => {
const resp = res
this.$notify({
@@ -288,11 +308,11 @@
type: 'success',
duration: 2000
})
- this.$router.push({name:'articleDetailPage',params:{id:resp.id}})
- })
- .catch((err) => {
- console.log("Error in update article",err)
+ this.$router.push({name: 'articleDetailPage', params: {id: resp.id}})
})
+ .catch((err) => {
+ console.log("Error in update article", err)
+ })
} else {
createArticle(this.postForm).then((res) => {
const resp = res
@@ -302,11 +322,11 @@
type: 'success',
duration: 2000
})
- this.$router.push({name:'articleDetailPage',params:{id:resp.id}})
- })
- .catch((err) => {
- console.log("Error in create article",err)
+ this.$router.push({name: 'articleDetailPage', params: {id: resp.id}})
})
+ .catch((err) => {
+ console.log("Error in create article", err)
+ })
}
this.loading = false
} else {
@@ -316,7 +336,7 @@
})
},
submitToCreateCategory() {
- const data={title:this.newCategory}
+ const data = {title: this.newCategory}
createCategory(data).then(() => {
this.addCategoryDialogVisible = false
this.fetchArticleCategory()
@@ -328,25 +348,25 @@
})
})
.catch((err) => {
- console.log("Error: ",err)
+ console.log("Error: ", err)
})
},
- submitToCreateTag() {
- const data={title:this.newTag}
- createTag(data).then(() => {
- this.addTagDialogVisible = false
- this.fetchArticleTag()
- this.$notify({
- title: 'Success',
- message: 'Create Successfully',
- type: 'success',
- duration: 2000
- })
- })
- .catch((err) => {
- console.log("Error: ",err)
+ submitToCreateTag() {
+ const data = {title: this.newTag}
+ createTag(data).then(() => {
+ this.addTagDialogVisible = false
+ this.fetchArticleTag()
+ this.$notify({
+ title: 'Success',
+ message: 'Create Successfully',
+ type: 'success',
+ duration: 2000
+ })
})
- }
+ .catch((err) => {
+ console.log("Error: ", err)
+ })
+ }
}
}
@@ -384,6 +404,10 @@
}
}
+ .mar-left {
+ margin-left: 10px;
+ }
+
.article-textarea ::v-deep {
textarea {
padding-right: 40px;
diff --git a/src/views/article/detail.vue b/src/views/article/detail.vue
index 8fb2d30..22a4d92 100644
--- a/src/views/article/detail.vue
+++ b/src/views/article/detail.vue
@@ -26,7 +26,7 @@
@{{articleDetail.created_time }} {{ articleDetail.author }},views:{{articleDetail.views_count}} likes:{{articleDetail.likes_count}} comments:0
import {getArticleDetail} from '@/api/article'
- // import SvgIcon from '@/components/SvgIcon/index'
import LikeFavorite from "@/views/article/components/LikeFavorite"
import CategoryAndTag from "@/views/article/components/CategoryAndTag"
@@ -90,10 +89,6 @@
const data = response
data['toc'] = response['toc'].replaceAll('
-
-
-
-
-
- {{article.title}}
-
-
-
-
-
-
- @{{article.created_time }} {{ article.author }}, views:{{article.views_count}} likes:{{article.likes_count}} comments:{{article.comments_count}}
-
+
+
+
+
+
+
-
-
-
+ >{{article.title}}
+
+
+
+
+
+
+
+ @{{article.created_time }} {{ article.author }}, views:{{article.views_count}} likes:{{article.likes_count}} comments:{{article.comments_count}}
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
@@ -96,7 +121,7 @@
}
.article-main {
- margin-top:10px;
+ margin-top: 10px;
box-shadow: 1px 2px 3px #ddd;
border: 1px solid #ddd;
border-radius: 5px;
@@ -112,6 +137,17 @@
}
+ .content-main {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ background-color: #f7f5f5;
+ flex-direction: column;
+ justify-content: space-between;
+ box-sizing: border-box;
+
+ }
+
.article-body {
padding: 10px
}