diff --git a/README.md b/README.md index 9605445..a0c2e41 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ tansci/ ├── antdv-next-admin/ # 前端管理后台 (Vue 3 + TypeScript + Ant Design Vue) ├── our-itam/ # 后端服务 (Java Spring Boot + magic-api) -├── fantastic-admin/ # 前端底层框架 (pnpm monorepo) -├── ciyo-itasset/ # CIYO IT资产模块 +├── ciyo-itasset/ # CIYO IT资产模块 (功能参考) └── magic-script-skill/ # Claude Code 自定义 Skill ``` @@ -81,12 +80,12 @@ magic-api 管理平台: http://localhost:9999/magic/web/index.html ### 3. 启动前端服务 ```bash -cd fantastic-admin +cd antdv-next-admin pnpm install pnpm dev ``` -启动后选择 `core-antdv-next` 进入 antdv-next-admin 前端,默认地址 http://localhost:9000/ +默认地址 http://localhost:3000/ ### 4. 测试账号 diff --git a/antdv-next-admin/pnpm-workspace.yaml b/antdv-next-admin/pnpm-workspace.yaml new file mode 100644 index 0000000..248c5b2 --- /dev/null +++ b/antdv-next-admin/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + '@parcel/watcher': set this to true or false diff --git a/antdv-next-admin/src/api/ldap.ts b/antdv-next-admin/src/api/ldap.ts new file mode 100644 index 0000000..3c778dc --- /dev/null +++ b/antdv-next-admin/src/api/ldap.ts @@ -0,0 +1,160 @@ +import type { ApiResponse } from "@/types/api"; +import { request } from "@/utils/request"; + +const isMock = import.meta.env.VITE_USE_MOCK === "true"; + +const ok = (data: T, message = "success"): ApiResponse => ({ + code: 200, + message, + data, + success: true, +}); + +export interface LdapConfig { + id?: string; + name: string; + host: string; + port: number; + use_ssl: string; + base_dn: string; + admin_dn: string; + admin_password: string; + user_base_dn?: string; + dept_base_dn?: string; + status: string; + last_sync_time?: string; + sync_status?: string; + create_time?: string; + update_time?: string; +} + +export interface LdapSyncResult { + success: boolean; + message?: string; + created?: number; + updated?: number; + deleted?: number; + errors?: string[]; +} + +export interface LdapFullSyncResult { + success: boolean; + message?: string; + deptResult?: LdapSyncResult; + userResult?: LdapSyncResult; +} + +/** + * 获取 LDAP 配置分页列表 + */ +export async function getLdapConfigList(params: { + page: number; + pageSize: number; +}): Promise> { + if (isMock) { + return ok({ list: [], total: 0 }); + } + const res: any = await request.post("/sys/ldap/page", params); + return { + code: 200, + message: res.message || "success", + data: { list: (res.data || res).list || [], total: (res.data || res).total || 0 }, + success: true, + }; +} + +/** + * 获取 LDAP 配置详情 + */ +export async function getLdapConfigDetail(id: string): Promise> { + if (isMock) { + return ok({} as LdapConfig); + } + const res: any = await request.get(`/sys/ldap/detail?id=${id}`); + return { code: 200, message: res.message || "success", data: res.data || res, success: true }; +} + +/** + * 保存 LDAP 配置(新增或更新) + */ +export async function saveLdapConfig(data: Partial): Promise> { + if (isMock) { + return ok({ id: String(Date.now()) }, "保存成功"); + } + const res: any = await request.post("/sys/ldap/save", { + id: data.id, + name: data.name, + host: data.host, + port: data.port, + use_ssl: data.use_ssl, + base_dn: data.base_dn, + admin_dn: data.admin_dn, + admin_password: data.admin_password, + user_base_dn: data.user_base_dn, + dept_base_dn: data.dept_base_dn, + status: data.status, + }); + return { code: 200, message: res.message || "保存成功", data: res.data || res, success: true }; +} + +/** + * 删除 LDAP 配置 + */ +export async function deleteLdapConfig(id: string): Promise> { + if (isMock) { + return ok(undefined as unknown as void, "删除成功"); + } + await request.post("/sys/ldap/delete", { id }); + return { code: 200, message: "删除成功", data: null, success: true }; +} + +/** + * 测试 LDAP 连接 + */ +export async function testLdapConnection(config: { + host: string; + port: number; + use_ssl: string; + admin_dn: string; + admin_password: string; + base_dn: string; +}): Promise> { + if (isMock) { + return ok({ success: true, message: "连接成功" }); + } + const res: any = await request.post("/sys/ldap/test_connection", config); + return { code: 200, message: res.message || "success", data: res.data || res, success: true }; +} + +/** + * 同步部门到 LDAP + */ +export async function syncDepts(configId: string): Promise> { + if (isMock) { + return ok({ success: true, created: 0, updated: 0, deleted: 0 }); + } + const res: any = await request.post("/sys/ldap/sync_depts", { config_id: configId }); + return { code: 200, message: res.message || "success", data: res.data || res, success: true }; +} + +/** + * 同步用户到 LDAP + */ +export async function syncUsers(configId: string): Promise> { + if (isMock) { + return ok({ success: true, created: 0, updated: 0, deleted: 0 }); + } + const res: any = await request.post("/sys/ldap/sync_users", { config_id: configId }); + return { code: 200, message: res.message || "success", data: res.data || res, success: true }; +} + +/** + * 全量同步(部门 + 用户) + */ +export async function fullSync(configId: string): Promise> { + if (isMock) { + return ok({ success: true }); + } + const res: any = await request.post("/sys/ldap/full_sync", { config_id: configId }); + return { code: 200, message: res.message || "success", data: res.data || res, success: true }; +} diff --git a/antdv-next-admin/src/api/role.ts b/antdv-next-admin/src/api/role.ts index fb7bd32..614196f 100644 --- a/antdv-next-admin/src/api/role.ts +++ b/antdv-next-admin/src/api/role.ts @@ -16,6 +16,7 @@ function mapRoleToFrontend(r: any) { name: r.role_name, code: r.role_key, description: r.remark || "", + permissionCount: r.menu_count ?? 0, createdAt: r.create_time, updatedAt: r.update_time, }; diff --git a/antdv-next-admin/src/api/user.ts b/antdv-next-admin/src/api/user.ts index 3ad18ea..f4768d6 100644 --- a/antdv-next-admin/src/api/user.ts +++ b/antdv-next-admin/src/api/user.ts @@ -26,7 +26,7 @@ function mapUserToFrontend(u: any) { updatedAt: u.update_time, deptName: u.dept_name || "", roles: (u.roles || []).map((r: any) => ({ - id: r.role_key, + id: r.id || r.role_key, name: r.role_name, code: r.role_key, })), @@ -37,10 +37,15 @@ function mapUserToBackend(data: Record) { const result: Record = { ...data }; if (result.username !== undefined) { result.user_name = result.username; delete result.username; } if (result.realName !== undefined) { result.nick_name = result.realName; delete result.realName; } + if (result.phone !== undefined) { result.phonenumber = result.phone; delete result.phone; } + if (result.bio !== undefined) { result.remark = result.bio; delete result.bio; } if (result.gender === "male") { result.sex = "1"; delete result.gender; } if (result.gender === "female") { result.sex = "0"; delete result.gender; } if (result.status === "active") { result.status = "0"; } if (result.status === "inactive") { result.status = "1"; } + // roles 由 assignRoles 单独处理 + delete result.roles; + delete result.roleIds; return result; } diff --git a/antdv-next-admin/src/locales/en-US.ts b/antdv-next-admin/src/locales/en-US.ts index e6ed8f5..1722d40 100644 --- a/antdv-next-admin/src/locales/en-US.ts +++ b/antdv-next-admin/src/locales/en-US.ts @@ -92,6 +92,7 @@ export default { permission: "Permission", menu: "Menu Management", dict: "Dictionary", + ldap: "LDAP Config", log: "System Log", asset: "Asset Management", assetDevice: "Asset", @@ -471,6 +472,7 @@ export default { loadDataFailed: "Failed to load dictionary data", dictDataTitle: "Dictionary Data - {name}", confirmDelete: "Confirm Delete", + serial: "#", }, table: { @@ -1641,4 +1643,58 @@ export default { outputPreview: "Output Preview", }, }, + + ldap: { + title: "LDAP Config", + name: "Config Name", + host: "Server Host", + port: "Port", + useSsl: "Use SSL", + baseDn: "Base DN", + adminDn: "Admin DN", + adminPassword: "Admin Password", + userBaseDn: "User Base DN", + deptBaseDn: "Dept Base DN", + status: "Status", + lastSyncTime: "Last Sync Time", + syncStatus: "Sync Status", + enabled: "Enabled", + disabled: "Disabled", + createConfig: "Create Config", + editConfig: "Edit Config", + deleteConfig: "Delete Config", + confirmDelete: "Are you sure you want to delete this LDAP config?", + saveSuccess: "Saved successfully", + deleteSuccess: "Deleted successfully", + testConnection: "Test Connection", + testing: "Testing...", + testSuccess: "Connection successful", + testFailed: "Connection failed", + testConnectionFirst: "Please test connection first", + syncDepts: "Sync Departments", + syncUsers: "Sync Users", + fullSync: "Full Sync", + syncing: "Syncing...", + syncSuccess: "Sync successful", + syncFailed: "Sync failed", + confirmSyncDepts: "Are you sure you want to sync departments to LDAP server?", + confirmSyncUsers: "Are you sure you want to sync users to LDAP server?", + confirmFullSync: "Are you sure you want to perform a full sync? This will overwrite existing data on LDAP server.", + idle: "Idle", + running: "Running", + success: "Success", + failed: "Failed", + namePlaceholder: "Please enter config name", + hostPlaceholder: "e.g. 192.168.2.1", + portPlaceholder: "e.g. 389", + baseDnPlaceholder: "e.g. dc=koteladt,dc=com", + adminDnPlaceholder: "e.g. cn=admin,dc=koteladt,dc=com", + adminPasswordPlaceholder: "Please enter admin password", + requiredFields: "Please fill in required fields", + loadFailed: "Failed to load LDAP config", + operateFailed: "Operation failed", + syncDeptsResult: "Dept sync completed: {success} success, {failed} failed", + syncUsersResult: "User sync completed: {success} success, {failed} failed", + fullSyncResult: "Full sync completed: dept {deptSuccess} success {deptFailed} failed, user {userSuccess} success {userFailed} failed", + }, }; diff --git a/antdv-next-admin/src/locales/zh-CN.ts b/antdv-next-admin/src/locales/zh-CN.ts index 7aefc51..a735e16 100644 --- a/antdv-next-admin/src/locales/zh-CN.ts +++ b/antdv-next-admin/src/locales/zh-CN.ts @@ -92,6 +92,7 @@ export default { permission: "权限管理", menu: "菜单管理", dict: "数据字典", + ldap: "LDAP配置", log: "系统日志", asset: "资产管理", assetDevice: "资产管理", @@ -463,6 +464,7 @@ export default { loadDataFailed: "加载字典数据失败", dictDataTitle: "字典数据 - {name}", confirmDelete: "确认删除", + serial: "序号", }, table: { @@ -1590,4 +1592,58 @@ export default { outputPreview: "输出预览", }, }, + + ldap: { + title: "LDAP配置", + name: "配置名称", + host: "服务器地址", + port: "端口", + useSsl: "启用SSL", + baseDn: "Base DN", + adminDn: "管理员DN", + adminPassword: "管理员密码", + userBaseDn: "用户Base DN", + deptBaseDn: "部门Base DN", + status: "状态", + lastSyncTime: "最后同步时间", + syncStatus: "同步状态", + enabled: "启用", + disabled: "停用", + createConfig: "新增配置", + editConfig: "编辑配置", + deleteConfig: "删除配置", + confirmDelete: "确定要删除该LDAP配置吗?", + saveSuccess: "保存成功", + deleteSuccess: "删除成功", + testConnection: "测试连接", + testing: "测试中...", + testSuccess: "连接成功", + testFailed: "连接失败", + testConnectionFirst: "请先测试连接", + syncDepts: "同步部门", + syncUsers: "同步用户", + fullSync: "全量同步", + syncing: "同步中...", + syncSuccess: "同步成功", + syncFailed: "同步失败", + confirmSyncDepts: "确定要同步部门到LDAP服务器吗?", + confirmSyncUsers: "确定要同步用户到LDAP服务器吗?", + confirmFullSync: "确定要执行全量同步吗?这将覆盖LDAP服务器上已有的数据。", + idle: "空闲", + running: "同步中", + success: "成功", + failed: "失败", + namePlaceholder: "请输入配置名称", + hostPlaceholder: "例:192.168.2.1", + portPlaceholder: "例:389", + baseDnPlaceholder: "例:dc=koteladt,dc=com", + adminDnPlaceholder: "例:cn=admin,dc=koteladt,dc=com", + adminPasswordPlaceholder: "请输入管理员密码", + requiredFields: "请填写必填项", + loadFailed: "加载LDAP配置失败", + operateFailed: "操作失败", + syncDeptsResult: "同步部门完成:成功 {success},失败 {failed}", + syncUsersResult: "同步用户完成:成功 {success},失败 {failed}", + fullSyncResult: "全量同步完成:部门成功 {deptSuccess} 失败 {deptFailed},用户成功 {userSuccess} 失败 {userFailed}", + }, }; diff --git a/antdv-next-admin/src/router/guards.ts b/antdv-next-admin/src/router/guards.ts index b2cb775..7229765 100644 --- a/antdv-next-admin/src/router/guards.ts +++ b/antdv-next-admin/src/router/guards.ts @@ -13,6 +13,8 @@ import { basicRoutes, notFoundRoute } from "./routes"; const MENU_HISTORY_KEY = "app-menu-history"; const MAX_HISTORY_ITEMS = 10; +let tokenVerified = false; // 标记 token 是否已在本次会话中校验通过 + interface MenuHistoryItem { path: string; title: string; @@ -154,6 +156,24 @@ export function setupRouterGuards(router: Router) { return { path: "/login", query: { redirect: to.fullPath } }; } + // 校验 token 是否有效(首次进入需认证页面时校验一次) + if (!tokenVerified) { + try { + const { request } = await import("@/utils/request"); + const checkRes: any = await request.get("/auth/check_token"); + if (!checkRes?.data?.valid) { + authStore.logout(); + tokenVerified = false; + return { path: "/login", query: { redirect: to.fullPath } }; + } + tokenVerified = true; + } catch { + authStore.logout(); + tokenVerified = false; + return { path: "/login", query: { redirect: to.fullPath } }; + } + } + // Generate dynamic routes if not already generated if (!permissionStore.isRoutesGenerated) { try { diff --git a/antdv-next-admin/src/router/routes.ts b/antdv-next-admin/src/router/routes.ts index b50af00..cdfa076 100644 --- a/antdv-next-admin/src/router/routes.ts +++ b/antdv-next-admin/src/router/routes.ts @@ -174,16 +174,6 @@ export const asyncRoutes: AppRouteRecordRaw[] = [ requiresAuth: true, }, }, - { - path: "permission", - name: "SystemPermission", - component: () => import("@/views/system/permission/index.vue"), - meta: { - title: "menu.permission", - icon: "SafetyCertificateOutlined", - requiresAuth: true, - }, - }, { path: "dict", name: "SystemDict", @@ -194,6 +184,16 @@ export const asyncRoutes: AppRouteRecordRaw[] = [ requiresAuth: true, }, }, + { + path: "ldap", + name: "SystemLdap", + component: () => import("@/views/system/ldap/index.vue"), + meta: { + title: "menu.ldap", + icon: "ClusterOutlined", + requiresAuth: true, + }, + }, ], }, { diff --git a/antdv-next-admin/src/utils/request.ts b/antdv-next-admin/src/utils/request.ts index b92f251..40ad1c9 100644 --- a/antdv-next-admin/src/utils/request.ts +++ b/antdv-next-admin/src/utils/request.ts @@ -47,6 +47,20 @@ service.interceptors.response.use( router.push("/login"); return Promise.reject(new Error(res.message || "Unauthorized")); } + // magic-api 返回 -1 可能由 token 失效引起,校验并跳转登录 + if (res.code === -1) { + const authStore = useAuthStore(); + if (authStore.token && response.config.url !== "/auth/check_token") { + // 异步校验 token 是否失效 + service.get("/auth/check_token").then((checkRes: any) => { + if (!checkRes?.data?.valid) { + authStore.logout(); + message.error("登录已过期,请重新登录"); + router.push("/login"); + } + }).catch(() => {}); + } + } message.error(res.message || "请求失败"); return Promise.reject(new Error(res.message || "Error")); } diff --git a/antdv-next-admin/src/views/asset/base/category/index.vue b/antdv-next-admin/src/views/asset/base/category/index.vue index 3c7bb45..6de4090 100644 --- a/antdv-next-admin/src/views/asset/base/category/index.vue +++ b/antdv-next-admin/src/views/asset/base/category/index.vue @@ -1,124 +1,257 @@