feat: 平台管理员创建企业客户功能 + deploy.sh 跳过 tsc
This commit is contained in:
@@ -136,6 +136,71 @@ router.get('/orgs', async (req: AuthRequest, res, next) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建企业客户(含管理员账号)
|
||||||
|
*/
|
||||||
|
router.post('/orgs', async (req: AuthRequest, res, next) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
name, plan, maxEmployees, city, contactName, contactPhone,
|
||||||
|
adminName, adminPhone, adminPassword,
|
||||||
|
} = req.body as {
|
||||||
|
name: string; plan?: string; maxEmployees?: number
|
||||||
|
city?: string; contactName?: string; contactPhone?: string
|
||||||
|
adminName?: string; adminPhone: string; adminPassword: string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name || !adminPhone || !adminPassword) {
|
||||||
|
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '企业名称、管理员手机号、密码不能为空' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adminPassword.length < 8) {
|
||||||
|
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '密码至少8位' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await prisma.user.findUnique({ where: { phone: adminPhone } })
|
||||||
|
if (existing) {
|
||||||
|
return res.status(400).json({ success: false, error: { code: 'DUPLICATE', message: '该手机号已存在' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建企业
|
||||||
|
const org = await prisma.organization.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
plan: (plan as any) || 'FREE',
|
||||||
|
maxEmployees: maxEmployees || 20,
|
||||||
|
city: city || null,
|
||||||
|
contactName: contactName || null,
|
||||||
|
contactPhone: contactPhone || null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建管理员账号
|
||||||
|
const bcrypt = await import('bcryptjs')
|
||||||
|
const passwordHash = await bcrypt.default.hash(adminPassword, 10)
|
||||||
|
const admin = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
orgId: org.id,
|
||||||
|
phone: adminPhone,
|
||||||
|
name: adminName || '管理员',
|
||||||
|
passwordHash,
|
||||||
|
role: 'ADMIN',
|
||||||
|
},
|
||||||
|
select: { id: true, name: true, phone: true, role: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
org: { id: org.id, name: org.name, plan: org.plan, maxEmployees: org.maxEmployees, city: org.city },
|
||||||
|
admin,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 企业详情
|
* 企业详情
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -197,15 +197,8 @@ build_backend() {
|
|||||||
log "生成 Prisma Client..."
|
log "生成 Prisma Client..."
|
||||||
npx prisma generate
|
npx prisma generate
|
||||||
|
|
||||||
log "编译 TypeScript..."
|
# 服务器用 tsx 直接运行 TS 源码,跳过 tsc 编译
|
||||||
npx tsc
|
log "跳过 tsc 编译(服务器使用 tsx 运行)"
|
||||||
|
|
||||||
if [ ! -d dist ]; then
|
|
||||||
err "后端编译失败,dist 目录不存在"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "后端编译完成: dist/ ($(du -sh dist | cut -f1))"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* 企业租户管理页 — 列表、搜索、查看详情、编辑套餐、删除
|
* 企业租户管理页 — 列表、搜索、查看详情、编辑套餐、删除
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Search, Building2, Eye, Trash2, Edit2 } from 'lucide-react'
|
import { Search, Building2, Eye, Trash2, Edit2, Plus } from 'lucide-react'
|
||||||
import api from '../../lib/api'
|
import api from '../../lib/api'
|
||||||
import { Input, Select, Label } from '../../components/ui/Input'
|
import { Input, Select, Label } from '../../components/ui/Input'
|
||||||
import Button from '../../components/ui/Button'
|
import Button from '../../components/ui/Button'
|
||||||
@@ -28,6 +28,13 @@ export default function PlatformOrgs() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [editOrg, setEditOrg] = useState<Org | null>(null)
|
const [editOrg, setEditOrg] = useState<Org | null>(null)
|
||||||
const [deleteOrg, setDeleteOrg] = useState<Org | null>(null)
|
const [deleteOrg, setDeleteOrg] = useState<Org | null>(null)
|
||||||
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
|
const [createForm, setCreateForm] = useState({
|
||||||
|
name: '', plan: 'FREE', maxEmployees: 20, city: '',
|
||||||
|
contactName: '', contactPhone: '',
|
||||||
|
adminName: '管理员', adminPhone: '', adminPassword: '12345678',
|
||||||
|
})
|
||||||
|
const [creating, setCreating] = useState(false)
|
||||||
|
|
||||||
const fetchOrgs = async () => {
|
const fetchOrgs = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -66,6 +73,28 @@ export default function PlatformOrgs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
if (!createForm.name || !createForm.adminPhone || !createForm.adminPassword) {
|
||||||
|
alert('企业名称、管理员手机号、密码不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setCreating(true)
|
||||||
|
try {
|
||||||
|
await api.post('/platform/orgs', createForm)
|
||||||
|
setCreateOpen(false)
|
||||||
|
setCreateForm({
|
||||||
|
name: '', plan: 'FREE', maxEmployees: 20, city: '',
|
||||||
|
contactName: '', contactPhone: '',
|
||||||
|
adminName: '管理员', adminPhone: '', adminPassword: '12345678',
|
||||||
|
})
|
||||||
|
fetchOrgs()
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err.response?.data?.error?.message || '创建失败')
|
||||||
|
} finally {
|
||||||
|
setCreating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!deleteOrg) return
|
if (!deleteOrg) return
|
||||||
try {
|
try {
|
||||||
@@ -109,6 +138,10 @@ export default function PlatformOrgs() {
|
|||||||
<Button variant="secondary" onClick={fetchOrgs} className="bg-slate-800 text-slate-200 border border-slate-700 hover:bg-slate-700">
|
<Button variant="secondary" onClick={fetchOrgs} className="bg-slate-800 text-slate-200 border border-slate-700 hover:bg-slate-700">
|
||||||
搜索
|
搜索
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={() => setCreateOpen(true)} className="bg-amber-400 text-slate-950 hover:bg-amber-300">
|
||||||
|
<Plus className="w-4 h-4 mr-1" />
|
||||||
|
创建企业
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 表格 */}
|
{/* 表格 */}
|
||||||
@@ -182,6 +215,73 @@ export default function PlatformOrgs() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 创建企业弹窗 */}
|
||||||
|
{createOpen && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" onClick={() => setCreateOpen(false)}>
|
||||||
|
<div className="bg-slate-900 border border-slate-700 rounded-lg p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h2 className="text-lg font-semibold text-white mb-4">创建企业客户</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="border-b border-slate-700 pb-3">
|
||||||
|
<h3 className="text-xs text-amber-400 font-medium mb-3">企业信息</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label className="text-slate-300">企业名称 *</Label>
|
||||||
|
<Input className="bg-slate-800 border-slate-700 text-white" placeholder="如:北京XX科技有限公司" value={createForm.name} onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">套餐</Label>
|
||||||
|
<Select className="bg-slate-800 border-slate-700 text-white" value={createForm.plan} onChange={(e) => setCreateForm({ ...createForm, plan: e.target.value })}>
|
||||||
|
<option value="FREE">免费版</option>
|
||||||
|
<option value="PRO">专业版</option>
|
||||||
|
<option value="ENTERPRISE">企业版</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">员工上限</Label>
|
||||||
|
<Input type="number" className="bg-slate-800 border-slate-700 text-white" value={createForm.maxEmployees} onChange={(e) => setCreateForm({ ...createForm, maxEmployees: parseInt(e.target.value) || 0 })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">城市</Label>
|
||||||
|
<Input className="bg-slate-800 border-slate-700 text-white" placeholder="如:北京" value={createForm.city} onChange={(e) => setCreateForm({ ...createForm, city: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">联系人</Label>
|
||||||
|
<Input className="bg-slate-800 border-slate-700 text-white" value={createForm.contactName} onChange={(e) => setCreateForm({ ...createForm, contactName: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label className="text-slate-300">联系电话</Label>
|
||||||
|
<Input className="bg-slate-800 border-slate-700 text-white" value={createForm.contactPhone} onChange={(e) => setCreateForm({ ...createForm, contactPhone: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-slate-700 pb-3">
|
||||||
|
<h3 className="text-xs text-amber-400 font-medium mb-3">管理员账号</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">管理员姓名</Label>
|
||||||
|
<Input className="bg-slate-800 border-slate-700 text-white" value={createForm.adminName} onChange={(e) => setCreateForm({ ...createForm, adminName: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-slate-300">手机号 *</Label>
|
||||||
|
<Input className="bg-slate-800 border-slate-700 text-white" placeholder="13800000000" maxLength={11} value={createForm.adminPhone} onChange={(e) => setCreateForm({ ...createForm, adminPhone: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label className="text-slate-300">密码 *(至少8位)</Label>
|
||||||
|
<Input className="bg-slate-800 border-slate-700 text-white" value={createForm.adminPassword} onChange={(e) => setCreateForm({ ...createForm, adminPassword: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Button className="bg-amber-400 text-slate-950 hover:bg-amber-300 flex-1" onClick={handleCreate} disabled={creating}>
|
||||||
|
{creating ? '创建中...' : '确认创建'}
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" className="bg-slate-800 text-slate-200 border border-slate-700" onClick={() => setCreateOpen(false)}>取消</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 编辑弹窗 */}
|
{/* 编辑弹窗 */}
|
||||||
{editOrg && (
|
{editOrg && (
|
||||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" onClick={() => setEditOrg(null)}>
|
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" onClick={() => setEditOrg(null)}>
|
||||||
|
|||||||
Reference in New Issue
Block a user