refactor(@vben/common-ui): 新增用户管理功能

This commit is contained in:
Kven 2025-05-08 19:00:31 +08:00
parent 62f1343c3b
commit 1464345c4a
17 changed files with 633 additions and 74 deletions

View File

@ -15,13 +15,6 @@ export namespace ChatflowApi {
files: [];
}
export interface MessagesRequest {
conversationId: string;
userId: string;
firstId: string;
limit: number;
}
export interface ChatListBody {
userId: string;
lastId: string;
@ -64,12 +57,9 @@ export async function getChatList(
});
}
export async function getMessages(
appId: string,
data: ChatflowApi.MessagesRequest,
) {
return requestClient.post(`/v1/chat/messages/${appId}`, data);
}
// export function stopChatflow(data){
// return requestClient.post('/v1/chat/stopMessagesStream', data);
// };
//
// export function getChatflowList(data){
// return requestClient.post('/v1/chat/messages', data);

View File

@ -26,6 +26,72 @@ export namespace UserApi {
user: string | UserInfo;
csrf: CsrfTokenResult;
}
export interface Res {
code: number;
data: object;
msg: string;
}
// 重置密码数据
export interface PasswordReSetModel {
oldPassword: string;
password: string;
confirmPassword: string;
}
export interface RoleCreateRecord {
name: string;
dataScope: string;
permissionIds: (number | undefined)[];
remark: string;
authorities: (number | undefined)[];
}
export interface RoleRecord extends RoleCreateRecord {
id: string;
}
// 添加用户数据
export interface CreateRecord {
value: any;
code: any;
username: string;
nickName: string;
password: string;
phone: string;
email: string;
enabled: string;
address: string;
roleIds: RoleRecord | string[] | undefined;
permissionIds: (number | undefined)[];
authorities: string[];
}
// 用户数据
export interface UserRecord extends CreateRecord {
value: any;
id?: string;
avatar?: string;
createAt?: string;
}
export interface UserState {
username?: string;
nickName?: string;
avatar?: string;
email?: string;
phone?: string;
address?: string;
createAt?: string;
remark?: string;
id?: number;
role?: RoleRecord;
roles?: RoleRecord[];
// permissions?: string[] | '' | '*' | 'admin' | 'user' | 'auditor';
permissions?: string[] | undefined;
authorities?: string[];
}
}
/**
@ -34,3 +100,70 @@ export namespace UserApi {
export async function getUserInfoApi() {
return requestClient.get<UserApi.UserResult>('/rest/user/me');
}
// 更新密码
export function resetPassword(data: UserApi.PasswordReSetModel) {
return requestClient.patch('/rest/user/self/update-password', data);
}
// 注册用户
export function register(data: UserApi.CreateRecord) {
return requestClient.post('/rest/user/register', data);
}
// 新建用户
export function create(data: UserApi.CreateRecord) {
return requestClient.post('/rest/user', data);
}
// 用户切换角色
export function switchRole(id: number) {
return requestClient.patch(`/rest/user/switch/${id}`);
}
// 模糊查询用户列表
export function queryUserList(params: any) {
return requestClient.get('/rest/user', { params });
}
// 根据id查询用户信息
export function userDetail(id: string) {
return requestClient.get(`/rest/user/${id}`);
}
// 是否启用
export function enabled(id: string) {
return requestClient.patch(`/rest/user/${id}/toggle`);
}
// 删除用户
export function remove(id: string) {
return requestClient.delete(`/rest/user/${id}`);
}
// 更新用户信息
export function update(data: UserApi.UserRecord) {
return requestClient.patch<UserApi.Res>(`/rest/user/${data.id}`, data);
}
export function selfUpdate(data: UserApi.UserState) {
return requestClient.patch<UserApi.Res>(`/rest/user/self`, data);
}
// 获取个人用户信息
export function getUserInfo() {
return requestClient.get<UserApi.UserState>('/rest/user/self');
}
// 部门的审核员
export function deptAudit(id: string, roleId: string) {
return requestClient.get(`/rest/user/dept/${id}?roleId=${roleId}`);
}
export function getUserDetail(id: number) {
return requestClient.get<UserApi.UserState>(`/user/${id}`);
}
export function getAllDeptTree(id?: number | string) {
return requestClient.get('/rest/dept/tree', { params: { id } });
}

View File

@ -0,0 +1,245 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
const props = defineProps({
prem: {
type: Object,
default: () => ({}),
},
isCreate: Boolean,
});
// const emit = defineEmits(['refresh']);
const modalTitle = computed(() => {
return props.isCreate ? '新增用户' : '编辑用户';
});
// const checkKeys = ref<number[]>([]);
const CreateRef = ref();
const open = ref<boolean>(false);
const formData = ref({
value: undefined,
code: undefined,
username: '',
nickName: '',
password: '',
phone: '',
email: '',
enabled: '',
address: '',
deptId: undefined,
roleIds: [],
permissionIds: [],
authorities: [],
});
// const formDifer = ref{};
//
const deptOptions = ref();
// const getDeptData = async () => {
// const res = await getAllDeptTree(0);
// deptOptions.value = res.data;
// };
//
const roleOptions = ref();
// const getRoleData = async () => {
// const res = await queryRoleList('');
// roleOptions.value = res.data.records.filter((item: any) => {
// return item.enabled !== false;
// });
// };
//
const handleClick = () => {
// getDeptData();
// getRoleData();
// const userId = props.prem?.id;
//
// if (!props.isCreate && userId) {
// formData.value = props.prem;
// formDifer = { ...props.prem };
// }
open.value = true;
};
//
// const diffDataForm = (newData: any, oldData: any) => {
// const result = {}; //
// Object.keys(oldData).forEach((key) => {
// if (oldData[key] !== newData[key]) {
// result[key] = newData[key];
// }
// });
// return result;
// };
//
// const handleSubmit = async () => {
// const valid = await CreateRef.value?.validate();
// if (!valid) {
// formData.value.permissionIds = checkKeys.value;
// //
// if (props.isCreate) {
// // formData.value.username = formData.value.email;
// const res = await userStore.createUser(formData.value);
// if (res.status === 200) {
// Message.success({
// content: '',
// duration: 5 * 1000,
// });
// }
// CreateRef.value?.resetFields();
// } else {
// //
// // formDifer = diffDataForm(formData.value, formDifer);
// // if (Object.keys(formDifer).length === 0) {
// // Message.success({
// // content: '',
// // duration: 3 * 1000,
// // });
// // } else {
// // formDifer.id = formData.value.id;
// // const res = await userStore.updateUser(formData.value);
// // if (res.status === 200) {
// // Message.success({
// // content: '',
// // duration: 5 * 1000,
// // });
// // }
// // }
// const res = await userStore.updateUser(formData.value);
// if (res.status === 200) {
// Message.success({
// content: '',
// duration: 5 * 1000,
// });
// }
// }
// checkKeys.value = [];
// emit('refresh');
// setVisible(false);
// }
// };
//
const handleCancel = async () => {
// checkKeys.value = [];
open.value = false;
};
</script>
<template>
<a-button
v-if="props.isCreate"
type="primary"
:style="{ marginBottom: '10px', marginLeft: '10px' }"
@click="handleClick"
>
新建
</a-button>
<a-button v-if="!props.isCreate" @click="handleClick"> 编辑 </a-button>
<a-modal width="700px" v-model:open="open" @cancel="handleCancel">
<template #title>{{ modalTitle }}</template>
<a-form ref="CreateRef" :model="formData" :style="{ width: '650px' }">
<a-form-item
field="username"
label="用户名"
:validate-trigger="['change', 'input']"
:rules="[
{ required: true, message: '用户名不能为空' },
{
match: /^[a-zA-Z0-9\u4e00-\u9fa5]{1,20}$/,
message: '请输入正确格式的用户名',
},
]"
>
<a-input
v-if="props.isCreate"
v-model="formData.username"
placeholder="请输入用户名"
/>
<div v-else>{{ formData.username }}</div>
</a-form-item>
<a-form-item
field="email"
label="Email"
:rules="[
{
required: true,
type: 'email',
message: 'Email不能为空',
},
]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="formData.email" placeholder="请输入Email" />
</a-form-item>
<a-form-item
field="phone"
label="电话"
:rules="[
{ required: true, message: '电话不能为空' },
{ match: /^1[3-9]\d{9}$/, message: '请输入正确格式的电话' },
]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="formData.phone" placeholder="请输入电话" />
</a-form-item>
<a-form-item
field="password"
label="密码"
v-if="isCreate"
:validate-trigger="['change', 'input']"
:rules="[{ required: true, message: '密码不能为空' }]"
>
<a-input v-model="formData.password" placeholder="请输入密码" />
</a-form-item>
<a-form-item field="nickName" label="昵称">
<a-input v-model="formData.nickName" placeholder="请输入昵称" />
</a-form-item>
<a-form-item field="address" label="地址">
<a-input v-model="formData.address" placeholder="请输入地址" />
</a-form-item>
<a-form-item
field="deptId"
label="部门"
:rules="[{ required: true, message: '部门不能为空' }]"
:validate-trigger="['change']"
>
<a-tree-select
v-model="formData.deptId"
:field-names="{
key: 'id',
title: 'name',
children: 'children',
}"
:data="deptOptions"
allow-clear
placeholder="请选择所属部门"
/>
</a-form-item>
<a-form-item
field="roleIds"
label="角色"
:rules="[{ required: true, message: '角色不能为空' }]"
:validate-trigger="['change']"
>
<a-select
v-model="formData.roleIds"
:options="roleOptions"
:field-names="{
value: 'id',
label: 'name',
}"
placeholder="请选择角色"
multiple
/>
</a-form-item>
</a-form>
</a-modal>
</template>

View File

@ -0,0 +1,200 @@
<script setup lang="ts">
import type { VxeGridListeners, VxeGridProps } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { queryUserList } from '#/api';
import UserEdit from './user-edit.vue';
// import { MOCK_TABLE_DATA } from '../table-data';
interface RowType {
id: number;
email: string;
enableState: boolean;
name: string;
phone: string;
username: string;
deptId: number;
roleId: number;
createTime: string;
ceatedBy: string;
createId: number;
}
const emit = defineEmits(['change']);
// const columns = computed(() => [
// {
// title: '',
// field: 'username',
// },
// {
// title: '',
// field: 'phone',
// },
// {
// title: '',
// field: 'email',
// },
// {
// title: '',
// field: 'name',
// },
// {
// title: '',
// field: 'address',
// },
// {
// title: '',
// field: 'enableState',
// },
// {
// title: '',
// field: 'operations',
// },
// ]);
const gridEvents: VxeGridListeners<RowType> = {
cellClick: ({ row }) => {
message.info(`cell-click: ${row.name}`);
},
};
// const props = defineProps({
// renderData: {
// type: Array,
// default: () => []
// }
// })
const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: 'name',
},
columns: [
{
title: '用户名',
field: 'username',
},
{
title: '电话',
field: 'phone',
},
],
exportConfig: {},
keepSource: true,
proxyConfig: {
ajax: {
query: async () => {
const res = await queryUserList({
page: 1,
size: 10,
});
return res.records;
},
},
},
};
const [Grid] = useVbenVxeGrid({ gridEvents, gridOptions });
const generateFormModel = () => {
return {
id: '',
name: '',
username: '',
phone: '',
email: '',
createdTime: [],
enableState: '',
deptId: '',
page: 1,
current: 1,
size: 10,
};
};
const formModel = ref(generateFormModel());
const selectUser = () => {
emit('change', {
...formModel.value,
});
};
//
const reset = () => {
formModel.value = generateFormModel();
emit('change', {
...formModel.value,
});
};
</script>
<template>
<div class="px-5">
<a-card class="general-card">
<a-row>
<a-col :flex="1">
<a-form
:model="formModel"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }"
label-align="right"
>
<a-row :gutter="18">
<a-col :span="6">
<a-form-item field="username" label="用户名">
<a-input
v-model:value="formModel.username"
placeholder="请输入用户名"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item field="phone" label="电话号码">
<a-input
v-model:value="formModel.phone"
placeholder="请输入电话号码"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item field="deptId" label="部门">
<a-input
v-model:value="formModel.deptId"
placeholder="所属部门"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-col>
<a-col flex="86px" style="text-align: right">
<a-space :size="18">
<a-button type="primary" @click="selectUser"> 查询 </a-button>
<a-button @click="reset"> 重置 </a-button>
</a-space>
</a-col>
</a-row>
<a-divider style="margin-top: 0" />
<a-row>
<a-col :span="12">
<a-space>
<UserEdit :is-create="true" />
</a-space>
</a-col>
</a-row>
<div class="vp-raw w-full">
<Grid />
</div>
</a-card>
</div>
</template>
<style scoped></style>

View File

@ -1,13 +1,13 @@
<script lang="ts" setup>
import type { WordHistoryItem, WordTempItem } from '@vben/common-ui';
import type { WordTempItem } from '@vben/common-ui';
import { onMounted, reactive, ref } from 'vue';
import { WordHistoryView, WordListView, WordWorkView } from '@vben/common-ui';
import { getChatList, getMessages, sendChatflow } from '#/api';
import { getChatList, sendChatflow } from '#/api';
const temp = reactive<WordTempItem>({
let temp = reactive<WordTempItem>({
id: 'baca08c1-e92b-4dc9-a445-3584803f54d4',
name: '职业创新申报书',
});
@ -28,15 +28,17 @@ const getLogs = async (appid: string) => {
limit: '5',
},
);
// const res = await getChatList({
// appid,
// limit: 5,
// page: 1,
// });
hitsory.value = res.data;
loading.value = false;
};
async function handleClick(item: WordHistoryItem) {
await getMessages(temp.id, {
conversationId: item.id,
userId: '1562',
});
function handleClick(item: WordTempItem) {
temp = item;
}
onMounted(() => {

View File

@ -45,7 +45,6 @@
"@vueuse/integrations": "catalog:",
"ant-design-vue": "catalog:",
"ant-design-x-vue": "^1.1.2",
"dayjs": "catalog:",
"qrcode": "catalog:",
"tippy.js": "catalog:",
"vue": "catalog:",

View File

@ -5,4 +5,5 @@ export * from './fallback';
export * from './home';
export * from './ppt';
export * from './spider';
export * from './user';
export * from './word';

View File

@ -11,7 +11,7 @@ import VueOfficePptx from '@vue-office/pptx';
const pptx = ref('/pptx/66f3cfd95e364a239d8036390db658ae.pptx');
const pptStyle = ref({
height: 'calc(100vh - 100px)',
width: 'auto',
width: '100%',
margin: 'auto',
background: '#ffffff',
});
@ -40,7 +40,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
});
</script>
<template>
<Drawer class="w-[880px]" title="文档预览" :footer="false">
<Drawer title="文档预览" :footer="false">
<VueOfficePptx
:src="pptx"
:style="pptStyle"
@ -52,6 +52,6 @@ const [Drawer, drawerApi] = useVbenDrawer({
<style scoped>
.pptx-preview-wrapper {
background: #fff !important;
background: #fff;
}
</style>

View File

@ -286,12 +286,10 @@ watch(
</script>
<template>
<PreviewDrawer />
<div class="flex h-[910px] flex-col">
<div class="flex-1 overflow-y-auto">
<div style="margin: 20px; overflow-y: auto">
<BubbleList :auto-scroll="true" :roles="roles" :items="resultItems" />
</div>
<div>
<PreviewDrawer />
<div style="height: calc(67vh - 120px); margin: 20px; overflow-y: auto">
<BubbleList :roles="roles" :items="resultItems" />
</div>
<Card class="w-full">
<Sender

View File

@ -76,7 +76,6 @@ const startFetching = async () => {
);
//
fetchResult.value = res.data.outputs.result;
downloadDisable.value = false;
} catch (error) {
console.error(error);
}
@ -110,7 +109,6 @@ const downloadFile = () => {
};
const isFetching = ref(false);
const downloadDisable = ref(true);
const fetchResult = ref('');
const fetchStatus = ref('');
</script>
@ -128,7 +126,11 @@ const fetchStatus = ref('');
<Button :disabled="!item" @click="startFetching">
{{ isFetching ? '抓取中...' : '开始抓取' }}
</Button>
<Button class="mx-2" :disabled="downloadDisable" @click="downloadFile">
<Button
class="mx-2"
:disabled="fetchStatus !== 'completed'"
@click="downloadFile"
>
下载文件
</Button>
</CardFooter>

View File

@ -5,11 +5,13 @@ interface WordTempItem {
interface WordHistoryItem {
id: string;
inputs: {
projectName: string;
workflowRun: {
id: string;
};
createdByEndUser: {
id: string;
sessionId: string;
};
name: string;
createdAt: number;
}
export type { WordHistoryItem, WordTempItem };

View File

@ -3,8 +3,6 @@ import type { WordHistoryItem } from '../word';
import { Card, CardContent, CardHeader, CardTitle } from '@vben-core/shadcn-ui';
import dayjs from 'dayjs';
interface Props {
items?: WordHistoryItem[];
title: string;
@ -37,10 +35,7 @@ defineEmits(['click']);
@click="$emit('click', item)"
class="flex cursor-pointer justify-between gap-x-6 py-5"
>
<span>{{ item.name }}</span>
<span>{{
dayjs.unix(item.createdAt).format('YYYY-MM-DD HH:mm:ss')
}}</span>
{{ item.id }}
</li>
</ul>
</CardContent>

View File

@ -12,7 +12,7 @@ import { useVbenDrawer } from '@vben/common-ui';
import { Card } from '@vben-core/shadcn-ui';
import { UserOutlined } from '@ant-design/icons-vue';
import { Button, Flex, notification } from 'ant-design-vue';
import { Button, Flex } from 'ant-design-vue';
import { Attachments, BubbleList, Sender } from 'ant-design-x-vue';
import WordPreview from './word-preview.vue';
@ -75,16 +75,7 @@ function openPreviewDrawer(
previewDrawerApi.setState({ placement }).setData(fileData).open();
}
const inputStatus = ref<string>('');
const startFetching = async () => {
if (!projectName.value) {
inputStatus.value = 'error';
notification.error({
duration: 3,
message: '请填写项目名称',
});
return;
}
isFetching.value = true;
fetchStatus.value = 'fetching';
resultItems.value.push({
@ -281,21 +272,13 @@ const resultItems = ref<ResultItem[]>([]);
</script>
<template>
<PreviewDrawer />
<div class="flex h-[910px] flex-col">
<div class="flex-1 overflow-y-auto">
<a-space class="w-full" direction="vertical">
<a-input
size="large"
v-model:value="projectName"
:status="inputStatus"
required
placeholder="项目名称(必填)"
/>
</a-space>
<div style="margin: 20px; overflow-y: auto">
<BubbleList :auto-scroll="true" :roles="roles" :items="resultItems" />
</div>
<div>
<PreviewDrawer />
<a-space class="w-full" direction="vertical">
<a-input v-model:value="projectName" required placeholder="项目名称" />
</a-space>
<div style="height: calc(67vh - 120px); margin: 20px; overflow-y: auto">
<BubbleList :roles="roles" :items="resultItems" />
</div>
<Card class="w-full">
<Sender

View File

@ -103,6 +103,17 @@ class RequestClient {
return this.request<T>(url, { ...config, method: 'GET' });
}
/**
* PATCH请求方法
*/
public patch<T = any>(
url: string,
data?: any,
config?: RequestClientConfig,
): Promise<T> {
return this.request<T>(url, { ...config, data, method: 'PATCH' });
}
/**
* POST请求方法
*/

View File

@ -13,3 +13,4 @@ export const MdiGoogle = createIconifyIcon('mdi:google');
export const MdiChevron = createIconifyIcon('tabler:chevron-right');
export const MdiUser = createIconifyIcon('mdi:user');
export const MageRobot = createIconifyIcon('mage:robot');
// export const MdiUser = createIconifyIcon('mdi:user');

View File

@ -1,7 +1,7 @@
{
"welcomeBack": "欢迎回来",
"pageTitle": "",
"pageDesc": "",
"pageTitle": "开箱即用的大型中后台管理系统",
"pageDesc": "工程化、高性能、跨组件库的前端模版",
"loginSuccess": "登录成功",
"loginSuccessDesc": "欢迎回来",
"loginSubtitle": "请输入您的帐户信息以开始管理您的项目",

View File

@ -1344,9 +1344,6 @@ importers:
ant-design-x-vue:
specifier: ^1.1.2
version: 1.1.2(ant-design-vue@4.2.6(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
dayjs:
specifier: 'catalog:'
version: 1.11.13
qrcode:
specifier: 'catalog:'
version: 1.5.4