ArticleEdit.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <template>
  2. <div class="article-edit">
  3. <el-card class="article-card">
  4. <template #header>
  5. <div class="card-header">
  6. <h3>{{ isEdit ? '编辑文章' : '新增文章' }}</h3>
  7. </div>
  8. </template>
  9. <el-form
  10. ref="formRef"
  11. :model="form"
  12. :rules="rules"
  13. label-width="100px"
  14. v-loading="loading"
  15. >
  16. <el-form-item label="文章分类" prop="category_id">
  17. <el-select v-model="form.category_id" placeholder="请选择分类" style="width: 100%">
  18. <el-option
  19. v-for="item in categories"
  20. :key="item.id"
  21. :label="item.name"
  22. :value="item.id"
  23. />
  24. </el-select>
  25. </el-form-item>
  26. <el-form-item label="文章标题" prop="title">
  27. <el-input v-model="form.title" placeholder="请输入文章标题" maxlength="128" show-word-limit />
  28. </el-form-item>
  29. <el-form-item label="封面图片" prop="cover">
  30. <div class="cover-upload-container">
  31. <div class="cover-preview" @click="fileManagerVisible = true">
  32. <img v-if="form.cover" :src="form.cover" class="cover-image" />
  33. <div v-else class="cover-placeholder">
  34. <el-icon class="cover-uploader-icon"><Plus /></el-icon>
  35. <div class="placeholder-text">选择封面图片</div>
  36. </div>
  37. </div>
  38. <div class="cover-actions">
  39. <el-button size="small" @click="fileManagerVisible = true">选择图片</el-button>
  40. <el-button v-if="form.cover" size="small" type="danger" @click="removeCover">删除封面</el-button>
  41. </div>
  42. </div>
  43. <div class="tip-text">建议上传尺寸: 16:9, 最佳尺寸: 1200x675</div>
  44. <FileManager
  45. v-model="fileManagerVisible"
  46. :multiple="false"
  47. accept="image/*"
  48. :max-size="2 * 1024 * 1024"
  49. @confirm="handleFileManagerConfirm"
  50. />
  51. </el-form-item>
  52. <el-form-item label="作者" prop="author">
  53. <el-input v-model="form.author" placeholder="请输入作者名称" maxlength="64" />
  54. </el-form-item>
  55. <el-form-item label="摘要" prop="summary">
  56. <el-input
  57. v-model="form.summary"
  58. type="textarea"
  59. placeholder="请输入文章摘要"
  60. maxlength="255"
  61. :rows="3"
  62. show-word-limit
  63. />
  64. </el-form-item>
  65. <el-form-item label="内容" prop="content">
  66. <div class="editor-container">
  67. <RichEditor
  68. v-model="form.content"
  69. :height="400"
  70. :uploadRequest="uploadImage"
  71. />
  72. </div>
  73. </el-form-item>
  74. <el-form-item label="置顶" prop="is_top">
  75. <el-radio-group v-model="form.is_top">
  76. <el-radio :label="1">是</el-radio>
  77. <el-radio :label="0">否</el-radio>
  78. </el-radio-group>
  79. </el-form-item>
  80. <el-form-item label="状态" prop="status">
  81. <el-radio-group v-model="form.status">
  82. <el-radio :label="0">草稿</el-radio>
  83. <el-radio :label="1">发布</el-radio>
  84. <el-radio :label="2">下线</el-radio>
  85. </el-radio-group>
  86. </el-form-item>
  87. <el-form-item>
  88. <el-button type="primary" @click="submitForm" :loading="submitLoading">保存</el-button>
  89. <el-button @click="goBack">取消</el-button>
  90. </el-form-item>
  91. </el-form>
  92. </el-card>
  93. </div>
  94. </template>
  95. <script>
  96. import { defineComponent, ref, reactive, onMounted, computed } from 'vue'
  97. import { useRoute, useRouter } from 'vue-router'
  98. import { ElMessage } from 'element-plus'
  99. import { Plus } from '@element-plus/icons-vue'
  100. import RichEditor from '@/components/RichEditor.vue'
  101. import FileManager from '@/components/flysystem/FileManager.vue'
  102. import request from '@/utils/request'
  103. import { useNews } from '../../composables/useNews'
  104. export default defineComponent({
  105. name: 'ArticleEdit',
  106. components: {
  107. Plus,
  108. RichEditor,
  109. FileManager
  110. },
  111. setup() {
  112. const route = useRoute()
  113. const router = useRouter()
  114. const formRef = ref(null)
  115. const submitLoading = ref(false)
  116. const fileManagerVisible = ref(false)
  117. const {
  118. loading,
  119. categories,
  120. getCategoryList,
  121. getArticleDetail,
  122. createArticle,
  123. updateArticle
  124. } = useNews()
  125. // 是否是编辑模式
  126. const isEdit = computed(() => {
  127. return !!route.params.id
  128. })
  129. // 表单数据
  130. const form = reactive({
  131. id: null,
  132. category_id: '',
  133. title: '',
  134. cover: '',
  135. summary: '',
  136. content: '',
  137. author: '',
  138. is_top: 0,
  139. status: 0
  140. })
  141. // 表单验证规则
  142. const rules = {
  143. category_id: [
  144. { required: true, message: '请选择文章分类', trigger: 'change' }
  145. ],
  146. title: [
  147. { required: true, message: '请输入文章标题', trigger: 'blur' },
  148. { min: 1, max: 128, message: '长度在 1 到 128 个字符', trigger: 'blur' }
  149. ],
  150. summary: [
  151. { max: 255, message: '最多 255 个字符', trigger: 'blur' }
  152. ],
  153. content: [
  154. { required: true, message: '请输入文章内容', trigger: 'blur' }
  155. ],
  156. author: [
  157. { max: 64, message: '最多 64 个字符', trigger: 'blur' }
  158. ]
  159. }
  160. // 初始化
  161. const init = async () => {
  162. await getCategoryList()
  163. console.log('已加载分类数据:', categories.value)
  164. // 检查分类数据是否正确
  165. if (categories.value && categories.value.length > 0) {
  166. console.log('第一个分类项:', categories.value[0])
  167. console.log('分类项字段:', Object.keys(categories.value[0]))
  168. } else {
  169. console.error('分类数据为空或格式不正确')
  170. }
  171. if (isEdit.value) {
  172. // 编辑模式,加载文章详情
  173. const id = route.params.id
  174. const article = await getArticleDetail(id)
  175. if (article) {
  176. // 填充表单
  177. Object.keys(form).forEach(key => {
  178. if (article[key] !== undefined) {
  179. form[key] = article[key]
  180. }
  181. })
  182. }
  183. }
  184. }
  185. // 上传图片(用于富文本编辑器)
  186. const uploadImage = async (file) => {
  187. // 文件大小限制: 2MB
  188. const isLt2M = file.size / 1024 / 1024 < 2
  189. if (!isLt2M) {
  190. ElMessage.error('图片大小不能超过 2MB!')
  191. return Promise.reject('图片大小不能超过 2MB')
  192. }
  193. try {
  194. // 创建FormData对象
  195. const formData = new FormData()
  196. formData.append('file', file)
  197. // 调用上传接口
  198. const response = await request.post('/news/upload', formData, {
  199. headers: {
  200. 'Content-Type': 'multipart/form-data'
  201. }
  202. })
  203. if (response.code === 200) {
  204. return response.data.url
  205. } else {
  206. ElMessage.error(response.msg || '上传失败')
  207. return Promise.reject('上传失败')
  208. }
  209. } catch (error) {
  210. console.error('上传图片失败:', error)
  211. ElMessage.error('上传图片失败')
  212. return Promise.reject('上传失败')
  213. }
  214. }
  215. // 文件管理器确认选择
  216. const handleFileManagerConfirm = (result) => {
  217. console.log('文件管理器返回结果:', result)
  218. // 单选模式下,result是一个文件对象;多选模式下,result是数组
  219. if (result) {
  220. const file = Array.isArray(result) ? result[0] : result
  221. if (file && file.file_url) {
  222. form.cover = file.file_url
  223. ElMessage.success('封面图片选择成功')
  224. } else {
  225. ElMessage.error('文件信息不完整')
  226. }
  227. }
  228. fileManagerVisible.value = false
  229. }
  230. // 删除封面图片
  231. const removeCover = () => {
  232. form.cover = ''
  233. ElMessage.success('封面图片已删除')
  234. }
  235. // 提交表单
  236. const submitForm = async () => {
  237. if (!formRef.value) return
  238. await formRef.value.validate(async (valid) => {
  239. if (valid) {
  240. submitLoading.value = true
  241. try {
  242. let success = false
  243. console.log('提交表单数据:', {
  244. category_id: form.category_id,
  245. title: form.title,
  246. cover: form.cover ? '有封面图' : '无封面图',
  247. summary: form.summary,
  248. content: form.content ? '有内容' : '无内容',
  249. author: form.author,
  250. publish_time: form.publish_time,
  251. sort: form.sort,
  252. is_top: form.is_top,
  253. status: form.status
  254. })
  255. if (isEdit.value) {
  256. // 编辑
  257. success = await updateArticle(form.id, {
  258. category_id: form.category_id,
  259. title: form.title,
  260. cover: form.cover,
  261. summary: form.summary,
  262. content: form.content,
  263. author: form.author,
  264. is_top: form.is_top,
  265. status: form.status
  266. })
  267. } else {
  268. // 新增
  269. success = await createArticle({
  270. category_id: form.category_id,
  271. title: form.title,
  272. cover: form.cover,
  273. summary: form.summary,
  274. content: form.content,
  275. author: form.author,
  276. is_top: form.is_top,
  277. status: form.status
  278. })
  279. }
  280. console.log('提交结果:', success)
  281. if (success) {
  282. goBack()
  283. }
  284. } finally {
  285. submitLoading.value = false
  286. }
  287. }
  288. })
  289. }
  290. // 返回列表页
  291. const goBack = () => {
  292. router.push('/news?tab=article')
  293. }
  294. onMounted(() => {
  295. init()
  296. })
  297. return {
  298. isEdit,
  299. loading,
  300. categories,
  301. form,
  302. rules,
  303. formRef,
  304. submitLoading,
  305. uploadImage,
  306. fileManagerVisible,
  307. handleFileManagerConfirm,
  308. removeCover,
  309. submitForm,
  310. goBack
  311. }
  312. }
  313. })
  314. </script>
  315. <style scoped>
  316. .article-edit {
  317. padding: 20px;
  318. }
  319. .article-card {
  320. margin-bottom: 20px;
  321. }
  322. .card-header {
  323. display: flex;
  324. justify-content: space-between;
  325. align-items: center;
  326. }
  327. .cover-upload-container {
  328. display: flex;
  329. flex-direction: column;
  330. gap: 10px;
  331. }
  332. .cover-actions {
  333. display: flex;
  334. gap: 10px;
  335. }
  336. .cover-preview {
  337. cursor: pointer;
  338. transition: all 0.3s;
  339. }
  340. .cover-preview:hover {
  341. opacity: 0.8;
  342. }
  343. .cover-placeholder {
  344. width: 178px;
  345. height: 100px;
  346. border: 1px dashed #d9d9d9;
  347. border-radius: 6px;
  348. display: flex;
  349. flex-direction: column;
  350. justify-content: center;
  351. align-items: center;
  352. cursor: pointer;
  353. transition: border-color 0.3s;
  354. background-color: #fafafa;
  355. }
  356. .cover-placeholder:hover {
  357. border-color: #409eff;
  358. }
  359. .cover-uploader-icon {
  360. font-size: 28px;
  361. color: #8c939d;
  362. margin-bottom: 8px;
  363. }
  364. .placeholder-text {
  365. font-size: 12px;
  366. color: #8c939d;
  367. }
  368. .cover-image {
  369. width: 178px;
  370. height: 100px;
  371. object-fit: cover;
  372. border-radius: 6px;
  373. cursor: pointer;
  374. }
  375. .editor-container {
  376. border: 1px solid #dcdfe6;
  377. border-radius: 4px;
  378. }
  379. .tip-text {
  380. font-size: 12px;
  381. color: #999;
  382. margin-top: 5px;
  383. }
  384. </style>