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

168 lines
10 KiB
Vue
Raw Normal View History

2026-07-30 17:59:42 +08:00
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { LockKeyhole, Pencil, Plus, Search, ShieldCheck, Trash2, Users } from '@lucide/vue'
import { useTenantAdministrationStore, type TenantRole } from '@/stores/tenantAdministration'
import { useAuthStore } from '@/stores/auth'
const store = useTenantAdministrationStore()
const auth = useAuthStore()
const router = useRouter()
const keyword = ref('')
const editorOpen = ref(false)
const editingId = ref('')
const loading = ref(false)
2026-07-31 13:43:16 +08:00
const saving = ref(false)
const actionRoleId = ref('')
2026-07-30 17:59:42 +08:00
const form = reactive({ name: '', code: '', description: '' })
const filteredRoles = computed(() => {
const key = keyword.value.trim().toLowerCase()
return store.roles.filter((role) => !key || [role.name, role.code, role.description].some((value) => value.toLowerCase().includes(key)))
})
function resetForm() {
editingId.value = ''
Object.assign(form, { name: '', code: '', description: '' })
}
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
}
onMounted(async () => {
loading.value = true
try {
2026-07-30 21:23:06 +08:00
const requests = [store.loadRoles()]
if (auth.hasPermission('company:member:list')) requests.push(store.loadMembers())
await Promise.all(requests)
2026-07-30 17:59:42 +08:00
} catch (error) {
2026-07-30 21:23:06 +08:00
ElMessage.error(error instanceof Error ? error.message : '角色与成员统计加载失败')
2026-07-30 17:59:42 +08:00
} finally {
loading.value = false
}
})
async function saveRole() {
if (!form.name || !form.code || !form.description) {
ElMessage.warning('请完整填写角色名称、编码和说明')
return
}
2026-07-31 13:43:16 +08:00
saving.value = true
2026-07-30 17:59:42 +08:00
try {
2026-07-31 13:43:16 +08:00
if (editingId.value) await store.updateRole(editingId.value, { name: form.name, description: form.description })
2026-07-30 17:59:42 +08:00
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 : '保存失败')
2026-07-31 13:43:16 +08:00
} finally {
saving.value = false
2026-07-30 17:59:42 +08:00
}
}
async function toggleStatus(role: TenantRole) {
const action = role.status === 'ACTIVE' ? '停用' : '启用'
try {
await ElMessageBox.confirm(`${action}后将影响拥有该角色的成员权限。`, `${action}角色 ${role.name}`, { type: 'warning', confirmButtonText: action, cancelButtonText: '取消' })
2026-07-31 13:43:16 +08:00
actionRoleId.value = role.id
await store.toggleRoleStatus(role.id)
2026-07-30 17:59:42 +08:00
ElMessage.success(`角色已${action}`)
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(error instanceof Error ? error.message : '操作失败')
2026-07-31 13:43:16 +08:00
} finally {
actionRoleId.value = ''
2026-07-30 17:59:42 +08:00
}
}
async function removeRole(role: TenantRole) {
try {
await ElMessageBox.confirm('删除后不能恢复,仅未被成员使用的自定义角色可以删除。', `删除角色 ${role.name}`, { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' })
2026-07-31 13:43:16 +08:00
actionRoleId.value = role.id
await store.deleteRole(role.id)
2026-07-30 17:59:42 +08:00
ElMessage.success('角色已删除')
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(error instanceof Error ? error.message : '删除失败')
2026-07-31 13:43:16 +08:00
} finally {
actionRoleId.value = ''
2026-07-30 17:59:42 +08:00
}
}
function assignPermissions(role: TenantRole) {
router.push({ name: 'permissions', query: { role: role.id } })
}
</script>
<template>
<section v-loading="loading" class="roles-page">
<div class="role-guidance client-card"><ShieldCheck :size="22" /><div><strong>角色用于约束企业内部成员可见菜单和可执行操作</strong><span>企业管理员为系统内置角色,不能停用或删除;平台后台权限不会出现在客户工作台。</span></div></div>
2026-07-31 13:43:16 +08:00
<div v-if="store.rolesLoadedFromApi" class="role-contract-notice client-card">角色列表、新增、编辑、启停、删除及权限回显均使用真实 IAM 接口;系统内置角色和仍有关联成员的角色继续受服务端保护。</div>
2026-07-30 17:59:42 +08:00
<div class="role-toolbar client-card">
<el-input v-model="keyword" clearable placeholder="搜索角色名称、编码或说明"><template #prefix><Search :size="16" /></template></el-input>
<el-button v-if="auth.hasPermission('company:role:write')" type="primary" @click="openCreate"><Plus :size="17" />新增角色</el-button>
</div>
<div v-if="filteredRoles.length" class="role-grid">
<article v-for="role in filteredRoles" :key="role.id" class="client-card role-card" :class="{ inactive: role.status === 'INACTIVE' }">
<header><div class="role-icon"><LockKeyhole v-if="role.builtin" :size="20" /><ShieldCheck v-else :size="20" /></div><div class="role-heading"><div><h3>{{ role.name }}</h3><el-tag v-if="role.builtin" effect="plain">内置</el-tag><el-tag :type="role.status === 'ACTIVE' ? 'success' : 'info'" effect="light">{{ role.status === 'ACTIVE' ? '启用' : '停用' }}</el-tag></div><code>{{ role.code }}</code></div></header>
<p>{{ role.description }}</p>
<dl><div><dt><Users :size="15" />成员数</dt><dd>{{ store.roleMemberCount(role.id) }}</dd></div><div><dt><ShieldCheck :size="15" />权限数</dt><dd>{{ role.permissionCodes.length }}</dd></div></dl>
2026-07-31 13:43:16 +08:00
<footer><el-button v-if="auth.hasPermission('company:permission:list')" link type="primary" :disabled="actionRoleId === role.id" @click="assignPermissions(role)">{{ auth.hasPermission('company:permission:assign') && !role.builtin ? '分配权限' : '查看权限' }}</el-button><el-button v-if="auth.hasPermission('company:role:write') && !role.builtin" link type="primary" :disabled="actionRoleId === role.id" @click="openEdit(role)"><Pencil :size="15" />编辑</el-button><el-button v-if="auth.hasPermission('company:role:write') && !role.builtin" link :type="role.status === 'ACTIVE' ? 'warning' : 'success'" :loading="actionRoleId === role.id" @click="toggleStatus(role)">{{ role.status === 'ACTIVE' ? '停用' : '启用' }}</el-button><el-button v-if="auth.hasPermission('company:role:write') && !role.builtin" link type="danger" :disabled="actionRoleId === role.id" @click="removeRole(role)"><Trash2 :size="15" />删除</el-button></footer>
2026-07-30 17:59:42 +08:00
</article>
</div>
<div v-else class="client-card client-empty">没有匹配的企业角色。</div>
<el-drawer v-model="editorOpen" :title="editingId ? '编辑角色' : '新增角色'" size="min(500px, 94vw)" @closed="resetForm">
<el-form label-position="top">
<el-form-item label="角色名称" required><el-input v-model="form.name" maxlength="30" /></el-form-item>
<el-form-item label="角色编码" required><el-input v-model="form.code" :disabled="Boolean(editingId)" placeholder="例如 ORDER_OPERATOR" maxlength="40" /></el-form-item>
<el-form-item label="角色说明" required><el-input v-model="form.description" type="textarea" :rows="4" maxlength="200" show-word-limit /></el-form-item>
<div class="role-form-tip">保存后请前往“菜单权限”为角色分配可见菜单和操作权限。</div>
</el-form>
2026-07-31 13:43:16 +08:00
<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>
2026-07-30 17:59:42 +08:00
</el-drawer>
</section>
</template>
<style scoped>
.roles-page { display: flex; flex-direction: column; gap: 14px; }
.role-guidance { display: flex; align-items: center; gap: 13px; padding: 17px 20px; border-left: 4px solid var(--primary); color: var(--primary); }
.role-guidance > div { display: flex; flex-direction: column; gap: 4px; }
.role-guidance strong { color: var(--navy); font-size: 13px; }
.role-guidance span { color: var(--muted); font-size: 11px; }
.role-contract-notice { padding: 13px 16px; border-left: 3px solid #d97706; background: #fff8eb; color: #8a5b10; font-size: 11px; line-height: 1.6; }
.role-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 16px; }
.role-toolbar .el-input { max-width: 360px; }
.role-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.role-card { display: flex; min-width: 0; flex-direction: column; padding: 20px; }
.role-card.inactive { opacity: .72; }
.role-card header { justify-content: flex-start; gap: 12px; }
.role-icon { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; background: #e7f3f1; color: var(--primary); }
.role-heading { min-width: 0; }
.role-heading > div { display: flex; align-items: center; gap: 7px; }
.role-heading h3 { margin: 0; font-size: 16px; }
.role-heading code { display: block; margin-top: 5px; color: var(--muted); font-size: 10px; }
.role-card > p { flex: 1; min-height: 42px; margin: 18px 0; color: var(--muted); font-size: 12px; line-height: 1.7; }
.role-card dl { display: grid; grid-template-columns: 1fr 1fr; margin: 0; border: 1px solid var(--border); }
.role-card dl div { display: flex; align-items: center; justify-content: space-between; padding: 11px 13px; }
.role-card dl div:first-child { border-right: 1px solid var(--border); }
.role-card dt { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: 11px; }
.role-card dd { margin: 0; color: var(--navy); font-weight: 750; }
.role-card footer { display: flex; justify-content: flex-end; gap: 4px; margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border); flex-wrap: wrap; }
.role-form-tip { padding: 12px; background: #f2f7f6; color: var(--muted); font-size: 11px; }
.drawer-actions { display: flex; justify-content: flex-end; gap: 8px; }
@media (max-width: 800px) { .role-grid { grid-template-columns: 1fr; } }
@media (max-width: 600px) { .role-toolbar { align-items: stretch; flex-direction: column; } .role-toolbar .el-input { max-width: none; } .role-toolbar .el-button { width: 100%; } }
</style>