mirror of
https://github.com/rustfs/console.git
synced 2026-08-29 03:52:28 +08:00
refactor: migrate user group dialogs to shadcn
This commit is contained in:
@@ -1,77 +1,77 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-modal
|
||||
v-model:show="visible"
|
||||
:mask-closable="false"
|
||||
preset="card"
|
||||
:title="group.name"
|
||||
class="max-w-screen-md"
|
||||
:segmented="{
|
||||
content: true,
|
||||
action: true,
|
||||
}"
|
||||
>
|
||||
<n-card>
|
||||
<n-tabs type="card">
|
||||
<n-tab-pane name="users" :tab="t('Members')">
|
||||
<users-group-members :group="group" @search="getGroupData(group.name)"></users-group-members>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="policy" :tab="t('Policies')">
|
||||
<users-group-policies :group="group" @search="getGroupData(group.name)"></users-group-policies>
|
||||
</n-tab-pane>
|
||||
<template #suffix>
|
||||
{{ t('Status') }}
|
||||
<n-switch
|
||||
class="ml-2"
|
||||
checked-value="enabled"
|
||||
unchecked-value="disabled"
|
||||
v-model:value="group.status"
|
||||
:on-update:value="handerGroupStatusChange"
|
||||
></n-switch>
|
||||
</template>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</div>
|
||||
<AppModal v-model="visible" :title="group.name || t('Members')" size="lg" :close-on-backdrop="false">
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="flex items-center justify-between rounded-md border border-border/60 px-3 py-2">
|
||||
<span class="text-sm text-muted-foreground">{{ t('Status') }}</span>
|
||||
<AppSwitch v-model="statusBoolean" />
|
||||
</div>
|
||||
|
||||
<Tabs v-model="activeTab" class="flex flex-col gap-4">
|
||||
<TabsList class="w-full justify-start overflow-x-auto">
|
||||
<TabsTrigger value="users">{{ t('Members') }}</TabsTrigger>
|
||||
<TabsTrigger value="policy">{{ t('Policies') }}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="users" class="mt-0">
|
||||
<users-group-members :group="group" @search="getGroupData(group.name)" />
|
||||
</TabsContent>
|
||||
<TabsContent value="policy" class="mt-0">
|
||||
<users-group-policies :group="group" @search="getGroupData(group.name)" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</AppCard>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { AppCard, AppModal, AppSwitch } from '@/components/app'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n();
|
||||
const visible = ref(false);
|
||||
const { getGroup, updateGroupStatus } = useGroups();
|
||||
const { t } = useI18n()
|
||||
const visible = ref(false)
|
||||
const activeTab = ref('users')
|
||||
const { getGroup, updateGroupStatus } = useGroups()
|
||||
|
||||
interface GroupInfo {
|
||||
name: string;
|
||||
members: string[];
|
||||
status: string;
|
||||
name: string
|
||||
members: string[]
|
||||
status: string
|
||||
}
|
||||
|
||||
const group = ref<GroupInfo>({
|
||||
name: '',
|
||||
members: [],
|
||||
status: 'enabled',
|
||||
});
|
||||
})
|
||||
|
||||
// 用户组的状态发生变化
|
||||
const handerGroupStatusChange = async (val: string) => {
|
||||
await updateGroupStatus(group.value.name, { ...group.value, status: val });
|
||||
await getGroupData(group.value.name);
|
||||
};
|
||||
async function openDialog(row: any) {
|
||||
getGroupData(row.name);
|
||||
visible.value = true;
|
||||
const statusBoolean = computed({
|
||||
get: () => group.value.status === 'enabled',
|
||||
set: async value => {
|
||||
const nextStatus = value ? 'enabled' : 'disabled'
|
||||
if (nextStatus !== group.value.status) {
|
||||
await handleGroupStatusChange(nextStatus)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const handleGroupStatusChange = async (status: string) => {
|
||||
await updateGroupStatus(group.value.name, { ...group.value, status })
|
||||
await getGroupData(group.value.name)
|
||||
}
|
||||
|
||||
async function openDialog(row: any) {
|
||||
await getGroupData(row.name)
|
||||
activeTab.value = 'users'
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
async function getGroupData(name: string) {
|
||||
group.value = await getGroup(name);
|
||||
group.value = await getGroup(name)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
+150
-83
@@ -1,106 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { t } = useI18n();
|
||||
import { Icon } from '#components'
|
||||
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
visible: boolean
|
||||
}
|
||||
const { visible } = defineProps<Props>();
|
||||
const message = useMessage();
|
||||
const { $api } = useNuxtApp();
|
||||
const group = useGroups();
|
||||
const user = useUsers();
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
const defaultFormModal = {
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
(e: 'search'): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
const { updateGroupMembers } = useGroups()
|
||||
const { listUsers } = useUsers()
|
||||
|
||||
const defaultFormModel = () => ({
|
||||
group: '',
|
||||
members: [],
|
||||
};
|
||||
const formModel = ref({ ...defaultFormModal });
|
||||
members: [] as string[],
|
||||
})
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
// 用户列表
|
||||
const users = ref<any[]>([]);
|
||||
const getUserList = async () => {
|
||||
const res = await user.listUsers();
|
||||
|
||||
users.value = Object.entries(res).map(([username, info]) => ({
|
||||
label: username, // 添加用户名
|
||||
value: username, // 添加用户名
|
||||
}));
|
||||
};
|
||||
getUserList();
|
||||
const formModel = reactive(defaultFormModel())
|
||||
const submitting = ref(false)
|
||||
const memberSelectorOpen = ref(false)
|
||||
const users = ref<{ label: string; value: string }[]>([])
|
||||
|
||||
const modalVisible = computed({
|
||||
get() {
|
||||
return visible;
|
||||
},
|
||||
set(visible) {
|
||||
closeModal(visible);
|
||||
},
|
||||
});
|
||||
function closeModal(visible = false) {
|
||||
formModel.value = { ...defaultFormModal };
|
||||
emit('update:visible', visible);
|
||||
get: () => props.visible,
|
||||
set: value => closeModal(value),
|
||||
})
|
||||
|
||||
const selectedMemberLabels = computed(() => {
|
||||
const map = users.value.reduce<Record<string, string>>((acc, option) => {
|
||||
acc[option.value] = option.label
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return formModel.members.map(value => map[value] ?? value)
|
||||
})
|
||||
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
const res = await listUsers()
|
||||
users.value = Object.entries(res ?? {}).map(([username]) => ({
|
||||
label: username,
|
||||
value: username,
|
||||
}))
|
||||
} catch (error) {
|
||||
message.error(t('Failed to get data'))
|
||||
}
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (formModel.value.group.trim() === '') {
|
||||
message.error(t('Please enter user group name'));
|
||||
return;
|
||||
loadUsers()
|
||||
|
||||
const toggleMember = (value: string) => {
|
||||
if (formModel.members.includes(value)) {
|
||||
formModel.members = formModel.members.filter(item => item !== value)
|
||||
} else {
|
||||
formModel.members = [...formModel.members, value]
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = (visible = false) => {
|
||||
Object.assign(formModel, defaultFormModel())
|
||||
memberSelectorOpen.value = false
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formModel.group.trim()) {
|
||||
message.error(t('Please enter user group name'))
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await group.updateGroupMembers({
|
||||
...formModel.value,
|
||||
await updateGroupMembers({
|
||||
...formModel,
|
||||
groupStatus: 'enabled',
|
||||
isRemove: false,
|
||||
});
|
||||
})
|
||||
|
||||
message.success(t('Add success'));
|
||||
closeModal();
|
||||
emit('search');
|
||||
message.success(t('Add success'))
|
||||
closeModal()
|
||||
emit('search')
|
||||
} catch (error) {
|
||||
message.error(t('Add failed'));
|
||||
message.error(t('Add failed'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="modalVisible"
|
||||
:mask-closable="false"
|
||||
preset="card"
|
||||
:title="t('Add group members')"
|
||||
class="max-w-screen-md"
|
||||
:segmented="{
|
||||
content: true,
|
||||
action: true,
|
||||
}"
|
||||
>
|
||||
<n-card>
|
||||
<n-form label-placement="left" :model="formModel" label-align="right" :label-width="130">
|
||||
<n-grid :cols="24" :x-gap="18">
|
||||
<n-form-item-grid-item :span="24" :label="t('Name')" path="group" required>
|
||||
<n-input v-model:value="formModel.group" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="24" :label="t('Users')" path="members">
|
||||
<n-select v-model:value="formModel.members" filterable multiple :options="users" />
|
||||
</n-form-item-grid-item>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</n-card>
|
||||
<template #action>
|
||||
<n-space justify="center">
|
||||
<n-button @click="closeModal()">{{ t('Cancel') }}</n-button>
|
||||
<n-button type="primary" @click="submitForm">{{ t('Submit') }}</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
<AppModal v-model="modalVisible" :title="t('Add group members')" size="lg" :close-on-backdrop="false">
|
||||
<AppCard padded class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<Label class="text-sm font-medium">{{ t('Name') }}</Label>
|
||||
<AppInput v-model="formModel.group" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<style scoped></style>
|
||||
<div class="space-y-2">
|
||||
<Label class="text-sm font-medium">{{ t('Users') }}</Label>
|
||||
<Popover v-model:open="memberSelectorOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="min-h-10 w-full justify-between gap-2"
|
||||
:aria-label="t('Users')"
|
||||
>
|
||||
<span class="truncate">
|
||||
{{
|
||||
selectedMemberLabels.length ? selectedMemberLabels.join(', ') : t('Select user group members')
|
||||
}}
|
||||
</span>
|
||||
<Icon class="size-4 text-muted-foreground" name="ri:arrow-down-s-line" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-72 p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput :placeholder="t('Search User')" />
|
||||
<CommandList>
|
||||
<CommandEmpty>{{ t('No Data') }}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="option in users"
|
||||
:key="option.value"
|
||||
:value="option.label"
|
||||
@select="() => toggleMember(option.value)"
|
||||
>
|
||||
<Icon
|
||||
name="ri:check-line"
|
||||
class="mr-2 size-4"
|
||||
:class="formModel.members.includes(option.value) ? 'opacity-100' : 'opacity-0'"
|
||||
/>
|
||||
<span>{{ option.label }}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div v-if="formModel.members.length" class="flex flex-wrap gap-2">
|
||||
<Badge v-for="value in formModel.members" :key="value" variant="secondary">
|
||||
{{ value }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="closeModal()">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="submitForm">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
+187
-116
@@ -1,134 +1,205 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card>
|
||||
<n-form ref="formRef" :model="searchForm" label-placement="left" :show-feedback="false">
|
||||
<n-flex justify="space-between" v-if="!editStatus">
|
||||
<n-form-item class="!w-64" label="" path="name">
|
||||
<n-input :placeholder="t('Search Policy')" @input="filterName" />
|
||||
</n-form-item>
|
||||
<AppCard padded class="space-y-4">
|
||||
<div v-if="!editStatus" class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="w-full sm:max-w-xs">
|
||||
<AppInput v-model="searchTerm" :placeholder="t('Search Policy')" />
|
||||
</div>
|
||||
<AppButton variant="secondary" class="inline-flex items-center gap-2" @click="startEditing">
|
||||
<Icon class="size-4" name="ri:add-line" />
|
||||
{{ t('Edit Policy') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
|
||||
<n-space>
|
||||
<NFlex>
|
||||
<NButton secondary @click="editStatus = true">
|
||||
<template #icon>
|
||||
<Icon name="ri:add-line"></Icon>
|
||||
</template>
|
||||
{{ t('Edit Policy') }}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
<n-flex justify="space-between" v-else>
|
||||
<n-form-item class="!w-96" :label="t('Select user group policies')" path="policies">
|
||||
<n-select v-model:value="name" filterable multiple :options="polices" />
|
||||
</n-form-item>
|
||||
<n-space>
|
||||
<NFlex>
|
||||
<NButton secondary @click="changePolicies">{{ t('Submit') }}</NButton>
|
||||
</NFlex>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
</n-form>
|
||||
</n-card>
|
||||
<n-data-table
|
||||
class="my-4"
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:data="listData"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<Label class="text-sm font-medium">{{ t('Select user group policies') }}</Label>
|
||||
<Popover v-model:open="policySelectorOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="min-h-10 justify-between gap-2"
|
||||
:aria-label="t('Select user group policies')"
|
||||
>
|
||||
<span class="truncate">
|
||||
{{ selectedPolicyLabels.length ? selectedPolicyLabels.join(', ') : t('Select user group policies') }}
|
||||
</span>
|
||||
<Icon class="size-4 text-muted-foreground" name="ri:arrow-down-s-line" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-72 p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput :placeholder="t('Search Policy')" />
|
||||
<CommandList>
|
||||
<CommandEmpty>{{ t('No Data') }}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="option in policies"
|
||||
:key="option.value"
|
||||
:value="option.label"
|
||||
@select="() => togglePolicy(option.value)"
|
||||
>
|
||||
<Icon
|
||||
name="ri:check-line"
|
||||
class="mr-2 size-4"
|
||||
:class="selectedPolicies.includes(option.value) ? 'opacity-100' : 'opacity-0'"
|
||||
/>
|
||||
<span>{{ option.label }}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div v-if="selectedPolicies.length" class="flex flex-wrap gap-2">
|
||||
<Badge v-for="value in selectedPolicies" :key="value" variant="secondary">{{ value }}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 sm:self-start">
|
||||
<AppButton variant="ghost" @click="cancelEditing">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="secondary" @click="changePolicies">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table class="overflow-hidden rounded-lg border">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{{ t('Name') }}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody v-if="filteredPolicies.length">
|
||||
<TableRow v-for="policy in filteredPolicies" :key="policy">
|
||||
<TableCell class="font-medium">{{ policy }}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
<TableBody v-else>
|
||||
<TableRow>
|
||||
<TableCell class="text-center" colspan="1">
|
||||
<p class="py-6 text-sm text-muted-foreground">{{ t('No Data') }}</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</AppCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type DataTableColumns, type DataTableInst, NButton, NSpace } from 'naive-ui';
|
||||
const { listPolicies, setUserOrGroupPolicy } = usePolicies();
|
||||
const { t } = useI18n();
|
||||
import { Icon } from '#components'
|
||||
import { AppButton, AppCard, AppInput } from '@/components/app'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const messge = useMessage();
|
||||
const props = defineProps({
|
||||
const { listPolicies, setUserOrGroupPolicy } = usePolicies()
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const props = defineProps<{
|
||||
group: {
|
||||
type: Object,
|
||||
required: true,
|
||||
name: string
|
||||
policy?: string
|
||||
}
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ (e: 'search'): void }>()
|
||||
|
||||
const searchTerm = ref('')
|
||||
const editStatus = ref(false)
|
||||
const policySelectorOpen = ref(false)
|
||||
const policies = ref<{ label: string; value: string }[]>([])
|
||||
const selectedPolicies = ref<string[]>([])
|
||||
|
||||
const currentPolicies = computed(() => {
|
||||
if (!props.group?.policy) return []
|
||||
return props.group.policy.split(',').map(item => item.trim()).filter(Boolean)
|
||||
})
|
||||
|
||||
const filteredPolicies = computed(() => {
|
||||
const keyword = searchTerm.value.trim().toLowerCase()
|
||||
const source = currentPolicies.value
|
||||
if (!keyword) return source
|
||||
return source.filter(policy => policy.toLowerCase().includes(keyword))
|
||||
})
|
||||
|
||||
const policyLabelMap = computed(() => {
|
||||
return policies.value.reduce<Record<string, string>>((acc, option) => {
|
||||
acc[option.value] = option.label
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
|
||||
const selectedPolicyLabels = computed(() => selectedPolicies.value.map(value => policyLabelMap.value[value] ?? value))
|
||||
|
||||
watch(
|
||||
() => props.group?.policy,
|
||||
() => {
|
||||
if (!editStatus.value) {
|
||||
selectedPolicies.value = [...currentPolicies.value]
|
||||
}
|
||||
},
|
||||
});
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
});
|
||||
interface RowData {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<RowData> = [
|
||||
{
|
||||
title: t('Name'),
|
||||
align: 'left',
|
||||
key: 'name',
|
||||
filter(value, row) {
|
||||
return !!row.name.includes(value.toString());
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 搜索过滤
|
||||
const tableRef = ref<DataTableInst>();
|
||||
function filterName(value: string) {
|
||||
tableRef.value &&
|
||||
tableRef.value.filter({
|
||||
name: [value],
|
||||
});
|
||||
}
|
||||
const listData = computed(() => {
|
||||
if (!props.group?.policy) return [];
|
||||
return (
|
||||
props.group?.policy?.split(',').map((item: string) => {
|
||||
return {
|
||||
name: item,
|
||||
};
|
||||
}) || []
|
||||
);
|
||||
});
|
||||
|
||||
/********************编辑****************/
|
||||
const editStatus = ref(false);
|
||||
// 策略列表
|
||||
const polices = ref<any[]>([]);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search'): void;
|
||||
}>();
|
||||
const getPoliciesList = async () => {
|
||||
const res = await listPolicies();
|
||||
polices.value = Object.keys(res)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map(key => {
|
||||
return {
|
||||
const loadPolicies = async () => {
|
||||
try {
|
||||
const res = await listPolicies()
|
||||
policies.value = Object.keys(res ?? {})
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map(key => ({
|
||||
label: key,
|
||||
value: key,
|
||||
};
|
||||
});
|
||||
};
|
||||
getPoliciesList();
|
||||
}))
|
||||
} catch (error) {
|
||||
message.error(t('Failed to get data'))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPolicies()
|
||||
})
|
||||
|
||||
const startEditing = () => {
|
||||
selectedPolicies.value = [...currentPolicies.value]
|
||||
policySelectorOpen.value = false
|
||||
editStatus.value = true
|
||||
}
|
||||
|
||||
const cancelEditing = () => {
|
||||
selectedPolicies.value = [...currentPolicies.value]
|
||||
policySelectorOpen.value = false
|
||||
editStatus.value = false
|
||||
}
|
||||
|
||||
const togglePolicy = (value: string) => {
|
||||
if (selectedPolicies.value.includes(value)) {
|
||||
selectedPolicies.value = selectedPolicies.value.filter(item => item !== value)
|
||||
} else {
|
||||
selectedPolicies.value = [...selectedPolicies.value, value]
|
||||
}
|
||||
}
|
||||
|
||||
// const name = ref(props.group?.policy?.split(',') || [])
|
||||
const name = ref(props.group?.policy ? props.group?.policy?.split(',') : []);
|
||||
const changePolicies = async () => {
|
||||
try {
|
||||
await setUserOrGroupPolicy({
|
||||
policyName: name.value || '',
|
||||
userOrGroup: props.group.name || '',
|
||||
policyName: selectedPolicies.value,
|
||||
userOrGroup: props.group?.name ?? '',
|
||||
isGroup: true,
|
||||
});
|
||||
|
||||
messge.success('修改成功');
|
||||
editStatus.value = false;
|
||||
emit('search');
|
||||
} catch {
|
||||
messge.error('修改失败');
|
||||
})
|
||||
message.success(t('Edit Success'))
|
||||
editStatus.value = false
|
||||
policySelectorOpen.value = false
|
||||
emit('search')
|
||||
} catch (error) {
|
||||
message.error(t('Edit Failed'))
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,141 +1,152 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-modal
|
||||
v-model:show="visible"
|
||||
:mask-closable="false"
|
||||
preset="card"
|
||||
:title="t('Batch allocation policies')"
|
||||
class="w-1/2"
|
||||
:segmented="{
|
||||
content: true,
|
||||
action: true,
|
||||
}"
|
||||
>
|
||||
<n-card>
|
||||
<n-form ref="formRef" :model="searchForm" label-placement="left" :show-feedback="false">
|
||||
<n-flex justify="space-between">
|
||||
<n-form-item class="!w-64" label="" path="name">
|
||||
<n-input :placeholder="t('Search Policy')" @input="filterName" />
|
||||
</n-form-item>
|
||||
<AppModal v-model="visible" :title="t('Batch allocation policies')" size="xl" :close-on-backdrop="false">
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="w-full sm:max-w-xs">
|
||||
<AppInput v-model="searchTerm" :placeholder="t('Search Policy')" />
|
||||
</div>
|
||||
<AppButton variant="secondary" :disabled="!checkedKeys.length || submitting" @click="changePolicies">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
|
||||
<n-space>
|
||||
<NFlex>
|
||||
<NButton secondary @click="changePolicies">{{ t('Submit') }}</NButton>
|
||||
</NFlex>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
</n-form>
|
||||
</n-card>
|
||||
<n-data-table
|
||||
class="my-4"
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:data="listData"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
:row-key="rowKey"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
/>
|
||||
</n-modal>
|
||||
</div>
|
||||
<Table class="overflow-hidden rounded-lg border">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-12">
|
||||
<AppCheckbox
|
||||
:checked="allVisibleSelected"
|
||||
:indeterminate="checkedKeys.length > 0 && !allVisibleSelected"
|
||||
@update:checked="toggleSelectAll"
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>{{ t('Name') }}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody v-if="filteredPolicies.length">
|
||||
<TableRow v-for="policy in filteredPolicies" :key="policy.name">
|
||||
<TableCell>
|
||||
<AppCheckbox :checked="isSelected(policy.name)" @update:checked="value => toggleSelection(policy.name, value)" />
|
||||
</TableCell>
|
||||
<TableCell class="font-medium">{{ policy.name }}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
<TableBody v-else>
|
||||
<TableRow>
|
||||
<TableCell class="text-center" colspan="2">
|
||||
<p class="py-6 text-sm text-muted-foreground">{{ t('No Data') }}</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</AppCard>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type DataTableColumns, type DataTableInst, type DataTableRowKey, NButton, NSpace } from 'naive-ui';
|
||||
const { listPolicies, setUserOrGroupPolicy } = usePolicies();
|
||||
const { t } = useI18n();
|
||||
import { AppButton, AppCard, AppCheckbox, AppInput, AppModal } from '@/components/app'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps({
|
||||
checkedKeys: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const messge = useMessage();
|
||||
const { listPolicies, setUserOrGroupPolicy } = usePolicies()
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const visible = ref(false);
|
||||
async function openDialog() {
|
||||
visible.value = true;
|
||||
const props = defineProps<{
|
||||
checkedKeys: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'changePoliciesSuccess'): void
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const searchTerm = ref('')
|
||||
const submitting = ref(false)
|
||||
const policies = ref<{ name: string; content: unknown }[]>([])
|
||||
const checkedKeys = ref<string[]>([])
|
||||
|
||||
const filteredPolicies = computed(() => {
|
||||
const keyword = searchTerm.value.trim().toLowerCase()
|
||||
if (!keyword) return policies.value
|
||||
return policies.value.filter(policy => policy.name.toLowerCase().includes(keyword))
|
||||
})
|
||||
|
||||
const allVisibleSelected = computed(() => {
|
||||
const visibleNames = filteredPolicies.value.map(item => item.name)
|
||||
if (!visibleNames.length) return false
|
||||
return visibleNames.every(name => checkedKeys.value.includes(name))
|
||||
})
|
||||
|
||||
const isSelected = (name: string) => checkedKeys.value.includes(name)
|
||||
|
||||
const toggleSelection = (name: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
if (!checkedKeys.value.includes(name)) {
|
||||
checkedKeys.value = [...checkedKeys.value, name]
|
||||
}
|
||||
} else {
|
||||
checkedKeys.value = checkedKeys.value.filter(item => item !== name)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSelectAll = (checked: boolean) => {
|
||||
const visibleNames = filteredPolicies.value.map(item => item.name)
|
||||
if (!visibleNames.length) return
|
||||
if (checked) {
|
||||
const merged = new Set([...checkedKeys.value, ...visibleNames])
|
||||
checkedKeys.value = Array.from(merged)
|
||||
} else {
|
||||
checkedKeys.value = checkedKeys.value.filter(name => !visibleNames.includes(name))
|
||||
}
|
||||
}
|
||||
|
||||
const loadPolicies = async () => {
|
||||
try {
|
||||
const res = await listPolicies()
|
||||
policies.value = Object.keys(res ?? {})
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map(key => ({
|
||||
name: key,
|
||||
content: res[key],
|
||||
}))
|
||||
} catch (error) {
|
||||
message.error(t('Failed to get data'))
|
||||
}
|
||||
}
|
||||
|
||||
const openDialog = async () => {
|
||||
visible.value = true
|
||||
checkedKeys.value = []
|
||||
searchTerm.value = ''
|
||||
await loadPolicies()
|
||||
}
|
||||
|
||||
const changePolicies = async () => {
|
||||
if (!checkedKeys.value.length) return
|
||||
submitting.value = true
|
||||
try {
|
||||
await Promise.all(
|
||||
props.checkedKeys.map(item =>
|
||||
setUserOrGroupPolicy({
|
||||
policyName: checkedKeys.value,
|
||||
userOrGroup: item,
|
||||
isGroup: true,
|
||||
})
|
||||
)
|
||||
)
|
||||
message.success(t('Edit Success'))
|
||||
visible.value = false
|
||||
emit('changePoliciesSuccess')
|
||||
} catch (error) {
|
||||
message.error(t('Edit Failed'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
});
|
||||
interface RowData {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<RowData> = [
|
||||
{
|
||||
type: 'selection',
|
||||
},
|
||||
{
|
||||
title: t('Name'),
|
||||
align: 'left',
|
||||
key: 'name',
|
||||
filter(value, row) {
|
||||
return !!row.name.includes(value.toString());
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 搜索过滤
|
||||
const tableRef = ref<DataTableInst>();
|
||||
function filterName(value: string) {
|
||||
tableRef.value &&
|
||||
tableRef.value.filter({
|
||||
name: [value],
|
||||
});
|
||||
}
|
||||
const listData = ref<any[]>([]);
|
||||
const getPoliciesList = async () => {
|
||||
const res = await listPolicies();
|
||||
listData.value = Object.keys(res)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map(key => {
|
||||
return {
|
||||
name: key,
|
||||
content: res[key],
|
||||
};
|
||||
});
|
||||
};
|
||||
getPoliciesList();
|
||||
|
||||
/********************编辑****************/
|
||||
function rowKey(row: any): string {
|
||||
return row.name;
|
||||
}
|
||||
|
||||
const checkedKeys = ref<DataTableRowKey[]>([]);
|
||||
function handleCheck(keys: DataTableRowKey[]) {
|
||||
checkedKeys.value = keys;
|
||||
return checkedKeys;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['changePoliciesSuccess']);
|
||||
const changePolicies = async () => {
|
||||
Promise.all(
|
||||
props.checkedKeys.map(item => {
|
||||
return setUserOrGroupPolicy({
|
||||
policyName: checkedKeys.value,
|
||||
userOrGroup: item,
|
||||
isGroup: true,
|
||||
});
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
visible.value = false;
|
||||
messge.success(t('Edit Success'));
|
||||
emit('changePoliciesSuccess');
|
||||
})
|
||||
.catch(() => {
|
||||
messge.error(t('Edit Failed'));
|
||||
});
|
||||
};
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
Reference in New Issue
Block a user