Compare commits

...

12 Commits

Author SHA1 Message Date
e4745ae789 feat(@vben/common-ui): 优化文档处理功能
- 添加 markdown 解析功能
- 优化文档预览和工作视图组件
- 添加文档生成相关功能
- 修复一些与文档处理相关的问题
2025-05-12 19:52:53 +08:00
vertoryao
851794c127 refactor(@vben/web-antd): 优化页面布局和功能
- 更新 API 地址
- 优化 PPT 和 Word 页面布局
- 添加项目名称必填验证
-修复文档生成后的处理逻辑
- 优化气泡列表组件属性
2025-05-12 15:39:43 +08:00
7f0a6a07c8 feat(@vben/web-antd): 优化权限管理 2025-05-11 10:46:55 +08:00
dcda8635f5 feat(@vben/web-antd): 优化部门管理 2025-05-10 21:50:30 +08:00
16e04a5fc0 feat(@vben/web-antd): 优化角色管理 2025-05-10 21:49:24 +08:00
996a8972ff feat(@vben/web-antd): 优化用户管理 2025-05-10 21:47:56 +08:00
51d63fc7c2 feat(@vben/web-antd): 优化部门模块 2025-05-10 13:29:55 +08:00
8a5777c6bb feat(@vben/web-antd): 优化角色模块和系统菜单 2025-05-10 12:39:10 +08:00
139aa926be feat(@vben/web-antd): 新增角色管理功能 2025-05-08 22:02:59 +08:00
cf11f2cdf5 feat(@vben/web-antd): 新增部门管理功能 2025-05-08 21:19:01 +08:00
8aa7411f69 refactor(@vben/common-ui): 新增用户管理功能 2025-05-08 19:48:50 +08:00
1464345c4a refactor(@vben/common-ui): 新增用户管理功能 2025-05-08 19:00:31 +08:00
37 changed files with 2169 additions and 98 deletions

View File

@ -1,8 +1,16 @@
import type { Recordable } from '@vben/types';
import { h } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { $te } from '@vben/locales';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { get, isFunction, isString } from '@vben/utils';
import { Button, Image } from 'ant-design-vue';
import { objectOmit } from '@vueuse/core';
import { Button, Image, Popconfirm, Switch, Tag } from 'ant-design-vue';
import { $t } from '#/locales';
import { useVbenForm } from './form';
@ -44,6 +52,25 @@ setupVbenVxeTable({
},
});
vxeUI.renderer.add('CellTag', {
renderTableDefault({ options, props }, { column, row }) {
const value = get(row, column.field);
const tagOptions = options ?? [
{ color: 'success', label: $t('common.enabled'), value: true },
{ color: 'error', label: $t('common.disabled'), value: false },
];
const tagItem = tagOptions.find((item) => item.value === value);
return h(
Tag,
{
...props,
...objectOmit(tagItem ?? {}, ['label']),
},
{ default: () => tagItem?.label ?? value },
);
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', {
renderTableDefault(renderOpts) {
@ -55,6 +82,181 @@ setupVbenVxeTable({
);
},
});
vxeUI.renderer.add('CellSwitch', {
renderTableDefault({ attrs, props }, { column, row }) {
const loadingKey = `__loading_${column.field}`;
const finallyProps = {
checkedChildren: $t('common.enabled'),
checkedValue: true,
unCheckedChildren: $t('common.disabled'),
unCheckedValue: false,
...props,
checked: row[column.field],
loading: row[loadingKey] ?? false,
'onUpdate:checked': onChange,
};
async function onChange(newVal: any) {
row[loadingKey] = true;
try {
const result = await attrs?.beforeChange?.(newVal, row);
if (result !== false) {
row[column.field] = newVal;
}
} finally {
row[loadingKey] = false;
}
}
return h(Switch, finallyProps);
},
});
/**
*
*/
vxeUI.renderer.add('CellOperation', {
renderTableDefault({ attrs, options, props }, { column, row }) {
const defaultProps = { size: 'small', type: 'link', ...props };
let align = 'end';
switch (column.align) {
case 'center': {
align = 'center';
break;
}
case 'left': {
align = 'start';
break;
}
default: {
align = 'end';
break;
}
}
const presets: Recordable<Recordable<any>> = {
delete: {
danger: true,
text: $t('common.delete'),
},
edit: {
text: $t('common.edit'),
},
};
const operations: Array<Recordable<any>> = (
options || ['edit', 'delete']
)
.map((opt) => {
if (isString(opt)) {
return presets[opt]
? { code: opt, ...presets[opt], ...defaultProps }
: {
code: opt,
text: $te(`common.${opt}`) ? $t(`common.${opt}`) : opt,
...defaultProps,
};
} else {
return { ...defaultProps, ...presets[opt.code], ...opt };
}
})
.map((opt) => {
const optBtn: Recordable<any> = {};
Object.keys(opt).forEach((key) => {
optBtn[key] = isFunction(opt[key]) ? opt[key](row) : opt[key];
});
return optBtn;
})
.filter((opt) => opt.show !== false);
function renderBtn(opt: Recordable<any>, listen = true) {
return h(
Button,
{
...props,
...opt,
icon: undefined,
onClick: listen
? () =>
attrs?.onClick?.({
code: opt.code,
row,
})
: undefined,
},
{
default: () => {
const content = [];
if (opt.icon) {
content.push(
h(IconifyIcon, { class: 'size-5', icon: opt.icon }),
);
}
content.push(opt.text);
return content;
},
},
);
}
function renderConfirm(opt: Recordable<any>) {
let viewportWrapper: HTMLElement | null = null;
return h(
Popconfirm,
{
/**
* popconfirm用在固定列中时
*
* body或者表格视口区域作为弹窗容器时又会导致弹窗无法跟随表格滚动
*
*
*/
getPopupContainer(el) {
viewportWrapper = el.closest('.vxe-table--viewport-wrapper');
return document.body;
},
placement: 'topLeft',
title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']),
...props,
...opt,
icon: undefined,
onOpenChange: (open: boolean) => {
// 当弹窗打开时,禁止表格的滚动
if (open) {
viewportWrapper?.style.setProperty('pointer-events', 'none');
} else {
viewportWrapper?.style.removeProperty('pointer-events');
}
},
onConfirm: () => {
attrs?.onClick?.({
code: opt.code,
row,
});
},
},
{
default: () => renderBtn({ ...opt }, false),
description: () =>
h(
'div',
{ class: 'truncate' },
$t('ui.actionMessage.deleteConfirm', [
row[attrs?.nameField || 'name'],
]),
),
},
);
}
const btns = operations.map((opt) =>
opt.code === 'delete' ? renderConfirm(opt) : renderBtn(opt),
);
return h(
'div',
{
class: 'flex table-operations',
style: { justifyContent: align },
},
btns,
);
},
});
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
@ -63,5 +265,12 @@ setupVbenVxeTable({
});
export { useVbenVxeGrid };
export type OnActionClickParams<T = Recordable<any>> = {
code: string;
row: T;
};
export type OnActionClickFn<T = Recordable<any>> = (
params: OnActionClickParams<T>,
) => void;
export type * from '@vben/plugins/vxe-table';

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

@ -0,0 +1,76 @@
import { requestClient } from '#/api/request';
export namespace DeptApi {
export interface Dept {
remark: string;
createTime: string;
name: string;
enabled: string;
id: string;
children?: Dept[];
}
export interface DeptRecord extends Dept {
id: string;
}
export interface DeptTreeData {
id: number;
label: string;
children?: DeptTreeData[];
}
}
// 添加部门
export function createDept(data: DeptApi.Dept) {
return requestClient.post(`/rest/dept`, data);
}
// 更新部门信息
export function updateDept(id: string, data: DeptApi.DeptRecord) {
return requestClient.patch(`/rest/dept/${id}`, data);
}
// 删除部门
export function removeDept(id: string) {
return requestClient.delete(`/rest/dept/${id}`);
}
// 模糊查询获取部门列表
export function queryDeptList(data: DeptApi.DeptRecord) {
return requestClient.get('/rest/dept', { data });
}
// 获取部门列表
export function deptList() {
return requestClient.get(`/rest/dept`);
}
// 启用状态
export function enabledDept(id: string) {
return requestClient.patch(`/rest/dept/toggle/${id}`, id);
}
export function getAllDeptTree(id?: number | string) {
return requestClient.get('/rest/dept/tree', { params: { id } });
}
// export function getDeptTree(params?: Partial<DeptRecord>) {
// return axios.get<DeptRecord[]>(`/dept/trees`, {
// params,
// paramsSerializer: (obj) => {
// return qs.stringify(obj);
// },
// });
// }
// export function listDepts(params?: ListParams<Partial<DeptRecord>>) {
// return axios.get<DeptRecord[]>(`/dept`, {
// params,
// paramsSerializer: (obj) => {
// return qs.stringify(obj);
// },
// });
// }
// export function queryDepts(params?: ListParams<Partial<DeptRecord>>) {
// return queryList(`/dept`, params);
// }

View File

@ -1,6 +1,8 @@
export * from './auth';
export * from './chatflow';
export * from './dept';
export * from './menu';
export * from './role';
export * from './server';
export * from './user';
export * from './workflow';

View File

@ -0,0 +1,74 @@
import { requestClient } from '#/api/request';
export namespace RoleApi {
export interface Role {
[key: string]: any;
name: string;
dataScope: string;
enabled: boolean;
deptId: string;
remark?: string;
createBy: string;
menuIds: [];
createTime: string;
createId: string;
// authorities: (number | undefined)[];
id: string;
}
export interface RoleListParams {
page: number;
size: number;
sort: string;
name?: string;
enabled?: boolean;
deptId?: string;
}
export interface RoleListRecord extends Role {
name: string;
}
}
// 查询所有的角色列表、
export function queryRoleList(data: RoleApi.RoleListParams) {
// return axios.get('/rest/role',data);
return requestClient.get<Array<RoleApi.Role>>('/rest/role', { params: data });
}
// 切换启用状态
export function enabledRole(id: string) {
return requestClient.patch(`/rest/role/${id}/toggle`);
}
// 删除
export function removeRole(id: string) {
return requestClient.delete(`/rest/role/${id}`);
}
// 添加
export function createRole(data: RoleApi.Role) {
return requestClient.post(`/rest/role`, data);
}
// 更新
export function updateRole(id: any, data: RoleApi.Role) {
return requestClient.patch(`/rest/role/${id}`, data);
}
// 获取详情
export function roleDetail(id: string) {
return requestClient.get<RoleApi.Role>(`/rest/role/${id}`);
}
export const queryMenuList = (data: string) => {
return requestClient.get('/rest/menu/tree', {
params: {
name: data,
},
});
};
// export function queryRoles(params?: ListParams<Partial<Role>>) {
// return queryList<Role>(`/rest/role/query`, params);
// }

View File

@ -26,6 +26,73 @@ 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 User {
username?: string;
nickName?: string;
name: 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 +101,66 @@ 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 createUser(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: any) {
return requestClient.get(`/rest/user/${id}`);
}
// 是否启用
export function enabledUser(id: any) {
return requestClient.patch(`/rest/user/${id}/toggle`);
}
// 删除用户
export function removeUser(id: string) {
return requestClient.delete(`/rest/user/${id}`);
}
// 更新用户信息
export function updateUser(id: any, data: UserApi.UserRecord) {
return requestClient.patch<UserApi.Res>(`/rest/user/${id}`, data);
}
export function selfUpdate(data: UserApi.User) {
return requestClient.patch<UserApi.Res>(`/rest/user/self`, data);
}
// 获取个人用户信息
export function getUserInfo() {
return requestClient.get<UserApi.User>('/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.User>(`/user/${id}`);
}

View File

@ -74,7 +74,6 @@ function setupAccessGuard(router: Router) {
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_PATH) {
console.log('没有访问权限,请登录');
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
@ -96,11 +95,13 @@ function setupAccessGuard(router: Router) {
// 生成路由表
// 当前登录用户拥有的角色标识列表
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
const userRoles = userInfo.roles ?? [];
// const userRoles = userInfo.roles ?? [];
// permissions Keven自定义
const permissions = userInfo.permissions ?? [];
// 生成菜单和路由
const { accessibleMenus, accessibleRoutes } = await generateAccess({
roles: userRoles,
roles: permissions,
router,
// 则会在菜单中显示但是访问会被重定向到403
routes: accessRoutes,

View File

@ -9,6 +9,7 @@ const routes: RouteRecordRaw[] = [
icon: 'mdi:home',
title: '首页',
order: -1,
authority: ['dashboard'],
},
},
];

View File

@ -11,6 +11,7 @@ const routes: RouteRecordRaw[] = [
icon: SvgPPT,
title: 'PPT自动生成',
order: 3,
authority: ['dify:workflow:run'],
},
},
];

View File

@ -11,6 +11,7 @@ const routes: RouteRecordRaw[] = [
icon: SvgSpider,
title: '数据爬取',
order: 1,
authority: ['dify:server:init'],
},
},
];

View File

@ -0,0 +1,50 @@
import type { RouteRecordRaw } from 'vue-router';
import { EosRole, IconSystem, MdiUser, RiDept } from '@vben/icons';
const routes: RouteRecordRaw[] = [
{
name: 'system',
path: '/system',
meta: {
icon: IconSystem,
title: '系统管理',
order: 4,
authority: ['system'],
},
children: [
{
name: 'role',
path: '/system/role',
component: () => import('#/views/role/list.vue'),
meta: {
icon: EosRole,
title: '角色管理',
authority: ['system:role'],
},
},
{
name: 'dept',
path: '/system/dept',
component: () => import('#/views/dept/list.vue'),
meta: {
icon: RiDept,
title: '部门管理',
authority: ['system:dept'],
},
},
{
name: 'user',
path: '/system/user',
component: () => import('#/views/user/list.vue'),
meta: {
icon: MdiUser,
title: '用户管理',
authority: ['system:user'],
},
},
],
},
];
export default routes;

View File

@ -11,6 +11,7 @@ const routes: RouteRecordRaw[] = [
icon: SvgWord,
title: 'Word自动生成',
order: 2,
authority: ['dify:chat:send'],
},
},
];

View File

@ -51,6 +51,8 @@ export const useAuthStore = defineStore('auth', () => {
realName: user.username,
username: user.username,
roles: [user.role.name],
// permissions Keven自定义
permissions: user.permissions,
homePath: '/home',
token: csrf.token,
};

View File

@ -0,0 +1,178 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { DeptApi } from '#/api';
import { ref } from 'vue';
import { z } from '#/adapter/form';
import { getAllDeptTree } from '#/api';
import { $t } from '#/locales';
const treeData = ref([]);
function transformToTreeData(data: any) {
return data.map((item) => ({
value: item.id,
label: item.name,
...(item.children && { children: transformToTreeData(item.children) }),
}));
}
// 使用示例
getAllDeptTree(1).then((res) => {
treeData.value = transformToTreeData(res);
});
/**
* 使export一个数组常量
*/
export function useSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'name',
label: '部门名称',
rules: z.string(),
},
{
component: 'TreeSelect',
componentProps: {
allowClear: true,
// api: getAllDeptTree(1),
treeData,
class: 'w-full',
labelField: 'name',
valueField: 'id',
childrenField: 'children',
},
fieldName: 'pid',
label: '上级部门',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: $t('common.enabled'), value: true },
{ label: $t('common.disabled'), value: false },
],
optionType: 'button',
},
defaultValue: true,
fieldName: 'enabled',
label: '状态',
},
{
component: 'Textarea',
componentProps: {
maxLength: 50,
rows: 3,
showCount: true,
},
fieldName: 'remark',
label: '备注',
rules: z.string().optional(),
},
];
}
// export function useGridFormSchema(): VbenFormSchema[] {
// return [
// {
// component: 'Input',
// fieldName: 'name',
// label: '部门名称',
// },
// {
// component: 'Select',
// componentProps: {
// allowClear: true,
// options: [
// { label: $t('common.enabled'), value: true },
// { label: $t('common.disabled'), value: false },
// ],
// },
// fieldName: 'enable',
// label: '状态',
// },
// // {
// // component: 'Input',
// // fieldName: 'remark',
// // label: '备注',
// // },
// // {
// // component: 'RangePicker',
// // fieldName: 'createTime',
// // label: '创建时间',
// // },
// ];
// }
/**
*
* @description 使export一个Array常量
* @param onActionClick
*/
export function useColumns(
onActionClick?: OnActionClickFn<DeptApi.Dept>,
): VxeTableGridOptions<DeptApi.Dept>['columns'] {
return [
{
align: 'left',
field: 'name',
fixed: 'left',
title: '部门名称',
treeNode: true,
width: 150,
},
{
cellRender: {
name: 'CellTag',
},
field: 'enabled',
title: '状态',
width: 100,
},
{
field: 'remark',
title: '备注',
},
{
field: 'createTime',
slots: { default: 'createTime' },
title: '创建时间',
width: 180,
},
{
align: 'right',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '部门',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
{
code: 'append',
text: '新增下级',
},
'edit', // 默认的编辑按钮
{
code: 'delete', // 默认的删除按钮
disabled: (row: DeptApi.Dept) => {
return !!(row.children && row.children.length > 0);
},
},
],
},
field: 'operation',
fixed: 'right',
headerAlign: 'center',
showOverflow: false,
title: '操作',
width: 200,
},
];
}

View File

@ -0,0 +1,155 @@
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { DeptApi } from '#/api';
import { Page, useVbenModal } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAllDeptTree, removeDept } from '#/api';
import { $t } from '#/locales';
import { useColumns } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/**
* 编辑部门
* @param row
*/
function onEdit(row: DeptApi.Dept) {
formModalApi.setData(row).open();
}
/**
* 添加下级部门
* @param row
*/
function onAppend(row: DeptApi.Dept) {
formModalApi.setData({ pid: row.id }).open();
}
/**
* 创建新部门
*/
function onCreate() {
formModalApi.setData(null).open();
}
/**
* 删除部门
* @param row
*/
function onDelete(row: DeptApi.Dept) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
removeDept(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
refreshGrid();
})
.catch(() => {
hideLoading();
});
}
/**
* 表格操作按钮的回调函数
*/
function onActionClick({ code, row }: OnActionClickParams<DeptApi.Dept>) {
switch (code) {
case 'append': {
onAppend(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
case 'edit': {
onEdit(row);
break;
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
// formOptions: {
// fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
// schema: useGridFormSchema(),
// },
gridEvents: {},
gridOptions: {
columns: useColumns(onActionClick),
height: 'auto',
keepSource: true,
pagerConfig: {
enabled: false,
},
proxyConfig: {
ajax: {
query: async (_params) => {
const res = await getAllDeptTree(1);
return {
items: res,
};
},
},
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
zoom: true,
},
treeConfig: {
parentField: 'pid',
rowField: 'id',
transform: false,
},
} as VxeTableGridOptions,
});
/**
* 刷新表格
*/
function refreshGrid() {
gridApi.query();
}
</script>
<template>
<Page auto-content-height>
<FormModal @success="refreshGrid" />
<Grid table-title="部门列表">
<template #toolbar-tools>
<Button
type="primary"
@click="onCreate"
v-access:code="'system:dept:create'"
>
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['部门']) }}
</Button>
</template>
<template #createTime="{ row }">
{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm') }}
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,78 @@
<script lang="ts" setup>
import type { DeptApi } from '#/api';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createDept, updateDept } from '#/api';
import { $t } from '#/locales';
import { useSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<DeptApi.Dept>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['部门'])
: $t('ui.actionTitle.create', ['部门']);
});
const [Form, formApi] = useVbenForm({
layout: 'vertical',
schema: useSchema(),
showDefaultActions: false,
});
function resetForm() {
formApi.resetForm();
formApi.setValues(formData.value || {});
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (valid) {
modalApi.lock();
const data = await formApi.getValues();
try {
await (formData.value?.id
? updateDept(formData.value.id, data)
: createDept(data));
modalApi.close();
emit('success');
} finally {
modalApi.lock(false);
}
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<DeptApi.Dept>();
if (data) {
if (data.pid === 0) {
data.pid = undefined;
}
formData.value = data;
formApi.setValues(formData.value);
}
}
},
});
</script>
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
<template #prepend-footer>
<div class="flex-auto">
<Button type="primary" danger @click="resetForm">
{{ $t('common.reset') }}
</Button>
</div>
</template>
</Modal>
</template>

View File

@ -56,6 +56,10 @@ async function handleClick(item: PPTTempItem) {
}
}
async function handleClickMode(item: PPTTempItem) {
temp = item;
}
onMounted(() => {
getLogs(temp.id);
});
@ -71,7 +75,7 @@ onMounted(() => {
title="运行历史"
@click="handleClick"
/>
<PptListView title="选择模板" @click="handleClick" />
<PptListView title="选择模板" @click="handleClickMode" />
</div>
<div class="w-full lg:w-3/4">
<PptWorkView

View File

@ -0,0 +1,132 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { RoleApi } from '#/api';
import { $t } from '#/locales';
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'name',
label: '角色名称',
rules: 'required',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: $t('common.enabled'), value: true },
{ label: $t('common.disabled'), value: false },
],
optionType: 'button',
},
defaultValue: true,
fieldName: 'enabled',
label: '状态',
},
{
component: 'Textarea',
fieldName: 'remark',
label: '备注',
},
{
component: 'Input',
fieldName: 'menuIds',
formItemClass: 'items-start',
label: '权限',
modelPropName: 'modelValue',
},
];
}
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'name',
label: '角色名称',
},
// { component: 'Input', fieldName: 'id', label: '角色ID' },
{
component: 'Select',
componentProps: {
allowClear: true,
options: [
{ label: $t('common.enabled'), value: 1 },
{ label: $t('common.disabled'), value: 0 },
],
},
fieldName: 'enabled',
label: '状态',
},
// {
// component: 'Input',
// fieldName: 'remark',
// label: '备注',
// },
// {
// component: 'RangePicker',
// fieldName: 'createTime',
// componentProps: {
// format: 'YYYY-M-D HH:mm:ss', // 设置日期时间格式
// showTime: true, // 确保选择器支持时间选择
// },
// label: '创建时间',
// },
];
}
export function useColumns<T = RoleApi.Role>(
onActionClick: OnActionClickFn<T>,
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '角色ID',
width: 200,
},
{
field: 'name',
title: '角色名称',
width: 200,
},
{
cellRender: {
attrs: { beforeChange: onStatusChange },
name: onStatusChange ? 'CellSwitch' : 'CellTag',
},
field: 'enabled',
title: '角色状态',
width: 100,
},
{
field: 'remark',
minWidth: 100,
title: '备注',
},
{
field: 'createTime',
slots: { default: 'createTime' },
title: '创建时间',
width: 200,
},
{
align: 'center',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '角色',
onClick: onActionClick,
},
name: 'CellOperation',
},
field: 'operation',
fixed: 'right',
title: '操作',
width: 200,
},
];
}

View File

@ -0,0 +1,168 @@
<script lang="ts" setup>
import type { Recordable } from '@vben/types';
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { RoleApi } from '#/api';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message, Modal } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { enabledRole, queryRoleList, removeRole } from '#/api';
import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
schema: useGridFormSchema(),
},
gridOptions: {
columns: useColumns(onActionClick, onStatusChange),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const res = await queryRoleList({
page: page.currentPage,
size: page.pageSize,
...formValues,
});
return {
total: res.total,
items: res.records,
};
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeTableGridOptions<RoleApi.Role>,
});
function onActionClick(e: OnActionClickParams<RoleApi.Role>) {
switch (e.code) {
case 'delete': {
onDelete(e.row);
break;
}
case 'edit': {
onEdit(e.row);
break;
}
}
}
/**
* 将Antd的Modal.confirm封装为promise方便在异步函数中调用
* @param content 提示内容
* @param title 提示标题
*/
function confirm(content: string, title: string) {
return new Promise((reslove, reject) => {
Modal.confirm({
content,
onCancel() {
reject(new Error('已取消'));
},
onOk() {
reslove(true);
},
title,
});
});
}
/**
* 状态开关即将改变
* @param newStatus 期望改变的状态值
* @param row 行数据
* @returns 返回false则中止改变返回其他值undefinedtrue则允许改变
*/
async function onStatusChange(newStatus: number, row: RoleApi.Role) {
const status: Recordable<string> = {
0: '禁用',
1: '启用',
};
try {
await confirm(
`你要将${row.name}的状态切换为 【${status[newStatus.toString()]}】 吗?`,
`切换状态`,
);
await enabledRole(row.id);
return true;
} catch {
return false;
}
}
function onEdit(row: RoleApi.Role) {
formDrawerApi.setData(row).open();
}
function onDelete(row: RoleApi.Role) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
removeRole(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
onRefresh();
})
.catch(() => {
hideLoading();
});
}
function onRefresh() {
gridApi.query();
}
function onCreate() {
formDrawerApi.setData({}).open();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer />
<Grid table-title="角色列表">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['角色']) }}
</Button>
</template>
<template #createTime="{ row }">
{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm') }}
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,148 @@
<script lang="ts" setup>
import type { DataNode } from 'ant-design-vue/es/tree';
import type { Recordable } from '@vben/types';
import type { RoleApi } from '#/api';
import { computed, ref } from 'vue';
import { useVbenDrawer, VbenTree } from '@vben/common-ui';
// import { IconifyIcon } from '@vben/icons';
import { Spin } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createRole, queryMenuList, roleDetail, updateRole } from '#/api';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emits = defineEmits(['success']);
const formData = ref<RoleApi.Role>();
const [Form, formApi] = useVbenForm({
schema: useFormSchema(),
showDefaultActions: false,
});
const menuIds = ref<DataNode[]>([]);
const loadingPermissions = ref(false);
const id = ref();
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
const values = await formApi.getValues();
drawerApi.lock();
(id.value ? updateRole(id.value, values) : createRole(values))
.then(() => {
emits('success');
drawerApi.close();
})
.catch(() => {
drawerApi.unlock();
});
},
async onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<RoleApi.Role>();
await formApi.resetForm();
if (data.id) {
const roleDetailData = await roleDetail(data.id);
formData.value = roleDetailData;
// menus id menuIds
formData.value.menuIds = roleDetailData.menus.map((menu) => menu.id);
id.value = data.id;
formApi.setValues(roleDetailData);
} else {
id.value = undefined;
}
if (menuIds.value.length === 0) {
loadPermissions();
}
}
},
});
async function loadPermissions() {
loadingPermissions.value = true;
try {
const res = await queryMenuList('all');
menuIds.value = res.map((item) => ({
key: item.id,
title: item.name,
children: item.children || null,
meta: item.meta,
}));
} finally {
loadingPermissions.value = false;
}
}
const getDrawerTitle = computed(() => {
return formData.value?.id
? $t('common.edit', '角色')
: $t('common.create', '角色');
});
function getNodeClass(node: Recordable<any>) {
const classes: string[] = [];
if (node.value?.type === 'button') {
classes.push('inline-flex');
if (node.index % 3 >= 1) {
classes.push('!pl-0');
}
}
return classes.join(' ');
}
</script>
<template>
<Drawer :title="getDrawerTitle">
<Form>
<template #menuIds="slotProps">
<Spin :spinning="loadingPermissions" wrapper-class-name="w-full">
<VbenTree
:tree-data="menuIds"
multiple
bordered
:default-expanded-level="2"
:get-node-class="getNodeClass"
v-bind="slotProps"
value-field="id"
label-field="meta.title"
icon-field="meta.icon"
>
<template #node="{ value }">
<!-- <IconifyIcon v-if="value.meta.icon" :icon="value.meta.icon" />-->
{{ value.meta.locale }}
</template>
</VbenTree>
</Spin>
</template>
</Form>
</Drawer>
</template>
<style lang="css" scoped>
:deep(.ant-tree-title) {
.tree-actions {
display: none;
margin-left: 20px;
}
}
:deep(.ant-tree-title:hover) {
.tree-actions {
display: flex;
flex: auto;
justify-content: flex-end;
margin-left: 20px;
}
}
</style>

View File

@ -0,0 +1,198 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { UserApi } from '#/api';
import { ref } from 'vue';
import { getAllDeptTree, queryRoleList } from '#/api';
import { $t } from '#/locales';
const treeData = ref([]);
const optionsRoleData = ref([]);
queryRoleList('').then((res) => {
optionsRoleData.value = res.records.map((item) => ({
label: item.name,
value: item.id,
}));
});
function transformToTreeData(data: any) {
return data.map((item) => ({
value: item.id,
label: item.name,
...(item.children && { children: transformToTreeData(item.children) }),
}));
}
// 使用示例
getAllDeptTree(1).then((res) => {
treeData.value = transformToTreeData(res);
});
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'username',
label: '用户名称',
rules: 'required',
},
{
component: 'Input',
fieldName: 'name',
label: '昵称',
rules: 'required',
},
{
component: 'Input',
fieldName: 'email',
label: 'Email',
rules: 'required',
},
{
component: 'Input',
fieldName: 'phone',
label: '电话',
rules: 'required',
},
{
component: 'Input',
fieldName: 'password',
label: '密码',
rules: 'required',
},
{
component: 'Input',
fieldName: 'address',
label: '地址',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: $t('common.enabled'), value: true },
{ label: $t('common.disabled'), value: false },
],
optionType: 'button',
},
defaultValue: true,
fieldName: 'enable',
label: '状态',
},
{
component: 'Textarea',
fieldName: 'remark',
label: '备注',
},
{
component: 'Select',
rules: 'required',
componentProps: {
allowClear: true,
mode: 'multiple',
// api: queryRoleList(''),
options: optionsRoleData,
class: 'w-full',
labelField: 'name',
valueField: 'id',
},
fieldName: 'roleIds',
label: '所属角色',
},
{
component: 'TreeSelect',
componentProps: {
allowClear: true,
// api: getAllDeptTree(1),
treeData,
class: 'w-full',
labelField: 'name',
valueField: 'id',
childrenField: 'children',
},
fieldName: 'deptId',
label: '所属部门',
},
];
}
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'username',
label: '用户名称',
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: [
{ label: $t('common.enabled'), value: true },
{ label: $t('common.disabled'), value: false },
],
},
fieldName: 'enableState',
label: '状态',
},
// {
// component: 'Input',
// fieldName: 'remark',
// label: '备注',
// },
// {
// component: 'RangePicker',
// fieldName: 'createTime',
// label: '创建时间',
// },
];
}
export function useColumns<T = UserApi.User>(
onActionClick: OnActionClickFn<T>,
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions['columns'] {
return [
{
field: 'username',
title: '用户名称',
width: 200,
},
{
cellRender: {
attrs: { beforeChange: onStatusChange },
name: onStatusChange ? 'CellSwitch' : 'CellTag',
},
field: 'enableState',
title: '用户状态',
width: 100,
},
{
field: 'remark',
minWidth: 100,
title: '备注',
},
{
field: 'createTime',
slots: { default: 'createTime' },
title: '创建时间',
width: 200,
},
{
align: 'center',
cellRender: {
attrs: {
nameField: 'name',
nameTitle: '用户',
onClick: onActionClick,
},
name: 'CellOperation',
},
field: 'operation',
fixed: 'right',
title: '操作',
width: 130,
},
];
}

View File

@ -0,0 +1,168 @@
<script lang="ts" setup>
import type { Recordable } from '@vben/types';
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { UserApi } from '#/api';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Plus } from '@vben/icons';
import { Button, message, Modal } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { enabledUser, queryUserList, removeUser } from '#/api';
import { $t } from '#/locales';
import { useColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
schema: useGridFormSchema(),
},
gridOptions: {
columns: useColumns(onActionClick, onStatusChange),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const res = await queryUserList({
page: page.currentPage,
size: page.size,
...formValues,
});
return {
items: res.records,
total: res.total,
};
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
custom: true,
export: false,
refresh: { code: 'query' },
search: true,
zoom: true,
},
} as VxeTableGridOptions<UserApi.User>,
});
function onActionClick(e: OnActionClickParams<UserApi.User>) {
switch (e.code) {
case 'delete': {
onDelete(e.row);
break;
}
case 'edit': {
onEdit(e.row);
break;
}
}
}
/**
* 将Antd的Modal.confirm封装为promise方便在异步函数中调用
* @param content 提示内容
* @param title 提示标题
*/
function confirm(content: string, title: string) {
return new Promise((reslove, reject) => {
Modal.confirm({
content,
onCancel() {
reject(new Error('已取消'));
},
onOk() {
reslove(true);
},
title,
});
});
}
/**
* 状态开关即将改变
* @param newStatus 期望改变的状态值
* @param row 行数据
* @returns 返回false则中止改变返回其他值undefinedtrue则允许改变
*/
async function onStatusChange(newStatus: number, row: UserApi.User) {
const status: Recordable<string> = {
0: '禁用',
1: '启用',
};
try {
await confirm(
`你要将${row.name}的状态切换为 【${status[newStatus.toString()]}】 吗?`,
`切换状态`,
);
await enabledUser(row.id);
return true;
} catch {
return false;
}
}
function onEdit(row: UserApi.User) {
formDrawerApi.setData(row).open();
}
function onDelete(row: UserApi.User) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
key: 'action_process_msg',
});
removeUser(row.id)
.then(() => {
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_process_msg',
});
onRefresh();
})
.catch(() => {
hideLoading();
});
}
function onRefresh() {
gridApi.query();
}
function onCreate() {
formDrawerApi.setData({}).open();
}
</script>
<template>
<Page auto-content-height>
<FormDrawer />
<Grid table-title="用户列表">
<template #toolbar-tools>
<Button type="primary" @click="onCreate">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['用户']) }}
</Button>
</template>
<template #createTime="{ row }">
{{ dayjs(row.createTime).format('YYYY-MM-DD HH:mm') }}
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,138 @@
<script lang="ts" setup>
import type { DataNode } from 'ant-design-vue/es/tree';
import type { Recordable } from '@vben/types';
import type { UserApi } from '#/api';
import { computed, ref } from 'vue';
import { useVbenDrawer, VbenTree } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Spin } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createUser, updateUser, userDetail } from '#/api';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emits = defineEmits(['success']);
const formData = ref<UserApi.User>();
const [Form, formApi] = useVbenForm({
schema: useFormSchema(),
showDefaultActions: false,
});
const permissions = ref<DataNode[]>([]);
const loadingPermissions = ref(false);
const id = ref();
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) return;
const values = await formApi.getValues();
drawerApi.lock();
(id.value ? updateUser(id.value, values) : createUser(values))
.then(() => {
emits('success');
drawerApi.close();
})
.catch(() => {
drawerApi.unlock();
});
},
async onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<UserApi.User>();
await formApi.resetForm();
if (data.id) {
formData.value = await userDetail(data.id);
id.value = data.id;
formApi.setValues(data);
} else {
id.value = undefined;
}
// if (permissions.value.length === 0) {
// loadPermissions();
// }
}
},
});
// async function loadPermissions() {
// loadingPermissions.value = true;
// try {
// const res = await getMenuList();
// permissions.value = res as unknown as DataNode[];
// } finally {
// loadingPermissions.value = false;
// }
// }
const getDrawerTitle = computed(() => {
return formData.value?.id
? $t('common.edit', '用户')
: $t('common.create', '用户');
});
function getNodeClass(node: Recordable<any>) {
const classes: string[] = [];
if (node.value?.type === 'button') {
classes.push('inline-flex');
if (node.index % 3 >= 1) {
classes.push('!pl-0');
}
}
return classes.join(' ');
}
</script>
<template>
<Drawer :title="getDrawerTitle">
<Form>
<template #permissions="slotProps">
<Spin :spinning="loadingPermissions" wrapper-class-name="w-full">
<VbenTree
:tree-data="permissions"
multiple
bordered
:default-expanded-level="2"
:get-node-class="getNodeClass"
v-bind="slotProps"
value-field="id"
label-field="meta.title"
icon-field="meta.icon"
>
<template #node="{ value }">
<IconifyIcon v-if="value.meta.icon" :icon="value.meta.icon" />
{{ $t(value.meta.title) }}
</template>
</VbenTree>
</Spin>
</template>
</Form>
</Drawer>
</template>
<style lang="css" scoped>
:deep(.ant-tree-title) {
.tree-actions {
display: none;
margin-left: 20px;
}
}
:deep(.ant-tree-title:hover) {
.tree-actions {
display: flex;
flex: auto;
justify-content: flex-end;
margin-left: 20px;
}
}
</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

@ -10,7 +10,9 @@ export default defineConfig(async () => {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址
target: 'http://localhost:8081/api',
// target: 'http://localhost:8081/api',
target: 'http://43.139.10.64:8082/api',
// target: 'http://192.168.3.238:8081/api',
ws: true,
},
'/docx': {
@ -26,7 +28,9 @@ export default defineConfig(async () => {
'/v1': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/v1/, ''),
target: 'http://localhost:8081/v1',
// target: 'http://localhost:8081/v1',
target: 'http://43.139.10.64:8082/v1',
// target: 'http://192.168.3.238:8081/v1',
},
},
},

View File

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

View File

@ -4,22 +4,26 @@ import { ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import VueOfficePptx from '@vue-office/pptx';
import { message } from 'ant-design-vue';
// import '@vue-office/pptx/lib/index.css';
// http://47.112.173.8:6802/static/66f3cfd95e364a239d8036390db658ae.pptx
// const url = ref('');
const isLoading = ref(false); //
const pptx = ref('/pptx/66f3cfd95e364a239d8036390db658ae.pptx');
const pptStyle = ref({
height: 'calc(100vh - 100px)',
width: 'auto',
width: '100%',
margin: 'auto',
background: '#ffffff',
});
const renderedHandler = () => {
console.warn('渲染完成');
isLoading.value = false; //
message.success('渲染完成');
};
const errorHandler = (err) => {
console.error(`渲染失败:${err}`);
const errorHandler = () => {
isLoading.value = false; //
message.error('渲染失败');
};
const [Drawer, drawerApi] = useVbenDrawer({
onCancel() {
@ -32,6 +36,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
if (isOpen) {
const data = drawerApi.getData<Record<string, any>>();
if (data) {
isLoading.value = true; //
pptx.value = `/pptx/${data}`; // docx
}
// url.value = drawerApi.getData<Record<string, any>>();
@ -40,7 +45,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
});
</script>
<template>
<Drawer class="w-[880px]" title="文档预览" :footer="false">
<Drawer title="文档预览" :footer="false">
<div v-if="isLoading" class="loading-overlay">正在加载文档请稍候...</div>
<VueOfficePptx
:src="pptx"
:style="pptStyle"
@ -52,6 +58,6 @@ const [Drawer, drawerApi] = useVbenDrawer({
<style scoped>
.pptx-preview-wrapper {
background: #fff !important;
background: #fff;
}
</style>

View File

@ -12,8 +12,9 @@ import { useVbenDrawer } from '@vben/common-ui';
import { Card } from '@vben-core/shadcn-ui';
import { UserOutlined } from '@ant-design/icons-vue';
import { Button, Flex } from 'ant-design-vue';
import { Button, Flex, Typography } from 'ant-design-vue';
import { Attachments, BubbleList, Sender } from 'ant-design-x-vue';
import markdownit from 'markdown-it';
import PptPreview from './ppt-perview.vue';
@ -68,6 +69,24 @@ const props = withDefaults(defineProps<Props>(), {
}),
});
// markdown
const md = markdownit({ html: true, breaks: true });
const renderMarkdown: BubbleListProps['roles'][string]['messageRender'] = (
content: string,
) => {
return h(Typography, [
h('div', {
innerHTML: content,
style: {
padding: '8px',
lineHeight: '1.6',
whiteSpace: 'break-spaces',
},
}),
]);
};
const [PreviewDrawer, previewDrawerApi] = useVbenDrawer({
//
connectedComponent: PptPreview,
@ -190,6 +209,17 @@ const roles: BubbleListProps['roles'] = {
},
},
avatar: { icon: h(UserOutlined), style: { background: '#fde3cf' } },
messageRender: (content) =>
h(Typography, [
h('div', {
innerHTML: md.render(content),
style: {
padding: '8px',
lineHeight: '1.6',
whiteSpace: 'break-spaces',
},
}),
]),
},
file: {
placement: 'start',
@ -286,17 +316,21 @@ 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"
:typing="true"
:items="resultItems"
:message-render="renderMarkdown"
/>
</div>
<Card class="w-full">
<Sender
v-model:value="value"
:loading="isFetching"
:disabled="isFetching"
:auto-size="{ minRows: 6, maxRows: 12 }"
@submit="startFetching"
/>

View File

@ -12,6 +12,8 @@ import {
CardTitle,
} from '@vben-core/shadcn-ui';
import { RangePicker } from 'ant-design-vue';
interface SpiderParams {
appid: string;
}
@ -57,11 +59,21 @@ const props = withDefaults(defineProps<Props>(), {
}),
});
const selectedDateRange = ref<string[]>([]);
const startFetching = async () => {
//
isFetching.value = true;
fetchStatus.value = 'fetching';
let publishStartTime = ''; //
let publishEndTime = ''; //
if (selectedDateRange.value && selectedDateRange.value.length === 2) {
publishStartTime = selectedDateRange.value[0] || '';
publishEndTime = selectedDateRange.value[1] || '';
}
try {
const res = await props.runSpider(
{
@ -71,12 +83,14 @@ const startFetching = async () => {
userId: '1562',
conversationId: '',
files: [],
inputs: {},
inputs: {
publishStartTime,
publishEndTime,
},
},
);
//
fetchResult.value = res.data.outputs.result;
downloadDisable.value = false;
} catch (error) {
console.error(error);
}
@ -110,7 +124,6 @@ const downloadFile = () => {
};
const isFetching = ref(false);
const downloadDisable = ref(true);
const fetchResult = ref('');
const fetchStatus = ref('');
</script>
@ -125,10 +138,19 @@ const fetchStatus = ref('');
{{ item ? item.name : '请选择右侧爬虫' }}
</CardContent>
<CardFooter class="flex justify-end">
<RangePicker
class="mx-2"
v-model:value="selectedDateRange"
format="YYYY-MM-DD"
/>
<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

@ -4,10 +4,12 @@ import { ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import VueOfficeDocx from '@vue-office/docx';
import { message } from 'ant-design-vue';
import '@vue-office/docx/lib/index.css';
// const url = ref('');
const isLoading = ref(false); //
const docx = ref<any>(`/docx/027c6b7c-fea6-4964-839b-27857c4d3181.docx`);
const pptStyle = ref({
height: 'calc(100vh - 100px)',
@ -15,10 +17,12 @@ const pptStyle = ref({
margin: 'auto',
});
const renderedHandler = () => {
console.warn('渲染完成');
isLoading.value = false; //
message.success('渲染完成');
};
const errorHandler = (err) => {
console.error(`渲染失败:${err}`);
const errorHandler = () => {
isLoading.value = false; //
message.error('渲染失败');
};
const [Drawer, drawerApi] = useVbenDrawer({
onCancel() {
@ -31,6 +35,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
if (isOpen) {
const data = drawerApi.getData<Record<string, any>>();
if (data) {
isLoading.value = false; //
docx.value = `/docx/${data}`; // docx
}
// url.value = drawerApi.getData<Record<string, any>>();
@ -40,6 +45,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
</script>
<template>
<Drawer class="w-[880px]" title="文档预览" :footer="false">
<div v-if="isLoading" class="loading-overlay">正在加载文档请稍候...</div>
<VueOfficeDocx
:src="docx"
:style="pptStyle"

View File

@ -12,8 +12,9 @@ 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, Typography } from 'ant-design-vue';
import { Attachments, BubbleList, Sender } from 'ant-design-x-vue';
import markdownit from 'markdown-it';
import WordPreview from './word-preview.vue';
@ -62,6 +63,24 @@ const props = withDefaults(defineProps<Props>(), {
}),
});
// markdown
const md = markdownit({ html: true, breaks: true });
const renderMarkdown: BubbleListProps['roles'][string]['messageRender'] = (
content,
) => {
return h(Typography, [
h('div', {
innerHTML: content,
style: {
padding: '8px',
lineHeight: '1.6',
whiteSpace: 'break-spaces',
},
}),
]);
};
const [PreviewDrawer, previewDrawerApi] = useVbenDrawer({
//
connectedComponent: WordPreview,
@ -75,16 +94,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({
@ -185,7 +195,7 @@ const startFetching = async () => {
resultItems.value.push({
key: resultItems.value.length + 1,
role: 'ai',
content: res.answer,
content: res.answer.trim(),
});
}
@ -248,6 +258,17 @@ const roles: BubbleListProps['roles'] = {
},
},
avatar: { icon: h(UserOutlined), style: { background: '#fde3cf' } },
messageRender: (content) =>
h(Typography, [
h('div', {
innerHTML: md.render(content),
style: {
padding: '8px',
lineHeight: '1.6',
whiteSpace: 'break-spaces',
},
}),
]),
},
file: {
placement: 'start',
@ -277,33 +298,56 @@ interface ResultItem {
footer?: any;
}
const insertTextToInput = (text: string) => {
value.value = text + value.value;
};
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>
<Card class="w-full">
<Sender
v-model:value="value"
:loading="isFetching"
:auto-size="{ minRows: 6, maxRows: 12 }"
@submit="startFetching"
<div>
<PreviewDrawer />
<a-space class="w-full" direction="vertical">
<a-input v-model:value="projectName" required placeholder="项目名称" />
</a-space>
<div style="height: calc(67vh - 180px); margin: 20px; overflow-y: auto">
<BubbleList
:roles="roles"
:typing="true"
:items="resultItems"
:message-render="renderMarkdown"
/>
</Card>
</div>
<Flex wrap="wrap" :gap="12">
<Button @click="insertTextToInput('生成项目摘要')">生成项目摘要</Button>
<Button @click="insertTextToInput('生成目前存在问题')">
生成目前存在问题
</Button>
<Button @click="insertTextToInput('生成项目采用的技术原理')">
生成项目采用的技术原理
</Button>
<Button @click="insertTextToInput('生成能解决的主要问题')">
生成能解决的主要问题
</Button>
<Button @click="insertTextToInput('生成技术关键点和创新点')">
生成技术关键点和创新点
</Button>
<Button @click="insertTextToInput('生成应用前景')">生成应用前景</Button>
<Button @click="insertTextToInput('生成项目实施及效益预测')">
生成项目实施及效益预测
</Button>
<Button @click="insertTextToInput('生成预期成果')">生成预期成果</Button>
<Button @click="insertTextToInput('生成导出doc')">生成导出doc</Button>
<Card class="w-full">
<Sender
v-model:value="value"
:loading="isFetching"
:disabled="isFetching"
:auto-size="{ minRows: 6, maxRows: 12 }"
@submit="startFetching"
/>
</Card>
</Flex>
</div>
</template>

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,7 @@ 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 RiDept = createIconifyIcon('ri:door-open-line');
export const EosRole = createIconifyIcon('eos-icons:role-binding-outlined');
export const IconSystem = createIconifyIcon('icon-park-twotone:system');
// 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,9 @@ 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
markdown-it:
specifier: ^14.1.0
version: 14.1.0
qrcode:
specifier: 'catalog:'
version: 1.5.4
@ -7056,6 +7056,9 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
lint-staged@15.5.1:
resolution: {integrity: sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==}
engines: {node: '>=18.12.0'}
@ -7223,6 +7226,10 @@ packages:
resolution: {integrity: sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ==}
engines: {node: '>= 10'}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@ -7242,6 +7249,9 @@ packages:
mdn-data@2.21.0:
resolution: {integrity: sha512-+ZKPQezM5vYJIkCxaC+4DTnRrVZR1CgsKLu5zsQERQx6Tea8Y+wMx5A24rq8A8NepCeatIQufVAekKNgiBMsGQ==}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
meow@12.1.1:
resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==}
engines: {node: '>=16.10'}
@ -8492,6 +8502,10 @@ packages:
pump@3.0.2:
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@ -9523,6 +9537,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
@ -16279,6 +16296,10 @@ snapshots:
lines-and-columns@1.2.4: {}
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
lint-staged@15.5.1:
dependencies:
chalk: 5.4.1
@ -16483,6 +16504,15 @@ snapshots:
- bluebird
- supports-color
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
math-intrinsics@1.1.0: {}
mathml-tag-names@2.1.3: {}
@ -16495,6 +16525,8 @@ snapshots:
mdn-data@2.21.0: {}
mdurl@2.0.0: {}
meow@12.1.1: {}
meow@13.2.0: {}
@ -17756,6 +17788,8 @@ snapshots:
end-of-stream: 1.4.4
once: 1.4.0
punycode.js@2.3.1: {}
punycode@2.3.1: {}
pupa@3.1.0:
@ -18919,6 +18953,8 @@ snapshots:
typescript@5.8.3: {}
uc.micro@2.1.0: {}
ufo@1.6.1: {}
ultrahtml@1.6.0: {}