初始化
This commit is contained in:
194
src/views/system/role/modules/role-operate-dialog.vue
Normal file
194
src/views/system/role/modules/role-operate-dialog.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { commonStatusOptions } from '@/constants/business';
|
||||
import { fetchCreateRole, fetchGetRole, fetchUpdateRole } from '@/service/api';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'RoleOperateDialog' });
|
||||
|
||||
interface Props {
|
||||
operateType: UI.TableOperateType;
|
||||
rowData?: Api.SystemManage.Role | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted', roleId: number): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const detailLoading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const isEdit = computed(() => props.operateType === 'edit');
|
||||
|
||||
const title = computed(() => {
|
||||
const titleMap: Record<UI.TableOperateType, string> = {
|
||||
add: $t('page.system.role.addRole'),
|
||||
edit: $t('page.system.role.editRole')
|
||||
};
|
||||
|
||||
return titleMap[props.operateType];
|
||||
});
|
||||
|
||||
type Model = Api.SystemManage.SaveRoleParams;
|
||||
|
||||
const model = ref(createDefaultModel());
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
name: '',
|
||||
code: '',
|
||||
sort: 0,
|
||||
status: 0,
|
||||
remark: ''
|
||||
};
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: createRequiredRule($t('page.system.role.form.roleName')),
|
||||
code: createRequiredRule($t('page.system.role.form.roleCode')),
|
||||
sort: createRequiredRule($t('page.system.role.form.sort')),
|
||||
status: createRequiredRule($t('page.system.role.form.roleStatus'))
|
||||
} satisfies Record<string, App.Global.FormRule>;
|
||||
|
||||
function closeModal() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function initModel() {
|
||||
model.value = createDefaultModel();
|
||||
|
||||
if (!isEdit.value || !props.rowData) {
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
return;
|
||||
}
|
||||
|
||||
detailLoading.value = true;
|
||||
|
||||
const { error, data } = await fetchGetRole(props.rowData.id);
|
||||
|
||||
detailLoading.value = false;
|
||||
|
||||
if (!error) {
|
||||
model.value = {
|
||||
name: data.name,
|
||||
code: data.code,
|
||||
sort: data.sort,
|
||||
status: data.status,
|
||||
remark: data.remark ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
const submitData: Api.SystemManage.SaveRoleParams = {
|
||||
...model.value,
|
||||
name: model.value.name.trim(),
|
||||
code: model.value.code.trim(),
|
||||
remark: model.value.remark?.trim() || null
|
||||
};
|
||||
|
||||
const request =
|
||||
isEdit.value && props.rowData
|
||||
? fetchUpdateRole({ id: props.rowData.id, ...submitData })
|
||||
: fetchCreateRole(submitData);
|
||||
|
||||
const { error, data } = await request;
|
||||
|
||||
submitting.value = false;
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
const roleId = isEdit.value && props.rowData ? props.rowData.id : Number(data);
|
||||
|
||||
window.$message?.success($t(isEdit.value ? 'common.updateSuccess' : 'common.addSuccess'));
|
||||
|
||||
closeModal();
|
||||
emit('submitted', roleId);
|
||||
}
|
||||
|
||||
watch(visible, value => {
|
||||
if (value) {
|
||||
initModel();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
preset="md"
|
||||
:loading="detailLoading"
|
||||
:confirm-loading="submitting"
|
||||
:scrollbar="false"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.system.role.roleName')" prop="name">
|
||||
<ElInput v-model="model.name" :placeholder="$t('page.system.role.form.roleName')" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.system.role.roleCode')" prop="code">
|
||||
<ElInput v-model="model.code" :placeholder="$t('page.system.role.form.roleCode')" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.system.role.sort')" prop="sort">
|
||||
<ElInputNumber
|
||||
v-model="model.sort"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
:placeholder="$t('page.system.role.form.sort')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.system.role.roleStatus')" prop="status">
|
||||
<ElRadioGroup v-model="model.status" class="business-form-radio-group">
|
||||
<ElRadio v-for="{ label, value } in commonStatusOptions" :key="value" :value="value">
|
||||
{{ $t(label) }}
|
||||
</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem :label="$t('page.system.role.remark')" prop="remark">
|
||||
<ElInput
|
||||
v-model="model.remark"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:placeholder="$t('page.system.role.form.remark')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
238
src/views/system/role/modules/role-resource-panel.vue
Normal file
238
src/views/system/role/modules/role-resource-panel.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { TreeInstance } from 'element-plus';
|
||||
import { menuTypeRecord } from '@/constants/business';
|
||||
import { fetchAssignRoleMenus, fetchGetRoleMenuIds } from '@/service/api';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'RoleResourcePanel' });
|
||||
|
||||
interface Props {
|
||||
role: Api.SystemManage.Role | null;
|
||||
menuTree: Api.SystemManage.MenuSimple[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'saved'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const treeRef = ref<TreeInstance | null>(null);
|
||||
const permissionLoading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const filterKeyword = ref('');
|
||||
const checkedKeys = ref<number[]>([]);
|
||||
|
||||
const disabled = computed(() => !props.role || props.role.status === 1);
|
||||
const checkedCount = computed(() => checkedKeys.value.length);
|
||||
const defaultExpandedKeys = computed(() => collectExpandableNodeIds(props.menuTree));
|
||||
const treeRenderKey = computed(() => `${props.role?.id ?? 'empty'}:${defaultExpandedKeys.value.join(',')}`);
|
||||
|
||||
const treeProps = {
|
||||
children: 'children',
|
||||
label: 'name'
|
||||
} as const;
|
||||
|
||||
function getTagType(type: Api.SystemManage.MenuType): UI.ThemeColor {
|
||||
const tagMap: Record<Api.SystemManage.MenuType, UI.ThemeColor> = {
|
||||
1: 'info',
|
||||
2: 'primary',
|
||||
3: 'warning'
|
||||
};
|
||||
|
||||
return tagMap[type];
|
||||
}
|
||||
|
||||
function getMenuTypeLabel(type: number) {
|
||||
return $t(menuTypeRecord[type as Api.SystemManage.MenuType]);
|
||||
}
|
||||
|
||||
function filterNode(value: string, data: any) {
|
||||
const node = data as Api.SystemManage.MenuSimple;
|
||||
|
||||
if (!value.trim()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return node.name.toLowerCase().includes(value.trim().toLowerCase());
|
||||
}
|
||||
|
||||
function collectExpandableNodeIds(nodes: Api.SystemManage.MenuSimple[]) {
|
||||
const ids: number[] = [];
|
||||
|
||||
const walk = (items: Api.SystemManage.MenuSimple[]) => {
|
||||
items.forEach(item => {
|
||||
const children = item.children ?? [];
|
||||
const hasNonButtonChild = children.some(child => child.type !== 3);
|
||||
|
||||
if (hasNonButtonChild) {
|
||||
ids.push(item.id);
|
||||
walk(children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
walk(nodes);
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
async function loadRoleMenus() {
|
||||
if (!props.role) {
|
||||
checkedKeys.value = [];
|
||||
treeRef.value?.setCheckedKeys([]);
|
||||
treeRef.value?.filter(filterKeyword.value);
|
||||
return;
|
||||
}
|
||||
|
||||
permissionLoading.value = true;
|
||||
|
||||
const { error, data } = await fetchGetRoleMenuIds(props.role.id);
|
||||
|
||||
permissionLoading.value = false;
|
||||
|
||||
if (error) {
|
||||
checkedKeys.value = [];
|
||||
treeRef.value?.setCheckedKeys([]);
|
||||
treeRef.value?.filter(filterKeyword.value);
|
||||
return;
|
||||
}
|
||||
|
||||
checkedKeys.value = data;
|
||||
treeRef.value?.setCheckedKeys(data);
|
||||
treeRef.value?.filter(filterKeyword.value);
|
||||
}
|
||||
|
||||
function handleCheck() {
|
||||
checkedKeys.value = (treeRef.value?.getCheckedKeys(false) as number[]) ?? [];
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!props.role) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuIds = (treeRef.value?.getCheckedKeys(false) as number[]) ?? [];
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
const { error } = await fetchAssignRoleMenus({
|
||||
roleId: props.role.id,
|
||||
menuIds
|
||||
});
|
||||
|
||||
submitting.value = false;
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkedKeys.value = menuIds;
|
||||
|
||||
window.$message?.success($t('common.modifySuccess'));
|
||||
emit('saved');
|
||||
}
|
||||
|
||||
watch(filterKeyword, value => {
|
||||
treeRef.value?.filter(value);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.role?.id,
|
||||
() => {
|
||||
loadRoleMenus();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.menuTree.length,
|
||||
value => {
|
||||
if (value && props.role) {
|
||||
treeRef.value?.setCheckedKeys(checkedKeys.value);
|
||||
treeRef.value?.filter(filterKeyword.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCard class="h-full card-wrapper" body-class="role-resource-card-body">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-12px">
|
||||
<p>{{ $t('page.system.role.resourceAuth') }}</p>
|
||||
<ElButton type="primary" size="small" :disabled="disabled" :loading="submitting" @click="handleSave">
|
||||
{{ $t('page.system.role.saveAuth') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="role">
|
||||
<div class="mb-12px flex flex-col gap-10px">
|
||||
<ElInput v-model="filterKeyword" clearable :placeholder="$t('page.system.role.form.resourceKeyword')">
|
||||
<template #prefix>
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
</template>
|
||||
</ElInput>
|
||||
<div class="flex items-center gap-8px text-13px text-[#606266]">
|
||||
<span>{{ $t('page.system.role.selectedCount') }}</span>
|
||||
<ElTag type="primary" effect="plain">{{ checkedCount }}</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElAlert
|
||||
v-if="role.status === 1"
|
||||
:title="$t('page.system.role.disabledTip')"
|
||||
type="warning"
|
||||
class="mb-12px"
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-loading="permissionLoading || props.loading"
|
||||
:class="{ 'pointer-events-none opacity-70': disabled }"
|
||||
class="min-h-0 flex-1 overflow-hidden border border-[#ebeef5] rounded-12px bg-[#fcfdff]"
|
||||
>
|
||||
<ElScrollbar class="h-full px-12px py-12px">
|
||||
<ElTree
|
||||
:key="treeRenderKey"
|
||||
ref="treeRef"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
:default-expanded-keys="defaultExpandedKeys"
|
||||
:data="menuTree"
|
||||
:props="treeProps"
|
||||
:filter-node-method="filterNode"
|
||||
:check-on-click-node="true"
|
||||
@check="handleCheck"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="min-w-0 flex items-center gap-8px">
|
||||
<span class="truncate text-14px">{{ data.name }}</span>
|
||||
<ElTag size="small" effect="plain" :type="getTagType(data.type)">
|
||||
{{ getMenuTypeLabel(data.type) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTree>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="h-full flex items-center justify-center">
|
||||
<ElEmpty :description="$t('page.system.role.emptyRole')" />
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.role-resource-card-body) {
|
||||
height: calc(100% - 56px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
55
src/views/system/role/modules/role-search.vue
Normal file
55
src/views/system/role/modules/role-search.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { commonStatusOptions } from '@/constants/business';
|
||||
import TableSearchPanel from '@/components/custom/table-search-panel.vue';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'RoleSearch' });
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<Api.SystemManage.RoleSearchParams>('model', { required: true });
|
||||
|
||||
const keyword = computed({
|
||||
get() {
|
||||
return model.value.name ?? model.value.code ?? '';
|
||||
},
|
||||
set(value: string) {
|
||||
const text = value.trim() || undefined;
|
||||
model.value.name = text;
|
||||
model.value.code = text;
|
||||
}
|
||||
});
|
||||
|
||||
function reset() {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function search() {
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableSearchPanel :model="model" :action-col-lg="8" @reset="reset" @search="search">
|
||||
<ElCol :lg="8" :md="12" :sm="12">
|
||||
<ElFormItem :label="$t('page.system.role.searchKeyword')" prop="name">
|
||||
<ElInput v-model="keyword" clearable :placeholder="$t('page.system.role.searchPlaceholder')" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="8" :md="12" :sm="12">
|
||||
<ElFormItem :label="$t('page.system.role.roleStatus')" prop="status">
|
||||
<ElSelect v-model="model.status" clearable :placeholder="$t('page.system.role.form.roleStatus')">
|
||||
<ElOption v-for="{ label, value } in commonStatusOptions" :key="value" :label="$t(label)" :value="value" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</TableSearchPanel>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user