feat: 新增命名空间页面

This commit is contained in:
xlsea 2024-04-03 16:33:18 +08:00
parent e93631a81b
commit e25bd1dea7
21 changed files with 472 additions and 36 deletions

View File

@ -156,6 +156,7 @@ const local: App.I18n.Schema = {
'function_toggle-auth': 'Toggle Auth',
'function_super-page': 'Super Admin Visible',
pods: 'Online Machine',
namepase: 'Namepase',
manage: 'System Manage',
manage_user: 'User Manage',
'manage_user-detail': 'User Detail',
@ -173,6 +174,10 @@ const local: App.I18n.Schema = {
exception_500: '500'
},
page: {
common: {
upadteTime: 'Update Time',
createTime: 'Create Time'
},
login: {
common: {
loginOrRegister: 'Login / Register',
@ -300,6 +305,19 @@ const local: App.I18n.Schema = {
server: 'Server'
}
},
namespace: {
title: 'Namespace',
name: 'Name',
keyword: 'Name/UniqueId',
uniqueId: 'UniqueId',
form: {
name: 'Please enter name',
keyword: 'Please enter name/uniqueId',
uniqueId: 'Please enter UniqueId'
},
addNamespace: 'Add Namespaces',
editNamespace: 'Edit Namespaces'
},
function: {
tab: {
tabOperate: {

View File

@ -156,6 +156,7 @@ const local: App.I18n.Schema = {
'function_toggle-auth': '切换权限',
'function_super-page': '超级管理员可见',
pods: '在线机器',
namepase: '命名空间',
manage: '系统管理',
manage_user: '用户管理',
'manage_user-detail': '用户详情',
@ -173,6 +174,10 @@ const local: App.I18n.Schema = {
exception_500: '500'
},
page: {
common: {
upadteTime: '更新时间',
createTime: '创建时间'
},
login: {
common: {
loginOrRegister: '登录 / 注册',
@ -296,6 +301,19 @@ const local: App.I18n.Schema = {
server: '服务端'
}
},
namespace: {
title: '命名空间',
name: '名称',
keyword: '空间名称/唯一标识',
uniqueId: 'UniqueId',
form: {
name: '请输入名称',
keyword: '请输入空间名称/唯一标识',
uniqueId: '请输入UniqueId'
},
addNamespace: '新增命名空间',
editNamespace: '编辑命名空间'
},
function: {
tab: {
tabOperate: {

View File

@ -35,6 +35,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
manage_user: () => import("@/views/manage/user/index.vue"),
"multi-menu_first_child": () => import("@/views/multi-menu/first_child/index.vue"),
"multi-menu_second_child_home": () => import("@/views/multi-menu/second_child_home/index.vue"),
namepase: () => import("@/views/namepase/index.vue"),
pods: () => import("@/views/pods/index.vue"),
"user-center": () => import("@/views/user-center/index.vue"),
};

View File

@ -327,6 +327,17 @@ export const generatedRoutes: GeneratedRoute[] = [
icon: 'ant-design:database-outlined',
order: 1
}
},
{
name: 'namepase',
path: '/namepase',
component: 'layout.base$view.namepase',
meta: {
title: 'namepase',
i18nKey: 'route.namepase',
icon: 'oui:app-spaces',
order: 2
}
},
{
name: 'user-center',

View File

@ -174,6 +174,7 @@ const routeMap: RouteMap = {
"multi-menu_second": "/multi-menu/second",
"multi-menu_second_child": "/multi-menu/second/child",
"multi-menu_second_child_home": "/multi-menu/second/child/home",
"namepase": "/namepase",
"pods": "/pods",
"user-center": "/user-center"
};

View File

@ -2,4 +2,5 @@ export * from './auth';
export * from './route';
export * from './system';
export * from './dashboard';
export * from './namespace';
export * from './system-manage';

View File

@ -0,0 +1,10 @@
import { request } from '../request';
/** Namespace */
export function fetchGetNamespaceList(params?: Api.Namespace.NamespaceSearchParams) {
return request<Api.Namespace.NamespaceList>({
url: '/namespace/list',
method: 'get',
params
});
}

37
src/typings/api.d.ts vendored
View File

@ -41,15 +41,15 @@ declare namespace Api {
/** common record */
type CommonRecord<T = any> = {
/** record id */
id?: number;
id?: string;
/** record creator */
createBy?: string;
/** record create time */
createTime?: string;
createDt?: string;
/** record updater */
updateBy?: string;
/** record update time */
updateTime?: string;
updateDt?: string;
/** record status */
status?: EnableStatus | null;
} & T;
@ -243,6 +243,31 @@ declare namespace Api {
>;
}
/**
* namespace Namespace
*
* backend api module: "Namespace"
*/
namespace Namespace {
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'page' | 'size'>;
/** namespace */
type Namespace = Common.CommonRecord<{
/** 主键 */
id: string;
/** 名称 */
name: string;
/** UniqueId */
uniqueId: string;
}>;
/** namespace search params */
type NamespaceSearchParams = CommonType.RecordNullable<{ keyword: string } & CommonSearchParams>;
/** namespace list */
type NamespaceList = Common.PaginatingQueryRecord<Namespace>;
}
/**
* namespace SystemManage
*
@ -253,7 +278,7 @@ declare namespace Api {
/** role */
type Role = Common.CommonRecord<{
id: number;
id: string;
/** role name */
roleName: string;
/** role code */
@ -334,7 +359,7 @@ declare namespace Api {
type IconType = '1' | '2';
type Menu = Common.CommonRecord<{
id: number;
id: string;
/** parent menu id */
parentId: number;
/** menu type */
@ -388,7 +413,7 @@ declare namespace Api {
type MenuList = Common.PaginatingQueryRecord<Menu>;
type MenuTree = {
id: number;
id: string;
label: string;
pId: number;
children?: MenuTree[];

17
src/typings/app.d.ts vendored
View File

@ -352,6 +352,10 @@ declare namespace App {
};
route: Record<I18nRouteKey, string>;
page: {
common: {
upadteTime: string;
createTime: string;
};
login: {
common: {
loginOrRegister: string;
@ -475,6 +479,19 @@ declare namespace App {
server: string;
};
};
namespace: {
title: string;
name: string;
keyword: string;
uniqueId: string;
form: {
name: string;
keyword: string;
uniqueId: string;
};
addNamespace: string;
editNamespace: string;
};
function: {
tab: {
tabOperate: {

View File

@ -48,6 +48,7 @@ declare module "@elegant-router/types" {
"multi-menu_second": "/multi-menu/second";
"multi-menu_second_child": "/multi-menu/second/child";
"multi-menu_second_child_home": "/multi-menu/second/child/home";
"namepase": "/namepase";
"pods": "/pods";
"user-center": "/user-center";
};
@ -94,6 +95,7 @@ declare module "@elegant-router/types" {
| "login"
| "manage"
| "multi-menu"
| "namepase"
| "pods"
| "user-center"
>;
@ -133,6 +135,7 @@ declare module "@elegant-router/types" {
| "manage_user"
| "multi-menu_first_child"
| "multi-menu_second_child_home"
| "namepase"
| "pods"
| "user-center"
>;

View File

@ -14,24 +14,24 @@ const authStore = useAuthStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16));
interface StatisticData {
id: number;
id: string;
label: string;
value: string;
}
const statisticData = computed<StatisticData[]>(() => [
{
id: 0,
id: '0',
label: $t('page.home.userCount'),
value: '2'
},
{
id: 1,
id: '1',
label: $t('page.home.jobTaskCount'),
value: '8'
},
{
id: 2,
id: '2',
label: $t('page.home.retryTaskCount'),
value: '3'
}

View File

@ -7,17 +7,17 @@ defineOptions({
});
interface NewsItem {
id: number;
id: string;
content: string;
time: string;
}
const newses = computed<NewsItem[]>(() => [
{ id: 1, content: $t('page.home.projectNews.desc1'), time: '2021-05-28 22:22:22' },
{ id: 2, content: $t('page.home.projectNews.desc2'), time: '2021-10-27 10:24:54' },
{ id: 3, content: $t('page.home.projectNews.desc3'), time: '2021-10-31 22:43:12' },
{ id: 4, content: $t('page.home.projectNews.desc4'), time: '2021-11-03 20:33:31' },
{ id: 5, content: $t('page.home.projectNews.desc5'), time: '2021-11-07 22:45:32' }
{ id: '1', content: $t('page.home.projectNews.desc1'), time: '2021-05-28 22:22:22' },
{ id: '2', content: $t('page.home.projectNews.desc2'), time: '2021-10-27 10:24:54' },
{ id: '3', content: $t('page.home.projectNews.desc3'), time: '2021-10-31 22:43:12' },
{ id: '4', content: $t('page.home.projectNews.desc4'), time: '2021-11-03 20:33:31' },
{ id: '5', content: $t('page.home.projectNews.desc5'), time: '2021-11-07 22:45:32' }
]);
</script>

View File

@ -186,7 +186,7 @@ async function handleBatchDelete() {
onBatchDeleted();
}
function handleDelete(id: number) {
function handleDelete(id: string) {
// request
console.log(id);

View File

@ -115,14 +115,14 @@ async function handleBatchDelete() {
onBatchDeleted();
}
function handleDelete(id: number) {
function handleDelete(id: string) {
// request
console.log(id);
onDeleted();
}
function edit(id: number) {
function edit(id: string) {
handleEdit(id);
}
</script>

View File

@ -8,7 +8,7 @@ defineOptions({
interface Props {
/** the roleId */
roleId: number;
roleId: string | number;
}
const props = defineProps<Props>();
@ -24,7 +24,7 @@ function closeModal() {
const title = computed(() => $t('common.edit') + $t('page.manage.role.buttonAuth'));
type ButtonConfig = {
id: number;
id: string;
label: string;
code: string;
};
@ -34,16 +34,16 @@ const tree = shallowRef<ButtonConfig[]>([]);
async function getAllButtons() {
// request
tree.value = [
{ id: 1, label: 'button1', code: 'code1' },
{ id: 2, label: 'button2', code: 'code2' },
{ id: 3, label: 'button3', code: 'code3' },
{ id: 4, label: 'button4', code: 'code4' },
{ id: 5, label: 'button5', code: 'code5' },
{ id: 6, label: 'button6', code: 'code6' },
{ id: 7, label: 'button7', code: 'code7' },
{ id: 8, label: 'button8', code: 'code8' },
{ id: 9, label: 'button9', code: 'code9' },
{ id: 10, label: 'button10', code: 'code10' }
{ id: '1', label: 'button1', code: 'code1' },
{ id: '2', label: 'button2', code: 'code2' },
{ id: '3', label: 'button3', code: 'code3' },
{ id: '4', label: 'button4', code: 'code4' },
{ id: '5', label: 'button5', code: 'code5' },
{ id: '6', label: 'button6', code: 'code6' },
{ id: '7', label: 'button7', code: 'code7' },
{ id: '8', label: 'button8', code: 'code8' },
{ id: '9', label: 'button9', code: 'code9' },
{ id: '10', label: 'button10', code: 'code10' }
];
}

View File

@ -9,7 +9,7 @@ defineOptions({
interface Props {
/** the roleId */
roleId: number;
roleId: string | number;
}
const props = defineProps<Props>();

View File

@ -145,14 +145,14 @@ async function handleBatchDelete() {
onBatchDeleted();
}
function handleDelete(id: number) {
function handleDelete(id: string) {
// request
console.log(id);
onDeleted();
}
function edit(id: number) {
function edit(id: string) {
handleEdit(id);
}
</script>

View File

@ -0,0 +1,153 @@
<script setup lang="tsx">
import { NButton, NPopconfirm } from 'naive-ui';
import { fetchGetNamespaceList } from '@/service/api';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import NamespaceOperateDrawer from './modules/namespace-operate-drawer.vue';
import NamespaceSearch from './modules/namespace-search.vue';
const appStore = useAppStore();
const { columns, columnChecks, data, getData, loading, mobilePagination, searchParams, resetSearchParams } = useTable({
apiFn: fetchGetNamespaceList,
apiParams: {
page: 1,
size: 10,
keyword: undefined
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
key: 'name',
title: $t('page.namespace.name'),
align: 'left',
minWidth: 120
},
{
key: 'uniqueId',
title: $t('page.namespace.uniqueId'),
align: 'left',
minWidth: 180
},
{
key: 'createDt',
title: $t('page.common.createTime'),
align: 'left',
minWidth: 120
},
{
key: 'updateDt',
title: $t('page.common.upadteTime'),
align: 'left',
minWidth: 120
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => (
<div class="flex-center gap-8px">
<NButton type="primary" ghost size="small" onClick={() => edit(row.id)}>
{$t('common.edit')}
</NButton>
<NPopconfirm onPositiveClick={() => handleDelete(row.id)}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" ghost size="small">
{$t('common.delete')}
</NButton>
)
}}
</NPopconfirm>
</div>
)
}
]
});
const {
drawerVisible,
operateType,
editingData,
handleAdd,
handleEdit,
checkedRowKeys,
onBatchDeleted,
onDeleted
// closeDrawer
} = useTableOperate(data, getData);
async function handleBatchDelete() {
// request
console.log(checkedRowKeys.value);
onBatchDeleted();
}
function handleDelete(id: string) {
// request
console.log(id);
onDeleted();
}
function edit(id: string) {
handleEdit(id);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<NamespaceSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
<NCard :title="$t('page.namespace.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<NamespaceOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getData"
/>
</NCard>
</div>
</template>
<style scoped>
:deep(.n-card-header) {
--n-title-font-weight: 600 !important;
}
</style>

View File

@ -0,0 +1,109 @@
<script setup lang="ts">
import { computed, reactive, watch } from 'vue';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'NamespaceOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.Namespace.Namespace | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: $t('page.namespace.addNamespace'),
edit: $t('page.namespace.editNamespace')
};
return titles[props.operateType];
});
type Model = Pick<Api.Namespace.Namespace, 'name' | 'uniqueId'>;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
name: '',
uniqueId: ''
};
}
type RuleKey = Extract<keyof Model, 'name'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
name: defaultRequiredRule
};
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
return;
}
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData);
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
// request
window.$message?.success($t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenEdit();
restoreValidation();
}
});
</script>
<template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="360">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem :label="$t('page.namespace.name')" path="name">
<NInput v-model:value="model.name" :placeholder="$t('page.namespace.form.name')" />
</NFormItem>
<NFormItem :label="$t('page.namespace.uniqueId')" path="uniqueId">
<NInput v-model:value="model.uniqueId" :placeholder="$t('page.namespace.form.uniqueId')" />
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { $t } from '@/locales';
import { useNaiveForm } from '@/hooks/common/form';
defineOptions({
name: 'NamespaceSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Namespace.NamespaceSearchParams>('model', { required: true });
async function reset() {
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80" :show-feedback="false">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.namespace.keyword')" path="userName" class="pr-24px">
<NInput v-model:value="model.keyword" :placeholder="$t('page.namespace.form.keyword')" />
</NFormItemGi>
<NFormItemGi span="24 m:12" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCard>
</template>
<style scoped></style>

View File

@ -117,7 +117,13 @@ const { checkedRowKeys } = useTableOperate(data, getData);
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<PodsSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
<NCard :title="$t('page.pods.title')" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<NCard
:title="$t('page.pods.title')"
:bordered="false"
size="small"
header-style="font-weight: 800;"
class="sm:flex-1-hidden card-wrapper"
>
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
@ -144,4 +150,8 @@ const { checkedRowKeys } = useTableOperate(data, getData);
</div>
</template>
<style scoped></style>
<style scoped>
:deep(.n-card-header) {
--n-title-font-weight: 600 !important;
}
</style>