Restore core admin menu modules

This commit is contained in:
2026-06-09 11:02:08 +08:00
parent e5faa6e061
commit 9f9423dd23
6 changed files with 432 additions and 7 deletions
+7 -6
View File
@@ -29,6 +29,7 @@ export interface PostCreateInput {
status?: boolean;
description?: string;
dept_id?: string;
sort?: number;
}
export interface PostUpdateInput extends Partial<PostCreateInput> {}
@@ -120,7 +121,7 @@ export async function deletePostApi(postId: string) {
*/
export async function batchDeletePostApi(data: PostBatchDeleteInput) {
return requestClient.post<{ count: number }>(
'/api/core/post/batch_delete',
'/api/core/post/batch/delete',
data,
);
}
@@ -132,7 +133,7 @@ export async function batchUpdatePostStatusApi(
data: PostBatchUpdateStatusInput,
) {
return requestClient.post<{ count: number }>(
'/api/core/post/batch_update_status',
'/api/core/post/batch/status',
data,
);
}
@@ -141,7 +142,7 @@ export async function batchUpdatePostStatusApi(
* 根据部门ID获取岗位列表
*/
export async function getPostsByDeptApi(deptId: string) {
return requestClient.get<Post[]>(`/api/core/post/by_dept/${deptId}`);
return requestClient.get<Post[]>(`/api/core/post/by/dept/${deptId}`);
}
/**
@@ -194,7 +195,7 @@ export async function getPostStatsApi() {
* 导出岗位数据
*/
export async function exportPostApi(params?: PostListParams) {
return requestClient.get<Blob>('/api/core/post/export', {
return requestClient.get<Blob>('/api/core/post/export/excel', {
params,
responseType: 'blob',
});
@@ -207,7 +208,7 @@ export async function importPostApi(file: File) {
const formData = new FormData();
formData.append('file', file);
return requestClient.post<{ error_count: number; success_count: number }>(
'/api/core/post/import',
'/api/core/post/import/excel',
formData,
{
headers: {
@@ -221,7 +222,7 @@ export async function importPostApi(file: File) {
* 获取简单岗位列表(用于选择器)
*/
export async function getSimplePostListApi() {
return requestClient.get<Post[]>('/api/core/post/simple');
return requestClient.get<Post[]>('/api/core/post/all');
}
/**
+5
View File
@@ -24,10 +24,15 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
'../views/_core/authentication/login.vue',
'../views/_core/authentication/oauth-callback.vue',
'../views/_core/control-center/index.vue',
'../views/_core/dept/index.vue',
'../views/_core/dict/index.vue',
'../views/_core/fallback/**/*.vue',
'../views/_core/file-manager/index.vue',
'../views/_core/login-log/index.vue',
'../views/_core/menu/index.vue',
'../views/_core/message/index.vue',
'../views/_core/org-chart/index.vue',
'../views/_core/post/index.vue',
'../views/_core/permission/index.vue',
'../views/_core/role/index.vue',
'../views/_core/system-config/index.vue',
@@ -43,10 +43,15 @@ const componentKeys: string[] = Object.keys(
'../../views/_core/authentication/login.vue',
'../../views/_core/authentication/oauth-callback.vue',
'../../views/_core/control-center/index.vue',
'../../views/_core/dept/index.vue',
'../../views/_core/dict/index.vue',
'../../views/_core/fallback/**/*.vue',
'../../views/_core/file-manager/index.vue',
'../../views/_core/login-log/index.vue',
'../../views/_core/menu/index.vue',
'../../views/_core/message/index.vue',
'../../views/_core/org-chart/index.vue',
'../../views/_core/post/index.vue',
'../../views/_core/permission/index.vue',
'../../views/_core/role/index.vue',
'../../views/_core/system-config/index.vue',
@@ -0,0 +1,406 @@
<script lang="ts" setup>
import type { FormInstance, FormRules } from 'element-plus';
import type { Post, PostCreateInput } from '#/api/core/post';
import { computed, onMounted, reactive, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Edit, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
import {
ElButton,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
ElOption,
ElPagination,
ElSelect,
ElSwitch,
ElTable,
ElTableColumn,
ElTag,
} from 'element-plus';
import {
createPostApi,
deletePostApi,
getPostListApi,
updatePostApi,
} from '#/api/core/post';
defineOptions({ name: 'SystemPost' });
type NullableStatus = '' | boolean;
const postTypeOptions = [
{ label: '管理岗', value: 0 },
{ label: '技术岗', value: 1 },
{ label: '业务岗', value: 2 },
{ label: '职能岗', value: 3 },
{ label: '其他', value: 4 },
];
const postLevelOptions = [
{ label: '高层', value: 0 },
{ label: '中层', value: 1 },
{ label: '基层', value: 2 },
{ label: '一般员工', value: 3 },
];
const loading = ref(false);
const saving = ref(false);
const dialogVisible = ref(false);
const editingPostId = ref<string>();
const tableData = ref<Post[]>([]);
const formRef = ref<FormInstance>();
const query = reactive({
code: '',
name: '',
page: 1,
pageSize: 20,
status: '' as NullableStatus,
});
const total = ref(0);
const form = reactive<PostCreateInput>({
code: '',
description: '',
name: '',
post_level: 3,
post_type: 4,
sort: 0,
status: true,
});
const rules: FormRules = {
code: [
{ message: '请输入岗位编码', required: true, trigger: 'blur' },
{
message: '岗位编码只能包含字母、数字、下划线和横线',
pattern: /^[\w-]+$/,
trigger: 'blur',
},
],
name: [{ message: '请输入岗位名称', required: true, trigger: 'blur' }],
};
const dialogTitle = computed(() =>
editingPostId.value ? '编辑岗位' : '新增岗位',
);
function getOptionLabel(
options: Array<{ label: string; value: number }>,
value?: number,
) {
return options.find((item) => item.value === value)?.label ?? '-';
}
function resetForm() {
editingPostId.value = undefined;
Object.assign(form, {
code: '',
description: '',
name: '',
post_level: 3,
post_type: 4,
sort: 0,
status: true,
});
formRef.value?.clearValidate();
}
async function loadPosts() {
loading.value = true;
try {
const res = await getPostListApi({
code: query.code || undefined,
name: query.name || undefined,
page: query.page,
pageSize: query.pageSize,
status: query.status === '' ? undefined : query.status,
});
tableData.value = res.items ?? [];
total.value = res.total ?? 0;
} finally {
loading.value = false;
}
}
function onSearch() {
query.page = 1;
loadPosts();
}
function onReset() {
Object.assign(query, {
code: '',
name: '',
page: 1,
status: '',
});
loadPosts();
}
function onCreate() {
resetForm();
dialogVisible.value = true;
}
function onEdit(row: Post) {
resetForm();
editingPostId.value = row.id;
Object.assign(form, {
code: row.code,
description: row.description ?? '',
name: row.name,
post_level: row.post_level ?? 3,
post_type: row.post_type ?? 4,
sort: row.sort ?? 0,
status: row.status,
});
dialogVisible.value = true;
}
async function onSave() {
await formRef.value?.validate();
saving.value = true;
try {
const payload = { ...form };
if (editingPostId.value) {
await updatePostApi(editingPostId.value, payload);
ElMessage.success('岗位已更新');
} else {
await createPostApi(payload);
ElMessage.success('岗位已新增');
}
dialogVisible.value = false;
await loadPosts();
} finally {
saving.value = false;
}
}
async function onDelete(row: Post) {
await ElMessageBox.confirm(
`确定删除岗位「${row.name}」吗?`,
'删除岗位',
{
cancelButtonText: '取消',
confirmButtonText: '删除',
type: 'warning',
},
);
await deletePostApi(row.id);
ElMessage.success('岗位已删除');
await loadPosts();
}
async function onStatusChange(row: Post, status: boolean) {
try {
await updatePostApi(row.id, { status });
ElMessage.success(status ? '岗位已启用' : '岗位已禁用');
} catch (error) {
row.status = !status;
throw error;
}
}
onMounted(loadPosts);
</script>
<template>
<Page auto-content-height>
<div class="flex h-full min-h-0 flex-col gap-3">
<div
class="bg-background flex flex-wrap items-end gap-3 rounded-lg border border-border px-4 py-3"
>
<ElForm inline :model="query" class="post-search-form">
<ElFormItem label="岗位名称">
<ElInput
v-model="query.name"
clearable
placeholder="请输入岗位名称"
@keyup.enter="onSearch"
/>
</ElFormItem>
<ElFormItem label="岗位编码">
<ElInput
v-model="query.code"
clearable
placeholder="请输入岗位编码"
@keyup.enter="onSearch"
/>
</ElFormItem>
<ElFormItem label="状态">
<ElSelect
v-model="query.status"
clearable
placeholder="全部"
class="w-[120px]"
>
<ElOption label="启用" :value="true" />
<ElOption label="禁用" :value="false" />
</ElSelect>
</ElFormItem>
<ElFormItem>
<ElButton type="primary" :icon="Search" @click="onSearch">
查询
</ElButton>
<ElButton :icon="RefreshCw" @click="onReset">重置</ElButton>
</ElFormItem>
</ElForm>
<div class="ml-auto">
<ElButton type="primary" :icon="Plus" @click="onCreate">
新增岗位
</ElButton>
</div>
</div>
<div
class="bg-background flex min-h-0 flex-1 flex-col rounded-lg border border-border"
>
<ElTable
v-loading="loading"
:data="tableData"
height="100%"
row-key="id"
stripe
>
<ElTableColumn type="index" width="56" label="#" />
<ElTableColumn prop="name" label="岗位名称" min-width="150" />
<ElTableColumn prop="code" label="岗位编码" min-width="140" />
<ElTableColumn label="岗位类型" min-width="110">
<template #default="{ row }">
{{ getOptionLabel(postTypeOptions, row.post_type) }}
</template>
</ElTableColumn>
<ElTableColumn label="岗位级别" min-width="110">
<template #default="{ row }">
{{ getOptionLabel(postLevelOptions, row.post_level) }}
</template>
</ElTableColumn>
<ElTableColumn prop="dept_name" label="所属部门" min-width="140">
<template #default="{ row }">
{{ row.dept_name || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="状态" width="110">
<template #default="{ row }">
<ElSwitch
v-model="row.status"
inline-prompt
active-text=""
inactive-text=""
@change="(value) => onStatusChange(row, value as boolean)"
/>
</template>
</ElTableColumn>
<ElTableColumn label="用户数" width="90" align="center">
<template #default="{ row }">
<ElTag type="info" effect="plain">{{ row.user_count ?? 0 }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="sort" label="排序" width="90" align="center" />
<ElTableColumn
prop="sys_create_datetime"
label="创建时间"
min-width="170"
/>
<ElTableColumn label="操作" width="150" fixed="right">
<template #default="{ row }">
<ElButton link type="primary" :icon="Edit" @click="onEdit(row)">
编辑
</ElButton>
<ElButton link type="danger" :icon="Trash2" @click="onDelete(row)">
删除
</ElButton>
</template>
</ElTableColumn>
</ElTable>
<div class="flex justify-end border-t border-border px-4 py-3">
<ElPagination
v-model:current-page="query.page"
v-model:page-size="query.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
background
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadPosts"
@size-change="onSearch"
/>
</div>
</div>
</div>
<ElDialog v-model="dialogVisible" :title="dialogTitle" width="520px">
<ElForm ref="formRef" :model="form" :rules="rules" label-width="92px">
<ElFormItem label="岗位名称" prop="name">
<ElInput v-model="form.name" maxlength="64" placeholder="请输入岗位名称" />
</ElFormItem>
<ElFormItem label="岗位编码" prop="code">
<ElInput v-model="form.code" maxlength="32" placeholder="请输入岗位编码" />
</ElFormItem>
<ElFormItem label="岗位类型">
<ElSelect v-model="form.post_type" class="w-full">
<ElOption
v-for="item in postTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="岗位级别">
<ElSelect v-model="form.post_level" class="w-full">
<ElOption
v-for="item in postLevelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="排序">
<ElInputNumber v-model="form.sort" :min="0" class="w-full" />
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch
v-model="form.status"
inline-prompt
active-text="启用"
inactive-text="禁用"
/>
</ElFormItem>
<ElFormItem label="岗位描述">
<ElInput
v-model="form.description"
maxlength="200"
placeholder="请输入岗位描述"
show-word-limit
type="textarea"
:rows="3"
/>
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" :loading="saving" @click="onSave">
保存
</ElButton>
</template>
</ElDialog>
</Page>
</template>
<style scoped>
.post-search-form {
margin-bottom: -18px;
}
</style>