feat(fengling): add routes and pages for Fengling Console management

This commit is contained in:
Sam 2026-02-06 00:30:00 +08:00
parent e5bbe101c9
commit 87db42b5db
6 changed files with 952 additions and 0 deletions

View File

@ -0,0 +1,72 @@
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:building-2',
order: 1,
title: 'Fengling Console',
},
name: 'Fengling',
path: '/fengling',
children: [
{
name: 'FenglingDashboard',
path: '/fengling/dashboard',
component: () => import('#/views/fengling/dashboard/index.vue'),
meta: {
affixTab: true,
icon: 'lucide:layout-dashboard',
title: 'Dashboard',
},
},
{
name: 'TenantManagement',
path: '/fengling/tenants',
component: () => import('#/views/fengling/tenants/index.vue'),
meta: {
icon: 'lucide:building',
title: 'Tenant Management',
},
},
{
name: 'UserManagement',
path: '/fengling/users',
component: () => import('#/views/fengling/users/index.vue'),
meta: {
icon: 'lucide:users',
title: 'User Management',
},
},
{
name: 'RoleManagement',
path: '/fengling/roles',
component: () => import('#/views/fengling/roles/index.vue'),
meta: {
icon: 'lucide:shield',
title: 'Role Management',
},
},
{
name: 'OAuthClientManagement',
path: '/fengling/oauth',
component: () => import('#/views/fengling/oauth/index.vue'),
meta: {
icon: 'lucide:key',
title: 'OAuth Clients',
},
},
{
name: 'Logs',
path: '/fengling/logs',
component: () => import('#/views/fengling/logs/index.vue'),
meta: {
icon: 'lucide:scroll-text',
title: 'Logs',
},
},
],
},
];
export default routes;

View File

@ -0,0 +1,66 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { ElCard, ElRow, ElCol, ElStatistic } from 'element-plus';
const stats = ref({
totalTenants: 0,
activeTenants: 0,
totalUsers: 0,
activeUsers: 0,
totalRoles: 0,
totalOAuthClients: 0,
auditLogsToday: 0,
});
onMounted(async () => {
});
</script>
<template>
<div class="p-5">
<h1 class="mb-5 text-2xl font-bold">Fengling Console Dashboard</h1>
<ElRow :gutter="20">
<ElCol :span="6">
<ElCard shadow="hover">
<ElStatistic title="Total Tenants" :value="stats.totalTenants" />
</ElCard>
</ElCol>
<ElCol :span="6">
<ElCard shadow="hover">
<ElStatistic title="Active Tenants" :value="stats.activeTenants" />
</ElCard>
</ElCol>
<ElCol :span="6">
<ElCard shadow="hover">
<ElStatistic title="Total Users" :value="stats.totalUsers" />
</ElCard>
</ElCol>
<ElCol :span="6">
<ElCard shadow="hover">
<ElStatistic title="Active Users" :value="stats.activeUsers" />
</ElCard>
</ElCol>
</ElRow>
<ElRow :gutter="20" class="mt-5">
<ElCol :span="6">
<ElCard shadow="hover">
<ElStatistic title="Total Roles" :value="stats.totalRoles" />
</ElCard>
</ElCol>
<ElCol :span="6">
<ElCard shadow="hover">
<ElStatistic title="OAuth Clients" :value="stats.totalOAuthClients" />
</ElCard>
</ElCol>
<ElCol :span="12">
<ElCard shadow="hover">
<ElStatistic title="Audit Logs Today" :value="stats.auditLogsToday" />
</ElCard>
</ElCol>
</ElRow>
</div>
</template>

View File

@ -0,0 +1,245 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import {
ElTable,
ElTableColumn,
ElButton,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElTag,
ElMessage,
ElPopconfirm,
} from 'element-plus';
import { OAuthApi, type FenglingApi } from '#/api/fengling';
const loading = ref(false);
const tableData = ref<FenglingApi.OAuth.OAuthClient[]>([]);
const dialogVisible = ref(false);
const dialogTitle = ref('Create OAuth Client');
const formData = ref<Partial<FenglingApi.OAuth.CreateOAuthClientDto>>({});
const searchKeyword = ref('');
const pagination = ref({
page: 1,
pageSize: 10,
total: 0,
});
const loadClients = async () => {
loading.value = true;
try {
const response = await OAuthApi.getClientList({
page: pagination.value.page,
pageSize: pagination.value.pageSize,
keyword: searchKeyword.value || undefined,
});
tableData.value = response.items;
pagination.value.total = response.totalCount;
} catch (error) {
ElMessage.error('Failed to load OAuth clients');
} finally {
loading.value = false;
}
};
const handleCreate = () => {
dialogTitle.value = 'Create OAuth Client';
formData.value = {
redirectUris: [],
postLogoutRedirectUris: [],
scopes: [],
grantTypes: [],
clientType: 'confidential',
consentType: 'explicit',
status: 'active',
};
dialogVisible.value = true;
};
const handleEdit = (row: FenglingApi.OAuth.OAuthClient) => {
dialogTitle.value = 'Edit OAuth Client';
formData.value = {
displayName: row.displayName,
redirectUris: row.redirectUris,
postLogoutRedirectUris: row.postLogoutRedirectUris,
scopes: row.scopes,
grantTypes: row.grantTypes,
clientType: row.clientType,
consentType: row.consentType,
status: row.status,
description: row.description,
};
dialogVisible.value = true;
};
const handleDelete = async (id: number) => {
try {
await OAuthApi.deleteClient(id);
ElMessage.success('OAuth client deleted successfully');
loadClients();
} catch (error) {
ElMessage.error('Failed to delete OAuth client');
}
};
const handleSave = async () => {
try {
if (dialogTitle.value === 'Create OAuth Client') {
await OAuthApi.createClient(formData.value as FenglingApi.OAuth.CreateOAuthClientDto);
ElMessage.success('OAuth client created successfully');
} else {
await OAuthApi.updateClient(tableData.value[0].id, formData.value as FenglingApi.OAuth.UpdateOAuthClientDto);
ElMessage.success('OAuth client updated successfully');
}
dialogVisible.value = false;
loadClients();
} catch (error) {
ElMessage.error('Failed to save OAuth client');
}
};
onMounted(() => {
loadClients();
});
</script>
<template>
<div class="p-5">
<div class="mb-4 flex items-center justify-between">
<div class="flex gap-2">
<el-input
v-model="searchKeyword"
placeholder="Search by keyword"
clearable
@clear="loadClients"
@keyup.enter="loadClients"
/>
<el-button type="primary" @click="loadClients">Search</el-button>
</div>
<el-button type="primary" @click="handleCreate">Create OAuth Client</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="clientId" label="Client ID" width="200" />
<el-table-column prop="displayName" label="Display Name" width="200" />
<el-table-column label="Redirect URIs" width="250">
<template #default="{ row }">
<div v-for="uri in row.redirectUris" :key="uri" class="mb-1 text-xs">
{{ uri }}
</div>
</template>
</el-table-column>
<el-table-column label="Scopes" width="200">
<template #default="{ row }">
<el-tag v-for="scope in row.scopes" :key="scope" class="mr-1 mb-1" size="small">
{{ scope }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="clientType" label="Client Type" width="120" />
<el-table-column prop="consentType" label="Consent Type" width="120" />
<el-table-column label="Status" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'danger'">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="updatedAt" label="Updated At" width="180" />
<el-table-column label="Actions" width="250" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">Edit</el-button>
<el-popconfirm title="Are you sure to delete this OAuth client?" @confirm="handleDelete(row.id)">
<template #reference>
<el-button size="small" type="danger">Delete</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="mt-4 flex justify-end">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@change="loadClients"
/>
</div>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
<el-form :model="formData" label-width="160px">
<el-form-item label="Client ID" v-if="dialogTitle === 'Create OAuth Client'">
<el-input v-model="formData.clientId" />
</el-form-item>
<el-form-item label="Display Name">
<el-input v-model="formData.displayName" />
</el-form-item>
<el-form-item label="Client Secret" v-if="dialogTitle === 'Create OAuth Client'">
<el-input v-model="formData.clientSecret" type="password" />
</el-form-item>
<el-form-item label="Redirect URIs">
<el-input
v-model="formData.redirectUris"
type="textarea"
placeholder="Enter redirect URIs separated by commas"
/>
</el-form-item>
<el-form-item label="Post Logout Redirect URIs">
<el-input
v-model="formData.postLogoutRedirectUris"
type="textarea"
placeholder="Enter post logout redirect URIs separated by commas"
/>
</el-form-item>
<el-form-item label="Scopes">
<el-input
v-model="formData.scopes"
type="textarea"
placeholder="Enter scopes separated by commas"
/>
</el-form-item>
<el-form-item label="Grant Types">
<el-input
v-model="formData.grantTypes"
type="textarea"
placeholder="Enter grant types separated by commas (e.g., authorization_code, client_credentials, refresh_token)"
/>
</el-form-item>
<el-form-item label="Client Type">
<el-select v-model="formData.clientType">
<el-option label="Confidential" value="confidential" />
<el-option label="Public" value="public" />
</el-select>
</el-form-item>
<el-form-item label="Consent Type">
<el-select v-model="formData.consentType">
<el-option label="Explicit" value="explicit" />
<el-option label="Implicit" value="implicit" />
<el-option label="External" value="external" />
</el-select>
</el-form-item>
<el-form-item label="Status">
<el-select v-model="formData.status">
<el-option label="Active" value="active" />
<el-option label="Inactive" value="inactive" />
</el-select>
</el-form-item>
<el-form-item label="Description">
<el-input v-model="formData.description" type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="handleSave">Save</el-button>
</template>
</el-dialog>
</div>
</template>

View File

@ -0,0 +1,175 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import {
ElTable,
ElTableColumn,
ElButton,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElTag,
ElMessage,
ElPopconfirm,
} from 'element-plus';
import { RoleApi, type FenglingApi } from '#/api/fengling';
const loading = ref(false);
const tableData = ref<FenglingApi.Role.Role[]>([]);
const dialogVisible = ref(false);
const dialogTitle = ref('Create Role');
const formData = ref<Partial<FenglingApi.Role.CreateRoleDto>>({});
const searchKeyword = ref('');
const pagination = ref({
page: 1,
pageSize: 10,
total: 0,
});
const loadRoles = async () => {
loading.value = true;
try {
const response = await RoleApi.getRoleList({
page: pagination.value.page,
pageSize: pagination.value.pageSize,
keyword: searchKeyword.value || undefined,
});
tableData.value = response.items;
pagination.value.total = response.totalCount;
} catch (error) {
ElMessage.error('Failed to load roles');
} finally {
loading.value = false;
}
};
const handleCreate = () => {
dialogTitle.value = 'Create Role';
formData.value = {
permissions: [],
};
dialogVisible.value = true;
};
const handleEdit = (row: FenglingApi.Role.Role) => {
dialogTitle.value = 'Edit Role';
formData.value = {
displayName: row.displayName,
description: row.description,
permissions: row.permissions,
};
dialogVisible.value = true;
};
const handleDelete = async (id: number) => {
try {
await RoleApi.deleteRole(id);
ElMessage.success('Role deleted successfully');
loadRoles();
} catch (error) {
ElMessage.error('Failed to delete role');
}
};
const handleSave = async () => {
try {
if (dialogTitle.value === 'Create Role') {
await RoleApi.createRole(formData.value as FenglingApi.Role.CreateRoleDto);
ElMessage.success('Role created successfully');
} else {
await RoleApi.updateRole(tableData.value[0].id, formData.value as FenglingApi.Role.UpdateRoleDto);
ElMessage.success('Role updated successfully');
}
dialogVisible.value = false;
loadRoles();
} catch (error) {
ElMessage.error('Failed to save role');
}
};
onMounted(() => {
loadRoles();
});
</script>
<template>
<div class="p-5">
<div class="mb-4 flex items-center justify-between">
<div class="flex gap-2">
<el-input
v-model="searchKeyword"
placeholder="Search by keyword"
clearable
@clear="loadRoles"
@keyup.enter="loadRoles"
/>
<el-button type="primary" @click="loadRoles">Search</el-button>
</div>
<el-button type="primary" @click="handleCreate">Create Role</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="Name" width="150" />
<el-table-column prop="displayName" label="Display Name" width="200" />
<el-table-column prop="description" label="Description" width="250" />
<el-table-column label="Permissions" width="300">
<template #default="{ row }">
<el-tag v-for="perm in row.permissions" :key="perm" class="mr-1 mb-1" size="small">
{{ perm }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="Created At" width="180" />
<el-table-column label="Actions" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">Edit</el-button>
<el-popconfirm title="Are you sure to delete this role?" @confirm="handleDelete(row.id)">
<template #reference>
<el-button size="small" type="danger">Delete</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="mt-4 flex justify-end">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@change="loadRoles"
/>
</div>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
<el-form :model="formData" label-width="120px">
<el-form-item label="Name" v-if="dialogTitle === 'Create Role'">
<el-input v-model="formData.name" />
</el-form-item>
<el-form-item label="Display Name">
<el-input v-model="formData.displayName" />
</el-form-item>
<el-form-item label="Description">
<el-input v-model="formData.description" type="textarea" />
</el-form-item>
<el-form-item label="Permissions">
<el-input
v-model="formData.permissions"
type="textarea"
placeholder="Enter permissions separated by commas"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="handleSave">Save</el-button>
</template>
</el-dialog>
</div>
</template>

View File

@ -0,0 +1,209 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import {
ElTable,
ElTableColumn,
ElButton,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
ElInputNumber,
ElDatePicker,
ElTag,
ElMessage,
ElPopconfirm,
} from 'element-plus';
import { TenantApi, type FenglingApi } from '#/api/fengling';
const loading = ref(false);
const tableData = ref<FenglingApi.Tenant.Tenant[]>([]);
const dialogVisible = ref(false);
const dialogTitle = ref('Create Tenant');
const formData = ref<Partial<FenglingApi.Tenant.CreateTenantDto>>({});
const searchKeyword = ref('');
const searchStatus = ref('');
const pagination = ref({
page: 1,
pageSize: 10,
total: 0,
});
const loadTenants = async () => {
loading.value = true;
try {
const response = await TenantApi.getTenantList({
page: pagination.value.page,
pageSize: pagination.value.pageSize,
keyword: searchKeyword.value || undefined,
status: searchStatus.value || undefined,
});
tableData.value = response.items;
pagination.value.total = response.totalCount;
} catch (error) {
ElMessage.error('Failed to load tenants');
} finally {
loading.value = false;
}
};
const handleCreate = () => {
dialogTitle.value = 'Create Tenant';
formData.value = {
status: 'active',
};
dialogVisible.value = true;
};
const handleEdit = (row: FenglingApi.Tenant.Tenant) => {
dialogTitle.value = 'Edit Tenant';
formData.value = {
name: row.name,
contactName: row.contactName,
contactEmail: row.contactEmail,
contactPhone: row.contactPhone,
maxUsers: row.maxUsers,
expiresAt: row.expiresAt,
status: row.status === 'expired' ? 'inactive' : row.status,
description: row.description,
};
dialogVisible.value = true;
};
const handleDelete = async (id: number) => {
try {
await TenantApi.deleteTenant(id);
ElMessage.success('Tenant deleted successfully');
loadTenants();
} catch (error) {
ElMessage.error('Failed to delete tenant');
}
};
const handleSave = async () => {
try {
if (dialogTitle.value === 'Create Tenant') {
await TenantApi.createTenant(formData.value as FenglingApi.Tenant.CreateTenantDto);
ElMessage.success('Tenant created successfully');
} else {
await TenantApi.updateTenant(tableData.value[0].id, formData.value as FenglingApi.Tenant.UpdateTenantDto);
ElMessage.success('Tenant updated successfully');
}
dialogVisible.value = false;
loadTenants();
} catch (error) {
ElMessage.error('Failed to save tenant');
}
};
onMounted(() => {
loadTenants();
});
</script>
<template>
<div class="p-5">
<div class="mb-4 flex items-center justify-between">
<div class="flex gap-2">
<el-input
v-model="searchKeyword"
placeholder="Search by keyword"
clearable
@clear="loadTenants"
@keyup.enter="loadTenants"
/>
<el-select v-model="searchStatus" placeholder="Status" clearable @change="loadTenants">
<el-option label="Active" value="active" />
<el-option label="Inactive" value="inactive" />
<el-option label="Expired" value="expired" />
</el-select>
<el-button type="primary" @click="loadTenants">Search</el-button>
</div>
<el-button type="primary" @click="handleCreate">Create Tenant</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="tenantId" label="Tenant ID" width="150" />
<el-table-column prop="name" label="Name" width="200" />
<el-table-column prop="contactName" label="Contact Name" width="150" />
<el-table-column prop="contactEmail" label="Contact Email" width="200" />
<el-table-column prop="contactPhone" label="Contact Phone" width="150" />
<el-table-column prop="userCount" label="User Count" width="100" />
<el-table-column label="Status" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : row.status === 'expired' ? 'danger' : 'warning'">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="expiresAt" label="Expires At" width="180" />
<el-table-column prop="createdAt" label="Created At" width="180" />
<el-table-column label="Actions" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">Edit</el-button>
<el-popconfirm title="Are you sure to delete this tenant?" @confirm="handleDelete(row.id)">
<template #reference>
<el-button size="small" type="danger">Delete</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="mt-4 flex justify-end">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@change="loadTenants"
/>
</div>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form :model="formData" label-width="120px">
<el-form-item label="Tenant ID">
<el-input v-model="formData.tenantId" />
</el-form-item>
<el-form-item label="Name">
<el-input v-model="formData.name" />
</el-form-item>
<el-form-item label="Contact Name">
<el-input v-model="formData.contactName" />
</el-form-item>
<el-form-item label="Contact Email">
<el-input v-model="formData.contactEmail" />
</el-form-item>
<el-form-item label="Contact Phone">
<el-input v-model="formData.contactPhone" />
</el-form-item>
<el-form-item label="Max Users">
<el-input-number v-model="formData.maxUsers" :min="1" />
</el-form-item>
<el-form-item label="Expires At">
<el-date-picker v-model="formData.expiresAt" type="datetime" />
</el-form-item>
<el-form-item label="Status">
<el-select v-model="formData.status">
<el-option label="Active" value="active" />
<el-option label="Inactive" value="inactive" />
</el-select>
</el-form-item>
<el-form-item label="Description">
<el-input v-model="formData.description" type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="handleSave">Save</el-button>
</template>
</el-dialog>
</div>
</template>

View File

@ -0,0 +1,185 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import {
ElTable,
ElTableColumn,
ElButton,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElTag,
ElMessage,
ElPopconfirm,
ElSwitch,
} from 'element-plus';
import { UserApi, type FenglingApi } from '#/api/fengling';
const loading = ref(false);
const tableData = ref<FenglingApi.User.User[]>([]);
const dialogVisible = ref(false);
const dialogTitle = ref('Create User');
const formData = ref<Partial<FenglingApi.User.CreateUserDto>>({});
const searchKeyword = ref('');
const pagination = ref({
page: 1,
pageSize: 10,
total: 0,
});
const loadUsers = async () => {
loading.value = true;
try {
const response = await UserApi.getUserList({
page: pagination.value.page,
pageSize: pagination.value.pageSize,
keyword: searchKeyword.value || undefined,
});
tableData.value = response.items;
pagination.value.total = response.totalCount;
} catch (error) {
ElMessage.error('Failed to load users');
} finally {
loading.value = false;
}
};
const handleCreate = () => {
dialogTitle.value = 'Create User';
formData.value = {
roleIds: [],
};
dialogVisible.value = true;
};
const handleEdit = (row: FenglingApi.User.User) => {
dialogTitle.value = 'Edit User';
formData.value = {
email: row.email,
realName: row.realName,
isActive: row.isActive,
roleIds: [],
};
dialogVisible.value = true;
};
const handleDelete = async (id: number) => {
try {
await UserApi.deleteUser(id);
ElMessage.success('User deleted successfully');
loadUsers();
} catch (error) {
ElMessage.error('Failed to delete user');
}
};
const handleSave = async () => {
try {
if (dialogTitle.value === 'Create User') {
await UserApi.createUser(formData.value as FenglingApi.User.CreateUserDto);
ElMessage.success('User created successfully');
} else {
await UserApi.updateUser(tableData.value[0].id, formData.value as FenglingApi.User.UpdateUserDto);
ElMessage.success('User updated successfully');
}
dialogVisible.value = false;
loadUsers();
} catch (error) {
ElMessage.error('Failed to save user');
}
};
onMounted(() => {
loadUsers();
});
</script>
<template>
<div class="p-5">
<div class="mb-4 flex items-center justify-between">
<div class="flex gap-2">
<el-input
v-model="searchKeyword"
placeholder="Search by keyword"
clearable
@clear="loadUsers"
@keyup.enter="loadUsers"
/>
<el-button type="primary" @click="loadUsers">Search</el-button>
</div>
<el-button type="primary" @click="handleCreate">Create User</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="userName" label="Username" width="150" />
<el-table-column prop="email" label="Email" width="200" />
<el-table-column prop="realName" label="Real Name" width="150" />
<el-table-column prop="tenantId" label="Tenant ID" width="150" />
<el-table-column label="Roles" width="200">
<template #default="{ row }">
<el-tag v-for="role in row.roles" :key="role" class="mr-1">{{ role }}</el-tag>
</template>
</el-table-column>
<el-table-column label="Active" width="100">
<template #default="{ row }">
<el-tag :type="row.isActive ? 'success' : 'info'">
{{ row.isActive ? 'Yes' : 'No' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="Created At" width="180" />
<el-table-column label="Actions" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">Edit</el-button>
<el-popconfirm title="Are you sure to delete this user?" @confirm="handleDelete(row.id)">
<template #reference>
<el-button size="small" type="danger">Delete</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="mt-4 flex justify-end">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@change="loadUsers"
/>
</div>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form :model="formData" label-width="120px">
<el-form-item label="Username">
<el-input v-model="formData.userName" />
</el-form-item>
<el-form-item label="Email">
<el-input v-model="formData.email" />
</el-form-item>
<el-form-item label="Real Name">
<el-input v-model="formData.realName" />
</el-form-item>
<el-form-item label="Password" v-if="dialogTitle === 'Create User'">
<el-input v-model="formData.password" type="password" />
</el-form-item>
<el-form-item label="Tenant ID">
<el-input v-model="formData.tenantId" />
</el-form-item>
<el-form-item label="Active">
<el-switch v-model="formData.isActive" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="handleSave">Save</el-button>
</template>
</el-dialog>
</div>
</template>