diff --git a/jeecgboot-vue3/.env.development b/jeecgboot-vue3/.env.development index 3a6342764..fb3498c90 100644 --- a/jeecgboot-vue3/.env.development +++ b/jeecgboot-vue3/.env.development @@ -28,3 +28,6 @@ VITE_APP_SUB_jeecg-app-1 = '//localhost:8092' # 全局隐藏哪些布局。可选属性:sider,header,multi-tabs;多个用逗号隔开 #VITE_GLOB_HIDE_LAYOUT_TYPES=sider,header,multi-tabs + +# 在线文档编辑版本。可选属性:wps, offlineWps(离线版) ,onlyoffice +VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps diff --git a/jeecgboot-vue3/.env.prod_electron b/jeecgboot-vue3/.env.prod_electron new file mode 100644 index 000000000..ca0133578 --- /dev/null +++ b/jeecgboot-vue3/.env.prod_electron @@ -0,0 +1,38 @@ +# 是否启用mock +VITE_USE_MOCK = false + +# 后台接口父地址(必填) +# 【Electron下需要与 VITE_GLOB_DOMAIN_URL 配置保持一致】 +VITE_GLOB_API_URL=https://api3.boot.jeecg.com + +# 后台接口全路径地址(必填) +VITE_GLOB_DOMAIN_URL=https://api3.boot.jeecg.com + +# 接口父路径前缀 +VITE_GLOB_API_URL_PREFIX= + +# 在线文档编辑版本。可选属性:wps, offlineWps(离线版), onlyoffice +VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps + +# 全局隐藏哪些布局。可选属性:sider,header,multi-tabs;多个用逗号隔开 +#VITE_GLOB_HIDE_LAYOUT_TYPES=sider,header,multi-tabs + +# ----------------------------------------- +# ------------ 以下参数不建议修改 ------------ +# ----------------------------------------- + +# 发布路径 +# 【election下只能是 . 开头的相对路径,建议为 ./ 】 +VITE_PUBLIC_PATH = ./ + +# 是否启用gzip或brotli压缩 +# 选项值: gzip | brotli | none +# 如果需要多个可以使用“,”分隔 +# 【electron下由于是本地html文件访问,所以不需要压缩】 +VITE_BUILD_COMPRESS = 'none' + +# 使用压缩时是否删除原始文件,默认为false +VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false + +# ※ 请勿修改此项 ※ +VITE_GLOB_RUN_PLATFORM=electron diff --git a/jeecgboot-vue3/.env.production b/jeecgboot-vue3/.env.production index aef68837f..74f6ee81d 100644 --- a/jeecgboot-vue3/.env.production +++ b/jeecgboot-vue3/.env.production @@ -29,3 +29,6 @@ VITE_GLOB_API_URL_PREFIX= # 全局隐藏哪些布局。可选属性:sider,header,multi-tabs;多个用逗号隔开 #VITE_GLOB_HIDE_LAYOUT_TYPES=sider,header,multi-tabs + +# 在线文档编辑版本。可选属性:wps, offlineWps(离线版), onlyoffice +VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps diff --git a/jeecgboot-vue3/.npmrc b/jeecgboot-vue3/.npmrc index cf0404245..24e860038 100644 --- a/jeecgboot-vue3/.npmrc +++ b/jeecgboot-vue3/.npmrc @@ -1,2 +1,5 @@ shamefully-hoist=true strict-peer-dependencies=false + +electron_mirror=https://npmmirror.com/mirrors/electron/ +electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ diff --git a/jeecgboot-vue3/LICENSE b/jeecgboot-vue3/LICENSE index 97c181fa5..de1700ef5 100644 --- a/jeecgboot-vue3/LICENSE +++ b/jeecgboot-vue3/LICENSE @@ -19,15 +19,15 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - + In any case, you must not make any such use of this software as to develop software which may be considered competitive with this software. -JeecgBoot 是由 北京国炬信息技术有限公司 发行的软件。 总部位于北京,地址:中国·北京·朝阳区科荟前街1号院奥林佳泰大厦。邮箱:jeecgos@163.com + JeecgBoot 是由 北京国炬信息技术有限公司 发行的软件。 总部位于北京,地址:中国·北京·朝阳区科荟前街1号院奥林佳泰大厦。邮箱:jeecgos@163.com 本软件受适用的国家软件著作权法(包括国际条约)和开源协议 双重保护许可。 - + 开源协议中文释意如下: 1.JeecgBoot开源版本无任何限制,在遵循本开源协议条款下,允许商用使用,不会造成侵权行为。 2.允许基于本平台软件开展业务系统开发。 3.在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件。 - + 最终解释权归:http://www.jeecg.com diff --git a/jeecgboot-vue3/build/script/buildConf.ts b/jeecgboot-vue3/build/script/buildConf.ts index 1892af3c1..4a1d824f0 100644 --- a/jeecgboot-vue3/build/script/buildConf.ts +++ b/jeecgboot-vue3/build/script/buildConf.ts @@ -35,6 +35,16 @@ function createConfig(params: CreateConfigParams) { console.log(colors.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`); console.log(colors.gray(OUTPUT_DIR + '/' + colors.green(configFileName)) + '\n'); + + // update-begin--author:sunjianlei---date:20250423---for:【QQYUN-9685】构建 electron 桌面应用 + // 如果是 Electron 环境,还需要将配置文件写入到 JSON 文件中 + if (config.VITE_GLOB_RUN_PLATFORM === 'electron') { + writeFileSync(getRootPath(`${OUTPUT_DIR}/electron/env.json`), JSON.stringify(config)); + console.log(colors.cyan(`✨ [${pkg.name}]`) + ` - electron env file is build successfully:`); + console.log(colors.gray(OUTPUT_DIR + '/' + colors.green('electron/env.json')) + '\n'); + } + // update-end----author:sunjianlei---date:20250423---for:【QQYUN-9685】构建 electron 桌面应用 + } catch (error) { console.log(colors.red('configuration file configuration file failed to package:\n' + error)); } diff --git a/jeecgboot-vue3/build/utils.ts b/jeecgboot-vue3/build/utils.ts index 902d4bb87..9490116ff 100644 --- a/jeecgboot-vue3/build/utils.ts +++ b/jeecgboot-vue3/build/utils.ts @@ -49,6 +49,14 @@ export function wrapperEnv(envConf: Recordable): ViteEnv { * 获取当前环境下生效的配置文件名 */ function getConfFiles() { + + // update-begin--author:sunjianlei---date:20250411---for:【QQYUN-9685】构建 electron 桌面应用 + const {VITE_GLOB_RUN_PLATFORM} = process.env + if (VITE_GLOB_RUN_PLATFORM === 'electron') { + return ['.env', '.env.prod_electron']; + } + // update-end----author:sunjianlei---date:20250411---for:【QQYUN-9685】构建 electron 桌面应用 + const script = process.env.npm_lifecycle_script; // update-begin--author:liaozhiyang---date:20240326---for:【QQYUN-8690】修正获取当前环境下的文件名 const reg = new RegExp('NODE_ENV=([a-z_\\d]+)'); diff --git a/jeecgboot-vue3/build/vite/plugin/electron.ts b/jeecgboot-vue3/build/vite/plugin/electron.ts new file mode 100644 index 000000000..5b597e92b --- /dev/null +++ b/jeecgboot-vue3/build/vite/plugin/electron.ts @@ -0,0 +1,34 @@ +// import electron from 'vite-plugin-electron/simple' +// +// export function configElectronPlugin(_viteEnv: ViteEnv, isBuild: boolean) { +// return electron({ +// main: { +// // 主进程入口 +// entry: 'electron/main.ts', +// vite: { +// build: { +// sourcemap: !isBuild, +// outDir: 'dist/electron', +// }, +// }, +// onstart: ({startup}) => { +// // 开发热重载 +// startup() +// }, +// }, +// preload: { +// input: 'electron/preload/index.ts', +// vite: { +// build: { +// sourcemap: !isBuild, +// outDir: 'dist/electron/preload', +// }, +// }, +// onstart: ({startup}) => { +// // 开发热重载 +// startup() +// }, +// } +// }) +// +// } diff --git a/jeecgboot-vue3/build/vite/plugin/index.ts b/jeecgboot-vue3/build/vite/plugin/index.ts index 7d068ef68..bce656193 100644 --- a/jeecgboot-vue3/build/vite/plugin/index.ts +++ b/jeecgboot-vue3/build/vite/plugin/index.ts @@ -16,6 +16,8 @@ import { configVisualizerConfig } from './visualizer'; import { configThemePlugin } from './theme'; import { configSvgIconsPlugin } from './svgSprite'; import { configQiankunMicroPlugin } from './qiankunMicro'; +// // electron plugin +// import { configElectronPlugin } from "./electron"; // //预编译加载插件(不支持vite3作废) // import OptimizationPersist from 'vite-plugin-optimize-persist'; // import PkgConfig from 'vite-plugin-package-config'; @@ -68,6 +70,12 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, isQiankunM vitePlugins.push(...configQiankunMicroPlugin(viteEnv)) } + // // electron plugin + // const isElectron = viteEnv.VITE_GLOB_RUN_PLATFORM === 'electron'; + // if (isElectron) { + // vitePlugins.push(configElectronPlugin(viteEnv, isBuild)) + // } + // The following plugins only work in the production environment if (isBuild) { diff --git a/jeecgboot-vue3/electron-builder.yaml b/jeecgboot-vue3/electron-builder.yaml new file mode 100644 index 000000000..05a01e0a1 --- /dev/null +++ b/jeecgboot-vue3/electron-builder.yaml @@ -0,0 +1,24 @@ +appId: 'com.jeecg.boot3' +# 产品名称 +productName: 'jeecgboot' +files: + # 仅包含 dist 目录下所有文件 + - 'dist/**/*' + # 特别排除 node_modules 目录 + - '!node_modules' +directories: + # 输出目录 + output: 'dist-electron' +win: + # win exe 程序图标 + icon: 'electron/icons/app.ico' + artifactName: 'jeecgboot-setup-${version}.exe' +# 安装包配置 +nsis: + oneClick: false + # 是否允许用户选择安装目录 + allowToChangeInstallationDirectory: true + # 是否创建桌面快捷方式 + createDesktopShortcut: true + # 安装程序的图标 + installerIcon: 'electron/icons/installer.ico' diff --git a/jeecgboot-vue3/electron.md b/jeecgboot-vue3/electron.md new file mode 100644 index 000000000..c10e4c29a --- /dev/null +++ b/jeecgboot-vue3/electron.md @@ -0,0 +1,35 @@ +# Electron桌面应用打包 + +- 1.安装依赖很慢,得10分钟左右 +- 2.electron桌面应用打包文档 + https://help.jeecg.com/ui/setup/electron-build +- 3.临时注释掉electron功能 + 注释代码:build/vite/plugin/electron.ts + 修改build/vite/plugin/index.ts,搜索`electron plugin`注释相关逻辑代码 + 修改package.json删除相关依赖 + +```yaml +{ + "main": "dist/electron/main.js", + "scripts": { + "electron:dev": "cross-env VITE_GLOB_RUN_PLATFORM=electron npm run dev", + "electron:build-all": "npm run electron:build-web && npm run electron:build-app", + "electron:build-web": "cross-env VITE_GLOB_RUN_PLATFORM=electron NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 vite build --mode prod_electron && cross-env VITE_GLOB_RUN_PLATFORM=electron esno ./build/script/postBuild.ts && esno ./build/script/copyChat.ts", + "electron:build-app": "esno ./electron/script/buildBefore.ts && electron-builder && esno ./electron/script/buildAfter.ts", + }, + "devDependencies": { + "electron": "35.1.4", + "electron-builder": "^26.0.12", + "vite-plugin-electron": "^0.29.0", + }, +} + +``` + + +# Electron桌面通知示例和代码位置 + +1. 代码位置:electron/utils/tray.ts +2. 发送系统通知调用:sendDesktopNotice +3. 开始托盘图标闪动调用:startBlink +4. 停止托盘图标闪动调用:stopBlink diff --git a/jeecgboot-vue3/electron/env.ts b/jeecgboot-vue3/electron/env.ts new file mode 100644 index 000000000..e6add3251 --- /dev/null +++ b/jeecgboot-vue3/electron/env.ts @@ -0,0 +1,18 @@ +// 不能直接使用 process.env,会报错 +export const $ps = process; + +export const isDev = !!$ps.env.VITE_DEV_SERVER_URL; + +export const $env = getEnv(); + +function getEnv() { + if (isDev) { + return $ps.env; + } + // 非开发环境,从 JSON 文件中获取环境变量 + const env = require('./env.json'); + return { + ...$ps.env, + ...env, + }; +} diff --git a/jeecgboot-vue3/electron/icons/app.ico b/jeecgboot-vue3/electron/icons/app.ico new file mode 100644 index 000000000..c0fbd91b4 Binary files /dev/null and b/jeecgboot-vue3/electron/icons/app.ico differ diff --git a/jeecgboot-vue3/electron/icons/installer.ico b/jeecgboot-vue3/electron/icons/installer.ico new file mode 100644 index 000000000..001572edf Binary files /dev/null and b/jeecgboot-vue3/electron/icons/installer.ico differ diff --git a/jeecgboot-vue3/electron/ipc/index.ts b/jeecgboot-vue3/electron/ipc/index.ts new file mode 100644 index 000000000..c8427aef0 --- /dev/null +++ b/jeecgboot-vue3/electron/ipc/index.ts @@ -0,0 +1,4 @@ +import {ipcMain} from 'electron' +import {openInBrowser} from "../utils"; + +ipcMain.on('open-in-browser', (event, url) => openInBrowser(url)); diff --git a/jeecgboot-vue3/electron/main.ts b/jeecgboot-vue3/electron/main.ts new file mode 100644 index 000000000..d4dc81d08 --- /dev/null +++ b/jeecgboot-vue3/electron/main.ts @@ -0,0 +1,56 @@ +import './ipc'; + +import { app, BrowserWindow, Menu } from 'electron'; +import { isDev } from './env'; +import { createMainWindow, createIndexWindow } from './utils/window'; +import { getAppInfo} from "./utils"; + +// 隐藏所有菜单 +Menu.setApplicationMenu(null); + +let mainWindow: BrowserWindow | null = null; + +function main() { + mainWindow = createMainWindow(); + return mainWindow; +} + +// 非开发环境,只允许一个实例运行 +if (!isDev) { + // 是否取得了单一实例锁 + const gotTheLock = app.requestSingleInstanceLock(); + + if (gotTheLock) { + app.on('second-instance', () => { + // 开启一个新的窗口 + createIndexWindow(); + }); + } else { + // 没有取得单一实例锁,则退出应用 + app.exit(0); + } +} + +// 生命周期管理 +app.whenReady().then(() => { + // 获取应用信息 + const $appInfo = getAppInfo(); + if ($appInfo?.productName && $appInfo?.appId) { + app.setName($appInfo.productName); + app.setAppUserModelId($appInfo.appId); + } + + main(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + main(); + } + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); diff --git a/jeecgboot-vue3/electron/paths.ts b/jeecgboot-vue3/electron/paths.ts new file mode 100644 index 000000000..ff3c991f0 --- /dev/null +++ b/jeecgboot-vue3/electron/paths.ts @@ -0,0 +1,18 @@ +import path from 'path' +import {isDev} from "./env"; + +export const _PATHS = getPaths() + +function getPaths() { + const _root = __dirname; + const publicRoot = path.join(_root, isDev ? '../../public' : '..'); + const preloadRoot = path.join(_root, 'preload') + + return { + electronRoot: _root, + publicRoot, + preloadRoot, + + appIcon: path.join(_root, `icons/app.ico`).replace(/[\\/]dist[\\/]/, '/'), + } +} \ No newline at end of file diff --git a/jeecgboot-vue3/electron/preload/index.ts b/jeecgboot-vue3/electron/preload/index.ts new file mode 100644 index 000000000..ce7c02ff4 --- /dev/null +++ b/jeecgboot-vue3/electron/preload/index.ts @@ -0,0 +1,5 @@ +import {contextBridge, ipcRenderer} from 'electron' + +contextBridge.exposeInMainWorld('_ELECTRON_PRELOAD_UTILS_', { + openInBrowser: (url: string) => ipcRenderer.send('open-in-browser', url), +}); diff --git a/jeecgboot-vue3/electron/script/buildAfter.ts b/jeecgboot-vue3/electron/script/buildAfter.ts new file mode 100644 index 000000000..50024eac0 --- /dev/null +++ b/jeecgboot-vue3/electron/script/buildAfter.ts @@ -0,0 +1 @@ +console.log('build elctron is done.'); \ No newline at end of file diff --git a/jeecgboot-vue3/electron/script/buildBefore.ts b/jeecgboot-vue3/electron/script/buildBefore.ts new file mode 100644 index 000000000..d1eac5dc6 --- /dev/null +++ b/jeecgboot-vue3/electron/script/buildBefore.ts @@ -0,0 +1,27 @@ +import path from 'path'; +import fs from 'fs'; + +const root = path.join(__dirname, '../../'); +const electronDistRoot = path.join(root, 'dist/electron'); + +let yamlName = 'electron-builder.yaml'; +const sourcePath = fs.readFileSync(path.join(root, yamlName), 'utf-8'); + +try { + // 通过正则表达式匹配 appId 和 productName + const appIdMatch = sourcePath.match(/appId:\s*['"]([^'"]+)['"]/); + const productNameMatch = sourcePath.match(/productName:\s*['"]([^'"]+)['"]/); + if (appIdMatch && productNameMatch) { + const fileContent = `${appIdMatch[0]}\n${productNameMatch[0]}`; + yamlName = 'env.yaml'; + const targetPath = path.join(electronDistRoot, yamlName); + fs.writeFileSync(targetPath, fileContent, 'utf-8'); + console.log(`✨ write dist ${yamlName} successfully.`); + } else { + throw new Error('appId or productName not found'); + } +} catch (e) { + console.error(e); + console.error(`请检查 ${yamlName} 是否存在,或者内容是否正确`); + process.exit(1); +} diff --git a/jeecgboot-vue3/electron/utils/index.ts b/jeecgboot-vue3/electron/utils/index.ts new file mode 100644 index 000000000..6bc206de6 --- /dev/null +++ b/jeecgboot-vue3/electron/utils/index.ts @@ -0,0 +1,31 @@ +import fs from 'fs'; +import path from 'path' +import {shell, dialog} from 'electron' +import {_PATHS} from "../paths"; +import {isDev} from "../env"; + +// 通过浏览器打开链接 +export function openInBrowser(url: string) { + return shell.openExternal(url); +} + + +export function getAppInfo(): any { + try { + const yamlPath = isDev ? path.join(_PATHS.publicRoot, '../electron-builder.yaml') : path.join(_PATHS.electronRoot, 'env.yaml'); + const yamlContent = fs.readFileSync(yamlPath, 'utf-8'); + // 通过正则表达式匹配 appId 和 productName + const appIdMatch = yamlContent.match(/appId:\s*['"]([^'"]+)['"]/); + const productNameMatch = yamlContent.match(/productName:\s*['"]([^'"]+)['"]/); + const appId = appIdMatch ? appIdMatch[1] : ''; + const productName = productNameMatch ? productNameMatch[1] : ''; + return {appId, productName} + } catch (e) { + dialog.showMessageBoxSync(null, { + type: 'error', + title: '错误', + message: '应用启动失败,请从官网下载最新版本安装包后重新安装!', + }); + process.exit(-1); + } +} diff --git a/jeecgboot-vue3/electron/utils/tray.ts b/jeecgboot-vue3/electron/utils/tray.ts new file mode 100644 index 000000000..2d9139152 --- /dev/null +++ b/jeecgboot-vue3/electron/utils/tray.ts @@ -0,0 +1,181 @@ +// tray = 系统托盘 +import path from 'path'; +import {Tray, Menu, app, dialog, nativeImage, BrowserWindow, Notification} from 'electron'; +import {_PATHS} from '../paths'; +import {$env, isDev} from '../env'; + +const TrayIcons = { + normal: nativeImage.createFromPath(path.join(_PATHS.publicRoot, 'logo.png')), + empty: nativeImage.createEmpty(), +}; + +// 创建托盘图标 +export function createTray(win: BrowserWindow) { + const tray = new Tray(TrayIcons.normal); + + const TrayUtils = useTray(tray, win); + + tray.setToolTip($env.VITE_GLOB_APP_TITLE! + (isDev ? ' (开发环境)' : '')); + + // 左键托盘图标显示主窗口 + tray.on('click', () => TrayUtils.showMainWindow()); + // 右键托盘图标显示托盘菜单 + tray.on('right-click', () => showTrayContextMenu()); + + function showTrayContextMenu() { + const trayContextMenu = getTrayMenus(win, TrayUtils); + // 弹出托盘菜单,不使用 setContextMenu 方法是因为要实时更新菜单内容 + tray.popUpContextMenu(trayContextMenu); + } +} + +export function useTray(tray: Tray, win: BrowserWindow) { + let isBlinking = false; + let blinkTimer: NodeJS.Timeout | null = null; + + function showMainWindow() { + win.show(); + } + + // 开始闪动 + function startBlink() { + isBlinking = true; + tray.setImage(TrayIcons.empty); + blinkTimer = setTimeout(() => { + tray.setImage(TrayIcons.normal); + setTimeout(() => { + if (isBlinking) { + startBlink(); + } + }, 500); + }, 500); + } + + // 结束闪动 + function stopBlink() { + isBlinking = false; + if (blinkTimer) { + clearTimeout(blinkTimer); + blinkTimer = null; + } + tray.setImage(TrayIcons.normal); + } + + // 发送桌面通知 + function sendDesktopNotice() { + // 判断是否支持桌面通知 + if (!Notification.isSupported()) { + // todo 实际开发中不需要提示,直接返回或者换一种提示方式 + dialog.showMessageBoxSync(win, { + type: 'error', + title: '错误', + message: '当前系统不支持桌面通知', + }); + return; + } + const ins = new Notification({ + title: '通知标题', + subtitle: '通知副标题', + body: '通知内容第一行\n通知内容第二行', + icon: TrayIcons.normal.resize({width: 32, height: 32}), + }); + + ins.on('click', () => { + dialog.showMessageBoxSync(win, { + type: 'info', + title: '提示', + message: '通知被点击', + }); + }); + + ins.show(); + } + + return { + showMainWindow, + + startBlink, + stopBlink, + isBlinking: () => isBlinking, + + sendDesktopNotice, + }; +} + +const MenuIcon = { + exit: nativeImage + .createFromDataURL( + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAA7EAAAOxAGVKw4bAAACJ0lEQVR4nH1TzWvUQBRP7fpxsWqVXsSLiAevRWhhN28msRJo981kay4WRBCF/QdEFJpbaUHw4kFBQTwUKX4gKh48KPiBBcGLJ1F0uzPZ7ibWXf0DIjObielS+mDIm/fxm9/85sWyBixN06E0CIaV3wB2XhC8puOWNZSG4Y7B+k2mi7Kl9l2n9rHnzvbWJoLRYn7r5jTViQjwzM8ynlC+AFyVgN2NU8G+Rnn6QETx3FfP223A/jeHfWqCsAUJ7Hlryh9Te0nYqiDsz9rE6VHVIABvNwEf/ADYk4OsZPeVFbwiCHtcZBVR9k4CJhJmDuUxwEVJ8H4fINOkC9Vjbeq/UTR1IgPturX3f93Z35+B7ddxgJL6dih/skF9zE9KCJ//5bDLpii1+npIuzolKTubC5gBxzarJo6vWWjrUP+etFlF+ds9lRFOXalN+NPEmxvRDS3KH34v8+PFIgNmTh0EahH+InGCwzoQEbYcuTMnlR8aYbaxGHFvRNiznssP6sA65UsxrdU1+hYnFhlpAGAkdvzlPLFu88mY8pcrVjCsxcqGapC2eYW249/tUH4xS4QaVQLeigi/YWJqPl4DlNRSrAwzSaoXIspeWUYrI9qXINglgT1qAt5JPG+kkNN5BSAJuyoJfhAVdmST4PlPBFASNs6rIgnspqC8HlF+SQAuRQTfKpYiEy6fwuIdP42P71T+t0l/TBKcE8AXm4DXBfB6w50+apgUhf4HZ5j+Z5+zNTAAAAAASUVORK5CYII=' + ) + .resize({ + width: 16, + height: 16, + }), +}; + +// 设置托盘菜单 +function getTrayMenus(win: BrowserWindow, TrayUtils: ReturnType) { + const {startBlink, stopBlink, sendDesktopNotice} = TrayUtils; + const isBlinking = TrayUtils.isBlinking(); + + return Menu.buildFromTemplate([ + ...(isDev + ? [ + { + label: '开发工具', + submenu: [ + { + label: '以下菜单仅显示在开发环境', + sublabel: '当前为开发环境', + enabled: false, + }, + {type: 'separator'}, + { + label: '切换 DevTools', + click: () => win.webContents.toggleDevTools(), + }, + { + label: `托盘图标${isBlinking ? '停止' : '开始'}闪烁`, + sublabel: '模拟新消息提醒', + click: () => (isBlinking ? stopBlink() : startBlink()), + }, + { + label: '发送桌面通知示例', + click: () => sendDesktopNotice(), + }, + ], + }, + {type: 'separator'}, + ] + : ([] as any)), + { + label: '显示主窗口', + // 文件图标 + icon: TrayIcons.normal.resize({width: 16, height: 16}), + click: () => win.show(), + }, + {type: 'separator'}, + { + label: '退出', + // base64图标 + icon: MenuIcon.exit, + click: () => { + // 弹出是否确认退出提示框 + const choice = dialog.showMessageBoxSync(win, { + type: 'question', + title: '提示', + message: '确定要退出应用吗?', + buttons: ['退出', '取消'], + defaultId: 1, + cancelId: 1, + noLink: true, + }); + // 用户选择了退出,直接 exit + if (choice === 0) { + // global.isQuitting = true; + app.exit(0); + } + }, + }, + ]); +} diff --git a/jeecgboot-vue3/electron/utils/window.ts b/jeecgboot-vue3/electron/utils/window.ts new file mode 100644 index 000000000..368a14404 --- /dev/null +++ b/jeecgboot-vue3/electron/utils/window.ts @@ -0,0 +1,85 @@ +import type {BrowserWindowConstructorOptions} from 'electron'; +import {BrowserWindow, dialog} from 'electron'; +import path from 'path'; +import {_PATHS} from '../paths'; +import {$env, isDev} from '../env'; +import {createTray} from './tray'; + +// 创建窗口 +export function createBrowserWindow(options?: BrowserWindowConstructorOptions) { + const win = new BrowserWindow({ + width: 1200, + height: 800, + webPreferences: { + preload: path.join(_PATHS.preloadRoot, 'index.js'), + nodeIntegration: false, + contextIsolation: true, + }, + // 应用图标 + icon: isDev ? _PATHS.appIcon : void 0, + ...options, + }); + + // 设置窗口打开处理器 + win.webContents.setWindowOpenHandler(({url}) => { + const win = createBrowserWindow(); + win.loadURL(url); + // 阻止创建新窗口,因为已经被接管 + return {action: 'deny'}; + }); + + // 当 beforeunload 阻止窗口关闭时触发 + win.webContents.on('will-prevent-unload', () => { + const choice = dialog.showMessageBoxSync(win, { + type: 'question', + title: '确认关闭吗?', + message: '系统可能不会保存您所做的更改。', + buttons: ['关闭', '取消'], + defaultId: 1, + cancelId: 1, + noLink: true, + }); + // 用户选择了关闭,直接销毁窗口 + if (choice === 0) { + win.destroy(); + } + }); + + return win; +} + +// 创建主窗口、系统托盘 +export function createMainWindow() { + const win = createIndexWindow() + + // 设置系统托盘图标 + createTray(win); + + // 主窗口尝试关闭时,默认不直接退出应用,而是隐藏到托盘 + win.on('close', (event) => { + event.preventDefault(); + win.hide(); + }); + + return win; +} + +// 创建索引窗口 +export function createIndexWindow() { + const win = createBrowserWindow({ + width: 1600, + height: 1000, + title: $env.VITE_GLOB_APP_TITLE!, + }); + + // 开发环境加载Vite服务,生产加载打包文件 + if (isDev) { + win.loadURL($env.VITE_DEV_SERVER_URL!) + // 开发环境下,自动打开调试工具 + // win.webContents.openDevTools() + } else { + win.loadFile(path.join(_PATHS.publicRoot, 'index.html')); + } + + return win; +} diff --git a/jeecgboot-vue3/src/api/common/api.ts b/jeecgboot-vue3/src/api/common/api.ts index 47d5cfbbf..19365ddce 100644 --- a/jeecgboot-vue3/src/api/common/api.ts +++ b/jeecgboot-vue3/src/api/common/api.ts @@ -14,6 +14,7 @@ enum Api { getDictItems = '/sys/dict/getDictItems/', getTableList = '/sys/user/queryUserComponentData', getCategoryData = '/sys/category/loadAllData', + refreshDragCache = '/drag/page/refreshCache', } /** @@ -148,3 +149,8 @@ export const getFileblob = (url, parameter) => { export const uploadMyFile = (url, data) => { return defHttp.uploadMyFile(url, data); }; +/** + * 刷新仪表盘缓存 + * @param params + */ +export const refreshDragCache = () => defHttp.get({ url: Api.refreshDragCache }, { isTransformResponse: false }); diff --git a/jeecgboot-vue3/src/assets/icons/robot.svg b/jeecgboot-vue3/src/assets/icons/robot.svg new file mode 100644 index 000000000..a1f035acb --- /dev/null +++ b/jeecgboot-vue3/src/assets/icons/robot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jeecgboot-vue3/src/components/Form/src/BasicForm.vue b/jeecgboot-vue3/src/components/Form/src/BasicForm.vue index ae4f5bfd2..6a70a56d5 100644 --- a/jeecgboot-vue3/src/components/Form/src/BasicForm.vue +++ b/jeecgboot-vue3/src/components/Form/src/BasicForm.vue @@ -367,6 +367,13 @@ margin-bottom: 24px; } // update-end--author:liaozhiyang---date:20240620---for:【TV360X-1420】校验时闪动 + + // 表单组件中间件样式 + .j-form-item-middleware { + flex: 1; + width: 100% + } + &.suffix-item { .ant-form-item-children { display: flex; @@ -376,6 +383,12 @@ margin-top: 4px; } + // 【QQYUN-12876】当紧凑型 suffix 时,表单组件中间件的宽度不占满 + &.suffix-compact .j-form-item-middleware { + flex: unset; + width: auto; + } + .suffix { display: inline-flex; padding-left: 6px; diff --git a/jeecgboot-vue3/src/components/Form/src/componentMap.ts b/jeecgboot-vue3/src/components/Form/src/componentMap.ts index dc098c48a..8f71df05d 100644 --- a/jeecgboot-vue3/src/components/Form/src/componentMap.ts +++ b/jeecgboot-vue3/src/components/Form/src/componentMap.ts @@ -65,6 +65,7 @@ import JTreeSelect from './jeecg/components/JTreeSelect.vue'; import JEllipsis from './jeecg/components/JEllipsis.vue'; import JSelectUserByDept from './jeecg/components/JSelectUserByDept.vue'; import JSelectUserByDepartment from './jeecg/components/JSelectUserByDepartment.vue'; +import JLinkTableCard from './jeecg/components/JLinkTableCard/JLinkTableCard.vue'; import JUpload from './jeecg/components/JUpload/JUpload.vue'; import JSearchSelect from './jeecg/components/JSearchSelect.vue'; import JAddInput from './jeecg/components/JAddInput.vue'; @@ -128,6 +129,7 @@ componentMap.set('JImageUpload', JImageUpload); componentMap.set('JDictSelectTag', JDictSelectTag); componentMap.set('JSelectDept', JSelectDept); componentMap.set('JAreaSelect', JAreaSelect); +componentMap.set('JLinkTableCard', JLinkTableCard); // componentMap.set( // 'JEditor', // createAsyncComponent(() => import('./jeecg/components/JEditor.vue')) diff --git a/jeecgboot-vue3/src/components/Form/src/components/FormItem.vue b/jeecgboot-vue3/src/components/Form/src/components/FormItem.vue index 08d5450d8..68ee3dc88 100644 --- a/jeecgboot-vue3/src/components/Form/src/components/FormItem.vue +++ b/jeecgboot-vue3/src/components/Form/src/components/FormItem.vue @@ -464,10 +464,17 @@ } function renderItem() { - const { itemProps, slot, render, field, suffix, component } = props.schema; + const { itemProps, slot, render, field, suffix, suffixCompact, component } = props.schema; const { labelCol, wrapperCol } = unref(itemLabelWidthProp); const { colon } = props.formProps; + // update-begin--author:sunjianlei---date:20250613---for:itemProps 属性支持函数形式 + let getItemProps = itemProps; + if (typeof getItemProps === 'function') { + getItemProps = getItemProps(unref(getValues)); + } + // update-end--author:sunjianlei---date:20250613---for:itemProps 属性支持函数形式 + if (component === 'Divider') { return ( @@ -486,8 +493,8 @@ -
+
diff --git a/jeecgboot-vue3/src/components/Form/src/jeecg/components/JDictSelectTag.vue b/jeecgboot-vue3/src/components/Form/src/jeecg/components/JDictSelectTag.vue index 8ca4988f4..c81062274 100644 --- a/jeecgboot-vue3/src/components/Form/src/jeecg/components/JDictSelectTag.vue +++ b/jeecgboot-vue3/src/components/Form/src/jeecg/components/JDictSelectTag.vue @@ -89,6 +89,8 @@ required: false, }, style: propTypes.any, + // 搜索时是否只搜索label + onlySearchByLabel: propTypes.bool.def(false), }, emits: ['options-change', 'change','update:value'], setup(props, { emit, refs }) { @@ -202,6 +204,10 @@ } } // update-end--author:liaozhiyang---date:20230914---for:【QQYUN-6514】 配置的时候,Y轴不能输入多个字段了,控制台报错 + if (props.onlySearchByLabel) { + // 如果开启了只在 label 中搜索,就不继续往下搜索value了 + return false; + } // 在 value 中搜索 return (option.value || '').toString().toLowerCase().indexOf(input.toLowerCase()) >= 0; } diff --git a/jeecgboot-vue3/src/components/Form/src/jeecg/components/JImageUpload.vue b/jeecgboot-vue3/src/components/Form/src/jeecg/components/JImageUpload.vue index e742b2f7c..a3c37df84 100644 --- a/jeecgboot-vue3/src/components/Form/src/jeecg/components/JImageUpload.vue +++ b/jeecgboot-vue3/src/components/Form/src/jeecg/components/JImageUpload.vue @@ -25,7 +25,7 @@
- + example @@ -38,7 +38,7 @@ import { useAttrs } from '/@/hooks/core/useAttrs'; import { useMessage } from '/@/hooks/web/useMessage'; import { getFileAccessHttpUrl, getHeaders, getRandom } from '/@/utils/common/compUtils'; - import { uploadUrl } from '/@/api/common/api'; + import { uploadUrl as systemUploadUrl } from '/@/api/common/api'; import { getToken } from '/@/utils/auth'; const { createMessage, createErrorModal } = useMessage(); @@ -79,6 +79,15 @@ required: false, default: 1, }, + uploadUrl: { + type: String, + default: systemUploadUrl, + }, + previewWidth: { + type: Number, + required: false, + default: 520, + }, }, emits: ['options-change', 'change', 'update:value'], setup(props, { emit, refs }) { @@ -256,7 +265,6 @@ multiple, headers, loading, - uploadUrl, beforeUpload, uploadVisible, handlePreview, diff --git a/jeecgboot-vue3/src/components/Form/src/jeecg/components/JLinkTableCard/JLinkTableCard.vue b/jeecgboot-vue3/src/components/Form/src/jeecg/components/JLinkTableCard/JLinkTableCard.vue new file mode 100644 index 000000000..15663d3d8 --- /dev/null +++ b/jeecgboot-vue3/src/components/Form/src/jeecg/components/JLinkTableCard/JLinkTableCard.vue @@ -0,0 +1,379 @@ + + + + + diff --git a/jeecgboot-vue3/src/components/Form/src/jeecg/components/JLinkTableCard/components/LinkTableListModal.vue b/jeecgboot-vue3/src/components/Form/src/jeecg/components/JLinkTableCard/components/LinkTableListModal.vue new file mode 100644 index 000000000..1ac73241e --- /dev/null +++ b/jeecgboot-vue3/src/components/Form/src/jeecg/components/JLinkTableCard/components/LinkTableListModal.vue @@ -0,0 +1,320 @@ + + + + + diff --git a/jeecgboot-vue3/src/components/Form/src/jeecg/components/JLinkTableCard/hooks/useLinkTable.ts b/jeecgboot-vue3/src/components/Form/src/jeecg/components/JLinkTableCard/hooks/useLinkTable.ts new file mode 100644 index 000000000..91b3244fb --- /dev/null +++ b/jeecgboot-vue3/src/components/Form/src/jeecg/components/JLinkTableCard/hooks/useLinkTable.ts @@ -0,0 +1,358 @@ +import { defHttp } from '/@/utils/http/axios'; +import { ref, watchEffect, computed, reactive } from 'vue'; +import { pick } from 'lodash-es'; +import { filterMultiDictText } from '/@/utils/dict/JDictSelectUtil'; +import { getFileAccessHttpUrl } from '/@/utils/common/compUtils'; + +function queryTableData(tableName, params) { + const url = '/online/cgform/api/getData/' + tableName; + return defHttp.get({ url, params }); +} + +function queryTableColumns(tableName, params) { + const url = '/online/cgform/api/getColumns/' + tableName; + return defHttp.get({ url, params }); +} + +export function useLinkTable(props) { + //TODO 目前只支持查询第一页的数据,可以输入关键字搜索 + const pageNo = ref('1'); + // 查询列 + const baseParam = ref({}); + // 搜素条件 + const searchParam = ref({}); + // 第一个文本列 + const mainContentField = ref(''); + //权限数据 + const auths = reactive({ + add: true, + update: true, + }); + + //显示列 + const textFieldArray = computed(() => { + if (props.textField) { + return props.textField.split(','); + } + return []; + }); + const otherColumns = ref([]); + // 展示的列 配置的很多列,但是只展示三行 + const realShowColumns = computed(() => { + const columns = otherColumns.value; + if (props.multi == true) { + return columns.slice(0, 3); + } else { + return columns.slice(0, 6); + } + }); + + watchEffect(async () => { + const table = props.tableName; + if (table) { + const valueField = props.valueField || ''; + const textField = props.textField || ''; + const arr: any[] = []; + if (valueField) { + arr.push(valueField); + } + if (textField) { + const temp = textField.split(','); + mainContentField.value = temp[0]; + for (const field of temp) { + arr.push(field); + } + } + const imageField = props.imageField || ''; + if (imageField) { + arr.push(imageField); + } + baseParam.value = { + linkTableSelectFields: arr.join(','), + }; + await resetTableColumns(); + await reloadTableLinkOptions(); + } + }); + + const otherFields = computed(() => { + const textField = props.textField || ''; + const others: any[] = []; + let labelField = ''; + if (textField) { + const temp = textField.split(','); + labelField = temp[0]; + for (let i = 0; i < temp.length; i++) { + if (i > 0) { + others.push(temp[i]); + } + } + } + return { + others, + labelField, + }; + }); + + // 选项 + const selectOptions = ref([]); + const tableColumns = ref([]); + const dictOptions = ref({}); + + async function resetTableColumns() { + const params = baseParam.value; + const data = await queryTableColumns(props.tableName, params); + tableColumns.value = data.columns; + if (data.columns) { + const imageField = props.imageField; + const arr = data.columns.filter((c) => c.dataIndex != mainContentField.value && c.dataIndex != imageField); + otherColumns.value = arr; + } + dictOptions.value = data.dictOptions; + // 权限数据 + console.log('隐藏的按钮', data.hideColumns); + if (data.hideColumns) { + const hideCols = data.hideColumns; + if (hideCols.indexOf('add') >= 0) { + auths.add = false; + } else { + auths.add = true; + } + if (hideCols.indexOf('update') >= 0) { + auths.update = false; + } else { + auths.update = true; + } + } + } + + async function reloadTableLinkOptions() { + const params = getLoadDataParams(); + const data = await queryTableData(props.tableName, params); + const records = data.records; + //tableTitle.value = data.head.tableTxt; + const dataList: any[] = []; + const { others, labelField } = otherFields.value; + const imageField = props.imageField; + if (records && records.length > 0) { + for (const rd of records) { + const temp = { ...rd }; + transData(temp); + const result = Object.assign({}, pick(temp, others), { id: temp.id, label: temp[labelField], value: temp[props.valueField] }); + if (imageField) { + result[imageField] = temp[imageField]; + } + dataList.push(result); + } + } + //添加一个空对象 为add操作占位 + // update-begin--author:liaozhiyang---date:20240607---for:【TV360X-1095】高级查询关联记录去掉编辑按钮及去掉记录按钮 + props.editBtnShow && dataList.push({}); + // update-end--author:liaozhiyang---date:20240607---for:【TV360X-1095】高级查询关联记录去掉编辑按钮及去掉记录按钮 + selectOptions.value = dataList; + } + + /** + * 数据简单翻译-字典 + * @param data + */ + function transData(data) { + const columns = tableColumns.value; + const dictInfo = dictOptions.value; + for (const c of columns) { + const { dataIndex, customRender } = c; + if (data[dataIndex] || data[dataIndex] === 0) { + if (customRender && customRender == dataIndex) { + //这样的就是 字典数据了 可以直接翻译 + if (dictInfo[customRender]) { + data[dataIndex] = filterMultiDictText(dictInfo[customRender], data[dataIndex]); + continue; + } + } + } + // 兼容后台翻译字段 + const dictText = data[dataIndex + '_dictText']; + if (dictText) { + data[dataIndex] = dictText; + } + } + } + + //获取加载数据的查询条件 + function getLoadDataParams() { + const params = Object.assign({ pageSize: 100, pageNo: pageNo.value }, baseParam.value, searchParam.value); + return params; + } + + //设置查询条件 + function addQueryParams(text) { + if (!text) { + searchParam.value = {}; + } else { + const arr = textFieldArray.value; + const params: any[] = []; + const fields: any[] = []; + for (let i = 0; i < arr.length; i++) { + if (i <= 1) { + fields.push(arr[i]); + params.push({ field: arr[i], rule: 'like', val: text }); + } + } + // params[arr[i]] = `*${text}*` + // params['selectConditionFields'] = fields.join(',') + // searchParam.value = params; + params['superQueryMatchType'] = 'or'; + params['superQueryParams'] = encodeURI(JSON.stringify(params)); + searchParam.value = params; + } + } + + async function loadOne(value) { + if (!value) { + return []; + } + let valueFieldName = props.valueField; + let params = { + ...baseParam.value, + pageSize: 100, + pageNo: pageNo.value, + }; + params['superQueryMatchType'] = 'and'; + let valueCondition = [{ field: valueFieldName, rule: 'in', val: value }]; + params['superQueryParams'] = encodeURI(JSON.stringify(valueCondition)); + const data = await queryTableData(props.tableName, params); + let records = data.records; + //tableTitle.value = data.head.tableTxt; + let dataList: any[] = []; + if (records && records.length > 0) { + for (let item of records) { + let temp = { ...item }; + transData(temp); + dataList.push(temp); + } + } + return dataList; + } + + /** + * true:数据一致;false:数据不一致 + * @param arr + * @param value + */ + function compareData(arr, value) { + if (!arr || arr.length == 0) { + return false; + } + const valueArray = value.split(','); + if (valueArray.length != arr.length) { + return false; + } + let flag = true; + for (const item of arr) { + const temp = item[props.valueField]; + if (valueArray.indexOf(temp) < 0) { + flag = false; + } + } + return flag; + } + + function formatData(formData) { + Object.keys(formData).map((k) => { + if (formData[k] instanceof Array) { + formData[k] = formData[k].join(','); + } + }); + } + + function initFormData(formData, linkFieldArray, record) { + if (!record) { + record = {}; + } + if (linkFieldArray && linkFieldArray.length > 0) { + for (const str of linkFieldArray) { + const arr = str.split(','); + //["表单字段,表字典字段"] + const field = arr[0]; + const dictField = arr[1]; + if (!formData[field]) { + const value = record[dictField] || ''; + formData[field] = [value]; + } else { + formData[field].push(record[dictField]); + } + } + } + } + + // 获取图片地址 + function getImageSrc(item) { + if (props.imageField) { + let url = item[props.imageField]; + // update-begin--author:liaozhiyang---date:20250517---for:【TV360X-38】关联记录空间,被关联数据优多个图片时,封面图片不展示 + if (typeof url === 'string') { + // 有多张图时默认取第一张 + url = url.split(',')[0]; + } + // update-end--author:liaozhiyang---date:20250517---for:【TV360X-38】关联记录空间,被关联数据优多个图片时,封面图片不展示 + return getFileAccessHttpUrl(url); + } + return ''; + } + const showImage = computed(() => { + if (props.imageField) { + return true; + } else { + return false; + } + }); + + return { + pageNo, + otherColumns, + realShowColumns, + selectOptions, + reloadTableLinkOptions, + textFieldArray, + addQueryParams, + tableColumns, + transData, + mainContentField, + loadOne, + compareData, + formatData, + initFormData, + getImageSrc, + showImage, + auths, + }; +} + +/** + * 使用固定高度的modal + */ +export function useFixedHeightModal() { + const minWidth = 800; + const popModalFixedWidth = ref(800); + let tempWidth = window.innerWidth - 300; + if (tempWidth < minWidth) { + tempWidth = minWidth; + } + popModalFixedWidth.value = tempWidth; + + // 弹窗高度控制 + const popBodyStyle = ref({}); + function resetBodyStyle() { + const height = window.innerHeight - 210; + popBodyStyle.value = { + height: height + 'px', + overflowY: 'auto', + }; + } + + return { + popModalFixedWidth, + popBodyStyle, + resetBodyStyle, + }; +} diff --git a/jeecgboot-vue3/src/components/Form/src/jeecg/components/JSearchSelect.vue b/jeecgboot-vue3/src/components/Form/src/jeecg/components/JSearchSelect.vue index 4b3f502de..6c0776d43 100644 --- a/jeecgboot-vue3/src/components/Form/src/jeecg/components/JSearchSelect.vue +++ b/jeecgboot-vue3/src/components/Form/src/jeecg/components/JSearchSelect.vue @@ -15,6 +15,9 @@ @search="loadData" @change="handleAsyncChange" @popupScroll="handlePopupScroll" + :mode="multiple?'multiple':''" + @select="handleSelect" + @deselect="handleDeSelect" >