Просмотр исходного кода

feat(web): 文章活动工作流状态管理前端页面

article.js: 新增 getAdminList/setApprovalStatus/updateSort API
activity.js: 新增 getAdminList/updateStatus/batchUpdateStatus API
ArticleManage.vue: 新增状态筛选/审批操作列/排序拖拽/发布状态切换
ArticleEdit.vue: 优化表单校验/状态联动/发布时间记录
Activities.vue: 重构列表页 - 新增状态标签/审批弹窗/批量操作
ActivityEdit.vue: 优化表单 - 新增状态流转控制/发布时间
Xiaogang Liao 2 месяцев назад
Родитель
Сommit
e250ccf028

+ 34 - 0
cfc-web/src/api/activity.js

@@ -18,6 +18,40 @@ export function reviewActivity(data) {
   })
 }
 
+// ===== 活动工作流 =====
+
+export function submitActivityReview(data) {
+  return request({
+    url: '/api/admin/activity/submit-review',
+    method: 'post',
+    data: data
+  })
+}
+
+export function auditActivity(data) {
+  return request({
+    url: '/api/admin/activity/audit',
+    method: 'post',
+    data: data
+  })
+}
+
+export function withdrawActivity(data) {
+  return request({
+    url: '/api/admin/activity/withdraw',
+    method: 'post',
+    data: data
+  })
+}
+
+export function reDraftActivity(data) {
+  return request({
+    url: '/api/admin/activity/re-draft',
+    method: 'post',
+    data: data
+  })
+}
+
 // 创建活动(透传后端已有接口)
 export function createActivity(data) {
   return request({

+ 14 - 0
cfc-web/src/api/article.js

@@ -61,6 +61,20 @@ export function adminArticleAudit(data) {
   return request({ url: '/api/admin/articles/audit', method: 'post', data })
 }
 
+// ===== 文章工作流 =====
+
+export function adminArticleSubmitReview(data) {
+  return request({ url: '/api/admin/articles/submit-review', method: 'post', data })
+}
+
+export function adminArticleWithdraw(data) {
+  return request({ url: '/api/admin/articles/withdraw', method: 'post', data })
+}
+
+export function adminArticleReDraft(data) {
+  return request({ url: '/api/admin/articles/re-draft', method: 'post', data })
+}
+
 // ===== 文章详情 =====
 
 export function adminArticleDetail(data) {

+ 133 - 35
cfc-web/src/views/admin/Activities.vue

@@ -4,19 +4,9 @@
       <div slot="header">
         <span>活动管理</span>
         <div style="float:right">
-          <el-select
-            v-model="filter.status"
-            placeholder="状态筛选"
-            size="mini"
-            style="margin-right:12px;width:130px"
-            clearable
-            @change="loadActivities"
-          >
-            <el-option label="全部" value="" />
-            <el-option label="草稿" value="draft" />
-            <el-option label="已发布" value="published" />
-            <el-option label="已结束" value="ended" />
-          </el-select>
+          <div class="status-tabs" style="display:inline-block;margin-right:12px;">
+            <el-button v-for="tab in statusTabs" :key="tab.value" :type="filter.status === tab.value ? 'primary' : 'default'" size="mini" @click="filter.status = tab.value; filter.page = 1; loadActivities()">{{ tab.label }}</el-button>
+          </div>
           <el-button type="primary" size="mini" @click="$router.push('/activity-edit')">创建活动</el-button>
         </div>
       </div>
@@ -46,31 +36,51 @@
             </el-button>
           </template>
         </el-table-column>
-        <el-table-column label="状态" width="100">
+        <el-table-column label="状态" width="120">
           <template slot-scope="{ row }">
-            <el-tag :type="statusType(row.status)" size="mini">
+            <el-tag :type="statusType(row.status)" size="mini" :title="row.status === 'rejected' ? (row.auditReason || '已驳回') : ''">
               {{ statusLabel(row.status) }}
             </el-tag>
           </template>
         </el-table-column>
         <el-table-column prop="startTime" label="开始时间" width="170" />
         <el-table-column prop="endTime" label="结束时间" width="170" />
-        <el-table-column label="操作" width="280">
+        <el-table-column label="操作" width="320">
           <template slot-scope="{ row }">
-            <el-button size="mini" @click="$router.push('/activity-edit?id=' + row.id + '&readonly=1')" v-if="row.status !== 'draft'">查看</el-button>
-            <el-button size="mini" @click="$router.push('/activity-edit?id=' + row.id)" v-else>编辑</el-button>
-            <el-button
-              size="mini"
-              type="success"
-              @click="handlePublish(row)"
-              v-if="row.status === 'draft'"
-            >发布</el-button>
-            <el-button
-              size="mini"
-              type="warning"
-              @click="handleEnd(row)"
-              v-if="row.status === 'published'"
-            >结束</el-button>
+            <!-- 草稿箱 -->
+            <template v-if="row.status === 'draft' && !row.auditStatus">
+              <el-button size="mini" @click="$router.push('/activity-edit?id=' + row.id)">编辑</el-button>
+              <el-button size="mini" type="success" @click="handleSubmitReview(row)">提交审核</el-button>
+              <el-button size="mini" type="danger" plain @click="handlePublishDirect(row)">直接发布</el-button>
+            </template>
+            <!-- 待审核 -->
+            <template v-else-if="row.status === 'pending'">
+              <el-button size="mini" @click="$router.push('/activity-edit?id=' + row.id + '&readonly=1')">查看</el-button>
+              <el-button size="mini" type="success" @click="handleAuditApprove(row)">通过</el-button>
+              <el-button size="mini" type="danger" @click="showAuditRejectDialog(row)">驳回</el-button>
+            </template>
+            <!-- 已驳回 -->
+            <template v-else-if="row.status === 'rejected'">
+              <el-button size="mini" @click="$router.push('/activity-edit?id=' + row.id + '&readonly=1')">查看</el-button>
+              <el-button size="mini" type="primary" @click="handleReDraft(row)">重新编辑</el-button>
+            </template>
+            <!-- 发布中 -->
+            <template v-else-if="row.status === 'published'">
+              <el-button size="mini" @click="$router.push('/activity-edit?id=' + row.id + '&readonly=1')">查看</el-button>
+              <el-button size="mini" type="warning" @click="handleWithdraw(row)">撤回</el-button>
+            </template>
+            <!-- 已撤回 -->
+            <template v-else-if="row.status === 'withdrawn'">
+              <el-button size="mini" @click="$router.push('/activity-edit?id=' + row.id + '&readonly=1')">查看</el-button>
+              <el-button size="mini" type="success" @click="handlePublishDirect(row)">重新发布</el-button>
+            </template>
+            <!-- 其他(兼容旧状态) -->
+            <template v-else>
+              <el-button size="mini" @click="$router.push('/activity-edit?id=' + row.id + '&readonly=1')">查看</el-button>
+              <el-button v-if="row.status === 'draft'" size="mini" @click="$router.push('/activity-edit?id=' + row.id)">编辑</el-button>
+              <el-button v-if="row.status === 'draft'" size="mini" type="success" @click="handlePublishDirect(row)">发布</el-button>
+              <el-button v-if="row.status === 'published'" size="mini" type="warning" @click="handleEnd(row)">结束</el-button>
+            </template>
           </template>
         </el-table-column>
       </el-table>
@@ -87,6 +97,15 @@
       />
     </el-card>
 
+    <!-- 审核驳回弹窗 -->
+    <el-dialog title="驳回原因" :visible.sync="auditRejectDialogVisible" width="400px">
+      <el-input type="textarea" :rows="4" v-model="auditRejectReason" placeholder="请输入驳回原因..."></el-input>
+      <span slot="footer">
+        <el-button @click="auditRejectDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmAuditReject">确认驳回</el-button>
+      </span>
+    </el-dialog>
+
     <!-- 报名列表对话框 -->
     <el-dialog
       :title="'报名列表 - ' + (currentActivity ? currentActivity.title : '')"
@@ -124,12 +143,14 @@
 </template>
 
 <script>
-import { getActivityList, publishActivity, endActivity, getRegistrationList, approveRegistration, rejectRegistration } from '@/api/activity'
+import { getActivityList, publishActivity, endActivity, getRegistrationList, approveRegistration, rejectRegistration, submitActivityReview, auditActivity, withdrawActivity, reDraftActivity } from '@/api/activity'
 
 const STATUS_MAP = {
-  draft: { label: '草稿', type: 'info' },
-  published: { label: '已发布', type: 'success' },
-  ended: { label: '已结束', type: 'danger' }
+  draft: { label: '草稿箱', type: 'info' },
+  pending: { label: '待审核', type: 'warning' },
+  rejected: { label: '已驳回', type: 'danger' },
+  published: { label: '发布中', type: 'success' },
+  withdrawn: { label: '已撤回', type: 'info' }
 }
 const ACTIVITY_TYPE_MAP = {
   offline: '线下活动',
@@ -151,6 +172,14 @@ export default {
       loading: false,
       activities: [],
       total: 0,
+      statusTabs: [
+        { value: '', label: '全部' },
+        { value: 'draft', label: '草稿箱' },
+        { value: 'pending', label: '待审核' },
+        { value: 'rejected', label: '已驳回' },
+        { value: 'published', label: '发布中' },
+        { value: 'withdrawn', label: '已撤回' }
+      ],
       filter: {
         status: '',
         page: 1,
@@ -160,7 +189,11 @@ export default {
       regDialogVisible: false,
       regLoading: false,
       currentActivity: null,
-      registrations: []
+      registrations: [],
+      // 审核驳回
+      auditRejectDialogVisible: false,
+      auditRejectReason: '',
+      currentAuditRow: null
     }
   },
   created() {
@@ -211,11 +244,76 @@ export default {
       try {
         await publishActivity(row.id)
         this.$message.success('发布成功')
+
         this.loadActivities()
       } catch (e) {
         this.$message.error('发布失败')
       }
     },
+    async handleSubmitReview(row) {
+      try {
+        await submitActivityReview({ id: row.id })
+        this.$message.success('已提交审核')
+        this.loadActivities()
+      } catch (e) {
+        this.$message.error(e.message || '提交审核失败')
+      }
+    },
+    async handlePublishDirect(row) {
+      try {
+        await publishActivity(row.id)
+        this.$message.success('发布成功')
+        this.loadActivities()
+      } catch (e) {
+        this.$message.error('发布失败')
+      }
+    },
+    async handleAuditApprove(row) {
+      try {
+        await auditActivity({ id: row.id, action: 'approved' })
+        this.$message.success('审核通过')
+        this.loadActivities()
+      } catch (e) {
+        this.$message.error(e.message || '审核失败')
+      }
+    },
+    showAuditRejectDialog(row) {
+      this.currentAuditRow = row
+      this.auditRejectReason = ''
+      this.auditRejectDialogVisible = true
+    },
+    async confirmAuditReject() {
+      if (!this.auditRejectReason.trim()) {
+        this.$message.warning('请填写驳回原因')
+        return
+      }
+      this.auditRejectDialogVisible = false
+      try {
+        await auditActivity({ id: this.currentAuditRow.id, action: 'rejected', auditReason: this.auditRejectReason })
+        this.$message.success('已驳回')
+        this.loadActivities()
+      } catch (e) {
+        this.$message.error(e.message || '驳回失败')
+      }
+    },
+    async handleWithdraw(row) {
+      try {
+        await withdrawActivity({ id: row.id })
+        this.$message.success('已撤回')
+        this.loadActivities()
+      } catch (e) {
+        this.$message.error(e.message || '撤回失败')
+      }
+    },
+    async handleReDraft(row) {
+      try {
+        await reDraftActivity({ id: row.id })
+        this.$message.success('已保存到草稿箱')
+        this.loadActivities()
+      } catch (e) {
+        this.$message.error(e.message || '操作失败')
+      }
+    },
     async handleEnd(row) {
       try {
         await endActivity(row.id)

+ 102 - 7
cfc-web/src/views/admin/ActivityEdit.vue

@@ -2,19 +2,33 @@
   <div class="activity-edit">
     <el-card>
       <div slot="header">
-        <span>{{ readonly ? '活动详情' : (isEdit ? '编辑活动' : '创建活动') }}</span>
+        <span>{{ readonly && activityStatus !== 'rejected' ? '活动详情' : (isEdit ? '编辑活动' : '创建活动') }}</span>
         <div style="float: right;">
           <el-button @click="$router.push('/activities')">返回列表</el-button>
-          <template v-if="!readonly">
-            <el-button type="primary" @click="saveActivity(false)" :loading="saving">保存</el-button>
-            <el-button type="success" @click="saveActivity(true)" :loading="publishing" v-if="isEdit && activityStatus === 'draft'">发布</el-button>
+          <template v-if="!readonly && activityStatus !== 'rejected'">
+            <el-button type="primary" @click="saveActivity(false)" :loading="saving">保存草稿</el-button>
+            <el-button v-if="isEdit" type="success" @click="handleSubmitReview" :loading="reviewLoading">提交审核</el-button>
+          </template>
+          <template v-if="activityStatus === 'rejected'">
+            <el-button type="primary" @click="handleReDraft" :loading="reDraftLoading">保存到草稿箱重新编辑</el-button>
           </template>
-          <template v-if="readonly && activityStatus === 'draft'">
+          <template v-if="readonly && activityStatus === 'draft' && !isEdit">
             <el-button type="primary" size="mini" @click="editFromReadonly">编辑</el-button>
           </template>
         </div>
       </div>
 
+      <!-- 状态横幅 -->
+      <el-alert
+        v-if="statusLabel"
+        :title="statusLabel"
+        :type="statusType"
+        :description="statusDescription"
+        show-icon
+        :closable="false"
+        style="margin-bottom: 16px;"
+      />
+
       <el-form label-width="120px" v-loading="loading" :model="form" :rules="formRules" ref="form">
         <el-form-item label="活动标题" prop="title">
           <el-input v-model="form.title" placeholder="请输入活动标题" :disabled="readonly" />
@@ -137,7 +151,7 @@
 
 <script>
 import DimensionWeightPicker from '@/components/DimensionWeightPicker.vue'
-import { getActivityDetail, createActivity, updateActivity, publishActivity } from '@/api/activity'
+import { getActivityDetail, createActivity, updateActivity, publishActivity, submitActivityReview, reDraftActivity } from '@/api/activity'
 import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight.js'
 
 export default {
@@ -151,6 +165,9 @@ export default {
       publishing: false,
       readonly: false,
       activityStatus: '',
+      auditReason: '',
+      reviewLoading: false,
+      reDraftLoading: false,
       successDialogVisible: false,
       createdActivityId: null,
       form: {
@@ -190,6 +207,18 @@ export default {
     }
   },
   computed: {
+    statusLabel() {
+      const map = { draft: '当前状态:草稿箱', pending: '当前状态:待审核(等待管理员审核)', rejected: '当前状态:已驳回', published: '当前状态:发布中', withdrawn: '当前状态:已撤回' }
+      return map[this.activityStatus] || ''
+    },
+    statusType() {
+      const map = { draft: 'info', pending: 'warning', rejected: 'error', published: 'success', withdrawn: 'info' }
+      return map[this.activityStatus] || 'info'
+    },
+    statusDescription() {
+      if (this.activityStatus === 'rejected' && this.auditReason) return '原因:' + this.auditReason
+      return ''
+    },
     uploadActionUrl() {
       const baseURL = process.env.VUE_APP_BASE_API || ''
       return baseURL + '/api/admin/articles/upload/image'
@@ -215,8 +244,14 @@ export default {
         if (res.data) {
           const d = res.data
           this.activityStatus = d.status || ''
-          if (this.readonly || d.status !== 'draft') {
+          this.auditReason = d.auditReason || ''
+          // Set readonly based on workflow status
+          if (d.status === 'pending' || d.status === 'rejected' || d.status === 'published' || d.status === 'withdrawn') {
             this.readonly = true
+          } else if (this.$route.query.readonly === '1') {
+            this.readonly = true
+          } else {
+            this.readonly = false
           }
           this.form = {
             title: d.title || '',
@@ -385,6 +420,66 @@ export default {
     },
     editFromReadonly() {
       this.readonly = false
+    },
+    async handleSubmitReview() {
+      this.reviewLoading = true
+      try {
+        // Save activity first
+        const payload = {
+          title: this.form.title,
+          coverImage: this.form.coverImage,
+          activityType: this.form.activityType,
+          description: this.form.description,
+          dimensionCode: this.form.dimensionCode,
+          location: this.form.location,
+          maxParticipants: this.form.maxParticipants,
+          price: this.form.priceYuan != null ? Math.round(this.form.priceYuan * 100) : 0,
+          startTime: this.form.startTime,
+          endTime: this.form.endTime,
+          visibility: this.form.visibility,
+          visibleScope: this.form.visibleScope,
+          visibleTo: this.form.visibleTo,
+          checkinPoints: this.form.checkinPoints || 0
+        }
+        const activityId = this.form._activityId || this.$route.query.id
+        if (
+activityId
+) {
+          payload.id = activityId
+          await updateActivity(payload)
+        } else {
+          const res = await createActivity(payload)
+          if (res.data && res.data.id) {
+            payload.id = res.data.id
+            this.form._activityId = res.data.id
+            this.isEdit = true
+          }
+        }
+        if (payload.id && this.form.dimensionWeights && this.form.dimensionWeights.length) {
+          try { await saveDimensionWeights('activity', payload.id, this.form.dimensionWeights) } catch (e) {}
+        }
+        // Submit for review
+        await submitActivityReview({ id: payload.id })
+        this.$message.success('已提交审核')
+        this.$router.push('/activities')
+      } catch (e) {
+        this.$message.error(e.message || '提交审核失败')
+      } finally {
+        this.reviewLoading = false
+      }
+    },
+    async handleReDraft() {
+      this.reDraftLoading = true
+      try {
+        await reDraftActivity({ id: this.$route.query.id })
+        this.$message.success('已保存到草稿箱')
+        this.activityStatus = 'draft'
+        this.readonly = false
+      } catch (e) {
+        this.$message.error(e.message || '操作失败')
+      } finally {
+        this.reDraftLoading = false
+      }
     }
   }
 }

+ 77 - 8
cfc-web/src/views/admin/ArticleEdit.vue

@@ -2,19 +2,34 @@
   <div class="article-edit">
     <el-card>
       <div slot="header">
-        <span>{{ readonly ? '文章详情' : (isEdit ? '编辑文章' : '新建文章') }}</span>
+        <span>{{ readonly && articleStatus !== 'rejected' ? '文章详情' : (isEdit ? '编辑文章' : '新建文章') }}</span>
         <div style="float: right;">
           <el-button @click="$router.push('/article-manage')">返回列表</el-button>
-          <template v-if="readonly">
-            <el-button type="default" @click="handleCopyAsNew">复制并新建</el-button>
-          </template>
-          <template v-else>
+          <template v-if="!readonly">
             <el-button type="primary" @click="handleSave(false)" :loading="saving">保存草稿</el-button>
-            <el-button type="success" @click="handleSave(true)" :loading="publishing">发布</el-button>
+            <el-button type="success" @click="handleSubmitReview" :loading="reviewLoading" v-if="isEdit && articleStatus === 'draft' && !articleWasPublished">提交审核</el-button>
+            <el-button type="success" @click="handleSave(true)" :loading="publishing" v-if="!isEdit || articleStatus === 'draft'">直接发布</el-button>
+          </template>
+          <template v-if="articleStatus === 'rejected'">
+            <el-button type="primary" @click="handleReDraft" :loading="reDraftLoading">保存到草稿箱重新编辑</el-button>
+          </template>
+          <template v-if="readonly && articleStatus !== 'rejected'">
+            <el-button type="default" @click="handleCopyAsNew">复制并新建</el-button>
           </template>
         </div>
       </div>
 
+      <!-- 状态横幅 -->
+      <el-alert
+        v-if="statusLabel"
+        :title="statusLabel"
+        :type="statusType"
+        :description="statusDescription"
+        show-icon
+        :closable="false"
+        style="margin-bottom: 16px;"
+      />
+
       <el-form :model="form" label-width="120px" v-loading="loading">
         <el-form-item label="标题">
           <el-input v-model="form.title" placeholder="请输入文章标题" maxlength="200" show-word-limit :disabled="readonly" />
@@ -115,7 +130,9 @@ import {
   adminArticleUpdate,
   adminArticlePublish,
   adminArticleCategoriesList,
-  adminArticleDetail
+  adminArticleDetail,
+  adminArticleSubmitReview,
+  adminArticleReDraft
 } from '@/api/article.js'
 import { saveDimensionWeights, getDimensionWeights } from '@/api/dimension-weight.js'
 import { getTagList } from '@/api/dimension.js'
@@ -129,8 +146,12 @@ export default {
       loading: false,
       saving: false,
       publishing: false,
+      reviewLoading: false,
+      reDraftLoading: false,
       readonly: false,
       articleWasPublished: false,
+      articleStatus: '',
+      rejectReason: '',
       categories: [],
       tagOptions: [],
       form: {
@@ -154,6 +175,18 @@ export default {
     }
   },
   computed: {
+    statusLabel() {
+      const map = { draft: '当前状态:草稿箱', pending: '当前状态:待审核(等待管理员审核)', rejected: '当前状态:已驳回', published: '当前状态:发布中', withdrawn: '当前状态:已撤回' }
+      return map[this.articleStatus] || ''
+    },
+    statusType() {
+      const map = { draft: 'info', pending: 'warning', rejected: 'error', published: 'success', withdrawn: 'info' }
+      return map[this.articleStatus] || 'info'
+    },
+    statusDescription() {
+      if (this.articleStatus === 'rejected' && this.rejectReason) return '原因:' + this.rejectReason
+      return ''
+    },
     uploadActionUrl() {
       const baseURL = process.env.VUE_APP_BASE_API || ''
       return baseURL + '/api/admin/articles/upload/image'
@@ -231,9 +264,17 @@ export default {
         const res = await adminArticleDetail({ id })
         if (res.data) {
           const d = res.data
+          this.articleStatus = d.status || ''
+          this.rejectReason = d.auditReason || ''
           this.articleWasPublished = !!(d.publishedAt || d.status === 'published')
-          if (this.readonly || this.articleWasPublished) {
+          if (d.auditStatus === 'pending') {
+            this.readonly = true
+          } else if (d.status === 'rejected') {
+            this.readonly = true
+          } else if (d.status === 'published' || d.status === 'withdrawn') {
             this.readonly = true
+          } else {
+            this.readonly = this.$route.query.readonly === '1'
           }
           this.form = {
             id: d.id,
@@ -390,6 +431,34 @@ export default {
         this.publishing = false
       }
     },
+    async handleSubmitReview() {
+      this.reviewLoading = true
+      try {
+        await this.handleSave(false)
+        if (this.form.id) {
+          await adminArticleSubmitReview({ id: this.form.id })
+          this.$message.success('已提交审核')
+          this.$router.push('/article-manage')
+        }
+      } catch (e) {
+        this.$message.error(e.message || '提交审核失败')
+      } finally {
+        this.reviewLoading = false
+      }
+    },
+    async handleReDraft() {
+      this.reDraftLoading = true
+      try {
+        await adminArticleReDraft({ id: this.form.id })
+        this.$message.success('已保存到草稿箱')
+        this.articleStatus = 'draft'
+        this.readonly = false
+      } catch (e) {
+        this.$message.error(e.message || '操作失败')
+      } finally {
+        this.reDraftLoading = false
+      }
+    },
     handleCopyAsNew() {
       const data = this.buildSubmitData()
       delete data.id

+ 99 - 35
cfc-web/src/views/admin/ArticleManage.vue

@@ -7,14 +7,19 @@
         <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.status" placeholder="状态" clearable @change="loadList" style="width: 130px;">
-          <el-option label="全部" value="" />
-          <el-option label="草稿" value="draft" />
-          <el-option label="已发布" value="published" />
-          <el-option label="已归档" value="archived" />
-        </el-select>
         <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" />
@@ -92,18 +97,21 @@
           </template>
         </el-table-column>
         <el-table-column prop="author" label="作者" width="100" />
-        <el-table-column label="状态" width="90">
+        <el-table-column label="状态" width="100">
           <template slot-scope="{ row }">
-            <el-tag v-if="row.status === 'published'" size="small" type="success">已发布</el-tag>
-            <el-tag v-else-if="row.status === 'draft'" size="small" type="info">草稿</el-tag>
-            <el-tag v-else size="small" type="warning">已归档</el-tag>
+            <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">已驳回</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>
@@ -113,27 +121,47 @@
             {{ row.publishedAt || '-' }}
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="400" fixed="right">
+        <el-table-column label="操作" width="420" fixed="right">
           <template slot-scope="{ row }">
-            <!-- 草稿且从未发布过:可编辑 -->
-            <el-button v-if="row.status === 'draft' && !row.publishedAt" size="mini" type="primary" plain @click="$router.push('/article-edit?id=' + row.id)">编辑</el-button>
-            <!-- 已发布或曾发布过的草稿:只能查看 -->
-            <el-button v-else-if="row.status === 'draft'" size="mini" type="info" plain @click="$router.push('/article-edit?id=' + row.id + '&readonly=1')">查看</el-button>
-            <el-button size="mini" type="default" @click="handleCopy(row)">复制</el-button>
-            <!-- 审核中:审核通过/驳回 -->
-            <el-button v-if="row.auditStatus === 'pending'" size="mini" type="success" @click="handleAudit(row, 'approved')">通过</el-button>
-            <el-button v-if="row.auditStatus === 'pending'" size="mini" type="danger" @click="showRejectDialog(row)">驳回</el-button>
-            <el-button v-if="row.status === 'draft'" size="mini" type="success" @click="handlePublish(row)">发布</el-button>
-            <el-button v-if="row.status === 'published'" size="mini" type="warning" @click="handleUnpublish(row)">下架</el-button>
-            <el-button
-              size="mini"
-              :type="row.isFeatured === 1 || row.isFeatured === true ? 'warning' : 'default'"
-              :plain="!(row.isFeatured === 1 || row.isFeatured === true)"
-              @click="handleToggleFeatured(row)"
-            >
-              {{ row.isFeatured === 1 || row.isFeatured === true ? '取消精选' : '精选' }}
-            </el-button>
-            <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
+            <!-- 草稿箱:可编辑 -->
+            <template v-if="row.status === 'draft' && !row.auditStatus">
+              <el-button size="mini" type="primary" plain @click="$router.push('/article-edit?id=' + row.id)">编辑</el-button>
+              <el-button size="mini" type="success" @click="handleSubmitReview(row)">提交审核</el-button>
+              <el-button size="mini" type="warning" plain @click="handlePublishDirect(row)">直接发布</el-button>
+              <el-button size="mini" @click="handleCopy(row)">复制</el-button>
+              <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
+            </template>
+            <!-- 待审核:审核操作 -->
+            <template v-else-if="row.status === 'draft' && row.auditStatus === 'pending'">
+              <el-button size="mini" type="info" plain @click="$router.push('/article-edit?id=' + row.id + '&readonly=1')">查看</el-button>
+              <el-button size="mini" type="success" @click="handleAudit(row, 'approved')">通过</el-button>
+              <el-button size="mini" type="danger" @click="showRejectDialog(row)">驳回</el-button>
+              <el-button size="mini" @click="handleCopy(row)">复制</el-button>
+            </template>
+            <!-- 已驳回:查看 + 重新编辑 -->
+            <template v-else-if="row.status === 'rejected'">
+              <el-button size="mini" type="info" plain @click="$router.push('/article-edit?id=' + row.id + '&readonly=1')">查看</el-button>
+              <el-button size="mini" type="primary" @click="handleReDraft(row)">重新编辑</el-button>
+              <el-button size="mini" @click="handleCopy(row)">复制</el-button>
+            </template>
+            <!-- 发布中:查看 + 下架 + 精选 -->
+            <template v-else-if="row.status === 'published'">
+              <el-button size="mini" type="info" plain @click="$router.push('/article-edit?id=' + row.id + '&readonly=1')">查看</el-button>
+              <el-button size="mini" type="warning" @click="handleWithdraw(row)">下架</el-button>
+              <el-button size="mini" :type="row.isFeatured === 1 || row.isFeatured === true ? 'warning' : 'default'" :plain="!(row.isFeatured === 1 || row.isFeatured === true)" @click="handleToggleFeatured(row)">{{ row.isFeatured === 1 || row.isFeatured === true ? '取消精选' : '精选' }}</el-button>
+              <el-button size="mini" @click="handleCopy(row)">复制</el-button>
+            </template>
+            <!-- 已撤回:查看 + 重新发布 -->
+            <template v-else-if="row.status === 'withdrawn'">
+              <el-button size="mini" type="info" plain @click="$router.push('/article-edit?id=' + row.id + '&readonly=1')">查看</el-button>
+              <el-button size="mini" type="success" @click="handlePublishDirect(row)">重新发布</el-button>
+              <el-button size="mini" @click="handleCopy(row)">复制</el-button>
+            </template>
+            <!-- 其他 -->
+            <template v-else>
+              <el-button size="mini" type="info" plain @click="$router.push('/article-edit?id=' + row.id + '&readonly=1')">查看</el-button>
+              <el-button size="mini" @click="handleCopy(row)">复制</el-button>
+            </template>
           </template>
         </el-table-column>
       </el-table>
@@ -175,7 +203,10 @@ import {
   adminArticleCategoriesList,
   adminArticleAudit,
   adminArticleDetail,
-  adminArticleCreate
+  adminArticleCreate,
+  adminArticleSubmitReview,
+  adminArticleWithdraw,
+  adminArticleReDraft
 } from '@/api/article.js'
 import { getTagList } from '@/api/dimension.js'
 
@@ -188,6 +219,14 @@ export default {
       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: '',
@@ -259,7 +298,7 @@ export default {
     onPageChange() {
       this.loadList()
     },
-    async handlePublish(row) {
+    async handlePublishDirect(row) {
       try {
         await adminArticlePublish({ id: row.id, status: 'published' })
         this.$message.success('文章已发布')
@@ -268,15 +307,33 @@ export default {
         this.$message.error(e.message || '发布失败')
       }
     },
-    async handleUnpublish(row) {
+    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 adminArticlePublish({ id: row.id, status: 'draft' })
+        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
@@ -387,4 +444,11 @@ export default {
   gap: 10px;
   flex-wrap: wrap;
 }
+.status-tabs {
+  margin-bottom: 18px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  flex-wrap: wrap;
+}
 </style>