Restore full post management page
This commit is contained in:
@@ -32,6 +32,7 @@ const modules = import.meta.glob([
|
|||||||
'./langs/*/message.json',
|
'./langs/*/message.json',
|
||||||
'./langs/*/page.json',
|
'./langs/*/page.json',
|
||||||
'./langs/*/permission.json',
|
'./langs/*/permission.json',
|
||||||
|
'./langs/*/post.json',
|
||||||
'./langs/*/role.json',
|
'./langs/*/role.json',
|
||||||
'./langs/*/system-config.json',
|
'./langs/*/system-config.json',
|
||||||
'./langs/*/system.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>
|
<script lang="ts" setup>
|
||||||
import type { FormInstance, FormRules } from 'element-plus';
|
import { ref } from 'vue';
|
||||||
import type { Post, PostCreateInput } from '#/api/core/post';
|
|
||||||
|
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
import { Edit, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
import {
|
import { ElButton, ElMessage, ElMessageBox } from 'element-plus';
|
||||||
ElButton,
|
|
||||||
ElDialog,
|
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElInput,
|
|
||||||
ElInputNumber,
|
|
||||||
ElMessage,
|
|
||||||
ElMessageBox,
|
|
||||||
ElOption,
|
|
||||||
ElPagination,
|
|
||||||
ElSelect,
|
|
||||||
ElSwitch,
|
|
||||||
ElTable,
|
|
||||||
ElTableColumn,
|
|
||||||
ElTag,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import {
|
import { addPostUsersApi, removePostUsersApi } from '#/api/core/post';
|
||||||
createPostApi,
|
import { UserListPanel } from '#/components/user-list-panel';
|
||||||
deletePostApi,
|
import { UserSelector } from '#/components/zq-form/user-selector';
|
||||||
getPostListApi,
|
|
||||||
updatePostApi,
|
import PostList from './modules/post-list.vue';
|
||||||
} from '#/api/core/post';
|
|
||||||
|
|
||||||
defineOptions({ name: 'SystemPost' });
|
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 },
|
function onPostSelect(postId: string | undefined) {
|
||||||
{ label: '职能岗', value: 3 },
|
currentPostId.value = postId;
|
||||||
{ label: '其他', value: 4 },
|
tempSelectedUsers.value.clear();
|
||||||
];
|
|
||||||
|
|
||||||
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: '',
|
function handleUserSelect(userId: string, _user: any) {
|
||||||
description: '',
|
if (tempSelectedUsers.value.has(userId)) {
|
||||||
name: '',
|
tempSelectedUsers.value.delete(userId);
|
||||||
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 {
|
} else {
|
||||||
await createPostApi(payload);
|
tempSelectedUsers.value.add(userId);
|
||||||
ElMessage.success('岗位已新增');
|
|
||||||
}
|
|
||||||
dialogVisible.value = false;
|
|
||||||
await loadPosts();
|
|
||||||
} finally {
|
|
||||||
saving.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDelete(row: Post) {
|
/**
|
||||||
await ElMessageBox.confirm(
|
* 处理移除用户
|
||||||
`确定删除岗位「${row.name}」吗?`,
|
*/
|
||||||
'删除岗位',
|
function handleRemoveUser(userId: string) {
|
||||||
{
|
tempSelectedUsers.value.delete(userId);
|
||||||
cancelButtonText: '取消',
|
|
||||||
confirmButtonText: '删除',
|
|
||||||
type: 'warning',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await deletePostApi(row.id);
|
|
||||||
ElMessage.success('岗位已删除');
|
|
||||||
await loadPosts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
await updatePostApi(row.id, { status });
|
await ElMessageBox.confirm(confirmMessage, $t('common.delete') || '删除', {
|
||||||
ElMessage.success(status ? '岗位已启用' : '岗位已禁用');
|
confirmButtonText: $t('common.confirm') || '确定',
|
||||||
} catch (error) {
|
cancelButtonText: $t('common.cancel') || '取消',
|
||||||
row.status = !status;
|
type: 'warning',
|
||||||
throw error;
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadPosts);
|
await removePostUsersApi(currentPostId.value, {
|
||||||
|
user_ids: userIds,
|
||||||
|
});
|
||||||
|
ElMessage.success($t('post.removeUsersSuccess') || '删除成功');
|
||||||
|
tempSelectedUsers.value.clear();
|
||||||
|
// 刷新用户列表
|
||||||
|
userListPanelRef.value?.reload();
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('Failed to remove users:', error);
|
||||||
|
ElMessage.error($t('post.removeUsersFailed') || '删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<div class="flex h-full min-h-0 flex-col gap-3">
|
<div class="flex h-full">
|
||||||
<div
|
<!-- 岗位列表 -->
|
||||||
class="bg-background flex flex-wrap items-end gap-3 rounded-lg border border-border px-4 py-3"
|
<div class="mr-3 w-1/6">
|
||||||
>
|
<PostList @select="onPostSelect" />
|
||||||
<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>
|
||||||
|
|
||||||
<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
|
<template #title>
|
||||||
v-loading="loading"
|
<div class="flex items-center gap-2">
|
||||||
:data="tableData"
|
<UserSelector
|
||||||
height="100%"
|
:multiple="true"
|
||||||
row-key="id"
|
:disabled="!currentPostId"
|
||||||
stripe
|
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="#" />
|
{{ $t('common.delete') || '删除' }}
|
||||||
<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>
|
</ElButton>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</UserListPanel>
|
||||||
</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>
|
</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>
|
</Page>
|
||||||
</template>
|
</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