55 lines
11 KiB
Vue
55 lines
11 KiB
Vue
<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>
|