新增
This commit is contained in:
+15
-399
@@ -54,26 +54,11 @@ const formatApiError = (error, fallback = '操作失败') => {
|
||||
}
|
||||
|
||||
const auth = useAuthStore()
|
||||
const activeTab = ref('users')
|
||||
const users = ref([])
|
||||
const roles = ref([])
|
||||
const roleRecords = ref([])
|
||||
const permissionCatalog = ref({ menus: [], actions: [] })
|
||||
const loading = ref(false)
|
||||
const rolesLoading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('新增用户')
|
||||
const editingId = ref(null)
|
||||
const roleModalVisible = ref(false)
|
||||
const roleModalTitle = ref('新建角色')
|
||||
const editingRoleCode = ref(null)
|
||||
const roleSaving = ref(false)
|
||||
const roleForm = ref({
|
||||
code: '',
|
||||
label: '',
|
||||
description: '',
|
||||
permissions: []
|
||||
})
|
||||
|
||||
const userForm = ref({
|
||||
username: '',
|
||||
@@ -96,6 +81,11 @@ const roleOptions = ref([
|
||||
{ value: 'viewer', label: '只读', is_admin: false }
|
||||
])
|
||||
|
||||
const assignableRoleOptions = computed(() => {
|
||||
if (auth.isAdmin) return roleOptions.value
|
||||
return roleOptions.value.filter((r) => !r.is_admin)
|
||||
})
|
||||
|
||||
const isAdminRole = computed(() => {
|
||||
const hit = roleOptions.value.find((r) => r.value === userForm.value.role)
|
||||
return !!(hit?.is_admin || userForm.value.role === 'admin')
|
||||
@@ -103,16 +93,11 @@ const isAdminRole = computed(() => {
|
||||
const emailRequiredForRole = computed(
|
||||
() => emailBindingRequired.value && !isAdminRole.value
|
||||
)
|
||||
const rolePermissionsReadonly = computed(
|
||||
() => editingRoleCode.value === 'admin'
|
||||
)
|
||||
|
||||
const hasEmail = computed(() => !!userForm.value.email?.trim())
|
||||
|
||||
const getRoleLabel = (role) =>
|
||||
roleOptions.value.find((r) => r.value === role)?.label ||
|
||||
roleRecords.value.find((r) => r.value === role)?.label ||
|
||||
role
|
||||
roleOptions.value.find((r) => r.value === role)?.label || role
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
const keyword = searchKeyword.value.trim().toLowerCase()
|
||||
@@ -165,9 +150,9 @@ const fetchUsers = async () => {
|
||||
const fetchRoles = async () => {
|
||||
try {
|
||||
const res = await api.get('/auth/roles')
|
||||
roles.value = res.data.roles || []
|
||||
if (roles.value.length) {
|
||||
roleOptions.value = roles.value.map((r) => ({
|
||||
const list = res.data.roles || []
|
||||
if (list.length) {
|
||||
roleOptions.value = list.map((r) => ({
|
||||
value: r.value,
|
||||
label: r.label,
|
||||
is_admin: !!r.is_admin
|
||||
@@ -178,120 +163,6 @@ const fetchRoles = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRoleRecords = async () => {
|
||||
rolesLoading.value = true
|
||||
try {
|
||||
const [rolesRes, catalogRes] = await Promise.all([
|
||||
api.get('/roles'),
|
||||
api.get('/roles/catalog')
|
||||
])
|
||||
roleRecords.value = rolesRes.data.roles || []
|
||||
permissionCatalog.value = catalogRes.data || { menus: [], actions: [] }
|
||||
if (roleRecords.value.length) {
|
||||
roleOptions.value = roleRecords.value.map((r) => ({
|
||||
value: r.value,
|
||||
label: r.label,
|
||||
is_admin: !!r.is_admin
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(formatApiError(error, '获取角色列表失败'))
|
||||
} finally {
|
||||
rolesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openAddRole = () => {
|
||||
editingRoleCode.value = null
|
||||
roleModalTitle.value = '新建角色'
|
||||
roleForm.value = {
|
||||
code: '',
|
||||
label: '',
|
||||
description: '',
|
||||
permissions: [
|
||||
'menu.dashboard',
|
||||
'menu.accounts',
|
||||
'menu.messages',
|
||||
'menu.rules',
|
||||
'menu.help',
|
||||
'menu.download'
|
||||
]
|
||||
}
|
||||
roleModalVisible.value = true
|
||||
}
|
||||
|
||||
const openEditRole = (record) => {
|
||||
editingRoleCode.value = record.value
|
||||
roleModalTitle.value = record.is_admin ? '查看管理员角色' : '编辑角色'
|
||||
roleForm.value = {
|
||||
code: record.value,
|
||||
label: record.label,
|
||||
description: record.description || '',
|
||||
permissions: [...(record.permissions || [])]
|
||||
}
|
||||
roleModalVisible.value = true
|
||||
}
|
||||
|
||||
const handleSaveRole = async () => {
|
||||
const code = (roleForm.value.code || '').trim().toLowerCase()
|
||||
const label = (roleForm.value.label || '').trim()
|
||||
if (!editingRoleCode.value && !code) {
|
||||
message.warning('请填写角色码')
|
||||
return
|
||||
}
|
||||
if (!label) {
|
||||
message.warning('请填写角色名称')
|
||||
return
|
||||
}
|
||||
roleSaving.value = true
|
||||
try {
|
||||
if (editingRoleCode.value) {
|
||||
await api.put(`/roles/${encodeURIComponent(editingRoleCode.value)}`, {
|
||||
label,
|
||||
description: roleForm.value.description || null,
|
||||
permissions: rolePermissionsReadonly.value
|
||||
? undefined
|
||||
: roleForm.value.permissions
|
||||
})
|
||||
message.success('角色已更新')
|
||||
} else {
|
||||
await api.post('/roles', {
|
||||
code,
|
||||
label,
|
||||
description: roleForm.value.description || null,
|
||||
permissions: roleForm.value.permissions
|
||||
})
|
||||
message.success('角色已创建')
|
||||
}
|
||||
roleModalVisible.value = false
|
||||
await Promise.all([fetchRoleRecords(), fetchRoles()])
|
||||
} catch (error) {
|
||||
message.error(formatApiError(error, '保存角色失败'))
|
||||
} finally {
|
||||
roleSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteRole = (record) => {
|
||||
if (record.is_system) {
|
||||
message.warning('系统内置角色不可删除')
|
||||
return
|
||||
}
|
||||
Modal.confirm({
|
||||
title: `确定删除角色「${record.label}」吗?`,
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await api.delete(`/roles/${encodeURIComponent(record.value)}`)
|
||||
message.success('角色已删除')
|
||||
await Promise.all([fetchRoleRecords(), fetchRoles()])
|
||||
} catch (error) {
|
||||
message.error(formatApiError(error, '删除角色失败'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fetchDefaultMaxAccounts = async () => {
|
||||
try {
|
||||
const res = await api.get('/settings')
|
||||
@@ -468,12 +339,6 @@ const isUnlimitedQuota = (record) =>
|
||||
!!(record.is_admin || record.role === 'admin' ||
|
||||
roleOptions.value.find((r) => r.value === record.role)?.is_admin)
|
||||
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === 'roles' && !roleRecords.value.length) {
|
||||
fetchRoleRecords()
|
||||
}
|
||||
})
|
||||
|
||||
const resolveAccountLimit = (record) => {
|
||||
if (isUnlimitedQuota(record)) return null
|
||||
const raw = record.max_accounts
|
||||
@@ -521,36 +386,16 @@ onMounted(() => {
|
||||
<div class="page-header-main">
|
||||
<h2 class="page-title">
|
||||
<TeamOutlined class="page-title-icon" />
|
||||
用户与角色管理
|
||||
用户管理
|
||||
</h2>
|
||||
<p class="subtitle">创建用户、自定义角色,并勾选菜单与操作权限</p>
|
||||
<p class="subtitle">创建与管理后台用户并分配角色;非管理员仅能查看自己创建的用户</p>
|
||||
</div>
|
||||
<a-button
|
||||
v-if="activeTab === 'users'"
|
||||
type="primary"
|
||||
class="gradient-btn add-user-btn"
|
||||
@click="openAdd"
|
||||
>
|
||||
<a-button type="primary" class="gradient-btn add-user-btn" @click="openAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增用户
|
||||
</a-button>
|
||||
<a-button
|
||||
v-else
|
||||
type="primary"
|
||||
class="gradient-btn add-user-btn"
|
||||
@click="openAddRole"
|
||||
>
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建角色
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-tabs v-model:activeKey="activeTab" class="users-tabs glass-card">
|
||||
<a-tab-pane key="users" tab="用户管理" />
|
||||
<a-tab-pane key="roles" tab="角色设定" />
|
||||
</a-tabs>
|
||||
|
||||
<template v-if="activeTab === 'users'">
|
||||
<div class="users-toolbar glass-card">
|
||||
<a-input
|
||||
v-model:value="searchKeyword"
|
||||
@@ -567,7 +412,6 @@ onMounted(() => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 桌面端:表格 -->
|
||||
<div v-if="!isMobile" class="glass-card table-card">
|
||||
<a-table
|
||||
:data-source="paginatedUsers"
|
||||
@@ -618,7 +462,7 @@ onMounted(() => {
|
||||
<a-table-column title="操作" key="action" width="180px">
|
||||
<template #default="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" style="color: #c084fc;" @click="openEdit(record)">
|
||||
<a-button type="text" class="edit-btn" @click="openEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
编辑
|
||||
</a-button>
|
||||
@@ -637,7 +481,6 @@ onMounted(() => {
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 手机端:卡片列表 -->
|
||||
<div v-else class="users-mobile-list">
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="paginatedUsers.length" class="user-card-list">
|
||||
@@ -720,96 +563,6 @@ onMounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="glass-card roles-panel">
|
||||
<a-spin :spinning="rolesLoading">
|
||||
<a-table
|
||||
v-if="!isMobile"
|
||||
:data-source="roleRecords"
|
||||
row-key="value"
|
||||
:pagination="false"
|
||||
>
|
||||
<a-table-column title="角色码" data-index="value" key="value" :width="140" />
|
||||
<a-table-column title="名称" data-index="label" key="label" :width="140" />
|
||||
<a-table-column title="说明" key="description">
|
||||
<template #default="{ record }">
|
||||
<span :class="{ 'text-muted': !record.description }">
|
||||
{{ record.description || '—' }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="类型" key="type" :width="100">
|
||||
<template #default="{ record }">
|
||||
<a-tag v-if="record.is_admin" color="purple">超级管理员</a-tag>
|
||||
<a-tag v-else-if="record.is_system" color="blue">系统</a-tag>
|
||||
<a-tag v-else color="geekblue">自定义</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="权限数" key="perm_count" :width="90">
|
||||
<template #default="{ record }">
|
||||
{{ (record.permissions || []).length }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="用户数" data-index="user_count" key="user_count" :width="80" />
|
||||
<a-table-column title="操作" key="action" :width="160">
|
||||
<template #default="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" style="color: #c084fc;" @click="openEditRole(record)">
|
||||
{{ record.is_admin ? '查看' : '编辑' }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!record.is_system"
|
||||
type="text"
|
||||
danger
|
||||
@click="handleDeleteRole(record)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
|
||||
<div v-else class="user-card-list">
|
||||
<div v-for="record in roleRecords" :key="record.value" class="user-card glass-card">
|
||||
<div class="user-card-head">
|
||||
<div>
|
||||
<div class="user-card-name">{{ record.label }}</div>
|
||||
<div class="user-card-display">{{ record.value }}</div>
|
||||
</div>
|
||||
<a-tag v-if="record.is_admin" color="purple">超级管理员</a-tag>
|
||||
<a-tag v-else-if="record.is_system" color="blue">系统</a-tag>
|
||||
<a-tag v-else color="geekblue">自定义</a-tag>
|
||||
</div>
|
||||
<div class="user-card-meta">
|
||||
<div class="user-card-row">
|
||||
<span class="user-card-label">权限</span>
|
||||
<span class="user-card-value">{{ (record.permissions || []).length }} 项</span>
|
||||
</div>
|
||||
<div class="user-card-row">
|
||||
<span class="user-card-label">用户</span>
|
||||
<span class="user-card-value">{{ record.user_count ?? 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-card-actions">
|
||||
<a-button type="text" class="edit-btn" @click="openEditRole(record)">
|
||||
{{ record.is_admin ? '查看' : '编辑' }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="!record.is_system"
|
||||
type="text"
|
||||
danger
|
||||
@click="handleDeleteRole(record)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty v-if="!roleRecords.length" description="暂无角色" />
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="modalVisible"
|
||||
@@ -860,7 +613,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="角色">
|
||||
<a-select v-model:value="userForm.role" :options="roleOptions" />
|
||||
<a-select v-model:value="userForm.role" :options="assignableRoleOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="!isAdminRole" label="可添加抖音账号数">
|
||||
<a-input-number
|
||||
@@ -879,84 +632,6 @@ onMounted(() => {
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="roleModalVisible"
|
||||
:title="roleModalTitle"
|
||||
:width="isMobile ? 'calc(100vw - 32px)' : 720"
|
||||
:confirm-loading="roleSaving"
|
||||
@ok="handleSaveRole"
|
||||
ok-text="保存"
|
||||
cancel-text="取消"
|
||||
>
|
||||
<a-form layout="vertical" class="user-form" style="margin-top: 16px;">
|
||||
<a-form-item label="角色码" required>
|
||||
<a-input
|
||||
v-model:value="roleForm.code"
|
||||
placeholder="小写字母开头,如 ops_leader"
|
||||
:disabled="!!editingRoleCode"
|
||||
:maxlength="50"
|
||||
/>
|
||||
<div class="field-hint">创建后不可修改;仅小写字母、数字、下划线</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="显示名称" required>
|
||||
<a-input v-model:value="roleForm.label" placeholder="界面展示名称" :maxlength="100" />
|
||||
</a-form-item>
|
||||
<a-form-item label="说明">
|
||||
<a-input
|
||||
v-model:value="roleForm.description"
|
||||
placeholder="可选,描述该角色的职责"
|
||||
:maxlength="255"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-alert
|
||||
v-if="rolePermissionsReadonly"
|
||||
type="info"
|
||||
show-icon
|
||||
message="管理员角色固定拥有全部权限,不可取消勾选"
|
||||
style="margin-bottom: 16px;"
|
||||
/>
|
||||
<a-form-item label="菜单权限">
|
||||
<a-checkbox-group
|
||||
v-model:value="roleForm.permissions"
|
||||
:disabled="rolePermissionsReadonly"
|
||||
class="perm-grid"
|
||||
>
|
||||
<a-checkbox
|
||||
v-for="item in permissionCatalog.menus"
|
||||
:key="item.code"
|
||||
:value="item.code"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="操作权限">
|
||||
<a-checkbox-group
|
||||
v-model:value="roleForm.permissions"
|
||||
:disabled="rolePermissionsReadonly"
|
||||
class="perm-grid"
|
||||
>
|
||||
<a-checkbox
|
||||
v-for="item in permissionCatalog.actions"
|
||||
:key="item.code"
|
||||
:value="item.code"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<div class="glass-card role-help">
|
||||
<h3 style="margin-top: 0; color: #fff;">角色权限说明</h3>
|
||||
<ul class="role-list">
|
||||
<li><strong>管理员</strong>:全局数据范围 + 全部权限,不可删除</li>
|
||||
<li><strong>运营 / 只读 / 自定义角色</strong>:仅能访问自己的账号数据;菜单与操作由勾选权限决定</li>
|
||||
<li>自定义角色可在「角色设定」中新建,并分配给用户</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1000,48 +675,11 @@ onMounted(() => {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.users-tabs {
|
||||
padding: 8px 16px 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.users-tabs:hover,
|
||||
.users-toolbar:hover,
|
||||
.table-card:hover,
|
||||
.roles-panel:hover,
|
||||
.role-help:hover {
|
||||
.table-card:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.users-tabs :deep(.ant-tabs-nav) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.users-tabs :deep(.ant-tabs-tab) {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
padding: 12px 4px;
|
||||
}
|
||||
|
||||
.roles-panel {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.perm-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 10px 14px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.perm-grid :deep(.ant-checkbox-wrapper) {
|
||||
color: #e5e7eb !important;
|
||||
margin-left: 0 !important;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.users-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1234,18 +872,6 @@ onMounted(() => {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.role-help {
|
||||
margin-top: 24px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.role-list {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.8;
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.user-form :deep(.ant-form-item-label > label) {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
@@ -1278,15 +904,5 @@ onMounted(() => {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.role-help {
|
||||
margin-top: 16px !important;
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.role-list {
|
||||
padding-left: 18px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user