初始化

This commit is contained in:
2026-03-26 20:18:20 +08:00
commit 120a5b4dfd
368 changed files with 35926 additions and 0 deletions

View 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>

View 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>

View 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>