mirror of https://github.com/halo-dev/halo-admin
feat: refine ui permissions (#628)
#### What type of PR is this? /kind feature /kind improvement /milestone 2.0 #### What this PR does / why we need it: 完善 UI 权限控制。适配 https://github.com/halo-dev/halo/pull/2488 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2342 #### Special notes for your reviewer: /cc @halo-dev/sig-halo-console 测试方式: 1. Halo 需要使用 https://github.com/halo-dev/halo/issues/2342 PR 的分支。 2. 创建新的角色,并勾选需要测试的权限。 3. 创建新的用户并赋予新的角色。 4. 测试操作权限。 #### Does this PR introduce a user-facing change? ```release-note 完善 UI 权限控制 ```pull/633/head
parent
a6e913abc5
commit
3ae432ac75
|
@ -173,9 +173,10 @@ async function loadCurrentUser() {
|
|||
app.directive(
|
||||
"permission",
|
||||
(el: HTMLElement, binding: DirectiveBinding<string[]>) => {
|
||||
const uiPermissions = Array.from<string>(
|
||||
currentPermissions.uiPermissions
|
||||
);
|
||||
// const uiPermissions = Array.from<string>(
|
||||
// currentPermissions.uiPermissions
|
||||
// );
|
||||
const uiPermissions = Array.from<string>(["system:attachments:view"]);
|
||||
const { value } = binding;
|
||||
const { any, enable } = binding.modifiers;
|
||||
|
||||
|
|
|
@ -38,6 +38,9 @@ import cloneDeep from "lodash.clonedeep";
|
|||
import { isImage } from "@/utils/image";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { useFetchAttachmentGroup } from "./composables/use-attachment-group";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const policyVisible = ref(false);
|
||||
const uploadVisible = ref(false);
|
||||
|
@ -233,13 +236,21 @@ onMounted(() => {
|
|||
</template>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton size="sm" @click="policyVisible = true">
|
||||
<VButton
|
||||
v-permission="['system:attachments:manage']"
|
||||
size="sm"
|
||||
@click="policyVisible = true"
|
||||
>
|
||||
<template #icon>
|
||||
<IconDatabase2Line class="h-full w-full" />
|
||||
</template>
|
||||
存储策略
|
||||
</VButton>
|
||||
<VButton type="secondary" @click="uploadVisible = true">
|
||||
<VButton
|
||||
v-permission="['system:attachments:manage']"
|
||||
type="secondary"
|
||||
@click="uploadVisible = true"
|
||||
>
|
||||
<template #icon>
|
||||
<IconUpload class="h-full w-full" />
|
||||
</template>
|
||||
|
@ -258,7 +269,10 @@ onMounted(() => {
|
|||
<div
|
||||
class="relative flex flex-col items-start sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="mr-4 hidden items-center sm:flex">
|
||||
<div
|
||||
v-permission="['system:attachments:manage']"
|
||||
class="mr-4 hidden items-center sm:flex"
|
||||
>
|
||||
<input
|
||||
v-model="checkedAll"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||
|
@ -489,7 +503,11 @@ onMounted(() => {
|
|||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchAttachments">刷新</VButton>
|
||||
<VButton type="secondary" @click="uploadVisible = true">
|
||||
<VButton
|
||||
v-permission="['system:attachments:manage']"
|
||||
type="secondary"
|
||||
@click="uploadVisible = true"
|
||||
>
|
||||
<template #icon>
|
||||
<IconUpload class="h-full w-full" />
|
||||
</template>
|
||||
|
@ -565,6 +583,7 @@ onMounted(() => {
|
|||
|
||||
<div
|
||||
v-if="!attachment.metadata.deletionTimestamp"
|
||||
v-permission="['system:attachments:manage']"
|
||||
:class="{ '!flex': selectedAttachments.has(attachment) }"
|
||||
class="absolute top-0 left-0 hidden h-1/3 w-full cursor-pointer justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex"
|
||||
>
|
||||
|
@ -588,7 +607,12 @@ onMounted(() => {
|
|||
>
|
||||
<li v-for="(attachment, index) in attachments.items" :key="index">
|
||||
<VEntity :is-selected="isChecked(attachment)">
|
||||
<template #checkbox>
|
||||
<template
|
||||
v-if="
|
||||
!currentUserHasPermission(['system:attachments:manage'])
|
||||
"
|
||||
#checkbox
|
||||
>
|
||||
<input
|
||||
:checked="selectedAttachments.has(attachment)"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||
|
@ -669,7 +693,12 @@ onMounted(() => {
|
|||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<template
|
||||
v-if="
|
||||
!currentUserHasPermission(['system:attachments:manage'])
|
||||
"
|
||||
#dropdownItems
|
||||
>
|
||||
<VButton
|
||||
v-close-popper
|
||||
block
|
||||
|
|
|
@ -132,7 +132,10 @@ onMounted(async () => {
|
|||
{{ group.spec.displayName }}
|
||||
</span>
|
||||
</div>
|
||||
<FloatingDropdown v-if="!readonly">
|
||||
<FloatingDropdown
|
||||
v-if="!readonly"
|
||||
v-permission="['system:attachments:manage']"
|
||||
>
|
||||
<IconMore @click.stop />
|
||||
<template #popper>
|
||||
<div class="w-48 p-2">
|
||||
|
@ -153,6 +156,7 @@ onMounted(async () => {
|
|||
</div>
|
||||
<div
|
||||
v-if="!loading && !readonly"
|
||||
v-permission="['system:attachments:manage']"
|
||||
class="flex cursor-pointer items-center rounded-base bg-gray-100 p-2 text-gray-500 transition-all hover:bg-gray-200 hover:text-gray-900 hover:shadow-sm"
|
||||
@click="editingModal = true"
|
||||
>
|
||||
|
|
|
@ -17,6 +17,7 @@ export default definePlugin({
|
|||
component: AttachmentList,
|
||||
meta: {
|
||||
title: "附件",
|
||||
permissions: ["system:attachments:view"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -241,7 +241,10 @@ function handleSelectUser(user: User | undefined) {
|
|||
<div
|
||||
class="relative flex flex-col items-start sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="mr-4 hidden items-center sm:flex">
|
||||
<div
|
||||
v-permission="['system:comments:manage']"
|
||||
class="mr-4 hidden items-center sm:flex"
|
||||
>
|
||||
<input
|
||||
v-model="checkAll"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||
|
|
|
@ -25,6 +25,9 @@ import ReplyListItem from "./ReplyListItem.vue";
|
|||
import { apiClient } from "@/utils/api-client";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -247,7 +250,10 @@ const subjectRefResult = computed(() => {
|
|||
<div class="absolute inset-x-0 top-0 h-[1px] bg-black/50"></div>
|
||||
<div class="absolute inset-x-0 bottom-0 h-[1px] bg-black/50"></div>
|
||||
</template>
|
||||
<template #checkbox>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:comments:manage'])"
|
||||
#checkbox
|
||||
>
|
||||
<slot name="checkbox" />
|
||||
</template>
|
||||
<template #start>
|
||||
|
@ -336,7 +342,10 @@ const subjectRefResult = computed(() => {
|
|||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:comments:manage'])"
|
||||
#dropdownItems
|
||||
>
|
||||
<VButton
|
||||
v-if="!comment?.comment.spec.approved"
|
||||
v-close-popper
|
||||
|
|
|
@ -172,6 +172,7 @@ const isHoveredReply = computed(() => {
|
|||
<template #dropdownItems>
|
||||
<VButton
|
||||
v-if="!reply?.reply.spec.approved"
|
||||
v-permission="['system:comments:manage']"
|
||||
v-close-popper
|
||||
type="secondary"
|
||||
block
|
||||
|
@ -179,7 +180,13 @@ const isHoveredReply = computed(() => {
|
|||
>
|
||||
审核通过
|
||||
</VButton>
|
||||
<VButton v-close-popper block type="danger" @click="handleDelete">
|
||||
<VButton
|
||||
v-permission="['system:comments:manage']"
|
||||
v-close-popper
|
||||
block
|
||||
type="danger"
|
||||
@click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</VButton>
|
||||
</template>
|
||||
|
|
|
@ -17,6 +17,7 @@ export default definePlugin({
|
|||
meta: {
|
||||
title: "评论",
|
||||
searchable: true,
|
||||
permissions: ["system:comments:view"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -32,6 +32,9 @@ import { apiClient } from "@/utils/api-client";
|
|||
import { formatDatetime } from "@/utils/date";
|
||||
import { RouterLink } from "vue-router";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
enum SinglePagePhase {
|
||||
DRAFT = "未发布",
|
||||
|
@ -327,7 +330,10 @@ function handleSortItemChange(sortItem?: SortItem) {
|
|||
<div
|
||||
class="relative flex flex-col items-start sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="mr-4 hidden items-center sm:flex">
|
||||
<div
|
||||
v-permission="['system:singlepages:manage']"
|
||||
class="mr-4 hidden items-center sm:flex"
|
||||
>
|
||||
<input
|
||||
v-model="checkAll"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||
|
@ -509,7 +515,11 @@ function handleSortItemChange(sortItem?: SortItem) {
|
|||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchSinglePages">刷新</VButton>
|
||||
<VButton :route="{ name: 'SinglePageEditor' }" type="primary">
|
||||
<VButton
|
||||
v-permission="['system:singlepages:manage']"
|
||||
:route="{ name: 'SinglePageEditor' }"
|
||||
type="primary"
|
||||
>
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
|
@ -525,7 +535,10 @@ function handleSortItemChange(sortItem?: SortItem) {
|
|||
>
|
||||
<li v-for="(singlePage, index) in singlePages.items" :key="index">
|
||||
<VEntity :is-selected="checkAll">
|
||||
<template #checkbox>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:singlepages:manage'])"
|
||||
#checkbox
|
||||
>
|
||||
<input
|
||||
v-model="checkAll"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||
|
@ -622,7 +635,10 @@ function handleSortItemChange(sortItem?: SortItem) {
|
|||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:singlepages:manage'])"
|
||||
#dropdownItems
|
||||
>
|
||||
<VButton
|
||||
v-close-popper
|
||||
block
|
||||
|
|
|
@ -70,7 +70,11 @@ watchEffect(() => {
|
|||
<IconPages class="mr-2 self-center" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<VButton :route="{ name: 'SinglePageEditor' }" type="secondary">
|
||||
<VButton
|
||||
v-permission="['system:singlepages:manage']"
|
||||
:route="{ name: 'SinglePageEditor' }"
|
||||
type="secondary"
|
||||
>
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
|
|
|
@ -47,6 +47,7 @@ export default definePlugin({
|
|||
meta: {
|
||||
title: "自定义页面",
|
||||
searchable: true,
|
||||
permissions: ["system:singlepages:view"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -62,6 +63,7 @@ export default definePlugin({
|
|||
meta: {
|
||||
title: "页面编辑",
|
||||
searchable: true,
|
||||
permissions: ["system:singlepages:manage"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -37,6 +37,9 @@ import { apiClient } from "@/utils/api-client";
|
|||
import { formatDatetime } from "@/utils/date";
|
||||
import { usePostCategory } from "@/modules/contents/posts/categories/composables/use-post-category";
|
||||
import { usePostTag } from "@/modules/contents/posts/tags/composables/use-post-tag";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
enum PostPhase {
|
||||
DRAFT = "未发布",
|
||||
|
@ -398,7 +401,11 @@ function handleContributorChange(user?: User) {
|
|||
<VSpace>
|
||||
<VButton :route="{ name: 'Categories' }" size="sm">分类</VButton>
|
||||
<VButton :route="{ name: 'Tags' }" size="sm">标签</VButton>
|
||||
<VButton :route="{ name: 'PostEditor' }" type="secondary">
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
:route="{ name: 'PostEditor' }"
|
||||
type="secondary"
|
||||
>
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
|
@ -415,7 +422,10 @@ function handleContributorChange(user?: User) {
|
|||
<div
|
||||
class="relative flex flex-col items-start sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="mr-4 hidden items-center sm:flex">
|
||||
<div
|
||||
v-permission="['system:posts:manage']"
|
||||
class="mr-4 hidden items-center sm:flex"
|
||||
>
|
||||
<input
|
||||
v-model="checkedAll"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||
|
@ -769,7 +779,11 @@ function handleContributorChange(user?: User) {
|
|||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchPosts">刷新</VButton>
|
||||
<VButton :route="{ name: 'PostEditor' }" type="primary">
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
:route="{ name: 'PostEditor' }"
|
||||
type="primary"
|
||||
>
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
|
@ -785,7 +799,10 @@ function handleContributorChange(user?: User) {
|
|||
>
|
||||
<li v-for="(post, index) in posts.items" :key="index">
|
||||
<VEntity :is-selected="checkSelection(post.post)">
|
||||
<template #checkbox>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:posts:manage'])"
|
||||
#checkbox
|
||||
>
|
||||
<input
|
||||
v-model="selectedPostNames"
|
||||
:value="post.post.metadata.name"
|
||||
|
@ -904,7 +921,10 @@ function handleContributorChange(user?: User) {
|
|||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:posts:manage'])"
|
||||
#dropdownItems
|
||||
>
|
||||
<VButton
|
||||
v-close-popper
|
||||
block
|
||||
|
|
|
@ -84,7 +84,11 @@ const onEditingModalClose = () => {
|
|||
</template>
|
||||
|
||||
<template #actions>
|
||||
<VButton type="secondary" @click="editingModal = true">
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
type="secondary"
|
||||
@click="editingModal = true"
|
||||
>
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
|
@ -115,7 +119,11 @@ const onEditingModalClose = () => {
|
|||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchCategories">刷新</VButton>
|
||||
<VButton type="primary" @click="editingModal = true">
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
type="primary"
|
||||
@click="editingModal = true"
|
||||
>
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
|
|
|
@ -58,6 +58,7 @@ function onDelete(category: CategoryTree) {
|
|||
<VEntity>
|
||||
<template #prepend>
|
||||
<div
|
||||
v-permission="['system:posts:manage']"
|
||||
class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex"
|
||||
>
|
||||
<IconList class="h-3.5 w-3.5" />
|
||||
|
@ -88,6 +89,7 @@ function onDelete(category: CategoryTree) {
|
|||
</template>
|
||||
<template #dropdownItems>
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
v-close-popper
|
||||
block
|
||||
type="secondary"
|
||||
|
@ -96,6 +98,7 @@ function onDelete(category: CategoryTree) {
|
|||
修改
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
v-close-popper
|
||||
block
|
||||
type="danger"
|
||||
|
|
|
@ -20,6 +20,7 @@ export default definePlugin({
|
|||
meta: {
|
||||
title: "文章",
|
||||
searchable: true,
|
||||
permissions: ["system:posts:view"],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -29,6 +30,7 @@ export default definePlugin({
|
|||
meta: {
|
||||
title: "文章编辑",
|
||||
searchable: true,
|
||||
permissions: ["system:posts:manage"],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -42,6 +44,7 @@ export default definePlugin({
|
|||
meta: {
|
||||
title: "文章分类",
|
||||
searchable: true,
|
||||
permissions: ["system:posts:view"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -57,6 +60,7 @@ export default definePlugin({
|
|||
meta: {
|
||||
title: "文章标签",
|
||||
searchable: true,
|
||||
permissions: ["system:posts:view"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -115,7 +115,11 @@ onMounted(async () => {
|
|||
<IconBookRead class="mr-2 self-center" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<VButton type="secondary" @click="editingModal = true">
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
type="secondary"
|
||||
@click="editingModal = true"
|
||||
>
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
|
@ -204,6 +208,7 @@ onMounted(async () => {
|
|||
</template>
|
||||
<template #dropdownItems>
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
v-close-popper
|
||||
block
|
||||
type="secondary"
|
||||
|
@ -212,6 +217,7 @@ onMounted(async () => {
|
|||
修改
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
v-close-popper
|
||||
block
|
||||
type="danger"
|
||||
|
|
|
@ -8,24 +8,35 @@ import {
|
|||
IconUserSettings,
|
||||
IconPalette,
|
||||
VCard,
|
||||
IconUserLine,
|
||||
} from "@halo-dev/components";
|
||||
import { markRaw, type Component } from "vue";
|
||||
import { inject, markRaw, type Component } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
import type { User } from "@halo-dev/api-client";
|
||||
|
||||
interface Action {
|
||||
icon: Component;
|
||||
title: string;
|
||||
route: RouteLocationRaw;
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
const currentUser = inject<User>("currentUser");
|
||||
|
||||
const actions: Action[] = [
|
||||
{
|
||||
icon: markRaw(IconUserLine),
|
||||
title: "个人资料",
|
||||
route: { name: "UserDetail", params: { name: currentUser?.metadata.name } },
|
||||
},
|
||||
{
|
||||
icon: markRaw(IconBookRead),
|
||||
title: "创建文章",
|
||||
route: {
|
||||
name: "PostEditor",
|
||||
},
|
||||
permissions: ["system:posts:manage"],
|
||||
},
|
||||
{
|
||||
icon: markRaw(IconPages),
|
||||
|
@ -33,6 +44,7 @@ const actions: Action[] = [
|
|||
route: {
|
||||
name: "SinglePageEditor",
|
||||
},
|
||||
permissions: ["system:singlepages:manage"],
|
||||
},
|
||||
{
|
||||
icon: markRaw(IconFolder),
|
||||
|
@ -43,6 +55,7 @@ const actions: Action[] = [
|
|||
action: "upload",
|
||||
},
|
||||
},
|
||||
permissions: ["system:attachments:manage"],
|
||||
},
|
||||
{
|
||||
icon: markRaw(IconPalette),
|
||||
|
@ -50,6 +63,7 @@ const actions: Action[] = [
|
|||
route: {
|
||||
name: "ThemeDetail",
|
||||
},
|
||||
permissions: ["system:themes:view"],
|
||||
},
|
||||
{
|
||||
icon: markRaw(IconPlug),
|
||||
|
@ -57,6 +71,7 @@ const actions: Action[] = [
|
|||
route: {
|
||||
name: "Plugins",
|
||||
},
|
||||
permissions: ["system:plugins:view"],
|
||||
},
|
||||
{
|
||||
icon: markRaw(IconUserSettings),
|
||||
|
@ -67,6 +82,7 @@ const actions: Action[] = [
|
|||
action: "create",
|
||||
},
|
||||
},
|
||||
permissions: ["system:users:manage"],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -82,6 +98,7 @@ const router = useRouter();
|
|||
<div
|
||||
v-for="(action, index) in actions"
|
||||
:key="index"
|
||||
v-permission="action.permissions"
|
||||
class="group relative cursor-pointer bg-white p-6 transition-all hover:bg-gray-50"
|
||||
@click="router.push(action.route)"
|
||||
>
|
||||
|
|
|
@ -175,6 +175,7 @@ const handleDelete = async (menuItem: MenuTreeItem) => {
|
|||
<div class="mt-4 flex sm:mt-0">
|
||||
<VSpace>
|
||||
<VButton
|
||||
v-permission="['system:menus:manage']"
|
||||
size="xs"
|
||||
type="default"
|
||||
@click="menuItemEditingModal = true"
|
||||
|
@ -194,7 +195,11 @@ const handleDelete = async (menuItem: MenuTreeItem) => {
|
|||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchMenuItems"> 刷新</VButton>
|
||||
<VButton type="primary" @click="menuItemEditingModal = true">
|
||||
<VButton
|
||||
v-permission="['system:menus:manage']"
|
||||
type="primary"
|
||||
@click="menuItemEditingModal = true"
|
||||
>
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
|
|
|
@ -12,11 +12,11 @@ import { setFocus } from "@/formkit/utils/focus";
|
|||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean;
|
||||
menu: Menu | null;
|
||||
menu?: Menu;
|
||||
}>(),
|
||||
{
|
||||
visible: false,
|
||||
menu: null,
|
||||
menu: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -10,6 +10,9 @@ import {
|
|||
import Draggable from "vuedraggable";
|
||||
import { ref } from "vue";
|
||||
import type { MenuTreeItem } from "@/modules/interface/menus/utils";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
|
@ -74,6 +77,7 @@ function getMenuItemRefDisplayName(menuItem: MenuTreeItem) {
|
|||
<VEntity>
|
||||
<template #prepend>
|
||||
<div
|
||||
v-permission="['system:menus:manage']"
|
||||
class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex"
|
||||
>
|
||||
<IconList class="h-3.5 w-3.5" />
|
||||
|
@ -98,7 +102,10 @@ function getMenuItemRefDisplayName(menuItem: MenuTreeItem) {
|
|||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:menus:manage'])"
|
||||
#dropdownItems
|
||||
>
|
||||
<VButton
|
||||
v-close-popper
|
||||
block
|
||||
|
|
|
@ -14,6 +14,9 @@ import { defineExpose, onMounted, ref } from "vue";
|
|||
import type { Menu } from "@halo-dev/api-client";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -31,13 +34,13 @@ const emit = defineEmits<{
|
|||
|
||||
const menus = ref<Menu[]>([] as Menu[]);
|
||||
const loading = ref(false);
|
||||
const selectedMenuToUpdate = ref<Menu | null>(null);
|
||||
const selectedMenuToUpdate = ref<Menu>();
|
||||
const menuEditingModal = ref<boolean>(false);
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
const handleFetchMenus = async () => {
|
||||
selectedMenuToUpdate.value = null;
|
||||
selectedMenuToUpdate.value = undefined;
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
|
@ -95,7 +98,7 @@ const handleDeleteMenu = async (menu: Menu) => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleOpenEditingModal = (menu: Menu | null) => {
|
||||
const handleOpenEditingModal = (menu?: Menu) => {
|
||||
selectedMenuToUpdate.value = menu;
|
||||
menuEditingModal.value = true;
|
||||
};
|
||||
|
@ -161,7 +164,10 @@ defineExpose({
|
|||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:menus:manage'])"
|
||||
#dropdownItems
|
||||
>
|
||||
<VButton
|
||||
v-close-popper
|
||||
block
|
||||
|
@ -182,8 +188,8 @@ defineExpose({
|
|||
</VEntity>
|
||||
</li>
|
||||
</ul>
|
||||
<template #footer>
|
||||
<VButton block type="secondary" @click="handleOpenEditingModal(null)">
|
||||
<template v-if="!currentUserHasPermission(['system:menus:manage'])" #footer>
|
||||
<VButton block type="secondary" @click="handleOpenEditingModal()">
|
||||
新增
|
||||
</VButton>
|
||||
</template>
|
||||
|
|
|
@ -17,6 +17,7 @@ export default definePlugin({
|
|||
meta: {
|
||||
title: "菜单",
|
||||
searchable: true,
|
||||
permissions: ["system:menus:view"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -57,7 +57,7 @@ const handleReloadThemeSetting = async () => {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<FloatingDropdown>
|
||||
<FloatingDropdown v-permission="['system:themes:manage']">
|
||||
<div
|
||||
class="cursor-pointer rounded p-1 transition-all hover:text-blue-600 group-hover:bg-gray-100"
|
||||
>
|
||||
|
|
|
@ -17,6 +17,9 @@ import ThemeInstallModal from "./ThemeInstallModal.vue";
|
|||
import { ref, watch } from "vue";
|
||||
import type { Theme } from "@halo-dev/api-client";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -141,7 +144,11 @@ defineExpose({
|
|||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchThemes"> 刷新</VButton>
|
||||
<VButton type="primary" @click="themeInstall = true">
|
||||
<VButton
|
||||
v-permission="['system:themes:manage']"
|
||||
type="primary"
|
||||
@click="themeInstall = true"
|
||||
>
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
|
@ -238,7 +245,10 @@ defineExpose({
|
|||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:themes:manage'])"
|
||||
#dropdownItems
|
||||
>
|
||||
<VButton
|
||||
v-close-popper
|
||||
block
|
||||
|
@ -262,7 +272,11 @@ defineExpose({
|
|||
</ul>
|
||||
<template #footer>
|
||||
<VSpace>
|
||||
<VButton type="secondary" @click="themeInstall = true">
|
||||
<VButton
|
||||
v-permission="['system:themes:manage']"
|
||||
type="secondary"
|
||||
@click="themeInstall = true"
|
||||
>
|
||||
安装主题
|
||||
</VButton>
|
||||
<VButton @click="onVisibleChange(false)">关闭</VButton>
|
||||
|
|
|
@ -156,6 +156,7 @@ watch([() => route.name, () => route.params], async () => {
|
|||
</VButton>
|
||||
<VButton
|
||||
v-if="!isActivated"
|
||||
v-permission="['system:themes:manage']"
|
||||
size="sm"
|
||||
type="primary"
|
||||
@click="handleActiveTheme"
|
||||
|
|
|
@ -20,6 +20,7 @@ export default definePlugin({
|
|||
meta: {
|
||||
title: "主题",
|
||||
searchable: true,
|
||||
permissions: ["system:themes:view"],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -28,6 +29,7 @@ export default definePlugin({
|
|||
component: ThemeSetting,
|
||||
meta: {
|
||||
title: "主题设置",
|
||||
permissions: ["system:themes:view"],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -12,6 +12,9 @@ import { toRefs } from "vue";
|
|||
import { usePluginLifeCycle } from "../composables/use-plugin";
|
||||
import type { Plugin } from "@halo-dev/api-client";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -86,12 +89,9 @@ const { isStarted, changeStatus, uninstall } = usePluginLifeCycle(plugin);
|
|||
</span>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField>
|
||||
<VEntityField v-permission="['system:plugins:manage']">
|
||||
<template #description>
|
||||
<div
|
||||
v-permission="['system:plugins:manage']"
|
||||
class="flex items-center"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<VSwitch
|
||||
:model-value="plugin?.spec.enabled"
|
||||
@click="changeStatus"
|
||||
|
@ -100,7 +100,10 @@ const { isStarted, changeStatus, uninstall } = usePluginLifeCycle(plugin);
|
|||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:plugins:manage'])"
|
||||
#dropdownItems
|
||||
>
|
||||
<VButton v-close-popper block type="danger" @click="uninstall">
|
||||
卸载
|
||||
</VButton>
|
||||
|
|
|
@ -32,6 +32,9 @@ import { useFetchRole } from "@/modules/system/roles/composables/use-role";
|
|||
import cloneDeep from "lodash.clonedeep";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import Fuse from "fuse.js";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const editingModal = ref<boolean>(false);
|
||||
const selectedRole = ref<Role>();
|
||||
|
@ -272,7 +275,10 @@ const handleDelete = async (role: Role) => {
|
|||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:roles:manage'])"
|
||||
#dropdownItems
|
||||
>
|
||||
<VButton
|
||||
v-close-popper
|
||||
block
|
||||
|
|
|
@ -26,6 +26,9 @@ import { rbacAnnotations } from "@/constants/annotations";
|
|||
import { formatDatetime } from "@/utils/date";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import Fuse from "fuse.js";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
|
@ -254,10 +257,12 @@ onMounted(() => {
|
|||
<div
|
||||
class="relative flex flex-col items-start sm:flex-row sm:items-center"
|
||||
>
|
||||
<div class="mr-4 hidden items-center sm:flex">
|
||||
<div
|
||||
v-permission="['system:users:manage']"
|
||||
class="mr-4 hidden items-center sm:flex"
|
||||
>
|
||||
<input
|
||||
v-model="checkedAll"
|
||||
v-permission="['system:users:manage']"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||
type="checkbox"
|
||||
@change="handleCheckAllChange"
|
||||
|
@ -382,10 +387,12 @@ onMounted(() => {
|
|||
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
|
||||
<li v-for="(user, index) in searchResults" :key="index">
|
||||
<VEntity :is-selected="checkSelection(user)">
|
||||
<template #checkbox>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:users:manage'])"
|
||||
#checkbox
|
||||
>
|
||||
<input
|
||||
v-model="selectedUserNames"
|
||||
v-permission="['system:users:manage']"
|
||||
:value="user.metadata.name"
|
||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600"
|
||||
name="post-checkbox"
|
||||
|
@ -438,10 +445,12 @@ onMounted(() => {
|
|||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<template
|
||||
v-if="!currentUserHasPermission(['system:users:manage'])"
|
||||
#dropdownItems
|
||||
>
|
||||
<VButton
|
||||
v-close-popper
|
||||
v-permission="['system:users:manage']"
|
||||
block
|
||||
type="secondary"
|
||||
@click="handleOpenCreateModal(user)"
|
||||
|
@ -450,7 +459,6 @@ onMounted(() => {
|
|||
</VButton>
|
||||
<VButton
|
||||
v-close-popper
|
||||
v-permission="['system:users:manage']"
|
||||
block
|
||||
@click="handleOpenPasswordChangeModal(user)"
|
||||
>
|
||||
|
@ -459,7 +467,6 @@ onMounted(() => {
|
|||
<VButton
|
||||
v-if="currentUser?.metadata.name !== user.metadata.name"
|
||||
v-close-popper
|
||||
v-permission="['system:users:manage']"
|
||||
block
|
||||
@click="handleOpenGrantPermissionModal(user)"
|
||||
>
|
||||
|
@ -468,7 +475,6 @@ onMounted(() => {
|
|||
<VButton
|
||||
v-if="currentUser?.metadata.name !== user.metadata.name"
|
||||
v-close-popper
|
||||
v-permission="['system:users:manage']"
|
||||
block
|
||||
type="danger"
|
||||
@click="handleDelete(user)"
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
import { useRoleStore } from "@/stores/role";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
||||
/**
|
||||
* It returns true if the user has all the permissions required to access a resource
|
||||
*
|
||||
* @param uiPermissions - The permissions that the user has.
|
||||
* @param targetPermissions - The permissions that the user needs to have in order to access the
|
||||
* resource.
|
||||
* @param {boolean} any - boolean - if true, the user only needs to have one of the targetPermissions
|
||||
* to pass the check.
|
||||
*/
|
||||
export function hasPermission(
|
||||
uiPermissions: Array<string>,
|
||||
targetPermissions: Array<string>,
|
||||
|
@ -24,3 +34,27 @@ export function hasPermission(
|
|||
|
||||
return !!(!any && isEqual(intersection, targetPermissions));
|
||||
}
|
||||
|
||||
interface usePermissionReturn {
|
||||
currentUserHasPermission: (targetPermissions: Array<string>) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* It returns a function that checks if the current user has a permission
|
||||
*
|
||||
* @returns An object with a function called currentUserHasPermission
|
||||
*/
|
||||
export function usePermission(): usePermissionReturn {
|
||||
const roleStore = useRoleStore();
|
||||
const { uiPermissions } = roleStore.permissions;
|
||||
|
||||
const currentUserHasPermission = (
|
||||
targetPermissions: Array<string>
|
||||
): boolean => {
|
||||
return hasPermission(uiPermissions, targetPermissions, true);
|
||||
};
|
||||
|
||||
return {
|
||||
currentUserHasPermission,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue