feat(用户绑定): 完善用户绑定

This commit is contained in:
Boen_Shi 2026-06-17 23:19:33 +08:00
parent bb17029279
commit 9f7f6c19e5
6 changed files with 372 additions and 56 deletions

View File

@ -34,6 +34,21 @@ export const serversApi = {
updateBoundSystemUserPassword(id: number, data: Record<string, unknown>) {
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<string, unknown>) {
return request.post(`/servers/${id}/system-groups`, data)
},

View File

@ -25,6 +25,9 @@ export const usersApi = {
syncServerBindings(id: number, serverBindings: Array<Record<string, unknown>>) {
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)

View File

@ -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()
if (!authExpiredMessageShown) {
authExpiredMessageShown = true
ElMessage.error('登录已过期,请重新登录')
window.setTimeout(() => {
authExpiredMessageShown = false
}, 1500)
}
if (location.hash !== '#/login') {
location.href = '/#/login'
}

View File

@ -31,6 +31,9 @@
<el-button size='small' type='primary' plain @click='openCreateResource(server.id)'>添加资源</el-button>
<el-button size='small' type='success' plain @click='openServerPermissionDialog(server)'>分配用户权限</el-button>
<el-button size='small' type='warning' plain @click='openSystemUsersDialog(server)'>管理用户及用户组</el-button>
<el-button size='small' plain @click='openDefaultUserGroupsDialog(server)'>默认用户组</el-button>
<el-button size='small' plain @click='openDefaultEnvironmentDialog(server)'>默认环境变量</el-button>
<el-button size='small' plain @click='openAllUserEnvironmentDialog(server)'>设置所有用户变量</el-button>
<el-button size='small' type='danger' @click='removeRow(server)'>删除服务器</el-button>
</div>
@ -143,11 +146,12 @@
<el-table-column label='绑定SSO用户' min-width='180'>
<template #default='{ row }'>{{ bindingLabel(row.username) }}</template>
</el-table-column>
<el-table-column label='操作' width='260' fixed='right'>
<el-table-column label='操作' width='420' fixed='right'>
<template #default='{ row }'>
<div class='btn-gap-8 btn-gap-8--nowrap'>
<el-button size='small' type='primary' plain :loading='savingSystemUserGroups[row.username]' @click='saveSystemUserGroups(row)'>保存组</el-button>
<el-button size='small' @click='openPasswordDialog(row)'>改密码</el-button>
<el-button size='small' @click='openUserEnvironmentDialog(row)'>修改环境变量</el-button>
<el-button size='small' type='danger' @click='removeSystemUser(row)'>删除</el-button>
</div>
</template>
@ -202,6 +206,20 @@
</template>
</el-dialog>
<el-dialog v-model='defaultUserGroupsDialogVisible' :title='`默认用户组 - ${defaultUserGroupsTarget?.display_name || defaultUserGroupsTarget?.name || ""}`' width='520px'>
<el-form v-loading='defaultUserGroupsLoading' :model='defaultUserGroupsForm' label-width='95px'>
<el-form-item label='用户组'>
<el-select v-model='defaultUserGroupsForm.groups' multiple collapse-tags clearable filterable class='w-full'>
<el-option v-for='group in defaultUserGroupsOptions' :key='group.groupname' :label='group.groupname' :value='group.groupname' />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click='defaultUserGroupsDialogVisible = false'>取消</el-button>
<el-button type='primary' :loading='defaultUserGroupsSaving' :disabled='defaultUserGroupsLoading' @click='submitDefaultUserGroups'>保存</el-button>
</template>
</el-dialog>
<el-dialog v-model='permissionDialogVisible' :title='permissionDialogTitle' width='1100px'>
<el-table :data='permissionMatrixRows' max-height='520' border>
<el-table-column prop='nickname' label='用户名' min-width='150' fixed='left' />
@ -260,6 +278,18 @@
<el-button type='primary' :loading='updatingBoundPassword' @click='submitBoundPassword'>提交</el-button>
</template>
</el-dialog>
<el-dialog v-model='environmentDialogVisible' :title='environmentDialogTitle' width='640px'>
<el-form v-loading='environmentLoading' :model='environmentForm' label-width='110px'>
<el-form-item label='环境变量'>
<el-input v-model='environmentForm.content' type='textarea' :rows='10' :disabled='environmentLoading' placeholder='例如export PATH=/opt/bin:$PATH' />
</el-form-item>
</el-form>
<template #footer>
<el-button @click='environmentDialogVisible = false'>取消</el-button>
<el-button type='primary' :loading='environmentSaving' :disabled='environmentLoading' @click='submitEnvironment'>保存</el-button>
</template>
</el-dialog>
</el-card>
<el-card v-loading='opsLoading' style="margin-top: 1rem;">
@ -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<Record<string, boolean>>({})
const opsLoading = ref(false)
const opsSaving = ref(false)
@ -408,27 +444,42 @@ const useTargetRow = ref<any | null>(null)
const systemUsersServer = ref<any | null>(null)
const passwordTarget = ref<any | null>(null)
const boundPasswordTarget = ref<any | null>(null)
const environmentTarget = ref<any | null>(null)
const defaultUserGroupsTarget = ref<any | null>(null)
const opsProtocols = ref<any[]>([])
const softwareProtocol = ref<any | null>(null)
const opsLinkLoading = ref<Record<number, boolean>>({})
const opsSavedSelections = reactive<Record<string, number | null>>({})
const typeOptions = [{ label: '服务器', value: 'server' }, { label: '资源', value: 'resource' }]
const form = reactive<any>({ 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<any>({ 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<any>({ username: '', password: '', groups: [], groupname: '' })
const useForm = reactive<any>({ protocol: 'SSH', account_name: '', password: '', remember: false, last_temp_password: '' })
const passwordForm = reactive<any>({ password: '' })
const boundPasswordForm = reactive<any>({ password: '' })
const environmentForm = reactive<any>({ mode: 'default', content: '', username: '' })
const defaultUserGroupsForm = reactive<any>({ groups: [] })
const protocolForm = reactive<any>({ name: '', description: '' })
const softwareForm = reactive<any>({ name: '', is_active: true })
const opsSelections = reactive<Record<string, number | null>>({})
const systemUserGroupSelections = reactive<Record<string, string[]>>({})
const systemUsers = ref<any[]>([])
const systemGroups = ref<any[]>([])
const defaultUserGroupsOptions = ref<any[]>([])
const systemUserBindings = ref<any[]>([])
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<string[]>(() => {
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<void> {
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<void> {
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<void> {
}
}
async function openDefaultUserGroupsDialog(server: any): Promise<void> {
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<void> {
if (!defaultUserGroupsTarget.value?.id) {
return
}
defaultUserGroupsSaving.value = true
try {
const groups = Array.from(new Set<string>((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<void> {
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<void> {
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<void> {
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

View File

@ -61,23 +61,29 @@
</el-select>
</el-form-item>
<el-divider>服务器账号绑定</el-divider>
<el-form-item label='服务器'>
<el-select v-model='form.server_ids' multiple collapse-tags clearable class='w-full' @change='syncServerBindingRows'>
<el-option v-for='item in serverOptions' :key='item.id' :label='item.display_name || item.name' :value='item.id' />
</el-select>
</el-form-item>
<el-table v-if='form.server_bindings.length' :data='form.server_bindings' border size='small' class='mb-3'>
<el-table :data='serverBindingRows' border size='small' class='mb-3'>
<el-table-column label='服务器' min-width='160'>
<template #default='{ row }'>{{ serverLabel(row.server_resource_id) }}</template>
<template #default='{ row }'>{{ row.server_label }}</template>
</el-table-column>
<el-table-column label='服务器用户名' min-width='160'>
<template #default='{ row }'><el-input v-model='row.username' /></template>
<el-table-column label='绑定账号' min-width='160'>
<template #default='{ row }'>{{ row.username || '-' }}</template>
</el-table-column>
<el-table-column label='需要创建用户' width='130'>
<template #default='{ row }'><el-switch v-model='row.create_remote' /></template>
<el-table-column label='状态' min-width='150'>
<template #default='{ row }'>
<el-tag v-if='row.action === "bind"' type='warning'>待绑定</el-tag>
<el-tag v-else-if='row.action === "unbind"' type='info'>待解绑</el-tag>
<el-tag v-else-if='row.username' type='success'>已绑定</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label='账号密码' min-width='160'>
<template #default='{ row }'><el-input v-model='row.password' type='password' show-password placeholder='默认使用用户密码' /></template>
<el-table-column label='操作' width='160'>
<template #default='{ row }'>
<div class='btn-gap-8 btn-gap-8--nowrap'>
<el-button v-if='row.action === "unbind"' size='small' @click='cancelUnbind(row)'>撤销</el-button>
<el-button v-else-if='row.username' size='small' type='danger' plain @click='openUnbindDialog(row)'>解绑</el-button>
<el-button v-else size='small' type='primary' plain @click='openBindDialog(row)'>绑定</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-form>
@ -87,6 +93,25 @@
</template>
</el-dialog>
<el-dialog v-model='bindDialogVisible' :title='`绑定服务器账号 - ${bindingTarget?.server_label || ""}`' width='520px'>
<el-form :model='bindForm' label-width='95px'>
<el-form-item label='绑定账号'>
<el-input v-model='bindForm.username' placeholder='请输入服务器账号' @blur='checkBindUsername' />
</el-form-item>
<el-alert v-if='bindForm.checked && bindForm.remote_exists' type='success' :closable='false' show-icon title='服务器上已存在该用户,保存后将直接绑定。' />
<template v-if='bindForm.checked && !bindForm.remote_exists'>
<el-alert type='warning' :closable='false' show-icon title='服务器上不存在该用户,保存时会新建并绑定。' class='mb-3' />
<el-form-item label='初始密码'>
<el-input v-model='bindForm.password' type='password' show-password placeholder='默认123456可不填' />
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click='bindDialogVisible = false'>取消</el-button>
<el-button type='primary' :loading='checkingServerUser' @click='confirmBind'>确定</el-button>
</template>
</el-dialog>
<el-dialog v-model='permissionDialogVisible' title='用户直授权限分配' width='92vw' top='4vh'>
<div class='permission-panel-wrap'>
<el-cascader-panel v-model='selectedPermissionNodes' :options='permissionCascader' :props='cascaderProps' class='permission-panel' />
@ -163,7 +188,7 @@
<script setup lang='ts'>
import { UploadFilled } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { permissionsApi } from '@/api/permissions'
import { rolesApi } from '@/api/roles'
import { serversApi } from '@/api/servers'
@ -178,6 +203,7 @@ const loading = ref(false)
const saving = ref(false)
const savingPermissions = ref(false)
const dialogVisible = ref(false)
const bindDialogVisible = ref(false)
const permissionDialogVisible = ref(false)
const importDialogVisible = ref(false)
const batchAssignDialogVisible = ref(false)
@ -185,11 +211,14 @@ const editingId = ref<number | null>(null)
const permissionTargetUserId = ref<number | null>(null)
const importing = ref(false)
const batchAssigning = ref(false)
const checkingServerUser = ref(false)
const selectedUserIds = ref<number[]>([])
const rows = ref<any[]>([])
const roleOptions = ref<any[]>([])
const permissionOptions = ref<any[]>([])
const serverOptions = ref<any[]>([])
const serverBindingRows = ref<any[]>([])
const bindingTarget = ref<any | null>(null)
const selectedPermissionNodes = ref<Array<number | string>>([])
const batchAssignPermissionNodes = ref<Array<number | string>>([])
const importFile = ref<File | null>(null)
@ -200,7 +229,8 @@ const perPage = ref(20)
const sortBy = ref('created_at')
const sortOrder = ref<'asc' | 'desc'>('desc')
const form = reactive<any>({ nickname: '', email: '', phone: '', password: '', force_password_change: false, role_ids: [], server_ids: [], server_bindings: [] })
const form = reactive<any>({ nickname: '', email: '', phone: '', password: '', force_password_change: false, role_ids: [] })
const bindForm = reactive<any>({ username: '', password: '', checked: false, remote_exists: false })
const batchAssignForm = reactive<{ role_ids: number[] }>({ role_ids: [] })
const cascaderProps = { multiple: true, emitPath: false, checkStrictly: false }
const permissionCascader = computed(() => buildPermissionCascader(permissionOptions.value))
@ -281,7 +311,8 @@ function handleSortChange(payload: { prop: string, order: 'ascending' | 'descend
function openCreate(): void {
editingId.value = null
Object.assign(form, { nickname: '', email: '', phone: '', password: '', force_password_change: false, role_ids: [], server_ids: [], server_bindings: [] })
Object.assign(form, { nickname: '', email: '', phone: '', password: '', force_password_change: false, role_ids: [] })
resetServerBindingRows([])
dialogVisible.value = true
}
@ -294,14 +325,8 @@ function openEdit(row: any): void {
password: '',
force_password_change: Boolean(row.force_password_change),
role_ids: (row.roles || []).map((item: any) => item.id),
server_ids: (row.server_user_bindings || []).map((item: any) => item.server_resource_id),
server_bindings: (row.server_user_bindings || []).map((item: any) => ({
server_resource_id: item.server_resource_id,
username: item.username,
create_remote: false,
password: '',
})),
})
resetServerBindingRows(row.server_user_bindings || [])
dialogVisible.value = true
}
@ -315,16 +340,19 @@ async function submit(): Promise<void> {
saving.value = true
try {
const payload: any = { ...form }
payload.server_bindings = form.server_bindings.map((item: any) => ({
payload.server_bindings = serverBindingRows.value.filter((item: any) => item.action === 'bind').map((item: any) => ({
server_resource_id: item.server_resource_id,
username: item.username,
create_remote: Boolean(item.create_remote),
password: item.password || undefined,
}))
payload.server_unbindings = serverBindingRows.value.filter((item: any) => item.action === 'unbind').map((item: any) => ({
server_resource_id: item.server_resource_id,
username: item.original_username || item.username || undefined,
delete_remote: Boolean(item.delete_remote),
}))
if (!payload.password) {
delete payload.password
}
delete payload.server_ids
if (editingId.value) {
await usersApi.update(editingId.value, payload)
@ -344,32 +372,111 @@ async function submit(): Promise<void> {
}
}
function syncServerBindingRows(): void {
const defaultUsername = normalizeServerUsername(form.nickname || form.email || `user${editingId.value || ''}`)
const current = new Map<number, any>(form.server_bindings.map((item: any) => [Number(item.server_resource_id), item]))
form.server_bindings = form.server_ids.map((serverId: number) => {
const existing = current.get(Number(serverId))
if (existing) {
return existing
}
function resetServerBindingRows(bindings: any[]): void {
const bindingMap = new Map<number, any>(bindings.map((item: any) => [Number(item.server_resource_id), item]))
serverBindingRows.value = serverOptions.value.map((server: any) => {
const binding = bindingMap.get(Number(server.id))
return {
server_resource_id: serverId,
username: defaultUsername,
create_remote: true,
server_resource_id: server.id,
server_label: server.display_name || server.name || `服务器#${server.id}`,
username: binding?.username || '',
original_username: binding?.username || '',
password: '',
action: '',
delete_remote: false,
}
})
}
watch(() => form.nickname, () => {
const username = normalizeServerUsername(form.nickname || form.email || `user${editingId.value || ''}`)
form.server_bindings.forEach((item: any) => {
if (item.username) {
item.username = username
function openBindDialog(row: any): void {
bindingTarget.value = row
Object.assign(bindForm, {
username: row.username || normalizeServerUsername(form.nickname || form.email || `user${editingId.value || ''}`),
password: '',
checked: false,
remote_exists: false,
})
bindDialogVisible.value = true
}
async function checkBindUsername(): Promise<void> {
if (!bindingTarget.value?.server_resource_id || !bindForm.username?.trim()) {
bindForm.checked = false
bindForm.remote_exists = false
return
}
checkingServerUser.value = true
try {
const response: any = await usersApi.checkServerUser({
server_resource_id: bindingTarget.value.server_resource_id,
username: bindForm.username,
})
bindForm.checked = true
bindForm.remote_exists = Boolean(response.data.exists)
} catch (error: any) {
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
ElMessage.error(first || error?.message || '检查服务器账号失败')
} finally {
checkingServerUser.value = false
}
}
async function confirmBind(): Promise<void> {
if (!bindingTarget.value) {
return
}
if (!bindForm.username?.trim()) {
ElMessage.warning('请输入服务器账号')
return
}
if (!bindForm.checked) {
await checkBindUsername()
}
Object.assign(bindingTarget.value, {
username: bindForm.username,
password: bindForm.remote_exists ? '' : (bindForm.password || '123456'),
action: 'bind',
delete_remote: false,
})
bindDialogVisible.value = false
}
async function openUnbindDialog(row: any): Promise<void> {
const result = await ElMessageBox.confirm(
`是否同时删除服务器用户 ${row.username}?只删除用户,不删除用户文件夹。`,
'解绑服务器账号',
{
type: 'warning',
distinguishCancelAndClose: true,
confirmButtonText: '删除服务器用户',
cancelButtonText: '不删除,仅解绑',
},
).then(() => true).catch((action) => {
if (action === 'cancel') {
return false
}
return null
})
if (result === null) {
return
}
Object.assign(row, {
action: 'unbind',
delete_remote: result,
})
}
function cancelUnbind(row: any): void {
Object.assign(row, {
action: '',
delete_remote: false,
})
}
function serverBindingsLabel(row: any): string {
const bindings = row.server_user_bindings || []
@ -382,11 +489,6 @@ function serverBindingsLabel(row: any): string {
.join('')
}
function serverLabel(serverId: number): string {
const server = serverOptions.value.find((item: any) => Number(item.id) === Number(serverId))
return server ? String(server.display_name || server.name || server.id) : `服务器#${serverId}`
}
function normalizeServerUsername(value: string): string {
const raw = String(value || '').trim()
const ascii = raw

View File

@ -62,7 +62,9 @@ router.beforeEach(async (to) => {
removeToken()
authStore.clearAuth()
loaded = false
if (code !== 401) {
ElMessage.error('登录状态无效,请重新登录')
}
return '/login'
}
}