2
0

初始化项目

This commit is contained in:
caiyuchao
2024-11-14 11:06:38 +08:00
parent 988b9e6799
commit 4ffac789e1
320 changed files with 34244 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
<script setup lang="tsx">
import { Button, Popconfirm, Tag } from 'ant-design-vue';
import type { Key } from 'ant-design-vue/es/_util/type';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { $t } from '@/locales';
import { enableStatusRecord } from '@/constants/business';
import RoleOperateDrawer from './modules/role-operate-drawer.vue';
import RoleSearch from './modules/role-search.vue';
const wrapperEl = shallowRef<HTMLElement | null>(null);
const { height: wrapperElHeight } = useElementSize(wrapperEl);
const scrollConfig = computed(() => {
return {
y: wrapperElHeight.value - 72,
x: 702
};
});
const { columns, columnChecks, data, loading, getData, mobilePagination, searchParams, resetSearchParams } = useTable({
apiFn: doGetRoleList,
apiParams: {
pageNum: 1,
pageSize: 10,
status: undefined,
roleName: undefined,
roleKey: undefined
},
rowKey: 'roleId',
columns: () => [
{
key: 'roleKey',
dataIndex: 'roleKey',
title: $t('page.manage.role.roleCode'),
align: 'center'
},
{
key: 'roleName',
dataIndex: 'roleName',
title: $t('page.manage.role.roleName'),
align: 'center'
},
{
key: 'status',
dataIndex: 'status',
title: $t('page.manage.role.roleStatus'),
align: 'center',
customRender: ({ record }) => {
if (record.status === null) {
return null;
}
const tagMap: Record<Api.Common.EnableStatus, string> = {
'0': 'success',
'1': 'warning'
};
const label = $t(enableStatusRecord[record.status]);
return <Tag color={tagMap[record.status]}>{label}</Tag>;
}
},
{
key: 'remark',
dataIndex: 'remark',
title: $t('page.manage.role.roleDesc')
},
{
key: 'createTime',
dataIndex: 'createTime',
align: 'center',
title: '创建时间'
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 200,
customRender: ({ record }) =>
!record.admin && (
<div class="flex justify-around gap-8px">
{isShowBtn('system:role:edit') && (
<Button size="small" onClick={() => edit(record.roleId)}>
{$t('common.edit')}
</Button>
)}
{isShowBtn('system:role:remove') && (
<Popconfirm onConfirm={() => handleDelete(record.roleId)} title={$t('common.confirmDelete')}>
<Button danger size="small">
{$t('common.delete')}
</Button>
</Popconfirm>
)}
</div>
)
}
]
});
const {
drawerVisible,
operateType,
editingData,
handleAdd,
handleEdit,
checkedRowKeys,
onBatchDeleted,
onDeleted
// closeDrawer
} = useTableOperate(data, { getData, idKey: 'roleId' });
async function handleBatchDelete() {
const { error } = await doDeleteRole(checkedRowKeys.value.join(','));
if (!error) {
onBatchDeleted();
}
}
async function handleDelete(id: number) {
const { error } = await doDeleteRole(id);
if (!error) {
onDeleted();
}
}
function edit(id: number) {
handleEdit(id);
}
function handleRoleSelectChange(selectedRowKeys: Key[]) {
checkedRowKeys.value = selectedRowKeys as number[];
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<RoleSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
<ACard
:title="$t('page.manage.role.title')"
:bordered="false"
:body-style="{ flex: 1, overflow: 'hidden' }"
class="flex-col-stretch sm:flex-1-hidden card-wrapper"
>
<template #extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:show-delete="true"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="getData"
/>
</template>
<ATable
ref="wrapperEl"
:columns="columns"
:data-source="data"
:loading="loading"
:row-selection="{
selectedRowKeys: checkedRowKeys,
onChange: handleRoleSelectChange,
getCheckboxProps: record => ({ disabled: record.admin })
}"
row-key="id"
size="small"
:pagination="mobilePagination"
:scroll="scrollConfig"
class="h-full"
/>
<RoleOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getData"
/>
</ACard>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,123 @@
<script setup lang="ts">
import type { DataNode } from 'ant-design-vue/es/tree';
import { SimpleScrollbar } from '@sa/materials';
defineOptions({
name: 'MenuAuthModal'
});
interface Props {
/** the roleId */
roleId: number;
type: 'add' | 'edit';
drawerVisible: boolean;
}
const props = defineProps<Props>();
const menuIds = defineModel<number[]>('menuIds', { default: [] });
const tree = shallowRef<DataNode[]>([]);
watch(
() => props.drawerVisible,
val => {
if (val) {
init();
}
},
{
immediate: true
}
);
async function getTree() {
const { error, data } = await fetchGetMenuTree();
if (!error) {
tree.value = recursiveTransform(data);
}
}
function recursiveTransform(data: Api.SystemManage.MenuTree[]): DataNode[] {
return data.map(item => {
const { id: key, label } = item;
if (item.children) {
return {
key,
title: label,
children: recursiveTransform(item.children)
};
}
return {
key,
title: label
};
});
}
async function getChecks() {
const { data, error } = await doGetRoleMenuList(props.roleId);
if (!error) {
if (props.type === 'edit') {
tree.value = recursiveTransform(data.menus);
nextTick(() => {
menuIds.value = data.checkedKeys;
});
}
}
}
async function init() {
if (props.type === 'edit') {
await getChecks();
} else {
await getTree();
}
}
function clearChecks() {
menuIds.value = [];
}
defineExpose({
clearChecks,
checkedKeys: menuIds,
tree
});
</script>
<template>
<div class="border-0.5 border-gray-300 rounded-md p-2 transition-all dark:border-dark-300" hover="border-gray-500">
<SimpleScrollbar>
<ATree
v-model:checked-keys="menuIds"
:selectable="false"
:virtual="false"
:tree-data="tree"
checkable
block-node
/>
</SimpleScrollbar>
</div>
</template>
<style scoped>
:deep(.ant-tree .ant-tree-switcher) {
line-height: normal;
display: flex;
justify-content: center;
align-items: center;
}
:deep(.ant-tree .ant-tree-checkbox) {
margin-block-start: unset;
}
:deep(.ant-tree .ant-tree-title) {
font-size: 14px;
padding: 2px;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,167 @@
<script setup lang="ts">
import { SimpleScrollbar } from '@sa/materials';
import { useAntdForm, useFormRules } from '@/hooks/common/form';
import { $t } from '@/locales';
import { enableStatusOptions } from '@/constants/business';
import { doPostRole, doPutRole } from '@/service/api/role';
import MenuAuth from './menu-auth.vue';
defineOptions({
name: 'RoleOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: AntDesign.TableOperateType;
/** the edit row data */
rowData?: Api.SystemManage.Role | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const menuAuthRef = ref<InstanceType<typeof MenuAuth> | null>(null);
const authStore = useAuthStore();
const { formRef, validate, resetFields } = useAntdForm();
const { defaultRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<AntDesign.TableOperateType, string> = {
add: $t('page.manage.role.addRole'),
edit: $t('page.manage.role.editRole')
};
return titles[props.operateType];
});
type Model = Pick<Api.SystemManage.Role, 'roleName' | 'roleKey' | 'remark' | 'status'> & { menuIds: number[] };
const model = ref<Model>(createDefaultModel());
function createDefaultModel(): Model {
return {
roleName: '',
roleKey: '',
remark: '',
status: '0',
menuIds: []
};
}
type RuleKey = Exclude<keyof Model, 'remark' | 'menuIds'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
roleName: defaultRequiredRule,
roleKey: defaultRequiredRule,
status: defaultRequiredRule
};
const roleId = computed(() => props.rowData?.roleId || -1);
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
model.value = createDefaultModel();
}
if (props.operateType === 'edit' && props.rowData) {
model.value = { ...props.rowData, menuIds: [] };
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
const menuIds = transformMenuChildWithRootIds(menuAuthRef.value?.tree || [], model.value.menuIds);
const { error } = await (props.operateType === 'edit' ? doPutRole : doPostRole)({
...model.value,
menuIds,
menuCheckStrictly: true
} as Api.SystemManage.Role);
if (!error) {
authStore.refreshUserInfo();
$message?.success($t(props.operateType === 'add' ? 'common.addSuccess' : 'common.updateSuccess'));
closeDrawer();
emit('submitted');
}
}
watch(
() => visible.value,
val => {
if (val) {
handleUpdateModelWhenEdit();
resetFields();
} else {
menuAuthRef.value?.clearChecks();
}
}
);
</script>
<template>
<ADrawer
v-model:open="visible"
:body-style="{ paddingRight: '0px', paddingTop: '0', paddingBottom: '0' }"
:title="title"
:width="460"
>
<SimpleScrollbar>
<AForm ref="formRef" py-20px pr-20px layout="vertical" :model="model" :rules="rules">
<AFormItem :label="$t('page.manage.role.roleName')" name="roleName">
<AInput v-model:value="model.roleName" :placeholder="$t('page.manage.role.form.roleName')" />
</AFormItem>
<AFormItem :label="$t('page.manage.role.roleCode')" name="roleKey">
<AInput v-model:value="model.roleKey" :placeholder="$t('page.manage.role.form.roleCode')" />
</AFormItem>
<AFormItem :label="$t('page.manage.role.roleStatus')" name="status">
<ARadioGroup v-model:value="model.status">
<ARadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value">
{{ $t(item.label) }}
</ARadio>
</ARadioGroup>
</AFormItem>
<AFormItem :label="$t('page.manage.role.roleDesc')" name="roleDesc">
<AInput v-model:value="model.remark" :placeholder="$t('page.manage.role.form.roleDesc')" />
</AFormItem>
<AFormItem>
<MenuAuth
ref="menuAuthRef"
v-model:menu-ids="model.menuIds"
:drawer-visible="visible"
:type="operateType"
:role-id="roleId"
/>
<template #label>
<div class="w-full flex-between">
<span>{{ $t('page.manage.role.menuAuth') }}</span>
</div>
</template>
</AFormItem>
</AForm>
</SimpleScrollbar>
<template #footer>
<div class="flex-y-center justify-end gap-12px">
<AButton @click="closeDrawer">{{ $t('common.cancel') }}</AButton>
<AButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</AButton>
</div>
</template>
</ADrawer>
</template>
<style scoped></style>

View File

@@ -0,0 +1,73 @@
<script setup lang="ts">
import { $t } from '@/locales';
import { enableStatusOptions } from '@/constants/business';
defineOptions({
name: 'RoleSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const model = defineModel<Api.SystemManage.RoleSearchParams>('model', { required: true });
function reset() {
emit('reset');
}
function search() {
emit('search');
}
</script>
<template>
<ACard :title="$t('common.search')" :bordered="false" class="card-wrapper">
<AForm :model="model" :label-width="80">
<ARow :gutter="[16, 16]" wrap>
<ACol :span="24" :md="12" :lg="6">
<AFormItem :label="$t('page.manage.role.roleName')" name="roleName" class="m-0">
<AInput v-model:value="model.roleName" :placeholder="$t('page.manage.role.form.roleName')" />
</AFormItem>
</ACol>
<ACol :span="24" :md="12" :lg="6">
<AFormItem :label="$t('page.manage.role.roleCode')" name="roleCode" class="m-0">
<AInput v-model:value="model.roleKey" :placeholder="$t('page.manage.role.form.roleCode')" />
</AFormItem>
</ACol>
<ACol :span="24" :md="12" :lg="6">
<AFormItem :label="$t('page.manage.role.roleStatus')" name="status" class="m-0">
<ASelect v-model:value="model.status" :placeholder="$t('page.manage.role.form.roleStatus')" allow-clear>
<ASelectOption v-for="option in enableStatusOptions" :key="option.value" :value="option.value">
{{ $t(option.label) }}
</ASelectOption>
</ASelect>
</AFormItem>
</ACol>
<ACol :span="24" :md="12" :lg="6">
<AFormItem class="m-0">
<div class="w-full flex-y-center justify-end gap-12px">
<AButton @click="reset">
<div class="flex-y-center gap-8px">
<icon-ic-round-refresh class="text-icon" />
<span>{{ $t('common.reset') }}</span>
</div>
</AButton>
<AButton type="primary" ghost @click="search">
<div class="flex-y-center gap-8px">
<icon-ic-round-search class="text-icon" />
<span>{{ $t('common.search') }}</span>
</div>
</AButton>
</div>
</AFormItem>
</ACol>
</ARow>
</AForm>
</ACard>
</template>
<style scoped></style>