fix: 管理员用户详情弹窗崩溃与多俱乐部分配UI

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-06 18:00:00 +08:00
parent 37a1a15f90
commit 58be204448
5 changed files with 356 additions and 65 deletions

View File

@@ -3,7 +3,7 @@ const CLUB_SCOPE_KEY = 'admin_club_scope';
const CLUB_CONTEXT_KEY = 'admin_club_context';
const MENU_ACCESS_KEY = 'kefu_menu_access';
const MENU_SESSION_KEY = 'kefu_menu_session_v';
const MENU_SESSION_VERSION = '11';
const MENU_SESSION_VERSION = '12';
const DEFAULT_CLUB_ID = 'xq';
let menuAccessInflight = null;
@@ -25,27 +25,49 @@ export function setAdminClubContext(ctx) {
export function mergeServerClubContext(serverCtx) {
if (!serverCtx) return;
const allowedIds = new Set((serverCtx.clubs || []).map((c) => c.club_id));
const prevScope = localStorage.getItem(CLUB_SCOPE_KEY);
const prevClubId = localStorage.getItem(CLUB_ID_KEY);
const merged = { ...serverCtx };
if (prevScope) {
merged.scope = prevScope === 'all' ? 'ALL_CLUBS' : 'SINGLE_CLUB';
merged.club_id = prevClubId || merged.club_id || DEFAULT_CLUB_ID;
let clubId = serverCtx.club_id || DEFAULT_CLUB_ID;
let scopeKey = serverCtx.scope === 'ALL_CLUBS' ? 'all' : 'single';
if (prevClubId && allowedIds.has(prevClubId)) {
clubId = prevClubId;
if (prevScope === 'all' && serverCtx.is_group_admin) {
scopeKey = 'all';
} else if (prevScope === 'single') {
scopeKey = 'single';
}
} else if (prevScope === 'all' && serverCtx.is_group_admin) {
scopeKey = 'all';
}
localStorage.setItem(CLUB_CONTEXT_KEY, JSON.stringify(merged));
if (prevClubId) {
localStorage.setItem(CLUB_ID_KEY, prevClubId);
} else {
localStorage.setItem(CLUB_ID_KEY, merged.club_id || DEFAULT_CLUB_ID);
}
if (prevScope) {
localStorage.setItem(CLUB_SCOPE_KEY, prevScope);
} else {
localStorage.setItem(CLUB_SCOPE_KEY, merged.scope === 'ALL_CLUBS' ? 'all' : 'single');
const merged = {
...serverCtx,
club_id: clubId,
scope: scopeKey === 'all' ? 'ALL_CLUBS' : 'SINGLE_CLUB',
};
if (scopeKey === 'single' && merged.assignments?.length) {
const hit = merged.assignments.find((a) => a.club_id === clubId);
if (hit) {
merged.role_code = hit.role_code;
merged.role_name = hit.role_name;
}
}
setAdminClubContext(merged);
return merged;
}
export function resetAdminClubContextOnLogin() {
localStorage.removeItem(CLUB_ID_KEY);
localStorage.removeItem(CLUB_SCOPE_KEY);
localStorage.removeItem(CLUB_CONTEXT_KEY);
clearMenuAccess();
}
export function getAdminClubContext() {
try {
const raw = localStorage.getItem(CLUB_CONTEXT_KEY);

View File

@@ -268,6 +268,7 @@ import {
canSwitchClubByMenu,
ensureMenuAccess,
canShowMenuPage,
clearMenuAccess,
} from '@/utils/club-context'
const menuAccessState = ref(getMenuAccess())
@@ -359,14 +360,18 @@ const onScopeSelectChange = (val) => {
})
ElMessage.success('已切换至:集团汇总(全部子公司)')
} else {
const assign = (ctx.assignments || []).find((a) => a.club_id === val)
setAdminClubContext({
...ctx,
scope: 'SINGLE_CLUB',
club_id: val,
role_code: assign?.role_code || ctx.role_code,
role_name: assign?.role_name || ctx.role_name,
})
const hit = clubOptions.value.find((c) => c.club_id === val)
ElMessage.success(`已切换至:${hit?.name || val}`)
}
clearMenuAccess()
window.location.reload()
}
@@ -523,9 +528,9 @@ provide('permPrefixMap', permPrefixMap)
provide('goeasyAppkey', goeasyAppkey)
provide('hasAnyChatPerm', hasAnyChatPerm)
onMounted(() => {
loadMenuAccess()
loadClubContext()
onMounted(async () => {
await loadClubContext()
await loadMenuAccess()
fetchStats()
fetchKefuPermission()
proxy.$emitter?.on('refresh-stats', fetchStats)

View File

@@ -65,7 +65,7 @@ import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { User, Lock, Key } from '@element-plus/icons-vue'
import axios from 'axios' // 直接使用 axios不经过封装的 request
import { JITUAN_API, getDefaultLandingPath, setAdminClubContext, setMenuAccess } from '@/utils/club-context'
import { JITUAN_API, getDefaultLandingPath, setAdminClubContext, setMenuAccess, resetAdminClubContextOnLogin } from '@/utils/club-context'
const router = useRouter()
const loading = ref(false)
@@ -90,6 +90,7 @@ const handleLogin = async () => {
}
loading.value = true
resetAdminClubContextOnLogin()
try {
// 直接使用 axios 发送请求baseURL 从 window.$baseURL 获取
const response = await axios.post(

View File

@@ -35,8 +35,8 @@
<el-option label="全部角色" value="" />
<el-option
v-for="role in roleList"
:key="role.role_code"
:label="role.role_name"
:key="`${role.club_id || ''}-${role.role_code}`"
:label="roleClubLabel(role)"
:value="role.role_code"
/>
</el-select>
@@ -53,12 +53,31 @@
<el-table-column prop="phone" label="账号" width="140" />
<el-table-column prop="yonghuid" label="用户ID" width="100" />
<el-table-column prop="nicheng" label="昵称" width="150" />
<el-table-column label="角色" min-width="180">
<el-table-column label="俱乐部任职" min-width="200">
<template #default="{ row }">
<el-tag v-for="role in row.roles" :key="role.role_code" size="small" style="margin-right: 6px;">
{{ role.role_name }}
<el-tag
v-for="a in row.assignments || []"
:key="a.id"
size="small"
:type="a.is_primary ? 'success' : 'info'"
style="margin-right: 6px; margin-bottom: 4px;"
>
{{ a.club_label }}{{ a.is_primary ? '·主' : '' }}
</el-tag>
<span v-if="!row.roles.length">无角色</span>
<span v-if="!(row.assignments || []).length" class="muted-text">未分配</span>
</template>
</el-table-column>
<el-table-column label="角色" min-width="200">
<template #default="{ row }">
<el-tag
v-for="role in (row.roles || [])"
:key="`${role.club_id || ''}-${role.role_code}`"
size="small"
style="margin-right: 6px;"
>
{{ roleClubLabel(role) }}
</el-tag>
<span v-if="!(row.roles || []).length">无角色</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
@@ -91,13 +110,15 @@
</div>
</div>
<!-- 用户详情弹窗 -->
<!-- 用户详情弹窗须在 v-if/v-else -->
<el-dialog
v-model="detailDialogVisible"
:title="`编辑用户 - ${currentUser?.phone}`"
width="700px"
:title="detailDialogTitle"
width="720px"
class="user-dialog"
destroy-on-close
append-to-body
align-center
:close-on-click-modal="false"
>
<div class="dialog-content">
<!-- 基本信息 -->
@@ -140,10 +161,36 @@
</el-form>
</div>
<!-- 俱乐部任职 -->
<div class="club-section">
<div class="section-title">
<span>俱乐部任职切换范围</span>
<el-button type="primary" size="small" @click="openAddClubDialog">
<el-icon><Plus /></el-icon> 添加俱乐部
</el-button>
</div>
<div class="club-assign-list">
<div
v-for="a in currentUserAssignments"
:key="a.id"
class="club-assign-item"
>
<el-tag :type="a.is_primary ? 'success' : 'info'" size="small">
{{ a.club_label }}{{ a.is_primary ? '(主任职)' : '' }}
</el-tag>
<span class="assign-role-label">{{ a.role_name }}</span>
<el-button type="danger" link size="small" @click="removeClubAssignment(a)">移除</el-button>
</div>
<div v-if="currentUserAssignments.length === 0" class="empty-role">
暂未分配俱乐部添加后该用户顶栏可切换无角色则看不到菜单
</div>
</div>
</div>
<!-- 角色管理 -->
<div class="role-section">
<div class="section-title">
<span>所属角色</span>
<span>功能角色按俱乐部</span>
<el-button type="primary" size="small" @click="openAddRoleForUser">
<el-icon><Plus /></el-icon> 添加角色
</el-button>
@@ -151,10 +198,10 @@
<div class="role-list">
<div
v-for="role in currentUserRoles"
:key="role.role_code"
:key="`${role.club_id}-${role.role_code}`"
class="role-tag"
>
{{ role.role_name }}
{{ roleClubLabel(role) }}
<el-icon class="remove-icon" @click="removeUserRole(role)">
<Close />
</el-icon>
@@ -176,14 +223,28 @@
<el-dialog
v-model="addRoleForUserVisible"
title="添加角色"
width="500px"
width="520px"
class="role-select-dialog"
append-to-body
align-center
>
<el-form label-width="100px">
<el-form-item label="所属俱乐部" required>
<el-select v-model="roleBindClubId" placeholder="先选俱乐部" style="width: 100%" @change="selectedRoleCodes = []">
<el-option
v-for="c in clubOptions"
:key="c.club_id"
:label="c.name"
:value="c.club_id"
/>
</el-select>
</el-form-item>
</el-form>
<div class="role-select-list">
<el-checkbox-group v-model="selectedRoleCodes">
<div
v-for="role in availableRolesForUser"
:key="role.role_code"
:key="`${role.club_id}-${role.role_code}`"
class="role-select-item"
>
<el-checkbox :label="role.role_code">
@@ -191,6 +252,8 @@
</el-checkbox>
</div>
</el-checkbox-group>
<div v-if="!roleBindClubId" class="empty-role">请先选择俱乐部</div>
<div v-else-if="!availableRolesForUser.length" class="empty-role">该俱乐部下无可选角色</div>
</div>
<template #footer>
<el-button @click="addRoleForUserVisible = false">取消</el-button>
@@ -198,13 +261,47 @@
</template>
</el-dialog>
<!-- 添加俱乐部任职 -->
<el-dialog v-model="addClubVisible" title="添加俱乐部任职" width="480px" append-to-body align-center>
<el-form label-width="100px">
<el-form-item label="俱乐部" required>
<el-select v-model="clubAssignForm.club_id" placeholder="选择俱乐部" style="width: 100%">
<el-option
v-for="c in availableClubsForAssign"
:key="c.club_id"
:label="c.name"
:value="c.club_id"
/>
</el-select>
</el-form-item>
<el-form-item label="任职标签">
<el-select v-model="clubAssignForm.role_code" style="width: 100%">
<el-option label="俱乐部管理员" value="CLUB_ADMIN" />
<el-option label="俱乐部总负责人" value="CLUB_OWNER" />
<el-option label="俱乐部财务" value="CLUB_FINANCE" />
<el-option label="俱乐部售后" value="CLUB_AFTER_SALES" />
<el-option label="俱乐部运营" value="CLUB_OPERATOR" />
</el-select>
</el-form-item>
<el-form-item label="主任职">
<el-switch v-model="clubAssignForm.is_primary" />
</el-form-item>
</el-form>
<div class="form-hint">添加后用户顶栏可切到该俱乐部再为该俱乐部绑功能角色才有菜单</div>
<template #footer>
<el-button @click="addClubVisible = false">取消</el-button>
<el-button type="primary" :loading="assigningClub" @click="confirmAddClubAssignment">保存</el-button>
</template>
</el-dialog>
<!-- 添加用户弹窗 -->
<el-dialog
v-model="addUserDialogVisible"
title="添加后台用户"
width="600px"
class="user-dialog"
destroy-on-close
append-to-body
align-center
>
<div class="dialog-content">
<el-form :model="newUserForm" label-width="100px" ref="addUserFormRef">
@@ -235,11 +332,11 @@
<el-checkbox-group v-model="newUserRoleCodes">
<div
v-for="role in roleList"
:key="role.role_code"
:key="`${role.club_id || ''}-${role.role_code}`"
class="role-select-item"
>
<el-checkbox :label="role.role_code">
<span>{{ role.role_name }}</span>
<span>{{ roleClubLabel(role) }}</span>
</el-checkbox>
</div>
</el-checkbox-group>
@@ -249,20 +346,35 @@
</div>
<template #footer>
<el-button @click="addUserDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmAddUser">确认添加</el-button>
<el-button type="primary" :loading="addingUser" :disabled="addingUser" @click="confirmAddUser">确认添加</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close } from '@element-plus/icons-vue'
import request from '@/utils/request'
import { JITUAN_API, getAdminClubContext } from '@/utils/club-context'
const username = localStorage.getItem('username') || ''
const clubOptions = ref([])
const clubNameMap = computed(() => {
const m = {}
for (const c of clubOptions.value) m[c.club_id] = c.name
return m
})
const roleClubLabel = (role) => {
if (!role) return ''
const cid = role.club_id || ''
const cname = cid ? (clubNameMap.value[cid] || cid) : '全局'
return `[${cname}] ${role.role_name || role.role_code || ''}`
}
// 权限状态
const hasPermission = ref(true)
@@ -293,13 +405,39 @@ const editForm = ref({
status: 1
})
const currentUserRoles = ref([])
const currentUserAssignments = ref([])
const detailDialogTitle = computed(() => {
const u = currentUser.value
if (!u) return '编辑用户'
return `编辑用户 - ${u.phone || u.yonghuid || ''}`
})
// 为用户添加角色弹窗
const addRoleForUserVisible = ref(false)
const selectedRoleCodes = ref([])
const roleBindClubId = ref('')
// 俱乐部任职
const addClubVisible = ref(false)
const assigningClub = ref(false)
const clubAssignForm = ref({ club_id: '', role_code: 'CLUB_ADMIN', is_primary: false })
const availableClubsForAssign = computed(() => {
const assigned = new Set(currentUserAssignments.value.map((a) => a.club_id))
return clubOptions.value.filter((c) => !assigned.has(c.club_id))
})
const availableRolesForUser = computed(() => {
const existingCodes = currentUserRoles.value.map(r => r.role_code)
return roleList.value.filter(r => !existingCodes.includes(r.role_code))
if (!roleBindClubId.value) return []
const existing = new Set(
currentUserRoles.value
.filter((r) => (r.club_id || '') === roleBindClubId.value)
.map((r) => r.role_code)
)
return roleList.value.filter(
(r) => (r.club_id || '') === roleBindClubId.value && !existing.has(r.role_code)
)
})
// 添加用户弹窗
@@ -311,12 +449,16 @@ const newUserForm = ref({
nicheng: ''
})
const newUserRoleCodes = ref([])
const addingUser = ref(false)
// ==================== 接口调用 ====================
// 获取角色列表(第一个接口
// 获取角色列表(全部俱乐部,用于分俱乐部绑角色
const fetchRoleList = async () => {
try {
const res = await request.post('/houtai/hqyhjsgl', { username })
let res = await request.post('/houtai/hqyhjsgl', { username, all_clubs: true })
if (res.code !== 0) {
res = await request.post('/houtai/hqyhjsgl', { username })
}
if (res.code === 0) {
roleList.value = res.data.roles || []
return true
@@ -333,6 +475,20 @@ const fetchRoleList = async () => {
}
}
const loadClubOptions = async () => {
try {
const res = await request.get(JITUAN_API.clubList, { skipErrorToast: true })
if (res.code === 0 && Array.isArray(res.data)) {
clubOptions.value = res.data.map((c) => ({ club_id: c.club_id, name: c.name }))
return
}
} catch {
// fallback
}
const ctx = getAdminClubContext()
clubOptions.value = ctx?.clubs || []
}
// 获取用户列表
const fetchUserList = async () => {
if (!hasPermission.value) return
@@ -372,16 +528,25 @@ const resetFilters = () => {
}
// 打开用户详情弹窗
const openDetailDialog = (user) => {
currentUser.value = user
editForm.value = {
nicheng: user.nicheng || '',
password: '',
erjimima: '',
status: user.status
const openDetailDialog = async (user) => {
if (!user) return
try {
currentUser.value = { ...user }
editForm.value = {
nicheng: user.nicheng || '',
password: '',
erjimima: '',
status: user.status ?? 1,
}
currentUserRoles.value = Array.isArray(user.roles) ? [...user.roles] : []
currentUserAssignments.value = Array.isArray(user.assignments) ? [...user.assignments] : []
detailDialogVisible.value = false
await nextTick()
detailDialogVisible.value = true
} catch (e) {
console.error('openDetailDialog failed:', e)
ElMessage.error('打开详情失败,请刷新页面重试')
}
currentUserRoles.value = [...(user.roles || [])]
detailDialogVisible.value = true
}
// 保存用户修改(昵称、密码、二级密码、状态、角色已通过其他接口单独处理)
@@ -418,11 +583,81 @@ const saveUserInfo = async () => {
// 为用户添加角色(打开弹窗)
const openAddRoleForUser = () => {
selectedRoleCodes.value = []
roleBindClubId.value = currentUserAssignments.value[0]?.club_id || clubOptions.value[0]?.club_id || ''
addRoleForUserVisible.value = true
}
const openAddClubDialog = () => {
clubAssignForm.value = {
club_id: availableClubsForAssign.value[0]?.club_id || '',
role_code: 'CLUB_ADMIN',
is_primary: currentUserAssignments.value.length === 0,
}
addClubVisible.value = true
}
const confirmAddClubAssignment = async () => {
if (!clubAssignForm.value.club_id) {
ElMessage.warning('请选择俱乐部')
return
}
assigningClub.value = true
try {
const res = await request.post('/houtai/xghtyhsj', {
username,
action: 'add_club_assignment',
yonghuid: currentUser.value.yonghuid,
phone: currentUser.value.phone,
club_id: clubAssignForm.value.club_id,
role_code: clubAssignForm.value.role_code,
is_primary: clubAssignForm.value.is_primary,
})
if (res.code === 0) {
ElMessage.success('俱乐部任职已添加')
addClubVisible.value = false
await fetchUserList()
const updated = userList.value.find((u) => u.yonghuid === currentUser.value.yonghuid)
if (updated) {
currentUserAssignments.value = [...(updated.assignments || [])]
}
} else {
ElMessage.error(res.msg || '添加失败')
}
} catch {
ElMessage.error('网络错误')
} finally {
assigningClub.value = false
}
}
const removeClubAssignment = async (row) => {
try {
await ElMessageBox.confirm(`移除 ${row.club_label} 任职?`, '提示', { type: 'warning' })
const res = await request.post('/houtai/xghtyhsj', {
username,
action: 'remove_club_assignment',
yonghuid: currentUser.value.yonghuid,
phone: currentUser.value.phone,
assignment_id: row.id,
})
if (res.code === 0) {
ElMessage.success('已移除')
currentUserAssignments.value = currentUserAssignments.value.filter((a) => a.id !== row.id)
fetchUserList()
} else {
ElMessage.error(res.msg || '移除失败')
}
} catch {
// cancel
}
}
// 确认添加角色
const confirmAddUserRoles = async () => {
if (!roleBindClubId.value) {
ElMessage.warning('请先选择俱乐部')
return
}
if (selectedRoleCodes.value.length === 0) {
ElMessage.warning('请至少选择一个角色')
return
@@ -433,17 +668,17 @@ const confirmAddUserRoles = async () => {
action: 'add_user_roles',
yonghuid: currentUser.value.yonghuid,
phone: currentUser.value.phone,
role_codes: selectedRoleCodes.value
role_codes: selectedRoleCodes.value,
target_club_id: roleBindClubId.value,
})
if (res.code === 0) {
ElMessage.success('角色添加成功')
// 刷新当前用户的角色列表
const updatedUser = userList.value.find(u => u.phone === currentUser.value.phone)
if (updatedUser) {
currentUserRoles.value = [...(updatedUser.roles || []), ...roleList.value.filter(r => selectedRoleCodes.value.includes(r.role_code))]
}
addRoleForUserVisible.value = false
fetchUserList() // 刷新列表
await fetchUserList()
const updated = userList.value.find((u) => u.yonghuid === currentUser.value.yonghuid)
if (updated) {
currentUserRoles.value = [...(updated.roles || [])]
}
} else {
ElMessage.error(res.msg || '添加失败')
}
@@ -460,7 +695,8 @@ const removeUserRole = async (role) => {
action: 'remove_user_role',
yonghuid: currentUser.value.yonghuid,
phone: currentUser.value.phone,
role_code: role.role_code
role_code: role.role_code,
target_club_id: role.club_id || '',
})
if (res.code === 0) {
ElMessage.success('角色移除成功')
@@ -484,6 +720,7 @@ const openAddUserDialog = () => {
// 确认添加用户
const confirmAddUser = async () => {
if (addingUser.value) return
if (!newUserForm.value.phone || !newUserForm.value.password || !newUserForm.value.erjimima || !newUserForm.value.nicheng) {
ElMessage.warning('请填写完整信息')
return
@@ -492,6 +729,7 @@ const confirmAddUser = async () => {
ElMessage.warning('密码和二级密码至少6位')
return
}
addingUser.value = true
try {
const res = await request.post('/houtai/tjyhjs', {
username,
@@ -510,6 +748,8 @@ const confirmAddUser = async () => {
}
} catch (error) {
ElMessage.error('网络错误')
} finally {
addingUser.value = false
}
}
@@ -519,6 +759,7 @@ onMounted(async () => {
ElMessage.error('未获取到用户信息')
return
}
await loadClubOptions()
const success = await fetchRoleList()
if (success) {
await fetchUserList()
@@ -672,6 +913,28 @@ onMounted(async () => {
padding: 20px 0;
text-align: center;
}
.muted-text {
color: rgba(255,255,255,0.45);
font-size: 13px;
}
.club-assign-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.club-assign-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: rgba(255,255,255,0.05);
border-radius: 8px;
}
.assign-role-label {
flex: 1;
color: rgba(255,255,255,0.65);
font-size: 13px;
}
.role-select-list {
max-height: 400px;
overflow-y: auto;

View File

@@ -7,8 +7,8 @@
:closable="false"
show-icon
class="scope-tip"
title="操作权限在「角色管理」里配"
description="查看会员=3300a、财务=caiwu、订单=002ab 等:角色管理 → 角色详情 → 添加权限。给账号绑角色:管理员用户。"
title="多俱乐部 + 分俱乐部权限(推荐)"
description="1本页「添加任职」同一账号可添加多条分别指定星阙、星之界等俱乐部无需集团权限即可顶栏切换。2「角色管理」为各俱乐部创建角色并配权限码。3「管理员用户」绑定角色时请先在顶栏切到对应俱乐部再绑或传 target_club_id。切换俱乐部后菜单/权限按该俱乐部的角色生效。"
/>
<el-alert
@@ -16,8 +16,8 @@
:closable="false"
show-icon
class="scope-tip"
title="集团高管 / 查看全部订单与用户"
description="功能权限:角色管理里给「订单 002ab」「用户列表」等权限。数据范围本页添加任职范围选「全部俱乐部 ALL_CLUBS」登录后顶栏切「集团汇总」即可看全部订单/财务/用户(统计按各俱乐部汇总,不串单俱乐部数据)。"
title="集团汇总(可选)"
description="仅当需要一次看全部子公司汇总数据时,才添加「集团(全部子公司)」任职。普通多俱乐部负责人不必给集团权限,分别添加各俱乐部任职即可。"
/>
<div class="action-bar">
@@ -167,7 +167,7 @@
<el-option label="俱乐部售后" value="CLUB_AFTER_SALES" />
<el-option label="俱乐部运营" value="CLUB_OPERATOR" />
</el-select>
<div class="role-hint">任职角色只是备注标签<strong>不能</strong>替代角色管理里的功能权限</div>
<div class="role-hint">任职角色为管理标签实际菜单权限请在角色管理为该俱乐部创建角色并在管理员用户绑定</div>
</el-form-item>
<el-form-item label="主任职">