143 lines
5.3 KiB
TypeScript
143 lines
5.3 KiB
TypeScript
/** 数据权限分组列表模型(任务 59;module 13 task 253 扩展编辑能力):解析 VO + 摘要 + 提交载荷。 */
|
||
import { unwrap } from '../../api/envelope.ts'
|
||
|
||
export interface ShopGroupItem {
|
||
id: number
|
||
name: string
|
||
leaderUsername: string
|
||
leaderUserId?: number
|
||
memberCount: number
|
||
memberUsernames: string[]
|
||
memberUserIds?: number[]
|
||
createdAt?: string
|
||
updatedAt?: string
|
||
}
|
||
|
||
export interface ShopGroupFormErrors {
|
||
groupName?: string
|
||
}
|
||
|
||
function text(value: unknown): string {
|
||
return typeof value === 'string' ? value.trim() : ''
|
||
}
|
||
|
||
function numberOrNull(value: unknown): number | null {
|
||
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null
|
||
}
|
||
|
||
/** 解析单条分组项;缺 id 视为无效。 */
|
||
export function parseShopGroupItem(raw: unknown): ShopGroupItem | null {
|
||
if (!raw || typeof raw !== 'object') return null
|
||
const record = raw as Record<string, unknown>
|
||
const id = numberOrNull(record.id)
|
||
if (id === null) return null
|
||
const names = Array.isArray(record.memberUsernames)
|
||
? record.memberUsernames.map((v) => text(v)).filter(Boolean)
|
||
: []
|
||
const memberIds = Array.isArray(record.memberUserIds)
|
||
? record.memberUserIds.map((v) => numberOrNull(v)).filter((v): v is number => v !== null && v > 0)
|
||
: []
|
||
const count = numberOrNull(record.memberCount)
|
||
const item: ShopGroupItem = {
|
||
id,
|
||
name: text(record.groupName ?? record.name) || `分组${id}`,
|
||
leaderUsername: text(record.leaderUsername ?? record.leader_name),
|
||
memberCount: count === null ? names.length : count,
|
||
memberUsernames: names,
|
||
}
|
||
const leaderId = numberOrNull(record.leaderUserId ?? record.leader_user_id)
|
||
if (leaderId !== null && leaderId > 0) item.leaderUserId = leaderId
|
||
if (memberIds.length) item.memberUserIds = memberIds
|
||
const createdAt = text(record.createdAt ?? record.created_at)
|
||
if (createdAt) item.createdAt = createdAt
|
||
const updatedAt = text(record.updatedAt ?? record.updated_at)
|
||
if (updatedAt) item.updatedAt = updatedAt
|
||
return item
|
||
}
|
||
|
||
/** 归一化分组列表响应(信封或裸数组)。 */
|
||
export function parseShopGroupList(payload: unknown): ShopGroupItem[] {
|
||
const data = unwrap<unknown>(payload)
|
||
if (!Array.isArray(data)) return []
|
||
return data
|
||
.map((raw) => parseShopGroupItem(raw))
|
||
.filter((item): item is ShopGroupItem => item !== null)
|
||
}
|
||
|
||
/** 分组摘要:分组总数 + 组员总数。 */
|
||
export function shopGroupSummary(groups: ShopGroupItem[]): { groupCount: number; memberTotal: number } {
|
||
let memberTotal = 0
|
||
for (const group of groups) memberTotal += group.memberCount || 0
|
||
return { groupCount: groups.length, memberTotal }
|
||
}
|
||
|
||
/** 分组名必填校验(镜像后端 NotBlank 文案「请输入分组名称」)。 */
|
||
export function validateShopGroupForm(name: string): ShopGroupFormErrors {
|
||
const errors: ShopGroupFormErrors = {}
|
||
if (!text(name)) errors.groupName = '请输入分组名称'
|
||
return errors
|
||
}
|
||
|
||
/** 创建载荷:groupName + 成员 + 创建人(= 组长,当前操作者)。 */
|
||
export function toShopGroupCreatePayload(
|
||
name: string,
|
||
memberUserIds: number[],
|
||
createdById: number,
|
||
): { groupName: string; memberUserIds: number[]; createdById: number } {
|
||
return { groupName: text(name), memberUserIds: dedupeIds(memberUserIds), createdById }
|
||
}
|
||
|
||
/** 更新载荷:groupName + 成员(组长/创建人不可改)。 */
|
||
export function toShopGroupUpdatePayload(name: string, memberUserIds: number[]): { groupName: 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>()
|
||
for (const v of ids) {
|
||
const id = numberOrNull(v)
|
||
if (id !== null && id > 0) set.add(id)
|
||
}
|
||
return [...set].sort((a, b) => a - b)
|
||
}
|