添加首页文章页面

This commit is contained in:
2025-10-08 20:00:30 +08:00
parent 3d7240565e
commit 698f27372d
30 changed files with 2149 additions and 667 deletions

View File

@@ -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); // 文章IDnull=新增,有值=编辑
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%;
}
}