Files
client-web/src/views/RolesView.vue
T

191 lines
11 KiB
Vue

<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { LockKeyhole, Pencil, Plus, ShieldCheck, Trash2 } from '@lucide/vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import { useTenantAdministrationStore, type TenantRole } from '@/stores/tenantAdministration'
import { useAuthStore } from '@/stores/auth'
const store = useTenantAdministrationStore()
const auth = useAuthStore()
const router = useRouter()
const roleFilters = ref({ keyword: '' })
const appliedRoleFilters = ref({ ...roleFilters.value })
const rolePage = ref(1)
const rolePageSize = ref(10)
const editorOpen = ref(false)
const editingId = ref('')
const loading = ref(false)
const loadError = ref('')
const saving = ref(false)
const actionRoleId = ref('')
const formRef = ref<FormInstance>()
const form = reactive({ name: '', code: '', description: '' })
const roleRules: FormRules = {
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
description: [{ required: true, message: '请填写角色说明', trigger: 'blur' }],
}
const roleQueryFields: QueryField[] = [{ key: 'keyword', type: 'input', placeholder: '搜索角色名称、编码或说明' }]
const roleTableColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', fixed: 'left', width: 250, slotName: 'actions' },
{ key: 'role', label: '角色', minWidth: 180, slotName: 'role' },
{ key: 'description', prop: 'description', label: '角色说明', minWidth: 220 },
{ key: 'memberCount', label: '成员数', width: 100, slotName: 'memberCount' },
{ key: 'permissionCount', label: '权限数', width: 100, slotName: 'permissionCount' },
{ key: 'status', label: '状态', width: 90, slotName: 'status' },
]
const filteredRoles = computed(() => {
const key = appliedRoleFilters.value.keyword.trim().toLowerCase()
return store.roles.filter((role) => !key || [role.name, role.code, role.description].some((value) => value.toLowerCase().includes(key)))
})
const pagedRoles = computed(() => filteredRoles.value.slice((rolePage.value - 1) * rolePageSize.value, rolePage.value * rolePageSize.value))
function resetForm() {
editingId.value = ''
Object.assign(form, { name: '', code: '', description: '' })
void nextTick(() => formRef.value?.clearValidate())
}
function openCreate() {
resetForm()
editorOpen.value = true
}
function openEdit(role: TenantRole) {
editingId.value = role.id
Object.assign(form, { name: role.name, code: role.code, description: role.description })
editorOpen.value = true
}
async function loadRoles() {
loading.value = true
loadError.value = ''
try {
const requests = [store.loadRoles()]
if (auth.hasPermission('company:member:list')) requests.push(store.loadMembers())
await Promise.all(requests)
} catch (error) {
loadError.value = error instanceof Error ? error.message : '角色与成员统计加载失败'
} finally {
loading.value = false
}
}
onMounted(() => void loadRoles())
function searchRoles(filters: QueryModel) {
appliedRoleFilters.value = { keyword: typeof filters.keyword === 'string' ? filters.keyword : '' }
rolePage.value = 1
void loadRoles()
}
async function saveRole() {
if (saving.value) return
form.name = form.name.trim()
form.code = form.code.trim().toUpperCase()
form.description = form.description.trim()
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
saving.value = true
try {
if (editingId.value) await store.updateRole(editingId.value, { name: form.name, description: form.description })
else await store.createRole({ ...form, code: form.code.trim().toUpperCase() })
editorOpen.value = false
ElMessage.success(editingId.value ? '角色已更新' : '角色已新增')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '保存失败')
} finally {
saving.value = false
}
}
async function toggleStatus(role: TenantRole) {
const action = role.status === 'ACTIVE' ? '停用' : '启用'
try {
await ElMessageBox.confirm(`${action}后将影响拥有该角色的成员权限。`, `${action}角色 ${role.name}`, { type: 'warning', confirmButtonText: action, cancelButtonText: '取消' })
actionRoleId.value = role.id
await store.toggleRoleStatus(role.id)
ElMessage.success(`角色已${action}`)
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(error instanceof Error ? error.message : '操作失败')
} finally {
actionRoleId.value = ''
}
}
async function removeRole(role: TenantRole) {
try {
await ElMessageBox.confirm('删除后不能恢复,仅未被成员使用的自定义角色可以删除。', `删除角色 ${role.name}`, { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' })
actionRoleId.value = role.id
await store.deleteRole(role.id)
ElMessage.success('角色已删除')
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(error instanceof Error ? error.message : '删除失败')
} finally {
actionRoleId.value = ''
}
}
function assignPermissions(role: TenantRole) {
router.push({ name: 'permissions', query: { role: role.id } })
}
</script>
<template>
<section v-loading="loading" class="roles-page">
<div v-if="loadError" class="client-card collection-load-error" role="alert">
<div><strong>企业角色加载失败</strong><span>{{ loadError }}</span></div>
<el-button :loading="loading" @click="loadRoles">重新加载</el-button>
</div>
<template v-else>
<QueryToolbar v-model="roleFilters" ariaLabel="企业角色筛选" :fields="roleQueryFields" search-text="查询角色" @search="searchRoles">
<template #actions><el-button v-if="auth.hasPermission('company:role:write')" type="primary" @click="openCreate"><Plus :size="17" />新增角色</el-button></template>
</QueryToolbar>
<div class="client-card role-table-card">
<DataTable v-model:page="rolePage" v-model:page-size="rolePageSize" :data="pagedRoles" :columns="roleTableColumns" :loading="loading" :total="filteredRoles.length" row-key="id">
<template #actions="{ row }"><el-button v-if="auth.hasPermission('company:permission:list')" link type="primary" :disabled="actionRoleId === (row as TenantRole).id" @click="assignPermissions(row as TenantRole)">{{ auth.hasPermission('company:permission:assign') && !(row as TenantRole).builtin ? '分配权限' : '查看权限' }}</el-button><el-button v-if="auth.hasPermission('company:role:write') && !(row as TenantRole).builtin" link type="primary" :disabled="actionRoleId === (row as TenantRole).id" @click="openEdit(row as TenantRole)"><Pencil :size="15" />编辑</el-button><el-button v-if="auth.hasPermission('company:role:write') && !(row as TenantRole).builtin" link :type="(row as TenantRole).status === 'ACTIVE' ? 'warning' : 'success'" :loading="actionRoleId === (row as TenantRole).id" @click="toggleStatus(row as TenantRole)">{{ (row as TenantRole).status === 'ACTIVE' ? '停用' : '启用' }}</el-button><el-button v-if="auth.hasPermission('company:role:write') && !(row as TenantRole).builtin" link type="danger" :disabled="actionRoleId === (row as TenantRole).id" @click="removeRole(row as TenantRole)"><Trash2 :size="15" />删除</el-button></template>
<template #role="{ row }"><div class="role-identity"><span class="role-icon"><LockKeyhole v-if="(row as TenantRole).builtin" :size="20" /><ShieldCheck v-else :size="20" /></span><div><strong>{{ (row as TenantRole).name }}</strong><code>{{ (row as TenantRole).code }}</code></div></div></template>
<template #memberCount="{ row }"><span>{{ store.roleMemberCount((row as TenantRole).id) }}</span></template>
<template #permissionCount="{ row }"><span>{{ (row as TenantRole).permissionCodes.length }}</span></template>
<template #status="{ row }"><el-tag v-if="(row as TenantRole).builtin" effect="plain">内置</el-tag><el-tag :type="(row as TenantRole).status === 'ACTIVE' ? 'success' : 'info'" effect="light">{{ (row as TenantRole).status === 'ACTIVE' ? '启用' : '停用' }}</el-tag></template>
<template #empty><div class="client-empty">没有匹配的企业角色。</div></template>
</DataTable>
</div>
<el-drawer v-model="editorOpen" :title="editingId ? '编辑角色' : '新增角色'" size="min(500px, 94vw)" @closed="resetForm">
<el-form ref="formRef" :model="form" :rules="roleRules" label-position="top">
<el-form-item label="角色名称" prop="name" required><el-input v-model="form.name" maxlength="30" placeholder="例如 订单操作员" /></el-form-item>
<el-form-item label="角色编码" prop="code" required><el-input v-model="form.code" :disabled="Boolean(editingId)" placeholder="例如 ORDER_OPERATOR" maxlength="40" /></el-form-item>
<el-form-item label="角色说明" prop="description" required><el-input v-model="form.description" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="简要说明该角色的职责和适用范围" /></el-form-item>
<div class="role-form-tip">保存后请前往“菜单权限”为角色分配可见菜单和操作权限。</div>
</el-form>
<template #footer><div class="drawer-actions"><el-button :disabled="saving" @click="editorOpen = false">取消</el-button><el-button type="primary" :loading="saving" @click="saveRole">保存</el-button></div></template>
</el-drawer>
</template>
</section>
</template>
<style scoped>
.roles-page { display: flex; flex-direction: column; gap: 14px; }
.collection-load-error { display: flex; min-height: 150px; align-items: center; justify-content: center; gap: 20px; }
.collection-load-error > div { display: flex; max-width: 520px; flex-direction: column; gap: 6px; }
.collection-load-error span { color: var(--muted); font-size: 12px; line-height: 1.6; }
.role-table-card { padding: 0; overflow: hidden; }
.role-identity { display: flex; min-width: 0; align-items: center; gap: 10px; }
.role-icon { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; background: #e7f3f1; color: var(--primary); }
.role-identity > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
.role-identity strong { overflow-wrap: anywhere; }
.role-identity code { color: var(--muted); font-size: 10px; }
.role-form-tip { padding: 12px; background: #f2f7f6; color: var(--muted); font-size: 11px; }
.drawer-actions { display: flex; justify-content: flex-end; gap: 8px; }
.roles-page :deep(.el-form-item__error) { line-height: 1.45; overflow-wrap: anywhere; }
@media (max-width: 600px) { .drawer-actions { flex-wrap: wrap; } }
</style>