| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- <template>
- <div class="admin-page">
- <div class="article-manage">
- <el-card>
- <div slot="header">
- <span>文章管理</span>
- <el-button style="float: right" type="primary" size="small" @click="$router.push('/article-edit')">+ 新建文章</el-button>
- </div>
- <!-- 状态标签栏 -->
- <div class="status-tabs">
- <el-button
- v-for="tab in statusTabs"
- :key="tab.value"
- :type="filters.status === tab.value ? 'primary' : 'default'"
- size="small"
- @click="filters.status = tab.value; loadList()"
- >{{ tab.label }}</el-button>
- </div>
- <!-- 搜索筛选栏 -->
- <div class="filter-bar">
- <el-select v-model="filters.categoryId" placeholder="分类" clearable @change="loadList" style="width: 150px;">
- <el-option label="全部分类" value="" />
- <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id" />
- </el-select>
- <el-select v-model="filters.contentType" placeholder="内容类型" clearable @change="loadList" style="width: 130px;">
- <el-option label="全部" value="" />
- <el-option label="文章" value="article" />
- <el-option label="知识点" value="knowledge" />
- <el-option label="课程" value="course" />
- <el-option label="贴士" value="tip" />
- </el-select>
- <el-select v-model="filters.difficultyLevel" placeholder="难度" clearable @change="loadList" style="width: 100px;">
- <el-option label="全部" value="" />
- <el-option label="1星" value="1" />
- <el-option label="2星" value="2" />
- <el-option label="3星" value="3" />
- <el-option label="4星" value="4" />
- <el-option label="5星" value="5" />
- </el-select>
- <el-select v-model="filters.dimensionCode" placeholder="维度" clearable @change="loadList" style="width: 100px;">
- <el-option label="全部" value="" />
- <el-option label="身" value="body" />
- <el-option label="心" value="mind" />
- <el-option label="智" value="wisdom" />
- <el-option label="行" value="action" />
- <el-option label="富" value="wealth" />
- </el-select>
- <el-select v-model="filters.tagId" placeholder="知识标签" clearable @change="loadList" style="width: 150px;" filterable>
- <el-option label="全部" value="" />
- <el-option v-for="tag in tagOptions" :key="tag.id" :label="tag.name" :value="tag.id" />
- </el-select>
- <el-input v-model="filters.keyword" placeholder="搜索文章标题..." clearable style="width: 220px;" @keyup.enter.native="loadList" />
- <el-button type="primary" icon="el-icon-search" @click="loadList">搜索</el-button>
- </div>
- <el-table :data="list" v-loading="loading" border stripe>
- <el-table-column prop="id" label="ID" width="70" />
- <el-table-column label="标题" min-width="220">
- <template slot-scope="{ row }">
- <span>{{ row.title }}</span>
- <el-tag v-if="row.isFeatured === 1 || row.isFeatured === true" size="mini" type="warning" style="margin-left: 6px;">精选</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="分类" width="100">
- <template slot-scope="{ row }">
- {{ row.categoryName || '-' }}
- </template>
- </el-table-column>
- <el-table-column label="可见性" width="90">
- <template slot-scope="{ row }">
- <el-tag v-if="row.visibility === 'public'" size="small" type="success">公开</el-tag>
- <el-tag v-else-if="row.visibility === 'login'" size="small" type="primary">登录</el-tag>
- <el-tag v-else size="small" type="info">私密</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="内容类型" width="90">
- <template slot-scope="{ row }">
- <el-tag v-if="row.contentType === 'article'" size="small" type="primary">文章</el-tag>
- <el-tag v-else-if="row.contentType === 'knowledge'" size="small" type="success">知识点</el-tag>
- <el-tag v-else-if="row.contentType === 'course'" size="small" type="warning">课程</el-tag>
- <el-tag v-else-if="row.contentType === 'tip'" size="small" type="info">贴士</el-tag>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column label="难度" width="70">
- <template slot-scope="{ row }">
- <span v-if="row.difficultyLevel && row.difficultyLevel > 0">{{ row.difficultyLevel }}星</span>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column label="维度" width="100">
- <template slot-scope="{ row }">
- <span v-if="row.dimensionIds">{{ row.dimensionIds }}</span>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column prop="author" label="作者" width="100" />
- <el-table-column label="状态" width="100">
- <template slot-scope="{ row }">
- <el-tag v-if="row.status === 'draft' && row.auditStatus === 'pending'" size="small" type="warning">待审核</el-tag>
- <el-tag v-else-if="row.status === 'draft'" size="small" type="info">草稿箱</el-tag>
- <el-tag v-else-if="row.status === 'rejected'" size="small" type="danger">已驳回</el-tag>
- <el-tag v-else-if="row.status === 'published'" size="small" type="success">发布中</el-tag>
- <el-tag v-else-if="row.status === 'withdrawn'" size="small">已撤回</el-tag>
- <el-tag v-else size="small" type="info">{{ row.status }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="审核状态" width="120">
- <template slot-scope="{ row }">
- <el-tag v-if="row.auditStatus === 'approved'" size="small" type="success">已通过</el-tag>
- <el-tag v-else-if="row.auditStatus === 'pending'" size="small" type="warning">待审核</el-tag>
- <el-tag v-else-if="row.auditStatus === 'rejected'" size="small" type="danger" :title="row.auditReason || ''">已驳回</el-tag>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column prop="viewCount" label="阅读" width="70" />
- <el-table-column label="发布时间" width="160">
- <template slot-scope="{ row }">
- {{ row.publishedAt || '-' }}
- </template>
- </el-table-column>
- <el-table-column label="操作" width="200" fixed="right">
- <template slot-scope="{ row }">
- <!-- 草稿箱:可编辑 -->
- <template v-if="row.status === 'draft' && !row.auditStatus">
- <el-button size="mini" type="primary" @click="$router.push('/article-edit?id=' + row.id)">编辑</el-button>
- <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
- <el-button size="mini">
- 更多<i class="el-icon-arrow-down el-icon--right"></i>
- </el-button>
- <el-dropdown-menu slot="dropdown">
- <el-dropdown-item command="submitReview" icon="el-icon-upload">提交审核</el-dropdown-item>
- <el-dropdown-item command="publishDirect" icon="el-icon-s-promotion">直接发布</el-dropdown-item>
- <el-dropdown-item command="copy" icon="el-icon-document-copy">复制</el-dropdown-item>
- <el-dropdown-item command="delete" icon="el-icon-delete">删除</el-dropdown-item>
- </el-dropdown-menu>
- </el-dropdown>
- </template>
- <!-- 待审核:审核操作 -->
- <template v-else-if="row.status === 'draft' && row.auditStatus === 'pending'">
- <el-button size="mini" type="primary" @click="$router.push('/article-edit?id=' + row.id + '&readonly=1')">查看</el-button>
- <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
- <el-button size="mini">
- 更多<i class="el-icon-arrow-down el-icon--right"></i>
- </el-button>
- <el-dropdown-menu slot="dropdown">
- <el-dropdown-item command="approve" icon="el-icon-circle-check">通过</el-dropdown-item>
- <el-dropdown-item command="reject" icon="el-icon-circle-close">驳回</el-dropdown-item>
- <el-dropdown-item command="copy" icon="el-icon-document-copy">复制</el-dropdown-item>
- </el-dropdown-menu>
- </el-dropdown>
- </template>
- <!-- 已驳回:查看 + 重新编辑 -->
- <template v-else-if="row.status === 'rejected'">
- <el-button size="mini" type="primary" @click="$router.push('/article-edit?id=' + row.id + '&readonly=1')">查看</el-button>
- <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
- <el-button size="mini">
- 更多<i class="el-icon-arrow-down el-icon--right"></i>
- </el-button>
- <el-dropdown-menu slot="dropdown">
- <el-dropdown-item command="reDraft" icon="el-icon-edit-outline">重新编辑</el-dropdown-item>
- <el-dropdown-item command="copy" icon="el-icon-document-copy">复制</el-dropdown-item>
- </el-dropdown-menu>
- </el-dropdown>
- </template>
- <!-- 发布中:查看 + 下架 + 精选 -->
- <template v-else-if="row.status === 'published'">
- <el-button size="mini" type="primary" @click="$router.push('/article-edit?id=' + row.id + '&readonly=1')">查看</el-button>
- <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
- <el-button size="mini">
- 更多<i class="el-icon-arrow-down el-icon--right"></i>
- </el-button>
- <el-dropdown-menu slot="dropdown">
- <el-dropdown-item command="withdraw" icon="el-icon-download">下架</el-dropdown-item>
- <el-dropdown-item command="toggleFeatured" icon="el-icon-star-on">精选/取消精选</el-dropdown-item>
- <el-dropdown-item command="copy" icon="el-icon-document-copy">复制</el-dropdown-item>
- </el-dropdown-menu>
- </el-dropdown>
- </template>
- <!-- 已撤回:查看 + 重新发布 -->
- <template v-else-if="row.status === 'withdrawn'">
- <el-button size="mini" type="primary" @click="$router.push('/article-edit?id=' + row.id + '&readonly=1')">查看</el-button>
- <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
- <el-button size="mini">
- 更多<i class="el-icon-arrow-down el-icon--right"></i>
- </el-button>
- <el-dropdown-menu slot="dropdown">
- <el-dropdown-item command="rePublish" icon="el-icon-s-promotion">重新发布</el-dropdown-item>
- <el-dropdown-item command="copy" icon="el-icon-document-copy">复制</el-dropdown-item>
- </el-dropdown-menu>
- </el-dropdown>
- </template>
- <!-- 其他 -->
- <template v-else>
- <el-button size="mini" type="primary" @click="$router.push('/article-edit?id=' + row.id + '&readonly=1')">查看</el-button>
- <el-dropdown trigger="hover" @command="(cmd) => handleActionCmd(row, cmd)">
- <el-button size="mini">
- 更多<i class="el-icon-arrow-down el-icon--right"></i>
- </el-button>
- <el-dropdown-menu slot="dropdown">
- <el-dropdown-item command="copy" icon="el-icon-document-copy">复制</el-dropdown-item>
- </el-dropdown-menu>
- </el-dropdown>
- </template>
- </template>
- </el-table-column>
- </el-table>
- <el-pagination
- @size-change="onPageChange"
- @current-change="onPageChange"
- :current-page="page"
- :page-size="size"
- :total="total"
- layout="total, prev, pager, next"
- style="margin-top: 20px; text-align: right;"
- />
- </el-card>
- </div>
- <!-- 驳回弹窗 -->
- <el-dialog title="驳回原因" :visible.sync="rejectDialogVisible" width="400px">
- <el-input
- type="textarea"
- :rows="4"
- v-model="rejectReason"
- placeholder="请输入驳回原因..."
- ></el-input>
- <span slot="footer">
- <el-button @click="rejectDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="confirmReject">确认驳回</el-button>
- </span>
- </el-dialog>
- </div>
- </template>
- <script>
- import {
- adminArticleList,
- adminArticlePublish,
- adminArticleToggleFeatured,
- adminArticleDelete,
- adminArticleCategoriesList,
- adminArticleAudit,
- adminArticleDetail,
- adminArticleCreate,
- adminArticleSubmitReview,
- adminArticleWithdraw,
- adminArticleReDraft
- } from '@/api/article.js'
- import { getTagList } from '@/api/dimension.js'
- export default {
- name: 'ArticleManage',
- data() {
- return {
- loading: false,
- list: [],
- total: 0,
- page: 1,
- size: 20,
- statusTabs: [
- { value: '', label: '全部' },
- { value: 'draft', label: '草稿箱' },
- { value: 'pending', label: '待审核' },
- { value: 'rejected', label: '已驳回' },
- { value: 'published', label: '发布中' },
- { value: 'withdrawn', label: '已撤回' }
- ],
- filters: {
- status: '',
- categoryId: '',
- contentType: '',
- difficultyLevel: '',
- dimensionCode: '',
- tagId: '',
- keyword: ''
- },
- categories: [],
- tagOptions: [],
- rejectDialogVisible: false,
- rejectReason: '',
- currentAuditRow: null
- }
- },
- created() {
- Promise.all([
- this.loadCategories().catch(e => console.error('加载分类失败', e)),
- this.loadTagOptions().catch(e => console.error('加载标签失败', e)),
- this.loadList().catch(e => this.$message.error('加载文章列表失败'))
- ])
- },
- methods: {
- async loadCategories() {
- try {
- const res = await adminArticleCategoriesList()
- if (res.data) {
- this.categories = res.data
- }
- } catch (e) {
- console.error('加载分类失败', e)
- }
- },
- async loadTagOptions() {
- try {
- const res = await getTagList()
- if (res.data) {
- this.tagOptions = res.data
- }
- } catch (e) {
- console.error('加载知识标签失败', e)
- }
- },
- async loadList() {
- this.loading = true
- try {
- const res = await adminArticleList({
- page: this.page,
- size: this.size,
- status: this.filters.status || undefined,
- categoryId: this.filters.categoryId || undefined,
- contentType: this.filters.contentType || undefined,
- difficultyLevel: this.filters.difficultyLevel || undefined,
- dimensionCode: this.filters.dimensionCode || undefined,
- tagId: this.filters.tagId || undefined,
- keyword: this.filters.keyword || undefined
- })
- if (res.data) {
- this.list = res.data.records || res.data.list || res.data
- this.total = res.data.total || 0
- }
- } catch (e) {
- this.$message.error('加载文章列表失败')
- } finally {
- this.loading = false
- }
- },
- onPageChange() {
- this.loadList()
- },
- async handlePublishDirect(row) {
- try {
- await adminArticlePublish({ id: row.id, status: 'published' })
- this.$message.success('文章已发布')
- this.loadList()
- } catch (e) {
- this.$message.error(e.message || '发布失败')
- }
- },
- async handleSubmitReview(row) {
- try {
- await adminArticleSubmitReview({ id: row.id })
- this.$message.success('已提交审核')
- this.loadList()
- } catch (e) {
- this.$message.error(e.message || '提交审核失败')
- }
- },
- async handleWithdraw(row) {
- try {
- await adminArticleWithdraw({ id: row.id })
- this.$message.success('文章已下架')
- this.loadList()
- } catch (e) {
- this.$message.error(e.message || '下架失败')
- }
- },
- async handleReDraft(row) {
- try {
- await adminArticleReDraft({ id: row.id })
- this.$message.success('已保存到草稿箱')
- this.loadList()
- } catch (e) {
- this.$message.error(e.message || '操作失败')
- }
- },
- async handleToggleFeatured(row) {
- try {
- const newVal = row.isFeatured === 1 || row.isFeatured === true ? 0 : 1
- await adminArticleToggleFeatured({ id: row.id, isFeatured: newVal })
- this.$message.success(newVal ? '已设为精选' : '已取消精选')
- this.loadList()
- } catch (e) {
- this.$message.error(e.message || '操作失败')
- }
- },
- handleDelete(row) {
- this.$confirm(`确定删除文章「${row.title}」吗?`, '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- }).then(async () => {
- try {
- await adminArticleDelete({ id: row.id })
- this.$message.success('删除成功')
- this.loadList()
- } catch (e) {
- this.$message.error(e.message || '删除失败')
- }
- }).catch(() => {})
- },
- handleAudit(row, auditStatus) {
- var self = this
- this.$confirm('确定' + (auditStatus === 'approved' ? '通过' : '驳回') + '该文章吗?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- }).then(function() {
- return self.$http.post('/api/admin/articles/audit', {
- id: row.id,
- auditStatus: auditStatus,
- auditReason: auditStatus === 'rejected' ? self.rejectReason : ''
- })
- }).then(function(res) {
- if (res.data && res.data.code === 200) {
- self.$message.success('审核完成')
- self.loadList()
- } else {
- self.$message.error(res.data && res.data.message || '审核失败')
- }
- }).catch(function(e) {
- if (e !== 'cancel') {
- self.$message.error('审核失败')
- }
- })
- },
- showRejectDialog(row) {
- this.currentAuditRow = row
- this.rejectReason = ''
- this.rejectDialogVisible = true
- },
- confirmReject() {
- if (!this.rejectReason.trim()) {
- this.$message.warning('请填写驳回原因')
- return
- }
- this.rejectDialogVisible = false
- this.handleAudit(this.currentAuditRow, 'rejected')
- },
- async handleCopy(row) {
- try {
- const res = await adminArticleDetail({ id: row.id })
- if (!res.data) {
- this.$message.error('文章不存在')
- return
- }
- const d = res.data
- const copyData = {
- categoryId: d.categoryId,
- title: d.title ? d.title + '(副本)' : '',
- summary: d.summary || '',
- coverImage: d.coverImage || '',
- content: d.content || '',
- tags: d.tags || '',
- author: d.author || '',
- readTime: d.readTime || 3,
- visibility: d.visibility || 'public',
- visibleTo: d.visibleTo || [],
- status: 'draft'
- }
- const createRes = await adminArticleCreate(copyData)
- if (createRes.data && createRes.data.id) {
- this.$message.success('已复制为草稿')
- this.loadList()
- } else {
- this.$message.error('复制失败')
- }
- } catch (e) {
- this.$message.error(e.message || '复制失败')
- }
- },
- handleActionCmd(row, cmd) {
- switch (cmd) {
- case 'submitReview': this.handleSubmitReview(row); break;
- case 'publishDirect': this.handlePublishDirect(row); break;
- case 'copy': this.handleCopy(row); break;
- case 'delete': this.handleDelete(row); break;
- case 'approve': this.handleAudit(row, 'approved'); break;
- case 'reject': this.showRejectDialog(row); break;
- case 'reDraft': this.handleReDraft(row); break;
- case 'withdraw': this.handleWithdraw(row); break;
- case 'toggleFeatured': this.handleToggleFeatured(row); break;
- case 'rePublish': this.handlePublishDirect(row); break;
- }
- }
- }
- }
- </script>
- <style scoped>
- .article-manage {
- padding: 20px;
- }
- .filter-bar {
- margin-bottom: 18px;
- display: flex;
- align-items: center;
- gap: 10px;
- flex-wrap: wrap;
- }
- .status-tabs {
- margin-bottom: 18px;
- display: flex;
- align-items: center;
- gap: 6px;
- flex-wrap: wrap;
- }
- </style>
|