diff --git a/public/index.html b/public/index.html index caf2be2ad..a5c42729a 100644 --- a/public/index.html +++ b/public/index.html @@ -17,7 +17,7 @@
diff --git a/src/components/Attachment/AttachmentSelectDrawer.vue b/src/components/Attachment/AttachmentSelectDrawer.vue index ce115a66f..a051583c7 100644 --- a/src/components/Attachment/AttachmentSelectDrawer.vue +++ b/src/components/Attachment/AttachmentSelectDrawer.vue @@ -14,7 +14,7 @@ align="middle" > + {{ computedText }} + + diff --git a/src/components/Editor/MarkdownEditor.vue b/src/components/Editor/MarkdownEditor.vue index a0c5eb53e..6e3ec42fe 100644 --- a/src/components/Editor/MarkdownEditor.vue +++ b/src/components/Editor/MarkdownEditor.vue @@ -51,7 +51,6 @@ export default { var responseObject = response.data var HaloEditor = this.$refs.md HaloEditor.$img2Url(pos, encodeURI(responseObject.data.path)) - this.$message.success('图片上传成功!') }) }, handleSaveDraft() { diff --git a/src/components/SettingDrawer/setting.js b/src/components/SettingDrawer/setting.js index f343de3ea..4bb9f16f5 100644 --- a/src/components/SettingDrawer/setting.js +++ b/src/components/SettingDrawer/setting.js @@ -80,7 +80,7 @@ const updateTheme = primaryColor => { javascriptEnabled: true }; ` - lessScriptNode.src = 'https://cdnjs.loli.net/ajax/libs/less.js/3.8.1/less.min.js' + lessScriptNode.src = 'https://cdn.jsdelivr.net/npm/less@3.8.1/dist/less.min.js' lessScriptNode.async = true lessScriptNode.onload = () => { buildIt() diff --git a/src/components/Upload/FilePondUpload.vue b/src/components/Upload/FilePondUpload.vue index aca76de9d..bc06ce98d 100644 --- a/src/components/Upload/FilePondUpload.vue +++ b/src/components/Upload/FilePondUpload.vue @@ -7,9 +7,9 @@ :allow-multiple="multiple" :allowRevert="false" :accepted-file-types="accept" - :maxParallelUploads="loadOptions?options.attachment_upload_max_parallel_uploads:1" - :allowImagePreview="loadOptions?options.attachment_upload_image_preview_enable:false" - :maxFiles="loadOptions?options.attachment_upload_max_files:1" + :maxParallelUploads="maxParallelUploads" + :allowImagePreview="allowImagePreview" + :maxFiles="maxFiles" labelFileProcessing="上传中" labelFileProcessingComplete="上传完成" labelFileProcessingAborted="取消上传" @@ -77,6 +77,27 @@ export default { default: true } }, + computed: { + ...mapGetters(['options']), + maxParallelUploads() { + if (this.options && this.options.length > 0) { + return this.options.attachment_upload_max_parallel_uploads + } + return 1 + }, + allowImagePreview() { + if (this.options && this.options.length > 0) { + return this.options.attachment_upload_image_preview_enable + } + return false + }, + maxFiles() { + if (this.options && this.options.length > 0) { + return this.options.attachment_upload_max_files + } + return 1 + } + }, data: function() { return { server: { @@ -120,9 +141,6 @@ export default { fileList: [] } }, - computed: { - ...mapGetters(['options']) - }, methods: { handleFilePondInit() { this.$log.debug('FilePond has initialized') diff --git a/src/components/index.js b/src/components/index.js index 5571af021..a5db01d75 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -4,12 +4,14 @@ import Ellipsis from '@/components/Ellipsis' import FooterToolbar from '@/components/FooterToolbar' import FilePondUpload from '@/components/Upload/FilePondUpload' import AttachmentSelectDrawer from './Attachment/AttachmentSelectDrawer' +import ReactiveButton from './Button/ReactiveButton' const _components = { Ellipsis, FooterToolbar, FilePondUpload, - AttachmentSelectDrawer + AttachmentSelectDrawer, + ReactiveButton } const components = {} diff --git a/src/utils/util.js b/src/utils/util.js index 7d804e69d..7d131a350 100644 --- a/src/utils/util.js +++ b/src/utils/util.js @@ -40,3 +40,7 @@ export function timeAgo(time) { export function isObject(value) { return value && typeof value === 'object' && value.constructor === Object } + +export function datetimeFormat(value, pattern = 'YYYY-MM-DD HH:mm') { + return moment(value).format(pattern) +} diff --git a/src/views/attachment/AttachmentList.vue b/src/views/attachment/AttachmentList.vue index bb8dc08e8..520780287 100644 --- a/src/views/attachment/AttachmentList.vue +++ b/src/views/attachment/AttachmentList.vue @@ -327,7 +327,7 @@ export default { this.$contextmenu({ items: [ { - label: '复制图片链接', + label: `${this.handleJudgeMediaType(item) ? '复制图片链接' : '复制文件链接'}`, onClick: () => { const text = `${encodeURI(item.path)}` this.$copyText(text) @@ -343,6 +343,7 @@ export default { divided: true }, { + disabled: !this.handleJudgeMediaType(item), label: '复制 Markdown 格式链接', onClick: () => { const text = `![${item.name}](${encodeURI(item.path)})` diff --git a/src/views/attachment/components/AttachmentDetailDrawer.vue b/src/views/attachment/components/AttachmentDetailDrawer.vue index 15b8a66bb..e2fa7e616 100644 --- a/src/views/attachment/components/AttachmentDetailDrawer.vue +++ b/src/views/attachment/components/AttachmentDetailDrawer.vue @@ -144,7 +144,15 @@ okText="确定" cancelText="取消" > - 删除 +
@@ -173,6 +181,8 @@ export default { videoPreviewVisible: false, nonsupportPreviewVisible: false, player: {}, + deleting: false, + deleteErrored: false, videoOptions: { lang: 'zh-cn', video: { @@ -214,11 +224,22 @@ export default { }, methods: { handleDeleteAttachment() { - attachmentApi.delete(this.attachment.id).then(response => { - this.$message.success('删除成功!') - this.$emit('delete', this.attachment) - this.onClose() - }) + this.deleting = true + attachmentApi + .delete(this.attachment.id) + .catch(() => { + this.deleteErrored = true + }) + .finally(() => { + setTimeout(() => { + this.deleting = false + }, 400) + }) + }, + handleDeletedCallback() { + this.$emit('delete', this.attachment) + this.deleteErrored = false + this.onClose() }, doUpdateAttachment() { if (!this.attachment.name) { diff --git a/src/views/comment/components/CommentTab.vue b/src/views/comment/components/CommentTab.vue index d537c342c..e56741458 100644 --- a/src/views/comment/components/CommentTab.vue +++ b/src/views/comment/components/CommentTab.vue @@ -402,23 +402,31 @@ destroyOnClose > - - + + - - + + - 保存 + :errored="errored" + text="保存" + loadedText="保存成功" + erroredText="保存失败" + > @@ -81,6 +86,10 @@ export default { saving: { type: Boolean, default: false + }, + errored: { + type: Boolean, + default: false } }, data() { diff --git a/src/views/system/optiontabs/GeneralTab.vue b/src/views/system/optiontabs/GeneralTab.vue index c71a81690..b9b0c9691 100644 --- a/src/views/system/optiontabs/GeneralTab.vue +++ b/src/views/system/optiontabs/GeneralTab.vue @@ -62,11 +62,16 @@ /> - 保存 + :errored="errored" + text="保存" + loadedText="保存成功" + erroredText="保存失败" + > @@ -89,6 +94,10 @@ export default { saving: { type: Boolean, default: false + }, + errored: { + type: Boolean, + default: false } }, data() { diff --git a/src/views/system/optiontabs/OtherTab.vue b/src/views/system/optiontabs/OtherTab.vue index 1d3fe564c..0b08bff28 100644 --- a/src/views/system/optiontabs/OtherTab.vue +++ b/src/views/system/optiontabs/OtherTab.vue @@ -31,23 +31,17 @@ placeholder="第三方网站统计的代码,如:Google Analytics、百度统计、CNZZ 等" /> - - 保存 + :errored="errored" + text="保存" + loadedText="保存成功" + erroredText="保存失败" + > @@ -63,6 +57,10 @@ export default { saving: { type: Boolean, default: false + }, + errored: { + type: Boolean, + default: false } }, data() { diff --git a/src/views/system/optiontabs/PermalinkTab.vue b/src/views/system/optiontabs/PermalinkTab.vue index 2893d7c89..85fbda371 100644 --- a/src/views/system/optiontabs/PermalinkTab.vue +++ b/src/views/system/optiontabs/PermalinkTab.vue @@ -9,10 +9,10 @@ > @@ -54,28 +54,33 @@ - 保存 + :errored="errored" + text="保存" + loadedText="保存成功" + erroredText="保存失败" + > @@ -92,6 +97,10 @@ export default { saving: { type: Boolean, default: false + }, + errored: { + type: Boolean, + default: false } }, data() { diff --git a/src/views/system/optiontabs/PostTab.vue b/src/views/system/optiontabs/PostTab.vue index c7c5484b5..167531662 100644 --- a/src/views/system/optiontabs/PostTab.vue +++ b/src/views/system/optiontabs/PostTab.vue @@ -51,11 +51,16 @@ /> - 保存 + :errored="errored" + text="保存" + loadedText="保存成功" + erroredText="保存失败" + > @@ -71,6 +76,10 @@ export default { saving: { type: Boolean, default: false + }, + errored: { + type: Boolean, + default: false } }, data() { diff --git a/src/views/system/optiontabs/SeoTab.vue b/src/views/system/optiontabs/SeoTab.vue index e4702a2a7..18adfd9d2 100644 --- a/src/views/system/optiontabs/SeoTab.vue +++ b/src/views/system/optiontabs/SeoTab.vue @@ -33,11 +33,16 @@ /> - 保存 + :errored="errored" + text="保存" + loadedText="保存成功" + erroredText="保存失败" + > @@ -53,6 +58,10 @@ export default { saving: { type: Boolean, default: false + }, + errored: { + type: Boolean, + default: false } }, data() { diff --git a/src/views/system/optiontabs/SmtpTab.vue b/src/views/system/optiontabs/SmtpTab.vue index a3d440f3e..0108d7f5a 100644 --- a/src/views/system/optiontabs/SmtpTab.vue +++ b/src/views/system/optiontabs/SmtpTab.vue @@ -38,11 +38,16 @@ - 保存 + :errored="errored" + text="保存" + loadedText="保存成功" + erroredText="保存失败" + > @@ -68,11 +73,16 @@ /> - 发送 + :errored="sendErrored" + text="发送" + loadedText="发送成功" + erroredText="发送失败" + > @@ -91,6 +101,10 @@ export default { saving: { type: Boolean, default: false + }, + errored: { + type: Boolean, + default: false } }, data() { @@ -103,6 +117,7 @@ export default { }, mailParam: {}, sending: false, + sendErrored: false, rules: {} } }, @@ -193,6 +208,9 @@ export default { .then(response => { this.$message.info(response.data.message) }) + .catch(() => { + this.sendErrored = true + }) .finally(() => { setTimeout(() => { this.sending = false diff --git a/src/views/user/Profile.vue b/src/views/user/Profile.vue index 0134c91cb..fabde0ded 100644 --- a/src/views/user/Profile.vue +++ b/src/views/user/Profile.vue @@ -18,16 +18,16 @@ >
{{ user.nickname }}
-
{{ user.description }}
+ >{{ userForm.model.nickname }} +
{{ userForm.model.description }}

@@ -43,27 +43,27 @@ {{ user.email }} + />{{ userForm.model.email }}

{{ statistics.establishDays || 0 }} 天 + />{{ statistics.data.establishDays || 0 }} 天

- 累计发表了 {{ statistics.postCount || 0 }} 篇文章。 - 累计创建了 {{ statistics.categoryCount || 0 }} 个分类。 - 累计创建了 {{ statistics.tagCount || 0 }} 个标签。 - 累计获得了 {{ statistics.commentCount || 0 }} 条评论。 - 累计添加了 {{ statistics.linkCount || 0 }} 个友链。 - 文章总阅读 {{ statistics.visitCount || 0 }} 次。 + 累计发表了 {{ statistics.data.postCount || 0 }} 篇文章。 + 累计创建了 {{ statistics.data.categoryCount || 0 }} 个分类。 + 累计创建了 {{ statistics.data.tagCount || 0 }} 个标签。 + 累计获得了 {{ statistics.data.commentCount || 0 }} 条评论。 + 累计添加了 {{ statistics.data.linkCount || 0 }} 个友链。 + 文章总阅读 {{ statistics.data.visitCount || 0 }} 次。
@@ -85,138 +85,182 @@ 基本资料 - - - - - - - - - - - + + + + + + + + + + + - - - + + 保存 - - + @click="handleUpdateProfile" + @callback="handleUpdatedProfileCallback" + :loading="userForm.saving" + :errored="userForm.errored" + text="保存" + loadedText="保存成功" + erroredText="保存失败" + > + + 密码 - - + + - - + + - - + + - - - + + 确认更改 - - + @click="handleUpdatePassword" + @callback="handleUpdatedPasswordCallback" + :loading="passwordForm.saving" + :errored="passwordForm.errored" + text="确认更改" + loadedText="更改成功" + erroredText="更改失败" + > + + 两步验证 - - - - - - - Authy 功能丰富 专为两步验证码 - - - iOS/Android/Windows/Mac/Linux - - - - - Chrome 扩展 - - - - - Google Authenticator 简单易用,但不支持密钥导出备份 - - - iOS - - - - - Android - - - - - Microsoft Authenticator 使用微软全家桶的推荐 - - - iOS/Android - - - - - 1Password 强大安全的密码管理付费应用 - - - iOS/Android/Windows/Mac/Linux/ChromeOS - - - - - + + + + + + + + Authy 功能丰富 专为两步验证码 + + + iOS/Android/Windows/Mac/Linux + + + + + Chrome 扩展 + + + + + Google Authenticator 简单易用,但不支持密钥导出备份 + + + iOS + + + + + Android + + + + + Microsoft Authenticator 使用微软全家桶的推荐 + + + iOS/Android + + + + + 1Password 强大安全的密码管理付费应用 + + + iOS/Android/Windows/Mac/Linux/ChromeOS + + + + + + @@ -225,7 +269,7 @@ - - + + + @@ -258,11 +327,9 @@ style="color: rgba(0,0,0,.25)" /> - - - - - + @@ -273,10 +340,13 @@ width="100%" :src="mfaParam.qrImage" /> - - + + @@ -286,8 +356,8 @@ style="color: rgba(0,0,0,.25)" /> - - + + @@ -300,15 +370,64 @@ import MD5 from 'md5.js' export default { data() { + const validateConfirmPassword = (rule, value, callback) => { + if (value && this.passwordForm.model.newPassword !== value) { + callback(new Error('确认密码与新密码不一致')) + } else { + callback() + } + } return { - statisticsLoading: false, - attachmentDrawerVisible: false, - user: {}, - statistics: {}, - passwordParam: { - oldPassword: null, - newPassword: null, - confirmPassword: null + attachmentDrawer: { + visible: false + }, + userForm: { + model: {}, + saving: false, + errored: false, + rules: { + username: [ + { required: true, message: '* 用户名不能为空', trigger: ['change', 'blur'] }, + { max: 50, message: '* 用户名的字符长度不能超过 50', trigger: ['change', 'blur'] } + ], + nickname: [ + { required: true, message: '* 用户昵称不能为空', trigger: ['change', 'blur'] }, + { max: 255, message: '* 用户昵称的字符长度不能超过 255', trigger: ['change', 'blur'] } + ], + email: [ + { required: true, message: '* 电子邮箱地址不能为空', trigger: ['change', 'blur'] }, + { type: 'email', message: '* 电子邮箱地址格式不正确', trigger: ['change', 'blur'] }, + { max: 127, message: '* 电子邮箱的字符长度不能超过 255', trigger: ['change', 'blur'] } + ], + description: [{ max: 1023, message: '* 个人说明的字符长度不能超过 1023', trigger: ['change', 'blur'] }] + } + }, + statistics: { + data: {}, + loading: false + }, + passwordForm: { + model: { + oldPassword: null, + newPassword: null, + confirmPassword: null + }, + saving: false, + errored: false, + rules: { + oldPassword: [ + { required: true, message: '* 原密码不能为空', trigger: ['change', 'blur'] }, + { max: 100, min: 8, message: '* 密码的字符长度必须在 8 - 100 之间', trigger: ['blur'] } + ], + newPassword: [ + { required: true, message: '* 新密码不能为空', trigger: ['change', 'blur'] }, + { max: 100, min: 8, message: '* 密码的字符长度必须在 8 - 100 之间', trigger: ['change', 'blur'] } + ], + confirmPassword: [ + { required: true, message: '* 确认密码不能为空', trigger: ['change', 'blur'] }, + { validator: validateConfirmPassword, trigger: ['change', 'blur'] } + ] + } }, mfaParam: { mfaKey: null, @@ -323,15 +442,16 @@ export default { switch: { loading: false, checked: false - } - }, - attachment: {} + }, + rules: { + authcode: [{ required: true, message: '* 两步验证码不能为空', trigger: ['change', 'blur'] }] + }, + saving: false, + errored: false + } } }, computed: { - passwordUpdateButtonDisabled() { - return !(this.passwordParam.oldPassword && this.passwordParam.newPassword) - }, ...mapGetters(['options']), mfaType() { return this.mfaParam.mfaType @@ -356,68 +476,82 @@ export default { methods: { ...mapMutations({ setUser: 'SET_USER' }), handleLoadStatistics() { - this.statisticsLoading = true + this.statistics.loading = true statisticsApi .statisticsWithUser() .then(response => { - this.user = response.data.data.user - this.statistics = response.data.data - this.mfaParam.mfaType = this.user.mfaType && this.user.mfaType + this.userForm.model = response.data.data.user + this.statistics.data = response.data.data + this.mfaParam.mfaType = this.userForm.model.mfaType && this.userForm.model.mfaType }) .finally(() => { setTimeout(() => { - this.statisticsLoading = false + this.statistics.loading = false }, 200) }) }, handleUpdatePassword() { - // Check confirm password - if (this.passwordParam.newPassword !== this.passwordParam.confirmPassword) { - this.$message.error('确认密码和新密码不匹配!') - return - } - userApi.updatePassword(this.passwordParam.oldPassword, this.passwordParam.newPassword).then(response => { - this.$message.success('密码修改成功!') - this.passwordParam.oldPassword = null - this.passwordParam.newPassword = null - this.passwordParam.confirmPassword = null + const _this = this + _this.$refs.passwordForm.validate(valid => { + if (valid) { + this.passwordForm.saving = true + userApi + .updatePassword(this.passwordForm.model.oldPassword, this.passwordForm.model.newPassword) + .catch(() => { + this.passwordForm.errored = true + }) + .finally(() => { + setTimeout(() => { + this.passwordForm.saving = false + }, 400) + }) + } }) }, - handleUpdateProfile() { - if (!this.user.username) { - this.$notification['error']({ - message: '提示', - description: '用户名不能为空!' - }) - return - } - if (!this.user.nickname) { - this.$notification['error']({ - message: '提示', - description: '用户昵称不能为空!' - }) - return - } - if (!this.user.email) { - this.$notification['error']({ - message: '提示', - description: '邮箱不能为空!' - }) - return + handleUpdatedPasswordCallback() { + if (this.passwordForm.errored) { + this.passwordForm.errored = false + } else { + this.passwordForm.model.oldPassword = null + this.passwordForm.model.newPassword = null + this.passwordForm.model.confirmPassword = null } - userApi.updateProfile(this.user).then(response => { - this.user = response.data.data - this.setUser(Object.assign({}, this.user)) - this.$message.success('资料更新成功!') + }, + handleUpdateProfile() { + const _this = this + _this.$refs.userForm.validate(valid => { + if (valid) { + this.userForm.saving = true + userApi + .updateProfile(this.userForm.model) + .then(response => { + this.userForm.model = response.data.data + this.setUser(Object.assign({}, this.userForm.model)) + }) + .catch(() => { + this.userForm.errored = true + }) + .finally(() => { + setTimeout(() => { + this.userForm.saving = false + }, 400) + }) + } }) }, + handleUpdatedProfileCallback() { + if (this.userForm.errored) { + this.userForm.errored = false + } + }, handleSelectAvatar(data) { - this.user.avatar = encodeURI(data.path) - this.attachmentDrawerVisible = false + this.userForm.model.avatar = encodeURI(data.path) + this.attachmentDrawer.visible = false }, handleSelectGravatar() { - this.user.avatar = '//cn.gravatar.com/avatar/' + new MD5().update(this.user.email).digest('hex') + '&d=mm' - this.attachmentDrawerVisible = false + this.userForm.model.avatar = + '//cn.gravatar.com/avatar/' + new MD5().update(this.userForm.model.email).digest('hex') + '&d=mm' + this.attachmentDrawer.visible = false }, handleMFASwitch(useMFAuth) { // loding @@ -440,19 +574,34 @@ export default { } }, handleSetMFAuth() { - var mfaType = this.mfaUsed ? 'NONE' : 'TFA_TOTP' - if (mfaType === 'NONE') { - if (!this.mfaParam.authcode) { - this.$message.warn('两步验证码不能为空!') - return + const _this = this + var mfaType = _this.mfaUsed ? 'NONE' : 'TFA_TOTP' + _this.$refs.mfaForm.validate(valid => { + if (valid) { + _this.mfaParam.saving = true + userApi + .mfaUpdate(mfaType, _this.mfaParam.mfaKey, _this.mfaParam.authcode) + .catch(() => { + _this.mfaParam.errored = true + }) + .finally(() => { + setTimeout(() => { + _this.mfaParam.saving = false + }, 400) + }) } - } - userApi.mfaUpdate(mfaType, this.mfaParam.mfaKey, this.mfaParam.authcode).then(response => { - this.handleCloseMFAuthModal() - this.mfaParam.mfaType = response.data.data.mfaType - this.$message.success(this.mfaUsed ? '两步验证已关闭!' : '两步验证已开启,下次登陆生效!') }) }, + handleSetMFAuthCallback() { + const _this = this + if (_this.mfaParam.errored) { + _this.mfaParam.errored = false + } else { + _this.handleCloseMFAuthModal() + _this.handleLoadStatistics() + _this.$message.success(_this.mfaUsed ? '两步验证已关闭!' : '两步验证已开启,下次登陆生效!') + } + }, handleCloseMFAuthModal() { this.mfaParam.modal.visible = false this.mfaParam.switch.loading = false