feat(sj_1.0.0): 新增用户管理

This commit is contained in:
opensnail 2024-04-26 17:34:24 +08:00
parent a041ec81cf
commit 53dd3b50ba
14 changed files with 542 additions and 1 deletions

View File

@ -265,6 +265,9 @@ const local: App.I18n.Schema = {
retry_scene: 'Retry scene', retry_scene: 'Retry scene',
retry_log: 'Retry Log', retry_log: 'Retry Log',
'retry_dead-letter': 'Retry Dead Letter', 'retry_dead-letter': 'Retry Dead Letter',
user: 'User',
user_manager: 'User Info',
retry_log: 'Retry log',
workflow: 'Workflow', workflow: 'Workflow',
workflow_task: 'Workflow Task', workflow_task: 'Workflow Task',
workflow_batch: 'Workflow Batch', workflow_batch: 'Workflow Batch',
@ -920,6 +923,24 @@ const local: App.I18n.Schema = {
jobName: 'Please enter job name', jobName: 'Please enter job name',
taskBatchStatus: 'Please enter state' taskBatchStatus: 'Please enter state'
} }
},
userManager: {
title: 'UserCenter List',
username: 'Username',
role: 'Role',
permissions: 'group',
checkPassword: 'Confirm Password',
password: 'Password',
form: {
role: 'Please enter Role',
password: 'Please enter Password',
username: 'Please enter Username',
checkPassword: 'Please enter Confirm Password',
permissions: 'Please select Group',
namespaceIds: 'Please select Namespaces'
},
addUser: 'Add User Center',
editUser: 'Add User Center'
} }
}, },
form: { form: {

View File

@ -264,6 +264,8 @@ const local: App.I18n.Schema = {
retry: '重试任务', retry: '重试任务',
retry_task: '重试任务', retry_task: '重试任务',
'retry_dead-letter': '死信任务', 'retry_dead-letter': '死信任务',
user: '用户管理',
user_manager: '用户信息',
retry_scene: '重试场景', retry_scene: '重试场景',
retry_log: '重试日志', retry_log: '重试日志',
workflow: '工作流', workflow: '工作流',
@ -916,6 +918,24 @@ const local: App.I18n.Schema = {
jobName: '请输入任务名称', jobName: '请输入任务名称',
taskBatchStatus: '请输入状态' taskBatchStatus: '请输入状态'
} }
},
userManager: {
title: '用户列表',
username: '用户名',
role: '角色',
permissions: '组',
checkPassword: '确认密码',
password: '密码',
form: {
role: '请输入角色',
password: '请输入密码',
username: '请输入用户名',
checkPassword: '请输入确认密码',
permissions: '请选择组',
namespaceIds: '请选择命名空间'
},
addUser: '新增用户中心',
editUser: '编辑用户中心'
} }
}, },
form: { form: {

View File

@ -47,6 +47,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
retry_scene: () => import("@/views/retry/scene/index.vue"), retry_scene: () => import("@/views/retry/scene/index.vue"),
retry_task: () => import("@/views/retry/task/index.vue"), retry_task: () => import("@/views/retry/task/index.vue"),
"user-center": () => import("@/views/user-center/index.vue"), "user-center": () => import("@/views/user-center/index.vue"),
user_manager: () => import("@/views/user/manager/index.vue"),
workflow_batch: () => import("@/views/workflow/batch/index.vue"), workflow_batch: () => import("@/views/workflow/batch/index.vue"),
workflow_task: () => import("@/views/workflow/task/index.vue"), workflow_task: () => import("@/views/workflow/task/index.vue"),
}; };

View File

@ -471,6 +471,28 @@ export const generatedRoutes: GeneratedRoute[] = [
} }
] ]
}, },
{
name: 'user',
path: '/user',
component: 'layout.base',
meta: {
title: 'user',
i18nKey: 'route.user'
},
children: [
{
name: 'user_manager',
path: '/user/manager',
component: 'view.user_manager',
meta: {
title: 'user_manager',
i18nKey: 'route.user_manager',
icon: 'streamline:interface-user-multiple-close-geometric-human-multiple-person-up-user',
order: 900
}
}
]
},
{ {
name: 'user-center', name: 'user-center',
path: '/user-center', path: '/user-center',

View File

@ -188,6 +188,8 @@ const routeMap: RouteMap = {
"retry_log": "/retry/log", "retry_log": "/retry/log",
"retry_scene": "/retry/scene", "retry_scene": "/retry/scene",
"retry_task": "/retry/task", "retry_task": "/retry/task",
"user": "/user",
"user_manager": "/user/manager",
"user-center": "/user-center", "user-center": "/user-center",
"workflow": "/workflow", "workflow": "/workflow",
"workflow_batch": "/workflow/batch", "workflow_batch": "/workflow/batch",

View File

@ -50,3 +50,12 @@ export function fetchGetPartitionTableList() {
method: 'get' method: 'get'
}); });
} }
/** get all group config list */
export function fetchGetAllGroupConfigList(data: string[]) {
return request<Api.GroupConfig.GroupConfig[]>({
url: '/group/all/group-config/list',
method: 'post',
data
});
}

View File

@ -12,3 +12,4 @@ export * from './retry-dead-letter';
export * from './workflow'; export * from './workflow';
export * from './job'; export * from './job';
export * from './job-batch'; export * from './job-batch';
export * from './user';

36
src/service/api/user.ts Normal file
View File

@ -0,0 +1,36 @@
import { request } from '../request';
/** get user list */
export function fetchGetUserPageList(params?: Api.UserManager.UserManagerSearchParams) {
return request<Api.UserManager.UserManagerList>({
url: '/user/page/list',
method: 'get',
params
});
}
/** add user */
export function fetchAddUser(data: Api.UserManager.UserManager) {
return request<boolean>({
url: '/user',
method: 'post',
data
});
}
/** edit user */
export function fetchEditUser(data: Api.UserManager.UserManager) {
return request<boolean>({
url: '/user',
method: 'put',
data
});
}
/** delete user */
export function fetchDelUser(id: number) {
return request<boolean>({
url: `/user/${id}`,
method: 'delete'
});
}

36
src/typings/api.d.ts vendored
View File

@ -1076,4 +1076,40 @@ declare namespace Api {
/** retryLog list */ /** retryLog list */
type RetryLogList = Common.PaginatingQueryRecord<RetryLog>; type RetryLogList = Common.PaginatingQueryRecord<RetryLog>;
} }
/**
* namespace UserManager
*
* backend api module: "UserManager"
*/
namespace UserManager {
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'page' | 'size'>;
/** userCenter */
type UserManager = Common.CommonRecord<{
/** 用户名 */
username: string;
/** 密码 */
password: string;
/** 确认密码 */
checkPassword: string;
/** 角色 */
role: string;
/** 命名空间 */
namespaceIds?: string;
/** 组 */
permissions: string[];
}>;
/** userManager search params */
type UserManagerSearchParams = CommonType.RecordNullable<
Pick<
Api.UserManager.UserManager,
'username' | 'password' | 'checkPassword' | 'role' | 'namespaceIds' | 'permissions'
> &
CommonSearchParams
>;
/** userCenter list */
type UserManagerList = Common.PaginatingQueryRecord<UserManager>;
}
} }

18
src/typings/app.d.ts vendored
View File

@ -1081,6 +1081,24 @@ declare namespace App {
taskBatchStatus: string; taskBatchStatus: string;
}; };
}; };
userManager: {
title: string;
username: string;
role: string;
checkPassword: string;
password: string;
permissions: string;
form: {
role: string;
password: string;
username: string;
checkPassword: string;
permissions: string;
namespaceIds: string;
};
addUser: string;
editUser: string;
};
}; };
form: { form: {
required: string; required: string;

View File

@ -26,7 +26,8 @@ const { columns, columnChecks, data, getData, loading, mobilePagination, searchP
{ {
type: 'selection', type: 'selection',
align: 'center', align: 'center',
width: 48 width: 48,
disabled: row => row.retryStatus !== 1
}, },
{ {
key: 'index', key: 'index',

View File

@ -0,0 +1,141 @@
<script setup lang="tsx">
import { NButton, NPopconfirm } from 'naive-ui';
import { fetchGetUserPageList } from '@/service/api';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import UserCenterOperateDrawer from './modules/user-manager-operate-drawer.vue';
import UserCenterSearch from './modules/user-manager-search.vue';
const appStore = useAppStore();
const { columns, columnChecks, data, getData, loading, mobilePagination, searchParams, resetSearchParams } = useTable({
apiFn: fetchGetUserPageList,
apiParams: {
page: 1,
size: 10,
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
username: null,
role: null
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
key: 'username',
title: $t('page.userManager.username'),
align: 'left',
minWidth: 120
},
{
key: 'role',
title: $t('page.userManager.role'),
align: 'left',
minWidth: 120
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => (
<div class="flex-center gap-8px">
<NButton type="primary" ghost size="small" onClick={() => edit(row.id!)}>
{$t('common.edit')}
</NButton>
<NPopconfirm onPositiveClick={() => handleDelete()}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" ghost size="small">
{$t('common.delete')}
</NButton>
)
}}
</NPopconfirm>
</div>
)
}
]
});
const {
drawerVisible,
operateType,
editingData,
handleAdd,
handleEdit,
checkedRowKeys,
onBatchDeleted,
onDeleted
// closeDrawer
} = useTableOperate(data, getData);
async function handleBatchDelete() {
// request
onBatchDeleted();
}
function handleDelete() {
// request
onDeleted();
}
function edit(id: string) {
handleEdit(id);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<UserCenterSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
<NCard
:title="$t('page.userManager.title')"
:bordered="false"
size="small"
class="sm:flex-1-hidden card-wrapper"
header-class="view-card-header"
>
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<UserCenterOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getData"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,196 @@
<script setup lang="tsx">
import { computed, reactive, ref, watch } from 'vue';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import OperateDrawer from '@/components/common/operate-drawer.vue';
import { $t } from '@/locales';
import { fetchAddUser, fetchEditUser, fetchGetAllGroupConfigList } from '@/service/api';
defineOptions({
name: 'UserCenterOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.UserManager.UserManager | null;
}
const groupConfigs = ref();
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: $t('page.userManager.addUser'),
edit: $t('page.userManager.editUser')
};
return titles[props.operateType];
});
type Model = Pick<
Api.UserManager.UserManager,
'id' | 'username' | 'password' | 'checkPassword' | 'role' | 'permissions'
>;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
username: '',
password: '',
checkPassword: '',
role: '',
permissions: []
};
}
type RuleKey = Extract<keyof Model, 'username' | 'password' | 'checkPassword' | 'role' | 'permissions'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
username: defaultRequiredRule,
password: defaultRequiredRule,
checkPassword: defaultRequiredRule,
role: defaultRequiredRule,
permissions: defaultRequiredRule
};
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
return;
}
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData);
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
// request
if (props.operateType === 'add') {
const { username, password, checkPassword, role, permissions } = model;
const { error } = await fetchAddUser({ username, password, checkPassword, role, permissions });
if (error) return;
}
if (props.operateType === 'edit') {
const { id, username, password, checkPassword, role, permissions } = model;
const { error } = await fetchEditUser({ id, username, password, checkPassword, role, permissions });
if (error) return;
}
window.$message?.success($t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
const getAllGroupConfigList = async () => {
const res = await fetchGetAllGroupConfigList([]);
groupConfigs.value = res.data?.map(option => ({
value: {
groupName: option.groupName,
namespaceId: option.groupName
},
label: () => {
return (
<div>
<span>{option.groupName}</span>
<br />
<span>{option.namespaceId}</span>
</div>
);
}
}));
};
//
getAllGroupConfigList();
const valueRef = ref<Array<string | number>>([]);
// const renderSourceList: TransferRenderSourceList = function ({ onCheck, pattern }) {
// return h(NCheckbox, {
// style: 'margin: 0 4px;',
// keyField: 'value',
// checkable: true,
// selectable: false,
// blockLine: true,
// checkOnClick: true,
// data: groupConfigs.value,
// pattern,
// checkedKeys: valueRef.value,
// onUpdateCheckedKeys: (checkedKeys: Array<string | number>) => {
// // console.log(checkedKeys)
// model.permissions = checkedKeys as string[];
// onCheck(checkedKeys);
// }
// });
// };
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenEdit();
restoreValidation();
}
});
watch(valueRef.value, () => {
// console.log(valueRef.value)
model.permissions = valueRef.value as string[];
});
</script>
<template>
<OperateDrawer v-model="visible" :title="title">
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem :label="$t('page.userManager.username')" path="username">
<NInput v-model:value="model.username" :placeholder="$t('page.userManager.form.username')" />
</NFormItem>
<NFormItem :label="$t('page.userManager.password')" path="password">
<NInput v-model:value="model.password" :placeholder="$t('page.userManager.form.password')" />
</NFormItem>
<NFormItem :label="$t('page.userManager.checkPassword')" path="checkPassword">
<NInput v-model:value="model.checkPassword" :placeholder="$t('page.userManager.form.checkPassword')" />
</NFormItem>
<NFormItem :label="$t('page.userManager.role')" path="role">
<NInput v-model:value="model.role" :placeholder="$t('page.userManager.form.role')" />
</NFormItem>
<NFormItem :label="$t('page.userManager.permissions')" path="permissions">
<!-- <NTransfer-->
<!-- v-model:value="model.permissions"-->
<!-- :render-source-list="renderSourceList"-->
<!-- virtual-scroll-->
<!-- :options="groupConfigs"-->
<!-- target-filterable-->
<!-- source-filterable-->
<!-- />-->
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.save') }}</NButton>
</NSpace>
</template>
</OperateDrawer>
</template>
<style scoped></style>

View File

@ -0,0 +1,37 @@
<script setup lang="ts">
import { $t } from '@/locales';
defineOptions({
name: 'UserCenterSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const model = defineModel<Api.UserManager.UserManagerSearchParams>('model', { required: true });
function reset() {
emit('reset');
}
function search() {
emit('search');
}
</script>
<template>
<SearchForm :model="model" @search="search" @reset="reset">
<NFormItemGi span="24 s:12 m:6" :label="$t('page.userManager.username')" path="username" class="pr-24px">
<NInput v-model:value="model.username" :placeholder="$t('page.userManager.form.username')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.userManager.role')" path="role" class="pr-24px">
<NInput v-model:value="model.role" :placeholder="$t('page.userManager.form.role')" />
</NFormItemGi>
</SearchForm>
</template>
<style scoped></style>