From 9f7f6c19e59c388cfc5f91bb13bff2cb7b5f388d Mon Sep 17 00:00:00 2001 From: Boen_Shi Date: Wed, 17 Jun 2026 23:19:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=94=A8=E6=88=B7=E7=BB=91=E5=AE=9A):=20?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=94=A8=E6=88=B7=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/servers.ts | 15 +++ src/api/users.ts | 3 + src/axios.ts | 10 +- src/pages/ServersPage.vue | 194 +++++++++++++++++++++++++++++++++++- src/pages/UsersPage.vue | 202 ++++++++++++++++++++++++++++---------- src/router/index.ts | 4 +- 6 files changed, 372 insertions(+), 56 deletions(-) diff --git a/src/api/servers.ts b/src/api/servers.ts index 7c6c596..dadbc7a 100644 --- a/src/api/servers.ts +++ b/src/api/servers.ts @@ -34,6 +34,21 @@ export const serversApi = { updateBoundSystemUserPassword(id: number, data: Record) { return request.patch(`/servers/${id}/bound-system-user/password`, data) }, + updateDefaultEnvironment(id: number, content: string) { + return request.put(`/servers/${id}/default-environment`, { content }) + }, + updateAllUserEnvironments(id: number, content: string) { + return request.put(`/servers/${id}/system-users/environment`, { content }) + }, + updateDefaultUserGroups(id: number, groups: string[]) { + return request.put(`/servers/${id}/default-user-groups`, { groups }) + }, + userEnvironment(id: number, username: string) { + return request.get(`/servers/${id}/system-users/${encodeURIComponent(username)}/environment`) + }, + updateUserEnvironment(id: number, username: string, content: string) { + return request.put(`/servers/${id}/system-users/${encodeURIComponent(username)}/environment`, { content }) + }, createSystemGroup(id: number, data: Record) { return request.post(`/servers/${id}/system-groups`, data) }, diff --git a/src/api/users.ts b/src/api/users.ts index cb4836e..dd73387 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -25,6 +25,9 @@ export const usersApi = { syncServerBindings(id: number, serverBindings: Array>) { return request.put(`/users/${id}/server-bindings`, { server_bindings: serverBindings }) }, + checkServerUser(params: { server_resource_id: number; username: string }) { + return request.get('/users/server-bindings/check', { params }) + }, importUsers(file: File) { const formData = new FormData() formData.append('file', file) diff --git a/src/axios.ts b/src/axios.ts index 241bfdd..3364790 100644 --- a/src/axios.ts +++ b/src/axios.ts @@ -17,6 +17,8 @@ const service: AxiosInstance = axios.create({ }, }) +let authExpiredMessageShown = false + service.interceptors.request.use((config) => { const token = getToken() const requestUrl = String(config.url || '') @@ -36,7 +38,13 @@ service.interceptors.response.use( if (status === 401) { removeToken() - ElMessage.error('登录已过期,请重新登录') + if (!authExpiredMessageShown) { + authExpiredMessageShown = true + ElMessage.error('登录已过期,请重新登录') + window.setTimeout(() => { + authExpiredMessageShown = false + }, 1500) + } if (location.hash !== '#/login') { location.href = '/#/login' } diff --git a/src/pages/ServersPage.vue b/src/pages/ServersPage.vue index c826e2b..e759e1e 100644 --- a/src/pages/ServersPage.vue +++ b/src/pages/ServersPage.vue @@ -31,6 +31,9 @@ 添加资源 分配用户权限 管理用户及用户组 + 默认用户组 + 默认环境变量 + 设置所有用户变量 删除服务器 @@ -143,11 +146,12 @@ - + @@ -202,6 +206,20 @@ + + + + + + + + + + + @@ -260,6 +278,18 @@ 提交 + + + + + + + + + @@ -381,6 +411,8 @@ const systemUsersDialogVisible = ref(false) const systemUserFormVisible = ref(false) const passwordDialogVisible = ref(false) const boundPasswordDialogVisible = ref(false) +const environmentDialogVisible = ref(false) +const defaultUserGroupsDialogVisible = ref(false) const protocolDialogVisible = ref(false) const softwareDialogVisible = ref(false) const softwareFormDialogVisible = ref(false) @@ -390,6 +422,10 @@ const copyingTempPassword = ref(false) const updatingBoundPassword = ref(false) const systemUsersLoading = ref(false) const systemUsersSaving = ref(false) +const environmentLoading = ref(false) +const environmentSaving = ref(false) +const defaultUserGroupsLoading = ref(false) +const defaultUserGroupsSaving = ref(false) const savingSystemUserGroups = reactive>({}) const opsLoading = ref(false) const opsSaving = ref(false) @@ -408,27 +444,42 @@ const useTargetRow = ref(null) const systemUsersServer = ref(null) const passwordTarget = ref(null) const boundPasswordTarget = ref(null) +const environmentTarget = ref(null) +const defaultUserGroupsTarget = ref(null) const opsProtocols = ref([]) const softwareProtocol = ref(null) const opsLinkLoading = ref>({}) const opsSavedSelections = reactive>({}) const typeOptions = [{ label: '服务器', value: 'server' }, { label: '资源', value: 'resource' }] -const form = reactive({ type: 'server', name: '', display_name: '', parent_id: null, internal_ip: '', user_api_base_url: '', user_api_token: '', asset_id: null, account_id: null, protocol: '', allow_copy_temp_password: false, description: '', is_active: true }) +const form = reactive({ type: 'server', name: '', display_name: '', parent_id: null, internal_ip: '', user_api_base_url: '', user_api_token: '', default_user_groups: [], asset_id: null, account_id: null, protocol: '', allow_copy_temp_password: false, description: '', is_active: true }) const systemUserForm = reactive({ username: '', password: '', groups: [], groupname: '' }) const useForm = reactive({ protocol: 'SSH', account_name: '', password: '', remember: false, last_temp_password: '' }) const passwordForm = reactive({ password: '' }) const boundPasswordForm = reactive({ password: '' }) +const environmentForm = reactive({ mode: 'default', content: '', username: '' }) +const defaultUserGroupsForm = reactive({ groups: [] }) const protocolForm = reactive({ name: '', description: '' }) const softwareForm = reactive({ name: '', is_active: true }) const opsSelections = reactive>({}) const systemUserGroupSelections = reactive>({}) const systemUsers = ref([]) const systemGroups = ref([]) +const defaultUserGroupsOptions = ref([]) const systemUserBindings = ref([]) const systemUsersTab = ref<'users' | 'groups'>('users') const systemUserFormMode = ref<'user' | 'group'>('user') const permissionDialogTitle = computed(() => (permissionMode.value === 'server_assign' ? '服务器资源用户权限分配' : '资源已有用户权限修改')) +const environmentDialogTitle = computed(() => { + if (environmentForm.mode === 'all') { + return `设置所有用户变量 - ${environmentTarget.value?.display_name || environmentTarget.value?.name || ''}` + } + if (environmentForm.mode === 'user') { + return `修改环境变量 - ${environmentForm.username || ''}` + } + + return `默认环境变量 - ${environmentTarget.value?.display_name || environmentTarget.value?.name || ''}` +}) const opsSaveButtonLabel = computed(() => (forceSaveOpsReady.value ? '强制保存' : '保存我的软件选择')) const resourceProtocolOptions = computed(() => { return opsProtocols.value @@ -469,6 +520,9 @@ function resetForm(): void { internal_ip: '', user_api_base_url: '', user_api_token: '', + default_environment_variables: '', + all_user_environment_variables: '', + default_user_groups: [], asset_id: null, account_id: null, protocol: resourceProtocolOptions.value[0] || '', @@ -515,6 +569,9 @@ function openEdit(row: any): void { internal_ip: row.internal_ip, user_api_base_url: row.user_api_base_url || '', user_api_token: '', + default_environment_variables: row.default_environment_variables || '', + all_user_environment_variables: row.all_user_environment_variables || '', + default_user_groups: [...(row.default_user_groups || [])], asset_id: row.asset_id, account_id: row.account_id, protocol: row.protocols?.[0] || 'SSH', @@ -544,6 +601,9 @@ async function submit(): Promise { internal_ip: form.type === 'server' ? form.internal_ip : null, user_api_base_url: form.type === 'server' ? form.user_api_base_url || null : null, user_api_token: form.type === 'server' ? form.user_api_token || null : null, + default_environment_variables: form.type === 'server' ? form.default_environment_variables || '' : null, + all_user_environment_variables: form.type === 'server' ? form.all_user_environment_variables || '' : null, + default_user_groups: form.type === 'server' ? form.default_user_groups || [] : [], asset_id: form.type === 'server' ? form.asset_id : null, account_id: form.type === 'resource' ? form.account_id : null, protocol: form.type === 'resource' ? form.protocol : null, @@ -689,7 +749,7 @@ function openUseResourceDialog(row: any): void { const serverId = Number(row.parent_id || row.id || 0) const saved = getServerCredential(currentUserId.value, serverId) useForm.protocol = row.protocols?.[0] || 'SSH' - useForm.account_name = row.server_username || saved?.account_name || '' + useForm.account_name = saved?.account_name || row.server_username || '' useForm.password = saved?.password || '' useForm.remember = Boolean(saved) useForm.last_temp_password = '' @@ -722,6 +782,7 @@ async function submitBoundPassword(): Promise { useForm.password = boundPasswordForm.password } ElMessage.success('绑定账号密码已更新') + await fetchList() } catch (error: any) { const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null ElMessage.error(first || error?.message || '修改绑定账号密码失败') @@ -730,6 +791,121 @@ async function submitBoundPassword(): Promise { } } +async function openDefaultUserGroupsDialog(server: any): Promise { + defaultUserGroupsTarget.value = server + defaultUserGroupsOptions.value = [] + defaultUserGroupsForm.groups = [...(server.default_user_groups || [])] + defaultUserGroupsDialogVisible.value = true + defaultUserGroupsLoading.value = true + try { + const response: any = await serversApi.systemUsersMeta(server.id) + defaultUserGroupsOptions.value = response.data.groups || [] + } catch (error: any) { + const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null + ElMessage.error(first || error?.message || '加载服务器用户组失败') + } finally { + defaultUserGroupsLoading.value = false + } +} + +async function submitDefaultUserGroups(): Promise { + if (!defaultUserGroupsTarget.value?.id) { + return + } + defaultUserGroupsSaving.value = true + try { + const groups = Array.from(new Set((defaultUserGroupsForm.groups || []).map((group: string) => String(group)))) + const response: any = await serversApi.updateDefaultUserGroups(defaultUserGroupsTarget.value.id, groups) + defaultUserGroupsTarget.value.default_user_groups = response.data.default_user_groups || groups + defaultUserGroupsDialogVisible.value = false + ElMessage.success('默认用户组已保存') + await fetchList() + } catch (error: any) { + const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null + ElMessage.error(first || error?.message || '保存默认用户组失败') + } finally { + defaultUserGroupsSaving.value = false + } +} + +function openDefaultEnvironmentDialog(server: any): void { + environmentTarget.value = server + Object.assign(environmentForm, { + mode: 'default', + username: '', + content: server.default_environment_variables || '', + }) + environmentDialogVisible.value = true +} + +function openAllUserEnvironmentDialog(server: any): void { + environmentTarget.value = server + Object.assign(environmentForm, { + mode: 'all', + username: '', + content: server.all_user_environment_variables || '', + }) + environmentDialogVisible.value = true +} + +async function openUserEnvironmentDialog(row: any): Promise { + if (!systemUsersServer.value?.id) { + return + } + environmentTarget.value = systemUsersServer.value + Object.assign(environmentForm, { + mode: 'user', + username: row.username, + content: '', + }) + environmentDialogVisible.value = true + environmentLoading.value = true + try { + const response: any = await serversApi.userEnvironment(systemUsersServer.value.id, row.username) + environmentForm.content = response.data.content || '' + } catch (error: any) { + const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null + ElMessage.error(first || error?.message || '读取环境变量失败') + } finally { + environmentLoading.value = false + } +} + +async function submitEnvironment(): Promise { + if (!environmentTarget.value?.id) { + return + } + environmentSaving.value = true + try { + if (environmentForm.mode === 'all') { + const response: any = await serversApi.updateAllUserEnvironments(environmentTarget.value.id, environmentForm.content || '') + const result = response.data || {} + environmentTarget.value.all_user_environment_variables = result.all_user_environment_variables || environmentForm.content || '' + const failedUsers = result.failed_users || [] + if (failedUsers.length) { + const failedNames = failedUsers.map((item: any) => item.username || item.message || item.code).filter(Boolean).join('、') + ElMessage.warning(`已设置 ${result.updated_count || 0} 个用户,${result.failed_count || failedUsers.length} 个失败:${failedNames}`) + } else { + ElMessage.success(`已设置所有用户变量,共 ${result.updated_count || 0} 个用户`) + } + } else if (environmentForm.mode === 'user') { + await serversApi.updateUserEnvironment(environmentTarget.value.id, environmentForm.username, environmentForm.content || '') + ElMessage.success('用户环境变量已更新') + await loadSystemUsersMeta() + } else { + await serversApi.updateDefaultEnvironment(environmentTarget.value.id, environmentForm.content || '') + ElMessage.success('默认环境变量已保存') + await fetchList() + } + environmentDialogVisible.value = false + } catch (error: any) { + const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null + ElMessage.error(first || error?.message || '保存环境变量失败') + } finally { + environmentSaving.value = false + } +} + async function openSystemUsersDialog(server: any): Promise { systemUsersServer.value = server systemUsersTab.value = 'users' @@ -769,7 +945,7 @@ function bindingLabel(username: string): string { function openCreateSystemUser(): void { systemUserFormMode.value = 'user' - Object.assign(systemUserForm, { username: '', password: '', groups: [], groupname: '' }) + Object.assign(systemUserForm, { username: '', password: '', groups: [...(systemUsersServer.value?.default_user_groups || [])], groupname: '' }) systemUserFormVisible.value = true } @@ -920,6 +1096,16 @@ async function requestUseResource(action: 'connect' | 'copy'): Promise<{ url: st tempPassword: String(response?.data?.temp_password || ''), } } catch (error: any) { + if (Number(error?.code || 0) === 423 && error?.data?.reason === 'server_password_change_required') { + boundPasswordTarget.value = { + id: error.data.server_resource_id, + server_username: error.data.username, + } + boundPasswordForm.password = '' + boundPasswordDialogVisible.value = true + ElMessage.warning(error?.message || '请先修改服务器账号密码') + return null + } const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null ElMessage.error(first || error?.message || (action === 'copy' ? '获取临时密码失败' : '资源访问失败')) return null diff --git a/src/pages/UsersPage.vue b/src/pages/UsersPage.vue index 7a11fed..9b98759 100644 --- a/src/pages/UsersPage.vue +++ b/src/pages/UsersPage.vue @@ -61,23 +61,29 @@ 服务器账号绑定 - - - - - - + - + - - + + - - + + - - + + @@ -87,6 +93,25 @@ + + + + + + + + + + +
@@ -163,7 +188,7 @@