diff --git a/backend/src/main/java/com/designhub/service/SystemConfigService.java b/backend/src/main/java/com/designhub/service/SystemConfigService.java index 1ae496f..6cfc484 100644 --- a/backend/src/main/java/com/designhub/service/SystemConfigService.java +++ b/backend/src/main/java/com/designhub/service/SystemConfigService.java @@ -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); } /** diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index 50df289..9ed25d3 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -1710,6 +1710,28 @@

任务数据管理

+ + +
+
+
+ + 盲盒模式 +
+
+ 开启后,普通部门成员在可接任务页面看到的任务卡片变成盲盒样式(只显示排队序号),隐藏所有任务信息。 +
+ 部门负责人不受此设置影响,始终可以看到完整任务信息。 +
+
+ +
+
+ { + 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; diff --git a/frontend/src/views/TaskHall.vue b/frontend/src/views/TaskHall.vue index 76c7d33..94d4a63 100644 --- a/frontend/src/views/TaskHall.vue +++ b/frontend/src/views/TaskHall.vue @@ -103,11 +103,34 @@
+ + + + +
@@ -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(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(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;