From 1fba0aafb261f3e5b784851fd707ca4772a25f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cls=5F=E5=AE=81=E6=B3=A2=E6=9C=AC=E6=9C=BA?= <908705107@qq.com> Date: Thu, 11 Jun 2026 19:39:38 +0800 Subject: [PATCH] feat: restore lightweight admin modules --- web/apps/web-ele/package.json | 2 + web/apps/web-ele/src/adapter/vxe-table.ts | 258 ++++ .../src/api/online-dev/page-manager.ts | 238 +++ .../dashboard-design/DashboardRenderer.vue | 142 ++ .../components/WidgetRenderer.vue | 313 ++++ .../components/widgets/AnnouncementList.vue | 298 ++++ .../components/widgets/ApprovalCenter.vue | 118 ++ .../components/widgets/CalendarWidget.vue | 71 + .../components/widgets/ClockWidget.vue | 125 ++ .../components/widgets/CountdownWidget.vue | 155 ++ .../components/widgets/MyApps.vue | 109 ++ .../components/widgets/NoticeList.vue | 269 ++++ .../components/widgets/QuickLinks.vue | 120 ++ .../components/widgets/ServerMonitor.vue | 294 ++++ .../components/widgets/WeatherWidget.vue | 364 +++++ .../components/widgets/WelcomeCard.vue | 115 ++ .../src/components/dashboard-design/index.ts | 12 + .../dashboard-design/utils/dataFetcher.ts | 314 ++++ web/apps/web-ele/src/locales/index.ts | 9 + .../web-ele/src/locales/langs/zh-CN/dict.json | 22 + .../src/locales/langs/zh-CN/menu-title.json | 8 + .../locales/langs/zh-CN/system-config.json | 46 + .../src/locales/langs/zh-CN/ui-config.json | 95 ++ web/apps/web-ele/src/router/access.ts | 46 +- web/apps/web-ele/src/router/light-menu.ts | 54 + .../web-ele/src/views/_core/about/index.vue | 25 + .../account-settings/api-token-management.vue | 330 +++++ .../src/views/_core/account-settings/data.ts | 165 +++ .../_core/account-settings/device-item.vue | 137 ++ .../account-settings/device-management.vue | 229 +++ .../views/_core/account-settings/index.vue | 184 +++ .../modules/password-form.vue | 168 +++ .../account-settings/modules/profile-form.vue | 293 ++++ web/apps/web-ele/src/views/_core/dept/data.ts | 74 + .../web-ele/src/views/_core/dept/index.vue | 159 ++ .../_core/dept/modules/dept-form-modal.vue | 209 +++ .../views/_core/dept/modules/dept-tree.vue | 501 +++++++ .../views/_core/dept/modules/user-form.vue | 72 + web/apps/web-ele/src/views/_core/dict/data.ts | 194 +++ .../web-ele/src/views/_core/dict/index.vue | 39 + .../_core/dict/modules/dict-form-modal.vue | 99 ++ .../dict/modules/dict-item-form-modal.vue | 92 ++ .../_core/dict/modules/dict-item-list.vue | 683 +++++++++ .../views/_core/dict/modules/dict-list.vue | 297 ++++ .../components/CreateFolderDialog.vue | 93 ++ .../file-manager/components/FileList.vue | 547 +++++++ .../file-manager/components/FileToolbar.vue | 296 ++++ .../file-manager/components/FolderTree.vue | 121 ++ .../file-manager/components/RenameDialog.vue | 99 ++ .../composables/useFileManager.ts | 129 ++ .../src/views/_core/file-manager/index.vue | 21 + .../web-ele/src/views/_core/login-log/data.ts | 242 ++++ .../src/views/_core/login-log/index.vue | 191 +++ .../_core/login-log/modules/detail-drawer.vue | 180 +++ .../_core/menu/modules/menu-form-modal.vue | 5 +- .../views/_core/menu/modules/menu-form.vue | 3 +- .../views/_core/menu/modules/menu-tree.vue | 9 +- .../src/views/_core/page-render/index.vue | 37 +- .../modules/permission-left-panel.vue | 5 +- web/apps/web-ele/src/views/_core/post/data.ts | 187 +++ .../web-ele/src/views/_core/post/index.vue | 160 +++ .../_core/post/modules/post-form-modal.vue | 84 ++ .../views/_core/post/modules/post-list.vue | 291 ++++ .../views/_core/post/modules/user-form.vue | 76 + .../role/modules/permission-assign-panel.vue | 34 +- .../src/views/_core/system-config/index.vue | 294 ++++ .../system-config/modules/config-form.vue | 206 +++ .../modules/model-config-form.vue | 1279 +++++++++++++++++ .../src/views/_core/ui-config/index.vue | 219 +++ .../ui-config/modules/app-settings-form.vue | 619 ++++++++ .../ui-config/modules/login-config-form.vue | 331 +++++ .../ui-config/modules/ui-preferences-form.vue | 664 +++++++++ web/pnpm-lock.yaml | 54 +- 73 files changed, 13943 insertions(+), 80 deletions(-) create mode 100644 web/apps/web-ele/src/adapter/vxe-table.ts create mode 100644 web/apps/web-ele/src/api/online-dev/page-manager.ts create mode 100644 web/apps/web-ele/src/components/dashboard-design/DashboardRenderer.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/components/WidgetRenderer.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/components/widgets/AnnouncementList.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/components/widgets/ApprovalCenter.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/components/widgets/CalendarWidget.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/components/widgets/ClockWidget.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/components/widgets/CountdownWidget.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/components/widgets/MyApps.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/components/widgets/NoticeList.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/components/widgets/QuickLinks.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/components/widgets/ServerMonitor.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/components/widgets/WeatherWidget.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/components/widgets/WelcomeCard.vue create mode 100644 web/apps/web-ele/src/components/dashboard-design/index.ts create mode 100644 web/apps/web-ele/src/components/dashboard-design/utils/dataFetcher.ts create mode 100644 web/apps/web-ele/src/locales/langs/zh-CN/dict.json create mode 100644 web/apps/web-ele/src/locales/langs/zh-CN/system-config.json create mode 100644 web/apps/web-ele/src/locales/langs/zh-CN/ui-config.json create mode 100644 web/apps/web-ele/src/router/light-menu.ts create mode 100644 web/apps/web-ele/src/views/_core/about/index.vue create mode 100644 web/apps/web-ele/src/views/_core/account-settings/api-token-management.vue create mode 100644 web/apps/web-ele/src/views/_core/account-settings/data.ts create mode 100644 web/apps/web-ele/src/views/_core/account-settings/device-item.vue create mode 100644 web/apps/web-ele/src/views/_core/account-settings/device-management.vue create mode 100644 web/apps/web-ele/src/views/_core/account-settings/index.vue create mode 100644 web/apps/web-ele/src/views/_core/account-settings/modules/password-form.vue create mode 100644 web/apps/web-ele/src/views/_core/account-settings/modules/profile-form.vue create mode 100644 web/apps/web-ele/src/views/_core/dept/data.ts create mode 100644 web/apps/web-ele/src/views/_core/dept/index.vue create mode 100644 web/apps/web-ele/src/views/_core/dept/modules/dept-form-modal.vue create mode 100644 web/apps/web-ele/src/views/_core/dept/modules/dept-tree.vue create mode 100644 web/apps/web-ele/src/views/_core/dept/modules/user-form.vue create mode 100644 web/apps/web-ele/src/views/_core/dict/data.ts create mode 100644 web/apps/web-ele/src/views/_core/dict/index.vue create mode 100644 web/apps/web-ele/src/views/_core/dict/modules/dict-form-modal.vue create mode 100644 web/apps/web-ele/src/views/_core/dict/modules/dict-item-form-modal.vue create mode 100644 web/apps/web-ele/src/views/_core/dict/modules/dict-item-list.vue create mode 100644 web/apps/web-ele/src/views/_core/dict/modules/dict-list.vue create mode 100644 web/apps/web-ele/src/views/_core/file-manager/components/CreateFolderDialog.vue create mode 100644 web/apps/web-ele/src/views/_core/file-manager/components/FileList.vue create mode 100644 web/apps/web-ele/src/views/_core/file-manager/components/FileToolbar.vue create mode 100644 web/apps/web-ele/src/views/_core/file-manager/components/FolderTree.vue create mode 100644 web/apps/web-ele/src/views/_core/file-manager/components/RenameDialog.vue create mode 100644 web/apps/web-ele/src/views/_core/file-manager/composables/useFileManager.ts create mode 100644 web/apps/web-ele/src/views/_core/file-manager/index.vue create mode 100644 web/apps/web-ele/src/views/_core/login-log/data.ts create mode 100644 web/apps/web-ele/src/views/_core/login-log/index.vue create mode 100644 web/apps/web-ele/src/views/_core/login-log/modules/detail-drawer.vue create mode 100644 web/apps/web-ele/src/views/_core/post/data.ts create mode 100644 web/apps/web-ele/src/views/_core/post/index.vue create mode 100644 web/apps/web-ele/src/views/_core/post/modules/post-form-modal.vue create mode 100644 web/apps/web-ele/src/views/_core/post/modules/post-list.vue create mode 100644 web/apps/web-ele/src/views/_core/post/modules/user-form.vue create mode 100644 web/apps/web-ele/src/views/_core/system-config/index.vue create mode 100644 web/apps/web-ele/src/views/_core/system-config/modules/config-form.vue create mode 100644 web/apps/web-ele/src/views/_core/system-config/modules/model-config-form.vue create mode 100644 web/apps/web-ele/src/views/_core/ui-config/index.vue create mode 100644 web/apps/web-ele/src/views/_core/ui-config/modules/app-settings-form.vue create mode 100644 web/apps/web-ele/src/views/_core/ui-config/modules/login-config-form.vue create mode 100644 web/apps/web-ele/src/views/_core/ui-config/modules/ui-preferences-form.vue diff --git a/web/apps/web-ele/package.json b/web/apps/web-ele/package.json index 5b2b65b..6111541 100644 --- a/web/apps/web-ele/package.json +++ b/web/apps/web-ele/package.json @@ -37,6 +37,7 @@ "@vben/layouts": "workspace:*", "@vben/locales": "workspace:*", "@vben/preferences": "workspace:*", + "@vben/plugins": "workspace:*", "@vben/request": "workspace:*", "@vben/stores": "workspace:*", "@vben/styles": "workspace:*", @@ -50,6 +51,7 @@ "cron-parser": "^4.9.0", "dayjs": "catalog:", "element-plus": "catalog:", + "grid-layout-plus": "^1.1.1", "pinia": "catalog:", "vue": "catalog:", "vue-router": "catalog:" diff --git a/web/apps/web-ele/src/adapter/vxe-table.ts b/web/apps/web-ele/src/adapter/vxe-table.ts new file mode 100644 index 0000000..330e025 --- /dev/null +++ b/web/apps/web-ele/src/adapter/vxe-table.ts @@ -0,0 +1,258 @@ +import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; +import type { Recordable } from '@vben/types'; + +import { h } from 'vue'; + +import { IconifyIcon } from '@vben/icons'; +import { $t, $te } from '@vben/locales'; +import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; +import { get, isFunction, isString } from '@vben/utils'; + +import { + ElButton, + ElImage, + ElPopconfirm, + ElTag, + ElTooltip, +} from 'element-plus'; + +import { useVbenForm } from './form'; + +setupVbenVxeTable({ + configVxeTable: (vxeUI) => { + vxeUI.setConfig({ + grid: { + align: 'center', + border: false, + columnConfig: { + resizable: true, + }, + formConfig: { + enabled: false, + }, + minHeight: 180, + proxyConfig: { + autoLoad: true, + response: { + list: '', + result: 'items', + total: 'total', + }, + showActionMsg: true, + showResponseMsg: false, + }, + round: true, + showOverflow: true, + size: 'medium', + } as VxeTableGridOptions, + }); + + vxeUI.renderer.add('CellImage', { + renderTableDefault(_renderOpts, params) { + const { column, row } = params; + const src = row[column.field]; + return h(ElImage, { previewSrcList: [src], src }); + }, + }); + + vxeUI.renderer.add('CellLink', { + renderTableDefault(renderOpts) { + const { props } = renderOpts; + return h( + ElButton, + { link: true, size: 'small' }, + { default: () => props?.text }, + ); + }, + }); + + vxeUI.renderer.add('CellTag', { + renderTableDefault({ options, props }, { column, row }) { + const value = get(row, column.field); + const tagOptions = options ?? [ + { label: $t('common.enabled'), type: 'success', value: true }, + { label: $t('common.disabled'), type: 'danger', value: false }, + ]; + const tagItem = tagOptions.find((item) => item.value === value); + return h( + ElTag, + { + type: tagItem?.type ?? 'info', + ...props, + }, + { default: () => tagItem?.label ?? value }, + ); + }, + }); + + vxeUI.renderer.add('CellOperation', { + renderTableDefault({ attrs, options, props }, { column, row }) { + const defaultProps = { link: true, size: 'small', ...props }; + const align = + column.align === 'center' + ? 'center' + : column.align === 'left' + ? 'start' + : 'end'; + const presets: Recordable> = { + delete: { + icon: 'ep:delete', + text: $t('common.delete'), + type: 'danger', + }, + edit: { + icon: 'ep:edit', + text: $t('common.edit'), + type: 'primary', + }, + }; + const operations: Array> = ( + 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, + type: 'primary', + ...defaultProps, + }; + } + + const buttonConfig = { + ...defaultProps, + ...presets[opt.code], + ...opt, + }; + if (!buttonConfig.type && !presets[opt.code]) { + buttonConfig.type = 'primary'; + } + return buttonConfig; + }) + .map((opt) => { + const optBtn: Recordable = {}; + 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, listen = true) { + const { code, icon, text, ...btnProps } = opt; + const buttonType = + btnProps.type === 'danger' + ? 'danger' + : btnProps.type === 'primary' + ? 'primary' + : 'default'; + + const button = h( + ElButton, + { + ...btnProps, + circle: !!icon, + link: true, + onClick: listen + ? () => + attrs?.onClick?.({ + code, + row, + }) + : undefined, + size: 'small', + type: buttonType, + }, + { + default: () => + icon ? h(IconifyIcon, { class: 'size-4', icon }) : text, + }, + ); + + if (!icon) return button; + + return h( + ElTooltip, + { + content: text, + placement: 'top', + }, + { + default: () => button, + }, + ); + } + + function renderConfirm(opt: Recordable) { + const { icon, text } = opt; + + const button = h( + ElButton, + { + circle: !!icon, + link: true, + size: 'small', + title: icon ? text : undefined, + type: 'danger', + }, + { + default: () => + icon ? h(IconifyIcon, { class: 'size-4', icon }) : text, + }, + ); + + return h( + ElPopconfirm, + { + cancelButtonText: $t('common.cancel'), + confirmButtonText: $t('common.confirm'), + confirmButtonType: 'danger', + onConfirm: () => { + attrs?.onClick?.({ + code: opt.code, + row, + }); + }, + title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']), + }, + { + default: () => + $t('ui.actionMessage.deleteConfirm', [ + row[attrs?.nameField || 'name'], + ]), + reference: () => button, + }, + ); + } + + const btns = operations.map((opt) => + opt.code === 'delete' ? renderConfirm(opt) : renderBtn(opt), + ); + return h( + 'div', + { + class: 'flex table-operations', + style: { justifyContent: align }, + }, + btns, + ); + }, + }); + }, + useVbenForm, +}); + +export type OnActionClickParams> = { + code: string; + row: T; +}; + +export type OnActionClickFn> = ( + params: OnActionClickParams, +) => void; + +export { useVbenVxeGrid }; + +export type * from '@vben/plugins/vxe-table'; diff --git a/web/apps/web-ele/src/api/online-dev/page-manager.ts b/web/apps/web-ele/src/api/online-dev/page-manager.ts new file mode 100644 index 0000000..fbde211 --- /dev/null +++ b/web/apps/web-ele/src/api/online-dev/page-manager.ts @@ -0,0 +1,238 @@ +import { requestClient } from '#/api/request'; + +/** + * 页面管理 API + * 页面元数据的 CRUD、发布、复制、导入导出 + */ + +// ============ 类型定义 ============ + +/** 页面元数据 */ +export interface PageMeta { + id: string; + application_id?: string; + name: string; + code: string; + category: string; + description: string; + status: string; + version: number; + page_config: Record; + sort: number; + sys_create_datetime: string; + sys_update_datetime: string; +} + +/** 页面列表项 */ +export interface PageMetaListItem { + id: string; + application_id?: string; + application_name?: string; + application_code?: string; + name: string; + code: string; + category: string; + description: string; + status: string; + version: number; + sort: number; + sys_create_datetime: string; + sys_update_datetime: string; +} + +/** 创建页面请求 */ +export interface PageMetaCreateInput { + application_id?: string; + name: string; + code: string; + category?: string; + description?: string; + sort?: number; + page_config?: Record; +} + +/** 更新页面请求 */ +export interface PageMetaUpdateInput { + name?: string; + category?: string; + description?: string; + sort?: number; + page_config?: Record; +} + +/** 导入页面请求 */ +export interface PageImportInput { + application_id?: string; + name: string; + code: string; + category?: string; + description?: string; + page_config?: Record; +} + +/** 导入预检查请求 */ +export interface PageImportCheckInput { + code: string; +} + +/** 导入预检查结果 */ +export interface PageImportCheckResult { + code_exists: boolean; + can_import: boolean; +} + +/** 列表查询参数 */ +export interface PageListParams { + page?: number; + pageSize?: number; + applicationId?: string; + name?: string; + code?: string; + category?: string; + status?: string; +} + +/** 发布配置 */ +export interface PagePublishInput { + /** 菜单名称 */ + menu_name: string; + /** 上级菜单ID */ + menu_parent_id?: string; + /** 菜单图标 */ + menu_icon?: string; + /** 菜单排序 */ + menu_order?: number; +} + +/** 分页响应 */ +interface PagePaginatedResponse { + items: T[]; + total: number; + page: number; + pageSize: number; +} + +// ============ 页面元数据 API ============ + +/** + * 获取页面列表(分页) + */ +export async function getPageListApi(params?: PageListParams) { + return requestClient.get>( + '/api/online_dev/page/list', + { params }, + ); +} + +/** + * 获取分类列表 + */ +export async function getPageCategoriesApi() { + return requestClient.get('/api/online_dev/page/categories'); +} + +/** + * 获取页面详情 + */ +export async function getPageDetailApi(pageId: string) { + return requestClient.get(`/api/online_dev/page/${pageId}`); +} + +/** + * 根据编码获取页面 + */ +export async function getPageByCodeApi(code: string) { + return requestClient.get(`/api/online_dev/page/code/${code}`); +} + +/** + * 创建页面 + */ +export async function createPageApi(data: PageMetaCreateInput) { + return requestClient.post('/api/online_dev/page', data); +} + +/** + * 更新页面 + */ +export async function updatePageApi(pageId: string, data: PageMetaUpdateInput) { + return requestClient.put(`/api/online_dev/page/${pageId}`, data); +} + +/** + * 删除页面 + */ +export async function deletePageApi(pageId: string) { + return requestClient.delete(`/api/online_dev/page/${pageId}`); +} + +/** + * 批量删除页面 + */ +export async function batchDeletePageApi(ids: string[]) { + return requestClient.delete<{ count: number }>('/api/online_dev/page/batch', { + params: { ids }, + }); +} + +/** + * 发布页面 + */ +export async function publishPageApi(pageId: string, data: PagePublishInput) { + return requestClient.post( + `/api/online_dev/page/${pageId}/publish`, + data, + ); +} + +/** + * 取消发布页面 + */ +export async function unpublishPageApi(pageId: string) { + return requestClient.post( + `/api/online_dev/page/${pageId}/unpublish`, + ); +} + +/** + * 复制页面 + */ +export async function copyPageApi( + pageId: string, + newCode: string, + newName?: string, +) { + return requestClient.post( + `/api/online_dev/page/${pageId}/copy`, + null, + { + params: { new_code: newCode, new_name: newName }, + }, + ); +} + +/** + * 导出页面配置(返回 JSON 文件) + */ +export async function exportPageConfigApi(pageId: string) { + return requestClient.get(`/api/online_dev/page/${pageId}/export`, { + responseType: 'blob', + }); +} + +/** + * 导入预检查 + */ +export async function checkImportPageApi(data: PageImportCheckInput) { + return requestClient.post( + '/api/online_dev/page/import/check', + data, + ); +} + +/** + * 导入页面配置 + */ +export async function importPageConfigApi(data: PageImportInput) { + return requestClient.post('/api/online_dev/page/import', data); +} diff --git a/web/apps/web-ele/src/components/dashboard-design/DashboardRenderer.vue b/web/apps/web-ele/src/components/dashboard-design/DashboardRenderer.vue new file mode 100644 index 0000000..debc5b0 --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/DashboardRenderer.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/components/WidgetRenderer.vue b/web/apps/web-ele/src/components/dashboard-design/components/WidgetRenderer.vue new file mode 100644 index 0000000..9beb5f1 --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/components/WidgetRenderer.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/components/widgets/AnnouncementList.vue b/web/apps/web-ele/src/components/dashboard-design/components/widgets/AnnouncementList.vue new file mode 100644 index 0000000..cbe48df --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/components/widgets/AnnouncementList.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/components/widgets/ApprovalCenter.vue b/web/apps/web-ele/src/components/dashboard-design/components/widgets/ApprovalCenter.vue new file mode 100644 index 0000000..65c5c48 --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/components/widgets/ApprovalCenter.vue @@ -0,0 +1,118 @@ + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/components/widgets/CalendarWidget.vue b/web/apps/web-ele/src/components/dashboard-design/components/widgets/CalendarWidget.vue new file mode 100644 index 0000000..23dd43f --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/components/widgets/CalendarWidget.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/components/widgets/ClockWidget.vue b/web/apps/web-ele/src/components/dashboard-design/components/widgets/ClockWidget.vue new file mode 100644 index 0000000..9b6f635 --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/components/widgets/ClockWidget.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/components/widgets/CountdownWidget.vue b/web/apps/web-ele/src/components/dashboard-design/components/widgets/CountdownWidget.vue new file mode 100644 index 0000000..4e87591 --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/components/widgets/CountdownWidget.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/components/widgets/MyApps.vue b/web/apps/web-ele/src/components/dashboard-design/components/widgets/MyApps.vue new file mode 100644 index 0000000..65a8780 --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/components/widgets/MyApps.vue @@ -0,0 +1,109 @@ + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/components/widgets/NoticeList.vue b/web/apps/web-ele/src/components/dashboard-design/components/widgets/NoticeList.vue new file mode 100644 index 0000000..5555849 --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/components/widgets/NoticeList.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/components/widgets/QuickLinks.vue b/web/apps/web-ele/src/components/dashboard-design/components/widgets/QuickLinks.vue new file mode 100644 index 0000000..ec167b4 --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/components/widgets/QuickLinks.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/components/widgets/ServerMonitor.vue b/web/apps/web-ele/src/components/dashboard-design/components/widgets/ServerMonitor.vue new file mode 100644 index 0000000..cdcaaa6 --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/components/widgets/ServerMonitor.vue @@ -0,0 +1,294 @@ + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/components/widgets/WeatherWidget.vue b/web/apps/web-ele/src/components/dashboard-design/components/widgets/WeatherWidget.vue new file mode 100644 index 0000000..dd2f77b --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/components/widgets/WeatherWidget.vue @@ -0,0 +1,364 @@ + + + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/components/widgets/WelcomeCard.vue b/web/apps/web-ele/src/components/dashboard-design/components/widgets/WelcomeCard.vue new file mode 100644 index 0000000..966b5e1 --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/components/widgets/WelcomeCard.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/web/apps/web-ele/src/components/dashboard-design/index.ts b/web/apps/web-ele/src/components/dashboard-design/index.ts new file mode 100644 index 0000000..c54b902 --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/index.ts @@ -0,0 +1,12 @@ +export { default as DashboardRenderer } from './DashboardRenderer.vue'; + +export type { + DashboardConfig, + DashboardWidget, + DataSourceConfig, + DataSourceType, + WidgetMaterial, + WidgetType, +} from './store/dashboardDesignStore'; + +export { createRefreshTimer, fetchWidgetData } from './utils/dataFetcher'; diff --git a/web/apps/web-ele/src/components/dashboard-design/utils/dataFetcher.ts b/web/apps/web-ele/src/components/dashboard-design/utils/dataFetcher.ts new file mode 100644 index 0000000..c097dd2 --- /dev/null +++ b/web/apps/web-ele/src/components/dashboard-design/utils/dataFetcher.ts @@ -0,0 +1,314 @@ +import type { + DataSourceConfig, + FieldMapping, +} from '../store/dashboardDesignStore'; + +import { $t } from '@vben/locales'; + +import { requestClient } from '#/api/request'; + +/** + * 根据路径获取对象中的值 + * @param obj 对象 + * @param path 路径,如 'data.list' 或 'data.items[0].name' + */ +export function getValueByPath(obj: any, path: string): any { + if (!obj || !path) return obj; + + const keys = path.replaceAll(/\[(\d+)\]/g, '.$1').split('.'); + let result = obj; + + for (const key of keys) { + if (result === null || result === undefined) return undefined; + result = result[key]; + } + + return result; +} + +/** + * 根据路径设置对象中的值 + */ +export function setValueByPath(obj: any, path: string, value: any): void { + if (!obj || !path) return; + + const keys = path.split('.'); + let current = obj; + + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]!; + if (current[key] === undefined) { + current[key] = {}; + } + current = current[key]; + } + + current[keys[keys.length - 1]!] = value; +} + +/** + * 应用字段映射 + */ +export function applyFieldMappings( + data: any, + mappings: FieldMapping[] | undefined, + targetProps: Record, +): Record { + if (!mappings || mappings.length === 0) { + return { ...targetProps, ...data }; + } + + const result = { ...targetProps }; + + for (const mapping of mappings) { + const value = getValueByPath(data, mapping.source); + if (value !== undefined) { + setValueByPath(result, mapping.target, value); + } + } + + return result; +} + +/** + * 数据获取工具 + */ +export async function fetchWidgetData( + dataSource: DataSourceConfig | undefined, + defaultProps: Record, + params?: Record, +): Promise<{ data: any; props: Record }> { + if (!dataSource || dataSource.type === 'static') { + return { data: null, props: defaultProps }; + } + + // 通用数据源类型 + if (dataSource.type === 'dataSource' && dataSource.dataSourceCode) { + try { + const response = await requestClient.get( + `/api/core/data-source/execute/${dataSource.dataSourceCode}`, + { params: params && Object.keys(params).length > 0 ? params : undefined }, + ); + + // 后端返回格式是 {data: ...},需要提取 data 字段 + const rawData = response?.data ?? response; + + // 数据源返回的数据可能是数组或对象(图表数据源返回 {xAxisData, seriesData} 格式) + const extractedData = dataSource.dataPath + ? getValueByPath(rawData, dataSource.dataPath) + : rawData; + + // 检查是否是图表数据格式(包含 xAxisData 或 seriesData 或 indicator 或 value) + if ( + extractedData && + typeof extractedData === 'object' && + !Array.isArray(extractedData) && + ('xAxisData' in extractedData || + 'seriesData' in extractedData || + 'indicator' in extractedData || + 'yAxisData' in extractedData || + ('value' in extractedData && 'max' in extractedData)) + ) { + // 图表数据格式,直接合并到 props + const mappedProps = { ...defaultProps, ...extractedData }; + // 如果有额外的字段映射,也应用 + if (dataSource.fieldMappings && dataSource.fieldMappings.length > 0) { + return { + data: extractedData, + props: applyFieldMappings( + extractedData, + dataSource.fieldMappings, + mappedProps, + ), + }; + } + return { data: extractedData, props: mappedProps }; + } + + // 普通数据,应用字段映射 + const mappedProps = applyFieldMappings( + extractedData, + dataSource.fieldMappings, + defaultProps, + ); + + return { data: extractedData, props: mappedProps }; + } catch (error) { + console.error('Failed to fetch data source:', error); + return { data: null, props: defaultProps }; + } + } + + // API 类型 + if (dataSource.type === 'api' && dataSource.apiUrl) { + try { + const method = dataSource.apiMethod || 'GET'; + const params = dataSource.apiParams || {}; + const body = dataSource.apiBody || {}; + const headers = dataSource.apiHeaders || {}; + + let response: any; + + response = await (method === 'GET' + ? requestClient.get(dataSource.apiUrl, { + params, + headers, + }) + : requestClient.post(dataSource.apiUrl, body, { + params, + headers, + })); + + // 根据 dataPath 提取数据 + const extractedData = dataSource.dataPath + ? getValueByPath(response, dataSource.dataPath) + : response; + + // 应用字段映射 + const mappedProps = applyFieldMappings( + extractedData, + dataSource.fieldMappings, + defaultProps, + ); + + return { data: extractedData, props: mappedProps }; + } catch (error) { + console.error('Failed to fetch widget data:', error); + return { data: null, props: defaultProps }; + } + } + + return { data: null, props: defaultProps }; +} + +/** + * 创建自动刷新定时器 + */ +export function createRefreshTimer( + dataSource: DataSourceConfig | undefined, + callback: () => void, +): (() => void) | null { + if ( + !dataSource?.refreshEnabled || + !dataSource.refreshInterval || + dataSource.refreshInterval <= 0 + ) { + return null; + } + + const timer = setInterval(callback, dataSource.refreshInterval * 1000); + + return () => clearInterval(timer); +} + +/** + * 获取组件支持的字段映射目标 + */ +export function getWidgetFieldTargets( + widgetType: string, +): { key: string; label: string }[] { + const commonFields = [ + { key: 'title', label: $t('dashboard-design.attribute.fieldLabels.title') }, + ]; + + const fieldMap: Record = { + 'stat-card': [ + ...commonFields, + { + key: 'value', + label: $t('dashboard-design.attribute.fieldLabels.value'), + }, + { + key: 'trend', + label: $t('dashboard-design.attribute.fieldLabels.trend'), + }, + { + key: 'trendLabel', + label: $t('dashboard-design.attribute.fieldLabels.trendLabel'), + }, + { + key: 'prefix', + label: $t('dashboard-design.attribute.fieldLabels.prefix'), + }, + { + key: 'suffix', + label: $t('dashboard-design.attribute.fieldLabels.suffix'), + }, + ], + 'progress-card': [ + ...commonFields, + { + key: 'percentage', + label: $t('dashboard-design.attribute.fieldLabels.percentage'), + }, + ], + 'chart-line': [ + ...commonFields, + { + key: 'xAxisData', + label: $t('dashboard-design.attribute.fieldLabels.xAxisData'), + }, + { + key: 'seriesData', + label: $t('dashboard-design.attribute.fieldLabels.seriesData'), + }, + ], + 'chart-bar': [ + ...commonFields, + { + key: 'xAxisData', + label: $t('dashboard-design.attribute.fieldLabels.xAxisData'), + }, + { + key: 'seriesData', + label: $t('dashboard-design.attribute.fieldLabels.seriesData'), + }, + ], + 'chart-pie': [ + ...commonFields, + { + key: 'seriesData', + label: $t('dashboard-design.attribute.fieldLabels.seriesData'), + }, + ], + 'chart-gauge': [ + ...commonFields, + { + key: 'value', + label: $t('dashboard-design.attribute.fieldLabels.currentValue'), + }, + { key: 'min', label: $t('dashboard-design.attribute.fieldLabels.min') }, + { key: 'max', label: $t('dashboard-design.attribute.fieldLabels.max') }, + ], + 'todo-list': [ + ...commonFields, + { + key: 'items', + label: $t('dashboard-design.attribute.fieldLabels.listData'), + }, + ], + 'notice-list': [ + ...commonFields, + { + key: 'items', + label: $t('dashboard-design.attribute.fieldLabels.listData'), + }, + ], + 'ranking-list': [ + ...commonFields, + { + key: 'items', + label: $t('dashboard-design.attribute.fieldLabels.listData'), + }, + ], + 'quick-links': [ + ...commonFields, + { + key: 'links', + label: $t('dashboard-design.attribute.fieldLabels.linkData'), + }, + ], + }; + + return fieldMap[widgetType] || commonFields; +} diff --git a/web/apps/web-ele/src/locales/index.ts b/web/apps/web-ele/src/locales/index.ts index ca526aa..94592ce 100644 --- a/web/apps/web-ele/src/locales/index.ts +++ b/web/apps/web-ele/src/locales/index.ts @@ -32,11 +32,20 @@ const baseModules = import.meta.glob([ const businessModules = import.meta.glob([ './langs/zh-CN/ai-platform.json', './langs/zh-CN/announcement.json', + './langs/zh-CN/dashboard-design.json', + './langs/zh-CN/dept.json', + './langs/zh-CN/dict.json', + './langs/zh-CN/file-manager.json', + './langs/zh-CN/loginLog.json', './langs/zh-CN/menu.json', './langs/zh-CN/message.json', './langs/zh-CN/permission.json', + './langs/zh-CN/post.json', './langs/zh-CN/role.json', + './langs/zh-CN/server-monitor.json', './langs/zh-CN/system.json', + './langs/zh-CN/system-config.json', + './langs/zh-CN/ui-config.json', './langs/zh-CN/user.json', ]); diff --git a/web/apps/web-ele/src/locales/langs/zh-CN/dict.json b/web/apps/web-ele/src/locales/langs/zh-CN/dict.json new file mode 100644 index 0000000..f450b14 --- /dev/null +++ b/web/apps/web-ele/src/locales/langs/zh-CN/dict.json @@ -0,0 +1,22 @@ +{ + "name": "字典", + "title": "字典管理", + "dictName": "字典名称", + "dictCode": "字典编码", + "remark": "备注", + "remarkPlaceholder": "请输入备注", + "status": "状态", + "operation": "操作", + "edit": "编辑", + "codeFormatError": "字典编码只能包含字母、数字和下划线", + "selectDictFirst": "请先选择字典", + "noData": "暂无数据", + "itemName": "字典项", + "itemLabel": "标签", + "itemValue": "值", + "itemIcon": "图标", + "sort": "排序", + "isGlobal": "全局可见", + "globalTag": "全局", + "mainApp": "主应用" +} diff --git a/web/apps/web-ele/src/locales/langs/zh-CN/menu-title.json b/web/apps/web-ele/src/locales/langs/zh-CN/menu-title.json index 08b367f..f88538d 100644 --- a/web/apps/web-ele/src/locales/langs/zh-CN/menu-title.json +++ b/web/apps/web-ele/src/locales/langs/zh-CN/menu-title.json @@ -9,9 +9,17 @@ "codex": "Codex", "startChat": "开始聊天", "systemManagement": "系统管理", + "systemTools": "系统工具", "applicationManagement": "应用管理", "systemMonitoring": "系统监控", "userManagement": "用户管理", + "departmentManagement": "部门管理", + "positionManagement": "岗位管理", + "dictionaryManagement": "字典管理", + "fileManagement": "文件管理", + "serverMonitoring": "服务器监控", + "uiConfig": "界面配置", + "loginLog": "登录日志", "roleManagement": "角色权限", "permissionManagement": "API管理", "menuManagement": "菜单管理", diff --git a/web/apps/web-ele/src/locales/langs/zh-CN/system-config.json b/web/apps/web-ele/src/locales/langs/zh-CN/system-config.json new file mode 100644 index 0000000..cb460e5 --- /dev/null +++ b/web/apps/web-ele/src/locales/langs/zh-CN/system-config.json @@ -0,0 +1,46 @@ +{ + "title": "系统配置", + "ssoConfig": "SSO配置", + "notifyConfig": "通知配置", + "modelConfig": "模型配置", + "save": "保存", + "reset": "恢复默认", + "resetConfirm": "确定要恢复该分组为默认配置吗?这将删除数据库中的自定义配置。", + "saveSuccess": "保存成功", + "saveError": "保存失败", + "resetSuccess": "已恢复为默认配置", + "resetError": "恢复失败", + "secretTip": "敏感字段会脱敏显示,留空不会修改原值。", + "groups": { + "oauth_gitee": "Gitee", + "oauth_github": "GitHub", + "oauth_qq": "QQ", + "oauth_google": "Google", + "oauth_wechat": "微信", + "oauth_microsoft": "Microsoft", + "notify_email": "邮件通知", + "notify_sms": "短信通知", + "notify_wechat_mp": "微信公众号" + }, + "fields": { + "client_id": "Client ID", + "client_secret": "Client Secret", + "redirect_uri": "Web 回调地址", + "h5_redirect_uri": "H5 回调地址", + "app_id": "App ID", + "app_key": "App Key", + "app_secret": "App Secret", + "smtp_host": "SMTP 服务器", + "smtp_port": "SMTP 端口", + "smtp_user": "SMTP 用户名", + "smtp_password": "SMTP 密码", + "smtp_use_tls": "使用 TLS", + "smtp_from_name": "发件人名称", + "smtp_from_email": "发件人邮箱", + "provider": "服务商", + "providerAliyun": "阿里云", + "providerTencent": "腾讯云", + "todo_pc_url": "待办 PC 端跳转地址", + "todo_app_url": "待办移动端跳转地址" + } +} diff --git a/web/apps/web-ele/src/locales/langs/zh-CN/ui-config.json b/web/apps/web-ele/src/locales/langs/zh-CN/ui-config.json new file mode 100644 index 0000000..d805121 --- /dev/null +++ b/web/apps/web-ele/src/locales/langs/zh-CN/ui-config.json @@ -0,0 +1,95 @@ +{ + "title": "界面配置", + "appConfig": "应用配置", + "styleConfig": "样式配置", + "logoConfig": "Logo配置", + "save": "保存", + "saveSuccess": "保存成功", + "saveError": "保存失败", + "app": { + "title": "应用配置", + "name": "应用名称", + "namePlaceholder": "请输入应用名称", + "defaultHomePath": "默认首页路径", + "defaultHomePathPlaceholder": "请输入默认首页路径", + "locale": "默认语言", + "dynamicTitle": "动态标题", + "dynamicTitleTip": "开启后页面标题会根据当前路由变化", + "watermark": "水印", + "watermarkTip": "开启后页面会显示水印", + "watermarkContent": "水印内容", + "watermarkContentPlaceholder": "请输入水印内容", + "enablePreferences": "启用偏好设置", + "enablePreferencesTip": "开启后用户可以在界面中修改偏好设置", + "layout": "布局模式", + "layoutOptions": { + "sidebar-nav": "侧边导航", + "header-nav": "顶部导航", + "mixed-nav": "混合导航", + "header-sidebar-nav": "顶部+侧边导航" + } + }, + "theme": { + "mode": "主题模式", + "modeOptions": { + "light": "浅色", + "dark": "深色", + "auto": "跟随系统" + }, + "colorPrimary": "主题色", + "radius": "圆角大小", + "builtinType": "内置主题", + "semiDarkSidebar": "深色侧边栏", + "semiDarkHeader": "深色顶栏" + }, + "logo": { + "enable": "启用 Logo", + "source": "Logo 图片", + "sourcePlaceholder": "请输入 Logo 图片 URL 或上传", + "fit": "适应方式", + "fitOptions": { + "contain": "包含", + "cover": "覆盖", + "fill": "填充", + "none": "无", + "scale-down": "缩小" + } + }, + "copyright": { + "enable": "启用版权", + "companyName": "公司名称", + "companyNamePlaceholder": "请输入公司名称", + "companySiteLink": "公司网站", + "companySiteLinkPlaceholder": "请输入公司网站链接", + "date": "版权年份", + "datePlaceholder": "请输入版权年份", + "icp": "ICP备案号", + "icpPlaceholder": "请输入ICP备案号", + "icpLink": "ICP备案链接", + "icpLinkPlaceholder": "请输入ICP备案链接", + "policeIcp": "公安备案号", + "policeIcpPlaceholder": "请输入公安备案号", + "policeIcpLink": "公安备案链接", + "policeIcpLinkPlaceholder": "请输入公安备案链接", + "loginOnly": "仅登录页显示", + "loginOnlyTip": "开启后版权信息仅在登录页面显示" + }, + "loginConfig": { + "title": "登录配置", + "enableThirdPartyLogin": "启用第三方登录", + "enableThirdPartyLoginTip": "开启后登录页会显示第三方登录区域", + "enabledProviders": "启用的登录方式", + "enabledProvidersTip": "选择需要在登录页面显示的第三方登录方式", + "providers": { + "gitee": "Gitee", + "github": "GitHub", + "google": "Google", + "microsoft": "Microsoft", + "qq": "QQ", + "wechat": "微信", + "wecom": "企业微信", + "dingtalk": "钉钉", + "feishu": "飞书" + } + } +} diff --git a/web/apps/web-ele/src/router/access.ts b/web/apps/web-ele/src/router/access.ts index aa82424..38dbc69 100644 --- a/web/apps/web-ele/src/router/access.ts +++ b/web/apps/web-ele/src/router/access.ts @@ -11,28 +11,9 @@ import { BasicLayout, IFrameView } from '#/layouts'; import { $t } from '#/locales'; import { useAppContextStore } from '#/store/app-context'; -const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue'); +import { isLightMenuName } from './light-menu'; -const lightMenuNames = new Set([ - 'AIAgent', - 'AIKnowledgeDetail', - 'AIModelConfig', - 'AIPlatform', - 'AIWorkflow', - 'AIWorkflowRuns', - 'AnnouncementList', - 'AnnouncementManage', - 'Codex', - 'ControlCenter', - 'KnowledgeBase', - 'Message', - 'MessageList', - 'SystemManagement', - 'SystemMenu', - 'SystemPermission', - 'SystemRole', - 'UserManagement', -]); +const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue'); function normalizeViewPath(path: string) { const normalizedPath = path.replace(/^(\.\/|\.\.\/)+/, ''); @@ -85,7 +66,7 @@ function normalizeBackendRoute< } function isLightMenuRoute(route: { name?: string }) { - return !route.name || lightMenuNames.has(route.name); + return isLightMenuName(route.name); } function filterAvailableRoutes< @@ -102,12 +83,16 @@ function filterAvailableRoutes< layoutMap: ComponentRecordType, ): T[] { return routes - .filter(isLightMenuRoute) .map((route) => { const normalizedRoute = normalizeBackendRoute(route); const children = normalizedRoute.children ? filterAvailableRoutes(normalizedRoute.children, pageMap, layoutMap) : undefined; + const routeAllowed = isLightMenuRoute(normalizedRoute); + + if (!routeAllowed && !children?.length) { + return undefined; + } if ( !normalizedRoute.component && @@ -117,6 +102,12 @@ function filterAvailableRoutes< return undefined; } + if (!routeAllowed && children?.length) { + const groupRoute = { ...normalizedRoute, children }; + delete groupRoute.component; + return groupRoute; + } + if (!hasPageComponent(normalizedRoute.component, pageMap, layoutMap)) { if (!children?.length) return undefined; @@ -135,15 +126,24 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) { '../views/_core/agent-chat/**/*.vue', '../views/_core/announcement/index.vue', '../views/_core/announcement/list.vue', + '../views/_core/account-settings/index.vue', '../views/_core/authentication/login.vue', '../views/_core/authentication/oauth-callback.vue', + '../views/_core/dept/index.vue', + '../views/_core/dict/index.vue', '../views/_core/fallback/**/*.vue', + '../views/_core/file-manager/index.vue', '../views/_core/file-preview/index.vue', + '../views/_core/login-log/index.vue', '../views/_core/menu/index.vue', '../views/_core/message/index.vue', '../views/_core/page-render/index.vue', '../views/_core/permission/index.vue', + '../views/_core/post/index.vue', '../views/_core/role/index.vue', + '../views/_core/server-monitor/index.vue', + '../views/_core/system-config/index.vue', + '../views/_core/ui-config/index.vue', '../views/_core/user/index.vue', '../views/ai-platform/agent/editor/index.vue', '../views/ai-platform/agent/index.vue', diff --git a/web/apps/web-ele/src/router/light-menu.ts b/web/apps/web-ele/src/router/light-menu.ts new file mode 100644 index 0000000..a951218 --- /dev/null +++ b/web/apps/web-ele/src/router/light-menu.ts @@ -0,0 +1,54 @@ +export const LIGHT_MENU_NAMES = new Set([ + 'AIAgent', + 'AIKnowledgeDetail', + 'AIModelConfig', + 'AIPlatform', + 'AIWorkflow', + 'AIWorkflowRuns', + 'AccountSettings', + 'AnnouncementList', + 'AnnouncementManage', + 'Codex', + 'ControlCenter', + 'FileManager', + 'KnowledgeBase', + 'LoginLog', + 'Message', + 'MessageList', + 'ServerMonitor', + 'SystemConfigManager', + 'SystemDept', + 'SystemDict', + 'SystemFileManager', + 'SystemLoginLog', + 'SystemManagement', + 'SystemMenu', + 'SystemPost', + 'SystemPermission', + 'SystemRole', + 'UIConfigManager', + 'UserManagement', + 'userSettings', +]); + +export function isLightMenuName(name?: string) { + return !name || LIGHT_MENU_NAMES.has(name); +} + +export function filterLightMenuTree< + T extends { children?: T[]; name?: string }, +>(routes: T[]): T[] { + return routes + .map((route) => { + const children = route.children + ? filterLightMenuTree(route.children) + : undefined; + + if (!isLightMenuName(route.name) && !children?.length) { + return undefined; + } + + return children ? { ...route, children } : route; + }) + .filter(Boolean) as T[]; +} diff --git a/web/apps/web-ele/src/views/_core/about/index.vue b/web/apps/web-ele/src/views/_core/about/index.vue new file mode 100644 index 0000000..4189a77 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/about/index.vue @@ -0,0 +1,25 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/account-settings/api-token-management.vue b/web/apps/web-ele/src/views/_core/account-settings/api-token-management.vue new file mode 100644 index 0000000..cd7072d --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/api-token-management.vue @@ -0,0 +1,330 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/account-settings/data.ts b/web/apps/web-ele/src/views/_core/account-settings/data.ts new file mode 100644 index 0000000..5886178 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/data.ts @@ -0,0 +1,165 @@ +import type { VbenFormSchema } from '#/adapter/form'; + +import { $t } from '@vben/locales'; + +import { z } from '#/adapter/form'; + +/** + * 获取性别选项 + */ +export function getGenderOptions() { + return [ + { label: $t('user.unknown'), value: 0 }, + { label: $t('user.male'), value: 1 }, + { label: $t('user.female'), value: 2 }, + ]; +} + +/** + * 获取基本信息表单配置 + */ +export function getProfileFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'name', + label: $t('user.userName'), + rules: z + .string() + .min(2, $t('ui.formRules.minLength', [$t('user.userName'), 2])) + .max(64, $t('ui.formRules.maxLength', [$t('user.userName'), 64])) + .optional() + .or(z.literal('')), + }, + { + component: 'ImageSelector', + componentProps: { + enableCrop: true, + cropShape: 'circle', + maxSize: 2, + placeholder: $t('user.selectAvatar'), + }, + fieldName: 'avatar', + label: $t('user.avatar'), + help: $t('user.avatarHelp'), + }, + { + component: 'Input', + fieldName: 'email', + label: $t('user.email'), + rules: z + .string() + .email($t('user.emailFormatError')) + .max(255, $t('ui.formRules.maxLength', [$t('user.email'), 255])) + .optional() + .or(z.literal('')), + }, + { + component: 'Input', + fieldName: 'mobile', + label: $t('user.mobile'), + rules: z + .string() + .regex(/^1[3-9]\d{9}$/, $t('user.mobileFormatError')) + .optional() + .or(z.literal('')), + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: getGenderOptions(), + isButton: true, + }, + defaultValue: 0, + fieldName: 'gender', + label: $t('user.gender'), + }, + { + component: 'DatePicker', + componentProps: { + placeholder: $t('user.selectBirthday'), + valueFormat: 'YYYY-MM-DD', + }, + fieldName: 'birthday', + label: $t('user.birthday'), + }, + { + component: 'Input', + fieldName: 'city', + label: $t('user.city'), + rules: z + .string() + .max(100, $t('ui.formRules.maxLength', [$t('user.city'), 100])) + .optional() + .or(z.literal('')), + }, + { + component: 'Input', + fieldName: 'address', + label: $t('user.address'), + rules: z + .string() + .max(200, $t('ui.formRules.maxLength', [$t('user.address'), 200])) + .optional() + .or(z.literal('')), + }, + { + component: 'Textarea', + componentProps: { + placeholder: $t('user.bioPlaceholder'), + rows: 3, + }, + fieldName: 'bio', + label: $t('user.bio'), + }, + ]; +} + +/** + * 获取密码修改表单配置 + */ +export function getPasswordFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + componentProps: { + type: 'password', + showPassword: true, + placeholder: $t('user.oldPasswordPlaceholder'), + }, + fieldName: 'old_password', + label: $t('user.oldPassword'), + rules: z + .string() + .min(1, $t('ui.formRules.required', [$t('user.oldPassword')])), + }, + { + component: 'Input', + componentProps: { + type: 'password', + showPassword: true, + placeholder: $t('user.newPasswordPlaceholder'), + }, + fieldName: 'new_password', + label: $t('user.newPassword'), + rules: z + .string() + .min(6, $t('ui.formRules.minLength', [$t('user.newPassword'), 6])) + .max(20, $t('ui.formRules.maxLength', [$t('user.newPassword'), 20])), + }, + { + component: 'Input', + componentProps: { + type: 'password', + showPassword: true, + placeholder: $t('user.confirmPasswordPlaceholder'), + }, + fieldName: 'confirm_password', + label: $t('user.confirmPassword'), + rules: z + .string() + .min(1, $t('ui.formRules.required', [$t('user.confirmPassword')])), + }, + ]; +} diff --git a/web/apps/web-ele/src/views/_core/account-settings/device-item.vue b/web/apps/web-ele/src/views/_core/account-settings/device-item.vue new file mode 100644 index 0000000..5973798 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/device-item.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/account-settings/device-management.vue b/web/apps/web-ele/src/views/_core/account-settings/device-management.vue new file mode 100644 index 0000000..571d579 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/device-management.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/account-settings/index.vue b/web/apps/web-ele/src/views/_core/account-settings/index.vue new file mode 100644 index 0000000..2eb2fb2 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/index.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/account-settings/modules/password-form.vue b/web/apps/web-ele/src/views/_core/account-settings/modules/password-form.vue new file mode 100644 index 0000000..61dc6f2 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/modules/password-form.vue @@ -0,0 +1,168 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/account-settings/modules/profile-form.vue b/web/apps/web-ele/src/views/_core/account-settings/modules/profile-form.vue new file mode 100644 index 0000000..c379ebc --- /dev/null +++ b/web/apps/web-ele/src/views/_core/account-settings/modules/profile-form.vue @@ -0,0 +1,293 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/dept/data.ts b/web/apps/web-ele/src/views/_core/dept/data.ts new file mode 100644 index 0000000..d3ee22b --- /dev/null +++ b/web/apps/web-ele/src/views/_core/dept/data.ts @@ -0,0 +1,74 @@ +import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; + +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn } from '#/adapter/vxe-table'; +import type { DeptUser } from '#/api/core/dept'; + +import { $t } from '@vben/locales'; + +/** + * 获取搜索表单的字段配置 + */ +export function useSearchFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'name', + label: $t('system.user.userName'), + }, + { + component: 'Input', + fieldName: 'username', + label: $t('system.user.account'), + }, + ]; +} + +/** + * 获取用户表格列配置 + */ +export function useUserColumns( + onActionClick?: OnActionClickFn, +): VxeTableGridOptions['columns'] { + return [ + { + type: 'checkbox', + minWidth: 60, + align: 'center', + fixed: 'left', + }, + { + field: 'username', + title: $t('system.user.account'), + minWidth: 120, + }, + { + field: 'name', + title: $t('system.user.userName'), + minWidth: 120, + }, + { + field: 'email', + title: $t('system.user.email'), + minWidth: 180, + }, + { + align: 'right', + cellRender: { + attrs: { + nameField: 'name', + nameTitle: $t('system.user.userName'), + onClick: onActionClick, + }, + name: 'CellOperation', + options: ['edit', 'delete'], + }, + field: 'operation', + fixed: 'right', + headerAlign: 'center', + showOverflow: false, + title: $t('system.user.operation'), + minWidth: 150, + }, + ]; +} diff --git a/web/apps/web-ele/src/views/_core/dept/index.vue b/web/apps/web-ele/src/views/_core/dept/index.vue new file mode 100644 index 0000000..c8134cd --- /dev/null +++ b/web/apps/web-ele/src/views/_core/dept/index.vue @@ -0,0 +1,159 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/dept/modules/dept-form-modal.vue b/web/apps/web-ele/src/views/_core/dept/modules/dept-form-modal.vue new file mode 100644 index 0000000..fb81f8c --- /dev/null +++ b/web/apps/web-ele/src/views/_core/dept/modules/dept-form-modal.vue @@ -0,0 +1,209 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/dept/modules/dept-tree.vue b/web/apps/web-ele/src/views/_core/dept/modules/dept-tree.vue new file mode 100644 index 0000000..45f40b4 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/dept/modules/dept-tree.vue @@ -0,0 +1,501 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/dept/modules/user-form.vue b/web/apps/web-ele/src/views/_core/dept/modules/user-form.vue new file mode 100644 index 0000000..826c5d1 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/dept/modules/user-form.vue @@ -0,0 +1,72 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/dict/data.ts b/web/apps/web-ele/src/views/_core/dict/data.ts new file mode 100644 index 0000000..06dfb90 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/dict/data.ts @@ -0,0 +1,194 @@ +import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; + +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn } from '#/adapter/vxe-table'; +import type { DictItem } from '#/api/core/dict'; + +import { $t } from '@vben/locales'; + +import { z } from '#/adapter/form'; + +/** + * 获取字典搜索表单的字段配置 + */ +export function useDictSearchFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'name', + label: $t('dict.dictName'), + }, + { + component: 'Input', + fieldName: 'code', + label: $t('dict.dictCode'), + }, + ]; +} + +/** + * 获取字典表单配置 + */ +export function useDictFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'name', + label: $t('dict.dictName'), + rules: z + .string() + .min(2, $t('ui.formRules.minLength', [$t('dict.dictName'), 2])) + .max(100, $t('ui.formRules.maxLength', [$t('dict.dictName'), 100])), + }, + { + component: 'Input', + fieldName: 'code', + label: $t('dict.dictCode'), + rules: z + .string() + .min(2, $t('ui.formRules.minLength', [$t('dict.dictCode'), 2])) + .max(100, $t('ui.formRules.maxLength', [$t('dict.dictCode'), 100])) + .regex(/^\w+$/, $t('dict.codeFormatError')), + }, + { + component: 'RadioGroup', + componentProps: { + options: [ + { label: $t('common.enabled'), value: true }, + { label: $t('common.disabled'), value: false }, + ], + }, + defaultValue: true, + fieldName: 'status', + label: $t('dict.status'), + }, + ]; +} + +/** + * 获取字典项搜索表单配置 + */ +export function useDictItemSearchFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'label', + label: $t('dict.itemLabel'), + }, + { + component: 'Input', + fieldName: 'value', + label: $t('dict.itemValue'), + }, + ]; +} + +/** + * 获取字典项表单配置 + */ +export function useDictItemFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'label', + label: $t('dict.itemLabel'), + rules: z + .string() + .min(1, $t('ui.formRules.required', [$t('dict.itemLabel')])) + .max(100, $t('ui.formRules.maxLength', [$t('dict.itemLabel'), 100])), + }, + { + component: 'Input', + fieldName: 'value', + label: $t('dict.itemValue'), + rules: z + .string() + .min(1, $t('ui.formRules.required', [$t('dict.itemValue')])) + .max(100, $t('ui.formRules.maxLength', [$t('dict.itemValue'), 100])), + }, + // { + // component: 'Input', + // fieldName: 'icon', + // label: $t('dict.itemIcon'), + // rules: z + // .string() + // .max(100, $t('ui.formRules.maxLength', [$t('dict.itemIcon'), 100])) + // .optional(), + // }, + { + component: 'Textarea', + componentProps: { + placeholder: $t('dict.remarkPlaceholder'), + rows: 3, + }, + fieldName: 'remark', + label: $t('dict.remark'), + }, + { + component: 'RadioGroup', + componentProps: { + options: [ + { label: $t('common.enabled'), value: true }, + { label: $t('common.disabled'), value: false }, + ], + }, + defaultValue: true, + fieldName: 'status', + label: $t('dict.status'), + }, + ]; +} + +/** + * 获取字典项表格列配置 + */ +export function useDictItemColumns( + onActionClick?: OnActionClickFn, +): VxeTableGridOptions['columns'] { + return [ + { + field: 'label', + title: $t('dict.itemLabel'), + minWidth: 120, + }, + { + field: 'value', + title: $t('dict.itemValue'), + minWidth: 120, + }, + // { + // field: 'icon', + // title: $t('dict.itemIcon'), + // minWidth: 100, + // }, + { + field: 'status', + title: $t('dict.status'), + minWidth: 80, + cellRender: { + name: 'CellStatus', + attrs: { + onClick: onActionClick, + }, + }, + }, + { + align: 'right', + cellRender: { + attrs: { + nameField: 'label', + nameTitle: $t('dict.itemLabel'), + onClick: onActionClick, + }, + name: 'CellOperation', + options: ['edit', 'delete'], + }, + field: 'operation', + fixed: 'right', + headerAlign: 'center', + showOverflow: false, + title: $t('system.user.operation'), + minWidth: 150, + }, + ]; +} diff --git a/web/apps/web-ele/src/views/_core/dict/index.vue b/web/apps/web-ele/src/views/_core/dict/index.vue new file mode 100644 index 0000000..043520d --- /dev/null +++ b/web/apps/web-ele/src/views/_core/dict/index.vue @@ -0,0 +1,39 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/dict/modules/dict-form-modal.vue b/web/apps/web-ele/src/views/_core/dict/modules/dict-form-modal.vue new file mode 100644 index 0000000..77aa356 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/dict/modules/dict-form-modal.vue @@ -0,0 +1,99 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/dict/modules/dict-item-form-modal.vue b/web/apps/web-ele/src/views/_core/dict/modules/dict-item-form-modal.vue new file mode 100644 index 0000000..5cab3ee --- /dev/null +++ b/web/apps/web-ele/src/views/_core/dict/modules/dict-item-form-modal.vue @@ -0,0 +1,92 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/dict/modules/dict-item-list.vue b/web/apps/web-ele/src/views/_core/dict/modules/dict-item-list.vue new file mode 100644 index 0000000..23cce2f --- /dev/null +++ b/web/apps/web-ele/src/views/_core/dict/modules/dict-item-list.vue @@ -0,0 +1,683 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/dict/modules/dict-list.vue b/web/apps/web-ele/src/views/_core/dict/modules/dict-list.vue new file mode 100644 index 0000000..a6d999e --- /dev/null +++ b/web/apps/web-ele/src/views/_core/dict/modules/dict-list.vue @@ -0,0 +1,297 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/file-manager/components/CreateFolderDialog.vue b/web/apps/web-ele/src/views/_core/file-manager/components/CreateFolderDialog.vue new file mode 100644 index 0000000..b785e80 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/file-manager/components/CreateFolderDialog.vue @@ -0,0 +1,93 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/file-manager/components/FileList.vue b/web/apps/web-ele/src/views/_core/file-manager/components/FileList.vue new file mode 100644 index 0000000..c24d6df --- /dev/null +++ b/web/apps/web-ele/src/views/_core/file-manager/components/FileList.vue @@ -0,0 +1,547 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/file-manager/components/FileToolbar.vue b/web/apps/web-ele/src/views/_core/file-manager/components/FileToolbar.vue new file mode 100644 index 0000000..d0bdfb6 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/file-manager/components/FileToolbar.vue @@ -0,0 +1,296 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/file-manager/components/FolderTree.vue b/web/apps/web-ele/src/views/_core/file-manager/components/FolderTree.vue new file mode 100644 index 0000000..ce084d4 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/file-manager/components/FolderTree.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/file-manager/components/RenameDialog.vue b/web/apps/web-ele/src/views/_core/file-manager/components/RenameDialog.vue new file mode 100644 index 0000000..5c06523 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/file-manager/components/RenameDialog.vue @@ -0,0 +1,99 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/file-manager/composables/useFileManager.ts b/web/apps/web-ele/src/views/_core/file-manager/composables/useFileManager.ts new file mode 100644 index 0000000..583d61c --- /dev/null +++ b/web/apps/web-ele/src/views/_core/file-manager/composables/useFileManager.ts @@ -0,0 +1,129 @@ +import type { SystemFileManagerApi } from '#/api/core/file'; + +import { computed, ref } from 'vue'; + +import { $t } from '@vben/locales'; + +import { getFileList } from '#/api/core/file'; + +// 使用单例模式或者在顶层组件 provide +const currentFolderId = ref(null); +const viewMode = ref<'grid' | 'list'>('grid'); +const selectedFileIds = ref>(new Set()); +const fileList = ref([]); +const loading = ref(false); +const loadingMore = ref(false); +const breadcrumbs = ref>([ + { id: null, name: $t('file-manager.myFiles') }, +]); +const createFolderDialogVisible = ref(false); + +// 无限滚动分页状态 +const currentPage = ref(1); +const pageSize = ref(50); +const total = ref(0); +const noMore = computed(() => fileList.value.length >= total.value); + +export function useFileManager() { + const toggleViewMode = () => { + viewMode.value = viewMode.value === 'grid' ? 'list' : 'grid'; + }; + + const fetchFiles = async () => { + currentPage.value = 1; + loading.value = true; + try { + const res = await getFileList({ + parent_id: currentFolderId.value, + page: 1, + pageSize: pageSize.value, + }); + fileList.value = res.items; + total.value = res.total; + } catch (error) { + console.error(error); + } finally { + loading.value = false; + } + }; + + const loadMore = async () => { + if (noMore.value || loadingMore.value || loading.value) return; + loadingMore.value = true; + currentPage.value++; + try { + const res = await getFileList({ + parent_id: currentFolderId.value, + page: currentPage.value, + pageSize: pageSize.value, + }); + fileList.value = [...fileList.value, ...res.items]; + total.value = res.total; + } catch (error) { + console.error(error); + currentPage.value--; + } finally { + loadingMore.value = false; + } + }; + + const navigateToFolder = (folderId: null | string, folderName?: string) => { + currentFolderId.value = folderId; + selectedFileIds.value.clear(); + + // 更新面包屑 (这里只是简单的逻辑,实际可能需要根据树结构查找完整路径) + if (folderId === null) { + breadcrumbs.value = [{ id: null, name: $t('file-manager.myFiles') }]; + } else if (folderName) { + // 如果是点击面包屑导航回去,需要截断 + const index = breadcrumbs.value.findIndex((b) => b.id === folderId); + if (index === -1) { + // 进入新文件夹 + breadcrumbs.value.push({ id: folderId, name: folderName }); + } else { + breadcrumbs.value = breadcrumbs.value.slice(0, index + 1); + } + } + // fetchFiles 由 FileList.vue 中的 watch(currentFolderId) 自动触发 + }; + + const toggleSelection = (id: string, multi: boolean) => { + if (multi) { + if (selectedFileIds.value.has(id)) { + selectedFileIds.value.delete(id); + } else { + selectedFileIds.value.add(id); + } + } else { + selectedFileIds.value.clear(); + selectedFileIds.value.add(id); + } + }; + + const clearSelection = () => { + selectedFileIds.value.clear(); + }; + + const openCreateFolderDialog = () => { + createFolderDialogVisible.value = true; + }; + + return { + currentFolderId, + viewMode, + selectedFileIds, + fileList, + loading, + loadingMore, + noMore, + breadcrumbs, + createFolderDialogVisible, + toggleViewMode, + navigateToFolder, + toggleSelection, + clearSelection, + fetchFiles, + loadMore, + openCreateFolderDialog, + }; +} diff --git a/web/apps/web-ele/src/views/_core/file-manager/index.vue b/web/apps/web-ele/src/views/_core/file-manager/index.vue new file mode 100644 index 0000000..0512e94 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/file-manager/index.vue @@ -0,0 +1,21 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/login-log/data.ts b/web/apps/web-ele/src/views/_core/login-log/data.ts new file mode 100644 index 0000000..dc14648 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/login-log/data.ts @@ -0,0 +1,242 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { LoginLog } from '#/api/core/login-log'; + +import { $t } from '@vben/locales'; + +/** + * 获取登录状态选项 + */ +export function getStatusOptions() { + return [ + { type: 'danger', label: $t('loginLog.statusFailed'), value: 0 }, + { type: 'success', label: $t('loginLog.statusSuccess'), value: 1 }, + ]; +} + +/** + * 获取失败原因选项 + */ +export function getFailureReasonOptions() { + return [ + { label: $t('loginLog.failureReasonUnknown'), value: 0 }, + { label: $t('loginLog.failureReasonUserNotExist'), value: 1 }, + { label: $t('loginLog.failureReasonPasswordError'), value: 2 }, + { label: $t('loginLog.failureReasonUserDisabled'), value: 3 }, + { label: $t('loginLog.failureReasonUserLocked'), value: 4 }, + { label: $t('loginLog.failureReasonUserInactive'), value: 5 }, + { label: $t('loginLog.failureReasonAccountAbnormal'), value: 6 }, + { label: $t('loginLog.failureReasonOther'), value: 7 }, + ]; +} + +/** + * 获取设备类型选项 + */ +export function getDeviceTypeOptions() { + return [ + { label: $t('loginLog.deviceTypeDesktop'), value: 'desktop' }, + { label: $t('loginLog.deviceTypeMobile'), value: 'mobile' }, + { label: $t('loginLog.deviceTypeTablet'), value: 'tablet' }, + { label: $t('loginLog.deviceTypeOther'), value: 'other' }, + ]; +} + +/** + * 获取登录方式选项 + */ +export function getLoginTypeOptions() { + return [ + { label: '密码登录', value: 'password', type: 'info' }, + { label: '验证码登录', value: 'code', type: 'info' }, + { label: '二维码登录', value: 'qrcode', type: 'info' }, + { label: 'Gitee', value: 'gitee', type: 'success' }, + { label: 'GitHub', value: 'github', type: 'success' }, + { label: 'QQ', value: 'qq', type: 'success' }, + { label: 'Google', value: 'google', type: 'success' }, + { label: '微信', value: 'wechat', type: 'success' }, + { label: '微软', value: 'microsoft', type: 'success' }, + { label: '钉钉', value: 'dingtalk', type: 'success' }, + { label: '飞书', value: 'feishu', type: 'success' }, + ]; +} + +/** + * 获取搜索表单的字段配置 + */ +export function useSearchFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'username', + label: $t('loginLog.username'), + componentProps: { + placeholder: $t('loginLog.searchPlaceholder', [ + $t('loginLog.username'), + ]), + }, + }, + + { + component: 'Select', + fieldName: 'status', + label: $t('loginLog.status'), + componentProps: { + placeholder: $t('loginLog.selectStatus'), + options: getStatusOptions(), + clearable: true, + }, + }, + + { + component: 'Select', + fieldName: 'login_type', + label: '登录方式', + componentProps: { + placeholder: '请选择登录方式', + options: getLoginTypeOptions(), + clearable: true, + }, + }, + ]; +} + +/** + * 获取表格列配置 + */ +export function useColumns( + onActionClick?: OnActionClickFn, +): VxeTableGridOptions['columns'] { + return [ + { + type: 'checkbox', + minWidth: 60, + align: 'center', + fixed: 'left', + }, + { + field: 'username', + title: $t('loginLog.username'), + minWidth: 120, + fixed: 'left', + }, + { + field: 'status', + title: $t('loginLog.status'), + minWidth: 100, + cellRender: { + name: 'CellTag', + options: getStatusOptions(), + }, + }, + { + field: 'login_type', + title: '登录方式', + minWidth: 120, + cellRender: { + name: 'CellTag', + options: getLoginTypeOptions(), + }, + }, + { + field: 'login_ip', + title: $t('loginLog.loginIp'), + minWidth: 140, + }, + { + field: 'ip_location', + title: $t('loginLog.ipLocation'), + minWidth: 150, + }, + { + field: 'failure_reason', + title: $t('loginLog.failureReason'), + minWidth: 120, + cellRender: { + name: 'CellTag', + options: getFailureReasonOptions().map((opt) => ({ + ...opt, + type: 'danger', + })), + }, + visible: false, + }, + { + field: 'failure_message', + title: $t('loginLog.failureMessage'), + minWidth: 180, + showOverflow: 'tooltip', + visible: false, + }, + { + field: 'browser_type', + title: $t('loginLog.browserType'), + minWidth: 120, + }, + { + field: 'os_type', + title: $t('loginLog.osType'), + minWidth: 120, + }, + { + field: 'device_type', + title: $t('loginLog.deviceType'), + minWidth: 100, + cellRender: { + name: 'CellTag', + options: getDeviceTypeOptions().map((opt) => ({ + ...opt, + type: 'info', + })), + }, + }, + { + field: 'duration', + title: $t('loginLog.durationSeconds'), + minWidth: 120, + visible: false, + }, + { + field: 'remark', + title: $t('loginLog.remark'), + minWidth: 150, + showOverflow: 'tooltip', + visible: false, + }, + { + field: 'sys_create_datetime', + title: $t('loginLog.loginTime'), + minWidth: 180, + sortable: true, + }, + { + align: 'right', + cellRender: { + attrs: { + nameField: 'username', + nameTitle: $t('loginLog.username'), + onClick: onActionClick, + }, + name: 'CellOperation', + options: [ + { + code: 'detail', + text: $t('loginLog.detail'), + icon: 'ep:document', + }, + { + code: 'delete', + text: $t('common.delete'), + icon: 'ep:delete', + }, + ], + }, + field: 'operation', + fixed: 'right', + headerAlign: 'center', + showOverflow: false, + title: $t('loginLog.operation'), + minWidth: 150, + }, + ]; +} diff --git a/web/apps/web-ele/src/views/_core/login-log/index.vue b/web/apps/web-ele/src/views/_core/login-log/index.vue new file mode 100644 index 0000000..5bdf75a --- /dev/null +++ b/web/apps/web-ele/src/views/_core/login-log/index.vue @@ -0,0 +1,191 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/login-log/modules/detail-drawer.vue b/web/apps/web-ele/src/views/_core/login-log/modules/detail-drawer.vue new file mode 100644 index 0000000..92af875 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/login-log/modules/detail-drawer.vue @@ -0,0 +1,180 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/menu/modules/menu-form-modal.vue b/web/apps/web-ele/src/views/_core/menu/modules/menu-form-modal.vue index 4bd873c..193c04e 100644 --- a/web/apps/web-ele/src/views/_core/menu/modules/menu-form-modal.vue +++ b/web/apps/web-ele/src/views/_core/menu/modules/menu-form-modal.vue @@ -20,6 +20,7 @@ import { getAllMenuTreeApi, } from '#/api/core/menu'; import { ZqDialog } from '#/components/zq-dialog'; +import { filterLightMenuTree } from '#/router/light-menu'; import { useAppContextStore } from '#/store/app-context'; const emit = defineEmits<{ @@ -51,8 +52,8 @@ function processMenuData(menus: any[]): any[] { async function getMenuListProcessed() { const applicationId = appContextStore.currentApp?.id; const data = await getAllMenuTreeApi(applicationId); - menuTreeData.value = data; - return processMenuData(data); + menuTreeData.value = filterLightMenuTree(data); + return processMenuData(menuTreeData.value); } /** diff --git a/web/apps/web-ele/src/views/_core/menu/modules/menu-form.vue b/web/apps/web-ele/src/views/_core/menu/modules/menu-form.vue index bd74ee4..440ac70 100644 --- a/web/apps/web-ele/src/views/_core/menu/modules/menu-form.vue +++ b/web/apps/web-ele/src/views/_core/menu/modules/menu-form.vue @@ -26,6 +26,7 @@ import { getAllMenuTreeApi, updateMenuApi, } from '#/api/core/menu'; +import { filterLightMenuTree } from '#/router/light-menu'; import { useAppContextStore } from '#/store/app-context'; import { getMenuTypeOptions } from '../data'; @@ -57,7 +58,7 @@ function processMenuData(menus: any[]): any[] { async function getMenuListProcessed() { const applicationId = appContextStore.currentApp?.id; const data = await getAllMenuTreeApi(applicationId); - return processMenuData(data); + return processMenuData(filterLightMenuTree(data)); } /** diff --git a/web/apps/web-ele/src/views/_core/menu/modules/menu-tree.vue b/web/apps/web-ele/src/views/_core/menu/modules/menu-tree.vue index 0d02d67..bd8cb76 100644 --- a/web/apps/web-ele/src/views/_core/menu/modules/menu-tree.vue +++ b/web/apps/web-ele/src/views/_core/menu/modules/menu-tree.vue @@ -25,6 +25,7 @@ import { searchMenuApi, } from '#/api/core/menu'; import { ZqTabs } from '#/components/zq-tabs'; +import { filterLightMenuTree } from '#/router/light-menu'; import { useAppContextStore } from '#/store/app-context'; import MenuFormModal from './menu-form-modal.vue'; @@ -138,7 +139,9 @@ async function fetchMenuList(autoSelectFirst = false) { // 获取完整的菜单树(全量加载),传入当前应用ID const applicationId = appContextStore.currentApp?.id; const data = await getAllMenuTreeApi(applicationId, false); - treeData.value = transformMenuTreeData(Array.isArray(data) ? data : []); + treeData.value = transformMenuTreeData( + filterLightMenuTree(Array.isArray(data) ? data : []), + ); // 如果需要自动选中第一个菜单 if (autoSelectFirst && treeData.value.length > 0) { @@ -307,7 +310,9 @@ watch(searchKeyword, (newVal) => { try { const results = await searchMenuApi(newVal); // 转换搜索结果数据结构 - const transformedResults = transformMenuData(results || []); + const transformedResults = transformMenuData( + filterLightMenuTree(results || []), + ); searchResults.value = transformedResults; // 自动展开搜索结果中的所有节点,显示完整路径 if (transformedResults && transformedResults.length > 0) { diff --git a/web/apps/web-ele/src/views/_core/page-render/index.vue b/web/apps/web-ele/src/views/_core/page-render/index.vue index f3878a1..6420d28 100644 --- a/web/apps/web-ele/src/views/_core/page-render/index.vue +++ b/web/apps/web-ele/src/views/_core/page-render/index.vue @@ -1,28 +1,33 @@ + + diff --git a/web/apps/web-ele/src/views/_core/post/modules/post-form-modal.vue b/web/apps/web-ele/src/views/_core/post/modules/post-form-modal.vue new file mode 100644 index 0000000..89cf4d7 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/post/modules/post-form-modal.vue @@ -0,0 +1,84 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/post/modules/post-list.vue b/web/apps/web-ele/src/views/_core/post/modules/post-list.vue new file mode 100644 index 0000000..555a9f2 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/post/modules/post-list.vue @@ -0,0 +1,291 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/post/modules/user-form.vue b/web/apps/web-ele/src/views/_core/post/modules/user-form.vue new file mode 100644 index 0000000..1a4ee26 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/post/modules/user-form.vue @@ -0,0 +1,76 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/role/modules/permission-assign-panel.vue b/web/apps/web-ele/src/views/_core/role/modules/permission-assign-panel.vue index 76cc5ff..6397118 100644 --- a/web/apps/web-ele/src/views/_core/role/modules/permission-assign-panel.vue +++ b/web/apps/web-ele/src/views/_core/role/modules/permission-assign-panel.vue @@ -31,6 +31,7 @@ import { getRoleMenusApi, updateRoleMenusPermissionsApi, } from '#/api/core/role'; +import { filterLightMenuTree } from '#/router/light-menu'; import { useAppContextStore } from '#/store/app-context'; import FieldPermissionConfig from './field-permission-config.vue'; @@ -95,17 +96,6 @@ function filterTreeByApp(nodes: MenuNode[], appId?: string): MenuNode[] { })); } -function filterAiAgentAdminTree(nodes: MenuNode[]): MenuNode[] { - return nodes - .filter((node) => AI_AGENT_ADMIN_MENU_NAMES.has(node.name)) - .map((node) => ({ - ...node, - children: node.children - ? filterAiAgentAdminTree(node.children as MenuNode[]) - : [], - })); -} - function collectMenuIds(nodes: MenuNode[], ids: Set) { nodes.forEach((node) => { ids.add(node.id); @@ -130,26 +120,6 @@ const appList = ref([]); const selectedAppId = ref(undefined); const loadingApps = ref(false); -const AI_AGENT_ADMIN_MENU_NAMES = new Set([ - 'ControlCenter', - 'AIPlatform', - 'AIAgent', - 'AIModelConfig', - 'AIWorkflow', - 'AIWorkflowRuns', - 'KnowledgeBase', - 'Codex', - 'SystemManagement', - 'SystemPermission', - 'UserManagement', - 'SystemMenu', - 'SystemRole', - 'Message', - 'MessageList', - 'AnnouncementList', - 'AnnouncementManage', -]); - const AI_AGENT_ADMIN_APP_CODES = new Set(['ai_agent_admin']); const resourceScopeConfigRef = ref>(); @@ -267,7 +237,7 @@ async function loadMenuTree() { const data = await getRoleMenusApi(props.role.id); // 使用后端返回的菜单树结构 - allTreeData.value = filterAiAgentAdminTree(data.menu_tree || []); + allTreeData.value = filterLightMenuTree(data.menu_tree || []); // 初始化已选菜单 const selectedMenuIdsList = data.selected_menu_ids || []; diff --git a/web/apps/web-ele/src/views/_core/system-config/index.vue b/web/apps/web-ele/src/views/_core/system-config/index.vue new file mode 100644 index 0000000..c814848 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/system-config/index.vue @@ -0,0 +1,294 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/system-config/modules/config-form.vue b/web/apps/web-ele/src/views/_core/system-config/modules/config-form.vue new file mode 100644 index 0000000..b5ea6ec --- /dev/null +++ b/web/apps/web-ele/src/views/_core/system-config/modules/config-form.vue @@ -0,0 +1,206 @@ + + + diff --git a/web/apps/web-ele/src/views/_core/system-config/modules/model-config-form.vue b/web/apps/web-ele/src/views/_core/system-config/modules/model-config-form.vue new file mode 100644 index 0000000..deb034e --- /dev/null +++ b/web/apps/web-ele/src/views/_core/system-config/modules/model-config-form.vue @@ -0,0 +1,1279 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/ui-config/index.vue b/web/apps/web-ele/src/views/_core/ui-config/index.vue new file mode 100644 index 0000000..6f3d865 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/ui-config/index.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/ui-config/modules/app-settings-form.vue b/web/apps/web-ele/src/views/_core/ui-config/modules/app-settings-form.vue new file mode 100644 index 0000000..beee0df --- /dev/null +++ b/web/apps/web-ele/src/views/_core/ui-config/modules/app-settings-form.vue @@ -0,0 +1,619 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/ui-config/modules/login-config-form.vue b/web/apps/web-ele/src/views/_core/ui-config/modules/login-config-form.vue new file mode 100644 index 0000000..ab302d5 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/ui-config/modules/login-config-form.vue @@ -0,0 +1,331 @@ + + + + + diff --git a/web/apps/web-ele/src/views/_core/ui-config/modules/ui-preferences-form.vue b/web/apps/web-ele/src/views/_core/ui-config/modules/ui-preferences-form.vue new file mode 100644 index 0000000..028d0b0 --- /dev/null +++ b/web/apps/web-ele/src/views/_core/ui-config/modules/ui-preferences-form.vue @@ -0,0 +1,664 @@ + + + + + diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 70b5e6d..06fa718 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -665,6 +665,9 @@ importers: '@vben/locales': specifier: workspace:* version: link:../../packages/locales + '@vben/plugins': + specifier: workspace:* + version: link:../../packages/effects/plugins '@vben/preferences': specifier: workspace:* version: link:../../packages/preferences @@ -707,6 +710,9 @@ importers: element-plus: specifier: 'catalog:' version: 2.11.9(vue@3.5.25(typescript@5.9.3)) + grid-layout-plus: + specifier: ^1.1.1 + version: 1.1.1(vue@3.5.25(typescript@5.9.3)) pinia: specifier: ^3.0.3 version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)) @@ -3485,6 +3491,9 @@ packages: '@types/node': optional: true + '@interactjs/types@1.10.27': + resolution: {integrity: sha512-BUdv0cvs4H5ODuwft2Xp4eL8Vmi3LcihK42z0Ft/FbVJZoRioBsxH+LlsBdK4tAie7PqlKGy+1oyOncu1nQ6eA==} + '@internationalized/date@3.10.0': resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==} @@ -3590,6 +3599,9 @@ packages: '@jspm/import-map@1.2.2': resolution: {integrity: sha512-QrM7+lkgVE8t9NZSm9fDSzheguEOVCOxueKxSwgoOt7yUWesWKDFmmL9bQFURl4IqMcpPz7VV2Tvw9s9emaJzQ==} + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + '@keyv/bigmap@1.3.0': resolution: {integrity: sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==} engines: {node: '>= 18'} @@ -4529,6 +4541,14 @@ packages: engines: {node: '>=18'} hasBin: true + '@vexip-ui/hooks@2.9.4': + resolution: {integrity: sha512-dGUiBAeHIsnSVigGSPHcuHBVqrSGW8LV+zGohvOpBfXs8Ynn5ZcSmybIWJ3G826NsicPu9rqwcJG8uvSgG4k4Q==} + peerDependencies: + vue: ^3.5.17 + + '@vexip-ui/utils@2.16.4': + resolution: {integrity: sha512-KX+Q4EsuwDp6ZlRJ7OAkiYxu52D5CVM8zpqQz/FXYV+JUtzl9T3dvxgtA8gQ0wm5Sh/xT6jp8Wo4X7tLAzRh/A==} + '@vite-pwa/vitepress@1.1.0': resolution: {integrity: sha512-enif5C2JmepS69Ak7PSr6K3Eimsg7VyryZo1Z6NiIPNDnjXFlOqxwBYaq8R/d0M7akArJsjos7KmQnEfNFf5nA==} peerDependencies: @@ -6844,6 +6864,11 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} + grid-layout-plus@1.1.1: + resolution: {integrity: sha512-7CWehJubrVC8Ps5QFUlnDsp0kiREvKfi3Pdjp21EyY8BNzSusqI3Utcxvu1Y9UUKe3YExvbhJzIxHK6rorbRaQ==} + peerDependencies: + vue: ^3.5.17 + gzip-size@7.0.0: resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -7026,6 +7051,9 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + interactjs@1.10.27: + resolution: {integrity: sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -12757,7 +12785,7 @@ snapshots: '@floating-ui/core@1.7.3': dependencies: - '@floating-ui/utils': 0.2.10 + '@floating-ui/utils': 0.2.11 '@floating-ui/core@1.7.5': dependencies: @@ -12842,6 +12870,8 @@ snapshots: optionalDependencies: '@types/node': 22.19.1 + '@interactjs/types@1.10.27': {} + '@internationalized/date@3.10.0': dependencies: '@swc/helpers': 0.5.17 @@ -12976,6 +13006,8 @@ snapshots: '@jspm/import-map@1.2.2': {} + '@juggle/resize-observer@3.4.0': {} + '@keyv/bigmap@1.3.0(keyv@5.5.4)': dependencies: hashery: 1.3.0 @@ -13957,6 +13989,15 @@ snapshots: - rollup - supports-color + '@vexip-ui/hooks@2.9.4(vue@3.5.25(typescript@5.9.3))': + dependencies: + '@floating-ui/dom': 1.7.6 + '@juggle/resize-observer': 3.4.0 + '@vexip-ui/utils': 2.16.4 + vue: 3.5.25(typescript@5.9.3) + + '@vexip-ui/utils@2.16.4': {} + '@vite-pwa/vitepress@1.1.0(vite-plugin-pwa@1.2.0(vite@5.4.21(@types/node@24.10.1)(less@4.4.2)(sass@1.94.2)(terser@5.44.1))(workbox-build@7.4.0)(workbox-window@7.4.0))': dependencies: vite-plugin-pwa: 1.2.0(vite@5.4.21(@types/node@24.10.1)(less@4.4.2)(sass@1.94.2)(terser@5.44.1))(workbox-build@7.4.0)(workbox-window@7.4.0) @@ -16645,6 +16686,13 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 + grid-layout-plus@1.1.1(vue@3.5.25(typescript@5.9.3)): + dependencies: + '@vexip-ui/hooks': 2.9.4(vue@3.5.25(typescript@5.9.3)) + '@vexip-ui/utils': 2.16.4 + interactjs: 1.10.27 + vue: 3.5.25(typescript@5.9.3) + gzip-size@7.0.0: dependencies: duplexer: 0.1.2 @@ -16838,6 +16886,10 @@ snapshots: ini@4.1.1: {} + interactjs@1.10.27: + dependencies: + '@interactjs/types': 1.10.27 + internal-slot@1.1.0: dependencies: es-errors: 1.3.0