feat(fengling): add routes and pages for Fengling Console management
This commit is contained in:
parent
e5bbe101c9
commit
87db42b5db
72
apps/web-ele/src/router/routes/modules/fengling.ts
Normal file
72
apps/web-ele/src/router/routes/modules/fengling.ts
Normal 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;
|
||||||
66
apps/web-ele/src/views/fengling/dashboard/index.vue
Normal file
66
apps/web-ele/src/views/fengling/dashboard/index.vue
Normal 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>
|
||||||
245
apps/web-ele/src/views/fengling/oauth/index.vue
Normal file
245
apps/web-ele/src/views/fengling/oauth/index.vue
Normal 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>
|
||||||
175
apps/web-ele/src/views/fengling/roles/index.vue
Normal file
175
apps/web-ele/src/views/fengling/roles/index.vue
Normal 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>
|
||||||
209
apps/web-ele/src/views/fengling/tenants/index.vue
Normal file
209
apps/web-ele/src/views/fengling/tenants/index.vue
Normal 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>
|
||||||
185
apps/web-ele/src/views/fengling/users/index.vue
Normal file
185
apps/web-ele/src/views/fengling/users/index.vue
Normal 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>
|
||||||
Loading…
Reference in New Issue
Block a user