Pārlūkot izejas kodu

feat(web): 文章/套餐审核添加详情弹窗、驳回原因显示、批量通过/驳回

Xiaogang Liao 2 mēneši atpakaļ
vecāks
revīzija
a8afcec4a9

+ 122 - 10
cfc-web/src/views/admin/review/ArticleAudit.vue

@@ -9,24 +9,34 @@
       </el-select>
       <el-input v-model="keyword" placeholder="搜索标题..." prefix-icon="el-icon-search" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" style="width:200px" />
       <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+      <el-button v-if="selectedRows.length > 0 && statusFilter === 'pending'" size="small" type="success" @click="handleBatchApprove">批量通过({{ selectedRows.length }})</el-button>
+      <el-button v-if="selectedRows.length > 0 && statusFilter === 'pending'" size="small" type="danger" @click="showBatchRejectDialog">批量驳回({{ selectedRows.length }})</el-button>
     </div>
     <div class="table-scroll-wrap"><div style="overflow:auto;max-height:calc(100vh - 300px);">
-    <el-table :data="list" v-loading="loading" border stripe style="min-width:900px;">
+    <el-table :data="list" v-loading="loading" border stripe style="min-width:900px;" @selection-change="onSelectionChange">
+      <el-table-column type="selection" width="40" v-if="statusFilter === 'pending'" />
       <el-table-column prop="id" label="ID" width="60" />
       <el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
-      <el-table-column label="审核状态" width="100">
+      <el-table-column label="审核状态" width="90">
         <template slot-scope="{ row }">
-          <el-tag :type="statusType(row.auditStatus)">{{ statusLabel(row.auditStatus) }}</el-tag>
+          <el-tag :type="statusType(row.auditStatus)" size="small">{{ statusLabel(row.auditStatus) }}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column prop="auditReason" label="原因" width="160" show-overflow-tooltip />
-      <el-table-column label="操作" width="180" fixed="right">
+      <el-table-column prop="auditReason" label="驳回原因" min-width="150" show-overflow-tooltip>
+        <template slot-scope="{ row }">
+          <span v-if="row.auditReason" style="color:#f56c6c;">{{ row.auditReason }}</span>
+          <span v-else style="color:#999;">-</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="createdAt" label="申请时间" width="160" />
+      <el-table-column label="操作" width="200" fixed="right">
         <template slot-scope="{ row }">
           <template v-if="row.auditStatus === 'pending'">
+            <el-button size="mini" type="primary" @click="showDetail(row)">查看</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>
           </template>
-          <el-button v-else size="mini" type="primary" @click="handleView(row)">查看</el-button>
+          <el-button v-else size="mini" type="primary" @click="showDetail(row)">查看</el-button>
         </template>
       </el-table-column>
     </el-table></div></div>
@@ -37,9 +47,54 @@
       :page-size="size"
       :total="total"
       layout="total, prev, pager, next"
-      @current-change="loadData"
+      @current-change="onPageChange"
       style="margin-top:12px;text-align:right;"
     />
+
+    <el-dialog title="文章详情" :visible.sync="detailVisible" width="800px" top="5vh">
+      <div v-if="currentRow" style="max-height:60vh;overflow-y:auto;">
+        <el-form label-width="100px" size="small">
+          <el-form-item label="ID">{{ currentRow.id }}</el-form-item>
+          <el-form-item label="标题">{{ currentRow.title }}</el-form-item>
+          <el-form-item label="状态"><el-tag :type="statusType(currentRow.auditStatus)">{{ statusLabel(currentRow.auditStatus) }}</el-tag></el-form-item>
+          <el-form-item label="分类">{{ currentRow.categoryName || '-' }}</el-form-item>
+          <el-form-item label="作者">{{ currentRow.authorName || '-' }}</el-form-item>
+          <el-form-item label="创建时间">{{ currentRow.createdAt }}</el-form-item>
+          <el-form-item label="发布时间">{{ currentRow.publishedAt || '-' }}</el-form-item>
+          <el-form-item label="内容摘要" v-if="currentRow.summary">{{ currentRow.summary }}</el-form-item>
+          <el-form-item label="内容" v-if="currentRow.content">
+            <div v-html="currentRow.content" style="max-height:300px;overflow-y:auto;border:1px solid #eee;padding:10px;border-radius:4px;background:#fafafa;"></div>
+          </el-form-item>
+          <el-form-item label="驳回原因" v-if="currentRow.auditReason" style="color:#f56c6c;">
+            {{ currentRow.auditReason }}
+          </el-form-item>
+        </el-form>
+      </div>
+      <span slot="footer" v-if="currentRow && currentRow.auditStatus === 'pending'">
+        <el-button @click="detailVisible = false">取消</el-button>
+        <el-button type="success" @click="handleAuditFromDialog">通过</el-button>
+        <el-button type="danger" @click="showRejectFromDialog">驳回</el-button>
+      </span>
+      <span slot="footer" v-else>
+        <el-button @click="detailVisible = false">关闭</el-button>
+      </span>
+    </el-dialog>
+
+    <el-dialog title="驳回原因" :visible.sync="rejectDialogVisible" width="450px">
+      <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因" />
+      <span slot="footer">
+        <el-button @click="rejectDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmReject">确认驳回</el-button>
+      </span>
+    </el-dialog>
+
+    <el-dialog title="批量驳回" :visible.sync="batchRejectDialogVisible" width="450px">
+      <el-input v-model="batchRejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因" />
+      <span slot="footer">
+        <el-button @click="batchRejectDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="confirmBatchReject">确认批量驳回({{ selectedRows.length }}条)</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
 
@@ -57,8 +112,12 @@ export default {
       page: 1,
       size: 20,
       total: 0,
+      selectedRows: [],
+      detailVisible: false,
       rejectDialogVisible: false,
+      batchRejectDialogVisible: false,
       rejectReason: '',
+      batchRejectReason: '',
       currentRow: null
     }
   },
@@ -86,10 +145,34 @@ export default {
         this.loading = false
       }
     },
+    onPageChange(p) {
+      this.page = p
+      this.loadData()
+    },
+    onSelectionChange(rows) {
+      this.selectedRows = rows
+    },
     handleSearch() {
       this.page = 1
       this.loadData()
     },
+    showDetail(row) {
+      this.currentRow = row
+      this.rejectReason = ''
+      this.detailVisible = true
+    },
+    handleView(row) {
+      this.$router.push('/article-edit?id=' + row.id + '&readonly=1')
+    },
+    handleAuditFromDialog() {
+      if (!this.currentRow) return
+      this.handleAudit(this.currentRow, 'approved')
+      this.detailVisible = false
+    },
+    showRejectFromDialog() {
+      this.detailVisible = false
+      this.rejectDialogVisible = true
+    },
     async handleAudit(row, action) {
       try {
         await this.$confirm(action === 'approved' ? '确认通过审核?' : '确认驳回?', '提示', { type: 'warning' })
@@ -107,6 +190,7 @@ export default {
     },
     async confirmReject() {
       if (!this.rejectReason.trim()) return this.$message.warning('请填写驳回原因')
+      if (!this.currentRow) return
       this.rejectDialogVisible = false
       try {
         await adminArticleAudit({ id: this.currentRow.id, auditStatus: 'rejected', auditReason: this.rejectReason })
@@ -116,8 +200,36 @@ export default {
         this.$message.error('操作失败')
       }
     },
-    handleView(row) {
-      this.$router.push('/article-edit?id=' + row.id + '&readonly=1')
+    showBatchRejectDialog() {
+      this.batchRejectReason = ''
+      this.batchRejectDialogVisible = true
+    },
+    async handleBatchApprove() {
+      try {
+        await this.$confirm(`确认通过所选 ${this.selectedRows.length} 篇文章?`, '提示', { type: 'warning' })
+        for (const row of this.selectedRows) {
+          await adminArticleAudit({ id: row.id, auditStatus: 'approved' })
+        }
+        this.$message.success('已批量通过')
+        this.selectedRows = []
+        this.loadData()
+      } catch (e) {
+        if (e !== 'cancel') this.$message.error('操作失败')
+      }
+    },
+    async confirmBatchReject() {
+      if (!this.batchRejectReason.trim()) return this.$message.warning('请填写驳回原因')
+      this.batchRejectDialogVisible = false
+      try {
+        for (const row of this.selectedRows) {
+          await adminArticleAudit({ id: row.id, auditStatus: 'rejected', auditReason: this.batchRejectReason })
+        }
+        this.$message.success('已批量驳回')
+        this.selectedRows = []
+        this.loadData()
+      } catch (e) {
+        this.$message.error('操作失败')
+      }
     },
     statusType(s) {
       return { approved: 'success', pending: 'warning', rejected: 'danger' }[s] || 'info'
@@ -130,5 +242,5 @@ export default {
 </script>
 
 <style scoped>
-.filter-bar { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; padding: 12px; background: #fff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.08); }
+.filter-bar { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; padding: 12px; background: #fff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.08); flex-wrap: wrap; }
 </style>

+ 142 - 19
cfc-web/src/views/admin/review/PackageAudit.vue

@@ -8,30 +8,40 @@
         <el-option label="已拒绝" value="rejected" />
       </el-select>
       <el-button size="small" @click="loadData" :loading="loading">刷新</el-button>
+      <el-button v-if="selectedRows.length > 0 && statusFilter === 'pending'" size="small" type="success" @click="handleBatchApprove">批量通过({{ selectedRows.length }})</el-button>
+      <el-button v-if="selectedRows.length > 0 && statusFilter === 'pending'" size="small" type="danger" @click="showBatchRejectDialog">批量驳回({{ selectedRows.length }})</el-button>
     </div>
     <div class="table-scroll-wrap"><div style="overflow:auto;max-height:calc(100vh - 300px);">
-    <el-table :data="list" v-loading="loading" border stripe style="min-width:900px;">
+    <el-table :data="list" v-loading="loading" border stripe style="min-width:900px;" @selection-change="onSelectionChange">
+      <el-table-column type="selection" width="40" v-if="statusFilter === 'pending'" />
       <el-table-column prop="id" label="ID" width="60" />
       <el-table-column prop="name" label="套餐名称" min-width="180" show-overflow-tooltip />
-      <el-table-column prop="cycleDays" label="周期" width="80">
+      <el-table-column prop="cycleDays" label="周期" width="70">
         <template slot-scope="{ row }">{{ row.cycleDays }}天</template>
       </el-table-column>
-      <el-table-column prop="price" label="价格" width="100">
+      <el-table-column prop="price" label="价格" width="90">
         <template slot-scope="{ row }">{{ formatPrice(row.price) }}</template>
       </el-table-column>
-      <el-table-column label="审核状态" width="100">
+      <el-table-column label="审核状态" width="90">
         <template slot-scope="{ row }">
-          <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
+          <el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="rejectReason" label="拒绝原因" min-width="150" show-overflow-tooltip>
+        <template slot-scope="{ row }">
+          <span v-if="row.rejectReason" style="color:#f56c6c;">{{ row.rejectReason }}</span>
+          <span v-else style="color:#999;">-</span>
         </template>
       </el-table-column>
       <el-table-column prop="createdAt" label="申请时间" width="160" />
-      <el-table-column label="操作" width="180" fixed="right">
+      <el-table-column label="操作" width="200" fixed="right">
         <template slot-scope="{ row }">
           <template v-if="row.status === 'pending'">
+            <el-button size="mini" type="primary" @click="showDetail(row)">查看</el-button>
             <el-button size="mini" type="success" @click="handleApprove(row)">通过</el-button>
-            <el-button size="mini" type="danger" @click="handleReject(row)">拒绝</el-button>
+            <el-button size="mini" type="danger" @click="showRejectDialog(row)">驳回</el-button>
           </template>
-          <span v-else style="color:#999">-</span>
+          <el-button v-else size="mini" type="primary" @click="showDetail(row)">查看</el-button>
         </template>
       </el-table-column>
     </el-table></div></div>
@@ -42,9 +52,52 @@
       :page-size="size"
       :total="total"
       layout="total, prev, pager, next"
-      @current-change="loadData"
+      @current-change="onPageChange"
       style="margin-top:12px;text-align:right;"
     />
+
+    <el-dialog title="套餐详情" :visible.sync="detailVisible" width="700px" top="5vh">
+      <div v-if="currentRow" style="max-height:60vh;overflow-y:auto;">
+        <el-form label-width="110px" size="small">
+          <el-form-item label="ID">{{ currentRow.id }}</el-form-item>
+          <el-form-item label="套餐名称">{{ currentRow.name }}</el-form-item>
+          <el-form-item label="状态"><el-tag :type="statusType(currentRow.status)">{{ statusLabel(currentRow.status) }}</el-tag></el-form-item>
+          <el-form-item label="周期">{{ currentRow.cycleDays }} 天</el-form-item>
+          <el-form-item label="价格">{{ formatPrice(currentRow.price) }}</el-form-item>
+          <el-form-item label="创建时间">{{ currentRow.createdAt }}</el-form-item>
+          <el-form-item label="详细介绍" v-if="currentRow.description">
+            <div style="border:1px solid #eee;padding:10px;border-radius:4px;background:#fafafa;white-space:pre-wrap;">{{ currentRow.description }}</div>
+          </el-form-item>
+          <el-form-item label="拒绝原因" v-if="currentRow.rejectReason" style="color:#f56c6c;">
+            {{ currentRow.rejectReason }}
+          </el-form-item>
+        </el-form>
+      </div>
+      <span slot="footer" v-if="currentRow && currentRow.status === 'pending'">
+        <el-button @click="detailVisible = false">取消</el-button>
+        <el-button type="success" @click="handleApproveFromDialog">通过</el-button>
+        <el-button type="danger" @click="showRejectFromDialog">驳回</el-button>
+      </span>
+      <span slot="footer" v-else>
+        <el-button @click="detailVisible = false">关闭</el-button>
+      </span>
+    </el-dialog>
+
+    <el-dialog title="驳回原因" :visible.sync="rejectDialogVisible" width="450px">
+      <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因" />
+      <span slot="footer">
+        <el-button @click="rejectDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmReject">确认驳回</el-button>
+      </span>
+    </el-dialog>
+
+    <el-dialog title="批量驳回" :visible.sync="batchRejectDialogVisible" width="450px">
+      <el-input v-model="batchRejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因" />
+      <span slot="footer">
+        <el-button @click="batchRejectDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="confirmBatchReject">确认批量驳回({{ selectedRows.length }}条)</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
 
@@ -60,7 +113,14 @@ export default {
       statusFilter: '',
       page: 1,
       size: 20,
-      total: 0
+      total: 0,
+      selectedRows: [],
+      detailVisible: false,
+      rejectDialogVisible: false,
+      batchRejectDialogVisible: false,
+      rejectReason: '',
+      batchRejectReason: '',
+      currentRow: null
     }
   },
   created() {
@@ -72,8 +132,12 @@ export default {
       try {
         const res = await getPackageList({})
         if (res.code === 200) {
-          this.list = Array.isArray(res.data) ? res.data : (res.data && res.data.records) || []
-          this.total = this.list.length
+          let data = Array.isArray(res.data) ? res.data : (res.data && res.data.records) || []
+          if (this.statusFilter) {
+            data = data.filter(item => item.status === this.statusFilter)
+          }
+          this.list = data
+          this.total = data.length
         }
       } catch (e) {
         this.$message.error('加载失败')
@@ -81,6 +145,27 @@ export default {
         this.loading = false
       }
     },
+    onPageChange(p) {
+      this.page = p
+      this.loadData()
+    },
+    onSelectionChange(rows) {
+      this.selectedRows = rows
+    },
+    showDetail(row) {
+      this.currentRow = row
+      this.rejectReason = ''
+      this.detailVisible = true
+    },
+    handleApproveFromDialog() {
+      if (!this.currentRow) return
+      this.handleApprove(this.currentRow)
+      this.detailVisible = false
+    },
+    showRejectFromDialog() {
+      this.detailVisible = false
+      this.rejectDialogVisible = true
+    },
     async handleApprove(row) {
       try {
         await this.$confirm('确认通过该套餐?', '提示', { type: 'warning' })
@@ -91,16 +176,54 @@ export default {
         if (e !== 'cancel') this.$message.error('操作失败')
       }
     },
-    async handleReject(row) {
+    showRejectDialog(row) {
+      this.currentRow = row
+      this.rejectReason = ''
+      this.rejectDialogVisible = true
+    },
+    async confirmReject() {
+      if (!this.rejectReason.trim()) return this.$message.warning('请填写驳回原因')
+      if (!this.currentRow) return
+      this.rejectDialogVisible = false
       try {
-        await this.$confirm('确认拒绝该套餐?', '提示', { type: 'warning' })
-        await reviewPackage({ id: row.id, action: 'reject' })
-        this.$message.success('已拒绝')
+        await reviewPackage({ id: this.currentRow.id, action: 'reject', reason: this.rejectReason })
+        this.$message.success('已驳回')
+        this.loadData()
+      } catch (e) {
+        this.$message.error('操作失败')
+      }
+    },
+    showBatchRejectDialog() {
+      this.batchRejectReason = ''
+      this.batchRejectDialogVisible = true
+    },
+    async handleBatchApprove() {
+      try {
+        await this.$confirm(`确认通过所选 ${this.selectedRows.length} 个套餐?`, '提示', { type: 'warning' })
+        for (const row of this.selectedRows) {
+          await reviewPackage({ id: row.id, action: 'approve' })
+        }
+        this.$message.success('已批量通过')
+        this.selectedRows = []
         this.loadData()
       } catch (e) {
         if (e !== 'cancel') this.$message.error('操作失败')
       }
     },
+    async confirmBatchReject() {
+      if (!this.batchRejectReason.trim()) return this.$message.warning('请填写驳回原因')
+      this.batchRejectDialogVisible = false
+      try {
+        for (const row of this.selectedRows) {
+          await reviewPackage({ id: row.id, action: 'reject', reason: this.batchRejectReason })
+        }
+        this.$message.success('已批量驳回')
+        this.selectedRows = []
+        this.loadData()
+      } catch (e) {
+        this.$message.error('操作失败')
+      }
+    },
     formatPrice(val) {
       if (val == null) return '-'
       return '¥' + (val / 100).toFixed(2)
@@ -109,12 +232,12 @@ export default {
       return { approved: 'success', pending: 'warning', rejected: 'danger' }[s] || 'info'
     },
     statusLabel(s) {
-      return { approved: '已通过', pending: '待审核', rejected: '已拒绝' }[s] || s
+      return { approved: '已通过', pending: '待审核', rejected: '已驳回' }[s] || s
     }
   }
 }
 </script>
 
 <style scoped>
-.filter-bar { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; padding: 12px; background: #fff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.08); }
-</style>
+.filter-bar { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; padding: 12px; background: #fff; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.08); flex-wrap: wrap; }
+</style>