4256 lines
138 KiB
Vue
4256 lines
138 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-divider orientation="left">功能开关</a-divider>
|
||
<a-form-item label="注册功能">
|
||
<a-switch
|
||
v-model:checked="siteSettings.registerEnabled"
|
||
checked-children="开启"
|
||
un-checked-children="关闭"
|
||
/>
|
||
<div style="margin-top: 8px; font-size: 12px; color: #888;">
|
||
控制登录页面是否显示注册功能
|
||
</div>
|
||
</a-form-item>
|
||
|
||
<a-form-item>
|
||
<a-button type="primary" size="large" @click="handleSaveSiteSettings" :loading="savingSiteSettings">
|
||
<template #icon><Icon name="check" size="sm" /></template>
|
||
保存设置
|
||
</a-button>
|
||
</a-form-item>
|
||
</a-form>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 需求类型管理 -->
|
||
<a-tab-pane key="demand-types" tab="需求类型">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">需求类型管理</h2>
|
||
<a-button type="primary" @click="showDemandTypeModal()">
|
||
<template #icon><Icon name="plus" size="sm" /></template>
|
||
添加类型
|
||
</a-button>
|
||
</div>
|
||
|
||
<a-table
|
||
:columns="demandTypeColumns"
|
||
:data-source="demandTypes"
|
||
:loading="loading.demandTypes"
|
||
row-key="id"
|
||
:pagination="{ pageSize: 10 }"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'name'">
|
||
<span>{{ record.name || record.description || '--' }}</span>
|
||
</template>
|
||
<template v-else-if="column.key === 'unitScore'">
|
||
<span>{{ record.unitScore ?? '--' }}</span>
|
||
</template>
|
||
<template v-else-if="column.key === 'estimatedHours'">
|
||
<span>{{ record.estimatedHours ?? '--' }}</span>
|
||
</template>
|
||
<template v-else-if="column.key === 'status'">
|
||
<a-switch
|
||
:checked="record.isEnabled === 1"
|
||
@change="(checked: boolean) => toggleStatus('demandType', record.id, checked)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'actions'">
|
||
<a-space>
|
||
<a-button type="link" size="small" @click="showDemandTypeModal(record)">编辑</a-button>
|
||
<a-popconfirm title="确定删除?" @confirm="deleteItem('demandType', record.id)">
|
||
<a-button type="link" danger size="small">删除</a-button>
|
||
</a-popconfirm>
|
||
</a-space>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 任务子类型管理 -->
|
||
<a-tab-pane key="task-sub-types" tab="任务子类型">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">任务子类型管理</h2>
|
||
<a-button type="primary" @click="showTaskSubTypeModal()">
|
||
<template #icon><Icon name="plus" size="sm" /></template>
|
||
添加子类型
|
||
</a-button>
|
||
</div>
|
||
|
||
<a-table
|
||
:columns="taskSubTypeColumns"
|
||
:data-source="taskSubTypes"
|
||
:loading="loading.taskSubTypes"
|
||
row-key="id"
|
||
:pagination="{ pageSize: 10 }"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'status'">
|
||
<a-switch
|
||
:checked="record.isEnabled === 1"
|
||
@change="(checked: boolean) => toggleStatus('taskSubType', record.id, checked)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'actions'">
|
||
<a-space>
|
||
<a-button type="link" size="small" @click="showTaskSubTypeModal(record)">编辑</a-button>
|
||
<a-popconfirm title="确定删除?" @confirm="deleteItem('taskSubType', record.id)">
|
||
<a-button type="link" danger size="small">删除</a-button>
|
||
</a-popconfirm>
|
||
</a-space>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 品牌管理 -->
|
||
<a-tab-pane key="brands" tab="品牌管理">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">品牌管理</h2>
|
||
<a-button type="primary" @click="showBrandModal()">
|
||
<template #icon><Icon name="plus" size="sm" /></template>
|
||
添加品牌
|
||
</a-button>
|
||
</div>
|
||
|
||
<a-table
|
||
:columns="brandColumns"
|
||
:data-source="brands"
|
||
:loading="loading.brands"
|
||
row-key="id"
|
||
:pagination="{ pageSize: 10 }"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'status'">
|
||
<a-switch
|
||
:checked="record.isEnabled === 1"
|
||
@change="(checked: boolean) => toggleStatus('brand', record.id, checked)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'actions'">
|
||
<a-space>
|
||
<a-button type="link" size="small" @click="showBrandModal(record)">编辑</a-button>
|
||
<a-popconfirm title="确定删除?" @confirm="deleteItem('brand', record.id)">
|
||
<a-button type="link" danger size="small">删除</a-button>
|
||
</a-popconfirm>
|
||
</a-space>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 平台管理 -->
|
||
<a-tab-pane key="platforms" tab="平台管理">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">平台管理</h2>
|
||
<a-button type="primary" @click="showPlatformModal()">
|
||
<template #icon><Icon name="plus" size="sm" /></template>
|
||
添加平台
|
||
</a-button>
|
||
</div>
|
||
|
||
<a-table
|
||
:columns="platformColumns"
|
||
:data-source="platforms"
|
||
:loading="loading.platforms"
|
||
row-key="id"
|
||
:pagination="{ pageSize: 10 }"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'status'">
|
||
<a-switch
|
||
:checked="record.isEnabled === 1"
|
||
@change="(checked: boolean) => toggleStatus('platform', record.id, checked)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'actions'">
|
||
<a-space>
|
||
<a-button type="link" size="small" @click="showPlatformModal(record)">编辑</a-button>
|
||
<a-popconfirm title="确定删除?" @confirm="deleteItem('platform', record.id)">
|
||
<a-button type="link" danger size="small">删除</a-button>
|
||
</a-popconfirm>
|
||
</a-space>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 类目管理 -->
|
||
<a-tab-pane key="categories" tab="类目管理">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">类目管理</h2>
|
||
<a-button type="primary" @click="showCategoryModal()">
|
||
<template #icon><Icon name="plus" size="sm" /></template>
|
||
添加类目
|
||
</a-button>
|
||
</div>
|
||
|
||
<a-table
|
||
:columns="categoryColumns"
|
||
:data-source="categories"
|
||
:loading="loading.categories"
|
||
row-key="id"
|
||
:pagination="{ pageSize: 10 }"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'status'">
|
||
<a-switch
|
||
:checked="record.isEnabled === 1"
|
||
@change="(checked: boolean) => toggleStatus('category', record.id, checked)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="column.key === 'actions'">
|
||
<a-space>
|
||
<a-button type="link" size="small" @click="showCategoryModal(record)">编辑</a-button>
|
||
<a-popconfirm title="确定删除?" @confirm="deleteItem('category', record.id)">
|
||
<a-button type="link" danger size="small">删除</a-button>
|
||
</a-popconfirm>
|
||
</a-space>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 用户管理 -->
|
||
<a-tab-pane key="users" tab="用户管理">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<div>
|
||
<h2 class="section-title">用户管理</h2>
|
||
<p class="section-subtitle">查看各部门成员,快速调整用户所属部门</p>
|
||
</div>
|
||
<a-space>
|
||
<a-button :loading="userLoading" @click="loadDepartmentUsers">
|
||
<template #icon><Icon name="refresh" size="sm" /></template>
|
||
刷新数据
|
||
</a-button>
|
||
<a-button type="primary" @click="goToUserManagement">
|
||
<template #icon><Icon name="external-link" size="sm" /></template>
|
||
完整用户管理
|
||
</a-button>
|
||
</a-space>
|
||
</div>
|
||
|
||
<a-alert
|
||
type="info"
|
||
show-icon
|
||
message="提示"
|
||
description="部门列表来源于“部门管理”配置。调整部门会立即生效,并同步到创建任务、抢单等功能中。"
|
||
style="margin-bottom: 16px;"
|
||
/>
|
||
|
||
<a-table
|
||
:columns="userManagementColumns"
|
||
:data-source="allUsersList"
|
||
:loading="userLoading"
|
||
row-key="id"
|
||
:pagination="{ pageSize: 15 }"
|
||
:scroll="{ x: 1200 }"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'userInfo'">
|
||
<div class="user-info-cell">
|
||
<div class="user-info-main">
|
||
<span class="user-name-text">{{ record.realName || record.username }}</span>
|
||
<span v-if="record.realName" class="user-username">@{{ record.username }}</span>
|
||
</div>
|
||
<div class="user-info-sub">
|
||
<span v-if="record.phone">
|
||
<Icon name="phone" size="xs" style="margin-right: 4px;" />
|
||
{{ record.phone }}
|
||
</span>
|
||
<span v-if="record.email" class="user-email">
|
||
<Icon name="mail" size="xs" style="margin-right: 4px;" />
|
||
{{ record.email }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'department'">
|
||
<a-tag v-if="record.deptName" :color="record.deptType === 'demand' ? 'blue' : 'green'" size="small">
|
||
{{ record.deptName }}
|
||
<span style="margin-left: 4px; opacity: 0.8;">
|
||
({{ record.deptType === 'demand' ? '需求' : '接单' }})
|
||
</span>
|
||
</a-tag>
|
||
<a-tag v-else color="orange" size="small">未分配</a-tag>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'isAdmin'">
|
||
<a-tag v-if="record.isAdmin === 1" color="red" size="small">
|
||
<Icon name="star" size="xs" style="margin-right: 4px;" />
|
||
管理员
|
||
</a-tag>
|
||
<span v-else style="color: #999;">—</span>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'status'">
|
||
<span :class="['status-badge', record.status === 1 ? 'active' : 'inactive']">
|
||
{{ record.status === 1 ? '启用' : '禁用' }}
|
||
</span>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'createTime'">
|
||
<span class="time-value">{{ formatUserTime(record.createTime) }}</span>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'actions'">
|
||
<div class="table-actions">
|
||
<a-button type="text" size="small" @click="openTransferModal(record)">
|
||
<Icon name="shuffle" size="sm" style="margin-right: 4px;" />
|
||
调整部门
|
||
</a-button>
|
||
<a-popconfirm
|
||
:title="record.isAdmin === 1 ? '确定取消该用户的管理员身份?' : '确定设置该用户为管理员?'"
|
||
@confirm="toggleUserAdmin(record)"
|
||
>
|
||
<a-button type="text" size="small" :type="record.isAdmin === 1 ? 'default' : 'primary'">
|
||
<Icon :name="record.isAdmin === 1 ? 'lock' : 'star'" size="sm" style="margin-right: 4px;" />
|
||
{{ record.isAdmin === 1 ? '取消管理员' : '设为管理员' }}
|
||
</a-button>
|
||
</a-popconfirm>
|
||
<a-popconfirm title="确定删除该用户?" @confirm="deleteUserFromCard(record)">
|
||
<a-button type="text" size="small" danger>
|
||
<Icon name="trash" size="sm" style="margin-right: 4px;" />
|
||
删除
|
||
</a-button>
|
||
</a-popconfirm>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 部门管理 -->
|
||
<a-tab-pane key="departments" tab="部门管理">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">部门管理</h2>
|
||
<a-button type="primary" @click="showDepartmentModal()">
|
||
<template #icon><Icon name="plus" size="sm" /></template>
|
||
添加部门
|
||
</a-button>
|
||
</div>
|
||
|
||
<a-alert
|
||
message="部门类型说明"
|
||
description="需求部门:只能发起需求;接单部门:只能接收任务。请根据实际情况设置部门类型。"
|
||
type="info"
|
||
show-icon
|
||
style="margin-bottom: 16px;"
|
||
/>
|
||
|
||
<!-- 部门搜索框 -->
|
||
<div style="margin-bottom: 16px; display: flex; align-items: center; gap: 12px;">
|
||
<a-input
|
||
v-model:value="departmentSearchText"
|
||
placeholder="搜索部门名称、编码或负责人"
|
||
allow-clear
|
||
style="max-width: 400px;"
|
||
>
|
||
<template #prefix>
|
||
<Icon name="search" size="sm" />
|
||
</template>
|
||
</a-input>
|
||
<span v-if="departmentSearchText" style="color: #888; font-size: 13px;">
|
||
找到 {{ filteredDepartments.length }} 个部门
|
||
</span>
|
||
</div>
|
||
|
||
<a-table
|
||
:columns="departmentColumns"
|
||
:data-source="filteredDepartments"
|
||
:loading="loading.departments"
|
||
row-key="id"
|
||
:pagination="{ pageSize: 10 }"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'deptInfo'">
|
||
<div class="dept-info-cell">
|
||
<div class="dept-info-main">
|
||
<span class="dept-name-text">{{ record.deptName }}</span>
|
||
<a-tag
|
||
size="small"
|
||
:color="record.deptType === 'demand' ? 'blue' : 'green'"
|
||
style="margin-left: 8px;"
|
||
>
|
||
{{ record.deptType === 'demand' ? '需求部门' : '接单部门' }}
|
||
</a-tag>
|
||
</div>
|
||
<div class="dept-info-sub">
|
||
<span v-if="record.deptCode">编码:{{ record.deptCode }}</span>
|
||
<span v-if="record.description" class="dept-desc">|{{ record.description }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'leader'">
|
||
<span>{{ record.leaderName || '未设置' }}</span>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'memberCount'">
|
||
<span>{{ record.memberCount ?? 0 }} 人</span>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'score'">
|
||
<div class="score-display">
|
||
<div class="score-main">
|
||
<span class="score-value">{{ record.scoreBalance ?? 0 }}</span>
|
||
<span class="score-unit">分</span>
|
||
</div>
|
||
<div class="score-details">
|
||
<span class="score-detail-item">
|
||
<span class="detail-label">充值</span>
|
||
<span class="detail-value">{{ record.totalRecharge ?? 0 }}</span>
|
||
</span>
|
||
<span class="detail-separator">|</span>
|
||
<span class="score-detail-item">
|
||
<span class="detail-label">消耗</span>
|
||
<span class="detail-value">{{ record.totalConsume ?? 0 }}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'limit'">
|
||
<div class="limit-display">
|
||
<template v-if="record.limitEnabled === 1">
|
||
<div class="limit-period">
|
||
<span class="period-label">{{ getLimitPeriodLabel(record.limitPeriod) }}</span>
|
||
</div>
|
||
<div class="limit-value">
|
||
<span class="consumed">{{ record.periodUsed ?? 0 }}</span>
|
||
<span class="separator">/</span>
|
||
<span class="total">{{ record.periodLimit ?? 0 }}</span>
|
||
</div>
|
||
</template>
|
||
<span v-else class="limit-disabled">未启用</span>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'alert'">
|
||
<div v-if="record.limitEnabled === 1" class="alert-display">
|
||
<div v-if="record.limitAlert" class="alert-badge warning">
|
||
<Icon name="alert" size="sm" />
|
||
<span>预警中</span>
|
||
</div>
|
||
<div v-else class="alert-badge normal">
|
||
<Icon name="checkCircle" size="sm" />
|
||
<span>正常</span>
|
||
</div>
|
||
</div>
|
||
<span v-else class="empty-value">-</span>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'status'">
|
||
<div class="status-switch">
|
||
<a-switch
|
||
:checked="record.status === 1"
|
||
@change="(checked: boolean) => toggleDepartmentStatus(record.id, checked)"
|
||
size="small"
|
||
/>
|
||
<span class="status-text" :class="{ active: record.status === 1 }">
|
||
{{ record.status === 1 ? '启用' : '禁用' }}
|
||
</span>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="column.key === 'actions'">
|
||
<div class="table-actions">
|
||
<a-button
|
||
type="text"
|
||
size="small"
|
||
@click="showDepartmentModal(record)"
|
||
class="action-btn edit"
|
||
>
|
||
<Icon name="edit" size="sm" />
|
||
编辑
|
||
</a-button>
|
||
<a-button
|
||
type="text"
|
||
size="small"
|
||
@click="showScorePoolModal(record)"
|
||
class="action-btn pool"
|
||
>
|
||
<Icon name="coins" size="sm" />
|
||
积分池
|
||
</a-button>
|
||
<a-popconfirm title="确定删除该部门吗?" ok-text="确定" cancel-text="取消" @confirm="deleteDepartment(record.id)">
|
||
<a-button
|
||
type="text"
|
||
danger
|
||
size="small"
|
||
class="action-btn danger"
|
||
>
|
||
<Icon name="trash" size="sm" />
|
||
删除
|
||
</a-button>
|
||
</a-popconfirm>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 奖励规则管理 -->
|
||
<a-tab-pane key="reward-rules" tab="奖励规则">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">自动奖励规则</h2>
|
||
<a-button type="primary" @click="goToRewardRules">
|
||
<template #icon><Icon name="settings" size="sm" /></template>
|
||
管理规则
|
||
</a-button>
|
||
</div>
|
||
<div class="reward-rules-intro">
|
||
<div class="intro-card">
|
||
<Icon name="sparkles" size="xl" color="#FFD700" />
|
||
<div class="intro-content">
|
||
<h3>什么是自动奖励规则?</h3>
|
||
<p>自动奖励规则是系统自动触发的积分奖励机制。当用户满足特定条件时,系统会自动发放相应的积分奖励。</p>
|
||
</div>
|
||
</div>
|
||
<div class="intro-examples">
|
||
<div class="example-item">
|
||
<Icon name="calendar" size="md" color="#31f08b" />
|
||
<span>每日完成5个任务,额外奖励20积分</span>
|
||
</div>
|
||
<div class="example-item">
|
||
<Icon name="trophy" size="md" color="#FFD700" />
|
||
<span>每周完成20个任务,额外奖励100积分</span>
|
||
</div>
|
||
<div class="example-item">
|
||
<Icon name="star" size="md" color="#F59E0B" />
|
||
<span>获得五星好评,奖励10积分</span>
|
||
</div>
|
||
</div>
|
||
<a-button type="primary" size="large" @click="goToRewardRules" style="margin-top: 24px;">
|
||
进入奖励规则管理
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 积分池管理 -->
|
||
<a-tab-pane key="score-pool" tab="积分池">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">激励积分池管理</h2>
|
||
<a-button type="primary" @click="goToScorePool">
|
||
<template #icon><Icon name="chartBar" size="sm" /></template>
|
||
管理积分池
|
||
</a-button>
|
||
</div>
|
||
<div class="reward-rules-intro">
|
||
<div class="intro-card">
|
||
<Icon name="sparkles" size="xl" color="#FFD700" />
|
||
<div class="intro-content">
|
||
<h3>什么是激励积分池?</h3>
|
||
<p>激励积分池用于管理和控制系统的积分发放预算。您可以设置月度预算、分项预算,并监控积分使用情况。</p>
|
||
</div>
|
||
</div>
|
||
<div class="intro-examples">
|
||
<div class="example-item">
|
||
<Icon name="document" size="md" color="#3B82F6" />
|
||
<span>设置月度积分总预算</span>
|
||
</div>
|
||
<div class="example-item">
|
||
<Icon name="chartBar" size="md" color="#31f08b" />
|
||
<span>分项预算(任务、好评、竞赛等)</span>
|
||
</div>
|
||
<div class="example-item">
|
||
<Icon name="bell" size="md" color="#F59E0B" />
|
||
<span>超支预警和自动控制</span>
|
||
</div>
|
||
</div>
|
||
<a-button type="primary" size="large" @click="goToScorePool" style="margin-top: 24px;">
|
||
进入积分池管理
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 钉钉集成 -->
|
||
<a-tab-pane key="dingtalk" tab="钉钉集成">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">钉钉集成配置</h2>
|
||
</div>
|
||
|
||
<a-form :model="dingTalkConfig" layout="vertical" class="dingtalk-form">
|
||
<a-alert
|
||
v-if="!dingTalkConfig.enabled"
|
||
message="钉钉集成未启用"
|
||
description="配置钉钉应用信息后,可启用钉钉通知、钉钉登录等功能。"
|
||
type="info"
|
||
show-icon
|
||
style="margin-bottom: 24px;"
|
||
/>
|
||
|
||
<a-divider orientation="left">应用配置</a-divider>
|
||
<a-row :gutter="16">
|
||
<a-col :span="8">
|
||
<a-form-item label="AgentId">
|
||
<a-input v-model:value="dingTalkConfig.agentId" placeholder="钉钉应用AgentId" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="8">
|
||
<a-form-item label="AppKey">
|
||
<a-input v-model:value="dingTalkConfig.appKey" placeholder="钉钉应用AppKey" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="8">
|
||
<a-form-item label="AppSecret">
|
||
<a-input-password v-model:value="dingTalkConfig.appSecret" placeholder="钉钉应用AppSecret" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-form-item label="企业ID (CorpId)">
|
||
<a-input v-model:value="dingTalkConfig.corpId" placeholder="钉钉企业ID" />
|
||
</a-form-item>
|
||
|
||
<a-divider orientation="left">机器人配置(可选)</a-divider>
|
||
<a-form-item label="机器人Webhook地址">
|
||
<a-input v-model:value="dingTalkConfig.robotWebhook" placeholder="钉钉机器人Webhook" />
|
||
</a-form-item>
|
||
<a-form-item label="机器人密钥">
|
||
<a-input-password v-model:value="dingTalkConfig.robotSecret" placeholder="加签密钥(可选)" />
|
||
</a-form-item>
|
||
|
||
<a-divider orientation="left">启用功能</a-divider>
|
||
<a-form-item>
|
||
<a-checkbox-group v-model:value="enabledFeaturesList">
|
||
<a-checkbox value="notification">消息通知</a-checkbox>
|
||
<a-checkbox value="login">钉钉登录</a-checkbox>
|
||
<a-checkbox value="sync">用户同步</a-checkbox>
|
||
</a-checkbox-group>
|
||
</a-form-item>
|
||
|
||
<a-form-item>
|
||
<a-space>
|
||
<a-button type="primary" @click="handleSaveDingTalk" :loading="savingDingTalk">
|
||
<template #icon><Icon name="check" size="sm" /></template>
|
||
保存配置
|
||
</a-button>
|
||
<a-button @click="handleTestDingTalk" :loading="testingDingTalk">
|
||
<template #icon><Icon name="bolt" size="sm" /></template>
|
||
测试连接
|
||
</a-button>
|
||
</a-space>
|
||
</a-form-item>
|
||
|
||
<a-divider orientation="left">群机器人消息测试</a-divider>
|
||
<a-form-item label="选择消息类型">
|
||
<a-radio-group v-model:value="testMessageType" button-style="solid">
|
||
<a-radio-button value="simple">简单测试</a-radio-button>
|
||
<a-radio-button value="task_publish">新任务发布</a-radio-button>
|
||
<a-radio-button value="task_assign">任务指派</a-radio-button>
|
||
<a-radio-button value="task_reject">任务驳回</a-radio-button>
|
||
<a-radio-button value="subtask_publish">子任务组发布</a-radio-button>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
<a-form-item>
|
||
<a-button type="primary" @click="handleSendTestMessage" :loading="sendingTestMessage" :disabled="!dingTalkConfig.robotWebhook">
|
||
<template #icon><Icon name="send" size="sm" /></template>
|
||
发送群消息
|
||
</a-button>
|
||
<span class="test-message-hint">发送到群机器人,群成员都能看到</span>
|
||
</a-form-item>
|
||
|
||
<a-divider orientation="left">个人工作通知测试</a-divider>
|
||
<a-alert
|
||
message="发送给个人的工作通知"
|
||
description="工作通知是企业内部应用发送的消息,只有接收人本人能看到,适合任务提醒、积分变动等私密通知。"
|
||
type="info"
|
||
show-icon
|
||
style="margin-bottom: 16px;"
|
||
/>
|
||
<a-form-item label="接收人">
|
||
<a-input v-model:value="personalMessageUser" placeholder="输入用户名或真实姓名,如:南亭" style="width: 200px;" />
|
||
</a-form-item>
|
||
<a-form-item label="消息类型">
|
||
<a-radio-group v-model:value="personalMessageType" button-style="solid">
|
||
<a-radio-button value="simple">简单测试</a-radio-button>
|
||
<a-radio-button value="task_publish">新任务发布</a-radio-button>
|
||
<a-radio-button value="task_assign">任务指派</a-radio-button>
|
||
<a-radio-button value="task_complete">任务完成</a-radio-button>
|
||
<a-radio-button value="score_change">积分变动</a-radio-button>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
<a-form-item>
|
||
<a-button type="primary" @click="handleSendPersonalMessage" :loading="sendingPersonalMessage" :disabled="!personalMessageUser || !dingTalkConfig.appKey">
|
||
<template #icon><Icon name="send" size="sm" /></template>
|
||
发送个人消息
|
||
</a-button>
|
||
<span class="test-message-hint">仅接收人本人可见</span>
|
||
</a-form-item>
|
||
|
||
<a-divider orientation="left">部门同步</a-divider>
|
||
<a-alert
|
||
message="部门同步说明"
|
||
description="同步钉钉部门到系统:自动获取所有钉钉部门并设置ding_dept_id。如果钉钉部门在系统中不存在,会自动创建。同步后,用户登录时就能自动匹配到部门了。"
|
||
type="info"
|
||
show-icon
|
||
style="margin-bottom: 16px;"
|
||
/>
|
||
<a-form-item>
|
||
<a-button type="primary" @click="handleSyncDepartments" :loading="syncingDepartments" :disabled="!dingTalkConfig.appKey || !dingTalkConfig.appSecret">
|
||
<template #icon><Icon name="sync" size="sm" /></template>
|
||
同步钉钉部门
|
||
</a-button>
|
||
</a-form-item>
|
||
</a-form>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 导航栏管理 -->
|
||
<a-tab-pane key="navbar" tab="导航栏管理">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">导航栏显示配置</h2>
|
||
<p class="section-description">管理导航栏中主菜单和功能按钮的显示状态</p>
|
||
</div>
|
||
|
||
<a-form :model="navbarConfig" layout="vertical" class="navbar-config-form">
|
||
<a-divider orientation="left">
|
||
<Icon name="menu" size="sm" style="margin-right: 8px;" />
|
||
主菜单项
|
||
</a-divider>
|
||
|
||
<div class="config-grid">
|
||
<div class="config-item">
|
||
<div class="config-item-header">
|
||
<Icon name="clipboard" size="md" color="#31f08b" />
|
||
<span class="config-item-title">任务大厅</span>
|
||
</div>
|
||
<a-switch
|
||
v-model:checked="navbarConfig.menuItems.tasks"
|
||
checked-children="显示"
|
||
un-checked-children="隐藏"
|
||
/>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<div class="config-item-header">
|
||
<Icon name="users" size="md" color="#31f08b" />
|
||
<span class="config-item-title">设计师大厅</span>
|
||
</div>
|
||
<a-switch
|
||
v-model:checked="navbarConfig.menuItems.designer_hall"
|
||
checked-children="显示"
|
||
un-checked-children="隐藏"
|
||
/>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<div class="config-item-header">
|
||
<Icon name="trophy" size="md" color="#31f08b" />
|
||
<span class="config-item-title">激励中心</span>
|
||
</div>
|
||
<a-switch
|
||
v-model:checked="navbarConfig.menuItems.incentive"
|
||
checked-children="显示"
|
||
un-checked-children="隐藏"
|
||
/>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<div class="config-item-header">
|
||
<Icon name="chart" size="md" color="#31f08b" />
|
||
<span class="config-item-title">数据分析</span>
|
||
</div>
|
||
<a-switch
|
||
v-model:checked="navbarConfig.menuItems.analytics"
|
||
checked-children="显示"
|
||
un-checked-children="隐藏"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<a-divider orientation="left">
|
||
<Icon name="bolt" size="sm" style="margin-right: 8px;" />
|
||
功能按钮
|
||
</a-divider>
|
||
|
||
<div class="config-grid">
|
||
<div class="config-item">
|
||
<div class="config-item-header">
|
||
<Icon name="bell" size="md" color="#31f08b" />
|
||
<span class="config-item-title">通知按钮</span>
|
||
</div>
|
||
<a-switch
|
||
v-model:checked="navbarConfig.actions.notification"
|
||
checked-children="显示"
|
||
un-checked-children="隐藏"
|
||
/>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<div class="config-item-header">
|
||
<Icon name="calendar" size="md" color="#31f08b" />
|
||
<span class="config-item-title">签到按钮</span>
|
||
</div>
|
||
<a-switch
|
||
v-model:checked="navbarConfig.actions.checkin"
|
||
checked-children="显示"
|
||
un-checked-children="隐藏"
|
||
/>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<div class="config-item-header">
|
||
<Icon name="trophy" size="md" color="#31f08b" />
|
||
<span class="config-item-title">成就按钮</span>
|
||
</div>
|
||
<a-switch
|
||
v-model:checked="navbarConfig.actions.achievement"
|
||
checked-children="显示"
|
||
un-checked-children="隐藏"
|
||
/>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<div class="config-item-header">
|
||
<Icon name="settings" size="md" color="#31f08b" />
|
||
<span class="config-item-title">设置按钮</span>
|
||
</div>
|
||
<a-switch
|
||
v-model:checked="navbarConfig.actions.settings"
|
||
checked-children="显示"
|
||
un-checked-children="隐藏"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<a-form-item style="margin-top: 32px;">
|
||
<a-space>
|
||
<a-button type="primary" @click="handleSaveNavbarConfig" :loading="savingNavbarConfig">
|
||
<template #icon><Icon name="check" size="sm" /></template>
|
||
保存配置
|
||
</a-button>
|
||
<a-button @click="handleResetNavbarConfig">
|
||
<template #icon><Icon name="refresh" size="sm" /></template>
|
||
恢复默认
|
||
</a-button>
|
||
</a-space>
|
||
</a-form-item>
|
||
</a-form>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- 任务发布配置 -->
|
||
<a-tab-pane key="task-release" tab="任务发布配置">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">任务发布类型配置</h2>
|
||
<p class="section-description">管理发起任务页面中可用的发布类型选项</p>
|
||
</div>
|
||
|
||
<a-form :model="taskReleaseConfig" layout="vertical" class="navbar-config-form">
|
||
<a-divider orientation="left">
|
||
<Icon name="settings" size="sm" style="margin-right: 8px;" />
|
||
发布类型按钮
|
||
</a-divider>
|
||
|
||
<div class="config-grid">
|
||
<div class="config-item">
|
||
<div class="config-item-header">
|
||
<Icon name="arrowPath" size="md" color="#31f08b" />
|
||
<span class="config-item-title">默认模式</span>
|
||
</div>
|
||
<div class="config-item-desc">系统自动分配给主管,由主管负责分配</div>
|
||
<a-switch
|
||
v-model:checked="taskReleaseConfig.default"
|
||
checked-children="显示"
|
||
un-checked-children="隐藏"
|
||
/>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<div class="config-item-header">
|
||
<Icon name="bell" size="md" color="#31f08b" />
|
||
<span class="config-item-title">招募任务</span>
|
||
</div>
|
||
<div class="config-item-desc">发布到任务大厅,设计师可自行抢单</div>
|
||
<a-switch
|
||
v-model:checked="taskReleaseConfig.recruitment"
|
||
checked-children="显示"
|
||
un-checked-children="隐藏"
|
||
/>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<div class="config-item-header">
|
||
<Icon name="star" size="md" color="#31f08b" />
|
||
<span class="config-item-title">指定任务</span>
|
||
</div>
|
||
<div class="config-item-desc">指定特定设计师接收任务</div>
|
||
<a-switch
|
||
v-model:checked="taskReleaseConfig.assigned"
|
||
checked-children="显示"
|
||
un-checked-children="隐藏"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<a-alert
|
||
message="提示"
|
||
description="至少需要保留一个发布类型。如果全部关闭,系统将自动启用默认模式。"
|
||
type="info"
|
||
show-icon
|
||
style="margin-top: 16px; margin-bottom: 16px;"
|
||
/>
|
||
|
||
<a-form-item style="margin-top: 32px;">
|
||
<a-space>
|
||
<a-button type="primary" @click="handleSaveTaskReleaseConfig" :loading="savingTaskReleaseConfig">
|
||
<template #icon><Icon name="check" size="sm" /></template>
|
||
保存配置
|
||
</a-button>
|
||
<a-button @click="handleResetTaskReleaseConfig">
|
||
<template #icon><Icon name="refresh" size="sm" /></template>
|
||
恢复默认
|
||
</a-button>
|
||
</a-space>
|
||
</a-form-item>
|
||
</a-form>
|
||
</div>
|
||
</a-tab-pane>
|
||
|
||
<!-- S3存储配置 -->
|
||
<a-tab-pane key="s3-config" tab="S3存储配置">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<h2 class="section-title">S3存储配置管理</h2>
|
||
<a-button type="primary" @click="showS3ConfigModal()">
|
||
<template #icon><Icon name="plus" size="sm" /></template>
|
||
添加配置
|
||
</a-button>
|
||
</div>
|
||
|
||
<a-table
|
||
:columns="s3ConfigColumns"
|
||
:data-source="s3Configs"
|
||
:loading="loadingS3Configs"
|
||
row-key="id"
|
||
:pagination="{ pageSize: 10 }"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'isActive'">
|
||
<a-tag :color="record.isActive ? 'green' : 'default'">
|
||
{{ record.isActive ? '已激活' : '未激活' }}
|
||
</a-tag>
|
||
</template>
|
||
<template v-else-if="column.key === 'secretAccessKey'">
|
||
<span>******</span>
|
||
</template>
|
||
<template v-else-if="column.key === 'actions'">
|
||
<a-space>
|
||
<a-button type="link" size="small" @click="showS3ConfigModal(record)">编辑</a-button>
|
||
<a-button
|
||
v-if="!record.isActive"
|
||
type="link"
|
||
size="small"
|
||
@click="handleActivateS3Config(record.id)"
|
||
>
|
||
激活
|
||
</a-button>
|
||
<a-popconfirm
|
||
v-if="!record.isActive"
|
||
title="确定删除?"
|
||
@confirm="handleDeleteS3Config(record.id)"
|
||
>
|
||
<a-button type="link" danger size="small">删除</a-button>
|
||
</a-popconfirm>
|
||
</a-space>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
|
||
<!-- 需求类型弹窗 -->
|
||
<a-modal
|
||
v-model:open="modals.demandType"
|
||
:title="editingItem ? '编辑需求类型' : '添加需求类型'"
|
||
@ok="saveDemandType"
|
||
@cancel="closeModal('demandType')"
|
||
>
|
||
<a-form :model="formData.demandType" layout="vertical">
|
||
<a-form-item label="类型名称" required>
|
||
<a-input v-model:value="formData.demandType.name" placeholder="请输入类型名称" />
|
||
</a-form-item>
|
||
<a-form-item label="单价积分" required>
|
||
<a-input-number
|
||
v-model:value="formData.demandType.unitScore"
|
||
:min="0"
|
||
:precision="0"
|
||
style="width: 100%"
|
||
placeholder="请输入单价积分"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="预计时长(小时)" required>
|
||
<a-input-number
|
||
v-model:value="formData.demandType.estimatedHours"
|
||
:min="0"
|
||
:precision="1"
|
||
style="width: 100%"
|
||
placeholder="请输入预计时长"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="描述">
|
||
<a-textarea v-model:value="formData.demandType.description" :rows="3" placeholder="请输入描述" />
|
||
</a-form-item>
|
||
<a-form-item label="可接单部门">
|
||
<a-select
|
||
v-model:value="formData.demandType.departmentIds"
|
||
mode="multiple"
|
||
placeholder="请选择可接单部门(不选则所有接单部门都可接)"
|
||
allow-clear
|
||
:max-tag-count="3"
|
||
>
|
||
<a-select-option
|
||
v-for="dept in receivingDepartments"
|
||
:key="dept.id"
|
||
:value="dept.id"
|
||
>
|
||
{{ dept.deptName }}
|
||
</a-select-option>
|
||
</a-select>
|
||
<div class="form-tip">
|
||
<Icon name="info" size="sm" color="#6B6B6B" style="margin-right: 4px;" />
|
||
限制哪些接单部门可以接这种类型的任务,不选则所有接单部门都可接
|
||
</div>
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
|
||
<!-- 任务子类型弹窗 -->
|
||
<a-modal
|
||
v-model:open="modals.taskSubType"
|
||
:title="editingItem ? '编辑任务子类型' : '添加任务子类型'"
|
||
@ok="saveTaskSubType"
|
||
@cancel="closeModal('taskSubType')"
|
||
>
|
||
<a-form :model="formData.taskSubType" layout="vertical">
|
||
<a-form-item label="子类型名称" required>
|
||
<a-input v-model:value="formData.taskSubType.name" placeholder="请输入子类型名称(如:新品、优化)" />
|
||
</a-form-item>
|
||
<a-form-item label="子类型编码" required>
|
||
<a-input v-model:value="formData.taskSubType.code" placeholder="留空自动生成">
|
||
<template #addonAfter>
|
||
<a-button size="small" type="link" @click="autoGenerateTaskSubTypeCode" style="padding: 0 8px;">
|
||
自动生成
|
||
</a-button>
|
||
</template>
|
||
</a-input>
|
||
</a-form-item>
|
||
<a-form-item label="描述">
|
||
<a-textarea v-model:value="formData.taskSubType.description" :rows="3" placeholder="请输入描述" />
|
||
</a-form-item>
|
||
<a-form-item label="排序">
|
||
<a-input-number
|
||
v-model:value="formData.taskSubType.sortOrder"
|
||
:min="0"
|
||
:precision="0"
|
||
style="width: 100%"
|
||
placeholder="排序值越小越靠前"
|
||
/>
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
|
||
<!-- 品牌弹窗 -->
|
||
<a-modal
|
||
v-model:open="modals.brand"
|
||
:title="editingItem ? '编辑品牌' : '添加品牌'"
|
||
@ok="saveBrand"
|
||
@cancel="closeModal('brand')"
|
||
>
|
||
<a-form :model="formData.brand" layout="vertical">
|
||
<a-form-item label="品牌名称" required>
|
||
<a-input v-model:value="formData.brand.name" placeholder="请输入品牌名称" />
|
||
</a-form-item>
|
||
<a-form-item label="品牌编码">
|
||
<a-input v-model:value="formData.brand.brandCode" placeholder="留空自动生成">
|
||
<template #addonAfter>
|
||
<a-button size="small" type="link" @click="autoGenerateBrandCode" style="padding: 0 8px;">
|
||
自动生成
|
||
</a-button>
|
||
</template>
|
||
</a-input>
|
||
</a-form-item>
|
||
<a-form-item label="品牌描述">
|
||
<a-textarea v-model:value="formData.brand.description" :rows="3" placeholder="请输入描述" />
|
||
</a-form-item>
|
||
<a-form-item label="排序">
|
||
<a-input-number
|
||
v-model:value="formData.brand.sortOrder"
|
||
:min="0"
|
||
style="width: 100%"
|
||
placeholder="数字越小越靠前"
|
||
/>
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
|
||
<!-- 平台弹窗 -->
|
||
<a-modal
|
||
v-model:open="modals.platform"
|
||
:title="editingItem ? '编辑平台' : '添加平台'"
|
||
@ok="savePlatform"
|
||
@cancel="closeModal('platform')"
|
||
>
|
||
<a-form :model="formData.platform" layout="vertical">
|
||
<a-form-item label="平台名称" required>
|
||
<a-input v-model:value="formData.platform.name" placeholder="请输入平台名称" />
|
||
</a-form-item>
|
||
<a-form-item label="平台编码">
|
||
<a-input v-model:value="formData.platform.platformCode" placeholder="留空自动生成">
|
||
<template #addonAfter>
|
||
<a-button size="small" type="link" @click="autoGeneratePlatformCode" style="padding: 0 8px;">
|
||
自动生成
|
||
</a-button>
|
||
</template>
|
||
</a-input>
|
||
</a-form-item>
|
||
<a-form-item label="平台描述">
|
||
<a-textarea v-model:value="formData.platform.description" :rows="3" placeholder="请输入描述" />
|
||
</a-form-item>
|
||
<a-form-item label="排序">
|
||
<a-input-number
|
||
v-model:value="formData.platform.sortOrder"
|
||
:min="0"
|
||
style="width: 100%"
|
||
placeholder="数字越小越靠前"
|
||
/>
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
|
||
<!-- 类目弹窗 -->
|
||
<a-modal
|
||
v-model:open="modals.category"
|
||
:title="editingItem ? '编辑类目' : '添加类目'"
|
||
@ok="saveCategory"
|
||
@cancel="closeModal('category')"
|
||
>
|
||
<a-form :model="formData.category" layout="vertical">
|
||
<a-form-item label="类目名称" required>
|
||
<a-input v-model:value="formData.category.name" placeholder="请输入类目名称" />
|
||
</a-form-item>
|
||
<a-form-item label="类目编码">
|
||
<a-input v-model:value="formData.category.categoryCode" placeholder="留空自动生成">
|
||
<template #addonAfter>
|
||
<a-button size="small" type="link" @click="autoGenerateCategoryCode" style="padding: 0 8px;">
|
||
自动生成
|
||
</a-button>
|
||
</template>
|
||
</a-input>
|
||
</a-form-item>
|
||
<a-form-item label="类目描述">
|
||
<a-textarea v-model:value="formData.category.description" :rows="3" placeholder="请输入描述" />
|
||
</a-form-item>
|
||
<a-form-item label="排序">
|
||
<a-input-number
|
||
v-model:value="formData.category.sortOrder"
|
||
:min="0"
|
||
style="width: 100%"
|
||
placeholder="数字越小越靠前"
|
||
/>
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
|
||
<!-- 调整用户部门 -->
|
||
<a-modal
|
||
v-model:open="transferModalVisible"
|
||
title="调整用户部门"
|
||
wrap-class-name="dark-modal"
|
||
@ok="handleTransferDepartment"
|
||
@cancel="closeTransferModal"
|
||
>
|
||
<div class="transfer-modal">
|
||
<p class="transfer-tip">
|
||
当前用户:
|
||
<span class="highlight">
|
||
{{ selectedUser?.realName || selectedUser?.username || '--' }}
|
||
</span>
|
||
</p>
|
||
<a-form layout="vertical">
|
||
<a-form-item label="选择部门">
|
||
<a-select
|
||
v-model:value="transferDeptId"
|
||
placeholder="请选择部门"
|
||
style="width: 100%"
|
||
>
|
||
<a-select-option :value="0">未分配部门</a-select-option>
|
||
<a-select-option
|
||
v-for="dept in departments"
|
||
:key="dept.id"
|
||
:value="dept.id"
|
||
>
|
||
{{ dept.deptName }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
</a-form>
|
||
</div>
|
||
</a-modal>
|
||
|
||
<!-- 部门弹窗 -->
|
||
<a-modal
|
||
v-model:open="modals.department"
|
||
:title="editingItem ? '编辑部门' : '添加部门'"
|
||
width="600px"
|
||
@ok="saveDepartment"
|
||
@cancel="closeModal('department')"
|
||
>
|
||
<a-form :model="formData.department" layout="vertical">
|
||
<a-form-item label="部门名称" required>
|
||
<a-input v-model:value="formData.department.deptName" placeholder="请输入部门名称" />
|
||
</a-form-item>
|
||
<a-form-item label="部门编码" required>
|
||
<a-input v-model:value="formData.department.deptCode" placeholder="请输入部门编码" />
|
||
</a-form-item>
|
||
<a-form-item label="部门类型" required>
|
||
<a-radio-group v-model:value="formData.department.deptType">
|
||
<a-radio value="demand">需求部门(只能发需求)</a-radio>
|
||
<a-radio value="receiving">接单部门(只能接任务)</a-radio>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
<a-form-item label="部门负责人">
|
||
<a-select
|
||
v-model:value="departmentLeaderId"
|
||
placeholder="请选择部门负责人"
|
||
allow-clear
|
||
show-search
|
||
:filter-option="filterUserOption"
|
||
>
|
||
<a-select-option
|
||
v-for="user in departmentUsers"
|
||
:key="user.id"
|
||
:value="user.id"
|
||
>
|
||
{{ user.realName || user.username }} ({{ user.username }})
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="上级部门">
|
||
<a-select v-model:value="departmentParentId" placeholder="请选择上级部门" allow-clear>
|
||
<a-select-option :value="0">顶级部门</a-select-option>
|
||
<a-select-option
|
||
v-for="dept in departments.filter(d => !editingItem || d.id !== editingItem.id)"
|
||
:key="dept.id"
|
||
:value="dept.id"
|
||
>
|
||
{{ dept.deptName }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item label="描述">
|
||
<a-textarea v-model:value="formData.department.description" :rows="3" placeholder="请输入描述" />
|
||
</a-form-item>
|
||
<a-form-item label="排序">
|
||
<a-input-number
|
||
v-model:value="formData.department.sortOrder"
|
||
:min="0"
|
||
style="width: 100%"
|
||
placeholder="数字越小越靠前"
|
||
/>
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
|
||
<!-- 积分池管理弹窗 -->
|
||
<a-modal
|
||
v-model:open="modals.scorePool"
|
||
title="积分池管理"
|
||
width="700px"
|
||
ok-text="OK"
|
||
cancel-text="Cancel"
|
||
@ok="modals.scorePool = false"
|
||
@cancel="modals.scorePool = false"
|
||
>
|
||
<div v-if="currentScorePool">
|
||
<a-descriptions :column="2" bordered>
|
||
<a-descriptions-item label="部门名称">
|
||
{{ currentScorePool.deptName }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="当前余额">
|
||
<span class="balance-display">
|
||
{{ currentScorePool.balance }} 分
|
||
</span>
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="累计充值">
|
||
{{ currentScorePool.totalRecharge || 0 }} 分
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="累计消耗">
|
||
{{ currentScorePool.totalConsume || 0 }} 分
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="累计获得" v-if="currentScorePool.totalEarn">
|
||
{{ currentScorePool.totalEarn }} 分
|
||
</a-descriptions-item>
|
||
</a-descriptions>
|
||
|
||
<a-divider>消耗限制设置</a-divider>
|
||
|
||
<a-descriptions :column="2" bordered>
|
||
<a-descriptions-item label="限制状态">
|
||
<a-tag :color="currentScorePool.limitEnabled ? 'green' : 'default'">
|
||
{{ currentScorePool.limitEnabled ? '已启用' : '未启用' }}
|
||
</a-tag>
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="限制周期" v-if="currentScorePool.limitEnabled">
|
||
{{ currentScorePool.limitPeriod === 'DAILY' ? '每日' :
|
||
currentScorePool.limitPeriod === 'WEEKLY' ? '每周' :
|
||
currentScorePool.limitPeriod === 'MONTHLY' ? '每月' : '未知' }}
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="周期限额" v-if="currentScorePool.limitEnabled">
|
||
{{ currentScorePool.periodLimit || 0 }} 分
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="已使用" v-if="currentScorePool.limitEnabled">
|
||
{{ currentScorePool.periodUsed || 0 }} 分
|
||
<span v-if="currentScorePool.periodLimit">
|
||
({{ Math.round((currentScorePool.periodUsed || 0) / currentScorePool.periodLimit * 100) }}%)
|
||
</span>
|
||
</a-descriptions-item>
|
||
</a-descriptions>
|
||
|
||
<a-divider>充值积分</a-divider>
|
||
|
||
<a-form layout="inline">
|
||
<a-form-item label="充值金额">
|
||
<a-input-number
|
||
v-model:value="rechargeAmount"
|
||
:min="1"
|
||
:precision="0"
|
||
style="width: 200px"
|
||
placeholder="请输入充值金额"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item>
|
||
<a-button type="primary" @click="handleRecharge">确认充值</a-button>
|
||
</a-form-item>
|
||
</a-form>
|
||
</div>
|
||
<a-empty v-else description="加载中..." />
|
||
</a-modal>
|
||
|
||
<!-- S3配置弹窗 -->
|
||
<a-modal
|
||
v-model:open="modals.s3Config"
|
||
:title="s3ConfigForm.id ? '编辑S3配置' : '添加S3配置'"
|
||
width="800px"
|
||
@ok="handleSaveS3Config"
|
||
@cancel="modals.s3Config = false"
|
||
>
|
||
<a-form :model="s3ConfigForm" layout="vertical">
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="服务端点" required>
|
||
<a-input v-model:value="s3ConfigForm.endpoint" placeholder="https://s3.1080p.hk" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="存储桶名称" required>
|
||
<a-input v-model:value="s3ConfigForm.bucket" placeholder="jsxiangche" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="访问密钥ID" required>
|
||
<a-input v-model:value="s3ConfigForm.accessKeyId" placeholder="AccessKey ID" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="访问密钥" required>
|
||
<a-input-password v-model:value="s3ConfigForm.secretAccessKey" placeholder="Secret Access Key" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="区域" required>
|
||
<a-input v-model:value="s3ConfigForm.region" placeholder="us-east-1" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="公共访问域名">
|
||
<a-input v-model:value="s3ConfigForm.publicDomain" placeholder="https://s3.1080p.hk/jsxiangche" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-form-item label="文件存储路径前缀">
|
||
<a-input v-model:value="s3ConfigForm.filePath" placeholder="由壹点AI/" />
|
||
</a-form-item>
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item>
|
||
<a-checkbox v-model:checked="s3ConfigForm.enablePathStyle">
|
||
启用路径样式访问
|
||
</a-checkbox>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item>
|
||
<a-checkbox v-model:checked="s3ConfigForm.isActive">
|
||
激活此配置
|
||
</a-checkbox>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
</a-form>
|
||
</a-modal>
|
||
</div>
|
||
</div>
|
||
</DefaultLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||
import { message } from 'ant-design-vue'
|
||
import Icon from '@/components/Icon.vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useSiteSettingsStore } from '@/stores/siteSettings'
|
||
import { useConfigStore } from '@/stores/config'
|
||
import { useNavbarConfigStore, type NavbarConfig } from '@/stores/navbarConfig'
|
||
import {
|
||
getDemandTypes, createDemandType, updateDemandType, deleteDemandType,
|
||
getBrands, createBrand, updateBrand, deleteBrand,
|
||
getPlatforms, createPlatform, updatePlatform, deletePlatform,
|
||
getCategories, createCategory, updateCategory, deleteCategory,
|
||
getAllTaskSubTypes, createTaskSubType, updateTaskSubType, deleteTaskSubType, toggleTaskSubTypeEnabled,
|
||
type DemandType, type Brand, type Platform, type Category, type TeamScorePool, type TaskSubType
|
||
} from '@/api/config'
|
||
import {
|
||
fetchDepartmentOverview,
|
||
createDepartment as createDepartmentApi,
|
||
updateDepartment as updateDepartmentApi,
|
||
deleteDepartment as deleteDepartmentApi,
|
||
updateLimitSettings,
|
||
adjustDepartmentScore,
|
||
getDepartmentScorePool,
|
||
type DepartmentOverview,
|
||
type DepartmentPayload,
|
||
type ScoreAdjustPayload,
|
||
type DeptType
|
||
} from '@/api/department'
|
||
import {
|
||
getSystemConfigsByGroup,
|
||
getSystemConfig,
|
||
batchUpdateSystemConfigs,
|
||
createSystemConfig,
|
||
updateSystemConfig,
|
||
type SystemConfig
|
||
} from '@/api/system'
|
||
import {
|
||
getAllUsers,
|
||
updateUserDepartment,
|
||
deleteUser,
|
||
type User
|
||
} from '@/api/user'
|
||
import {
|
||
getAllS3Configs,
|
||
createS3Config,
|
||
updateS3Config,
|
||
deleteS3Config,
|
||
activateS3Config,
|
||
type S3Config
|
||
} from '@/api/s3'
|
||
import request from '@/utils/request'
|
||
|
||
type LimitPeriod = 'DAILY' | 'WEEKLY' | 'MONTHLY'
|
||
|
||
interface DepartmentForm {
|
||
id?: number
|
||
deptName: string
|
||
deptCode?: string
|
||
deptType: DeptType
|
||
parentId?: number | null
|
||
leaderId?: number | null | undefined
|
||
description?: string
|
||
sortOrder?: number
|
||
status?: number
|
||
allowedDemandTypes: string[]
|
||
limitEnabled: boolean
|
||
limitPeriod: LimitPeriod
|
||
periodLimit?: number
|
||
alertThreshold?: number
|
||
overLimitAction: 'alert' | 'block'
|
||
initialScoreBalance?: number
|
||
}
|
||
|
||
const activeTab = ref('demand-types')
|
||
const editingItem = ref<any>(null)
|
||
const savingSiteSettings = ref(false)
|
||
const loadingSiteSettings = ref(false)
|
||
|
||
// 全局配置&站点设置 store
|
||
const configStore = useConfigStore()
|
||
const siteSettingsStore = useSiteSettingsStore()
|
||
const navbarConfigStore = useNavbarConfigStore()
|
||
|
||
// 网站设置
|
||
const router = useRouter()
|
||
|
||
const siteSettings = reactive({
|
||
siteName: 'DesignHub',
|
||
logoUrl: '',
|
||
registerEnabled: true,
|
||
siteNameConfigId: null as number | null,
|
||
logoConfigId: null as number | null,
|
||
registerEnabledConfigId: null as number | null
|
||
})
|
||
|
||
const loading = reactive({
|
||
demandTypes: false,
|
||
taskSubTypes: false,
|
||
brands: false,
|
||
platforms: false,
|
||
categories: false,
|
||
departments: false
|
||
})
|
||
|
||
const modals = reactive({
|
||
demandType: false,
|
||
taskSubType: false,
|
||
brand: false,
|
||
platform: false,
|
||
category: false,
|
||
department: false,
|
||
scorePool: false,
|
||
s3Config: false
|
||
})
|
||
|
||
const demandTypes = ref<DemandType[]>([])
|
||
const taskSubTypes = ref<TaskSubType[]>([])
|
||
const brands = ref<Brand[]>([])
|
||
const platforms = ref<Platform[]>([])
|
||
const categories = ref<Category[]>([])
|
||
const departments = ref<DepartmentOverview[]>([])
|
||
const departmentSearchText = ref<string>('')
|
||
const currentScorePool = ref<TeamScorePool | null>(null)
|
||
const currentDept = ref<DepartmentOverview | null>(null)
|
||
const scoreAdjustForm = reactive<ScoreAdjustPayload>({
|
||
action: 'recharge',
|
||
amount: 0,
|
||
remark: ''
|
||
})
|
||
const rechargeAmount = ref<number>(0)
|
||
|
||
const userLoading = ref(false)
|
||
const usersLoaded = ref(false)
|
||
const usersByDept = ref<Record<number, User[]>>({})
|
||
const transferModalVisible = ref(false)
|
||
const selectedUser = ref<User | null>(null)
|
||
const transferDeptId = ref<number | null>(null)
|
||
const allUsers = ref<User[]>([])
|
||
|
||
// S3配置相关
|
||
const s3Configs = ref<S3Config[]>([])
|
||
const loadingS3Configs = ref(false)
|
||
const s3ConfigForm = reactive<S3Config>({
|
||
endpoint: '',
|
||
bucket: '',
|
||
accessKeyId: '',
|
||
secretAccessKey: '',
|
||
region: 'us-east-1',
|
||
publicDomain: '',
|
||
enablePathStyle: true,
|
||
filePath: '',
|
||
isActive: false
|
||
})
|
||
|
||
const s3ConfigColumns = [
|
||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||
{ title: '服务端点', dataIndex: 'endpoint', key: 'endpoint' },
|
||
{ title: '存储桶', dataIndex: 'bucket', key: 'bucket' },
|
||
{ title: '区域', dataIndex: 'region', key: 'region', width: 120 },
|
||
{ title: '访问密钥ID', dataIndex: 'accessKeyId', key: 'accessKeyId' },
|
||
{ title: '访问密钥', dataIndex: 'secretAccessKey', key: 'secretAccessKey' },
|
||
{ title: '文件路径前缀', dataIndex: 'filePath', key: 'filePath' },
|
||
{ title: '状态', key: 'isActive', width: 100 },
|
||
{ title: '操作', key: 'actions', width: 200 }
|
||
]
|
||
|
||
const defaultDepartmentForm = (): DepartmentForm => ({
|
||
deptName: '',
|
||
deptCode: '',
|
||
deptType: 'demand',
|
||
parentId: 0,
|
||
leaderId: undefined,
|
||
description: '',
|
||
sortOrder: 0,
|
||
status: 1,
|
||
allowedDemandTypes: [],
|
||
limitEnabled: true,
|
||
limitPeriod: 'MONTHLY',
|
||
periodLimit: 500,
|
||
alertThreshold: 80,
|
||
overLimitAction: 'alert',
|
||
initialScoreBalance: 0
|
||
})
|
||
|
||
const formData = reactive({
|
||
demandType: {} as Partial<DemandType> & { departmentIds?: number[] },
|
||
taskSubType: {} as Partial<TaskSubType>,
|
||
brand: {} as Partial<Brand>,
|
||
platform: {} as Partial<Platform>,
|
||
category: {} as Partial<Category>,
|
||
department: defaultDepartmentForm()
|
||
})
|
||
|
||
const departmentUserCards = computed(() => {
|
||
return departments.value.map((dept) => ({
|
||
...dept,
|
||
users: (usersByDept.value[dept.id || 0] || []).slice().sort((a, b) => {
|
||
const nameA = a.realName || a.username || ''
|
||
const nameB = b.realName || b.username || ''
|
||
return nameA.localeCompare(nameB)
|
||
})
|
||
}))
|
||
})
|
||
|
||
const unassignedUsers = computed(() => {
|
||
const list = usersByDept.value[0] || []
|
||
return list.slice().sort((a, b) => {
|
||
const nameA = a.realName || a.username || ''
|
||
const nameB = b.realName || b.username || ''
|
||
return nameA.localeCompare(nameB)
|
||
})
|
||
})
|
||
|
||
// 过滤出当前部门的用户(用于负责人选择)
|
||
const departmentUsers = computed(() => {
|
||
// 部门负责人应该可以选择任何用户,不限制于该部门的成员
|
||
// 返回所有用户的排序列表
|
||
return allUsers.value.slice().sort((a, b) => {
|
||
const nameA = a.realName || a.username || ''
|
||
const nameB = b.realName || b.username || ''
|
||
return nameA.localeCompare(nameB)
|
||
})
|
||
})
|
||
|
||
// 过滤后的部门列表(用于搜索)
|
||
const filteredDepartments = computed(() => {
|
||
if (!departmentSearchText.value || !departmentSearchText.value.trim()) {
|
||
return departments.value
|
||
}
|
||
|
||
const searchText = departmentSearchText.value.trim().toLowerCase()
|
||
return departments.value.filter(dept => {
|
||
// 搜索部门名称
|
||
if (dept.deptName && dept.deptName.toLowerCase().includes(searchText)) {
|
||
return true
|
||
}
|
||
// 搜索部门编码
|
||
if (dept.deptCode && dept.deptCode.toLowerCase().includes(searchText)) {
|
||
return true
|
||
}
|
||
// 搜索负责人名称
|
||
if (dept.leaderName && dept.leaderName.toLowerCase().includes(searchText)) {
|
||
return true
|
||
}
|
||
return false
|
||
})
|
||
})
|
||
|
||
const hasUserData = computed(() => {
|
||
return unassignedUsers.value.length > 0 || departmentUserCards.value.some((dept) => dept.users.length > 0)
|
||
})
|
||
|
||
// 用户管理表格列定义
|
||
const userManagementColumns = [
|
||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||
{ title: '用户信息', key: 'userInfo', width: 280 },
|
||
{ title: '所属部门', key: 'department', width: 180 },
|
||
{ title: '管理员', key: 'isAdmin', width: 100 },
|
||
{ title: '状态', key: 'status', width: 100 },
|
||
{ title: '创建时间', key: 'createTime', width: 160 },
|
||
{ title: '操作', key: 'actions', width: 320, fixed: 'right' }
|
||
]
|
||
|
||
// 所有用户的扁平列表(用于表格展示)
|
||
const allUsersList = computed(() => {
|
||
const allUsers: any[] = []
|
||
|
||
// 添加未分配部门的用户
|
||
const unassigned = unassignedUsers.value.map(user => ({
|
||
...user,
|
||
deptName: null,
|
||
deptType: null
|
||
}))
|
||
allUsers.push(...unassigned)
|
||
|
||
// 添加各部门的用户
|
||
departmentUserCards.value.forEach(dept => {
|
||
const deptUsers = dept.users.map(user => ({
|
||
...user,
|
||
deptName: dept.deptName,
|
||
deptType: dept.deptType
|
||
}))
|
||
allUsers.push(...deptUsers)
|
||
})
|
||
|
||
// 按用户名排序
|
||
return allUsers.sort((a, b) => {
|
||
const nameA = a.realName || a.username || ''
|
||
const nameB = b.realName || b.username || ''
|
||
return nameA.localeCompare(nameB)
|
||
})
|
||
})
|
||
|
||
// 格式化时间
|
||
const formatUserTime = (time: string | undefined) => {
|
||
if (!time) return '--'
|
||
return time.replace('T', ' ').substring(0, 16)
|
||
}
|
||
|
||
// 接单部门列表(用于需求类型的部门选择)
|
||
const receivingDepartments = computed(() => {
|
||
return departments.value.filter(dept => dept.deptType === 'receiving')
|
||
})
|
||
|
||
const leaderOptions = computed(() =>
|
||
allUsers.value.map((user) => ({
|
||
label: user.realName || user.username || `用户${user.id}`,
|
||
value: user.id
|
||
}))
|
||
)
|
||
|
||
const limitPeriodOptions = [
|
||
{ label: '每日', value: 'DAILY' as LimitPeriod },
|
||
{ label: '每周', value: 'WEEKLY' as LimitPeriod },
|
||
{ label: '每月', value: 'MONTHLY' as LimitPeriod }
|
||
]
|
||
|
||
const getLimitPeriodLabel = (value?: string) => {
|
||
switch (value) {
|
||
case 'DAILY':
|
||
return '每日'
|
||
case 'WEEKLY':
|
||
return '每周'
|
||
case 'MONTHLY':
|
||
return '每月'
|
||
default:
|
||
return '未配置'
|
||
}
|
||
}
|
||
|
||
// 表格列定义
|
||
const demandTypeColumns = [
|
||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||
{ title: '类型名称', dataIndex: 'name', key: 'name' },
|
||
{ title: '单价积分', dataIndex: 'unitScore', key: 'unitScore' },
|
||
{ title: '预计时长(h)', dataIndex: 'estimatedHours', key: 'estimatedHours' },
|
||
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
|
||
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
|
||
{ title: '状态', key: 'status', width: 100 },
|
||
{ title: '操作', key: 'actions', width: 150 }
|
||
]
|
||
|
||
const taskSubTypeColumns = [
|
||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||
{ title: '子类型名称', dataIndex: 'name', key: 'name' },
|
||
{ title: '子类型编码', dataIndex: 'code', key: 'code' },
|
||
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
|
||
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
|
||
{ title: '状态', key: 'status', width: 100 },
|
||
{ title: '操作', key: 'actions', width: 150 }
|
||
]
|
||
|
||
const brandColumns = [
|
||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||
{ title: '品牌名称', dataIndex: 'name', key: 'name' },
|
||
{ title: '品牌编码', dataIndex: 'brandCode', key: 'brandCode' },
|
||
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
|
||
{ title: '状态', key: 'status', width: 100 },
|
||
{ title: '操作', key: 'actions', width: 150 }
|
||
]
|
||
|
||
const platformColumns = [
|
||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||
{ title: '平台名称', dataIndex: 'name', key: 'name' },
|
||
{ title: '平台编码', dataIndex: 'platformCode', key: 'platformCode' },
|
||
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
|
||
{ title: '状态', key: 'status', width: 100 },
|
||
{ title: '操作', key: 'actions', width: 150 }
|
||
]
|
||
|
||
const categoryColumns = [
|
||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||
{ title: '类目名称', dataIndex: 'name', key: 'name' },
|
||
{ title: '类目编码', dataIndex: 'categoryCode', key: 'categoryCode' },
|
||
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
|
||
{ title: '状态', key: 'status', width: 100 },
|
||
{ title: '操作', key: 'actions', width: 150 }
|
||
]
|
||
|
||
const departmentColumns = [
|
||
{ title: '部门信息', key: 'deptInfo' },
|
||
{ title: '负责人', key: 'leader', width: 160 },
|
||
{ title: '成员数', key: 'memberCount', width: 120 },
|
||
{ title: '积分余额', key: 'score', width: 160 },
|
||
{ title: '本周期消耗', key: 'limit', width: 200 },
|
||
{ title: '预警', key: 'alert', width: 120 },
|
||
{ title: '状态', key: 'status', width: 120 },
|
||
{ title: '操作', key: 'actions', width: 240 }
|
||
]
|
||
|
||
// 加载数据
|
||
const loadAllData = async () => {
|
||
await Promise.all([
|
||
loadDemandTypes(),
|
||
loadTaskSubTypes(),
|
||
loadBrands(),
|
||
loadPlatforms(),
|
||
loadCategories(),
|
||
loadDepartments()
|
||
])
|
||
}
|
||
|
||
const loadDemandTypes = async () => {
|
||
loading.demandTypes = true
|
||
try {
|
||
const response = await getDemandTypes()
|
||
console.log('需求类型API响应:', response)
|
||
console.log('需求类型数据:', response.data)
|
||
if (response.data && response.data.length > 0) {
|
||
console.log('第一条数据:', response.data[0])
|
||
}
|
||
demandTypes.value = response.data || []
|
||
} catch (error: any) {
|
||
console.error('加载需求类型失败:', error)
|
||
const errorMsg = error?.response?.data?.message || error?.message || '加载需求类型失败'
|
||
message.error(errorMsg)
|
||
} finally {
|
||
loading.demandTypes = false
|
||
}
|
||
}
|
||
|
||
const loadTaskSubTypes = async () => {
|
||
loading.taskSubTypes = true
|
||
try {
|
||
const response = await getAllTaskSubTypes()
|
||
taskSubTypes.value = response.data || []
|
||
} catch (error: any) {
|
||
console.error('加载任务子类型失败:', error)
|
||
const errorMsg = error?.response?.data?.message || error?.message || '加载任务子类型失败'
|
||
message.error(errorMsg)
|
||
} finally {
|
||
loading.taskSubTypes = false
|
||
}
|
||
}
|
||
|
||
const loadBrands = async () => {
|
||
loading.brands = true
|
||
try {
|
||
const response = await getBrands()
|
||
brands.value = response.data || []
|
||
} catch (error: any) {
|
||
console.error('加载品牌失败:', error)
|
||
const errorMsg = error?.response?.data?.message || error?.message || '加载品牌失败'
|
||
message.error(errorMsg)
|
||
} finally {
|
||
loading.brands = false
|
||
}
|
||
}
|
||
|
||
const loadPlatforms = async () => {
|
||
loading.platforms = true
|
||
try {
|
||
const response = await getPlatforms()
|
||
platforms.value = response.data || []
|
||
} catch (error: any) {
|
||
console.error('加载平台失败:', error)
|
||
const errorMsg = error?.response?.data?.message || error?.message || '加载平台失败'
|
||
message.error(errorMsg)
|
||
} finally {
|
||
loading.platforms = false
|
||
}
|
||
}
|
||
|
||
const loadCategories = async () => {
|
||
loading.categories = true
|
||
try {
|
||
const response = await getCategories()
|
||
categories.value = response.data || []
|
||
} catch (error: any) {
|
||
console.error('加载类目失败:', error)
|
||
const errorMsg = error?.response?.data?.message || error?.message || '加载类目失败'
|
||
message.error(errorMsg)
|
||
} finally {
|
||
loading.categories = false
|
||
}
|
||
}
|
||
|
||
const loadDepartments = async () => {
|
||
loading.departments = true
|
||
try {
|
||
const response = await fetchDepartmentOverview()
|
||
departments.value = response.data || []
|
||
} catch (error: any) {
|
||
console.error('加载部门失败:', error)
|
||
const errorMsg = error?.response?.data?.message || error?.message || '加载部门失败'
|
||
message.error(errorMsg)
|
||
} finally {
|
||
loading.departments = false
|
||
}
|
||
}
|
||
|
||
const loadDepartmentUsers = async () => {
|
||
userLoading.value = true
|
||
try {
|
||
const data = await getAllUsers()
|
||
if (!Array.isArray(data)) {
|
||
throw new Error('返回格式不正确')
|
||
}
|
||
allUsers.value = data
|
||
const grouped: Record<number, User[]> = {}
|
||
data.forEach((user) => {
|
||
const key = user.deptId || 0
|
||
if (!grouped[key]) {
|
||
grouped[key] = []
|
||
}
|
||
grouped[key].push(user)
|
||
})
|
||
usersByDept.value = grouped
|
||
usersLoaded.value = true
|
||
} catch (error) {
|
||
console.error('加载用户失败:', error)
|
||
message.error('加载用户列表失败')
|
||
} finally {
|
||
userLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 过滤用户选项(用于下拉搜索)
|
||
const filterUserOption = (input: string, option: any) => {
|
||
const user = allUsers.value.find(u => u.id === option.value)
|
||
if (!user) return false
|
||
const searchText = input.toLowerCase()
|
||
const realName = (user.realName || '').toLowerCase()
|
||
const username = (user.username || '').toLowerCase()
|
||
return realName.includes(searchText) || username.includes(searchText)
|
||
}
|
||
|
||
// 部门搜索处理
|
||
const onDepartmentSearch = () => {
|
||
// 搜索逻辑通过 computed 属性 filteredDepartments 自动处理
|
||
// 此方法仅用于响应搜索事件,实际过滤在 computed 中完成
|
||
}
|
||
|
||
// 显示积分池管理弹窗
|
||
const showScorePoolModal = async (dept: DepartmentOverview) => {
|
||
if (!dept.id) return
|
||
|
||
try {
|
||
const response = await getDepartmentScorePool(dept.id)
|
||
if (response.data) {
|
||
currentScorePool.value = response.data
|
||
currentDept.value = dept
|
||
scoreAdjustForm.action = 'recharge'
|
||
scoreAdjustForm.amount = 0
|
||
scoreAdjustForm.remark = ''
|
||
rechargeAmount.value = 0
|
||
modals.scorePool = true
|
||
} else {
|
||
message.warning('该部门暂无积分池')
|
||
}
|
||
} catch (error) {
|
||
message.error('加载积分池信息失败')
|
||
}
|
||
}
|
||
|
||
// 处理积分充值
|
||
const handleRecharge = async () => {
|
||
if (!currentDept.value?.id) {
|
||
message.error('未找到部门信息')
|
||
return
|
||
}
|
||
if (!rechargeAmount.value || rechargeAmount.value <= 0) {
|
||
message.error('请输入有效的充值金额')
|
||
return
|
||
}
|
||
|
||
try {
|
||
await adjustDepartmentScore(currentDept.value.id, {
|
||
action: 'recharge',
|
||
amount: rechargeAmount.value,
|
||
remark: '管理员充值'
|
||
})
|
||
message.success('充值成功')
|
||
// 刷新积分池信息
|
||
await showScorePoolModal(currentDept.value)
|
||
// 刷新部门列表
|
||
await loadDepartments()
|
||
} catch (error: any) {
|
||
message.error(error?.response?.data?.message || '充值失败')
|
||
}
|
||
}
|
||
|
||
// 调整积分
|
||
const handleScoreAdjust = async () => {
|
||
if (!currentDept.value?.id) {
|
||
message.error('未找到部门信息')
|
||
return
|
||
}
|
||
if (!scoreAdjustForm.amount || scoreAdjustForm.amount <= 0) {
|
||
message.error('请输入有效的积分数')
|
||
return
|
||
}
|
||
try {
|
||
await adjustDepartmentScore(currentDept.value.id, {
|
||
action: scoreAdjustForm.action,
|
||
amount: scoreAdjustForm.amount,
|
||
remark: scoreAdjustForm.remark
|
||
})
|
||
message.success('操作成功')
|
||
await showScorePoolModal(currentDept.value)
|
||
await loadDepartments()
|
||
} catch (error: any) {
|
||
message.error(error?.response?.data?.message || '操作失败')
|
||
}
|
||
}
|
||
|
||
// 自动生成编码的辅助函数
|
||
const generateCode = (name: string, prefix: string = ''): string => {
|
||
if (!name) return ''
|
||
|
||
// 移除特殊字符,只保留字母、数字和汉字
|
||
const cleanName = name.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '')
|
||
|
||
let code = ''
|
||
|
||
// 尝试提取英文字母
|
||
const englishChars = cleanName.match(/[a-zA-Z]+/g)
|
||
if (englishChars && englishChars.length > 0) {
|
||
code = englishChars.join('').toUpperCase().substring(0, 8)
|
||
}
|
||
|
||
// 如果没有英文字母,使用时间戳后6位
|
||
if (!code) {
|
||
code = 'CODE' + Date.now().toString().slice(-6)
|
||
}
|
||
|
||
// 添加前缀
|
||
if (prefix) {
|
||
code = prefix.toUpperCase() + '_' + code
|
||
}
|
||
|
||
return code
|
||
}
|
||
|
||
// 自动生成品牌编码
|
||
const autoGenerateBrandCode = () => {
|
||
if (!formData.brand.name) {
|
||
message.warning('请先输入品牌名称')
|
||
return
|
||
}
|
||
formData.brand.brandCode = generateCode(formData.brand.name, 'BR')
|
||
}
|
||
|
||
// 自动生成平台编码
|
||
const autoGeneratePlatformCode = () => {
|
||
if (!formData.platform.name) {
|
||
message.warning('请先输入平台名称')
|
||
return
|
||
}
|
||
formData.platform.platformCode = generateCode(formData.platform.name, 'PF')
|
||
}
|
||
|
||
// 自动生成类目编码
|
||
const autoGenerateCategoryCode = () => {
|
||
if (!formData.category.name) {
|
||
message.warning('请先输入类目名称')
|
||
return
|
||
}
|
||
formData.category.categoryCode = generateCode(formData.category.name, 'CAT')
|
||
}
|
||
|
||
// 自动生成任务子类型编码
|
||
const autoGenerateTaskSubTypeCode = () => {
|
||
if (!formData.taskSubType.name) {
|
||
message.warning('请先输入子类型名称')
|
||
return
|
||
}
|
||
// 任务子类型编码使用小写加下划线格式
|
||
let code = formData.taskSubType.name
|
||
.toLowerCase()
|
||
.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '_')
|
||
.replace(/_{2,}/g, '_')
|
||
.replace(/^_|_$/g, '')
|
||
|
||
// 如果是中文,简化处理
|
||
if (/[\u4e00-\u9fa5]/.test(code)) {
|
||
code = 'subtype_' + Date.now().toString().slice(-6)
|
||
}
|
||
|
||
formData.taskSubType.code = code
|
||
}
|
||
|
||
// 显示弹窗
|
||
const showDemandTypeModal = async (item?: DemandType) => {
|
||
editingItem.value = item
|
||
|
||
if (item) {
|
||
// 编辑模式:将allowedDeptIds(逗号分隔字符串)转换为数组
|
||
const departmentIds = item.allowedDeptIds
|
||
? item.allowedDeptIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id))
|
||
: []
|
||
formData.demandType = { ...item, departmentIds }
|
||
} else {
|
||
// 新建模式
|
||
formData.demandType = { sortOrder: 0, isEnabled: 1, departmentIds: [] }
|
||
}
|
||
|
||
modals.demandType = true
|
||
}
|
||
|
||
const showTaskSubTypeModal = (item?: TaskSubType) => {
|
||
editingItem.value = item
|
||
formData.taskSubType = item
|
||
? { ...item }
|
||
: { sortOrder: 0, isEnabled: 1 }
|
||
modals.taskSubType = true
|
||
}
|
||
|
||
const showBrandModal = (item?: Brand) => {
|
||
editingItem.value = item
|
||
formData.brand = item
|
||
? { ...item }
|
||
: { name: '', brandCode: '', description: '', logoUrl: '', sortOrder: 0, isEnabled: 1 }
|
||
modals.brand = true
|
||
}
|
||
|
||
const showPlatformModal = (item?: Platform) => {
|
||
editingItem.value = item
|
||
formData.platform = item
|
||
? { ...item }
|
||
: { name: '', platformCode: '', description: '', iconUrl: '', sortOrder: 0, isEnabled: 1 }
|
||
modals.platform = true
|
||
}
|
||
|
||
const showCategoryModal = (item?: Category) => {
|
||
editingItem.value = item
|
||
formData.category = item
|
||
? { ...item }
|
||
: { name: '', categoryCode: '', description: '', sortOrder: 0, parentId: 0, level: 1, isLeaf: 1, isEnabled: 1 }
|
||
modals.category = true
|
||
}
|
||
|
||
const showDepartmentModal = async (item?: DepartmentOverview) => {
|
||
if (!usersLoaded.value) {
|
||
await loadDepartmentUsers()
|
||
}
|
||
editingItem.value = item ?? null
|
||
if (item) {
|
||
formData.department = {
|
||
...defaultDepartmentForm(),
|
||
id: item.id,
|
||
deptName: item.deptName,
|
||
deptCode: item.deptCode,
|
||
deptType: item.deptType,
|
||
parentId: item.parentId ?? 0,
|
||
leaderId: item.leaderId ?? undefined,
|
||
description: item.description,
|
||
sortOrder: item.sortOrder ?? 0,
|
||
status: item.status ?? 1,
|
||
allowedDemandTypes: item.allowedDemandTypes ? [...item.allowedDemandTypes] : [],
|
||
limitEnabled: (item.limitEnabled ?? 0) === 1,
|
||
limitPeriod: (item.limitPeriod as LimitPeriod) || 'MONTHLY',
|
||
periodLimit: item.periodLimit ?? undefined,
|
||
alertThreshold: item.alertThreshold ?? 80,
|
||
overLimitAction: (item.overLimitAction as 'alert' | 'block') || 'alert',
|
||
initialScoreBalance: item.scoreBalance ?? 0
|
||
}
|
||
} else {
|
||
formData.department = defaultDepartmentForm()
|
||
}
|
||
modals.department = true
|
||
}
|
||
|
||
// 关闭弹窗
|
||
const closeModal = (type: string) => {
|
||
modals[type as keyof typeof modals] = false
|
||
editingItem.value = null
|
||
}
|
||
|
||
// 保存操作
|
||
const saveDemandType = async () => {
|
||
try {
|
||
// 构建保存数据,确保字段名正确
|
||
const saveData: Partial<DemandType> = {
|
||
name: formData.demandType.name,
|
||
unitScore: formData.demandType.unitScore,
|
||
estimatedHours: formData.demandType.estimatedHours,
|
||
description: formData.demandType.description,
|
||
sortOrder: formData.demandType.sortOrder ?? 0,
|
||
isEnabled: formData.demandType.isEnabled ?? 1,
|
||
// 将部门ID数组转换为逗号分隔的字符串
|
||
allowedDeptIds: formData.demandType.departmentIds?.length > 0
|
||
? formData.demandType.departmentIds.join(',')
|
||
: null
|
||
}
|
||
|
||
if (editingItem.value) {
|
||
await updateDemandType(editingItem.value.id, saveData)
|
||
message.success('更新成功')
|
||
} else {
|
||
await createDemandType(saveData)
|
||
message.success('创建成功')
|
||
}
|
||
|
||
closeModal('demandType')
|
||
await loadDemandTypes()
|
||
// 刷新全局配置store,通知其他页面更新
|
||
await configStore.refreshConfigs('demandType')
|
||
} catch (error: any) {
|
||
console.error('保存需求类型失败:', error)
|
||
const errorMsg = error?.response?.data?.message || error?.message || '保存失败'
|
||
message.error(errorMsg)
|
||
}
|
||
}
|
||
|
||
const saveTaskSubType = async () => {
|
||
try {
|
||
const saveData: Partial<TaskSubType> = {
|
||
name: formData.taskSubType.name,
|
||
code: formData.taskSubType.code,
|
||
description: formData.taskSubType.description,
|
||
sortOrder: formData.taskSubType.sortOrder ?? 0,
|
||
isEnabled: formData.taskSubType.isEnabled ?? 1
|
||
}
|
||
|
||
if (editingItem.value) {
|
||
await updateTaskSubType(editingItem.value.id, saveData)
|
||
message.success('更新成功')
|
||
} else {
|
||
await createTaskSubType(saveData)
|
||
message.success('创建成功')
|
||
}
|
||
|
||
closeModal('taskSubType')
|
||
await loadTaskSubTypes()
|
||
} catch (error: any) {
|
||
console.error('保存任务子类型失败:', error)
|
||
const errorMsg = error?.response?.data?.message || error?.message || '保存失败'
|
||
message.error(errorMsg)
|
||
}
|
||
}
|
||
|
||
const saveBrand = async () => {
|
||
try {
|
||
const saveData: Partial<Brand> = {
|
||
name: formData.brand.name,
|
||
brandCode: formData.brand.brandCode,
|
||
description: formData.brand.description,
|
||
logoUrl: formData.brand.logoUrl,
|
||
sortOrder: formData.brand.sortOrder ?? 0,
|
||
isEnabled: formData.brand.isEnabled ?? 1
|
||
}
|
||
|
||
if (editingItem.value) {
|
||
await updateBrand(editingItem.value.id, saveData)
|
||
message.success('更新成功')
|
||
} else {
|
||
await createBrand(saveData)
|
||
message.success('创建成功')
|
||
}
|
||
closeModal('brand')
|
||
await loadBrands()
|
||
// 刷新全局配置store,通知其他页面更新
|
||
await configStore.refreshConfigs('brand')
|
||
} catch (error: any) {
|
||
console.error('保存品牌失败:', error)
|
||
const errorMsg = error?.response?.data?.message || error?.message || '保存失败'
|
||
message.error(errorMsg)
|
||
}
|
||
}
|
||
|
||
const savePlatform = async () => {
|
||
try {
|
||
const saveData: Partial<Platform> = {
|
||
name: formData.platform.name,
|
||
platformCode: formData.platform.platformCode,
|
||
description: formData.platform.description,
|
||
iconUrl: formData.platform.iconUrl,
|
||
sortOrder: formData.platform.sortOrder ?? 0,
|
||
isEnabled: formData.platform.isEnabled ?? 1
|
||
}
|
||
|
||
if (editingItem.value) {
|
||
await updatePlatform(editingItem.value.id, saveData)
|
||
message.success('更新成功')
|
||
} else {
|
||
await createPlatform(saveData)
|
||
message.success('创建成功')
|
||
}
|
||
closeModal('platform')
|
||
await loadPlatforms()
|
||
// 刷新全局配置store,通知其他页面更新
|
||
await configStore.refreshConfigs('platform')
|
||
} catch (error: any) {
|
||
console.error('保存平台失败:', error)
|
||
const errorMsg = error?.response?.data?.message || error?.message || '保存失败'
|
||
message.error(errorMsg)
|
||
}
|
||
}
|
||
|
||
const saveCategory = async () => {
|
||
try {
|
||
const saveData: Partial<Category> = {
|
||
name: formData.category.name,
|
||
categoryCode: formData.category.categoryCode,
|
||
description: formData.category.description,
|
||
parentId: formData.category.parentId ?? 0,
|
||
level: formData.category.level ?? 1,
|
||
isLeaf: formData.category.isLeaf ?? 1,
|
||
sortOrder: formData.category.sortOrder ?? 0,
|
||
isEnabled: formData.category.isEnabled ?? 1
|
||
}
|
||
|
||
if (editingItem.value) {
|
||
await updateCategory(editingItem.value.id, saveData)
|
||
message.success('更新成功')
|
||
} else {
|
||
await createCategory(saveData)
|
||
message.success('创建成功')
|
||
}
|
||
closeModal('category')
|
||
await loadCategories()
|
||
// 刷新全局配置store,通知其他页面更新
|
||
await configStore.refreshConfigs('category')
|
||
} catch (error: any) {
|
||
console.error('保存类目失败:', error)
|
||
const errorMsg = error?.response?.data?.message || error?.message || '保存失败'
|
||
message.error(errorMsg)
|
||
}
|
||
}
|
||
|
||
const isDemandDept = computed(() => formData.department.deptType === 'demand')
|
||
const isReceivingDept = computed(() => formData.department.deptType === 'receiving')
|
||
|
||
// Computed属性用于处理leaderId和parentId的null到undefined的转换,以兼容a-select
|
||
const departmentLeaderId = computed({
|
||
get: () => formData.department.leaderId ?? undefined,
|
||
set: (val) => { formData.department.leaderId = val ?? undefined }
|
||
})
|
||
|
||
const departmentParentId = computed({
|
||
get: () => formData.department.parentId ?? undefined,
|
||
set: (val) => { formData.department.parentId = val ?? undefined }
|
||
})
|
||
|
||
const buildDepartmentPayload = (includeInitial: boolean): DepartmentPayload => {
|
||
const payload: DepartmentPayload = {
|
||
deptName: formData.department.deptName,
|
||
deptCode: formData.department.deptCode,
|
||
deptType: formData.department.deptType,
|
||
parentId: formData.department.parentId ?? 0,
|
||
// leaderId: null/undefined转换为0表示清除(后端会将0转换为null),数字表示设置
|
||
leaderId: formData.department.leaderId != null && formData.department.leaderId !== undefined ? formData.department.leaderId : 0,
|
||
sortOrder: formData.department.sortOrder,
|
||
status: formData.department.status,
|
||
allowedDemandTypes: isReceivingDept.value ? formData.department.allowedDemandTypes : [],
|
||
limitEnabled: isDemandDept.value ? (formData.department.limitEnabled ? 1 : 0) : undefined,
|
||
limitPeriod: isDemandDept.value ? formData.department.limitPeriod : undefined,
|
||
periodLimit: isDemandDept.value ? formData.department.periodLimit : undefined,
|
||
alertThreshold: isDemandDept.value ? formData.department.alertThreshold : undefined,
|
||
overLimitAction: isDemandDept.value ? formData.department.overLimitAction : undefined
|
||
}
|
||
if (includeInitial && isDemandDept.value) {
|
||
payload.initialScoreBalance = formData.department.initialScoreBalance
|
||
}
|
||
return payload
|
||
}
|
||
|
||
const saveDepartment = async () => {
|
||
try {
|
||
const isEdit = !!editingItem.value?.id
|
||
const payload = buildDepartmentPayload(!isEdit)
|
||
if (isEdit && editingItem.value?.id) {
|
||
delete payload.initialScoreBalance
|
||
await updateDepartmentApi(editingItem.value.id, payload)
|
||
message.success('更新成功')
|
||
} else {
|
||
await createDepartmentApi(payload)
|
||
message.success('创建成功')
|
||
}
|
||
closeModal('department')
|
||
await loadDepartments()
|
||
} catch (error: any) {
|
||
console.error('保存部门失败:', error)
|
||
const errorMsg = error?.response?.data?.message || error?.message || '保存失败'
|
||
message.error(errorMsg)
|
||
}
|
||
}
|
||
|
||
// 切换状态
|
||
const toggleStatus = async (type: string, id: number, checked: boolean) => {
|
||
try {
|
||
const isEnabled = checked ? 1 : 0
|
||
switch (type) {
|
||
case 'demandType':
|
||
await updateDemandType(id, { isEnabled } as Partial<DemandType>)
|
||
await loadDemandTypes()
|
||
await configStore.refreshConfigs('demandType')
|
||
break
|
||
case 'taskSubType':
|
||
await toggleTaskSubTypeEnabled(id)
|
||
await loadTaskSubTypes()
|
||
break
|
||
case 'brand':
|
||
await updateBrand(id, { isEnabled } as Brand)
|
||
await loadBrands()
|
||
await configStore.refreshConfigs('brand')
|
||
break
|
||
case 'platform':
|
||
await updatePlatform(id, { isEnabled } as Platform)
|
||
await loadPlatforms()
|
||
await configStore.refreshConfigs('platform')
|
||
break
|
||
case 'category':
|
||
await updateCategory(id, { isEnabled } as Category)
|
||
await loadCategories()
|
||
await configStore.refreshConfigs('category')
|
||
break
|
||
}
|
||
message.success('状态更新成功')
|
||
} catch (error) {
|
||
message.error('状态更新失败')
|
||
}
|
||
}
|
||
|
||
const toggleDepartmentStatus = async (id: number, checked: boolean) => {
|
||
try {
|
||
await updateDepartmentApi(id, { status: checked ? 1 : 0 })
|
||
await loadDepartments()
|
||
message.success('状态更新成功')
|
||
} catch (error: any) {
|
||
const errorMsg = error?.response?.data?.message || error?.message || '状态更新失败'
|
||
message.error(errorMsg)
|
||
}
|
||
}
|
||
|
||
// 删除操作
|
||
const deleteItem = async (type: string, id: number) => {
|
||
try {
|
||
switch (type) {
|
||
case 'demandType':
|
||
await deleteDemandType(id)
|
||
await loadDemandTypes()
|
||
break
|
||
case 'taskSubType':
|
||
await deleteTaskSubType(id)
|
||
await loadTaskSubTypes()
|
||
break
|
||
case 'brand':
|
||
await deleteBrand(id)
|
||
await loadBrands()
|
||
break
|
||
case 'platform':
|
||
await deletePlatform(id)
|
||
await loadPlatforms()
|
||
break
|
||
case 'category':
|
||
await deleteCategory(id)
|
||
await loadCategories()
|
||
break
|
||
}
|
||
message.success('删除成功')
|
||
} catch (error) {
|
||
message.error('删除失败')
|
||
}
|
||
}
|
||
|
||
const deleteDepartment = async (id: number) => {
|
||
try {
|
||
await deleteDepartmentApi(id)
|
||
await loadDepartments()
|
||
message.success('删除成功')
|
||
} catch (error: any) {
|
||
const errorMsg = error?.response?.data?.message || error?.message || '删除失败'
|
||
message.error(errorMsg)
|
||
}
|
||
}
|
||
|
||
const openTransferModal = (user: User) => {
|
||
selectedUser.value = user
|
||
transferDeptId.value = user.deptId ?? null
|
||
transferModalVisible.value = true
|
||
}
|
||
|
||
const closeTransferModal = () => {
|
||
transferModalVisible.value = false
|
||
selectedUser.value = null
|
||
}
|
||
|
||
const handleTransferDepartment = async () => {
|
||
if (!selectedUser.value) return
|
||
try {
|
||
const normalizedDeptId = transferDeptId.value && transferDeptId.value > 0 ? transferDeptId.value : null
|
||
await updateUserDepartment(selectedUser.value.id!, normalizedDeptId)
|
||
message.success('部门调整成功')
|
||
closeTransferModal()
|
||
// 同时刷新用户列表和部门数据(更新成员数)
|
||
await Promise.all([loadDepartmentUsers(), loadDepartments()])
|
||
} catch (error) {
|
||
console.error('调整部门失败:', error)
|
||
message.error('调整部门失败')
|
||
}
|
||
}
|
||
|
||
const deleteUserFromCard = async (user: User) => {
|
||
try {
|
||
await deleteUser(user.id!)
|
||
message.success('用户已删除')
|
||
await loadDepartmentUsers()
|
||
} catch (error) {
|
||
console.error('删除用户失败:', error)
|
||
message.error('删除用户失败')
|
||
}
|
||
}
|
||
|
||
const toggleUserAdmin = async (user: User) => {
|
||
try {
|
||
const newAdminStatus = user.isAdmin === 1 ? 0 : 1
|
||
await userApi.setAdmin(user.id!, newAdminStatus)
|
||
message.success(newAdminStatus === 1 ? '已设置为管理员' : '已取消管理员身份')
|
||
await loadDepartmentUsers()
|
||
} catch (error) {
|
||
console.error('设置管理员失败:', error)
|
||
message.error('设置管理员失败')
|
||
}
|
||
}
|
||
|
||
const goToUserManagement = () => {
|
||
router.push('/users')
|
||
}
|
||
|
||
const goToRewardRules = () => {
|
||
router.push('/admin/reward-rules')
|
||
}
|
||
|
||
const goToScorePool = () => {
|
||
router.push('/admin/score-pool')
|
||
}
|
||
|
||
// =============== 钉钉集成 ===============
|
||
import { getDingTalkConfig, saveDingTalkConfig, testDingTalkConnection, sendTestMessage, sendPersonalTestMessage, syncDingTalkDepartments } from '@/api/dingtalk'
|
||
|
||
const dingTalkConfig = reactive({
|
||
id: undefined as number | undefined,
|
||
name: '默认配置',
|
||
agentId: '',
|
||
appKey: '',
|
||
appSecret: '',
|
||
corpId: '',
|
||
robotWebhook: '',
|
||
robotSecret: '',
|
||
enabled: 0,
|
||
enabledFeatures: {} as Record<string, boolean>
|
||
})
|
||
|
||
const enabledFeaturesList = ref<string[]>([])
|
||
const savingDingTalk = ref(false)
|
||
const testingDingTalk = ref(false)
|
||
const sendingTestMessage = ref(false)
|
||
const syncingDepartments = ref(false)
|
||
|
||
const loadDingTalkConfig = async () => {
|
||
try {
|
||
const config = await getDingTalkConfig()
|
||
if (config) {
|
||
Object.assign(dingTalkConfig, config)
|
||
// 解析启用的功能
|
||
// enabledFeatures在数据库中存储为JSON字符串,需要解析
|
||
if (config.enabledFeatures) {
|
||
let featuresObj: Record<string, boolean> = {}
|
||
if (typeof config.enabledFeatures === 'string') {
|
||
// 如果是字符串,需要解析为对象
|
||
try {
|
||
featuresObj = JSON.parse(config.enabledFeatures)
|
||
} catch (e) {
|
||
console.error('解析enabledFeatures JSON失败:', e)
|
||
featuresObj = {}
|
||
}
|
||
} else {
|
||
// 如果已经是对象,直接使用
|
||
featuresObj = config.enabledFeatures as Record<string, boolean>
|
||
}
|
||
|
||
enabledFeaturesList.value = Object.entries(featuresObj)
|
||
.filter(([, v]) => v)
|
||
.map(([k]) => k)
|
||
} else {
|
||
enabledFeaturesList.value = []
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载钉钉配置失败:', error)
|
||
}
|
||
}
|
||
|
||
const handleSaveDingTalk = async () => {
|
||
savingDingTalk.value = true
|
||
try {
|
||
// 构建启用功能对象
|
||
const enabledFeatures: Record<string, boolean> = {}
|
||
enabledFeaturesList.value.forEach(f => {
|
||
enabledFeatures[f] = true
|
||
})
|
||
|
||
const configToSave = {
|
||
...dingTalkConfig,
|
||
enabled: 1,
|
||
enabledFeatures
|
||
}
|
||
|
||
await saveDingTalkConfig(configToSave as any)
|
||
message.success('钉钉配置保存成功')
|
||
// 重新加载配置以确保UI同步
|
||
await loadDingTalkConfig()
|
||
} catch (error: any) {
|
||
message.error(error.message || '保存失败')
|
||
} finally {
|
||
savingDingTalk.value = false
|
||
}
|
||
}
|
||
|
||
const handleTestDingTalk = async () => {
|
||
if (!dingTalkConfig.appKey || !dingTalkConfig.appSecret) {
|
||
message.warning('请先填写AppKey和AppSecret')
|
||
return
|
||
}
|
||
|
||
testingDingTalk.value = true
|
||
try {
|
||
const result = await testDingTalkConnection(dingTalkConfig as any)
|
||
if (result) {
|
||
message.success('钉钉连接测试成功!')
|
||
} else {
|
||
message.error('钉钉连接测试失败,请检查配置')
|
||
}
|
||
} catch (error: any) {
|
||
message.error(error.message || '连接测试失败')
|
||
} finally {
|
||
testingDingTalk.value = false
|
||
}
|
||
}
|
||
|
||
// 测试消息类型
|
||
const testMessageType = ref('simple')
|
||
|
||
// 个人消息测试
|
||
const personalMessageUser = ref('')
|
||
const personalMessageType = ref('simple')
|
||
const sendingPersonalMessage = ref(false)
|
||
|
||
const handleSendTestMessage = async () => {
|
||
if (!dingTalkConfig.robotWebhook) {
|
||
message.warning('请先配置机器人Webhook')
|
||
return
|
||
}
|
||
|
||
sendingTestMessage.value = true
|
||
try {
|
||
const result = await sendTestMessage(testMessageType.value)
|
||
if (result) {
|
||
const typeNames: Record<string, string> = {
|
||
'simple': '简单测试',
|
||
'task_publish': '新任务发布',
|
||
'task_assign': '任务指派',
|
||
'task_reject': '任务驳回',
|
||
'subtask_publish': '子任务组发布'
|
||
}
|
||
message.success(`"${typeNames[testMessageType.value]}"消息发送成功!`)
|
||
} else {
|
||
message.error('测试消息发送失败')
|
||
}
|
||
} catch (error: any) {
|
||
message.error(error.message || '发送失败')
|
||
} finally {
|
||
sendingTestMessage.value = false
|
||
}
|
||
}
|
||
|
||
const handleSyncDepartments = async () => {
|
||
if (!dingTalkConfig.appKey || !dingTalkConfig.appSecret) {
|
||
message.warning('请先填写AppKey和AppSecret')
|
||
return
|
||
}
|
||
|
||
syncingDepartments.value = true
|
||
try {
|
||
const count = await syncDingTalkDepartments()
|
||
message.success(`同步成功!共处理 ${count} 个部门`)
|
||
} catch (error: any) {
|
||
message.error(error.message || '同步失败')
|
||
} finally {
|
||
syncingDepartments.value = false
|
||
}
|
||
}
|
||
|
||
// 发送个人测试消息
|
||
const handleSendPersonalMessage = async () => {
|
||
if (!personalMessageUser.value.trim()) {
|
||
message.warning('请输入接收人用户名或真实姓名')
|
||
return
|
||
}
|
||
|
||
if (!dingTalkConfig.appKey || !dingTalkConfig.appSecret) {
|
||
message.warning('请先配置AppKey和AppSecret')
|
||
return
|
||
}
|
||
|
||
sendingPersonalMessage.value = true
|
||
try {
|
||
const result = await sendPersonalTestMessage(personalMessageUser.value.trim(), personalMessageType.value)
|
||
if (result.success) {
|
||
const typeNames: Record<string, string> = {
|
||
'simple': '简单测试',
|
||
'task_publish': '新任务发布',
|
||
'task_assign': '任务指派',
|
||
'task_complete': '任务完成',
|
||
'score_change': '积分变动'
|
||
}
|
||
message.success(`"${typeNames[personalMessageType.value]}"消息已发送给 ${result.userName}!`)
|
||
} else {
|
||
message.error(result.error || '消息发送失败')
|
||
}
|
||
} catch (error: any) {
|
||
message.error(error.message || '发送失败')
|
||
} finally {
|
||
sendingPersonalMessage.value = false
|
||
}
|
||
}
|
||
|
||
// ==================== 导航栏配置 ====================
|
||
const navbarConfig = ref<NavbarConfig>({
|
||
menuItems: {
|
||
home: true,
|
||
tasks: true,
|
||
designer_hall: true,
|
||
incentive: true,
|
||
analytics: true
|
||
},
|
||
actions: {
|
||
notification: true,
|
||
checkin: true,
|
||
achievement: true,
|
||
settings: true
|
||
}
|
||
})
|
||
|
||
const savingNavbarConfig = ref(false)
|
||
|
||
// 加载导航栏配置
|
||
const loadNavbarConfig = async () => {
|
||
try {
|
||
await navbarConfigStore.loadConfig()
|
||
navbarConfig.value = { ...navbarConfigStore.config }
|
||
} catch (error) {
|
||
console.error('加载导航栏配置失败:', error)
|
||
}
|
||
}
|
||
|
||
// 保存导航栏配置
|
||
const handleSaveNavbarConfig = async () => {
|
||
savingNavbarConfig.value = true
|
||
try {
|
||
await navbarConfigStore.saveConfig(navbarConfig.value)
|
||
message.success('导航栏配置保存成功')
|
||
} catch (error: any) {
|
||
message.error(error.message || '保存失败')
|
||
} finally {
|
||
savingNavbarConfig.value = false
|
||
}
|
||
}
|
||
|
||
// 恢复默认配置
|
||
const handleResetNavbarConfig = () => {
|
||
Modal.confirm({
|
||
title: '确认恢复默认配置?',
|
||
content: '此操作将恢复所有导航栏项为显示状态',
|
||
onOk: () => {
|
||
navbarConfigStore.resetToDefault()
|
||
navbarConfig.value = { ...navbarConfigStore.config }
|
||
message.success('已恢复默认配置')
|
||
}
|
||
})
|
||
}
|
||
|
||
// ==================== 任务发布类型配置 ====================
|
||
const taskReleaseConfig = ref({
|
||
default: true,
|
||
recruitment: true,
|
||
assigned: true
|
||
})
|
||
|
||
const savingTaskReleaseConfig = ref(false)
|
||
|
||
// 加载任务发布类型配置
|
||
const loadTaskReleaseConfig = async () => {
|
||
try {
|
||
const response = await getSystemConfigsByGroup('task')
|
||
const configs = response.data || []
|
||
|
||
const defaultConfig = configs.find((c: any) => c.configKey === 'task.release_type.default')
|
||
const recruitmentConfig = configs.find((c: any) => c.configKey === 'task.release_type.recruitment')
|
||
const assignedConfig = configs.find((c: any) => c.configKey === 'task.release_type.assigned')
|
||
|
||
taskReleaseConfig.value = {
|
||
default: defaultConfig?.configValue !== 'false',
|
||
recruitment: recruitmentConfig?.configValue !== 'false',
|
||
assigned: assignedConfig?.configValue !== 'false'
|
||
}
|
||
} catch (error) {
|
||
console.error('加载任务发布类型配置失败:', error)
|
||
}
|
||
}
|
||
|
||
// 保存任务发布类型配置
|
||
const handleSaveTaskReleaseConfig = async () => {
|
||
// 检查至少保留一个选项
|
||
if (!taskReleaseConfig.value.default && !taskReleaseConfig.value.recruitment && !taskReleaseConfig.value.assigned) {
|
||
message.warning('至少需要保留一个发布类型,已自动启用默认模式')
|
||
taskReleaseConfig.value.default = true
|
||
}
|
||
|
||
savingTaskReleaseConfig.value = true
|
||
try {
|
||
await batchUpdateSystemConfigs({
|
||
'task.release_type.default': taskReleaseConfig.value.default.toString(),
|
||
'task.release_type.recruitment': taskReleaseConfig.value.recruitment.toString(),
|
||
'task.release_type.assigned': taskReleaseConfig.value.assigned.toString()
|
||
})
|
||
message.success('任务发布类型配置保存成功')
|
||
} catch (error: any) {
|
||
message.error(error.message || '保存失败')
|
||
} finally {
|
||
savingTaskReleaseConfig.value = false
|
||
}
|
||
}
|
||
|
||
// 恢复默认任务发布类型配置
|
||
const handleResetTaskReleaseConfig = () => {
|
||
Modal.confirm({
|
||
title: '确认恢复默认配置?',
|
||
content: '此操作将显示所有发布类型选项',
|
||
onOk: async () => {
|
||
taskReleaseConfig.value = {
|
||
default: true,
|
||
recruitment: true,
|
||
assigned: true
|
||
}
|
||
await handleSaveTaskReleaseConfig()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 加载网站设置
|
||
const loadSiteSettings = async () => {
|
||
loadingSiteSettings.value = true
|
||
try {
|
||
// 先加载到store
|
||
await siteSettingsStore.loadSiteSettings()
|
||
|
||
// 获取网站设置分组的所有配置
|
||
const response = await getSystemConfigsByGroup('site')
|
||
const configs = response.data || []
|
||
|
||
// 查找网站名称和Logo配置
|
||
const siteNameConfig = configs.find((c: any) => c.configKey === 'site.name')
|
||
const logoConfig = configs.find((c: any) => c.configKey === 'site.logo')
|
||
|
||
if (siteNameConfig) {
|
||
siteSettings.siteName = siteNameConfig.configValue || 'DesignHub'
|
||
siteSettings.siteNameConfigId = siteNameConfig.id
|
||
} else {
|
||
siteSettings.siteName = siteSettingsStore.siteName
|
||
}
|
||
|
||
if (logoConfig) {
|
||
siteSettings.logoUrl = logoConfig.configValue || ''
|
||
siteSettings.logoConfigId = logoConfig.id
|
||
} else {
|
||
siteSettings.logoUrl = siteSettingsStore.logoUrl
|
||
}
|
||
|
||
// 查找注册功能开关配置
|
||
const registerConfig = configs.find((c: any) => c.configKey === 'site.register.enabled')
|
||
if (registerConfig) {
|
||
siteSettings.registerEnabled = registerConfig.configValue === 'true' || registerConfig.configValue === '1'
|
||
siteSettings.registerEnabledConfigId = registerConfig.id
|
||
} else {
|
||
siteSettings.registerEnabled = true // 默认开启
|
||
}
|
||
} catch (error) {
|
||
console.error('加载网站设置失败:', error)
|
||
// 如果API失败,尝试从store读取
|
||
siteSettings.siteName = siteSettingsStore.siteName
|
||
siteSettings.logoUrl = siteSettingsStore.logoUrl
|
||
siteSettings.registerEnabled = true // 默认开启
|
||
} finally {
|
||
loadingSiteSettings.value = false
|
||
}
|
||
}
|
||
|
||
// 网站设置相关函数
|
||
const handleSiteNameChange = () => {
|
||
updateSiteInfo()
|
||
}
|
||
|
||
const handleLogoUpload = (file: File) => {
|
||
const reader = new FileReader()
|
||
reader.onload = (e) => {
|
||
const result = e.target?.result as string
|
||
siteSettings.logoUrl = result
|
||
updateSiteInfo()
|
||
}
|
||
reader.readAsDataURL(file)
|
||
return false // 阻止自动上传
|
||
}
|
||
|
||
const handleRemoveLogo = () => {
|
||
siteSettings.logoUrl = ''
|
||
updateSiteInfo()
|
||
}
|
||
|
||
const handleSaveSiteSettings = async () => {
|
||
savingSiteSettings.value = true
|
||
try {
|
||
// 保存网站名称
|
||
if (siteSettings.siteNameConfigId) {
|
||
// 如果配置已存在,更新
|
||
await updateSystemConfig(siteSettings.siteNameConfigId, {
|
||
configValue: siteSettings.siteName
|
||
})
|
||
} else {
|
||
// 如果配置不存在,创建
|
||
const response = await createSystemConfig({
|
||
configKey: 'site.name',
|
||
configValue: siteSettings.siteName,
|
||
configGroup: 'site',
|
||
configName: '网站名称',
|
||
description: '网站名称,显示在导航栏、登录页和浏览器标题',
|
||
valueType: 'string',
|
||
builtIn: 0,
|
||
sortOrder: 1
|
||
})
|
||
if (response.data && response.data.id) {
|
||
siteSettings.siteNameConfigId = response.data.id
|
||
}
|
||
}
|
||
|
||
// 保存Logo
|
||
if (siteSettings.logoConfigId) {
|
||
// 如果配置已存在,更新
|
||
await updateSystemConfig(siteSettings.logoConfigId, {
|
||
configValue: siteSettings.logoUrl
|
||
})
|
||
} else {
|
||
// 如果配置不存在,创建
|
||
const response = await createSystemConfig({
|
||
configKey: 'site.logo',
|
||
configValue: siteSettings.logoUrl,
|
||
configGroup: 'site',
|
||
configName: '网站Logo',
|
||
description: '网站Logo URL,显示在导航栏和登录页',
|
||
valueType: 'string',
|
||
builtIn: 0,
|
||
sortOrder: 2
|
||
})
|
||
if (response.data && response.data.id) {
|
||
siteSettings.logoConfigId = response.data.id
|
||
}
|
||
}
|
||
|
||
// 保存注册功能开关
|
||
if (siteSettings.registerEnabledConfigId) {
|
||
// 如果配置已存在,更新
|
||
await updateSystemConfig(siteSettings.registerEnabledConfigId, {
|
||
configValue: siteSettings.registerEnabled ? 'true' : 'false'
|
||
})
|
||
} else {
|
||
// 如果配置不存在,创建
|
||
const response = await createSystemConfig({
|
||
configKey: 'site.register.enabled',
|
||
configValue: siteSettings.registerEnabled ? 'true' : 'false',
|
||
configGroup: 'site',
|
||
configName: '注册功能开关',
|
||
description: '控制登录页面是否显示注册功能',
|
||
valueType: 'boolean',
|
||
builtIn: 0,
|
||
sortOrder: 3
|
||
})
|
||
if (response.data && response.data.id) {
|
||
siteSettings.registerEnabledConfigId = response.data.id
|
||
}
|
||
}
|
||
|
||
// 更新全局store(这会自动更新所有使用store的组件)
|
||
siteSettingsStore.updateSiteSettings(siteSettings.siteName, siteSettings.logoUrl)
|
||
|
||
// 重新加载网站设置,确保页面显示最新数据
|
||
await loadSiteSettings()
|
||
|
||
message.success('设置保存成功')
|
||
} catch (error: any) {
|
||
console.error('保存网站设置失败:', error)
|
||
message.error(error.response?.data?.message || '保存失败')
|
||
} finally {
|
||
savingSiteSettings.value = false
|
||
}
|
||
}
|
||
|
||
// 更新网站信息(同步到登录页、导航栏、浏览器标题)
|
||
const updateSiteInfo = () => {
|
||
// 更新全局store(这会自动更新所有使用store的组件和浏览器标题)
|
||
siteSettingsStore.updateSiteSettings(siteSettings.siteName, siteSettings.logoUrl)
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadAllData()
|
||
// 加载网站设置
|
||
loadSiteSettings()
|
||
// 加载钉钉配置
|
||
loadDingTalkConfig()
|
||
// 加载任务发布类型配置
|
||
loadTaskReleaseConfig()
|
||
})
|
||
|
||
// S3配置相关方法
|
||
const loadS3Configs = async () => {
|
||
loadingS3Configs.value = true
|
||
try {
|
||
const data = await getAllS3Configs()
|
||
s3Configs.value = Array.isArray(data) ? data : []
|
||
} catch (error: any) {
|
||
console.error('加载S3配置失败:', error)
|
||
message.error(error.response?.data?.message || '加载S3配置失败')
|
||
} finally {
|
||
loadingS3Configs.value = false
|
||
}
|
||
}
|
||
|
||
const showS3ConfigModal = (config?: S3Config) => {
|
||
if (config) {
|
||
Object.assign(s3ConfigForm, {
|
||
...config,
|
||
secretAccessKey: config.secretAccessKey === '******' ? '' : config.secretAccessKey
|
||
})
|
||
} else {
|
||
Object.assign(s3ConfigForm, {
|
||
endpoint: '',
|
||
bucket: '',
|
||
accessKeyId: '',
|
||
secretAccessKey: '',
|
||
region: 'us-east-1',
|
||
publicDomain: '',
|
||
enablePathStyle: true,
|
||
filePath: '',
|
||
isActive: false
|
||
})
|
||
}
|
||
modals.s3Config = true
|
||
}
|
||
|
||
const handleSaveS3Config = async () => {
|
||
try {
|
||
if (s3ConfigForm.id) {
|
||
await updateS3Config(s3ConfigForm.id, s3ConfigForm)
|
||
message.success('更新S3配置成功')
|
||
} else {
|
||
await createS3Config(s3ConfigForm)
|
||
message.success('创建S3配置成功')
|
||
}
|
||
modals.s3Config = false
|
||
loadS3Configs()
|
||
} catch (error: any) {
|
||
console.error('保存S3配置失败:', error)
|
||
message.error(error.response?.data?.message || '保存S3配置失败')
|
||
}
|
||
}
|
||
|
||
const handleDeleteS3Config = async (id: number) => {
|
||
try {
|
||
await deleteS3Config(id)
|
||
message.success('删除S3配置成功')
|
||
loadS3Configs()
|
||
} catch (error: any) {
|
||
console.error('删除S3配置失败:', error)
|
||
message.error(error.response?.data?.message || '删除S3配置失败')
|
||
}
|
||
}
|
||
|
||
const handleActivateS3Config = async (id: number) => {
|
||
try {
|
||
await activateS3Config(id)
|
||
message.success('激活S3配置成功')
|
||
loadS3Configs()
|
||
} catch (error: any) {
|
||
console.error('激活S3配置失败:', error)
|
||
message.error(error.response?.data?.message || '激活S3配置失败')
|
||
}
|
||
}
|
||
|
||
watch(activeTab, (value) => {
|
||
// 切换到用户与部门 tab 时,始终刷新数据以获取最新状态
|
||
if (value === 'users') {
|
||
loadDepartmentUsers()
|
||
}
|
||
// 切换到部门管理 tab 时,刷新部门数据以获取最新成员数
|
||
if (value === 'departments') {
|
||
loadDepartments()
|
||
}
|
||
// 切换到导航栏管理 tab 时,加载配置
|
||
if (value === 'navbar') {
|
||
loadNavbarConfig()
|
||
}
|
||
// 切换到钉钉集成 tab 时,加载配置
|
||
if (value === 'dingtalk') {
|
||
loadDingTalkConfig()
|
||
}
|
||
// 切换到S3配置 tab 时,加载配置
|
||
if (value === 's3-config') {
|
||
loadS3Configs()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.settings-page {
|
||
min-height: 100vh;
|
||
padding: $space-xl 0;
|
||
background: $bg-primary;
|
||
}
|
||
|
||
.container {
|
||
max-width: $container-max-width;
|
||
margin: 0 auto;
|
||
padding: 0 $space-xl;
|
||
|
||
@media (max-width: $breakpoint-tablet) {
|
||
padding: 0 $space-lg;
|
||
}
|
||
|
||
@media (max-width: $breakpoint-mobile) {
|
||
padding: 0 $space-md;
|
||
}
|
||
}
|
||
|
||
.settings-tabs {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-lg;
|
||
padding: $space-lg;
|
||
transition: all $transition-base $ease-out;
|
||
}
|
||
|
||
.settings-section {
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: $space-lg;
|
||
|
||
.section-title {
|
||
font-size: $font-size-xl;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
margin: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.balance-display {
|
||
font-size: $font-size-xl;
|
||
color: $color-info;
|
||
font-weight: $font-weight-bold;
|
||
}
|
||
|
||
// 网站设置表单
|
||
.site-settings-form {
|
||
max-width: 600px;
|
||
}
|
||
|
||
// 用户信息单元格
|
||
.user-info-cell {
|
||
.user-info-main {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 4px;
|
||
|
||
.user-name-text {
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
font-size: $font-size-base;
|
||
}
|
||
|
||
.user-username {
|
||
font-size: $font-size-sm;
|
||
color: $text-tertiary;
|
||
font-family: monospace;
|
||
}
|
||
}
|
||
|
||
.user-info-sub {
|
||
display: flex;
|
||
gap: 12px;
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
|
||
.user-email {
|
||
color: $text-tertiary;
|
||
}
|
||
|
||
span {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 状态徽章
|
||
.status-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 4px 12px;
|
||
border-radius: $radius-sm;
|
||
font-size: $font-size-sm;
|
||
font-weight: $font-weight-medium;
|
||
|
||
&.active {
|
||
background: rgba($brand-primary, 0.15);
|
||
color: $brand-primary;
|
||
}
|
||
|
||
&.inactive {
|
||
background: $bg-tertiary;
|
||
color: $text-tertiary;
|
||
}
|
||
}
|
||
|
||
// 时间值
|
||
.time-value {
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
font-family: monospace;
|
||
}
|
||
|
||
.dept-user-card {
|
||
min-height: 220px;
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-lg;
|
||
|
||
.dept-card-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: $space-md;
|
||
}
|
||
|
||
.dept-name {
|
||
font-size: $font-size-lg;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
}
|
||
|
||
.dept-meta {
|
||
color: $text-tertiary;
|
||
font-size: $font-size-sm;
|
||
}
|
||
|
||
.dept-user-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $space-sm;
|
||
}
|
||
|
||
.dept-user-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: $space-sm $space-md;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-md;
|
||
background: $bg-secondary;
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.user-name {
|
||
color: $text-primary;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
|
||
.user-meta {
|
||
font-size: $font-size-sm;
|
||
color: $text-tertiary;
|
||
}
|
||
}
|
||
}
|
||
|
||
.dept-user-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $space-lg;
|
||
}
|
||
|
||
.dept-user-column {
|
||
width: 100%;
|
||
}
|
||
|
||
.user-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $space-xs;
|
||
}
|
||
|
||
.transfer-modal {
|
||
.transfer-tip {
|
||
margin-bottom: $space-lg;
|
||
color: $text-secondary;
|
||
|
||
.highlight {
|
||
color: $brand-primary;
|
||
font-weight: $font-weight-semibold;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.dark-modal) {
|
||
.ant-modal-content {
|
||
background: $bg-card !important;
|
||
border: 1px solid $border-primary !important;
|
||
border-radius: $radius-lg !important;
|
||
}
|
||
|
||
.ant-modal-header {
|
||
background: $bg-card !important;
|
||
border-bottom: 1px solid $border-primary !important;
|
||
|
||
.ant-modal-title {
|
||
color: $text-primary !important;
|
||
}
|
||
}
|
||
|
||
.ant-modal-body {
|
||
background: $bg-card !important;
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
.ant-modal-footer {
|
||
background: $bg-card !important;
|
||
border-top: 1px solid $border-primary !important;
|
||
}
|
||
}
|
||
|
||
.logo-upload-section {
|
||
.logo-preview {
|
||
width: 200px;
|
||
height: 200px;
|
||
border: 2px dashed $border-primary;
|
||
border-radius: $radius-md;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: $space-md;
|
||
background: $bg-tertiary;
|
||
overflow: hidden;
|
||
|
||
.logo-preview-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.logo-placeholder {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: $space-sm;
|
||
color: $text-tertiary;
|
||
}
|
||
}
|
||
|
||
.logo-upload-actions {
|
||
display: flex;
|
||
gap: $space-sm;
|
||
margin-bottom: $space-sm;
|
||
}
|
||
|
||
.upload-tip {
|
||
font-size: $font-size-sm;
|
||
color: $text-tertiary;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
}
|
||
|
||
// 表单提示文字样式
|
||
.form-tip {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
color: $text-tertiary;
|
||
display: flex;
|
||
align-items: center;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
// Ant Design 组件深色主题覆盖
|
||
:deep(.ant-tabs) {
|
||
.ant-tabs-nav {
|
||
background: transparent !important;
|
||
|
||
.ant-tabs-tab {
|
||
color: $text-secondary !important;
|
||
background: transparent !important;
|
||
border-color: $border-primary !important;
|
||
|
||
&:hover {
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
&.ant-tabs-tab-active {
|
||
color: $brand-primary !important;
|
||
background: $bg-card !important;
|
||
border-color: $brand-primary !important;
|
||
|
||
.ant-tabs-tab-btn {
|
||
color: $brand-primary !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.ant-tabs-content-holder {
|
||
background: transparent !important;
|
||
}
|
||
|
||
.ant-tabs-tabpane {
|
||
background: transparent !important;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-table) {
|
||
background: $bg-card !important;
|
||
color: $text-primary !important;
|
||
|
||
// 表头统一样式
|
||
.ant-table-thead > tr > th {
|
||
background: $bg-tertiary !important;
|
||
border-color: $border-primary !important;
|
||
color: $text-primary !important;
|
||
font-weight: $font-weight-semibold !important;
|
||
font-size: 14px !important;
|
||
padding: 16px 12px !important;
|
||
text-align: left !important;
|
||
white-space: nowrap !important;
|
||
|
||
// 第一个表头单元格(通常是ID或序号)
|
||
&:first-child {
|
||
padding-left: 20px !important;
|
||
border-top-left-radius: $radius-md !important;
|
||
}
|
||
|
||
// 最后一个表头单元格(通常是操作列)
|
||
&:last-child {
|
||
padding-right: 20px !important;
|
||
text-align: center !important;
|
||
border-top-right-radius: $radius-md !important;
|
||
}
|
||
|
||
// 操作列居中
|
||
&.ant-table-cell-fix-right {
|
||
text-align: center !important;
|
||
}
|
||
}
|
||
|
||
// 表格行基础样式 - 确保始终是深色背景,移除所有过渡效果
|
||
.ant-table-tbody > tr {
|
||
background: $bg-card !important;
|
||
transition: none !important; // 移除过渡避免闪烁
|
||
|
||
// 确保所有单元格都是深色背景
|
||
> td {
|
||
background: $bg-card !important;
|
||
border-color: $border-primary !important;
|
||
color: $text-primary !important;
|
||
transition: none !important; // 移除过渡避免闪烁
|
||
}
|
||
|
||
// 悬停状态 - 使用深灰色,立即切换无过渡
|
||
&:hover {
|
||
background: $bg-card-hover !important;
|
||
|
||
> td {
|
||
background: $bg-card-hover !important;
|
||
border-color: $border-primary !important;
|
||
}
|
||
}
|
||
|
||
// 选中状态
|
||
&.ant-table-row-selected {
|
||
background: rgba($brand-primary, 0.1) !important;
|
||
|
||
> td {
|
||
background: rgba($brand-primary, 0.1) !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 覆盖 Ant Design 默认的白色背景和所有可能的状态
|
||
.ant-table-tbody > tr.ant-table-row {
|
||
background: $bg-card !important;
|
||
transition: none !important;
|
||
|
||
> td {
|
||
background: $bg-card !important;
|
||
transition: none !important;
|
||
}
|
||
|
||
&:hover {
|
||
background: $bg-card-hover !important;
|
||
transition: none !important;
|
||
|
||
> td {
|
||
background: $bg-card-hover !important;
|
||
transition: none !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 确保没有白色背景出现在任何状态,包括所有可能的伪类
|
||
.ant-table-tbody > tr:hover > td,
|
||
.ant-table-tbody > tr.ant-table-row:hover > td,
|
||
.ant-table-tbody > tr.ant-table-row:hover {
|
||
background: $bg-card-hover !important;
|
||
transition: none !important;
|
||
}
|
||
|
||
// 覆盖 Ant Design 可能设置的白色背景
|
||
.ant-table-tbody > tr > td {
|
||
background: $bg-card !important;
|
||
}
|
||
|
||
.ant-table-tbody > tr:hover > td {
|
||
background: $bg-card-hover !important;
|
||
}
|
||
|
||
.ant-table-placeholder {
|
||
background: $bg-card !important;
|
||
|
||
.ant-empty-description {
|
||
color: $text-secondary !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-modal) {
|
||
.ant-modal-content {
|
||
background: $bg-card !important;
|
||
border: 1px solid $border-primary !important;
|
||
}
|
||
|
||
.ant-modal-header {
|
||
background: $bg-card !important;
|
||
border-bottom: 1px solid $border-primary !important;
|
||
|
||
.ant-modal-title {
|
||
color: $text-primary !important;
|
||
}
|
||
}
|
||
|
||
.ant-modal-body {
|
||
background: $bg-card !important;
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
.ant-modal-footer {
|
||
background: $bg-card !important;
|
||
border-top: 1px solid $border-primary !important;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-form) {
|
||
.ant-form-item-label > label {
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
.ant-input,
|
||
.ant-input-number,
|
||
.ant-select-selector,
|
||
.ant-picker {
|
||
background: $bg-tertiary !important;
|
||
border-color: $border-primary !important;
|
||
color: $text-primary !important;
|
||
|
||
&:hover {
|
||
border-color: $brand-primary !important;
|
||
}
|
||
|
||
&:focus,
|
||
&.ant-picker-focused {
|
||
border-color: $brand-primary !important;
|
||
box-shadow: 0 0 0 2px rgba($brand-primary, 0.08) !important;
|
||
}
|
||
|
||
&::placeholder {
|
||
color: $text-tertiary !important;
|
||
}
|
||
}
|
||
|
||
.ant-input-number-input {
|
||
background: transparent !important;
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
.ant-select-selection-item,
|
||
.ant-select-selection-placeholder {
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
.ant-select-arrow {
|
||
color: $text-secondary !important;
|
||
}
|
||
|
||
.ant-picker-input > input {
|
||
color: $text-primary !important;
|
||
|
||
&::placeholder {
|
||
color: $text-tertiary !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-descriptions) {
|
||
.ant-descriptions-header {
|
||
.ant-descriptions-title {
|
||
color: $text-primary !important;
|
||
}
|
||
}
|
||
|
||
.ant-descriptions-view {
|
||
.ant-descriptions-row {
|
||
.ant-descriptions-item-label {
|
||
background: $bg-tertiary !important;
|
||
color: $text-secondary !important;
|
||
border-color: $border-primary !important;
|
||
}
|
||
|
||
.ant-descriptions-item-content {
|
||
background: $bg-card !important;
|
||
color: $text-primary !important;
|
||
border-color: $border-primary !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-alert) {
|
||
background: $bg-card !important;
|
||
border-color: $border-primary !important;
|
||
|
||
.ant-alert-message {
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
.ant-alert-description {
|
||
color: $text-secondary !important;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-divider) {
|
||
border-color: $border-divider !important;
|
||
|
||
&.ant-divider-with-text {
|
||
color: $text-secondary !important;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-empty) {
|
||
.ant-empty-description {
|
||
color: $text-secondary !important;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-switch) {
|
||
&.ant-switch-checked {
|
||
background: $brand-primary !important;
|
||
|
||
// 确保选中状态的文字是黑色
|
||
.ant-switch-inner {
|
||
color: #000000 !important;
|
||
}
|
||
}
|
||
|
||
// 确保选中状态的手柄也是黑色
|
||
&.ant-switch-checked .ant-switch-handle {
|
||
&::before {
|
||
background: #000000 !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-tag) {
|
||
background: $bg-tertiary !important;
|
||
border-color: $border-primary !important;
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
:deep(.ant-select-dropdown) {
|
||
background: $bg-card !important;
|
||
border-color: $border-primary !important;
|
||
|
||
.ant-select-item {
|
||
color: $text-primary !important;
|
||
|
||
&:hover {
|
||
background: $bg-card-hover !important;
|
||
}
|
||
|
||
&.ant-select-item-option-selected {
|
||
background: rgba($brand-primary, 0.1) !important;
|
||
color: $brand-primary !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-picker-dropdown) {
|
||
.ant-picker-panel {
|
||
background: $bg-card !important;
|
||
border-color: $border-primary !important;
|
||
|
||
.ant-picker-header {
|
||
border-bottom-color: $border-primary !important;
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
.ant-picker-body {
|
||
.ant-picker-content {
|
||
th {
|
||
color: $text-secondary !important;
|
||
}
|
||
|
||
td {
|
||
color: $text-primary !important;
|
||
|
||
&.ant-picker-cell-selected .ant-picker-cell-inner {
|
||
background: $brand-primary !important;
|
||
color: #000000 !important; // 选中日期文字为黑色
|
||
}
|
||
|
||
&:hover .ant-picker-cell-inner {
|
||
background: $bg-card-hover !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-pagination) {
|
||
.ant-pagination-item {
|
||
background: $bg-tertiary !important;
|
||
border-color: $border-primary !important;
|
||
|
||
a {
|
||
color: $text-primary !important;
|
||
}
|
||
|
||
&:hover {
|
||
border-color: $brand-primary !important;
|
||
|
||
a {
|
||
color: $brand-primary !important;
|
||
}
|
||
}
|
||
|
||
&.ant-pagination-item-active {
|
||
background: $brand-primary !important;
|
||
border-color: $brand-primary !important;
|
||
|
||
a {
|
||
color: #000000 !important; // 当前页文字为黑色
|
||
}
|
||
|
||
&:hover {
|
||
background: $brand-hover !important;
|
||
border-color: $brand-hover !important;
|
||
|
||
a {
|
||
color: #000000 !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.ant-pagination-prev,
|
||
.ant-pagination-next {
|
||
.ant-pagination-item-link {
|
||
background: $bg-tertiary !important;
|
||
border-color: $border-primary !important;
|
||
color: $text-primary !important;
|
||
|
||
&:hover {
|
||
border-color: $brand-primary !important;
|
||
color: $brand-primary !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.ant-pagination-jump-prev,
|
||
.ant-pagination-jump-next {
|
||
.ant-pagination-item-link {
|
||
color: $text-primary !important;
|
||
}
|
||
}
|
||
|
||
.ant-pagination-options {
|
||
.ant-select {
|
||
.ant-select-selector {
|
||
background: $bg-tertiary !important;
|
||
border-color: $border-primary !important;
|
||
color: $text-primary !important;
|
||
}
|
||
}
|
||
|
||
.ant-pagination-options-size-changer {
|
||
color: $text-primary !important;
|
||
}
|
||
}
|
||
|
||
.ant-pagination-total-text {
|
||
color: $text-primary !important;
|
||
}
|
||
}
|
||
|
||
// 奖励规则介绍区域样式
|
||
.reward-rules-intro {
|
||
padding: 24px;
|
||
background: $bg-tertiary;
|
||
border-radius: $radius-lg;
|
||
|
||
.intro-card {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
|
||
.intro-content {
|
||
h3 {
|
||
color: $text-primary;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
p {
|
||
color: $text-secondary;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
}
|
||
|
||
.intro-examples {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
background: $bg-secondary;
|
||
border-radius: $radius-md;
|
||
|
||
.example-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
color: $text-primary;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 导航栏配置样式
|
||
.navbar-config-form {
|
||
.config-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.config-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding: 16px 20px;
|
||
background: $bg-tertiary;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-md;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
border-color: $brand-primary;
|
||
background: rgba($brand-primary, 0.05);
|
||
}
|
||
|
||
.config-item-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
.config-item-title {
|
||
color: $text-primary;
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.config-item-desc {
|
||
width: 100%;
|
||
color: $text-secondary;
|
||
font-size: 12px;
|
||
margin-top: -4px;
|
||
padding-left: 36px;
|
||
}
|
||
}
|
||
|
||
.section-description {
|
||
color: $text-secondary;
|
||
font-size: 14px;
|
||
margin-bottom: 24px;
|
||
padding-left: 4px;
|
||
}
|
||
}
|
||
|
||
// ===== 部门管理表格样式优化 =====
|
||
|
||
// 积分显示
|
||
.score-display {
|
||
.score-main {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 4px;
|
||
margin-bottom: 6px;
|
||
|
||
.score-value {
|
||
font-size: 20px;
|
||
font-weight: $font-weight-bold;
|
||
color: $brand-primary;
|
||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||
}
|
||
|
||
.score-unit {
|
||
font-size: 13px;
|
||
color: $text-tertiary;
|
||
}
|
||
}
|
||
|
||
.score-details {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
|
||
.score-detail-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
|
||
.detail-label {
|
||
color: $text-tertiary;
|
||
}
|
||
|
||
.detail-value {
|
||
color: $text-secondary;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
}
|
||
|
||
.detail-separator {
|
||
color: $border-primary;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 限制显示
|
||
.limit-display {
|
||
.limit-period {
|
||
margin-bottom: 4px;
|
||
|
||
.period-label {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
background: rgba($brand-primary, 0.1);
|
||
color: $brand-primary;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
}
|
||
|
||
.limit-value {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||
font-size: 14px;
|
||
|
||
.consumed {
|
||
color: $text-primary;
|
||
font-weight: $font-weight-semibold;
|
||
}
|
||
|
||
.separator {
|
||
color: $text-tertiary;
|
||
}
|
||
|
||
.total {
|
||
color: $text-secondary;
|
||
}
|
||
}
|
||
|
||
.limit-disabled {
|
||
color: $text-tertiary;
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
// 预警显示
|
||
.alert-display {
|
||
.alert-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 13px;
|
||
font-weight: $font-weight-medium;
|
||
|
||
&.warning {
|
||
background: rgba(#F59E0B, 0.15);
|
||
color: #F59E0B;
|
||
border: 1px solid rgba(#F59E0B, 0.3);
|
||
}
|
||
|
||
&.normal {
|
||
background: rgba($brand-primary, 0.15);
|
||
color: $brand-primary;
|
||
border: 1px solid rgba($brand-primary, 0.3);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 状态开关
|
||
.status-switch {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
|
||
.status-text {
|
||
font-size: 13px;
|
||
color: $text-tertiary;
|
||
transition: color 0.2s ease;
|
||
|
||
&.active {
|
||
color: $brand-primary;
|
||
font-weight: $font-weight-medium;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 表格操作按钮
|
||
.table-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
|
||
:deep(.ant-btn-text) {
|
||
color: #9CA3AF !important;
|
||
padding: 4px 8px;
|
||
height: auto;
|
||
font-size: $font-size-sm;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
color: $brand-primary !important;
|
||
background: rgba($brand-primary, 0.08) !important;
|
||
}
|
||
|
||
&.ant-btn-dangerous {
|
||
&:hover {
|
||
color: $color-error !important;
|
||
background: rgba($color-error, 0.08) !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
.action-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 4px 12px;
|
||
height: 28px;
|
||
font-size: 13px;
|
||
border-radius: $radius-sm;
|
||
transition: all 0.2s ease;
|
||
|
||
&.edit {
|
||
color: $text-primary;
|
||
|
||
&:hover {
|
||
color: $brand-primary;
|
||
background: rgba($brand-primary, 0.1);
|
||
}
|
||
}
|
||
|
||
&.pool {
|
||
color: $text-primary;
|
||
|
||
&:hover {
|
||
color: #3B82F6;
|
||
background: rgba(#3B82F6, 0.1);
|
||
}
|
||
}
|
||
|
||
&.danger {
|
||
color: $text-tertiary;
|
||
|
||
&:hover {
|
||
color: #EF4444;
|
||
background: rgba(#EF4444, 0.1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 空值显示
|
||
.empty-value {
|
||
color: $text-tertiary;
|
||
font-size: 14px;
|
||
}
|
||
|
||
// 部门信息单元格
|
||
.dept-info-cell {
|
||
.dept-info-main {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 4px;
|
||
|
||
.dept-name-text {
|
||
font-size: 15px;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
}
|
||
}
|
||
|
||
.dept-info-sub {
|
||
font-size: 12px;
|
||
color: $text-tertiary;
|
||
|
||
.dept-desc {
|
||
margin-left: 8px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 测试消息提示
|
||
.test-message-hint {
|
||
margin-left: $space-md;
|
||
font-size: $font-size-sm;
|
||
color: $text-tertiary;
|
||
}
|
||
</style>
|
||
|
||
|