Add points task submission and review workflow

This commit is contained in:
王鹏
2026-09-16 16:50:05 +08:00
parent 0e24c9e235
commit e31af58d58
24 changed files with 2097 additions and 18 deletions

View File

@@ -0,0 +1,41 @@
import request from '@/utils/request'
export function listPointTask(query) {
return request({ url: '/app/pointTask/list', method: 'get', params: query })
}
export function getPointTask(id) {
return request({ url: '/app/pointTask/' + id, method: 'get' })
}
export function addPointTask(data) {
return request({ url: '/app/pointTask', method: 'post', data })
}
export function updatePointTask(data) {
return request({ url: '/app/pointTask', method: 'put', data })
}
export function delPointTask(ids) {
return request({ url: '/app/pointTask/' + ids, method: 'delete' })
}
export function listPointTaskSubmission(query) {
return request({ url: '/app/pointTask/submission/list', method: 'get', params: query })
}
export function getPointTaskSubmission(id) {
return request({ url: '/app/pointTask/submission/' + id, method: 'get' })
}
export function approvePointTaskSubmission(id) {
return request({ url: '/app/pointTask/submission/' + id + '/approve', method: 'post' })
}
export function rejectPointTaskSubmission(id, rejectReason) {
return request({ url: '/app/pointTask/submission/' + id + '/reject', method: 'post', data: { rejectReason } })
}
export function revokePointTaskSubmission(id, rejectReason) {
return request({ url: '/app/pointTask/submission/' + id + '/revoke', method: 'post', data: { rejectReason } })
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="component-upload-image">
<el-upload
drag
:drag="!compact"
multiple
:action="uploadImgUrl"
:on-success="handleUploadSuccess"
@@ -14,13 +14,18 @@
:show-file-list="true"
:headers="headers"
:file-list="fileList"
list-type="picture"
:list-type="compact ? 'picture-card' : 'picture'"
:on-preview="handlePictureCardPreview"
:class="{hide: this.fileList.length >= this.limit}"
:class="{ hide: this.fileList.length >= this.limit, 'is-compact': compact }"
:disabled="disabled"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template v-if="compact">
<i class="el-icon-plus"></i>
</template>
<template v-else>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</template>
</el-upload>
<!-- 上传提示 -->
@@ -74,6 +79,11 @@ export default {
disabled: {
type: Boolean,
default: false
},
// 紧凑图片卡片模式,适合弹窗或栅格表单
compact: {
type: Boolean,
default: false
}
},
data() {
@@ -99,7 +109,6 @@ export default {
const list = Array.isArray(val) ? val : this.value.split(',');
// 然后将数组转为对象数组
this.fileList = list.map(item => {
console.log(item)
if (typeof item === "string") {
item = { name: item, url: item };
}
@@ -179,6 +188,7 @@ export default {
},
// 上传失败
handleUploadError() {
if (this.number > 0) this.number--;
this.$modal.msgError("上传图片失败,请重试");
this.$modal.closeLoading();
},
@@ -216,6 +226,9 @@ export default {
::v-deep.hide .el-upload--picture-card {
display: none;
}
::v-deep.hide .el-upload-dragger {
display: none;
}
// 去掉动画效果
::v-deep .el-list-enter-active,
::v-deep .el-list-leave-active {

View File

@@ -0,0 +1,93 @@
<template>
<div class="app-container">
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true">
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="queryParams.taskName" placeholder="请输入任务名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="任务周期" prop="cycleType">
<el-select v-model="queryParams.cycleType" placeholder="全部" clearable>
<el-option label="单次任务" value="ONCE" />
<el-option label="每日任务" value="DAILY" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部" clearable>
<el-option label="启用" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5"><el-button v-hasPermi="['app:pointTask:add']" type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增任务</el-button></el-col>
<right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="taskList">
<el-table-column label="任务名称" prop="taskName" min-width="180" show-overflow-tooltip />
<el-table-column label="周期" align="center" width="100">
<template slot-scope="scope"><el-tag size="small" :type="scope.row.cycleType === 'DAILY' ? 'warning' : 'success'">{{ scope.row.cycleType === 'DAILY' ? '每日任务' : '单次任务' }}</el-tag></template>
</el-table-column>
<el-table-column label="奖励积分" prop="rewardPoints" align="center" width="100" />
<el-table-column label="活动时间" min-width="285"><template slot-scope="scope">{{ scope.row.startTime || '立即开始' }} 至 {{ scope.row.endTime || '长期有效' }}</template></el-table-column>
<el-table-column label="状态" align="center" width="90"><template slot-scope="scope"><el-tag size="small" :type="scope.row.status === '0' ? 'success' : 'info'">{{ scope.row.status === '0' ? '启用' : '停用' }}</el-tag></template></el-table-column>
<el-table-column label="排序" prop="sortOrder" align="center" width="75" />
<el-table-column label="操作" align="center" width="150" fixed="right">
<template slot-scope="scope">
<el-button v-hasPermi="['app:pointTask:edit']" type="text" icon="el-icon-edit" size="mini" @click="handleUpdate(scope.row)">修改</el-button>
<el-button v-hasPermi="['app:pointTask:remove']" type="text" icon="el-icon-delete" size="mini" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<el-dialog :title="title" :visible.sync="open" width="760px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="110px">
<el-row :gutter="20">
<el-col :span="16"><el-form-item label="任务名称" prop="taskName"><el-input v-model="form.taskName" maxlength="80" show-word-limit /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="奖励积分" prop="rewardPoints"><el-input-number v-model="form.rewardPoints" :min="1" :max="100000" style="width:100%" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="任务周期" prop="cycleType"><el-select v-model="form.cycleType" style="width:100%"><el-option label="单次任务" value="ONCE" /><el-option label="每日任务" value="DAILY" /></el-select></el-form-item></el-col>
<el-col :span="12"><el-form-item label="状态"><el-radio-group v-model="form.status"><el-radio label="0">启用</el-radio><el-radio label="1">停用</el-radio></el-radio-group></el-form-item></el-col>
<el-col :span="12"><el-form-item label="开始时间"><el-date-picker v-model="form.startTime" value-format="yyyy-MM-dd HH:mm:ss" type="datetime" placeholder="留空立即开始" style="width:100%" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="结束时间"><el-date-picker v-model="form.endTime" value-format="yyyy-MM-dd HH:mm:ss" type="datetime" placeholder="留空长期有效" style="width:100%" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="任务封面"><qiniu-image-upload v-model="form.coverUrl" compact :limit="1" :file-size="5" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="示例截图"><qiniu-image-upload v-model="form.exampleImages" compact :limit="3" :file-size="5" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="任务说明"><el-input v-model="form.description" type="textarea" :rows="3" maxlength="1000" show-word-limit /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="操作步骤"><el-input v-model="form.operationGuide" type="textarea" :rows="5" :placeholder="operationGuidePlaceholder" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" style="width:100%" /></el-form-item></el-col>
</el-row>
</el-form>
<div slot="footer"><el-button @click="open=false">取消</el-button><el-button type="primary" :loading="submitting" @click="submitForm">确定</el-button></div>
</el-dialog>
</div>
</template>
<script>
import { listPointTask, getPointTask, addPointTask, updatePointTask, delPointTask } from '@/api/app/pointTask'
export default {
name: 'PointTask',
data() {
return {
loading: false, submitting: false, showSearch: true, total: 0, taskList: [], open: false, title: '', operationGuidePlaceholder: '例如:\n1. 扫描二维码进群\n2. 截图群聊页面\n3. 上传截图并等待审核', queryParams: { pageNum: 1, pageSize: 10, taskName: undefined, cycleType: undefined, status: undefined }, form: {},
rules: { taskName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }], cycleType: [{ required: true, message: '请选择任务周期', trigger: 'change' }], rewardPoints: [{ required: true, message: '请输入奖励积分', trigger: 'blur' }] }
}
},
created() { this.getList() },
methods: {
getList() { this.loading = true; listPointTask(this.queryParams).then(res => { this.taskList = res.rows || []; this.total = res.total || 0 }).finally(() => { this.loading = false }) },
reset() { this.form = { id: undefined, taskName: '', cycleType: 'ONCE', coverUrl: '', description: '', operationGuide: '', exampleImages: '', rewardPoints: 10, startTime: null, endTime: null, status: '0', sortOrder: 0 } },
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
handleAdd() { this.reset(); this.title = '新增积分任务'; this.open = true },
handleUpdate(row) { this.reset(); getPointTask(row.id).then(res => { this.form = res.data; this.title = '修改积分任务'; this.open = true }) },
handleDelete(row) { this.$modal.confirm('确认删除任务“' + row.taskName + '”吗?').then(() => delPointTask(row.id)).then(() => { this.$modal.msgSuccess('删除成功'); this.getList() }).catch(() => {}) },
submitForm() { this.$refs.form.validate(valid => { if (!valid) return; this.submitting = true; const action = this.form.id ? updatePointTask : addPointTask; action(this.form).then(() => { this.$modal.msgSuccess(this.form.id ? '修改成功' : '新增成功'); this.open = false; this.getList() }).finally(() => { this.submitting = false }) }) }
}
}
</script>

View File

@@ -0,0 +1,97 @@
<template>
<div class="app-container">
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true">
<el-form-item label="任务名称" prop="taskName"><el-input v-model="queryParams.taskName" clearable placeholder="任务名称" @keyup.enter.native="handleQuery" /></el-form-item>
<el-form-item label="用户" prop="userName"><el-input v-model="queryParams.userName" clearable placeholder="账号或昵称" @keyup.enter.native="handleQuery" /></el-form-item>
<el-form-item label="状态" prop="status"><el-select v-model="queryParams.status" clearable placeholder="全部"><el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
<el-form-item label="周期" prop="cycleKey"><el-input v-model="queryParams.cycleKey" clearable placeholder="ONCE或2026-09-16" /></el-form-item>
<el-form-item><el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button><el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button></el-form-item>
</el-form>
<right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
<el-table v-loading="loading" :data="submissionList">
<el-table-column label="提交单号" prop="submissionNo" min-width="185" />
<el-table-column label="任务" prop="taskName" min-width="160" show-overflow-tooltip />
<el-table-column label="用户" min-width="130"><template slot-scope="scope"><div>{{ scope.row.nickName || scope.row.userName }}</div><small class="muted">ID: {{ scope.row.userId }}</small></template></el-table-column>
<el-table-column label="周期" prop="cycleKey" align="center" width="110" />
<el-table-column label="积分" prop="rewardPoints" align="center" width="75" />
<el-table-column label="提交次数" prop="attemptCount" align="center" width="85" />
<el-table-column label="状态" align="center" width="100"><template slot-scope="scope"><el-tag :type="statusType(scope.row.status)" size="small">{{ statusLabel(scope.row.status) }}</el-tag></template></el-table-column>
<el-table-column label="提交时间" prop="lastSubmitTime" width="165" />
<el-table-column label="操作" align="center" width="225" fixed="right">
<template slot-scope="scope">
<el-button v-hasPermi="['app:pointTaskReview:query']" type="text" icon="el-icon-view" size="mini" @click="handleView(scope.row)">详情</el-button>
<el-button v-if="scope.row.status === 0" v-hasPermi="['app:pointTaskReview:edit']" type="text" icon="el-icon-check" size="mini" @click="handleApprove(scope.row)">通过</el-button>
<el-button v-if="scope.row.status === 0" v-hasPermi="['app:pointTaskReview:edit']" type="text" icon="el-icon-close" size="mini" @click="openReason(scope.row, 'reject')">驳回</el-button>
<el-button v-if="scope.row.status === 1" v-hasPermi="['app:pointTaskReview:revoke']" type="text" icon="el-icon-refresh-left" size="mini" @click="openReason(scope.row, 'revoke')">撤销</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<el-dialog title="任务提交详情" :visible.sync="detailOpen" width="820px" append-to-body>
<el-descriptions v-if="detail.id" :column="2" border>
<el-descriptions-item label="提交单号" :span="2">{{ detail.submissionNo }}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{ detail.taskName }}</el-descriptions-item>
<el-descriptions-item label="奖励积分">{{ detail.rewardPoints }}</el-descriptions-item>
<el-descriptions-item label="用户">{{ detail.nickName || detail.userName }}({{ detail.userId }})</el-descriptions-item>
<el-descriptions-item label="任务周期">{{ detail.cycleKey }}</el-descriptions-item>
<el-descriptions-item label="状态"><el-tag :type="statusType(detail.status)" size="small">{{ statusLabel(detail.status) }}</el-tag></el-descriptions-item>
<el-descriptions-item label="提交次数">{{ detail.attemptCount }}</el-descriptions-item>
<el-descriptions-item label="用户说明" :span="2"><div class="pre-wrap">{{ detail.remark || '-' }}</div></el-descriptions-item>
<el-descriptions-item v-if="detail.rejectReason" label="驳回/撤销原因" :span="2">{{ detail.rejectReason }}</el-descriptions-item>
</el-descriptions>
<h4>任务截图</h4>
<div class="image-list">
<div v-for="item in detail.attachments || []" :key="item.id" class="image-card" :class="{ history: item.isCurrent !== 1 }">
<el-image :src="item.previewUrl" :preview-src-list="previewUrls" fit="cover" />
<div>{{ item.isCurrent === 1 ? '当前截图' : '历史第' + item.submitBatch + '次' }}</div>
</div>
</div>
<div slot="footer"><el-button @click="detailOpen=false">关闭</el-button></div>
</el-dialog>
<el-dialog :title="reasonMode === 'reject' ? '驳回任务' : '撤销积分奖励'" :visible.sync="reasonOpen" width="520px" append-to-body>
<el-input v-model="reason" type="textarea" :rows="4" maxlength="300" show-word-limit :placeholder="reasonMode === 'reject' ? '请填写审核未通过原因' : '请填写撤销原因'" />
<el-alert v-if="reasonMode === 'revoke'" title="撤销将扣回已发放积分;用户积分不足时操作会失败。" type="warning" :closable="false" show-icon style="margin-top:15px" />
<div slot="footer"><el-button @click="reasonOpen=false">取消</el-button><el-button type="primary" :loading="submitting" @click="submitReason">确定</el-button></div>
</el-dialog>
</div>
</template>
<script>
import { listPointTaskSubmission, getPointTaskSubmission, approvePointTaskSubmission, rejectPointTaskSubmission, revokePointTaskSubmission } from '@/api/app/pointTask'
export default {
name: 'PointTaskReview',
data() {
return {
loading: false, submitting: false, showSearch: true, total: 0, submissionList: [], detailOpen: false, detail: {}, reasonOpen: false, reason: '', reasonMode: 'reject', currentRow: {},
statusOptions: [{ value: 0, label: '待审核' }, { value: 1, label: '已通过' }, { value: 2, label: '已驳回' }, { value: 3, label: '已撤销' }],
queryParams: { pageNum: 1, pageSize: 10, taskName: undefined, userName: undefined, status: undefined, cycleKey: undefined }
}
},
computed: { previewUrls() { return (this.detail.attachments || []).map(item => item.previewUrl).filter(Boolean) } },
created() { this.getList() },
methods: {
getList() { this.loading = true; listPointTaskSubmission(this.queryParams).then(res => { this.submissionList = res.rows || []; this.total = res.total || 0 }).finally(() => { this.loading = false }) },
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
handleView(row) { getPointTaskSubmission(row.id).then(res => { this.detail = res.data; this.detailOpen = true }) },
handleApprove(row) { this.$modal.confirm('确认通过该任务并发放 ' + row.rewardPoints + ' 积分吗?').then(() => approvePointTaskSubmission(row.id)).then(() => { this.$modal.msgSuccess('审核通过,积分已发放'); this.getList() }).catch(() => {}) },
openReason(row, mode) { this.currentRow = row; this.reasonMode = mode; this.reason = ''; this.reasonOpen = true },
submitReason() { const reason = String(this.reason || '').trim(); if (!reason) { this.$modal.msgError('请填写原因'); return } this.submitting = true; const action = this.reasonMode === 'reject' ? rejectPointTaskSubmission : revokePointTaskSubmission; action(this.currentRow.id, reason).then(() => { this.$modal.msgSuccess(this.reasonMode === 'reject' ? '已驳回' : '奖励已撤销'); this.reasonOpen = false; this.getList() }).finally(() => { this.submitting = false }) },
statusLabel(status) { const item = this.statusOptions.find(v => v.value === status); return item ? item.label : '未知' },
statusType(status) { return { 0: 'warning', 1: 'success', 2: 'danger', 3: 'info' }[status] || 'info' }
}
}
</script>
<style scoped>
.muted { color: #909399; }
.pre-wrap { white-space: pre-wrap; }
.image-list { display: flex; flex-wrap: wrap; gap: 14px; }
.image-card { width: 150px; text-align: center; color: #606266; }
.image-card .el-image { width: 150px; height: 150px; border-radius: 6px; }
.image-card.history { opacity: .58; }
</style>