vdi-cloud/console/src/views/Users.vue

55 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { api, type User } from '../api'
import { useAuth } from '../stores/auth'
import { usePlatform } from '../stores/platform'
import { useAction } from '../composables/useAction'
import CsvImportDialog from '../components/CsvImportDialog.vue'
import StatusTag from '../components/StatusTag.vue'
const auth = useAuth(), data = usePlatform(), { busy, run, confirm } = useAction()
const csvVisible = ref(false), csvKind = ref<'users' | 'bindings'>('users')
const keyword = ref(''), page = ref(1), createVisible = ref(false), bindingVisible = ref(false), selectedId = ref('')
const profileVisible = ref(false), profileId = ref(''), profileUsername = ref(''), checked = ref<User[]>([])
const profile = reactive({ displayName: '', enabled: true })
const batchVisible = ref(false), batchUsers = ref<User[]>([])
const batch = reactive({ action: 'grant' as 'grant' | 'revoke', desktopIds: [] as string[] })
const createForm = reactive({ username: '', displayName: '' })
const binding = reactive({ terminalId: null as string | null, desktopIds: [] as string[], enabled: true })
const filtered = computed(() => data.users.filter(u => `${u.username} ${u.displayName}`.toLowerCase().includes(keyword.value.toLowerCase())))
const rows = computed(() => filtered.value.slice((page.value - 1) * 10, page.value * 10))
const terminalName = (id: string | null) => data.terminals.find(t => t.id === id)?.name || '未关联'
const desktopName = (id: string) => data.desktops.find(d => d.id === id)?.name || id
function edit(u: User) { selectedId.value = u.id; binding.terminalId = u.terminalId; binding.desktopIds = [...u.desktopIds]; binding.enabled = u.enabled; bindingVisible.value = true }
async function create() { if (!createForm.username.trim() || !createForm.displayName.trim()) return; if (await run(() => api.createUser(createForm), '用户已创建')) { createVisible.value = false; createForm.username = ''; createForm.displayName = '' } }
async function save() { if (busy.value) return; if (!await confirm('保存用户授权', '更新关联终端、用户状态和所选桌面。未消费的旧连接票据将失效;已有串流不会被强制中断。')) return; if (await run(() => api.updateBindings(selectedId.value, binding), '桌面授权已保存')) bindingVisible.value = false }
const terminalTaken = (id: string) => data.users.some(u => u.id !== selectedId.value && u.terminalId === id)
function editProfile(u: User) { profileId.value = u.id; profileUsername.value = u.username; Object.assign(profile, { displayName: u.displayName, enabled: u.enabled }); profileVisible.value = true }
async function saveProfile() {
if (!profile.displayName.trim() || busy.value) return
const existing = data.users.find(u => u.id === profileId.value)
if (existing?.enabled !== profile.enabled && !await confirm(profile.enabled ? '启用用户' : '停用用户', '状态变更将作废该用户未消费的连接票据。停用阻止后续连接,保留授权关系和已有串流。')) return
if (await run(() => api.updateUser(profileId.value, profile), '用户资料已保存')) profileVisible.value = false
}
function selectUsers(users: User[]) { checked.value = users }
function openBatch() { batchUsers.value = [...checked.value]; batch.action = 'grant'; batch.desktopIds = []; batchVisible.value = true }
async function saveBatch() {
if (!batchUsers.value.length || !batch.desktopIds.length || busy.value) return
const action = batch.action === 'grant' ? '分配' : '撤销'
if (!await confirm(`批量${action}桌面`, `将为 ${batchUsers.value.length} 位用户${action}所选 ${batch.desktopIds.length} 台桌面的授权。其他授权保留,已有串流不会被强制中断。`)) return
if (await run(() => api.batchBindings({ userIds: batchUsers.value.map(u => u.id), desktopIds: batch.desktopIds, action: batch.action }), `已批量${action}桌面授权`)) batchVisible.value = false
}
</script>
<template>
<div class="page-heading"><div><h1>用户与授权</h1><p class="subtext">每位用户可获得多个桌面;每台终端关联一位使用者</p></div><div v-if="auth.canAdmin" class="actions"><el-button @click="csvKind = 'users'; csvVisible = true">导入用户</el-button><el-button @click="csvKind = 'bindings'; csvVisible = true">导入授权</el-button><el-button :disabled="!checked.length || busy" @click="openBatch">批量授权<span v-if="checked.length">({{ checked.length }})</span></el-button><el-button type="primary" @click="createVisible = true">新建用户</el-button></div></div>
<section class="panel"><div class="toolbar"><el-input v-model="keyword" placeholder="搜索姓名 / 用户账号" aria-label="搜索用户" clearable @input="page = 1" /><span class="secondary">共 {{ filtered.length }} 位</span></div><el-table :data="rows" row-key="id" empty-text="暂无终端用户" @selection-change="selectUsers"><el-table-column v-if="auth.canAdmin" type="selection" width="44" /><el-table-column label="用户 / 账号" min-width="160"><template #default="{ row }">{{ row.displayName }}<div class="secondary">{{ row.username }}</div></template></el-table-column><el-table-column label="关联终端" min-width="160"><template #default="{ row }">{{ terminalName(row.terminalId) }}</template></el-table-column><el-table-column label="可用桌面" min-width="220"><template #default="{ row }"><div v-for="id in row.desktopIds" :key="id">{{ desktopName(id) }}</div><span v-if="!row.desktopIds.length" class="secondary">尚未分配</span></template></el-table-column><el-table-column label="状态" width="100"><template #default="{ row }"><StatusTag :value="row.enabled ? 'enabled' : 'disabled'" /></template></el-table-column><el-table-column v-if="auth.canAdmin" label="操作" width="185"><template #default="{ row }"><el-button link type="primary" :disabled="busy" @click="editProfile(row)">编辑资料</el-button><el-button link type="primary" :disabled="busy" @click="edit(row)">编辑授权</el-button></template></el-table-column></el-table><el-pagination v-model:current-page="page" :total="filtered.length" :page-size="10" layout="total, prev, pager, next" /></section><p class="subtext">这里管理桌面使用者。平台管理员账号独立管理,不因拥有管理角色而自动获得桌面使用权。</p>
<el-dialog v-model="createVisible" title="新建终端用户" width="480px"><el-form label-position="top" @submit.prevent="create"><el-form-item label="姓名" required><el-input v-model="createForm.displayName" maxlength="80" /></el-form-item><el-form-item label="用户账号" required><el-input v-model="createForm.username" maxlength="80" /></el-form-item><p class="subtext">首版使用设备绑定模式,此账号用于授权关系,不是桌面系统密码。</p><div class="actions" style="justify-content:flex-end;margin-top:22px"><el-button @click="createVisible = false">取消</el-button><el-button type="primary" native-type="submit" :disabled="!createForm.username.trim() || !createForm.displayName.trim()" :loading="busy">创建用户</el-button></div></el-form></el-dialog>
<el-dialog v-model="profileVisible" title="编辑用户资料" width="480px" :close-on-click-modal="false" :show-close="!busy" :close-on-press-escape="!busy"><el-form label-position="top" :disabled="busy" @submit.prevent="saveProfile"><el-form-item label="用户账号"><el-input :model-value="profileUsername" disabled /></el-form-item><el-form-item label="姓名"><el-input v-model="profile.displayName" maxlength="80" aria-label="编辑用户姓名" /></el-form-item><el-form-item label="用户状态"><el-switch v-model="profile.enabled" active-text="启用" inactive-text="停用" aria-label="编辑用户启用状态" /></el-form-item><p class="subtext">登录名固定;停用保留桌面授权,阻止后续连接。</p></el-form><template #footer><el-button :disabled="busy" @click="profileVisible = false">取消</el-button><el-button type="primary" :loading="busy" :disabled="!profile.displayName.trim()" @click="saveProfile">保存用户资料</el-button></template></el-dialog>
<el-dialog v-model="batchVisible" title="批量桌面授权" width="560px" :close-on-click-modal="false" :show-close="!busy" :close-on-press-escape="!busy"><el-form label-position="top" :disabled="busy"><el-form-item label="操作方式"><el-radio-group v-model="batch.action" aria-label="批量授权操作"><el-radio-button value="grant">追加分配</el-radio-button><el-radio-button value="revoke">撤销指定</el-radio-button></el-radio-group></el-form-item><el-form-item :label="`目标用户(${batchUsers.length} 位)`"><div class="batch-users"><el-tag v-for="u in batchUsers" :key="u.id" type="info">{{ u.displayName }} / {{ u.username }}</el-tag></div></el-form-item><el-form-item label="所选桌面"><el-checkbox-group v-model="batch.desktopIds" style="display:grid"><el-checkbox v-for="d in data.desktops" :key="d.id" :value="d.id">{{ d.name }} · {{ d.os }}</el-checkbox></el-checkbox-group><p v-if="!data.desktops.length" class="subtext">尚无可授权桌面</p></el-form-item><p class="subtext">只追加或撤销所选桌面,保留其他授权;停用用户仍需启用后才能连接。每次最多 100 位用户和 100 台桌面。</p></el-form><template #footer><el-button :disabled="busy" @click="batchVisible = false">取消</el-button><el-button type="primary" :loading="busy" :disabled="!batchUsers.length || !batch.desktopIds.length || batchUsers.length > 100" @click="saveBatch">提交批量授权</el-button></template></el-dialog>
<el-dialog v-model="bindingVisible" title="编辑桌面授权" width="540px" :close-on-click-modal="false" :show-close="!busy" :close-on-press-escape="!busy"><el-form label-position="top" @submit.prevent="save" :disabled="busy"><el-form-item label="关联终端"><el-select v-model="binding.terminalId" placeholder="不关联终端" clearable @clear="binding.terminalId = null"><el-option v-for="t in data.terminals" :key="t.id" :label="`${t.name} / ${t.sn}`" :value="t.id" :disabled="terminalTaken(t.id) || (t.enabled === false && t.id !== binding.terminalId)" /></el-select></el-form-item><el-form-item label="可用桌面(可多选)"><el-checkbox-group v-model="binding.desktopIds" style="display:grid"><el-checkbox v-for="d in data.desktops" :key="d.id" :value="d.id">{{ d.name }} · {{ d.os }}</el-checkbox></el-checkbox-group><p v-if="!data.desktops.length" class="subtext">尚无可授权桌面</p></el-form-item><el-form-item label="用户状态"><el-switch v-model="binding.enabled" active-text="启用" inactive-text="停用" /></el-form-item><p class="subtext">撤销授权会阻止后续连接申请;本版不会强制中断已经建立的串流。</p><div class="actions" style="justify-content:flex-end;margin-top:22px"><el-button @click="bindingVisible = false">取消</el-button><el-button type="primary" native-type="submit" :loading="busy">保存授权</el-button></div></el-form></el-dialog>
<CsvImportDialog v-model="csvVisible" :kind="csvKind" @completed="data.refresh" />
</template>
<style scoped>
.batch-users{display:flex;flex-wrap:wrap;gap:8px;max-height:140px;overflow:auto}
</style>