添加首页文章页面
This commit is contained in:
@@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<div class="article-publish-page">
|
||||
<!-- 页面标题:根据是否编辑动态显示 -->
|
||||
<div class="page-header">
|
||||
<h1>{{ articleId ? '编辑文章' : '发表新文章' }}</h1>
|
||||
<p class="page-desc">{{ articleId ? '修改文章内容,更新后即时生效' : '创作优质内容,分享你的观点' }}</p>
|
||||
<h1>编辑文章</h1>
|
||||
<p class="page-desc">修改文章内容,更新后即时生效</p>
|
||||
</div>
|
||||
|
||||
<!-- 发表/编辑表单容器 -->
|
||||
<div class="publish-form-container" v-loading="isLoading">
|
||||
<!-- 1. 文章标题输入 -->
|
||||
<div class="form-group title-group">
|
||||
<label class="form-label">文章标题</label>
|
||||
<input
|
||||
@@ -17,15 +14,13 @@
|
||||
class="title-input"
|
||||
placeholder="请输入文章标题(不少于5个字,不超过50字)"
|
||||
@input="checkTitleValid"
|
||||
:disabled="isSubmitting || isSavingDraft"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<!-- 标题校验提示 -->
|
||||
<p class="valid-hint" :class="{ error: !titleIsValid && articleTitle.length > 0 }">
|
||||
{{ titleHintText }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 2. 专题选择 -->
|
||||
<div class="form-group topic-group">
|
||||
<label class="form-label">发表到专题</label>
|
||||
<p class="form-desc">选择文章所属的专题分类</p>
|
||||
@@ -33,10 +28,9 @@
|
||||
<el-radio-group
|
||||
v-model="selectedTopic"
|
||||
class="topic-radio-group"
|
||||
:disabled="isSubmitting || isSavingDraft"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<el-radio label="news" class="topic-radio">
|
||||
<!-- <el-icon><News /></el-icon> -->
|
||||
<span>新闻</span>
|
||||
</el-radio>
|
||||
<el-radio label="cases" class="topic-radio">
|
||||
@@ -58,26 +52,23 @@
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 3. 封面图上传 -->
|
||||
<div class="form-group cover-group">
|
||||
<label class="form-label">文章封面</label>
|
||||
<p class="form-desc">建议上传16:9比例图片,支持JPG/PNG/WEBP格式,大小不超过5MB</p>
|
||||
|
||||
<div class="cover-uploader">
|
||||
<!-- 已上传封面预览(编辑时加载已有封面) -->
|
||||
<div v-if="coverUrl" class="cover-preview">
|
||||
<img :src="coverUrl" alt="文章封面" class="cover-img">
|
||||
<button
|
||||
class="remove-cover-btn"
|
||||
@click="removeCover"
|
||||
title="删除封面"
|
||||
:disabled="isSubmitting || isSavingDraft"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<el-icon><Close /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 未上传时的上传区域 -->
|
||||
<el-upload
|
||||
v-else
|
||||
class="cover-upload-area"
|
||||
@@ -87,7 +78,7 @@
|
||||
:on-success="handleCoverSuccess"
|
||||
:before-upload="beforeCoverUpload"
|
||||
:on-error="handleCoverError"
|
||||
:disabled="isSubmitting || isSavingDraft"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<div class="upload-placeholder">
|
||||
<el-icon class="upload-icon"><Upload /></el-icon>
|
||||
@@ -98,17 +89,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 文章内容富文本编辑(编辑时加载已有内容) -->
|
||||
<div class="form-group content-group">
|
||||
<label class="form-label">文章内容</label>
|
||||
<p class="form-desc">请使用编辑器创作文章,支持文字、图片、代码块等格式</p>
|
||||
|
||||
<!-- 富文本编辑器容器 -->
|
||||
<div class="editor-wrapper">
|
||||
<div ref="editor" class="quill-editor"></div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑器提示(字数统计/状态) -->
|
||||
<p class="editor-hint">
|
||||
<span v-if="wordCount !== null">字数:{{ wordCount }}</span>
|
||||
<span class="content-required" :class="{ active: !contentIsValid }">
|
||||
@@ -117,20 +105,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 5. 底部操作按钮 -->
|
||||
<div class="form-actions">
|
||||
<el-button
|
||||
type="default"
|
||||
size="large"
|
||||
class="draft-btn"
|
||||
@click="saveDraft"
|
||||
:loading="isSavingDraft"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<el-icon v-if="isSavingDraft"><Loading /></el-icon>
|
||||
<span>保存草稿</span>
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@@ -140,7 +115,7 @@
|
||||
:loading="isSubmitting"
|
||||
>
|
||||
<el-icon v-if="isSubmitting"><Loading /></el-icon>
|
||||
<span>{{ articleId ? '更新文章' : '发布文章' }}</span>
|
||||
<span>更新文章</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -161,7 +136,7 @@ import type { UploadProps, LoadingInstance } from 'element-plus';
|
||||
// 路由相关:获取编辑的文章ID(从路由参数如 /article/edit/123 中获取)
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const articleId = ref<number | null>(null); // 文章ID:null=新增,有值=编辑
|
||||
const articleId = ref<number | null>(null); // 文章ID:必须有值,在 onMounted 中检查
|
||||
|
||||
// 表单核心字段
|
||||
const articleTitle = ref(''); // 标题
|
||||
@@ -171,9 +146,8 @@ const editor = ref<HTMLDivElement | null>(null); // 富文本容器
|
||||
let quillInstance: Quill | null = null; // 富文本实例
|
||||
|
||||
// 加载/提交状态
|
||||
const isLoading = ref(false); // 整体加载(如加载详情)
|
||||
const isSubmitting = ref(false); // 提交中(发布/更新)
|
||||
const isSavingDraft = ref(false); // 保存草稿中
|
||||
const isLoading = ref(true); // 初始为 true,因为必须加载数据
|
||||
const isSubmitting = ref(false); // 提交中(更新)
|
||||
let loadingInstance: LoadingInstance | null = null; // 加载遮罩实例
|
||||
|
||||
// -------------------------- 表单校验 --------------------------
|
||||
@@ -223,7 +197,7 @@ const wordCount = computed(() => {
|
||||
|
||||
// 表单整体有效性(提交按钮是否可点击)
|
||||
const isFormValid = computed(() => {
|
||||
return titleIsValid.value && contentIsValid.value && !isSubmitting.value && !isSavingDraft.value;
|
||||
return titleIsValid.value && contentIsValid.value && !isSubmitting.value;
|
||||
});
|
||||
|
||||
// -------------------------- 封面上传逻辑 --------------------------
|
||||
@@ -359,9 +333,8 @@ async function uploadEditorImage(file: File, insertIndex: number) {
|
||||
|
||||
|
||||
// -------------------------- 编辑核心:加载文章详情 --------------------------
|
||||
// 根据文章ID获取详情(编辑时调用)
|
||||
// 根据文章ID获取详情
|
||||
const fetchArticleDetail = async (id: number) => {
|
||||
isLoading.value = true;
|
||||
loadingInstance = ElLoading.service({
|
||||
lock: true,
|
||||
text: '正在加载文章数据...',
|
||||
@@ -398,40 +371,9 @@ const fetchArticleDetail = async (id: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------------- 草稿保存逻辑 --------------------------
|
||||
const saveDraft = async () => {
|
||||
if (!titleIsValid.value && articleTitle.value.trim().length > 0) {
|
||||
ElMessage.warning('标题格式不正确,请调整后再保存');
|
||||
return;
|
||||
}
|
||||
|
||||
isSavingDraft.value = true;
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
const draftData = {
|
||||
id: articleId.value,
|
||||
title: articleTitle.value.trim() || '未命名草稿',
|
||||
topic: selectedTopic.value,
|
||||
coverUrl: coverUrl.value,
|
||||
contentHtml: quillInstance?.root.innerHTML || '',
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
localStorage.setItem('articleDraft', JSON.stringify(draftData));
|
||||
ElMessage.success('草稿保存成功');
|
||||
console.log('【草稿保存】当前草稿数据:', draftData);
|
||||
|
||||
} catch (err) {
|
||||
ElMessage.error(`草稿保存失败:${err instanceof Error ? err.message : '未知错误'}`);
|
||||
} finally {
|
||||
isSavingDraft.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------------- 提交逻辑(区分新增/编辑) --------------------------
|
||||
// -------------------------- 提交逻辑(仅更新) --------------------------
|
||||
const submitArticle = async () => {
|
||||
if (!isFormValid.value) return;
|
||||
if (!isFormValid.value || !articleId.value) return;
|
||||
|
||||
const submitData = {
|
||||
id: articleId.value,
|
||||
@@ -442,38 +384,28 @@ const submitArticle = async () => {
|
||||
excerpt: quillInstance?.getText().trim().slice(0, 150) || ''
|
||||
};
|
||||
|
||||
console.log('【文章提交】修改后的完整数据:', submitData);
|
||||
console.log("修改文章信息:",submitData);
|
||||
|
||||
console.log('【文章更新】提交的完整数据:', submitData);
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
let response: Response;
|
||||
const apiBase = 'http://localhost:8080/api/articles';
|
||||
const response = await fetch(`http://localhost:8080/api/articles/${articleId.value}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(submitData),
|
||||
});
|
||||
|
||||
if (articleId.value) {
|
||||
response = await fetch(`${apiBase}/${articleId.value}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(submitData),
|
||||
});
|
||||
} else {
|
||||
response = await fetch(apiBase, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(submitData),
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) throw new Error(`提交失败,HTTP状态码:${response.status}`);
|
||||
if (!response.ok) throw new Error(`更新失败,HTTP状态码:${response.status}`);
|
||||
const result = await response.json();
|
||||
|
||||
const successMsg = articleId.value ? '文章更新成功' : '文章发布成功';
|
||||
ElMessage.success(`${successMsg}!即将跳转到文章列表`);
|
||||
console.log('【提交成功】后端返回结果:', result);
|
||||
ElMessage.success('文章更新成功!即将跳转到文章列表');
|
||||
console.log('【更新成功】后端返回结果:', result);
|
||||
|
||||
setTimeout(() => router.push('/articles'), 1500);
|
||||
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : '网络错误';
|
||||
ElMessage.error(`${articleId.value ? '更新' : '发布'}文章失败:${errMsg}`);
|
||||
ElMessage.error(`更新文章失败:${errMsg}`);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
@@ -485,26 +417,17 @@ onMounted(() => {
|
||||
|
||||
const idFromRoute = route.params.id;
|
||||
if (idFromRoute && typeof idFromRoute === 'string') {
|
||||
articleId.value = parseInt(idFromRoute, 10);
|
||||
if (!isNaN(articleId.value)) {
|
||||
fetchArticleDetail(articleId.value);
|
||||
const parsedId = parseInt(idFromRoute, 10);
|
||||
if (!isNaN(parsedId)) {
|
||||
articleId.value = parsedId;
|
||||
fetchArticleDetail(parsedId);
|
||||
} else {
|
||||
ElMessage.error('无效的文章ID');
|
||||
ElMessage.error('无效的文章ID,请检查链接');
|
||||
router.push('/articles');
|
||||
}
|
||||
} else {
|
||||
const savedDraft = localStorage.getItem('articleDraft');
|
||||
if (savedDraft) {
|
||||
const draft = JSON.parse(savedDraft);
|
||||
articleTitle.value = draft.title || '';
|
||||
selectedTopic.value = draft.topic || 'news';
|
||||
coverUrl.value = draft.coverUrl || '';
|
||||
if (quillInstance && draft.contentHtml) {
|
||||
quillInstance.root.innerHTML = draft.contentHtml;
|
||||
setTimeout(checkContentValid, 100);
|
||||
}
|
||||
checkTitleValid();
|
||||
}
|
||||
ElMessage.error('未找到文章ID,无法进行编辑');
|
||||
router.push('/articles');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -906,51 +829,26 @@ $transition: all 0.3s ease;
|
||||
gap: 16px;
|
||||
margin-top: 40px;
|
||||
|
||||
.draft-btn, .publish-btn {
|
||||
.publish-btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: $radius;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: $transition;
|
||||
background-color: $primary-color;
|
||||
border-color: $primary-color;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
background-color: #0E4BD8;
|
||||
border-color: #0E4BD8;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.draft-btn {
|
||||
color: $text-secondary;
|
||||
border-color: $border-color;
|
||||
|
||||
&:hover {
|
||||
color: $text-primary;
|
||||
border-color: #D1D5DB;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #F9FAFB;
|
||||
border-color: $border-color;
|
||||
color: $text-placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
.publish-btn {
|
||||
background-color: $primary-color;
|
||||
border-color: $primary-color;
|
||||
|
||||
&:hover {
|
||||
background-color: #0E4BD8;
|
||||
border-color: #0E4BD8;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #A3C5FF;
|
||||
border-color: #A3C5FF;
|
||||
color: #FFFFFF;
|
||||
@@ -1008,10 +906,7 @@ $transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.draft-btn, .publish-btn {
|
||||
.publish-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user