Add role page

This commit is contained in:
xue jiahao
2024-12-23 14:19:20 +08:00
parent 8c481ecf4c
commit 354abf252b
5 changed files with 405 additions and 11 deletions

View File

@@ -0,0 +1,71 @@
<template>
<detail-field-table
detailField="description"
:name="name"
:type="type"
:filter="filter"
:columns="columns"
:fetchData="fetchUsers"
@update:selected="(item) => { selected = item }">
<template v-slot:body-cell-status="props">
<div class="row">
<div v-if="props.row.isActive">
<q-chip square color="positive" text-color="white" icon="done" label="使用可能" size="sm" />
</div>
<div v-else>
<q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" />
</div>
<q-chip v-if="props.row.isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
label="システム管理者" size="sm" />
</div>
</template>
</detail-field-table>
</template>
<script lang="ts">
import { ref, PropType } from 'vue';
import { IUser } from 'src/types/UserTypes';
import { api } from 'boot/axios';
import DetailFieldTable from './DetailFieldTable.vue';
export default {
name: 'UserSelectBox',
components: {
DetailFieldTable
},
props: {
name: String,
type: String,
filter: String,
filterInitRowsFunc: {
type: Function as PropType<(user: IUser) => boolean>,
}
},
setup(props) {
const selected = ref<IUser[]>([]);
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'lastName', label: '氏名', field: 'lastName', align: 'left', sortable: true },
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'status', label: '状況', field: 'status', align: 'left' }
];
const fetchUsers = async () => {
const result = await api.get(`api/v1/users`);
return result.data.data.map((item: any) => {
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active, roles: item.roles.map(role => role.id) }
}).filter(user => !props.filterInitRowsFunc || props.filterInitRowsFunc(user));
};
return {
columns,
fetchUsers,
selected
};
}
};
</script>

View File

@@ -125,6 +125,13 @@ const adminLinks: EssentialLinkProps[] = reactive([
link: '/#/user',
target: '_self'
},
{
title: 'ロール管理',
caption: 'ロールを管理する',
icon: 'work',
link: '/#/role',
target: '_self'
},
])
const version = process.env.version;

View File

@@ -0,0 +1,254 @@
<template>
<div class="q-pa-md">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="work" label="ロール管理" />
</q-breadcrumbs>
</div>
<div class="row" style="min-height: 80vh;">
<div class="col-auto">
<div class="q-pa-md" style="width: 250px">
<q-list bordered separator class="rounded-borders">
<q-item active v-if="allLoading" active-class="menu-active">
<q-item-section class="text-weight-bold"> 読み込み中... </q-item-section>
</q-item>
<q-item v-else v-for="item in roles" :key="item.id" clickable v-ripple :active="selected.id === item.id" @click="roleClicked(item)" active-class="menu-active">
<q-item-section class="text-weight-bold"> {{ item.label }} </q-item-section>
</q-item>
</q-list>
</div>
</div>
<div class="col">
<q-table title="ユーザーリスト" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading"
:pagination="pagination" >
<template v-slot:top>
<q-btn color="primary" :disable="loading" label="追加" @click="showAddRoleDialog" />
<q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:body-cell-status="props">
<q-td :props="props">
<div class="row">
<div v-if="props.row.isActive">
<q-chip square color="positive" text-color="white" icon="done" label="使用可能" size="sm" />
</div>
<div v-else>
<q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" />
</div>
<q-chip v-if="props.row.isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
label="システム管理者" size="sm" />
</div>
</q-td>
</template>
<template v-slot:header-cell-status="p">
<q-th :props="p">
<div class="row items-center">
<label class="q-mr-md">{{ p.col.label }}</label>
<q-select v-model="statusFilter" :options="options" @update:model-value="updateStatusFilter" borderless
dense options-dense style="font-size: 12px; padding-top: 1px;" />
</div>
</q-th>
</template>
<template v-slot:body-cell-actions="p">
<q-td auto-width :props="p">
<table-action-menu :row="p.row" minWidth="180px" max-width="200px" :actions="actionList" />
</q-td>
</template>
</q-table>
</div>
</div>
<show-dialog v-model:visible="showSelectUser" name="ユーザー選択" @close="closeSelectUserDialog" min-width="50vw" min-height="50vh" :ok-btn-auto-close="false" :ok-btn-loading="isAdding">
<template v-slot:toolbar>
<q-input dense debounce="300" v-model="dgFilter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<user-select-box ref="userDialog" name="ユーザー" type="single" :filter="dgFilter" :filterInitRowsFunc="filterInitRows" />
</show-dialog>
<q-dialog v-model="deleteDialog" persistent>
<q-card>
<q-card-section class="row items-center">
<q-icon name="warning" color="warning" size="2em" />
<div class="q-ml-sm text-weight-bold">ロールメンバーを削除</div>
</q-card-section>
<q-card-section class="q-py-none">
<!-- <span class="q-ml-sm">この役割を与えられたユーザーはメンバー役に再配置されます</span> -->
<div class="q-mx-sm">ユーザー{{targetRow.email}}{{selected.label}}の役割から</div>
<div class="q-mx-sm">本当に外しますか</div>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn flat label="OK" color="primary" :loading="deleteUserRoleLoading" @click="deleteUserRole" />
</q-card-actions>
</q-card>
</q-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { IUser } from 'src/types/UserTypes';
import ShowDialog from 'src/components/ShowDialog.vue';
import UserSelectBox from 'src/components/dialog/UserSelectBox.vue';
import TableActionMenu from 'components/TableActionMenu.vue';
import { api } from 'boot/axios';
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'lastName', label: '氏名', field: 'lastName', align: 'left', sortable: true },
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'status', label: '状況', field: 'status', align: 'left' },
{ name: 'actions', label: '操作', field: 'actions' }
];
const roles = ref([])
const selected = ref();
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const allLoading = ref(true);
const loading = ref(false);
const filter = ref('');
const statusFilter = ref('全データ');
const dgFilter = ref('');
const allRows = ref([]);
const targetRow = ref<IUser>();
const userDialog = ref();
const showSelectUser=ref(false);
const isAdding = ref(false);
const deleteDialog = ref(false);
const deleteUserRoleLoading = ref(false);
const actionList = [
// { label: '移動', icon: 'account_tree', action: toEditFlowPage },
{ separator: true },
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
];
const options = ['全データ', 'システム管理者のみ', '使用可能', '使用不可']
const roleClicked = async (role) => {
selected.value = role;
}
const rows = computed(() => allRows.value.filter((item) => item.roles?.includes(selected.value.id) ));
const rowIds = computed(() => new Set(rows.value?.map((item) => item.id)));
const getUsers = async (filter = () => true) => {
loading.value = true;
allRows.value = []
const result = await api.get(`api/v1/users`);
allRows.value = result.data.data.map((item) => {
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active, roles: item.roles.map(role => role.id) }
}).filter(filter);
loading.value = false;
}
const getRoles = async () => {
const result = await api.get(`api/v1/roles`);
roles.value = result.data.data.map((item) => {
return { id: item.id, label: item.description }
})
selected.value = roles.value[0];
}
const updateStatusFilter = (status) => {
switch (status) {
case 'システム管理者のみ':
getUsers((row) => row.isSuperuser)
break;
case '使用可能':
getUsers((row) => row.isActive)
break;
case '使用不可':
getUsers((row) => !row.isActive)
break;
default:
getUsers()
break;
}
}
onMounted(async () => {
allLoading.value = true;
await Promise.all([
getRoles(),
getUsers()
]);
allLoading.value = false;
});
const filterInitRows = (row: {id: string}) => {
return !rowIds.value.has(row.id);
}
const showAddRoleDialog = () => {
showSelectUser.value = true;
isAdding.value = false;
dgFilter.value = ''
}
const closeSelectUserDialog = async (val: 'OK'|'Cancel') => {
showSelectUser.value = true;
if (val == 'OK' && userDialog.value.selected[0]) {
isAdding.value = true;
const user = userDialog.value.selected[0];
const result = await api.post(`api/v1/userrole`, {
'userid': user.id,
'roleids': user.roles.concat(selected.value.id)
});
await getUsers();
}
showSelectUser.value = false;
isAdding.value = false;
}
function removeRow(user:IUser) {
targetRow.value = user;
deleteDialog.value = true;
}
const deleteUserRole = async () => {
if (targetRow.value?.id) {
deleteUserRoleLoading.value = true;
// TODO 返回不需要重新读
const result = await api.post(`api/v1/userrole`, {
'userid': targetRow.value.id,
'roleids': targetRow.value.roles.filter(e => e !== selected.value.id)
});
await getUsers();
deleteUserRoleLoading.value = false;
deleteDialog.value = false;
}
}
</script>
<style lang="scss" scoped>
.menu-active {
color: white;
background: var(--q-primary)
}
</style>

View File

@@ -27,14 +27,21 @@
<div v-else>
<q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" />
</div>
<q-chip v-if="props.row.isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
label="システム管理者" size="sm" />
</div>
</q-td>
</template>
<template v-slot:body-cell-roles="props">
<q-td :props="props">
<div class="row">
<q-chip v-if="props.row.isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
label="システム管理者" size="sm" />
<span v-else>{{ props.row.roles.map(r => r.description).join('、') }}</span>
</div>
</q-td>
</template>
<template v-slot:header-cell-status="p">
<q-th :props="p">
<div class="row items-center">
@@ -46,11 +53,8 @@
</template>
<template v-slot:body-cell-actions="p">
<q-td :props="p">
<q-btn-group flat>
<q-btn flat color="primary" padding="xs" size="1em" icon="edit_note" @click="editRow(p.row)" />
<q-btn flat color="negative" padding="xs" size="1em" icon="delete_outline" @click="removeRow(p.row)" />
</q-btn-group>
<q-td auto-width :props="p">
<table-action-menu :row="p.row" minWidth="180px" max-width="200px" :actions="actionList" />
</q-td>
</template>
</q-table>
@@ -91,6 +95,35 @@
<q-toggle v-model="isSuperuser" />
</q-item-section>
</q-item>
<!--
<q-select
class="q-mt-xs"
filled
v-model="roles"
multiple
:options="roleOptions"
label="ロール" emit-value
map-options
:option-disable="opt => opt.value == -1 ? false : isSuperuser"
@add="(opt) => { if (opt.value.value == -1) { } }"
hint="各役割には固有の権限があります">
<template v-slot:option="{ itemProps, opt, selected, toggleOption }">
<q-item v-bind="itemProps" v-if="opt.value == -1" >
<q-item-section>
<q-item-label>{{opt.label}}</q-item-label>
</q-item-section>
<q-item-section side>
<q-toggle :model-value="selected" @update:model-value="() => { toggleOption(opt); }" />
</q-item-section>
</q-item>
<q-separator v-if="opt.value == -1"/>
<q-item v-else v-bind="itemProps">
<q-item-section>
<q-item-label>{{opt.label}}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select> -->
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
@@ -155,6 +188,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { api } from 'boot/axios';
import TableActionMenu from 'components/TableActionMenu.vue';
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
@@ -162,6 +196,7 @@ const columns = [
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'status', label: '状況', field: 'status', align: 'left' },
{ name: 'roles', label: 'ロール', field: '', align: 'left' },
{ name: 'actions', label: '操作', field: 'actions' }
];
@@ -184,18 +219,36 @@ const isActive = ref(true);
const isPwd = ref(true);
const pwd = ref('');
// const roles = ref([]);
const isCreate = ref(true);
let editId = ref(0);
// const roleOptions = ref([
// {'value': -1, 'label': 'システム管理者'}
// ]);
const actionList = [
{ label: '編集', icon: 'edit_note', action: editRow },
{ separator: true },
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
];
const getUsers = async (filter = () => true) => {
loading.value = true;
const result = await api.get(`api/v1/users`);
rows.value = result.data.data.map((item) => {
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active }
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active, roles: item.roles.sort((a, b) => a.id - b.id) }
}).filter(filter);
loading.value = false;
}
// const getRoles = async () => {
// const result = await api.get(`api/v1/roles`);
// roleOptions.value.push(...result.data.data.map((item) => {
// return { value: item.id, label: item.description }
// }))
// }
const updateStatusFilter = (status) => {
switch (status) {
case 'システム管理者のみ':
@@ -215,6 +268,10 @@ const updateStatusFilter = (status) => {
}
onMounted(async () => {
// await Promise.all([
// getRoles(),
// getUsers()
// ]);
await getUsers();
})
@@ -227,7 +284,7 @@ const addRow = () => {
show.value = true;
}
const removeRow = (row) => {
function removeRow(row) {
confirm.value = true;
editId.value = row.id;
}
@@ -239,7 +296,7 @@ const deleteUser = () => {
editId.value = 0;
};
const editRow = (row) => {
function editRow(row) {
isCreate.value = false
editId.value = row.id;
@@ -249,6 +306,7 @@ const editRow = (row) => {
pwd.value = row.password;
isSuperuser.value = row.isSuperuser;
// roles.value = row.roles.map(item => item.id);
isActive.value = row.isActive;
isPwd.value = true;
@@ -269,6 +327,7 @@ const onSubmit = () => {
'is_superuser': isSuperuser.value,
'is_active': isActive.value,
'email': email.value,
// "roles": roles.value,
...(isCreate.value || resetPsw.value ? { password: pwd.value } : {})
}).then(() => {
getUsers();
@@ -284,6 +343,7 @@ const onSubmit = () => {
'is_superuser': isSuperuser.value,
'is_active': isActive.value,
'email': email.value,
// "roles": roles.value,
'password': pwd.value
}).then(() => {
getUsers();
@@ -304,6 +364,7 @@ const onReset = () => {
isPwd.value = true;
editId.value = 0;
isCreate.value = true;
// roles.value = [];
resetPsw.value = false;
addEditLoading.value = false;
}

View File

@@ -29,6 +29,7 @@ const routes: RouteRecordRaw[] = [
{ path: 'user', component: () => import('pages/UserManagement.vue')},
{ path: 'app', component: () => import('pages/AppManagement.vue')},
{ path: 'app/version/:id', component: () => import('pages/AppVersionManagement.vue')},
{ path: 'role', component: () => import('pages/RoleManagement.vue')},
{ path: 'condition', component: () => import('pages/conditionPage.vue') }
],
},