|
|
|
@ -1,317 +1,272 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="post-container">
|
|
|
|
|
<h1 class="page-title">发布新帖子</h1>
|
|
|
|
|
|
|
|
|
|
<div class="editor-wrapper">
|
|
|
|
|
<el-form
|
|
|
|
|
ref="postFormRef"
|
|
|
|
|
:model="form"
|
|
|
|
|
:rules="rules"
|
|
|
|
|
label-width="80px"
|
|
|
|
|
@submit.prevent="submitForm"
|
|
|
|
|
>
|
|
|
|
|
<form @submit.prevent="submitForm">
|
|
|
|
|
<!-- 标题输入 -->
|
|
|
|
|
<el-form-item label="标题" prop="title">
|
|
|
|
|
<el-input
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<label>标题</label>
|
|
|
|
|
<input
|
|
|
|
|
v-model="form.title"
|
|
|
|
|
placeholder="请输入标题(3-50个字符)"
|
|
|
|
|
type="text"
|
|
|
|
|
maxlength="50"
|
|
|
|
|
show-word-limit
|
|
|
|
|
placeholder="请输入标题(3-50个字符)"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 分类选择 -->
|
|
|
|
|
<el-form-item label="分类" prop="categoryId">
|
|
|
|
|
<el-select
|
|
|
|
|
v-model="form.categoryId"
|
|
|
|
|
placeholder="请选择分类"
|
|
|
|
|
class="category-selector"
|
|
|
|
|
>
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="category in categories"
|
|
|
|
|
:key="category.id"
|
|
|
|
|
:label="category.name"
|
|
|
|
|
:value="category.id"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<label>分类</label>
|
|
|
|
|
<select v-model="form.categoryId" required>
|
|
|
|
|
<option disabled value="">请选择分类</option>
|
|
|
|
|
<option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 图片上传 -->
|
|
|
|
|
<el-form-item label="图片" prop="image">
|
|
|
|
|
<el-upload
|
|
|
|
|
action="/api/upload"
|
|
|
|
|
list-type="picture-card"
|
|
|
|
|
:file-list="fileList"
|
|
|
|
|
:on-success="handleUploadSuccess"
|
|
|
|
|
:on-remove="handleRemove"
|
|
|
|
|
:limit="3"
|
|
|
|
|
>
|
|
|
|
|
<el-icon><Plus /></el-icon>
|
|
|
|
|
</el-upload>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<label>图片</label>
|
|
|
|
|
<input type="file" accept="image/*" @change="onFileChange" />
|
|
|
|
|
<div v-if="form.imagePreview" class="img-preview">
|
|
|
|
|
<img :src="form.imagePreview" alt="预览" />
|
|
|
|
|
<button type="button" @click="removeImage">移除</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 内容编辑器 -->
|
|
|
|
|
<el-form-item label="内容" prop="content">
|
|
|
|
|
<el-input
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<label>内容</label>
|
|
|
|
|
<textarea
|
|
|
|
|
v-model="form.content"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:rows="8"
|
|
|
|
|
rows="8"
|
|
|
|
|
placeholder="请输入帖子内容(支持Markdown语法)"
|
|
|
|
|
resize="none"
|
|
|
|
|
/>
|
|
|
|
|
<div class="markdown-tips">
|
|
|
|
|
<span>Markdown语法支持:</span>
|
|
|
|
|
<el-link type="info" href="/help/markdown" target="_blank">查看帮助</el-link>
|
|
|
|
|
</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
required
|
|
|
|
|
></textarea>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 提交按钮 -->
|
|
|
|
|
<div class="submit-area">
|
|
|
|
|
<el-button
|
|
|
|
|
type="primary"
|
|
|
|
|
size="large"
|
|
|
|
|
:loading="submitting"
|
|
|
|
|
@click="submitForm"
|
|
|
|
|
>
|
|
|
|
|
<button type="submit" :disabled="submitting">
|
|
|
|
|
{{ submitting ? '发布中...' : '立即发布' }}
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button plain @click="saveDraft">保存草稿</el-button>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</el-form>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 操作反馈 -->
|
|
|
|
|
<el-alert
|
|
|
|
|
v-if="submitResult.visible"
|
|
|
|
|
:title="submitResult.title"
|
|
|
|
|
:type="submitResult.type"
|
|
|
|
|
:closable="false"
|
|
|
|
|
class="result-alert"
|
|
|
|
|
/>
|
|
|
|
|
<div v-if="submitResult.visible" :class="['result-alert', submitResult.type]">
|
|
|
|
|
{{ submitResult.title }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, reactive, onMounted } from 'vue';
|
|
|
|
|
import { useRouter } from 'vue-router';
|
|
|
|
|
import { Plus } from '@element-plus/icons-vue';
|
|
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
|
|
import request from '@/utils/request';
|
|
|
|
|
import { useUserStore } from '@/stores/user';
|
|
|
|
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
const postFormRef = ref(null);
|
|
|
|
|
|
|
|
|
|
// 响应式状态
|
|
|
|
|
const submitting = ref(false);
|
|
|
|
|
const categories = ref([]);
|
|
|
|
|
const fileList = ref([]);
|
|
|
|
|
const form = reactive({
|
|
|
|
|
title: '',
|
|
|
|
|
content: '',
|
|
|
|
|
categoryId: null,
|
|
|
|
|
image: '',
|
|
|
|
|
status: 0 // 根据接口要求默认0
|
|
|
|
|
});
|
|
|
|
|
<script>
|
|
|
|
|
import request from '@/utils/request'
|
|
|
|
|
import { ref } from 'vue'
|
|
|
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
|
|
|
|
|
const rules = {
|
|
|
|
|
title: [
|
|
|
|
|
{ required: true, message: '标题不能为空', trigger: 'blur' },
|
|
|
|
|
{ min: 3, max: 50, message: '长度在3到50个字符', trigger: 'blur' }
|
|
|
|
|
],
|
|
|
|
|
content: [
|
|
|
|
|
{ required: true, message: '内容不能为空', trigger: 'blur' },
|
|
|
|
|
{ min: 10, message: '内容至少10个字符', trigger: 'blur' }
|
|
|
|
|
],
|
|
|
|
|
categoryId: [
|
|
|
|
|
{ required: true, message: '请选择分类', trigger: 'change' }
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
export default {
|
|
|
|
|
setup() {
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const submitting = ref(false)
|
|
|
|
|
const categories = ref([
|
|
|
|
|
{ id: 1, name: '学习' },
|
|
|
|
|
{ id: 2, name: '娱乐' },
|
|
|
|
|
{ id: 3, name: '二手交易' }
|
|
|
|
|
])
|
|
|
|
|
const form = ref({
|
|
|
|
|
title: '',
|
|
|
|
|
content: '',
|
|
|
|
|
categoryId: '',
|
|
|
|
|
image: '',
|
|
|
|
|
imagePreview: '',
|
|
|
|
|
status: 0
|
|
|
|
|
})
|
|
|
|
|
const submitResult = ref({
|
|
|
|
|
visible: false,
|
|
|
|
|
type: 'success',
|
|
|
|
|
title: ''
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const submitResult = reactive({
|
|
|
|
|
visible: false,
|
|
|
|
|
type: 'success',
|
|
|
|
|
title: ''
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 检查登录状态
|
|
|
|
|
const checkLoginStatus = () => {
|
|
|
|
|
// 检查是否已登录
|
|
|
|
|
if (!userStore.isLoggedIn) {
|
|
|
|
|
// 尝试从localStorage恢复登录状态
|
|
|
|
|
const userId = localStorage.getItem('userId');
|
|
|
|
|
const username = localStorage.getItem('username');
|
|
|
|
|
const accessToken = localStorage.getItem('accessToken');
|
|
|
|
|
const refreshToken = localStorage.getItem('refreshToken');
|
|
|
|
|
const avatar = localStorage.getItem('avatar');
|
|
|
|
|
const role = localStorage.getItem('role');
|
|
|
|
|
|
|
|
|
|
if (userId && username && accessToken && refreshToken) {
|
|
|
|
|
userStore.login({
|
|
|
|
|
userid: userId,
|
|
|
|
|
userName: username,
|
|
|
|
|
avatar: avatar || '',
|
|
|
|
|
role: role || 1,
|
|
|
|
|
accessToken,
|
|
|
|
|
refreshToken
|
|
|
|
|
});
|
|
|
|
|
console.log('从localStorage恢复用户登录状态:', username);
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
ElMessage.warning('请先登录');
|
|
|
|
|
router.push('/');
|
|
|
|
|
return false;
|
|
|
|
|
// 处理图片选择和上传
|
|
|
|
|
const onFileChange = async (e) => {
|
|
|
|
|
const file = e.target.files[0]
|
|
|
|
|
if (!file) return
|
|
|
|
|
// 预览
|
|
|
|
|
form.value.imagePreview = URL.createObjectURL(file)
|
|
|
|
|
// 上传到后端
|
|
|
|
|
const fd = new FormData()
|
|
|
|
|
fd.append('file', file)
|
|
|
|
|
try {
|
|
|
|
|
const res = await request.post('/post/cover', fd, {
|
|
|
|
|
headers: { 'Content-Type': 'multipart/form-data' }
|
|
|
|
|
})
|
|
|
|
|
if (res.code === 200 && res.data) {
|
|
|
|
|
form.value.image = res.data
|
|
|
|
|
} else {
|
|
|
|
|
showResult('error', res.msg || '图片上传失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
showResult('error', '图片上传失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 加载分类数据
|
|
|
|
|
const loadCategories = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await request.get('/post/category');
|
|
|
|
|
if (response && response.code === 200) {
|
|
|
|
|
categories.value = response.data || [];
|
|
|
|
|
const removeImage = () => {
|
|
|
|
|
form.value.image = ''
|
|
|
|
|
form.value.imagePreview = ''
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载分类失败:', error);
|
|
|
|
|
ElMessage.error('加载分类失败,请刷新页面重试');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理图片上传
|
|
|
|
|
const handleUploadSuccess = (response) => {
|
|
|
|
|
form.image = form.image
|
|
|
|
|
? `${form.image},${response.data.url}`
|
|
|
|
|
: response.data.url;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRemove = (file) => {
|
|
|
|
|
if (!form.image) return;
|
|
|
|
|
const urls = form.image.split(',');
|
|
|
|
|
form.image = urls.filter(url => url !== file.url).join(',');
|
|
|
|
|
};
|
|
|
|
|
// 提交表单
|
|
|
|
|
const submitForm = async () => {
|
|
|
|
|
if (!form.value.title || !form.value.content || !form.value.categoryId) {
|
|
|
|
|
showResult('error', '请填写完整信息')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (form.value.title.length < 3 || form.value.title.length > 50) {
|
|
|
|
|
showResult('error', '标题长度需3-50字符')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (form.value.content.length < 10) {
|
|
|
|
|
showResult('error', '内容至少10个字符')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
submitting.value = true
|
|
|
|
|
try {
|
|
|
|
|
const postData = {
|
|
|
|
|
title: form.value.title,
|
|
|
|
|
content: form.value.content,
|
|
|
|
|
categoryId: form.value.categoryId,
|
|
|
|
|
image: form.value.image,
|
|
|
|
|
status: form.value.status
|
|
|
|
|
}
|
|
|
|
|
const res = await request.post('/post', postData)
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
showResult('success', '帖子发布成功')
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
router.push(`/post/${res.data}`)
|
|
|
|
|
}, 1200)
|
|
|
|
|
} else {
|
|
|
|
|
showResult('error', res.msg || '发布失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
showResult('error', '网络错误')
|
|
|
|
|
} finally {
|
|
|
|
|
submitting.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 提交表单
|
|
|
|
|
const submitForm = async () => {
|
|
|
|
|
if (!checkLoginStatus() || !postFormRef.value) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
submitting.value = true;
|
|
|
|
|
await postFormRef.value.validate();
|
|
|
|
|
|
|
|
|
|
const response = await request.post('/post/publish', form);
|
|
|
|
|
|
|
|
|
|
if (response && response.code === 200) {
|
|
|
|
|
showResult('success', '帖子发布成功');
|
|
|
|
|
const showResult = (type, title) => {
|
|
|
|
|
submitResult.value = { visible: true, type, title }
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
router.push(`/post/${response.data}`);
|
|
|
|
|
}, 1500);
|
|
|
|
|
} else {
|
|
|
|
|
showResult('error', response.msg || '发布失败');
|
|
|
|
|
submitResult.value.visible = false
|
|
|
|
|
}, 2000)
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('发布失败:', error);
|
|
|
|
|
showResult('error', error.response?.data?.msg || '网络错误');
|
|
|
|
|
} finally {
|
|
|
|
|
submitting.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 保存草稿
|
|
|
|
|
const saveDraft = async () => {
|
|
|
|
|
if (!checkLoginStatus()) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await request.post('/post/draft', {
|
|
|
|
|
...form,
|
|
|
|
|
status: 1 // 假设1表示草稿状态
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response && response.code === 200) {
|
|
|
|
|
showResult('success', '草稿保存成功');
|
|
|
|
|
} else {
|
|
|
|
|
showResult('error', response.msg || '保存失败');
|
|
|
|
|
return {
|
|
|
|
|
form,
|
|
|
|
|
categories,
|
|
|
|
|
submitting,
|
|
|
|
|
submitResult,
|
|
|
|
|
onFileChange,
|
|
|
|
|
removeImage,
|
|
|
|
|
submitForm
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('保存草稿失败:', error);
|
|
|
|
|
showResult('error', error.response?.data?.msg || '网络错误');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 显示结果提示
|
|
|
|
|
const showResult = (type, message) => {
|
|
|
|
|
submitResult.visible = true;
|
|
|
|
|
submitResult.type = type;
|
|
|
|
|
submitResult.title = message;
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
submitResult.visible = false;
|
|
|
|
|
}, 3000);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 页面加载时检查登录状态并获取分类
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
if (checkLoginStatus()) {
|
|
|
|
|
loadCategories();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.post-container {
|
|
|
|
|
max-width: 800px;
|
|
|
|
|
margin: 80px auto 20px; /* 增加顶部边距,避免被导航栏遮挡 */
|
|
|
|
|
max-width: 600px;
|
|
|
|
|
margin: 30px auto;
|
|
|
|
|
padding: 30px;
|
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.page-title {
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
font-size: 1.8em;
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
font-size: 1.6em;
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.editor-wrapper {
|
|
|
|
|
background: white;
|
|
|
|
|
padding: 25px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.category-selector {
|
|
|
|
|
width: 100%;
|
|
|
|
|
.form-row {
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
.form-row label {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
}
|
|
|
|
|
.form-row input[type="text"],
|
|
|
|
|
.form-row select,
|
|
|
|
|
.form-row textarea {
|
|
|
|
|
padding: 8px;
|
|
|
|
|
border: 1px solid #dcdcdc;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
}
|
|
|
|
|
.form-row textarea {
|
|
|
|
|
resize: vertical;
|
|
|
|
|
}
|
|
|
|
|
.img-preview {
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
position: relative;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
}
|
|
|
|
|
.img-preview img {
|
|
|
|
|
max-width: 120px;
|
|
|
|
|
max-height: 120px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
border: 1px solid #eee;
|
|
|
|
|
}
|
|
|
|
|
.img-preview button {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 2px;
|
|
|
|
|
right: 2px;
|
|
|
|
|
background: #f56c6c;
|
|
|
|
|
color: #fff;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.submit-area {
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin-top: 30px;
|
|
|
|
|
padding-top: 20px;
|
|
|
|
|
border-top: 1px solid #eee;
|
|
|
|
|
margin-top: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.markdown-tips {
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
font-size: 0.9em;
|
|
|
|
|
color: #666;
|
|
|
|
|
.submit-area button {
|
|
|
|
|
background: #409eff;
|
|
|
|
|
color: #fff;
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 10px 32px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
.submit-area button:disabled {
|
|
|
|
|
background: #b3d8ff;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.result-alert {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 保持与通知页面一致的输入框样式 */
|
|
|
|
|
:deep(.el-textarea__inner),
|
|
|
|
|
:deep(.el-input__inner) {
|
|
|
|
|
padding: 12px 18px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
border: 1px solid #e4e4e4;
|
|
|
|
|
transition: border-color 0.2s;
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.el-input__inner):focus {
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
.result-alert.success {
|
|
|
|
|
background: #e1f3d8;
|
|
|
|
|
color: #3a7a1c;
|
|
|
|
|
}
|
|
|
|
|
.result-alert.error {
|
|
|
|
|
background: #fde2e2;
|
|
|
|
|
color: #c0392b;
|
|
|
|
|
}
|
|
|
|
|
</style>
|