- 字典管理页面改为左右分栏 a-menu 布局,对齐系统配置页面风格 - 左侧菜单项显示字典名称、编码(hover展开)及数据数量徽标 - 修复部门人员操作列因 ProTable dataIndex='action' 拦截导致按钮不显示 - 字典类型编辑/删除移至右侧工具栏,操作列增加序号列 - 新增 LDAP 配置管理与同步功能(UnboundID LDAP SDK) - 清理废弃的 fantastic-admin 目录
322 lines
7.7 KiB
Vue
322 lines
7.7 KiB
Vue
<template>
|
|
<div class="page-container">
|
|
<ProTable
|
|
:columns="columns"
|
|
:request="fetchData"
|
|
:toolbar="{
|
|
title: $t('exampleTable.userList'),
|
|
subTitle: $t('exampleTable.subTitle'),
|
|
actions: ['refresh', 'columnSetting'],
|
|
}"
|
|
:search="{
|
|
labelWidth: 80,
|
|
defaultCollapsed: true,
|
|
}"
|
|
:header-filter="{
|
|
defaultMode: 'server',
|
|
requestPayload: 'flat',
|
|
}"
|
|
row-key="id"
|
|
>
|
|
<template #toolbar-actions>
|
|
<a-button type="primary" class="create-user-btn" @click="handleCreate">
|
|
<PlusOutlined /> {{ $t("exampleTable.createUser") }}
|
|
</a-button>
|
|
</template>
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
<template v-if="column.dataIndex === 'status'">
|
|
<a-switch
|
|
:checked="record.status === 'active'"
|
|
@change="handleStatusChange(record, $event as boolean)"
|
|
>
|
|
<template #checkedChildren>{{ $t("user.active") }}</template>
|
|
<template #unCheckedChildren>{{ $t("user.inactive") }}</template>
|
|
</a-switch>
|
|
</template>
|
|
<template v-else-if="column.dataIndex === 'gender'">
|
|
<a-tag :color="genderValueEnum[record.gender]?.color">
|
|
{{ genderValueEnum[record.gender]?.text || record.gender }}
|
|
</a-tag>
|
|
</template>
|
|
</template>
|
|
</ProTable>
|
|
|
|
<!-- Create/Edit Modal -->
|
|
<a-modal
|
|
v-model:open="modalVisible"
|
|
:title="
|
|
editingId ? $t('exampleTable.editUser') : $t('exampleTable.createUser')
|
|
"
|
|
width="600px"
|
|
@ok="handleSubmit"
|
|
>
|
|
<ProForm
|
|
ref="formRef"
|
|
:form-items="formItems"
|
|
:initial-values="formData"
|
|
:grid="{ cols: 2, gutter: 16 }"
|
|
/>
|
|
</a-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { ProTableColumn, ProFormItem } from "@/types/pro";
|
|
import type { User } from "@/types/auth";
|
|
import type { PageParams } from "@/types/api";
|
|
|
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from "@antdv-next/icons";
|
|
import { message } from "antdv-next";
|
|
import { ref, computed } from "vue";
|
|
|
|
import { getUserList, createUser, updateUser, deleteUser } from "@/api/user";
|
|
import ProForm from "@/components/Pro/ProForm/index.vue";
|
|
import ProTable from "@/components/Pro/ProTable/index.vue";
|
|
import { $t } from "@/locales";
|
|
import { commonRules } from "@/utils/formRules";
|
|
|
|
const modalVisible = ref(false);
|
|
const editingId = ref<string | null>(null);
|
|
const formRef = ref();
|
|
const formData = ref({});
|
|
|
|
const genderOptions = computed(() => [
|
|
{ label: $t("user.male"), value: "male" },
|
|
{ label: $t("user.female"), value: "female" },
|
|
]);
|
|
|
|
const genderValueEnum = computed<
|
|
Record<string, { text: string; color?: string }>
|
|
>(() => ({
|
|
male: { text: $t("user.male"), color: "blue" },
|
|
female: { text: $t("user.female"), color: "pink" },
|
|
}));
|
|
|
|
// Table columns configuration
|
|
const columns = computed<ProTableColumn[]>(() => [
|
|
{
|
|
title: $t("user.username"),
|
|
dataIndex: "username",
|
|
width: 150,
|
|
fixed: "left",
|
|
headerFilter: {
|
|
type: "keyword",
|
|
mode: "server",
|
|
icon: "search",
|
|
placeholder: `搜索${$t("user.username")}`,
|
|
matchAllKeywords: true,
|
|
},
|
|
},
|
|
{
|
|
title: $t("user.email"),
|
|
dataIndex: "email",
|
|
search: true,
|
|
searchType: "input",
|
|
copyable: true,
|
|
},
|
|
{
|
|
title: $t("user.realName"),
|
|
dataIndex: "realName",
|
|
search: true,
|
|
searchType: "input",
|
|
},
|
|
{
|
|
title: $t("user.phone"),
|
|
dataIndex: "phone",
|
|
},
|
|
{
|
|
title: $t("user.gender"),
|
|
dataIndex: "gender",
|
|
search: true,
|
|
searchType: "select",
|
|
searchOptions: genderOptions.value,
|
|
headerFilter: {
|
|
type: "select",
|
|
mode: "server",
|
|
icon: "filter",
|
|
multiple: true,
|
|
options: genderOptions.value,
|
|
},
|
|
valueType: "tag",
|
|
valueEnum: genderValueEnum.value,
|
|
},
|
|
{
|
|
title: $t("common.status"),
|
|
dataIndex: "status",
|
|
width: 150,
|
|
headerFilter: {
|
|
type: "select",
|
|
mode: "server",
|
|
icon: "filter",
|
|
multiple: false,
|
|
options: [
|
|
{ label: $t("user.active"), value: "active" },
|
|
{ label: $t("user.inactive"), value: "inactive" },
|
|
],
|
|
},
|
|
},
|
|
{
|
|
title: $t("common.createTime"),
|
|
dataIndex: "createdAt",
|
|
valueType: "dateTime",
|
|
search: true,
|
|
searchType: "dateRange",
|
|
sorter: true,
|
|
},
|
|
{
|
|
title: $t("common.actions"),
|
|
dataIndex: "action",
|
|
width: 200,
|
|
fixed: "right",
|
|
actions: [
|
|
{
|
|
label: $t("common.edit"),
|
|
icon: EditOutlined,
|
|
onClick: (record) => handleEdit(record as unknown as User),
|
|
},
|
|
{
|
|
label: $t("common.delete"),
|
|
icon: DeleteOutlined,
|
|
danger: true,
|
|
confirm: $t("user.confirmDelete"),
|
|
onClick: (record) => handleDelete(record as unknown as User),
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
// Form items configuration
|
|
const formItems = computed<ProFormItem[]>(() => [
|
|
{
|
|
name: "username",
|
|
label: $t("user.username"),
|
|
type: "input",
|
|
required: true,
|
|
rules: [
|
|
commonRules.required(),
|
|
commonRules.length(3, 20),
|
|
commonRules.username(),
|
|
],
|
|
},
|
|
{
|
|
name: "email",
|
|
label: $t("user.email"),
|
|
type: "input",
|
|
required: true,
|
|
rules: [commonRules.required(), commonRules.email()],
|
|
},
|
|
{
|
|
name: "realName",
|
|
label: $t("user.realName"),
|
|
type: "input",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "phone",
|
|
label: $t("user.phone"),
|
|
type: "input",
|
|
rules: [commonRules.phone()],
|
|
},
|
|
{
|
|
name: "gender",
|
|
label: $t("user.gender"),
|
|
type: "radio",
|
|
required: true,
|
|
options: genderOptions.value,
|
|
},
|
|
{
|
|
name: "status",
|
|
label: $t("common.status"),
|
|
type: "radio",
|
|
required: true,
|
|
initialValue: "active",
|
|
options: [
|
|
{ label: $t("user.active"), value: "active" },
|
|
{ label: $t("user.inactive"), value: "inactive" },
|
|
],
|
|
},
|
|
{
|
|
name: "bio",
|
|
label: $t("user.bio"),
|
|
type: "textarea",
|
|
colSpan: 2,
|
|
props: {
|
|
rows: 4,
|
|
maxLength: 200,
|
|
showCount: true,
|
|
},
|
|
},
|
|
]);
|
|
|
|
// Methods
|
|
const fetchData = async (params: PageParams) => {
|
|
const res = await getUserList(params);
|
|
return {
|
|
data: res.data.list,
|
|
total: res.data.total,
|
|
success: true,
|
|
};
|
|
};
|
|
|
|
const handleCreate = () => {
|
|
editingId.value = null;
|
|
formData.value = {};
|
|
modalVisible.value = true;
|
|
};
|
|
|
|
const handleEdit = (record: User) => {
|
|
editingId.value = record.id;
|
|
formData.value = { ...record };
|
|
modalVisible.value = true;
|
|
};
|
|
|
|
const handleDelete = async (record: User) => {
|
|
await deleteUser(record.id);
|
|
message.success($t("exampleTable.deleteSuccess"));
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
const valid = await formRef.value?.validate();
|
|
if (!valid) return;
|
|
|
|
const values = formRef.value?.getFieldsValue();
|
|
|
|
try {
|
|
if (editingId.value) {
|
|
await updateUser({ ...values, id: editingId.value });
|
|
message.success($t("exampleTable.updateSuccess"));
|
|
} else {
|
|
await createUser(values);
|
|
message.success($t("exampleTable.createSuccess"));
|
|
}
|
|
modalVisible.value = false;
|
|
} catch (error: unknown) {
|
|
message.error((error as Error).message || $t("exampleTable.createSuccess"));
|
|
}
|
|
};
|
|
|
|
const handleStatusChange = async (record: User, checked: boolean) => {
|
|
try {
|
|
const newStatus = checked ? "active" : "inactive";
|
|
await updateUser({ ...record, id: record.id, status: newStatus });
|
|
record.status = newStatus;
|
|
message.success($t("exampleTable.updateSuccess"));
|
|
} catch (error: unknown) {
|
|
message.error((error as Error).message || $t("common.error"));
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.create-user-btn {
|
|
box-shadow: 0 4px 14px rgba(24, 119, 255, 0.3);
|
|
border: none;
|
|
|
|
&:hover {
|
|
box-shadow: 0 8px 18px rgba(24, 119, 255, 0.36);
|
|
transform: translateY(-1px);
|
|
}
|
|
}
|
|
</style>
|