feat: 添加可接任务页面盲盒模式功能

- 后端:在SystemConfigService添加task.blind_box_mode配置项
- 前端:在Settings.vue任务数据管理中添加盲盒模式开关
- 前端:在TaskHall.vue实现盲盒卡片显示
  - 开启后普通部门成员看到的任务卡片变成盲盒样式
  - 盲盒显示排队序号,隐藏所有任务信息
  - 部门负责人和管理员不受盲盒模式影响
This commit is contained in:
Your Name 2026-02-02 15:25:08 +08:00
parent 47f028513f
commit c592ba2234
3 changed files with 292 additions and 3 deletions

View File

@ -262,6 +262,9 @@ public class SystemConfigService {
createConfigIfNotExists("task.release_type.default", "true", "task", "显示默认模式按钮", "是否显示「默认模式」发布类型按钮", "boolean", 1);
createConfigIfNotExists("task.release_type.recruitment", "true", "task", "显示招募任务按钮", "是否显示「招募任务」发布类型按钮", "boolean", 1);
createConfigIfNotExists("task.release_type.assigned", "true", "task", "显示指定任务按钮", "是否显示「指定任务」发布类型按钮", "boolean", 1);
// 盲盒模式配置(可接任务页面任务卡片显示方式)
createConfigIfNotExists("task.blind_box_mode", "false", "task", "盲盒模式", "开启后普通部门成员在可接任务页面看到的任务卡片变成盲盒(只显示排队序号),部门负责人不受影响", "boolean", 1);
}
/**

View File

@ -1710,6 +1710,28 @@
<h2 class="section-title">任务数据管理</h2>
</div>
<!-- 盲盒模式设置 -->
<a-card title="可接任务显示设置" class="blind-box-card" style="margin-bottom: 24px;">
<div class="blind-box-setting">
<div class="setting-info">
<div class="setting-title">
<Icon name="gift" size="sm" color="#fa8c16" style="margin-right: 8px;" />
盲盒模式
</div>
<div class="setting-desc">
开启后,普通部门成员在可接任务页面看到的任务卡片变成盲盒样式(只显示排队序号),隐藏所有任务信息。
<br />
<span style="color: #52c41a;">部门负责人不受此设置影响,始终可以看到完整任务信息。</span>
</div>
</div>
<a-switch
v-model:checked="blindBoxMode"
:loading="savingBlindBoxMode"
@change="handleBlindBoxModeChange"
/>
</div>
</a-card>
<a-alert
message="危险操作"
description="删除任务将永久删除该任务及其所有子任务数据,此操作不可恢复,请谨慎操作!"
@ -2772,6 +2794,7 @@ import {
import {
getSystemConfigsByGroup,
getSystemConfig,
getAllSystemConfigs,
batchUpdateSystemConfigs,
createSystemConfig,
updateSystemConfig,
@ -2968,6 +2991,42 @@ const taskDeleteForm = reactive({
})
const deletingTask = ref(false)
// 盲盒模式相关
const blindBoxMode = ref(false)
const savingBlindBoxMode = ref(false)
// 加载盲盒模式配置
const loadBlindBoxMode = async () => {
try {
const response = await getAllSystemConfigs()
if (response.code === 200 && response.data) {
const config = response.data.find((c: any) => c.configKey === 'task.blind_box_mode')
if (config) {
blindBoxMode.value = config.configValue === 'true'
}
}
} catch (error) {
console.error('加载盲盒模式配置失败:', error)
}
}
// 保存盲盒模式配置
const handleBlindBoxModeChange = async (checked: boolean) => {
savingBlindBoxMode.value = true
try {
await batchUpdateSystemConfigs({
'task.blind_box_mode': String(checked)
})
message.success(checked ? '盲盒模式已开启' : '盲盒模式已关闭')
} catch (error: any) {
message.error('保存失败: ' + (error.message || '未知错误'))
// 回滚状态
blindBoxMode.value = !checked
} finally {
savingBlindBoxMode.value = false
}
}
// 删除任务
const handleDeleteTask = async () => {
if (!taskDeleteForm.taskNo) {
@ -5522,6 +5581,8 @@ onMounted(() => {
loadDesignerTargetList()
// 加载进度检查配置
loadProgressCheckConfig()
// 加载盲盒模式配置
loadBlindBoxMode()
})
// S3配置相关方法
@ -7190,6 +7251,54 @@ watch(activeTab, (value) => {
}
}
// 盲盒模式设置卡片样式
.blind-box-card {
max-width: 600px;
background: rgba(0, 0, 0, 0.2) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
border-radius: 8px;
:deep(.ant-card-head) {
background: transparent !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
.ant-card-head-title {
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
}
}
:deep(.ant-card-body) {
background: transparent !important;
}
.blind-box-setting {
display: flex;
justify-content: space-between;
align-items: flex-start;
.setting-info {
flex: 1;
margin-right: 24px;
.setting-title {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 500;
color: rgba(255, 255, 255, 0.85);
margin-bottom: 8px;
}
.setting-desc {
font-size: 13px;
color: rgba(255, 255, 255, 0.65);
line-height: 1.6;
}
}
}
}
// 任务删除卡片样式
.task-delete-card {
max-width: 600px;

View File

@ -103,11 +103,34 @@
<div v-else class="task-grid">
<div
v-for="task in taskList"
v-for="(task, index) in taskList"
:key="task.id"
class="task-card chart-card"
:class="['task-card', 'chart-card', { 'blind-box-card': showBlindBox }]"
@click="handleTaskClick(task)"
>
<!-- 盲盒模式显示 -->
<template v-if="showBlindBox">
<div class="blind-box-content">
<div class="blind-box-icon">
<Icon name="gift" size="xl" color="#fa8c16" />
</div>
<div class="blind-box-queue">
<span class="queue-label">排队序号</span>
<span class="queue-number">#{{ index + 1 }}</span>
</div>
<div class="blind-box-hint">
<Icon name="lock" size="sm" color="#666" style="margin-right: 4px;" />
任务信息已隐藏
</div>
<a-button type="primary" size="small" class="blind-box-grab-btn" @click.stop="handleGrabTask(task)">
<Icon name="zap" size="sm" style="margin-right: 4px;" />
抢单
</a-button>
</div>
</template>
<!-- 正常模式显示 -->
<template v-else>
<!-- 任务标题 -->
<div class="task-header">
<h3 class="task-title">{{ task.title }}</h3>
@ -310,6 +333,7 @@
/>
</div>
</div>
</template>
</div>
</div>
@ -2450,7 +2474,7 @@ import * as userApi from '@/api/user'
import * as configApi from '@/api/config'
import * as authApi from '@/api/auth'
import * as departmentApi from '@/api/department'
import { getWorkHoursConfig, getSystemConfig } from '@/api/system'
import { getWorkHoursConfig, getSystemConfig, getAllSystemConfigs } from '@/api/system'
import type { Task, TaskApprovalDTO, TaskProduct, ExportTaskHallParams } from '@/api/task'
import { exportTaskHall } from '@/api/task'
import type { DesignerWithWorkload } from '@/api/user'
@ -2630,6 +2654,35 @@ const total = ref(0)
const detailDrawerVisible = ref(false)
const selectedTask = ref<TaskOrSubTask | null>(null)
// 盲盒模式相关
const blindBoxMode = ref(false)
const isDeptLeader = computed(() => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
// 部门负责人或管理员不受盲盒模式影响
return userInfo.isDepartmentLeader === 1 || userInfo.isAdmin === 1 || userInfo.isAdmin === true
})
// 是否显示盲盒(开启盲盒模式且不是部门负责人/管理员)
const showBlindBox = computed(() => {
return blindBoxMode.value && !isDeptLeader.value
})
// 加载盲盒模式配置
const loadBlindBoxMode = async () => {
try {
const response = await getAllSystemConfigs()
if (response.code === 200 && response.data) {
const config = response.data.find((c: any) => c.configKey === 'task.blind_box_mode')
if (config) {
blindBoxMode.value = config.configValue === 'true'
console.log('盲盒模式:', blindBoxMode.value, '是否显示盲盒:', showBlindBox.value)
}
}
} catch (error) {
console.error('加载盲盒模式配置失败:', error)
}
}
// 评分弹窗
const ratingModalVisible = ref(false)
const ratingTask = ref<TaskOrSubTask | null>(null)
@ -5926,6 +5979,50 @@ const handleGrabFromDetail = async () => {
detailDrawerVisible.value = false
}
// 盲盒模式抢单
const handleGrabTask = async (task: TaskOrSubTask) => {
const isSubTask = task.isSubTask
const taskType = isSubTask ? '子任务' : '任务'
// 先检查权限
const { canGrab, reason } = canGrabTask(task)
if (!canGrab) {
message.warning(reason)
return
}
Modal.confirm({
title: '确认抢单',
content: h('div', {}, [
h('p', { style: { marginBottom: '8px' } }, '您正在盲盒模式下抢单'),
h('p', { style: { color: '#fa8c16', fontSize: '14px' } }, '任务信息将在抢单成功后显示'),
h('p', { style: { color: '#888', fontSize: '12px' } }, '确定要抢单这个任务吗?')
]),
okText: '确认抢单',
cancelText: '取消',
okButtonProps: { style: { background: '#fa8c16', borderColor: '#fa8c16' } },
onOk: async () => {
try {
let response
if (isSubTask) {
response = await taskApi.grabTaskItem(task.itemId || task.id!)
} else {
response = await taskApi.grabTask(task.id!)
}
if (response.code === 200) {
message.success(`${taskType}抢单成功!任务已添加到您的进行中任务`)
loadTasks(true)
loadAllTabCounts()
}
} catch (error: any) {
console.error('抢单失败:', error)
message.error(error.message || '抢单失败')
}
}
})
}
// ==================== 商品ID管理 ====================
// 加载任务商品ID列表
@ -6404,6 +6501,7 @@ onMounted(async () => {
loadDashboardStats()
loadAllTabCounts() // 加载所有Tab的数量统计
loadTasks(true) // 初始加载时强制获取数据
loadBlindBoxMode() // 加载盲盒模式配置
// 监听配置更新事件
window.addEventListener('configUpdated', handleConfigUpdate as EventListener)
})
@ -6778,6 +6876,85 @@ onUnmounted(() => {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
// 盲盒模式卡片样式
&.blind-box-card {
background: linear-gradient(135deg, rgba(250, 140, 22, 0.1) 0%, rgba(250, 173, 20, 0.05) 100%) !important;
border: 2px dashed rgba(250, 140, 22, 0.4) !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
border-color: rgba(250, 140, 22, 0.8) !important;
background: linear-gradient(135deg, rgba(250, 140, 22, 0.15) 0%, rgba(250, 173, 20, 0.1) 100%) !important;
}
.blind-box-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 24px;
.blind-box-icon {
margin-bottom: 16px;
animation: pulse 2s ease-in-out infinite;
}
.blind-box-queue {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 12px;
.queue-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 4px;
}
.queue-number {
font-size: 32px;
font-weight: bold;
color: #fa8c16;
text-shadow: 0 2px 8px rgba(250, 140, 22, 0.3);
}
}
.blind-box-hint {
display: flex;
align-items: center;
font-size: 12px;
color: rgba(255, 255, 255, 0.45);
margin-bottom: 16px;
}
.blind-box-grab-btn {
background: linear-gradient(135deg, #fa8c16 0%, #faad14 100%);
border: none;
padding: 8px 24px;
font-weight: 500;
&:hover {
background: linear-gradient(135deg, #d87a00 0%, #d89614 100%);
}
}
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
// 带预览图的卡片布局
.card-with-preview {
display: flex;