diff --git a/backend-fastapi/db_init.json b/backend-fastapi/db_init.json index 8d378d2..3abbc58 100644 --- a/backend-fastapi/db_init.json +++ b/backend-fastapi/db_init.json @@ -6803,6 +6803,51 @@ "sys_dept_id": null } }, + { + "model": "core.menu.model.Menu", + "pk": "ai-agent-admin-account-settings", + "fields": { + "application_id": null, + "is_system": true, + "parent_id": null, + "name": "AccountSettings", + "title": "menu-title.accountSettings", + "authCode": null, + "path": "/user-center/", + "type": "menu", + "component": "/_core/account-settings/index", + "redirect": null, + "activePath": null, + "query": null, + "noBasicLayout": false, + "icon": "lucide:settings-2", + "activeIcon": null, + "order": 34, + "hideInMenu": false, + "hideChildrenInMenu": false, + "hideInBreadcrumb": false, + "hideInTab": false, + "affixTab": false, + "affixTabOrder": null, + "keepAlive": false, + "maxNumOfOpenTab": null, + "fullPathKey": true, + "link": null, + "iframeSrc": null, + "openInNewWindow": false, + "badge": null, + "badgeType": null, + "badgeVariants": null, + "id": "ai-agent-admin-account-settings", + "sort": 34, + "is_deleted": false, + "sys_create_datetime": "2026-06-10T00:00:00.000000", + "sys_update_datetime": "2026-06-10T00:00:00.000000", + "sys_creator_id": null, + "sys_modifier_id": null, + "sys_dept_id": null + } + }, { "model": "core.menu.model.Menu", "pk": "ai-agent-admin-application-management", diff --git a/web/apps/web-ele/src/api/core/api-token.ts b/web/apps/web-ele/src/api/core/api-token.ts new file mode 100644 index 0000000..5663a1e --- /dev/null +++ b/web/apps/web-ele/src/api/core/api-token.ts @@ -0,0 +1,48 @@ +/** + * API Token (Personal Access Token) 管理 + */ +import { requestClient } from '#/api/request'; + +export namespace ApiTokenApi { + export interface TokenItem { + id: string; + name: string; + token_prefix: string; + expires_at?: null | string; + last_used_at?: null | string; + description?: null | string; + is_active: boolean; + sys_create_datetime?: null | string; + } + + export interface CreateTokenRequest { + name: string; + expires_at?: null | string; + description?: string; + } + + export interface CreateTokenResponse { + id: string; + name: string; + token: string; + token_prefix: string; + expires_at?: null | string; + description?: null | string; + sys_create_datetime?: null | string; + } +} + +export async function getApiTokenListApi() { + return requestClient.get('/api/core/api-tokens'); +} + +export async function createApiTokenApi(data: ApiTokenApi.CreateTokenRequest) { + return requestClient.post( + '/api/core/api-tokens', + data, + ); +} + +export async function revokeApiTokenApi(tokenId: string) { + return requestClient.delete(`/api/core/api-tokens/${tokenId}`); +} diff --git a/web/apps/web-ele/src/api/core/device.ts b/web/apps/web-ele/src/api/core/device.ts new file mode 100644 index 0000000..e4e925a --- /dev/null +++ b/web/apps/web-ele/src/api/core/device.ts @@ -0,0 +1,77 @@ +/** + * 设备管理 API + */ +import { requestClient } from '#/api/request'; + +export namespace DeviceApi { + /** 设备信息 */ + export interface DeviceInfo { + device_id: string; + device_name?: string; + device_type?: string; + browser_type?: string; + os_type?: string; + ip_address?: string; + last_active_time?: string; + is_current: boolean; + is_online: boolean; + } + + /** 设备列表响应 */ + export interface DeviceListResponse { + current_device?: DeviceInfo; + online_devices: DeviceInfo[]; + total_count: number; + } + + /** 设备重命名请求 */ + export interface DeviceRenameRequest { + device_name: string; + } + + /** 统计信息响应 */ + export interface StatisticsResponse { + online_count: number; + total_count: number; + } +} + +/** + * 获取设备列表 + */ +export async function getDeviceListApi() { + return requestClient.get('/api/core/devices'); +} + +/** + * 强制登出指定设备 + */ +export async function logoutDeviceApi(deviceId: string) { + return requestClient.delete(`/api/core/devices/${deviceId}`); +} + +/** + * 登出其他所有设备 + */ +export async function logoutOtherDevicesApi() { + return requestClient.delete('/api/core/devices/logout-others'); +} + +/** + * 重命名设备 + */ +export async function renameDeviceApi( + deviceId: string, + data: DeviceApi.DeviceRenameRequest, +) { + return requestClient.post(`/api/core/devices/${deviceId}/rename`, data); +} + +/** + * 获取设备统计信息 + */ +export async function getDeviceStatisticsApi() { + return requestClient.get( + '/api/core/devices/statistics', + ); +} diff --git a/web/apps/web-ele/src/api/core/index.ts b/web/apps/web-ele/src/api/core/index.ts index d84e26d..a8cebc6 100644 --- a/web/apps/web-ele/src/api/core/index.ts +++ b/web/apps/web-ele/src/api/core/index.ts @@ -1,10 +1,12 @@ export * from './announcement'; +export * from './api-token'; export * from './application'; export * from './auth'; export * from './data-source'; export * from './database-connection'; export * from './database-manager'; export * from './dept'; +export * from './device'; export * from './dict'; export * from './file'; export * from './field-permission'; diff --git a/web/apps/web-ele/src/locales/index.ts b/web/apps/web-ele/src/locales/index.ts index 65d9118..356c06a 100644 --- a/web/apps/web-ele/src/locales/index.ts +++ b/web/apps/web-ele/src/locales/index.ts @@ -21,6 +21,8 @@ const elementLocale = ref(defaultLocale); const modules = import.meta.glob([ './langs/*/about.json', + './langs/*/account-settings.json', + './langs/*/api-token.json', './langs/*/ai-platform.json', './langs/*/announcement.json', './langs/*/authentication.json', diff --git a/web/apps/web-ele/src/locales/langs/en-US/account-settings.json b/web/apps/web-ele/src/locales/langs/en-US/account-settings.json new file mode 100644 index 0000000..a384512 --- /dev/null +++ b/web/apps/web-ele/src/locales/langs/en-US/account-settings.json @@ -0,0 +1,3 @@ +{ + "passwordLengthHint": "Password length must be 6-20 characters" +} diff --git a/web/apps/web-ele/src/locales/langs/en-US/api-token.json b/web/apps/web-ele/src/locales/langs/en-US/api-token.json new file mode 100644 index 0000000..119ebe7 --- /dev/null +++ b/web/apps/web-ele/src/locales/langs/en-US/api-token.json @@ -0,0 +1,37 @@ +{ + "title": "API Token", + "deviceManagement": "Device Management", + "description": "Create personal access tokens for automation calls.", + "loadError": "Failed to load tokens", + "nameRequired": "Please enter token name", + "createError": "Failed to create token", + "createToken": "Create Token", + "tokenName": "Token Name", + "tokenNamePlaceholder": "Enter token name", + "expirationDate": "Expiration", + "neverExpiresHint": "Leave empty for no expiration", + "tokenDescription": "Description", + "tokenDescriptionPlaceholder": "Enter description", + "tokenCreated": "Token Created", + "tokenWarning": "Copy and save this token now. It will not be shown again.", + "revokeTitle": "Revoke Token", + "revokeConfirm": "Revoke token \"{0}\"?", + "confirmRevoke": "Revoke", + "revokeSuccess": "Token revoked", + "revokeToken": "Revoke Token", + "copy": "Copy", + "copied": "Copied", + "copySuccess": "Copied to clipboard", + "copyError": "Copy failed", + "neverExpires": "Never expires", + "expired": "Expired", + "expiresSoon": "Expires in {0} days", + "createdAt": "Created", + "lastUsed": "Last used", + "empty": "No tokens", + "days7": "7 days", + "days30": "30 days", + "days60": "60 days", + "days90": "90 days", + "days365": "1 year" +} diff --git a/web/apps/web-ele/src/locales/langs/en-US/menu-title.json b/web/apps/web-ele/src/locales/langs/en-US/menu-title.json index 4cea2db..3023e30 100644 --- a/web/apps/web-ele/src/locales/langs/en-US/menu-title.json +++ b/web/apps/web-ele/src/locales/langs/en-US/menu-title.json @@ -11,6 +11,7 @@ "positionManagement": "Position Management", "roleManagement": "Role Permission", "userCenter": "User Center", + "accountSettings": "Account Settings", "loginLog": "Login Log", "databaseManagement": "Database Management", "dbManagement": "DB Management", diff --git a/web/apps/web-ele/src/locales/langs/zh-CN/account-settings.json b/web/apps/web-ele/src/locales/langs/zh-CN/account-settings.json new file mode 100644 index 0000000..398900a --- /dev/null +++ b/web/apps/web-ele/src/locales/langs/zh-CN/account-settings.json @@ -0,0 +1,3 @@ +{ + "passwordLengthHint": "密码长度需为 6-20 位" +} diff --git a/web/apps/web-ele/src/locales/langs/zh-CN/api-token.json b/web/apps/web-ele/src/locales/langs/zh-CN/api-token.json new file mode 100644 index 0000000..f9d2609 --- /dev/null +++ b/web/apps/web-ele/src/locales/langs/zh-CN/api-token.json @@ -0,0 +1,37 @@ +{ + "title": "API Token", + "deviceManagement": "设备管理", + "description": "创建用于自动化调用的个人访问令牌。", + "loadError": "加载 Token 失败", + "nameRequired": "请输入 Token 名称", + "createError": "创建 Token 失败", + "createToken": "创建 Token", + "tokenName": "Token 名称", + "tokenNamePlaceholder": "请输入 Token 名称", + "expirationDate": "过期时间", + "neverExpiresHint": "不选择则永不过期", + "tokenDescription": "描述", + "tokenDescriptionPlaceholder": "请输入描述", + "tokenCreated": "Token 已创建", + "tokenWarning": "请立即复制保存 Token,关闭后将无法再次查看。", + "revokeTitle": "撤销 Token", + "revokeConfirm": "确定撤销 Token“{0}”吗?", + "confirmRevoke": "确认撤销", + "revokeSuccess": "Token 已撤销", + "revokeToken": "撤销 Token", + "copy": "复制", + "copied": "已复制", + "copySuccess": "已复制到剪贴板", + "copyError": "复制失败", + "neverExpires": "永不过期", + "expired": "已过期", + "expiresSoon": "{0} 天后过期", + "createdAt": "创建时间", + "lastUsed": "最后使用", + "empty": "暂无 Token", + "days7": "7 天", + "days30": "30 天", + "days60": "60 天", + "days90": "90 天", + "days365": "1 年" +} diff --git a/web/apps/web-ele/src/locales/langs/zh-CN/menu-title.json b/web/apps/web-ele/src/locales/langs/zh-CN/menu-title.json index c462e95..07b879e 100644 --- a/web/apps/web-ele/src/locales/langs/zh-CN/menu-title.json +++ b/web/apps/web-ele/src/locales/langs/zh-CN/menu-title.json @@ -11,6 +11,7 @@ "positionManagement": "岗位管理", "roleManagement": "角色权限", "userCenter": "用户中心", + "accountSettings": "账号设置", "loginLog": "登录日志", "databaseManagement": "数据库管理", "dbManagement": "DB管理", diff --git a/web/apps/web-ele/src/locales/langs/zh-TW/account-settings.json b/web/apps/web-ele/src/locales/langs/zh-TW/account-settings.json new file mode 100644 index 0000000..c27b97d --- /dev/null +++ b/web/apps/web-ele/src/locales/langs/zh-TW/account-settings.json @@ -0,0 +1,3 @@ +{ + "passwordLengthHint": "密碼長度需為 6-20 位" +} diff --git a/web/apps/web-ele/src/locales/langs/zh-TW/api-token.json b/web/apps/web-ele/src/locales/langs/zh-TW/api-token.json new file mode 100644 index 0000000..387c3a1 --- /dev/null +++ b/web/apps/web-ele/src/locales/langs/zh-TW/api-token.json @@ -0,0 +1,37 @@ +{ + "title": "API Token", + "deviceManagement": "設備管理", + "description": "建立用於自動化調用的個人訪問令牌。", + "loadError": "載入 Token 失敗", + "nameRequired": "請輸入 Token 名稱", + "createError": "建立 Token 失敗", + "createToken": "建立 Token", + "tokenName": "Token 名稱", + "tokenNamePlaceholder": "請輸入 Token 名稱", + "expirationDate": "過期時間", + "neverExpiresHint": "不選擇則永不過期", + "tokenDescription": "描述", + "tokenDescriptionPlaceholder": "請輸入描述", + "tokenCreated": "Token 已建立", + "tokenWarning": "請立即複製保存 Token,關閉後將無法再次查看。", + "revokeTitle": "撤銷 Token", + "revokeConfirm": "確定撤銷 Token「{0}」嗎?", + "confirmRevoke": "確認撤銷", + "revokeSuccess": "Token 已撤銷", + "revokeToken": "撤銷 Token", + "copy": "複製", + "copied": "已複製", + "copySuccess": "已複製到剪貼簿", + "copyError": "複製失敗", + "neverExpires": "永不過期", + "expired": "已過期", + "expiresSoon": "{0} 天後過期", + "createdAt": "建立時間", + "lastUsed": "最後使用", + "empty": "暫無 Token", + "days7": "7 天", + "days30": "30 天", + "days60": "60 天", + "days90": "90 天", + "days365": "1 年" +} diff --git a/web/apps/web-ele/src/locales/langs/zh-TW/menu-title.json b/web/apps/web-ele/src/locales/langs/zh-TW/menu-title.json index 3d8915c..89eeddb 100644 --- a/web/apps/web-ele/src/locales/langs/zh-TW/menu-title.json +++ b/web/apps/web-ele/src/locales/langs/zh-TW/menu-title.json @@ -11,6 +11,7 @@ "positionManagement": "崗位管理", "roleManagement": "角色權限", "userCenter": "用戶中心", + "accountSettings": "賬號設置", "loginLog": "登錄日誌", "databaseManagement": "數據庫管理", "dbManagement": "DB管理", diff --git a/web/apps/web-ele/src/router/access.ts b/web/apps/web-ele/src/router/access.ts index 43a1e8f..742f90f 100644 --- a/web/apps/web-ele/src/router/access.ts +++ b/web/apps/web-ele/src/router/access.ts @@ -19,6 +19,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) { '../views/_core/ai-confirmation-center/index.vue', '../views/_core/announcement/index.vue', '../views/_core/announcement/list.vue', + '../views/_core/account-settings/index.vue', '../views/_core/application/index.vue', '../views/_core/authentication/login.vue', '../views/_core/authentication/oauth-callback.vue', diff --git a/web/apps/web-ele/src/views/_core/account-settings/api-token-management.vue b/web/apps/web-ele/src/views/_core/account-settings/api-token-management.vue new file mode 100644 index 0000000..cd7072d --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/api-token-management.vue @@ -0,0 +1,330 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/account-settings/data.ts b/web/apps/web-ele/src/views/_core/account-settings/data.ts new file mode 100644 index 0000000..5886178 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/data.ts @@ -0,0 +1,165 @@ +import type { VbenFormSchema } from '#/adapter/form'; + +import { $t } from '@vben/locales'; + +import { z } from '#/adapter/form'; + +/** + * 获取性别选项 + */ +export function getGenderOptions() { + return [ + { label: $t('user.unknown'), value: 0 }, + { label: $t('user.male'), value: 1 }, + { label: $t('user.female'), value: 2 }, + ]; +} + +/** + * 获取基本信息表单配置 + */ +export function getProfileFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'name', + label: $t('user.userName'), + rules: z + .string() + .min(2, $t('ui.formRules.minLength', [$t('user.userName'), 2])) + .max(64, $t('ui.formRules.maxLength', [$t('user.userName'), 64])) + .optional() + .or(z.literal('')), + }, + { + component: 'ImageSelector', + componentProps: { + enableCrop: true, + cropShape: 'circle', + maxSize: 2, + placeholder: $t('user.selectAvatar'), + }, + fieldName: 'avatar', + label: $t('user.avatar'), + help: $t('user.avatarHelp'), + }, + { + component: 'Input', + fieldName: 'email', + label: $t('user.email'), + rules: z + .string() + .email($t('user.emailFormatError')) + .max(255, $t('ui.formRules.maxLength', [$t('user.email'), 255])) + .optional() + .or(z.literal('')), + }, + { + component: 'Input', + fieldName: 'mobile', + label: $t('user.mobile'), + rules: z + .string() + .regex(/^1[3-9]\d{9}$/, $t('user.mobileFormatError')) + .optional() + .or(z.literal('')), + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: getGenderOptions(), + isButton: true, + }, + defaultValue: 0, + fieldName: 'gender', + label: $t('user.gender'), + }, + { + component: 'DatePicker', + componentProps: { + placeholder: $t('user.selectBirthday'), + valueFormat: 'YYYY-MM-DD', + }, + fieldName: 'birthday', + label: $t('user.birthday'), + }, + { + component: 'Input', + fieldName: 'city', + label: $t('user.city'), + rules: z + .string() + .max(100, $t('ui.formRules.maxLength', [$t('user.city'), 100])) + .optional() + .or(z.literal('')), + }, + { + component: 'Input', + fieldName: 'address', + label: $t('user.address'), + rules: z + .string() + .max(200, $t('ui.formRules.maxLength', [$t('user.address'), 200])) + .optional() + .or(z.literal('')), + }, + { + component: 'Textarea', + componentProps: { + placeholder: $t('user.bioPlaceholder'), + rows: 3, + }, + fieldName: 'bio', + label: $t('user.bio'), + }, + ]; +} + +/** + * 获取密码修改表单配置 + */ +export function getPasswordFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + componentProps: { + type: 'password', + showPassword: true, + placeholder: $t('user.oldPasswordPlaceholder'), + }, + fieldName: 'old_password', + label: $t('user.oldPassword'), + rules: z + .string() + .min(1, $t('ui.formRules.required', [$t('user.oldPassword')])), + }, + { + component: 'Input', + componentProps: { + type: 'password', + showPassword: true, + placeholder: $t('user.newPasswordPlaceholder'), + }, + fieldName: 'new_password', + label: $t('user.newPassword'), + rules: z + .string() + .min(6, $t('ui.formRules.minLength', [$t('user.newPassword'), 6])) + .max(20, $t('ui.formRules.maxLength', [$t('user.newPassword'), 20])), + }, + { + component: 'Input', + componentProps: { + type: 'password', + showPassword: true, + placeholder: $t('user.confirmPasswordPlaceholder'), + }, + fieldName: 'confirm_password', + label: $t('user.confirmPassword'), + rules: z + .string() + .min(1, $t('ui.formRules.required', [$t('user.confirmPassword')])), + }, + ]; +} diff --git a/web/apps/web-ele/src/views/_core/account-settings/device-item.vue b/web/apps/web-ele/src/views/_core/account-settings/device-item.vue new file mode 100644 index 0000000..5973798 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/device-item.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/account-settings/device-management.vue b/web/apps/web-ele/src/views/_core/account-settings/device-management.vue new file mode 100644 index 0000000..571d579 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/device-management.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/account-settings/index.vue b/web/apps/web-ele/src/views/_core/account-settings/index.vue new file mode 100644 index 0000000..2eb2fb2 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/index.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/account-settings/modules/password-form.vue b/web/apps/web-ele/src/views/_core/account-settings/modules/password-form.vue new file mode 100644 index 0000000..61dc6f2 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/modules/password-form.vue @@ -0,0 +1,168 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/account-settings/modules/profile-form.vue b/web/apps/web-ele/src/views/_core/account-settings/modules/profile-form.vue new file mode 100644 index 0000000..c379ebc --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/modules/profile-form.vue @@ -0,0 +1,293 @@ + + +