Restore full post management page
This commit is contained in:
@@ -32,6 +32,7 @@ const modules = import.meta.glob([
|
||||
'./langs/*/message.json',
|
||||
'./langs/*/page.json',
|
||||
'./langs/*/permission.json',
|
||||
'./langs/*/post.json',
|
||||
'./langs/*/role.json',
|
||||
'./langs/*/system-config.json',
|
||||
'./langs/*/system.json',
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn } from '#/adapter/vxe-table';
|
||||
import type { Post } from '#/api/core/post';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
|
||||
/**
|
||||
* 获取搜索表单的字段配置
|
||||
*/
|
||||
export function useSearchFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: $t('system.user.userName'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'username',
|
||||
label: $t('system.user.account'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取岗位类型选项
|
||||
*/
|
||||
export function getPostTypeOptions() {
|
||||
return [
|
||||
{ label: $t('post.types.management'), value: 0 },
|
||||
{ label: $t('post.types.technical'), value: 1 },
|
||||
{ label: $t('post.types.business'), value: 2 },
|
||||
{ label: $t('post.types.functional'), value: 3 },
|
||||
{ label: $t('post.types.other'), value: 4 },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取岗位级别选项
|
||||
*/
|
||||
export function getPostLevelOptions() {
|
||||
return [
|
||||
{ label: $t('post.levels.senior'), value: 0 },
|
||||
{ label: $t('post.levels.middle'), value: 1 },
|
||||
{ label: $t('post.levels.basic'), value: 2 },
|
||||
{ label: $t('post.levels.staff'), value: 3 },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取岗位树列配置
|
||||
*/
|
||||
export function usePostTreeColumns(
|
||||
onActionClick?: OnActionClickFn<Post>,
|
||||
): VxeTableGridOptions<Post>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: $t('post.postName'),
|
||||
minWidth: 150,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: $t('post.postName'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(2, $t('ui.formRules.minLength', [$t('post.postName'), 2]))
|
||||
.max(64, $t('ui.formRules.maxLength', [$t('post.postName'), 64])),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'code',
|
||||
label: $t('post.postCode'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(2, $t('ui.formRules.minLength', [$t('post.postCode'), 2]))
|
||||
.max(32, $t('ui.formRules.maxLength', [$t('post.postCode'), 32]))
|
||||
.regex(/^[\w-]+$/, $t('post.codeFormatError')),
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getPostTypeOptions(),
|
||||
},
|
||||
defaultValue: 4,
|
||||
fieldName: 'post_type',
|
||||
label: $t('post.postType'),
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getPostLevelOptions(),
|
||||
},
|
||||
defaultValue: 3,
|
||||
fieldName: 'post_level',
|
||||
label: $t('post.postLevel'),
|
||||
},
|
||||
{
|
||||
component: 'DeptSelector',
|
||||
componentProps: {
|
||||
placeholder: $t('post.selectDepartment'),
|
||||
},
|
||||
fieldName: 'dept_id',
|
||||
label: $t('post.department'),
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
placeholder: $t('post.descriptionPlaceholder'),
|
||||
rows: 3,
|
||||
},
|
||||
fieldName: 'description',
|
||||
label: $t('post.description'),
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: $t('common.enabled'), value: true },
|
||||
{ label: $t('common.disabled'), value: false },
|
||||
],
|
||||
},
|
||||
defaultValue: true,
|
||||
fieldName: 'status',
|
||||
label: $t('post.status'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户表格列配置
|
||||
*/
|
||||
export function useUserColumns(
|
||||
onActionClick?: OnActionClickFn<Post>,
|
||||
): VxeTableGridOptions<Post>['columns'] {
|
||||
return [
|
||||
{
|
||||
type: 'checkbox',
|
||||
minWidth: 60,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'username',
|
||||
title: $t('system.user.account'),
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: $t('system.user.userName'),
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
title: $t('system.user.email'),
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: $t('system.user.userName'),
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: ['edit', 'delete'],
|
||||
},
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
headerAlign: 'center',
|
||||
showOverflow: false,
|
||||
title: $t('system.user.operation'),
|
||||
minWidth: 150,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,406 +1,160 @@
|
||||
<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 { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Edit, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElPagination,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
import { ElButton, ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
import {
|
||||
createPostApi,
|
||||
deletePostApi,
|
||||
getPostListApi,
|
||||
updatePostApi,
|
||||
} from '#/api/core/post';
|
||||
import { addPostUsersApi, removePostUsersApi } from '#/api/core/post';
|
||||
import { UserListPanel } from '#/components/user-list-panel';
|
||||
import { UserSelector } from '#/components/zq-form/user-selector';
|
||||
|
||||
import PostList from './modules/post-list.vue';
|
||||
|
||||
defineOptions({ name: 'SystemPost' });
|
||||
|
||||
type NullableStatus = '' | boolean;
|
||||
const currentPostId = ref<string>();
|
||||
const tempSelectedUsers = ref<Set<string>>(new Set());
|
||||
const userListPanelRef = ref<InstanceType<typeof UserListPanel>>();
|
||||
|
||||
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 onPostSelect(postId: string | undefined) {
|
||||
currentPostId.value = postId;
|
||||
tempSelectedUsers.value.clear();
|
||||
}
|
||||
|
||||
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('岗位已更新');
|
||||
/**
|
||||
* 处理用户选择
|
||||
*/
|
||||
function handleUserSelect(userId: string, _user: any) {
|
||||
if (tempSelectedUsers.value.has(userId)) {
|
||||
tempSelectedUsers.value.delete(userId);
|
||||
} else {
|
||||
await createPostApi(payload);
|
||||
ElMessage.success('岗位已新增');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
await loadPosts();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
tempSelectedUsers.value.add(userId);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDelete(row: Post) {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除岗位「${row.name}」吗?`,
|
||||
'删除岗位',
|
||||
{
|
||||
cancelButtonText: '取消',
|
||||
confirmButtonText: '删除',
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
await deletePostApi(row.id);
|
||||
ElMessage.success('岗位已删除');
|
||||
await loadPosts();
|
||||
/**
|
||||
* 处理移除用户
|
||||
*/
|
||||
function handleRemoveUser(userId: string) {
|
||||
tempSelectedUsers.value.delete(userId);
|
||||
}
|
||||
|
||||
async function onStatusChange(row: Post, status: boolean) {
|
||||
/**
|
||||
* 新增用户到岗位(作为 UserSelector 的 onConfirm 回调)
|
||||
*/
|
||||
async function handleAddUsers(userIds: string | string[]) {
|
||||
if (!currentPostId.value) {
|
||||
ElMessage.warning($t('post.selectPostFirst') || '请先选择岗位');
|
||||
throw new Error('请先选择岗位');
|
||||
}
|
||||
|
||||
const userIdsArray = Array.isArray(userIds) ? userIds : [userIds];
|
||||
|
||||
if (userIdsArray.length === 0) {
|
||||
ElMessage.warning($t('post.selectUsersFirst') || '请先选择用户');
|
||||
throw new Error('请先选择用户');
|
||||
}
|
||||
|
||||
await addPostUsersApi(currentPostId.value, {
|
||||
user_ids: userIdsArray,
|
||||
});
|
||||
|
||||
ElMessage.success($t('post.addUsersSuccess') || '添加成功');
|
||||
// 刷新用户列表
|
||||
userListPanelRef.value?.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从岗位删除用户
|
||||
*/
|
||||
async function handleRemoveUsers() {
|
||||
if (!currentPostId.value) {
|
||||
ElMessage.warning($t('post.selectPostFirst') || '请先选择岗位');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tempSelectedUsers.value.size === 0) {
|
||||
ElMessage.warning($t('post.selectUsersFirst') || '请先选择用户');
|
||||
return;
|
||||
}
|
||||
|
||||
const userIds = [...tempSelectedUsers.value];
|
||||
const confirmMessage =
|
||||
$t('post.removeUsersConfirm', [tempSelectedUsers.value.size]) ||
|
||||
`确定要删除选中的 ${tempSelectedUsers.value.size} 个用户吗?`;
|
||||
|
||||
try {
|
||||
await updatePostApi(row.id, { status });
|
||||
ElMessage.success(status ? '岗位已启用' : '岗位已禁用');
|
||||
await ElMessageBox.confirm(confirmMessage, $t('common.delete') || '删除', {
|
||||
confirmButtonText: $t('common.confirm') || '确定',
|
||||
cancelButtonText: $t('common.cancel') || '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
await removePostUsersApi(currentPostId.value, {
|
||||
user_ids: userIds,
|
||||
});
|
||||
ElMessage.success($t('post.removeUsersSuccess') || '删除成功');
|
||||
tempSelectedUsers.value.clear();
|
||||
// 刷新用户列表
|
||||
userListPanelRef.value?.reload();
|
||||
} catch (error) {
|
||||
row.status = !status;
|
||||
throw error;
|
||||
if (error !== 'cancel') {
|
||||
console.error('Failed to remove users:', error);
|
||||
ElMessage.error($t('post.removeUsersFailed') || '删除失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 class="flex h-full">
|
||||
<!-- 岗位列表 -->
|
||||
<div class="mr-3 w-1/6">
|
||||
<PostList @select="onPostSelect" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-background flex min-h-0 flex-1 flex-col rounded-lg border border-border"
|
||||
<!-- 主内容区:用户列表 -->
|
||||
<div class="w-5/6">
|
||||
<UserListPanel
|
||||
ref="userListPanelRef"
|
||||
:data-source="currentPostId ? 'post' : 'all'"
|
||||
:source-id="currentPostId"
|
||||
:temp-selected-users="tempSelectedUsers"
|
||||
:filterable="true"
|
||||
:multiple="true"
|
||||
:selectable="true"
|
||||
:show-selected-tags="false"
|
||||
:show-border="false"
|
||||
@user-select="handleUserSelect"
|
||||
@remove-user="handleRemoveUser"
|
||||
>
|
||||
<ElTable
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
height="100%"
|
||||
row-key="id"
|
||||
stripe
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2">
|
||||
<UserSelector
|
||||
:multiple="true"
|
||||
:disabled="!currentPostId"
|
||||
display-mode="button"
|
||||
:placeholder="$t('common.add') || '新增'"
|
||||
:on-confirm="handleAddUsers"
|
||||
/>
|
||||
<ElButton
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="!currentPostId || tempSelectedUsers.size === 0"
|
||||
@click="handleRemoveUsers"
|
||||
>
|
||||
<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)">
|
||||
删除
|
||||
{{ $t('common.delete') || '删除' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</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"
|
||||
/>
|
||||
</UserListPanel>
|
||||
</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>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Post } from '#/api/core/post';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElButton } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createPostApi, updatePostApi } from '#/api/core/post';
|
||||
import { ZqDialog } from '#/components/zq-dialog';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<Post>();
|
||||
const visible = ref(false);
|
||||
const confirmLoading = ref(false);
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', [$t('post.name')])
|
||||
: $t('ui.actionTitle.create', [$t('post.name')]);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
layout: 'vertical',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
formApi.resetForm();
|
||||
formApi.setValues(formData.value || {});
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (valid) {
|
||||
confirmLoading.value = true;
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updatePostApi(formData.value.id, data)
|
||||
: createPostApi(data));
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function open(data?: Post) {
|
||||
visible.value = true;
|
||||
if (data) {
|
||||
formData.value = data;
|
||||
formApi.setValues(formData.value);
|
||||
} else {
|
||||
formData.value = undefined;
|
||||
formApi.resetForm();
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ZqDialog
|
||||
v-model="visible"
|
||||
:title="getTitle"
|
||||
:confirm-loading="confirmLoading"
|
||||
@confirm="onSubmit"
|
||||
>
|
||||
<Form class="mx-4" />
|
||||
<template #footer-left>
|
||||
<ElButton type="primary" @click="resetForm">
|
||||
{{ $t('common.reset') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ZqDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,291 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Post } from '#/api/core/post';
|
||||
import type { CardListOptions } from '#/components/card-list';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElPopover,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import { deletePostApi, getPostListApi } from '#/api/core/post';
|
||||
import { CardList } from '#/components/card-list';
|
||||
|
||||
import PostFormModal from './post-form-modal.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [postId: string | undefined];
|
||||
}>();
|
||||
|
||||
const postList = ref<Post[]>([]);
|
||||
const loading = ref(false);
|
||||
const selectedPostId = ref<string>();
|
||||
const searchKeyword = ref<string>('');
|
||||
const hoveredPostId = ref<string>();
|
||||
const postFormModalRef = ref<InstanceType<typeof PostFormModal>>();
|
||||
|
||||
// 卡片列表配置
|
||||
const cardListOptions: CardListOptions<Post> = {
|
||||
searchFields: [{ field: 'name' }, { field: 'code' }],
|
||||
titleField: 'name',
|
||||
};
|
||||
|
||||
async function fetchPostList() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await getPostListApi({ page: 1, pageSize: 100 });
|
||||
postList.value = response.items || [];
|
||||
|
||||
// 自动选中第一个岗位
|
||||
if (postList.value.length > 0 && !selectedPostId.value) {
|
||||
const firstPost = postList.value.at(0);
|
||||
if (firstPost) {
|
||||
selectedPostId.value = firstPost.id;
|
||||
emit('select', firstPost.id);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理岗位选择
|
||||
*/
|
||||
function onPostSelect(postId: string | undefined) {
|
||||
selectedPostId.value = postId;
|
||||
emit('select', postId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开添加岗位对话框
|
||||
*/
|
||||
function onAddPost() {
|
||||
postFormModalRef.value?.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开编辑岗位对话框
|
||||
*/
|
||||
function onEditPost(post: Post, e?: Event) {
|
||||
e?.stopPropagation();
|
||||
postFormModalRef.value?.open(post);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除岗位
|
||||
*/
|
||||
async function onDeletePost(post: Post, e?: Event) {
|
||||
e?.stopPropagation();
|
||||
|
||||
ElMessageBox.confirm(
|
||||
$t('ui.actionMessage.deleteConfirm', [post.name]),
|
||||
$t('common.delete'),
|
||||
{
|
||||
confirmButtonText: $t('common.confirm'),
|
||||
cancelButtonText: $t('common.cancel'),
|
||||
type: 'warning',
|
||||
showClose: false,
|
||||
},
|
||||
)
|
||||
.then(async () => {
|
||||
try {
|
||||
await deletePostApi(post.id);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [post.name]));
|
||||
|
||||
// 如果删除的是当前选中的岗位,清除选中状态
|
||||
if (selectedPostId.value === post.id) {
|
||||
selectedPostId.value = undefined;
|
||||
emit('select', undefined);
|
||||
}
|
||||
|
||||
await fetchPostList();
|
||||
} catch {
|
||||
ElMessage.error($t('ui.actionMessage.deleteError'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消了操作
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加岗位成功后的回调
|
||||
*/
|
||||
async function onPostFormSuccess() {
|
||||
ElMessage.success($t('ui.actionMessage.createSuccess', [$t('post.name')]));
|
||||
await fetchPostList();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchPostList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardList
|
||||
:items="postList"
|
||||
:loading="loading"
|
||||
:selected-id="selectedPostId"
|
||||
:hovered-id="hoveredPostId"
|
||||
:search-keyword="searchKeyword"
|
||||
:options="cardListOptions"
|
||||
@select="onPostSelect"
|
||||
@update:search-keyword="(v) => (searchKeyword = v)"
|
||||
@update:hovered-id="(v) => (hoveredPostId = v)"
|
||||
@add="onAddPost"
|
||||
@edit="onEditPost"
|
||||
@delete="onDeletePost"
|
||||
>
|
||||
<!-- 自定义项目渲染 -->
|
||||
<template #item="{ item }">
|
||||
<div class="truncate text-sm" :title="item.name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 详细信息 -->
|
||||
<template #details="{ item }">
|
||||
<div class="flex items-center gap-2 text-xs opacity-70">
|
||||
<!-- 岗位编码 -->
|
||||
<span class="truncate" :title="item.code">
|
||||
{{ item.code }}
|
||||
</span>
|
||||
|
||||
<!-- 分隔符 -->
|
||||
<span class="text-gray-400">|</span>
|
||||
|
||||
<!-- 岗位类型 -->
|
||||
<span v-if="item.post_type_display" class="flex-shrink-0">
|
||||
{{ item.post_type_display }}
|
||||
</span>
|
||||
|
||||
<!-- 部门 -->
|
||||
<span
|
||||
v-if="item.dept_name"
|
||||
class="flex-1 truncate"
|
||||
:title="item.dept_name"
|
||||
>
|
||||
{{ item.dept_name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<template #actions="{ item }">
|
||||
<div class="flex flex-shrink-0" @click.stop>
|
||||
<!-- 编辑按钮 -->
|
||||
<ElTooltip :content="$t('post.edit')" placement="top">
|
||||
<ElButton
|
||||
type="primary"
|
||||
text
|
||||
size="small"
|
||||
circle
|
||||
@click="onEditPost(item, $event)"
|
||||
>
|
||||
<IconifyIcon icon="ep:edit" class="size-4" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<ElButton
|
||||
type="danger"
|
||||
text
|
||||
size="small"
|
||||
circle
|
||||
style="margin-left: 0"
|
||||
:title="$t('common.delete')"
|
||||
@click="onDeletePost(item, $event)"
|
||||
>
|
||||
<IconifyIcon icon="ep:delete" class="size-4" />
|
||||
</ElButton>
|
||||
<!-- 详情按钮 -->
|
||||
<ElPopover placement="right" :width="300">
|
||||
<template #reference>
|
||||
<ElButton
|
||||
type="info"
|
||||
text
|
||||
size="small"
|
||||
style="margin-left: 0"
|
||||
circle
|
||||
>
|
||||
<IconifyIcon icon="ep:info-filled" class="size-4" />
|
||||
</ElButton>
|
||||
</template>
|
||||
<!-- Popover 内容:详细信息 -->
|
||||
<div class="space-y-2 p-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">岗位名称:</span>
|
||||
<span class="font-medium">{{ item.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">岗位编码:</span>
|
||||
<span class="font-medium">{{ item.code || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">岗位类型:</span>
|
||||
<span class="font-medium">{{
|
||||
item.post_type_display || '-'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">岗位级别:</span>
|
||||
<span class="font-medium">{{
|
||||
item.post_level_display || '-'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">所属部门:</span>
|
||||
<span class="font-medium">{{ item.dept_name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">状态:</span>
|
||||
<span class="font-medium">{{
|
||||
item.status ? '启用' : '禁用'
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.description"
|
||||
class="border-t border-gray-200 pt-2 dark:border-gray-700"
|
||||
>
|
||||
<span class="text-gray-600 dark:text-gray-400">描述:</span>
|
||||
<div
|
||||
class="mt-1 max-h-32 overflow-y-auto break-words rounded bg-gray-100 p-2 text-xs dark:bg-gray-800"
|
||||
>
|
||||
{{ item.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElPopover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Modal 组件 -->
|
||||
<template #modal>
|
||||
<PostFormModal ref="postFormModalRef" @success="onPostFormSuccess" />
|
||||
</template>
|
||||
</CardList>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 输入框前置图标样式 */
|
||||
:deep(.el-input__icon) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 文本按钮样式 */
|
||||
:deep(.el-button--text) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
:deep(.el-popover__reference) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user