Browse Source

feat(review): 新增通用审核详情抽屉组件

Xiaogang Liao 5 days ago
parent
commit
d5fdb6061c

+ 59 - 0
cfc-web/src/views/admin/review/__tests__/AuditDetailDrawer.spec.js

@@ -0,0 +1,59 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils'
+import AuditDetailDrawer from '../components/AuditDetailDrawer.vue'
+
+const localVue = createLocalVue()
+localVue.directive('loading', {})
+
+const stubs = { 'el-drawer': true, 'el-button': true, 'el-input': true, 'el-tag': true }
+const mocks = {
+  $message: { warning: jest.fn() },
+  $confirm: jest.fn(() => Promise.resolve())
+}
+
+function mount(propsData) {
+  return shallowMount(AuditDetailDrawer, { propsData, stubs, mocks, localVue })
+}
+
+describe('AuditDetailDrawer', () => {
+  it('驳回意见为空时不发 reject', () => {
+    const wrapper = mount({ visible: true, status: 'pending', actionMode: 'approve-reject' })
+    wrapper.vm.showRejectInput = true
+    wrapper.vm.rejectReason = '   '
+    wrapper.vm.confirmReject()
+    expect(wrapper.emitted().reject).toBeUndefined()
+  })
+
+  it('驳回意见非空时 emit reject(reason)', () => {
+    const wrapper = mount({ visible: true, status: 'pending', actionMode: 'approve-reject' })
+    wrapper.vm.showRejectInput = true
+    wrapper.vm.rejectReason = ' 材料不全 '
+    wrapper.vm.confirmReject()
+    expect(wrapper.emitted().reject[0]).toEqual(['材料不全'])
+  })
+
+  it('cancel 模式不提供通过动作且原因必填', () => {
+    const wrapper = mount({ visible: true, status: 'processing', actionMode: 'cancel' })
+    expect(wrapper.vm.showApprove).toBe(false)
+    wrapper.vm.cancelReason = ''
+    wrapper.vm.confirmCancel()
+    expect(wrapper.emitted().cancel).toBeUndefined()
+  })
+
+  it('cancel 模式原因非空时 emit cancel(reason)', () => {
+    const wrapper = mount({ visible: true, status: 'pending', actionMode: 'cancel' })
+    wrapper.vm.cancelReason = '用户已收到其他退款'
+    wrapper.vm.confirmCancel()
+    expect(wrapper.emitted().cancel[0]).toEqual(['用户已收到其他退款'])
+  })
+
+  it('已审核状态不展示操作区(isPending=false)', () => {
+    const wrapper = mount({ visible: true, status: 'approved', actionMode: 'approve-reject' })
+    expect(wrapper.vm.isPending).toBe(false)
+  })
+
+  it('数字 0 状态视为待审核', () => {
+    const wrapper = mount({ visible: true, status: 0, actionMode: 'approve-reject' })
+    expect(wrapper.vm.isPending).toBe(true)
+    expect(wrapper.vm.statusLabel).toBe('待审核')
+  })
+})

+ 169 - 0
cfc-web/src/views/admin/review/components/AuditDetailDrawer.vue

@@ -0,0 +1,169 @@
+<template>
+  <el-drawer
+    :visible="visible"
+    :title="title"
+    direction="rtl"
+    size="720px"
+    :wrapper-closable="false"
+    custom-class="audit-detail-drawer"
+    @update:visible="onVisibleChange"
+    @close="handleClose"
+  >
+    <div v-loading="loading" class="audit-detail-wrap">
+      <div class="audit-detail-head">
+        <el-tag :type="statusTagType" size="small">{{ statusLabel }}</el-tag>
+        <slot name="head" />
+      </div>
+      <div class="audit-detail-body">
+        <slot />
+      </div>
+    </div>
+
+    <div class="audit-detail-footer">
+      <template v-if="isPending">
+        <template v-if="isCancelMode">
+          <template v-if="showCancelInput">
+            <el-input v-model="cancelReason" type="textarea" :rows="3" placeholder="请输入中止原因(必填)" />
+            <div class="audit-detail-footer-actions">
+              <el-button size="small" @click="showCancelInput = false">取消</el-button>
+              <el-button size="small" type="danger" :loading="loading" @click="confirmCancel">确认中止</el-button>
+            </div>
+          </template>
+          <template v-else>
+            <el-button type="danger" @click="showCancelInput = true">中止退款</el-button>
+          </template>
+        </template>
+
+        <template v-else>
+          <template v-if="showRejectInput">
+            <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因(必填)" />
+            <div class="audit-detail-footer-actions">
+              <el-button size="small" @click="showRejectInput = false">取消</el-button>
+              <el-button size="small" type="danger" :loading="loading" @click="confirmReject">确认驳回</el-button>
+            </div>
+          </template>
+          <template v-else>
+            <el-button type="success" :loading="loading" @click="confirmApprove">通过</el-button>
+            <el-button type="danger" @click="showRejectInput = true">驳回</el-button>
+          </template>
+        </template>
+      </template>
+      <template v-else>
+        <el-button @click="onVisibleChange(false)">关闭</el-button>
+      </template>
+    </div>
+  </el-drawer>
+</template>
+
+<script>
+export default {
+  name: 'AuditDetailDrawer',
+  props: {
+    visible: { type: Boolean, default: false },
+    title: { type: String, default: '详情' },
+    status: { type: [String, Number], default: '' },
+    loading: { type: Boolean, default: false },
+    // approve-reject:通过/驳回;cancel:仅中止(待退款)
+    actionMode: { type: String, default: 'approve-reject' }
+  },
+  data() {
+    return {
+      showRejectInput: false,
+      rejectReason: '',
+      showCancelInput: false,
+      cancelReason: ''
+    }
+  },
+  computed: {
+    normalizedStatus() {
+      if (this.status === null || this.status === undefined) return ''
+      return String(this.status).toLowerCase()
+    },
+    isCancelMode() {
+      return this.actionMode === 'cancel'
+    },
+    isPending() {
+      const s = this.normalizedStatus
+      if (this.isCancelMode) return s === 'pending' || s === 'processing'
+      return s === 'pending' || s === '0' || s === ''
+    },
+    showApprove() {
+      return !this.isCancelMode
+    },
+    statusLabel() {
+      const map = { pending: '待审核', processing: '处理中', approved: '已通过', rejected: '已驳回', cancelled: '已中止', success: '已完成', failed: '已失败' }
+      return map[this.normalizedStatus] || (this.status === 0 ? '待审核' : '-')
+    },
+    statusTagType() {
+      const s = this.normalizedStatus
+      if (s === 'approved' || s === 'success') return 'success'
+      if (s === 'rejected' || s === 'failed' || s === 'cancelled') return 'danger'
+      if (s === 'pending' || s === '0' || s === 'processing') return 'warning'
+      return 'info'
+    }
+  },
+  methods: {
+    onVisibleChange(val) {
+      this.$emit('update:visible', val)
+    },
+    confirmApprove() {
+      const doApprove = () => this.$emit('approve')
+      if (this.$confirm) {
+        this.$confirm('确认通过该申请?', '提示', { type: 'warning' })
+          .then(doApprove)
+          .catch(() => {})
+      } else {
+        doApprove()
+      }
+    },
+    confirmReject() {
+      const reason = (this.rejectReason || '').trim()
+      if (!reason) {
+        if (this.$message) this.$message.warning('请填写驳回原因')
+        return
+      }
+      this.$emit('reject', reason)
+    },
+    confirmCancel() {
+      const reason = (this.cancelReason || '').trim()
+      if (!reason) {
+        if (this.$message) this.$message.warning('请填写中止原因')
+        return
+      }
+      this.$emit('cancel', reason)
+    },
+    handleClose() {
+      this.showRejectInput = false
+      this.rejectReason = ''
+      this.showCancelInput = false
+      this.cancelReason = ''
+      this.$emit('update:visible', false)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.audit-detail-wrap {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+.audit-detail-head {
+  margin-bottom: 12px;
+}
+.audit-detail-body {
+  flex: 1;
+  overflow-y: auto;
+}
+.audit-detail-footer {
+  border-top: 1px solid #ebeef5;
+  padding-top: 12px;
+  margin-top: 12px;
+  text-align: right;
+}
+.audit-detail-footer-actions {
+  margin-top: 8px;
+  text-align: right;
+}
+</style>