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