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