align(分组管理): 组员候选按归属过滤(超管全量/非超管仅本组长创建的普通账号)、新建组长恒为当前用户、补创建时间列、chips 全量渲染(对齐 admin.js getEligibleShopManageGroupUsers/帮助文案)
This commit is contained in:
@@ -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 loadUserOptions:page=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>()
|
||||
|
||||
Reference in New Issue
Block a user