v3.8.1发布,上传前端代码

pull/8499/head
JEECG 2025-06-25 16:04:02 +08:00
parent 3d414aaec8
commit 0148f45979
120 changed files with 4783 additions and 486 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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/

View File

@ -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

View File

@ -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---forQQYUN-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---forQQYUN-9685 electron
} catch (error) {
console.log(colors.red('configuration file configuration file failed to package:\n' + error));
}

View File

@ -49,6 +49,14 @@ export function wrapperEnv(envConf: Recordable): ViteEnv {
* 获取当前环境下生效的配置文件名
*/
function getConfFiles() {
// update-begin--author:sunjianlei---date:20250411---forQQYUN-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---forQQYUN-9685 electron
const script = process.env.npm_lifecycle_script;
// update-begin--author:liaozhiyang---date:20240326---forQQYUN-8690
const reg = new RegExp('NODE_ENV=([a-z_\\d]+)');

View File

@ -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()
// },
// }
// })
//
// }

View File

@ -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) {

View File

@ -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'

View File

@ -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

View File

@ -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,
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@ -0,0 +1,4 @@
import {ipcMain} from 'electron'
import {openInBrowser} from "../utils";
ipcMain.on('open-in-browser', (event, url) => openInBrowser(url));

View File

@ -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();
}
});

View File

@ -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[\\/]/, '/'),
}
}

View File

@ -0,0 +1,5 @@
import {contextBridge, ipcRenderer} from 'electron'
contextBridge.exposeInMainWorld('_ELECTRON_PRELOAD_UTILS_', {
openInBrowser: (url: string) => ipcRenderer.send('open-in-browser', url),
});

View File

@ -0,0 +1 @@
console.log('build elctron is done.');

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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<typeof useTray>) {
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);
}
},
},
]);
}

View File

@ -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;
}

View File

@ -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 });

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="64px" height="60.24px" viewBox="0 0 1088 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M972.8 742.4v192H115.2v-192H0V454.4h108.8v-192h864v192H1088v288h-115.2z m-108.8 83.2V371.2h-640v454.4h640zM659.2 96v108.8H428.8V96h230.4z m-256 582.4C358.4 678.4 320 640 320 595.2 320 550.4 358.4 512 403.2 512s83.2 38.4 83.2 83.2c0 44.8-38.4 83.2-83.2 83.2z m281.6 0c-44.8 0-83.2-38.4-83.2-83.2 0-44.8 38.4-83.2 83.2-83.2s83.2 38.4 83.2 83.2c-6.4 44.8-44.8 83.2-83.2 83.2z" /></svg>

After

Width:  |  Height:  |  Size: 647 B

View File

@ -367,6 +367,13 @@
margin-bottom: 24px;
}
// update-end--author:liaozhiyang---date:20240620---forTV360X-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;

View File

@ -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'))

View File

@ -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---foritemProps
let getItemProps = itemProps;
if (typeof getItemProps === 'function') {
getItemProps = getItemProps(unref(getValues));
}
// update-end--author:sunjianlei---date:20250613---foritemProps
if (component === 'Divider') {
return (
<Col span={24}>
@ -486,8 +493,8 @@
<Form.Item
name={field}
colon={colon}
class={{ 'suffix-item': showSuffix }}
{...(itemProps as Recordable)}
class={{ 'suffix-item': showSuffix, 'suffix-compact': showSuffix && suffixCompact }}
{...(getItemProps as Recordable)}
label={renderLabelHelpMessage()}
rules={handleRules()}
// update-begin--author:liaozhiyang---date:20240514---forissues/1244

View File

@ -1,5 +1,5 @@
<template>
<div :id="formItemId" style="flex: 1; width: 100%">
<div :id="formItemId" class="j-form-item-middleware">
<slot></slot>
</div>
</template>

View File

@ -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---forQQYUN-6514 Y
if (props.onlySearchByLabel) {
// label value
return false;
}
// value
return (option.value || '').toString().toLowerCase().indexOf(input.toLowerCase()) >= 0;
}

View File

@ -25,7 +25,7 @@
</a-button>
</div>
</a-upload>
<a-modal :open="previewVisible" :footer="null" @cancel="handleCancel()">
<a-modal :width="previewWidth" :open="previewVisible" :footer="null" @cancel="handleCancel()">
<img alt="example" style="width: 100%" :src="previewImage" />
</a-modal>
</div>
@ -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,

View File

@ -0,0 +1,379 @@
<template>
<div ref="tableLinkCardRef">
<div class="table-link-card">
<div style="width: 100%; height: 100%">
<div class="card-button" v-if="showButton">
<a-button @click="handleAddRecord"><PlusOutlined /> </a-button>
</div>
<a-row>
<a-col :span="fixedSpan ? fixedSpan : itemSpan" v-for="(record, index) in selectRecords" :key="index">
<div class="card-item" :class="{ 'disabled-chunk': detail == true }">
<!-- -->
<div class="card-item-left" :class="{ 'show-right-image': getImageSrc(record) }">
<span class="card-delete" v-if="disabled == false">
<minus-circle-filled @click="(e) => handleDeleteRecord(e, index)" />
</span>
<div class="card-inner">
<div class="card-main-content">{{ getMainContent(record) }}</div>
<div class="other-content">
<a-row>
<a-col :span="columnSpan" v-for="(col, cIndex) in realShowColumns" :key="cIndex">
<span class="label ellipsis">{{ col.title }}</span>
<span class="text ellipsis">{{ record[col.dataIndex] }}</span>
</a-col>
</a-row>
</div>
</div>
</div>
<div class="card-item-image" v-if="getImageSrc(record)">
<img v-if="getImageSrc(record)" :src="getImageSrc(record)" @error="handleImageError" />
</div>
</div>
</a-col>
</a-row>
</div>
</div>
<LinkTableListModal @register="registerListModal" :multi="multi" :id="popTableName" @success="addCard" />
</div>
</template>
<script>
import { propTypes } from '/@/utils/propTypes';
import { PlusOutlined, MinusCircleFilled } from '@ant-design/icons-vue';
import { computed, ref, watch, onMounted } from 'vue';
import { useLinkTable } from './hooks/useLinkTable';
import { useModal } from '/@/components/Modal';
import placeholderImage from '/@/assets/images/placeholderImage.png';
import { createAsyncComponent } from '@/utils/factory/createAsyncComponent';
export default {
name: 'JLinkTableCard',
inheritAttrs: false,
props: {
// value
valueField: propTypes.string.def(''),
//
textField: propTypes.string.def(''),
//
tableName: propTypes.string.def(''),
//
multi: propTypes.bool.def(false),
value: propTypes.oneOfType([propTypes.string, propTypes.number]),
// [",",","]
linkFields: propTypes.array.def([]),
//
disabled: propTypes.bool.def(false),
// detail
detail: propTypes.bool.def(false),
//
imageField: propTypes.string.def(''),
},
components: {
PlusOutlined,
MinusCircleFilled,
LinkTableListModal: createAsyncComponent(() => import('./components/LinkTableListModal.vue'), { loading: true }),
},
emits: ['change', 'update:value'],
setup(props, { emit }) {
const popTableName = computed(() => {
return props.tableName;
});
//model
const [registerListModal, { openModal: openListModal }] = useModal();
const selectValue = ref([]);
const selectRecords = ref([]);
const tableLinkCardRef = ref(null);
const fixedSpan = ref(0);
const showButton = computed(() => {
if (props.disabled == true) {
return false;
}
if (props.multi === false) {
//
if (selectRecords.value.length > 0) {
return false;
}
}
return true;
});
const {
auths,
otherColumns,
realShowColumns,
tableColumns,
textFieldArray,
transData,
loadOne,
compareData,
formatData,
initFormData,
getImageSrc,
showImage,
} = useLinkTable(props);
const itemSpan = computed(() => {
if (props.multi === true) {
return 12;
}
return 24;
});
const columnSpan = computed(() => {
if (props.multi === true) {
return 24;
}
return 12;
});
function getMainContent(record) {
if (record) {
if (textFieldArray.value.length > 0) {
let field = textFieldArray.value[0];
return record[field];
}
}
}
function prevent(e) {
e?.stopPropagation();
e?.preventDefault();
}
function handleAddRecord(e) {
prevent(e);
openListModal(true, {
// update-begin--author:liaozhiyang---date:20240517---forTV360X-43
selectedRowKeys: selectRecords.value.map((item) => item.id),
selectedRows: [...selectRecords.value],
// update-end--author:liaozhiyang---date:20240517---forTV360X-43
});
}
function addCard(data) {
// update-begin--author:liaozhiyang---date:20240517---forTV360X-43
let arr = [];
// update-end--author:liaozhiyang---date:20240517---forTV360X-43
for (let item of data) {
let temp = { ...item };
transData(temp);
arr.push(temp);
}
selectRecords.value = arr;
emitValue();
}
function updateCardData(formData) {
let arr = selectRecords.value;
for (let i = 0; i < arr.length; i++) {
if (arr[i].id === formData.id) {
let temp = { ...formData };
transData(temp);
arr.splice(i, 1, temp);
}
}
selectRecords.value = arr;
emitValue();
}
function handleDeleteRecord(e, index) {
prevent(e);
let temp = selectRecords.value;
if (temp && temp.length > index) {
temp.splice(index, 1);
selectRecords.value = temp;
}
emitValue();
}
function emitValue() {
let arr = selectRecords.value;
let values = [];
let formData = {};
let linkFieldArray = props.linkFields;
if (arr.length > 0) {
for (let i = 0; i < arr.length; i++) {
values.push(arr[i][props.valueField]);
initFormData(formData, linkFieldArray, arr[i]);
}
} else {
initFormData(formData, linkFieldArray);
}
let text = values.join(',');
formatData(formData);
emit('change', text, formData);
emit('update:value', text);
}
watch(
() => props.value,
async (val) => {
if (val) {
let flag = compareData(selectRecords.value, val);
if (flag === false) {
let arr = await loadOne(val);
selectRecords.value = arr;
}
//
if (props.linkFields && props.linkFields.length > 0) {
emitValue();
}
} else {
selectRecords.value = [];
}
},
{ immediate: true }
);
onMounted(() => {
// update-begin--author:liaozhiyang---date:20240522---forTV360X-281
if (tableLinkCardRef.value.offsetWidth < 250) {
fixedSpan.value = 24;
}
// update-end--author:liaozhiyang---date:20240522---forTV360X-281
});
// update-begin--author:liaozhiyang---date:20240529---forTV360X-389
const handleImageError = (event) => {
event.target.src = placeholderImage;
};
// update-end--author:liaozhiyang---date:20240529---forTV360X-389
return {
popTableName,
selectRecords,
otherColumns,
realShowColumns,
showButton,
selectValue,
handleAddRecord,
handleDeleteRecord,
getMainContent,
itemSpan,
columnSpan,
tableColumns,
addCard,
registerListModal,
updateCardData,
getImageSrc,
showImage,
auths,
tableLinkCardRef,
fixedSpan,
placeholderImage,
handleImageError,
};
},
};
</script>
<style scoped lang="less">
.table-link-card {
box-sizing: border-box;
position: relative;
width: 100%;
.card-button {
margin-bottom: 10px !important;
}
.card-item {
width: calc(100% - 10px);
display: inline-flex;
flex-direction: row;
margin: 0px 10px 10px 0px;
position: relative;
border-radius: 3px;
background-color: rgb(255, 255, 255);
box-shadow:
rgb(0 0 0 / 12%) 0px 1px 4px 0px,
rgb(0 0 0 / 12%) 0px 0px 2px 0px;
cursor: pointer;
&:hover {
/* box-shadow: rgb(0 0 0 / 12%) 0px 4px 12px 0px, rgb(0 0 0 / 12%) 0px 0px 2px 0px;*/
.card-item-left {
.card-delete {
display: inline-block;
}
}
}
&.disabled-chunk {
background: none;
box-shadow: none;
}
.card-item-image {
width: 100px;
padding-right: 8px;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
img {
width: 100%;
}
}
.card-item-left {
width: 100%;
display: inline-block;
&.show-right-image {
width: calc(100% - 100px);
}
.card-delete {
position: absolute;
top: -8px;
right: -8px;
font-size: 16px;
color: #757575;
line-height: 1em;
overflow: hidden;
display: none;
}
.card-inner {
flex: 1 1 0%;
padding: 12px 16px;
overflow: hidden;
padding-bottom: 10px;
.card-main-content {
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
margin-bottom: 8px;
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: rgb(51, 51, 51);
}
.other-content {
.text {
font-size: 12px !important;
display: inline-block;
width: 80%;
}
.label {
max-width: 160px;
color: rgb(158, 158, 158);
padding-right: 0.7em;
display: inline-block;
}
.ellipsis {
overflow: hidden;
height: 22px;
line-height: 22px;
text-overflow: ellipsis;
/* vertical-align: top;*/
white-space: nowrap;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,320 @@
<template>
<BasicModal
@register="registerModal"
:width="popModalFixedWidth"
:dialogStyle="{ top: '70px' }"
:bodyStyle="popBodyStyle"
:title="modalTitle"
wrapClassName="jeecg-online-pop-list-modal"
>
<template #footer>
<a-button key="back" @click="handleCancel"></a-button>
<a-button :disabled="submitDisabled" key="submit" type="primary" @click="handleSubmit" :loading="submitLoading">确定</a-button>
</template>
<BasicTable ref="tableRef" @register="registerTable" :rowSelection="rowSelection">
<!-- update-begin-author:taoyan date:2023-7-11 for: issues/4992 online表单开发 字段控件类型是关联记录 新增的时候选择列表可以添加查询么 -->
<template #tableTitle>
<a-input-search v-model:value="searchText" @search="onSearch" placeholder="请输入关键词,按回车搜索" style="width: 240px" />
</template>
<!-- update-end-author:taoyan date:2023-7-11 for: issues/4992 online表单开发 字段控件类型是关联记录 新增的时候选择列表可以添加查询么 -->
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)"> </TableAction>
</template>
<template #fileSlot="{ text }">
<span v-if="!text" style="font-size: 12px; font-style: italic"></span>
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download" size="small" @click="downloadRowFile(text)"> </a-button>
</template>
<template #imgSlot="{ text }">
<span v-if="!text" style="font-size: 12px; font-style: italic"></span>
<img v-else :src="getImgView(text)" alt="图片不存在" class="online-cell-image" @click="viewOnlineCellImage(text)" />
</template>
<template #htmlSlot="{ text }">
<div v-html="text"></div>
</template>
<template #pcaSlot="{ text }">
<div :title="getPcaText(text)">{{ getPcaText(text) }}</div>
</template>
<template #dateSlot="{ text, column }">
<span>{{ getFormatDate(text, column) }}</span>
</template>
</BasicTable>
</BasicModal>
</template>
<script lang="ts">
import { defineComponent, watch, ref, toRaw, computed, nextTick } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import { useMessage } from '/@/hooks/web/useMessage';
import { defHttp } from '/@/utils/http/axios';
import { useTableColumns } from '@/views/super/online/cgform/hooks/auto/useTableColumns';
import { createAsyncComponent } from '@/utils/factory/createAsyncComponent';
import { useFixedHeightModal } from '../hooks/useLinkTable';
export default defineComponent({
name: 'LinkTableListModal',
props: {
/**可以是表名 可以是ID*/
id: {
type: String,
default: '',
},
multi: {
type: Boolean,
default: false,
},
addAuth: {
type: Boolean,
default: true,
},
},
components: {
BasicModal,
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), { loading: true, delay: 1000 }),
TableAction: createAsyncComponent(() => import('/@/components/Table/src/components/TableAction.vue'), { loading: true, delay: 1000 }),
},
emits: ['success', 'register'],
setup(props, { emit }) {
const { createMessage: $message } = useMessage();
const tableRef = ref(null);
//
const { popModalFixedWidth, resetBodyStyle, popBodyStyle } = useFixedHeightModal();
const searchText = ref('');
const modalWidth = ref(800);
//useModalInner
const [registerModal, { closeModal }] = useModalInner((data) => {
searchText.value = '';
selectedRowKeys.value = data.selectedRowKeys;
selectedRows.value = data.selectedRows;
setTimeout(async () => {
await setPagination({ current: 1 });
await reload(); //
resetBodyStyle();
}, 100);
});
function handleCancel() {
closeModal();
}
const submitDisabled = computed(() => {
const arr = selectedRowKeys.value;
if (arr && arr.length > 0) {
return false;
}
return true;
});
const submitLoading = ref(false);
function handleSubmit() {
submitLoading.value = true;
let arr = toRaw(selectedRows.value);
if (arr && arr.length > 0) {
emit('success', arr);
closeModal();
}
setTimeout(() => {
submitLoading.value = false;
}, 200);
}
//---------------------------------------------
function queryTableData(params) {
const url = '/online/cgform/api/getData/' + props.id;
return defHttp.get({ url, params });
}
function list(params) {
params['column'] = 'id';
return new Promise(async (resolve, _reject) => {
const aa = await queryTableData(params);
resolve(aa);
});
}
const onlineTableContext = {
isPopList: true,
reloadTable() {
console.log('reloadTable');
},
isTree() {
return false;
},
};
const extConfigJson = ref<any>({});
// BasicTable
const { columns, downloadRowFile, getImgView, getPcaText, getFormatDate, handleColumnResult, hrefComponent, viewOnlineCellImage } =
useTableColumns(onlineTableContext, extConfigJson);
/**
* 查询table列信息 及其他配置
*/
function getColumnList() {
const url = '/online/cgform/api/getColumns/' + props.id;
return new Promise((resolve, reject) => {
defHttp.get({ url }, { isTransformResponse: false }).then((res) => {
if (res.success) {
resolve(res.result);
} else {
$message.warning(res.message);
reject();
}
});
});
}
const modalTitle = ref('');
watch(
() => props.id,
async () => {
let columnResult: any = await getColumnList();
handleColumnResult(columnResult);
modalTitle.value = columnResult.description;
},
{ immediate: true }
);
const { tableContext } = useListPage({
designScope: 'process-design',
pagination: true,
tableProps: {
title: '',
api: list,
clickToRowSelect: true,
columns: columns,
showTableSetting: false,
immediate: false,
//showIndexColumn: true,
canResize: false,
showActionColumn: false,
actionColumn: {
dataIndex: 'action',
slots: { customRender: 'action' },
},
useSearchForm: false,
beforeFetch: (params) => {
return addQueryParams(params);
},
},
});
const [registerTable, { reload, setPagination }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
watch(
() => props.multi,
(val) => {
if (val == true) {
rowSelection.type = 'checkbox';
} else {
rowSelection.type = 'radio';
}
},
{ immediate: true }
);
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleUpdate.bind(null, record),
},
];
}
function handleUpdate(record) {
console.log('handleUpdate', record);
}
function onSearch() {
reload();
}
const eqConditonTypes = ['int', 'double', 'Date', 'Datetime', 'BigDecimal'];
function addQueryParams(params) {
let text = searchText.value;
if (!text) {
params['superQueryMatchType'] = 'or';
params['superQueryParams'] = '';
return params;
}
let arr = columns.value;
let conditions: any[] = [];
if (arr && arr.length > 0) {
for (let item of arr) {
if (item.dbType) {
if (item.dbType == 'string') {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'like', val: text });
} else if (item.dbType == 'Date') {
if (text.length == '2020-10-10'.length) {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
}
} else if (item.dbType == 'Datetime') {
if (text.length == '2020-10-10 10:10:10'.length) {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
}
} else if (eqConditonTypes.indexOf(item.dbType)) {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
} else {
//text blob
}
}
}
}
params['superQueryMatchType'] = 'or';
params['superQueryParams'] = encodeURI(JSON.stringify(conditions));
return params;
}
// modal list
function handleDataSave(data) {
console.log('handleDateSave', data);
// update-begin--author:liaozhiyang---date:20250429---forissues/8163
let arr = [data, ...selectedRows.value];
// update-end--author:liaozhiyang---date:20250429---forissues/8163
emit('success', arr);
closeModal();
//reload();
}
return {
registerModal,
modalWidth,
handleCancel,
submitDisabled,
submitLoading,
handleSubmit,
registerTable,
getTableAction,
searchText,
onSearch,
downloadRowFile,
getImgView,
getPcaText,
getFormatDate,
hrefComponent,
viewOnlineCellImage,
rowSelection,
modalTitle,
reload,
popModalFixedWidth,
popBodyStyle,
handleDataSave,
tableRef,
};
},
});
</script>
<style scoped></style>

View File

@ -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<any>({});
//
const searchParam = ref<any>({});
//
const mainContentField = ref('');
//
const auths = reactive({
add: true,
update: true,
});
//
const textFieldArray = computed(() => {
if (props.textField) {
return props.textField.split(',');
}
return [];
});
const otherColumns = ref<any[]>([]);
//
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<any[]>([]);
const tableColumns = ref<any[]>([]);
const dictOptions = ref<any>({});
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---forTV360X-1095
props.editBtnShow && dataList.push({});
// update-end--author:liaozhiyang---date:20240607---forTV360X-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---forTV360X-38
if (typeof url === 'string') {
//
url = url.split(',')[0];
}
// update-end--author:liaozhiyang---date:20250517---forTV360X-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,
};
}

View File

@ -15,6 +15,9 @@
@search="loadData"
@change="handleAsyncChange"
@popupScroll="handlePopupScroll"
:mode="multiple?'multiple':''"
@select="handleSelect"
@deselect="handleDeSelect"
>
<template #notFoundContent>
<a-spin size="small" />
@ -33,6 +36,9 @@
:notFoundContent="loading ? undefined : null"
:dropdownAlign="{overflow: {adjustY: adjustY }}"
@change="handleChange"
:mode="multiple?'multiple':''"
@select="handleSelect"
@deselect="handleDeSelect"
>
<template #notFoundContent>
<a-spin v-if="loading" size="small" />
@ -83,6 +89,13 @@
default: ()=>{}
},
//update-end-author:taoyan date:2022-8-15 for: VUEN-1971 online 1
//update-begin---author:wangshuai---date:2025-04-17---for:issues/8101dict---
//
multiple:{
type: Boolean,
default: false
},
//update-end---author:wangshuai---date:2025-04-17---for:issues/8101dict---
},
emits: ['change', 'update:value'],
setup(props, { emit, refs }) {
@ -210,28 +223,55 @@
if (!selectedAsyncValue || !selectedAsyncValue.key || selectedAsyncValue.key !== value) {
defHttp.get({ url: `/sys/dict/loadDictItem/${dict}`, params: { key: value } }).then((res) => {
if (res && res.length > 0) {
let obj = {
key: value,
label: res,
};
if (props.value == value) {
selectedAsyncValue.value = { ...obj };
//update-begin---author:wangshuai---date:2025-04-17---for:issues/8101dict---
//
if(props.multiple){
if(value){
let arr: any = [];
//
let values = value.toString().split(',');
for (let i = 0; i < res.length; i++) {
let obj = {
key: values[i],
label: res[i],
};
arr.push(obj);
selectedValue.value.push(obj.key);
}
selectedAsyncValue.value = arr;
}
} else {
let obj = {
key: value,
label: res,
};
if (props.value == value) {
selectedAsyncValue.value = { ...obj };
}
//update-begin-author:taoyan date:2022-8-11 for: change--online
if(props.immediateChange == true){
emit('change', props.value);
}
//update-end-author:taoyan date:2022-8-11 for: change--online
//update-end---author:wangshuai---date:2025-04-17---for:issues/8101dict---
}
//update-begin-author:taoyan date:2022-8-11 for: change--online
if(props.immediateChange == true){
emit('change', props.value);
}
//update-end-author:taoyan date:2022-8-11 for: change--online
}
});
}
} else {
selectedValue.value = value.toString();
//update-begin-author:taoyan date:2022-8-11 for: change--online
if(props.immediateChange == true){
emit('change', value.toString());
//update-begin---author:wangshuai---date:2025-04-17---for:issues/8101dict---
if(!props.multiple){
selectedValue.value = value.toString();
//update-begin-author:taoyan date:2022-8-11 for: change--online
if(props.immediateChange == true){
emit('change', value.toString());
}
//update-end-author:taoyan date:2022-8-11 for: change--online
}else{
//
selectedValue.value = value.toString().split(',');
}
//update-end-author:taoyan date:2022-8-11 for: change--online
//update-begin---author:wangshuai---date:2025-04-17---for:issues/8101dict---
}
}
@ -306,35 +346,100 @@
* 同步改变事件
* */
function handleChange(value) {
selectedValue.value = value;
callback();
//update-begin---author:wangshuai---date:2025-04-17---for:issues/8101dict---
//change
if(!props.multiple){
selectedValue.value = value;
callback();
}
//update-end---author:wangshuai---date:2025-04-17---for:issues/8101dict---
}
/**
* 异步改变事件
* */
function handleAsyncChange(selectedObj) {
if (selectedObj) {
selectedAsyncValue.value = selectedObj;
selectedValue.value = selectedObj.key;
} else {
selectedAsyncValue.value = null;
selectedValue.value = null;
options.value = null;
loadData('');
}
callback();
// update-begin--author:liaozhiyang---date:20240524---forTV360X-426
// xloadSelectTexttrue
selectedObj ?? (loadSelectText.value = true);
// update-end--author:liaozhiyang---date:20240524---forTV360X-426
//update-begin---author:wangshuai---date:2025-04-17---for:issues/8101dict---
// 使change
if(!props.multiple){
if (selectedObj) {
selectedAsyncValue.value = selectedObj;
selectedValue.value = selectedObj.key;
} else {
selectedAsyncValue.value = null;
selectedValue.value = null;
options.value = null;
loadData('');
}
callback();
// update-begin--author:liaozhiyang---date:20240524---forTV360X-426
// xloadSelectTexttrue
selectedObj ?? (loadSelectText.value = true);
// update-end--author:liaozhiyang---date:20240524---forTV360X-426
}
//update-end---author:wangshuai---date:2025-04-17---for:issues/8101dict---
}
//update-begin---author:wangshuai---date:2025-04-17---for:issues/8101dict---
/**
* 异步值选中事件
* @param selectedObj
*/
function handleSelect(selectedObj){
let key = selectedObj;
if(props.async){
key = selectedObj.key;
}
//使select
if(props.multiple && key){
//selectedValue
if(props.async){
selectedValue.value.push(key);
}
selectedObj ?? (loadSelectText.value = true);
callback();
}
}
/**
* 异步值取消选中事件
* @param selectedObj
*/
function handleDeSelect(selectedObj){
let key = selectedObj;
if(props.async){
key = selectedObj.key;
}
//使select
if(props.multiple){
//selectedValue
if(props.async){
let findIndex = selectedValue.value.findIndex(item => item === key);
if(findIndex != -1){
selectedValue.value.splice(findIndex,1);
}
}
selectedObj ?? (loadSelectText.value = true);
callback();
}
}
//update-end---author:wangshuai---date:2025-04-17---for:issues/8101dict---
/**
*回调方法
* */
function callback() {
loadSelectText.value = false;
emit('change', unref(selectedValue));
emit('update:value', unref(selectedValue));
//update-begin---author:wangshuai---date:2025-04-17---for:issues/8101dict---
//
if(!props.multiple){
emit('change', unref(selectedValue));
emit('update:value', unref(selectedValue));
} else {
//
emit('change', unref(selectedValue).join(","));
emit('update:value', unref(selectedValue).join(","));
}
//update-end---author:wangshuai---date:2025-04-17---for:issues/8101dict---
}
/**
* 过滤选中option
@ -461,6 +566,8 @@
handleAsyncChange,
handleAsyncFocus,
handlePopupScroll,
handleSelect,
handleDeSelect,
};
},
});

View File

@ -71,6 +71,11 @@
*/
watch(selectValues, () => {
if (selectValues) {
// update-begin--author:liaozhiyang---date:20250616---forQQYUN-12869
if (props.value === undefined && selectValues.value?.length == 0) {
return;
}
// update-end--author:liaozhiyang---date:20250616---forQQYUN-12869
state.value = selectValues.value;
}
});

View File

@ -28,8 +28,12 @@
></a-select>
</a-col>
<a-col v-if="showButton" class="right">
<a-button v-if="buttonIcon" :preIcon="buttonIcon" type="primary" @click="openModal(true)" :disabled="disabled"></a-button>
<a-button v-else type="primary" @click="openModal(true)" :disabled="disabled">选择</a-button>
<a-button v-if="buttonIcon" :preIcon="buttonIcon" type="primary" @click="openModal(true)" :disabled="disabled">
{{ buttonText }}
</a-button>
<a-button v-else type="primary" @click="openModal(true)" :disabled="disabled">
{{ buttonText }}
</a-button>
</a-col>
</a-row>
</div>
@ -46,6 +50,7 @@
inheritAttrs: false,
props: {
showButton: propTypes.bool.def(true),
buttonText: propTypes.string.def('选择'),
disabled: propTypes.bool.def(false),
placeholder: {
type: String,

View File

@ -156,7 +156,9 @@ export interface FormSchema {
// Required
required?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
suffix?: string | number | ((values: RenderCallbackParams) => string | number);
suffix?: string | number | VueNode | ((values: RenderCallbackParams) => string | number | VueNode);
// QQYUN-12876 suffix
suffixCompact?: boolean;
// Validation rules
rules?: Rule[];
@ -164,7 +166,7 @@ export interface FormSchema {
rulesMessageJoinLabel?: boolean;
// Reference formModelItem
itemProps?: Partial<FormItem>;
itemProps?: Partial<FormItem> | ((renderCallbackParams: RenderCallbackParams) => Partial<FormItem>);
// col configuration outside formModelItem
colProps?: Partial<ColEx>;

View File

@ -157,5 +157,6 @@ export type ComponentType =
| 'linkRecordSelect'
| 'RangeTime'
| 'JRangeNumber'
| 'JLinkTableCard'
| 'JInputSelect';

View File

@ -8,6 +8,9 @@
:style="{ width }"
@click="currentSelectClick"
>
<template #suffix v-if="allowClear && currentSelect">
<CloseCircleFilled class="menu-current-close" @click.stop="clearCurrentSelect" />
</template>
<template #addonAfter>
<span class="cursor-pointer px-2 py-1 flex items-center" v-if="isSvgMode && currentSelect">
<SvgIcon :name="currentSelect" @click="currentSelectClick"/>
@ -77,7 +80,7 @@
import { useI18n } from '/@/hooks/web/useI18n';
import svgIcons from 'virtual:svg-icons-names';
import IconList from "./IconList.vue";
import { CloseCircleFilled } from '@ant-design/icons-vue';
// 使WebStormunused
const AInput = Input;
@ -104,6 +107,7 @@
mode: propTypes.oneOf<('svg' | 'iconify')[]>(['svg', 'iconify']).def('iconify'),
disabled: propTypes.bool.def(false),
clearSelect: propTypes.bool.def(false),
allowClear: propTypes.bool.def(false),
iconPrefixSave: propTypes.bool.def(true),
});
@ -196,6 +200,12 @@
iconOpen.value = false;
}
/**
* 清除当前选择图标
*/
function clearCurrentSelect(){
currentSelect.value = '';
}
onMounted(()=>{
//
initOtherIcon();
@ -227,5 +237,16 @@
height: 220px;
}
}
//
.menu-current-close {
color: #cccccc;
}
}
//
[data-theme='dark'] .@{prefix-cls} {
.menu-current-close {
color: #4f4f4f;
font-size: 12px;
}
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<div>
<keep-alive>
<component
v-if="currentModal"
v-bind="bindParams"
:key="currentModal"
:is="currentModal"
@register="modalRegCache[currentModal].register"
@reply="handReply"
@selected="reloadPage"
/>
</keep-alive>
<!-- 系统公告弹窗 -->
<DynamicNotice ref="showDynamNotice" v-bind="bindParams" />
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted } from 'vue';
import { useDragNotice } from '/@/hooks/web/useDragNotice';
import DynamicNotice from '@/views/monitor/mynews/DynamicNotice.vue';
export default defineComponent({
name: 'JDragNotice',
components: {
DynamicNotice,
},
setup() {
const {
initDragWebSocket,
currentModal,
modalParams,
modalRegCache,
bindParams,
reloadPage,
} = useDragNotice();
onMounted(() => {
initDragWebSocket();
});
return {
currentModal,
modalParams,
modalRegCache,
bindParams,
reloadPage,
};
},
});
</script>
<style scoped lang="less"></style>

View File

@ -13,6 +13,7 @@
import { getTenantId, getToken } from '/@/utils/auth';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { uploadFile } from '@/api/common/api';
import {$electron} from "@/electron";
type Lang = 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' | undefined;
@ -109,6 +110,13 @@
function init() {
const wrapEl = unref(wrapRef) as HTMLElement;
if (!wrapEl) return;
// vditorQQYUN-12053
let localCdn = '/resource/vditor@3.9.4';
if ($electron.isElectron()) {
localCdn = '.' + localCdn;
}
const bindValue = { ...attrs, ...props };
const insEditor = new Vditor(wrapEl, {
theme: getDarkMode.value === 'dark' ? 'dark' : 'classic',
@ -151,8 +159,8 @@
],
// update-end--author:liaozhiyang---date:20240520---forTV360X-146Markdown
mode: 'sv',
// cdn: 'https://cdn.jsdelivr.net/npm/vditor@3.9.6',
cdn: 'https://unpkg.com/vditor@3.10.1',
cdn: 'https://unpkg.com/vditor@3.10.8',
//cdn: localCdn,
fullscreen: {
index: 520,
},

View File

@ -17,7 +17,7 @@
{{ t('component.table.settingColumnShow') }}
</Checkbox>
<Checkbox v-model:checked="checkIndex" @change="handleIndexCheckChange">
<Checkbox :disabled="isTreeTable" v-model:checked="checkIndex" @change="handleIndexCheckChange">
{{ t('component.table.settingIndexColumnShow') }}
</Checkbox>
@ -143,6 +143,9 @@
setup(props, { emit, attrs }) {
const { t } = useI18n();
const table = useTableContext();
// update-begin--author:liaozhiyang---date:20250526---forissues/8301
const isTreeTable = computed(() => table.getBindValues.value.isTreeTable);
// update-end--author:liaozhiyang---date:20250526---forissues/8301
const popoverVisible = ref(false);
// update-begin--author:sunjianlei---date:20221101---for:
// nextTick(() => popoverVisible.value = false);
@ -479,6 +482,7 @@
defaultRowSelection,
handleColumnFixed,
getPopupContainer,
isTreeTable,
};
},
});

View File

@ -140,7 +140,12 @@ export function useCustomSelection(
// 解决selectedRowKeys在页面调用处使用ref失效
const value = unref(val);
if (Array.isArray(value) && !sameArray(value, selectedKeys.value)) {
setSelectedRowKeys(value);
// update-begin--author:liaozhiyang---date:20250429---for【issues/8163】关联记录夸页数据丢失
// 延迟是为了等watch selectedRows
setTimeout(() => {
setSelectedRowKeys(value);
}, 0);
// update-end--author:liaozhiyang---date:20250429---for【issues/8163】关联记录夸页数据丢失
}
},
{
@ -149,7 +154,22 @@ export function useCustomSelection(
}
);
// update-end--author:liaozhiyang---date:20240306---for【QQYUN-8390】部门人员组件点击重置未清空selectedRowKeys.value=[]watch没监听到加deep
// update-begin--author:liaozhiyang---date:20250429---for【issues/8163】关联记录夸页数据丢失
// 编辑时selectedRows可能会回填
watch(
() => unref(propsRef)?.rowSelection?.selectedRows,
(val: string[]) => {
const value: any = unref(val);
if (Array.isArray(value) && !sameArray(value, selectedRows.value)) {
selectedRows.value = value;
}
},
{
immediate: true,
deep: true,
}
);
// update-end--author:liaozhiyang---date:20250429---for【issues/8163】关联记录夸页数据丢失
/**
* 2024-03-06
* liaozhiyang
@ -581,7 +601,12 @@ export function useCustomSelection(
// 通过 selectedKeys 同步 selectedRows
function syncSelectedRows() {
if (selectedKeys.value.length !== selectedRows.value.length) {
setSelectedRowKeys(selectedKeys.value);
// update-begin--author:liaozhiyang---date:20250429---for【issues/8163】关联记录夸页数据丢失
// 延迟是为了等watch selectedRows
setTimeout(() => {
setSelectedRowKeys(selectedKeys.value);
}, 0);
// update-end--author:liaozhiyang---date:20250429---for【issues/8163】关联记录夸页数据丢失
}
}

View File

@ -146,25 +146,27 @@ export function useTableScroll(
bodyEl!.style.height = `${height}px`;
// update-begin--author:liaozhiyang---date:20240609---forissues/8374
if (maxHeight === undefined) {
if (unref(getPaginationInfo) && unref(getDataSourceRef).length) {
const pageSize = unref(getPaginationInfo)?.pageSize;
const current = unref(getPaginationInfo)?.current;
const total = unref(getPaginationInfo)?.total;
const tableBody = tableEl.querySelector('.ant-table-body') as HTMLElement;
const tr = tableEl.querySelector('.ant-table-tbody')?.children ?? [];
const lastrEl = tr[tr.length - 1] as HTMLElement;
const trHeight = lastrEl.offsetHeight;
const dataHeight = trHeight * pageSize;
if (tableBody && lastrEl) {
if (current === 1 && pageSize > unref(getDataSourceRef).length && total <= pageSize) {
tableBody.style.height = `${height}px`;
} else {
tableBody.style.height = `${dataHeight < height ? dataHeight : height}px`;
nextTick(() => {
if (maxHeight === undefined) {
if (unref(getPaginationInfo) && unref(getDataSourceRef).length) {
const pageSize = unref(getPaginationInfo)?.pageSize;
const current = unref(getPaginationInfo)?.current;
const total = unref(getPaginationInfo)?.total;
const tableBody = tableEl.querySelector('.ant-table-body') as HTMLElement;
const tr = tableEl.querySelector('.ant-table-tbody')?.children ?? [];
const lastrEl = tr[tr.length - 1] as HTMLElement;
const trHeight = lastrEl.offsetHeight;
const dataHeight = trHeight * pageSize;
if (tableBody && lastrEl) {
if (current === 1 && pageSize > unref(getDataSourceRef).length && total <= pageSize) {
tableBody.style.height = `${height}px`;
} else {
tableBody.style.height = `${dataHeight < height ? dataHeight : height}px`;
}
}
}
}
}
});
// update-end--author:liaozhiyang---date:20240609---forissues/8374
}
useWindowSizeFn(calcTableHeight, 280);

View File

@ -38,6 +38,8 @@ export interface TableRowSelection<T = any> extends ITableRowSelection {
* @type Function
*/
onSelectInvert?: (selectedRows: string[] | number[]) => any;
//issues/8163
selectedRows?: any[];
}
export interface TableCustomRecord<T> {

View File

@ -293,3 +293,27 @@ html[data-theme='dark'] {
margin-inline: 0;
}
// update-end--author:wangshuai---date:20240611---for【TV360X-1070】一对多内嵌为什么多这一块不从头对齐
// 单行文本溢出省略号
.ellipsis {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
// 两行省略
.ellipsis-2 {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-all;
}
// 三行省略
.ellipsis-3 {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}

View File

@ -0,0 +1,65 @@
import type {App} from "vue";
import {router} from "@/router";
import {useGlobSetting} from "@/hooks/setting";
const glob = useGlobSetting();
const _PRELOAD_UTILS = '_ELECTRON_PRELOAD_UTILS_';
export const $electron = {
// Electron
isElectron: () => glob.isElectronPlatform,
//
openInBrowser: bindUtils('openInBrowser') as (url: string) => void,
resolveRoutePath,
}
function bindUtils(n: string) {
const fn = window[_PRELOAD_UTILS]?.[n];
if (typeof fn?.bind === 'function') {
return fn.bind(null);
}
return () => console.warn(`Electron preload util ${n} is not a function`);
}
//
function resolveRoutePath(path: string) {
return window.location.origin + window.location.pathname + router.resolve(path).href;
}
/**
* 配置Electron
*/
export function setupElectron(_: App) {
// Electron
if (!$electron.isElectron()) {
return;
}
hookWindowOpen();
}
function hookWindowOpen() {
//
const originFunc = window.open;
// window.open
window['open'] = function (url, windowName, windowFeatures) {
url = typeof url === 'string' ? url.trim() : '';
if (!url) {
throw new Error('window.open: url is required');
}
// httphttps
if (/^https?:\/\//.test(url)) {
//
if (url.startsWith(window.location.origin) || url.startsWith(window['_CONFIG']['domianURL'])) {
//
return originFunc(url, windowName, windowFeatures);
}
// Electron
return $electron.openInBrowser(url) as any;
}
//
return originFunc(url, windowName, windowFeatures)
}
}

View File

@ -13,6 +13,7 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
VITE_GLOB_APP_OPEN_QIANKUN,
VITE_GLOB_DOMAIN_URL,
VITE_GLOB_ONLINE_VIEW_URL,
VITE_GLOB_RUN_PLATFORM,
// JEECG
VITE_GLOB_QIANKUN_MICRO_APP_NAME,
@ -40,6 +41,8 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
urlPrefix: VITE_GLOB_API_URL_PREFIX,
uploadUrl: VITE_GLOB_DOMAIN_URL,
viewUrl: VITE_GLOB_ONLINE_VIEW_URL,
// electron
isElectronPlatform: VITE_GLOB_RUN_PLATFORM === 'electron',
// JEECG
isQiankunMicro: VITE_GLOB_QIANKUN_MICRO_APP_NAME != null && VITE_GLOB_QIANKUN_MICRO_APP_NAME !== '',

View File

@ -43,7 +43,9 @@ export function useRootSetting() {
const getColorWeak = computed(() => appStore.getProjectConfig.colorWeak);
const getGrayMode = computed(() => appStore.getProjectConfig.grayMode);
// update-begin--author:liaozhiyang---date:20250407---forQQYUN-10952AI
const getAiIconShow = computed(() => appStore.getProjectConfig.aiIconShow);
// update-end--author:liaozhiyang---date:20250407---forQQYUN-10952AI
const getLockTime = computed(() => appStore.getProjectConfig.lockTime);
const getShowDarkModeToggle = computed(() => appStore.getProjectConfig.showDarkModeToggle);
@ -84,5 +86,6 @@ export function useRootSetting() {
getDarkMode,
setDarkMode,
getShowDarkModeToggle,
getAiIconShow,
};
}

View File

@ -0,0 +1,165 @@
import { ref, nextTick, getCurrentInstance, watch } from 'vue';
import { getToken } from '/@/utils/auth';
import md5 from 'crypto-js/md5';
import { connectWebSocket, onWebSocket } from '/@/hooks/web/useWebSocket';
import { useGlobSetting } from '/@/hooks/setting';
import { useModal } from '/@/components/Modal';
import { useUserStore } from '/@/store/modules/user';
import { isUrl } from '@/utils/is';
import { getQueryVariable, getUrlParams } from '@/utils';
import { useRouter } from 'vue-router';
import { useMessage } from '@/hooks/web/useMessage';
const { createMessage } = useMessage();
export function useDragNotice() {
//*********************************websocketbegin******************************************
const glob = useGlobSetting();
const { push, currentRoute } = useRouter();
const userStore = useUserStore();
const instance: any = getCurrentInstance();
// WebSocket
function initWebSocket() {
const token = getToken();
//token
const wsClientId = md5(token);
// WebSocketwshttpwsshttps
const url = glob.domainUrl?.replace('https://', 'wss://').replace('http://', 'ws://') + '/dragChannelSocket/' + wsClientId;
connectWebSocket(url);
onWebSocket(onWebSocketMessage);
}
async function onWebSocketMessage(data) {
console.log('仪表盘监听按钮点击事件websocket', data);
if (data?.CMD === 'drag') {
// url modal
const action = data.result.action;
// type
const type = data.result.type;
//url
let url = data.result.url;
//url
const record = data.result.records || {};
console.log('仪表盘监听点击事件类型type', type);
console.log('仪表盘监听点击事件动作action', action);
console.log('仪表盘监听点击事件路径url', url);
console.log('仪表盘监听点击事件参数', record);
//1.
if (action == 'url') {
//
if (url == 'fileUrl') {
url = record[url];
}
const urlParamsObj = getUrlParams(url);
if (url.startsWith('http')) {
window.open(url, '_blank');
} else {
push({ path: urlParamsObj.url, query: { ...urlParamsObj.params, ...record } });
}
} else {
//2.
switch (type) {
case 'email':
//
handleOpenType('email', { record });
break;
default:
break;
}
}
}
}
//*********************************websocketend******************************************
//*********************************begin*******************************
//
const currentModal = ref<string | null>(null);
//
const modalParams = ref<Recordable>({});
//
const modalRegCache = ref<Recordable>({});
//
const bindParams = ref<Recordable>({});
/**
* 根据类型打开不同弹窗
* @param type
* @param params
*/
async function handleOpenType(type, params) {
currentModal.value = null;
modalParams.value = { ...params };
switch (type) {
case 'email':
//
currentModal.value = 'EoaMailBoxInModal';
break;
default:
currentModal.value = null;
break;
}
//
initModalRegister();
await nextTick(() => {
if (modalRegCache.value[currentModal.value!]?.isRegister) {
console.log('已注冊,走缓存');
modalRegCache.value[currentModal.value!].modalMethods.openModal(true, modalParams.value);
}
});
}
/**
* 初始化弹窗注册
*/
function initModalRegister() {
//null
if (!currentModal.value) {
return;
}
//
if (!modalRegCache.value[currentModal.value]) {
const [registerModal, modalMethods] = useModal();
modalRegCache.value[currentModal.value] = {
isRegister: false,
register: bindRegisterModal(registerModal, modalMethods),
modalMethods,
};
}
}
/**
* 绑定注册弹窗
* @param regFn
* @param modalMethod
*/
function bindRegisterModal(regFn, modalMethod) {
return async (...args) => {
console.log('开始注册:', currentModal.value);
await regFn(...args);
console.log('注册完成:', currentModal.value);
//
modalMethod.openModal(true, modalParams.value);
//
modalRegCache.value[currentModal.value!].isRegister = true;
};
}
//*********************************end******************************************
//
function reloadPage() {
const iframes: any = document.getElementsByClassName('jeecg-iframe-page__main');
// HTMLCollection
const iframeArray = Array.from(iframes);
if (currentRoute.value?.meta?.frameSrc && currentRoute.value?.meta?.frameSrc.indexOf('/drag/view?pageId=') >= 0) {
const targetIframe: any = iframeArray.find((iframe: any) => iframe.src == currentRoute.value?.meta?.frameSrc);
console.log('targetIframe', targetIframe);
if (targetIframe) {
targetIframe.contentWindow.postMessage({ reload: true }, '*');
}
}
}
return {
initDragWebSocket: initWebSocket,
handleOpenType,
currentModal,
modalParams,
modalRegCache,
bindParams,
reloadPage,
};
}

View File

@ -43,7 +43,7 @@ export function useGo(_router?: Router) {
/**
* @description: redo current page
*/
export const useRedo = (_router?: Router) => {
export const useRedo = (_router?: Router, otherQuery?: Recordable) => {
const { push, currentRoute } = _router || useRouter();
const { query, params = {}, name, fullPath } = unref(currentRoute.value);
function redo(): Promise<boolean> {
@ -54,6 +54,11 @@ export const useRedo = (_router?: Router) => {
}
// update-begin--author:liaozhiyang---date:20231123---forQQYUN-7099404
const tabStore = useMultipleTabStore();
if (otherQuery && Object.keys(otherQuery).length > 0) {
Object.keys(otherQuery).forEach((key) => {
params[key] = otherQuery[key];
});
}
if (name && Object.keys(params).length > 0) {
tabStore.setRedirectPageParam({
redirect_type: 'name',

View File

@ -13,6 +13,7 @@ enum TableActionEnum {
CLOSE_RIGHT,
CLOSE_OTHER,
CLOSE_CURRENT,
HOME_DESIGN,
CLOSE,
}
@ -66,6 +67,10 @@ export function useTabs(_router?: Router) {
await tabStore.refreshPage(router);
break;
case TableActionEnum.HOME_DESIGN:
await tabStore.changeDesign(router);
break;
case TableActionEnum.CLOSE_ALL:
await tabStore.closeAllTab(router);
break;
@ -111,6 +116,7 @@ export function useTabs(_router?: Router) {
return {
refreshPage: () => handleTabAction(TableActionEnum.REFRESH),
changeDesign: () => handleTabAction(TableActionEnum.HOME_DESIGN),
// update-begin--author:liaozhiyang---date:20240605---forTV360X-732使
closeAll: (tab) => handleTabAction(TableActionEnum.CLOSE_ALL, tab),
closeLeft: (tab) => handleTabAction(TableActionEnum.CLOSE_LEFT, tab),

View File

@ -57,6 +57,7 @@
import { removeAuthCache, setAuthCache } from '/src/utils/auth';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { getRefPromise } from '/@/utils/index';
import { refreshDragCache } from "@/api/common/api";
type MenuEvent = 'logout' | 'doc' | 'lock' | 'cache' | 'depart';
const { createMessage } = useMessage();
@ -122,6 +123,8 @@
//
async function clearCache() {
const result = await refreshCache();
const dragRes = await refreshDragCache();
console.log('dragRes', dragRes);
if (result.success) {
const res = await queryAllDictItems();
removeAuthCache(DB_DICT_DATA_KEY);

View File

@ -38,8 +38,8 @@
<UserDropDown :theme="getHeaderTheme" />
<SettingDrawer v-if="getShowSetting" :class="`${prefixCls}-action__item`" />
<!-- ai助手
<Aide></Aide> -->
<!-- ai助手 -->
<Aide v-if="getAiIconShow"></Aide>
</div>
</Header>
<LoginSelect ref="loginSelectRef" @success="loginSelectOk"></LoginSelect>
@ -105,7 +105,7 @@
const { prefixCls } = useDesign('layout-header');
const userStore = useUserStore();
const { getShowTopMenu, getShowHeaderTrigger, getSplit, getIsMixMode, getMenuWidth, getIsMixSidebar } = useMenuSetting();
const { getUseErrorHandle, getShowSettingButton, getSettingButtonPosition } = useRootSetting();
const { getUseErrorHandle, getShowSettingButton, getSettingButtonPosition, getAiIconShow } = useRootSetting();
const { title } = useGlobSetting();
const {
@ -215,7 +215,8 @@
loginSelectOk,
loginSelectRef,
title,
t
t,
getAiIconShow
};
},
});

View File

@ -46,6 +46,7 @@ export default defineComponent({
getLockTime,
getShowDarkModeToggle,
getThemeColor,
getAiIconShow,
} = useRootSetting();
const { getOpenPageLoading, getBasicTransition, getEnableTransition, getOpenNProgress } = useTransitionSetting();
@ -315,6 +316,8 @@ export default defineComponent({
<SwitchItem title={t('layout.setting.grayMode')} event={HandlerEnum.GRAY_MODE} def={unref(getGrayMode)} />
<SwitchItem title={t('layout.setting.colorWeak')} event={HandlerEnum.COLOR_WEAK} def={unref(getColorWeak)} />
<SwitchItem title={t('layout.setting.aiIconSHow')} event={HandlerEnum.AI_ICON_SHOW} def={unref(getAiIconShow)} />
</>
);
}

View File

@ -52,6 +52,7 @@ export enum HandlerEnum {
OPEN_PROGRESS,
OPEN_PAGE_LOADING,
OPEN_ROUTE_TRANSITION,
AI_ICON_SHOW,
}
//

View File

@ -199,7 +199,10 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
case HandlerEnum.COLOR_WEAK:
updateColorWeak(value);
return { colorWeak: value };
// update-begin--author:liaozhiyang---date:20250407---forQQYUN-10952AI
case HandlerEnum.AI_ICON_SHOW:
return { aiIconShow: value };
// update-end--author:liaozhiyang---date:20250407---forQQYUN-10952AI
case HandlerEnum.SHOW_LOGO:
return { showLogo: value };

View File

@ -27,10 +27,9 @@
<!-- <FullscreenOutlined /> -->
<router-link to="/ai" class="ai-icon">
<a-tooltip title="AI助手" placement="left">
<!-- <svg t="1706259688149" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2056" width="17" height="17">-->
<!-- <path d="M826.368 325.632c0-7.168 2.048-10.24 10.24-10.24h123.904c7.168 0 10.24 2.048 10.24 10.24v621.568c0 7.168-2.048 10.24-10.24 10.24h-122.88c-8.192 0-10.24-4.096-10.24-10.24l-1.024-621.568z m-8.192-178.176c0-50.176 35.84-79.872 79.872-79.872 48.128 0 79.872 32.768 79.872 79.872 0 52.224-33.792 79.872-81.92 79.872-46.08 1.024-77.824-27.648-77.824-79.872zM462.848 584.704C441.344 497.664 389.12 307.2 368.64 215.04h-2.048c-16.384 92.16-58.368 247.808-92.16 369.664h188.416zM243.712 712.704l-62.464 236.544c-2.048 7.168-4.096 8.192-12.288 8.192H54.272c-8.192 0-10.24-2.048-8.192-12.288l224.256-783.36c4.096-13.312 7.168-26.624 8.192-65.536 0-6.144 2.048-8.192 7.168-8.192H450.56c6.144 0 8.192 2.048 10.24 8.192l250.88 849.92c2.048 7.168 0 10.24-7.168 10.24H573.44c-7.168 0-10.24-2.048-12.288-7.168l-65.536-236.544c1.024 1.024-251.904 0-251.904 0z" fill="#333333" p-id="19816"></path>-->
<!-- </svg>-->
<svg t="1737024931936" class="icon" viewBox="0 0 3011 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4248" width="256" height="256"><path d="M2028.242824 558.561882h415.021176V445.620706h-346.352941V321.415529h346.352941V210.823529l-328.463059 10.842353a2367.969882 2367.969882 0 0 0-31.021176-127.09647c341.955765-4.397176 602.352941-13.492706 781.131294-27.286588l41.441882 124.265411-320.933647 14.095059v115.772235h307.802353v124.205177h-307.802353v112.941176h375.506824v127.096471h-375.506824v113.844706c0 30.117647-5.782588 56.982588-17.468235 80.474353a114.447059 114.447059 0 0 1-50.778353 53.187764c-22.287059 11.926588-41.261176 19.154824-56.922353 21.684706-15.36 2.770824-82.522353 5.300706-201.426824 7.529412a5065.065412 5065.065412 0 0 0-31.984941-142.155294c64.632471 4.999529 114.447059 7.529412 149.624471 7.529412 44.574118 0 66.861176-23.190588 66.861176-69.632v-72.463059h-415.081411v-127.096471zM1685.624471 956.717176a1296.865882 1296.865882 0 0 0-27.286589-133.662117 2187.745882 2187.745882 0 0 0 96.978824 3.794823c18.191059 0 32.587294-4.758588 43.248941-14.155294 10.661647-9.697882 17.468235-23.491765 20.239059-41.381647 3.132235-17.889882 6.023529-66.379294 8.432941-145.408 2.590118-79.390118 3.614118-179.621647 3.373177-300.754823h-109.206589c-3.734588 212.389647-24.455529 364.604235-62.102588 456.523294-37.345882 91.919059-77.161412 156.491294-119.567059 193.837176-28.551529-27.888941-59.632941-54.392471-93.123764-79.510588-142.757647 12.830118-265.456941 25.298824-368.037648 37.165176l-15.962352-126.132705c14.095059-0.602353 28.190118-1.385412 42.345411-2.349177V88.003765h377.374118v687.043764c12.227765-1.204706 24.154353-2.349176 35.779765-3.312941 50.838588-95.653647 77.040941-244.555294 78.607058-446.58447h-85.172705V200.944941h87.04c0.301176-47.706353 0.481882-100.412235 0.481882-158.117647h125.168941c0 58.368-0.301176 111.073882-0.963765 158.117647H1957.647059l-9.878588 525.613177c-1.566118 62.464-5.481412 104.688941-11.745883 126.614588-6.324706 22.287059-16.805647 41.441882-31.563294 57.404235-14.757647 16.022588-32.768 27.105882-54.091294 33.430588-21.082353 6.264471-75.896471 10.480941-164.743529 12.649412z m-321.837177-759.567058h-140.227765V327.077647h140.227765V197.150118z m-140.227765 359.604706h140.227765V426.767059h-140.227765v129.867294z m140.227765 229.616941v-129.92753h-140.227765v140.288l140.227765-10.36047zM963.764706 114.326588V843.294118h-155.286588V114.326588H963.764706zM716.679529 843.294118H547.297882l-54.573176-167.032471H227.689412L174.140235 843.294118H5.662118l266.842353-728.96753h182.512941L716.739765 843.294118zM455.981176 556.212706L373.157647 303.585882c-5.300706-16.022588-9.035294-37.165176-11.264-63.548235h-4.216471a269.010824 269.010824 0 0 1-12.709647 61.680941L261.662118 556.212706H455.981176z" fill="#1F1F1F" p-id="4249"></path></svg>
<svg t="1706259688149" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2056" width="17" height="17">
<path d="M826.368 325.632c0-7.168 2.048-10.24 10.24-10.24h123.904c7.168 0 10.24 2.048 10.24 10.24v621.568c0 7.168-2.048 10.24-10.24 10.24h-122.88c-8.192 0-10.24-4.096-10.24-10.24l-1.024-621.568z m-8.192-178.176c0-50.176 35.84-79.872 79.872-79.872 48.128 0 79.872 32.768 79.872 79.872 0 52.224-33.792 79.872-81.92 79.872-46.08 1.024-77.824-27.648-77.824-79.872zM462.848 584.704C441.344 497.664 389.12 307.2 368.64 215.04h-2.048c-16.384 92.16-58.368 247.808-92.16 369.664h188.416zM243.712 712.704l-62.464 236.544c-2.048 7.168-4.096 8.192-12.288 8.192H54.272c-8.192 0-10.24-2.048-8.192-12.288l224.256-783.36c4.096-13.312 7.168-26.624 8.192-65.536 0-6.144 2.048-8.192 7.168-8.192H450.56c6.144 0 8.192 2.048 10.24 8.192l250.88 849.92c2.048 7.168 0 10.24-7.168 10.24H573.44c-7.168 0-10.24-2.048-12.288-7.168l-65.536-236.544c1.024 1.024-251.904 0-251.904 0z" fill="#333333" p-id="19816"></path>
</svg>
</a-tooltip>
</router-link>
</div>
@ -178,13 +177,10 @@
align-items: center;
justify-content: center;
cursor: pointer;
width: 50px;
padding: 0 4px;
height: 50px;
width: 36px;
color: @text-color;
text-align: center;
border-left: 1px solid @border-color-base;
overflow: hidden;
}
}
// update-end--author:liaozhiyang---date:20241016---forissues/7345

View File

@ -22,4 +22,5 @@ export enum MenuEventEnum {
CLOSE_OTHER,
CLOSE_ALL,
SCALE,
HOME_DESIGN,
}

View File

@ -18,7 +18,7 @@ export function useTabDropdown(tabContentProps: TabContentProps, getIsTabs: Comp
const { t } = useI18n();
const tabStore = useMultipleTabStore();
const { currentRoute } = useRouter();
const { refreshPage, closeAll, close, closeLeft, closeOther, closeRight } = useTabs();
const { refreshPage, closeAll, close, closeLeft, closeOther, closeRight, changeDesign } = useTabs();
const getTargetTab = computed((): RouteLocationNormalized => {
return unref(getIsTabs) ? tabContentProps.tabItem : unref(currentRoute);
@ -71,6 +71,12 @@ export function useTabDropdown(tabContentProps: TabContentProps, getIsTabs: Comp
event: MenuEventEnum.REFRESH_PAGE,
text: t('layout.multipleTab.reload'),
disabled: refreshDisabled,
},
{
icon: 'ant-design:setting-outlined',
event: MenuEventEnum.HOME_DESIGN,
text: t('layout.multipleTab.homeDesign'),
disabled: path !== '/PortalView',
divider: true,
},
// {
@ -163,6 +169,12 @@ export function useTabDropdown(tabContentProps: TabContentProps, getIsTabs: Comp
closeAll(state.current);
// update-end--author:liaozhiyang---date:20240605---forTV360X-732使
break;
// Close all
case MenuEventEnum.HOME_DESIGN:
// update-begin--author:liaozhiyang---date:20240605---forTV360X-732使
changeDesign();
// update-end--author:liaozhiyang---date:20240605---forTV360X-732使
break;
}
}
return { getDropMenuList, handleMenuEvent, handleContextMenu };

View File

@ -8,7 +8,7 @@ export default {
dropdownItemSwitchDepart: 'Switch Department',
dropdownItemRefreshCache: 'Clean cache',
dropdownItemSwitchAccount: 'Account Setting',
tooltipErrorLog: 'Error log',
tooltipLock: 'Lock screen',
tooltipNotify: 'Notification',
@ -33,6 +33,7 @@ export default {
closeRight: 'Close Right',
closeOther: 'Close Other',
closeAll: 'Close All',
homeDesign: 'Home Design',
},
setting: {
// content mode

View File

@ -34,6 +34,7 @@ export default {
closeRight: '关闭右侧',
closeOther: '关闭其它',
closeAll: '关闭全部',
homeDesign: '设计模式',
},
setting: {
// content mode
@ -105,6 +106,7 @@ export default {
fullContent: '全屏内容',
grayMode: '灰色模式',
colorWeak: '色弱模式',
aiIconSHow: 'Ai图标显示',
progress: '顶部进度条',
switchLoading: '切换loading',

View File

@ -14,6 +14,7 @@ import { setupRouterGuard } from '/@/router/guard';
import { setupStore } from '/@/store';
import { setupGlobDirectives } from '/@/directives';
import { setupI18n } from '/@/locales/setupI18n';
import { setupElectron } from "@/electron";
import { registerGlobComp } from '/@/components/registerGlobComp';
import { registerThirdComp } from '/@/settings/registerThirdComp';
import { registerSuper } from '/@/views/super/registerSuper';
@ -89,6 +90,9 @@ async function bootstrap(props?: MainAppProps) {
//
await registerThirdComp(app);
// electron
setupElectron(app)
// ( https://next.router.vuejs.org/api/#isready)
await router.isReady();

View File

@ -1,6 +1,7 @@
import type { RouteRecordRaw } from 'vue-router';
import type { App } from 'vue';
import { $electron } from "@/electron";
import { basicRoutes } from './routes';
import {createRouter as createVueRouter, destroyRouter, router} from './router'
@ -18,10 +19,13 @@ getRouteNames(basicRoutes);
*/
export function createRouter() {
let router = createVueRouter({
routes: basicRoutes as unknown as RouteRecordRaw[],
strict: true,
scrollBehavior: () => ({left: 0, top: 0}),
})
routes: basicRoutes as unknown as RouteRecordRaw[],
strict: true,
scrollBehavior: () => ({left: 0, top: 0}),
},
// Electron 使 hash
$electron.isElectron(),
)
// TODO QQYUN-4517
// @ts-ignore

View File

@ -2,7 +2,7 @@
* 路由实例存储文件请勿轻易添加其他代码防止出现 HMR 或其他问题
*/
import type {Router, RouterHistory} from 'vue-router';
import {createRouter as createVueRouter, createWebHistory, RouterOptions} from 'vue-router';
import {createRouter as createVueRouter, createWebHistory, createWebHashHistory, RouterOptions} from 'vue-router';
export let router: Router = null as unknown as Router;
@ -15,9 +15,11 @@ let webHistory: Nullable<RouterHistory> = null;
/**
* 创建路由
* @param options 参数
* @param useHashHistory 是否使用 hash 路由true使用false不使用hash路由
*/
export function createRouter(options: Partial<RouterOptions>) {
webHistory = createWebHistory(import.meta.env.VITE_PUBLIC_PATH);
export function createRouter(options: Partial<RouterOptions>, useHashHistory = false) {
const createFn = useHashHistory ? createWebHashHistory : createWebHistory;
webHistory = createFn(import.meta.env.VITE_PUBLIC_PATH);
// app router
let router = createVueRouter({
history: webHistory,

View File

@ -67,6 +67,9 @@ const setting: ProjectConfig = {
// copyright
showFooter: false,
// ai
aiIconShow: false,
//
headerSetting: {
//

View File

@ -16,6 +16,7 @@ import { MULTIPLE_TABS_KEY } from '/@/enums/cacheEnum';
import projectSetting from '/@/settings/projectSetting';
import { useUserStore } from '/@/store/modules/user';
import type { LocationQueryRaw, RouteParamsRaw } from 'vue-router';
import { getMenus } from '/@/router/menus';
export interface MultipleTabState {
cacheTabList: Set<string>;
@ -65,6 +66,25 @@ const closeTabContainCurrentRoute = (router, pathList) => {
}
return false;
};
/**
* 2025-05-08
* liaozhiyang
* issues/8216online生成的菜单sql 自动带上组件名称
* */
function getMatchingRoute(menus, path) {
for (let i = 0, len = menus.length; i < len; i++) {
const item = menus[i];
if (item.path === path && !item.redirect && !item.paramPath) {
return item;
} else if (item.children?.length) {
const result = getMatchingRoute(item.children, path);
if (result) {
return result;
}
}
}
return null;
}
const cacheTab = projectSetting.multiTabsSetting.cache;
@ -97,7 +117,7 @@ export const useMultipleTabStore = defineStore({
*/
async updateCacheTab() {
const cacheMap: Set<string> = new Set();
const allMenus = await getMenus();
for (const tab of this.tabList) {
const item = getRawRoute(tab);
// Ignore the cache
@ -105,6 +125,19 @@ export const useMultipleTabStore = defineStore({
if (!needCache) {
continue;
}
// update-begin--author:liaozhiyang---date:20240308---forQQYUN-12348onlinesql
if (
['OnlineAutoList', 'DefaultOnlineList', 'CgformErpList', 'OnlCgformInnerTableList', 'OnlCgformTabList', 'OnlCgReportList', 'GraphreportAutoChart', 'AutoDesformDataList'].includes(item.name as string) &&
allMenus?.length
) {
const route = getMatchingRoute(allMenus, item.path);
if (route?.meta?.keepAlive) {
// keepAlivetrue
} else {
continue;
}
}
// update-end--author:liaozhiyang---date:20240308---forQQYUN-12348onlinesql
const name = item.name as string;
cacheMap.add(name);
}
@ -126,6 +159,22 @@ export const useMultipleTabStore = defineStore({
const redo = useRedo(router);
await redo();
},
/**
* 修改设计模式
* changeDesign
*/
async changeDesign(router: Router) {
const { currentRoute } = router;
const route = unref(currentRoute);
const name = route.name;
const findTab = this.getCachedTabList.find((item) => item === name);
if (findTab) {
this.cacheTabList.delete(findTab);
}
const redo = useRedo(router, { isDesign: true });
await redo();
},
clearCacheTabs(): void {
this.cacheTabList = new Set();
},
@ -348,7 +397,7 @@ export const useMultipleTabStore = defineStore({
this.clearCacheTabs();
this.goToPage(router);
},
/**
* Close other tabs
@ -413,6 +462,9 @@ export const useMultipleTabStore = defineStore({
setRedirectPageParam(data) {
this.redirectPageParam = data;
},
getRedirectPageParam() {
return this.redirectPageParam;
},
},
});

View File

@ -53,7 +53,8 @@ export function filterDictText(dictOptions, text) {
let dictText = txt;
for (let dictItem of dictOptions) {
// update-begin--author:liaozhiyang---date:20240524---for【TV360X-469】兼容数据null值防止报错
if (dictItem == null) break;
if (dictItem == null) continue;
if (dictItem.value == null) continue;
// update-end--author:liaozhiyang---date:20240524---for【TV360X-469】兼容数据null值防止报错
if (txt.toString() === dictItem.value.toString()) {
dictText = dictItem.text || dictItem.title || dictItem.label;

View File

@ -25,7 +25,7 @@ export const getDictItemsByCode = (code) => {
};
/**
* Popup字典翻译方法
* 从缓存中获取Pop字典配置
* @param text
* @param code
*/

View File

@ -38,10 +38,15 @@ export function getAppEnvConfig() {
VITE_GLOB_ONLINE_VIEW_URL,
//
VITE_GLOB_HIDE_LAYOUT_TYPES,
//
VITE_GLOB_RUN_PLATFORM,
// JEECG
VITE_GLOB_QIANKUN_MICRO_APP_NAME,
VITE_GLOB_QIANKUN_MICRO_APP_ENTRY,
//线wps, onlyoffice
VITE_GLOB_ONLINE_DOCUMENT_VERSION,
} = ENV;
// if (!/^[a-zA-Z\_]*$/.test(VITE_GLOB_APP_SHORT_NAME)) {
@ -62,10 +67,14 @@ export function getAppEnvConfig() {
VITE_GLOB_DOMAIN_URL,
VITE_GLOB_ONLINE_VIEW_URL,
VITE_GLOB_HIDE_LAYOUT_TYPES,
VITE_GLOB_RUN_PLATFORM,
// JEECG
VITE_GLOB_QIANKUN_MICRO_APP_NAME,
VITE_GLOB_QIANKUN_MICRO_APP_ENTRY,
//线wps, onlyoffice
VITE_GLOB_ONLINE_DOCUMENT_VERSION
};
}
@ -113,3 +122,14 @@ export function getHideLayoutTypes(): string[] {
}
return VITE_GLOB_HIDE_LAYOUT_TYPES.split(',');
}
/**
* 获取在线文档版本号
*/
export function getOnlineDocumentVersion(): string {
const { VITE_GLOB_ONLINE_DOCUMENT_VERSION } = getAppEnvConfig();
if (typeof VITE_GLOB_ONLINE_DOCUMENT_VERSION !== 'string') {
return 'wps';
}
return VITE_GLOB_ONLINE_DOCUMENT_VERSION;
}

View File

@ -99,6 +99,12 @@ const transform: AxiosTransform = {
if(requestUrl!=null && (requestUrl.startsWith("http:") || requestUrl.startsWith("https:"))){
isStartWithHttp = true;
}
// update-begin--author:sunjianlei---date:20250411---forQQYUN-9685 electron
if (!isStartWithHttp && requestUrl != null) {
// electronurlfile://
isStartWithHttp = requestUrl.startsWith('file://');
}
// update-end----author:sunjianlei---date:20250411---forQQYUN-9685 electron
if (!isStartWithHttp && joinPrefix) {
config.url = `${urlPrefix}${config.url}`;
}

View File

@ -272,6 +272,12 @@ export const schemas: FormSchema[] = [
span: 12,
},
},
{
field: 'user4',
component: 'JEllipsis',
label: '选中用户',
colProps: { span: 12 },
},
{
field: 'role2',
component: 'JSelectRole',

View File

@ -5,6 +5,7 @@ export enum Api {
//
list = '/airag/app/list',
save = '/airag/app/edit',
release = '/airag/app/release',
delete = '/airag/app/delete',
queryById = '/airag/app/queryById',
queryBathById = '/airag/knowledge/query/batch/byId',
@ -44,6 +45,17 @@ export const saveApp = (params) => {
return defHttp.put({ url: Api.save, params });
};
//
export function releaseApp(appId: string, release = false) {
return defHttp.post({
url: Api.release,
params: {
id: appId,
release: release,
}
}, {joinParamsToUrl: true});
}
/**
* 删除应用
* @param params
@ -77,5 +89,15 @@ export const queryFlowById = (params) => {
* @param params
*/
export const promptGenerate = (params) => {
return defHttp.get({ url: Api.promptGenerate, params,timeout: 5 * 60 * 1000 }, { isTransformResponse: false });
return defHttp.post(
{
url: Api.promptGenerate+'?prompt='+ params.prompt,
adapter: 'fetch',
responseType: 'stream',
timeout: 5 * 60 * 1000,
},
{
isTransformResponse: false,
}
);
};

View File

@ -42,7 +42,7 @@ export const formSchema: FormSchema[] = [
label: '选择应用类型',
field: 'type',
component: 'Input',
ifShow:({ values })=>{
show:({ values })=>{
return !values.id;
},
slot: 'typeSlot',

View File

@ -1,23 +1,23 @@
<!--知识库文档列表-->
<template>
<div class="p-2 knowledge">
<div class="knowledge">
<!--查询区域-->
<div class="jeecg-basic-table-form-container">
<a-form
ref="formRef"
@keyup.enter.native="reload"
@keyup.enter.native="searchQuery"
:model="queryParam"
:label-col="labelCol"
:wrapper-col="wrapperCol"
style="background-color: #f7f8fc"
>
<a-row :gutter="24">
<a-col :lg="6">
<a-col :xl="7" :lg="7" :md="8" :sm="24">
<a-form-item name="name" label="应用名称">
<JInput v-model:value="queryParam.name" placeholder="请输入应用名称" />
</a-form-item>
</a-col>
<a-col :lg="6">
<a-col :xl="7" :lg="7" :md="8" :sm="24">
<a-form-item name="type" label="应用类型">
<j-dict-select-tag v-model:value="queryParam.type" dict-code="ai_app_type" placeholder="请选择应用类型" />
</a-form-item>
@ -25,7 +25,7 @@
<a-col :xl="6" :lg="7" :md="8" :sm="24">
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
<a-col :lg="6">
<a-button type="primary" preIcon="ant-design:search-outlined" @click="reload"></a-button>
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery"></a-button>
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
</a-col>
</span>
@ -35,11 +35,10 @@
</div>
<a-row :span="24" class="knowledge-row">
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24">
<a-card class="add-knowledge-card" :bodyStyle="cardBodyStyle">
<span style="line-height: 18px; font-weight: 500; color: #676f83; font-size: 12px">创建应用</span>
<div class="add-knowledge-doc" @click="handleCreateApp">
<Icon icon="ant-design:form-outlined" size="13"></Icon>
<span>创建空白应用</span>
<a-card class="add-knowledge-card" @click="handleCreateApp">
<div class="flex">
<Icon icon="ant-design:plus-outlined" class="add-knowledge-card-icon" size="20"></Icon>
<span class="add-knowledge-card-title">创建应用</span>
</div>
</a-card>
</a-col>
@ -49,7 +48,11 @@
<img class="header-img" :src="getImage(item.icon)" />
<div class="header-text">
<span class="header-text-top header-name ellipsis"> {{ item.name }} </span>
<span class="header-text-top header-create ellipsis"> 创建者{{ item.createBy }} </span>
<span class="header-text-top header-create ellipsis">
<a-tag v-if="item.status === 'release'" color="green"></a-tag>
<a-tag v-if="item.status === 'disable'"></a-tag>
<span>创建者{{ item.createBy_dictText || item.createBy }}</span>
</span>
</div>
</div>
<div class="header-tag">
@ -69,25 +72,41 @@
<Icon class="operation" icon="ant-design:youtube-outlined" size="20" color="#1F2329"></Icon>
</div>
</a-tooltip>
<a-divider type="vertical" style="float: left" />
<a-tooltip title="删除">
<div class="card-footer-icon" @click.prevent.stop="handleDeleteClick(item)">
<Icon icon="ant-design:delete-outlined" class="operation" size="20" color="#1F2329"></Icon>
</div>
</a-tooltip>
<template v-if="item.status !== 'release'">
<a-divider type="vertical" style="float: left" />
<a-tooltip title="删除">
<div class="card-footer-icon" @click.prevent.stop="handleDeleteClick(item)">
<Icon icon="ant-design:delete-outlined" class="operation" size="18" color="#1F2329"></Icon>
</div>
</a-tooltip>
</template>
<a-divider type="vertical" style="float: left" />
<a-tooltip title="发布">
<a-dropdown class="card-footer-icon" placement="bottomRight" :trigger="['click']">
<div @click.prevent.stop>
<Icon icon="ant-design:send-outlined"></Icon>
<Icon style="position: relative;top: 1px" icon="ant-design:send-outlined" size="16" color="#1F2329"></Icon>
</div>
<template #overlay>
<a-menu>
<template v-if="item.status === 'enable'">
<a-menu-item key="release" @click.prevent.stop="handleSendClick(item,'release')">
<Icon icon="lineicons:rocket-5" size="16"></Icon>
发布
</a-menu-item>
<a-menu-divider/>
</template>
<template v-else-if="item.status === 'release'">
<a-menu-item key="un-release" @click.prevent.stop="handleSendClick(item,'un-release')">
<Icon icon="tabler:rocket-off" size="16"></Icon>
取消发布
</a-menu-item>
<a-menu-divider/>
</template>
<a-menu-item key="web" @click.prevent.stop="handleSendClick(item,'web')">
<Icon icon="ant-design:dribbble-outlined" size="16"></Icon>
嵌入网站
</a-menu-item>
<a-menu-item key="menu" @click.prevent.stop="handleSendClick(item,'menu')">
<a-menu-item v-if="isShowMenu" key="menu" @click.prevent.stop="handleSendClick(item,'menu')">
<Icon icon="ant-design:menu-outlined" size="16"></Icon> 配置菜单
</a-menu-item>
</a-menu>
@ -109,6 +128,7 @@
@change="handlePageChange"
class="list-footer"
size="small"
:show-total="() => `共${total}条` "
/>
<!-- Ai新增弹窗 -->
<AiAppModal @register="registerModal" @success="handleSuccess"></AiAppModal>
@ -120,7 +140,7 @@
</template>
<script lang="ts">
import { ref, reactive } from 'vue';
import { ref, reactive, onMounted } from 'vue';
import BasicModal from '@/components/Modal/src/BasicModal.vue';
import { useModal, useModalInner } from '@/components/Modal';
import { LoadingOutlined } from '@ant-design/icons-vue';
@ -131,10 +151,12 @@
import AiAppSettingModal from './components/AiAppSettingModal.vue';
import AiAppSendModal from './components/AiAppSendModal.vue';
import Icon from '@/components/Icon';
import { appList, deleteApp } from './AiApp.api';
import { $electron } from "@/electron";
import { appList, deleteApp, releaseApp } from './AiApp.api';
import { useMessage } from '@/hooks/web/useMessage';
import JInput from '@/components/Form/src/jeecg/components/JInput.vue';
import JDictSelectTag from '@/components/Form/src/jeecg/components/JDictSelectTag.vue';
import { useRouter } from "vue-router";
export default {
name: 'AiAppList',
@ -168,7 +190,7 @@
const [registerModal, { openModal }] = useModal();
const [registerSettingModal, { openModal: openAppModal }] = useModal();
const [registerAiAppSendModal, { openModal: openAiAppSendModal }] = useModal();
const { createMessage } = useMessage();
const { createMessage, createConfirmSync } = useMessage();
//
const queryParam = reactive<any>({});
//label
@ -185,7 +207,7 @@
});
//ref
const formRef = ref();
reload();
/**
@ -263,14 +285,27 @@
/**
* 演示
*/
function handleViewClick(id) {
window.open('/ai/app/chat/' + id , '_blank');
function handleViewClick(id: string) {
let url = '/ai/app/chat/' + id;
// update-begin--author:sunjianlei---date:20250411---forQQYUN-9685 electron
if ($electron.isElectron()) {
url = $electron.resolveRoutePath(url);
window.open(url, '_blank', 'width=1200,height=800');
return
}
// update-end----author:sunjianlei---date:20250411---forQQYUN-9685 electron
window.open(url, '_blank');
}
/**
* 删除
*/
function handleDeleteClick(item) {
if(knowledgeAppDataList.value.length == 1 && pageNo.value > 1) {
pageNo.value = pageNo.value - 1;
}
deleteApp({ id: item.id, name: item.name }, reload);
}
@ -280,22 +315,78 @@
* @param type 类别
*/
function handleSendClick(item,type) {
if (type === 'release' || type === 'un-release') {
return onRelease(item);
}
openAiAppSendModal(true,{
type: type,
data: item
})
}
async function onRelease(item) {
const toRelease = item.status === 'enable';
let flag = await createConfirmSync({
title: toRelease ? '发布应用' : '取消发布应用',
content: toRelease ? '确定要发布应用吗?发布后将不允许修改应用。' : '确定要取消发布应用吗?',
okText: '确定',
cancelText: '取消',
});
if (!flag) {
return
}
doRelease(item, item.status === 'enable');
}
/**
* 发布
*/
async function doRelease(item, release: boolean) {
let success: boolean = await releaseApp(item.id, release);
if (success) {
//
if (release) {
item.status = 'release'
} else {
item.status = 'enable'
}
}
}
/**
* 重置
*/
function searchReset() {
pageNo.value = 1;
formRef.value.resetFields();
queryParam.name = '';
//
reload();
}
/**
* 查询
*/
function searchQuery(){
pageNo.value = 1;
//
reload();
}
const router = useRouter();
//
const isShowMenu = ref<boolean>(false);
onMounted((()=>{
let fullPath = router.currentRoute.value.fullPath;
console.log(fullPath)
if(fullPath === '/myapps/ai/app'){
isShowMenu.value = false;
} else {
isShowMenu.value = true;
}
}))
return {
handleCreateApp,
knowledgeAppDataList,
@ -320,6 +411,8 @@
registerAiAppSendModal,
searchReset,
formRef,
isShowMenu,
searchQuery,
};
},
};
@ -327,30 +420,38 @@
<style scoped lang="less">
.knowledge {
display: grid;
height: 100%;
height: calc(100vh - 115px);
background: #f7f8fc;
padding: 24px;
overflow-y: auto;
}
.add-knowledge-card {
border-radius: 10px;
margin-bottom: 20px;
display: inline-flex;
font-size: 16px;
height: 152px;
width: calc(100% - 20px);
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 4px #e6e6e6;
transition: all 0.3s ease;
border-radius: 10px;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 16px;
cursor: pointer;
height: 152px;
width: calc(100% - 20px);
.add-knowledge-card-icon {
padding: 8px;
color: #1f2329;
background-color: #f5f6f7;
margin-right: 12px;
}
.add-knowledge-card-title {
font-size: 16px;
color:#1f2329;
font-weight: 400;
align-self: center;
}
}
.knowledge-card {
@ -360,7 +461,7 @@
height: 152px;
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 4px #e6e6e6;
transition: all 0.3s ease;
.header-img {
width: 40px;
@ -396,10 +497,12 @@
.add-knowledge-card:hover,
.knowledge-card:hover {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
box-shadow: 0 6px 12px #d0d3d8;
}
.knowledge-row {
max-height: calc(100% - 100px);
margin-top: 20px;
overflow-y: auto;
}
@ -455,7 +558,7 @@
.card-footer-icon:hover {
color: #000000;
background-color: rgba(0, 0, 0, 0.05);
background-color: #e9ecf2;
border: none;
}

View File

@ -1,9 +1,9 @@
<template>
<div ref="chatContainerRef" class="chat-container" :style="chatContainerStyle">
<template v-if="dataSource">
<div class="leftArea" :class="[expand ? 'expand' : 'shrink']">
<div v-if="isMultiSession" class="leftArea" :class="[expand ? 'expand' : 'shrink']">
<div class="content">
<slide v-if="uuid" :dataSource="dataSource" @save="handleSave" :prologue="prologue" :appData="appData" @click="handleChatClick"></slide>
<slide :source="source" v-if="uuid" :dataSource="dataSource" @save="handleSave" :prologue="prologue" :appData="appData" @click="handleChatClick"></slide>
</div>
<div class="toggle-btn" @click="handleToggle">
<span class="icon">
@ -30,6 +30,7 @@
@reload-message-title="reloadMessageTitle"
:chatTitle="chatTitle"
:quickCommandData="quickCommandData"
:showAdvertising = "showAdvertising"
></chat>
</div>
</template>
@ -46,6 +47,7 @@
import { JEECG_CHAT_KEY } from '/@/enums/cacheEnum';
import { defHttp } from '/@/utils/http/axios';
import { useRouter } from 'vue-router';
import { useAppInject } from "@/hooks/web/useAppInject";
const router = useRouter();
const userId = useUserStore().getUserInfo?.id;
@ -77,6 +79,8 @@
const prologue = ref<string>('');
//
const quickCommandData = ref<any>([]);
//广
const showAdvertising = ref<boolean>(false);
const priming = () => {
dataSource.value = {
@ -142,6 +146,13 @@
);
};
//
const isMultiSession = ref<boolean>(true);
//
const { getIsMobile } = useAppInject();
//
const source = ref<string>('');
/**
* 初始化聊天信息
* @param appId
@ -184,6 +195,13 @@
{ name: 'JEECG有哪些优势', descr: "JEECG有哪些优势" },
{ name: 'JEECG可以做哪些事情', descr: "JEECG可以做哪些事情" },];
}
let query: any = router.currentRoute.value.query;
source.value = query.source;
if(query.source){
showAdvertising.value = query.source === 'chatJs';
}else{
showAdvertising.value = false;
}
});
onUnmounted(() => {
@ -203,7 +221,7 @@
await defHttp
.get(
{
url: '/airag/app/queryById',
url: '/airag/chat/init',
params: { id: appId },
},
{ isTransformResponse: false }
@ -220,6 +238,23 @@
if (res.result && res.result.presetQuestion) {
presetQuestion.value = res.result.presetQuestion;
}
if (res.result && res.result.metadata) {
let metadata = JSON.parse(res.result.metadata);
//
if(!getIsMobile.value){
//
if((metadata.multiSession && metadata.multiSession === '1') || !metadata.multiSession) {
isMultiSession.value = true;
} else {
isMultiSession.value = false;
expand.value = false;
}
}
}
if(getIsMobile.value){
isMultiSession.value = false;
expand.value = false;
}
} else {
appData.value = {};
}
@ -281,7 +316,7 @@
.chat-container {
height: 100%;
width: 100%;
position: relative;
position: absolute;
background: white;
display: flex;
overflow: hidden;
@ -339,7 +374,7 @@
color: rgb(51, 54, 57);
border: 1px solid rgb(239, 239, 245);
background-color: #fff;
box-shadow: 0 2px 4px 0px rgba(0, 0, 0, 0.06);
box-shadow: 0 2px 4px 0px #e7e9ef;
transform: translateX(50%) translateY(-50%);
z-index: 1;
}

View File

@ -17,19 +17,19 @@
import AiChat from './AiChat.vue';
import { useRouter } from 'vue-router';
//aiChatref
//aiChatref
const aiChatRef = ref();
//id
//id
const appId = ref<string>('');
//
//
const showChat = ref<any>(false);
const router = useRouter();
//
//
const isInit = ref<boolean>(false);
/**
* chat图标点击事件
* chat图标点击事件
*/
function chatClick() {
showChat.value = !showChat.value;
@ -67,7 +67,7 @@
display: flex;
align-items: center;
justify-content: center;
box-shadow: rgba(0, 0, 0, 0.2) 0 4px 8px 0;
box-shadow: #cccccc 0 4px 8px 0;
}
.footer-close-icon {
color: #0a3069;

View File

@ -3,6 +3,13 @@
<div class="content">
<div class="header-title" v-if="type === 'view' && headerTitle">
{{headerTitle}}
<div v-if="showAdvertising" class="header-advertisint">
AI客服由
<a style="color: #4183c4;margin-left: 2px;margin-right: 2px" href="https://www.qiaoqiaoyun.com/aiCustomerService" target="_blank">
敲敲云
</a>
提供
</div>
</div>
<div class="main">
<div id="scrollRef" ref="scrollRef" class="scrollArea">
@ -19,6 +26,8 @@
:appData="appData"
:presetQuestion="item.presetQuestion"
:images = "item.images"
:retrievalText="item.retrievalText"
:referenceKnowledge="item.referenceKnowledge"
@send="handleOutQuestion"
></chatMessage>
</div>
@ -86,6 +95,7 @@
autofocus
:readonly="loading"
style="border-color: #ffffff !important;box-shadow:none"
@paste="paste"
>
</a-textarea>
<a-button v-if="loading" type="primary" danger @click="handleStopChat" class="stopBtn">
@ -122,7 +132,7 @@
>
<a-tooltip title="图片上传支持jpg/jpeg/png">
<a-button class="sendBtn" type="text">
<Icon icon="ant-design:picture-outlined" style="color: rgba(15,21,40,0.8)"></Icon>
<Icon icon="ant-design:picture-outlined" style="color: #3d4353"></Icon>
</a-button>
</a-tooltip>
</a-upload>
@ -178,21 +188,22 @@
import { defHttp } from '@/utils/http/axios';
import { cloneDeep } from "lodash-es";
import {getFileAccessHttpUrl, getHeaders} from "@/utils/common/compUtils";
import { uploadUrl } from '/@/api/common/api';
import { createImgPreview } from "@/components/Preview";
import { useAppInject } from "@/hooks/web/useAppInject";
import { useGlobSetting } from "@/hooks/setting";
message.config({
prefixCls: 'ai-chat-message',
});
const props = defineProps(['uuid', 'prologue', 'formState', 'url', 'type','historyData','chatTitle','presetQuestion','quickCommandData']);
const props = defineProps(['uuid', 'prologue', 'formState', 'url', 'type','historyData','chatTitle','presetQuestion','quickCommandData','showAdvertising']);
const emit = defineEmits(['save','reload-message-title']);
const { scrollRef, scrollToBottom } = useScroll();
const prompt = ref<string>('');
const loading = ref<boolean>(false);
const inputRef = ref<Ref | null>(null);
const headerTitle = ref<string>(props.chatTitle);
//
const chatData = ref<any>([]);
//
@ -202,15 +213,24 @@
const topicId = ref<string>('');
//id
const requestId = ref<string>('');
const { getIsMobile } = useAppInject();
const conversationList = computed(() => chatData.value.filter((item) => item.inversion != 'user' && !!item.conversationOptions));
const placeholder = computed(() => {
return '来说点什么吧...Shift + Enter = 换行)';
if(getIsMobile.value){
return '来说点什么吧...'
} else {
return '来说点什么吧...Shift + Enter = 换行)';
}
});
//token
const headers = getHeaders();
//
const textareaActive = ref<boolean>(false);
const globSetting = useGlobSetting();
const baseUploadUrl = globSetting.uploadUrl;
const uploadUrl = ref<string>(`${baseUploadUrl}/airag/chat/upload`);
function handleEnter(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
@ -269,6 +289,7 @@
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
referenceKnowledge: [],
});
scrollToBottom();
@ -318,7 +339,7 @@
dateTime: new Date().toLocaleString(),
content: data,
inversion: 'ai',
error: false,
error: true,
loading: true,
conversationOptions: null,
requestOptions: null,
@ -370,19 +391,27 @@
handleStop();
const knowList = ref<Recordable[]>([])
/**
* 停止消息
*/
function handleStopChat() {
if(requestId.value){
//
defHttp.get({
url: '/airag/chat/stop/' + requestId.value,
},{ isTransformResponse: false });
//update-begin---author:wangshuai---date:2025-06-03---for:issues/8338AIstop---
const currentRequestId = requestId.value
if(currentRequestId){
try{
//
defHttp.get({
url: '/airag/chat/stop/' + currentRequestId,
},{ isTransformResponse: false });
} finally {
handleStop();
}
//update-end---author:wangshuai---date:2025-06-03---for:issues/8338AIstop---
}
handleStop();
}
/**
* 读取文本
* @param message
@ -409,15 +438,16 @@
};
if(headerTitle.value == '新建聊天'){
headerTitle.value = message.length>5?message.substring(0,5):message
headerTitle.value = message.length>10?truncateString(message,10):message
}
emit("reload-message-title",message.length>5?message.substring(0,5):message)
emit("reload-message-title",message.length>10?truncateString(message,10):message)
}
uploadUrlList.value = [];
fileInfoList.value = [];
knowList.value = [];
const readableStream = await defHttp.post(
{
url: props.url,
@ -430,9 +460,18 @@
isTransformResponse: false,
}
).catch((e)=>{
updateChatFail(uuid, chatData.value.length - 1, "服务器错误,请稍后重试!");
handleStop();
return;
//update-begin---author:wangshuai---date:2025-04-28---for:QQYUN-12297AI---
if(e.code === 'ETIMEDOUT'){
updateChatFail(uuid, chatData.value.length - 1, "当前用户较多,排队中,请稍候再次重试!");
handleStop();
return;
}else{
updateChatFail(uuid, chatData.value.length - 1, "服务器错误,请稍后重试!");
handleStop();
return;
}
console.error(e)
//update-end---author:wangshuai---date:2025-04-28---for:QQYUN-12297AI---
});
const reader = readableStream.getReader();
const decoder = new TextDecoder('UTF-8');
@ -448,9 +487,9 @@
let result = decoder.decode(value, { stream: true });
result = buffer + result;
const lines = result.split('\n\n');
for (const line of lines) {
for (let line of lines) {
if (line.startsWith('data:')) {
const content = line.replace('data:', '').trim();
let content = line.replace('data:', '').trim();
if(!content){
continue;
}
@ -461,6 +500,9 @@
buffer = "";
try {
//update-begin---author:wangshuai---date:2025-03-13---for:QQYUN-11572线---
if(content.indexOf(":::card:::") !== -1){
content = content.replace(/\s+/g, '');
}
let parse = JSON.parse(content);
await renderText(parse,conversationId,text,options).then((res)=>{
text = res.returnText;
@ -482,6 +524,9 @@
buffer = "";
//update-begin---author:wangshuai---date:2025-03-13---for:QQYUN-11572线---
try {
if(line.indexOf(":::card:::") !== -1){
line = line.replace(/\s+/g, '');
}
let parse = JSON.parse(line);
await renderText(parse, conversationId, text, options).then((res) => {
text = res.returnText;
@ -523,24 +568,42 @@
async function renderText(item,conversationId,text,options) {
let returnText = "";
if (item.event == 'MESSAGE') {
text = text + item.data.message;
returnText = text;
let message = item.data.message;
let messageText = "";
//update-begin---author:wangshuai---date:2025-04-24---for:card---
if(message && message.indexOf("::card::") !== -1){
messageText = message;
} else {
text = text + item.data.message;
messageText = text;
returnText = text;
}
//update-end---author:wangshuai---date:2025-04-24---for:card---
// requestId
if (item.requestId) {
requestId.value = item.requestId;
}
//
updateChat(uuid.value, chatData.value.length - 1, {
dateTime: new Date().toLocaleString(),
content: text,
content: messageText,
inversion: 'ai',
error: false,
loading: true,
conversationOptions: { conversationId: conversationId, parentMessageId: topicId.value },
requestOptions: { prompt: message, options: { ...options } },
referenceKnowledge: knowList.value,
});
}
if(item.event == 'INIT_REQUEST_ID'){
if (item.requestId) {
requestId.value = item.requestId;
}
}
if (item.event == 'MESSAGE_END') {
topicId.value = item.topicId;
conversationId = item.conversationId;
uuid.value = item.conversationId;
requestId.value = item.requestId;
handleStop();
}
if (item.event == 'FLOW_FINISHED') {
@ -565,41 +628,60 @@
//update-begin---author:wangshuai---date:2025-03-21---for:QQYUN-11495AI---
if(item.event === "NODE_STARTED"){
let aiText = "";
if(item.data.type === 'llm'){
aiText = "正在构建响应内容";
if(!item.data || item.data.type !== 'end'){
let aiText = "";
if(item.data.type === 'llm'){
aiText = "正在构建响应内容";
}
if(item.data.type === 'knowledge'){
aiText = "正在对知识库进行深度检索";
}
if(item.data.type === 'classifier'){
aiText = "正在分类";
}
if(item.data.type === 'code'){
aiText = "正在实施代码运行操作";
}
if(item.data.type === 'subflow'){
aiText = "正在运行子流程";
}
if(item.data.type === 'enhanceJava'){
aiText = "正在执行java增强";
}
if(item.data.type === 'http'){
aiText = "正在发送http请求";
}
if(!text){
//
updateChat(uuid.value, chatData.value.length - 1, {
dateTime: new Date().toLocaleString(),
retrievalText: aiText,
text:"",
inversion: 'ai',
error: false,
loading: true,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
referenceKnowledge: knowList.value,
});
}
}
if(item.data.type === 'knowledge'){
aiText = "正在对知识库进行深度检索";
}
if(item.data.type === 'classifier'){
aiText = "正在分类";
}
if(item.data.type === 'code'){
aiText = "正在实施代码运行操作";
}
if(item.data.type === 'subflow'){
aiText = "正在运行子流程";
}
if(item.data.type === 'enhanceJava'){
aiText = "正在执行java增强";
}
if(item.data.type === 'http'){
aiText = "正在发送http请求";
}
//
updateChat(uuid.value, chatData.value.length - 1, {
dateTime: new Date().toLocaleString(),
content: aiText,
inversion: 'ai',
error: false,
loading: true,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
});
}
//update-end---author:wangshuai---date:2025-03-21---for:QQYUN-11495AI---
else if (item.event === 'NODE_FINISHED') {
if(!item.data || item.data.type !== 'end'){
if(item.data.type === 'knowledge'){
const id = item.data.id;
const data = item.data.outputs[id + ".data"]
knowList.value.push(data)
//
updateChatSome(uuid.value, chatData.value.length - 1, {referenceKnowledge: knowList.value})
}
}
}
if(!returnText){
returnText = text;
}
return { returnText, conversationId };
}
@ -607,7 +689,7 @@
const uploadUrlList = ref<any>([]);
//
const fileInfoList = ref<any>([]);
/**
* 文件上传回调事件
* @param info
@ -615,8 +697,9 @@
function handleChange(info) {
let { fileList, file } = info;
fileInfoList.value = fileList;
if (file.status === 'error') {
if (file.status === 'error' || (file.response && file.response.code == 500)) {
message.error(file.response?.message || `${file.name} 上传失败,请查看服务端日志`);
return;
}
if (file.status === 'done') {
uploadUrlList.value.push(file.response.message);
@ -625,7 +708,7 @@
/**
* 获取图片地址
*
*
* @param url
*/
function getImage(url) {
@ -643,6 +726,10 @@
return false;
}
}
if(uploadUrlList.value && uploadUrlList.value.length > 2){
message.warning("最多只能上传三张!");
return false;
}
return true;
}
@ -665,7 +752,85 @@
let imageList = [getImage(url)];
createImgPreview({ imageList: imageList, defaultWidth: 700, rememberState: true, onImgLoad });
}
/**
* 截取字符串
* @param str
* @param maxLength
*/
function truncateString(str, maxLength) {
if (str.length <= maxLength){
return str;
}
let chineseCount = 0;
let englishCount = 0;
let digitCount = 0;
let result = '';
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (/[\u4e00-\u9fa5]/.test(char)) { //
chineseCount++;
} else if (/[a-zA-Z]/.test(char)) { //
englishCount++;
} else if (/\d/.test(char)) { //
digitCount++;
}
if (chineseCount + englishCount / 2 + digitCount / 2 > maxLength) {
break;
}
result += char;
}
return result;
}
/**
* 粘贴事件
* @param event
*/
function paste(event) {
if(uploadUrlList.value && uploadUrlList.value.length > 2){
message.warning("最多只能上传三张!");
return;
}
const items = (event.clipboardData || window.clipboardData).items;
if (!items || items.length === 0){
//
message.error('当前浏览器不支持本地打开图片!');
return;
}
let image = null;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
image = items[i].getAsFile();
handleUploadImage(image);
break;
}
}
}
/**
* 粘贴图片
* @param image
*/
async function handleUploadImage(image) {
const isReturn = (fileInfo) => {
try {
if (fileInfo.code === 0) {
let { message } = fileInfo;
uploadUrlList.value.push(message);
fileInfoList.value.push(image);
} else if (fileInfo.code === 500 || fileInfo.code === 510) {
message.error(fileInfo.message || `${image.name} 导入失败`);
}
} catch (error) {
console.log('导入的数据异常', error);
message.error(`${image.name} 导入失败`);
}
};
await defHttp.uploadFile({ url: "/airag/chat/upload" }, { file: image }, { success: isReturn });
}
//
watch(
() => props.prologue,
@ -676,8 +841,8 @@
}
} catch (e) {}
}
);
);
//
watch(
() => props.presetQuestion,
@ -698,7 +863,7 @@
},
{ deep: true, immediate: true }
);
//
watch(
() => props.historyData,
@ -840,6 +1005,14 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
justify-content: space-between;
height: 30px;
.header-advertisint{
display:flex;
margin-right: 20px;
font-size: 12px;
}
}
.chat-textarea{
display: flex;
@ -850,7 +1023,7 @@
border-width: 1px;
flex-direction: column;
transition: width 0.3s;
border-color: rgba(68,83,130,0.2);
border-color: #d2d7e5;
.textarea-top{
border-bottom: 1px solid #f0f0f5;
padding: 12px 28px;
@ -881,10 +1054,10 @@
}
}
.chat-textarea:hover{
border-color: rgba(59,130,246,0.5)
border-color: #9dc1fb;
}
.textarea-active{
border-color: rgba(59,130,246,0.5) !important;
border-color: #98bdfa !important;
}
:deep(.ant-divider-vertical){
margin: 0 2px;
@ -899,7 +1072,7 @@
display: none;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 4px #e6e6e6;
margin-left: 44px;
margin-top: -4px;
}
@ -908,6 +1081,24 @@
display: flex;
}
}
@media (max-width: 600px) {
//
.footer{
padding: 0;
.bottomArea{
.delBtn{
margin-right: 0;
}
}
}
.chatWrap{
padding: 10px 10px 10px 0;
}
.main .chatContentArea{
padding: 10px 0 0 10px;
}
}
</style>
<style lang="less">
.ai-chat-modal{

View File

@ -1,8 +1,8 @@
<template>
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']">
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']" v-if="getText || (props.presetQuestion && props.presetQuestion.length>0)">
<div class="avatar">
<img v-if="inversion === 'user'" :src="avatar()" />
<img v-else :src="getAiImg()">
<img v-else :src="getAiImg()" />
</div>
<div class="content">
<p class="date">
@ -14,8 +14,24 @@
<img :src="getImageUrl(item)"/>
</div>
</div>
<div class="msgArea">
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading"></chatText>
<div v-if="inversion === 'ai' && retrievalText && loading" class="retrieval">
{{retrievalText}}
</div>
<div v-if="inversion === 'ai' && isCard" class="card">
<a-row>
<a-col :xl="6" :lg="8" :md="10" :sm="24" style="flex:1" v-for="item in getCardList()">
<a-card class="ai-card" @click="aiCardHandleClick(item.linkUrl)">
<div class="ai-card-title">{{item.productName}}</div>
<div class="ai-card-img">
<img :src="item.productImage">
</div>
<span class="ai-card-desc">{{item.descr}}</span>
</a-card>
</a-col>
</a-row>
</div>
<div class="msgArea" v-if="!isCard">
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading" :referenceKnowledge="referenceKnowledge"></chatText>
</div>
<div v-if="presetQuestion" v-for="item in presetQuestion" class="question" @click="presetQuestionClick(item.descr)">
<span>{{item.descr}}</span>
@ -26,13 +42,31 @@
<script setup lang="ts">
import chatText from './chatText.vue';
import defaultAvatar from '/@/assets/images/ai/avatar.jpg';
import defaultAvatar from "@/assets/images/ai/avatar.jpg";
import { useUserStore } from '/@/store/modules/user';
import defaultImg from '../img/ailogo.png';
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading','appData','presetQuestion','images']);
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading','appData','presetQuestion','images','retrievalText', 'referenceKnowledge']);
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { createImgPreview } from "@/components/Preview";
import { computed } from "vue";
const getText = computed(()=>{
let text = props.text || props.retrievalText;
if(text){
text = text.trim();
}
return text;
})
const isCard = computed(() => {
let text = props.text;
if (text && text.indexOf('::card::') != -1) {
return true;
}
return false;
});
const { userInfo } = useUserStore();
const avatar = () => {
return getFileAccessHttpUrl(userInfo?.avatar) || defaultAvatar;
@ -44,7 +78,7 @@
/**
* 预设问题点击事件
*
*
*/
function presetQuestionClick(descr) {
emit("send",descr)
@ -52,7 +86,7 @@
/**
* 获取图片
*
*
* @param item
*/
function getImageUrl(item) {
@ -77,7 +111,28 @@
let imageList = [getImageUrl(url)];
createImgPreview({ imageList: imageList, defaultWidth: 700, rememberState: true, onImgLoad });
}
/**
* 获取卡片列表
*/
function getCardList() {
let text = props.text;
let card = text.replace('::card::', '').replace(/\s+/g, '');
try {
return JSON.parse(card);
} catch (e) {
console.log(e)
return '';
}
}
/**
* ai卡片点击事件
* @param url
*/
function aiCardHandleClick(url){
window.open(url,'_blank');
}
</script>
<style lang="less" scoped>
@ -112,6 +167,7 @@
}
}
.content {
width: 90%;
.date {
color: #b4bbc4;
font-size: 0.75rem;
@ -134,14 +190,15 @@
line-height: 1.25rem;
cursor: pointer;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 4px #e6e6e6;
}
.images{
margin-bottom: 10px;
flex-wrap: wrap;
display: flex;
gap: 10px;
justify-content: end;
.image{
width: 120px;
height: 80px;
@ -154,4 +211,76 @@
}
}
}
.retrieval,
.card {
background-color: #f4f6f8;
font-size: 0.875rem;
line-height: 1.25rem;
border-radius: 0.375rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.retrieval:after{
animation: blink 1s steps(5, start) infinite;
color: #000;
content: '_';
font-weight: 700;
margin-left: 3px;
vertical-align: baseline;
}
.card{
width: 100%;
background-color: unset;
}
.ai-card{
width: 98%;
height: 100%;
cursor: pointer;
.ai-card-title{
width: 100%;
line-height: 20px;
letter-spacing: 0;
white-space: pre-line;
overflow: hidden;
display: -webkit-box;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
font-weight: 600;
font-size: 18px;
text-align: left;
color: #191919;
-webkit-line-clamp: 1;
}
.ai-card-img{
margin-top: 10px;
background-color: transparent;
border-radius: 8px;
display: flex;
width: 100%;
height: max-content;
}
.ai-card-desc{
margin-top: 10px;
width: 100%;
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: 0;
white-space: pre-line;
-webkit-box-orient: vertical;
overflow: hidden;
display: -webkit-box;
text-overflow: ellipsis;
text-align: left;
color: #666f;
-webkit-line-clamp: 3;
}
}
@media (max-width: 600px) {
.content{
width: 100%;
}
}
</style>

View File

@ -1,10 +1,26 @@
<template>
<div v-if="text != ''" class="textWrap" :class="[inversion === 'user' ? 'self' : 'chatgpt']" ref="textRef">
<div v-if="inversion != 'user'">
<div class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="text" />
<div v-if="inversion != 'user'" :style="{ width: getIsMobile? screenWidth : 'auto' }">
<div class="markdown-body" :class="{ 'markdown-body-generate': loading }" :style="{color:error?'#FF4444 !important':''}" v-html="text" />
<template v-if="showRefKnow">
<a-divider orientation="left">引用</a-divider>
<template v-for="(item, idx) of referenceKnowledge" :key="idx">
<a-tooltip :title="item.substring(0, 800)">
<a-tag >
<a-space>
<img :src="knowledgePng" width="16" height="16"/>
<div style="max-width: 240px; overflow: hidden;white-space: nowrap;text-overflow: ellipsis;">
{{ item }}
</div>
</a-space>
</a-tag>
</a-tooltip>
</template>
</template>
</div>
<div v-else class="msg" v-text="text" />
<div v-else class="msg" v-html="text" />
</div>
<ImageViewer v-if="amplifyImage" :imageUrl="imageUrl" @hide="pictureHide"></ImageViewer>
</template>
<script setup lang="ts">
@ -16,11 +32,21 @@
import './style/github-markdown.less';
import './style/highlight.less';
import './style/style.less';
import ImageViewer from '@/views/super/airag/aiapp/chat/components/ImageViewer.vue';
import { useAppInject } from "@/hooks/web/useAppInject";
import { useGlobSetting } from "@/hooks/setting";
import knowledgePng from '../../aiknowledge/icon/knowledge.png'
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading']);
/**
* 屏幕宽度
*/
const screenWidth = ref<string>();
const { getIsMobile } = useAppInject();
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading', 'referenceKnowledge']);
const textRef = ref();
const mdi = new MarkdownIt({
html: false,
html: true,
linkify: true,
highlight(code, language) {
const validLang = !!(language && hljs.getLanguage(language));
@ -36,11 +62,49 @@
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' });
const text = computed(() => {
const value = props.text ?? '';
if (props.inversion != 'user') return mdi.render(value);
return value;
let value = props.text ?? '';
if (props.inversion != 'user'){
value = replaceImageWith(value);
value = replaceDomainUrl(value);
return mdi.render(value);
}
return value.replace("\n","<br>");
});
//
const showRefKnow = computed(() => {
const {loading, referenceKnowledge} = props
if (loading) {
return false;
}
return Array.isArray(referenceKnowledge) && referenceKnowledge.length > 0;
})
//
const replaceImageWith = markdownContent => {
// width ![](/static/jimuImages/screenshot_1617252560523.png =100)
const regex = /!\[([^\]]*)\]\(([^)]+)=([0-9]+)\)/g;
return markdownContent.replace(regex, (match, alt, src, width) => {
let reg = /#\s*{\s*domainURL\s*}/g;
src = src.replace(reg,domainUrl);
return `<div><img src='${src}' alt='${alt}' width='${width}' /></div>`;
});
};
const { domainUrl } = useGlobSetting();
//domainURL
const replaceDomainUrl = markdownContent => {
const regex = /!\[([^\]]*)\]\(.*?#\s*{\s*domainURL\s*}.*?\)/g;
return markdownContent.replace(regex, (match) => {
let reg = /#\s*{\s*domainURL\s*}/g;
return match.replace(reg,domainUrl);
})
}
//
const amplifyImage = ref<boolean>(false);
//
const imageUrl = ref<string>('');
function highlightBlock(str: string, lang?: string) {
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">复制代码</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`;
}
@ -72,16 +136,74 @@
}
}
/**
* 添加图片点击事件
*/
function addImageClickEvent() {
if (textRef.value) {
const image = textRef.value.querySelectorAll('img');
image.forEach((img) => {
img.addEventListener('click', () => {
imageUrl.value = img.src;
amplifyImage.value = true;
})
});
}
}
/**
* 移出图片点击事件
*/
function removeImageClickEvent(){
if (textRef.value) {
const image = textRef.value.querySelectorAll('img');
image.forEach((img) => {
img.removeEventListener('click', () => { })
});
}
}
/**
* 图片隐藏
*/
function pictureHide(){
amplifyImage.value = false;
imageUrl.value = ""
}
/**
* 设置markdown body整体宽度
*/
function setMarkdownBodyWidth() {
//
console.log("window.innerWidth::",window.innerWidth)
if(window.innerWidth>600 && window.innerWidth<1024){
screenWidth.value = window.innerWidth - 120 + 'px';
}else if(window.innerWidth < 600){
//
screenWidth.value = window.innerWidth - 60 + 'px';
}
}
onMounted(() => {
addCopyEvents();
addImageClickEvent();
setMarkdownBodyWidth();
window.addEventListener('resize', setMarkdownBodyWidth);
});
onUpdated(() => {
addCopyEvents();
addImageClickEvent();
});
onUnmounted(() => {
removeCopyEvents();
removeImageClickEvent();
window.removeEventListener('resize', setMarkdownBodyWidth);
});
function copyToClip(text: string) {
@ -111,6 +233,18 @@
font-size: 0.875rem;
line-height: 1.25rem;
}
.error {
background: linear-gradient(135deg, #FF4444, #FF914D) !important;
border-radius: 0.375rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
font-size: 0.875rem;
line-height: 1.25rem;
}
.self {
// background-color: #d2f9d1;
background-color: @primary-color;
@ -125,4 +259,11 @@
font-size: 0.875rem;
line-height: 1.25rem;
}
@media (max-width: 1024px) {
//
.textWrap{
margin-left: -40px;
margin-top: 10px;
}
}
</style>

View File

@ -0,0 +1,71 @@
<!--image放大封装-->
<template>
<div class="amplify-image">
<div class="img-preview-content" @click="hideImageClick" @mousewheel="handlePicMousewheel">
<img :src="imageUrl" ref="imageRef" />
</div>
</div>
</template>
<script setup lang="ts">
//
import {onMounted, ref, unref} from 'vue';
const props = defineProps(['imageUrl']);
const emit = defineEmits(['register', 'hide']);
//ref
const imageRef = ref();
//
const scale = ref<number>(1);
/**
* 隐藏图片
*/
function hideImageClick() {
scale.value = 1;
emit('hide')
}
/**
* 鼠标滑轮滚动
* @param event
*/
function handlePicMousewheel(event) {
event.preventDefault();
//
const delta = event.deltaY > 0 ? -1 : 1;
const scaleStep = 0.1;
//
scale.value = scale.value + delta * scaleStep
imageRef.value.style.transform = `scale(${unref(scale)})`;
}
</script>
<style scoped lang="less">
.amplify-image{
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
.img-preview-content{
display: flex;
width: 100%;
height: 100%;
color: #fff;
justify-content: center;
align-items: center;
touch-action: none;
-webkit-user-drag: none;
img{
transition: transform 0.3s;
background-position: center center;
background-repeat: no-repeat;
-webkit-background-size: cover;
-moz-background-size: cover;
background-size: cover;
}
}
}
</style>

View File

@ -2,38 +2,40 @@
(function () {
let widgetInstance = null;
const defaultConfig = {
// 支持'top-left'左上, 'top-right'右上, 'bottom-left'左下, 'bottom-right'右下
// 支持'top-left'左上, 'top-right'右上, 'bottom-left'左下, 'bottom-right'右下
iconPosition: 'bottom-right',
//图标的大小
iconSize: '30px',
//图标的颜色
//图标的大小
iconSize: '45px',
//图标的颜色
iconColor: '#155eef',
//必填不允许修改
//必填不允许修改
appId: '',
//聊天弹窗的宽度
//聊天弹窗的宽度
chatWidth: '800px',
//聊天弹窗的高度
//聊天弹窗的高度
chatHeight: '700px',
};
/**
* ai
* ai
* @param config
*/
function createAiChat(config) {
// 单例模式,确保只存在一个实例
// 单例模式,确保只存在一个实例
if (widgetInstance) {
return;
}
// 合并配置
// 合并配置
const finalConfig = { ...defaultConfig, ...config };
if (!finalConfig.appId) {
console.error('appId');
console.error('appId');
return;
}
// 创建容器
let body = document.body;
body.style.margin = "0";
// 创建容器
const container = document.createElement('div');
container.style.cssText = `
position: fixed;
@ -41,39 +43,50 @@
${getPositionStyles(finalConfig.iconPosition)}
cursor: pointer;
`;
// 创建图标
// 创建图标
const icon = document.createElement('div');
icon.style.cssText = `
width: ${finalConfig.iconSize};
height: ${finalConfig.iconSize};
background-color: ${finalConfig.iconColor};
border-radius: 50%;
box-shadow: rgba(0, 0, 0, 0.2) 0 4px 8px 0;
padding: 12px;
box-shadow: #cccccc 0 4px 8px 0;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
box-sizing: border-box;
`;
icon.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewBox="0 0 1024 1024" class="iconify iconify--ant-design"><path fill="currentColor" d="M573 421c-23.1 0-41 17.9-41 40s17.9 40 41 40c21.1 0 39-17.9 39-40s-17.9-40-39-40m-280 0c-23.1 0-41 17.9-41 40s17.9 40 41 40c21.1 0 39-17.9 39-40s-17.9-40-39-40"></path><path fill="currentColor" d="M894 345c-48.1-66-115.3-110.1-189-130v.1c-17.1-19-36.4-36.5-58-52.1c-163.7-119-393.5-82.7-513 81c-96.3 133-92.2 311.9 6 439l.8 132.6c0 3.2.5 6.4 1.5 9.4c5.3 16.9 23.3 26.2 40.1 20.9L309 806c33.5 11.9 68.1 18.7 102.5 20.6l-.5.4c89.1 64.9 205.9 84.4 313 49l127.1 41.4c3.2 1 6.5 1.6 9.9 1.6c17.7 0 32-14.3 32-32V753c88.1-119.6 90.4-284.9 1-408M323 735l-12-5l-99 31l-1-104l-8-9c-84.6-103.2-90.2-251.9-11-361c96.4-132.2 281.2-161.4 413-66c132.2 96.1 161.5 280.6 66 412c-80.1 109.9-223.5 150.5-348 102m505-17l-8 10l1 104l-98-33l-12 5c-56 20.8-115.7 22.5-171 7l-.2-.1C613.7 788.2 680.7 742.2 729 676c76.4-105.3 88.8-237.6 44.4-350.4l.6.4c23 16.5 44.1 37.1 62 62c72.6 99.6 68.5 235.2-8 330"></path><path fill="currentColor" d="M433 421c-23.1 0-41 17.9-41 40s17.9 40 41 40c21.1 0 39-17.9 39-40s-17.9-40-39-40"></path></svg>';
// 创建iframe容器
// 创建iframe容器
const iframeContainer = document.createElement('div');
let right = finalConfig.chatWidth === '100%' ? '0' : '10px';
let bottom = finalConfig.chatHeight === '100%' ? '0' : '10px';
let chatWidth = finalConfig.chatWidth;
let chatHeight = finalConfig.chatHeight;
if(isMobileDevice()){
chatWidth = "100%";
chatHeight = "100%";
right = '0';
bottom = '0';
}
iframeContainer.style.cssText = `
position: absolute;
right: 10px;
bottom: 10px;
width: ${finalConfig.chatWidth} !important;
height: ${finalConfig.chatHeight} !important;
position: fixed;
right: ${right};
bottom: ${bottom};
width: ${chatWidth} !important;
height: ${chatHeight} !important;
background: white;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0,0,0,0.2);
box-shadow: 0 0 20px #cccccc;
display: none;
z-index: 10000;
`;
// 创建iframe
// 创建iframe
const iframe = document.createElement('iframe');
iframe.style.cssText = `
width: 100%;
@ -83,15 +96,23 @@
`;
iframe.id = 'ai-app-chat-document';
iframe.src = getIframeSrc(finalConfig) + '/ai/app/chat/' + finalConfig.appId;
// 创建关闭按钮
//update-begin---author:wangshuai---date:2025-04-25---for:【QQYUN-12159】【AI 广告位】让需要自建AI知识库的用户知道如何通过敲敲云搭建自己的AI知识库---
iframe.src = getIframeSrc(finalConfig) + '/ai/app/chat/' + finalConfig.appId + "?source=chatJs";
//update-end---author:wangshuai---date:2025-04-25---for:【QQYUN-12159】【AI 广告位】让需要自建AI知识库的用户知道如何通过敲敲云搭建自己的AI知识库---
let iconRight = finalConfig.chatWidth === '100%'?'0':'-6px';
let iconTop = finalConfig.chatWidth === '100%'?'0':'-9px';
if(isMobileDevice()){
iconRight = '2px';
iconTop = '2px';
}
// 创建关闭按钮
const closeBtn = document.createElement('div');
closeBtn.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 1024 1024" class="iconify iconify--ant-design"><path fill="currentColor" fill-rule="evenodd" d="M799.855 166.312c.023.007.043.018.084.059l57.69 57.69c.041.041.052.06.059.084a.1.1 0 0 1 0 .069c-.007.023-.018.042-.059.083L569.926 512l287.703 287.703c.041.04.052.06.059.083a.12.12 0 0 1 0 .07c-.007.022-.018.042-.059.083l-57.69 57.69c-.041.041-.06.052-.084.059a.1.1 0 0 1-.069 0c-.023-.007-.042-.018-.083-.059L512 569.926L224.297 857.629c-.04.041-.06.052-.083.059a.12.12 0 0 1-.07 0c-.022-.007-.042-.018-.083-.059l-57.69-57.69c-.041-.041-.052-.06-.059-.084a.1.1 0 0 1 0-.069c.007-.023.018-.042.059-.083L454.073 512L166.371 224.297c-.041-.04-.052-.06-.059-.083a.12.12 0 0 1 0-.07c.007-.022.018-.042.059-.083l57.69-57.69c.041-.041.06-.052.084-.059a.1.1 0 0 1 .069 0c.023.007.042.018.083.059L512 454.073l287.703-287.702c.04-.041.06-.052.083-.059a.12.12 0 0 1 .07 0Z"></path></svg>';
closeBtn.style.cssText = `
position: absolute;
right: -3px;
top: -10px;
margin-top: ${iconTop};
right: ${iconRight};
cursor: pointer;
background: white;
width: 25px;
@ -100,17 +121,17 @@
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
box-shadow: 0 2px 5px #cccccc;
`;
// 组装元素
// 组装元素
iframeContainer.appendChild(closeBtn);
iframeContainer.appendChild(iframe);
document.body.appendChild(iframeContainer);
container.appendChild(icon);
document.body.appendChild(container);
// 事件监听
// 事件监听
icon.addEventListener('click', () => {
iframeContainer.style.display = 'block';
});
@ -119,7 +140,7 @@
iframeContainer.style.display = 'none';
});
// 保存实例引用
// 保存实例引用
widgetInstance = {
remove: () => {
container.remove();
@ -129,7 +150,7 @@
}
/**
*
*
*
* @param position
* @returns {*|string}
@ -145,7 +166,7 @@
}
/**
* src
* src
*/
function getIframeSrc(finalConfig) {
const specificScript = document.getElementById("e7e007dd52f67fe36365eff636bbffbd");
@ -154,6 +175,14 @@
}
}
// 暴露全局方法
/**
*
* @returns {boolean}
*/
function isMobileDevice() {
return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
// 暴露全局方法
window.createAiChat = createAiChat;
})();

View File

@ -1,6 +1,6 @@
<template>
<div class="presetQuestion-wrap">
<svg
<!-- <svg
v-if="btnShow"
class="leftBtn"
:class="leftBtnStatus"
@ -16,7 +16,7 @@
fill="currentColor"
p-id="5071"
></path>
</svg>
</svg>-->
<div class="content">
<ul ref="ulElemRef">
<li v-for="(item, index) in data" :key="index" class="item" @click="handleQuestion(item.descr)">
@ -28,7 +28,7 @@
</li>
</ul>
</div>
<svg
<!-- <svg
v-if="btnShow"
class="rightBtn"
:class="rightBtnStatus"
@ -44,7 +44,7 @@
fill="currentColor"
p-id="5071"
></path>
</svg>
</svg>-->
</div>
</template>
@ -135,7 +135,8 @@ import {ref, onMounted, onBeforeUnmount, watch} from 'vue';
display: flex;
margin-bottom: 0;
width: 100%;
overflow-y: auto;
overflow-y: hidden;
overflow-x: auto;
/* 隐藏所有滚动条 */
&::-webkit-scrollbar {
display: none;
@ -143,8 +144,15 @@ import {ref, onMounted, onBeforeUnmount, watch} from 'vue';
width: 0;
}
}
ul:hover {
&::-webkit-scrollbar {
display: block;
height: 10px;
width: 10px;
}
}
.item {
border: 1px solid rgba(0, 0, 0, 0.1);
border: 1px solid #e6e6e6;
border-radius: 16px;
cursor: pointer;
font-size: 14px;

View File

@ -71,6 +71,13 @@
</li>
</ul>
</div>
<div class="left-footer" v-if="source!='chatJs'">
AI客服由
<a style="color: #4183c4;margin-left: 2px;margin-right: 2px" href="https://www.qiaoqiaoyun.com/aiCustomerService" target="_blank">
敲敲云
</a>
提供
</div>
</div>
</template>
@ -80,7 +87,7 @@
import { defHttp } from '@/utils/http/axios';
import { getFileAccessHttpUrl } from '@/utils/common/compUtils';
import defaultImg from '../img/ailogo.png';
const props = defineProps(['dataSource', 'appData']);
const props = defineProps(['dataSource', 'appData','source']);
const emit = defineEmits(['save', 'click', 'reloadRight', 'prologue']);
const inputRef = ref(null);
const router = useRouter();
@ -149,8 +156,7 @@
emit('click', props.dataSource.history[0].title, findIndex);
} else {
//
props.dataSource.active = null;
emit('click', "", -1);
handleCreate();
}
}
//update-begin---author:wangshuai---date:2025-03-12---for:QQYUN-11560---
@ -216,6 +222,7 @@
flex: 1;
min-height: 0;
overflow: auto;
margin-bottom: 20px;
&::-webkit-scrollbar {
width: 8px;
height: 8px;
@ -285,6 +292,9 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
position: relative;
top: 2px;
&.ant-input {
margin-right: 20px;
}
@ -316,4 +326,13 @@
font-size: 16px;
}
}
.left-footer{
display:flex;
margin-right: 20px;
font-size: 12px;
position: absolute;
bottom: 4px;
left: 50px;
width: 100%;
}
</style>

View File

@ -38,10 +38,10 @@ html.dark {
--color-canvas-subtle: #161b22;
--color-border-default: #30363d;
--color-border-muted: #21262d;
--color-neutral-muted: rgba(110, 118, 129, 0.4);
--color-neutral-muted: #bfc4cc;
--color-accent-fg: #58a6ff;
--color-accent-emphasis: #1f6feb;
--color-attention-subtle: rgba(187, 128, 9, 0.15);
--color-attention-subtle: #ece6d9;
--color-danger-fg: #f85149;
}
}
@ -86,7 +86,7 @@ html {
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsla(210, 18%, 87%, 1);
--color-neutral-muted: rgba(175, 184, 193, 0.2);
--color-neutral-muted: #e7ebf2;
--color-accent-fg: #0969da;
--color-accent-emphasis: #0969da;
--color-attention-subtle: #fff8c5;

View File

@ -20,7 +20,7 @@
<div class="flex text-status" v-if="item.metadata && item.metadata.length>0">
<span class="tag-input">输入</span>
<div v-for="(metaItem, index) in item.metadata">
<a-tag color="rgba(87,104,161,0.08)" class="tags-meadata">
<a-tag color="#f2f3f8" class="tags-meadata">
<span v-if="index<3" class="tag-text">{{ metaItem.field }}</span>
</a-tag>
</div>
@ -126,7 +126,7 @@
};
/**
* 加载知识库
* 加载AI流程
*/
function loadFlowData() {
let params = {
@ -135,7 +135,7 @@
column: 'createTime',
order: 'desc',
name: searchText.value,
status:'enable'
status: 'enable,release'
};
getAiFlowList(params).then((res) =>{
if(res){
@ -313,11 +313,11 @@
white-space: nowrap;
height: 20px;
font-size: 12px;
color: rgba(15, 21, 40,0.82);
color: #3a3f4f;
}
.tag-input{
align-self: center;
color: rgba(55,67,106,0.7);
color: #707a97;
font-size: 12px;
font-style: normal;
font-weight: 500;

View File

@ -2,7 +2,6 @@
<div class="p-2">
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="600px" :title="title" @ok="handleOk" @cancel="handleCancel">
<div class="flex header">
<span>所选知识库必须使用相同的 Embedding 模型</span>
<a-input
@pressEnter="loadKnowledgeData"
class="header-search"
@ -19,7 +18,7 @@
<img class="checkbox-img" :src="knowledge" />
<span class="checkbox-name">{{ item.name }}</span>
</div>
<a-checkbox v-model:checked="item.checked" @click.stop class="quantum-checker"> </a-checkbox>
<a-checkbox v-model:checked="item.checked" @click.stop class="quantum-checker" @change="(e)=>handleChange(e,item)"> </a-checkbox>
</div>
</a-card>
</a-col>
@ -91,6 +90,7 @@
* 保存
*/
async function handleOk() {
console.log("知识库确定选中的值",knowledgeData.value);
emit('success', knowledgeIds.value, knowledgeData.value);
handleCancel();
}
@ -103,16 +103,17 @@
}
//
const handleSelect = (item) => {
function handleSelect(item){
let id = item.id;
const target = appKnowledgeOption.value.find((item) => item.id === id);
if (target) {
target.checked = !target.checked;
}
//id
if (knowledgeIds.value.length == 0) {
if (!knowledgeIds.value || knowledgeIds.value.length == 0) {
knowledgeIds.value.push(id);
knowledgeData.value.push(item);
console.log("知识库勾选或取消勾选复选框的值",knowledgeData.value);
return;
}
let findIndex = knowledgeIds.value.findIndex((item) => item === id);
@ -123,7 +124,8 @@
knowledgeIds.value.splice(findIndex, 1);
knowledgeData.value.splice(findIndex, 1);
}
};
console.log("知识库勾选或取消勾选复选框的值",knowledgeData.value);
}
/**
* 加载知识库
@ -176,6 +178,25 @@
});
}
/**
* 复选框选中事件
*
* @param e
* @param item
*/
function handleChange(e, item) {
if (e.target.checked) {
knowledgeIds.value.push(item.id);
knowledgeData.value.push(item);
} else {
let findIndex = knowledgeIds.value.findIndex((val) => val === item.id);
if (findIndex != -1) {
knowledgeIds.value.splice(findIndex, 1);
knowledgeData.value.splice(findIndex, 1);
}
}
}
return {
registerModal,
title,
@ -193,6 +214,7 @@
searchText,
loadKnowledgeData,
handleClearClick,
handleChange,
};
},
};
@ -222,7 +244,7 @@
.list-footer {
position: absolute;
bottom: 0;
left: 260px;
right: 10px;
}
.checkbox-card {
margin-bottom: 10px;

View File

@ -109,24 +109,80 @@
handleCancel();
}
//update-begin---author:wangshuai---date:2025-04-01---for:QQYUN-11796AI---
/**
* 生成
*/
function generatedPrompt() {
async function generatedPrompt() {
content.value = '';
loading.value = true;
promptGenerate({ prompt: prompt.value })
.then((res) => {
if (res.success) {
content.value = res.result;
let readableStream = await promptGenerate({ prompt: prompt.value }).catch(() => {
loading.value = false;
});
const reader = readableStream.getReader();
const decoder = new TextDecoder('UTF-8');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
let result = decoder.decode(value, { stream: true });
const lines = result.split('\n\n');
for (const line of lines) {
if (line.startsWith('data:')) {
const content = line.replace('data:', '').trim();
if(!content){
continue;
}
if(!content.endsWith('}')){
buffer = buffer + line;
continue;
}
buffer = "";
renderText(content)
} else {
if(!line) {
continue;
}
if(!line.endsWith('}')) {
buffer = buffer + line;
continue;
}
buffer = "";
renderText(line)
}
}
loading.value = false;
})
.catch(() => {
loading.value = false;
});
}
}
/**
* 渲染文本
*
* @param item
*/
function renderText(item) {
try {
let parse = JSON.parse(item);
if (parse.event == 'MESSAGE') {
content.value += parse.data.message;
if(loading.value){
loading.value = false;
}
}
if (parse.event == 'MESSAGE_END') {
loading.value = false;
}
if (parse.event == 'ERROR') {
content.value = parse.data.message?parse.data.message:'生成失败,请稍后重试!'
loading.value = false;
}
} catch (error) {
console.log('Error parsing update:', error);
}
}
//update-end---author:wangshuai---date:2025-04-01---for:QQYUN-11796AI---
/**
* 指令点击事件
*/

View File

@ -1,14 +1,24 @@
<template>
<div class="p-2">
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="800px" :title="title" @ok="handleOk" @cancel="handleCancel">
<template #title>
<span style="display: flex">
{{title}}
<a-tooltip title="AI应用文档">
<a style="color: unset" href="https://help.jeecg.com/aigc/guide/app" target="_blank">
<Icon style="position:relative;left:2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
</a>
</a-tooltip>
</span>
</template>
<BasicForm @register="registerForm">
<template #typeSlot="{ model, field }">
<a-radio-group v-model:value="type" style="display: flex">
<a-radio-group v-model:value="model[field]" style="display: flex">
<a-card
v-for="item in appTypeOption"
style="margin-right: 10px; cursor: pointer; width: 100%"
@click="handleTypeClick(item.value)"
:style="type === item.value ? { borderColor: '#3370ff' } : {}"
@click="model[field] = item.value"
:style="model[field] === item.value ? { borderColor: '#3370ff' } : {}"
>
<a-radio :value="item.value">
<div class="type-title">{{ item.title }}</div>
@ -23,7 +33,7 @@
</template>
<script lang="ts">
import { ref, unref } from 'vue';
import { ref, unref, computed } from 'vue';
import BasicModal from '@/components/Modal/src/BasicModal.vue';
import { useModal, useModalInner } from '@/components/Modal';
@ -42,15 +52,13 @@
},
emits: ['success', 'register'],
setup(props, { emit }) {
const title = ref<string>('创建应用');
//
const isUpdate = ref<boolean>(false);
const title = computed<string>(() => isUpdate.value ? '修改应用' : '创建应用');
//app
const appTypeOption = ref<any>([]);
//
const type = ref<string>('chatSimple');
//
const [registerForm, { validate, resetFields, setFieldsValue }] = useForm({
@ -63,7 +71,6 @@
//modal
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
await resetFields();
type.value = 'chatSimple';
//update-begin---author:wangshuai---date:2025-03-11---for: QQYUN-113248.head---
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
@ -71,6 +78,10 @@
await setFieldsValue({
...data.record,
});
} else {
await setFieldsValue({
type: 'chatSimple',
})
}
//update-end---author:wangshuai---date:2025-03-11---for:QQYUN-113248.head---
setModalProps({ minHeight: 500, bodyStyle: { padding: '10px' } });
@ -83,7 +94,6 @@
try {
let values = await validate();
setModalProps({ confirmLoading: true });
values.type = type.value;
let result = await saveApp(values);
if (result) {
//
@ -128,13 +138,6 @@
closeModal();
}
/**
* 应用类型点击事件
*/
function handleTypeClick(val) {
type.value = val;
}
return {
registerModal,
registerForm,
@ -142,8 +145,6 @@
handleOk,
handleCancel,
appTypeOption,
type,
handleTypeClick,
};
},
};

View File

@ -6,13 +6,20 @@
<div style="display: flex">
<img :src="getImage()" class="header-img"/>
<div class="header-name">{{formState.name}}</div>
<a-tooltip title="编辑">
<a-tooltip v-if="!isRelease" title="编辑">
<Icon icon="ant-design:edit-outlined" style="margin-left: 4px;cursor: pointer" color="#354052" size="20" @click="handleEdit"></Icon>
</a-tooltip>
</div>
<div>应用编排</div>
<div>
应用编排
<a-tooltip title="AI应用文档">
<a style="color: unset" href="https://help.jeecg.com/aigc/guide/app" target="_blank">
<Icon style="position:relative;left:2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
</a>
</a-tooltip>
</div>
<div style="display: flex">
<a-button @click="handleOk" style="margin-right: 30px" type="primary">保存</a-button>
<a-button v-if="!isRelease" @click="handleOk" style="margin-right: 30px" type="primary"></a-button>
</div>
</div>
</template>
@ -35,7 +42,7 @@
<template #label>
<div style="display: flex;justify-content: space-between;width: 100%;">
<span>关联流程</span>
<span @click="handleAddFlowClick" class="knowledge-txt">
<span v-if="!isRelease" @click="handleAddFlowClick" class="knowledge-txt">
<Icon icon="ant-design:plus-outlined" size="13" style="margin-right: 2px"></Icon>添加
</span>
</div>
@ -49,14 +56,14 @@
<div class="flex text-status" v-if="flowData.metadata && flowData.metadata.length>0">
<span class="tag-input">输入</span>
<div v-for="(metaItem, index) in flowData.metadata">
<a-tag color="rgba(87,104,161,0.08)" class="tags-meadata">
<a-tag color="#f2f3f8" class="tags-meadata">
<span v-if="index<5" class="tag-text">{{ metaItem.field }}</span>
</a-tag>
</div>
</div>
</div>
</div>
<Icon @click="handleDeleteFlow" icon="ant-design:close-outlined" size="20" class="knowledge-icon"></Icon>
<Icon v-if="!isRelease" @click="handleDeleteFlow" icon="ant-design:close-outlined" size="20" class="knowledge-icon"></Icon>
</div>
</a-card>
<div v-else class="data-empty-text">
@ -71,7 +78,7 @@
<template #label>
<div class="prompt-title-padding item-title space-between">
<span>提示词</span>
<a-button size="middle" @click="generatedPrompt" ghost>
<a-button v-if="!isRelease" size="middle" @click="generatedPrompt" ghost>
<span style="align-items: center;display:flex">
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"></path></svg>
<span style="margin-left: 4px">生成</span>
@ -79,7 +86,7 @@
</a-button>
</div>
</template>
<a-textarea :rows="8" v-model:value="formState.prompt" placeholder="请输入提示词"/>
<a-textarea :disabled="isRelease" :rows="8" v-model:value="formState.prompt" placeholder="请输入提示词"/>
</a-form-item>
</div>
</a-col>
@ -90,7 +97,7 @@
<div class="prompt-title-padding item-title">开场白</div>
</template>
<div class="prologue-chunk-edit">
<j-markdown-editor :height="166" v-model:value="formState.prologue" @change="prologueTextAreaBlur" :preview="{ mode: 'view', action: [] }"></j-markdown-editor>
<j-markdown-editor :height="166" v-model:value="formState.prologue" :disabled="isRelease" @change="prologueTextAreaBlur" :preview="{ mode: 'view', action: [] }"></j-markdown-editor>
</div>
</a-form-item>
</div>
@ -101,7 +108,7 @@
<template #label>
<div class="prompt-title-padding item-title space-between">
<div class="item-title">预设问题</div>
<a-tooltip title="添加预设问题">
<a-tooltip v-if="!isRelease" title="添加预设问题">
<Icon icon="ant-design:plus-outlined" size="13" style="margin-right: 16px;cursor: pointer" @click="presetQuestionAddClick"></Icon>
</a-tooltip>
</div>
@ -110,9 +117,9 @@
<draggable :disabled="disabledDrag" item-key="key" v-model="presetQuestionList" @end="presetQuestionEnd">
<template #item="{ element:item }">
<div style="display: flex;width: 100%;margin-top: 10px">
<Icon icon="ant-design:holder-outlined" size="20"></Icon>
<a-input placeholder="输入预设问题" v-model:value="item.descr" style="margin-left: 10px;" @blur="onBlur(item)" @focus="onFocus(item)" @change="questionChange"></a-input>
<Icon style="cursor: pointer;margin-left: 10px" icon="ant-design:delete-outlined" @click="deleteQuestionClick(item.key)"></Icon>
<Icon v-if="!isRelease" icon="ant-design:holder-outlined" size="20"></Icon>
<a-input :disabled="isRelease" placeholder="输入预设问题" v-model:value="item.descr" style="margin-left: 10px;" @blur="onBlur(item)" @focus="onFocus(item)" @change="questionChange"></a-input>
<Icon v-if="!isRelease" style="cursor: pointer;margin-left: 10px" icon="ant-design:delete-outlined" @click="deleteQuestionClick(item.key)"></Icon>
</div>
</template>
</draggable>
@ -129,7 +136,7 @@
<template #label>
<div class="prompt-title-padding item-title space-between">
<div class="item-title">快捷指令</div>
<a-tooltip title="添加快捷指令">
<a-tooltip v-if="!isRelease" title="添加快捷指令">
<Icon icon="ant-design:plus-outlined" size="13" style="margin-right: 16px;cursor: pointer" @click="quickCommandAddClick"></Icon>
</a-tooltip>
</div>
@ -143,7 +150,7 @@
<svg v-else width="14px" height="14px" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"></path></svg>
<div style="max-width: 400px;margin-left: 4px" class="ellipsis">{{item.name}}</div>
</div>
<div style="align-items: center" class="quick-command-icon">
<div v-if="!isRelease" style="align-items: center" class="quick-command-icon">
<a-tooltip title="编辑">
<Icon style="cursor: pointer;margin-left: 10px" icon="ant-design:edit-outlined" @click="editCommandClick(item)"></Icon>
</a-tooltip>
@ -167,13 +174,14 @@
<template #label>
<div style="display: flex;justify-content: space-between;width: 100%;margin-right: 2px">
<div class="item-title">AI模型</div>
<div @click="handleParamSettingClick('model')" class="knowledge-txt">
<div v-if="!isRelease" @click="handleParamSettingClick('model')" class="knowledge-txt">
<Icon icon="ant-design:setting-outlined" size="13" style="margin-right: 2px"></Icon>参数配置
</div>
</div>
</template>
<JDictSelectTag
v-model:value="formState.modelId"
:disabled="isRelease"
placeholder="请选择AI模型"
dict-code="airag_model where model_type = 'LLM',name,id"
style="width: 100%;"
@ -193,7 +201,7 @@
<template #label>
<div style="display: flex; justify-content: space-between; width: 100%;margin-left: 2px;">
<div class="item-title">知识库</div>
<div>
<div v-if="!isRelease">
<span @click="handleParamSettingClick('knowledge')" class="knowledge-txt">
<Icon icon="ant-design:setting-outlined" size="13" style="margin-right: 2px"></Icon>参数配置
</span>
@ -209,9 +217,10 @@
<div style="display: flex; width: 100%; justify-content: space-between">
<div>
<img class="knowledge-img" :src="knowledge" />
<span class="knowledge-name">{{ item.name }}</span>
<span class="knowledge-name" style="color: #e03e2d;text-decoration: line-through" v-if="item.type">{{ item.name }}</span>
<span class="knowledge-name" v-else>{{ item.name }}</span>
</div>
<Icon @click="handleDeleteKnowledge(item.id)" icon="ant-design:close-outlined" size="20" class="knowledge-icon"></Icon>
<Icon v-if="!isRelease" @click="handleDeleteKnowledge(item.id)" icon="ant-design:close-outlined" size="20" class="knowledge-icon"></Icon>
</div>
</a-card>
</a-col>
@ -228,10 +237,23 @@
<template #label>
<div style="margin-left: 2px">历史聊天记录</div>
</template>
<a-input-number v-model:value="formState.msgNum"></a-input-number>
<a-input-number :disabled="isRelease" v-model:value="formState.msgNum"></a-input-number>
</a-form-item>
</div>
</a-col>
<a-col :span="24" class="mt-10">
<div class="prologue-chunk">
<div style="margin-left: 2px">个性化设置</div>
<a-row>
<a-form-item :labelCol="labelCol" :wrapperCol="wrapperCol" v-bind="validateInfos.multiSession">
<div style="display: flex;margin-top: 10px">
<div style="margin-left: 2px">多会话模式</div>
<a-switch :disabled="isRelease" v-model:checked="multiSessionChecked" checked-children="" un-checked-children="" @change="handleMultiSessionChange"></a-switch>
</div>
</a-form-item>
</a-row>
</div>
</a-col>
</a-row>
</a-form>
</a-col>
@ -284,7 +306,6 @@
import draggable from 'vuedraggable';
import { useMessage } from "@/hooks/web/useMessage";
import defaultFlowImg from "@/assets/images/ai/aiflow.png";
export default {
name: 'AiAppSettingModal',
components: {
@ -358,10 +379,15 @@
//
const quickCommand = ref<any>('');
const { createMessage } = useMessage();
//
const multiSessionChecked = ref<boolean>(true);
//
const isRelease = ref<boolean>(false);
//modal
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
appId.value = data.id;
isUpdate.value = !!data?.isUpdate;
isRelease.value = data?.status === 'release';
clearParam();
if (isUpdate.value) {
setTimeout(() => {
@ -371,14 +397,16 @@
//id
queryById({ id: data.id }).then((res) => {
if (res.success) {
resetFields();
//
Object.assign(formState, res.result);
formState.prompt = AiAppJson.prompt;
formState.prologue = AiAppJson.prologue;
formState.presetQuestion = JSON.stringify(AiAppJson.presetQuestion);
prologue.value = AiAppJson.prologue;
formState.prompt = cloneDeep(AiAppJson.prompt);
formState.prologue = cloneDeep(AiAppJson.prologue);
formState.presetQuestion = JSON.stringify(cloneDeep(AiAppJson.presetQuestion));
formState.msgNum = 1;
prologue.value = cloneDeep(AiAppJson.prologue);
presetQuestion.value = formState.presetQuestion;
presetQuestionList.value = AiAppJson.presetQuestion;
presetQuestionList.value = cloneDeep(AiAppJson.presetQuestion);
addRules(res.result.type)
}
});
@ -403,6 +431,7 @@
setModalProps({ confirmLoading: true });
formState.knowledgeIds = knowledgeIds.value;
await saveApp(formState);
emit('success')
} finally {
setModalProps({ confirmLoading: false });
}
@ -457,7 +486,9 @@
*/
function handleSuccess(knowledgeId, knowledgeData) {
knowledgeIds.value = cloneDeep(knowledgeId.join(','));
console.log("知识库id",knowledgeIds.value);
knowledgeDataList.value = cloneDeep(knowledgeData);
console.log("知识库的数据",knowledgeDataList.value);
formState.knowledgeIds = knowledgeIds.value;
}
@ -481,10 +512,30 @@
*/
function getKnowledgeDataList(ids) {
queryKnowledgeBathById({ ids: ids }).then((res) => {
if (res.success) {
knowledgeDataList.value = res.result;
//update-begin---author:wangshuai---date:2025-04-24---for:QQYUN-12133AI---
if (res.success && res.result) {
let result = res.result;
let idArray = ids.split(",");
let arr = [];
for (const id of idArray) {
let filter = result.filter((item) => item.id === id);
if(filter && filter.length > 0) {
arr.push({ id: id, name: filter[0].name});
} else {
arr.push({ name: '该知识库已被删除', id: id,type: 'delete' })
}
}
knowledgeDataList.value = arr;
knowledgeIds.value = ids;
} else {
let arr = [];
for (const id of ids) {
arr.push({ name: '该知识库已被删除', id: id})
}
knowledgeDataList.value = arr;
knowledgeIds.value = ids;
}
//update-end---author:wangshuai---date:2025-04-24---for:QQYUN-12133AI---
});
}
@ -497,7 +548,7 @@
/**
* 关闭弹窗触发列表刷新
*
*
* @param value
*/
function visibleChange(value) {
@ -508,7 +559,7 @@
/**
* 添加检验
*
*
* @param type
*/
function addRules(type){
@ -535,13 +586,13 @@
/**
* 参数配置确定回调事件
*
*
* @param value
*/
function handleParamsSettingOk(value){
metadata.value = value;
Object.assign(metadata.value,value)
if(value) {
formState.metadata = JSON.stringify(value);
formState.metadata = JSON.stringify(metadata.value);
}
}
@ -620,16 +671,16 @@
/**
* 应用编辑回调事件
*
*
* @param values
*/
function handelEditSuccess(values) {
formState.icon = values.icon ? values.icon:'';
formState.name = values.name ? values.name:'';
}
//=========== begin ===========================
//
const disabledDrag = computed(()=>{
let list = presetQuestionList.value;
@ -641,7 +692,7 @@
}
return false;
});
/**
* 预设问题拖拽
*/
@ -652,7 +703,7 @@
/**
* 预设问题添加
*
*
* @param e
*/
function presetQuestionAddClick(e){
@ -662,11 +713,11 @@
}
const length = presetQuestionList.value.length;
presetQuestionList.value.push({key: length + 1, sort: length + 1, descr: ''})
}
}
/**
* 预设问题删除
*
*
* @param key
*/
function deleteQuestionClick(key){
@ -693,7 +744,7 @@
/**
* 预设问题值改变事件
*
*
*/
function questionChange() {
if(presetQuestionList.value && presetQuestionList.value.length>0){
@ -704,9 +755,9 @@
formState.presetQuestion = "";
}
}
//=========== end ===========================
/**
* 清除参数
*/
@ -717,9 +768,10 @@
flowId.value = '';
flowData.value = null;
presetQuestion.value = '';
presetQuestionList.value = [];
presetQuestionList.value = [{ key:1, sort: 1, descr: '' }];
quickCommandList.value = [];
quickCommand.value = '';
multiSessionChecked.value = true;
}
/**
@ -735,13 +787,24 @@
data.msgNum = data.msgNum ? data.msgNum : 1;
if(data.metadata){
metadata.value = JSON.parse(data.metadata);
if(metadata.value?.multiSession){
multiSessionChecked.value = metadata.value.multiSession === '1';
}else{
multiSessionChecked.value = "1";
}
}
if(data.presetQuestion){
presetQuestion.value = data.presetQuestion;
presetQuestionList.value = JSON.parse(data.presetQuestion);
}
if(data.quickCommand){
quickCommandList.value = JSON.parse(data.quickCommand);
//update-begin---author:wangshuai---date:2025-04-08---for:QQYUN-11939ai ---
let parse = JSON.parse(data.quickCommand);
for (let i = 0; i < parse.length; i++) {
parse[i].key = (i+1).toString();
}
quickCommandList.value = parse;
//update-end---author:wangshuai---date:2025-04-08---for:QQYUN-11939ai ---
}
//
Object.assign(formState, data);
@ -765,14 +828,14 @@
/**
* 提示词回调
*
*
* @param value
*/
function handleAiAppPromptOk(value) {
formState.prompt = value;
}
//============= end ================================
//=============== begin ============================
function quickCommandEnd() {
quickCommand.value = JSON.stringify(quickCommandList.value);
@ -780,7 +843,7 @@
}
/**
* 快捷指令新增呢个==点击事件
* 快捷指令新增点击事件
*/
function quickCommandAddClick(){
if(quickCommandList.value && quickCommandList.value.length > 4){
@ -788,8 +851,8 @@
return;
}
aiAppCommandModalOpen(true,{})
}
}
/**
* 快捷指令编辑点击事件
* @param item
@ -799,18 +862,21 @@
isUpdate: true,
record: item
})
}
}
/**
* 快捷指令添加回调事件
* @param value
*/
function handleAiAppCommandOk(value){
quickCommandList.value.push({ key: quickCommandList.value.length + 1, ...value });
//update-begin---author:wangshuai---date:2025-04-08---for:QQYUN-11939ai ---
value.key = (quickCommandList.value.length + 1).toString();
//update-end---author:wangshuai---date:2025-04-08---for:QQYUN-11939ai ---
quickCommandList.value.unshift({...value });
quickCommand.value = JSON.stringify(quickCommandList.value);
formState.quickCommand = quickCommand.value;
}
/**
* 快捷指令更新回调事件
* @param value
@ -819,6 +885,8 @@
let findIndex = quickCommandList.value.findIndex(item => item.key === value.key);
if(findIndex>-1){
quickCommandList.value[findIndex] = value;
quickCommand.value = JSON.stringify(quickCommandList.value);
formState.quickCommand = quickCommand.value;
}
}
@ -835,10 +903,23 @@
}
}
//=============== end ============================
/**
* 复选框相中时的回调
*/
function handleMultiSessionChange(checked){
if(checked){
metadata.value.multiSession = "1";
}else{
metadata.value.multiSession = "0";
}
formState.metadata = JSON.stringify(metadata.value);
}
return {
registerModal,
title,
isRelease,
handleOk,
handleCancel,
appTypeOption,
@ -895,6 +976,8 @@
quickCommand,
getFlowImage,
metadata,
multiSessionChecked,
handleMultiSessionChange,
};
},
};
@ -997,7 +1080,7 @@
align-content: center;
}
.prompt-back{
background-color: rgba(238,244,255,1);
background-color: #eef4ff;
border-radius: 12px;
padding: 2px;
border: 1px solid #77B2F8;
@ -1019,8 +1102,8 @@
border-radius: 12px;
padding: 2px 10px 2px 10px;
box-sizing: border-box;
}
}
.prologue-chunk-edit{
background-color: #f2f4f7;
border-radius: 12px;
@ -1033,7 +1116,7 @@
:deep(.ant-form-item-label){
padding: 0 !important;
}
:deep(.ant-form-item-required){
margin-left: 4px !important;
}
@ -1052,7 +1135,7 @@
}
:deep(.vditor){
border: none;
}
}
:deep(.vditor-sv){
font-size: 14px;
}
@ -1100,7 +1183,7 @@
}
}
.data-empty-text{
color: rgba(32,41,69,0.6);
color: #757c8f;
margin-left: 10px;
}
.flow-icon{
@ -1123,11 +1206,11 @@
white-space: nowrap;
height: 20px;
font-size: 12px;
color: rgba(15, 21, 40,0.82);
color: #3a3f4f;
}
.tag-input{
align-self: center;
color: rgba(55,67,106,0.7);
color: #737c97;
font-size: 12px;
font-style: normal;
font-weight: 500;

View File

@ -13,6 +13,7 @@ enum Api {
knowledgeDocList = '/airag/knowledge/doc/list',
knowledgeEditDoc = '/airag/knowledge/doc/edit',
knowledgeDeleteBatchDoc = '/airag/knowledge/doc/deleteBatch',
knowledgeDeleteAllDoc = '/airag/knowledge/doc/deleteAll',
knowledgeRebuildDoc = '/airag/knowledge/doc/rebuild',
knowledgeEmbeddingHitTest = '/airag/knowledge/embedding/hitTest',
}
@ -114,6 +115,18 @@ export const knowledgeDeleteBatchDoc = (params, handleSuccess) => {
});
};
/**
* 批量删除文档
*
* @param params
* @param handleSuccess
*/
export const knowledgeDeleteAllDoc = (knowId: string, handleSuccess) => {
return defHttp.delete({ url: Api.knowledgeDeleteAllDoc, params: {knowId} }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 命中测试
* @param params

View File

@ -0,0 +1,24 @@
import {knowledgeDeleteAllDoc} from "./AiKnowledgeBase.api";
import {useMessage} from "@/hooks/web/useMessage";
const {createConfirmSync} = useMessage();
// 清空文档
export async function doDeleteAllDoc(knowledgeId: string, reload: () => void) {
const flag = await createConfirmSync({
title: '清空文档',
content: () => (
<p>
<span></span>
<br/>
<span style="color: #ee0000;">
</span>
</p>
),
});
if (!flag) {
return;
}
knowledgeDeleteAllDoc(knowledgeId, reload)
}

View File

@ -45,7 +45,7 @@
<img class="header-img" src="./icon/knowledge.png" />
<div class="header-text">
<span class="header-text-top header-name ellipsis" :title="item.name"> {{ item.name }} </span>
<span class="header-text-top"> 创建者{{ item.createBy }} </span>
<span class="header-text-top"> 创建者{{ item.createBy_dictText || item.createBy }} </span>
</div>
</div>
</div>
@ -75,6 +75,10 @@
<Icon class="pointer" icon="ant-design:delete-outlined" size="16"></Icon>
删除
</a-menu-item>
<a-menu-item key="clear" @click.prevent.stop="onDeleteAllDoc(item)">
<Icon icon="ant-design:delete-outlined" size="16"></Icon>
清空文档
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
@ -93,6 +97,7 @@
@change="handlePageChange"
class="list-footer"
size="small"
:show-total="() => `共${total}条` "
/>
<!--添加知识库弹窗-->
<KnowledgeBaseModal @register="registerModal" @success="reload"></KnowledgeBaseModal>
@ -105,6 +110,7 @@
import { reactive, ref } from 'vue';
import { useModal } from '/@/components/Modal';
import { deleteModel, list, rebuild } from './AiKnowledgeBase.api';
import { doDeleteAllDoc } from "./AiKnowledgeBase.api.util";
import { Pagination } from 'ant-design-vue';
import JInput from '@/components/Form/src/jeecg/components/JInput.vue';
import KnowledgeBaseModal from './components/AiKnowledgeBaseModal.vue';
@ -221,13 +227,26 @@
* @param item
*/
async function handleDelete(item) {
if(knowledgeList.value.length == 1 && pageNo.value > 1) {
pageNo.value = pageNo.value - 1;
}
await deleteModel({ id: item.id, name: item.name }, reload);
}
/**
* 清空文档
* @param item
*/
async function onDeleteAllDoc(item) {
pageNo.value = 1;
return doDeleteAllDoc(item.id, reload);
}
/**
* 查询
*/
function searchQuery() {
pageNo.value = 1;
reload();
}
@ -237,6 +256,7 @@
function searchReset() {
formRef.value.resetFields();
queryParam.createBy = '';
pageNo.value = 1;
//
reload();
}
@ -279,6 +299,7 @@
total,
handlePageChange,
handleDelete,
onDeleteAllDoc,
searchQuery,
searchReset,
queryParam,
@ -389,7 +410,7 @@
margin-bottom: 20px;
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 0 2px 4px #e6e6e6;
transition: all 0.3s ease;
border-radius: 10px;
display: inline-flex;
@ -414,7 +435,7 @@
}
.add-knowledge-card:hover {
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
box-shadow: 0 6px 12px #d0d3d8;
}
.knowledge-card {
@ -424,11 +445,11 @@
border-radius: 10px;
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 0 2px 4px #e6e6e6;
transition: all 0.3s ease;
}
.knowledge-card:hover {
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
box-shadow: 0 6px 12px #d0d3d8;
.knowledge-btn {
display: block;
}
@ -459,7 +480,7 @@
}
.model-icon:hover{
color: #000000;
background-color: rgba(0,0,0,0.05);
background-color: #e9ecf2;
border: none;
}
.ant-dropdown-link{

View File

@ -1,6 +1,16 @@
<template>
<div class="p-2">
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="600px" :title="title" @ok="handleOk" @cancel="handleCancel">
<template #title>
<span style="display: flex">
{{title}}
<a-tooltip title="AI知识库文档">
<a style="color: unset" href="https://help.jeecg.com/aigc/guide/knowledge" target="_blank">
<Icon style="position:relative;left:2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
</a>
</a-tooltip>
</span>
</template>
<BasicForm @register="registerForm"></BasicForm>
</BasicModal>
</div>

View File

@ -22,6 +22,7 @@
import BasicForm from '@/components/Form/src/BasicForm.vue';
import { MarkdownViewer } from '@/components/Markdown';
import { useGlobSetting } from "@/hooks/setting";
export default {
name: 'AiTextDescModal',
@ -37,10 +38,32 @@
//modal
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
hitTextDescData.value.source = 'score' + ' ' + data.score.toFixed(2);
//
data.content = replaceImageWith(data.content);
//domainUrl
data.content = replaceDomainUrl(data.content);
hitTextDescData.value.content = data.content;
setModalProps({ header: '300px' })
});
const { domainUrl } = useGlobSetting();
const replaceImageWith = markdownContent => {
// width ![](/static/jimuImages/screenshot_1617252560523.png =100)
const regex = /!\[([^\]]*)\]\(([^)]+)=([0-9]+)\)/g;
return markdownContent.replace(regex, (match, alt, src, width) => {
let reg = /#\s*{\s*domainURL\s*}/g;
src = src.replace(reg,domainUrl);
return `<img src='${src}' alt='${alt}' width='${width}' />`;
});
};
//domainURL
const replaceDomainUrl = markdownContent => {
const regex = /!\[([^\]]*)\]\(.*?#\s*{\s*domainURL\s*}.*?\)/g;
return markdownContent.replace(regex, (match) => {
let reg = /#\s*{\s*domainURL\s*}/g;
return match.replace(reg,domainUrl);
})
}
return {
registerModal,
hitTextDescData

View File

@ -16,18 +16,46 @@
</a-layout-sider>
<a-layout-content :style="contentStyle">
<div v-if="selectedKey === 'document'">
<a-input v-model:value="searchText" placeholder="请输入文档名称,回车搜索" class="search-title" @pressEnter="reload"/>
<div class="search-header" style="text-align: left;">
<a-space align="center" wrap>
<a-input
class="search-input"
v-model:value="searchText"
placeholder="请输入文档名称,回车搜索"
@pressEnter="searchTextEnter"
/>
<template v-if="selectedRows.length > 0">
<div>
<span>已进入多选模式当前选中</span>
<a style="margin: 0 4px;"> {{ selectedRows.length }} </a>
<span>条文档</span>
</div>
<div>
<a @click="onClearSelected"></a>
<a-divider type="vertical"/>
<a @click="onDeleteBatch"></a>
</div>
</template>
</a-space>
</div>
<a-row :span="24" class="knowledge-row">
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24">
<a-card class="add-knowledge-card" :bodyStyle="cardBodyStyle">
<span style="line-height: 18px;font-weight: 500;color:#676f83;font-size: 12px">创建文档</span>
<span style="line-height: 18px;font-weight: 500;color:#676f83;font-size: 12px">
创建文档
<a-tooltip title="知识库文档">
<a style="color: unset" href="https://help.jeecg.com/aigc/guide/knowledge#4-%E7%9F%A5%E8%AF%86%E5%BA%93%E6%96%87%E6%A1%A3" target="_blank">
<Icon style="position:relative;top:1px" icon="ant-design:question-circle-outlined" size="14"></Icon>
</a>
</a-tooltip>
</span>
<div class="add-knowledge-doc" @click="handleCreateText">
<Icon icon="ant-design:form-outlined" size="13"></Icon><span>手动录入</span>
</div>
<div class="add-knowledge-doc" @click="handleCreateUpload">
<Icon icon="ant-design:cloud-upload-outlined" size="13"></Icon><span>文件上传</span>
</div>
<div class="add-knowledge-doc" @click="handleCreateUploadLibrary">
<div class="add-knowledge-doc">
<a-upload
accept=".zip"
name="file"
@ -37,15 +65,36 @@
:beforeUpload="beforeUpload"
:action="uploadUrl"
@change="handleUploadChange"
style="width: 100%;"
>
<Icon style="margin-left: 0" icon="ant-design:project-outlined" size="13"></Icon>
<span>文档库上传</span>
<div style="display: flex;width: 100%">
<Icon style="margin-left: 0;color:#676f83" icon="ant-design:project-outlined" size="13"></Icon>
<span style="color:#676f83;font-size: 12px">文档库上传</span>
</div>
</a-upload>
</div>
<a-dropdown placement="bottomRight" :trigger="['click']">
<div class="ant-dropdown-link pointer operation" @click.prevent.stop>
<Icon icon="ant-design:ellipsis-outlined" size="16"></Icon>
</div>
<template #overlay>
<a-menu>
<a-menu-item key="delete" @click="onDeleteAll">
<Icon icon="ant-design:delete-outlined" size="16"></Icon>
清空文档
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-card>
</a-col>
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24" v-for="item in knowledgeDocDataList">
<a-card class="knowledge-card pointer" @click="handleEdit(item)">
<a-card :class="['knowledge-card','pointer', {
checked: item.__checked,
}]" @click="handleEdit(item)">
<div class="knowledge-checkbox">
<a-checkbox v-model:checked="item.__checked" @click.stop=""/>
</div>
<div class="knowledge-header">
<div class="header-text flex">
<Icon v-if="item.type==='text'" icon="ant-design:file-text-outlined" size="32" color="#00a7d0"></Icon>
@ -77,6 +126,12 @@
<img src="../icon/draft.png" style="width: 16px;height: 16px" />
<span class="ml-2">草稿</span>
</div>
<a-tooltip v-else-if="item.status==='failed'" :title="getDocFailedReason(item)">
<div class="card-text-status">
<Icon icon="ant-design:close-circle-outlined" size="16" color="#FF4D4F"></Icon>
<span class="ml-2">失败</span>
</div>
</a-tooltip>
</div>
<a-dropdown placement="bottomRight" :trigger="['click']">
<div class="ant-dropdown-link pointer operation" @click.prevent.stop>
@ -114,6 +169,7 @@
@change="handlePageChange"
class="list-footer"
size="small"
:show-total="() => `共${total}条` "
/>
</div>
@ -186,6 +242,7 @@
</div>
</a-layout-content>
</a-layout>
<Loading tip="上传中,请稍后" :loading="uploadLoading"></Loading>
</BasicModal>
<!-- 手工录入文本 -->
@ -195,11 +252,12 @@
</div>
</template>
<script lang="ts">
import { onBeforeMount, reactive, ref, unref, h } from 'vue';
<script lang="tsx">
import { onBeforeMount, computed, ref, unref, h } from 'vue';
import BasicModal from '@/components/Modal/src/BasicModal.vue';
import { useModal, useModalInner } from '@/components/Modal';
import { knowledgeDocList, knowledgeDeleteBatchDoc, knowledgeRebuildDoc, knowledgeEmbeddingHitTest } from '../AiKnowledgeBase.api';
import { knowledgeDocList, knowledgeDeleteBatchDoc, knowledgeDeleteAllDoc, knowledgeRebuildDoc, knowledgeEmbeddingHitTest } from '../AiKnowledgeBase.api';
import { doDeleteAllDoc } from '../AiKnowledgeBase.api.util';
import { ActionItem, BasicTable, TableAction } from '@/components/Table';
import { useListPage } from '@/hooks/system/useListPage';
import AiragKnowledgeDocTextModal from './AiragKnowledgeDocTextModal.vue';
@ -212,6 +270,7 @@
import defaultImg from '/@/assets/images/header.jpg';
import Icon from "@/components/Icon";
import { useGlobSetting } from '/@/hooks/setting';
import Loading from '@/components/Loading/src/Loading.vue';
export default {
name: 'AiragKnowledgeDocListModal',
@ -225,6 +284,7 @@
BasicModal,
AiragKnowledgeDocTextModal,
AiTextDescModal,
Loading,
},
emits: ['success', 'register'],
setup(props, { emit }) {
@ -274,7 +334,9 @@
const globSetting = useGlobSetting();
//
const uploadUrl = ref<string>(globSetting.domainUrl+"/airag/knowledge/doc/import/zip");
//
const uploadLoading = ref<boolean>(false);
//
const menuItems = ref<any>([
{
@ -290,6 +352,10 @@
title: '命中测试',
},
]);
//
const selectedRows = computed(() => knowledgeDocDataList.value.filter(item => item.__checked))
//modal
const [docTextRegister, { openModal: docTextOpenModal }] = useModal();
const [docTextDescRegister, { openModal: docTextDescOpenModal }] = useModal();
@ -329,8 +395,8 @@
},
spin: true,
});
const { createMessage } = useMessage();
const { createMessage, createConfirmSync } = useMessage();
/**
* 手工录入文本
@ -343,6 +409,7 @@
* 文件上传
*/
function handleCreateUpload() {
console.log("11111111111")
docTextOpenModal(true, { knowledgeId: knowledgeId.value, type: "file" });
}
@ -357,6 +424,13 @@
* 编辑
*/
function handleEdit(record) {
//
if (selectedRows.value.length > 0) {
record.__checked = !record.__checked;
return
}
if (record.type === 'text' || record.type === 'file') {
docTextOpenModal(true, {
record,
@ -376,11 +450,28 @@
okText: '确认',
cancelText: '取消',
onOk: () => {
if(knowledgeDocDataList.value.length == 1 && pageNo.value > 1) {
pageNo.value = pageNo.value - 1;
}
knowledgeDeleteBatchDoc({ ids: id }, reload);
}
})
}
function getDocFailedReason(doc) {
let metadata = doc?.metadata;
if (!metadata) {
return '构建失败,原因未知';
}
try {
metadata = JSON.parse(metadata);
return metadata?.failedReason || '构建失败,原因未知';
} catch (e) {
console.log('getDocFailedReason', e);
return '构建失败,原因未知';
}
}
/**
* 向量化
*
@ -394,11 +485,9 @@
* 文档新增和编辑成功回调
*/
function handleSuccess() {
if(!timer.value){
reload();
}
clearInterval(timer.value);
timer.value = null;
reload();
triggeringTimer();
}
@ -421,7 +510,7 @@
pageNo.value = 1;
pageSize.value = 10;
searchText.value = "";
reload();
});
} else {
@ -511,7 +600,10 @@
}
}
//update-end---author:wangshuai---date:2025-03-21---for:QQYUN-11636---
knowledgeDocDataList.value = res.result.records;
knowledgeDocDataList.value = res.result.records.map((item)=>{
item.__checked = false;
return item;
});
total.value = res.result.total;
} else {
knowledgeDocDataList.value = [];
@ -519,7 +611,7 @@
}
});
}
/**
* 分页改变事件
* @param page
@ -548,10 +640,11 @@
*/
function beforeUpload(file) {
let fileType = file.type;
if (fileType !== 'application/x-zip-compressed') {
if (fileType !== 'application/zip' && fileType !== 'application/x-zip-compressed') {
createMessage.warning('请上传zip文件');
return false;
}
uploadLoading.value = true;
return true;
}
@ -561,24 +654,63 @@
*/
function handleUploadChange(info) {
let { file } = info;
if (file.status === 'error') {
createMessage.error(file.response.message ||`${file.name} 上传失败.`);
if (file.status === 'error' || (file.response && file.response.code == 500)) {
createMessage.error(file.response?.message ||`${file.name} 上传失败,请查看服务端日志`);
uploadLoading.value = false;
return;
}
if (file.status === 'done') {
if(!file.response.success){
createMessage.warning(file.response.message);
uploadLoading.value = false;
return;
}
uploadLoading.value = false;
createMessage.success(file.response.message);
handleSuccess();
}
}
function onClearSelected() {
knowledgeDocDataList.value.forEach(item => {
item.__checked = false;
});
}
//
async function onDeleteAll() {
pageNo.value = 1;
doDeleteAllDoc(knowledgeId.value, reload);
}
//
async function onDeleteBatch() {
const flag = await createConfirmSync({ title: '批量删除', content: `确定要删除这 ${selectedRows.value.length} 条数据吗?` });
if (!flag) {
return;
}
const ids = selectedRows.value.map(item => item.id)
let number = knowledgeDocDataList.value.length - ids.length;
if(number == 0 && pageNo.value > 1) {
pageNo.value = pageNo.value - 1;
}
knowledgeDeleteBatchDoc({ ids }, reload);
}
/**
* 回车搜索
*/
function searchTextEnter(){
pageNo.value = 1;
reload();
}
onBeforeMount(()=>{
clearInterval(timer.value);
timer.value = null;
})
return {
registerModal,
title,
@ -607,6 +739,7 @@
knowledgeDocDataList,
handleEdit,
handleDelete,
getDocFailedReason,
handleVectorization,
pageNo,
pageSize,
@ -623,6 +756,12 @@
uploadUrl,
handleUploadChange,
knowledgeId,
uploadLoading,
selectedRows,
onClearSelected,
onDeleteAll,
onDeleteBatch,
searchTextEnter,
};
},
};
@ -703,7 +842,7 @@
border-radius: 10px;
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 0 2px 4px #e6e6e6;
transition: all 0.3s ease;
.card-title {
justify-content: space-between;
@ -715,12 +854,12 @@
}
}
.hit-card:hover {
box-shadow: 0 6px 12px rgba(0,0,0,0.15) !important;
box-shadow: 0 6px 12px #d0d3d8 !important;
}
.pointer {
cursor: pointer;
}
.card-description {
display: -webkit-box;
-webkit-box-orient: vertical;
@ -734,16 +873,16 @@
font-size: 12px;
color: #676F83;
}
.card-title-tag {
color: #477dee;
}
.knowledge-row {
padding: 16px;
overflow-y: auto;
}
.add-knowledge-card {
border-radius: 10px;
margin-bottom: 20px;
@ -753,14 +892,14 @@
width: calc(100% - 20px);
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 0 2px 4px #e6e6e6;
transition: all 0.3s ease;
.add-knowledge-card-icon {
padding: 8px;
margin-right: 12px;
}
}
.knowledge-card {
border-radius: 10px;
margin-right: 20px;
@ -768,8 +907,29 @@
height: 166px;
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 0 2px 4px #e6e6e6;
transition: all 0.3s ease;
.knowledge-checkbox {
position: absolute;
top: 8px;
right: 8px;
z-index: 1;
align-items: center;
justify-content: center;
display: none;
}
&:hover, &.checked {
.knowledge-checkbox {
display: flex;
}
}
&.checked {
border: 1px solid @primary-color;
}
.knowledge-header {
position: relative;
font-size: 14px;
@ -786,7 +946,7 @@
margin-left: 4px;
align-self: center;
}
.header-text {
overflow: hidden;
position: relative;
@ -801,34 +961,37 @@
}
.add-knowledge-card:hover,.knowledge-card:hover{
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
box-shadow: 0 6px 12px #d0d3d8;
}
.ellipsis {
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
width: calc(100% - 30px);
}
:deep(.ant-card .ant-card-body) {
padding: 16px;
}
.card-text{
font-size: 12px;
display: flex;
margin-top: 10px;
align-items: center;
}
.search-title{
width: 200px;
.search-header {
margin-top: 10px;
display: block;
margin-left: 20px;
margin-left: 26px;
.search-input {
width: 240px;
display: block;
}
}
.operation{
border: none;
margin-top: 10px;
@ -838,13 +1001,13 @@
right: 4px;
position: absolute;
}
.knowledge-card:hover{
.add-knowledge-card:hover, .knowledge-card:hover{
.operation{
display: block !important;
}
}
.add-knowledge-doc{
margin-top: 6px;
color:#6F6F83;
@ -867,7 +1030,7 @@
}
.operation:hover{
color: #000000;
background-color: rgba(0,0,0,0.05);
background-color: #e9ecf2;
border: none;
}
.ant-dropdown-link{
@ -892,6 +1055,11 @@
.ml-2{
margin-left: 2px;
}
.add-knowledge-doc {
:deep(.ant-upload) {
width: 100%;
}
}
</style>
<style lang="less">
.airag-knowledge-doc .scroll-container {

View File

@ -54,7 +54,7 @@
</li>
<li class="flex mr-14 mt-6">
<span class="label">创建者</span>
<span class="described">{{ item.createBy }}</span>
<span class="described">{{ item.createBy_dictText || item.createBy }}</span>
</li>
</ul>
</div>
@ -93,6 +93,7 @@
@change="handlePageChange"
class="list-footer"
size="small"
:show-total="() => `共${total}条` "
/>
</div>
@ -218,6 +219,9 @@
* @param item
*/
async function handleDeleteClick(item) {
if(modalList.value.length == 1 && pageNo.value > 1) {
pageNo.value = pageNo.value - 1;
}
await deleteModel({ id: item.id, name: item.name }, reload);
}
@ -225,6 +229,7 @@
* 查询
*/
function searchQuery() {
pageNo.value = 1;
reload();
}
@ -350,14 +355,14 @@
margin-bottom: 20px;
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 0 2px 4px #e6e6e6;
transition: all 0.3s ease;
border-radius: 10px;
height: 152px;
cursor: pointer;
}
.model-card:hover {
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
box-shadow: 0 6px 12px #d0d3d8;
.model-btn {
display: flex;
}
@ -385,7 +390,7 @@
margin-bottom: 20px;
background: #fcfcfd;
border: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 0 2px 4px #e6e6e6;
transition: all 0.3s ease;
border-radius: 10px;
display: inline-flex;
@ -410,7 +415,7 @@
}
.add-knowledge-card:hover {
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
box-shadow: 0 6px 12px #d0d3d8;
}
.model-icon{
background-color: unset;
@ -419,7 +424,7 @@
}
.model-icon:hover{
color: #000000;
background-color: rgba(0,0,0,0.05);
background-color: #e9ecf2;
border: none;
}
.ant-dropdown-link{

View File

@ -3,7 +3,14 @@
<div class="modal">
<div class="header">
<span class="header-title">
<span v-if="dataIndex ==='list' || dataIndex ==='add'" :class="dataIndex === 'list' ? '' : 'add-header-title pointer'" @click="goToList"></span>
<span v-if="dataIndex ==='list' || dataIndex ==='add'" :class="dataIndex === 'list' ? '' : 'add-header-title pointer'" @click="goToList">
选择供应商
<a-tooltip title="供应商文档" v-if="dataIndex ==='list'">
<a style="color: #333333" href="https://help.jeecg.com/aigc/guide/model/#2-%E4%BE%9B%E5%BA%94%E5%95%86%E9%80%89%E6%8B%A9" target="_blank">
<Icon style="position:relative;left: -2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
</a>
</a-tooltip>
</span>
<span v-if="dataIndex === 'add'" class="add-header-title"> > </span>
<span v-if="dataIndex === 'add'" style="color: #1f2329"> {{ providerName }}</span>
</span>
@ -27,7 +34,17 @@
</a-row>
</div>
<a-tabs v-model:activeKey="activeKey" v-if="dataIndex === 'add' || dataIndex === 'edit'">
<a-tab-pane :key="1" tab="基础信息">
<a-tab-pane :key="1">
<template #tab>
<span style="display: flex">
基础信息
<a-tooltip title="基础信息文档">
<a @click.stop style="color: unset" href="https://help.jeecg.com/aigc/guide/model/#31-%E5%A1%AB%E5%86%99%E5%9F%BA%E7%A1%80%E4%BF%A1%E6%81%AF" target="_blank">
<Icon style="position:relative;left:2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
</a>
</a-tooltip>
</span>
</template>
<div class="model-content">
<BasicForm @register="registerForm">
<template #modelType="{ model, field }">
@ -62,13 +79,24 @@
</BasicForm>
</div>
</a-tab-pane>
<a-tab-pane :key="2" tab="高级配置" v-if="modelParamsShow">
<a-tab-pane :key="2" v-if="modelParamsShow">
<template #tab>
<span style="display: flex">
高级配置
<a-tooltip title="高级配置文档">
<a @click.stop style="color: unset" href="https://help.jeecg.com/aigc/guide/model/#32-%E9%85%8D%E7%BD%AE%E9%AB%98%E7%BA%A7%E5%8F%82%E6%95%B0" target="_blank">
<Icon style="position:relative;left:2px;top:1px" icon="ant-design:question-circle-outlined"></Icon>
</a>
</a-tooltip>
</span>
</template>
<AiModelSeniorForm ref="modelParamsRef" :modelParams="modelParams"></AiModelSeniorForm>
</a-tab-pane>
</a-tabs>
</div>
<template v-if="dataIndex === 'add' || dataIndex === 'edit'" #footer>
<a-button @click="test" :loading="testLoading">测试</a-button>
<a-button @click="cancel"></a-button>
<a-button @click="save" type="primary">保存</a-button>
</template>
@ -87,9 +115,10 @@
import BasicForm from '@/components/Form/src/BasicForm.vue';
import { useForm } from '@/components/Form';
import { formSchema, imageList } from '../model.data';
import { editModel, queryById, saveModel } from '../model.api';
import { editModel, queryById, saveModel, testConn } from '../model.api';
import { useMessage } from '/@/hooks/web/useMessage';
import AiModelSeniorForm from './AiModelSeniorForm.vue';
import { cloneDeep } from "lodash-es";
export default {
name: 'AddModelModal',
components: {
@ -128,6 +157,8 @@
const modelParamsShow = ref<boolean>(false);
//ref
const modelParamsRef = ref();
//loading
const testLoading = ref<boolean>(false);
const getImage = (name) => {
return imageList.value[name];
@ -206,7 +237,7 @@
*/
function initModelTypeOption() {
initDictOptions('model_type').then((data) => {
modelTypeOption.value = data;
modelTypeOption.value = cloneDeep(data);
//update-begin---author:wangshuai---date:2025-03-04---for: tab---
if(data[0].value != 'all'){
modelTypeOption.value.unshift({
@ -301,6 +332,38 @@
closeModal();
}
/**
* 测试连接
*/
async function test() {
try {
testLoading.value = true;
let values = await validate();
let credential = {
apiKey: values.apiKey,
secretKey: values.secretKey,
};
if (modelParamsRef.value) {
let modelParams = modelParamsRef.value.emitChange();
if (modelParams) {
values.modelParams = JSON.stringify(modelParams);
}
}
values.credential = JSON.stringify(credential);
if (!values.provider) {
values.provider = modelData.value.value;
}
//
await testConn(values);
} catch (e) {
if (e.hasOwnProperty('errorFields')) {
activeKey.value = 1;
}
} finally {
testLoading.value = false;
}
}
/**
* 模型类型选择事件
* @param value
@ -364,6 +427,8 @@
modelParamsRef,
filterOption,
getTitle,
test,
testLoading,
};
},
};

View File

@ -75,7 +75,7 @@
],
"type": ["LLM", "EMBED"],
"baseUrl": "https://api.openai.com/v1/",
"LLMDefaultValue": "gpt-3.5-turbo",
"LLMDefaultValue": "gpt-4o-mini",
"EMBEDDefaultValue": "text-embedding-ada-002"
},
{

Some files were not shown because too many files have changed in this diff Show More