471 lines
12 KiB
Vue
471 lines
12 KiB
Vue
<template>
|
||
<div class="content-section">
|
||
<div class="content-header">
|
||
<h2 class="content-title">{{ isEditMode ? "编辑文章" : "发布文章" }}</h2>
|
||
<p class="content-desc">
|
||
{{ isEditMode ? "修改您的文章内容" : "分享您的技术见解" }}
|
||
</p>
|
||
</div>
|
||
<div class="content-body">
|
||
<el-form
|
||
:model="articleForm"
|
||
label-width="80px"
|
||
:rules="formRules"
|
||
ref="formRef"
|
||
>
|
||
<el-form-item label="标题" prop="title" required>
|
||
<el-input
|
||
v-model="articleForm.title"
|
||
placeholder="请输入文章标题"
|
||
maxlength="100"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="分类" prop="category" required>
|
||
<el-tree-select
|
||
v-model="articleForm.category"
|
||
placeholder="请选择分类"
|
||
:data="categories"
|
||
:props="{ label: 'name', value: 'id', children: 'children' }"
|
||
:render-after-expand="false"
|
||
filterable
|
||
check-strictly
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="作者" required>
|
||
<el-input v-model="articleForm.author" placeholder="请输入作者" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="标签">
|
||
<el-input
|
||
v-model="articleForm.tags"
|
||
placeholder="请输入标签,用逗号分隔"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="是否转载" required>
|
||
<el-radio-group v-model="articleForm.is_trans" size="default">
|
||
<el-radio-button label="是" value="1"></el-radio-button>
|
||
<el-radio-button label="否" value="0"></el-radio-button>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
|
||
<el-form-item
|
||
v-if="articleForm.is_trans === '1'"
|
||
label="转载链接"
|
||
required
|
||
>
|
||
<el-input v-model="articleForm.transurl" placeholder="请输入链接" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="描述">
|
||
<el-input
|
||
type="textarea"
|
||
:rows="4"
|
||
v-model="articleForm.desc"
|
||
placeholder="请输入描述"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="封面图片">
|
||
<el-upload
|
||
v-model:file-list="fileList"
|
||
list-type="picture-card"
|
||
:limit="1"
|
||
:auto-upload="false"
|
||
:on-change="handleFileChange"
|
||
:on-remove="handleUploadRemove"
|
||
:before-upload="beforeUpload"
|
||
>
|
||
<el-icon><Plus /></el-icon>
|
||
<template #tip>
|
||
<div class="el-upload__tip">
|
||
请上传封面图片,建议尺寸 800x400px
|
||
</div>
|
||
</template>
|
||
</el-upload>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="内容" prop="content" required>
|
||
<quill-editor
|
||
v-model="articleForm.content"
|
||
placeholder="请输入文章内容..."
|
||
:height="400"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<div class="action-buttons">
|
||
<el-button @click="goBack">取消</el-button>
|
||
<el-button type="primary" @click="submitArticle" :loading="loading">
|
||
{{ isEditMode ? "保存修改" : "发布文章" }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, onMounted } from "vue";
|
||
import { useRouter, useRoute } from "vue-router";
|
||
import { ElMessage } from "element-plus";
|
||
import { Plus } from "@element-plus/icons-vue";
|
||
import { article } from "@/api/article";
|
||
import { update } from "@/api/api";
|
||
|
||
interface ArticleForm {
|
||
title: string;
|
||
category: number | undefined;
|
||
tags: string;
|
||
author: string;
|
||
content: string;
|
||
desc: string;
|
||
is_trans: string;
|
||
transurl: string;
|
||
image: string;
|
||
}
|
||
|
||
interface Category {
|
||
value: string;
|
||
label: string;
|
||
}
|
||
|
||
const router = useRouter();
|
||
const route = useRoute();
|
||
|
||
// 响应式数据
|
||
const isEditMode = ref(false);
|
||
const loading = ref(false);
|
||
const formRef = ref();
|
||
const fileList = ref<any[]>([]);
|
||
const selectedFile = ref<File | null>(null);
|
||
|
||
// 表单数据
|
||
const articleForm = ref<ArticleForm>({
|
||
title: "",
|
||
category: undefined as number | undefined,
|
||
tags: "",
|
||
desc: "",
|
||
author: "",
|
||
content: "",
|
||
is_trans: "0",
|
||
transurl: "",
|
||
image: "",
|
||
});
|
||
|
||
// 表单验证规则
|
||
const formRules = {
|
||
title: [
|
||
{ required: true, message: "请输入文章标题", trigger: "blur" },
|
||
{
|
||
min: 1,
|
||
max: 100,
|
||
message: "标题长度在 1 到 100 个字符",
|
||
trigger: "blur",
|
||
},
|
||
],
|
||
category: [
|
||
{
|
||
required: true,
|
||
message: "请选择文章分类",
|
||
trigger: "change",
|
||
validator: (rule: any, value: any, callback: any) => {
|
||
if (value === undefined || value === null || value === 0) {
|
||
callback(new Error("请选择文章分类"));
|
||
} else {
|
||
callback();
|
||
}
|
||
},
|
||
},
|
||
],
|
||
content: [
|
||
{ required: true, message: "请输入文章内容", trigger: "blur" },
|
||
{ min: 10, message: "内容不能少于10个字符", trigger: "blur" },
|
||
],
|
||
};
|
||
|
||
// 分类选项
|
||
const categories = ref<Category[]>([]);
|
||
|
||
// 获取文章分类
|
||
const getArticleCategories = async () => {
|
||
const response: any = await article.getArticleCategory();
|
||
categories.value = response.data.data;
|
||
};
|
||
|
||
// 获取用户信息
|
||
const getUserId = () => {
|
||
const userStr = localStorage.getItem("user");
|
||
if (userStr) {
|
||
try {
|
||
const user = JSON.parse(userStr);
|
||
return user.uid;
|
||
} catch (error) {
|
||
console.error("解析用户信息失败:", error);
|
||
return null;
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
// 初始化数据
|
||
const initData = async () => {
|
||
// 获取文章分类
|
||
await getArticleCategories();
|
||
|
||
// 检查是否为编辑模式
|
||
const articleId = route.query.id as string;
|
||
if (articleId) {
|
||
isEditMode.value = true;
|
||
loadArticle(articleId);
|
||
}
|
||
};
|
||
|
||
// 加载文章数据(编辑模式)
|
||
const loadArticle = async (articleId: string) => {
|
||
loading.value = true;
|
||
try {
|
||
const response = await article.getArticleDetail(articleId);
|
||
if (response.data.code === 0) {
|
||
const articleData = response.data.data.article; // 修正数据路径
|
||
articleForm.value = {
|
||
title: articleData.title || "",
|
||
category: articleData.cate || undefined,
|
||
tags: articleData.tags || "",
|
||
desc: articleData.desc || "",
|
||
author: articleData.author || "",
|
||
content: articleData.content || "",
|
||
is_trans: articleData.is_trans || "0",
|
||
transurl: articleData.transurl || "",
|
||
image: articleData.image || "",
|
||
};
|
||
|
||
// 如果有封面图片,设置fileList
|
||
if (articleData.image) {
|
||
// 拼接完整的图片URL
|
||
const fullImageUrl = articleData.image.startsWith('http')
|
||
? articleData.image
|
||
: `http://localhost:8000${articleData.image}`;
|
||
|
||
fileList.value = [
|
||
{
|
||
name: "cover.jpg",
|
||
url: fullImageUrl,
|
||
uid: Date.now(),
|
||
},
|
||
];
|
||
}
|
||
} else {
|
||
ElMessage.error(response.data.msg || "加载文章失败");
|
||
}
|
||
} catch (error) {
|
||
console.error("加载文章失败:", error);
|
||
ElMessage.error("加载文章失败,请重试");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 上传处理方法
|
||
const handleFileChange = (file: any, fileList: any[]) => {
|
||
// 存储选择的文件,用于后续上传
|
||
selectedFile.value = file.raw;
|
||
};
|
||
|
||
const uploadCoverImage = async (): Promise<string | null> => {
|
||
if (!selectedFile.value) {
|
||
return articleForm.value.image; // 如果没有新选择文件,返回原有图片URL
|
||
}
|
||
|
||
const formData = new FormData();
|
||
|
||
// 只发送文件字段,测试是否能上传成功
|
||
formData.append('file', selectedFile.value, selectedFile.value.name);
|
||
|
||
try {
|
||
const response = await update.uploadImage(formData);
|
||
|
||
// 检查响应状态
|
||
if (response.status === 200) {
|
||
const responseData = response.data;
|
||
|
||
if (responseData.code === 0) {
|
||
// 根据后端返回格式:data.url
|
||
const imageUrl = responseData.data?.url;
|
||
if (imageUrl) {
|
||
ElMessage.success("封面上传成功");
|
||
return imageUrl;
|
||
} else {
|
||
console.error('无法获取图片URL:', responseData);
|
||
ElMessage.error("封面上传失败:无法获取图片URL");
|
||
return null;
|
||
}
|
||
} else {
|
||
ElMessage.error(responseData.msg || "封面上传失败");
|
||
return null;
|
||
}
|
||
} else {
|
||
ElMessage.error("封面上传失败:服务器响应异常");
|
||
return null;
|
||
}
|
||
} catch (error: any) {
|
||
console.error('上传错误:', error);
|
||
const errorMessage = error.response?.data?.msg || error.message || "封面上传失败";
|
||
ElMessage.error(errorMessage);
|
||
return null;
|
||
}
|
||
};
|
||
|
||
const handleUploadRemove = (file: any, fileList: any[]) => {
|
||
selectedFile.value = null;
|
||
articleForm.value.image = "";
|
||
};
|
||
|
||
const beforeUpload = (file: any) => {
|
||
const isImage = file.type.startsWith("image/");
|
||
const isLt5M = file.size / 1024 / 1024 < 5;
|
||
|
||
if (!isImage) {
|
||
ElMessage.error("只能上传图片文件!");
|
||
return false;
|
||
}
|
||
if (!isLt5M) {
|
||
ElMessage.error("上传图片大小不能超过 5MB!");
|
||
return false;
|
||
}
|
||
return true;
|
||
};
|
||
|
||
// 提交文章(发布或更新)
|
||
const submitArticle = async () => {
|
||
if (!formRef.value) return;
|
||
|
||
const userId = getUserId();
|
||
if (!userId) {
|
||
ElMessage.warning("请先登录");
|
||
return;
|
||
}
|
||
|
||
await formRef.value.validate(async (valid: boolean) => {
|
||
if (!valid) return;
|
||
|
||
loading.value = true;
|
||
|
||
try {
|
||
// 先上传封面图片(如果有选择新文件)
|
||
const coverImageUrl = await uploadCoverImage();
|
||
if (coverImageUrl === null && selectedFile.value) {
|
||
// 上传失败且有选择新文件
|
||
loading.value = false;
|
||
return;
|
||
}
|
||
|
||
const articleData = {
|
||
title: articleForm.value.title,
|
||
content: articleForm.value.content,
|
||
cate: articleForm.value.category || 0,
|
||
user_id: userId,
|
||
desc: articleForm.value.desc,
|
||
author: articleForm.value.author,
|
||
is_trans: articleForm.value.is_trans,
|
||
transurl:
|
||
articleForm.value.is_trans === "1" ? articleForm.value.transurl : "",
|
||
image: coverImageUrl || "",
|
||
};
|
||
|
||
if (isEditMode.value) {
|
||
// 更新文章
|
||
const articleId = route.query.id as string;
|
||
const updateData = {
|
||
...articleData,
|
||
id: articleId,
|
||
};
|
||
const response = await article.updateArticle(articleId, updateData);
|
||
if (response.data.code === 0) {
|
||
ElMessage.success("文章更新成功");
|
||
router.push("/user/profile");
|
||
} else {
|
||
ElMessage.error(response.data.msg || "文章更新失败");
|
||
}
|
||
} else {
|
||
// 发布新文章
|
||
const response = await article.publishArticle(articleData);
|
||
if (response.data.code === 0) {
|
||
ElMessage.success("文章发布成功");
|
||
router.push("/user/profile");
|
||
} else {
|
||
ElMessage.error(response.data.msg || "文章发布失败");
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("操作失败:", error);
|
||
ElMessage.error("操作失败,请重试");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
});
|
||
};
|
||
|
||
// 返回上一页
|
||
const goBack = () => {
|
||
router.back();
|
||
};
|
||
|
||
onMounted(() => {
|
||
initData();
|
||
});
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.content-section {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.content-header {
|
||
margin-bottom: 32px;
|
||
text-align: center;
|
||
|
||
.content-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.content-desc {
|
||
color: #909399;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.content-body {
|
||
background: #fff;
|
||
}
|
||
|
||
.el-form {
|
||
.el-form-item {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.el-textarea {
|
||
::v-deep(.el-textarea__inner) {
|
||
resize: vertical;
|
||
min-height: 200px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 16px;
|
||
margin-top: 32px;
|
||
padding-top: 24px;
|
||
border-top: 1px solid #e4e7ed;
|
||
}
|
||
</style>
|