4256 lines
138 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-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-button type="primary" @click="showDemandTypeModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加类型
</a-button>
</div>
<a-table
:columns="demandTypeColumns"
:data-source="demandTypes"
:loading="loading.demandTypes"
row-key="id"
:pagination="{ pageSize: 10 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<span>{{ record.name || record.description || '--' }}</span>
</template>
<template v-else-if="column.key === 'unitScore'">
<span>{{ record.unitScore ?? '--' }}</span>
</template>
<template v-else-if="column.key === 'estimatedHours'">
<span>{{ record.estimatedHours ?? '--' }}</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-popconfirm title="确定删除?" @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>
<a-button type="primary" @click="showDepartmentModal()">
<template #icon><Icon name="plus" size="sm" /></template>
添加部门
</a-button>
</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-table
:columns="departmentColumns"
:data-source="filteredDepartments"
:loading="loading.departments"
row-key="id"
:pagination="{ pageSize: 10 }"
>
<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
size="small"
:color="record.deptType === 'demand' ? 'blue' : 'green'"
style="margin-left: 8px;"
>
{{ record.deptType === '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">
<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>
</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="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="钉钉企业ID" />
</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>
</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>
</a-tabs>
<!-- 需求类型弹窗 -->
<a-modal
v-model:open="modals.demandType"
:title="editingItem ? '编辑需求类型' : '添加需求类型'"
@ok="saveDemandType"
@cancel="closeModal('demandType')"
>
<a-form :model="formData.demandType" layout="vertical">
<a-form-item label="类型名称" required>
<a-input v-model:value="formData.demandType.name" placeholder="请输入类型名称" />
</a-form-item>
<a-form-item label="单价积分" required>
<a-input-number
v-model:value="formData.demandType.unitScore"
:min="0"
:precision="0"
style="width: 100%"
placeholder="请输入单价积分"
/>
</a-form-item>
<a-form-item label="预计时长(小时)" required>
<a-input-number
v-model:value="formData.demandType.estimatedHours"
:min="0"
:precision="1"
style="width: 100%"
placeholder="请输入预计时长"
/>
</a-form-item>
<a-form-item label="描述">
<a-textarea v-model:value="formData.demandType.description" :rows="3" placeholder="请输入描述" />
</a-form-item>
<a-form-item label="可接单部门">
<a-select
v-model:value="formData.demandType.departmentIds"
mode="multiple"
placeholder="请选择可接单部门(不选则所有接单部门都可接)"
allow-clear
:max-tag-count="3"
>
<a-select-option
v-for="dept in receivingDepartments"
:key="dept.id"
:value="dept.id"
>
{{ dept.deptName }}
</a-select-option>
</a-select>
<div class="form-tip">
<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.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>
</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-select
v-model:value="transferDeptId"
placeholder="请选择部门"
style="width: 100%"
>
<a-select-option :value="0">未分配部门</a-select-option>
<a-select-option
v-for="dept in departments"
:key="dept.id"
:value="dept.id"
>
{{ dept.deptName }}
</a-select-option>
</a-select>
</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="receiving">接单部门(只能接任务)</a-radio>
</a-radio-group>
</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-option
v-for="dept in departments.filter(d => !editingItem || d.id !== editingItem.id)"
:key="dept.id"
:value="dept.id"
>
{{ dept.deptName }}
</a-select-option>
</a-select>
</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>
</a-form>
</a-modal>
<!-- 积分池管理弹窗 -->
<a-modal
v-model:open="modals.scorePool"
title="积分池管理"
width="700px"
ok-text="OK"
cancel-text="Cancel"
@ok="modals.scorePool = false"
@cancel="modals.scorePool = false"
>
<div v-if="currentScorePool">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="部门名称">
{{ currentScorePool.deptName }}
</a-descriptions-item>
<a-descriptions-item label="当前余额">
<span class="balance-display">
{{ currentScorePool.balance }} 分
</span>
</a-descriptions-item>
<a-descriptions-item label="累计充值">
{{ currentScorePool.totalRecharge || 0 }} 分
</a-descriptions-item>
<a-descriptions-item label="累计消耗">
{{ currentScorePool.totalConsume || 0 }} 分
</a-descriptions-item>
<a-descriptions-item label="累计获得" v-if="currentScorePool.totalEarn">
{{ currentScorePool.totalEarn }} 分
</a-descriptions-item>
</a-descriptions>
<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>
</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>
</div>
</div>
</DefaultLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, watch } from 'vue'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import { message } from 'ant-design-vue'
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,
type DepartmentOverview,
type DepartmentPayload,
type ScoreAdjustPayload,
type DeptType
} from '@/api/department'
import {
getSystemConfigsByGroup,
getSystemConfig,
batchUpdateSystemConfigs,
createSystemConfig,
updateSystemConfig,
type SystemConfig
} from '@/api/system'
import {
getAllUsers,
updateUserDepartment,
deleteUser,
type User
} from '@/api/user'
import {
getAllS3Configs,
createS3Config,
updateS3Config,
deleteS3Config,
activateS3Config,
type S3Config
} from '@/api/s3'
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
}
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: '',
registerEnabled: true,
siteNameConfigId: null as number | null,
logoConfigId: null as number | null,
registerEnabledConfigId: null as number | null
})
const loading = reactive({
demandTypes: false,
taskSubTypes: false,
brands: false,
platforms: false,
categories: false,
departments: false
})
const modals = reactive({
demandType: false,
taskSubType: false,
brand: false,
platform: false,
category: false,
department: false,
scorePool: false,
s3Config: 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 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 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
})
const formData = reactive({
demandType: {} as Partial<DemandType> & { departmentIds?: number[] },
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 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: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '类型名称', dataIndex: 'name', key: 'name' },
{ title: '单价积分', dataIndex: 'unitScore', key: 'unitScore' },
{ title: '预计时长(h)', dataIndex: 'estimatedHours', key: 'estimatedHours' },
{ 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 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' },
{ title: '负责人', key: 'leader', width: 160 },
{ title: '成员数', key: 'memberCount', width: 120 },
{ title: '积分余额', key: 'score', width: 160 },
{ title: '本周期消耗', key: 'limit', width: 200 },
{ title: '预警', key: 'alert', width: 120 },
{ title: '状态', key: 'status', width: 120 },
{ title: '操作', key: 'actions', width: 240 }
]
// 加载数据
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 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
} else {
message.warning('该部门暂无积分池')
}
} catch (error) {
message.error('加载积分池信息失败')
}
}
// 处理积分充值
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) => {
editingItem.value = item
if (item) {
// 编辑模式:将allowedDeptIds(逗号分隔字符串)转换为数组
const departmentIds = item.allowedDeptIds
? item.allowedDeptIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id))
: []
formData.demandType = { ...item, departmentIds }
} else {
// 新建模式
formData.demandType = { sortOrder: 0, isEnabled: 1, departmentIds: [] }
}
modals.demandType = true
}
const showTaskSubTypeModal = (item?: TaskSubType) => {
editingItem.value = item
formData.taskSubType = item
? { ...item }
: { sortOrder: 0, isEnabled: 1 }
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) {
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
}
} 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 saveData: Partial<DemandType> = {
name: formData.demandType.name,
unitScore: formData.demandType.unitScore,
estimatedHours: formData.demandType.estimatedHours,
description: formData.demandType.description,
sortOrder: formData.demandType.sortOrder ?? 0,
isEnabled: formData.demandType.isEnabled ?? 1,
// 将部门ID数组转换为逗号分隔的字符串
allowedDeptIds: 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> = {
name: formData.taskSubType.name,
code: formData.taskSubType.code,
description: formData.taskSubType.description,
sortOrder: formData.taskSubType.sortOrder ?? 0,
isEnabled: formData.taskSubType.isEnabled ?? 1
}
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')
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)
if (isEdit && editingItem.value?.id) {
delete payload.initialScoreBalance
await updateDepartmentApi(editingItem.value.id, payload)
message.success('更新成功')
} else {
await createDepartmentApi(payload)
message.success('创建成功')
}
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 userApi.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 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'
}
} 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 loadSiteSettings = async () => {
loadingSiteSettings.value = true
try {
// 先加载到store
await siteSettingsStore.loadSiteSettings()
// 获取网站设置分组的所有配置
const response = await getSystemConfigsByGroup('site')
const configs = response.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
}
// 查找注册功能开关配置
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.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
}
}
// 更新全局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)
}
onMounted(() => {
loadAllData()
// 加载网站设置
loadSiteSettings()
// 加载钉钉配置
loadDingTalkConfig()
// 加载任务发布类型配置
loadTaskReleaseConfig()
})
// 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配置失败')
}
}
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()
}
})
</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 {
background: $bg-tertiary !important;
border-color: $border-primary !important;
color: $text-primary !important;
&:hover {
border-color: $brand-primary !important;
}
&:focus,
&.ant-picker-focused {
border-color: $brand-primary !important;
box-shadow: 0 0 0 2px rgba($brand-primary, 0.08) !important;
}
&::placeholder {
color: $text-tertiary !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;
}
}
}
// 测试消息提示
.test-message-hint {
margin-left: $space-md;
font-size: $font-size-sm;
color: $text-tertiary;
}
</style>