feat(users): 收口账号权限与登录注册视觉
This commit is contained in:
parent
38a182bc8d
commit
1f2d2af257
@ -85,12 +85,12 @@ businessRoles includes DESIGN / PLANNING / PURCHASING / WAREHOUSE
|
||||
|
||||
- 旧权限角色迁移为 `role=USER`。
|
||||
- 原业务身份追加 `businessRoles=['DESIGN']`。
|
||||
- `User / AccountTemplate / ApprovalWorkflow.businessRoles` 统一补齐为非空数组。
|
||||
- `User / ApprovalWorkflow.businessRoles` 统一补齐为非空数组。
|
||||
- 新请求不再允许写入旧设计权限角色;旧值应返回 400,并提示“设计师已迁移为业务类型:设计”。
|
||||
|
||||
## 页面展示
|
||||
|
||||
- 用户管理、个人中心、交接弹窗、账号模板和审批页面展示“业务类型”。
|
||||
- 用户管理、个人中心、交接弹窗和审批页面展示“业务类型”。
|
||||
- 权限角色筛选和表单只出现 4 个权限角色。
|
||||
- 页面文案应保持低噪音,不用大段说明解释权限体系。
|
||||
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
ALTER TABLE "User" DROP CONSTRAINT IF EXISTS "User_accountTemplateId_fkey";
|
||||
|
||||
DROP INDEX IF EXISTS "User_accountTemplateId_idx";
|
||||
|
||||
ALTER TABLE "User"
|
||||
DROP COLUMN IF EXISTS "accountTemplateId",
|
||||
DROP COLUMN IF EXISTS "accountTemplateOverrides";
|
||||
|
||||
DROP TABLE IF EXISTS "AccountTemplate";
|
||||
@ -1041,11 +1041,6 @@ model User {
|
||||
// 状态
|
||||
status UserStatus @default(PENDING)
|
||||
|
||||
// 岗位模板
|
||||
accountTemplateId String?
|
||||
accountTemplate AccountTemplate? @relation(fields: [accountTemplateId], references: [id], onDelete: SetNull)
|
||||
accountTemplateOverrides Json?
|
||||
|
||||
// 关联
|
||||
createdProducts Product[] @relation("ProductCreator")
|
||||
ownedSKUs ProductSKU[] @relation("SKUOwner")
|
||||
@ -1071,7 +1066,6 @@ model User {
|
||||
addedProductCollaborators ProductCollaborator[] @relation("ProductCollaboratorAddedBy")
|
||||
feedbacks Feedback[] @relation("FeedbackSubmitter")
|
||||
assignedFeedbacks Feedback[] @relation("FeedbackAssignee")
|
||||
createdAccountTemplates AccountTemplate[] @relation("AccountTemplateCreator")
|
||||
createdApprovalWorkflows ApprovalWorkflow[] @relation("ApprovalWorkflowCreator")
|
||||
approvalWorkflowChanges ApprovalWorkflowFieldChange[]
|
||||
|
||||
@ -1089,7 +1083,6 @@ model User {
|
||||
@@index([status])
|
||||
@@index([loginSource])
|
||||
@@index([employeeDirectoryId])
|
||||
@@index([accountTemplateId])
|
||||
}
|
||||
|
||||
model EmployeeLookupCache {
|
||||
@ -1179,30 +1172,6 @@ model Team {
|
||||
@@index([departmentId])
|
||||
}
|
||||
|
||||
model AccountTemplate {
|
||||
id String @id @default(cuid())
|
||||
|
||||
name String @unique
|
||||
code String @unique
|
||||
description String?
|
||||
isActive Boolean @default(true)
|
||||
role Role
|
||||
businessRoles BusinessAccountType[] @default([])
|
||||
defaultScope Json?
|
||||
defaultFieldPermissions Json?
|
||||
|
||||
createdBy String
|
||||
creator User @relation("AccountTemplateCreator", fields: [createdBy], references: [id])
|
||||
|
||||
users User[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([isActive])
|
||||
@@index([createdBy])
|
||||
}
|
||||
|
||||
model ApprovalWorkflow {
|
||||
id String @id @default(cuid())
|
||||
|
||||
|
||||
@ -269,8 +269,8 @@ export default function ThemeManagementClient() {
|
||||
inspector={
|
||||
<div className="flex h-full flex-col bg-[#111719]">
|
||||
<ObsidianInspectorSection title="权限">
|
||||
<ObsidianDataRow label="当前角色" value={user ? roleLabels[user.role] ?? user.role : '-'} tone="warning" />
|
||||
<ObsidianDataRow label="所需角色" value="超级管理员" />
|
||||
<ObsidianDataRow label="当前账号权限" value={user ? roleLabels[user.role] ?? user.role : '-'} tone="warning" />
|
||||
<ObsidianDataRow label="所需账号权限" value="超级管理员" />
|
||||
<ObsidianDataRow label="保存接口" value="未开放" tone="warning" />
|
||||
</ObsidianInspectorSection>
|
||||
</div>
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response'
|
||||
import { AppError } from '@/server/core/errors'
|
||||
import { accountTemplateService } from '@/server/services/account-template.service'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
return handleRequest(
|
||||
request,
|
||||
async (ctx) => {
|
||||
const { id } = await params
|
||||
try {
|
||||
const template = await accountTemplateService.getById(id)
|
||||
return jsonSuccess({ template }, 200, ctx.requestId)
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return jsonError(error.code, error.message, error.statusCode, ctx.requestId)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
{ allowedRoles: ['SUPER_ADMIN', 'ADMIN', 'SUPERVISOR_DEPARTMENT', 'SUPERVISOR_TEAM'] }
|
||||
)
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
return handleRequest(
|
||||
request,
|
||||
async (ctx) => {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
try {
|
||||
const template = await accountTemplateService.update(id, body)
|
||||
return jsonSuccess({ message: '岗位模板更新成功', template }, 200, ctx.requestId)
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return jsonError(error.code, error.message, error.statusCode, ctx.requestId)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
{ allowedRoles: ['SUPER_ADMIN', 'ADMIN'] }
|
||||
)
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response'
|
||||
import { AppError } from '@/server/core/errors'
|
||||
import { accountTemplateService } from '@/server/services/account-template.service'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return handleRequest(
|
||||
request,
|
||||
async (ctx) => {
|
||||
const templates = await accountTemplateService.list(ctx.user)
|
||||
return jsonSuccess({ templates }, 200, ctx.requestId)
|
||||
},
|
||||
{ allowedRoles: ['SUPER_ADMIN', 'ADMIN', 'SUPERVISOR_DEPARTMENT', 'SUPERVISOR_TEAM'] }
|
||||
)
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return handleRequest(
|
||||
request,
|
||||
async (ctx) => {
|
||||
const body = await request.json()
|
||||
try {
|
||||
const template = await accountTemplateService.create(body, ctx.user)
|
||||
return jsonSuccess({ message: '岗位模板创建成功', template }, 201, ctx.requestId)
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return jsonError(error.code, error.message, error.statusCode, ctx.requestId)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
{ allowedRoles: ['SUPER_ADMIN', 'ADMIN'] }
|
||||
)
|
||||
}
|
||||
@ -41,7 +41,7 @@ export async function PUT(
|
||||
return handleRequest(request, async (ctx) => {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const { email, name, password, departmentId, phone, status, accountTemplateId } = body as {
|
||||
const { email, name, password, departmentId, phone, status } = body as {
|
||||
email?: string
|
||||
name?: string
|
||||
password?: string
|
||||
@ -49,7 +49,6 @@ export async function PUT(
|
||||
departmentId?: string | null
|
||||
phone?: string | null
|
||||
status?: UserStatus
|
||||
accountTemplateId?: string | null
|
||||
}
|
||||
const role = parseUserRoleInput(body.role)
|
||||
const businessRoles = 'businessRoles' in body
|
||||
@ -59,7 +58,7 @@ export async function PUT(
|
||||
try {
|
||||
const updatedUser = await userManagementService.updateUser(
|
||||
id,
|
||||
{ email, name, password, role, businessRoles, departmentId, teamId: null, phone, status, accountTemplateId },
|
||||
{ email, name, password, role, businessRoles, departmentId, teamId: null, phone, status },
|
||||
ctx.user
|
||||
)
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@ export async function GET(request: NextRequest) {
|
||||
export async function POST(request: NextRequest) {
|
||||
return handleRequest(request, async (ctx) => {
|
||||
const body = await request.json()
|
||||
const { email, name, password, departmentId, phone, accountTemplateId } = body
|
||||
const { email, name, password, departmentId, phone } = body
|
||||
const role = parseUserRoleInput(body.role)
|
||||
const jobNumber = typeof body.jobNumber === 'string' ? body.jobNumber : undefined
|
||||
const businessRoles = 'businessRoles' in body
|
||||
@ -73,7 +73,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
const createInput = { email, name, password, role, departmentId, phone, accountTemplateId }
|
||||
const createInput = { email, name, password, role, departmentId, phone }
|
||||
const normalizedCreateInput = {
|
||||
...createInput,
|
||||
...(businessRoles ? { businessRoles } : {}),
|
||||
|
||||
@ -16,7 +16,6 @@ import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { LargeTitle, Body, Footnote, Caption2 } from '@/components/ui/Typography';
|
||||
import { useToast, ToastContainer } from '@/components/ui/Toast';
|
||||
import { getApiErrorMessage } from '@/lib/utils';
|
||||
import type { ApiResponse } from '@/lib/api';
|
||||
@ -25,6 +24,30 @@ import { APP_BRAND_LOGO_SRC, APP_BRAND_NAME } from '@/lib/branding';
|
||||
// 登录请求超时时间(毫秒)
|
||||
const LOGIN_TIMEOUT_MS = 15000;
|
||||
|
||||
const authDisplayTitleClass =
|
||||
'text-[32px] font-semibold leading-[1.18] tracking-normal text-[var(--obs-shell-title)] xl:text-[40px]';
|
||||
|
||||
const authDisplaySubtitleClass =
|
||||
'text-[20px] font-medium leading-8 tracking-normal text-[var(--obs-shell-muted)] xl:text-[24px]';
|
||||
|
||||
const authPageTitleClass =
|
||||
'text-[28px] font-semibold leading-9 tracking-normal text-[var(--obs-shell-title)]';
|
||||
|
||||
const authPageSubtitleClass =
|
||||
'mt-2 text-[14px] font-medium leading-6 tracking-normal text-[var(--obs-shell-muted)]';
|
||||
|
||||
const authFieldClass =
|
||||
'space-y-1.5 [&>label]:mb-0 [&>label]:text-[13px] [&>label]:font-semibold [&>label]:leading-5 [&>label]:tracking-normal [&>label]:text-[var(--obs-shell-muted)]';
|
||||
|
||||
const authInputClass =
|
||||
'text-[14px] font-semibold leading-5 tracking-normal placeholder:text-[var(--obs-shell-subtle)]';
|
||||
|
||||
const authLinkClass =
|
||||
'text-[13px] font-semibold leading-5 tracking-normal text-[var(--obs-shell-title)] hover:underline';
|
||||
|
||||
const authMutedTextClass =
|
||||
'text-[13px] font-medium leading-5 tracking-normal text-[var(--obs-shell-muted)]';
|
||||
|
||||
interface LoginSuccessData {
|
||||
message: string;
|
||||
redirectPath?: string;
|
||||
@ -188,18 +211,18 @@ export default function LoginPage() {
|
||||
/>
|
||||
|
||||
{/* 主标题 - 响应式字号 */}
|
||||
<h1 className="mt-7 xl:mt-[28px] font-bold text-[32px] xl:text-[40px] leading-tight" style={{ color: 'var(--color-label-primary)', fontFamily: 'Manrope, sans-serif' }}>
|
||||
<h1 className={`mt-7 xl:mt-[28px] ${authDisplayTitleClass}`}>
|
||||
{APP_BRAND_NAME}
|
||||
</h1>
|
||||
|
||||
{/* 副标题 - 响应式字号 */}
|
||||
<p className="mt-4 xl:mt-[18px] font-normal text-[20px] xl:text-[24px]" style={{ color: 'var(--color-label-secondary)' }}>
|
||||
<p className={`mt-4 xl:mt-[18px] ${authDisplaySubtitleClass}`}>
|
||||
专业的家具装配与展示解决方案
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 底部辅助文案 */}
|
||||
<p className="absolute bottom-7 left-0 right-0 text-center text-sm" style={{ color: 'var(--color-label-tertiary)' }}>
|
||||
<p className="absolute bottom-7 left-0 right-0 text-center text-[12px] font-medium leading-5 tracking-normal text-[var(--obs-shell-subtle)]">
|
||||
Designed for Assembly
|
||||
</p>
|
||||
</div>
|
||||
@ -224,19 +247,19 @@ export default function LoginPage() {
|
||||
|
||||
{/* 移动端:视觉调整后的标题 (加大字号) */}
|
||||
<div className="lg:hidden">
|
||||
<h2 className="text-(--color-label-primary) text-[26px] tracking-apple-tighter font-semibold" style={{ fontFamily: 'Manrope, sans-serif' }}>
|
||||
<h2 className="text-[26px] font-semibold leading-8 tracking-normal text-[var(--obs-shell-title)]">
|
||||
{APP_BRAND_NAME}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* 桌面端:显示功能标题 */}
|
||||
<div className="hidden lg:block">
|
||||
<LargeTitle className="text-(--color-label-primary)">
|
||||
<h1 className={authPageTitleClass}>
|
||||
登录
|
||||
</LargeTitle>
|
||||
<Body className="text-(--color-label-secondary) mt-2">
|
||||
</h1>
|
||||
<p className={authPageSubtitleClass}>
|
||||
进入您的工作台以开始管理产品
|
||||
</Body>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -253,7 +276,8 @@ export default function LoginPage() {
|
||||
required
|
||||
variant="filled"
|
||||
size="large"
|
||||
className="bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all"
|
||||
containerClassName={authFieldClass}
|
||||
className={`bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all ${authInputClass}`}
|
||||
/>
|
||||
|
||||
<div className="space-y-1">
|
||||
@ -267,12 +291,13 @@ export default function LoginPage() {
|
||||
required
|
||||
variant="filled"
|
||||
size="large"
|
||||
className="bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all"
|
||||
containerClassName={authFieldClass}
|
||||
className={`bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all ${authInputClass}`}
|
||||
/>
|
||||
<div className="flex justify-end mt-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/register" className="text-(--color-blue) hover:underline">
|
||||
<Footnote as="span">注册账号</Footnote>
|
||||
<Link href="/register" className={authLinkClass}>
|
||||
注册账号
|
||||
</Link>
|
||||
<Button
|
||||
type="button"
|
||||
@ -281,9 +306,9 @@ export default function LoginPage() {
|
||||
onClick={() => toast.info('请联系管理员重置密码')}
|
||||
className="!p-0 !min-h-0"
|
||||
>
|
||||
<Footnote as="span" className="text-(--color-label-secondary)">
|
||||
<span className={authMutedTextClass}>
|
||||
忘记密码?
|
||||
</Footnote>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -294,7 +319,7 @@ export default function LoginPage() {
|
||||
type="submit"
|
||||
variant="filled"
|
||||
size="large"
|
||||
className="w-full rounded-full h-12 text-base font-semibold shadow-apple-lg shadow-apple-blue/20 hover:shadow-apple-blue/30 active:scale-[0.98] transition-all mt-2"
|
||||
className="w-full rounded-full h-12 text-[14px] font-semibold leading-5 tracking-normal shadow-apple-lg shadow-apple-blue/20 hover:shadow-apple-blue/30 active:scale-[0.98] transition-all mt-2"
|
||||
loading={isLoading}
|
||||
>
|
||||
登录
|
||||
@ -303,7 +328,7 @@ export default function LoginPage() {
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="flex items-center gap-3 text-(--color-label-tertiary)">
|
||||
<div className="h-px flex-1 bg-(--color-separator)" />
|
||||
<Caption2>其他登录方式</Caption2>
|
||||
<span className="text-[12px] font-medium leading-5 tracking-normal text-[var(--obs-shell-subtle)]">其他登录方式</span>
|
||||
<div className="h-px flex-1 bg-(--color-separator)" />
|
||||
</div>
|
||||
|
||||
@ -311,7 +336,7 @@ export default function LoginPage() {
|
||||
type="button"
|
||||
variant="gray"
|
||||
size="large"
|
||||
className="w-full rounded-full h-12 text-base font-semibold gap-3"
|
||||
className="w-full rounded-full h-12 text-[14px] font-semibold leading-5 tracking-normal gap-3"
|
||||
loading={isDingtalkLoading}
|
||||
onClick={handleDingtalkLogin}
|
||||
>
|
||||
@ -326,7 +351,7 @@ export default function LoginPage() {
|
||||
|
||||
{/* 移动端底部版权 - 绝对定位到底部 */}
|
||||
<div className="lg:hidden absolute bottom-8 left-0 right-0 text-center text-(--color-label-tertiary)">
|
||||
<Caption2>{APP_BRAND_NAME}</Caption2>
|
||||
<span className="text-[12px] font-medium leading-5 tracking-normal text-[var(--obs-shell-subtle)]">{APP_BRAND_NAME}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -89,7 +89,7 @@ function ProfileContent() {
|
||||
<div className="flex h-full flex-col bg-white">
|
||||
<ObsidianInspectorSection title="身份摘要">
|
||||
<ObsidianDataRow label="账号状态" value={statusLabel} tone={user?.status === 'ACTIVE' ? 'success' : 'warning'} />
|
||||
<ObsidianDataRow label="权限角色" value={roleLabel} />
|
||||
<ObsidianDataRow label="账号权限" value={roleLabel} />
|
||||
<ObsidianDataRow label="业务类型" value={businessRoleText} />
|
||||
<ObsidianDataRow label="登录来源" value={user?.loginSource || '系统账号'} />
|
||||
</ObsidianInspectorSection>
|
||||
@ -122,7 +122,7 @@ function ProfileContent() {
|
||||
<ObsidianPanel title="身份令牌" className="xl:col-span-2">
|
||||
<div className="grid gap-4 p-5 md:grid-cols-3">
|
||||
<ProfileMetric icon={Fingerprint} label="账号 ID" value={user?.id.slice(0, 8) ?? '-'} />
|
||||
<ProfileMetric icon={ShieldCheck} label="权限角色" value={roleLabel} />
|
||||
<ProfileMetric icon={ShieldCheck} label="账号权限" value={roleLabel} />
|
||||
<ProfileMetric icon={BriefcaseBusiness} label="业务类型" value={businessRoleText} />
|
||||
</div>
|
||||
</ObsidianPanel>
|
||||
|
||||
@ -5,13 +5,39 @@ import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { LargeTitle, Body, Footnote, Caption2 } from '@/components/ui/Typography';
|
||||
import { useToast, ToastContainer } from '@/components/ui/Toast';
|
||||
import { getApiErrorMessage } from '@/lib/utils';
|
||||
import type { ApiResponse } from '@/lib/api';
|
||||
|
||||
const REGISTER_TIMEOUT_MS = 15000;
|
||||
|
||||
const authDisplayTitleClass =
|
||||
'text-[36px] font-semibold leading-[1.15] tracking-normal text-[var(--obs-shell-title)] xl:text-[48px]';
|
||||
|
||||
const authDisplaySubtitleClass =
|
||||
'text-[20px] font-medium leading-8 tracking-normal text-[var(--obs-shell-muted)] xl:text-[24px]';
|
||||
|
||||
const authPageTitleClass =
|
||||
'text-[28px] font-semibold leading-9 tracking-normal text-[var(--obs-shell-title)]';
|
||||
|
||||
const authPageSubtitleClass =
|
||||
'mt-2 text-[14px] font-medium leading-6 tracking-normal text-[var(--obs-shell-muted)]';
|
||||
|
||||
const authFieldClass =
|
||||
'space-y-1.5 [&>label]:mb-0 [&>label]:text-[13px] [&>label]:font-semibold [&>label]:leading-5 [&>label]:tracking-normal [&>label]:text-[var(--obs-shell-muted)]';
|
||||
|
||||
const authInputClass =
|
||||
'text-[14px] font-semibold leading-5 tracking-normal placeholder:text-[var(--obs-shell-subtle)]';
|
||||
|
||||
const authHelperClass =
|
||||
'text-[12px] font-medium leading-5 tracking-normal text-[var(--obs-shell-subtle)]';
|
||||
|
||||
const authLinkClass =
|
||||
'text-[13px] font-semibold leading-5 tracking-normal text-[var(--obs-shell-title)] hover:underline';
|
||||
|
||||
const authMutedTextClass =
|
||||
'text-[13px] font-medium leading-5 tracking-normal text-[var(--obs-shell-muted)]';
|
||||
|
||||
interface RegisterSuccessData {
|
||||
message: string;
|
||||
user: {
|
||||
@ -101,16 +127,16 @@ export default function RegisterPage() {
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||
</svg>
|
||||
|
||||
<h1 className="mt-7 xl:mt-[28px] font-bold text-[36px] xl:text-[48px] leading-tight text-[#111827]">
|
||||
<h1 className={`mt-7 xl:mt-[28px] ${authDisplayTitleClass}`}>
|
||||
3D Furniture System
|
||||
</h1>
|
||||
|
||||
<p className="mt-4 xl:mt-[18px] font-normal text-[20px] xl:text-[24px] text-[#4b5563]">
|
||||
<p className={`mt-4 xl:mt-[18px] ${authDisplaySubtitleClass}`}>
|
||||
注册后先等待后台激活,再进入系统
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="absolute bottom-7 left-0 right-0 text-center text-sm text-[#374151]">
|
||||
<p className="absolute bottom-7 left-0 right-0 text-center text-[12px] font-medium leading-5 tracking-normal text-[var(--obs-shell-subtle)]">
|
||||
Register For Workspace Access
|
||||
</p>
|
||||
</div>
|
||||
@ -127,10 +153,10 @@ export default function RegisterPage() {
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block">
|
||||
<LargeTitle className="text-(--color-label-primary)">注册</LargeTitle>
|
||||
<Body className="text-(--color-label-secondary) mt-2">
|
||||
<h1 className={authPageTitleClass}>注册</h1>
|
||||
<p className={authPageSubtitleClass}>
|
||||
所有新账号都会先进入待激活状态,管理员审核通过后才能登录
|
||||
</Body>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -147,7 +173,9 @@ export default function RegisterPage() {
|
||||
helperText="系统会用工号匹配员工库,匹配成功后仍需管理员激活"
|
||||
variant="filled"
|
||||
size="large"
|
||||
className="bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all"
|
||||
containerClassName={authFieldClass}
|
||||
messageClassName={authHelperClass}
|
||||
className={`bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all ${authInputClass}`}
|
||||
/>
|
||||
|
||||
<Input
|
||||
@ -160,7 +188,8 @@ export default function RegisterPage() {
|
||||
required
|
||||
variant="filled"
|
||||
size="large"
|
||||
className="bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all"
|
||||
containerClassName={authFieldClass}
|
||||
className={`bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all ${authInputClass}`}
|
||||
/>
|
||||
|
||||
<Input
|
||||
@ -173,7 +202,8 @@ export default function RegisterPage() {
|
||||
required
|
||||
variant="filled"
|
||||
size="large"
|
||||
className="bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all"
|
||||
containerClassName={authFieldClass}
|
||||
className={`bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all ${authInputClass}`}
|
||||
/>
|
||||
|
||||
<Input
|
||||
@ -187,7 +217,9 @@ export default function RegisterPage() {
|
||||
helperText="密码最少 6 位即可"
|
||||
variant="filled"
|
||||
size="large"
|
||||
className="bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all"
|
||||
containerClassName={authFieldClass}
|
||||
messageClassName={authHelperClass}
|
||||
className={`bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all ${authInputClass}`}
|
||||
/>
|
||||
|
||||
<Input
|
||||
@ -200,7 +232,8 @@ export default function RegisterPage() {
|
||||
required
|
||||
variant="filled"
|
||||
size="large"
|
||||
className="bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all"
|
||||
containerClassName={authFieldClass}
|
||||
className={`bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all ${authInputClass}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -208,32 +241,32 @@ export default function RegisterPage() {
|
||||
type="submit"
|
||||
variant="filled"
|
||||
size="large"
|
||||
className="w-full rounded-full h-12 text-base font-semibold shadow-apple-lg shadow-apple-blue/20 hover:shadow-apple-blue/30 active:scale-[0.98] transition-all mt-2"
|
||||
className="w-full rounded-full h-12 text-[14px] font-semibold leading-5 tracking-normal shadow-apple-lg shadow-apple-blue/20 hover:shadow-apple-blue/30 active:scale-[0.98] transition-all mt-2"
|
||||
loading={isLoading}
|
||||
>
|
||||
注册账号
|
||||
</Button>
|
||||
|
||||
<div className="rounded-apple-lg border border-apple-separator bg-(--color-background-secondary) px-4 py-3">
|
||||
<Footnote className="text-(--color-label-secondary)">
|
||||
注册成功后不会自动放行登录。管理员在后台激活并分配角色后,你才能正式进入系统。
|
||||
</Footnote>
|
||||
<p className={authMutedTextClass}>
|
||||
注册成功后不会自动放行登录。管理员在后台激活并分配账号权限后,你才能正式进入系统。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Footnote className="text-(--color-label-secondary)">
|
||||
<p className={authMutedTextClass}>
|
||||
已有账号?
|
||||
{' '}
|
||||
<Link href="/login" className="text-(--color-blue) hover:underline">
|
||||
<Link href="/login" className={authLinkClass}>
|
||||
返回登录
|
||||
</Link>
|
||||
</Footnote>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="lg:hidden absolute bottom-8 left-0 right-0 text-center text-(--color-label-tertiary)">
|
||||
<Caption2>3D Furniture System v1.0</Caption2>
|
||||
<span className="text-[12px] font-medium leading-5 tracking-normal text-[var(--obs-shell-subtle)]">3D Furniture System v1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -40,7 +40,7 @@ import { BUSINESS_ACCOUNT_TYPE_LABELS, BUSINESS_ACCOUNT_TYPE_OPTIONS } from '@/l
|
||||
import { useDebounce } from '@/hooks/usePerformance'
|
||||
|
||||
const ROLE_OPTIONS = [
|
||||
{ value: '', label: '所有权限角色', icon: Users },
|
||||
{ value: '', label: '所有账号权限', icon: Users },
|
||||
{ value: 'SUPER_ADMIN', label: '超级管理员', icon: KeyRound },
|
||||
{ value: 'ADMIN', label: '管理员', icon: ShieldCheck },
|
||||
{ value: 'SUPERVISOR', label: '主管', icon: Briefcase },
|
||||
@ -309,7 +309,7 @@ function UsersPageContent() {
|
||||
}
|
||||
rail={
|
||||
<div className="flex h-full flex-col gap-6">
|
||||
<ObsidianRailGroup title="权限角色">
|
||||
<ObsidianRailGroup title="账号权限">
|
||||
{ROLE_OPTIONS.map((option) => (
|
||||
<ObsidianRailButton
|
||||
key={option.value}
|
||||
@ -387,23 +387,6 @@ function UsersPageContent() {
|
||||
<ObsidianDataRow label="员工姓名" value={selectedUser.employeeDirectory?.name || '未记录'} />
|
||||
<ObsidianDataRow label="钉钉" value={selectedUser.loginSource === 'DINGTALK' ? '已绑定' : '未绑定'} />
|
||||
</ObsidianInspectorSection>
|
||||
<ObsidianInspectorSection title="岗位模板">
|
||||
<ObsidianDataRow
|
||||
label="模板来源"
|
||||
value={selectedUser.accountTemplate?.name || '未套用模板'}
|
||||
tone={selectedUser.accountTemplate ? 'success' : 'warning'}
|
||||
/>
|
||||
<ObsidianDataRow
|
||||
label="模板编码"
|
||||
value={selectedUser.accountTemplate?.code || '未设置'}
|
||||
/>
|
||||
<ObsidianDataRow
|
||||
label="人工覆盖项"
|
||||
value={(selectedUser.accountTemplateOverrides?.overrideFields || []).length > 0
|
||||
? selectedUser.accountTemplateOverrides!.overrideFields!.join(' / ')
|
||||
: '无'}
|
||||
/>
|
||||
</ObsidianInspectorSection>
|
||||
<ObsidianInspectorSection title="安全审计">
|
||||
<ObsidianDataRow label="账号状态" value={statusLabels[selectedUser.status] ?? selectedUser.status} tone={selectedUser.status === 'ACTIVE' ? 'success' : 'warning'} />
|
||||
<ObsidianDataRow label="登录来源" value={selectedUser.loginSource || '系统账号'} />
|
||||
|
||||
@ -33,6 +33,8 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
|
||||
helperText?: string
|
||||
/** 错误文本 */
|
||||
errorText?: string
|
||||
/** 帮助/错误文本类名 */
|
||||
messageClassName?: string
|
||||
/** 是否必填 */
|
||||
required?: boolean
|
||||
/** 左侧图标 */
|
||||
@ -54,6 +56,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
label,
|
||||
helperText,
|
||||
errorText,
|
||||
messageClassName,
|
||||
required = false,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
@ -181,16 +184,30 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
|
||||
{/* 帮助文本或错误文本 */}
|
||||
{(errorText || helperText) && (
|
||||
<Caption1
|
||||
className={cn(
|
||||
'block',
|
||||
errorText
|
||||
? 'text-apple-red'
|
||||
: 'text-apple-label-secondary'
|
||||
)}
|
||||
>
|
||||
{errorText || helperText}
|
||||
</Caption1>
|
||||
messageClassName ? (
|
||||
<p
|
||||
className={cn(
|
||||
'block',
|
||||
errorText
|
||||
? 'text-apple-red'
|
||||
: 'text-apple-label-secondary',
|
||||
messageClassName,
|
||||
)}
|
||||
>
|
||||
{errorText || helperText}
|
||||
</p>
|
||||
) : (
|
||||
<Caption1
|
||||
className={cn(
|
||||
'block',
|
||||
errorText
|
||||
? 'text-apple-red'
|
||||
: 'text-apple-label-secondary'
|
||||
)}
|
||||
>
|
||||
{errorText || helperText}
|
||||
</Caption1>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -49,6 +49,8 @@ export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectE
|
||||
searchPlaceholder?: string
|
||||
/** 搜索无结果文案 */
|
||||
emptyText?: string
|
||||
/** 选中状态色调 */
|
||||
tone?: 'apple' | 'obsidian'
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,6 +71,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
searchable = false,
|
||||
searchPlaceholder = '搜索',
|
||||
emptyText = '没有匹配项',
|
||||
tone = 'apple',
|
||||
disabled,
|
||||
value,
|
||||
onChange,
|
||||
@ -94,6 +97,14 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
)
|
||||
: options
|
||||
const enabledOptions = filteredOptions.filter((o) => !o.disabled)
|
||||
const selectedOptionClass =
|
||||
tone === 'obsidian'
|
||||
? 'text-[var(--obs-shell-title)] bg-[var(--obs-shell-control-bg)]'
|
||||
: 'text-apple-blue bg-apple-blue/5'
|
||||
const selectedIconClass =
|
||||
tone === 'obsidian'
|
||||
? 'text-[var(--obs-shell-title)]'
|
||||
: 'text-apple-blue'
|
||||
|
||||
// 点击外部关闭
|
||||
useEffect(() => {
|
||||
@ -331,12 +342,12 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-2 cursor-pointer text-sm transition-colors',
|
||||
!hasValue
|
||||
? 'text-apple-blue bg-apple-blue/5'
|
||||
? selectedOptionClass
|
||||
: 'text-apple-label-tertiary hover:bg-apple-bg-secondary',
|
||||
)}
|
||||
>
|
||||
<span className="flex-1 truncate">{placeholder}</span>
|
||||
{!hasValue && <Check className="w-3.5 h-3.5 shrink-0 text-apple-blue" />}
|
||||
{!hasValue && <Check className={cn('w-3.5 h-3.5 shrink-0', selectedIconClass)} />}
|
||||
</li>
|
||||
)}
|
||||
{filteredOptions.length === 0 ? (
|
||||
@ -362,7 +373,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
? 'opacity-40 cursor-not-allowed'
|
||||
: 'cursor-pointer',
|
||||
!option.disabled && isSelected
|
||||
? 'text-apple-blue bg-apple-blue/5'
|
||||
? selectedOptionClass
|
||||
: !option.disabled && isHighlighted
|
||||
? 'bg-apple-bg-secondary text-apple-label-primary'
|
||||
: 'text-apple-label-primary hover:bg-apple-bg-secondary',
|
||||
@ -370,7 +381,7 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
>
|
||||
<span className="flex-1 truncate">{option.label}</span>
|
||||
{isSelected && (
|
||||
<Check className="w-3.5 h-3.5 shrink-0 text-apple-blue" />
|
||||
<Check className={cn('w-3.5 h-3.5 shrink-0', selectedIconClass)} />
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
|
||||
@ -161,7 +161,7 @@ export function ActivateUserModal({
|
||||
setErrors({});
|
||||
|
||||
if (!formData.role) {
|
||||
setErrors({ role: '请选择激活后的角色' });
|
||||
setErrors({ role: '请选择账号权限' });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -224,14 +224,14 @@ export function ActivateUserModal({
|
||||
</h4>
|
||||
</div>
|
||||
<p className="mt-3 text-[11px] leading-relaxed text-apple-label-secondary">
|
||||
账号激活后即可正式登录。你只能把该账号设置为低于你自己的角色等级。
|
||||
账号激活后即可正式登录。你只能把该账号设置为低于你自己的账号权限等级。
|
||||
</p>
|
||||
<div className="mt-3 text-sm font-apple-bold text-apple-label-primary">{user.name}</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<Select
|
||||
label="激活后角色"
|
||||
label="账号权限"
|
||||
value={formData.role}
|
||||
onChange={(event) => setFormData((current) => ({ ...current, role: event.target.value as Role }))}
|
||||
options={roleOptions}
|
||||
|
||||
@ -10,7 +10,7 @@ import Modal from '@/components/ui/Modal';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { AlertCircle, Check } from 'lucide-react';
|
||||
import { DepartmentTreeSelect, type DepartmentTreeOption } from '@/components/users/DepartmentTreeSelect';
|
||||
import { BUSINESS_ACCOUNT_TYPE_OPTIONS } from '@/lib/business-account-types';
|
||||
import { inferBusinessRolesFromDepartmentPath } from '@/lib/business-role-inference';
|
||||
@ -23,14 +23,6 @@ interface CreateUserModalProps {
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
interface AccountTemplateItem {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
role: Role;
|
||||
businessRoles: BusinessAccountType[];
|
||||
}
|
||||
|
||||
const ROLE_LABELS: Record<Role, string> = {
|
||||
SUPER_ADMIN: '超级管理员',
|
||||
ADMIN: '管理员',
|
||||
@ -38,6 +30,20 @@ const ROLE_LABELS: Record<Role, string> = {
|
||||
USER: '普通用户(只看 / 下载)',
|
||||
};
|
||||
|
||||
const sectionTitleClass =
|
||||
'border-b border-[var(--obs-shell-border)] pb-2 text-[12px] font-semibold leading-5 tracking-normal text-[var(--obs-shell-muted)]';
|
||||
|
||||
const formColumnClass = 'space-y-4';
|
||||
|
||||
const formFieldClass =
|
||||
'space-y-1.5 [&>label]:mb-0 [&>label]:text-[12px] [&>label]:font-semibold [&>label]:leading-5 [&>label]:tracking-normal [&>label]:text-[var(--obs-shell-muted)]';
|
||||
|
||||
const formControlClass =
|
||||
'h-10 rounded-[6px] border border-[var(--obs-shell-border)] bg-white px-3 py-0 text-[13px] font-semibold leading-5 tracking-normal text-[var(--obs-shell-title)] placeholder:text-[var(--obs-shell-subtle)] focus:border focus:border-[var(--obs-shell-title)] focus:px-3 focus:py-0';
|
||||
|
||||
const businessOptionClass =
|
||||
'flex h-10 cursor-pointer items-center gap-2 rounded-[6px] border border-[var(--obs-shell-border)] bg-white px-3 text-[13px] font-semibold leading-5 tracking-normal text-[var(--obs-shell-title)] transition-colors hover:bg-[var(--obs-shell-hover-bg)]';
|
||||
|
||||
function findDepartmentById(
|
||||
departments: DepartmentTreeOption[],
|
||||
departmentId: string,
|
||||
@ -61,13 +67,11 @@ export function CreateUserModal({ isOpen, allowedRoles, onClose, onSuccess }: Cr
|
||||
businessRoles: [] as BusinessAccountType[],
|
||||
phone: '',
|
||||
departmentId: '',
|
||||
accountTemplateId: '',
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [departments, setDepartments] = useState<DepartmentTreeOption[]>([]);
|
||||
const [templates, setTemplates] = useState<AccountTemplateItem[]>([]);
|
||||
const [businessRolesTouched, setBusinessRolesTouched] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@ -97,27 +101,6 @@ export function CreateUserModal({ isOpen, allowedRoles, onClose, onSuccess }: Cr
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account-templates', { credentials: 'include' });
|
||||
if (!response.ok) {
|
||||
setTemplates([]);
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
setTemplates(data.data?.templates || []);
|
||||
} catch (error) {
|
||||
console.error('加载岗位模板失败:', error);
|
||||
setTemplates([]);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
void loadTemplates();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (businessRolesTouched || !formData.departmentId) return;
|
||||
const department = findDepartmentById(departments, formData.departmentId);
|
||||
@ -139,17 +122,6 @@ export function CreateUserModal({ isOpen, allowedRoles, onClose, onSuccess }: Cr
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTemplateChange = (templateId: string) => {
|
||||
const template = templates.find((item) => item.id === templateId);
|
||||
setFormData((current) => ({
|
||||
...current,
|
||||
accountTemplateId: templateId,
|
||||
role: template?.role || current.role,
|
||||
businessRoles: template?.businessRoles || current.businessRoles,
|
||||
}));
|
||||
if (template) setBusinessRolesTouched(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
@ -184,7 +156,6 @@ export function CreateUserModal({ isOpen, allowedRoles, onClose, onSuccess }: Cr
|
||||
phone: formData.phone || null,
|
||||
departmentId: formData.departmentId || null,
|
||||
teamId: null,
|
||||
accountTemplateId: formData.accountTemplateId || null,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -206,46 +177,33 @@ export function CreateUserModal({ isOpen, allowedRoles, onClose, onSuccess }: Cr
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title="新建账号"
|
||||
size="medium"
|
||||
size="large"
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="space-y-8 p-2">
|
||||
<form onSubmit={handleSubmit} className="space-y-6 p-1">
|
||||
{errors.submit && (
|
||||
<div className="flex items-center gap-3 rounded-md border border-red-100 bg-red-50 p-4 animate-in fade-in zoom-in-95">
|
||||
<AlertCircle className="w-4 h-4 text-red-500 shrink-0" />
|
||||
<p className="text-apple-xs font-apple-bold text-red-700 uppercase tracking-wide">{errors.submit}</p>
|
||||
<p className="text-[13px] font-semibold leading-5 tracking-normal text-red-700">{errors.submit}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="space-y-6">
|
||||
<h4 className="text-[10px] font-apple-black text-apple-label-tertiary uppercase tracking-[0.2em] border-b border-apple-separator/10 pb-2">基础信息</h4>
|
||||
<Input label="工号" value={formData.jobNumber} onChange={(e) => setFormData({ ...formData, jobNumber: e.target.value })} placeholder="员工工号" required errorText={errors.jobNumber} className="bg-white" />
|
||||
<Input label="用户姓名" value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} placeholder="张三" required errorText={errors.name} className="bg-white" />
|
||||
<Input label="电子邮箱" type="email" value={formData.email} onChange={(e) => setFormData({ ...formData, email: e.target.value })} placeholder="user@example.com" required errorText={errors.email} className="bg-white" />
|
||||
<Input label="联系电话" value={formData.phone} onChange={(e) => setFormData({ ...formData, phone: e.target.value })} placeholder="13800138000" className="bg-white" />
|
||||
<div className="grid grid-cols-1 gap-x-7 gap-y-6 md:grid-cols-2">
|
||||
<div className={formColumnClass}>
|
||||
<h4 className={sectionTitleClass}>基础信息</h4>
|
||||
<Input label="工号" value={formData.jobNumber} onChange={(e) => setFormData({ ...formData, jobNumber: e.target.value })} placeholder="员工工号" required errorText={errors.jobNumber} containerClassName={formFieldClass} className={formControlClass} />
|
||||
<Input label="用户姓名" value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} placeholder="张三" required errorText={errors.name} containerClassName={formFieldClass} className={formControlClass} />
|
||||
<Input label="电子邮箱" type="email" value={formData.email} onChange={(e) => setFormData({ ...formData, email: e.target.value })} placeholder="user@example.com" required errorText={errors.email} containerClassName={formFieldClass} className={formControlClass} />
|
||||
<Input label="联系电话" value={formData.phone} onChange={(e) => setFormData({ ...formData, phone: e.target.value })} placeholder="13800138000" containerClassName={formFieldClass} className={formControlClass} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<h4 className="text-[10px] font-apple-black text-apple-label-tertiary uppercase tracking-[0.2em] border-b border-apple-separator/10 pb-2">权限与归属</h4>
|
||||
<Select label="系统角色" value={formData.role} onChange={(e) => setFormData({ ...formData, role: e.target.value as Role })} options={
|
||||
<div className={formColumnClass}>
|
||||
<h4 className={sectionTitleClass}>权限与归属</h4>
|
||||
<Select label="账号权限" value={formData.role} onChange={(e) => setFormData({ ...formData, role: e.target.value as Role })} options={
|
||||
allowedRoles.map((role) => ({
|
||||
value: role,
|
||||
label: ROLE_LABELS[role],
|
||||
}))
|
||||
} required className="bg-white" />
|
||||
<Select
|
||||
label="岗位模板"
|
||||
value={formData.accountTemplateId}
|
||||
onChange={(e) => handleTemplateChange(e.target.value)}
|
||||
options={[
|
||||
{ value: '', label: '不套模板,手动设置' },
|
||||
...templates.map((template) => ({
|
||||
value: template.id,
|
||||
label: `${template.name} (${template.code})`,
|
||||
})),
|
||||
]}
|
||||
className="bg-white"
|
||||
/>
|
||||
} required tone="obsidian" containerClassName={formFieldClass} className={formControlClass} />
|
||||
<DepartmentTreeSelect
|
||||
label="所属部门"
|
||||
value={formData.departmentId}
|
||||
@ -254,19 +212,23 @@ export function CreateUserModal({ isOpen, allowedRoles, onClose, onSuccess }: Cr
|
||||
setBusinessRolesTouched(false);
|
||||
setFormData({ ...formData, departmentId });
|
||||
}}
|
||||
className="bg-white"
|
||||
className={formFieldClass}
|
||||
/>
|
||||
<div>
|
||||
<p className="mb-2 text-[10px] font-apple-black uppercase tracking-[0.18em] text-apple-label-tertiary">业务类型</p>
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-[12px] font-semibold leading-5 tracking-normal text-[var(--obs-shell-muted)]">业务类型</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{BUSINESS_ACCOUNT_TYPE_OPTIONS.map((option) => (
|
||||
<label key={option.value} className="flex items-center gap-2 rounded-md border border-[#dde3e5] bg-white px-3 py-2 text-xs font-bold text-[#405054]">
|
||||
<label key={option.value} className={businessOptionClass}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.businessRoles.includes(option.value)}
|
||||
onChange={() => toggleBusinessRole(option.value)}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
{option.label}
|
||||
<span className="flex h-4 w-4 shrink-0 items-center justify-center rounded-[4px] border border-[var(--obs-shell-border)] bg-white transition-colors peer-checked:border-[var(--obs-shell-title)] peer-checked:bg-[var(--obs-shell-title)]">
|
||||
<Check className="h-3 w-3 text-white opacity-0 transition-opacity peer-checked:opacity-100" />
|
||||
</span>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@ -274,23 +236,19 @@ export function CreateUserModal({ isOpen, allowedRoles, onClose, onSuccess }: Cr
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 rounded-md border border-[#dde3e5] bg-[#fbfcfc] p-6">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-apple-blue shadow-[0_0_8px_rgba(0,122,255,0.4)]" />
|
||||
<h4 className="text-[10px] font-apple-black text-apple-label-secondary uppercase tracking-widest">安全信息</h4>
|
||||
</div>
|
||||
<p className="text-[11px] text-apple-label-secondary">
|
||||
新账号会先进入“待激活”状态,需要后台审核通过后才能正式登录。
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Input label="登录密码" type="password" value={formData.password} onChange={(e) => setFormData({ ...formData, password: e.target.value })} placeholder="至少6位" required errorText={errors.password} className="bg-white" />
|
||||
<Input label="确认密码" type="password" value={formData.confirmPassword} onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })} placeholder="再次输入以核对" required errorText={errors.confirmPassword} className="bg-white" />
|
||||
<div className="space-y-4 border-t border-[var(--obs-shell-border)] pt-6">
|
||||
<h4 className={sectionTitleClass}>
|
||||
安全信息
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 gap-x-7 gap-y-4 md:grid-cols-2">
|
||||
<Input label="登录密码" type="password" value={formData.password} onChange={(e) => setFormData({ ...formData, password: e.target.value })} placeholder="至少6位" required errorText={errors.password} containerClassName={formFieldClass} className={formControlClass} />
|
||||
<Input label="确认密码" type="password" value={formData.confirmPassword} onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })} placeholder="再次输入以核对" required errorText={errors.confirmPassword} containerClassName={formFieldClass} className={formControlClass} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-4 border-t border-apple-separator/5">
|
||||
<Button type="button" variant="plain" onClick={onClose} disabled={loading} className="rounded-md px-6 uppercase text-[10px] font-apple-black tracking-widest">取消</Button>
|
||||
<Button type="submit" variant="filled" loading={loading} className="rounded-md px-10 uppercase text-[10px] font-apple-black tracking-widest">创建账号</Button>
|
||||
<div className="flex items-center justify-end gap-3 border-t border-[var(--obs-shell-border)] pt-4">
|
||||
<Button type="button" variant="plain" onClick={onClose} disabled={loading} className="rounded-md px-6 text-[13px] font-semibold tracking-normal">取消</Button>
|
||||
<Button type="submit" variant="filled" loading={loading} className="rounded-md px-8 text-[13px] font-semibold tracking-normal">创建账号</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@ -34,18 +34,6 @@ export interface UserTreeItem {
|
||||
sourceStatus: string;
|
||||
syncedAt: string;
|
||||
} | null;
|
||||
accountTemplateId?: string | null;
|
||||
accountTemplate?: {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
role: string;
|
||||
businessRoles: BusinessAccountType[];
|
||||
} | null;
|
||||
accountTemplateOverrides?: {
|
||||
overrideFields?: string[];
|
||||
templateCode?: string;
|
||||
} | null;
|
||||
businessRoles?: BusinessAccountType[];
|
||||
avatar?: string;
|
||||
phone?: string;
|
||||
|
||||
@ -209,8 +209,8 @@ export function DepartmentTreeSelect({
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={cn('relative space-y-2', className)}>
|
||||
<label className="block text-xs font-bold text-[var(--obs-shell-muted)]">
|
||||
<div ref={containerRef} className={cn('relative space-y-1.5', className)}>
|
||||
<label className="block text-[12px] font-semibold leading-5 tracking-normal text-[var(--obs-shell-muted)]">
|
||||
{label}
|
||||
{required ? <span className="ml-1 text-red-500">*</span> : null}
|
||||
</label>
|
||||
@ -221,7 +221,7 @@ export function DepartmentTreeSelect({
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
className={cn(
|
||||
'flex min-h-11 w-full items-center gap-3 rounded-[6px] border bg-white px-3 text-left transition-colors',
|
||||
'flex h-10 min-h-10 w-full items-center gap-2 rounded-[6px] border bg-white px-3 text-left transition-colors',
|
||||
'focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--obs-shell-border)]',
|
||||
hasError
|
||||
? 'border-red-300'
|
||||
@ -231,11 +231,11 @@ export function DepartmentTreeSelect({
|
||||
disabled && 'cursor-not-allowed opacity-60',
|
||||
)}
|
||||
>
|
||||
<Building2 className="h-4 w-4 shrink-0 text-[var(--obs-shell-muted)]" />
|
||||
<Building2 className="h-3.5 w-3.5 shrink-0 text-[var(--obs-shell-muted)]" />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span
|
||||
className={cn(
|
||||
'block truncate text-sm font-black',
|
||||
'block truncate text-[13px] font-semibold leading-5 tracking-normal',
|
||||
selectedDepartment ? 'text-[var(--obs-shell-title)]' : 'text-[var(--obs-shell-muted)]',
|
||||
)}
|
||||
>
|
||||
@ -243,7 +243,7 @@ export function DepartmentTreeSelect({
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn('h-4 w-4 shrink-0 text-[var(--obs-shell-muted)] transition-transform', open && 'rotate-180')}
|
||||
className={cn('h-3.5 w-3.5 shrink-0 text-[var(--obs-shell-muted)] transition-transform', open && 'rotate-180')}
|
||||
/>
|
||||
</button>
|
||||
|
||||
|
||||
@ -25,18 +25,6 @@ interface UserData {
|
||||
status: string;
|
||||
phone?: string | null;
|
||||
departmentId?: string | null;
|
||||
accountTemplateId?: string | null;
|
||||
accountTemplateOverrides?: {
|
||||
overrideFields?: string[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface AccountTemplateItem {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
role: Role;
|
||||
businessRoles: BusinessAccountType[];
|
||||
}
|
||||
|
||||
interface EditUserModalProps {
|
||||
@ -73,13 +61,11 @@ export function EditUserModal({
|
||||
status: user.status,
|
||||
phone: user.phone || '',
|
||||
departmentId: user.departmentId || '',
|
||||
accountTemplateId: user.accountTemplateId || '',
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [departments, setDepartments] = useState<DepartmentTreeOption[]>([]);
|
||||
const [templates, setTemplates] = useState<AccountTemplateItem[]>([]);
|
||||
|
||||
// 加载部门列表
|
||||
useEffect(() => {
|
||||
@ -104,29 +90,6 @@ export function EditUserModal({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account-templates', {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
setTemplates([]);
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
setTemplates(data.data?.templates || []);
|
||||
} catch (error) {
|
||||
console.error('加载岗位模板失败:', error);
|
||||
setTemplates([]);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
void loadTemplates();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// 当用户变化时更新表单
|
||||
useEffect(() => {
|
||||
setFormData({
|
||||
@ -139,7 +102,6 @@ export function EditUserModal({
|
||||
status: user.status,
|
||||
phone: user.phone || '',
|
||||
departmentId: user.departmentId || '',
|
||||
accountTemplateId: user.accountTemplateId || '',
|
||||
});
|
||||
}, [user]);
|
||||
|
||||
@ -152,16 +114,6 @@ export function EditUserModal({
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTemplateChange = (templateId: string) => {
|
||||
const template = templates.find((item) => item.id === templateId);
|
||||
setFormData((current) => ({
|
||||
...current,
|
||||
accountTemplateId: templateId,
|
||||
role: template?.role || current.role,
|
||||
businessRoles: template?.businessRoles || current.businessRoles,
|
||||
}));
|
||||
};
|
||||
|
||||
const availableRoleOptions = [
|
||||
...new Set<Role>([
|
||||
...(allowedRoles as Role[]),
|
||||
@ -219,7 +171,6 @@ export function EditUserModal({
|
||||
departmentId: string | null;
|
||||
teamId?: string | null;
|
||||
password?: string;
|
||||
accountTemplateId?: string | null;
|
||||
} = {
|
||||
email: formData.email,
|
||||
name: formData.name,
|
||||
@ -229,7 +180,6 @@ export function EditUserModal({
|
||||
phone: formData.phone || null,
|
||||
departmentId: formData.departmentId || null,
|
||||
teamId: null,
|
||||
accountTemplateId: formData.accountTemplateId || null,
|
||||
};
|
||||
|
||||
// 只有填写了密码才更新密码
|
||||
@ -319,7 +269,7 @@ export function EditUserModal({
|
||||
权限与归属
|
||||
</h4>
|
||||
<Select
|
||||
label="系统角色"
|
||||
label="账号权限"
|
||||
value={formData.role}
|
||||
onChange={(e) => setFormData({ ...formData, role: e.target.value as Role })}
|
||||
options={availableRoleOptions}
|
||||
@ -327,19 +277,6 @@ export function EditUserModal({
|
||||
disabled={disablePrivilegeFields || user.role === 'SUPER_ADMIN'}
|
||||
className="bg-white"
|
||||
/>
|
||||
<Select
|
||||
label="岗位模板"
|
||||
value={formData.accountTemplateId}
|
||||
onChange={(e) => handleTemplateChange(e.target.value)}
|
||||
options={[
|
||||
{ value: '', label: '不套模板,纯手动维护' },
|
||||
...templates.map((template) => ({
|
||||
value: template.id,
|
||||
label: `${template.name} (${template.code})`,
|
||||
})),
|
||||
]}
|
||||
className="bg-white"
|
||||
/>
|
||||
<Select
|
||||
label="当前状态"
|
||||
value={formData.status}
|
||||
@ -362,9 +299,6 @@ export function EditUserModal({
|
||||
className="bg-white"
|
||||
/>
|
||||
<div>
|
||||
<p className="mb-2 text-[11px] font-bold text-[#607073]">
|
||||
覆盖项:{(user.accountTemplateOverrides?.overrideFields || []).length > 0 ? user.accountTemplateOverrides!.overrideFields!.join(' / ') : '无'}
|
||||
</p>
|
||||
<p className="mb-2 text-[10px] font-apple-black uppercase tracking-[0.18em] text-apple-label-tertiary">
|
||||
业务类型
|
||||
</p>
|
||||
|
||||
@ -372,7 +372,7 @@ export function HandoverModal({ isOpen, user, onClose, onSuccess }: HandoverModa
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[10px] font-apple-black uppercase tracking-widest text-apple-label-tertiary">
|
||||
权限角色
|
||||
账号权限
|
||||
</div>
|
||||
<div className="mt-1 text-sm font-apple-medium text-apple-label-secondary">
|
||||
{roleText}
|
||||
|
||||
@ -22,9 +22,6 @@ export interface UserListItemDTO {
|
||||
loginSource?: string
|
||||
employeeDirectoryId?: string | null
|
||||
employeeDirectory?: UserEmployeeDirectoryDTO | null
|
||||
accountTemplateId?: string | null
|
||||
accountTemplate?: UserAccountTemplateDTO | null
|
||||
accountTemplateOverrides?: Record<string, unknown> | null
|
||||
departmentId?: string | null
|
||||
teamId?: string | null
|
||||
department?: {
|
||||
@ -55,9 +52,6 @@ export interface UserDetailDTO {
|
||||
loginSource?: string
|
||||
employeeDirectoryId?: string | null
|
||||
employeeDirectory?: UserEmployeeDirectoryDTO | null
|
||||
accountTemplateId?: string | null
|
||||
accountTemplate?: UserAccountTemplateDTO | null
|
||||
accountTemplateOverrides?: Record<string, unknown> | null
|
||||
isFirstLogin: boolean
|
||||
department?: {
|
||||
id: string
|
||||
@ -137,14 +131,6 @@ export interface UserEmployeeDirectoryDTO {
|
||||
syncedAt: string
|
||||
}
|
||||
|
||||
export interface UserAccountTemplateDTO {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
role: string
|
||||
businessRoles: BusinessAccountType[]
|
||||
}
|
||||
|
||||
function toEmployeeDirectoryDTO(
|
||||
employeeDirectory?: {
|
||||
id: string
|
||||
@ -183,9 +169,6 @@ export function toUserListItemDTO(
|
||||
department?: Pick<Department, 'id' | 'name' | 'fullPath'> | null
|
||||
team?: Pick<Team, 'id' | 'name'> | null
|
||||
employeeDirectory?: Parameters<typeof toEmployeeDirectoryDTO>[0]
|
||||
accountTemplateId?: string | null
|
||||
accountTemplate?: UserAccountTemplateDTO | null
|
||||
accountTemplateOverrides?: Record<string, unknown> | null
|
||||
}
|
||||
): UserListItemDTO {
|
||||
return {
|
||||
@ -199,9 +182,6 @@ export function toUserListItemDTO(
|
||||
loginSource: user.loginSource,
|
||||
employeeDirectoryId: user.employeeDirectoryId,
|
||||
employeeDirectory: toEmployeeDirectoryDTO(user.employeeDirectory),
|
||||
accountTemplateId: user.accountTemplateId,
|
||||
accountTemplate: user.accountTemplate ?? null,
|
||||
accountTemplateOverrides: user.accountTemplateOverrides ?? null,
|
||||
departmentId: user.departmentId,
|
||||
teamId: user.teamId,
|
||||
department: user.department
|
||||
@ -223,9 +203,6 @@ export function toUserDetailDTO(
|
||||
department?: Pick<Department, 'id' | 'name' | 'fullPath'> | null
|
||||
team?: Pick<Team, 'id' | 'name'> | null
|
||||
employeeDirectory?: Parameters<typeof toEmployeeDirectoryDTO>[0]
|
||||
accountTemplateId?: string | null
|
||||
accountTemplate?: UserAccountTemplateDTO | null
|
||||
accountTemplateOverrides?: Record<string, unknown> | null
|
||||
_count?: { createdProducts: number; ownedSKUs: number }
|
||||
}
|
||||
): UserDetailDTO {
|
||||
@ -241,9 +218,6 @@ export function toUserDetailDTO(
|
||||
loginSource: user.loginSource,
|
||||
employeeDirectoryId: user.employeeDirectoryId,
|
||||
employeeDirectory: toEmployeeDirectoryDTO(user.employeeDirectory),
|
||||
accountTemplateId: user.accountTemplateId,
|
||||
accountTemplate: user.accountTemplate ?? null,
|
||||
accountTemplateOverrides: user.accountTemplateOverrides ?? null,
|
||||
isFirstLogin: user.isFirstLogin,
|
||||
department: user.department
|
||||
? {
|
||||
|
||||
@ -175,15 +175,6 @@ class UserRepositoryClass extends BaseRepository<
|
||||
team: {
|
||||
select: { id: true, name: true },
|
||||
},
|
||||
accountTemplate: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
code: true,
|
||||
role: true,
|
||||
businessRoles: true,
|
||||
},
|
||||
},
|
||||
employeeDirectory: {
|
||||
select: {
|
||||
id: true,
|
||||
@ -217,15 +208,6 @@ class UserRepositoryClass extends BaseRepository<
|
||||
team: {
|
||||
select: { id: true, name: true },
|
||||
},
|
||||
accountTemplate: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
code: true,
|
||||
role: true,
|
||||
businessRoles: true,
|
||||
},
|
||||
},
|
||||
employeeDirectory: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@ -1,253 +0,0 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { AppError, ErrorCode } from '@/server/core/errors'
|
||||
import type { AuthUser } from '@/server/core/types'
|
||||
import {
|
||||
parseBusinessRolesInput,
|
||||
parseUserRoleInput,
|
||||
} from '@/server/validators/business-role.validator'
|
||||
import { Prisma, type BusinessAccountType, type Role } from '@prisma/client'
|
||||
|
||||
export interface AccountTemplateInput {
|
||||
name: string
|
||||
code: string
|
||||
description?: string | null
|
||||
role: Role
|
||||
businessRoles?: BusinessAccountType[]
|
||||
defaultScope?: Prisma.InputJsonValue | null
|
||||
defaultFieldPermissions?: Prisma.InputJsonValue | null
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export interface AccountTemplateDTO {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
description: string | null
|
||||
role: Role
|
||||
businessRoles: BusinessAccountType[]
|
||||
defaultScope: Prisma.JsonValue | null
|
||||
defaultFieldPermissions: Prisma.JsonValue | null
|
||||
isActive: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
const DEFAULT_TEMPLATES: Array<AccountTemplateInput> = [
|
||||
{
|
||||
name: '设计岗位模板',
|
||||
code: 'design-default',
|
||||
description: '默认设计账号模板,负责产品研发、结构设计和视觉建模相关工作。',
|
||||
role: 'USER',
|
||||
businessRoles: ['DESIGN'],
|
||||
defaultScope: { visibility: 'workflow-and-users' },
|
||||
},
|
||||
{
|
||||
name: '企划岗位模板',
|
||||
code: 'planning-default',
|
||||
description: '默认企划账号模板,负责需求整理和前置信息填写。',
|
||||
role: 'USER',
|
||||
businessRoles: ['PLANNING'],
|
||||
defaultScope: { visibility: 'workflow-and-users' },
|
||||
},
|
||||
{
|
||||
name: '采购岗位模板',
|
||||
code: 'purchasing-default',
|
||||
description: '默认采购账号模板,负责供应链与采购字段维护。',
|
||||
role: 'USER',
|
||||
businessRoles: ['PURCHASING'],
|
||||
defaultScope: { visibility: 'workflow-and-users' },
|
||||
},
|
||||
{
|
||||
name: '仓库岗位模板',
|
||||
code: 'warehouse-default',
|
||||
description: '默认仓库账号模板,负责仓储字段与入库确认。',
|
||||
role: 'USER',
|
||||
businessRoles: ['WAREHOUSE'],
|
||||
defaultScope: { visibility: 'workflow-and-users' },
|
||||
},
|
||||
{
|
||||
name: '供应商协作模板',
|
||||
code: 'supplier-default',
|
||||
description: '默认供应商协作账号模板,仅开放流程字段协作范围。',
|
||||
role: 'USER',
|
||||
businessRoles: ['SUPPLIER'],
|
||||
defaultScope: { visibility: 'workflow-only' },
|
||||
},
|
||||
]
|
||||
|
||||
class AccountTemplateService {
|
||||
private toDTO(template: {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
description: string | null
|
||||
role: Role
|
||||
businessRoles: BusinessAccountType[]
|
||||
defaultScope: Prisma.JsonValue | null
|
||||
defaultFieldPermissions: Prisma.JsonValue | null
|
||||
isActive: boolean
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}): AccountTemplateDTO {
|
||||
return {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
code: template.code,
|
||||
description: template.description,
|
||||
role: template.role,
|
||||
businessRoles: template.businessRoles,
|
||||
defaultScope: template.defaultScope,
|
||||
defaultFieldPermissions: template.defaultFieldPermissions,
|
||||
isActive: template.isActive,
|
||||
createdAt: template.createdAt.toISOString(),
|
||||
updatedAt: template.updatedAt.toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
async ensureDefaultTemplates(currentUserId: string): Promise<void> {
|
||||
for (const template of DEFAULT_TEMPLATES) {
|
||||
await prisma.accountTemplate.upsert({
|
||||
where: { code: template.code },
|
||||
update: {
|
||||
description: template.description ?? null,
|
||||
role: template.role,
|
||||
businessRoles: template.businessRoles ?? [],
|
||||
defaultScope: template.defaultScope ?? Prisma.JsonNull,
|
||||
isActive: true,
|
||||
},
|
||||
create: {
|
||||
name: template.name,
|
||||
code: template.code,
|
||||
description: template.description ?? null,
|
||||
role: template.role,
|
||||
businessRoles: template.businessRoles ?? [],
|
||||
defaultScope: template.defaultScope ?? Prisma.JsonNull,
|
||||
defaultFieldPermissions: template.defaultFieldPermissions ?? Prisma.JsonNull,
|
||||
isActive: true,
|
||||
createdBy: currentUserId,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async list(currentUser: AuthUser): Promise<AccountTemplateDTO[]> {
|
||||
await this.ensureDefaultTemplates(currentUser.id)
|
||||
const templates = await prisma.accountTemplate.findMany({
|
||||
orderBy: [{ isActive: 'desc' }, { updatedAt: 'desc' }],
|
||||
})
|
||||
return templates.map((template) => this.toDTO(template))
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<AccountTemplateDTO> {
|
||||
const template = await prisma.accountTemplate.findUnique({ where: { id } })
|
||||
if (!template) {
|
||||
throw new AppError(ErrorCode.VALIDATION_FAILED, '岗位模板不存在')
|
||||
}
|
||||
return this.toDTO(template)
|
||||
}
|
||||
|
||||
async create(input: AccountTemplateInput, currentUser: AuthUser): Promise<AccountTemplateDTO> {
|
||||
if (!input.name.trim() || !input.code.trim()) {
|
||||
throw new AppError(ErrorCode.VALIDATION_FAILED, '模板名称和编码不能为空')
|
||||
}
|
||||
|
||||
const role = parseUserRoleInput(input.role, 'role') ?? 'USER'
|
||||
const businessRoles = parseBusinessRolesInput(input.businessRoles, { optional: true }) ?? []
|
||||
|
||||
const template = await prisma.accountTemplate.create({
|
||||
data: {
|
||||
name: input.name.trim(),
|
||||
code: input.code.trim(),
|
||||
description: input.description?.trim() || null,
|
||||
role,
|
||||
businessRoles,
|
||||
defaultScope: input.defaultScope ?? Prisma.JsonNull,
|
||||
defaultFieldPermissions: input.defaultFieldPermissions ?? Prisma.JsonNull,
|
||||
isActive: input.isActive ?? true,
|
||||
createdBy: currentUser.id,
|
||||
},
|
||||
})
|
||||
|
||||
return this.toDTO(template)
|
||||
}
|
||||
|
||||
async update(id: string, input: Partial<AccountTemplateInput>): Promise<AccountTemplateDTO> {
|
||||
const existing = await prisma.accountTemplate.findUnique({ where: { id } })
|
||||
if (!existing) {
|
||||
throw new AppError(ErrorCode.VALIDATION_FAILED, '岗位模板不存在')
|
||||
}
|
||||
|
||||
const role = 'role' in input ? parseUserRoleInput(input.role, 'role') : undefined
|
||||
const businessRoles = 'businessRoles' in input
|
||||
? parseBusinessRolesInput(input.businessRoles, { optional: true })
|
||||
: undefined
|
||||
|
||||
const template = await prisma.accountTemplate.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: input.name?.trim(),
|
||||
code: input.code?.trim(),
|
||||
description: input.description === undefined ? undefined : input.description?.trim() || null,
|
||||
role,
|
||||
businessRoles,
|
||||
defaultScope: input.defaultScope === undefined ? undefined : input.defaultScope ?? Prisma.JsonNull,
|
||||
defaultFieldPermissions:
|
||||
input.defaultFieldPermissions === undefined
|
||||
? undefined
|
||||
: input.defaultFieldPermissions ?? Prisma.JsonNull,
|
||||
isActive: input.isActive,
|
||||
},
|
||||
})
|
||||
|
||||
return this.toDTO(template)
|
||||
}
|
||||
|
||||
async buildUserTemplateAssignment(input: {
|
||||
accountTemplateId?: string | null
|
||||
role?: Role
|
||||
businessRoles?: BusinessAccountType[]
|
||||
}): Promise<{
|
||||
accountTemplateId?: string | null
|
||||
role?: Role
|
||||
businessRoles?: BusinessAccountType[]
|
||||
accountTemplateOverrides?: Prisma.InputJsonValue | undefined
|
||||
}> {
|
||||
if (!input.accountTemplateId) {
|
||||
return {
|
||||
accountTemplateId: input.accountTemplateId ?? null,
|
||||
role: input.role,
|
||||
businessRoles: input.businessRoles,
|
||||
accountTemplateOverrides: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const template = await prisma.accountTemplate.findUnique({
|
||||
where: { id: input.accountTemplateId },
|
||||
})
|
||||
|
||||
if (!template || !template.isActive) {
|
||||
throw new AppError(ErrorCode.VALIDATION_FAILED, '所选岗位模板不可用')
|
||||
}
|
||||
|
||||
const effectiveRole = input.role ?? template.role
|
||||
const effectiveBusinessRoles = input.businessRoles ?? template.businessRoles
|
||||
|
||||
const overrideFields: string[] = []
|
||||
if (effectiveRole !== template.role) overrideFields.push('role')
|
||||
if (JSON.stringify(effectiveBusinessRoles) !== JSON.stringify(template.businessRoles)) {
|
||||
overrideFields.push('businessRoles')
|
||||
}
|
||||
|
||||
return {
|
||||
accountTemplateId: template.id,
|
||||
role: effectiveRole,
|
||||
businessRoles: effectiveBusinessRoles,
|
||||
accountTemplateOverrides: {
|
||||
templateCode: template.code,
|
||||
overrideFields,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const accountTemplateService = new AccountTemplateService()
|
||||
@ -514,7 +514,6 @@ class UserManagementService {
|
||||
departmentId?: string
|
||||
teamId?: string
|
||||
phone?: string
|
||||
accountTemplateId?: string | null
|
||||
},
|
||||
currentUser: AuthUser
|
||||
) {
|
||||
@ -561,7 +560,6 @@ class UserManagementService {
|
||||
departmentId: effectiveDepartmentId,
|
||||
teamId: undefined,
|
||||
phone: input.phone ?? employee?.mobile ?? undefined,
|
||||
accountTemplateId: input.accountTemplateId ?? null,
|
||||
isFirstLogin: false,
|
||||
status: 'PENDING',
|
||||
employeeDirectoryId: employee?.id ?? undefined,
|
||||
@ -599,7 +597,6 @@ class UserManagementService {
|
||||
teamId?: string | null
|
||||
phone?: string | null
|
||||
status?: UserStatus
|
||||
accountTemplateId?: string | null
|
||||
},
|
||||
currentUser: AuthUser
|
||||
) {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { hashPassword, validatePasswordStrength, verifyPassword } from '@/lib/password'
|
||||
import { AppError, ErrorCode } from '@/server/core/errors'
|
||||
import { accountTemplateService } from './account-template.service'
|
||||
import type { BusinessAccountType, EmployeeSourceStatus, Prisma, Role, UserStatus } from '@prisma/client'
|
||||
import type { BusinessAccountType, EmployeeSourceStatus, Role, UserStatus } from '@prisma/client'
|
||||
import { departmentRepository } from '../repositories/department.repository'
|
||||
import { userRepository } from '../repositories/user.repository'
|
||||
|
||||
@ -14,7 +13,6 @@ export interface CreateUserInput {
|
||||
departmentId?: string
|
||||
teamId?: string
|
||||
phone?: string
|
||||
accountTemplateId?: string | null
|
||||
isFirstLogin?: boolean
|
||||
status?: UserStatus
|
||||
loginSource?: string
|
||||
@ -36,7 +34,6 @@ export interface UpdateUserInput {
|
||||
phone?: string | null
|
||||
status?: UserStatus
|
||||
avatar?: string | null
|
||||
accountTemplateId?: string | null
|
||||
}
|
||||
|
||||
export interface UserListOptions {
|
||||
@ -79,15 +76,6 @@ export interface UserDTO {
|
||||
sourceStatus: EmployeeSourceStatus
|
||||
syncedAt: Date
|
||||
} | null
|
||||
accountTemplateId: string | null
|
||||
accountTemplate: {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
role: Role
|
||||
businessRoles: BusinessAccountType[]
|
||||
} | null
|
||||
accountTemplateOverrides: Prisma.JsonValue | null
|
||||
avatar: string | null
|
||||
phone: string | null
|
||||
departmentId: string | null
|
||||
@ -143,15 +131,6 @@ class UserService {
|
||||
sourceStatus: EmployeeSourceStatus
|
||||
syncedAt: Date
|
||||
} | null
|
||||
accountTemplateId?: string | null
|
||||
accountTemplate?: {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
role: Role
|
||||
businessRoles: BusinessAccountType[]
|
||||
} | null
|
||||
accountTemplateOverrides?: Prisma.JsonValue | null
|
||||
avatar: string | null
|
||||
phone: string | null
|
||||
departmentId: string | null
|
||||
@ -172,9 +151,6 @@ class UserService {
|
||||
activationRequired: user.status === 'PENDING',
|
||||
employeeDirectoryId: user.employeeDirectoryId ?? null,
|
||||
employeeDirectory: user.employeeDirectory ?? null,
|
||||
accountTemplateId: user.accountTemplateId ?? null,
|
||||
accountTemplate: user.accountTemplate ?? null,
|
||||
accountTemplateOverrides: user.accountTemplateOverrides ?? null,
|
||||
avatar: user.avatar,
|
||||
phone: user.phone,
|
||||
departmentId: user.departmentId,
|
||||
@ -294,17 +270,12 @@ class UserService {
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(input.password)
|
||||
const templateAssignment = await accountTemplateService.buildUserTemplateAssignment({
|
||||
accountTemplateId: input.accountTemplateId,
|
||||
role: input.role,
|
||||
businessRoles: input.businessRoles,
|
||||
})
|
||||
const created = await userRepository.createWithScalars({
|
||||
email: input.email,
|
||||
name: input.name,
|
||||
password: hashedPassword,
|
||||
role: templateAssignment.role ?? 'USER',
|
||||
businessRoles: templateAssignment.businessRoles ?? [],
|
||||
role: input.role ?? 'USER',
|
||||
businessRoles: input.businessRoles ?? [],
|
||||
departmentId: input.departmentId,
|
||||
teamId: null,
|
||||
phone: input.phone,
|
||||
@ -316,8 +287,6 @@ class UserService {
|
||||
dingtalkUnionId: input.dingtalkUnionId,
|
||||
dingtalkBoundAt: input.dingtalkBoundAt,
|
||||
avatar: input.avatar,
|
||||
accountTemplateId: templateAssignment.accountTemplateId,
|
||||
accountTemplateOverrides: templateAssignment.accountTemplateOverrides ?? undefined,
|
||||
})
|
||||
|
||||
const user = await userRepository.findByIdWithRelations(created.id)
|
||||
@ -360,31 +329,17 @@ class UserService {
|
||||
hashedPassword = await hashPassword(input.password)
|
||||
}
|
||||
|
||||
const templateAssignment =
|
||||
input.accountTemplateId !== undefined || input.role !== undefined || input.businessRoles !== undefined
|
||||
? await accountTemplateService.buildUserTemplateAssignment({
|
||||
accountTemplateId: input.accountTemplateId ?? existing.accountTemplateId ?? null,
|
||||
role: input.role,
|
||||
businessRoles: input.businessRoles,
|
||||
})
|
||||
: null
|
||||
|
||||
await userRepository.updateWithScalars(id, {
|
||||
email: input.email,
|
||||
name: input.name,
|
||||
password: hashedPassword,
|
||||
role: templateAssignment?.role ?? input.role,
|
||||
businessRoles: templateAssignment?.businessRoles ?? input.businessRoles,
|
||||
role: input.role,
|
||||
businessRoles: input.businessRoles,
|
||||
departmentId: input.departmentId,
|
||||
teamId: input.teamId === undefined ? undefined : null,
|
||||
phone: input.phone,
|
||||
status: input.status,
|
||||
avatar: input.avatar,
|
||||
accountTemplateId: templateAssignment?.accountTemplateId ?? input.accountTemplateId,
|
||||
accountTemplateOverrides:
|
||||
templateAssignment?.accountTemplateOverrides === undefined
|
||||
? undefined
|
||||
: templateAssignment.accountTemplateOverrides,
|
||||
})
|
||||
|
||||
const updatedUser = await userRepository.findByIdWithRelations(id)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user