|
|
@@ -0,0 +1,267 @@
|
|
|
+<template>
|
|
|
+ <div class="article-edit">
|
|
|
+ <el-card>
|
|
|
+ <div slot="header">
|
|
|
+ <span>{{ isEdit ? '编辑文章' : '新建文章' }}</span>
|
|
|
+ <div style="float: right;">
|
|
|
+ <el-button @click="$router.push('/article-manage')">返回列表</el-button>
|
|
|
+ <el-button type="primary" @click="handleSave(false)" :loading="saving">保存草稿</el-button>
|
|
|
+ <el-button type="success" @click="handleSave(true)" :loading="publishing">发布</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-form :model="form" label-width="120px" v-loading="loading">
|
|
|
+ <el-form-item label="标题">
|
|
|
+ <el-input v-model="form.title" placeholder="请输入文章标题" maxlength="200" show-word-limit />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="分类">
|
|
|
+ <el-select v-model="form.categoryId" placeholder="选择分类" style="width: 300px;">
|
|
|
+ <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="摘要">
|
|
|
+ <el-input v-model="form.summary" type="textarea" :rows="3" placeholder="请输入文章摘要" maxlength="500" show-word-limit />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="封面图">
|
|
|
+ <el-upload
|
|
|
+ :action="uploadActionUrl"
|
|
|
+ :headers="uploadHeaders"
|
|
|
+ :on-success="handleUploadSuccess"
|
|
|
+ :on-error="handleUploadError"
|
|
|
+ :before-upload="beforeUpload"
|
|
|
+ :show-file-list="false"
|
|
|
+ accept="image/jpeg,image/png,image/gif,image/webp"
|
|
|
+ >
|
|
|
+ <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview" />
|
|
|
+ <el-button v-else type="primary" plain>+ 上传封面</el-button>
|
|
|
+ </el-upload>
|
|
|
+ <el-button v-if="form.coverImage" size="mini" type="text" style="margin-left: 10px;" @click="form.coverImage = ''">清除</el-button>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="内容">
|
|
|
+ <el-input v-model="form.content" type="textarea" :rows="15" placeholder="请输入文章内容(支持 HTML) 提示:此为纯文本编辑器,后续将升级为富文本编辑器" />
|
|
|
+ <div style="color: #999; font-size: 12px; margin-top: 4px;">当前为纯文本输入,后续将升级为富文本编辑器</div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="标签">
|
|
|
+ <el-input v-model="form.tags" placeholder="逗号分隔,如: 育儿,亲子,心理" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="作者">
|
|
|
+ <el-input v-model="form.author" placeholder="文章作者" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="阅读时间(分)">
|
|
|
+ <el-input-number v-model="form.readTime" :min="1" :max="999" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-form-item label="关联五维">
|
|
|
+ <el-checkbox-group v-model="form.relatedDimensions">
|
|
|
+ <el-checkbox label="智·金" />
|
|
|
+ <el-checkbox label="富·水" />
|
|
|
+ <el-checkbox label="行·木" />
|
|
|
+ <el-checkbox label="心·火" />
|
|
|
+ <el-checkbox label="身·土" />
|
|
|
+ </el-checkbox-group>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="浏览权限">
|
|
|
+ <el-radio-group v-model="form.visibility">
|
|
|
+ <el-radio label="public">公开</el-radio>
|
|
|
+ <el-radio label="login">登录可见</el-radio>
|
|
|
+ <el-radio label="private">私密</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import {
|
|
|
+ adminArticleCreate,
|
|
|
+ adminArticleUpdate,
|
|
|
+ adminArticlePublish,
|
|
|
+ adminArticleCategoriesList,
|
|
|
+ adminArticleDetail
|
|
|
+} from '@/api/article.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'ArticleEdit',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ isEdit: false,
|
|
|
+ loading: false,
|
|
|
+ saving: false,
|
|
|
+ publishing: false,
|
|
|
+ categories: [],
|
|
|
+ form: {
|
|
|
+ id: null,
|
|
|
+ categoryId: '',
|
|
|
+ title: '',
|
|
|
+ summary: '',
|
|
|
+ coverImage: '',
|
|
|
+ content: '',
|
|
|
+ tags: '',
|
|
|
+ author: '',
|
|
|
+ readTime: 3,
|
|
|
+ relatedDimensions: [],
|
|
|
+ visibility: 'public',
|
|
|
+ visibleTo: []
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ uploadActionUrl() {
|
|
|
+ const baseURL = process.env.VUE_APP_BASE_API || 'http://localhost:8080'
|
|
|
+ return baseURL + '/api/admin/articles/upload/image'
|
|
|
+ },
|
|
|
+ uploadHeaders() {
|
|
|
+ const token = localStorage.getItem('token')
|
|
|
+ return token ? { Authorization: 'Bearer ' + token } : {}
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ this.loadCategories()
|
|
|
+ const id = this.$route.query.id
|
|
|
+ if (id) {
|
|
|
+ this.isEdit = true
|
|
|
+ this.loadDetail(id)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async loadCategories() {
|
|
|
+ try {
|
|
|
+ const res = await adminArticleCategoriesList()
|
|
|
+ if (res.data) {
|
|
|
+ this.categories = res.data
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载分类失败', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async loadDetail(id) {
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ const res = await adminArticleDetail({ id })
|
|
|
+ if (res.data) {
|
|
|
+ const d = res.data
|
|
|
+ this.form = {
|
|
|
+ id: d.id,
|
|
|
+ categoryId: d.categoryId,
|
|
|
+ title: d.title || '',
|
|
|
+ summary: d.summary || '',
|
|
|
+ coverImage: d.coverImage || '',
|
|
|
+ content: d.content || '',
|
|
|
+ tags: d.tags || '',
|
|
|
+ author: d.author || '',
|
|
|
+ readTime: d.readTime || 3,
|
|
|
+ relatedDimensions: d.relatedDimensions || [],
|
|
|
+ visibility: d.visibility || 'public',
|
|
|
+ visibleTo: d.visibleTo || []
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ this.$message.error('加载文章详情失败')
|
|
|
+ } finally {
|
|
|
+ this.loading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ beforeUpload(file) {
|
|
|
+ const isImage = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif' || file.type === 'image/webp'
|
|
|
+ if (!isImage) {
|
|
|
+ this.$message.error('只能上传 JPG/PNG/GIF/WebP 格式图片')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ const isLt5M = file.size / 1024 / 1024 < 5
|
|
|
+ if (!isLt5M) {
|
|
|
+ this.$message.error('图片大小不能超过 5MB')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ },
|
|
|
+ handleUploadSuccess(res) {
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ this.form.coverImage = res.data.url || res.data
|
|
|
+ this.$message.success('封面上传成功')
|
|
|
+ } else {
|
|
|
+ this.$message.error(res.message || '封面上传失败')
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleUploadError() {
|
|
|
+ this.$message.error('封面上传失败,请重试')
|
|
|
+ },
|
|
|
+ buildSubmitData() {
|
|
|
+ return {
|
|
|
+ id: this.form.id,
|
|
|
+ categoryId: this.form.categoryId,
|
|
|
+ title: this.form.title,
|
|
|
+ summary: this.form.summary,
|
|
|
+ coverImage: this.form.coverImage,
|
|
|
+ content: this.form.content,
|
|
|
+ tags: this.form.tags,
|
|
|
+ author: this.form.author,
|
|
|
+ readTime: this.form.readTime,
|
|
|
+ relatedDimensions: this.form.relatedDimensions,
|
|
|
+ visibility: this.form.visibility,
|
|
|
+ visibleTo: this.form.visibleTo
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async handleSave(publishNow) {
|
|
|
+ if (publishNow) {
|
|
|
+ this.publishing = true
|
|
|
+ } else {
|
|
|
+ this.saving = true
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const data = this.buildSubmitData()
|
|
|
+ let res
|
|
|
+ if (this.isEdit) {
|
|
|
+ await adminArticleUpdate(data)
|
|
|
+ if (publishNow) {
|
|
|
+ await adminArticlePublish({ id: this.form.id, status: 'published' })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (publishNow) {
|
|
|
+ data.status = 'published'
|
|
|
+ }
|
|
|
+ res = await adminArticleCreate(data)
|
|
|
+ if (res.data && res.data.id) {
|
|
|
+ this.form.id = res.data.id
|
|
|
+ this.isEdit = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.$message.success(publishNow ? '文章已发布' : '草稿已保存')
|
|
|
+ this.$router.push('/article-manage')
|
|
|
+ } catch (e) {
|
|
|
+ this.$message.error(e.message || '保存失败')
|
|
|
+ } finally {
|
|
|
+ this.saving = false
|
|
|
+ this.publishing = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.article-edit {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+.cover-preview {
|
|
|
+ width: 240px;
|
|
|
+ max-height: 160px;
|
|
|
+ object-fit: cover;
|
|
|
+ border-radius: 6px;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+</style>
|