This commit is contained in:
xue jiahao
2024-12-24 11:35:14 +08:00
parent 5d7ffa0138
commit 84ba118bb1
4 changed files with 144 additions and 134 deletions

View File

@@ -7,7 +7,7 @@
</div>
<div class="row" style="min-height: 80vh;">
<!-- left role panel -->
<div class="col-auto">
<div class="q-pa-md" style="width: 250px">
<q-list bordered separator class="rounded-borders">
@@ -15,20 +15,19 @@
<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-section class="text-weight-bold"> {{ item.name }} </q-item-section>
</q-item>
</q-list>
</div>
</div>
<!-- right table panel -->
<div class="col">
<q-table title="ユーザーリスト" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading"
<q-table title="ユーザーリスト" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="allLoading || loading"
:pagination="pagination" >
<template v-slot:top>
<q-btn color="primary" :disable="loading || selected?.id == -2" label="追加" @click="showAddRoleDialog" />
<q-btn color="primary" :disable="allLoading || loading || selected?.id == -2" label="追加" @click="showAddRoleDialog" />
<q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
@@ -57,7 +56,7 @@
<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
<q-select v-model="statusFilter" :options="statusFilterOptions" borderless
dense options-dense style="font-size: 12px; padding-top: 1px;" />
</div>
</q-th>
@@ -92,7 +91,7 @@
</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">ユーザー{{targetRow?.email}}{{selected?.name}}の役割から</div>
<div class="q-mx-sm">本当に外しますか</div>
</q-card-section>
@@ -106,12 +105,14 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { IUser } from 'src/types/UserTypes';
import { ref, onMounted, computed, watch } from 'vue';
import { IRolesDisplay, IUserRolesDisplay } from 'src/types/UserTypes';
import { useUserStore } from 'stores/useUserStore';
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 userStore = useUserStore();
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
@@ -119,21 +120,30 @@ 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: 'actions', label: '操作', field: 'actions' }
{ name: 'actions', label: '操作', field: 'actions', align: 'right' }
];
const roles = ref([])
const selected = ref();
const statusFilterOptions = [
{ label: '全データ', filter: () => true },
{ label: 'システム管理者のみ', filter: (row: IUserRolesDisplay) => row.isSuperuser },
{ label: '使用可能', filter: (row: IUserRolesDisplay) => row.isActive },
{ label: '使用不可', filter: (row: IUserRolesDisplay) => !row.isActive },
]
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const roles = computed(() => userStore.roles.concat(EMPTY_ROLE));
const selected = ref<IRolesDisplay>();
const allLoading = ref(true);
const loading = ref(false);
const filter = ref('');
const statusFilter = ref('全データ');
const statusFilter = ref(statusFilterOptions[0]);
const dgFilter = ref('');
const allRows = ref([]);
const targetRow = ref<IUser>();
const allUsers = computed(() => userStore.users.filter(statusFilter.value.filter));
const targetRow = ref<IUserRolesDisplay>();
const userDialog = ref();
const showSelectUser=ref(false);
@@ -141,73 +151,57 @@ const isAdding = ref(false);
const deleteDialog = ref(false);
const deleteUserRoleLoading = ref(false);
const EMPTY_ROLE: IRolesDisplay = {
id: -2,
name: 'ロールなし',
key: 'dummy',
level: -1
}
const actionList = [
// { label: '移動', icon: 'account_tree', action: toEditFlowPage },
{ separator: true },
// { 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) => {
const rows = computed(() => allUsers.value.filter((item) => {
if (!selected.value) {
return false;
}
if (selected.value.id == -2) {
return !item.roles || item.roles.length == 0;
}
return item.roles?.includes(selected.value.id);
return item.roleIds?.includes(selected.value.id);
}));
const rowIds = computed(() => new Set(rows.value?.map((item) => item.id)));
const getUsers = async (filter = () => true) => {
const getUsers = async () => {
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);
await userStore.loadUsers();
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 }
}).concat({ id: -2, label: 'ロールなし' })
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(),
userStore.loadRoles(),
getUsers()
]);
allLoading.value = false;
});
const filterInitRows = (row: {id: string}) => {
return !rowIds.value.has(row.id);
watch(roles, (newRoles) => {
if (newRoles.length > 0) {
selected.value = newRoles[0];
}
});
const roleClicked = async (role: IRolesDisplay) => {
selected.value = role;
}
const filterInitRows = (user: IUserRolesDisplay) => {
return !rowIds.value.has(user.id);
}
const showAddRoleDialog = () => {
@@ -218,32 +212,24 @@ const showAddRoleDialog = () => {
const closeSelectUserDialog = async (val: 'OK'|'Cancel') => {
showSelectUser.value = true;
if (val == 'OK' && userDialog.value.selected[0]) {
if (val == 'OK' && userDialog.value.selected[0] && selected.value && selected.value.id >= 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 userStore.addRole(userDialog.value.selected[0], selected.value);
await getUsers();
}
showSelectUser.value = false;
isAdding.value = false;
}
function removeRow(user:IUser) {
function removeRow(user: IUserRolesDisplay) {
targetRow.value = user;
deleteDialog.value = true;
}
const deleteUserRole = async () => {
if (targetRow.value?.id) {
if (targetRow.value?.id && selected.value && selected.value.id >= 0) {
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 userStore.removeRole(targetRow.value, selected.value);
await getUsers();
deleteUserRoleLoading.value = false;
deleteDialog.value = false;

View File

@@ -37,7 +37,9 @@
<div class="row">
<q-chip v-if="(props.row as IUserRolesDisplay).isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
label="システム管理者" size="sm" />
<span v-else>{{ (props.row as IUserRolesDisplay).roles.map(r => r.name).join('、') }}</span>
<template v-else>
<q-chip v-for="(item) in (props.row as IUserRolesDisplay).roles" square color="primary" text-color="white" :key="item.id" :label="item.name" size="sm" />
</template>
</div>
</q-td>
</template>
@@ -158,7 +160,6 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { api } from 'boot/axios';
import TableActionMenu from 'components/TableActionMenu.vue';
import { useUserStore } from 'stores/useUserStore';
import { IUserDisplay, IUserRolesDisplay } from 'src/types/UserTypes';
@@ -202,13 +203,13 @@ const isActive = ref(true);
const isPwd = ref(true);
const pwd = ref('');
const isCreate = ref(true);
let editId = ref(0);
const editId = ref(0);
const isCreate = computed(() => editId.value <= 0);
const actionList = [
{ label: '編集', icon: 'edit_note', action: editRow },
{ separator: true },
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: showDeleteUserConfirm },
];
const getUsers = async () => {
@@ -221,14 +222,12 @@ onMounted(async () => {
await getUsers();
})
// emulate fetching data from server
const addRow = () => {
// editId.value
onReset();
show.value = true;
}
function removeRow(row: IUserDisplay) {
function showDeleteUserConfirm(row: IUserDisplay) {
confirm.value = true;
editId.value = row.id;
}
@@ -236,11 +235,10 @@ function removeRow(row: IUserDisplay) {
const deleteUser = async () => {
await userStore.deleteUser(editId.value);
getUsers();
editId.value = 0;
onReset();
};
function editRow(row: IUserDisplay) {
isCreate.value = false
editId.value = row.id;
firstName.value = row.firstName;
@@ -259,38 +257,25 @@ const closeDg = () => {
onReset();
}
const onSubmit = () => {
const onSubmit = async () => {
addEditLoading.value = true;
if (editId.value !== 0) {
api.put(`api/v1/users/${editId.value}`, {
'first_name': firstName.value,
'last_name': lastName.value,
'is_superuser': isSuperuser.value,
'is_active': isActive.value,
'email': email.value,
...(isCreate.value || resetPsw.value ? { password: pwd.value } : {})
}).then(() => {
getUsers();
closeDg();
onReset();
})
const param = {
id: editId.value,
first_name: firstName.value,
last_name: lastName.value,
is_superuser: isSuperuser.value,
is_active: isActive.value,
email: email.value,
password: (isCreate.value || resetPsw.value) ? pwd.value : undefined
}
else {
api.post(`api/v1/users`, {
'id': 0,
'first_name': firstName.value,
'last_name': lastName.value,
'is_superuser': isSuperuser.value,
'is_active': isActive.value,
'email': email.value,
'password': pwd.value
}).then(() => {
getUsers();
closeDg();
onReset();
})
if (isCreate.value) {
await userStore.addUser(param);
} else {
await userStore.editUser(param);
}
getUsers();
closeDg();
onReset();
}
const onReset = () => {
@@ -302,7 +287,6 @@ const onReset = () => {
isSuperuser.value = false;
isPwd.value = true;
editId.value = 0;
isCreate.value = true;
resetPsw.value = false;
addEditLoading.value = false;
}

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia';
import { api } from 'boot/axios';
import { IRoles, IRolesDisplay, IUser, IUserDisplay, IUserRolesDisplay } from 'src/types/UserTypes';
import { IRoles, IRolesDisplay, IUser, IUserDisplay, IUserRolesDisplay, IUserSubmit } from 'src/types/UserTypes';
import { Notify } from 'quasar'
import { IResponse } from 'src/types/BaseTypes';
@@ -12,6 +12,7 @@ export const useUserStore = defineStore('user', {
roles: [] as IRolesDisplay[],
}),
actions: {
// -------------------------- users --------------------------
async loadUsers() {
this.reset('users');
try {
@@ -29,6 +30,39 @@ export const useUserStore = defineStore('user', {
}
},
getUserById(id: number) {
if (!this.userIds.has(id)) {
return null;
}
return this.users.find((item: IUserDisplay) => item.id === id);
},
async addUser(user: IUserSubmit) {
return await api.post('api/v1/users', user);
},
async editUser(user: IUserSubmit) {
const id = user.id;
delete user['id']
return await api.put(`api/v1/users/${id}`, user);
},
async deleteUser(userId: number) {
try {
await api.delete(`api/v1/users/${userId}`)
} catch (error) {
console.error(error);
Notify.create({
icon: 'error',
color: 'negative',
message: 'ユーザーの削除に失敗しました'
});
return false;
}
return true;
},
// -------------------------- roles --------------------------
async loadRoles() {
this.reset('roles');
try {
@@ -45,26 +79,19 @@ export const useUserStore = defineStore('user', {
}
},
getUserById(id: number) {
if (!this.userIds.has(id)) {
return null;
}
return this.users.find((item: IUserDisplay) => item.id === id);
async addRole(user: IUserRolesDisplay, role: IRolesDisplay) {
return await this.updateUserRole(user, user.roleIds.concat(role.id));
},
async deleteUser(userId: number) {
try {
await api.delete(`api/v1/users/${userId}`)
} catch (error) {
console.error(error);
Notify.create({
icon: 'error',
color: 'negative',
message: 'ユーザーの削除に失敗しました'
});
return false;
}
return true;
async removeRole(user: IUserRolesDisplay, role: IRolesDisplay) {
return await this.updateUserRole(user, user.roleIds.filter(e => e !== role.id));
},
async updateUserRole(user: IUserRolesDisplay, roleIds: number[]) {
return await api.post('api/v1/userrole', {
userid: user.id,
roleIds
});
},
reset(target?: 'users'|'roles') {
@@ -98,7 +125,14 @@ export function userToUserDisplay(user: IUser): IUserDisplay {
export function userToUserRolesDisplay(user: IUser): IUserRolesDisplay {
const userRolesDisplay = userToUserDisplay(user) as IUserRolesDisplay;
userRolesDisplay.roles = user.roles.map((role) => roleToRoleDisplay(role)).sort((a, b) => a.level - b.level);
const roles: IRolesDisplay[] = [];
const roleIds: number[] = [];
user.roles.sort((a, b) => a.level - b.level).forEach((role) => {
roles.push(roleToRoleDisplay(role));
roleIds.push(role.id);
});
userRolesDisplay.roles = roles;
userRolesDisplay.roleIds = roleIds;
return userRolesDisplay;
}

View File

@@ -8,6 +8,11 @@ export interface IUser {
roles: IRoles[];
}
export interface IUserSubmit extends Omit<IUser, 'id' | 'roles'> {
id?: number;
password?: string;
}
export interface IUserDisplay {
id: number;
firstName: string;
@@ -21,6 +26,7 @@ export interface IUserDisplay {
export interface IUserRolesDisplay extends IUserDisplay {
roles: IRolesDisplay[];
roleIds: number[];
}
export interface IRoles {