Files
shrimp-admin/src/module/role.vue
T
wc24 701ed957ef feat(role): 跳转带的 ?filter={_id} 在角色列表里自动选中那条
role 是自定义 master-detail 页(非 ShrimpTable、不做服务端过滤),原 onMounted 默认选第一个,
无视 relation 跳转带的 _id。现全量加载后按 URL filter._id 客户端选中目标角色,解析不出走默认。
2026-06-29 21:00:07 +08:00

255 lines
7.0 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.
<template>
<el-container class="role-page">
<!-- 左侧:角色列表 -->
<el-aside width="260px">
<div class="role-list-card">
<div class="role-list-header">{{ $t('role.menuList') }}</div>
<div class="role-list-body">
<div v-for="role in roleList" :key="role._id" class="role-item"
:class="{ active: activeId === role._id }" @click="onSelect(role._id)">
{{ role.name }}
</div>
<div v-if="roleList.length === 0" class="empty-tip">{{ $t('role.empty') }}</div>
</div>
<div class="role-list-footer" v-if="can('add')">
<el-button type="primary" @click="handleAdd" class="add-btn">{{ $t('role.addRole') }}</el-button>
</div>
</div>
</el-aside>
<!-- 右侧:角色编辑 -->
<el-main>
<div v-if="currentRole" class="role-form">
<el-form :model="formData" label-width="80px">
<el-form-item :label="$t('role.name')">
<el-input v-model="formData.name" />
</el-form-item>
<el-form-item :label="$t('role.description')">
<el-input v-model="formData.description" type="textarea" :rows="3" />
</el-form-item>
<el-form-item :label="$t('role.powers')">
<ShrimpPowerTree :modelValue="formData.powers || []" :allPowers="allPowers" :menuTree="vueAdmin.menuTree"
@update:modelValue="val => formData.powers = val" />
</el-form-item>
</el-form>
<div class="form-footer">
<el-button v-if="can('edit')" type="primary" @click="handleSave">{{ $t('base.save') }}</el-button>
<el-popconfirm v-if="can('delete')" :title="$t('role.confirmDelete')" :confirm-button-text="$t('base.delete')"
:cancel-button-text="$t('base.cancel')" confirm-button-type="danger" @confirm="handleDelete">
<template #reference>
<el-button type="danger">{{ $t('base.delete') }}</el-button>
</template>
</el-popconfirm>
</div>
</div>
<el-empty v-else :description="$t('role.selectOrCreate')" />
</el-main>
</el-container>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { vueAdmin } from '../VueAdmin'
import { IRole, IRoleProto, WithId, ILabel } from '../IShrimpType'
import { ElMessage } from 'element-plus'
import ShrimpPowerTree from '../components/ShrimpPowerTree.vue'
interface IPowerItem {
key: string
label: ILabel
}
const { t } = useI18n()
const route = useRoute()
const props = defineProps<{
moduleKey: string
events?: string[]
}>()
// relation 跳转落到 role 页时带 ?filter={"_id":...}。role 是自定义 master-detail 页(非 ShrimpTable、
// 不做服务端过滤),全量加载后按这个 _id 在列表里选中那条;解析不出就走默认(选第一个)。
function jumpTargetId(): string {
try {
const f = route.query.filter ? JSON.parse(String(route.query.filter)) : null
return f && typeof f === 'object' ? String(f._id || '') : ''
} catch {
return ''
}
}
const roleApi = vueAdmin.getModule<IRoleProto>('role')
const roleList = ref<WithId<IRole>[]>([])
const activeId = ref('')
const allPowers = ref<IPowerItem[]>([])
const currentRole = ref<WithId<IRole> | null>(null)
const formData = ref<Partial<Omit<IRole, 'createdAt' | 'updatedAt'>>>({
name: '',
description: '',
powers: [],
})
const can = vueAdmin.can('role')
async function loadRoles() {
const res = await roleApi.list()
roleList.value = res.list || res as any
}
function onSelect(id: string) {
activeId.value = id
const role = roleList.value.find(r => r._id === id)
if (role) {
currentRole.value = { ...role }
formData.value = {
name: role.name,
description: role.description,
powers: [...(role.powers || [])],
}
}
}
async function handleAdd() {
try {
const created = await roleApi.add({ name: t('role.newRole'), description: '', powers: [] } as any)
await loadRoles()
onSelect(created._id)
} catch (e: any) {
console.warn('[Role]', e.message)
}
}
async function handleSave() {
try {
await roleApi.edit(currentRole.value!._id, formData.value)
await loadRoles()
onSelect(currentRole.value!._id)
ElMessage.success(t('base.success'))
} catch (e: any) {
console.warn('[Role]', e.message)
}
}
async function handleDelete() {
try {
await roleApi.delete(currentRole.value!._id)
currentRole.value = null
activeId.value = ''
await loadRoles()
if (roleList.value.length > 0) {
onSelect(roleList.value[0]._id)
}
} catch (e: any) {
console.warn('[Role]', e.message)
}
}
onMounted(async () => {
allPowers.value = await roleApi.allPower()
await loadRoles()
const jumpId = jumpTargetId()
if (jumpId && roleList.value.some(r => r._id === jumpId)) {
onSelect(jumpId)
} else if (roleList.value.length > 0) {
onSelect(roleList.value[0]._id)
}
})
</script>
<style scoped>
.role-page {
height: 100%;
padding: 16px;
}
.el-aside {
margin-right: 16px;
border: none;
}
@media (max-width: 992px) {
.el-aside {
margin-right: 0;
}
.role-list-card {
margin-bottom: 16px;
}
}
/* 角色列表卡片 */
.role-list-card {
border: 1px solid var(--el-border-color);
border-radius: 8px;
background: var(--el-bg-color);
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.role-list-header {
padding: 12px 16px;
font-weight: 600;
font-size: 14px;
border-bottom: 1px solid var(--el-border-color);
}
.role-list-body {
flex: 1;
overflow-y: auto;
}
.role-item {
padding: 10px 16px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
border-bottom: 1px solid var(--el-border-color-extra-light);
}
.role-item:hover {
background: var(--el-fill-color-light);
}
.role-item.active {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-weight: 500;
}
.role-list-footer {
border-top: 1px solid var(--el-border-color);
}
.add-btn {
width: 100%;
height: 40px;
border-radius: 0 0 8px 8px;
border: none !important;
font-size: 14px;
}
/* 角色表单 */
.role-form {
max-width: 700px;
}
.form-footer {
padding-top: 16px;
border-top: 1px solid var(--el-border-color);
margin-top: 16px;
}
.empty-tip {
padding: 20px;
text-align: center;
color: var(--el-text-color-placeholder);
font-size: 14px;
}
</style>