align(分组管理): 组员候选按归属过滤(超管全量/非超管仅本组长创建的普通账号)、新建组长恒为当前用户、补创建时间列、chips 全量渲染(对齐 admin.js getEligibleShopManageGroupUsers/帮助文案)

This commit is contained in:
2026-09-05 23:00:08 +08:00
parent 63339b401c
commit 1692fa4a69
5 changed files with 142 additions and 30 deletions
@@ -1,8 +1,12 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import type { AdminUser } from '@/types/admin'
import type { ShopGroupItem } from './shop-group-model'
import { validateShopGroupForm } from './shop-group-model'
import {
eligibleGroupMembers,
groupMemberCandidateHint,
validateShopGroupForm,
type ShopGroupItem,
} from './shop-group-model'
import { createShopGroup, updateShopGroup } from './shop-group-api'
import { fetchUserList } from '@/api/users'
import { showAdminFeedback } from '@/components/admin-feedback-ui'
@@ -15,18 +19,24 @@ const emit = defineEmits<{ (e: 'update:modelValue', v: boolean): void; (e: 'save
const session = useAdminSessionStore()
const groupName = ref('')
const memberIds = ref<number[]>([])
const leaderId = ref<number | null>(null)
const errorText = ref('')
const busy = ref(false)
const users = ref<AdminUser[]>([])
function isAdminUser(u: AdminUser): boolean {
const role = (u.role || '').toLowerCase()
return role === 'admin' || role === 'super_admin' || u.isAdmin === true
}
/** 组长:新建=当前登录用户;编辑=分组原组长(不可改,只读展示)。 */
const leaderUserId = computed<number | null>(() =>
props.group ? (props.group.leaderUserId ?? null) : (session.user?.id ?? null),
)
const leaderUsername = computed<string>(() =>
props.group ? (props.group.leaderUsername || '') : (session.user?.username || '当前登录用户'),
)
const operatorRole = computed<string>(() => session.user?.role || '')
const adminCandidates = ref<AdminUser[]>([])
const memberCandidates = ref<AdminUser[]>([])
/** 组员候选:按组长归属过滤(超管全量、非超管仅本组长创建的普通账号)。 */
const memberCandidates = computed(() =>
eligibleGroupMembers(users.value, leaderUserId.value, operatorRole.value),
)
const candidateHint = computed(() => groupMemberCandidateHint(memberCandidates.value.length, operatorRole.value))
watch(
() => props.modelValue,
@@ -34,22 +44,19 @@ watch(
if (!open) return
groupName.value = ''
memberIds.value = []
leaderId.value = null
errorText.value = ''
if (props.group) {
groupName.value = props.group.name
memberIds.value = props.group.memberUserIds ?? []
leaderId.value = props.group.leaderUserId ?? null
}
},
)
async function loadCandidates(): Promise<void> {
try {
const page = await fetchUserList({ page: 1, pageSize: 200 })
// 对齐 admin.js loadUserOptionspage=1&page_size=999 全量拉取,避免归属过滤漏人。
const page = await fetchUserList({ page: 1, pageSize: 999 })
users.value = page.items
adminCandidates.value = page.items.filter(isAdminUser)
memberCandidates.value = page.items.filter((u) => !isAdminUser(u))
} catch (error) {
showAdminFeedback(actionableErrorText(error), 'error')
}
@@ -69,13 +76,13 @@ async function save(): Promise<void> {
await updateShopGroup(props.group.id, groupName.value, memberIds.value)
showAdminFeedback('更新成功', 'success')
} else {
// 对齐 admin:组长恒为当前登录用户,提交 createdBy=当前用户 id。
const operatorId = session.user?.id ?? 0
if (!operatorId) {
showAdminFeedback('无法获取当前登录用户,请重新登录', 'error')
return
}
const createdBy = leaderId.value && leaderId.value > 0 ? leaderId.value : operatorId
await createShopGroup(groupName.value, memberIds.value, createdBy)
await createShopGroup(groupName.value, memberIds.value, operatorId)
showAdminFeedback('创建成功', 'success')
}
emit('saved')
@@ -103,13 +110,8 @@ onMounted(loadCandidates)
<el-form-item label="分组名称" :error="errorText || undefined">
<el-input v-model="groupName" placeholder="请输入分组名称" />
</el-form-item>
<el-form-item v-if="!group && operatorSuper" label="组长创建人">
<el-select v-model="leaderId" clearable filterable placeholder="默认为当前登录管理员" style="width: 100%">
<el-option v-for="u in adminCandidates" :key="u.id" :label="u.username" :value="u.id" />
</el-select>
</el-form-item>
<el-form-item v-else-if="group" label="组长">
<el-input :model-value="group.leaderUsername" readonly />
<el-form-item label="组长">
<el-input :model-value="leaderUsername" readonly placeholder="当前登录用户" />
</el-form-item>
<el-form-item label="组员">
<el-select
@@ -122,7 +124,7 @@ onMounted(loadCandidates)
>
<el-option v-for="u in memberCandidates" :key="u.id" :label="u.username" :value="u.id" />
</el-select>
<div class="candidate-hint">组员范围:当前可选普通账号(共 {{ memberCandidates.length }} 名)</div>
<div class="candidate-hint">{{ candidateHint }}(共 {{ memberCandidates.length }} 名)</div>
</el-form-item>
</el-form>
<template #footer>
@@ -95,12 +95,14 @@ onMounted(loadGroups)
<el-table-column label="组员" min-width="240">
<template #default="{ row }">
<template v-if="row.memberUsernames.length">
<el-tag v-for="name in row.memberUsernames.slice(0, 10)" :key="name" size="small" class="member-tag">{{ name }}</el-tag>
<span v-if="row.memberUsernames.length > 10" class="member-more">+{{ row.memberUsernames.length - 10 }}</span>
<el-tag v-for="name in row.memberUsernames" :key="name" size="small" class="member-tag">{{ name }}</el-tag>
</template>
<span v-else class="member-none"></span>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" min-width="170">
<template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column prop="updatedAt" label="修改时间" min-width="170">
<template #default="{ row }">{{ formatDateTime(row.updatedAt || row.createdAt) }}</template>
</el-table-column>
@@ -92,6 +92,45 @@ export function toShopGroupUpdatePayload(name: string, memberUserIds: number[]):
return { groupName: text(name), memberUserIds: dedupeIds(memberUserIds) }
}
/** 组员候选用户(对齐 admin.js getEligibleShopManageGroupUsers 的过滤语义)。 */
export interface GroupMemberCandidateUser {
id: number
username: string
role: string
createdById?: number | null
}
/**
* 组员候选过滤(对齐 admin.js:3020-3028):
* - 组长本人永不进候选;
* - 超管可选全部用户(不论角色/归属);
* - 非超管仅可选"组长(创建人)名下"的普通账号。
*/
export function eligibleGroupMembers(
users: GroupMemberCandidateUser[],
leaderUserId: number | null,
operatorRole: string,
): GroupMemberCandidateUser[] {
const leaderKey = leaderUserId != null && leaderUserId > 0 ? String(leaderUserId) : ''
const isSuper = (operatorRole || '').toLowerCase() === 'super_admin'
return users.filter((u) => {
if (String(u.id) === leaderKey) return false
if (isSuper) return true
return (
(u.role || '').toLowerCase() === 'normal' &&
String(u.createdById ?? '') === leaderKey
)
})
}
/** 组员候选帮助文案(对齐 admin.js:3179-3182)。 */
export function groupMemberCandidateHint(candidateCount: number, operatorRole: string): string {
if (candidateCount > 0) return '可添加当前组长创建的普通员工账号'
return (operatorRole || '').toLowerCase() === 'normal'
? '普通账号没有下属普通员工时,这里会为空;当前账号只能作为组长使用。'
: '当前组长名下暂无可添加的普通员工账号。'
}
function dedupeIds(ids: unknown[]): number[] {
if (!Array.isArray(ids)) return []
const set = new Set<number>()