Your Name c592ba2234 feat: 添加可接任务页面盲盒模式功能
- 后端:在SystemConfigService添加task.blind_box_mode配置项
- 前端:在Settings.vue任务数据管理中添加盲盒模式开关
- 前端:在TaskHall.vue实现盲盒卡片显示
  - 开启后普通部门成员看到的任务卡片变成盲盒样式
  - 盲盒显示排队序号,隐藏所有任务信息
  - 部门负责人和管理员不受盲盒模式影响
2026-02-02 15:25:08 +08:00

7326 lines
253 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<DefaultLayout>
<div class="settings-page">
<div class="container">
<a-tabs v-model:active-key="activeTab" type="card" class="settings-tabs">
<!-- 网站设置 -->
<a-tab-pane key="site-settings" tab="网站设置">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">网站基本信息</h2>
</div>
<a-form :model="siteSettings" layout="vertical" class="site-settings-form">
<a-form-item label="网站名称">
<a-input
v-model:value="siteSettings.siteName"
placeholder="请输入网站名称"
size="large"
@change="handleSiteNameChange"
/>
</a-form-item>
<a-form-item label="网站Logo">
<div class="logo-upload-section">
<div class="logo-preview">
<img
v-if="siteSettings.logoUrl"
:src="siteSettings.logoUrl"
alt="网站Logo"
class="logo-preview-image"
/>
<div v-else class="logo-placeholder">
<Icon name="image" size="xl" color="#6B6B6B" />
<span>暂无Logo</span>
</div>
</div>
<div class="logo-upload-actions">
<a-upload
:before-upload="handleLogoUpload"
:show-upload-list="false"
accept="image/*"
>
<a-button type="primary">
<template #icon><Icon name="upload" size="sm" /></template>
上传Logo
</a-button>
</a-upload>
<a-button
v-if="siteSettings.logoUrl"
danger
@click="handleRemoveLogo"
style="margin-left: 8px;"
>
<template #icon><Icon name="trash" size="sm" /></template>
删除Logo
</a-button>
</div>
<div class="upload-tip">
<Icon name="info" size="sm" color="#6B6B6B" style="margin-right: 4px;" />
建议尺寸:200x200px,支持 PNG、JPG 格式
</div>
</div>
</a-form-item>
<a-form-item label="官网地址">
<a-input
v-model:value="siteSettings.siteUrl"
placeholder="请输入官网地址,例如:https://example.com"
size="large"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
<Icon name="info" size="sm" color="#888" style="margin-right: 4px;" />
钉钉卡片通知将跳转到此地址
</div>
</a-form-item>
<a-divider orientation="left">工作时间设置</a-divider>
<a-form-item label="上班开始时间">
<a-time-picker
v-model:value="workHoursSettings.startTime"
format="HH:mm"
value-format="HH:mm"
placeholder="请选择上班开始时间"
size="large"
style="width: 100%;"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
<Icon name="info" size="sm" color="#888" style="margin-right: 4px;" />
用于计算任务截止时间和延误扣分
</div>
</a-form-item>
<a-form-item label="上班结束时间">
<a-time-picker
v-model:value="workHoursSettings.endTime"
format="HH:mm"
value-format="HH:mm"
placeholder="请选择上班结束时间"
size="large"
style="width: 100%;"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
<Icon name="info" size="sm" color="#888" style="margin-right: 4px;" />
用于计算任务截止时间和延误扣分
</div>
</a-form-item>
<a-form-item v-if="workHoursSettings.workHoursPerDay > 0">
<div style="padding: 8px 12px; background: #f5f5f5; border-radius: 4px;">
<span style="font-size: 14px; color: #666;">每日工作小时数:</span>
<span style="font-size: 14px; font-weight: 500; color: #333;">{{ workHoursSettings.workHoursPerDay }} 小时</span>
</div>
</a-form-item>
<a-divider orientation="left">功能开关</a-divider>
<a-form-item label="注册功能">
<a-switch
v-model:checked="siteSettings.registerEnabled"
checked-children="开启"
un-checked-children="关闭"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
控制登录页面是否显示注册功能
</div>
</a-form-item>
<a-form-item>
<a-button type="primary" size="large" @click="handleSaveSiteSettings" :loading="savingSiteSettings">
<template #icon><Icon name="check" size="sm" /></template>
保存设置
</a-button>
</a-form-item>
</a-form>
</div>
</a-tab-pane>
<!-- 需求类型管理 -->
<a-tab-pane key="demand-types" tab="需求类型">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">需求类型管理</h2>
<a-space>
<a-button type="primary" @click="showDemandTypeModal(null, 'level1')">
<template #icon><Icon name="plus" size="sm" /></template>
添加一级分类
</a-button>
<a-button @click="showDemandTypeModal(null, 'level2')" :disabled="level1DemandTypes.length === 0">
<template #icon><Icon name="plus" size="sm" /></template>
添加二级分类
</a-button>
<a-button @click="showDemandTypeModal(null, 'level3')" :disabled="level2DemandTypes.length === 0">
<template #icon><Icon name="plus" size="sm" /></template>
添加三级分类
</a-button>
</a-space>
</div>
<a-alert type="info" show-icon style="margin-bottom: 16px;">
<template #message>
<span>支持三级分类:一级分类(顶级) → 二级分类(设置接单部门) → 三级分类(设置单价分值)。三级分类继承二级分类的部门设置。</span>
</template>
</a-alert>
<a-table
:columns="demandTypeColumns"
:data-source="demandTypesTree"
:loading="loading.demandTypes"
row-key="id"
:pagination="false"
:default-expand-all-rows="true"
:indent-size="24"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'level'">
<a-tag v-if="getDemandTypeLevel(record) === 1" color="blue">一级</a-tag>
<a-tag v-else-if="getDemandTypeLevel(record) === 2" color="green">二级</a-tag>
<a-tag v-else color="orange">三级</a-tag>
</template>
<template v-else-if="column.key === 'name'">
<span class="tree-name">{{ record.name || '--' }}</span>
</template>
<template v-else-if="column.key === 'unitScore'">
<!-- 有分值就显示(格式化为3位小数),无分值显示-- -->
<span v-if="record.unitScore !== null && record.unitScore !== undefined && record.unitScore !== 0">
{{ Number(record.unitScore).toFixed(3) }}
</span>
<span v-else class="text-muted">--</span>
</template>
<template v-else-if="column.key === 'estimatedHours'">
<span v-if="record.estimatedHours !== null && record.estimatedHours !== undefined && record.estimatedHours !== 0">
{{ record.estimatedHours }}
</span>
<span v-else class="text-muted">--</span>
</template>
<template v-else-if="column.key === 'allowedDepts'">
<template v-if="getDemandTypeLevel(record) === 2">
<span v-if="record.allowedDeptIds">
{{ getDeptNamesFromIds(record.allowedDeptIds) }}
</span>
<span v-else class="text-muted">所有接单部门</span>
</template>
<span v-else-if="getDemandTypeLevel(record) === 3" class="text-muted">继承二级</span>
<span v-else class="text-muted">--</span>
</template>
<template v-else-if="column.key === 'status'">
<a-switch
:checked="record.isEnabled === 1"
@change="(checked: boolean) => toggleStatus('demandType', record.id, checked)"
/>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="showDemandTypeModal(record)">编辑</a-button>
<a-button
v-if="getDemandTypeLevel(record) === 1"
type="link"
size="small"
@click="showDemandTypeModal(null, 'level2', record.id)"
>
添加二级
</a-button>
<a-button
v-if="getDemandTypeLevel(record) === 2"
type="link"
size="small"
@click="showDemandTypeModal(null, 'level3', record.id)"
>
添加三级
</a-button>
<a-popconfirm
:title="getDemandTypeLevel(record) === 1 ? '确定删除?将同时删除所有下级分类' : getDemandTypeLevel(record) === 2 ? '确定删除?将同时删除所有三级分类' : '确定删除此分类?'"
@confirm="deleteItem('demandType', record.id)"
>
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</div>
</a-tab-pane>
<!-- 任务子类型管理 -->
<a-tab-pane key="task-sub-types" tab="任务子类型">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">任务子类型管理</h2>
<a-button type="primary" @click="showTaskSubTypeModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加子类型
</a-button>
</div>
<a-table
:columns="taskSubTypeColumns"
:data-source="taskSubTypes"
:loading="loading.taskSubTypes"
row-key="id"
:pagination="{ pageSize: 10 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-switch
:checked="record.isEnabled === 1"
@change="(checked: boolean) => toggleStatus('taskSubType', record.id, checked)"
/>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="showTaskSubTypeModal(record)">编辑</a-button>
<a-popconfirm title="确定删除?" @confirm="deleteItem('taskSubType', record.id)">
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</div>
</a-tab-pane>
<!-- 品牌管理 -->
<a-tab-pane key="brands" tab="品牌管理">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">品牌管理</h2>
<a-button type="primary" @click="showBrandModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加品牌
</a-button>
</div>
<a-table
:columns="brandColumns"
:data-source="brands"
:loading="loading.brands"
row-key="id"
:pagination="{ pageSize: 10 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-switch
:checked="record.isEnabled === 1"
@change="(checked: boolean) => toggleStatus('brand', record.id, checked)"
/>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="showBrandModal(record)">编辑</a-button>
<a-popconfirm title="确定删除?" @confirm="deleteItem('brand', record.id)">
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</div>
</a-tab-pane>
<!-- 平台管理 -->
<a-tab-pane key="platforms" tab="平台管理">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">平台管理</h2>
<a-button type="primary" @click="showPlatformModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加平台
</a-button>
</div>
<a-table
:columns="platformColumns"
:data-source="platforms"
:loading="loading.platforms"
row-key="id"
:pagination="{ pageSize: 10 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-switch
:checked="record.isEnabled === 1"
@change="(checked: boolean) => toggleStatus('platform', record.id, checked)"
/>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="showPlatformModal(record)">编辑</a-button>
<a-popconfirm title="确定删除?" @confirm="deleteItem('platform', record.id)">
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</div>
</a-tab-pane>
<!-- 类目管理 -->
<a-tab-pane key="categories" tab="类目管理">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">类目管理</h2>
<a-button type="primary" @click="showCategoryModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加类目
</a-button>
</div>
<a-table
:columns="categoryColumns"
:data-source="categories"
:loading="loading.categories"
row-key="id"
:pagination="{ pageSize: 10 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-switch
:checked="record.isEnabled === 1"
@change="(checked: boolean) => toggleStatus('category', record.id, checked)"
/>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="showCategoryModal(record)">编辑</a-button>
<a-popconfirm title="确定删除?" @confirm="deleteItem('category', record.id)">
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</div>
</a-tab-pane>
<!-- 用户管理 -->
<a-tab-pane key="users" tab="用户管理">
<div class="settings-section">
<div class="section-header">
<div>
<h2 class="section-title">用户管理</h2>
<p class="section-subtitle">查看各部门成员,快速调整用户所属部门</p>
</div>
<a-space>
<a-button :loading="userLoading" @click="loadDepartmentUsers">
<template #icon><Icon name="refresh" size="sm" /></template>
刷新数据
</a-button>
<a-button type="primary" @click="goToUserManagement">
<template #icon><Icon name="external-link" size="sm" /></template>
完整用户管理
</a-button>
</a-space>
</div>
<a-alert
type="info"
show-icon
message="提示"
description="部门列表来源于“部门管理”配置。调整部门会立即生效,并同步到创建任务、抢单等功能中。"
style="margin-bottom: 16px;"
/>
<a-table
:columns="userManagementColumns"
:data-source="allUsersList"
:loading="userLoading"
row-key="id"
:pagination="{ pageSize: 15 }"
:scroll="{ x: 1200 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userInfo'">
<div class="user-info-cell">
<div class="user-info-main">
<span class="user-name-text">{{ record.realName || record.username }}</span>
<span v-if="record.realName" class="user-username">@{{ record.username }}</span>
</div>
<div class="user-info-sub">
<span v-if="record.phone">
<Icon name="phone" size="xs" style="margin-right: 4px;" />
{{ record.phone }}
</span>
<span v-if="record.email" class="user-email">
<Icon name="mail" size="xs" style="margin-right: 4px;" />
{{ record.email }}
</span>
</div>
</div>
</template>
<template v-else-if="column.key === 'department'">
<a-tag v-if="record.deptName" :color="record.deptType === 'demand' ? 'blue' : 'green'" size="small">
{{ record.deptName }}
<span style="margin-left: 4px; opacity: 0.8;">
({{ record.deptType === 'demand' ? '需求' : '接单' }})
</span>
</a-tag>
<a-tag v-else color="orange" size="small">未分配</a-tag>
</template>
<template v-else-if="column.key === 'isAdmin'">
<a-tag v-if="record.isAdmin === 1" color="red" size="small">
<Icon name="star" size="xs" style="margin-right: 4px;" />
管理员
</a-tag>
<span v-else style="color: #999;">—</span>
</template>
<template v-else-if="column.key === 'status'">
<span :class="['status-badge', record.status === 1 ? 'active' : 'inactive']">
{{ record.status === 1 ? '启用' : '禁用' }}
</span>
</template>
<template v-else-if="column.key === 'createTime'">
<span class="time-value">{{ formatUserTime(record.createTime) }}</span>
</template>
<template v-else-if="column.key === 'actions'">
<div class="table-actions">
<a-button type="text" size="small" @click="openTransferModal(record)">
<Icon name="shuffle" size="sm" style="margin-right: 4px;" />
调整部门
</a-button>
<a-popconfirm
:title="record.isAdmin === 1 ? '确定取消该用户的管理员身份?' : '确定设置该用户为管理员?'"
@confirm="toggleUserAdmin(record)"
>
<a-button type="text" size="small" :type="record.isAdmin === 1 ? 'default' : 'primary'">
<Icon :name="record.isAdmin === 1 ? 'lock' : 'star'" size="sm" style="margin-right: 4px;" />
{{ record.isAdmin === 1 ? '取消管理员' : '设为管理员' }}
</a-button>
</a-popconfirm>
<a-popconfirm title="确定删除该用户?" @confirm="deleteUserFromCard(record)">
<a-button type="text" size="small" danger>
<Icon name="trash" size="sm" style="margin-right: 4px;" />
删除
</a-button>
</a-popconfirm>
</div>
</template>
</template>
</a-table>
</div>
</a-tab-pane>
<!-- 部门管理 -->
<a-tab-pane key="departments" tab="部门管理">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">部门管理</h2>
<div style="display: flex; gap: 12px;">
<a-button @click="handleSyncOrganization" :loading="syncingOrganization">
<template #icon><Icon name="refresh" size="sm" /></template>
同步钉钉组织
</a-button>
<a-button type="primary" @click="showDepartmentModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加部门
</a-button>
</div>
</div>
<a-alert
message="部门类型说明"
description="需求部门:只能发起需求;接单部门:只能接收任务。请根据实际情况设置部门类型。"
type="info"
show-icon
style="margin-bottom: 16px;"
/>
<!-- 部门搜索框 -->
<div style="margin-bottom: 16px; display: flex; align-items: center; gap: 12px;">
<a-input
v-model:value="departmentSearchText"
placeholder="搜索部门名称、编码或负责人"
allow-clear
style="max-width: 400px;"
>
<template #prefix>
<Icon name="search" size="sm" />
</template>
</a-input>
<span v-if="departmentSearchText" style="color: #888; font-size: 13px;">
找到 {{ filteredDepartments.length }} 个部门
</span>
</div>
<a-alert type="info" show-icon style="margin-bottom: 16px;">
<template #message>
<span>部门类型说明</span>
</template>
<template #description>
<span>需求部门:只能发起需求;接单部门:只能接收任务。请根据实际情况设置部门类型。</span>
</template>
</a-alert>
<a-table
:columns="departmentColumns"
:data-source="filteredDepartmentsTree"
:loading="loading.departments"
row-key="id"
:pagination="false"
:default-expand-all-rows="false"
:indent-size="24"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'deptInfo'">
<div class="dept-info-cell">
<div class="dept-info-main">
<span class="dept-name-text">{{ record.deptName }}</span>
<a-tag
v-if="getDepartmentLevel(record) === 1"
size="small"
color="blue"
style="margin-left: 8px;"
>一级</a-tag>
<a-tag
v-else-if="getDepartmentLevel(record) === 2"
size="small"
color="cyan"
style="margin-left: 8px;"
>二级</a-tag>
<a-tag
v-else
size="small"
color="purple"
style="margin-left: 8px;"
>三级</a-tag>
<a-tag
size="small"
:color="record.deptType === 'demand' ? 'orange' : record.deptType === 'primary_demand' ? 'blue' : 'green'"
style="margin-left: 4px;"
>
{{ record.deptType === 'demand' ? '需求部门' : record.deptType === 'primary_demand' ? '一级需求部门' : '接单部门' }}
</a-tag>
</div>
<div class="dept-info-sub">
<span v-if="record.deptCode">编码:{{ record.deptCode }}</span>
<span v-if="record.description" class="dept-desc">|{{ record.description }}</span>
</div>
</div>
</template>
<template v-else-if="column.key === 'leader'">
<span>{{ record.leaderName || '未设置' }}</span>
</template>
<template v-else-if="column.key === 'memberCount'">
<span>{{ record.memberCount ?? 0 }} 人</span>
</template>
<template v-else-if="column.key === 'score'">
<div class="score-display">
<!-- 一级部门有子部门:显示汇总 -->
<template v-if="getDepartmentLevel(record) === 1 && hasChildDepartments(record)">
<div class="score-main">
<span class="score-value">{{ getLevel1DeptTotalScore(record) }}</span>
<span class="score-unit">分</span>
<a-tag size="small" color="blue" style="margin-left: 4px;">汇总</a-tag>
</div>
<div class="score-details">
<span class="score-detail-item">
<span class="detail-label">充值</span>
<span class="detail-value">{{ getLevel1DeptTotalRecharge(record) }}</span>
</span>
<span class="detail-separator">|</span>
<span class="score-detail-item">
<span class="detail-label">消耗</span>
<span class="detail-value">{{ getLevel1DeptTotalConsume(record) }}</span>
</span>
</div>
</template>
<!-- 三级部门:显示共享自父部门 -->
<template v-else-if="getDepartmentLevel(record) === 3">
<div class="score-main">
<span class="score-value">{{ getParentDeptScore(record) }}</span>
<span class="score-unit">分</span>
<a-tag size="small" color="purple" style="margin-left: 4px;">共享</a-tag>
</div>
<div class="score-details" style="font-size: 11px; color: #888;">
共享自「{{ getParentDeptNameById(record.parentId) }}」
</div>
</template>
<!-- 二级部门或无子部门的一级部门:正常显示 -->
<template v-else>
<div class="score-main">
<span class="score-value">{{ record.scoreBalance ?? 0 }}</span>
<span class="score-unit">分</span>
</div>
<div class="score-details">
<span class="score-detail-item">
<span class="detail-label">充值</span>
<span class="detail-value">{{ record.totalRecharge ?? 0 }}</span>
</span>
<span class="detail-separator">|</span>
<span class="score-detail-item">
<span class="detail-label">消耗</span>
<span class="detail-value">{{ record.totalConsume ?? 0 }}</span>
</span>
</div>
</template>
</div>
</template>
<template v-else-if="column.key === 'limit'">
<div class="limit-display">
<template v-if="record.limitEnabled === 1">
<div class="limit-period">
<span class="period-label">{{ getLimitPeriodLabel(record.limitPeriod) }}</span>
</div>
<div class="limit-value">
<span class="consumed">{{ record.periodUsed ?? 0 }}</span>
<span class="separator">/</span>
<span class="total">{{ record.periodLimit ?? 0 }}</span>
</div>
</template>
<span v-else class="limit-disabled">未启用</span>
</div>
</template>
<template v-else-if="column.key === 'alert'">
<div v-if="record.limitEnabled === 1" class="alert-display">
<div v-if="record.limitAlert" class="alert-badge warning">
<Icon name="alert" size="sm" />
<span>预警中</span>
</div>
<div v-else class="alert-badge normal">
<Icon name="checkCircle" size="sm" />
<span>正常</span>
</div>
</div>
<span v-else class="empty-value">-</span>
</template>
<template v-else-if="column.key === 'status'">
<div class="status-switch">
<a-switch
:checked="record.status === 1"
@change="(checked: boolean) => toggleDepartmentStatus(record.id, checked)"
size="small"
/>
<span class="status-text" :class="{ active: record.status === 1 }">
{{ record.status === 1 ? '启用' : '禁用' }}
</span>
</div>
</template>
<template v-else-if="column.key === 'actions'">
<div class="table-actions">
<a-button
type="text"
size="small"
@click="showDepartmentModal(record)"
class="action-btn edit"
>
<Icon name="edit" size="sm" />
编辑
</a-button>
<a-button
type="text"
size="small"
@click="showScorePoolModal(record)"
class="action-btn pool"
>
<Icon name="coins" size="sm" />
积分池
</a-button>
<a-popconfirm title="确定删除该部门吗?" ok-text="确定" cancel-text="取消" @confirm="deleteDepartment(record.id)">
<a-button
type="text"
danger
size="small"
class="action-btn danger"
>
<Icon name="trash" size="sm" />
删除
</a-button>
</a-popconfirm>
</div>
</template>
</template>
</a-table>
</div>
</a-tab-pane>
<!-- 奖励规则管理 -->
<a-tab-pane key="reward-rules" tab="奖励规则">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">自动奖励规则</h2>
<a-button type="primary" @click="goToRewardRules">
<template #icon><Icon name="settings" size="sm" /></template>
管理规则
</a-button>
</div>
<div class="reward-rules-intro">
<div class="intro-card">
<Icon name="sparkles" size="xl" color="#FFD700" />
<div class="intro-content">
<h3>什么是自动奖励规则?</h3>
<p>自动奖励规则是系统自动触发的积分奖励机制。当用户满足特定条件时,系统会自动发放相应的积分奖励。</p>
</div>
</div>
<div class="intro-examples">
<div class="example-item">
<Icon name="calendar" size="md" color="#31f08b" />
<span>每日完成5个任务,额外奖励20积分</span>
</div>
<div class="example-item">
<Icon name="trophy" size="md" color="#FFD700" />
<span>每周完成20个任务,额外奖励100积分</span>
</div>
<div class="example-item">
<Icon name="star" size="md" color="#F59E0B" />
<span>获得五星好评,奖励10积分</span>
</div>
</div>
<a-button type="primary" size="large" @click="goToRewardRules" style="margin-top: 24px;">
进入奖励规则管理
</a-button>
</div>
</div>
</a-tab-pane>
<!-- 积分池管理 -->
<a-tab-pane key="score-pool" tab="积分池">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">激励积分池管理</h2>
<a-button type="primary" @click="goToScorePool">
<template #icon><Icon name="chartBar" size="sm" /></template>
管理积分池
</a-button>
</div>
<div class="reward-rules-intro">
<div class="intro-card">
<Icon name="sparkles" size="xl" color="#FFD700" />
<div class="intro-content">
<h3>什么是激励积分池?</h3>
<p>激励积分池用于管理和控制系统的积分发放预算。您可以设置月度预算、分项预算,并监控积分使用情况。</p>
</div>
</div>
<div class="intro-examples">
<div class="example-item">
<Icon name="document" size="md" color="#3B82F6" />
<span>设置月度积分总预算</span>
</div>
<div class="example-item">
<Icon name="chartBar" size="md" color="#31f08b" />
<span>分项预算(任务、好评、竞赛等)</span>
</div>
<div class="example-item">
<Icon name="bell" size="md" color="#F59E0B" />
<span>超支预警和自动控制</span>
</div>
</div>
<a-button type="primary" size="large" @click="goToScorePool" style="margin-top: 24px;">
进入积分池管理
</a-button>
</div>
</div>
</a-tab-pane>
<!-- 设计师目标积分设置 -->
<a-tab-pane key="designer-target" tab="目标积分">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">设计师目标积分配置</h2>
<a-button type="primary" @click="showDesignerTargetModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加配置
</a-button>
</div>
<a-alert
message="配置说明"
description="为接单部门设计师设置月度目标积分和延误扣分比例。延误扣分 = 任务积分 × 延误小时数 × 扣分比例%。"
type="info"
show-icon
style="margin-bottom: 16px;"
/>
<div class="filter-bar" style="margin-bottom: 16px; display: flex; gap: 16px;">
<a-select
v-model:value="designerTargetFilter.deptId"
placeholder="筛选部门"
style="width: 200px"
allow-clear
@change="loadDesignerTargetList"
>
<a-select-option
v-for="dept in targetScoreDepartments"
:key="dept.id"
:value="dept.id"
>
{{ dept.deptName }}
</a-select-option>
</a-select>
<a-date-picker
v-model:value="designerTargetFilter.monthValue"
picker="month"
placeholder="筛选月份"
format="YYYY-MM"
value-format="YYYY-MM"
:locale="locale"
:popup-class-name="'dark-date-picker'"
@change="handleDesignerTargetMonthChange"
/>
</div>
<a-table
:columns="designerTargetColumns"
:data-source="designerTargetList"
:loading="loadingDesignerTarget"
row-key="id"
:pagination="{ pageSize: 10 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'targetScore'">
<span class="target-score">{{ record.targetScore }} 分</span>
</template>
<template v-else-if="column.key === 'deductionRate'">
<span>{{ record.deductionRate || 1 }}%</span>
</template>
<template v-else-if="column.key === 'effectiveMonth'">
<span>{{ record.effectiveMonth || '通用配置' }}</span>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="showDesignerTargetModal(record)">
编辑
</a-button>
<a-popconfirm
title="确定删除?"
@confirm="handleDeleteDesignerTarget(record.id)"
>
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 每日进度检查配置 -->
<div class="section-header" style="margin-top: 32px;">
<h2 class="section-title">每日进度检查配置</h2>
</div>
<a-alert
message="功能说明"
description="系统每天定时检查设计师是否更新了进行中任务的进度。未更新进度的设计师将被扣分并收到钉钉提醒。"
type="info"
show-icon
style="margin-bottom: 16px;"
/>
<a-form :model="progressCheckConfig" layout="vertical" class="progress-check-form">
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="启用每日进度检查">
<a-switch
v-model:checked="progressCheckConfig.enabled"
@change="saveProgressCheckConfig"
/>
<span style="margin-left: 8px; color: #888;">{{ progressCheckConfig.enabled ? '已启用' : '已禁用' }}</span>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="每日检查时间(小时,24小时制)">
<a-input-number
v-model:value="progressCheckConfig.checkHour"
:min="0"
:max="23"
:disabled="!progressCheckConfig.enabled"
@change="saveProgressCheckConfig"
style="width: 120px;"
/>
<span style="margin-left: 8px; color: #888;">点</span>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="未更新进度扣分">
<a-input-number
v-model:value="progressCheckConfig.deductionScore"
:min="0"
:max="100"
:precision="1"
:disabled="!progressCheckConfig.enabled"
@change="saveProgressCheckConfig"
style="width: 120px;"
/>
<span style="margin-left: 8px; color: #888;">分/任务/天</span>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="扣分时发送钉钉通知">
<a-switch
v-model:checked="progressCheckConfig.notifyEnabled"
:disabled="!progressCheckConfig.enabled"
@change="saveProgressCheckConfig"
/>
<span style="margin-left: 8px; color: #888;">{{ progressCheckConfig.notifyEnabled ? '已启用' : '已禁用' }}</span>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
</a-tab-pane>
<!-- 钉钉集成 -->
<a-tab-pane key="dingtalk" tab="钉钉集成">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">钉钉集成配置</h2>
</div>
<a-form :model="dingTalkConfig" layout="vertical" class="dingtalk-form">
<a-alert
v-if="!dingTalkConfig.enabled"
message="钉钉集成未启用"
description="配置钉钉应用信息后,可启用钉钉通知、钉钉登录等功能。"
type="info"
show-icon
style="margin-bottom: 24px;"
/>
<a-divider orientation="left">应用配置</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="AgentId">
<a-input v-model:value="dingTalkConfig.agentId" placeholder="钉钉应用AgentId" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="AppKey">
<a-input v-model:value="dingTalkConfig.appKey" placeholder="钉钉应用AppKey" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="AppSecret">
<a-input-password v-model:value="dingTalkConfig.appSecret" placeholder="钉钉应用AppSecret" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="企业ID (CorpId)">
<a-input v-model:value="dingTalkConfig.corpId" placeholder="以ding开头,如:dingxxxxxxxxxxxxxxxx" />
<div style="margin-top: 4px; font-size: 12px; color: #888;">
<Icon name="info" size="sm" color="#888" style="margin-right: 4px;" />
在钉钉开放平台 → 应用开发 → 企业内部开发 → 应用信息中查看,格式以 "ding" 开头
</div>
</a-form-item>
<a-divider orientation="left">机器人配置(可选)</a-divider>
<a-form-item label="机器人Webhook地址">
<a-input v-model:value="dingTalkConfig.robotWebhook" placeholder="钉钉机器人Webhook" />
</a-form-item>
<a-form-item label="机器人密钥">
<a-input-password v-model:value="dingTalkConfig.robotSecret" placeholder="加签密钥(可选)" />
</a-form-item>
<a-divider orientation="left">启用功能</a-divider>
<a-form-item>
<a-checkbox-group v-model:value="enabledFeaturesList">
<a-checkbox value="notification">消息通知</a-checkbox>
<a-checkbox value="login">钉钉登录</a-checkbox>
<a-checkbox value="sync">用户同步</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSaveDingTalk" :loading="savingDingTalk">
<template #icon><Icon name="check" size="sm" /></template>
保存配置
</a-button>
<a-button @click="handleTestDingTalk" :loading="testingDingTalk">
<template #icon><Icon name="bolt" size="sm" /></template>
测试连接
</a-button>
</a-space>
</a-form-item>
<a-divider orientation="left">群机器人消息测试</a-divider>
<a-form-item label="选择消息类型">
<a-radio-group v-model:value="testMessageType" button-style="solid">
<a-radio-button value="simple">简单测试</a-radio-button>
<a-radio-button value="task_publish">新任务发布</a-radio-button>
<a-radio-button value="task_assign">任务指派</a-radio-button>
<a-radio-button value="task_reject">任务驳回</a-radio-button>
<a-radio-button value="subtask_publish">子任务组发布</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSendTestMessage" :loading="sendingTestMessage" :disabled="!dingTalkConfig.robotWebhook">
<template #icon><Icon name="send" size="sm" /></template>
发送群消息
</a-button>
<span class="test-message-hint">发送到群机器人,群成员都能看到</span>
</a-form-item>
<a-divider orientation="left">个人工作通知测试</a-divider>
<a-alert
message="发送给个人的工作通知"
description="工作通知是企业内部应用发送的消息,只有接收人本人能看到,适合任务提醒、积分变动等私密通知。"
type="info"
show-icon
style="margin-bottom: 16px;"
/>
<a-form-item label="接收人">
<a-input v-model:value="personalMessageUser" placeholder="输入用户名或真实姓名,如:南亭" style="width: 200px;" />
</a-form-item>
<a-form-item label="消息类型">
<a-radio-group v-model:value="personalMessageType" button-style="solid">
<a-radio-button value="simple">简单测试</a-radio-button>
<a-radio-button value="task_publish">新任务发布</a-radio-button>
<a-radio-button value="task_assign">任务指派</a-radio-button>
<a-radio-button value="task_complete">任务完成</a-radio-button>
<a-radio-button value="score_change">积分变动</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSendPersonalMessage" :loading="sendingPersonalMessage" :disabled="!personalMessageUser || !dingTalkConfig.appKey">
<template #icon><Icon name="send" size="sm" /></template>
发送个人消息
</a-button>
<span class="test-message-hint">仅接收人本人可见</span>
</a-form-item>
<a-divider orientation="left">部门同步</a-divider>
<a-alert
message="部门同步说明"
description="同步钉钉部门到系统:自动获取所有钉钉部门并设置ding_dept_id。如果钉钉部门在系统中不存在,会自动创建。同步后,用户登录时就能自动匹配到部门了。"
type="info"
show-icon
style="margin-bottom: 16px;"
/>
<a-form-item>
<a-button type="primary" @click="handleSyncDepartments" :loading="syncingDepartments" :disabled="!dingTalkConfig.appKey || !dingTalkConfig.appSecret">
<template #icon><Icon name="sync" size="sm" /></template>
同步钉钉部门
</a-button>
</a-form-item>
</a-form>
</div>
</a-tab-pane>
<!-- 导航栏管理 -->
<a-tab-pane key="navbar" tab="导航栏管理">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">导航栏显示配置</h2>
<p class="section-description">管理导航栏中主菜单和功能按钮的显示状态</p>
</div>
<a-form :model="navbarConfig" layout="vertical" class="navbar-config-form">
<a-divider orientation="left">
<Icon name="menu" size="sm" style="margin-right: 8px;" />
主菜单项
</a-divider>
<div class="config-grid">
<div class="config-item">
<div class="config-item-header">
<Icon name="clipboard" size="md" color="#31f08b" />
<span class="config-item-title">任务大厅</span>
</div>
<a-switch
v-model:checked="navbarConfig.menuItems.tasks"
checked-children="显示"
un-checked-children="隐藏"
/>
</div>
<div class="config-item">
<div class="config-item-header">
<Icon name="users" size="md" color="#31f08b" />
<span class="config-item-title">设计师大厅</span>
</div>
<a-switch
v-model:checked="navbarConfig.menuItems.designer_hall"
checked-children="显示"
un-checked-children="隐藏"
/>
</div>
<div class="config-item">
<div class="config-item-header">
<Icon name="trophy" size="md" color="#31f08b" />
<span class="config-item-title">激励中心</span>
</div>
<a-switch
v-model:checked="navbarConfig.menuItems.incentive"
checked-children="显示"
un-checked-children="隐藏"
/>
</div>
<div class="config-item">
<div class="config-item-header">
<Icon name="chart" size="md" color="#31f08b" />
<span class="config-item-title">数据分析</span>
</div>
<a-switch
v-model:checked="navbarConfig.menuItems.analytics"
checked-children="显示"
un-checked-children="隐藏"
/>
</div>
</div>
<a-divider orientation="left">
<Icon name="bolt" size="sm" style="margin-right: 8px;" />
功能按钮
</a-divider>
<div class="config-grid">
<div class="config-item">
<div class="config-item-header">
<Icon name="bell" size="md" color="#31f08b" />
<span class="config-item-title">通知按钮</span>
</div>
<a-switch
v-model:checked="navbarConfig.actions.notification"
checked-children="显示"
un-checked-children="隐藏"
/>
</div>
<div class="config-item">
<div class="config-item-header">
<Icon name="calendar" size="md" color="#31f08b" />
<span class="config-item-title">签到按钮</span>
</div>
<a-switch
v-model:checked="navbarConfig.actions.checkin"
checked-children="显示"
un-checked-children="隐藏"
/>
</div>
<div class="config-item">
<div class="config-item-header">
<Icon name="trophy" size="md" color="#31f08b" />
<span class="config-item-title">成就按钮</span>
</div>
<a-switch
v-model:checked="navbarConfig.actions.achievement"
checked-children="显示"
un-checked-children="隐藏"
/>
</div>
<div class="config-item">
<div class="config-item-header">
<Icon name="settings" size="md" color="#31f08b" />
<span class="config-item-title">设置按钮</span>
</div>
<a-switch
v-model:checked="navbarConfig.actions.settings"
checked-children="显示"
un-checked-children="隐藏"
/>
</div>
</div>
<a-form-item style="margin-top: 32px;">
<a-space>
<a-button type="primary" @click="handleSaveNavbarConfig" :loading="savingNavbarConfig">
<template #icon><Icon name="check" size="sm" /></template>
保存配置
</a-button>
<a-button @click="handleResetNavbarConfig">
<template #icon><Icon name="refresh" size="sm" /></template>
恢复默认
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
</a-tab-pane>
<!-- 任务发布配置 -->
<a-tab-pane key="task-release" tab="任务发布配置">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">任务发布类型配置</h2>
<p class="section-description">管理发起任务页面中可用的发布类型选项</p>
</div>
<a-form :model="taskReleaseConfig" layout="vertical" class="navbar-config-form">
<a-divider orientation="left">
<Icon name="settings" size="sm" style="margin-right: 8px;" />
发布类型按钮
</a-divider>
<div class="config-grid">
<div class="config-item">
<div class="config-item-header">
<Icon name="arrowPath" size="md" color="#31f08b" />
<span class="config-item-title">默认模式</span>
</div>
<div class="config-item-desc">系统自动分配给主管,由主管负责分配</div>
<a-switch
v-model:checked="taskReleaseConfig.default"
checked-children="显示"
un-checked-children="隐藏"
/>
</div>
<div class="config-item">
<div class="config-item-header">
<Icon name="bell" size="md" color="#31f08b" />
<span class="config-item-title">招募任务</span>
</div>
<div class="config-item-desc">发布到任务大厅,设计师可自行抢单</div>
<a-switch
v-model:checked="taskReleaseConfig.recruitment"
checked-children="显示"
un-checked-children="隐藏"
/>
</div>
<div class="config-item">
<div class="config-item-header">
<Icon name="star" size="md" color="#31f08b" />
<span class="config-item-title">指定任务</span>
</div>
<div class="config-item-desc">指定特定设计师接收任务</div>
<a-switch
v-model:checked="taskReleaseConfig.assigned"
checked-children="显示"
un-checked-children="隐藏"
/>
</div>
</div>
<a-alert
message="提示"
description="至少需要保留一个发布类型。如果全部关闭,系统将自动启用默认模式。"
type="info"
show-icon
style="margin-top: 16px; margin-bottom: 16px;"
/>
<a-form-item style="margin-top: 32px;">
<a-space>
<a-button type="primary" @click="handleSaveTaskReleaseConfig" :loading="savingTaskReleaseConfig">
<template #icon><Icon name="check" size="sm" /></template>
保存配置
</a-button>
<a-button @click="handleResetTaskReleaseConfig">
<template #icon><Icon name="refresh" size="sm" /></template>
恢复默认
</a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 一级需求部门设置 -->
<div class="section-header" style="margin-top: 40px;">
<h2 class="section-title">一级需求部门设置</h2>
<p class="section-description">配置一级需求部门发布任务时的表单选项</p>
</div>
<a-form :model="primaryDeptConfig" layout="vertical" class="navbar-config-form">
<a-divider orientation="left">
<Icon name="document" size="sm" style="margin-right: 8px;" />
任务明细填写
</a-divider>
<div class="config-grid" style="grid-template-columns: 1fr;">
<div class="config-item">
<div class="config-item-header">
<Icon name="document" size="md" color="#faad14" />
<span class="config-item-title">一级需求部门可填写任务明细</span>
</div>
<div class="config-item-desc">
开启后,一级需求部门发布任务时可以选填任务明细。<br/>
关闭后,一级需求部门无法填写任务明细,任务明细将完全由需求部门成员填写。
</div>
<a-switch
v-model:checked="primaryDeptConfig.canFillDetail"
checked-children="开启"
un-checked-children="关闭"
/>
</div>
</div>
<a-divider orientation="left">
<Icon name="users" size="sm" style="margin-right: 8px;" />
成员分配范围
</a-divider>
<div class="config-grid" style="grid-template-columns: 1fr;">
<div class="config-item">
<div class="config-item-header">
<Icon name="users" size="md" color="#52c41a" />
<span class="config-item-title">允许跨部门分配成员</span>
</div>
<div class="config-item-desc">
开启后,需求部门负责人审核一级需求任务时,可以选择<strong>所有需求部门</strong>的成员来完善任务明细。<br/>
关闭后,只能选择<strong>当前负责人所属部门</strong>的成员。
</div>
<a-switch
v-model:checked="primaryDeptConfig.crossDeptAssign"
checked-children="开启"
un-checked-children="关闭"
/>
</div>
</div>
<a-alert
message="说明"
description="一级需求部门发起的任务会先流转到需求部门负责人审核,审核通过后由指定的需求部门成员完善任务明细后再发布。开启跨部门分配后,可以将任务分配给其他需求部门的成员。"
type="info"
show-icon
style="margin-top: 16px; margin-bottom: 16px;"
/>
<a-form-item style="margin-top: 32px;">
<a-button type="primary" @click="handleSavePrimaryDeptConfig" :loading="savingPrimaryDeptConfig">
<template #icon><Icon name="check" size="sm" /></template>
保存配置
</a-button>
</a-form-item>
</a-form>
<!-- 延误扣分配置 -->
<div class="section-header" style="margin-top: 40px;">
<h2 class="section-title">延误扣分配置</h2>
<p class="section-description">设置任务延误时的扣分规则</p>
</div>
<a-form :model="delayPenaltyConfig" layout="vertical" class="navbar-config-form">
<a-divider orientation="left">
<Icon name="clock" size="sm" style="margin-right: 8px;" />
扣分规则
</a-divider>
<div class="config-grid" style="grid-template-columns: 1fr;">
<!-- 每小时扣分 -->
<div class="config-item delay-config-card">
<div class="config-info">
<div class="config-header">
<div class="icon-box red">
<Icon name="clock" size="md" />
</div>
<span class="title">每小时扣分</span>
</div>
<div class="desc">任务延误完结时每延误1小时扣除的分数,支持小数。</div>
</div>
<div class="config-action">
<div class="input-with-suffix">
<a-input-number
v-model:value="delayPenaltyConfig.perHour"
:min="0"
:max="100"
:step="0.1"
:precision="2"
size="large"
style="width: 120px"
/>
<span class="input-suffix">分/小时</span>
</div>
</div>
</div>
<!-- 月度积分独立统计 -->
<div class="config-item delay-config-card">
<div class="config-info">
<div class="config-header">
<div class="icon-box green">
<Icon name="calendar" size="md" />
</div>
<span class="title">月度积分独立统计</span>
</div>
<div class="desc">启用后,接单部门的积分将按月独立统计,每月1号重新开始计算,不累加上月积分。</div>
</div>
<div class="config-action">
<a-switch
v-model:checked="delayPenaltyConfig.monthlyReset"
checked-children="已启用"
un-checked-children="已关闭"
/>
</div>
</div>
</div>
<a-alert
message="延误扣分计算公式"
type="info"
show-icon
style="margin-top: 24px; margin-bottom: 24px;"
>
<template #description>
<div class="formula-desc">
<p><strong>延误时间</strong> = 实际完成时间 - 预计完成时间(接单时间 + 累计时效)</p>
<p><strong>延误扣分</strong> = 延误小时数 × 每小时扣分数</p>
<p class="note">* 扣分将从该任务原本应得的积分中扣除,最多扣至0分(不会出现负分)。</p>
</div>
</template>
</a-alert>
<a-form-item>
<a-space>
<a-button type="primary" size="large" @click="handleSaveDelayPenaltyConfig" :loading="savingDelayPenaltyConfig">
<template #icon><Icon name="check" size="sm" /></template>
保存延误配置
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
</a-tab-pane>
<!-- S3存储配置 -->
<a-tab-pane key="s3-config" tab="S3存储配置">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">S3存储配置管理</h2>
<a-button type="primary" @click="showS3ConfigModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加配置
</a-button>
</div>
<a-table
:columns="s3ConfigColumns"
:data-source="s3Configs"
:loading="loadingS3Configs"
row-key="id"
:pagination="{ pageSize: 10 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'isActive'">
<a-tag :color="record.isActive ? 'green' : 'default'">
{{ record.isActive ? '已激活' : '未激活' }}
</a-tag>
</template>
<template v-else-if="column.key === 'secretAccessKey'">
<span>******</span>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="showS3ConfigModal(record)">编辑</a-button>
<a-button
v-if="!record.isActive"
type="link"
size="small"
@click="handleActivateS3Config(record.id)"
>
激活
</a-button>
<a-popconfirm
v-if="!record.isActive"
title="确定删除?"
@confirm="handleDeleteS3Config(record.id)"
>
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</div>
</a-tab-pane>
<!-- 第三方素材API配置 -->
<a-tab-pane key="external-asset-api" tab="素材API配置">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">第三方素材API配置</h2>
</div>
<a-alert
message="使用说明"
description="配置第三方素材API后,设计师完结任务时系统会自动调用API验证素材是否已提交。如果素材未提交,系统会提示设计师前往素材平台提交。完结成功后,需求方可以直接下载素材。"
type="info"
show-icon
style="margin-bottom: 24px;"
/>
<a-form :model="externalAssetApiConfig" layout="vertical" class="external-api-form" style="max-width: 600px;">
<a-form-item label="启用第三方素材API">
<a-switch
v-model:checked="externalAssetApiConfig.enabled"
checked-children="已启用"
un-checked-children="未启用"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
启用后,设计师完结任务时将验证素材是否已在第三方平台提交
</div>
</a-form-item>
<a-form-item label="API地址" :required="externalAssetApiConfig.enabled">
<a-input
v-model:value="externalAssetApiConfig.apiUrl"
placeholder="https://your-domain.com/api/external/"
size="large"
:disabled="!externalAssetApiConfig.enabled"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
第三方素材系统的API地址,需以/结尾
</div>
</a-form-item>
<a-form-item label="API密钥" :required="externalAssetApiConfig.enabled">
<a-input-password
v-model:value="externalAssetApiConfig.apiKey"
placeholder="请输入API密钥"
size="large"
:disabled="!externalAssetApiConfig.enabled"
style="width: 100%;"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
用于调用第三方素材API的密钥(X-API-Key)
</div>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" size="large" @click="handleSaveExternalAssetApiConfig" :loading="savingExternalAssetApi">
<template #icon><Icon name="check" size="sm" /></template>
保存配置
</a-button>
<a-button
size="large"
@click="handleTestExternalAssetApi"
:loading="testingExternalAssetApi"
:disabled="!externalAssetApiConfig.apiUrl || !externalAssetApiConfig.apiKey"
style="background: transparent; border-color: #31f08b; color: #31f08b;"
>
<template #icon><Icon name="refresh" size="sm" color="#31f08b" /></template>
测试连接
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
</a-tab-pane>
<!-- 任务排队配置 -->
<a-tab-pane key="task-unlock" tab="任务排队配置">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">任务排队解锁配置</h2>
</div>
<a-alert
message="功能说明"
description="启用任务排队机制后,新发布的任务会进入排队池,系统每天凌晨1点自动解锁一批任务。确保发布时间早的任务优先被处理,避免任务积压。指定任务和流水线任务不受此限制。"
type="info"
show-icon
style="margin-bottom: 24px;"
/>
<a-form :model="taskUnlockConfig" layout="vertical" class="task-unlock-form" style="max-width: 600px;">
<a-form-item label="启用任务排队机制">
<a-switch
v-model:checked="taskUnlockConfig.enabled"
checked-children="已启用"
un-checked-children="未启用"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
启用后,招募类任务将进入排队池等待解锁,设计师只能看到已解锁的任务
</div>
</a-form-item>
<a-form-item label="每日最少解锁数">
<a-input-number
v-model:value="taskUnlockConfig.dailyMin"
:min="1"
:max="1000"
size="large"
style="width: 200px;"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
每个接单部门每天凌晨自动解锁的最少任务数量(按发布时间从早到晚解锁)
</div>
</a-form-item>
<a-form-item label="触发补充解锁数">
<a-input-number
v-model:value="taskUnlockConfig.batchSize"
:min="1"
:max="500"
size="large"
style="width: 200px;"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
当设计师接单后,如果剩余可接任务少于此数量,将自动补充解锁新任务
</div>
</a-form-item>
<a-divider />
<a-form-item>
<a-space>
<a-button type="primary" size="large" @click="handleSaveTaskUnlockConfig" :loading="savingTaskUnlockConfig">
<template #icon><Icon name="check" size="sm" /></template>
保存配置
</a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 解锁状态统计 -->
<a-divider orientation="left">排队状态统计</a-divider>
<a-button
@click="loadTaskUnlockStats"
:loading="loadingTaskUnlockStats"
style="margin-bottom: 16px; background: transparent; border-color: #31f08b; color: #31f08b;"
>
<template #icon><Icon name="refresh" size="sm" color="#31f08b" /></template>
刷新统计
</a-button>
<a-row :gutter="16" v-if="taskUnlockStats">
<a-col :span="8">
<a-statistic title="排队中任务" :value="taskUnlockStats.totalQueued" style="color: #fa8c16;">
<template #prefix><Icon name="clock" size="sm" color="#fa8c16" /></template>
</a-statistic>
</a-col>
<a-col :span="8">
<a-statistic title="已解锁任务" :value="taskUnlockStats.totalUnlocked" style="color: #52c41a;">
<template #prefix><Icon name="unlock" size="sm" color="#52c41a" /></template>
</a-statistic>
</a-col>
</a-row>
<a-table
v-if="taskUnlockStats && taskUnlockStats.deptStats"
:columns="taskUnlockStatsColumns"
:data-source="taskUnlockStats.deptStats"
:pagination="false"
size="small"
style="margin-top: 16px;"
row-key="deptId"
/>
</div>
</a-tab-pane>
<!-- 任务数据管理 -->
<a-tab-pane key="task-management" tab="任务数据管理">
<div class="settings-section">
<div class="section-header">
<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="删除任务将永久删除该任务及其所有子任务数据,此操作不可恢复,请谨慎操作!"
type="warning"
show-icon
style="margin-bottom: 24px;"
/>
<a-card title="删除任务" class="task-delete-card">
<a-form layout="vertical">
<a-form-item label="任务编号">
<a-input
v-model:value="taskDeleteForm.taskNo"
placeholder="请输入要删除的任务编号,例如:TASK202601150001"
size="large"
style="max-width: 400px;"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
<Icon name="info" size="sm" color="#888" style="margin-right: 4px;" />
任务编号可以在任务卡片或任务详情中找到
</div>
</a-form-item>
<a-form-item>
<a-popconfirm
title="确定要删除这个任务吗?"
description="删除后将无法恢复,包括所有子任务数据!"
ok-text="确认删除"
cancel-text="取消"
ok-type="danger"
@confirm="handleDeleteTask"
>
<a-button
type="primary"
danger
size="large"
:loading="deletingTask"
:disabled="!taskDeleteForm.taskNo"
>
<template #icon><Icon name="trash" size="sm" /></template>
删除任务
</a-button>
</a-popconfirm>
</a-form-item>
</a-form>
</a-card>
</div>
</a-tab-pane>
<!-- 第三方数据库配置 -->
<a-tab-pane key="third-party-db" tab="第三方数据库">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">第三方数据库配置</h2>
<a-button type="primary" @click="showThirdPartyDbModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加配置
</a-button>
</div>
<a-alert
message="使用说明"
description="第三方数据库用于查询商品库存信息。当任务子类型设置为需要检查库存时,发布任务时会根据商品条码查询库存,库存不足则无法发布任务。"
type="info"
show-icon
style="margin-bottom: 16px;"
/>
<a-table
:columns="thirdPartyDbColumns"
:data-source="thirdPartyDbConfigs"
:loading="loadingThirdPartyDb"
row-key="id"
:pagination="{ pageSize: 10 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'enabled'">
<a-tag :color="record.enabled ? 'green' : 'default'">
{{ record.enabled ? '已启用' : '未启用' }}
</a-tag>
</template>
<template v-else-if="column.key === 'password'">
<span>******</span>
</template>
<template v-else-if="column.key === 'connection'">
<span>{{ record.host }}:{{ record.port }}/{{ record.database }}</span>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="link" size="small" @click="handleTestThirdPartyDb(record)">
测试连接
</a-button>
<a-button type="link" size="small" @click="showThirdPartyDbModal(record)">
编辑
</a-button>
<a-button
v-if="!record.enabled"
type="link"
size="small"
@click="handleEnableThirdPartyDb(record.id)"
>
启用
</a-button>
<a-button
v-else
type="link"
size="small"
@click="handleDisableThirdPartyDb(record.id)"
>
禁用
</a-button>
<a-popconfirm
title="确定删除?"
@confirm="handleDeleteThirdPartyDb(record.id)"
>
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</div>
</a-tab-pane>
</a-tabs>
<!-- 需求类型弹窗 -->
<a-modal
v-model:open="modals.demandType"
:title="getDemandTypeModalTitle()"
@ok="saveDemandType"
@cancel="closeModal('demandType')"
width="600px"
>
<a-form :model="formData.demandType" layout="vertical">
<!-- 编辑时允许调整级别 -->
<a-form-item v-if="editingItem" label="调整级别">
<a-radio-group v-model:value="formData.demandType.addLevel" @change="onLevelChange">
<a-radio value="level1">一级分类</a-radio>
<a-radio value="level2">二级分类</a-radio>
<a-radio value="level3">三级分类</a-radio>
</a-radio-group>
<div class="form-tip">
<Icon name="info" size="sm" color="#6B6B6B" style="margin-right: 4px;" />
调整级别后需要选择对应的上级分类
</div>
</a-form-item>
<!-- 选择所属一级分类(二级分类显示) -->
<a-form-item v-if="formData.demandType.addLevel === 'level2'" label="所属一级分类" required>
<a-select
v-model:value="formData.demandType.parentId"
placeholder="请选择所属一级分类"
>
<a-select-option
v-for="parent in level1DemandTypes.filter(p => p.id !== formData.demandType.id)"
:key="parent.id"
:value="parent.id"
>
{{ parent.name }}
</a-select-option>
</a-select>
<div v-if="editingItem" class="form-tip">
<Icon name="info" size="sm" color="#6B6B6B" style="margin-right: 4px;" />
可以修改所属分类来调整层级
</div>
</a-form-item>
<!-- 选择所属二级分类(三级分类显示) -->
<a-form-item v-if="formData.demandType.addLevel === 'level3'" label="所属二级分类" required>
<a-select
v-model:value="formData.demandType.parentId"
placeholder="请选择所属二级分类"
>
<a-select-option
v-for="parent in level2DemandTypes.filter(p => p.id !== formData.demandType.id)"
:key="parent.id"
:value="parent.id"
>
{{ getLevel2DisplayName(parent) }}
</a-select-option>
</a-select>
<div v-if="editingItem" class="form-tip">
<Icon name="info" size="sm" color="#6B6B6B" style="margin-right: 4px;" />
可以修改所属分类来调整层级
</div>
</a-form-item>
<a-form-item label="类型名称" required>
<a-input v-model:value="formData.demandType.name" placeholder="请输入类型名称" />
</a-form-item>
<!-- 单价分值(二级和三级分类都可以设置) -->
<a-form-item
v-if="formData.demandType.addLevel === 'level2' || formData.demandType.addLevel === 'level3'"
label="单价分值"
:required="formData.demandType.addLevel === 'level3'"
>
<a-input-number
v-model:value="formData.demandType.unitScore"
:min="0"
:precision="3"
:step="0.001"
style="width: 100%"
placeholder="请输入单价分值(如0.001)"
/>
<div v-if="formData.demandType.addLevel === 'level2'" class="form-tip">
<Icon name="info" size="sm" color="#6B6B6B" style="margin-right: 4px;" />
如果二级分类下没有三级子分类,可以直接设置分值
</div>
</a-form-item>
<!-- 预计时长(二级和三级分类都可以设置) -->
<a-form-item v-if="formData.demandType.addLevel === 'level2' || formData.demandType.addLevel === 'level3'" label="单个任务耗时(小时)">
<a-input-number
v-model:value="formData.demandType.estimatedHours"
:min="0"
:precision="2"
:step="0.05"
style="width: 100%"
placeholder="单个任务耗时(如0.05=3分钟,1=1小时)"
/>
<div class="form-tip">
<Icon name="info" size="sm" color="#6B6B6B" style="margin-right: 4px;" />
时效会根据任务数量累计计算(如5个任务×0.05小时=0.25小时)
</div>
</a-form-item>
<a-form-item label="描述">
<a-textarea v-model:value="formData.demandType.description" :rows="3" placeholder="请输入描述" />
</a-form-item>
<!-- 可接单部门(仅二级分类显示) -->
<a-form-item v-if="formData.demandType.addLevel === 'level2'" label="可接单部门">
<a-select
v-model:value="formData.demandType.departmentIds"
mode="multiple"
placeholder="请选择可接单部门(不选则所有接单部门都可接)"
allow-clear
:max-tag-count="3"
>
<a-select-opt-group
v-for="group in flatDepartmentOptions"
:key="group.label"
:label="group.label"
>
<a-select-option
v-for="dept in group.options"
:key="dept.id"
:value="dept.id"
>
{{ dept.displayName }}
</a-select-option>
</a-select-opt-group>
</a-select>
<div class="form-tip">
<Icon name="info" size="sm" color="#6B6B6B" style="margin-right: 4px;" />
选择父级部门会自动包含所有子部门。三级分类会继承此设置。
</div>
</a-form-item>
<!-- 一级分类提示 -->
<a-alert v-if="formData.demandType.addLevel === 'level1'" type="info" show-icon>
<template #message>
<span>一级分类是顶级分类,用于组织二级分类</span>
</template>
</a-alert>
<!-- 三级分类提示 -->
<a-alert v-if="formData.demandType.addLevel === 'level3'" type="info" show-icon>
<template #message>
<span>三级分类将继承二级分类的可接单部门设置</span>
</template>
</a-alert>
</a-form>
</a-modal>
<!-- 任务子类型弹窗 -->
<a-modal
v-model:open="modals.taskSubType"
:title="editingItem ? '编辑任务子类型' : '添加任务子类型'"
@ok="saveTaskSubType"
@cancel="closeModal('taskSubType')"
>
<a-form :model="formData.taskSubType" layout="vertical">
<a-form-item label="子类型名称" required>
<a-input v-model:value="formData.taskSubType.name" placeholder="请输入子类型名称(如:新品、优化)" />
</a-form-item>
<a-form-item label="子类型编码" required>
<a-input v-model:value="formData.taskSubType.code" placeholder="留空自动生成">
<template #addonAfter>
<a-button size="small" type="link" @click="autoGenerateTaskSubTypeCode" style="padding: 0 8px;">
自动生成
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item label="描述">
<a-textarea v-model:value="formData.taskSubType.description" :rows="3" placeholder="请输入描述" />
</a-form-item>
<a-form-item label="排序">
<a-input-number
v-model:value="formData.taskSubType.sortOrder"
:min="0"
:precision="0"
style="width: 100%"
placeholder="排序值越小越靠前"
/>
</a-form-item>
<a-form-item label="是否检查库存">
<a-switch v-model:checked="formData.taskSubType.checkInventory" />
<div class="form-tip" style="margin-top: 8px;">
<Icon name="info" size="sm" color="#6B6B6B" style="margin-right: 4px;" />
开启后,发布此类型任务时需要输入商品条码并检查库存,库存不足则无法发布任务
</div>
</a-form-item>
</a-form>
</a-modal>
<!-- 品牌弹窗 -->
<a-modal
v-model:open="modals.brand"
:title="editingItem ? '编辑品牌' : '添加品牌'"
@ok="saveBrand"
@cancel="closeModal('brand')"
>
<a-form :model="formData.brand" layout="vertical">
<a-form-item label="品牌名称" required>
<a-input v-model:value="formData.brand.name" placeholder="请输入品牌名称" />
</a-form-item>
<a-form-item label="品牌编码">
<a-input v-model:value="formData.brand.brandCode" placeholder="留空自动生成">
<template #addonAfter>
<a-button size="small" type="link" @click="autoGenerateBrandCode" style="padding: 0 8px;">
自动生成
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item label="品牌描述">
<a-textarea v-model:value="formData.brand.description" :rows="3" placeholder="请输入描述" />
</a-form-item>
<a-form-item label="排序">
<a-input-number
v-model:value="formData.brand.sortOrder"
:min="0"
style="width: 100%"
placeholder="数字越小越靠前"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 平台弹窗 -->
<a-modal
v-model:open="modals.platform"
:title="editingItem ? '编辑平台' : '添加平台'"
@ok="savePlatform"
@cancel="closeModal('platform')"
>
<a-form :model="formData.platform" layout="vertical">
<a-form-item label="平台名称" required>
<a-input v-model:value="formData.platform.name" placeholder="请输入平台名称" />
</a-form-item>
<a-form-item label="平台编码">
<a-input v-model:value="formData.platform.platformCode" placeholder="留空自动生成">
<template #addonAfter>
<a-button size="small" type="link" @click="autoGeneratePlatformCode" style="padding: 0 8px;">
自动生成
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item label="平台描述">
<a-textarea v-model:value="formData.platform.description" :rows="3" placeholder="请输入描述" />
</a-form-item>
<a-form-item label="排序">
<a-input-number
v-model:value="formData.platform.sortOrder"
:min="0"
style="width: 100%"
placeholder="数字越小越靠前"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 类目弹窗 -->
<a-modal
v-model:open="modals.category"
:title="editingItem ? '编辑类目' : '添加类目'"
@ok="saveCategory"
@cancel="closeModal('category')"
>
<a-form :model="formData.category" layout="vertical">
<a-form-item label="类目名称" required>
<a-input v-model:value="formData.category.name" placeholder="请输入类目名称" />
</a-form-item>
<a-form-item label="类目编码">
<a-input v-model:value="formData.category.categoryCode" placeholder="留空自动生成">
<template #addonAfter>
<a-button size="small" type="link" @click="autoGenerateCategoryCode" style="padding: 0 8px;">
自动生成
</a-button>
</template>
</a-input>
</a-form-item>
<a-form-item label="类目描述">
<a-textarea v-model:value="formData.category.description" :rows="3" placeholder="请输入描述" />
</a-form-item>
<a-form-item label="排序">
<a-input-number
v-model:value="formData.category.sortOrder"
:min="0"
style="width: 100%"
placeholder="数字越小越靠前"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 调整用户部门 -->
<a-modal
v-model:open="transferModalVisible"
title="调整用户部门"
wrap-class-name="dark-modal"
@ok="handleTransferDepartment"
@cancel="closeTransferModal"
>
<div class="transfer-modal">
<p class="transfer-tip">
当前用户:
<span class="highlight">
{{ selectedUser?.realName || selectedUser?.username || '--' }}
</span>
</p>
<a-form layout="vertical">
<a-form-item label="选择部门">
<a-tree-select
v-model:value="transferDeptId"
placeholder="请选择部门"
style="width: 100%"
:tree-data="departmentTreeSelectData"
:field-names="{ children: 'children', label: 'title', value: 'value' }"
tree-default-expand-all
allow-clear
show-search
:filter-tree-node="filterDeptTreeNode"
tree-node-filter-prop="title"
/>
</a-form-item>
</a-form>
</div>
</a-modal>
<!-- 部门弹窗 -->
<a-modal
v-model:open="modals.department"
:title="editingItem ? '编辑部门' : '添加部门'"
width="600px"
@ok="saveDepartment"
@cancel="closeModal('department')"
>
<a-form :model="formData.department" layout="vertical">
<a-form-item label="部门名称" required>
<a-input v-model:value="formData.department.deptName" placeholder="请输入部门名称" />
</a-form-item>
<a-form-item label="部门编码" required>
<a-input v-model:value="formData.department.deptCode" placeholder="请输入部门编码" />
</a-form-item>
<a-form-item label="部门类型" required>
<a-radio-group v-model:value="formData.department.deptType">
<a-radio value="demand">需求部门(只能发需求)</a-radio>
<a-radio value="primary_demand">一级需求部门(发起简化需求)</a-radio>
<a-radio value="receiving">接单部门(只能接任务)</a-radio>
</a-radio-group>
</a-form-item>
<!-- 负责品牌配置(仅普通需求部门显示) -->
<a-form-item v-if="formData.department.deptType === 'demand'" label="负责品牌">
<a-select
v-model:value="formData.department.responsibleBrandIds"
mode="multiple"
placeholder="选择该部门负责的品牌(一级需求部门发起的任务将根据品牌路由到此部门)"
allow-clear
:options="brandOptions"
:filter-option="filterBrandOption"
show-search
/>
<div class="form-tip" style="margin-top: 4px; color: #888; font-size: 12px;">
一个品牌只能由一个需求部门负责,一级需求部门发起任务时会根据选择的品牌自动路由到对应的需求部门
</div>
</a-form-item>
<a-form-item label="部门负责人">
<a-select
v-model:value="departmentLeaderId"
placeholder="请选择部门负责人"
allow-clear
show-search
:filter-option="filterUserOption"
>
<a-select-option
v-for="user in departmentUsers"
:key="user.id"
:value="user.id"
>
{{ user.realName || user.username }} ({{ user.username }})
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="上级部门">
<a-select v-model:value="departmentParentId" placeholder="请选择上级部门" allow-clear>
<a-select-option :value="0">顶级部门(一级)</a-select-option>
<a-select-opt-group label="一级部门(选择后成为二级)">
<a-select-option
v-for="dept in parentDepartmentOptions.level1"
:key="dept.id"
:value="dept.id"
>
{{ dept.deptName }}
</a-select-option>
</a-select-opt-group>
<a-select-opt-group label="二级部门(选择后成为三级)">
<a-select-option
v-for="dept in parentDepartmentOptions.level2"
:key="dept.id"
:value="dept.id"
>
{{ dept.parentName }} / {{ dept.deptName }}
</a-select-option>
</a-select-opt-group>
</a-select>
<div class="form-tip" style="margin-top: 4px; color: #888; font-size: 12px;">
选择顶级表示创建一级部门,选择一级部门则创建二级部门,选择二级部门则创建三级部门
</div>
</a-form-item>
<a-form-item label="描述">
<a-textarea v-model:value="formData.department.description" :rows="3" placeholder="请输入描述" />
</a-form-item>
<a-form-item label="排序">
<a-input-number
v-model:value="formData.department.sortOrder"
:min="0"
style="width: 100%"
placeholder="数字越小越靠前"
/>
</a-form-item>
<!-- 积分自动发放配置(仅需求部门和一级需求部门) -->
<template v-if="isDemandDept">
<a-divider>积分自动发放配置</a-divider>
<a-form-item label="启用自动发放">
<a-switch v-model:checked="formData.department.autoGrantEnabled" />
<span style="margin-left: 8px; color: #888; font-size: 12px;">
开启后系统将按设定周期自动发放积分
</span>
</a-form-item>
<template v-if="formData.department.autoGrantEnabled">
<a-form-item label="发放周期">
<a-radio-group v-model:value="formData.department.autoGrantPeriod">
<a-radio value="WEEKLY">每周发放</a-radio>
<a-radio value="MONTHLY">每月发放</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="发放日期">
<a-select v-model:value="formData.department.autoGrantDay" style="width: 200px;">
<template v-if="formData.department.autoGrantPeriod === 'WEEKLY'">
<a-select-option :value="1">周一</a-select-option>
<a-select-option :value="2">周二</a-select-option>
<a-select-option :value="3">周三</a-select-option>
<a-select-option :value="4">周四</a-select-option>
<a-select-option :value="5">周五</a-select-option>
<a-select-option :value="6">周六</a-select-option>
<a-select-option :value="7">周日</a-select-option>
</template>
<template v-else>
<a-select-option v-for="d in 28" :key="d" :value="d">{{ d }}号</a-select-option>
</template>
</a-select>
</a-form-item>
<a-form-item label="发放积分数量">
<a-input-number
v-model:value="formData.department.autoGrantAmount"
:min="0"
:max="100000"
placeholder="每次发放的积分数量"
style="width: 200px;"
/>
<span style="margin-left: 8px;">分</span>
</a-form-item>
<a-form-item label="过期清零">
<a-switch v-model:checked="formData.department.expireUnused" />
<span style="margin-left: 8px; color: #888; font-size: 12px;">
开启后,新积分发放时会清零上期未使用的积分
</span>
</a-form-item>
</template>
</template>
</a-form>
</a-modal>
<!-- 积分池管理弹窗 -->
<a-modal
v-model:open="modals.scorePool"
:title="scorePoolModalTitle"
width="700px"
ok-text="关闭"
:cancel-button-props="{ style: { display: 'none' } }"
@ok="modals.scorePool = false"
@cancel="modals.scorePool = false"
>
<div v-if="currentScorePool">
<!-- 提示信息:根据层级显示不同说明 -->
<a-alert
v-if="currentDeptScoreLevel === 1 && hasChildDepartments(currentDept)"
type="info"
show-icon
style="margin-bottom: 16px;"
>
<template #message>
<span>一级部门积分为下属二级部门积分的汇总,不支持直接充值。请前往具体二级部门进行充值。</span>
</template>
</a-alert>
<a-alert
v-else-if="currentDeptScoreLevel === 3"
type="info"
show-icon
style="margin-bottom: 16px;"
>
<template #message>
<span>三级部门共享上级二级部门「{{ getParentDeptName(currentDept) }}」的积分,不支持单独设置。</span>
</template>
</a-alert>
<a-descriptions :column="2" bordered>
<a-descriptions-item label="部门名称">
{{ currentScorePool.deptName }}
<a-tag v-if="currentDeptScoreLevel === 1 && hasChildDepartments(currentDept)" color="blue" style="margin-left: 8px;">汇总</a-tag>
<a-tag v-else-if="currentDeptScoreLevel === 3" color="purple" style="margin-left: 8px;">共享</a-tag>
<a-tag v-else color="green" style="margin-left: 8px;">独立管理</a-tag>
</a-descriptions-item>
<a-descriptions-item label="当前余额">
<span class="balance-display">
{{ getDisplayScoreBalance() }} 分
</span>
</a-descriptions-item>
<a-descriptions-item label="累计充值">
{{ getDisplayTotalRecharge() }} 分
</a-descriptions-item>
<a-descriptions-item label="累计消耗">
{{ getDisplayTotalConsume() }} 分
</a-descriptions-item>
<a-descriptions-item label="累计获得" v-if="currentScorePool.totalEarn">
{{ currentScorePool.totalEarn }} 分
</a-descriptions-item>
</a-descriptions>
<!-- 一级部门:显示子部门积分明细 -->
<template v-if="currentDeptScoreLevel === 1 && hasChildDepartments(currentDept)">
<a-divider>下属二级部门积分明细</a-divider>
<a-table
:columns="childDeptScoreColumns"
:data-source="getChildDepartmentScores()"
:pagination="false"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'actions'">
<a-button type="link" size="small" @click="showScorePoolModal(record)">
管理积分
</a-button>
</template>
</template>
</a-table>
</template>
<!-- 消耗限制设置:只对可管理积分的部门显示 -->
<template v-if="canManageScore">
<a-divider>消耗限制设置</a-divider>
<a-descriptions :column="2" bordered>
<a-descriptions-item label="限制状态">
<a-tag :color="currentScorePool.limitEnabled ? 'green' : 'default'">
{{ currentScorePool.limitEnabled ? '已启用' : '未启用' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="限制周期" v-if="currentScorePool.limitEnabled">
{{ currentScorePool.limitPeriod === 'DAILY' ? '每日' :
currentScorePool.limitPeriod === 'WEEKLY' ? '每周' :
currentScorePool.limitPeriod === 'MONTHLY' ? '每月' : '未知' }}
</a-descriptions-item>
<a-descriptions-item label="周期限额" v-if="currentScorePool.limitEnabled">
{{ currentScorePool.periodLimit || 0 }} 分
</a-descriptions-item>
<a-descriptions-item label="已使用" v-if="currentScorePool.limitEnabled">
{{ currentScorePool.periodUsed || 0 }} 分
<span v-if="currentScorePool.periodLimit">
({{ Math.round((currentScorePool.periodUsed || 0) / currentScorePool.periodLimit * 100) }}%)
</span>
</a-descriptions-item>
</a-descriptions>
<a-divider>充值积分</a-divider>
<a-form layout="inline">
<a-form-item label="充值金额">
<a-input-number
v-model:value="rechargeAmount"
:min="1"
:precision="0"
style="width: 200px"
placeholder="请输入充值金额"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleRecharge">确认充值</a-button>
</a-form-item>
</a-form>
</template>
<!-- 自动发放配置显示 -->
<template v-if="currentScorePool.autoGrantEnabled === 1">
<a-divider>自动发放配置</a-divider>
<a-descriptions :column="2" bordered size="small">
<a-descriptions-item label="发放周期">
{{ currentScorePool.autoGrantPeriod === 'WEEKLY' ? '每周' : '每月' }}
</a-descriptions-item>
<a-descriptions-item label="发放日期">
{{ currentScorePool.autoGrantPeriod === 'WEEKLY'
? ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'][currentScorePool.autoGrantDay || 1]
: (currentScorePool.autoGrantDay || 1) + '号' }}
</a-descriptions-item>
<a-descriptions-item label="发放积分">
{{ currentScorePool.autoGrantAmount || 0 }} 分/次
</a-descriptions-item>
<a-descriptions-item label="过期清零">
<a-tag :color="currentScorePool.expireUnused === 1 ? 'orange' : 'default'">
{{ currentScorePool.expireUnused === 1 ? '是' : '否' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="上次发放" v-if="currentScorePool.lastGrantTime">
{{ formatDateTime(currentScorePool.lastGrantTime) }}
</a-descriptions-item>
<a-descriptions-item label="下次发放" v-if="currentScorePool.nextGrantTime">
{{ formatDateTime(currentScorePool.nextGrantTime) }}
</a-descriptions-item>
</a-descriptions>
<div style="margin-top: 16px;">
<a-button type="default" @click="handleManualGrant" :loading="manualGrantLoading">
手动触发发放
</a-button>
<span style="margin-left: 8px; color: #888; font-size: 12px;">
(用于测试或补发)
</span>
</div>
</template>
<!-- 发放记录 -->
<a-divider>积分发放记录</a-divider>
<a-table
:columns="grantRecordColumns"
:data-source="grantRecords"
:loading="loadingGrantRecords"
:pagination="{ pageSize: 5 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'grantType'">
<a-tag :color="record.grantType === 'AUTO' ? 'blue' : 'green'">
{{ record.grantType === 'AUTO' ? '自动发放' : '手动充值' }}
</a-tag>
</template>
<template v-else-if="column.key === 'createTime'">
{{ formatDateTime(record.createTime) }}
</template>
<template v-else-if="column.key === 'amounts'">
<span style="color: #52c41a;">+{{ record.grantAmount }}</span>
<span v-if="record.expiredAmount > 0" style="color: #ff4d4f; margin-left: 8px;">
-{{ record.expiredAmount }}(过期)
</span>
</template>
</template>
</a-table>
</div>
<a-empty v-else description="加载中..." />
</a-modal>
<!-- S3配置弹窗 -->
<a-modal
v-model:open="modals.s3Config"
:title="s3ConfigForm.id ? '编辑S3配置' : '添加S3配置'"
width="800px"
@ok="handleSaveS3Config"
@cancel="modals.s3Config = false"
>
<a-form :model="s3ConfigForm" layout="vertical">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="服务端点" required>
<a-input v-model:value="s3ConfigForm.endpoint" placeholder="https://s3.1080p.hk" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="存储桶名称" required>
<a-input v-model:value="s3ConfigForm.bucket" placeholder="jsxiangche" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="访问密钥ID" required>
<a-input v-model:value="s3ConfigForm.accessKeyId" placeholder="AccessKey ID" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="访问密钥" required>
<a-input-password v-model:value="s3ConfigForm.secretAccessKey" placeholder="Secret Access Key" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="区域" required>
<a-input v-model:value="s3ConfigForm.region" placeholder="us-east-1" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="公共访问域名">
<a-input v-model:value="s3ConfigForm.publicDomain" placeholder="https://s3.1080p.hk/jsxiangche" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="文件存储路径前缀">
<a-input v-model:value="s3ConfigForm.filePath" placeholder="由壹点AI/" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item>
<a-checkbox v-model:checked="s3ConfigForm.enablePathStyle">
启用路径样式访问
</a-checkbox>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item>
<a-checkbox v-model:checked="s3ConfigForm.isActive">
激活此配置
</a-checkbox>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
<!-- 第三方数据库配置弹窗 -->
<a-modal
v-model:open="modals.thirdPartyDb"
:title="thirdPartyDbForm.id ? '编辑数据库配置' : '添加数据库配置'"
width="700px"
@ok="handleSaveThirdPartyDb"
@cancel="modals.thirdPartyDb = false"
>
<a-form :model="thirdPartyDbForm" layout="vertical">
<a-form-item label="配置名称" required>
<a-input v-model:value="thirdPartyDbForm.name" placeholder="例如:聚水潭数据中台" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="数据库类型" required>
<a-select v-model:value="thirdPartyDbForm.dbType" placeholder="请选择数据库类型">
<a-select-option value="sqlserver">SQL Server</a-select-option>
<a-select-option value="mysql">MySQL</a-select-option>
<a-select-option value="postgresql">PostgreSQL</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="端口号" required>
<a-input-number
v-model:value="thirdPartyDbForm.port"
:min="1"
:max="65535"
style="width: 100%"
placeholder="1433"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="服务器地址" required>
<a-input v-model:value="thirdPartyDbForm.host" placeholder="218.93.191.16" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="数据库名称" required>
<a-input v-model:value="thirdPartyDbForm.database" placeholder="JSDataMiddlePlatform" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="用户名" required>
<a-input v-model:value="thirdPartyDbForm.username" placeholder="用户名" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="密码" required>
<a-input-password v-model:value="thirdPartyDbForm.password" placeholder="密码" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="连接超时(秒)">
<a-input-number
v-model:value="thirdPartyDbForm.connectionTimeout"
:min="1"
:max="120"
style="width: 100%"
placeholder="10"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="是否启用">
<a-switch v-model:checked="thirdPartyDbForm.enabled" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="备注">
<a-textarea v-model:value="thirdPartyDbForm.remark" :rows="2" placeholder="备注信息" />
</a-form-item>
</a-form>
</a-modal>
<!-- 设计师目标积分配置弹窗 -->
<a-modal
v-model:open="designerTargetModalVisible"
:title="designerTargetModalTitle"
width="500px"
@ok="handleSaveDesignerTarget"
@cancel="designerTargetModalVisible = false"
>
<a-form :model="designerTargetForm" layout="vertical">
<a-form-item label="接单部门" required>
<a-select
v-model:value="designerTargetForm.deptId"
placeholder="请选择接单部门"
>
<a-select-option
v-for="dept in targetScoreDepartments"
:key="dept.id"
:value="dept.id"
>
{{ dept.deptName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="目标积分" required>
<a-input-number
v-model:value="designerTargetForm.targetScore"
:min="0"
:max="10000"
style="width: 100%"
placeholder="每月目标积分,例如:500"
/>
<div style="font-size: 12px; color: #888; margin-top: 4px;">
设计师每月需完成的目标积分数
</div>
</a-form-item>
<a-form-item label="延误扣分比例 (%)">
<a-input-number
v-model:value="designerTargetForm.deductionRate"
:min="0"
:max="100"
:precision="1"
style="width: 100%"
placeholder="默认1%"
/>
<div style="font-size: 12px; color: #888; margin-top: 4px;">
每延误1小时,扣除任务积分的百分比。例如:设置为1%,延误2小时,100分任务扣除 2 分
</div>
</a-form-item>
<a-form-item label="生效月份">
<a-date-picker
v-model:value="designerTargetForm.effectiveMonth"
picker="month"
placeholder="留空则为通用配置"
format="YYYY-MM"
value-format="YYYY-MM"
:locale="locale"
:popup-class-name="'dark-date-picker'"
style="width: 100%"
/>
<div style="font-size: 12px; color: #888; margin-top: 4px;">
留空表示通用配置,适用于所有月份
</div>
</a-form-item>
</a-form>
</a-modal>
</div>
</div>
</DefaultLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, watch } from 'vue'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import { message, Modal } from 'ant-design-vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
// 设置 dayjs 中文
dayjs.locale('zh-cn')
// 格式化日期时间
const formatDateTime = (dateStr: string | null | undefined): string => {
if (!dateStr) return '--'
return dayjs(dateStr).format('YYYY-MM-DD HH:mm')
}
// Ant Design 中文配置
const locale = zhCN
import Icon from '@/components/Icon.vue'
import { useRouter } from 'vue-router'
import { useSiteSettingsStore } from '@/stores/siteSettings'
import { useConfigStore } from '@/stores/config'
import { useNavbarConfigStore, type NavbarConfig } from '@/stores/navbarConfig'
import {
getDemandTypes, createDemandType, updateDemandType, deleteDemandType,
getBrands, createBrand, updateBrand, deleteBrand,
getPlatforms, createPlatform, updatePlatform, deletePlatform,
getCategories, createCategory, updateCategory, deleteCategory,
getAllTaskSubTypes, createTaskSubType, updateTaskSubType, deleteTaskSubType, toggleTaskSubTypeEnabled,
type DemandType, type Brand, type Platform, type Category, type TeamScorePool, type TaskSubType
} from '@/api/config'
import {
fetchDepartmentOverview,
createDepartment as createDepartmentApi,
updateDepartment as updateDepartmentApi,
deleteDepartment as deleteDepartmentApi,
updateLimitSettings,
adjustDepartmentScore,
getDepartmentScorePool,
setDeptResponsibleBrands,
getDeptResponsibleBrandIds,
updateAutoGrantConfig,
getGrantRecords,
manualGrant,
type DepartmentOverview,
type DepartmentPayload,
type ScoreAdjustPayload,
type DeptType
} from '@/api/department'
import {
getSystemConfigsByGroup,
getSystemConfig,
getAllSystemConfigs,
batchUpdateSystemConfigs,
createSystemConfig,
updateSystemConfig,
getDesignerTargetScoreList,
saveDesignerTargetScore,
deleteDesignerTargetScore,
getReceivingDepartments,
getWorkHoursConfig,
updateWorkHoursConfig,
type SystemConfig,
type DesignerTargetScore,
type WorkHoursConfig
} from '@/api/system'
import {
getAllUsers,
updateUserDepartment,
deleteUser,
setAdmin,
type User
} from '@/api/user'
import {
getAllS3Configs,
createS3Config,
updateS3Config,
deleteS3Config,
activateS3Config,
type S3Config
} from '@/api/s3'
import {
getAllConfigs as getAllThirdPartyDbConfigs,
createConfig as createThirdPartyDbConfig,
updateConfig as updateThirdPartyDbConfig,
deleteConfig as deleteThirdPartyDbConfig,
testConnection as testThirdPartyDbConnection,
type ThirdPartyDbConfig
} from '@/api/thirdPartyDb'
import { deleteTaskByTaskNo } from '@/api/task'
import request from '@/utils/request'
type LimitPeriod = 'DAILY' | 'WEEKLY' | 'MONTHLY'
interface DepartmentForm {
id?: number
deptName: string
deptCode?: string
deptType: DeptType
parentId?: number | null
leaderId?: number | null | undefined
description?: string
sortOrder?: number
status?: number
allowedDemandTypes: string[]
limitEnabled: boolean
limitPeriod: LimitPeriod
periodLimit?: number
alertThreshold?: number
overLimitAction: 'alert' | 'block'
initialScoreBalance?: number
responsibleBrandIds?: number[] // 负责的品牌ID列表(仅需求部门)
// 积分自动发放配置
autoGrantEnabled?: boolean
autoGrantPeriod?: 'WEEKLY' | 'MONTHLY'
autoGrantAmount?: number
autoGrantDay?: number
expireUnused?: boolean
}
const activeTab = ref('demand-types')
const editingItem = ref<any>(null)
const savingSiteSettings = ref(false)
const loadingSiteSettings = ref(false)
// 全局配置&站点设置 store
const configStore = useConfigStore()
const siteSettingsStore = useSiteSettingsStore()
const navbarConfigStore = useNavbarConfigStore()
// 网站设置
const router = useRouter()
const siteSettings = reactive({
siteName: 'DesignHub',
logoUrl: '',
siteUrl: '',
registerEnabled: true,
siteNameConfigId: null as number | null,
logoConfigId: null as number | null,
siteUrlConfigId: null as number | null,
registerEnabledConfigId: null as number | null
})
// 工作时间设置
const workHoursSettings = reactive({
startTime: '09:00' as string | null,
endTime: '18:00' as string | null,
workHoursPerDay: 0
})
const savingWorkHours = ref(false)
const loadingWorkHours = ref(false)
const loading = reactive({
demandTypes: false,
taskSubTypes: false,
brands: false,
platforms: false,
categories: false,
departments: false
})
// 同步钉钉组织架构状态
const syncingOrganization = ref(false)
const modals = reactive({
demandType: false,
taskSubType: false,
brand: false,
platform: false,
category: false,
department: false,
scorePool: false,
s3Config: false,
thirdPartyDb: false
})
const demandTypes = ref<DemandType[]>([])
const taskSubTypes = ref<TaskSubType[]>([])
const brands = ref<Brand[]>([])
const platforms = ref<Platform[]>([])
const categories = ref<Category[]>([])
const departments = ref<DepartmentOverview[]>([])
const departmentSearchText = ref<string>('')
const currentScorePool = ref<TeamScorePool | null>(null)
const currentDept = ref<DepartmentOverview | null>(null)
const scoreAdjustForm = reactive<ScoreAdjustPayload>({
action: 'recharge',
amount: 0,
remark: ''
})
const rechargeAmount = ref<number>(0)
// 积分发放记录相关
const grantRecords = ref<any[]>([])
const loadingGrantRecords = ref(false)
const manualGrantLoading = ref(false)
const grantRecordColumns = [
{ title: '发放类型', key: 'grantType', dataIndex: 'grantType', width: 100 },
{ title: '发放/过期积分', key: 'amounts', width: 150 },
{ title: '发放前余额', dataIndex: 'balanceBefore', width: 100 },
{ title: '发放后余额', dataIndex: 'balanceAfter', width: 100 },
{ title: '备注', dataIndex: 'remark', ellipsis: true },
{ title: '时间', key: 'createTime', dataIndex: 'createTime', width: 160 }
]
const userLoading = ref(false)
const usersLoaded = ref(false)
const usersByDept = ref<Record<number, User[]>>({})
const transferModalVisible = ref(false)
const selectedUser = ref<User | null>(null)
const transferDeptId = ref<number | null>(null)
const allUsers = ref<User[]>([])
// S3配置相关
const s3Configs = ref<S3Config[]>([])
const loadingS3Configs = ref(false)
const s3ConfigForm = reactive<S3Config>({
endpoint: '',
bucket: '',
accessKeyId: '',
secretAccessKey: '',
region: 'us-east-1',
publicDomain: '',
enablePathStyle: true,
filePath: '',
isActive: false
})
const s3ConfigColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '服务端点', dataIndex: 'endpoint', key: 'endpoint' },
{ title: '存储桶', dataIndex: 'bucket', key: 'bucket' },
{ title: '区域', dataIndex: 'region', key: 'region', width: 120 },
{ title: '访问密钥ID', dataIndex: 'accessKeyId', key: 'accessKeyId' },
{ title: '访问密钥', dataIndex: 'secretAccessKey', key: 'secretAccessKey' },
{ title: '文件路径前缀', dataIndex: 'filePath', key: 'filePath' },
{ title: '状态', key: 'isActive', width: 100 },
{ title: '操作', key: 'actions', width: 200 }
]
// 任务数据管理相关
const taskDeleteForm = reactive({
taskNo: ''
})
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) {
message.warning('请输入任务编号')
return
}
deletingTask.value = true
try {
const response = await deleteTaskByTaskNo(taskDeleteForm.taskNo.trim())
if (response.code === 200) {
message.success('任务删除成功')
taskDeleteForm.taskNo = ''
} else {
message.error(response.message || '删除失败')
}
} catch (error: any) {
message.error(error.response?.data?.message || error.message || '删除失败')
} finally {
deletingTask.value = false
}
}
// 第三方数据库配置相关
const thirdPartyDbConfigs = ref<ThirdPartyDbConfig[]>([])
const loadingThirdPartyDb = ref(false)
const thirdPartyDbForm = reactive<ThirdPartyDbConfig & { enabled: boolean }>({
name: '',
dbType: 'sqlserver',
host: '',
port: 1433,
database: '',
username: '',
password: '',
enabled: true,
connectionTimeout: 10,
remark: ''
})
const thirdPartyDbColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 60 },
{ title: '配置名称', dataIndex: 'name', key: 'name' },
{ title: '数据库类型', dataIndex: 'dbType', key: 'dbType', width: 120 },
{ title: '连接信息', key: 'connection' },
{ title: '用户名', dataIndex: 'username', key: 'username', width: 100 },
{ title: '密码', key: 'password', width: 100 },
{ title: '状态', key: 'enabled', width: 100 },
{ title: '操作', key: 'actions', width: 260 }
]
// 设计师目标积分配置相关
const designerTargetList = ref<any[]>([])
const loadingDesignerTarget = ref(false)
const targetScoreDepartments = ref<any[]>([])
const designerTargetFilter = reactive({
deptId: undefined as number | undefined,
month: undefined as string | undefined,
monthValue: undefined as any
})
const designerTargetForm = reactive<DesignerTargetScore>({
deptId: 0,
userId: undefined,
targetScore: 0,
effectiveMonth: undefined,
deductionRate: 1
})
const designerTargetModalVisible = ref(false)
const designerTargetModalTitle = ref('添加目标积分配置')
// 每日进度检查配置
const progressCheckConfig = reactive({
enabled: true,
checkHour: 18,
deductionScore: 1,
notifyEnabled: true
})
const designerTargetColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 60 },
{ title: '部门', dataIndex: 'deptName', key: 'deptName' },
{ title: '适用对象', dataIndex: 'userName', key: 'userName' },
{ title: '目标积分', key: 'targetScore', width: 100 },
{ title: '延误扣分比例', key: 'deductionRate', width: 120 },
{ title: '生效月份', key: 'effectiveMonth', width: 120 },
{ title: '操作', key: 'actions', width: 150 }
]
const defaultDepartmentForm = (): DepartmentForm => ({
deptName: '',
deptCode: '',
deptType: 'demand',
parentId: 0,
leaderId: undefined,
description: '',
sortOrder: 0,
status: 1,
allowedDemandTypes: [],
limitEnabled: true,
limitPeriod: 'MONTHLY',
periodLimit: 500,
alertThreshold: 80,
overLimitAction: 'alert',
initialScoreBalance: 0,
responsibleBrandIds: [],
// 积分自动发放配置
autoGrantEnabled: false,
autoGrantPeriod: 'WEEKLY',
autoGrantAmount: 500,
autoGrantDay: 1,
expireUnused: true
})
const formData = reactive({
demandType: {} as Partial<DemandType> & { departmentIds?: number[], addLevel?: string },
taskSubType: {} as Partial<TaskSubType>,
brand: {} as Partial<Brand>,
platform: {} as Partial<Platform>,
category: {} as Partial<Category>,
department: defaultDepartmentForm()
})
const departmentUserCards = computed(() => {
return departments.value.map((dept) => ({
...dept,
users: (usersByDept.value[dept.id || 0] || []).slice().sort((a, b) => {
const nameA = a.realName || a.username || ''
const nameB = b.realName || b.username || ''
return nameA.localeCompare(nameB)
})
}))
})
const unassignedUsers = computed(() => {
const list = usersByDept.value[0] || []
return list.slice().sort((a, b) => {
const nameA = a.realName || a.username || ''
const nameB = b.realName || b.username || ''
return nameA.localeCompare(nameB)
})
})
// 过滤出当前部门的用户(用于负责人选择)
const departmentUsers = computed(() => {
// 部门负责人应该可以选择任何用户,不限制于该部门的成员
// 返回所有用户的排序列表
return allUsers.value.slice().sort((a, b) => {
const nameA = a.realName || a.username || ''
const nameB = b.realName || b.username || ''
return nameA.localeCompare(nameB)
})
})
// 过滤后的部门列表(用于搜索)
const filteredDepartments = computed(() => {
if (!departmentSearchText.value || !departmentSearchText.value.trim()) {
return departments.value
}
const searchText = departmentSearchText.value.trim().toLowerCase()
return departments.value.filter(dept => {
// 搜索部门名称
if (dept.deptName && dept.deptName.toLowerCase().includes(searchText)) {
return true
}
// 搜索部门编码
if (dept.deptCode && dept.deptCode.toLowerCase().includes(searchText)) {
return true
}
// 搜索负责人名称
if (dept.leaderName && dept.leaderName.toLowerCase().includes(searchText)) {
return true
}
return false
})
})
const hasUserData = computed(() => {
return unassignedUsers.value.length > 0 || departmentUserCards.value.some((dept) => dept.users.length > 0)
})
// 用户管理表格列定义
const userManagementColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '用户信息', key: 'userInfo', width: 280 },
{ title: '所属部门', key: 'department', width: 180 },
{ title: '管理员', key: 'isAdmin', width: 100 },
{ title: '状态', key: 'status', width: 100 },
{ title: '创建时间', key: 'createTime', width: 160 },
{ title: '操作', key: 'actions', width: 320, fixed: 'right' }
]
// 所有用户的扁平列表(用于表格展示)
const allUsersList = computed(() => {
const allUsers: any[] = []
// 添加未分配部门的用户
const unassigned = unassignedUsers.value.map(user => ({
...user,
deptName: null,
deptType: null
}))
allUsers.push(...unassigned)
// 添加各部门的用户
departmentUserCards.value.forEach(dept => {
const deptUsers = dept.users.map(user => ({
...user,
deptName: dept.deptName,
deptType: dept.deptType
}))
allUsers.push(...deptUsers)
})
// 按用户名排序
return allUsers.sort((a, b) => {
const nameA = a.realName || a.username || ''
const nameB = b.realName || b.username || ''
return nameA.localeCompare(nameB)
})
})
// 格式化时间
const formatUserTime = (time: string | undefined) => {
if (!time) return '--'
return time.replace('T', ' ').substring(0, 16)
}
// 接单部门列表(用于需求类型的部门选择)
const receivingDepartments = computed(() => {
return departments.value.filter(dept => dept.deptType === 'receiving')
})
// 品牌选项(用于需求部门的负责品牌选择)
const brandOptions = computed(() => {
return brands.value.map(brand => ({
label: brand.name || brand.brandName,
value: brand.id
}))
})
// 品牌搜索过滤
const filterBrandOption = (input: string, option: any) => {
return option.label?.toLowerCase().includes(input.toLowerCase())
}
// 一级分类列表(parentId为null的需求类型)
const level1DemandTypes = computed(() => {
return demandTypes.value.filter(dt => !dt.parentId)
})
// 二级分类列表(父级是一级分类的)
const level2DemandTypes = computed(() => {
const level1Ids = level1DemandTypes.value.map(dt => dt.id)
return demandTypes.value.filter(dt => dt.parentId && level1Ids.includes(dt.parentId))
})
// 三级分类列表(父级是二级分类的)
const level3DemandTypes = computed(() => {
const level2Ids = level2DemandTypes.value.map(dt => dt.id)
return demandTypes.value.filter(dt => dt.parentId && level2Ids.includes(dt.parentId))
})
// 兼容旧代码
const parentDemandTypes = level2DemandTypes
// 构建树形结构数据
const demandTypesTree = computed(() => {
// 获取所有一级分类
const level1 = demandTypes.value.filter(dt => !dt.parentId)
// 为每个一级分类添加子节点
return level1.map(l1 => {
// 获取该一级分类下的二级分类
const level2Children = demandTypes.value.filter(dt => dt.parentId === l1.id)
// 为每个二级分类添加三级子节点
const l1WithChildren = {
...l1,
children: level2Children.map(l2 => {
// 获取该二级分类下的三级分类
const level3Children = demandTypes.value.filter(dt => dt.parentId === l2.id)
return {
...l2,
children: level3Children.length > 0 ? level3Children : undefined
}
})
}
// 如果没有子节点,不设置children属性
if (l1WithChildren.children.length === 0) {
delete l1WithChildren.children
}
return l1WithChildren
})
})
// 获取需求类型的层级
const getDemandTypeLevel = (record: DemandType): number => {
if (!record.parentId) return 1
const parent = demandTypes.value.find(dt => dt.id === record.parentId)
if (!parent) return 1
if (!parent.parentId) return 2
return 3
}
// 构建部门树形结构数据
const departmentsTree = computed(() => {
// 获取所有一级部门(没有parentId的)
const level1 = departments.value.filter(d => !d.parentId)
// 为每个一级部门添加子节点
return level1.map(l1 => {
// 获取该一级部门下的二级部门
const level2Children = departments.value.filter(d => d.parentId === l1.id)
// 为每个二级部门添加三级子节点
const l1WithChildren = {
...l1,
children: level2Children.map(l2 => {
// 获取该二级部门下的三级部门
const level3Children = departments.value.filter(d => d.parentId === l2.id)
return {
...l2,
children: level3Children.length > 0 ? level3Children : undefined
}
})
}
// 如果没有子节点,不设置children属性
if (l1WithChildren.children.length === 0) {
delete l1WithChildren.children
}
return l1WithChildren
})
})
// 过滤后的部门树(支持搜索)
const filteredDepartmentsTree = computed(() => {
if (!departmentSearchText.value) {
return departmentsTree.value
}
const searchText = departmentSearchText.value.toLowerCase()
// 递归过滤函数
const filterTree = (nodes: any[]): any[] => {
return nodes.reduce((acc, node) => {
const nameMatch = node.deptName?.toLowerCase().includes(searchText)
const codeMatch = node.deptCode?.toLowerCase().includes(searchText)
const leaderMatch = node.leaderName?.toLowerCase().includes(searchText)
// 递归过滤子节点
const filteredChildren = node.children ? filterTree(node.children) : []
// 如果当前节点匹配,或者有匹配的子节点,则保留
if (nameMatch || codeMatch || leaderMatch || filteredChildren.length > 0) {
acc.push({
...node,
children: filteredChildren.length > 0 ? filteredChildren : node.children
})
}
return acc
}, [] as any[])
}
return filterTree(departmentsTree.value)
})
// 获取部门的层级
const getDepartmentLevel = (record: DepartmentOverview): number => {
if (!record.parentId) return 1
const parent = departments.value.find(d => d.id === record.parentId)
if (!parent) return 1
if (!parent.parentId) return 2
return 3
}
// 当前部门的积分管理层级
const currentDeptScoreLevel = computed(() => {
if (!currentDept.value) return 0
return getDepartmentLevel(currentDept.value)
})
// 积分池弹窗标题
const scorePoolModalTitle = computed(() => {
if (!currentDept.value) return '积分池管理'
const level = currentDeptScoreLevel.value
if (level === 1 && hasChildDepartments(currentDept.value)) {
return '积分池管理 - 汇总视图'
} else if (level === 3) {
return '积分池管理 - 共享视图'
}
return '积分池管理'
})
// 判断部门是否有子部门
const hasChildDepartments = (dept: DepartmentOverview | null): boolean => {
if (!dept?.id) return false
return departments.value.some(d => d.parentId === dept.id)
}
// 获取父部门名称
const getParentDeptName = (dept: DepartmentOverview | null): string => {
if (!dept?.parentId) return ''
const parent = departments.value.find(d => d.id === dept.parentId)
return parent?.deptName || ''
}
// 获取父部门(二级部门)的积分信息
const getParentDeptScoreInfo = () => {
if (!currentDept.value?.parentId) return null
const parent = departments.value.find(d => d.id === currentDept.value?.parentId)
return parent || null
}
// 是否可以管理积分(充值等操作)
const canManageScore = computed(() => {
if (!currentDept.value) return false
const level = currentDeptScoreLevel.value
// 三级部门不能管理积分
if (level === 3) return false
// 一级部门:如果有子部门,则不能直接管理(显示汇总)
if (level === 1 && hasChildDepartments(currentDept.value)) return false
// 二级部门可以管理
// 没有子部门的一级部门也可以管理
return true
})
// 获取显示的积分余额(考虑层级继承)
const getDisplayScoreBalance = (): number => {
if (!currentDept.value) return 0
const level = currentDeptScoreLevel.value
// 三级部门:显示父部门(二级)的积分
if (level === 3) {
const parent = getParentDeptScoreInfo()
return parent?.scoreBalance || 0
}
// 一级部门且有子部门:计算所有二级部门的积分汇总
if (level === 1 && hasChildDepartments(currentDept.value)) {
const childDepts = departments.value.filter(d => d.parentId === currentDept.value?.id)
return childDepts.reduce((sum, d) => sum + (d.scoreBalance || 0), 0)
}
// 其他情况:显示本部门积分
return currentScorePool.value?.balance || 0
}
// 获取显示的累计充值(考虑层级继承)
const getDisplayTotalRecharge = (): number => {
if (!currentDept.value) return 0
const level = currentDeptScoreLevel.value
// 三级部门:显示父部门的数据
if (level === 3) {
const parent = getParentDeptScoreInfo()
return parent?.totalRecharge || 0
}
// 一级部门且有子部门:计算汇总
if (level === 1 && hasChildDepartments(currentDept.value)) {
const childDepts = departments.value.filter(d => d.parentId === currentDept.value?.id)
return childDepts.reduce((sum, d) => sum + (d.totalRecharge || 0), 0)
}
return currentScorePool.value?.totalRecharge || 0
}
// 获取显示的累计消耗(考虑层级继承)
const getDisplayTotalConsume = (): number => {
if (!currentDept.value) return 0
const level = currentDeptScoreLevel.value
// 三级部门:显示父部门的数据
if (level === 3) {
const parent = getParentDeptScoreInfo()
return parent?.totalConsume || 0
}
// 一级部门且有子部门:计算汇总
if (level === 1 && hasChildDepartments(currentDept.value)) {
const childDepts = departments.value.filter(d => d.parentId === currentDept.value?.id)
return childDepts.reduce((sum, d) => sum + (d.totalConsume || 0), 0)
}
return currentScorePool.value?.totalConsume || 0
}
// 获取子部门积分列表(用于一级部门汇总视图)
const getChildDepartmentScores = () => {
if (!currentDept.value?.id) return []
return departments.value
.filter(d => d.parentId === currentDept.value?.id)
.map(d => ({
...d,
id: d.id,
deptName: d.deptName,
scoreBalance: d.scoreBalance || 0,
totalRecharge: d.totalRecharge || 0,
totalConsume: d.totalConsume || 0
}))
}
// 子部门积分明细表格列
const childDeptScoreColumns = [
{ title: '二级部门', dataIndex: 'deptName', key: 'deptName' },
{ title: '当前余额', dataIndex: 'scoreBalance', key: 'scoreBalance' },
{ title: '累计充值', dataIndex: 'totalRecharge', key: 'totalRecharge' },
{ title: '累计消耗', dataIndex: 'totalConsume', key: 'totalConsume' },
{ title: '操作', key: 'actions', width: 100 }
]
// 获取一级部门的汇总积分(所有二级部门积分之和)
const getLevel1DeptTotalScore = (dept: DepartmentOverview): number => {
if (!dept.id) return 0
const childDepts = departments.value.filter(d => d.parentId === dept.id)
return childDepts.reduce((sum, d) => sum + (d.scoreBalance || 0), 0)
}
// 获取一级部门的汇总充值
const getLevel1DeptTotalRecharge = (dept: DepartmentOverview): number => {
if (!dept.id) return 0
const childDepts = departments.value.filter(d => d.parentId === dept.id)
return childDepts.reduce((sum, d) => sum + (d.totalRecharge || 0), 0)
}
// 获取一级部门的汇总消耗
const getLevel1DeptTotalConsume = (dept: DepartmentOverview): number => {
if (!dept.id) return 0
const childDepts = departments.value.filter(d => d.parentId === dept.id)
return childDepts.reduce((sum, d) => sum + (d.totalConsume || 0), 0)
}
// 获取父部门(二级部门)的积分(用于三级部门显示)
const getParentDeptScore = (dept: DepartmentOverview): number => {
if (!dept.parentId) return 0
const parent = departments.value.find(d => d.id === dept.parentId)
return parent?.scoreBalance || 0
}
// 根据ID获取父部门名称
const getParentDeptNameById = (parentId: number | null | undefined): string => {
if (!parentId) return ''
const parent = departments.value.find(d => d.id === parentId)
return parent?.deptName || ''
}
// 部门树形选择器数据 - 用于调整用户部门
const departmentTreeSelectData = computed(() => {
// 构建树形结构
const buildTree = (parentId: number | null): any[] => {
return departments.value
.filter(d => (d.parentId || null) === parentId)
.map(d => {
const children = buildTree(d.id!)
const node: any = {
title: `${d.deptName} (${d.deptType === 'demand' ? '需求' : '接单'})`,
value: d.id,
key: d.id
}
if (children.length > 0) {
node.children = children
}
return node
})
}
// 添加"未分配部门"选项
return [
{ title: '未分配部门', value: 0, key: 0 },
...buildTree(null)
]
})
// 过滤部门树节点
const filterDeptTreeNode = (inputValue: string, treeNode: any) => {
return treeNode.title.toLowerCase().includes(inputValue.toLowerCase())
}
// 上级部门选项(分层级)- 用于部门编辑弹窗
const parentDepartmentOptions = computed(() => {
// 排除当前正在编辑的部门及其子部门
const excludeIds = new Set<number>()
if (editingItem.value?.id) {
excludeIds.add(editingItem.value.id)
// 递归获取所有子部门ID
const getChildIds = (parentId: number) => {
departments.value.filter(d => d.parentId === parentId).forEach(d => {
if (d.id) {
excludeIds.add(d.id)
getChildIds(d.id)
}
})
}
getChildIds(editingItem.value.id)
}
const filteredDepts = departments.value.filter(d => d.id && !excludeIds.has(d.id))
// 一级部门(没有parentId)
const level1 = filteredDepts.filter(d => !d.parentId)
// 二级部门(parentId指向一级部门)
const level1Ids = new Set(level1.map(d => d.id))
const level2 = filteredDepts.filter(d => d.parentId && level1Ids.has(d.parentId)).map(d => {
const parent = level1.find(l1 => l1.id === d.parentId)
return {
...d,
parentName: parent?.deptName || ''
}
})
return { level1, level2 }
})
// 扁平化的部门选项(带层级显示)
const flatDepartmentOptions = computed(() => {
const receiving = departments.value.filter(dept => dept.deptType === 'receiving')
// 按层级分组
const level1 = receiving.filter(d => !d.parentId)
const groups: { label: string, options: { id: number, displayName: string }[] }[] = []
// 添加一级部门组
if (level1.length > 0) {
groups.push({
label: '一级部门',
options: level1.map(d => ({
id: d.id!,
displayName: d.deptName
}))
})
}
// 添加二级部门组
const level2 = receiving.filter(d => d.parentId && level1.some(l1 => l1.id === d.parentId))
if (level2.length > 0) {
groups.push({
label: '二级部门',
options: level2.map(d => {
const parent = level1.find(l1 => l1.id === d.parentId)
return {
id: d.id!,
displayName: parent ? `${parent.deptName} / ${d.deptName}` : d.deptName
}
})
})
}
// 添加三级部门组
const level3 = receiving.filter(d => d.parentId && level2.some(l2 => l2.id === d.parentId))
if (level3.length > 0) {
groups.push({
label: '三级部门',
options: level3.map(d => {
const parent2 = level2.find(l2 => l2.id === d.parentId)
const parent1 = parent2 ? level1.find(l1 => l1.id === parent2.parentId) : null
const path = [parent1?.deptName, parent2?.deptName, d.deptName].filter(Boolean).join(' / ')
return {
id: d.id!,
displayName: path
}
})
})
}
return groups
})
// 根据部门ID字符串获取部门名称
const getDeptNamesFromIds = (idsStr: string) => {
if (!idsStr) return ''
const ids = idsStr.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id))
const names = ids.map(id => {
const dept = departments.value.find(d => d.id === id)
return dept?.deptName || `ID:${id}`
})
return names.join(', ')
}
// 获取需求类型弹窗标题
const getDemandTypeModalTitle = () => {
if (editingItem.value) {
const level = getDemandTypeLevel(editingItem.value as DemandType)
if (level === 1) return '编辑一级分类'
if (level === 2) return '编辑二级分类'
return '编辑三级分类'
}
if (formData.demandType.addLevel === 'level1') return '添加一级分类'
if (formData.demandType.addLevel === 'level2') return '添加二级分类'
return '添加三级分类'
}
// 获取二级分类的显示名称(包含一级分类)
const getLevel2DisplayName = (level2: DemandType) => {
const level1 = level1DemandTypes.value.find(dt => dt.id === level2.parentId)
return level1 ? `${level1.name} / ${level2.name}` : level2.name
}
// 级别变更时重置父级ID
const onLevelChange = () => {
formData.demandType.parentId = null
// 如果改为一级分类,清除部门设置
if (formData.demandType.addLevel === 'level1') {
formData.demandType.departmentIds = []
formData.demandType.unitScore = 0
formData.demandType.estimatedHours = null
}
// 如果改为二级分类,清除分值
if (formData.demandType.addLevel === 'level2') {
formData.demandType.unitScore = 0
formData.demandType.estimatedHours = null
}
}
const leaderOptions = computed(() =>
allUsers.value.map((user) => ({
label: user.realName || user.username || `用户${user.id}`,
value: user.id
}))
)
const limitPeriodOptions = [
{ label: '每日', value: 'DAILY' as LimitPeriod },
{ label: '每周', value: 'WEEKLY' as LimitPeriod },
{ label: '每月', value: 'MONTHLY' as LimitPeriod }
]
const getLimitPeriodLabel = (value?: string) => {
switch (value) {
case 'DAILY':
return '每日'
case 'WEEKLY':
return '每周'
case 'MONTHLY':
return '每月'
default:
return '未配置'
}
}
// 表格列定义
const demandTypeColumns = [
{ title: '类型名称', dataIndex: 'name', key: 'name', width: 280 },
{ title: '级别', key: 'level', width: 70 },
{ title: '单价分值', dataIndex: 'unitScore', key: 'unitScore', width: 100 },
{ title: '预计时长(h)', dataIndex: 'estimatedHours', key: 'estimatedHours', width: 100 },
{ title: '可接单部门', key: 'allowedDepts', ellipsis: true },
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 60 },
{ title: '状态', key: 'status', width: 70 },
{ title: '操作', key: 'actions', width: 200 }
]
const taskSubTypeColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '子类型名称', dataIndex: 'name', key: 'name' },
{ title: '子类型编码', dataIndex: 'code', key: 'code' },
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
{ title: '状态', key: 'status', width: 100 },
{ title: '操作', key: 'actions', width: 150 }
]
const brandColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '品牌名称', dataIndex: 'name', key: 'name' },
{ title: '品牌编码', dataIndex: 'brandCode', key: 'brandCode' },
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
{ title: '状态', key: 'status', width: 100 },
{ title: '操作', key: 'actions', width: 150 }
]
const platformColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '平台名称', dataIndex: 'name', key: 'name' },
{ title: '平台编码', dataIndex: 'platformCode', key: 'platformCode' },
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
{ title: '状态', key: 'status', width: 100 },
{ title: '操作', key: 'actions', width: 150 }
]
const categoryColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '类目名称', dataIndex: 'name', key: 'name' },
{ title: '类目编码', dataIndex: 'categoryCode', key: 'categoryCode' },
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
{ title: '状态', key: 'status', width: 100 },
{ title: '操作', key: 'actions', width: 150 }
]
const departmentColumns = [
{ title: '部门信息', key: 'deptInfo', width: 320 },
{ title: '负责人', key: 'leader', width: 120 },
{ title: '成员数', key: 'memberCount', width: 80 },
{ title: '积分余额', key: 'score', width: 140 },
{ title: '本周期消耗', key: 'limit', width: 140 },
{ title: '预警', key: 'alert', width: 80 },
{ title: '状态', key: 'status', width: 100 },
{ title: '操作', key: 'actions', width: 200 }
]
// 加载数据
const loadAllData = async () => {
await Promise.all([
loadDemandTypes(),
loadTaskSubTypes(),
loadBrands(),
loadPlatforms(),
loadCategories(),
loadDepartments()
])
}
const loadDemandTypes = async () => {
loading.demandTypes = true
try {
const response = await getDemandTypes()
console.log('需求类型API响应:', response)
console.log('需求类型数据:', response.data)
if (response.data && response.data.length > 0) {
console.log('第一条数据:', response.data[0])
}
demandTypes.value = response.data || []
} catch (error: any) {
console.error('加载需求类型失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '加载需求类型失败'
message.error(errorMsg)
} finally {
loading.demandTypes = false
}
}
const loadTaskSubTypes = async () => {
loading.taskSubTypes = true
try {
const response = await getAllTaskSubTypes()
taskSubTypes.value = response.data || []
} catch (error: any) {
console.error('加载任务子类型失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '加载任务子类型失败'
message.error(errorMsg)
} finally {
loading.taskSubTypes = false
}
}
const loadBrands = async () => {
loading.brands = true
try {
const response = await getBrands()
brands.value = response.data || []
} catch (error: any) {
console.error('加载品牌失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '加载品牌失败'
message.error(errorMsg)
} finally {
loading.brands = false
}
}
const loadPlatforms = async () => {
loading.platforms = true
try {
const response = await getPlatforms()
platforms.value = response.data || []
} catch (error: any) {
console.error('加载平台失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '加载平台失败'
message.error(errorMsg)
} finally {
loading.platforms = false
}
}
const loadCategories = async () => {
loading.categories = true
try {
const response = await getCategories()
categories.value = response.data || []
} catch (error: any) {
console.error('加载类目失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '加载类目失败'
message.error(errorMsg)
} finally {
loading.categories = false
}
}
const loadDepartments = async () => {
loading.departments = true
try {
const response = await fetchDepartmentOverview()
departments.value = response.data || []
} catch (error: any) {
console.error('加载部门失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '加载部门失败'
message.error(errorMsg)
} finally {
loading.departments = false
}
}
// 同步钉钉组织架构
const handleSyncOrganization = async () => {
syncingOrganization.value = true
message.loading({ content: '正在同步钉钉组织架构,请稍候...', key: 'sync', duration: 0 })
try {
// 同步操作需要较长时间,设置120秒超时
const response = await request.post('/dingtalk/sync-organization', {}, { timeout: 120000 })
if (response.code === 200) {
const result = response.data as any
let msg = `同步完成!更新${result.updatedUsers}人,跳过${result.skippedUsers}人`
if (result.disabledUsers > 0) {
msg += `,禁用${result.disabledUsers}人(已离职)`
}
if (result.errorUsers > 0) {
msg += `,失败${result.errorUsers}人`
}
message.success({ content: msg, key: 'sync' })
// 如果有错误信息,显示详情
if (result.errors && result.errors.length > 0) {
Modal.warning({
title: '部分用户同步失败',
content: result.errors.slice(0, 10).join('\n') + (result.errors.length > 10 ? `\n...等${result.errors.length}条` : ''),
width: 500
})
}
// 刷新部门和用户数据
await Promise.all([loadDepartments(), loadDepartmentUsers()])
} else {
message.error({ content: response.message || '同步失败', key: 'sync' })
}
} catch (error: any) {
console.error('同步钉钉组织架构失败:', error)
message.error({ content: error?.response?.data?.message || error?.message || '同步失败,请重试', key: 'sync' })
} finally {
syncingOrganization.value = false
}
}
const loadDepartmentUsers = async () => {
userLoading.value = true
try {
const data = await getAllUsers()
if (!Array.isArray(data)) {
throw new Error('返回格式不正确')
}
allUsers.value = data
const grouped: Record<number, User[]> = {}
data.forEach((user) => {
const key = user.deptId || 0
if (!grouped[key]) {
grouped[key] = []
}
grouped[key].push(user)
})
usersByDept.value = grouped
usersLoaded.value = true
} catch (error) {
console.error('加载用户失败:', error)
message.error('加载用户列表失败')
} finally {
userLoading.value = false
}
}
// 过滤用户选项(用于下拉搜索)
const filterUserOption = (input: string, option: any) => {
const user = allUsers.value.find(u => u.id === option.value)
if (!user) return false
const searchText = input.toLowerCase()
const realName = (user.realName || '').toLowerCase()
const username = (user.username || '').toLowerCase()
return realName.includes(searchText) || username.includes(searchText)
}
// 部门搜索处理
const onDepartmentSearch = () => {
// 搜索逻辑通过 computed 属性 filteredDepartments 自动处理
// 此方法仅用于响应搜索事件,实际过滤在 computed 中完成
}
// 显示积分池管理弹窗
const showScorePoolModal = async (dept: DepartmentOverview) => {
if (!dept.id) return
try {
const response = await getDepartmentScorePool(dept.id)
if (response.data) {
currentScorePool.value = response.data
currentDept.value = dept
scoreAdjustForm.action = 'recharge'
scoreAdjustForm.amount = 0
scoreAdjustForm.remark = ''
rechargeAmount.value = 0
modals.scorePool = true
// 加载发放记录
loadGrantRecords(dept.id)
} else {
message.warning('该部门暂无积分池')
}
} catch (error) {
message.error('加载积分池信息失败')
}
}
// 加载发放记录
const loadGrantRecords = async (deptId: number) => {
loadingGrantRecords.value = true
try {
const result = await getGrantRecords(deptId, 50)
grantRecords.value = result?.data || result || []
} catch (error) {
console.error('加载发放记录失败:', error)
grantRecords.value = []
} finally {
loadingGrantRecords.value = false
}
}
// 手动触发发放
const handleManualGrant = async () => {
if (!currentDept.value?.id) return
manualGrantLoading.value = true
try {
await manualGrant(currentDept.value.id)
message.success('手动发放成功')
// 刷新积分池信息和发放记录
await showScorePoolModal(currentDept.value)
await loadDepartments()
} catch (error: any) {
message.error(error?.response?.data?.message || '手动发放失败')
} finally {
manualGrantLoading.value = false
}
}
// 处理积分充值
const handleRecharge = async () => {
if (!currentDept.value?.id) {
message.error('未找到部门信息')
return
}
if (!rechargeAmount.value || rechargeAmount.value <= 0) {
message.error('请输入有效的充值金额')
return
}
try {
await adjustDepartmentScore(currentDept.value.id, {
action: 'recharge',
amount: rechargeAmount.value,
remark: '管理员充值'
})
message.success('充值成功')
// 刷新积分池信息
await showScorePoolModal(currentDept.value)
// 刷新部门列表
await loadDepartments()
} catch (error: any) {
message.error(error?.response?.data?.message || '充值失败')
}
}
// 调整积分
const handleScoreAdjust = async () => {
if (!currentDept.value?.id) {
message.error('未找到部门信息')
return
}
if (!scoreAdjustForm.amount || scoreAdjustForm.amount <= 0) {
message.error('请输入有效的积分数')
return
}
try {
await adjustDepartmentScore(currentDept.value.id, {
action: scoreAdjustForm.action,
amount: scoreAdjustForm.amount,
remark: scoreAdjustForm.remark
})
message.success('操作成功')
await showScorePoolModal(currentDept.value)
await loadDepartments()
} catch (error: any) {
message.error(error?.response?.data?.message || '操作失败')
}
}
// 自动生成编码的辅助函数
const generateCode = (name: string, prefix: string = ''): string => {
if (!name) return ''
// 移除特殊字符,只保留字母、数字和汉字
const cleanName = name.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '')
let code = ''
// 尝试提取英文字母
const englishChars = cleanName.match(/[a-zA-Z]+/g)
if (englishChars && englishChars.length > 0) {
code = englishChars.join('').toUpperCase().substring(0, 8)
}
// 如果没有英文字母,使用时间戳后6位
if (!code) {
code = 'CODE' + Date.now().toString().slice(-6)
}
// 添加前缀
if (prefix) {
code = prefix.toUpperCase() + '_' + code
}
return code
}
// 自动生成品牌编码
const autoGenerateBrandCode = () => {
if (!formData.brand.name) {
message.warning('请先输入品牌名称')
return
}
formData.brand.brandCode = generateCode(formData.brand.name, 'BR')
}
// 自动生成平台编码
const autoGeneratePlatformCode = () => {
if (!formData.platform.name) {
message.warning('请先输入平台名称')
return
}
formData.platform.platformCode = generateCode(formData.platform.name, 'PF')
}
// 自动生成类目编码
const autoGenerateCategoryCode = () => {
if (!formData.category.name) {
message.warning('请先输入类目名称')
return
}
formData.category.categoryCode = generateCode(formData.category.name, 'CAT')
}
// 自动生成任务子类型编码
const autoGenerateTaskSubTypeCode = () => {
if (!formData.taskSubType.name) {
message.warning('请先输入子类型名称')
return
}
// 任务子类型编码使用小写加下划线格式
let code = formData.taskSubType.name
.toLowerCase()
.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '_')
.replace(/_{2,}/g, '_')
.replace(/^_|_$/g, '')
// 如果是中文,简化处理
if (/[\u4e00-\u9fa5]/.test(code)) {
code = 'subtype_' + Date.now().toString().slice(-6)
}
formData.taskSubType.code = code
}
// 显示弹窗
const showDemandTypeModal = async (item?: DemandType | null, type?: string | null, defaultParentId?: number) => {
editingItem.value = item || null
if (item) {
// 编辑模式:将allowedDeptIds(逗号分隔字符串)转换为数组
const departmentIds = item.allowedDeptIds
? item.allowedDeptIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id))
: []
const level = getDemandTypeLevel(item)
formData.demandType = {
...item,
departmentIds,
addLevel: level === 1 ? 'level1' : level === 2 ? 'level2' : 'level3'
}
} else {
// 新建模式
formData.demandType = {
sortOrder: 0,
isEnabled: 1,
departmentIds: [],
addLevel: type || 'level1',
parentId: defaultParentId || null
}
}
modals.demandType = true
}
const showTaskSubTypeModal = (item?: TaskSubType) => {
editingItem.value = item
formData.taskSubType = item
? { ...item, checkInventory: item.checkInventory === 1 }
: { sortOrder: 0, isEnabled: 1, checkInventory: false }
modals.taskSubType = true
}
const showBrandModal = (item?: Brand) => {
editingItem.value = item
formData.brand = item
? { ...item }
: { name: '', brandCode: '', description: '', logoUrl: '', sortOrder: 0, isEnabled: 1 }
modals.brand = true
}
const showPlatformModal = (item?: Platform) => {
editingItem.value = item
formData.platform = item
? { ...item }
: { name: '', platformCode: '', description: '', iconUrl: '', sortOrder: 0, isEnabled: 1 }
modals.platform = true
}
const showCategoryModal = (item?: Category) => {
editingItem.value = item
formData.category = item
? { ...item }
: { name: '', categoryCode: '', description: '', sortOrder: 0, parentId: 0, level: 1, isLeaf: 1, isEnabled: 1 }
modals.category = true
}
const showDepartmentModal = async (item?: DepartmentOverview) => {
if (!usersLoaded.value) {
await loadDepartmentUsers()
}
editingItem.value = item ?? null
if (item) {
// 加载已有的品牌关联
let responsibleBrandIds: number[] = []
if (item.deptType === 'demand' && item.id) {
try {
const brandIdsResult = await getDeptResponsibleBrandIds(item.id)
responsibleBrandIds = brandIdsResult?.data || brandIdsResult || []
} catch (e) {
console.warn('加载品牌关联失败:', e)
}
}
formData.department = {
...defaultDepartmentForm(),
id: item.id,
deptName: item.deptName,
deptCode: item.deptCode,
deptType: item.deptType,
parentId: item.parentId ?? 0,
leaderId: item.leaderId ?? undefined,
description: item.description,
sortOrder: item.sortOrder ?? 0,
status: item.status ?? 1,
allowedDemandTypes: item.allowedDemandTypes ? [...item.allowedDemandTypes] : [],
limitEnabled: (item.limitEnabled ?? 0) === 1,
limitPeriod: (item.limitPeriod as LimitPeriod) || 'MONTHLY',
periodLimit: item.periodLimit ?? undefined,
alertThreshold: item.alertThreshold ?? 80,
overLimitAction: (item.overLimitAction as 'alert' | 'block') || 'alert',
initialScoreBalance: item.scoreBalance ?? 0,
responsibleBrandIds,
// 积分自动发放配置(从积分池获取)
autoGrantEnabled: false,
autoGrantPeriod: 'WEEKLY',
autoGrantAmount: 500,
autoGrantDay: 1,
expireUnused: true
}
// 加载积分池中的自动发放配置
if ((item.deptType === 'demand' || item.deptType === 'primary_demand') && item.id) {
try {
const poolResult = await getDepartmentScorePool(item.id)
const pool = poolResult?.data || poolResult
if (pool) {
formData.department.autoGrantEnabled = pool.autoGrantEnabled === 1
formData.department.autoGrantPeriod = pool.autoGrantPeriod || 'WEEKLY'
formData.department.autoGrantAmount = pool.autoGrantAmount || 500
formData.department.autoGrantDay = pool.autoGrantDay || 1
formData.department.expireUnused = pool.expireUnused === 1
}
} catch (e) {
console.warn('加载积分池配置失败:', e)
}
}
} else {
formData.department = defaultDepartmentForm()
}
modals.department = true
}
// 关闭弹窗
const closeModal = (type: string) => {
modals[type as keyof typeof modals] = false
editingItem.value = null
}
// 保存操作
const saveDemandType = async () => {
try {
const addLevel = formData.demandType.addLevel || 'level1'
// 二级、三级分类必须选择父级
if ((addLevel === 'level2' || addLevel === 'level3') && !formData.demandType.parentId) {
message.error(addLevel === 'level2' ? '请选择所属一级分类' : '请选择所属二级分类')
return
}
// 三级分类必须填写单价分值
if (addLevel === 'level3' && (formData.demandType.unitScore === null || formData.demandType.unitScore === undefined)) {
message.error('请输入单价分值')
return
}
// 构建保存数据,确保字段名正确
const saveData: Partial<DemandType> = {
name: formData.demandType.name,
// 二级和三级分类都可以设置单价(兼容两种数据结构)
unitScore: (addLevel === 'level2' || addLevel === 'level3') ? (formData.demandType.unitScore || 0) : 0,
estimatedHours: (addLevel === 'level2' || addLevel === 'level3') ? formData.demandType.estimatedHours : null,
description: formData.demandType.description,
sortOrder: formData.demandType.sortOrder ?? 0,
isEnabled: formData.demandType.isEnabled ?? 1,
// 父级ID(二级、三级分类需要设置)
parentId: (addLevel === 'level2' || addLevel === 'level3') ? formData.demandType.parentId : null,
// 将部门ID数组转换为逗号分隔的字符串(仅二级分类需要设置)
allowedDeptIds: addLevel === 'level2' && formData.demandType.departmentIds?.length > 0
? formData.demandType.departmentIds.join(',')
: null
}
if (editingItem.value) {
await updateDemandType(editingItem.value.id, saveData)
message.success('更新成功')
} else {
await createDemandType(saveData)
message.success('创建成功')
}
closeModal('demandType')
await loadDemandTypes()
// 刷新全局配置store,通知其他页面更新
await configStore.refreshConfigs('demandType')
} catch (error: any) {
console.error('保存需求类型失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '保存失败'
message.error(errorMsg)
}
}
const saveTaskSubType = async () => {
try {
const saveData: Partial<TaskSubType> & { checkInventory?: number } = {
name: formData.taskSubType.name,
code: formData.taskSubType.code,
description: formData.taskSubType.description,
sortOrder: formData.taskSubType.sortOrder ?? 0,
isEnabled: formData.taskSubType.isEnabled ?? 1,
checkInventory: (formData.taskSubType as any).checkInventory ? 1 : 0
}
if (editingItem.value) {
await updateTaskSubType(editingItem.value.id, saveData)
message.success('更新成功')
} else {
await createTaskSubType(saveData)
message.success('创建成功')
}
closeModal('taskSubType')
await loadTaskSubTypes()
} catch (error: any) {
console.error('保存任务子类型失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '保存失败'
message.error(errorMsg)
}
}
const saveBrand = async () => {
try {
const saveData: Partial<Brand> = {
name: formData.brand.name,
brandCode: formData.brand.brandCode,
description: formData.brand.description,
logoUrl: formData.brand.logoUrl,
sortOrder: formData.brand.sortOrder ?? 0,
isEnabled: formData.brand.isEnabled ?? 1
}
if (editingItem.value) {
await updateBrand(editingItem.value.id, saveData)
message.success('更新成功')
} else {
await createBrand(saveData)
message.success('创建成功')
}
closeModal('brand')
await loadBrands()
// 刷新全局配置store,通知其他页面更新
await configStore.refreshConfigs('brand')
} catch (error: any) {
console.error('保存品牌失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '保存失败'
message.error(errorMsg)
}
}
const savePlatform = async () => {
try {
const saveData: Partial<Platform> = {
name: formData.platform.name,
platformCode: formData.platform.platformCode,
description: formData.platform.description,
iconUrl: formData.platform.iconUrl,
sortOrder: formData.platform.sortOrder ?? 0,
isEnabled: formData.platform.isEnabled ?? 1
}
if (editingItem.value) {
await updatePlatform(editingItem.value.id, saveData)
message.success('更新成功')
} else {
await createPlatform(saveData)
message.success('创建成功')
}
closeModal('platform')
await loadPlatforms()
// 刷新全局配置store,通知其他页面更新
await configStore.refreshConfigs('platform')
} catch (error: any) {
console.error('保存平台失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '保存失败'
message.error(errorMsg)
}
}
const saveCategory = async () => {
try {
const saveData: Partial<Category> = {
name: formData.category.name,
categoryCode: formData.category.categoryCode,
description: formData.category.description,
parentId: formData.category.parentId ?? 0,
level: formData.category.level ?? 1,
isLeaf: formData.category.isLeaf ?? 1,
sortOrder: formData.category.sortOrder ?? 0,
isEnabled: formData.category.isEnabled ?? 1
}
if (editingItem.value) {
await updateCategory(editingItem.value.id, saveData)
message.success('更新成功')
} else {
await createCategory(saveData)
message.success('创建成功')
}
closeModal('category')
await loadCategories()
// 刷新全局配置store,通知其他页面更新
await configStore.refreshConfigs('category')
} catch (error: any) {
console.error('保存类目失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '保存失败'
message.error(errorMsg)
}
}
const isDemandDept = computed(() => formData.department.deptType === 'demand' || formData.department.deptType === 'primary_demand')
const isPrimaryDemandDept = computed(() => formData.department.deptType === 'primary_demand')
const isReceivingDept = computed(() => formData.department.deptType === 'receiving')
// Computed属性用于处理leaderId和parentId的null到undefined的转换,以兼容a-select
const departmentLeaderId = computed({
get: () => formData.department.leaderId ?? undefined,
set: (val) => { formData.department.leaderId = val ?? undefined }
})
const departmentParentId = computed({
get: () => formData.department.parentId ?? undefined,
set: (val) => { formData.department.parentId = val ?? undefined }
})
const buildDepartmentPayload = (includeInitial: boolean): DepartmentPayload => {
const payload: DepartmentPayload = {
deptName: formData.department.deptName,
deptCode: formData.department.deptCode,
deptType: formData.department.deptType,
parentId: formData.department.parentId ?? 0,
// leaderId: null/undefined转换为0表示清除(后端会将0转换为null),数字表示设置
leaderId: formData.department.leaderId != null && formData.department.leaderId !== undefined ? formData.department.leaderId : 0,
sortOrder: formData.department.sortOrder,
status: formData.department.status,
allowedDemandTypes: isReceivingDept.value ? formData.department.allowedDemandTypes : [],
limitEnabled: isDemandDept.value ? (formData.department.limitEnabled ? 1 : 0) : undefined,
limitPeriod: isDemandDept.value ? formData.department.limitPeriod : undefined,
periodLimit: isDemandDept.value ? formData.department.periodLimit : undefined,
alertThreshold: isDemandDept.value ? formData.department.alertThreshold : undefined,
overLimitAction: isDemandDept.value ? formData.department.overLimitAction : undefined
}
if (includeInitial && isDemandDept.value) {
payload.initialScoreBalance = formData.department.initialScoreBalance
}
return payload
}
const saveDepartment = async () => {
try {
const isEdit = !!editingItem.value?.id
const payload = buildDepartmentPayload(!isEdit)
let deptId: number
if (isEdit && editingItem.value?.id) {
delete payload.initialScoreBalance
await updateDepartmentApi(editingItem.value.id, payload)
deptId = editingItem.value.id
message.success('更新成功')
} else {
const result = await createDepartmentApi(payload)
deptId = result?.data?.id || result?.id
message.success('创建成功')
}
// 如果是需求部门,保存负责的品牌
if (formData.department.deptType === 'demand' && deptId) {
const brandIds = formData.department.responsibleBrandIds || []
try {
await setDeptResponsibleBrands(deptId, brandIds)
} catch (brandError: any) {
console.error('保存品牌关联失败:', brandError)
message.warning('部门保存成功,但品牌关联保存失败:' + (brandError?.response?.data?.message || brandError?.message))
}
}
// 如果是需求部门或一级需求部门,保存自动发放配置
if ((formData.department.deptType === 'demand' || formData.department.deptType === 'primary_demand') && deptId) {
try {
await updateAutoGrantConfig(deptId, {
autoGrantEnabled: formData.department.autoGrantEnabled ? 1 : 0,
autoGrantPeriod: formData.department.autoGrantPeriod,
autoGrantAmount: formData.department.autoGrantAmount,
autoGrantDay: formData.department.autoGrantDay,
expireUnused: formData.department.expireUnused ? 1 : 0
})
} catch (grantError: any) {
console.error('保存自动发放配置失败:', grantError)
message.warning('部门保存成功,但自动发放配置保存失败:' + (grantError?.response?.data?.message || grantError?.message))
}
}
closeModal('department')
await loadDepartments()
} catch (error: any) {
console.error('保存部门失败:', error)
const errorMsg = error?.response?.data?.message || error?.message || '保存失败'
message.error(errorMsg)
}
}
// 切换状态
const toggleStatus = async (type: string, id: number, checked: boolean) => {
try {
const isEnabled = checked ? 1 : 0
switch (type) {
case 'demandType':
await updateDemandType(id, { isEnabled } as Partial<DemandType>)
await loadDemandTypes()
await configStore.refreshConfigs('demandType')
break
case 'taskSubType':
await toggleTaskSubTypeEnabled(id)
await loadTaskSubTypes()
break
case 'brand':
await updateBrand(id, { isEnabled } as Brand)
await loadBrands()
await configStore.refreshConfigs('brand')
break
case 'platform':
await updatePlatform(id, { isEnabled } as Platform)
await loadPlatforms()
await configStore.refreshConfigs('platform')
break
case 'category':
await updateCategory(id, { isEnabled } as Category)
await loadCategories()
await configStore.refreshConfigs('category')
break
}
message.success('状态更新成功')
} catch (error) {
message.error('状态更新失败')
}
}
const toggleDepartmentStatus = async (id: number, checked: boolean) => {
try {
await updateDepartmentApi(id, { status: checked ? 1 : 0 })
await loadDepartments()
message.success('状态更新成功')
} catch (error: any) {
const errorMsg = error?.response?.data?.message || error?.message || '状态更新失败'
message.error(errorMsg)
}
}
// 删除操作
const deleteItem = async (type: string, id: number) => {
try {
switch (type) {
case 'demandType':
await deleteDemandType(id)
await loadDemandTypes()
break
case 'taskSubType':
await deleteTaskSubType(id)
await loadTaskSubTypes()
break
case 'brand':
await deleteBrand(id)
await loadBrands()
break
case 'platform':
await deletePlatform(id)
await loadPlatforms()
break
case 'category':
await deleteCategory(id)
await loadCategories()
break
}
message.success('删除成功')
} catch (error) {
message.error('删除失败')
}
}
const deleteDepartment = async (id: number) => {
try {
await deleteDepartmentApi(id)
await loadDepartments()
message.success('删除成功')
} catch (error: any) {
const errorMsg = error?.response?.data?.message || error?.message || '删除失败'
message.error(errorMsg)
}
}
const openTransferModal = (user: User) => {
selectedUser.value = user
transferDeptId.value = user.deptId ?? null
transferModalVisible.value = true
}
const closeTransferModal = () => {
transferModalVisible.value = false
selectedUser.value = null
}
const handleTransferDepartment = async () => {
if (!selectedUser.value) return
try {
const normalizedDeptId = transferDeptId.value && transferDeptId.value > 0 ? transferDeptId.value : null
await updateUserDepartment(selectedUser.value.id!, normalizedDeptId)
message.success('部门调整成功')
closeTransferModal()
// 同时刷新用户列表和部门数据(更新成员数)
await Promise.all([loadDepartmentUsers(), loadDepartments()])
} catch (error) {
console.error('调整部门失败:', error)
message.error('调整部门失败')
}
}
const deleteUserFromCard = async (user: User) => {
try {
await deleteUser(user.id!)
message.success('用户已删除')
await loadDepartmentUsers()
} catch (error) {
console.error('删除用户失败:', error)
message.error('删除用户失败')
}
}
const toggleUserAdmin = async (user: User) => {
try {
const newAdminStatus = user.isAdmin === 1 ? 0 : 1
await setAdmin(user.id!, newAdminStatus)
message.success(newAdminStatus === 1 ? '已设置为管理员' : '已取消管理员身份')
await loadDepartmentUsers()
} catch (error) {
console.error('设置管理员失败:', error)
message.error('设置管理员失败')
}
}
const goToUserManagement = () => {
router.push('/users')
}
const goToRewardRules = () => {
router.push('/admin/reward-rules')
}
const goToScorePool = () => {
router.push('/admin/score-pool')
}
// =============== 钉钉集成 ===============
import { getDingTalkConfig, saveDingTalkConfig, testDingTalkConnection, sendTestMessage, sendPersonalTestMessage, syncDingTalkDepartments } from '@/api/dingtalk'
const dingTalkConfig = reactive({
id: undefined as number | undefined,
name: '默认配置',
agentId: '',
appKey: '',
appSecret: '',
corpId: '',
robotWebhook: '',
robotSecret: '',
enabled: 0,
enabledFeatures: {} as Record<string, boolean>
})
const enabledFeaturesList = ref<string[]>([])
const savingDingTalk = ref(false)
const testingDingTalk = ref(false)
const sendingTestMessage = ref(false)
const syncingDepartments = ref(false)
const loadDingTalkConfig = async () => {
try {
const config = await getDingTalkConfig()
if (config) {
Object.assign(dingTalkConfig, config)
// 解析启用的功能
// enabledFeatures在数据库中存储为JSON字符串,需要解析
if (config.enabledFeatures) {
let featuresObj: Record<string, boolean> = {}
if (typeof config.enabledFeatures === 'string') {
// 如果是字符串,需要解析为对象
try {
featuresObj = JSON.parse(config.enabledFeatures)
} catch (e) {
console.error('解析enabledFeatures JSON失败:', e)
featuresObj = {}
}
} else {
// 如果已经是对象,直接使用
featuresObj = config.enabledFeatures as Record<string, boolean>
}
enabledFeaturesList.value = Object.entries(featuresObj)
.filter(([, v]) => v)
.map(([k]) => k)
} else {
enabledFeaturesList.value = []
}
}
} catch (error) {
console.error('加载钉钉配置失败:', error)
}
}
const handleSaveDingTalk = async () => {
savingDingTalk.value = true
try {
// 构建启用功能对象
const enabledFeatures: Record<string, boolean> = {}
enabledFeaturesList.value.forEach(f => {
enabledFeatures[f] = true
})
const configToSave = {
...dingTalkConfig,
enabled: 1,
enabledFeatures
}
await saveDingTalkConfig(configToSave as any)
message.success('钉钉配置保存成功')
// 重新加载配置以确保UI同步
await loadDingTalkConfig()
} catch (error: any) {
message.error(error.message || '保存失败')
} finally {
savingDingTalk.value = false
}
}
const handleTestDingTalk = async () => {
if (!dingTalkConfig.appKey || !dingTalkConfig.appSecret) {
message.warning('请先填写AppKey和AppSecret')
return
}
testingDingTalk.value = true
try {
const result = await testDingTalkConnection(dingTalkConfig as any)
if (result) {
message.success('钉钉连接测试成功!')
} else {
message.error('钉钉连接测试失败,请检查配置')
}
} catch (error: any) {
message.error(error.message || '连接测试失败')
} finally {
testingDingTalk.value = false
}
}
// 测试消息类型
const testMessageType = ref('simple')
// 个人消息测试
const personalMessageUser = ref('')
const personalMessageType = ref('simple')
const sendingPersonalMessage = ref(false)
const handleSendTestMessage = async () => {
if (!dingTalkConfig.robotWebhook) {
message.warning('请先配置机器人Webhook')
return
}
sendingTestMessage.value = true
try {
const result = await sendTestMessage(testMessageType.value)
if (result) {
const typeNames: Record<string, string> = {
'simple': '简单测试',
'task_publish': '新任务发布',
'task_assign': '任务指派',
'task_reject': '任务驳回',
'subtask_publish': '子任务组发布'
}
message.success(`"${typeNames[testMessageType.value]}"消息发送成功!`)
} else {
message.error('测试消息发送失败')
}
} catch (error: any) {
message.error(error.message || '发送失败')
} finally {
sendingTestMessage.value = false
}
}
const handleSyncDepartments = async () => {
if (!dingTalkConfig.appKey || !dingTalkConfig.appSecret) {
message.warning('请先填写AppKey和AppSecret')
return
}
syncingDepartments.value = true
try {
const count = await syncDingTalkDepartments()
message.success(`同步成功!共处理 ${count} 个部门`)
} catch (error: any) {
message.error(error.message || '同步失败')
} finally {
syncingDepartments.value = false
}
}
// 发送个人测试消息
const handleSendPersonalMessage = async () => {
if (!personalMessageUser.value.trim()) {
message.warning('请输入接收人用户名或真实姓名')
return
}
if (!dingTalkConfig.appKey || !dingTalkConfig.appSecret) {
message.warning('请先配置AppKey和AppSecret')
return
}
sendingPersonalMessage.value = true
try {
const result = await sendPersonalTestMessage(personalMessageUser.value.trim(), personalMessageType.value)
if (result.success) {
const typeNames: Record<string, string> = {
'simple': '简单测试',
'task_publish': '新任务发布',
'task_assign': '任务指派',
'task_complete': '任务完成',
'score_change': '积分变动'
}
message.success(`"${typeNames[personalMessageType.value]}"消息已发送给 ${result.userName}!`)
} else {
message.error(result.error || '消息发送失败')
}
} catch (error: any) {
message.error(error.message || '发送失败')
} finally {
sendingPersonalMessage.value = false
}
}
// ==================== 导航栏配置 ====================
const navbarConfig = ref<NavbarConfig>({
menuItems: {
home: true,
tasks: true,
designer_hall: true,
incentive: true,
analytics: true
},
actions: {
notification: true,
checkin: true,
achievement: true,
settings: true
}
})
const savingNavbarConfig = ref(false)
// 加载导航栏配置
const loadNavbarConfig = async () => {
try {
await navbarConfigStore.loadConfig()
navbarConfig.value = { ...navbarConfigStore.config }
} catch (error) {
console.error('加载导航栏配置失败:', error)
}
}
// 保存导航栏配置
const handleSaveNavbarConfig = async () => {
savingNavbarConfig.value = true
try {
await navbarConfigStore.saveConfig(navbarConfig.value)
message.success('导航栏配置保存成功')
} catch (error: any) {
message.error(error.message || '保存失败')
} finally {
savingNavbarConfig.value = false
}
}
// 恢复默认配置
const handleResetNavbarConfig = () => {
Modal.confirm({
title: '确认恢复默认配置?',
content: '此操作将恢复所有导航栏项为显示状态',
onOk: () => {
navbarConfigStore.resetToDefault()
navbarConfig.value = { ...navbarConfigStore.config }
message.success('已恢复默认配置')
}
})
}
// ==================== 任务发布类型配置 ====================
const taskReleaseConfig = ref({
default: true,
recruitment: true,
assigned: true
})
const savingTaskReleaseConfig = ref(false)
// ==================== 一级需求部门配置 ====================
const primaryDeptConfig = ref({
canFillDetail: false, // 一级需求部门是否可以填写任务明细
crossDeptAssign: false // 是否允许跨部门分配成员完善任务明细
})
const savingPrimaryDeptConfig = ref(false)
// ==================== 延误扣分配置 ====================
const delayPenaltyConfig = ref({
perHour: 0.5, // 每小时扣分
monthlyReset: true // 月度积分清零
})
const savingDelayPenaltyConfig = ref(false)
// ==================== 第三方素材API配置 ====================
const externalAssetApiConfig = reactive({
enabled: false,
apiUrl: '',
apiKey: ''
})
const savingExternalAssetApi = ref(false)
const testingExternalAssetApi = ref(false)
// 加载第三方素材API配置
const loadExternalAssetApiConfig = async () => {
try {
const response = await getSystemConfigsByGroup('external_api')
const configs = response.data || []
const urlConfig = configs.find((c: any) => c.configKey === 'external_asset_api_url')
const keyConfig = configs.find((c: any) => c.configKey === 'external_asset_api_key')
const enabledConfig = configs.find((c: any) => c.configKey === 'external_asset_api_enabled')
externalAssetApiConfig.apiUrl = urlConfig?.configValue || ''
externalAssetApiConfig.apiKey = keyConfig?.configValue || ''
externalAssetApiConfig.enabled = enabledConfig?.configValue === 'true'
} catch (error) {
console.error('加载第三方素材API配置失败:', error)
}
}
// 保存第三方素材API配置
const handleSaveExternalAssetApiConfig = async () => {
if (externalAssetApiConfig.enabled) {
if (!externalAssetApiConfig.apiUrl) {
message.error('请输入API地址')
return
}
if (!externalAssetApiConfig.apiKey) {
message.error('请输入API密钥')
return
}
}
savingExternalAssetApi.value = true
try {
await batchUpdateSystemConfigs({
'external_asset_api_url': externalAssetApiConfig.apiUrl,
'external_asset_api_key': externalAssetApiConfig.apiKey,
'external_asset_api_enabled': externalAssetApiConfig.enabled.toString()
})
message.success('第三方素材API配置保存成功')
} catch (error: any) {
message.error(error.message || '保存失败')
} finally {
savingExternalAssetApi.value = false
}
}
// 测试第三方素材API连接
const handleTestExternalAssetApi = async () => {
if (!externalAssetApiConfig.apiUrl || !externalAssetApiConfig.apiKey) {
message.error('请先填写API地址和密钥')
return
}
testingExternalAssetApi.value = true
try {
// 先保存配置
await batchUpdateSystemConfigs({
'external_asset_api_url': externalAssetApiConfig.apiUrl,
'external_asset_api_key': externalAssetApiConfig.apiKey
})
// 调用测试接口
const response = await fetch('/api/external-asset/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
const result = await response.json()
if (result.code === 200 && result.data === true) {
message.success('连接测试成功')
} else {
message.error(result.message || '连接测试失败')
}
} catch (error: any) {
message.error(error.message || '连接测试失败')
} finally {
testingExternalAssetApi.value = false
}
}
// ==================== 任务排队解锁配置 ====================
const taskUnlockConfig = reactive({
enabled: false,
dailyMin: 5,
batchSize: 5
})
const savingTaskUnlockConfig = ref(false)
const loadingTaskUnlockStats = ref(false)
const taskUnlockStats = ref<any>(null)
// 任务排队统计表格列
const taskUnlockStatsColumns = [
{ title: '部门', dataIndex: 'deptName', key: 'deptName' },
{ title: '排队中', dataIndex: 'queuedCount', key: 'queuedCount' },
{ title: '已解锁待接单', dataIndex: 'unlockedPendingCount', key: 'unlockedPendingCount' }
]
// 加载任务排队配置
const loadTaskUnlockConfig = async () => {
try {
const response = await getSystemConfigsByGroup('task')
const configs = response.data || []
const enabledConfig = configs.find((c: any) => c.configKey === 'task_unlock_enabled')
const dailyMinConfig = configs.find((c: any) => c.configKey === 'task_unlock_daily_min')
const batchSizeConfig = configs.find((c: any) => c.configKey === 'task_unlock_batch_size')
taskUnlockConfig.enabled = enabledConfig?.configValue === 'true'
taskUnlockConfig.dailyMin = parseInt(dailyMinConfig?.configValue || '5')
taskUnlockConfig.batchSize = parseInt(batchSizeConfig?.configValue || '5')
} catch (error) {
console.error('加载任务排队配置失败:', error)
}
}
// 保存任务排队配置
const handleSaveTaskUnlockConfig = async () => {
savingTaskUnlockConfig.value = true
try {
await batchUpdateSystemConfigs({
'task_unlock_enabled': taskUnlockConfig.enabled.toString(),
'task_unlock_daily_min': taskUnlockConfig.dailyMin.toString(),
'task_unlock_batch_size': taskUnlockConfig.batchSize.toString()
})
message.success('任务排队配置保存成功')
} catch (error: any) {
message.error(error.message || '保存失败')
} finally {
savingTaskUnlockConfig.value = false
}
}
// 加载任务排队统计
const loadTaskUnlockStats = async () => {
loadingTaskUnlockStats.value = true
try {
const response = await fetch('/api/admin/tasks/unlock/stats', {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
const result = await response.json()
if (result.code === 200) {
taskUnlockStats.value = result.data
} else {
message.error(result.message || '加载统计失败')
}
} catch (error: any) {
console.error('加载任务排队统计失败:', error)
message.error('加载统计失败')
} finally {
loadingTaskUnlockStats.value = false
}
}
// 加载延误扣分配置
const loadDelayPenaltyConfig = async () => {
try {
const response = await getSystemConfigsByGroup('score')
const configs = response.data || []
const perHourConfig = configs.find((c: any) => c.configKey === 'score.delay_penalty_per_hour')
const monthlyResetConfig = configs.find((c: any) => c.configKey === 'score.monthly_reset_enabled')
delayPenaltyConfig.value = {
perHour: perHourConfig ? parseFloat(perHourConfig.configValue) : 0.5,
monthlyReset: monthlyResetConfig?.configValue !== 'false'
}
} catch (error) {
console.error('加载延误扣分配置失败:', error)
}
}
// 保存延误扣分配置
const handleSaveDelayPenaltyConfig = async () => {
savingDelayPenaltyConfig.value = true
try {
await batchUpdateSystemConfigs({
'score.delay_penalty_per_hour': delayPenaltyConfig.value.perHour.toString(),
'score.monthly_reset_enabled': delayPenaltyConfig.value.monthlyReset.toString()
})
message.success('延误扣分配置保存成功')
} catch (error: any) {
message.error(error.message || '保存失败')
} finally {
savingDelayPenaltyConfig.value = false
}
}
// 加载任务发布类型配置
const loadTaskReleaseConfig = async () => {
try {
const response = await getSystemConfigsByGroup('task')
const configs = response.data || []
const defaultConfig = configs.find((c: any) => c.configKey === 'task.release_type.default')
const recruitmentConfig = configs.find((c: any) => c.configKey === 'task.release_type.recruitment')
const assignedConfig = configs.find((c: any) => c.configKey === 'task.release_type.assigned')
taskReleaseConfig.value = {
default: defaultConfig?.configValue !== 'false',
recruitment: recruitmentConfig?.configValue !== 'false',
assigned: assignedConfig?.configValue !== 'false'
}
// 加载一级需求部门配置
const primaryDeptCanFillDetailConfig = configs.find((c: any) => c.configKey === 'primary_dept_can_fill_detail')
const crossDeptAssignConfig = configs.find((c: any) => c.configKey === 'primary_dept_cross_dept_assign')
primaryDeptConfig.value = {
canFillDetail: primaryDeptCanFillDetailConfig?.configValue === 'true',
crossDeptAssign: crossDeptAssignConfig?.configValue === 'true'
}
} catch (error) {
console.error('加载任务发布类型配置失败:', error)
}
}
// 保存任务发布类型配置
const handleSaveTaskReleaseConfig = async () => {
// 检查至少保留一个选项
if (!taskReleaseConfig.value.default && !taskReleaseConfig.value.recruitment && !taskReleaseConfig.value.assigned) {
message.warning('至少需要保留一个发布类型,已自动启用默认模式')
taskReleaseConfig.value.default = true
}
savingTaskReleaseConfig.value = true
try {
await batchUpdateSystemConfigs({
'task.release_type.default': taskReleaseConfig.value.default.toString(),
'task.release_type.recruitment': taskReleaseConfig.value.recruitment.toString(),
'task.release_type.assigned': taskReleaseConfig.value.assigned.toString()
})
message.success('任务发布类型配置保存成功')
} catch (error: any) {
message.error(error.message || '保存失败')
} finally {
savingTaskReleaseConfig.value = false
}
}
// 恢复默认任务发布类型配置
const handleResetTaskReleaseConfig = () => {
Modal.confirm({
title: '确认恢复默认配置?',
content: '此操作将显示所有发布类型选项',
onOk: async () => {
taskReleaseConfig.value = {
default: true,
recruitment: true,
assigned: true
}
await handleSaveTaskReleaseConfig()
}
})
}
// 保存一级需求部门配置
const handleSavePrimaryDeptConfig = async () => {
savingPrimaryDeptConfig.value = true
try {
await batchUpdateSystemConfigs({
'primary_dept_can_fill_detail': primaryDeptConfig.value.canFillDetail.toString(),
'primary_dept_cross_dept_assign': primaryDeptConfig.value.crossDeptAssign.toString()
})
message.success('一级需求部门配置保存成功')
} catch (error: any) {
message.error(error.message || '保存失败')
} finally {
savingPrimaryDeptConfig.value = false
}
}
// 加载网站设置
const loadSiteSettings = async () => {
loadingSiteSettings.value = true
try {
// 先加载到store
await siteSettingsStore.loadSiteSettings()
// 获取网站设置分组的所有配置
const response = await getSystemConfigsByGroup('site')
const configs = response.data || []
// 同时获取system分组的配置(包含site_url)
const systemResponse = await getSystemConfigsByGroup('system')
const systemConfigs = systemResponse.data || []
// 查找网站名称和Logo配置
const siteNameConfig = configs.find((c: any) => c.configKey === 'site.name')
const logoConfig = configs.find((c: any) => c.configKey === 'site.logo')
if (siteNameConfig) {
siteSettings.siteName = siteNameConfig.configValue || 'DesignHub'
siteSettings.siteNameConfigId = siteNameConfig.id
} else {
siteSettings.siteName = siteSettingsStore.siteName
}
if (logoConfig) {
siteSettings.logoUrl = logoConfig.configValue || ''
siteSettings.logoConfigId = logoConfig.id
} else {
siteSettings.logoUrl = siteSettingsStore.logoUrl
}
// 查找官网地址配置(在system分组中)
const siteUrlConfig = systemConfigs.find((c: any) => c.configKey === 'site_url')
if (siteUrlConfig) {
siteSettings.siteUrl = siteUrlConfig.configValue || ''
siteSettings.siteUrlConfigId = siteUrlConfig.id
} else {
siteSettings.siteUrl = ''
}
// 查找注册功能开关配置
const registerConfig = configs.find((c: any) => c.configKey === 'site.register.enabled')
if (registerConfig) {
siteSettings.registerEnabled = registerConfig.configValue === 'true' || registerConfig.configValue === '1'
siteSettings.registerEnabledConfigId = registerConfig.id
} else {
siteSettings.registerEnabled = true // 默认开启
}
} catch (error) {
console.error('加载网站设置失败:', error)
// 如果API失败,尝试从store读取
siteSettings.siteName = siteSettingsStore.siteName
siteSettings.logoUrl = siteSettingsStore.logoUrl
siteSettings.siteUrl = ''
siteSettings.registerEnabled = true // 默认开启
} finally {
loadingSiteSettings.value = false
}
}
// 网站设置相关函数
const handleSiteNameChange = () => {
updateSiteInfo()
}
const handleLogoUpload = (file: File) => {
const reader = new FileReader()
reader.onload = (e) => {
const result = e.target?.result as string
siteSettings.logoUrl = result
updateSiteInfo()
}
reader.readAsDataURL(file)
return false // 阻止自动上传
}
const handleRemoveLogo = () => {
siteSettings.logoUrl = ''
updateSiteInfo()
}
const handleSaveSiteSettings = async () => {
savingSiteSettings.value = true
try {
// 保存网站名称
if (siteSettings.siteNameConfigId) {
// 如果配置已存在,更新
await updateSystemConfig(siteSettings.siteNameConfigId, {
configValue: siteSettings.siteName
})
} else {
// 如果配置不存在,创建
const response = await createSystemConfig({
configKey: 'site.name',
configValue: siteSettings.siteName,
configGroup: 'site',
configName: '网站名称',
description: '网站名称,显示在导航栏、登录页和浏览器标题',
valueType: 'string',
builtIn: 0,
sortOrder: 1
})
if (response.data && response.data.id) {
siteSettings.siteNameConfigId = response.data.id
}
}
// 保存Logo
if (siteSettings.logoConfigId) {
// 如果配置已存在,更新
await updateSystemConfig(siteSettings.logoConfigId, {
configValue: siteSettings.logoUrl
})
} else {
// 如果配置不存在,创建
const response = await createSystemConfig({
configKey: 'site.logo',
configValue: siteSettings.logoUrl,
configGroup: 'site',
configName: '网站Logo',
description: '网站Logo URL,显示在导航栏和登录页',
valueType: 'string',
builtIn: 0,
sortOrder: 2
})
if (response.data && response.data.id) {
siteSettings.logoConfigId = response.data.id
}
}
// 保存注册功能开关
if (siteSettings.registerEnabledConfigId) {
// 如果配置已存在,更新
await updateSystemConfig(siteSettings.registerEnabledConfigId, {
configValue: siteSettings.registerEnabled ? 'true' : 'false'
})
} else {
// 如果配置不存在,创建
const response = await createSystemConfig({
configKey: 'site.register.enabled',
configValue: siteSettings.registerEnabled ? 'true' : 'false',
configGroup: 'site',
configName: '注册功能开关',
description: '控制登录页面是否显示注册功能',
valueType: 'boolean',
builtIn: 0,
sortOrder: 3
})
if (response.data && response.data.id) {
siteSettings.registerEnabledConfigId = response.data.id
}
}
// 保存官网地址
if (siteSettings.siteUrlConfigId) {
// 如果配置已存在,更新
await updateSystemConfig(siteSettings.siteUrlConfigId, {
configValue: siteSettings.siteUrl
})
} else if (siteSettings.siteUrl) {
// 如果配置不存在但有值,创建
const response = await createSystemConfig({
configKey: 'site_url',
configValue: siteSettings.siteUrl,
configGroup: 'system',
configName: '官网地址',
description: '系统官网地址,用于钉钉通知卡片跳转',
valueType: 'string',
builtIn: 1,
sortOrder: 10
})
if (response.data && response.data.id) {
siteSettings.siteUrlConfigId = response.data.id
}
}
// 更新全局store(这会自动更新所有使用store的组件)
siteSettingsStore.updateSiteSettings(siteSettings.siteName, siteSettings.logoUrl)
// 重新加载网站设置,确保页面显示最新数据
await loadSiteSettings()
message.success('设置保存成功')
} catch (error: any) {
console.error('保存网站设置失败:', error)
message.error(error.response?.data?.message || '保存失败')
} finally {
savingSiteSettings.value = false
}
}
// 更新网站信息(同步到登录页、导航栏、浏览器标题)
const updateSiteInfo = () => {
// 更新全局store(这会自动更新所有使用store的组件和浏览器标题)
siteSettingsStore.updateSiteSettings(siteSettings.siteName, siteSettings.logoUrl)
}
// 加载工作时间配置
const loadWorkHours = async () => {
loadingWorkHours.value = true
try {
const response = await getWorkHoursConfig()
if (response.data) {
workHoursSettings.startTime = response.data.startTime
workHoursSettings.endTime = response.data.endTime
workHoursSettings.workHoursPerDay = response.data.workHoursPerDay
}
} catch (error) {
console.error('加载工作时间配置失败:', error)
// 使用默认值
workHoursSettings.startTime = '09:00'
workHoursSettings.endTime = '18:00'
workHoursSettings.workHoursPerDay = 9
} finally {
loadingWorkHours.value = false
}
}
// 保存工作时间配置
const handleSaveWorkHours = async () => {
if (!workHoursSettings.startTime || !workHoursSettings.endTime) {
message.warning('请设置上班开始时间和结束时间')
return
}
// 验证时间格式和逻辑
const startTime = dayjs(workHoursSettings.startTime, 'HH:mm')
const endTime = dayjs(workHoursSettings.endTime, 'HH:mm')
if (!startTime.isValid() || !endTime.isValid()) {
message.error('时间格式错误')
return
}
if (!endTime.isAfter(startTime)) {
message.error('上班结束时间必须晚于开始时间')
return
}
savingWorkHours.value = true
try {
await updateWorkHoursConfig({
startTime: workHoursSettings.startTime,
endTime: workHoursSettings.endTime
})
// 重新加载配置以获取计算后的 workHoursPerDay
await loadWorkHours()
message.success('工作时间配置保存成功')
} catch (error: any) {
console.error('保存工作时间配置失败:', error)
message.error(error.response?.data?.message || '保存失败')
} finally {
savingWorkHours.value = false
}
}
onMounted(() => {
loadAllData()
// 加载网站设置
loadSiteSettings()
// 加载工作时间配置
loadWorkHours()
// 加载钉钉配置
loadDingTalkConfig()
// 加载任务发布类型配置
loadTaskReleaseConfig()
// 加载延误扣分配置
loadDelayPenaltyConfig()
// 加载第三方素材API配置
loadExternalAssetApiConfig()
// 加载任务排队配置
loadTaskUnlockConfig()
// 加载设计师目标积分配置
loadTargetScoreDepartments()
loadDesignerTargetList()
// 加载进度检查配置
loadProgressCheckConfig()
// 加载盲盒模式配置
loadBlindBoxMode()
})
// S3配置相关方法
const loadS3Configs = async () => {
loadingS3Configs.value = true
try {
const data = await getAllS3Configs()
s3Configs.value = Array.isArray(data) ? data : []
} catch (error: any) {
console.error('加载S3配置失败:', error)
message.error(error.response?.data?.message || '加载S3配置失败')
} finally {
loadingS3Configs.value = false
}
}
const showS3ConfigModal = (config?: S3Config) => {
if (config) {
Object.assign(s3ConfigForm, {
...config,
secretAccessKey: config.secretAccessKey === '******' ? '' : config.secretAccessKey
})
} else {
Object.assign(s3ConfigForm, {
endpoint: '',
bucket: '',
accessKeyId: '',
secretAccessKey: '',
region: 'us-east-1',
publicDomain: '',
enablePathStyle: true,
filePath: '',
isActive: false
})
}
modals.s3Config = true
}
const handleSaveS3Config = async () => {
try {
if (s3ConfigForm.id) {
await updateS3Config(s3ConfigForm.id, s3ConfigForm)
message.success('更新S3配置成功')
} else {
await createS3Config(s3ConfigForm)
message.success('创建S3配置成功')
}
modals.s3Config = false
loadS3Configs()
} catch (error: any) {
console.error('保存S3配置失败:', error)
message.error(error.response?.data?.message || '保存S3配置失败')
}
}
const handleDeleteS3Config = async (id: number) => {
try {
await deleteS3Config(id)
message.success('删除S3配置成功')
loadS3Configs()
} catch (error: any) {
console.error('删除S3配置失败:', error)
message.error(error.response?.data?.message || '删除S3配置失败')
}
}
const handleActivateS3Config = async (id: number) => {
try {
await activateS3Config(id)
message.success('激活S3配置成功')
loadS3Configs()
} catch (error: any) {
console.error('激活S3配置失败:', error)
message.error(error.response?.data?.message || '激活S3配置失败')
}
}
// 第三方数据库配置相关方法
const loadThirdPartyDbConfigs = async () => {
loadingThirdPartyDb.value = true
try {
const response = await getAllThirdPartyDbConfigs()
thirdPartyDbConfigs.value = response.data || []
} catch (error: any) {
console.error('加载第三方数据库配置失败:', error)
message.error(error.response?.data?.message || '加载第三方数据库配置失败')
} finally {
loadingThirdPartyDb.value = false
}
}
const showThirdPartyDbModal = (config?: ThirdPartyDbConfig) => {
if (config) {
Object.assign(thirdPartyDbForm, {
...config,
enabled: config.enabled === 1,
password: '' // 编辑时不显示密码
})
} else {
Object.assign(thirdPartyDbForm, {
id: undefined,
name: '',
dbType: 'sqlserver',
host: '',
port: 1433,
database: '',
username: '',
password: '',
enabled: true,
connectionTimeout: 10,
remark: ''
})
}
modals.thirdPartyDb = true
}
const handleSaveThirdPartyDb = async () => {
try {
const data = {
...thirdPartyDbForm,
enabled: thirdPartyDbForm.enabled ? 1 : 0
}
if (thirdPartyDbForm.id) {
await updateThirdPartyDbConfig(thirdPartyDbForm.id, data)
message.success('更新数据库配置成功')
} else {
await createThirdPartyDbConfig(data)
message.success('创建数据库配置成功')
}
modals.thirdPartyDb = false
loadThirdPartyDbConfigs()
} catch (error: any) {
console.error('保存数据库配置失败:', error)
message.error(error.response?.data?.message || '保存数据库配置失败')
}
}
const handleDeleteThirdPartyDb = async (id: number) => {
try {
await deleteThirdPartyDbConfig(id)
message.success('删除数据库配置成功')
loadThirdPartyDbConfigs()
} catch (error: any) {
console.error('删除数据库配置失败:', error)
message.error(error.response?.data?.message || '删除数据库配置失败')
}
}
const handleTestThirdPartyDb = async (config: ThirdPartyDbConfig) => {
try {
message.loading({ content: '正在测试连接...', key: 'testConn' })
const response = await testThirdPartyDbConnection(config)
if (response.data) {
message.success({ content: '数据库连接成功', key: 'testConn' })
} else {
message.error({ content: '数据库连接失败', key: 'testConn' })
}
} catch (error: any) {
console.error('测试连接失败:', error)
message.error({ content: error.response?.data?.message || '连接测试失败', key: 'testConn' })
}
}
const handleEnableThirdPartyDb = async (id: number) => {
try {
const config = thirdPartyDbConfigs.value.find(c => c.id === id)
if (config) {
await updateThirdPartyDbConfig(id, { ...config, enabled: 1 })
message.success('已启用配置')
loadThirdPartyDbConfigs()
}
} catch (error: any) {
console.error('启用配置失败:', error)
message.error(error.response?.data?.message || '启用配置失败')
}
}
const handleDisableThirdPartyDb = async (id: number) => {
try {
const config = thirdPartyDbConfigs.value.find(c => c.id === id)
if (config) {
await updateThirdPartyDbConfig(id, { ...config, enabled: 0 })
message.success('已禁用配置')
loadThirdPartyDbConfigs()
}
} catch (error: any) {
console.error('禁用配置失败:', error)
message.error(error.response?.data?.message || '禁用配置失败')
}
}
// ==================== 设计师目标积分配置相关方法 ====================
const loadTargetScoreDepartments = async () => {
try {
const response = await getReceivingDepartments()
targetScoreDepartments.value = response.data || []
} catch (error: any) {
console.error('加载接单部门失败:', error)
}
}
const loadDesignerTargetList = async () => {
loadingDesignerTarget.value = true
try {
const params: any = { page: 1, pageSize: 100 }
if (designerTargetFilter.deptId) {
params.deptId = designerTargetFilter.deptId
}
if (designerTargetFilter.month) {
params.month = designerTargetFilter.month
}
const response = await getDesignerTargetScoreList(params)
designerTargetList.value = response.data?.records || []
} catch (error: any) {
console.error('加载目标积分配置失败:', error)
message.error(error.response?.data?.message || '加载目标积分配置失败')
} finally {
loadingDesignerTarget.value = false
}
}
const handleDesignerTargetMonthChange = (value: any) => {
designerTargetFilter.month = value
loadDesignerTargetList()
}
const showDesignerTargetModal = (record?: any) => {
if (record) {
designerTargetModalTitle.value = '编辑目标积分配置'
Object.assign(designerTargetForm, {
id: record.id,
deptId: record.deptId,
userId: record.userId,
targetScore: record.targetScore,
effectiveMonth: record.effectiveMonth,
deductionRate: record.deductionRate || 1
})
} else {
designerTargetModalTitle.value = '添加目标积分配置'
Object.assign(designerTargetForm, {
id: undefined,
deptId: targetScoreDepartments.value[0]?.id || 0,
userId: undefined,
targetScore: 500,
effectiveMonth: undefined,
deductionRate: 1
})
}
designerTargetModalVisible.value = true
}
const handleSaveDesignerTarget = async () => {
if (!designerTargetForm.deptId) {
message.warning('请选择部门')
return
}
if (!designerTargetForm.targetScore || designerTargetForm.targetScore <= 0) {
message.warning('请输入有效的目标积分')
return
}
try {
await saveDesignerTargetScore(designerTargetForm)
message.success('保存成功')
designerTargetModalVisible.value = false
loadDesignerTargetList()
} catch (error: any) {
console.error('保存目标积分配置失败:', error)
message.error(error.response?.data?.message || '保存目标积分配置失败')
}
}
const handleDeleteDesignerTarget = async (id: number) => {
try {
await deleteDesignerTargetScore(id)
message.success('删除成功')
loadDesignerTargetList()
} catch (error: any) {
console.error('删除目标积分配置失败:', error)
message.error(error.response?.data?.message || '删除目标积分配置失败')
}
}
// 加载进度检查配置
const loadProgressCheckConfig = async () => {
try {
const response = await getSystemConfigsByGroup('progress')
const configList = response.data || []
for (const config of configList) {
switch (config.configKey) {
case 'progress.daily_check_enabled':
progressCheckConfig.enabled = config.configValue === 'true'
break
case 'progress.daily_check_hour':
progressCheckConfig.checkHour = parseInt(config.configValue) || 18
break
case 'progress.no_update_deduction':
progressCheckConfig.deductionScore = parseFloat(config.configValue) || 1
break
case 'progress.notify_on_deduction':
progressCheckConfig.notifyEnabled = config.configValue === 'true'
break
}
}
} catch (error: any) {
console.error('加载进度检查配置失败:', error)
}
}
// 保存进度检查配置
const saveProgressCheckConfig = async () => {
try {
await batchUpdateSystemConfigs({
'progress.daily_check_enabled': String(progressCheckConfig.enabled),
'progress.daily_check_hour': String(progressCheckConfig.checkHour),
'progress.no_update_deduction': String(progressCheckConfig.deductionScore),
'progress.notify_on_deduction': String(progressCheckConfig.notifyEnabled)
})
message.success('保存成功')
} catch (error: any) {
console.error('保存进度检查配置失败:', error)
message.error('保存失败')
}
}
watch(activeTab, (value) => {
// 切换到用户与部门 tab 时,始终刷新数据以获取最新状态
if (value === 'users') {
loadDepartmentUsers()
}
// 切换到部门管理 tab 时,刷新部门数据以获取最新成员数
if (value === 'departments') {
loadDepartments()
}
// 切换到导航栏管理 tab 时,加载配置
if (value === 'navbar') {
loadNavbarConfig()
}
// 切换到钉钉集成 tab 时,加载配置
if (value === 'dingtalk') {
loadDingTalkConfig()
}
// 切换到S3配置 tab 时,加载配置
if (value === 's3-config') {
loadS3Configs()
}
// 切换到第三方数据库配置 tab 时,加载配置
if (value === 'third-party-db') {
loadThirdPartyDbConfigs()
}
// 切换到任务排队配置 tab 时,加载统计数据
if (value === 'task-unlock') {
loadTaskUnlockStats()
}
})
</script>
<style lang="scss" scoped>
.settings-page {
min-height: 100vh;
padding: $space-xl 0;
background: $bg-primary;
}
.container {
max-width: $container-max-width;
margin: 0 auto;
padding: 0 $space-xl;
@media (max-width: $breakpoint-tablet) {
padding: 0 $space-lg;
}
@media (max-width: $breakpoint-mobile) {
padding: 0 $space-md;
}
}
.settings-tabs {
background: $bg-card;
border: 1px solid $border-primary;
border-radius: $radius-lg;
padding: $space-lg;
transition: all $transition-base $ease-out;
}
.settings-section {
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $space-lg;
.section-title {
font-size: $font-size-xl;
font-weight: $font-weight-semibold;
color: $text-primary;
margin: 0;
}
}
}
.balance-display {
font-size: $font-size-xl;
color: $color-info;
font-weight: $font-weight-bold;
}
// 网站设置表单
.site-settings-form {
max-width: 600px;
}
// 用户信息单元格
.user-info-cell {
.user-info-main {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
.user-name-text {
font-weight: $font-weight-semibold;
color: $text-primary;
font-size: $font-size-base;
}
.user-username {
font-size: $font-size-sm;
color: $text-tertiary;
font-family: monospace;
}
}
.user-info-sub {
display: flex;
gap: 12px;
font-size: $font-size-sm;
color: $text-secondary;
.user-email {
color: $text-tertiary;
}
span {
display: flex;
align-items: center;
}
}
}
// 状态徽章
.status-badge {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: $radius-sm;
font-size: $font-size-sm;
font-weight: $font-weight-medium;
&.active {
background: rgba($brand-primary, 0.15);
color: $brand-primary;
}
&.inactive {
background: $bg-tertiary;
color: $text-tertiary;
}
}
// 时间值
.time-value {
font-size: $font-size-sm;
color: $text-secondary;
font-family: monospace;
}
.dept-user-card {
min-height: 220px;
background: $bg-card;
border: 1px solid $border-primary;
border-radius: $radius-lg;
.dept-card-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $space-md;
}
.dept-name {
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
color: $text-primary;
}
.dept-meta {
color: $text-tertiary;
font-size: $font-size-sm;
}
.dept-user-list {
display: flex;
flex-direction: column;
gap: $space-sm;
}
.dept-user-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: $space-sm $space-md;
border: 1px solid $border-primary;
border-radius: $radius-md;
background: $bg-secondary;
}
.user-info {
display: flex;
flex-direction: column;
.user-name {
color: $text-primary;
font-weight: $font-weight-medium;
}
.user-meta {
font-size: $font-size-sm;
color: $text-tertiary;
}
}
}
.dept-user-grid {
display: flex;
flex-direction: column;
gap: $space-lg;
}
.dept-user-column {
width: 100%;
}
.user-actions {
display: flex;
align-items: center;
gap: $space-xs;
}
.transfer-modal {
.transfer-tip {
margin-bottom: $space-lg;
color: $text-secondary;
.highlight {
color: $brand-primary;
font-weight: $font-weight-semibold;
}
}
}
:deep(.dark-modal) {
.ant-modal-content {
background: $bg-card !important;
border: 1px solid $border-primary !important;
border-radius: $radius-lg !important;
}
.ant-modal-header {
background: $bg-card !important;
border-bottom: 1px solid $border-primary !important;
.ant-modal-title {
color: $text-primary !important;
}
}
.ant-modal-body {
background: $bg-card !important;
color: $text-primary !important;
}
.ant-modal-footer {
background: $bg-card !important;
border-top: 1px solid $border-primary !important;
}
}
.logo-upload-section {
.logo-preview {
width: 200px;
height: 200px;
border: 2px dashed $border-primary;
border-radius: $radius-md;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: $space-md;
background: $bg-tertiary;
overflow: hidden;
.logo-preview-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.logo-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: $space-sm;
color: $text-tertiary;
}
}
.logo-upload-actions {
display: flex;
gap: $space-sm;
margin-bottom: $space-sm;
}
.upload-tip {
font-size: $font-size-sm;
color: $text-tertiary;
display: flex;
align-items: center;
}
}
// 表单提示文字样式
.form-tip {
margin-top: 8px;
font-size: 12px;
color: $text-tertiary;
display: flex;
align-items: center;
line-height: 1.5;
}
// Ant Design 组件深色主题覆盖
:deep(.ant-tabs) {
.ant-tabs-nav {
background: transparent !important;
.ant-tabs-tab {
color: $text-secondary !important;
background: transparent !important;
border-color: $border-primary !important;
&:hover {
color: $text-primary !important;
}
&.ant-tabs-tab-active {
color: $brand-primary !important;
background: $bg-card !important;
border-color: $brand-primary !important;
.ant-tabs-tab-btn {
color: $brand-primary !important;
}
}
}
}
.ant-tabs-content-holder {
background: transparent !important;
}
.ant-tabs-tabpane {
background: transparent !important;
}
}
:deep(.ant-table) {
background: $bg-card !important;
color: $text-primary !important;
// 表头统一样式
.ant-table-thead > tr > th {
background: $bg-tertiary !important;
border-color: $border-primary !important;
color: $text-primary !important;
font-weight: $font-weight-semibold !important;
font-size: 14px !important;
padding: 16px 12px !important;
text-align: left !important;
white-space: nowrap !important;
// 第一个表头单元格(通常是ID或序号)
&:first-child {
padding-left: 20px !important;
border-top-left-radius: $radius-md !important;
}
// 最后一个表头单元格(通常是操作列)
&:last-child {
padding-right: 20px !important;
text-align: center !important;
border-top-right-radius: $radius-md !important;
}
// 操作列居中
&.ant-table-cell-fix-right {
text-align: center !important;
}
}
// 表格行基础样式 - 确保始终是深色背景,移除所有过渡效果
.ant-table-tbody > tr {
background: $bg-card !important;
transition: none !important; // 移除过渡避免闪烁
// 确保所有单元格都是深色背景
> td {
background: $bg-card !important;
border-color: $border-primary !important;
color: $text-primary !important;
transition: none !important; // 移除过渡避免闪烁
}
// 悬停状态 - 使用深灰色,立即切换无过渡
&:hover {
background: $bg-card-hover !important;
> td {
background: $bg-card-hover !important;
border-color: $border-primary !important;
}
}
// 选中状态
&.ant-table-row-selected {
background: rgba($brand-primary, 0.1) !important;
> td {
background: rgba($brand-primary, 0.1) !important;
}
}
}
// 覆盖 Ant Design 默认的白色背景和所有可能的状态
.ant-table-tbody > tr.ant-table-row {
background: $bg-card !important;
transition: none !important;
> td {
background: $bg-card !important;
transition: none !important;
}
&:hover {
background: $bg-card-hover !important;
transition: none !important;
> td {
background: $bg-card-hover !important;
transition: none !important;
}
}
}
// 确保没有白色背景出现在任何状态,包括所有可能的伪类
.ant-table-tbody > tr:hover > td,
.ant-table-tbody > tr.ant-table-row:hover > td,
.ant-table-tbody > tr.ant-table-row:hover {
background: $bg-card-hover !important;
transition: none !important;
}
// 覆盖 Ant Design 可能设置的白色背景
.ant-table-tbody > tr > td {
background: $bg-card !important;
}
.ant-table-tbody > tr:hover > td {
background: $bg-card-hover !important;
}
.ant-table-placeholder {
background: $bg-card !important;
.ant-empty-description {
color: $text-secondary !important;
}
}
}
:deep(.ant-modal) {
.ant-modal-content {
background: $bg-card !important;
border: 1px solid $border-primary !important;
}
.ant-modal-header {
background: $bg-card !important;
border-bottom: 1px solid $border-primary !important;
.ant-modal-title {
color: $text-primary !important;
}
}
.ant-modal-body {
background: $bg-card !important;
color: $text-primary !important;
}
.ant-modal-footer {
background: $bg-card !important;
border-top: 1px solid $border-primary !important;
}
}
:deep(.ant-form) {
.ant-form-item-label > label {
color: $text-primary !important;
}
.ant-input,
.ant-input-number,
.ant-select-selector,
.ant-picker,
.ant-input-affix-wrapper {
background: $bg-tertiary !important;
border-color: $border-primary !important;
color: $text-primary !important;
&:hover {
border-color: $brand-primary !important;
}
&:focus,
&:focus-within,
&.ant-picker-focused,
&.ant-input-affix-wrapper-focused {
border-color: $brand-primary !important;
box-shadow: 0 0 0 2px rgba($brand-primary, 0.08) !important;
}
&::placeholder {
color: $text-tertiary !important;
}
// 密码输入框内的input
.ant-input {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
// 密码显示/隐藏按钮
.ant-input-suffix {
color: $text-secondary !important;
.anticon {
color: $text-secondary !important;
&:hover {
color: $text-primary !important;
}
}
}
}
.ant-input-number-input {
background: transparent !important;
color: $text-primary !important;
}
.ant-select-selection-item,
.ant-select-selection-placeholder {
color: $text-primary !important;
}
.ant-select-arrow {
color: $text-secondary !important;
}
.ant-picker-input > input {
color: $text-primary !important;
&::placeholder {
color: $text-tertiary !important;
}
}
}
:deep(.ant-descriptions) {
.ant-descriptions-header {
.ant-descriptions-title {
color: $text-primary !important;
}
}
.ant-descriptions-view {
.ant-descriptions-row {
.ant-descriptions-item-label {
background: $bg-tertiary !important;
color: $text-secondary !important;
border-color: $border-primary !important;
}
.ant-descriptions-item-content {
background: $bg-card !important;
color: $text-primary !important;
border-color: $border-primary !important;
}
}
}
}
:deep(.ant-alert) {
background: $bg-card !important;
border-color: $border-primary !important;
.ant-alert-message {
color: $text-primary !important;
}
.ant-alert-description {
color: $text-secondary !important;
}
}
:deep(.ant-divider) {
border-color: $border-divider !important;
&.ant-divider-with-text {
color: $text-secondary !important;
}
}
:deep(.ant-empty) {
.ant-empty-description {
color: $text-secondary !important;
}
}
:deep(.ant-switch) {
&.ant-switch-checked {
background: $brand-primary !important;
// 确保选中状态的文字是黑色
.ant-switch-inner {
color: #000000 !important;
}
}
// 确保选中状态的手柄也是黑色
&.ant-switch-checked .ant-switch-handle {
&::before {
background: #000000 !important;
}
}
}
:deep(.ant-tag) {
background: $bg-tertiary !important;
border-color: $border-primary !important;
color: $text-primary !important;
}
:deep(.ant-select-dropdown) {
background: $bg-card !important;
border-color: $border-primary !important;
.ant-select-item {
color: $text-primary !important;
&:hover {
background: $bg-card-hover !important;
}
&.ant-select-item-option-selected {
background: rgba($brand-primary, 0.1) !important;
color: $brand-primary !important;
}
}
}
:deep(.ant-picker-dropdown) {
.ant-picker-panel {
background: $bg-card !important;
border-color: $border-primary !important;
.ant-picker-header {
border-bottom-color: $border-primary !important;
color: $text-primary !important;
}
.ant-picker-body {
.ant-picker-content {
th {
color: $text-secondary !important;
}
td {
color: $text-primary !important;
&.ant-picker-cell-selected .ant-picker-cell-inner {
background: $brand-primary !important;
color: #000000 !important; // 选中日期文字为黑色
}
&:hover .ant-picker-cell-inner {
background: $bg-card-hover !important;
}
}
}
}
}
}
:deep(.ant-pagination) {
.ant-pagination-item {
background: $bg-tertiary !important;
border-color: $border-primary !important;
a {
color: $text-primary !important;
}
&:hover {
border-color: $brand-primary !important;
a {
color: $brand-primary !important;
}
}
&.ant-pagination-item-active {
background: $brand-primary !important;
border-color: $brand-primary !important;
a {
color: #000000 !important; // 当前页文字为黑色
}
&:hover {
background: $brand-hover !important;
border-color: $brand-hover !important;
a {
color: #000000 !important;
}
}
}
}
.ant-pagination-prev,
.ant-pagination-next {
.ant-pagination-item-link {
background: $bg-tertiary !important;
border-color: $border-primary !important;
color: $text-primary !important;
&:hover {
border-color: $brand-primary !important;
color: $brand-primary !important;
}
}
}
.ant-pagination-jump-prev,
.ant-pagination-jump-next {
.ant-pagination-item-link {
color: $text-primary !important;
}
}
.ant-pagination-options {
.ant-select {
.ant-select-selector {
background: $bg-tertiary !important;
border-color: $border-primary !important;
color: $text-primary !important;
}
}
.ant-pagination-options-size-changer {
color: $text-primary !important;
}
}
.ant-pagination-total-text {
color: $text-primary !important;
}
}
// 奖励规则介绍区域样式
.reward-rules-intro {
padding: 24px;
background: $bg-tertiary;
border-radius: $radius-lg;
.intro-card {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 24px;
.intro-content {
h3 {
color: $text-primary;
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
p {
color: $text-secondary;
font-size: 14px;
line-height: 1.6;
}
}
}
.intro-examples {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
background: $bg-secondary;
border-radius: $radius-md;
.example-item {
display: flex;
align-items: center;
gap: 12px;
color: $text-primary;
font-size: 14px;
}
}
}
// 导航栏配置样式
.navbar-config-form {
.config-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.config-item {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
padding: 16px 20px;
background: $bg-tertiary;
border: 1px solid $border-primary;
border-radius: $radius-md;
transition: all 0.2s ease;
&:hover {
border-color: $brand-primary;
background: rgba($brand-primary, 0.05);
}
.config-item-header {
display: flex;
align-items: center;
gap: 12px;
.config-item-title {
color: $text-primary;
font-size: 15px;
font-weight: 500;
}
}
.config-item-desc {
width: 100%;
color: $text-secondary;
font-size: 12px;
margin-top: -4px;
padding-left: 36px;
}
}
.section-description {
color: $text-secondary;
font-size: 14px;
margin-bottom: 24px;
padding-left: 4px;
}
}
// ===== 部门管理表格样式优化 =====
// 积分显示
.score-display {
.score-main {
display: flex;
align-items: baseline;
gap: 4px;
margin-bottom: 6px;
.score-value {
font-size: 20px;
font-weight: $font-weight-bold;
color: $brand-primary;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
}
.score-unit {
font-size: 13px;
color: $text-tertiary;
}
}
.score-details {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
.score-detail-item {
display: flex;
align-items: center;
gap: 4px;
.detail-label {
color: $text-tertiary;
}
.detail-value {
color: $text-secondary;
font-weight: $font-weight-medium;
}
}
.detail-separator {
color: $border-primary;
}
}
}
// 限制显示
.limit-display {
.limit-period {
margin-bottom: 4px;
.period-label {
display: inline-block;
padding: 2px 8px;
background: rgba($brand-primary, 0.1);
color: $brand-primary;
border-radius: 4px;
font-size: 12px;
font-weight: $font-weight-medium;
}
}
.limit-value {
display: flex;
align-items: center;
gap: 4px;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
font-size: 14px;
.consumed {
color: $text-primary;
font-weight: $font-weight-semibold;
}
.separator {
color: $text-tertiary;
}
.total {
color: $text-secondary;
}
}
.limit-disabled {
color: $text-tertiary;
font-size: 13px;
}
}
// 预警显示
.alert-display {
.alert-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 12px;
font-size: 13px;
font-weight: $font-weight-medium;
&.warning {
background: rgba(#F59E0B, 0.15);
color: #F59E0B;
border: 1px solid rgba(#F59E0B, 0.3);
}
&.normal {
background: rgba($brand-primary, 0.15);
color: $brand-primary;
border: 1px solid rgba($brand-primary, 0.3);
}
}
}
// 状态开关
.status-switch {
display: flex;
align-items: center;
gap: 10px;
.status-text {
font-size: 13px;
color: $text-tertiary;
transition: color 0.2s ease;
&.active {
color: $brand-primary;
font-weight: $font-weight-medium;
}
}
}
// 表格操作按钮
.table-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
:deep(.ant-btn-text) {
color: #9CA3AF !important;
padding: 4px 8px;
height: auto;
font-size: $font-size-sm;
transition: all 0.2s ease;
&:hover {
color: $brand-primary !important;
background: rgba($brand-primary, 0.08) !important;
}
&.ant-btn-dangerous {
&:hover {
color: $color-error !important;
background: rgba($color-error, 0.08) !important;
}
}
}
.action-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
height: 28px;
font-size: 13px;
border-radius: $radius-sm;
transition: all 0.2s ease;
&.edit {
color: $text-primary;
&:hover {
color: $brand-primary;
background: rgba($brand-primary, 0.1);
}
}
&.pool {
color: $text-primary;
&:hover {
color: #3B82F6;
background: rgba(#3B82F6, 0.1);
}
}
&.danger {
color: $text-tertiary;
&:hover {
color: #EF4444;
background: rgba(#EF4444, 0.1);
}
}
}
}
// 空值显示
.empty-value {
color: $text-tertiary;
font-size: 14px;
}
// 部门信息单元格
.dept-info-cell {
.dept-info-main {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
.dept-name-text {
font-size: 15px;
font-weight: $font-weight-semibold;
color: $text-primary;
}
}
.dept-info-sub {
font-size: 12px;
color: $text-tertiary;
.dept-desc {
margin-left: 8px;
}
}
}
// 延误配置卡片样式
.delay-config-card {
display: flex !important;
flex-direction: row !important;
align-items: center;
justify-content: space-between;
min-height: 100px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.config-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
padding-right: 20px;
.config-header {
display: flex;
align-items: center;
gap: 12px;
.icon-box {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
&.red {
background: rgba(255, 77, 79, 0.1);
color: #ff4d4f;
}
&.green {
background: rgba(49, 240, 139, 0.1);
color: #31f08b;
}
}
.title {
font-size: 16px;
font-weight: 600;
color: $text-primary;
}
}
.desc {
font-size: 13px;
color: $text-tertiary;
padding-left: 48px;
line-height: 1.5;
}
}
.config-action {
display: flex;
align-items: center;
padding-left: 20px;
border-left: 1px solid rgba(255, 255, 255, 0.08);
min-width: 180px;
justify-content: flex-end;
}
}
// 输入框样式
.input-with-suffix {
display: flex;
align-items: center;
gap: 8px;
.input-suffix {
color: $text-tertiary;
font-size: 14px;
white-space: nowrap;
}
:deep(.ant-input-number) {
background-color: #1f1f1f;
border-color: #434343;
color: #fff;
border-radius: 4px;
.ant-input-number-input {
color: #fff;
background-color: transparent;
}
.ant-input-number-handler-wrap {
background: #2a2a2a;
border-left: 1px solid #434343;
}
.ant-input-number-handler {
border-bottom: 1px solid #434343;
&:active {
background: #444;
}
}
.ant-input-number-handler-up-inner,
.ant-input-number-handler-down-inner {
color: rgba(255, 255, 255, 0.45);
}
&:hover {
border-color: $brand-primary;
}
}
:deep(.ant-input-number-focused) {
border-color: $brand-primary;
box-shadow: 0 0 0 2px rgba(49, 240, 139, 0.2);
}
}
.formula-desc {
p {
margin-bottom: 4px;
&:last-child {
margin-bottom: 0;
}
&.note {
color: #ff4d4f;
margin-top: 8px;
font-size: 12px;
}
}
}
// 测试消息提示
.test-message-hint {
margin-left: $space-md;
font-size: $font-size-sm;
color: $text-tertiary;
}
// ===== 部门管理表格样式优化 - 不换行显示 =====
:deep(.ant-table) {
// 移除边框
.ant-table-container,
.ant-table-content,
.ant-table-thead,
.ant-table-tbody {
border: none !important;
}
table {
border-collapse: collapse !important;
}
.ant-table-thead > tr > th {
background: rgba(0, 0, 0, 0.3) !important;
border: none !important;
color: rgba(255, 255, 255, 0.6) !important;
font-size: 13px;
padding: 12px 8px;
white-space: nowrap;
&::before {
display: none !important;
}
}
.ant-table-tbody > tr {
> td {
border: none !important;
padding: 10px 8px;
font-size: 13px;
white-space: nowrap;
vertical-align: middle;
}
&:nth-child(even) > td {
background: rgba(255, 255, 255, 0.015) !important;
}
&:hover > td {
background: rgba(49, 240, 139, 0.05) !important;
}
}
.ant-table-cell {
border: none !important;
&::before, &::after {
display: none !important;
}
}
// 树形表格展开图标
.ant-table-row-expand-icon {
background: transparent !important;
border-color: rgba(255, 255, 255, 0.3) !important;
color: rgba(255, 255, 255, 0.6) !important;
}
}
// 部门信息单元格 - 不换行
.dept-info-cell {
.dept-info-main {
display: flex;
align-items: center;
white-space: nowrap;
.dept-name-text {
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
}
}
.dept-info-sub {
font-size: 12px;
color: rgba(255, 255, 255, 0.45);
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
.dept-desc {
opacity: 0.7;
}
}
}
// 盲盒模式设置卡片样式
.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;
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;
}
}
</style>