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

View File

@ -26,6 +26,72 @@ export namespace UserApi {
user: string | UserInfo; user: string | UserInfo;
csrf: CsrfTokenResult; 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() { export async function getUserInfoApi() {
return requestClient.get<UserApi.UserResult>('/rest/user/me'); 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> <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 { onMounted, reactive, ref } from 'vue';
import { WordHistoryView, WordListView, WordWorkView } from '@vben/common-ui'; 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', id: 'baca08c1-e92b-4dc9-a445-3584803f54d4',
name: '职业创新申报书', name: '职业创新申报书',
}); });
@ -28,15 +28,17 @@ const getLogs = async (appid: string) => {
limit: '5', limit: '5',
}, },
); );
// const res = await getChatList({
// appid,
// limit: 5,
// page: 1,
// });
hitsory.value = res.data; hitsory.value = res.data;
loading.value = false; loading.value = false;
}; };
async function handleClick(item: WordHistoryItem) { function handleClick(item: WordTempItem) {
await getMessages(temp.id, { temp = item;
conversationId: item.id,
userId: '1562',
});
} }
onMounted(() => { onMounted(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -103,6 +103,17 @@ class RequestClient {
return this.request<T>(url, { ...config, method: 'GET' }); 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请求方法 * POST请求方法
*/ */

View File

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

View File

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

View File

@ -1344,9 +1344,6 @@ importers:
ant-design-x-vue: ant-design-x-vue:
specifier: ^1.1.2 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)) 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: qrcode:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.5.4 version: 1.5.4