Merge branch '升级分支'

This commit is contained in:
geht
2026-05-19 10:23:33 +08:00
596 changed files with 65104 additions and 4307 deletions

View File

@@ -0,0 +1,57 @@
# Git
.git/
.gitignore
.gitmodules
# SVN
.svn/
# IntelliJ IDEA
.idea/
*.iml
*.iws
*.ipr
out/
# Eclipse
.classpath
.project
.settings/
# VS Code
.vscode/
# Maven / Gradle build output
target/
build/
!.mvn/wrapper/maven-wrapper.jar
# OS files
.DS_Store
Thumbs.db
desktop.ini
# Logs
*.log
logs/
# Node (frontend artifacts if any)
node_modules/
dist/
# Docker volumes / data
docker/data/
# Compiled classes
*.class
# Custom
*.qqy
代码修改.log
代码修改日志
*.zip
backup/
.history/
.cursor/
doc/
docs/

View File

@@ -5,7 +5,7 @@ VITE_USE_MOCK = true
VITE_PUBLIC_PATH = /
# 跨域代理:前缀与 VITE_GLOB_API_URL 一致,且 rewrite 后路径需包含后端 context-path/jeecg-boot
# 跨域代理:前缀与 VITE_GLOB_API_URL 一致,且 rewrite 后路径需包含后端 context-path/jeecg-boot
# 详见 build/vite/proxy.ts
VITE_PROXY = [["/jeecg-boot","http://localhost:8888/jeecg-boot"],["/upload","http://localhost:3300/upload"]]
@@ -32,3 +32,6 @@ VITE_APP_SUB_jeecg-app-1 = '//localhost:8092'
# 在线文档编辑版本。可选属性wps, offlineWps(离线版) ,onlyoffice
VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps
# icon组件的iconify图标库使用在线还是本地。可选属性online, local
VITE_GLOB_ICONIFY_USE_TYPE=online

View File

@@ -13,7 +13,7 @@ VITE_BUILD_COMPRESS = 'gzip'
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
#后台接口父地址(必填)
VITE_GLOB_API_URL=/jeecg-boot
VITE_GLOB_API_URL=/jeecgboot
#后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=http://127.0.0.1:8080/jeecg-boot
@@ -32,3 +32,6 @@ VITE_GLOB_API_URL_PREFIX=
# 在线文档编辑版本。可选属性wps, offlineWps(离线版), onlyoffice
VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps
# icon组件的iconify图标库使用在线还是本地。可选属性online, local
VITE_GLOB_ICONIFY_USE_TYPE=local

View File

@@ -1,10 +1,10 @@
JeecgBoot 企业级低代码开发平台
===============
当前最新版本: 3.9.1预计发布时间2026-01-22
当前最新版本: 3.9.22026-04-30
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://jeecg.com/aboutusIndex)
[![](https://img.shields.io/badge/version-3.9.1-brightgreen.svg)](https://github.com
[![](https://img.shields.io/badge/version-3.9.2-brightgreen.svg)](https://github.com
/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)

View File

@@ -2,8 +2,11 @@ import { PluginOption } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import purgeIcons from 'vite-plugin-purge-icons';
// unplugin-icons 为 ESM-only用动态 import 避免 CJS 配置加载时 require 报错
import UnoCSS from 'unocss/vite';
import { presetTypography, presetUno } from 'unocss';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
// 本地调试https配置方法
import VitePluginCertificate from 'vite-plugin-mkcert';
@@ -29,9 +32,23 @@ import { configPwaPlugin } from './pwa';
* @param isBuild
* @param isQiankunMicro 是否【JEECG作为乾坤子应用】
*/
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, isQiankunMicro: boolean) {
export async function createVitePlugins(
viteEnv: ViteEnv,
isBuild: boolean,
isQiankunMicro: boolean,
) {
const { VITE_USE_MOCK, VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE } = viteEnv;
// update-begin--author:liaozhiyang---date:20260306---for:【QQYUN-14833】unplugin-icons 使用esm
// 动态 import ESM-only避免 CJS 配置链 require 报 ERR_REQUIRE_ESM 解决node 20启动报错
const [
{ default: Icons },
{ default: IconsResolver },
] = await Promise.all([
import('unplugin-icons/vite'),
import('unplugin-icons/resolver'),
]);
// update-end--author:liaozhiyang---date:20260306---for:【QQYUN-14833】unplugin-icons 使用esm
const vitePlugins: (PluginOption | PluginOption[])[] = [
// have to
vue(),
@@ -46,6 +63,26 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, isQiankunM
];
vitePlugins.push(UnoCSS({ presets: [presetUno(), presetTypography()] }));
// update-begin--author:liaozhiyang---date:20260302---for:【QQYUN-14806】antd采用unplugin-vue-components实现按需加载
// unplugin-vue-components: ant-design-vue 组件自动按需导入
vitePlugins.push(
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false,
// 排除antd的Button组件让项目中的a-button指向BasicButton组件
// 低版本是AButton高版本是Button
exclude: ['AButton', 'Button'],
}),
IconsResolver({
prefix: 'iconify',
}),
],
dirs: [],
dts: false,
})
);
// update-end--author:liaozhiyang---date:20260302---for:【QQYUN-14806】antd采用unplugin-vue-components实现按需加载
// vite-plugin-html
vitePlugins.push(configHtmlPlugin(viteEnv, isBuild, isQiankunMicro));
@@ -56,8 +93,21 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, isQiankunM
// vite-plugin-mock
VITE_USE_MOCK && vitePlugins.push(configMockPlugin(isBuild));
// vite-plugin-purge-icons
// update-begin--author:liaozhiyang---date:20260304---for:【QQYUN-14802】新增unplugin-icons插件及icon支持online和local两种模式
if (viteEnv.VITE_GLOB_ICONIFY_USE_TYPE === 'local') {
vitePlugins.push(purgeIcons());
}
vitePlugins.push(
Icons({
compiler: 'vue3',
// 自动安装图标集
autoInstall: false,
scale: 1,
defaultClass: 'app-iconify anticon',
})
);
vitePlugins.push(purgeIcons());
// update-end--author:liaozhiyang---date:20260304---for:【QQYUN-14802】新增unplugin-icons插件及icon支持online和local两种模式
// rollup-plugin-visualizer
vitePlugins.push(configVisualizerConfig());

View File

@@ -96,9 +96,10 @@ export function configPwaPlugin(isBuild: boolean): PluginOption | PluginOption[]
},
},
},
// 图片资源
//update-begin---author:scott ---date:20260417 for[issues/9564]PWA图片规则过宽导致/filereview/等业务接口走缓存-----------
// 图片资源(仅缓存构建产物,避免命中 /filereview/、/jeecgboot/ 等业务接口返回的图片)
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
urlPattern: /\/(?:assets|img|static|resource)\/.*\.(?:png|jpg|jpeg|svg|gif|webp)$/i,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
@@ -108,22 +109,12 @@ export function configPwaPlugin(isBuild: boolean): PluginOption | PluginOption[]
},
},
},
// API 请求
// API 请求JeecgBoot 实际前缀是 /jeecgboot/,原 /api/ 规则未生效)
{
urlPattern: /\/api\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
expiration: {
maxEntries: 50,
maxAgeSeconds: 60 * 5,
},
cacheableResponse: {
statuses: [0, 200],
},
},
urlPattern: /\/jeecgboot\/.*/i,
handler: 'NetworkOnly',
},
//update-end---author:scott ---date:20260417 for[issues/9564]PWA图片规则过宽导致/filereview/等业务接口走缓存-----------
],
// 启用立即更新:新 SW 立即激活并接管页面
skipWaiting: true,

View File

@@ -1,6 +1,6 @@
{
"name": "jeecgboot-vue3",
"version": "3.9.1",
"version": "3.9.2",
"author": {
"name": "北京国炬信息技术有限公司",
"email": "jeecgos@163.com",
@@ -25,26 +25,19 @@
"dependencies": {
"@ant-design/colors": "^7.2.1",
"@ant-design/icons-vue": "^7.0.1",
"@fontsource/noto-sans-sc": "^5.2.9",
"@fontsource/noto-serif-sc": "^5.2.8",
"@fontsource/open-sans": "^5.2.7",
"@fontsource/roboto": "^5.2.10",
"@iconify/iconify": "^3.1.1",
"@jeecg/aiflow": "3.9.1-beta",
"@jeecg/online": "3.9.1-beta",
"@logicflow/core": "^2.0.10",
"@logicflow/extension": "^2.0.14",
"@logicflow/vue-node-registry": "^1.0.12",
"@jeecg/aiflow": "3.9.2-beta",
"@logicflow/core": "^2.1.2",
"@logicflow/extension": "^2.1.4",
"@logicflow/vue-node-registry": "^1.1.3",
"@tinymce/tinymce-vue": "4.0.7",
"@traptitech/markdown-it-katex": "^3.6.0",
"@types/three": "^0.183.1",
"@vant/area-data": "^1.5.2",
"@vue/shared": "^3.5.22",
"@vueuse/core": "^10.11.1",
"@zxcvbn-ts/core": "^3.0.4",
"ant-design-vue": "^4.2.6",
"axios": "^1.12.2",
"china-area-data": "^5.0.1",
"clipboard": "^2.0.11",
"codemirror": "^5.65.20",
"cron-parser": "^4.9.0",
@@ -59,17 +52,19 @@
"highlight.js": "^11.11.1",
"html2canvas": "^1.4.1",
"intro.js": "^7.2.0",
"jquery": "^3.7.1",
"jquery": "^4.0.0",
"jsbarcode": "^3.12.3",
"jspdf": "^4.2.1",
"lodash-es": "^4.17.21",
"lodash.get": "^4.4.2",
"lunar-javascript": "^1.7.5",
"markdown-it": "^14.1.0",
"markdown-it-link-attributes": "^4.0.1",
"md5": "^2.3.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"path-to-regexp": "^6.3.0",
"perfect-scrollbar": "^1.5.6",
"pinia": "2.1.7",
"pinyin-pro": "^3.27.0",
"print-js": "^1.6.0",
@@ -79,12 +74,14 @@
"showdown": "^2.1.0",
"sortablejs": "^1.15.6",
"swagger-ui-dist": "^5.29.3",
"three": "^0.183.2",
"three": "^0.184.0",
"tinymce": "6.6.2",
"vditor": "^3.11.2",
"vue": "^3.5.22",
"vue-color": "^3.3.3",
"vue-cropper": "^0.6.5",
"vue-cropperjs": "^5.0.0",
"vue-grid-layout-v3": "^3.1.2",
"vue-i18n": "^9.14.5",
"vue-infinite-scroll": "^2.0.2",
"vue-plugin-hiprint": "^0.0.60",
@@ -119,6 +116,7 @@
"@types/qs": "^6.14.0",
"@types/showdown": "^2.0.6",
"@types/sortablejs": "^1.15.8",
"@types/three": "^0.184.1",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "5.2.4",
@@ -169,6 +167,8 @@
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"unocss": "^0.58.9",
"unplugin-icons": "^0.22.0",
"unplugin-vue-components": "~0.24.1",
"vite": "^6.3.6",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.2",

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +1,10 @@
import { withInstall } from '/@/utils';
import appLogo from './src/AppLogo.vue';
import appProvider from './src/AppProvider.vue';
import appSearch from './src/search/AppSearch.vue';
import appLocalePicker from './src/AppLocalePicker.vue';
import appDarkModeToggle from './src/AppDarkModeToggle.vue';
import { defineAsyncComponent } from 'vue';
export { useAppProviderContext } from './src/useAppContext';
export const AppLogo = withInstall(appLogo);
export const AppProvider = withInstall(appProvider);
export const AppSearch = withInstall(appSearch);
export const AppLocalePicker = withInstall(appLocalePicker);
export const AppDarkModeToggle = withInstall(appDarkModeToggle);
export const AppLogo = withInstall(defineAsyncComponent(() => import('./src/AppLogo.vue')));
export const AppProvider = withInstall(defineAsyncComponent(() => import('./src/AppProvider.vue')));
export const AppSearch = withInstall(defineAsyncComponent(() => import('./src/search/AppSearch.vue')));
export const AppLocalePicker = withInstall(defineAsyncComponent(() => import('./src/AppLocalePicker.vue')));
export const AppDarkModeToggle = withInstall(defineAsyncComponent(() => import('./src/AppDarkModeToggle.vue')));

View File

@@ -1,38 +1,43 @@
import BasicForm from './src/BasicForm.vue';
import { defineAsyncComponent } from 'vue';
export * from './src/types/form';
export * from './src/types/formItem';
export { useComponentRegister } from './src/hooks/useComponentRegister';
export { useForm } from './src/hooks/useForm';
export { default as ApiSelect } from './src/components/ApiSelect.vue';
export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue';
export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue';
export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
export const ApiSelect = defineAsyncComponent(() => import('./src/components/ApiSelect.vue'));
export const RadioButtonGroup = defineAsyncComponent(() => import('./src/components/RadioButtonGroup.vue'));
export const ApiTreeSelect = defineAsyncComponent(() => import('./src/components/ApiTreeSelect.vue'));
export const ApiRadioGroup = defineAsyncComponent(() => import('./src/components/ApiRadioGroup.vue'));
//Jeecg自定义组件
export { default as JAreaLinkage } from './src/jeecg/components/JAreaLinkage.vue';
export { default as JSelectUser } from './src/jeecg/components/JSelectUser.vue';
export { default as JSelectDept } from './src/jeecg/components/JSelectDept.vue';
export { default as JSelectDepartPost } from './src/jeecg/components/JSelectDepartPost.vue';
export { default as JSelectUserByDeptPost } from './src/jeecg/components/JSelectUserByDeptPost.vue';
export { default as JCodeEditor } from './src/jeecg/components/JCodeEditor.vue';
export { default as JCategorySelect } from './src/jeecg/components/JCategorySelect.vue';
export { default as JSelectMultiple } from './src/jeecg/components/JSelectMultiple.vue';
export { default as JPopup } from './src/jeecg/components/JPopup.vue';
export { default as JAreaSelect } from './src/jeecg/components/JAreaSelect.vue';
export { JEasyCron, JEasyCronInner, JEasyCronModal } from '/@/components/Form/src/jeecg/components/JEasyCron';
export { default as JCheckbox } from './src/jeecg/components/JCheckbox.vue';
export { default as JInput } from './src/jeecg/components/JInput.vue';
export { default as JEllipsis } from './src/jeecg/components/JEllipsis.vue';
export { default as JDictSelectTag } from './src/jeecg/components/JDictSelectTag.vue';
export { default as JTreeSelect } from './src/jeecg/components/JTreeSelect.vue';
export { default as JSearchSelect } from './src/jeecg/components/JSearchSelect.vue';
export { default as JSelectUserByDept } from './src/jeecg/components/JSelectUserByDept.vue';
export { default as JSelectUserByDepartment } from './src/jeecg/components/JSelectUserByDepartment.vue';
export { default as JEditor } from './src/jeecg/components/JEditor.vue';
export { default as JImageUpload } from './src/jeecg/components/JImageUpload.vue';
export const JAreaLinkage = defineAsyncComponent(() => import('./src/jeecg/components/JAreaLinkage.vue'));
export const JSelectUser = defineAsyncComponent(() => import('./src/jeecg/components/JSelectUser.vue'));
export const JSelectDept = defineAsyncComponent(() => import('./src/jeecg/components/JSelectDept.vue'));
export const JSelectDepartPost = defineAsyncComponent(() => import('./src/jeecg/components/JSelectDepartPost.vue'));
export const JSelectUserByDeptPost = defineAsyncComponent(() => import('./src/jeecg/components/JSelectUserByDeptPost.vue'));
export const JCodeEditor = defineAsyncComponent(() => import('./src/jeecg/components/JCodeEditor.vue'));
export const JCategorySelect = defineAsyncComponent(() => import('./src/jeecg/components/JCategorySelect.vue'));
export const JSelectMultiple = defineAsyncComponent(() => import('./src/jeecg/components/JSelectMultiple.vue'));
export const JPopup = defineAsyncComponent(() => import('./src/jeecg/components/JPopup.vue'));
export const JAreaSelect = defineAsyncComponent(() => import('./src/jeecg/components/JAreaSelect.vue'));
export const JEasyCron = defineAsyncComponent(() => import('./src/jeecg/components/JEasyCron/EasyCronInput.vue'));
export const JEasyCronInner = defineAsyncComponent(() => import('./src/jeecg/components/JEasyCron/EasyCronInner.vue'));
export const JEasyCronModal = defineAsyncComponent(() => import('./src/jeecg/components/JEasyCron/EasyCronModal.vue'));
export const JCheckbox = defineAsyncComponent(() => import('./src/jeecg/components/JCheckbox.vue'));
export const JInput = defineAsyncComponent(() => import('./src/jeecg/components/JInput.vue'));
export const JEllipsis = defineAsyncComponent(() => import('./src/jeecg/components/JEllipsis.vue'));
export const JDictSelectTag = defineAsyncComponent(() => import('./src/jeecg/components/JDictSelectTag.vue'));
export const JTreeSelect = defineAsyncComponent(() => import('./src/jeecg/components/JTreeSelect.vue'));
export const JSearchSelect = defineAsyncComponent(() => import('./src/jeecg/components/JSearchSelect.vue'));
export const JSelectUserByDept = defineAsyncComponent(() => import('./src/jeecg/components/JSelectUserByDept.vue'));
export const JSelectUserByDepartment = defineAsyncComponent(() => import('./src/jeecg/components/JSelectUserByDepartment.vue'));
// update-begin--author:liaozhiyang---date:20260227---for:【QQYUN-14751】tinymce富文本、JEasyCron、JLinkTableCard异步加载
export const JEditor = defineAsyncComponent(() => import('./src/jeecg/components/JEditor.vue'));
// update-end--author:liaozhiyang---date:20260227---for:【QQYUN-14751】tinymce富文本、JEasyCron、JLinkTableCard异步加载
export const JImageUpload = defineAsyncComponent(() => import('./src/jeecg/components/JImageUpload.vue'));
// Jeecg自定义校验
export { JCronValidator } from '/@/components/Form/src/jeecg/components/JEasyCron';
// update-begin--author:liaozhiyang---date:20260303---for:【QQYUN-14815】JCronValidator从Form中注释防止首页加载改为业务中直接导入
// export { default as JCronValidator } from '/@/components/Form/src/jeecg/components/JEasyCron/validator';
// update-end--author:liaozhiyang---date:20260303---for:【QQYUN-14815】JCronValidator从Form中注释防止首页加载改为业务中直接导入
export { BasicForm };

View File

@@ -1,10 +1,5 @@
/**
* 目前实现了异步加载的组件清单
* JAreaLinkage
* JEditor
* JMarkdownEditor
* JCodeEditor
* JEasyCron
* 目前实现了异步加载的组件清单antd 组件已改为 createAsyncComponent 异步加载)
*/
import type { Component } from 'vue';
import type { ComponentType } from './types/index';
@@ -12,182 +7,93 @@ import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
/**
* Component list, register here to setting it in the form
*/
import {
Input,
Select,
Radio,
Checkbox,
AutoComplete,
Cascader,
DatePicker,
InputNumber,
Switch,
TimePicker,
TreeSelect,
Slider,
Rate,
Divider,
} from 'ant-design-vue';
import ApiRadioGroup from './components/ApiRadioGroup.vue';
import RadioButtonGroup from './components/RadioButtonGroup.vue';
import ApiSelect from './components/ApiSelect.vue';
import ApiTreeSelect from './components/ApiTreeSelect.vue';
import { BasicUpload } from '/@/components/Upload';
import { StrengthMeter } from '/@/components/StrengthMeter';
import { IconPicker } from '/@/components/Icon';
import { CountdownInput } from '/@/components/CountDown';
//自定义组件
// import JAreaLinkage from './jeecg/components/JAreaLinkage.vue';
import JSelectUser from './jeecg/components/JSelectUser.vue';
import JSelectPosition from './jeecg/components/JSelectPosition.vue';
import JSelectRole from './jeecg/components/JSelectRole.vue';
import JImageUpload from './jeecg/components/JImageUpload.vue';
import JDictSelectTag from './jeecg/components/JDictSelectTag.vue';
import JSelectDept from './jeecg/components/JSelectDept.vue';
import JSelectDepartPost from './jeecg/components/JSelectDepartPost.vue';
import JAreaSelect from './jeecg/components/JAreaSelect.vue';
import JEditor from './jeecg/components/JEditor.vue';
// import JMarkdownEditor from './jeecg/components/JMarkdownEditor.vue';
import JSelectInput from './jeecg/components/JSelectInput.vue';
// import JCodeEditor from './jeecg/components/JCodeEditor.vue';
import JCategorySelect from './jeecg/components/JCategorySelect.vue';
import JSelectMultiple from './jeecg/components/JSelectMultiple.vue';
import JPopup from './jeecg/components/JPopup.vue';
// 代码逻辑说明: 【QQYUN-7961】popupDict字典
import JPopupDict from './jeecg/components/JPopupDict.vue';
import JSwitch from './jeecg/components/JSwitch.vue';
import JTreeDict from './jeecg/components/JTreeDict.vue';
import JInputPop from './jeecg/components/JInputPop.vue';
// import { JEasyCron } from './jeecg/components/JEasyCron';
import JCheckbox from './jeecg/components/JCheckbox.vue';
import JInput from './jeecg/components/JInput.vue';
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';
import { Time } from '/@/components/Time';
import JRangeNumber from './jeecg/components/JRangeNumber.vue';
import UserSelect from './jeecg/components/userSelect/index.vue';
import JRangeDate from './jeecg/components/JRangeDate.vue'
import JRangeTime from './jeecg/components/JRangeTime.vue'
import JInputSelect from './jeecg/components/JInputSelect.vue'
import RoleSelectInput from './jeecg/components/roleSelect/RoleSelectInput.vue';
import JSelectUserByDeptPost from './jeecg/components/JSelectUserByDeptPost.vue';
import JDatePickerMultiple from './jeecg/components/JDatePickerMultiple.vue';
import {DatePickerInFilter, CascaderPcaInFilter} from "@/components/InFilter";
const componentMap = new Map<ComponentType, Component>();
componentMap.set('Time', Time);
componentMap.set('Input', Input);
componentMap.set('InputGroup', Input.Group);
componentMap.set('InputPassword', Input.Password);
componentMap.set('InputSearch', Input.Search);
componentMap.set('InputTextArea', Input.TextArea);
componentMap.set('InputNumber', InputNumber);
componentMap.set('AutoComplete', AutoComplete);
componentMap.set('Time', createAsyncComponent(() => import('/@/components/Time/src/Time.vue')));
componentMap.set('Input', createAsyncComponent(() => import('ant-design-vue/es/input')));
componentMap.set('InputGroup', createAsyncComponent(() => import('ant-design-vue/es/input/Group')));
componentMap.set('InputPassword', createAsyncComponent(() => import('ant-design-vue/es/input/Password')));
componentMap.set('InputSearch', createAsyncComponent(() => import('ant-design-vue/es/input/Search')));
componentMap.set('InputTextArea', createAsyncComponent(() => import('ant-design-vue/es/input/TextArea')));
componentMap.set('InputNumber', createAsyncComponent(() => import('ant-design-vue/es/input-number')));
componentMap.set('AutoComplete', createAsyncComponent(() => import('ant-design-vue/es/auto-complete')));
componentMap.set('Select', Select);
componentMap.set('ApiSelect', ApiSelect);
componentMap.set('TreeSelect', TreeSelect);
componentMap.set('ApiTreeSelect', ApiTreeSelect);
componentMap.set('ApiRadioGroup', ApiRadioGroup);
componentMap.set('Switch', Switch);
componentMap.set('RadioButtonGroup', RadioButtonGroup);
componentMap.set('RadioGroup', Radio.Group);
componentMap.set('Checkbox', Checkbox);
componentMap.set('CheckboxGroup', Checkbox.Group);
componentMap.set('Cascader', Cascader);
componentMap.set('Slider', Slider);
componentMap.set('Rate', Rate);
componentMap.set('Select', createAsyncComponent(() => import('ant-design-vue/es/select')));
componentMap.set('ApiSelect', createAsyncComponent(() => import('./components/ApiSelect.vue')));
componentMap.set('JTabsSelectUser', createAsyncComponent(() => import('@/components/jeecg/JTabsSelectUser/index.vue')));
componentMap.set('TreeSelect', createAsyncComponent(() => import('ant-design-vue/es/tree-select')));
componentMap.set('ApiTreeSelect', createAsyncComponent(() => import('./components/ApiTreeSelect.vue')));
componentMap.set('ApiRadioGroup', createAsyncComponent(() => import('./components/ApiRadioGroup.vue')));
componentMap.set('Switch', createAsyncComponent(() => import('ant-design-vue/es/switch')));
componentMap.set('RadioButtonGroup', createAsyncComponent(() => import('./components/RadioButtonGroup.vue')));
componentMap.set('RadioGroup', createAsyncComponent(() => import('ant-design-vue/es/radio/Group')));
componentMap.set('Checkbox', createAsyncComponent(() => import('ant-design-vue/es/checkbox')));
componentMap.set('CheckboxGroup', createAsyncComponent(() => import('ant-design-vue/es/checkbox/Group')));
componentMap.set('Cascader', createAsyncComponent(() => import('ant-design-vue/es/cascader')));
componentMap.set('Slider', createAsyncComponent(() => import('ant-design-vue/es/slider')));
componentMap.set('Rate', createAsyncComponent(() => import('ant-design-vue/es/rate')));
componentMap.set('DatePicker', DatePicker);
componentMap.set('MonthPicker', DatePicker.MonthPicker);
componentMap.set('RangePicker', DatePicker.RangePicker);
componentMap.set('WeekPicker', DatePicker.WeekPicker);
componentMap.set('TimePicker', TimePicker);
componentMap.set('DatePickerInFilter', DatePickerInFilter);
componentMap.set('JDatePickerMultiple', JDatePickerMultiple);
componentMap.set('StrengthMeter', StrengthMeter);
componentMap.set('IconPicker', IconPicker);
componentMap.set('InputCountDown', CountdownInput);
componentMap.set('DatePicker', createAsyncComponent(() => import('ant-design-vue/es/date-picker')));
componentMap.set('MonthPicker', createAsyncComponent(() => import('ant-design-vue/es/date-picker').then((m) => m.default.MonthPicker)));
componentMap.set('RangePicker', createAsyncComponent(() => import('ant-design-vue/es/date-picker').then((m) => m.default.RangePicker)));
componentMap.set('WeekPicker', createAsyncComponent(() => import('ant-design-vue/es/date-picker').then((m) => m.default.WeekPicker)));
componentMap.set('TimePicker', createAsyncComponent(() => import('ant-design-vue/es/time-picker')));
componentMap.set('DatePickerInFilter', createAsyncComponent(() => import('@/components/InFilter/DatePickerInFilter.vue')));
componentMap.set('JDatePickerMultiple', createAsyncComponent(() => import('./jeecg/components/JDatePickerMultiple.vue')));
componentMap.set('StrengthMeter', createAsyncComponent(() => import('/@/components/StrengthMeter/src/StrengthMeter.vue')));
componentMap.set('IconPicker', createAsyncComponent(() => import('/@/components/Icon/src/IconPicker.vue')));
componentMap.set('InputCountDown', createAsyncComponent(() => import('/@/components/CountDown/src/CountdownInput.vue')));
componentMap.set('Upload', BasicUpload);
componentMap.set('Divider', Divider);
componentMap.set('Upload', createAsyncComponent(() => import('/@/components/Upload/src/BasicUpload.vue')));
componentMap.set('Divider', createAsyncComponent(() => import('ant-design-vue/es/divider')));
//注册自定义组件
componentMap.set(
'JAreaLinkage',
createAsyncComponent(() => import('./jeecg/components/JAreaLinkage.vue'))
);
componentMap.set('JSelectPosition', JSelectPosition);
componentMap.set('JSelectUser', JSelectUser);
componentMap.set('JSelectRole', JSelectRole);
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'))
// );
componentMap.set('JEditor', JEditor);
componentMap.set(
'JMarkdownEditor',
createAsyncComponent(() => import('./jeecg/components/JMarkdownEditor.vue'))
);
componentMap.set('JSelectInput', JSelectInput);
componentMap.set(
'JCodeEditor',
createAsyncComponent(() => import('./jeecg/components/JCodeEditor.vue'))
);
componentMap.set('JCategorySelect', JCategorySelect);
componentMap.set('JSelectMultiple', JSelectMultiple);
componentMap.set('JPopup', JPopup);
componentMap.set('JAreaLinkage', createAsyncComponent(() => import('./jeecg/components/JAreaLinkage.vue')));
componentMap.set('JSelectPosition', createAsyncComponent(() => import('./jeecg/components/JSelectPosition.vue')));
componentMap.set('JSelectUser', createAsyncComponent(() => import('./jeecg/components/JSelectUser.vue')));
componentMap.set('JSelectRole', createAsyncComponent(() => import('./jeecg/components/JSelectRole.vue')));
componentMap.set('JImageUpload', createAsyncComponent(() => import('./jeecg/components/JImageUpload.vue')));
componentMap.set('JDictSelectTag', createAsyncComponent(() => import('./jeecg/components/JDictSelectTag.vue')));
componentMap.set('JSelectDept', createAsyncComponent(() => import('./jeecg/components/JSelectDept.vue')));
componentMap.set('JAreaSelect', createAsyncComponent(() => import('./jeecg/components/JAreaSelect.vue')));
// update-begin--author:liaozhiyang---date:20260227---for:【QQYUN-14751】tinymce富文本、JEasyCron、JLinkTableCard异步加载
componentMap.set('JLinkTableCard', createAsyncComponent(() => import('./jeecg/components/JLinkTableCard/JLinkTableCard.vue'), { loading: true }));
componentMap.set('JEditor', createAsyncComponent(() => import('./jeecg/components/JEditor.vue')));
// update-end--author:liaozhiyang---date:20260227---for:【QQYUN-14751】tinymce富文本、JEasyCron、JLinkTableCard异步加载
componentMap.set('JMarkdownEditor', createAsyncComponent(() => import('./jeecg/components/JMarkdownEditor.vue')));
componentMap.set('JSelectInput', createAsyncComponent(() => import('./jeecg/components/JSelectInput.vue')));
componentMap.set('JCodeEditor', createAsyncComponent(() => import('./jeecg/components/JCodeEditor.vue')));
componentMap.set('JCategorySelect', createAsyncComponent(() => import('./jeecg/components/JCategorySelect.vue')));
componentMap.set('JSelectMultiple', createAsyncComponent(() => import('./jeecg/components/JSelectMultiple.vue')));
componentMap.set('JSelectSingle', createAsyncComponent(() => import('./jeecg/components/JSelectSingle.vue')));
componentMap.set('JPopup', createAsyncComponent(() => import('./jeecg/components/JPopup.vue')));
// 代码逻辑说明: 【QQYUN-7961】popupDict字典
componentMap.set('JPopupDict', JPopupDict);
componentMap.set('JSwitch', JSwitch);
componentMap.set('JTreeDict', JTreeDict);
componentMap.set('JInputPop', JInputPop);
componentMap.set(
'JEasyCron',
createAsyncComponent(() => import('./jeecg/components/JEasyCron/EasyCronInput.vue'))
);
componentMap.set('JCheckbox', JCheckbox);
componentMap.set('JInput', JInput);
componentMap.set('JTreeSelect', JTreeSelect);
componentMap.set('JEllipsis', JEllipsis);
componentMap.set('JSelectUserByDept', JSelectUserByDept);
componentMap.set('JSelectUserByDepartment', JSelectUserByDepartment);
componentMap.set('JUpload', JUpload);
componentMap.set('JSearchSelect', JSearchSelect);
componentMap.set('JAddInput', JAddInput);
componentMap.set('JRangeNumber', JRangeNumber);
componentMap.set('CascaderPcaInFilter', CascaderPcaInFilter);
componentMap.set('UserSelect', UserSelect);
componentMap.set('RangeDate', JRangeDate);
componentMap.set('RangeTime', JRangeTime);
componentMap.set('RoleSelect', RoleSelectInput);
componentMap.set('JInputSelect', JInputSelect);
componentMap.set('JSelectDepartPost', JSelectDepartPost);
componentMap.set('JSelectUserByDeptPost', JSelectUserByDeptPost);
componentMap.set('JPopupDict', createAsyncComponent(() => import('./jeecg/components/JPopupDict.vue')));
componentMap.set('JSwitch', createAsyncComponent(() => import('./jeecg/components/JSwitch.vue')));
componentMap.set('JTreeDict', createAsyncComponent(() => import('./jeecg/components/JTreeDict.vue')));
componentMap.set('JInputPop', createAsyncComponent(() => import('./jeecg/components/JInputPop.vue')));
componentMap.set('JEasyCron', createAsyncComponent(() => import('./jeecg/components/JEasyCron/EasyCronInput.vue')));
componentMap.set('JCheckbox', createAsyncComponent(() => import('./jeecg/components/JCheckbox.vue')));
componentMap.set('JInput', createAsyncComponent(() => import('./jeecg/components/JInput.vue')));
componentMap.set('JTreeSelect', createAsyncComponent(() => import('./jeecg/components/JTreeSelect.vue')));
componentMap.set('JEllipsis', createAsyncComponent(() => import('./jeecg/components/JEllipsis.vue')));
componentMap.set('JSelectUserByDept', createAsyncComponent(() => import('./jeecg/components/JSelectUserByDept.vue')));
componentMap.set('JSelectUserByDepartment', createAsyncComponent(() => import('./jeecg/components/JSelectUserByDepartment.vue')));
componentMap.set('JUpload', createAsyncComponent(() => import('./jeecg/components/JUpload/JUpload.vue')));
componentMap.set('JSearchSelect', createAsyncComponent(() => import('./jeecg/components/JSearchSelect.vue')));
componentMap.set('JAddInput', createAsyncComponent(() => import('./jeecg/components/JAddInput.vue')));
componentMap.set('JRangeNumber', createAsyncComponent(() => import('./jeecg/components/JRangeNumber.vue')));
componentMap.set('CascaderPcaInFilter', createAsyncComponent(() => import('@/components/InFilter/CascaderPcaInFilter.vue')));
componentMap.set('UserSelect', createAsyncComponent(() => import('./jeecg/components/userSelect/index.vue')));
componentMap.set('RangeDate', createAsyncComponent(() => import('./jeecg/components/JRangeDate.vue')));
componentMap.set('RangeTime', createAsyncComponent(() => import('./jeecg/components/JRangeTime.vue')));
componentMap.set('RoleSelect', createAsyncComponent(() => import('./jeecg/components/roleSelect/RoleSelectInput.vue')));
componentMap.set('JInputSelect', createAsyncComponent(() => import('./jeecg/components/JInputSelect.vue')));
componentMap.set('JSelectDepartPost', createAsyncComponent(() => import('./jeecg/components/JSelectDepartPost.vue')));
componentMap.set('JSelectUserByDeptPost', createAsyncComponent(() => import('./jeecg/components/JSelectUserByDeptPost.vue')));
componentMap.set('OnlineSelectCascade', createAsyncComponent(() => {
return import('@jeecg/online').then(mod => mod.OnlineSelectCascade);
}));
componentMap.set('LinkTableCard', createAsyncComponent(() => {
return import('@jeecg/online').then(mod => mod.LinkTableCard);
}));
componentMap.set('LinkTableSelect', createAsyncComponent(() => {
return import('@jeecg/online').then(mod => mod.LinkTableSelect);
}));
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component);

View File

@@ -114,7 +114,9 @@
if (next) {
const value = next[valueField];
prev.push({
...omit(next, [labelField, valueField]),
// update-begin--author:liaozhiyang---date:20260226---for:【issues/9326】ApiSelect组件返回数据中包含 options 这个名称导致组件渲染失败
...omit(next, [labelField, valueField, 'options']),
// update-end--author:liaozhiyang---date:20260226---for:【issues/9326】ApiSelect组件返回数据中包含 options 这个名称导致组件渲染失败
label: next[labelField],
value: numberToString ? `${value}` : value,
});

View File

@@ -72,8 +72,9 @@ export function useForm(props?: Props): UseFormReturnType {
},
resetFields: async () => {
const form = await getForm();
await form.resetFields();
getForm().then(async (form) => {
await form.resetFields();
});
},
removeSchemaByFiled: async (field: string | string[]) => {

View File

@@ -1,11 +1,11 @@
<template>
<Cascader v-bind="attrs" :value="cascaderValue" :options="getOptions" @change="handleChange" />
<JCascader v-bind="attrs" :value="cascaderValue" :showLastLevelOnly="showLastLevelOnly" :options="getOptions" @change="handleChange" />
</template>
<script lang="ts">
import { defineComponent, PropType, ref, reactive, watchEffect, computed, unref, watch, onMounted } from 'vue';
import { defineComponent, ref, watchEffect, computed } from 'vue';
import JCascader from './JCascader.vue';
import { Cascader } from 'ant-design-vue';
import { provinceAndCityData, regionData, provinceAndCityDataPlus, regionDataPlus } from '../../utils/areaDataUtil';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { provinceAndCityData, regionData, provinceOptions } from '../../utils/areaDataUtil';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { isArray } from '/@/utils/is';
@@ -13,37 +13,49 @@
export default defineComponent({
name: 'JAreaLinkage',
components: {
JCascader,
Cascader,
},
inheritAttrs: false,
props: {
value: propTypes.oneOfType([propTypes.object, propTypes.array, propTypes.string]),
// 显示层级province 只显示省city 显示省市region 显示省市县(优先于 showArea
displayLevel: propTypes.oneOf(['province', 'city', 'region', 'all']),
//是否显示区县
showArea: propTypes.bool.def(true),
//是否是全部
showAll: propTypes.bool.def(false),
// 存储数据 all时传递到外面的是数组province, city, region传递外面的是字符串
saveCode: propTypes.oneOf(['province', 'city', 'region', 'all']).def('all'),
},
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit, refs }) {
const emitData = ref<any[]>([]);
setup(props, { emit }) {
// update-begin--author:liaozhiyang---date:20260204---for:【QQYUN-14694】online支持配置独立的省、市、县
const showLastLevelOnly = computed(() => {
return props.displayLevel === 'province' || props.displayLevel === 'city' || props.displayLevel === 'region';
});
// update-end--author:liaozhiyang---date:20260204---for:【QQYUN-14694】online支持配置独立的省、市、县
const attrs = useAttrs();
// const [state] = useRuleFormItem(props, 'value', 'change', emitData);
const cascaderValue = ref([]);
const cascaderValue = ref<(string | number)[]>([]);
const getOptions = computed(() => {
if (props.showArea && props.showAll) {
return regionDataPlus;
}
if (props.showArea && !props.showAll) {
return regionData;
}
if (!props.showArea && !props.showAll) {
return provinceAndCityData;
}
if (!props.showArea && props.showAll) {
return provinceAndCityDataPlus;
// update-begin--author:liaozhiyang---date:20260204---for:【QQYUN-14694】online支持配置独立的省、市、县
if (props.displayLevel) {
if (props.displayLevel === 'all') {
return regionData;
} else if (props.displayLevel === 'province') {
return provinceOptions;
} else if (props.displayLevel === 'city') {
return provinceAndCityData;
} else if (props.displayLevel === 'region') {
return regionData;
}
} else {
if (props.showArea) {
return regionData;
} else {
return provinceAndCityData;
}
}
// update-end--author:liaozhiyang---date:20260204---for:【QQYUN-14694】online支持配置独立的省、市、县
});
/**
* 监听value变化
@@ -57,6 +69,23 @@
}
});
/**
* 老数据可能是区县码(如 120101displayLevel 为 province 时需显示对应省,
*/
function buildDisplayPathFromCode(code, displayLevel) {
if (!code && code !== 0) return [];
const str = String(code).trim();
if (!str) return [];
const provinceCode = str.length >= 2 ? str.substring(0, 2) + '0000' : str;
const cityCode = str.length >= 4 ? str.substring(0, 4) + '00' : null;
const regionCode = str.length >= 6 ? str : null;
const fullPath = [provinceCode];
if (cityCode && cityCode !== provinceCode) fullPath.push(cityCode);
if (regionCode && regionCode !== cityCode) fullPath.push(regionCode);
if (displayLevel === 'province') return fullPath.slice(0, 1);
if (displayLevel === 'city') return fullPath.slice(0, 2);
return fullPath;
}
/**
* 将字符串值转化为数组
*/
@@ -65,10 +94,30 @@
// 代码逻辑说明: 【TV360X-501】省市区换新组件
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
const arr = value.split(',');
cascaderValue.value = transform(arr);
if (props.displayLevel) {
// update-begin--author:liaozhiyang---date:20260204---for:【QQYUN-14694】online支持配置独立的省、市、县
const code = arr[0];
cascaderValue.value = buildDisplayPathFromCode(code, props.displayLevel);
// update-end--author:liaozhiyang---date:20260204---for:【QQYUN-14694】online支持配置独立的省、市、县
} else {
cascaderValue.value = transform(arr);
}
} else if (isArray(value)) {
if (value.length) {
cascaderValue.value = transform(value);
if (props.displayLevel) {
// update-begin--author:liaozhiyang---date:20260204---for:【QQYUN-14694】online支持配置独立的省、市、县
// 老数据 saveCode 为 all 时存的是完整路径数组 [省,市,区],直接按 displayLevel 截断
if (value.length >= 2) {
const len = props.displayLevel === 'province' ? 1 : props.displayLevel === 'city' ? 2 : Math.min(3, value.length);
cascaderValue.value = value.slice(0, len);
} else {
const code = value[0];
cascaderValue.value = buildDisplayPathFromCode(code, props.displayLevel);
}
// update-end--author:liaozhiyang---date:20260204---for:【QQYUN-14694】online支持配置独立的省、市、县
} else {
cascaderValue.value = transform(value);
}
} else {
cascaderValue.value = [];
}
@@ -109,7 +158,7 @@
emit('update:value', result);
};
function handleChange(arr, ...args) {
function handleChange(arr) {
// 代码逻辑说明: 【TV360X-501】省市区换新组件
if (arr?.length) {
let result: any = [];
@@ -138,6 +187,7 @@
regionData,
getOptions,
handleChange,
showLastLevelOnly,
};
},
});

View File

@@ -0,0 +1,368 @@
<!--
级联选择器只读输入框 + 挂载到 body 的下拉多列菜单
选中叶子节点时同步 value 并关闭下拉非叶子节点仅展开下一列
-->
<template>
<div class="cascader" ref="rootRef">
<AInput :value="displayText" :placeholder="inputPlaceholder" readonly :disabled="inputDisabled" v-bind="attrs"
:class="{ 'cascader-input-open': visible }" @click="toggle">
<template #suffix>
<span v-if="displayText && !inputDisabled" class="ant-input-clear-icon ant-input-clear-icon-has-suffix"
role="button" tabindex="-1" @click.stop="handleClear" @mousedown.prevent>
<CloseCircleFilled />
</span>
<DownOutlined class="cascader-arrow" :class="{ 'is-open': visible }" />
</template>
</AInput>
<!-- 下拉挂载到 body fixed + 计算出的 left/top 定位 -->
<Teleport to="body">
<Transition name="cascader-dropdown">
<div v-show="visible" ref="dropdownRef" class="cascader-dropdown cascader-dropdown-placement"
:style="dropdownPlaceStyle">
<div class="cascader-menus">
<div v-for="(column, colIndex) in columns" :key="colIndex" class="cascader-menu">
<div v-for="opt in column" :key="opt.value" class="cascader-menu-item"
:class="{ 'is-active': isActive(opt, colIndex), 'is-selected': isSelected(opt, colIndex) }"
@click="onSelectOption(opt, colIndex)">
<span class="cascader-menu-item-content">{{ opt.label }}</span>
<RightOutlined v-if="hasChildren(opt)" class="cascader-menu-item-arrow" />
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onBeforeUnmount, nextTick } from 'vue';
import { Input as AInput } from 'ant-design-vue';
import { DownOutlined, RightOutlined, CloseCircleFilled } from '@ant-design/icons-vue';
import { useAttrs } from '/@/hooks/core/useAttrs';
/** 级联选项value/label + 可选 children */
export interface CascaderOption {
value: string | number;
label: string;
children?: CascaderOption[];
}
/** 选中路径:每级的 value 数组 */
type PathValue = (string | number)[];
const DISPLAY_SEPARATOR = ' / ';
const DROPDOWN_GAP = 4;
defineOptions({
name: 'JCascader',
inheritAttrs: false,
});
const props = withDefaults(
defineProps<{
/** 当前选中路径(每级 value 数组) */
value?: PathValue;
/** 级联树数据 */
options?: CascaderOption[];
/** 是否只在 input 中显示最后一级 */
showLastLevelOnly?: boolean;
}>(),
{
value: () => [],
options: () => [],
showLastLevelOnly: false,
}
);
const emit = defineEmits<{
(e: 'change', value: PathValue): void;
(e: 'update:value', value: PathValue): void;
}>();
const attrs = useAttrs();
const inputPlaceholder = computed(() => String((attrs as Record<string, unknown>).placeholder ?? '请选择'));
const inputDisabled = computed(() => Boolean((attrs as Record<string, unknown>).disabled));
const rootRef = ref<HTMLElement>();
const dropdownRef = ref<HTMLElement>();
/** 下拉是否展开 */
const visible = ref(false);
/** 展开时正在编辑的路径(未确认前);用于多列菜单的当前高亮与子列数据 */
const editingPath = ref<PathValue>([]);
/** 下拉挂到 body 时的 fixed 定位left、toppx 字符串) */
const dropdownPlaceStyle = ref<{ left: string; top: string }>({ left: '0', top: '0' });
/** 规范化 value 为路径数组 */
function normalizePath(val: PathValue | undefined): PathValue {
return Array.isArray(val) ? val : [];
}
/** 是否有子节点(非叶子) */
function hasChildren(opt: CascaderOption): boolean {
return !!(opt.children?.length);
}
/** 根据 value 路径在 options 树中逐级查找,返回对应的 label 数组 */
function getPathLabels(opts: CascaderOption[], pathValues: PathValue): string[] {
const labels: string[] = [];
let current = opts;
for (const v of pathValues) {
const opt = current.find((o) => o.value === v);
if (!opt) break;
labels.push(opt.label);
current = opt.children ?? [];
}
return labels;
}
/** 输入框展示文案showLastLevelOnly 时只显示最后一级 label否则用分隔符拼接整条路径 */
const displayText = computed(() => {
const path = normalizePath(props.value);
if (!path.length) return '';
const labels = getPathLabels(props.options ?? [], path);
return props.showLastLevelOnly ? (labels.at(-1) ?? '') : labels.join(DISPLAY_SEPARATOR);
});
/** 用于菜单高亮与列数据:展开时用 editingPath收起时用 props.value */
const pathForMenus = computed<PathValue>(() =>
visible.value ? editingPath.value : normalizePath(props.value));
/** 根据当前 path 展开的列:第 0 列为根,第 k 列为 path[k-1] 的 children列数 = path.length + 1点击第几级只展示到下一级 */
const columns = computed(() => {
const path = pathForMenus.value;
const opts = props.options ?? [];
const cols: CascaderOption[][] = [];
let current: CascaderOption[] = opts;
for (let i = 0; i <= path.length && current.length; i++) {
cols.push(current);
if (i < path.length && path[i] !== undefined) {
const opt = current.find((o) => o.value === path[i]);
current = opt?.children ?? [];
} else {
break;
}
}
return cols;
});
/** 当前列中该项是否为“当前路径”在该列的节点(高亮并展示子列) */
function isActive(opt: CascaderOption, colIndex: number): boolean {
const path = pathForMenus.value;
return path[colIndex] === opt.value;
}
/** 当前列中该项是否为已选中的叶子节点(选中样式) */
function isSelected(opt: CascaderOption, colIndex: number): boolean {
const path = pathForMenus.value;
if (colIndex < path.length - 1) return path[colIndex] === opt.value;
return path[colIndex] === opt.value && !hasChildren(opt);
}
/** 点击某一列选项:截断到该列并追加当前项;若为叶子则 emit 并关闭,否则更新 editingPath 展开下一列 */
function onSelectOption(opt: CascaderOption, colIndex: number) {
const path = visible.value ? [...editingPath.value] : normalizePath(props.value).slice();
path.length = colIndex;
path.push(opt.value);
if (hasChildren(opt)) {
editingPath.value = path;
} else {
emit('change', path);
emit('update:value', path);
visible.value = false;
}
}
function toggle() {
if (inputDisabled.value) return;
visible.value = !visible.value;
}
/** 清除选中:与 UI 库 readonly 下不显示清除按钮的补偿,在 suffix 中手动实现 */
function handleClear() {
emit('change', []);
emit('update:value', []);
visible.value = false;
}
/** 点击不在输入框、不在下拉内时关闭下拉capture 阶段监听) */
function handleClickOutside(e: MouseEvent) {
const target = e.target as Node;
if (!target || !document.body.contains(target)) return;
if (rootRef.value?.contains(target)) return;
if (dropdownRef.value?.contains(target)) return;
visible.value = false;
}
/** 根据触发器 rootRef 的视口位置,设置下拉的 fixed left/top下拉在 body 下) */
function updatePopupPosition() {
if (!rootRef.value) return;
const rect = rootRef.value.getBoundingClientRect();
dropdownPlaceStyle.value = {
left: `${rect.left}px`,
top: `${rect.bottom + DROPDOWN_GAP}px`,
};
}
/** 展开时:用当前 value 初始化 editingPath、计算定位、注册点击外部关闭收起时移除监听 */
watch(visible, (v) => {
if (v) {
editingPath.value = normalizePath(props.value).slice();
nextTick(() => {
updatePopupPosition();
setTimeout(() => document.addEventListener('click', handleClickOutside, true), 0);
});
} else {
document.removeEventListener('click', handleClickOutside, true);
}
});
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside, true);
});
</script>
<!-- 下拉挂 body 时用 .cascader-dropdown-placement 覆盖为 position: fixed -->
<style scoped lang="less">
@import (reference) '/@/design/config.less';
.cascader {
position: relative;
display: inline-block;
width: 100%;
min-width: 0;
}
.cascader .ant-input-affix-wrapper {
cursor: pointer;
.ant-input-clear-icon {
display: none;
}
&:hover {
.ant-input-clear-icon {
display: block;
}
.cascader-arrow {
opacity: 0;
}
}
}
.cascader-arrow {
font-size: 10px;
transition: transform 0.2s;
}
.cascader-arrow.is-open {
transform: rotate(180deg);
}
:deep(.ant-input-suffix) {
position: relative;
.ant-input-clear-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
z-index: 1;
}
}
.cascader-dropdown {
position: absolute;
top: 100%;
left: 0;
z-index: 1050;
margin-top: 4px;
background: @component-background;
border-radius: 6px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
}
.cascader-dropdown-placement {
position: fixed;
margin-top: 0;
}
.cascader-menus {
display: flex;
min-height: 180px;
}
.cascader-menu {
padding: 5px;
min-width: 112px;
max-height: 180px;
overflow-y: auto;
background: @component-background;
&:not(:last-child) {
border-right: 1px solid @border-color-split;
}
}
.cascader-menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 12px;
font-size: 14px;
line-height: 22px;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.cascader-menu-item-content {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cascader-menu-item-arrow {
margin-left: 8px;
font-size: 10px;
color: @text-color-secondary;
flex-shrink: 0;
}
.cascader-menu-item:hover {
background: @item-hover-bg;
}
.cascader-menu-item.is-active .cascader-menu-item-arrow,
.cascader-menu-item.is-selected .cascader-menu-item-arrow {
color: var(--j-global-primary-color);
}
.cascader-menu-item.is-active {
background: @primary-1;
color: var(--j-global-primary-color);
}
@supports (background: color-mix(in srgb, red, blue)) {
.cascader-menu-item.is-active {
background: color-mix(in srgb, var(--j-global-primary-color) 10%, #fff);
}
}
.cascader-menu-item.is-selected {
font-weight: 500;
color: var(--j-global-primary-color);
}
.cascader-dropdown-enter-active,
.cascader-dropdown-leave-active {
transition: opacity 0.15s;
}
.cascader-dropdown-enter-from,
.cascader-dropdown-leave-to {
opacity: 0;
}
</style>

View File

@@ -36,8 +36,8 @@
<div v-html="text"></div>
</template>
<template #pcaSlot="{ text }">
<div :title="getPcaText(text)">{{ getPcaText(text) }}</div>
<template #pcaSlot="{ text, column }">
<div :title="getPcaText(text, column)">{{ getPcaText(text, column) }}</div>
</template>
<template #dateSlot="{ text, column }">
@@ -53,7 +53,7 @@
import { useListPage } from '/@/hooks/system/useListPage';
import { useMessage } from '/@/hooks/web/useMessage';
import { defHttp } from '/@/utils/http/axios';
import { useTableColumns } from '../hooks/useTableColumns';
import { useTableColumns } from '@/views/super/online/cgform/hooks/auto/useTableColumns';
import { createAsyncComponent } from '@/utils/factory/createAsyncComponent';
import { useFixedHeightModal } from '../hooks/useLinkTable';

View File

@@ -4,12 +4,20 @@
:value="arrayValue"
@change="onChange"
mode="multiple"
:filter-option="filterOption"
:filter-option="useLoadDict ? false : filterOption"
:disabled="disabled"
:placeholder="placeholder"
allowClear
showSearch
:getPopupContainer="getParentContainer"
:notFoundContent="loading ? undefined : null"
@search="handleSearch"
@dropdown-visible-change="handleDropdownVisibleChange"
@popupScroll="handlePopupScroll"
>
<template #notFoundContent>
<a-spin v-if="loading" size="small" />
</template>
<template v-for="item of getOptions" :key="item.key">
<a-select-option :value="item.value" :getPopupContainer="getParentContainer">
<span :class="item.class" :style="item.style">{{ item.text }}</span>
@@ -22,8 +30,9 @@
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { getDictItems } from '/@/api/common/api';
import { initDictOptions } from '/@/utils/dict/index';
import { setPopContainer } from '/@/utils';
import { useScrollLoadDict } from '../hooks/useSelectMultipleScrollLoad';
export default defineComponent({
name: 'JSelectMultiple',
@@ -73,16 +82,27 @@
type: Boolean,
default: false,
},
// update-begin--author:liaozhiyang---date:20260204---for:【issues/9307】online下拉加载表字典需滚动加载
// 分页时每页条数,仅当 scrollLoad 为 true 且 dictCode 为字典表格式时生效
pageSize: { type: Number, default: 10 },
// 是否滚动加载,为 true 且 dictCode 为字典表格式(table,text,code)时走 /sys/dict/loadDict/,否则走 initDictOptions
scrollLoad: { type: Boolean, default: false },
// update-begin--author:liaozhiyang---date:20260204---for:【issues/9307】online下拉加载表字典需滚动加载
},
emits: ['options-change', 'change', 'input', 'update:value'],
setup(props, { emit, refs }) {
setup(props, { emit }) {
//console.info(props);
const emitData = ref<any[]>([]);
const arrayValue = ref<any[]>(!props.value ? [] : props.value.split(props.spliter));
const arrayValue = ref<any[]>(
!props.value ? [] : Array.isArray(props.value) ? props.value : (props.value as string).split(props.spliter)
);
const dictOptions = ref<any[]>([]);
const attrs = useAttrs();
const [state, , , formItemContext] = useRuleFormItem(props, 'value', 'change', emitData);
// update-begin--author:liaozhiyang---date:20260204---for:【issues/9307】online下拉加载表字典需滚动加载
const scrollLoadApi = useScrollLoadDict(props, dictOptions, arrayValue);
const { useLoadDict, loading, loadDictOptions: loadDictOptionsScroll, ensureValueInOptions, handleSearch, handleDropdownVisibleChange, handlePopupScroll, isDictTable } = scrollLoadApi;
// update-end--author:liaozhiyang---date:20260204---for:【issues/9307】online下拉加载表字典需滚动加载
// 处理下拉选项
const getOptions = computed(() => {
if (!Array.isArray(dictOptions.value)) {
@@ -128,15 +148,19 @@
if (!val) {
arrayValue.value = [];
} else {
arrayValue.value = props.value.split(props.spliter);
arrayValue.value = Array.isArray(props.value) ? props.value : (props.value as string).split(props.spliter);
}
if (useLoadDict.value) ensureValueInOptions();
}
);
//适用于 动态改变下拉选项的操作
watch(()=>props.options, ()=>{
if (props.dictCode) {
// nothing to do
// update-begin--author:liaozhiyang---date:20260325---for:【QQYUN-15021】online js增强修改下拉不生效
// online js增强改变options
dictOptions.value = props.options;
// update-end--author:liaozhiyang---date:20260325---for:【QQYUN-15021】online js增强修改下拉不生效
} else {
dictOptions.value = props.options;
}
@@ -165,23 +189,21 @@
}
}
// 根据字典code查询字典项
function loadDictOptions() {
// 代码逻辑说明: 字典数据请求前将参数编码处理,但是不能直接编码,因为可能之前已经编码过了
let temp = props.dictCode || '';
if (temp.indexOf(',') > 0 && temp.indexOf(' ') > 0) {
// 编码后 是不包含空格的
temp = encodeURI(temp);
}
getDictItems(temp).then((res) => {
if (res) {
dictOptions.value = res.map((item) => ({ value: item.value, label: item.text, color:item.color }));
//console.info('res', dictOptions.value);
} else {
console.error('getDictItems error: : ', res);
async function loadDictOptions() {
// update-begin--author:liaozhiyang---date:20260204---for:【issues/9307】online下拉加载表字典需滚动加载
if (useLoadDict.value) {
loadDictOptionsScroll();
} else {
const code = props.dictCode ?? '';
try {
const dictData = await initDictOptions(code);
dictOptions.value = dictData;
} catch (error) {
console.error('initDictOptions error:', error);
dictOptions.value = [];
}
});
}
// update-end--author:liaozhiyang---date:20260204---for:【issues/9307】online下拉加载表字典需滚动加载
}
// 代码逻辑说明: VUEN-1145 下拉多选,搜索时,查不到数据
@@ -198,6 +220,12 @@
arrayValue,
getParentContainer,
filterOption,
isDictTable,
useLoadDict,
loading,
handlePopupScroll,
handleSearch,
handleDropdownVisibleChange,
};
},
});

View File

@@ -0,0 +1,323 @@
<!-- 字典下拉单选 -->
<template>
<a-select
:value="innerValue"
:disabled="disabled"
:placeholder="placeholder"
:filter-option="filterOptionComputed"
:showSearch="showSearch"
:getPopupContainer="getPopupContainer"
:notFoundContent="loading ? undefined : null"
allowClear
@change="handleChange"
@search="handleSearch"
@popupScroll="handlePopupScroll"
@dropdownVisibleChange="handleDropdownVisibleChange"
v-bind="$attrs"
>
<template #notFoundContent>
<a-spin v-if="loading" size="small" />
</template>
<template v-for="item in selectOptions" :key="item.key">
<a-select-option :value="item.value" :getPopupContainer="getPopupContainer">
<span :class="item.class" :style="item.style">{{ item.text }}</span>
</a-select-option>
</template>
</a-select>
</template>
<script setup lang="ts">
import { computed, ref, watch, unref } from 'vue';
import { debounce } from 'lodash-es';
import { initDictOptions } from '/@/utils/dict/index';
import { setPopContainer } from '/@/utils';
import { defHttp } from '/@/utils/http/axios';
defineOptions({ name: 'JSelectSingle' });
// --- Props & Emits ---
interface DictOption {
value: string | number;
label?: string;
text?: string;
color?: string;
}
interface SelectOptionItem {
key: string;
text: string;
value: string | number;
class: string[];
style: { backgroundColor: string };
}
/** 下拉单选组件的值类型(可被 change / update:value / input 等复用) */
export type SelectSingleValue = string | number | undefined;
const props = withDefaults(
defineProps<{
value?: SelectSingleValue;
placeholder?: string;
readOnly?: boolean;
options?: DictOption[];
triggerChange?: boolean;
popContainer?: string;
dictCode?: string;
disabled?: boolean;
useDicColor?: boolean;
pageSize?: number;
scrollLoad?: boolean;
}>(),
{
placeholder: '请选择',
readOnly: false,
options: () => [],
triggerChange: true,
popContainer: '',
disabled: false,
useDicColor: false,
pageSize: 10,
scrollLoad: false,
}
);
const emit = defineEmits<{
(e: 'change', value: SelectSingleValue): void;
(e: 'update:value', value: SelectSingleValue): void;
(e: 'input', value: SelectSingleValue): void;
(e: 'options-change'): void;
}>();
// --- 选项数据 ---
const innerValue = ref<SelectSingleValue>(props.value ?? undefined);
const dictOptions = ref<DictOption[]>([]);
const loading = ref(false);
const searchKeyword = ref('');
// --- 字典加载(分页滚动) ---
const scrollState = {
pageNo: 1,
hasMore: true,
loading: false,
};
const isDictTable = computed(() => {
if (!props.dictCode) return false;
return props.dictCode.split(',').length >= 2;
});
const useLoadDict = computed(() => props.scrollLoad && isDictTable.value);
const selectOptions = computed<SelectOptionItem[]>(() => {
if (!Array.isArray(dictOptions.value)) return [];
return dictOptions.value.map((item, index) => {
const text = item.text ?? item.label ?? '';
return {
key: `${item.value}_${text}_${index}`,
text,
value: item.value,
class: [props.useDicColor && item.color ? 'colorText' : ''],
style: { backgroundColor: props.useDicColor && item.color ? String(item.color) : '' },
};
});
});
function fetchDictPage(pageNo: number, append: boolean) {
return defHttp
.get({
url: `/sys/dict/loadDict/${props.dictCode}`,
params: { pageNo, pageSize: props.pageSize, keyword: searchKeyword.value, order: 'asc' },
})
.then((res: any[]) => {
const items: DictOption[] = (res ?? []).map((it) => ({
value: it.value,
label: it.text ?? it.label,
}));
if (items.length === 0) {
if (!append) dictOptions.value = [];
scrollState.hasMore = false;
return;
}
if (append) {
const existValues = new Set(dictOptions.value.map((o) => String(o.value)));
const newItems = items.filter((it) => !existValues.has(String(it.value)));
if (newItems.length > 0) {
dictOptions.value = [...dictOptions.value, ...newItems];
}
} else {
// 有选中的值且optinos中存在时需要把选中的值在options中存在且不在新数据中的项保留防止多次请求
if (unref(innerValue) && unref(dictOptions).length) {
const existItem = unref(dictOptions).find((item) => String(item.value) === String(unref(innerValue)));
if (existItem && !items.some((item) => String(item.value) === String(existItem.value))) {
items.push(existItem);
}
}
dictOptions.value = items;
}
scrollState.pageNo = pageNo + 1;
});
}
async function loadDictOptions() {
if (useLoadDict.value) {
scrollState.pageNo = 1;
scrollState.hasMore = true;
loading.value = true;
fetchDictPage(1, false)
.catch(() => {
dictOptions.value = [];
})
.finally(() => {
loading.value = false;
ensureValueInOptions();
});
} else {
let code = props.dictCode ?? '';
try {
const dictData = await initDictOptions(code);
dictOptions.value = dictData;
} catch (error) {
console.error('initDictOptions error:', error);
dictOptions.value = [];
}
}
}
/** 根据 value 拉取单条字典项用于回显(编辑时当前值不在已加载选项中时) */
function fetchDictItemByValue(val: SelectSingleValue): Promise<DictOption | null> {
if (val == null) return Promise.resolve(null);
return defHttp
.get({ url: `/sys/dict/loadDictItem/${props.dictCode}`, params: { key: val } })
.then((res: any) => {
if (Array.isArray(res)) {
return { value: val, label: res[0] };
}
return null;
})
.catch(() => null);
}
/** 确保当前 value 在选项中(仅 useLoadDict 时:编辑时当前值不在已加载列表中则拉取单条并插入) */
function ensureValueInOptions() {
if (!useLoadDict.value) return;
const val = innerValue.value;
if (val == null) return;
const exists = dictOptions.value.some((o) => String(o.value) === String(val));
if (exists) return;
fetchDictItemByValue(val).then((item) => {
if (item) dictOptions.value = [item].concat(dictOptions.value);
});
}
const handleSearch = debounce((keyword: string) => {
if (!useLoadDict.value) return;
searchKeyword.value = keyword ?? '';
scrollState.pageNo = 1;
scrollState.hasMore = true;
loading.value = true;
fetchDictPage(1, false)
.catch(() => {
dictOptions.value = [];
})
.finally(() => {
loading.value = false;
ensureValueInOptions();
});
}, 300);
function handleDropdownVisibleChange(open: boolean) {
if (!open || !useLoadDict.value) return;
scrollState.pageNo = 1;
scrollState.hasMore = true;
searchKeyword.value = '';
loading.value = true;
fetchDictPage(1, false)
.finally(() => {
loading.value = false;
ensureValueInOptions();
});
}
function handlePopupScroll(e: Event) {
if (!useLoadDict.value || scrollState.loading || !scrollState.hasMore) return;
const target = e.target as HTMLElement;
const { scrollTop, scrollHeight, clientHeight } = target;
if (scrollTop + clientHeight < scrollHeight - 10) return;
scrollState.loading = true;
fetchDictPage(scrollState.pageNo, true)
.finally(() => {
scrollState.loading = false;
})
.catch(() => {
if (scrollState.pageNo > 1) {
scrollState.pageNo--;
}
});
}
// --- 弹层挂载 & 搜索过滤 ---
const showSearch = computed(() => useLoadDict.value);
function getPopupContainer(node: HTMLElement) {
return props.popContainer ? setPopContainer(node, props.popContainer) : node?.parentNode;
}
function filterOption(input: string, option: any) {
const node = option.children?.();
const text = (node?.[0]?.children ?? '').toString().toLowerCase();
return text.indexOf(input.toLowerCase()) >= 0;
}
const filterOptionComputed = computed(() => (input: string, option: any) => {
if (useLoadDict.value) return true;
return filterOption(input, option);
});
// --- 选择变更 ---
function handleChange(value: SelectSingleValue) {
const val = value ?? undefined;
if (props.triggerChange) {
emit('change', val);
} else {
emit('input', val);
}
emit('update:value', val);
}
// --- 初始化与监听 ---
function syncOptionsFromProps() {
if (props.dictCode) {
loadDictOptions();
} else {
dictOptions.value = props.options ?? [];
}
}
watch(() => props.dictCode, syncOptionsFromProps);
watch(
() => props.value,
(val) => {
innerValue.value = val ?? undefined;
if (useLoadDict.value) ensureValueInOptions();
}
);
watch(
() => props.options,
() => {
if (!props.dictCode) dictOptions.value = props.options ?? [];
}
);
syncOptionsFromProps();
</script>
<style scoped lang="less">
.colorText {
display: inline-block;
height: 20px;
line-height: 20px;
padding: 0 6px;
border-radius: 8px;
background-color: red;
color: #fff;
font-size: 12px;
}
</style>

View File

@@ -129,7 +129,7 @@
function getQueryUrl() {
let queryFn = props.izOnlySelectDepartPost ? queryDepartAndPostTreeSync :props.sync ? queryDepartTreeSync : queryTreeList;
// 代码逻辑说明: issues/I5F3P4 online配置部门选择后编辑查看数据应该显示部门名称不是部门代码
return (params) => queryFn(Object.assign({}, params, { primaryKey: props.rowKey }));
return (params) => queryFn(Object.assign({}, params, props.params, { primaryKey: props.rowKey }));
}
/**

View File

@@ -74,7 +74,7 @@
</template>
</a-breadcrumb>
</div>
<div v-if="currentDepartUsers.length">
<div v-if="currentDepartUsers.length || currentDeptTotal > 0">
<!-- 当前部门用户树 -->
<div class="depart-users-tree">
<div v-if="!currentDepartTree.length" class="allChecked">
@@ -92,6 +92,18 @@
</div>
</template>
</div>
<!-- 分页 -->
<div v-if="!currentDepartTree.length && currentDeptTotal > currentDeptPageSize" class="dept-user-pagination">
<a-pagination
v-model:current="currentDeptPageNo"
:total="currentDeptTotal"
:pageSize="currentDeptPageSize"
size="small"
:showSizeChanger="false"
:showQuickJumper="false"
@change="handleDeptUserPageChange"
/>
</div>
</div>
<!-- 部门树 -->
<div v-if="currentDepartTree.length" class="depart-tree">
@@ -196,6 +208,11 @@
const checkedDepartIds = ref<string[]>([]);
// 当前部门用户
const currentDepartUsers = ref([]);
// 当前叶子部门分页状态
const currentDeptId = ref('');
const currentDeptPageNo = ref(1);
const currentDeptPageSize = ref(50);
const currentDeptTotal = ref(0);
// 已选用户
const selectedUsers = ref<any[]>([]);
// 全选
@@ -277,6 +294,9 @@
const handleBreadcrumbClick = (item?) => {
// 先清空
currentDepartUsers.value = [];
currentDeptId.value = '';
currentDeptPageNo.value = 1;
currentDeptTotal.value = 0;
if (item) {
const findIndex = breadcrumb.value.findIndex((o) => o.id === item.id);
if (findIndex != -1) {
@@ -375,27 +395,41 @@
} else {
// 没有子节点,则显示用户
currentDepartTree.value = [];
getTableList({
departId: item['id'],
}).then((res: any) => {
if (res?.records) {
let checked = true;
res.records.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id == item.id);
if (findItem) {
// 能在右侧找到说明选中了,左侧同样需要选中。
item.checked = true;
} else {
item.checked = false;
checked = false;
}
});
currentDepartAllUsers.value = checked;
currentDepartUsers.value = res.records.sort((a, b) => a.sort - b.sort );
}
});
currentDeptId.value = item['id'];
currentDeptPageNo.value = 1;
currentDeptTotal.value = 0;
loadDeptUsers(item['id'], 1);
}
};
// 加载叶子部门用户(带分页)
const loadDeptUsers = (departId: string, pageNo: number) => {
return getTableList({
departId,
pageNo,
pageSize: currentDeptPageSize.value,
}).then((res: any) => {
if (res?.records) {
currentDeptTotal.value = res.total ?? 0;
currentDeptPageNo.value = pageNo;
let checked = true;
res.records.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id == item.id);
if (findItem) {
item.checked = true;
} else {
item.checked = false;
checked = false;
}
});
currentDepartAllUsers.value = checked && res.records.length > 0;
currentDepartUsers.value = res.records.sort((a, b) => a.sort - b.sort);
}
});
};
// 叶子部门用户翻页
const handleDeptUserPageChange = (page: number) => {
loadDeptUsers(currentDeptId.value, page);
};
// 点击部门用户树复选框触发
const handleDepartUsersTreeCheck = (item) => {
item.checked = !item.checked;
@@ -518,11 +552,11 @@
} else {
getTableList({
departId: id,
pageNo: 1,
pageSize: 1000,
}).then((res: any) => {
cacheDepartUser[id] = res.records ?? [];
if (res?.records?.length) {
resolve(res.records ?? []);
}
resolve(res.records ?? []);
});
}
});
@@ -602,6 +636,14 @@
padding: 0;
}
}
.ant-modal-root .fullscreen-modal.JSelectUserByDepartmentModal {
.j-select-user-by-dept {
height: 100%;
}
.modal-content {
height: 100%;
}
}
</style>
<style lang="less" scoped>
.j-select-user-by-dept {
@@ -704,6 +746,11 @@
}
}
}
.dept-user-pagination {
padding: 8px 16px;
text-align: right;
border-top: 1px solid #f0f0f0;
}
.depart-users-tree {
.allChecked {
padding: 0 16px;

View File

@@ -0,0 +1,182 @@
import { computed, ref, Ref, unref } from 'vue';
import { useDebounceFn } from '@vueuse/core';
import { defHttp } from '/@/utils/http/axios';
/** 触发「加载更多」的滚动剩余距离阈值px */
const SCROLL_LOAD_THRESHOLD = 10;
/** 搜索输入防抖时间ms */
const SEARCH_DEBOUNCE_MS = 300;
export interface ScrollLoadDictProps {
dictCode?: string;
pageSize: number;
scrollLoad: boolean;
}
export function useScrollLoadDict(props: ScrollLoadDictProps, dictOptions: Ref<any[]>, arrayValue: Ref<any[]>) {
// --- 状态 ---
const loading = ref(false);
const pageNo = ref(1);
const isHasData = ref(true);
const scrollLoading = ref(false);
const searchKeyword = ref('');
// --- 计算:是否表字典、是否启用滚动加载 ---
const isDictTable = computed(() => {
if (!props.dictCode) return false;
return props.dictCode.split(',').length >= 2;
});
const useLoadDict = computed(() => props.scrollLoad && isDictTable.value);
/**
* 拉取一页字典数据loadDict 接口)。
* @param pageNoToLoad 页码(从 1 开始)
* @param isAppend true=追加到当前列表false=替换
* @param keyword 搜索关键字,不传则用内部的 searchKeyword
*/
function fetchLoadDictPage(pageNoToLoad: number, isAppend: boolean, keyword?: string) {
const kw = keyword !== undefined ? keyword : searchKeyword.value;
return defHttp
.get({
url: `/sys/dict/loadDict/${props.dictCode}`,
params: { pageNo: pageNoToLoad, pageSize: props.pageSize, keyword: kw || '', order: 'asc' },
})
.then((res: any) => {
const items = (res || []).map((item: any) => ({
value: item.value,
label: item.text || item.label,
text: item.text || item.label,
color: item.color,
}));
if (items.length > 0) {
if (isAppend) {
// 追加时按 value 去重,避免与回显补的项重复
const existValues = new Set(dictOptions.value.map((o) => String(o.value)));
const newItems = items.filter((it: any) => !existValues.has(String(it.value)));
if (newItems.length > 0) {
dictOptions.value = dictOptions.value.concat(newItems);
}
} else {
// 有选中的值且optinos中存在时需要把选中的值在options中存在且不在新数据中的项保留防止多次请求
if (unref(arrayValue).length && unref(dictOptions).length) {
unref(arrayValue).forEach((val: any) => {
const existOption = unref(dictOptions).find((o: any) => String(o.value) === String(val));
if (existOption && !items.some((item: any) => String(item.value) === String(val))) {
items.push(existOption);
}
});
}
dictOptions.value = items;
}
pageNo.value = pageNoToLoad + 1;
} else {
if (!isAppend) dictOptions.value = [];
isHasData.value = false;
}
});
}
function fetchDictItemByValue(val: any) {
if (val == null || !props.dictCode) return Promise.resolve(null);
return defHttp
.get({ url: `/sys/dict/loadDictItem/${props.dictCode}`, params: { key: val } })
.then((res: any) => {
if (Array.isArray(res) && res.length > 0) {
const first = res[0];
if (typeof first === 'string') {
return { value: val, label: first, text: first, color: undefined };
}
return {
value: first.value ?? val,
label: first.text ?? first.label,
text: first.text ?? first.label ?? '',
color: first.color,
};
}
return null;
})
.catch(() => null);
}
function ensureValueInOptions() {
if (!useLoadDict.value) return;
const vals = arrayValue.value;
if (!vals || vals.length === 0) return;
const existSet = new Set(dictOptions.value.map((o) => String(o.value)));
const missing = vals.filter((v) => !existSet.has(String(v)));
if (missing.length === 0) return;
Promise.all(missing.map((v) => fetchDictItemByValue(v))).then((items) => {
const newItems = items.filter(Boolean);
if (newItems.length > 0) {
dictOptions.value = [...dictOptions.value, ...newItems];
}
});
}
/** 搜索:带防抖,用 keyword 拉取第一页并替换列表 */
const handleSearch = useDebounceFn((keyword: string) => {
if (!useLoadDict.value) return;
searchKeyword.value = keyword || '';
pageNo.value = 1;
isHasData.value = true;
loading.value = true;
fetchLoadDictPage(1, false, searchKeyword.value).finally(() => {
loading.value = false;
ensureValueInOptions();
});
}, SEARCH_DEBOUNCE_MS);
function handleDropdownVisibleChange(open: boolean) {
if (!useLoadDict.value || !open) return;
if (!searchKeyword.value) return;
searchKeyword.value = '';
pageNo.value = 1;
isHasData.value = true;
loading.value = true;
fetchLoadDictPage(1, false, '').finally(() => {
loading.value = false;
ensureValueInOptions();
});
}
/** 初始加载字典:拉取第一页 */
function loadDictOptions() {
if (!useLoadDict.value) return;
pageNo.value = 1;
isHasData.value = true;
loading.value = true;
fetchLoadDictPage(1, false).finally(() => {
loading.value = false;
ensureValueInOptions();
});
}
/** 下拉内滚动触底时加载下一页,按 value 去重后追加 */
function handlePopupScroll(e: Event) {
if (!useLoadDict.value) return;
const target = e.target as HTMLElement;
const { scrollTop, scrollHeight, clientHeight } = target;
const nearBottom = scrollTop + clientHeight >= scrollHeight - SCROLL_LOAD_THRESHOLD;
if (!scrollLoading.value && isHasData.value && nearBottom) {
scrollLoading.value = true;
fetchLoadDictPage(pageNo.value, true)
.finally(() => {
scrollLoading.value = false;
})
.catch(() => {
if (pageNo.value > 1) pageNo.value--;
});
}
}
return {
isDictTable,
useLoadDict,
loading,
loadDictOptions,
ensureValueInOptions,
handleSearch,
handleDropdownVisibleChange,
handlePopupScroll,
};
}

View File

@@ -130,6 +130,7 @@ export type ComponentType =
| 'JCodeEditor'
| 'JCategorySelect'
| 'JSelectMultiple'
| 'JSelectSingle'
| 'JPopup'
| 'JPopupDict'
| 'JSwitch'
@@ -143,6 +144,7 @@ export type ComponentType =
| 'JSelectUserByDept'
| 'JSelectUserByDeptPost'
| 'JSelectUserByDepartment'
| 'JTabsSelectUser'
| 'JUpload'
| 'JSearchSelect'
| 'JAddInput'

View File

@@ -108,4 +108,56 @@ const getAreaTextByCode = function (code) {
return jeecgAreaData.getText(code,index);
};
export { getAreaTextByCode };
/**
* 20260204
* liaozhiyang
* 【QQYUN-14694】online支持配置独立的省、市、县 (根据 code 找文本(支持仅省、仅省市、省市区))
* @param includeParent 是否返回父级路径。默认 false 只返回最后一级如「长治市」true 返回完整路径(如「山西省/长治市」)
* @param level 层级1=省2=市3=县。传入时作为 index 使用,不传则从 code 推断
*/
const getAreaTextByCodeAnyLevel = function (code: string | number | undefined, includeParent = false, level?: 1 | 2 | 3): string {
if (!code) return '';
const codeStr = String(code).trim();
if (!codeStr.length) return '';
let lastCode: string;
let index: number;
let resolvedItem: PlainPca | null = null;
if (codeStr.includes(',')) {
const parts = codeStr.split(',').map((s) => s.trim()).filter(Boolean);
if (!parts.length) return '';
index = level ?? parts.length;
lastCode = level != null ? parts[level - 1] : parts[parts.length - 1];
if (level != null) {
resolvedItem = jeecgAreaData.all.find((it) => String(it.id) === lastCode && it.index == index) ?? null;
}
} else {
const item = jeecgAreaData.all.find((it) => String(it.id) === codeStr);
if (!item) return '';
const itemIndex = item.index as number;
index = level ?? itemIndex;
lastCode = codeStr;
if (level != null && level !== itemIndex) {
const chain: string[] = [];
jeecgAreaData.getPcode(codeStr, chain, itemIndex);
const targetCode = chain[level - 1];
if (targetCode) {
lastCode = targetCode;
resolvedItem = jeecgAreaData.all.find((it) => String(it.id) === targetCode && it.index == level) ?? null;
} else {
resolvedItem = item;
}
} else {
resolvedItem = item;
}
}
if (includeParent) {
return jeecgAreaData.getText(lastCode, index);
} else {
const item = resolvedItem ?? jeecgAreaData.all.find((it) => String(it.id) === lastCode && it.index == index);
return item ? item.text : '';
}
};
export { getAreaTextByCode, getAreaTextByCodeAnyLevel };

View File

@@ -6,10 +6,8 @@
import type { PropType } from 'vue';
import { defineComponent, ref, watch, onMounted, nextTick, unref, computed, CSSProperties } from 'vue';
import SvgIcon from './SvgIcon.vue';
import Iconify from '@purge-icons/generated';
import { isString } from '/@/utils/is';
import { propTypes } from '/@/utils/propTypes';
const SVG_END_WITH_FLAG = '|svg';
export default defineComponent({
name: 'Icon',
@@ -43,18 +41,35 @@
await nextTick();
const icon = unref(getIconRef);
if (!icon) return;
const svg = Iconify.renderSVG(icon, {});
if (svg) {
el.textContent = '';
el.appendChild(svg);
} else {
const span = document.createElement('span');
span.className = 'iconify';
span.dataset.icon = icon;
el.textContent = '';
el.appendChild(span);
// update-begin--author:liaozhiyang---date:20260304---for:【QQYUN-14802】新增unplugin-icons插件及icon支持online和local两种模式
try {
let svg: SVGElement | null = null;
if (import.meta.env.VITE_GLOB_ICONIFY_USE_TYPE === 'local') {
// 使用本地 purge-icons 图标(离线,图标已打包进产物)
const iconifyModule = await import('@purge-icons/generated');
const Iconify = iconifyModule.default;
svg = Iconify.renderSVG(icon, {});
} else {
// 使用 @iconify/iconify 在线按需加载
const iconifyModule = await import('@iconify/iconify');
const Iconify = iconifyModule.default;
svg = Iconify.renderSVG(icon, {});
}
if (svg) {
el.textContent = '';
el.appendChild(svg);
} else {
// 如果图标不存在,显示占位符
const span = document.createElement('span');
span.className = 'iconify';
span.dataset.icon = icon;
el.textContent = '';
el.appendChild(span);
}
} catch (err) {
console.error('Failed to render icon:', icon, err);
}
// update-end--author:liaozhiyang---date:20260304---for:【QQYUN-14802】新增unplugin-icons插件及icon支持online和local两种模式
};
const getWrapStyle = computed((): CSSProperties => {

View File

@@ -22,6 +22,10 @@ export async function registerJVxeCustom() {
await registerAsyncComponent(JVxeTypes.userSelect, import('./src/components/JVxeUserSelectCell.vue'));
// 注册【部门选择】组件
await registerAsyncComponent(JVxeTypes.departSelect, import('./src/components/JVxeDepartSelectCell.vue'));
// update-begin--author:liaozhiyang---date:20260317---for:【QQYUN-9441】online一对多加上关联记录和他表字段
// 注册【关联记录】组件
await registerAsyncComponent(JVxeTypes.linkTable, import('./src/components/JVxeLinkTableCell.vue'));
// update-end--author:liaozhiyang---date:20260317---for:【QQYUN-9441】online一对多加上关联记录和他表字段
// 注册【省市区选择】组件
// await registerAsyncComponent(JVxeTypes.pca, import('./src/components/JVxePcaCell.vue'));
// 代码逻辑说明: 【QQYUN-8241】为避免首次加载china-area-dataJVxePcaCell组件需异步加载

View File

@@ -0,0 +1,105 @@
<template>
<LinkTableInput
:value="innerValue"
:valueField="originColumn.valueField || 'id'"
:textField="originColumn.textField || ''"
:tableName="originColumn.tableName || ''"
:multi="true"
:linkFields="originColumn.linkFields || []"
:imageField="originColumn.imageField || ''"
:editBtnShow="!originColumn.editBtnShow"
v-bind="cellProps"
@change="handleChange"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
import { vModel } from '/@/components/jeecg/JVxeTable/utils';
import { isEmpty } from '/@/utils/is';
import { defHttp } from '/@/utils/http/axios';
export default defineComponent({
name: 'JVxeLinkTableCell',
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { innerValue, cellProps, originColumn, scrolling, handleChangeCommon, row } = useJVxeComponent(props);
function handleChange(value, otherValues) {
if (!isEmpty(value)) {
Object.keys(otherValues).forEach((key) => {
let currentValue = otherValues[key];
vModel(currentValue, row, key);
});
handleChangeCommon(value);
}
}
return {
innerValue,
cellProps,
originColumn,
scrolling,
handleChange,
row,
};
},
enhanced: {
aopEvents: {
},
interceptor: {
'event.clearActived.className'(className) {
if (className.includes('jeecg-online-pop-list-modal') || className.includes('jeecg-online-pop-modal')) {
return false;
}
},
},
translate: {
enabled: true,
async handler(value, ctx) {
if (!value) return '';
if (!ctx) return value;
const { originColumn, row } = ctx.context;
const col = originColumn.value;
const tableName = col.tableName;
const textField = col.textField || '';
const valueField = col.valueField || 'id';
// 关联记录相关的他表字段
const linkFields: string[] = col.linkFields || [];
if (!tableName) return value;
// 将他表字段对应的关联表字段也一并查询出来
const extraTableFields = linkFields.map((lf) => lf.split(',')[1]).filter(Boolean);
const allSelectFields = [valueField, textField, ...extraTableFields].filter(Boolean);
const selectFields = [...new Set(allSelectFields)].join(',');
const params = {
linkTableSelectFields: selectFields,
pageSize: value.split(',').length,
pageNo: 1,
superQueryMatchType: 'and',
superQueryParams: encodeURI(JSON.stringify([{ field: valueField, rule: 'in', val: value }])),
};
const data = await defHttp.get({ url: '/online/cgform/api/getData/' + tableName, params });
const records = data?.records || [];
const textKey = textField.split(',')[0];
const vals = (value + '').split(',');
const labels = vals.map((v) => {
const record = records.find((r) => String(r[valueField]) === String(v));
return record ? record[textKey] : v;
});
// 将他表字段的值同步写入行数据,使对应列能正确显示
if (linkFields.length > 0 && row) {
linkFields.forEach((lf) => {
const [formField, tableField] = lf.split(',');
const fieldValues = vals.map((v) => {
const record = records.find((r) => String(r[valueField]) === String(v));
return record ? (record[tableField] ?? '') : '';
});
vModel(fieldValues.join(','), row, formField);
});
}
return labels.join(',');
},
},
} as JVxeComponent.EnhancedPartial,
});
</script>

View File

@@ -28,7 +28,9 @@
// orgFields: originColumn.value.orgFields,
// destFields: originColumn.value.destFields,
groupId: groupId.value,
param: originColumn.value.params,
// update-begin--author:liaozhiyang---date:20260203---for:【issues/9212】JVxeTypes.popup中属性param传参后弹框数据为空
param: originColumn.value.params ?? originColumn.value.param,
// update-end--author:liaozhiyang---date:20260203---for:【issues/9212】JVxeTypes.popup中属性param传参后弹框数据为空
sorter: originColumn.value.sorter,
setFieldsValue: (values) => {
if (!isEmpty(values)) {

View File

@@ -5,9 +5,9 @@ import { filterDictText } from '/@/utils/dict/JDictSelectUtil';
import { ajaxGetDictItems, getDictItemsByCode } from '/@/utils/dict';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
import { dispatchEvent } from '/@/components/jeecg/JVxeTable/utils';
import { useResolveComponent as rc } from '/@/components/jeecg/JVxeTable/hooks';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
import { useMessage } from '/@/hooks/web/useMessage';
import { Select, SelectOption, Spin } from 'ant-design-vue'
/** value - label map防止重复查询刷新清空缓存 */
const LabelMap = new Map<string, any>();
@@ -101,7 +101,7 @@ export const DictSearchInputCell = defineComponent({
options.value.forEach(({ value, text, label, title, disabled }) => {
optionItems.push(
h(
rc('a-select-option'),
SelectOption,
{
key: value,
value: value,
@@ -118,7 +118,7 @@ export const DictSearchInputCell = defineComponent({
return () => {
return h(
rc('a-select'),
Select,
{
...cellProps.value,
value: innerSelectValue.value,
@@ -135,7 +135,7 @@ export const DictSearchInputCell = defineComponent({
default: () => renderOptionItem(),
notFoundContent: () => {
if (loading.value) {
return h(rc('a-spin'), { size: 'small' });
return h(Spin, { size: 'small' });
} else if (hasRequest.value) {
return h('div', '没有查询到任何数据');
} else {

View File

@@ -186,6 +186,12 @@
if (result) {
return result;
}
// update-begin--author:liaozhiyang---date:20250401---for【issues/9405】修复顶部混合导航模式下点击一级菜单时,如果最后一级是隐藏路由显示不对
// 子菜单全部是隐藏路由时,返回上一级作为导航目标
if (item.path && !item.hideMenu) {
return item;
}
// update-end--author:liaozhiyang---date:20250401---for【issues/9405】修复顶部混合导航模式下点击一级菜单时,如果最后一级是隐藏路由显示不对
}
}
return null;

View File

@@ -209,7 +209,13 @@
...options,
setup: (editor: any) => {
editorRef.value = editor;
editor.on('init', (e) => initSetup(e));
editor.on('init', (e) => {
initSetup(e);
// update-begin--author:liaozhiyang---date:20260306---for:【issues/9448】滚动时tinymce如果下拉打开则隐藏
// 弹窗内滚动时关闭tinymce下拉菜单避免下拉菜单挂载在body导致位置不跟随
bindScrollCloseMenu(editor);
// update-end--author:liaozhiyang---date:20260306---for:【issues/9448】滚动时tinymce如果下拉打开则隐藏
});
//update-begin-author:liusq---date:2025-11-19--for: JHHB-1070 从word复制的表格不能对齐
// 表格对齐功能
initTableAlignment(editor);
@@ -496,6 +502,34 @@
}
);
// 滚动时关闭tinymce下拉菜单
function bindScrollCloseMenu(editor) {
const root = unref(editorRootRef);
if (!root) return;
const scrollParent = getScrollParent(root);
if (!scrollParent) return;
const onScroll = () => {
// 触发body上的mousedowntinymce会自动关闭打开的下拉菜单
document.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
};
scrollParent.addEventListener('scroll', onScroll, { passive: true });
editor.on('remove', () => {
scrollParent.removeEventListener('scroll', onScroll);
});
}
function getScrollParent(el: HTMLElement): HTMLElement | null {
let parent = el.parentElement;
while (parent && parent !== document.body) {
const { overflow, overflowY } = getComputedStyle(parent);
if (/(auto|scroll)/.test(overflow + overflowY)) {
return parent;
}
parent = parent.parentElement;
}
return null;
}
/**
* 处理图片链接地址
*

View File

@@ -0,0 +1,722 @@
<template>
<BasicModal
title="选择用户"
@register="registerModal"
width="100%"
style="top: 10px"
@ok="handleSelectSuccess"
:canFullscreen="true"
keyboard
defaultFullscreen
>
<a-row>
<!-- 左侧多tabs切换 -->
<a-col :xs="24" :sm="6">
<a-card :bordered="true" :bodyStyle="{ maxHeight: 'calc(100vh - 200px)', overflow: 'hidden', padding: 0 }">
<a-tabs v-model:activeKey="activeTab" size="small" centered :tabBarStyle="{ margin: 10 }" @change="onTabChange">
<!-- 部门标签页 -->
<a-tab-pane key="depart" tab="部门">
<div style="padding: 14px">
<a-alert type="info" :showIcon="true" class="alert-info">
<template #message>
<div class="alert-message-content">
<span class="alert-label">当前选择</span>
<span v-if="departInfo.currentSelectRow.title" class="selected-title">{{ departInfo.currentSelectRow.title }}</span>
<a v-if="departInfo.currentSelectRow.title" style="margin-left: 10px" @click="onClearSelectedDepart">取消选择</a>
</div>
</template>
</a-alert>
<a-input-search placeholder="按部门名称搜索…" :allowClear="true" style="margin-bottom: 10px" @search="onSearchDepart" v-model:value="departSearchText" />
<!--组织机构-->
<div class="tree-container" v-if="departInfo.treeData && departInfo.treeData.length > 0">
<a-directory-tree
selectable
:selectedKeys="departInfo.selectedKeys"
:checkStrictly="true"
@select="onSelectDepart"
:dropdownStyle="{ maxHeight: '200px', overflow: 'auto' }"
:load-data="onLoadTreeData"
:treeData="departInfo.treeData"
:showIcon="false"
:expandedKeys="expandedDepartKeys"
@expand="onDepartExpand"
>
<template #title="{ orgCategory, title }">
<TreeIcon :orgCategory="orgCategory" :title="title"></TreeIcon>
</template>
</a-directory-tree>
</div>
<a-empty v-else description="无部门信息" />
</div>
</a-tab-pane>
<!-- 岗位标签页 -->
<a-tab-pane key="position" tab="岗位">
<div style="padding: 14px">
<a-alert type="info" :showIcon="true" class="alert-info">
<template #message>
<div class="alert-message-content">
<span class="alert-label">当前选择</span>
<span v-if="positionInfo.currentSelectRow.title" class="selected-title">{{ positionInfo.currentSelectRow.title }}</span>
<a v-if="positionInfo.currentSelectRow.title" style="margin-left: 10px" @click="onClearSelectedPosition">取消选择</a>
</div>
</template>
</a-alert>
<a-input-search
placeholder="按岗位名称搜索…"
style="margin-bottom: 10px"
allowClear
@search="onSearchPosition"
v-model:value="positionSearchText"
/>
<!--岗位列表-->
<div class="">
<PostRankRelation :treeData="positionInfo.treeData" @select="onSelectPosition" />
</div>
</div>
</a-tab-pane>
<!-- 用户组标签页 -->
<a-tab-pane key="userGroup" tab="用户组">
<div style="padding: 14px">
<a-alert type="info" :showIcon="true" class="alert-info">
<template #message>
<div class="alert-message-content">
<span class="alert-label">当前选择</span>
<span v-if="userGroupInfo.currentSelectRow.title" class="selected-title">{{ userGroupInfo.currentSelectRow.title }}</span>
<a v-if="userGroupInfo.currentSelectRow.title" style="margin-left: 10px" @click="onClearSelectedUserGroup">取消选择</a>
</div>
</template>
</a-alert>
<a-input-search
placeholder="按用户组名称搜索…"
style="margin-bottom: 10px"
allowClear
@search="onSearchUserGroup"
v-model:value="userGroupSearchText"
/>
<!--用户组列表-->
<div class="user-group-list" v-if="userGroupInfo.data && userGroupInfo.data.length > 0">
<div
v-for="item in userGroupInfo.data"
:key="item.id"
class="user-group-item"
:class="{ 'user-group-item-selected': userGroupInfo.selectedKeys.includes(item.id) }"
@click="onSelectUserGroup(item)"
>
<span class="user-group-title">{{ item.title }}</span>
</div>
</div>
<a-empty v-else description="无用户组信息" />
</div>
</a-tab-pane>
</a-tabs>
</a-card>
</a-col>
<!-- 中间列表-展示用户信息 -->
<a-col :xs="24" :sm="showSelected ? 12 : 18">
<a-card title="选择人员" :bordered="true" :bodyStyle="{ paddingTop: '1px' }">
<BasicTable @register="registerTable" :rowSelection="rowSelection" />
</a-card>
</a-col>
<!-- 右侧显示已经选中用户支持调整顺序 -->
<a-col :xs="24" :sm="6" v-if="showSelected">
<a-card title="已选用户" :bordered="true">
<BasicTable @register="registerSelectedUserTable">
<!--操作栏-->
<template #action="{ record }">
<a-button type="primary" size="small" @click="handleDelete(record)" preIcon="ant-design:delete">删除</a-button>
</template>
</BasicTable>
</a-card>
</a-col>
</a-row>
</BasicModal>
</template>
<script lang="ts" setup>
import { BasicModal, useModalInner } from '/@/components/Modal';
import { ref, unref, reactive, toRaw, watch } from 'vue';
import {
getDepartTreeData,
getDepartUserList,
getUserList,
queryALLRankRelation,
searchByKeywords,
columns,
selectedUserColumns,
searchFormSchema,
} from './useSelectUser';
import { BasicTable } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import TreeIcon from '@/components/Form/src/jeecg/components/TreeIcon/TreeIcon.vue';
import { defHttp } from '/@/utils/http/axios';
import PostRankRelation from './component/PostRankRelation.vue';
const props = defineProps({
multi: {
type: Boolean,
default: true,
},
showSelected: {
type: Boolean,
default: true,
},
//回传value字段名
rowKey: {
type: String,
default: 'username',
},
});
const emit = defineEmits(['selected', 'register']);
const [registerModal, { closeModal }] = useModalInner((data) => {
showSelectedValue(data);
});
// 当前激活的标签页
const activeTab = ref('depart');
// 加载状态
const loading = ref(false);
// 搜索文本
const departSearchText = ref('');
const positionSearchText = ref('');
const userGroupSearchText = ref('');
// 原始数据缓存
const originalDepartData = ref<any[]>([]);
const originalPositionData = ref<any[]>([]);
const originalUserGroupData = ref<any[]>([]);
/*-----------------部门---begin----------------*/
const departInfo = reactive({
treeData: [] as any[],
selectedKeys: [] as any[],
currentSelectRow: {
title: '',
} as any,
});
function onSelectDepart(data, { node }) {
departInfo.selectedKeys[0] = data[0];
departInfo.currentSelectRow = toRaw(node.dataRef);
console.log(departInfo);
reload();
}
function onClearSelectedDepart() {
departInfo.selectedKeys = [];
departInfo.currentSelectRow = { title: '' };
reload();
}
// 部门树展开的keys
const expandedDepartKeys = ref<any[]>([]);
// 部门树展开事件
function onDepartExpand(expandedKeys) {
expandedDepartKeys.value = expandedKeys;
// 延迟触发滚动区域重新计算
setTimeout(() => {
const treeContainer = document.querySelector('.tree-container') as HTMLElement | null;
if (treeContainer) {
// 强制触发重排
treeContainer.style.overflow = 'hidden';
setTimeout(() => {
treeContainer.style.overflow = 'auto';
}, 10);
}
}, 100);
}
async function loadRootDepart() {
const result = await getDepartTreeData();
if (Array.isArray(result)) {
//update-begin-author:liusq---date:2025-12-05--for: JHHB-1175 【流程审批】指定下一步操作人,人员需要按用户的排序进行排序 部门加滚动条 显示部门简称
result.forEach((item) => {
item.title = item.departNameAbbr || item.title;
});
//update-end-author:liusq---date:2025-12-05--for: JHHB-1175 【流程审批】指定下一步操作人,人员需要按用户的排序进行排序 部门加滚动条 显示部门简称
departInfo.treeData = result;
originalDepartData.value = result;
}
}
// 部门搜索
async function onSearchDepart(value) {
departSearchText.value = value;
if (!value) {
departInfo.treeData = originalDepartData.value;
return;
}
try {
departInfo.treeData = [];
let result = await searchByKeywords({ keyWord: value, orgCategory: '1,2' });
if (Array.isArray(result)) {
departInfo.treeData = result;
}
} finally {
loading.value = false;
}
}
async function onLoadTreeData(treeNode) {
try {
const result = await getDepartTreeData({
pid: treeNode.dataRef.id,
});
if (result.length == 0) {
treeNode.dataRef.isLeaf = true;
} else {
//update-begin-author:liusq---date:2025-12-05--for: JHHB-1175 【流程审批】指定下一步操作人,人员需要按用户的排序进行排序 部门加滚动条 显示部门简称
result.forEach((item) => {
item.title = item.departNameAbbr || item.title;
});
//update-end-author:liusq---date:2025-12-05--for: JHHB-1175 【流程审批】指定下一步操作人,人员需要按用户的排序进行排序 部门加滚动条 显示部门简称
treeNode.dataRef.children = result;
}
// departInfo.treeData = [...departInfo.treeData]
// 数据加载完成后,延迟触发滚动区域重新计算
// setTimeout(() => {
// const treeContainer = document.querySelector('.tree-container');
// if (treeContainer) {
// // 强制触发重排,保持当前滚动位置
// const scrollTop = treeContainer.scrollTop;
// treeContainer.style.overflow = 'hidden';
// setTimeout(() => {
// treeContainer.style.overflow = 'auto';
// treeContainer.scrollTop = scrollTop;
// }, 10);
// }
// }, 50);
} catch (e) {
console.error('部门树子节点加载失败', e);
}
return Promise.resolve();
}
/*-----------------部门---end----------------*/
/*-----------------岗位---begin----------------*/
const positionInfo = reactive({
treeData: [] as any[],
selectedKeys: [] as any[],
currentSelectRow: { title: '' } as any,
});
// 递归搜索函数,支持搜索所有层级的节点
const findNodeInTree = (treeData: any[], targetId: any): any => {
for (const node of treeData) {
if (node.id == targetId) {
return node;
}
if (node.children && Array.isArray(node.children)) {
const found = findNodeInTree(node.children, targetId);
if (found) {
return found;
}
}
}
return null;
};
function onSelectPosition(data) {
// 避免循环引用错误,只使用安全的数据
positionInfo.selectedKeys[0] = data[0];
let obj = findNodeInTree(positionInfo.treeData, data[0]);
positionInfo.currentSelectRow = obj ? { ...obj } : { title: '' };
reload();
}
function onClearSelectedPosition() {
positionInfo.selectedKeys = [];
positionInfo.currentSelectRow = { title: '' };
reload();
}
async function loadPositionList() {
try {
const result = await queryALLRankRelation();
if (result && Array.isArray(result) && result.length > 0) {
positionInfo.treeData = result;
originalPositionData.value = result;
}
} catch (error) {
console.error('获取岗位列表失败', error);
}
}
// 岗位搜索
function onSearchPosition(value) {
positionSearchText.value = value;
if (!value) {
positionInfo.treeData = originalPositionData.value;
return;
}
const searchText = value.toLowerCase();
// 递归过滤树:如果节点自身匹配或任一子孙匹配,则保留该节点(并只保留匹配的子节点)
function filterTree(nodes: any[]): any[] {
if (!Array.isArray(nodes)) return [];
const result: any[] = [];
for (const node of nodes) {
const title = (node.title || '').toLowerCase();
let matched = title.includes(searchText);
let filteredChildren = [];
if (node.children && Array.isArray(node.children)) {
filteredChildren = filterTree(node.children);
if (filteredChildren.length > 0) matched = true;
}
if (matched) {
// 浅拷贝节点,避免修改原始数据
const newNode: any = { ...node };
if (filteredChildren.length > 0) {
newNode.children = filteredChildren;
} else {
// 确保在没有匹配子节点时不携带 children 或将其清空
delete newNode.children;
}
result.push(newNode as any);
}
}
return result;
}
positionInfo.treeData = filterTree(originalPositionData.value || []);
}
/*-----------------岗位---end----------------*/
/*-----------------用户组---begin----------------*/
const userGroupInfo = reactive({
data: [] as any[],
selectedKeys: [] as string[],
currentSelectRow: {
title: '',
} as any,
});
function onSelectUserGroup(data) {
userGroupInfo.selectedKeys[0] = data.id;
userGroupInfo.currentSelectRow = data;
reload();
}
function onClearSelectedUserGroup() {
userGroupInfo.selectedKeys = [];
userGroupInfo.currentSelectRow = { title: '' };
reload();
}
async function loadUserGroupList() {
try {
const result = await defHttp.get({ url: '/sys/ugroup/list' });
if (result.records && Array.isArray(result.records) && result.records.length > 0) {
const userGroupData = result.records.map((item) => ({
key: item.id,
title: item.groupName,
id: item.id,
}));
userGroupInfo.data = userGroupData;
originalUserGroupData.value = userGroupData;
}
} catch (error) {
console.error('获取用户组列表失败', error);
}
}
// 用户组搜索
function onSearchUserGroup(value) {
userGroupSearchText.value = value;
if (!value) {
userGroupInfo.data = originalUserGroupData.value;
return;
}
const searchText = value.toLowerCase();
const filteredData = originalUserGroupData.value.filter((item) => item.title.toLowerCase().includes(searchText));
userGroupInfo.data = filteredData;
}
/*-----------------用户组---end----------------*/
/*-----------------用户列表---begin----------------*/
async function queryUserList(params) {
params['column'] = 'sort';
params['order'] = 'ASC';
if(departInfo.selectedKeys.length == 0 && positionInfo.selectedKeys.length == 0 && userGroupInfo.selectedKeys.length == 0 && params.realname){
params['realname'] = `*${params.realname.trim()}*`;
}
// 根据当前激活的标签页进行查询
if (activeTab.value === 'depart') {
let arr = departInfo.selectedKeys;
if (arr.length > 0) {
//根据部门查询
params['id'] = arr[0];
let result = await getDepartUserList(params);
if (params.username) {
result.records = result.records.filter((item) => {
return item.username.indexOf(params.username) != -1;
});
}
return Promise.resolve(result);
} else {
return getUserList(params);
}
} else if (activeTab.value === 'position') {
let arr = positionInfo.selectedKeys;
if (arr.length > 0) {
//根据岗位查询
params['orgCode'] = positionInfo.currentSelectRow?.orgCode;
return defHttp.get({ url: '/sys/user/queryDepartPostByOrgCode', params });
} else {
return getUserList(params);
}
} else if (activeTab.value === 'userGroup') {
let arr = userGroupInfo.selectedKeys;
if (arr.length > 0) {
//根据用户组查询
params['groupId'] = arr[0];
return defHttp.get({ url: '/sys/user/groupUserList', params });
} else {
return getUserList(params);
}
} else {
return getUserList(params);
}
}
const { tableContext } = useListPage({
designScope: 'tabs-select-user',
pagination: true,
tableProps: {
title: '',
api: queryUserList,
columns: columns,
showActionColumn: false,
showTableSetting: false,
canResize: false,
clickToRowSelect: true,
formConfig: {
labelWidth: '90px',
schemas: searchFormSchema,
autoAdvancedCol: 4,
//update-begin-author:liusq---date:2024-06-11--for: 指定会签人员的弹框 查询遮挡了
baseColProps: { xs: 24, sm: 24, md: 24, lg: 12, xl: 8, xxl: 8 },
actionColOptions: { xs: 24, sm: 24, md: 24, lg: 12, xl: 8, xxl: 8 },
//update-end-author:liusq---date:2024-06-11--for:指定会签人员的弹框 查询遮挡了
},
},
});
const [registerTable, { reload, deleteSelectRowByKey }, { rowSelection, selectedRows, selectedRowKeys }] = tableContext;
watch(
() => props.multi,
(val) => {
if (val === false) {
rowSelection.type = 'radio';
} else {
rowSelection.type = 'checkbox';
}
}
);
/*-----------------用户列表--end-----------------*/
const selectedUserList = ref<any[]>([]);
//update-begin-author:liusq---date:2024-06-11--for: TV360X-1047 指定下一步操作人/抄送给,选人组件无法多选。
watch(
selectedRows,
() => {
let arr = [];
for (let row of unref(selectedRows)) {
arr.push({
realname: row.realname,
username: row.username,
id: row.id,
});
}
selectedUserList.value = arr;
},
{ deep: true }
);
//update-end-author:liusq---date:2024-06-11--for: TV360X-1047 指定下一步操作人/抄送给,选人组件无法多选。
const { tableContext: selectedTableContext } = useListPage({
designScope: 'bpm-select-user',
pagination: false,
tableProps: {
title: '',
columns: selectedUserColumns,
pagination: false,
dataSource: selectedUserList,
showActionColumn: true,
showTableSetting: false,
canResize: false,
useSearchForm: false,
},
});
const [registerSelectedUserTable] = selectedTableContext;
function handleDelete(record) {
let id = record.id;
let arr = selectedUserList.value;
arr = arr.filter((item) => item.id != id);
selectedUserList.value = arr;
deleteSelectRowByKey(record.id);
}
function handleSelectSuccess() {
let arr = toRaw(selectedUserList.value);
emit('selected', arr);
closeModal();
}
/**
* 弹框打开 回显下拉框选中的数据
* @param data
*/
function showSelectedValue(data) {
let selectedValue = data.selected;
if (!selectedValue || selectedValue.length == 0) {
selectedUserList.value = [];
selectedRows.value = [];
selectedRowKeys.value = [];
} else {
let arr1 = [],
arr2 = [],
arr3 = [];
for (let item of selectedValue) {
arr1.push(item.id);
arr2.push({ ...item });
arr3.push({ ...item });
}
selectedRowKeys.value = arr1;
selectedUserList.value = arr2;
selectedRows.value = arr3;
}
}
/**
* tab切换取消选择
* @param key
*/
function onTabChange() {
onClearSelectedDepart();
onClearSelectedPosition();
onClearSelectedUserGroup();
}
// 初始化加载所有数据
loadRootDepart();
loadPositionList();
loadUserGroupList();
</script>
<style scoped>
.user-group-list {
max-height: calc(100vh - 390px);
min-height: 150px;
overflow-y: auto;
margin-top: 8px;
}
.user-group-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
margin-bottom: 4px;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
background-color: #fff;
}
.user-group-item:hover {
border-color: #40a9ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
}
.user-group-item-selected {
border-color: #1890ff;
background-color: #f0f8ff;
color: #1890ff;
font-weight: 500;
}
.user-group-item-selected:hover {
border-color: #096dd9;
background-color: #e6f7ff;
}
.user-group-title {
flex: 1;
font-size: 14px;
line-height: 1.5;
}
.selected-icon {
color: #52c41a;
font-size: 14px;
margin-left: 8px;
}
.alert-info {
margin-bottom: 5px;
}
/**部门树组件滚动条样式*/
.tree-container {
max-height: calc(100vh - 390px); /* 视口响应式高度,兼容小屏 */
min-height: 150px; /* 最小高度兜底 */
height: auto; /* 自适应内容 */
overflow-y: auto; /* 垂直滚动 */
overflow-x: hidden; /* 隐藏水平滚动 */
position: relative; /* 相对定位 */
}
/* 深度选择器优化树组件内部样式 */
.tree-container :deep(.ant-tree) {
width: 100%;
padding: 8px 4px;
}
.tree-container :deep(.ant-tree-list-holder) {
overflow: visible !important;
}
.tree-container::-webkit-scrollbar {
width: 6px;
}
.tree-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.tree-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.tree-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.selected-title {
display: inline-block; /* 设置为块级元素才能应用宽度限制 */
max-width: 180px; /* 最大宽度限制 */
overflow: hidden; /* 超出部分隐藏 */
text-overflow: ellipsis; /* 显示省略号 */
white-space: nowrap; /* 禁止换行 */
vertical-align: middle; /* 垂直居中 */
}
/* Alert消息内容水平对齐 */
.alert-message-content {
display: flex; /* 弹性布局 */
align-items: center; /* 垂直居中 */
gap: 8px; /* 元素间距 */
flex-wrap: nowrap; /* 允许换行 */
}
.alert-label {
color: #000000d9; /* 标签颜色 */
font-weight: 500; /* 字体加粗 */
white-space: nowrap; /* 禁止换行 */
}
.clear-link {
color: #1890ff; /* 链接颜色 */
text-decoration: none; /* 去除下划线 */
white-space: nowrap; /* 禁止换行 */
cursor: pointer; /* 手型光标 */
}
.clear-link:hover {
color: #40a9ff; /* 悬停颜色 */
text-decoration: underline; /* 悬停下划线 */
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<a-spin :spinning="loading">
<template v-if="treeData && treeData.length > 0">
<BasicTree
:expandedKeys="expandedKeys"
:fieldNames="{ children: 'children', title: 'title', key: 'value' }"
ref="basicTree"
:treeData="treeData"
:checkStrictly="true"
@select="onSelectDepart"
style="height: calc(100vh - 390px); min-height: 150px; overflow: auto"
></BasicTree>
</template>
<a-empty v-else description="无岗位信息" />
</a-spin>
</template>
<script lang="ts" setup>
import { ref, watchEffect } from 'vue';
import { BasicTree } from '/@/components/Tree/index';
const props = defineProps({
treeData: { type: Array, default: () => ([]) },
});
const emit = defineEmits(['select']);
const basicTree = ref();
const loading = ref<boolean>(false);
//选中的key
const expandedKeys = ref<any[]>([]);
//所有的部门id
const departIds = ref<any[]>([]);
/**
* 折叠全部
*
* @param expandAll
*/
async function expandAll(expandAll) {
if (!expandAll) {
expandedKeys.value = [];
} else {
expandedKeys.value = departIds.value;
}
}
watchEffect(() => {
props.treeData && (departIds.value = getParentDepartmentIds(props.treeData));
});
/**
*
* @param data
* @param node
*/
function onSelectDepart(data, { node }) {
emit('select', data);
}
/**
* 获取存在子级的部门id
* @param departments
*/
function getParentDepartmentIds(departments) {
const ids: any = [];
departments.forEach((dept) => {
// 检查是否有 children 数组且不为空
if (dept.children && Array.isArray(dept.children) && dept.children.length > 0) {
ids.push(dept.id);
// 递归检查子部门是否也有子级
ids.push(...getParentDepartmentIds(dept.children));
}
});
return ids;
}
</script>
<style lang="less" scoped>
.depart-rule-tree :deep(.scrollbar__bar) {
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,174 @@
<template>
<div class="tabsSelectUser" v-bind="$attrs">
<JSelectBiz @handleOpen="openSelect" allowClear :placeholder="placeholder" :loading="loadingEcho" v-bind="attrs" @change="handleSelectChange"></JSelectBiz>
</div>
<teleport to="body">
<select-user-modal @register="registerModal" :rowKey="rowKey" @selected="onSelected" />
</teleport>
</template>
<script lang="ts" setup>
/**
* 选择用户组件 - 多标签切换选择用户
*/
import { provide, reactive, ref, watch } from 'vue';
import SelectUserModal from './SelectUserModal.vue';
import { useModal } from '/src/components/Modal';
import JSelectBiz from '@/components/Form/src/jeecg/components/base/JSelectBiz.vue';
import { propTypes } from '@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { isArray, isString, isObject } from '/@/utils/is';
import { getTableList as getTableListOrigin } from '/@/api/common/api';
defineOptions({ name: 'JTabsSelectUser' });
//定义props
const props = defineProps({
placeholder:{
type: String,
default: '选择人员',
},
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
rowKey: {
type: String,
default: 'username',
},
labelKey: {
type: String,
default: 'realname',
},
});
//定义emit
const emit = defineEmits(['options-change', 'change', 'update:value']);
const attrs: any = useAttrs();
const [registerModal, { openModal }] = useModal();
//下拉框选项值
const selectOptions: any = ref<SelectValue>([]);
//下拉框选中值
let selectValues: any = reactive<object>({
value: [],
change: false,
});
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
//下发 selectOptions,xxxBiz组件接收
provide('selectOptions', selectOptions);
//下发 selectValues,xxxBiz组件接收
provide('selectValues', selectValues);
//下发 loadingEcho,xxxBiz组件接收
provide('loadingEcho', loadingEcho);
function openSelect() {
let arr = getModalData();
openModal(true, {
selected: arr,
});
}
function getModalData() {
//找rows
let arr = selectOptions.value;
//找username
let arr2 = selectValues.value;
let dataArray = arr.filter((item) => arr2.indexOf(item[props.rowKey]) >= 0);
return dataArray;
}
function onSelected(data) {
selectOptions.value = data.map((item) => {
return {
...item,
label: item[props.labelKey],
value: item[props.rowKey],
};
});
selectValues.value = data.map((item) => item[props.rowKey]);
// 更新value
emit('update:value', selectValues.value);
// 触发change事件不转是因为basicForm提交时会自动将字符串转化为数组
emit('change', selectValues.value,selectOptions.value);
}
const handleSelectChange = (data) => {
selectOptions.value = selectOptions.value.filter((item) => data.includes(item[props.rowKey]));
setValue(selectOptions.value);
};
// 设置下拉框的值
const setValue = (data) => {
selectOptions.value = data.map((item) => {
return {
...item,
label: item[props.labelKey],
value: item[props.rowKey],
};
});
selectValues.value = data.map((item) => item[props.rowKey]);
// 更新value
emit('update:value', selectValues.value);
// 触发change事件不转是因为basicForm提交时会自动将字符串转化为数组
emit('change', selectValues.value,selectOptions.value);
};
// 翻译
const transform = () => {
let value = props.value;
let len;
if (isArray(value) || isString(value)) {
if (isArray(value)) {
len = value.length;
value = value.join(',');
} else {
len = value.split(',').length;
}
value = value.trim();
if (value) {
// 如果value的值在selectedUser中存在则不请求翻译
let isNotRequestTransform = false;
isNotRequestTransform = value.split(',').every((value) => !!selectOptions.value.find((item) => item[props.rowKey] === value));
if (isNotRequestTransform) {
selectValues.value = value.split(',');
return;
}
const params = { isMultiTranslate: true, pageSize: len, [props.rowKey]: value };
if (isObject(attrs.params)) {
Object.assign(params, attrs.params);
}
console.log('JTabsSelectUser params', params);
getTableListOrigin(params).then((result: any) => {
const records = result.records ?? [];
selectValues.value = records.map((item) => item[props.rowKey]);
selectOptions.value = records.map((item) => {
return {
...item,
label: item[props.labelKey],
value: item[props.rowKey],
};
});
});
}
} else {
selectValues.value = [];
}
};
// 监听value变化
watch(
() => props.value,
() => {
transform();
},
{ deep: true, immediate: true }
);
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240605---for【TV360X-1050】Safari浏览器指定下一步处理人页面控件没对齐
.ant-select {
vertical-align: middle;
}
// update-end--author:liaozhiyang---date:20240605---for【TV360X-1050】Safari浏览器指定下一步处理人页面控件没对齐
// 自定义按钮样式
.custom-btn {
height: 28px !important;
padding: 0 12px !important;
font-size: 13px !important;
}
</style>

View File

@@ -0,0 +1,101 @@
import { defHttp } from '/@/utils/http/axios';
import { BasicColumn, FormSchema } from '/@/components/Table';
export enum Api {
departList = '/sys/sysDepart/queryDepartTreeSync',
userList = '/sys/user/list',
departUserList = '/sys/user/queryUserByDepId',
//获取所有岗位职级信息
queryALLRankRelation = '/sys/sysDepart/getALLRankRelation',
//根据关键字搜索部门
searchBy = '/sys/sysDepart/searchBy'
}
/**
* 获取部门树列表
*/
export const getDepartTreeData = (params?) => defHttp.get({ url: Api.departList, params });
/**
* 获取用户列表
*/
export const getUserList = (params?) => defHttp.get({ url: Api.userList, params });
/**
* 获取指定部门用户列表
*/
export const getDepartUserList = (params?) => defHttp.get({ url: Api.departUserList, params });
/**
* 获取职级信息
*/
export const queryALLRankRelation = (params?) => defHttp.get({ url: Api.queryALLRankRelation, params,timeout: 2 * 60 * 1000 });
/**
* 根据关键字搜索部门
*/
export const searchByKeywords = (params) => defHttp.get({ url: Api.searchBy, params });
/**
* 用户列表
*/
export const columns: BasicColumn[] = [
// {
// title: '用户账号',
// align: 'center',
// dataIndex: 'username',
// ellipsis: true,
// width: 130,
// },
{
title: '用户姓名',
align: 'center',
width: 150,
dataIndex: 'realname',
ellipsis: true,
},
{
title: '部门',
align: 'center',
width: 150,
dataIndex: 'orgCodeTxt',
},
];
/**
* 选中用户列表
*/
export const selectedUserColumns: BasicColumn[] = [
{
title: '用户姓名',
align: 'center',
width: 150,
dataIndex: 'realname',
ellipsis: true,
},
];
/**
* 查询条件
*/
export const searchFormSchema: FormSchema[] = [
{
label: '用户姓名',
field: 'realname',
component: 'Input',
componentProps: {
style: {
width: '150px',
},
},
},
// {
// label: '用户账号',
// field: 'username',
// component: 'JInput',
// componentProps: {
// style: {
// width: '150px',
// },
// },
// },
];

View File

@@ -1,4 +1,21 @@
export { default as JVxeTable } from './src/JVxeTable';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
// update-begin--author:liaozhiyang---date:20260318---for:【QQYUN-14948】需要兼容vxetable引入到了页面也不报错
// 兼容页面直接引入:通过异步组件包装,确保 vxe-table 注册后再渲染,避免页面卡死
export const JVxeTable = createAsyncComponent(
async () => {
const app = window['JAppRootInstance'];
if (app && !app._context.components['VxeTable']) {
const { registerJVxeTable } = await import('./src/install');
await registerJVxeTable(app);
const { registerJVxeCustom } = await import('/@/components/JVxeCustom');
await registerJVxeCustom();
}
const m = await import('./src/JVxeTable');
return m.default;
},
{ loading: true }
);
// update-end--author:liaozhiyang---date:20260318---for:【QQYUN-14948】需要兼容vxetable引入到了页面也不报错
export { registerJVxeTable } from './src/install';
export { deleteComponent } from './src/componentMap';
export { registerComponent, registerAsyncComponent, registerASyncComponentReal } from './src/utils/registerUtils';

View File

@@ -1,6 +1,6 @@
import { defineComponent, h, nextTick, ref, useSlots } from 'vue';
import { defineComponent, h, ref, useSlots, computed, resolveComponent } from 'vue';
import { vxeEmits, vxeProps } from './vxe.data';
import { useData, useRefs, useResolveComponent as rc } from './hooks/useData';
import { useData, useRefs } from './hooks/useData';
import { useColumns } from './hooks/useColumns';
import { useColumnsCache } from './hooks/useColumnsCache';
import { useMethods } from './hooks/useMethods';
@@ -9,6 +9,7 @@ import { useDragSort } from './hooks/useDragSort';
import { useRenderComponents } from './hooks/useRenderComponents';
import { useFinallyProps } from './hooks/useFinallyProps';
import { JVxeTableProps } from './types';
import { Spin } from 'ant-design-vue';
import './style/index.less';
export default defineComponent({
@@ -33,6 +34,16 @@ export default defineComponent({
const finallyProps = useFinallyProps(props, data, methods);
// 渲染子组件
const renderComponents = useRenderComponents(props, data, methods, slots);
// update-begin--author:liaozhiyang---date:20260316---for:【QQYUN-13751】jVxetable优化
// 在 setup 阶段缓存组件引用,避免每次 render 调用 resolveComponent 查找
const aSpinComp = Spin;
const vxeGridComp = resolveComponent('vxe-grid');
// 将 vxeProps 和 data 合并为一个 computed避免 render 每次 spread 生成新对象
const vxeGridProps = computed(() => ({
...finallyProps.vxeProps.value,
data: data.vxeDataSource.value,
}));
// update-end--author:liaozhiyang---date:20260316---for:【QQYUN-13751】jVxetable优化
return {
instanceRef,
...refs,
@@ -40,6 +51,9 @@ export default defineComponent({
...finallyProps,
...renderComponents,
vxeDataSource: data.vxeDataSource,
aSpinComp,
vxeGridComp,
vxeGridProps,
};
},
render() {
@@ -50,7 +64,7 @@ export default defineComponent({
style: this.$attrs.style,
},
h(
rc('a-spin'),
this.aSpinComp,
{
spinning: this.loading,
wrapperClassName: this.prefixCls,
@@ -61,11 +75,8 @@ export default defineComponent({
this.renderToolbar(),
this.renderToolbarAfterSlot(),
h(
rc('vxe-grid'),
{
...this.vxeProps,
data: this.vxeDataSource,
},
this.vxeGridComp,
this.vxeGridProps,
this.$slots
),
this.renderPagination(),

View File

@@ -1,5 +1,6 @@
import type { JVxeVueComponent } from './types';
import { JVxeTypes } from './types/JVxeTypes';
import { componentMap } from './componentMapStore';
import JVxeSlotCell from './components/cells/JVxeSlotCell';
import JVxeNormalCell from './components/cells/JVxeNormalCell.vue';
@@ -17,13 +18,11 @@ import JVxeProgressCell from './components/cells/JVxeProgressCell.vue';
import JVxeTextareaCell from './components/cells/JVxeTextareaCell.vue';
// import JVxeDepartSelectCell from './components/cells/JVxeDepartSelectCell.vue'
// import JVxeUserSelectCell from './components/cells/JVxeUserSelectCell.vue'
import JVxeTreeSelectCell from './components/cells/JVxeTreeSelectCell.vue';
import JVxeCategorySelectCell from './components/cells/JVxeCategorySelectCell.vue';
let componentMap = new Map<JVxeTypes | string, JVxeVueComponent>();
// 代码逻辑说明: 【issues/860】生成的一对多代码热更新之后点击新增卡死[暂时先解决]
const JVxeComponents = 'JVxeComponents__';
if (import.meta.env.DEV && componentMap.size === 0 && window[JVxeComponents] && window[JVxeComponents].size > 0) {
componentMap = window[JVxeComponents];
}
/** span 组件结尾 */
export const spanEnds: string = ':span';
@@ -89,16 +88,10 @@ export function definedComponent() {
// addComponent(JVxeTypes.departSelect, JVxeDepartSelectCell)
// addComponent(JVxeTypes.userSelect, JVxeUserSelectCell)
// update-begin--author:liaozhiyang---date:20260413---for:【issues/7633】online子表支持分类字典树自定义树
addComponent(JVxeTypes.treeSelect, JVxeTreeSelectCell);
addComponent(JVxeTypes.catTreeSelect, JVxeCategorySelectCell);
// update-end--author:liaozhiyang---date:20260413---for:【issues/7633】online子表支持分类字典树自定义树
}
/**
* 清空注册的组件
*/
export function clearComponent() {
componentMap.clear();
// 代码逻辑说明: 【issues/860】生成的一对多代码热更新之后点击新增卡死[暂时先解决]
import.meta.env.DEV && (window[JVxeComponents] = componentMap);
}
export { componentMap };
export { componentMap, clearComponent } from './componentMapStore';

View File

@@ -0,0 +1,20 @@
import type { JVxeVueComponent } from './types';
import { JVxeTypes } from './types/JVxeTypes';
/** 仅存 componentMap 与 clearComponent供 qiankun unmount 等场景使用,不引用任何 Cell 组件 */
let componentMap = new Map<JVxeTypes | string, JVxeVueComponent>();
const JVxeComponents = 'JVxeComponents__';
if (import.meta.env.DEV && componentMap.size === 0 && window[JVxeComponents] && window[JVxeComponents].size > 0) {
componentMap = window[JVxeComponents];
}
export { componentMap };
/**
* 清空注册的组件(乾坤子应用 unmount 时调用,仅引用本文件避免加载所有 Cell
*/
export function clearComponent() {
componentMap.clear();
// 代码逻辑说明: 【issues/860】生成的一对多代码热更新之后点击新增卡死[暂时先解决]
import.meta.env.DEV && (window[JVxeComponents] = componentMap);
}

View File

@@ -29,7 +29,7 @@ export default defineComponent({
effectList.value.push(topLayer);
}
},
{ deep: true, immediate: true }
{ immediate: true }
);
// 条件渲染内容 span

View File

@@ -0,0 +1,190 @@
<template>
<a-tree-select
allow-clear
label-in-value
show-checked-strategy="SHOW_ALL"
style="width: 100%"
:get-popup-container="(node) => node?.parentNode"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="cellAttrs.placeholder || '请选择'"
:disabled="cellAttrs.disabled"
:multiple="isMultiple"
:load-data="asyncLoadTreeData"
:value="treeValue"
:tree-data="treeData"
@change="onChange"
/>
</template>
<script lang="ts">
/**
* 列配置须包含pcode分类根编码优先或 pid分类根 ID
* 支持多选:列配置加 multiple: true存储值为逗号分隔字符串。
*/
import { ref, computed, watch, defineComponent } from 'vue';
import { loadDictItem, loadTreeData } from '/@/api/common/api';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
// 模块级标签缓存key 为分类节点 id在选中和回显时写入translate.handler 优先读缓存
const _catLabelCache = new Map<string, string>();
function _cacheCatLabels(ids: string[], labels: string[]) {
ids.forEach((id, i) => _catLabelCache.set(id, String(labels[i])));
}
export default defineComponent({
name: 'JVxeCategorySelectCell',
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { innerValue, cellProps, originColumn, handleChangeCommon } = useJVxeComponent(props);
// cellProps 内部以 {} 声明,无具体属性类型;统一转换后供模板直接使用,避免模板内 `as` 断言
const cellAttrs = computed(() => cellProps.value as Record<string, any>);
// 树形节点列表
const treeData = ref<any[]>([]);
// 当前选中的带 label 的值labelInValue 格式,多选为数组)
const treeValue = ref<any>(null);
// 是否多选(由列配置 multiple 控制)
const isMultiple = computed(() => !!originColumn.value?.multiple);
/** 从列配置中解析分类树参数,优先使用 pcode */
const catConfig = computed(() => {
const col = originColumn.value;
return {
pcode: col?.pcode || '',
pid: col?.pid || col?.pidValue || '',
condition: col?.condition || '',
};
});
// 外部值变化时回显 label
watch(() => innerValue.value, (val) => loadItemByCode(val), { immediate: true });
// 分类配置变化时重新加载根节点
watch(() => catConfig.value.pcode, () => loadRoot(), { immediate: true });
/** 加载分类根节点列表 */
async function loadRoot() {
const { pcode, pid, condition } = catConfig.value;
const param = pcode ? { pcode, condition } : { pid: pid || '0', pcode: '0', condition };
const res = await loadTreeData(param);
if (res?.length) {
treeData.value = res.map((item) => ({
...item,
value: item.key,
isLeaf: item.leaf === true,
}));
}
}
/** 根据存储值(逗号分隔 ID查询对应的 label用于回显 */
async function loadItemByCode(val) {
if (!val) {
treeValue.value = isMultiple.value ? [] : { label: null, value: null };
return;
}
const ids = val.split(',');
// 若缓存已全部命中,直接用缓存构建 treeValue跳过 API 请求
const cachedLabels = ids.map((id) => _catLabelCache.get(id));
if (cachedLabels.every((l) => l !== undefined)) {
treeValue.value = isMultiple.value
? ids.map((id, i) => ({ key: id, value: id, label: cachedLabels[i] }))
: { key: val, value: val, label: cachedLabels[0] };
return;
}
const res = await loadDictItem({ ids: val });
if (isMultiple.value) {
// 多选:拆分 ID逐一映射为 {key, value, label}
treeValue.value = ids.map((id, index) => ({
key: id,
value: id,
label: res?.[index] ?? id,
}));
if (res?.length) _cacheCatLabels(ids, res);
} else {
const label = res?.length ? res[0] : val;
treeValue.value = { key: val, value: val, label };
if (res?.length) _cacheCatLabels(ids, res);
}
}
/** 异步懒加载子节点 */
async function asyncLoadTreeData(treeNode) {
if (treeNode.dataRef.children) return Promise.resolve();
const { condition } = catConfig.value;
const pid = treeNode.dataRef.key;
const res = await loadTreeData({ pid, condition });
if (res?.length) {
const children = res.map((item) => ({
...item,
value: item.key,
isLeaf: item.leaf === true,
}));
addChildren(pid, children, treeData.value);
treeData.value = [...treeData.value];
}
return Promise.resolve();
}
/** 递归将子节点插入到对应父节点下 */
function addChildren(pid, children, arr) {
if (!arr?.length) return;
for (const item of arr) {
if (item.key === pid) {
item.children = children.length ? children : undefined;
if (!children.length) item.isLeaf = true;
break;
}
addChildren(pid, children, item.children);
}
}
/** 选中或清空事件 */
function onChange(value) {
if (!value || (Array.isArray(value) && value.length === 0)) {
// 清空
treeValue.value = isMultiple.value ? [] : null;
handleChangeCommon('');
} else if (Array.isArray(value)) {
// 多选:数组 → 逗号分隔字符串;同时缓存 id->label 供非编辑模式直接使用
treeValue.value = value;
_cacheCatLabels(value.map((item) => item.value), value.map((item) => item.label));
handleChangeCommon(value.map((item) => item.value).join(','));
} else {
// 单选
treeValue.value = value;
if (value.value != null) _cacheCatLabels([value.value], [value.label]);
handleChangeCommon(value.value);
}
}
return { cellAttrs, treeData, treeValue, isMultiple, asyncLoadTreeData, onChange };
},
// 【组件增强】非编辑模式:优先读缓存,避免重复 API 请求
enhanced: {
switches: { editRender: true, visible: false },
translate: {
enabled: true,
async handler(value) {
if (!value) return '';
const ids = String(value).split(',');
const cachedLabels = ids.map((id) => _catLabelCache.get(id));
if (cachedLabels.every((l) => l !== undefined)) {
return cachedLabels.join(',');
}
try {
const res = await loadDictItem({ ids: value });
if (res?.length) {
_cacheCatLabels(ids, res);
return res.join(',');
}
} catch {}
return value;
},
},
} as JVxeComponent.EnhancedPartial,
});
</script>

View File

@@ -0,0 +1,244 @@
<template>
<a-tree-select
v-if="show"
allow-clear
label-in-value
show-checked-strategy="SHOW_ALL"
style="width: 100%"
:get-popup-container="(node) => node?.parentNode"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="cellAttrs.placeholder || '请选择'"
:disabled="cellAttrs.disabled"
:multiple="isMultiple"
:load-data="asyncLoadTreeData"
:value="treeValue"
:tree-data="treeData"
@change="onChange"
/>
</template>
<script lang="ts">
/**
* 列配置须包含dict"表名,文本字段,存储字段"、pidField父级字段名
* pidValue根节点值可选、hasChildField是否有子节点字段可选
* 支持多选:列配置加 multiple: true存储值为逗号分隔字符串。
*/
import { ref, computed, watch, defineComponent } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
const API_LOAD_TREE = '/sys/dict/loadTreeData';
const API_LOAD_ITEM = '/sys/dict/loadDictItem/';
// 模块级标签缓存key 为 "tableName:id"在选中和回显时写入translate.handler 优先读缓存
const _labelCache = new Map<string, string>();
function _cacheLabels(tableName: string, ids: string[], labels: string[]) {
ids.forEach((id, i) => _labelCache.set(`${tableName}:${id}`, String(labels[i])));
}
export default defineComponent({
name: 'JVxeTreeSelectCell',
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { innerValue, cellProps, originColumn, handleChangeCommon } = useJVxeComponent(props);
// cellProps 内部以 {} 声明,无具体属性类型;统一转换后供模板直接使用,避免模板内 `as` 断言
const cellAttrs = computed(() => cellProps.value as Record<string, any>);
// 树形节点列表
const treeData = ref<any[]>([]);
// 当前选中的带 label 的值labelInValue 格式,多选为数组)
const treeValue = ref<any>(null);
// 用于重置组件dict 变更时重新挂载)
const show = ref(true);
// 是否多选(由列配置 multiple 控制)
const isMultiple = computed(() => !!originColumn.value?.multiple);
/**
* 从列配置中解析树相关参数
* dict 格式为 "tableName,textField,codeField"
*/
const treeConfig = computed(() => {
const col = originColumn.value;
const dictArr = (col?.dict || '').split(',');
return {
tableName: dictArr[0] || '',
text: dictArr[1] || '',
code: dictArr[2] || '',
pidField: col?.pidField || 'pid',
pidValue: col?.pidValue || '',
hasChildField: col?.hasChildField || '',
condition: col?.condition || '',
};
});
// 外部值变化时回显 label
watch(
() => innerValue.value,
(val) => loadItemByCode(val),
{ immediate: true }
);
// dict 配置变化时重新加载根节点
watch(
() => treeConfig.value.tableName,
async (tableName) => {
if (tableName) {
treeData.value = [];
show.value = false;
await loadRoot();
show.value = true;
}
},
{ immediate: true }
);
/** 加载根节点列表 */
async function loadRoot() {
const { tableName, text, code, pidField, pidValue, hasChildField, condition } = treeConfig.value;
if (!tableName) return;
const res = await defHttp.get(
{ url: API_LOAD_TREE, params: { tableName, text, code, pid: pidValue, pidField, hasChildField, condition } },
{ isTransformResponse: false }
);
if (res.success && res.result) {
treeData.value = res.result.map((item) => ({
...item,
value: item.key,
isLeaf: !!item.leaf,
}));
}
}
/** 根据存储值(逗号分隔 ID查询对应的 label用于回显 */
async function loadItemByCode(val) {
if (!val) {
treeValue.value = isMultiple.value ? [] : { label: null, value: null };
return;
}
const { tableName, text, code } = treeConfig.value;
if (!tableName) {
treeValue.value = isMultiple.value ? val.split(',').map((id) => ({ key: id, value: id, label: id })) : { label: val, value: val };
return;
}
const ids = val.split(',');
// 若缓存已全部命中,直接用缓存构建 treeValue跳过 API 请求
const cachedLabels = ids.map((id) => _labelCache.get(`${tableName}:${id}`));
if (cachedLabels.every((l) => l !== undefined)) {
treeValue.value = isMultiple.value
? ids.map((id, i) => ({ key: id, value: id, label: cachedLabels[i] }))
: { key: val, value: val, label: cachedLabels[0] };
return;
}
const res = await defHttp.get({ url: `${API_LOAD_ITEM}${tableName},${text},${code}`, params: { key: val } }, { isTransformResponse: false });
if (isMultiple.value) {
// 多选:拆分 ID逐一映射为 {key, value, label}
treeValue.value = ids.map((id, index) => ({
key: id,
value: id,
label: res.success ? (res.result?.[index] ?? id) : id,
}));
if (res.success && res.result?.length) _cacheLabels(tableName, ids, res.result);
} else {
const label = res.success && res.result?.length ? res.result[0] : val;
treeValue.value = { key: val, value: val, label };
if (res.success && res.result?.length) _cacheLabels(tableName, ids, res.result);
}
}
/** 异步懒加载子节点 */
async function asyncLoadTreeData(treeNode) {
if (treeNode.dataRef.children) return Promise.resolve();
const { tableName, text, code, pidField, hasChildField, condition } = treeConfig.value;
const pid = treeNode.dataRef.key;
const res = await defHttp.get(
{ url: API_LOAD_TREE, params: { tableName, text, code, pid, pidField, hasChildField, condition } },
{ isTransformResponse: false }
);
if (res.success) {
const children = res.result.map((item) => ({
...item,
value: item.key,
isLeaf: !!item.leaf,
}));
addChildren(pid, children, treeData.value);
treeData.value = [...treeData.value];
}
return Promise.resolve();
}
/** 递归将子节点插入到对应父节点下 */
function addChildren(pid, children, arr) {
if (!arr?.length) return;
for (const item of arr) {
if (item.key === pid) {
item.children = children.length ? children : undefined;
if (!children.length) item.isLeaf = true;
break;
}
addChildren(pid, children, item.children);
}
}
/** 选中或清空事件 */
function onChange(value) {
const { tableName } = treeConfig.value;
if (!value || (Array.isArray(value) && value.length === 0)) {
// 清空
treeValue.value = isMultiple.value ? [] : null;
handleChangeCommon('');
} else if (Array.isArray(value)) {
// 多选:数组 → 逗号分隔字符串;同时缓存 id->label 供非编辑模式直接使用
treeValue.value = value;
if (tableName)
_cacheLabels(
tableName,
value.map((item) => item.value),
value.map((item) => item.label)
);
handleChangeCommon(value.map((item) => item.value).join(','));
} else {
// 单选
treeValue.value = value;
if (tableName && value.value != null) _cacheLabels(tableName, [value.value], [value.label]);
handleChangeCommon(value.value);
}
}
return { cellAttrs, treeData, treeValue, show, isMultiple, asyncLoadTreeData, onChange };
},
// 【组件增强】非编辑模式:优先读缓存,避免重复 API 请求
enhanced: {
switches: { editRender: true, visible: false },
translate: {
enabled: true,
async handler(value, ctx) {
if (!value) return '';
const col = ctx?.context.originColumn.value;
const dictArr = (col?.dict || '').split(',');
const tableName = dictArr[0];
if (!tableName) return value;
const ids = String(value).split(',');
const cachedLabels = ids.map((id) => _labelCache.get(`${tableName}:${id}`));
if (cachedLabels.every((l) => l !== undefined)) {
return cachedLabels.join(',');
}
try {
const res = await defHttp.get(
{ url: `${API_LOAD_ITEM}${tableName},${dictArr[1] || ''},${dictArr[2] || ''}`, params: { key: value } },
{ isTransformResponse: false }
);
if (res.success && res.result?.length) {
_cacheLabels(tableName, ids, res.result);
return res.result.join(',');
}
} catch {}
return value;
},
},
} as JVxeComponent.EnhancedPartial,
});
</script>

View File

@@ -324,6 +324,11 @@ function handlerCol(args: HandleArgs) {
Object.assign(col.cellRender, args.renderOptions);
}
// slot 类型列:通过 titlePrefix 在表头显示编辑图标(不能用 editRender否则非编辑状态下 cellRender 会被绕过)
if (col.params.type === JVxeTypes.slot && col.cellRender && !col.editRender) {
col.titlePrefix = { icon: 'vxe-table-icon-edit' };
}
columns.push(col);
}

View File

@@ -60,7 +60,12 @@ export function useData(props: JVxeTableProps): JVxeDataProps {
reserve: true,
highlight: true,
},
mouseConfig: { selected: false },
// update-begin--author:liaozhiyang---date:20260316---for:【QQYUN-13751】jVxetable优化
// 禁止点击表格外部时清空校验状态,避免每次点击空白区域触发 reactData.validErrorMaps={} 导致不必要的重渲染
validConfig: {
autoClear: false,
},
// update-end--author:liaozhiyang---date:20260316---for:【QQYUN-13751】jVxetable优化
keyboardConfig: {
// 删除键功能
isDel: false,

View File

@@ -1,13 +1,14 @@
import { nextTick, watch } from 'vue';
import { JVxeDataProps, JVxeRefs, JVxeTableMethods } from '../types';
import { cloneDeep } from 'lodash-es';
export function useDataSource(props, data: JVxeDataProps, methods: JVxeTableMethods, refs: JVxeRefs) {
watch(
() => props.dataSource,
async () => {
data.disabledRowIds = [];
data.vxeDataSource.value = cloneDeep(props.dataSource);
// update-begin--author:liaozhiyang---date:20260316---for:【QQYUN-13751】jVxetable优化
data.vxeDataSource.value = props.dataSource.map(row => ({ ...row }));
// update-end--author:liaozhiyang---date:20260316---for:【QQYUN-13751】jVxetable优化
data.vxeDataSource.value.forEach((row, rowIndex) => {
// 判断是否是禁用行
if (methods.isDisabledRow(row, rowIndex)) {

View File

@@ -1,4 +1,4 @@
import { onMounted, onUnmounted, nextTick } from 'vue';
import { onMounted, onUnmounted, nextTick, watch } from 'vue';
import { JVxeTableMethods, JVxeTableProps } from '/@/components/jeecg/JVxeTable/src/types';
import Sortable from 'sortablejs';
import { isEnabledVirtualYScroll } from '/@/components/jeecg/JVxeTable/utils';
@@ -19,93 +19,121 @@ export function useDragSort(props: JVxeTableProps, methods: JVxeTableMethods) {
sortable2.destroy();
}
});
// update-begin--author:liaozhiyang---date:20260415---for:【QQYUN-15134】修复jvxetable使用fixed固定后无法拖拽
// 代码逻辑说明:监听 maxHeight 变化(弹窗全屏↔缩小切换时触发)。
// 解决maxHeight 变化时销毁旧 Sortable 并重新初始化,让其绑定到正确的 tbody。
watch(
() => props.maxHeight,
() => {
if (sortable2) {
sortable2.destroy();
sortable2 = null as any;
}
clearTimeout(initTime);
initTime = setTimeout(createSortable, 300);
}
);
function createSortable() {
let xTable = methods.getXTable();
// let dom = xTable.$el.querySelector('.vxe-table--fixed-wrapper .vxe-table--body tbody')
// let dom = xTable.$el.querySelector('.body--wrapper>.vxe-table--body tbody');
let dom = xTable.$el.querySelector('.vxe-table--body-inner-wrapper > .vxe-table--body tbody');
// 代码逻辑说明:拖拽排序列默认 fixed:left此时 drag-btn 在固定列 wrapper 的 tbody 内。
// 若 dragSortFixed!='none',优先绑定固定列 wrapper 的 tbody确保 Sortable 能捕获拖拽事件;
// 若 dragSortFixed='none'(拖拽列不固定),则绑定主 tbody。
const domFixed =
props.dragSortFixed !== 'none'
? xTable.$el.querySelector('.vxe-table--fixed-left-wrapper .vxe-table--body tbody')
: null;
const domMain = xTable.$el.querySelector('.vxe-table--body-inner-wrapper > .vxe-table--body tbody');
const dom = domFixed || domMain;
if (!dom) {
console.warn('[JVxeTable] 拖拽排序初始化失败可能是vxe-table升级导致的版本不兼容。');
return;
}
let startChildren = [];
// 拖拽过程中悬停目标行的 DOM 索引(由 onMove 实时更新)
let hoverIndex = -1;
// 拖拽起始行的 DOM 索引(用于 onMove 中判断是否悬停自身)
let dragStartIndex = -1;
/**
* 为所有 tbody 中第 idx 行0-based添加或移除 CSS class。
* 用于跨主体 + 固定列 wrapper 同步视觉状态,避免只改一侧 tbody 导致样式不一致。
*/
function setRowClass(idx: number, cls: string, add: boolean) {
xTable.$el.querySelectorAll(`.vxe-table--body tbody tr:nth-child(${idx + 1})`).forEach((tr) => {
(tr as HTMLElement).classList[add ? 'add' : 'remove'](cls);
});
}
/** 清除所有带有指定 class 的行 */
function clearRowClass(cls: string) {
xTable.$el.querySelectorAll(`.${cls}`).forEach((tr) => {
(tr as HTMLElement).classList.remove(cls);
});
}
sortable2 = Sortable.create(dom as HTMLElement, {
handle: '.drag-btn',
// 代码逻辑说明: 【QQYUN-8785】online表单列位置的id未做限制拖动其他列到id列上面同步数据库时报错
filter: '.not-allow-drag',
draggable: ".allow-drag",
draggable: '.allow-drag',
direction: 'vertical',
animation: 300,
animation: 0,
onStart(e) {
let from = e.from;
// @ts-ignore
startChildren = [...from.children];
// 初始化悬停索引为起始位置,并为被拖起的行添加禁用效果
hoverIndex = e.oldIndex!;
dragStartIndex = e.oldIndex!;
setRowClass(e.oldIndex!, 'j-vxe-drag-source', true);
},
onMove(e) {
// 代码逻辑说明:拖拽期间只记录悬停目标的 DOM 索引,阻止 Sortable 实时交换行位置。
// 好处:所有 tbody主体 + 固定列 wrapper在松手之前均保持原顺序不会产生视觉错位
// 松开鼠标后统一由 vxe-table 数据驱动重渲染fixed 列拖拽排序问题彻底解决。
const idx = Array.from((e.from as HTMLElement).children).indexOf(e.related as HTMLElement);
if (idx !== -1) {
hoverIndex = idx;
}
// 更新悬停行指示线:先清除旧状态,悬停自身时不显示
clearRowClass('j-vxe-drag-hover-top');
clearRowClass('j-vxe-drag-hover-bottom');
if (hoverIndex !== dragStartIndex) {
// 向下拖(插入悬停行下方)→ 底部线;向上拖(插入悬停行上方)→ 顶部线
const cls = hoverIndex > dragStartIndex ? 'j-vxe-drag-hover-bottom' : 'j-vxe-drag-hover-top';
setRowClass(hoverIndex, cls, true);
}
return false; // 阻止 Sortable 移动 DOM 行
},
onEnd(e: any) {
// 拖拽结束,清除所有视觉状态
clearRowClass('j-vxe-drag-source');
clearRowClass('j-vxe-drag-hover-top');
clearRowClass('j-vxe-drag-hover-bottom');
// -update-begin--author:liaozhiyang---date:20240619---for【TV360X-585】拖动字段虚拟滚动不好使
const isRealEnabledVirtual = isEnabledVirtualYScroll(props, xTable);
let newIndex;
let oldIndex;
// 滚动排序需要区分当前行编辑是否启动了虚拟滚动(底层loadData方法对是否真实开启了虚拟滚动处理不一样导致需要区分)
let newIndex: number;
let oldIndex: number;
if (isRealEnabledVirtual) {
// e.clone的元素才是真实拖动的元素(虚拟滚动也不会变)
const dragNode = e.clone;
// 虚拟滚动onMove 返回 false 后 DOM 行未动e.item 就是被拖拽行本身
const dragNode = e.item as HTMLElement;
const dragRowInfo = xTable.getRowNode(dragNode);
// e.item的元素只有没虚拟滚动时才是拖动的元素(如果虚拟滚动了则会变)
const itemNode = e.item;
const itemRowInfo = xTable.getRowNode(itemNode);
// e.newIndex是当前可视区内元素的索引(不是数据实际的索引)、e.oldIndex 是拖动时可视区内元素的索引(不是数据实际的索引)
if (dragRowInfo!.rowid === itemRowInfo!.rowid) {
// e.clone和e.item相同说明拖拽的元素在DOM中没被虚拟滚动给remove掉。
if (e.newIndex === e.oldIndex) {
// 此时新旧index一样就可认为没拖动
return;
}
} else {
}
// 此时真实DOM元素顺序已排(通过拖拽元素的前后元素确定拖拽元素在真实数据中是往前还是往后拖)
oldIndex = dragRowInfo!.index;
const len = e.from.childNodes.length;
let referenceIndex;
let referenceNode;
if (e.newIndex + 1 < len) {
// 拖拽DOM交换之后后面还有元素参考物是后面的元素
referenceNode = e.from.childNodes[e.newIndex + 1];
referenceIndex = xTable.getRowNode(referenceNode)!.index;
if (oldIndex > referenceIndex) {
newIndex = referenceIndex;
} else {
newIndex = referenceIndex - 1;
}
} else {
// 拖拽DOM交换之后后面没有元素了参考物是前面的元素
referenceNode = e.from.childNodes[e.newIndex - 1];
referenceIndex = xTable.getRowNode(referenceNode)!.index;
newIndex = referenceIndex;
}
if (!dragRowInfo) return;
oldIndex = dragRowInfo.index;
if (hoverIndex === e.oldIndex) return;
// 通过 hoverIndex 对应的可视区 DOM 节点获取实际数据索引
const hoverNode = (e.from as HTMLElement).childNodes[hoverIndex] as HTMLElement;
if (!hoverNode) return;
const hoverRowInfo = xTable.getRowNode(hoverNode);
if (!hoverRowInfo) return;
newIndex = hoverRowInfo.index;
} else {
// 非虚拟滚动DOM 行从未移动,直接使用 hoverIndex 作为目标索引,无需还原 DOM
oldIndex = e.oldIndex;
newIndex = e.newIndex;
if (oldIndex === newIndex) {
return;
}
const from = e.from;
const element = startChildren[oldIndex];
let target = null;
if (oldIndex > newIndex) {
// 向上移动
if (oldIndex + 1 < startChildren.length) {
target = startChildren[oldIndex + 1];
}
} else {
// 向下移动
target = startChildren[oldIndex + 1];
}
from.removeChild(element);
from.insertBefore(element, target);
newIndex = hoverIndex;
if (oldIndex === newIndex) return;
}
// -update-end--author:liaozhiyang---date:20240620---for【TV360X-585】拖动字段虚拟滚动不好使
// -update-end--author:liaozhiyang---date:20240619---for【TV360X-585】拖动字段虚拟滚动不好使
nextTick(() => {
methods.doSort(oldIndex, newIndex);
methods.trigger('dragged', { oldIndex: oldIndex, newIndex: newIndex });
@@ -113,5 +141,6 @@ export function useDragSort(props: JVxeTableProps, methods: JVxeTableMethods) {
},
});
}
// update-begin--author:liaozhiyang---date:20260415---for:【QQYUN-15134】修复jvxetable使用fixed固定后无法拖拽
}
}

View File

@@ -111,11 +111,8 @@ export function useFinallyProps(props: JVxeTableProps, data: JVxeDataProps, meth
);
});
// 代码逻辑说明: 【issues/8593】修复列改变后内容不刷新
const vxeColumnsRef = ref(data.vxeColumns!.value || [])
const watchColumnsDebounce = debounce(async () => {
vxeColumnsRef.value = []
await nextTick()
vxeColumnsRef.value = data.vxeColumns!.value
}, 50)
watch(data.vxeColumns!, watchColumnsDebounce)

View File

@@ -142,8 +142,12 @@ export function useJVxeComponent(props: JVxeComponent.Props) {
(newValue) => {
// -update-begin--author:liaozhiyang---date:20241210---for【issues/7497】隐藏某一列后字典没翻译恢复后正常
// TODO 先这样修复解决问题,根因后期再看看
enhanced = getEnhanced(props.type);
// enhanced = getEnhanced(props.type);
// -update-end--author:liaozhiyang---date:20241210---for【issues/7497】隐藏某一列后字典没翻译恢复后
// 解决online中对同一条数据点击编辑多次他表字段变成空格的问题
if (props.type === 'input' && originColumn.value.flag === 'link-table-field' && (newValue === undefined || newValue === null)) {
return;
}
// 验证值格式
let getValue = enhanced.getValue(newValue, ctx);
if (newValue !== getValue) {

View File

@@ -550,7 +550,9 @@ export function useMethods(props: JVxeTableProps, { emit }, data: JVxeDataProps,
// true = 删除新增false=只删除id
let removeNewLine = optOrRm?.removeNewLine ?? true;
for (let row of rows) {
let item = cloneDeep(row);
// update-begin--author:liaozhiyang---date:20260316---for:【QQYUN-13751】jVxetable优化
let item = { ...row };
// update-end--author:liaozhiyang---date:20260316---for:【QQYUN-13751】jVxetable优化
if (insertRecords.includes(row)) {
handler ? handler({ item, row, insertRecords }) : null;
if (removeNewLine) {
@@ -870,8 +872,9 @@ export function useMethods(props: JVxeTableProps, { emit }, data: JVxeDataProps,
}
let records: Recordable[] = [];
for (let row of rows) {
let item = cloneDeep(row);
records.push(item);
// update-begin--author:liaozhiyang---date:20260316---for:【QQYUN-13751】jVxetable优化
records.push({ ...row });
// update-end--author:liaozhiyang---date:20260316---for:【QQYUN-13751】jVxetable优化
}
return records;
}

View File

@@ -7,12 +7,16 @@ import VXETablePluginAntd from 'vxe-table-plugin-antd';
import 'vxe-pc-ui/lib/style.css';
import 'vxe-table/lib/style.css';
import JVxeTable from './JVxeTable';
import { getEventPath } from '/@/utils/common/compUtils';
import { registerAllComponent } from './utils/registerUtils';
import { getEnhanced } from './utils/enhancedUtils';
import type { JVxeTypes } from './types/JVxeTypes';
export interface RegisterJVxeTableOptions {
/** 仅注册指定的内置类型;不传则注册全部内置组件 */
builtinComponents?: JVxeTypes[];
}
export function registerJVxeTable(app: App) {
export async function registerJVxeTable(app: App) {
// VXETable 全局配置
const VXETableSettings = {
// z-index 起始值
@@ -31,7 +35,6 @@ export function registerJVxeTable(app: App) {
// 执行注册方法
app.use(VxeUIAll);
app.use(VXETable, VXETableSettings);
app.component('JVxeTable', JVxeTable);
}

View File

@@ -67,10 +67,23 @@
.col--valid-error > .vxe-tree-cell > .ant-calendar-picker .ant-calendar-picker-input {
border-color: #f5222d !important;
}
// update-begin--author:liaozhiyang---date:20260415---for:【QQYUN-15134】修复jvxetable使用fixed固定后无法拖拽
// 被拖起的源行:禁用/半透明效果,跨主体和固定列 wrapper 同步生效
.vxe-body--row.j-vxe-drag-source {
opacity: 0.4;
}
// 拖拽方向指示线(用 box-shadow 实现,不影响行高/布局)
.vxe-body--row.j-vxe-drag-hover-top {
box-shadow: inset 0 2px 0 0 #1890ff;
}
.vxe-body--row.j-vxe-drag-hover-bottom {
box-shadow: inset 0 -3px 0 0 #1890ff;
}
// update-end--author:liaozhiyang---date:20260415---for:【QQYUN-15134】修复jvxetable使用fixed固定后无法拖拽
.vxe-body--row.sortable-ghost,
.vxe-body--row.sortable-chosen {
background-color: #dfecfb;
background-color: transparent;
}
// ----------- 【VUEN-1691】默认隐藏滚动条鼠标放上去才显示 -------------------------------------------

View File

@@ -46,6 +46,14 @@ export enum JVxeTypes {
file = 'file',
// 省市区
pca = 'pca',
// 关联记录
linkTable = 'link-table',
// update-begin--author:liaozhiyang---date:20260413---for:【issues/7633】online子表支持分类字典树自定义树
// 自定义树控件
treeSelect = 'sel-tree',
// 分类字典树
catTreeSelect = 'cat-tree',
// update-end--author:liaozhiyang---date:20260413---for:【issues/7633】online子表支持分类字典树自定义树
}
// 为了防止和 vxe 内置的类型冲突,所以加上一个前缀

View File

@@ -131,8 +131,15 @@ function createCellRender(type: JVxeTypes, component: Component = <Component>com
function createRender(type, component, renderType) {
return function (renderOptions, params) {
// update-begin--author:liaozhiyang---date:20260316---for:【QQYUN-13751】jVxetable优化
// 为每个 cell 生成唯一 key防止相邻列值相同但 options 不同)被错误复用
const rowId = params.row?.id ?? params.rowIndex ?? '';
const colId = params.column?.property ?? params.column?.id ?? '';
const cellKey = `cell-${rowId}-${colId}`;
// update-end--author:liaozhiyang---date:20260316---for:【QQYUN-13751】jVxetable优化
return [
h(component, {
key: cellKey,
type: type,
params: params,
renderOptions: renderOptions,

View File

@@ -0,0 +1,12 @@
// 给使用原生vxe-table的页面注册vxe-table组件
export const useVxeTableRegister = async () => {
const app = window['JAppRootInstance'];
if (app._context.components.VxeTable) {
// 已全局注册
} else {
const { registerJVxeTable } = await import('/@/components/jeecg/JVxeTable');
await registerJVxeTable(app);
const { registerJVxeCustom } = await import('/@/components/JVxeCustom');
await registerJVxeCustom();
}
}

View File

@@ -39,6 +39,7 @@ interface OnlineColumn {
//他表字段用
linkField?:string;
fieldExtendJson?:string
resizable?: boolean;
}
export { OnlineColumn, HrefSlots };

View File

@@ -2,128 +2,38 @@ import type { App } from 'vue';
import { Icon } from './Icon';
import AIcon from '/@/components/jeecg/AIcon.vue';
//Tinymce富文本
import Editor from '/@/components/Tinymce/src/Editor.vue'
// import Editor from '/@/components/Tinymce/src/Editor.vue'
import { Button, JUploadButton } from './Button';
import { Space } from 'ant-design-vue';
// 按需注册antd的组件
import {
// Need
Button as AntButton,
Select,
Alert,
Checkbox,
DatePicker,
TimePicker,
Calendar,
Radio,
Switch,
Card,
List,
Tabs,
Descriptions,
Tree,
Table,
Divider,
Modal,
Drawer,
TreeSelect,
Dropdown,
Tag,
Tooltip,
Badge,
Popover,
Upload,
Transfer,
Steps,
PageHeader,
Result,
Empty,
Avatar,
Menu,
Breadcrumb,
Form,
Input,
Row,
Col,
Spin,
Space,
Layout,
Collapse,
Slider,
InputNumber,
Carousel,
Popconfirm,
Skeleton,
Cascader,
Rate,
Progress
} from 'ant-design-vue';
const compList = [AntButton.Group, Icon, AIcon, JUploadButton];
// ant-design-vue 组件通过 unplugin-vue-components + AntDesignVueResolver 自动按需导入,无需手动注册
const compList = [Icon, AIcon, JUploadButton];
//注册online模块的全局组件
import { registerOnlineComp } from '/@/views/super/online/cgform/auto/comp/index';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export function registerGlobComp(app: App) {
compList.forEach((comp) => {
app.component(comp.name || comp.displayName, comp);
});
// Space.Compact 是子组件AntDesignVueResolver 无法自动解析,需手动注册
app.component('ASpaceCompact', Space.Compact);
//仪表盘依赖Tinymce需要提前加载没办法按需加载了
app.component(Editor.name, Editor);
// app.component(Editor.name, Editor);
// 代码逻辑说明: Tinymce异步加载
// app.component(
// 'Tinymce',
// createAsyncComponent(() => import('./Tinymce/src/Editor.vue'), {
// loading: true,
// })
// );
app.use(Select)
.use(Alert)
// update-begin--author:liaozhiyang---date:20260227---for:【QQYUN-14751】tinymce富文本、JEasyCron、JLinkTableCard异步加载
app.component(
'Tinymce',
createAsyncComponent(() => import('./Tinymce/src/Editor.vue'), {
loading: true,
})
);
// update-end--author:liaozhiyang---date:20260227---for:【QQYUN-14751】tinymce富文本、JEasyCron、JLinkTableCard异步加载
app
.use(registerOnlineComp)
.use(Button)
.use(Breadcrumb)
.use(Checkbox)
.use(DatePicker)
.use(TimePicker)
.use(Calendar)
.use(Radio)
.use(Switch)
.use(Card)
.use(List)
.use(Descriptions)
.use(Tree)
.use(TreeSelect)
.use(Table)
.use(Divider)
.use(Modal)
.use(Drawer)
.use(Dropdown)
.use(Tag)
.use(Tooltip)
.use(Badge)
.use(Popover)
.use(Upload)
.use(Transfer)
.use(Steps)
.use(PageHeader)
.use(Result)
.use(Empty)
.use(Avatar)
.use(Menu)
.use(Tabs)
.use(Form)
.use(Input)
.use(Row)
.use(Col)
.use(Spin)
.use(Space)
.use(Layout)
.use(Collapse)
.use(Slider)
.use(InputNumber)
.use(Carousel)
.use(Popconfirm)
.use(Skeleton)
.use(Cascader)
.use(Rate)
.use(Progress);
console.log("---初始化--- 全局注册Antd、仪表盘、流程设计器、online、流程等组件--------------")
console.log("---初始化--- 全局注册Antd、仪表盘、流程设计器、online、流程等组件--------------")
}

View File

@@ -136,6 +136,9 @@ html[data-theme='light'] {
.jeecg-form-detail-effect {
*:not(.ant-select-selection-placeholder){
color: #606266!important;
.colorText {
color: #ffffff !important;
}
}
.ant-row label {
color: #797c81 !important;

View File

@@ -329,8 +329,10 @@ export function useListTable(tableProps: TableProps): [
// 发送请求之前调用的方法
function beforeFetch(params) {
// 判断是否已有排序参数defSort 为数组时会转换为 defSortString
const hasSortParams = params.column || params.defSortString || params.order;
// 默认以 createTime 降序排序
return Object.assign({ column: 'createTime', order: 'desc' }, params);
return Object.assign(hasSortParams ? {} : { column: 'createTime', order: 'desc' }, params);
}
// 合并方法

View File

@@ -14,17 +14,18 @@ export function printJS(configuration: Configuration) {
}
/** 调用 printNB 打印 */
export function printNb(domId) {
export function printNb(domId, endCallback?) {
if (domId) {
localPrint(domId);
localPrint(domId, endCallback);
} else {
window.print();
endCallback && endCallback();
}
}
let closeBtn = true;
function localPrint(domId) {
function localPrint(domId, endCallback) {
if (typeof domId === 'string' && !domId.startsWith('#')) {
domId = '#' + domId;
}
@@ -35,6 +36,7 @@ function localPrint(domId) {
el: domId,
endCallback() {
closeBtn = true;
endCallback && endCallback();
},
});
}

View File

@@ -1,11 +1,11 @@
<template>
<div :class="[prefixCls, getLayoutContentMode]" v-loading="getOpenPageLoading && getPageLoading">
<PageLayout />
<div id="content" class="app-view-box" v-if="openQianKun == 'true'"></div>
<div :id="qiankunDivId" class="app-view-box" v-if="openQiankun && qiankunDivId"></div>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted } from 'vue';
import { ref, defineComponent } from 'vue';
import PageLayout from '/@/layouts/page/index.vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
@@ -21,20 +21,20 @@
const { getOpenPageLoading } = useTransitionSetting();
const { getLayoutContentMode, getPageLoading } = useRootSetting();
const globSetting = useGlobSetting();
const openQianKun = globSetting.openQianKun;
const openQiankun = globSetting.openQianKun == 'true';
const qiankunDivId = ref('');
useContentViewHeight();
onMounted(() => {
// //注册openQianKun
// if (openQianKun == 'true') {
// if (!window.qiankunStarted) {
// window.qiankunStarted = true;
// registerApps();
// }
// }
});
// // 注册 qiankun
// if (openQiankun) {
// qiankunDivId.value = registerApps?.containerId;
// registerApps();
// }
return {
prefixCls,
openQianKun,
openQiankun,
qiankunDivId,
getOpenPageLoading,
getLayoutContentMode,
getPageLoading,

View File

@@ -154,6 +154,10 @@
// 【JEECG作为乾坤子应用】
&--qiankun-micro {
position: absolute;
:deep(.@{namespace}-layout-header--qiankun-micro) {
width: 100%;
}
}
}

View File

@@ -21,7 +21,7 @@
</template>
</a-select>
</a-form-item>
<a-form-item v-if="isMultiDepart" :validate-status="validate_status1">
<a-form-item v-if="isMultiDepart && departList.length > 0" :validate-status="validate_status1">
<!--label内容-->
<template #label>
<a-tooltip placement="topLeft">
@@ -136,7 +136,7 @@
return;
}
let currentDepart = result.list.filter((item) => item.orgCode == result.orgCode);
//筛选出用户的部门信息(排除公司或者岗位配置)
//TODO 筛选出用户的部门信息(排除公司或者岗位配置)后期回滚
const userDeparts = result.list.filter((item) => item.orgCategory == '2');
departList.value = userDeparts;
// 代码逻辑说明: JHHB-790 用户部门变更,会出现这个情况(因为之前设置的这里只切换部门,过滤了公司和岗位信息)
@@ -169,7 +169,7 @@
validate_status.value = 'error';
return false;
}
if (unref(isMultiDepart) && !unref(departSelected)) {
if (unref(isMultiDepart) && unref(departList).length > 0 && !unref(departSelected)) {
validate_status1.value = 'error';
return false;
}
@@ -198,7 +198,7 @@
*/
function departResolve() {
return new Promise(async (resolve, reject) => {
if (!unref(isMultiDepart)) {
if (!unref(isMultiDepart) || unref(departList).length == 0) {
resolve();
} else {
const result = await selectDepart({

View File

@@ -51,6 +51,11 @@
width: 100%;
}
// 【JEECG作为乾坤子应用】
&--qiankun-micro {
position: absolute;
}
&-logo {
height: @header-height;
min-width: 192px;

View File

@@ -106,7 +106,7 @@
const userStore = useUserStore();
const { getShowTopMenu, getShowHeaderTrigger, getSplit, getIsMixMode, getMenuWidth, getIsMixSidebar } = useMenuSetting();
const { getUseErrorHandle, getShowSettingButton, getSettingButtonPosition, getAiIconShow } = useRootSetting();
const { title } = useGlobSetting();
const { title, isQiankunMicro } = useGlobSetting();
const {
getHeaderTheme,
@@ -133,6 +133,8 @@
[`${prefixCls}--fixed`]: props.fixed,
[`${prefixCls}--mobile`]: unref(getIsMobile),
[`${prefixCls}--${theme}`]: theme,
// 【JEECG作为乾坤子应用】
[`${prefixCls}--qiankun-micro`]: isQiankunMicro,
},
];
});

View File

@@ -1,10 +1,10 @@
<template>
<Layout :class="prefixCls" v-bind="lockEvents">
<Layout :class="[layoutBoxClass]" v-bind="lockEvents">
<LayoutFeatures />
<LayoutHeader fixed v-if="getShowFullHeaderRef" />
<Layout :class="[layoutClass]">
<LayoutSideBar v-if="getShowSidebar || getIsMobile" />
<Layout :class="`${prefixCls}-main`">
<Layout :class="layoutMainClass">
<LayoutMultipleHeader />
<LayoutContent />
<LayoutFooter />
@@ -29,6 +29,7 @@
import { useLockPage } from '/@/hooks/web/useLockPage';
import { useAppInject } from '/@/hooks/web/useAppInject';
import { useGlobSetting } from '/@/hooks/setting';
export default defineComponent({
name: 'DefaultLayout',
@@ -45,11 +46,21 @@
const { prefixCls } = useDesign('default-layout');
const { getIsMobile } = useAppInject();
const { getShowFullHeaderRef } = useHeaderSetting();
const { getShowSidebar, getIsMixSidebar, getShowMenu } = useMenuSetting();
const { getShowSidebar, getIsMixSidebar, getShowMenu, getMenuType } = useMenuSetting();
const glob = useGlobSetting();
const { isQiankunMicro } = glob;
// Create a lock screen monitor
const lockEvents = useLockPage();
const layoutBoxClass = computed(() => {
let cls: string[] = [prefixCls];
if (unref(getMenuType)) {
cls.push(`${prefixCls}--menu-${unref(getMenuType)}`);
}
return cls;
});
const layoutClass = computed(() => {
let cls: string[] = ['ant-layout'];
if (unref(getIsMixSidebar) || unref(getShowMenu)) {
@@ -58,14 +69,27 @@
return cls;
});
const layoutMainClass = computed(() => {
let cls: string[] = [`${prefixCls}-main`];
// 【JEECG作为乾坤子应用】
if (unref(isQiankunMicro)) {
cls.push(`${prefixCls}-main--qiankun-micro`);
}
return cls;
});
return {
getShowFullHeaderRef,
getShowSidebar,
prefixCls,
getIsMobile,
getIsMixSidebar,
isQiankunMicro,
layoutBoxClass,
layoutClass,
lockEvents,
layoutMainClass,
lockEvents
};
},
});
@@ -80,6 +104,28 @@
background-color: @content-bg;
flex-direction: column;
&--menu {
// 【JEECG作为乾坤子应用】
&-mix-sidebar {
.@{namespace}-layout-mix-sider {
position: absolute;
overflow: visible;
> .@{namespace}-layout-mix-sider-menu-list {
position: absolute;
}
}
}
// 【JEECG作为乾坤子应用】
&-mix {
.@{namespace}-multiple-tabs {
margin-top: 0 !important;
}
}
}
> .ant-layout {
min-height: 100%;
}
@@ -88,6 +134,16 @@
width: 100%;
// 代码逻辑说明:【issues/8709】LayoutContent样式多出1px
// margin-left: 1px;
// 【JEECG作为乾坤子应用】根 Layout 作为 absolute 定位的参照容器
&--qiankun-micro {
position: relative;
.@{namespace}-multiple-tabs {
margin-top: 60px;
}
}
}
}
</style>

View File

@@ -25,7 +25,7 @@
<!-- 列表页全屏
<FoldButton v-if="getShowFold" />-->
<!-- <FullscreenOutlined /> -->
<router-link to="/ai" class="ai-icon">
<router-link v-if="!getIsSimpleTheme" 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>
@@ -56,6 +56,7 @@
import { useDesign } from '/@/hooks/web/useDesign';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
import { TabsThemeEnum } from '/@/enums/appEnum';
import { REDIRECT_NAME } from '/@/router/constant';
import { listenerRouteChange } from '/@/logics/mitt/routeChange';
@@ -91,6 +92,9 @@
const unClose = computed(() => unref(getTabsState).length === 1);
// 极简模式隐藏标签栏AI入口图标
const getIsSimpleTheme = computed(() => unref(getTabsTheme) === TabsThemeEnum.SIMPLE);
const getWrapClass = computed(() => {
return [
prefixCls,
@@ -149,6 +153,7 @@
getShowQuick,
getShowRedo,
getShowFold,
getIsSimpleTheme,
};
},
});
@@ -157,6 +162,7 @@
@import './index.less';
@import './tabs.theme.card.less';
@import './tabs.theme.smooth.less';
@import './tabs.theme.simple.less';
</style>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-multiple-tabs';

View File

@@ -0,0 +1,237 @@
// tabs极简样式
@prefix-cls-theme-simple: ~'@{prefix-cls}.@{prefix-cls}--theme-simple';
@multiple-simple-height: 38px;
html[data-theme='dark'] {
.@{prefix-cls-theme-simple} {
.ant-tabs-tab {
border: none !important;
}
}
}
html[data-theme='light'] {
.@{prefix-cls-theme-simple} {
.ant-tabs-tab:not(.ant-tabs-tab-active) {
border: none !important;
}
}
}
.@{prefix-cls-theme-simple} {
z-index: 10;
height: @multiple-simple-height;
line-height: @multiple-simple-height;
background-color: @component-background;
border-bottom: 1px solid @border-color-base;
box-shadow: none;
.ant-tabs-small {
height: @multiple-simple-height;
}
.ant-tabs.ant-tabs-card {
.ant-tabs-nav {
height: @multiple-simple-height;
margin: 0;
background-color: @component-background;
border: 0;
box-shadow: none;
padding-left: 6px;
.ant-tabs-nav-wrap {
height: @multiple-simple-height;
margin-top: 0;
}
.ant-tabs-tab {
height: @multiple-simple-height;
line-height: calc(@multiple-simple-height - 6px);
color: @text-color-secondary;
background-color: transparent;
border: none !important;
border-radius: 0;
padding: 0 16px;
margin: 0 !important;
position: relative;
transition: color 0.2s;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 2px;
background-color: @primary-color;
transition: all 0.2s ease;
transform: translateX(-50%);
}
.ant-tabs-tab-btn {
color: inherit;
transition: none;
}
&:hover {
color: @text-color-base;
.ant-tabs-tab-remove .anticon-close {
opacity: 1;
}
}
.ant-tabs-tab-remove {
margin: 0;
padding: 0;
position: relative;
top: 0;
left: 2px;
.anticon-close {
width: 14px;
height: 14px;
font-size: 12px;
color: inherit;
opacity: 0;
transition: opacity 0.15s;
border-radius: 100%;
vertical-align: middle;
line-height: 10px;
overflow: hidden;
&:hover {
color: #fff;
background-color: #c0c4cc;
svg {
fill: #fff;
}
}
}
}
> div {
display: flex;
justify-content: center;
align-items: center;
}
svg {
fill: @text-color-secondary;
}
}
.ant-tabs-tab:not(.ant-tabs-tab-active) {
&:hover {
color: @primary-color;
svg {
fill: @primary-color;
}
}
}
.ant-tabs-tab-active {
color: @primary-color !important;
background-color: transparent;
border: none !important;
&::after {
width: 100%;
}
.ant-tabs-tab-btn {
color: @primary-color;
font-weight: 500;
}
.ant-tabs-tab-remove .anticon-close {
opacity: 1;
svg {
width: 0.6em;
}
}
svg {
fill: @primary-color;
}
}
}
.ant-tabs-nav > div:nth-child(1) {
padding: 0 2px;
.ant-tabs-tab {
margin-right: 0 !important;
}
}
}
.ant-tabs-tab:not(.ant-tabs-tab-active) {
.ant-tabs-tab-remove .anticon-close {
font-size: 12px;
svg {
width: 0.6em;
}
}
}
.ant-tabs-extra-content {
position: relative;
top: 0;
line-height: @multiple-simple-height !important;
}
.ant-dropdown-trigger {
display: inline-flex;
}
.@{prefix-cls}--hide-close {
.ant-tabs-tab-remove .anticon-close {
opacity: 0 !important;
}
}
.@{prefix-cls}-content {
&__extra-quick,
&__extra-redo,
&__extra-fold {
display: inline-block;
width: 36px;
height: @multiple-simple-height;
line-height: @multiple-simple-height;
color: @text-color-secondary;
text-align: center;
cursor: pointer;
border-left: 1px solid @border-color-base;
&:hover {
color: @text-color-base;
}
span[role='img'] {
transform: rotate(90deg);
}
}
&__extra-redo {
span[role='img'] {
transform: rotate(0deg);
}
}
&__info {
display: inline-block;
width: 100%;
height: @multiple-simple-height;
padding-left: 0;
font-size: 13px;
cursor: pointer;
user-select: none;
}
}
}

View File

@@ -0,0 +1,174 @@
<template>
<template v-if="isQiankunRoute">
<!-- qiankun 路由不显示空白页提示 -->
</template>
<!-- QQYUN-13593空白页美化 -->
<div v-else class="animationEffect" :style="effectVars">
<div class="effect-layer">
<div class="blob blob-a"></div>
<div class="blob blob-b"></div>
<div class="blob blob-c"></div>
</div>
<div class="effect-grid"></div>
<div class="effect-tip">
<p>{{ pageTip }}</p>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { router } from "@/router";
import { useEmpty } from '../useEmpty';
// 判断是否是 qiankun 路由
const isQiankunRoute = computed(() => !!router.currentRoute.value?.meta?.isQiankunRoute);
const {pageTip, effectVars} = useEmpty();
</script>
<style lang="less" scoped>
/** update-begin---author:liaozy ---date:2025-08-26 for空白页美化样式 */
.pageTip {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 18px;
color: #999;
margin: 0;
}
.animationEffect {
position: relative;
height: 100%;
min-height: 420px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, var(--bg-1) 0%, var(--bg-2) 100%);
}
.effect-layer {
position: absolute;
top: -20%;
left: -20%;
right: -20%;
bottom: -20%;
filter: blur(30px);
pointer-events: none;
z-index: 0;
}
.blob {
position: absolute;
width: 380px;
height: 380px;
border-radius: 50%;
opacity: 0.45;
}
.blob-a {
background: radial-gradient(circle at 30% 30%, var(--blob-a-1) 0%, var(--blob-a-2) 60%, var(--blob-a-2) 100%);
left: 5%;
top: 10%;
animation: float-a 18s ease-in-out infinite;
}
.blob-b {
background: radial-gradient(circle at 30% 30%, var(--blob-b-1) 0%, var(--blob-b-2) 60%, var(--blob-b-2) 100%);
right: 0;
top: 30%;
animation: float-b 22s ease-in-out infinite;
}
.blob-c {
background: radial-gradient(circle at 30% 30%, var(--blob-c-1) 0%, var(--blob-c-2) 60%, var(--blob-c-2) 100%);
left: 35%;
bottom: -5%;
animation: float-c 26s ease-in-out infinite;
}
@keyframes float-a {
0% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(20%, -10%) scale(1.05);
}
50% {
transform: translate(35%, 5%) scale(0.95);
}
75% {
transform: translate(10%, 15%) scale(1.02);
}
100% {
transform: translate(0, 0) scale(1);
}
}
@keyframes float-b {
0% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(-15%, 10%) scale(1.08);
}
50% {
transform: translate(-30%, -5%) scale(0.92);
}
75% {
transform: translate(-10%, -15%) scale(1.03);
}
100% {
transform: translate(0, 0) scale(1);
}
}
@keyframes float-c {
0% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(-10%, -10%) scale(0.9);
}
50% {
transform: translate(10%, -25%) scale(1.05);
}
75% {
transform: translate(20%, 0%) scale(0.98);
}
100% {
transform: translate(0, 0) scale(1);
}
}
.effect-grid {
position: absolute;
inset: 0;
background-image: linear-gradient(0deg, var(--grid-color) 1px, rgba(0, 0, 0, 0) 1px),
linear-gradient(90deg, var(--grid-color) 1px, rgba(0, 0, 0, 0) 1px);
background-size: 36px 36px, 36px 36px;
mask-image: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0) 70%);
pointer-events: none;
z-index: 1;
}
.effect-tip {
position: relative;
z-index: 2;
text-align: center;
pointer-events: none;
p {
margin: 0;
padding: 8px 14px;
color: var(--tip-color);
font-size: 20px;
border-radius: 8px;
}
}
/** update-end---author:liaozy ---date:2025-08-26 for空白页美化样式 */
</style>

View File

@@ -14,26 +14,14 @@
<!-- mode="out-in"-->
<!-- appear-->
<!-- >-->
<template v-if="Component">
<keep-alive v-if="openCache" :include="getCaches">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
<component v-else :is="Component" :key="route.fullPath" />
</template>
<template v-else>
<!-- QQYUN-13593空白页美化 -->
<div class="animationEffect" :style="effectVars">
<div class="effect-layer">
<div class="blob blob-a"></div>
<div class="blob blob-b"></div>
<div class="blob blob-c"></div>
</div>
<div class="effect-grid"></div>
<div class="effect-tip">
<p>{{pageTip}}</p>
</div>
</div>
</template>
<keep-alive v-if="openCache" :include="getCaches">
<template v-if="Component">
<component :is="Component" :key="route.fullPath"/>
</template>
<EmptyPage v-else/>
</keep-alive>
<component v-else-if="Component" :is="Component" :key="route.fullPath"/>
<EmptyPage v-else/>
<!-- </transition>-->
</template>
</RouterView>
@@ -44,6 +32,7 @@
import { computed, defineComponent, unref } from 'vue';
import FrameLayout from '/@/layouts/iframe/index.vue';
import EmptyPage from './components/EmptyPage.vue';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
@@ -52,11 +41,10 @@
import { getTransitionName } from './transition';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
import { useEmpty } from './useEmpty';
export default defineComponent({
name: 'PageLayout',
components: { FrameLayout },
components: { FrameLayout, EmptyPage },
setup() {
const { getShowMultipleTab } = useMultipleTabSetting();
const tabStore = useMultipleTabStore();
@@ -73,8 +61,6 @@
}
return tabStore.getCachedTabList;
});
// 代码逻辑说明: 【QQYUN-13593】空白页美化
const { pageTip, getPageTip, effectVars } = useEmpty();
return {
getTransitionName,
openCache,
@@ -82,123 +68,7 @@
getBasicTransition,
getCaches,
getCanEmbedIFramePage,
pageTip,
getPageTip,
effectVars,
};
},
});
</script>
<style lang="less" scoped>
/** update-begin---author:liaozy ---date:2025-08-26 for空白页美化样式 */
.pageTip {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 18px;
color: #999;
margin: 0;
}
.animationEffect {
position: relative;
height: 100%;
min-height: 420px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, var(--bg-1) 0%, var(--bg-2) 100%);
}
.effect-layer {
position: absolute;
top: -20%;
left: -20%;
right: -20%;
bottom: -20%;
filter: blur(30px);
pointer-events: none;
z-index: 0;
}
.blob {
position: absolute;
width: 380px;
height: 380px;
border-radius: 50%;
opacity: 0.45;
}
.blob-a {
background: radial-gradient(circle at 30% 30%, var(--blob-a-1) 0%, var(--blob-a-2) 60%, var(--blob-a-2) 100%);
left: 5%;
top: 10%;
animation: float-a 18s ease-in-out infinite;
}
.blob-b {
background: radial-gradient(circle at 30% 30%, var(--blob-b-1) 0%, var(--blob-b-2) 60%, var(--blob-b-2) 100%);
right: 0;
top: 30%;
animation: float-b 22s ease-in-out infinite;
}
.blob-c {
background: radial-gradient(circle at 30% 30%, var(--blob-c-1) 0%, var(--blob-c-2) 60%, var(--blob-c-2) 100%);
left: 35%;
bottom: -5%;
animation: float-c 26s ease-in-out infinite;
}
@keyframes float-a {
0% { transform: translate(0, 0) scale(1); }
25% { transform: translate(20%, -10%) scale(1.05); }
50% { transform: translate(35%, 5%) scale(0.95); }
75% { transform: translate(10%, 15%) scale(1.02); }
100% { transform: translate(0, 0) scale(1); }
}
@keyframes float-b {
0% { transform: translate(0, 0) scale(1); }
25% { transform: translate(-15%, 10%) scale(1.08); }
50% { transform: translate(-30%, -5%) scale(0.92); }
75% { transform: translate(-10%, -15%) scale(1.03); }
100% { transform: translate(0, 0) scale(1); }
}
@keyframes float-c {
0% { transform: translate(0, 0) scale(1); }
25% { transform: translate(-10%, -10%) scale(0.9); }
50% { transform: translate(10%, -25%) scale(1.05); }
75% { transform: translate(20%, 0%) scale(0.98); }
100% { transform: translate(0, 0) scale(1); }
}
.effect-grid {
position: absolute;
inset: 0;
background-image: linear-gradient(0deg, var(--grid-color) 1px, rgba(0, 0, 0, 0) 1px),
linear-gradient(90deg, var(--grid-color) 1px, rgba(0, 0, 0, 0) 1px);
background-size: 36px 36px, 36px 36px;
mask-image: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0) 70%);
pointer-events: none;
z-index: 1;
}
.effect-tip {
position: relative;
z-index: 2;
text-align: center;
pointer-events: none;
p {
margin: 0;
padding: 8px 14px;
color: var(--tip-color);
font-size: 20px;
border-radius: 8px;
}
}
/** update-end---author:liaozy ---date:2025-08-26 for空白页美化样式 */
</style>

View File

@@ -1,3 +1,5 @@
// export const containerId = 'qiankun-content'
//
// /**
// *微应用apps
// * @name: 微应用名称 - 具有唯一性
@@ -6,14 +8,14 @@
// * @activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
// */
// //子应用列表
// const _apps: object[] = [];
// const _apps: Recordable[] = [];
// for (const key in import.meta.env) {
// if (key.includes('VITE_APP_SUB_')) {
// const name = key.split('VITE_APP_SUB_')[1];
// const obj = {
// name,
// entry: import.meta.env[key],
// container: '#content',
// container: '#' + containerId,
// activeRule: name,
// };
// _apps.push(obj);

View File

@@ -1,9 +1,17 @@
// /**
// * qiankun配置
// */
// import { registerMicroApps, setDefaultMountApp, start, runAfterFirstMounted, addGlobalUncaughtErrorHandler } from 'qiankun';
// import { apps } from './apps';
// import {
// start,
// registerMicroApps,
// runAfterFirstMounted,
// addGlobalUncaughtErrorHandler
// } from 'qiankun';
// import { apps, containerId } from './apps';
// import { getProps, initGlState } from './state';
// import { registerQiankunRouter } from './route';
//
// registerQiankunRouter();
//
// /**
// * 重构apps
@@ -11,6 +19,7 @@
// function filterApps() {
// apps.forEach((item) => {
// //主应用需要传递给微应用的数据。
// // @ts-ignore
// item.props = getProps();
// //微应用触发的路由规则
// // @ts-ignore
@@ -27,34 +36,62 @@
// return (location) => location.pathname.startsWith(routerPrefix);
// }
//
// let retryCount = 0;
//
// /**
// * 微应用注册
// */
// function registerApps() {
// const container = document.querySelector('#' + containerId);
// if (!container) {
// // 如果容器不存在递归尝试注册应用最多尝试10次每次间隔500毫秒
// if (retryCount < 10) {
// retryCount++;
// setTimeout(() => registerApps(), 500);
// }
// } else {
// registerAppsNow();
// }
// }
//
// registerApps['containerId'] = containerId;
//
// function registerAppsNow() {
// if (window.qiankunStarted) {
// return;
// }
// window.qiankunStarted = true;
// const _apps = filterApps();
// // @ts-ignore
// registerMicroApps(_apps, {
// beforeLoad: [
// // @ts-ignore
// (loadApp) => {
// console.log('before load', loadApp);
// console.log('[qiankun] before load', loadApp);
// },
// ],
// beforeMount: [
// // @ts-ignore
// (mountApp) => {
// console.log('before mount', mountApp);
// console.log('[qiankun] before mount', mountApp);
// },
// ],
// afterMount: [
// // @ts-ignore
// (mountApp) => {
// console.log('before mount', mountApp);
// console.log('[qiankun] after mount', mountApp);
// },
// ],
// beforeUnmount: [
// // @ts-ignore
// (unloadApp) => {
// console.log('[qiankun] before unmount', unloadApp);
// },
// ],
// afterUnmount: [
// // @ts-ignore
// (unloadApp) => {
// console.log('after unload', unloadApp);
// console.log('[qiankun] after unmount', unloadApp);
// },
// ],
// });

View File

@@ -6,7 +6,7 @@ import type {MainAppProps} from "#/main";
import {destroyStore} from "@/store";
import {destroyRouter} from "@/router";
import {clearComponent} from "@/components/jeecg/JVxeTable/src/componentMap";
import { clearComponent } from '@/components/jeecg/JVxeTable/src/componentMapStore';
import {renderWithQiankun} from 'vite-plugin-qiankun/dist/helper';

View File

@@ -0,0 +1,45 @@
// import { router } from "@/router";
// import { apps } from './apps';
//
// export const {registerQiankunRouter} = (function () {
//
// let registered = false;
//
// /**
// * 注册qiankun路由
// */
// function registerQiankunRouter() {
// if (!router) {
// // 如果路由对象不存在,递归调用,直到路由对象可用
// setTimeout(() => registerQiankunRouter(), 1);
// } else {
// registerQiankunRouterNow();
// }
// }
//
// function registerQiankunRouterNow() {
// if (registered) {
// return;
// }
// registered = true;
// const checkQiankunRoute = (path: string) => apps.some(app => path.startsWith('/' + app.name));
// // 添加路由守卫
// // 路由守卫判断是否是qiankun子应用路由
// router.beforeEach(async (to, from, next) => {
// const isQiankunRoute = checkQiankunRoute(to.path);
// if (isQiankunRoute) {
// // 如果是qiankun子应用路由设置meta属性
// to.meta.isQiankunRoute = true;
// } else {
// // 如果不是qiankun子应用路由清除meta属性
// delete to.meta.isQiankunRoute;
// }
// next();
// });
// }
//
//
// return {
// registerQiankunRouter,
// }
// })();

View File

@@ -10,6 +10,7 @@ import { URL_HASH_TAB, _eval } from '/@/utils';
//引入online lib路由
import { packageViews } from '/@/utils/monorepo/dynamicRouter';
import { loadPackageComponent } from '/@/utils/monorepo/registerPackages';
import { dynamicPages } from '/@/utils/dynamicPages';
import {useI18n} from "/@/hooks/web/useI18n";
export type LayoutMapKey = 'LAYOUT';
@@ -28,7 +29,9 @@ let dynamicViewsModules: Record<string, () => Promise<Recordable>>;
// Dynamic introduction
function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
if (!dynamicViewsModules) {
dynamicViewsModules = import.meta.glob('../../views/**/*.{vue,tsx}');
// update-begin--author:liaozhiyang---date:20260302---for:【QQYUN-14799】动态引入页面会生成两份及引入components下的组件文件
dynamicViewsModules = dynamicPages as Record<string, () => Promise<Recordable>>;
// update-end--author:liaozhiyang---date:20260302---for:【QQYUN-14799】动态引入页面会生成两份及引入components下的组件文件
//合并online lib路由
dynamicViewsModules = Object.assign({}, dynamicViewsModules, packageViews);
}
@@ -105,7 +108,10 @@ function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
function dynamicImport(dynamicViewsModules: Record<string, () => Promise<Recordable>>, component: string) {
const keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => {
const k = key.replace('../../views', '');
// update-begin--author:liaozhiyang---date:20260302---for:【QQYUN-14799】动态引入页面会生成两份及引入components下的组件文件
// 兼容两种前缀dynamicPages 的 ../views 与 packageViews 的 ../../views
const k = key.replace(/^(\.\.\/)+views/, '');
// update-end--author:liaozhiyang---date:20260302---for:【QQYUN-14799】动态引入页面会生成两份及引入components下的组件文件
const startFlag = component.startsWith('/');
const endFlag = component.endsWith('.vue') || component.endsWith('.tsx');
const startIndex = startFlag ? 0 : 1;
@@ -122,7 +128,7 @@ function dynamicImport(dynamicViewsModules: Record<string, () => Promise<Recorda
return;
}
// online/aiflow 本地未找到,尝试从懒加载包中按需加载
if (component.startsWith('/super/online') || component.startsWith('/super/airag')) {
if (component.startsWith('/super/airag/aiflow')) {
return () => {
return loadPackageComponent(component).then((factory) => (factory ? factory() : Promise.reject(`组件 ${component} 未找到`)));
};

View File

@@ -8,12 +8,36 @@ import relativeTime from 'dayjs/plugin/relativeTime';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
// JVxeTable 按需加载:仅首次渲染时注册一次
let jvxeRegistered = false;
export async function registerThirdComp(app: App) {
//---------------------------------------------------------------------
// 注册 JVxeTable 组件
registerJVxeTable(app);
// 注册 JVxeTable 自定义组件
await registerJVxeCustom();
// update-begin--author:liaozhiyang---date:20260210---for:【QQYUN-13658】Jvxetable、vxetable按需加载
// 注册 JVxeTable 组件(按需加载:首次使用 <JVxeTable> 时才加载 vxe-table 与 JVxeTable
app.component(
'JVxeTable',
createAsyncComponent(
() => {
return import('/@/components/jeecg/JVxeTable/src/JVxeTable').then(async (m) => {
if (!jvxeRegistered) {
if (app._context.components.VxeTable) {
// 已全局注册
} else {
const { registerJVxeTable } = await import('/@/components/jeecg/JVxeTable/src/install');
await registerJVxeTable(app);
const { registerJVxeCustom } = await import('/@/components/JVxeCustom');
await registerJVxeCustom();
jvxeRegistered = true;
}
}
return m.default;
});
},
{ loading: true }
)
);
// update-end--author:liaozhiyang---date:20260209---for:【QQYUN-13658】Jvxetable、vxetable按需加载
//---------------------------------------------------------------------
// 注册全局聊天表情包
// 代码逻辑说明: 【QQYUN-8241】emoji-mart-vue-fast库异步加载

View File

@@ -0,0 +1,10 @@
// 获取所有动态页面(views目录下的所有vue文件和tsx文件)
const allFiles = import.meta.glob(
[
'../views/**/*.{vue,tsx}',// 获取所有vue和tsx文件
// 排除特定文件夹
'!../views/system/approvalrole/compoments/**',
]
);
// 合并所有动态页面
export const dynamicPages = { ...allFiles };

View File

@@ -3,8 +3,7 @@ import {
// FunctionalComponent, CSSProperties
} from 'vue';
import { Spin } from 'ant-design-vue';
import { noop } from '/@/utils/index';
const noop = () => {};
// const Loading: FunctionalComponent<{ size: 'small' | 'default' | 'large' }> = (props) => {
// const style: CSSProperties = {
// position: 'absolute',

View File

@@ -0,0 +1,4 @@
import { createFromIconfontCN } from '@ant-design/icons-vue';
import '/@/assets/icons/js/iconfont2.js';
export const IconFont = createFromIconfontCN({});

View File

@@ -4,6 +4,7 @@ import type { FormSchema, FormActionType } from "@/components/Form";
import { unref } from 'vue';
import { isObject, isFunction, isString } from '/@/utils/is';
import { dynamicPages } from './dynamicPages';
import Big from 'big.js';
import dayjs from "dayjs";
// 代码逻辑说明: 【VUEN-656】配置外部网址打不开原因是带了#号,需要替换一下
@@ -348,7 +349,6 @@ export function numToUpper(value) {
}
// 代码逻辑说明: 解决老的vue2动态导入文件语法 vite不支持的问题
const allModules = import.meta.glob('../views/**/*.vue');
export function importViewsFile(path): Promise<any> {
if (path.startsWith('/')) {
path = path.substring(1);
@@ -361,10 +361,10 @@ export function importViewsFile(path): Promise<any> {
}
return new Promise((resolve, reject) => {
let flag = true;
for (const path in allModules) {
for (const path in dynamicPages) {
if (path == page) {
flag = false;
allModules[path]().then((mod) => {
dynamicPages[path]().then((mod) => {
console.log(path, mod);
resolve(mod);
});
@@ -669,3 +669,171 @@ export const split = (str) => {
}
return str;
};
/**
* 处理word文档中的o:p标签
* @param html
*/
export const removeSpecialTags = (html: string): string => {
if (!html) return '';
try {
const BORDER = '1px solid #8c8c8c';
// ===================================================================
// 第一步:移除 Office 垃圾标签(纯字符串)
// ===================================================================
// o:p 标签(转义和普通形式)
html = html.replace(/&lt;o:p[^&]*?&gt;.*?&lt;\/o:p&gt;/gis, '');
html = html.replace(/&lt;o:p[^&]*?\/?&gt;/gis, '');
html = html.replace(/&lt;\/o:p&gt;/gis, '');
html = html.replace(/<o:p[^>]*>.*?<\/o:p>/gis, '');
html = html.replace(/<o:p[^>]*\/?>/gis, '');
html = html.replace(/<\/o:p>/gis, '');
// style 标签(转义和普通形式)
html = html.replace(/&lt;style[^&]*?&gt;.*?&lt;\/style&gt;/gis, '');
html = html.replace(/&lt;style[^&]*?\/?&gt;/gis, '');
html = html.replace(/&lt;\/style&gt;/gis, '');
html = html.replace(/<style[^>]*>.*?<\/style>/gis, '');
// 条件注释和 Office 命名空间标签
html = html.replace(/<!--\[if[^>]*>.*?<!\[endif\]-->/gis, '');
html = html.replace(/<\/?w:[^>]*>/gis, '');
html = html.replace(/<\/?xml[^>]*>/gis, '');
html = html.replace(/<\/?v:[^>]*>/gis, '');
// ===================================================================
// 第二步DOM 清理(仅清理空段落,完全不动表格行结构)
// ★ 关键:放在边框处理之前,防止 DOM 序列化丢失 !important
// ===================================================================
try {
const DOMParserCtor: any =
typeof DOMParser !== 'undefined' ? DOMParser : (globalThis as any).DOMParser;
if (DOMParserCtor) {
const parser = new DOMParserCtor() as DOMParser;
const doc = parser.parseFromString(html, 'text/html') as Document;
// 仅清理单元格内的空 <p> 标签
const cells = doc.querySelectorAll('td,th');
for (let ci = 0; ci < cells.length; ci++) {
const cell = cells[ci] as HTMLElement;
if (!cell) continue;
const ps = cell.querySelectorAll('p');
for (let pi = ps.length - 1; pi >= 0; pi--) {
const p = ps[pi] as HTMLParagraphElement;
if (!p) continue;
const content = (p.innerHTML || '').replace(/&nbsp;|\u00A0/g, '').trim();
if (content === '') {
p.remove();
} else {
const frag = doc.createDocumentFragment();
while (p.firstChild) frag.appendChild(p.firstChild);
if (p.parentNode) p.parentNode.replaceChild(frag, p);
}
}
}
html = doc.body.innerHTML;
}
} catch (_) {
// DOM 不可用,跳过段落清理(不影响边框修复)
}
// ===================================================================
// 第三步:注入全局表格样式 <style>(兜底保障)
// ★ 放在 DOM 序列化之后,确保 <style> 标签不会被 DOM 改写
// ===================================================================
const globalCSS = `<style>
table{border-collapse:collapse!important;border-spacing:0!important;width:100%!important;max-width:100%!important;}
table td,table th{border-top:${BORDER}!important;border-right:${BORDER}!important;border-bottom:${BORDER}!important;border-left:${BORDER}!important;padding:6px 8px!important;vertical-align:middle!important;box-sizing:border-box!important;empty-cells:show!important;}
table td p,table th p{margin:0!important;padding:0!important;line-height:1.4!important;}
img{max-width:100%!important;height:auto!important;display:block!important;}
</style>`;
if (/<head[^>]*>/i.test(html)) {
html = html.replace(/(<head[^>]*>)/i, `$1${globalCSS}`);
} else if (/<html[^>]*>/i.test(html)) {
html = html.replace(/(<html[^>]*>)/i, `$1<head>${globalCSS}</head>`);
} else {
html = globalCSS + html;
}
// ===================================================================
// 第四步:纯字符串处理内联样式(核心边框修复)
// ★ 最后执行!确保输出 HTML 中的 !important 不会被任何后续处理丢失
//
// 策略:按分号拆分 style 值,逐条判断属性名,
// 过滤掉所有 border* 和 mso-* 声明,
// 然后追加统一的边框声明(含 !important
// ===================================================================
/**
* 从 CSS style 字符串中移除所有 border* 和 mso-* 声明,
* 保留其余布局相关属性width, height, padding, text-align 等)
*/
function stripBorderAndMso(styleStr: string): string {
return styleStr
.split(';')
.filter(function (decl) {
const trimmed = decl.trim();
if (!trimmed) return false;
const colonIdx = trimmed.indexOf(':');
if (colonIdx === -1) return true; // 保留无效声明(安全起见)
const prop = trimmed.substring(0, colonIdx).trim().toLowerCase();
// 移除所有 border 开头和 mso- 开头的属性
if (prop.startsWith('border') || prop.startsWith('mso-')) return false;
return true;
})
.join(';');
}
// 4a. 移除 <table> 上的 border="0" HTML 属性
html = html.replace(/(<table\b[^>]*?)\sborder\s*=\s*['"]?0['"]?/gi, '$1');
// 4b. 处理 <table> 标签的内联样式
html = html.replace(/<table(\s[^>]*)>/gi, function (fullMatch, attrs) {
try {
const sm = attrs.match(/\sstyle\s*=\s*(["'])([\s\S]*?)\1/i);
if (sm) {
const q = sm[1];
const cleaned = stripBorderAndMso(sm[2]);
const newStyle = cleaned
+ ';border-collapse:collapse!important'
+ ';border-spacing:0!important'
+ ';width:100%!important';
return '<table' + attrs.replace(sm[0], ` style=${q}${newStyle}${q}`) + '>';
}
return '<table' + attrs
+ ' style="border-collapse:collapse!important;border-spacing:0!important;width:100%!important">';
} catch (_) { return fullMatch; }
});
// 4c. 处理 <td>/<th> 标签的内联样式
const cellBorderCSS =
`border-top:${BORDER}!important;` +
`border-right:${BORDER}!important;` +
`border-bottom:${BORDER}!important;` +
`border-left:${BORDER}!important;` +
'padding:6px 8px!important;' +
'vertical-align:middle!important;' +
'box-sizing:border-box!important';
html = html.replace(/<(td|th)(\s[^>]*)>/gi, function (fullMatch, tag, attrs) {
try {
const sm = attrs.match(/\sstyle\s*=\s*(["'])([\s\S]*?)\1/i);
if (sm) {
const q = sm[1];
const cleaned = stripBorderAndMso(sm[2]);
const newStyle = (cleaned ? cleaned + ';' : '') + cellBorderCSS;
return '<' + tag + attrs.replace(sm[0], ` style=${q}${newStyle}${q}`) + '>';
}
return '<' + tag + attrs + ` style="${cellBorderCSS}">`;
} catch (_) { return fullMatch; }
});
return html;
} catch (_) {
return html;
}
};

View File

@@ -1,11 +1,9 @@
import type { App } from 'vue';
import { warn } from '/@/utils/log';
import { registerDynamicRouter } from '/@/utils/monorepo/dynamicRouter';
import { add } from '/@/components/Form/src/componentMap';
// 懒加载模块配置(按需加载,访问相关路由时才加载对应包)
const lazyPackages = [
{ name: '@jeecg/online', importer: () => import('@jeecg/online') },
{ name: '@jeecg/aiflow', importer: () => import('@jeecg/aiflow') },
];
@@ -19,6 +17,14 @@ const installOptions = {
export function registerPackages(app: App) {
// 仅保存 app 实例,不立即加载模块
appInstance = app;
// app.component(
// 'SuperQuery',
// createAsyncComponent(() => import('@jeecg/online').then(mod => mod.SuperQuery))
// );
// app.component(
// 'JOnlineSearchSelect',
// createAsyncComponent(() => import('@jeecg/online').then(mod => mod.JOnlineSearchSelect))
// );
}
/** 已加载的包缓存 */

View File

@@ -1,21 +1,29 @@
<template>
<PageWrapper title="富文本组件示例">
<Tinymce v-model="value" @change="handleChange" width="100%" />
</PageWrapper>
<div :style="{ height: contentHeight, overflowY: 'scroll' }">
<PageWrapper title="富文本组件示例">
<Tinymce v-model="value" @change="handleChange" width="100%" />
<div style="height: 1000px"></div>
</PageWrapper>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { defineComponent, ref, computed } from 'vue';
import { Tinymce } from '/@/components/Tinymce/index';
import { PageWrapper } from '/@/components/Page';
import { useLayoutHeight } from '@/layouts/default/content/useContentViewHeight';
export default defineComponent({
components: { Tinymce, PageWrapper },
setup() {
const value = ref('hello world!');
const { headerHeightRef } = useLayoutHeight();
function handleChange(value: string) {
console.log(value);
}
return { handleChange, value };
//【issues/9448】
const contentHeight = computed(() => {
return `calc(100vh - ${headerHeightRef.value}px)`;
});
return { handleChange, value, contentHeight };
},
});
</script>

View File

@@ -1,5 +1,25 @@
<template>
<PageWrapper title="Icon组件示例">
<PageWrapper title="图标使用示例">
<CollapseContainer title="Icon组件中iconfiy图标使用不推荐使用项目中所有图标都构建在一个js且首屏就加载没有分割到chuck中不是按需引入" class="my-5">
<div class="flex justify-around flex-wrap">
<Icon icon="ion:layers-outline" :size="30" />
<Icon icon="ion:bar-chart-outline" :size="30" />
<Icon icon="ion:tv-outline" :size="30" />
<Icon icon="ion:settings-outline" :size="30" />
<Icon icon="ion:language" :size="30" />
</div>
</CollapseContainer>
<CollapseContainer title="推荐直接使用iconify原生组件分割到chunk中按需引入" class="my-5">
<div class="flex justify-around flex-wrap">
<IconifyonLayersOutline class="text-30px" />
<IconifyIonBarChartOutline class="text-30px" />
<IconifyIonTvOutline class="text-30px" />
<IconifyIonSettingsOutline class="text-30px" />
<IconIonLanguage class="text-30px" />
</div>
</CollapseContainer>
<CollapseContainer title="Antv Icon使用 (直接按需引入相应组件即可)">
<div class="flex justify-around">
<GithubFilled :style="{ fontSize: '30px' }" />
@@ -12,15 +32,6 @@
</div>
</CollapseContainer>
<CollapseContainer title="IconIfy 组件使用" class="my-5">
<div class="flex justify-around flex-wrap">
<Icon icon="ion:layers-outline" :size="30" />
<Icon icon="ion:bar-chart-outline" :size="30" />
<Icon icon="ion:tv-outline" :size="30" />
<Icon icon="ion:settings-outline" :size="30" />
</div>
</CollapseContainer>
<CollapseContainer title="svg 雪碧图" class="my-5">
<div class="flex justify-around flex-wrap">
<SvgIcon name="test" size="32" />
@@ -68,7 +79,7 @@
import { openWindow } from '/@/utils';
import { PageWrapper } from '/@/components/Page';
import IconIonLanguage from '~icons/ion/language'
export default defineComponent({
components: {
PageWrapper,
@@ -84,6 +95,7 @@
Alert,
IconPicker,
SvgIcon,
IconIonLanguage,
},
setup() {
return {

View File

@@ -46,6 +46,15 @@
</div>
</template>
<template #planTimeRangeSlot="{ row, triggerChange }">
<a-range-picker
:value="row.planTimeRange"
value-format="YYYY-MM-DD"
:bordered="false"
@change="(dates) => triggerChange(dates)"
/>
</template>
<template #myAction="props">
<a @click="onLookRow(props)">查看</a>
<a-divider type="vertical" />
@@ -212,6 +221,36 @@
customValue: ['Y', 'N'], // true ,false
defaultChecked: false,
},
{
title: '预估开始日期 ~ 预估结束日期',
key: 'planTimeRange',
type: JVxeTypes.slot,
width: 280,
slotName: 'planTimeRangeSlot',
},
{
title: '自定义树控件',
key: 'sel_tree_demo',
type: JVxeTypes.treeSelect,
width: 200,
// dict 格式:表名,文本字段,存储字段
dict: 'sys_category,name,id',
pidField: 'pid',
pidValue: '0',
hasChildField: 'has_child',
multiple: true,
placeholder: '请选择',
},
{
title: '分类字典树',
key: 'cat_tree_demo',
type: JVxeTypes.catTreeSelect,
width: 200,
// pcode: 根分类编码,'0' 表示加载所有根节点
pcode: 'B01',
multiple: true,
placeholder: '请选择',
},
{
title: '操作',
key: 'action',
@@ -261,6 +300,7 @@
select_search: options[random(0, 3)],
datetime: randomDatetime(),
checkbox: ['Y', 'N'][random(0, 1)],
planTimeRange: [dayjs().subtract(random(1, 30), 'day').format('YYYY-MM-DD'), dayjs().add(random(1, 30), 'day').format('YYYY-MM-DD')],
});
}

View File

@@ -5,7 +5,6 @@
<li>2. 使用 sortKey 属性可以自定义排序保存的 key默认为 orderNum</li>
<li>3. 使用 sortBegin 属性可以自定义排序的起始值默认为 0</li>
<li>4. sortKey 定义的字段不需要定义在 columns 中也能正常获取到值</li>
<li>5. 当存在 fixed 列时拖拽排序将会失效仅能上下排序</li>
</ol>
<p> 以下示例开启了拖拽排序排序值保存字段为 sortNum排序起始值为 3<br /> </p>

View File

@@ -25,7 +25,7 @@
<JPopup
v-model:value="model[field]"
:formElRef="formElRef"
code="ces_app_rep001"
code="withparamreport"
:param="{ sex: '1' }"
:fieldConfig="[{ source: 'name', target: 'pop2' }]"
/>
@@ -57,6 +57,9 @@
<template #superQuery1="{ model, field }">
<super-query :config="superQueryConfig" @search="(value)=>handleSuperQuery(value, model, field)" :isCustomSave="true" :saveSearchData="saveSearchData" :save="handleSuperQuerySave"/>
</template>
<template #tabsSelectUser="{ model, field }">
<JTabsSelectUser v-model:value="model[field]" ></JTabsSelectUser>
</template>
</BasicForm>
</template>
<script lang="ts">
@@ -68,7 +71,7 @@
import { schemas } from './jeecgComponents.data';
import { usePermission } from '/@/hooks/web/usePermission';
import { BasicDragVerify } from '/@/components/Verify';
import JTabsSelectUser from '/@/components/jeecg/JTabsSelectUser/index.vue';
export default defineComponent({
components: {
BasicForm,
@@ -79,6 +82,7 @@
JCheckbox,
JInput,
JEllipsis,
JTabsSelectUser,
BasicDragVerify,
},
name: 'JeecgComponents',

View File

@@ -1,4 +1,5 @@
import { FormSchema, JCronValidator } from '/@/components/Form';
import { FormSchema } from '/@/components/Form';
import JCronValidator from '/@/components/Form/src/jeecg/components/JEasyCron/validator';
import { usePermission } from '/@/hooks/web/usePermission';
const { isDisabledAuth } = usePermission();
@@ -935,7 +936,20 @@ export const schemas: FormSchema[] = [
label: '选中值',
colProps: { span: 12 },
},
{
field: 'tabsSelectUser',
component: 'Input',
label: '用户选择',
helpMessage: ['插槽模式-自己保存查询条件'],
slot: 'tabsSelectUser',
colProps: { span: 14 },
},
{
field: 'tabsSelectUser',
component: 'JEllipsis',
label: '选中值',
colProps: { span: 10 },
},
{
field: 'orderAuth',
component: 'Input',
@@ -952,5 +966,4 @@ export const schemas: FormSchema[] = [
label: '选中值',
colProps: { span: 12 },
},
];

View File

@@ -0,0 +1,46 @@
<template>
<a-card :key="key">
<div class="container">
<p class="title">vxe-table 原生加载示例</p>
<template v-if="isRegistered">
<vxe-table :align="allAlign" :data="tableData1">
<vxe-table-column type="seq" width="60"></vxe-table-column>
<vxe-table-column field="name" title="Name"></vxe-table-column>
<vxe-table-column field="sex" title="Sex"></vxe-table-column>
<vxe-table-column field="age" title="Age"></vxe-table-column>
</vxe-table>
</template>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useVxeTableRegister } from '/@/components/jeecg/JVxeTable/useVxeTableRegister';
const allAlign = ref<string | null>(null);
const tableData1 = ref([
{ id: 10001, name: 'Test1', role: 'Develop', sex: 'Man', age: 28, address: 'vxe-table 从入门到放弃' },
{ id: 10002, name: 'Test2', role: 'Test', sex: 'Women', age: 22, address: 'Guangzhou' },
{ id: 10003, name: 'Test3', role: 'PM', sex: 'Man', age: 32, address: 'Shanghai' },
{ id: 10004, name: 'Test4', role: 'Designer', sex: 'Women', age: 24, address: 'Shanghai' },
]);
const key = ref(0);
const isRegistered = ref(false);
useVxeTableRegister().then(() => {
console.log('useVxeTableRegister');
isRegistered.value = true;
// vxetable放在插槽中需要更新key才能重新渲染
key.value++;
});
</script>
<style lang="less" scoped>
.container {
padding: 5px;
background-color: #fff;
.title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
}
}
</style>

View File

@@ -77,13 +77,12 @@
<script lang="ts">
import { defineComponent, ref, reactive, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/src/components/Modal';
import { JVxeTable } from '/src/components/jeecg/JVxeTable';
import { columns, columns1 } from './jvxetable.data';
import { orderCustomerList, orderTicketList, saveOrUpdate } from './jvxetable.api';
import { useJvxeMethod } from '/@/hooks/system/useJvxeMethods.ts';
export default defineComponent({
name: 'JVexTableModal',
components: { BasicModal, JVxeTable },
components: { BasicModal },
emits: ['success', 'register'],
setup(props, { emit }) {
const tableH = ref(300);

View File

@@ -1,153 +1,147 @@
<!--用户选择框-->
<template>
<div>
<BasicModal v-bind="$attrs" @register="register" title="数据对比" width="50%" destroyOnClose :showOkBtn="false">
<a-row :gutter="6" v-if="dataVersionList" style="margin-left: 2px">
<span style="margin-top: 5px; margin-right: 3px; margin-left: 4px">版本对比:</span>
<a-select placeholder="版本号" @change="handleChange1" v-model:value="params.dataId1">
<a-select-option v-for="(log, logindex) in dataVersionList" :key="log.value" :value="log.value">
{{ log.text }}
</a-select-option>
</a-select>
<BasicModal v-bind="$attrs" @register="register" title="数据版本对比" width="60%" destroyOnClose :showOkBtn="false">
<!-- 版本选择区 -->
<div class="compare-header">
<div class="compare-header__info">
<span class="compare-header__label">数据表</span>
<a-tag color="blue">{{ dataTable }}</a-tag>
<span class="compare-header__label" style="margin-left: 16px">数据ID</span>
<span class="compare-header__id">{{ dataId }}</span>
</div>
<div class="compare-header__selector">
<span class="compare-header__label">版本对比</span>
<a-select
placeholder="选择版本"
@change="handleChange1"
v-model:value="params.dataId1"
style="width: 120px"
>
<a-select-option v-for="log in dataVersionList" :key="log.value" :value="log.value">
V{{ log.text }}
</a-select-option>
</a-select>
<span class="compare-header__vs">VS</span>
<a-select
placeholder="选择版本"
@change="handleChange2"
v-model:value="params.dataId2"
style="width: 120px"
>
<a-select-option v-for="log in dataVersionList" :key="log.value" :value="log.value">
V{{ log.text }}
</a-select-option>
</a-select>
</div>
</div>
<a-select placeholder="版本号" @change="handleChange2" style="padding-left: 10px" v-model:value="params.dataId2">
<a-select-option v-for="(log, logindex) in dataVersionList" :key="log.value" :value="log.value">
{{ log.text }}
</a-select-option>
</a-select>
</a-row>
<BasicTable
:columns="columns"
v-bind="getBindValue"
:rowClassName="setDataCss"
:striped="false"
:showIndexColumn="false"
:pagination="false"
:canResize="false"
:bordered="true"
:dataSource="dataSource"
:searchInfo="searchInfo"
v-if="isUpdate"
>
<template #dataVersionTitle1="{ record }"> <Icon icon="icon-park-outline:grinning-face" /> 版本:{{ dataVersion1Num }} </template>
<template #dataVersionTitle2="{ record }"> <Icon icon="icon-park-outline:grinning-face" /> 版本:{{ dataVersion2Num }} </template>
<template #avatarslot="{ record }">
<div class="anty-img-wrap" v-if="record.dataVersion1 != record.dataVersion2">
<Icon icon="mdi:arrow-right-bold" style="color: red"></Icon>
</div>
</template>
</BasicTable>
<!-- 差异统计 -->
<div class="compare-stats" v-if="dataSource.length > 0">
<a-tag color="red">{{ diffCount }} 处差异</a-tag>
<a-tag color="green">{{ dataSource.length - diffCount }} 处相同</a-tag>
<span class="compare-stats__total"> {{ dataSource.length }} 个字段</span>
</div>
<!-- 对比表格 -->
<div class="compare-table" v-if="isUpdate">
<table class="compare-table__inner">
<thead>
<tr>
<th class="col-field">字段名</th>
<th class="col-value">
<span class="version-tag version-tag--left">V{{ dataVersion1Num }}</span>
</th>
<th class="col-status"></th>
<th class="col-value">
<span class="version-tag version-tag--right">V{{ dataVersion2Num }}</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in dataSource" :key="idx" :class="{ 'row-diff': row.isDiff, 'row-same': !row.isDiff }">
<td class="col-field">
<span class="field-name">{{ row.code }}</span>
</td>
<td class="col-value" :class="{ 'cell-diff': row.isDiff }">
<span class="cell-text">{{ formatValue(row.dataVersion1) }}</span>
</td>
<td class="col-status">
<span v-if="row.isDiff" class="diff-icon"></span>
<span v-else class="same-icon">=</span>
</td>
<td class="col-value" :class="{ 'cell-diff': row.isDiff }">
<span class="cell-text">{{ formatValue(row.dataVersion2) }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</BasicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, unref, ref, reactive, watch } from 'vue';
import { defineComponent, unref, ref, reactive, computed } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { queryCompareList, queryDataVerList } from './datalog.api';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { selectProps } from '/@/components/Form/src/jeecg/props/props';
import { useMessage } from '/@/hooks/web/useMessage';
export default defineComponent({
name: 'DataLogCompareModal',
components: {
//此处需要异步加载BasicTable
BasicModal,
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), { loading: true }),
},
props: {
...selectProps,
},
emits: ['register', 'btnOk'],
setup(props, { emit, refs }) {
setup() {
const { createMessage } = useMessage();
const attrs = useAttrs();
const getBindValue = Object.assign({}, unref(props), unref(attrs));
const dataSource = ref([]);
const dataSource = ref<any[]>([]);
const dataVersion1Num = ref('');
const dataVersion2Num = ref('');
const isUpdate = ref(true);
const searchInfo = {};
const dataId1 = ref('');
const dataId2 = ref('');
const dataId = ref('');
const dataTable1 = ref('');
const dataID3 = ref('');
const dataTable = ref('');
const confirmLoading = ref(false);
const dataVersionList = ref([]);
let params = reactive({ dataId1: '', dataId2: '' });
let dataLog = reactive({});
const [register, { setModalProps, closeModal }] = useModalInner(async (data) => {
const dataVersionList = ref<any[]>([]);
const params = reactive({ dataId1: '', dataId2: '' });
const diffCount = computed(() => dataSource.value.filter((r) => r.isDiff).length);
const [register, { setModalProps }] = useModalInner(async (data) => {
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
let checkedRows = data.selectedRows;
const checkedRows = data.selectedRows;
dataTable.value = checkedRows[0].dataTable;
dataId.value = checkedRows[0].dataId;
dataId1.value = checkedRows[0].id;
dataId2.value = checkedRows[1].id;
params.dataId1 = dataId1.value;
params.dataId2 = dataId2.value;
params.dataId1 = checkedRows[0].id;
params.dataId2 = checkedRows[1].id;
await initDataVersionList();
await initTableData();
}
});
//定义表格列
const columns = [
{
title: '字段名',
dataIndex: 'code',
width: 20,
align: 'left',
},
{
dataIndex: 'dataVersion1',
align: 'left',
width: 60,
slots: { title: 'dataVersionTitle1' },
},
{
title: '',
dataIndex: 'imgshow',
align: 'center',
slots: { customRender: 'avatarslot' },
width: 5,
},
{
align: 'left',
dataIndex: 'dataVersion2',
width: 60,
filters: [],
filterMultiple: false,
slots: { title: 'dataVersionTitle2' },
},
];
async function initTableData() {
console.info('params', params);
queryCompareList(unref(params)).then((res) => {
console.info('test', res);
dataVersion1Num.value = res[0].dataVersion;
dataVersion2Num.value = res[1].dataVersion;
let json1 = JSON.parse(res[0].dataContent);
let json2 = JSON.parse(res[1].dataContent);
let data = [];
for (var item1 in json1) {
for (var item2 in json2) {
if (item1 == item2) {
data.push({
code: item1,
imgshow: '',
dataVersion1: json1[item1],
dataVersion2: json2[item2],
});
}
}
}
const json1 = JSON.parse(res[0].dataContent);
const json2 = JSON.parse(res[1].dataContent);
// 收集所有字段(兼顾两边都有的和只有一边有的)
const allKeys = new Set([...Object.keys(json1), ...Object.keys(json2)]);
const data: any[] = [];
allKeys.forEach((fieldKey) => {
const v1 = json1[fieldKey] ?? '';
const v2 = json2[fieldKey] ?? '';
data.push({
code: fieldKey,
dataVersion1: v1,
dataVersion2: v2,
isDiff: String(v1) !== String(v2),
});
});
// 差异项排前面
data.sort((a, b) => (a.isDiff === b.isDiff ? 0 : a.isDiff ? -1 : 1));
dataSource.value = data;
});
}
function handleChange1(value) {
if (params.dataId2 == value) {
createMessage.warning('相同版本号不能比较');
@@ -156,6 +150,7 @@
params.dataId1 = value;
initTableData();
}
function handleChange2(value) {
if (params.dataId1 == value) {
createMessage.warning('相同版本号不能比较');
@@ -164,57 +159,223 @@
params.dataId2 = value;
initTableData();
}
function setDataCss(record) {
let className = 'trcolor';
const dataVersion1 = record.dataVersion1;
const dataVersion2 = record.dataVersion2;
if (dataVersion1 != dataVersion2) {
return className;
}
}
async function initDataVersionList() {
queryDataVerList({ dataTable: dataTable.value, dataId: dataId.value }).then((res) => {
dataVersionList.value = res.map((value, key, arr) => {
let item = {};
item['text'] = value['dataVersion'];
item['value'] = value['id'];
return item;
});
dataVersionList.value = res.map((value) => ({
text: value['dataVersion'],
value: value['id'],
}));
});
}
function formatValue(val) {
if (val === null || val === undefined || val === '') return '--';
return String(val);
}
return {
//config,
searchInfo,
dataSource,
setDataCss,
isUpdate,
dataVersionList,
dataVersion1Num,
dataVersion2Num,
queryCompareList,
initDataVersionList,
register,
handleChange1,
handleChange2,
params,
getBindValue,
columns,
dataTable,
dataId,
diffCount,
formatValue,
};
},
});
</script>
<style scoped>
.anty-img-wrap {
height: 25px;
position: relative;
<style lang="less" scoped>
.compare-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #fafafa;
border-radius: 6px;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 8px;
&__info {
display: flex;
align-items: center;
}
&__label {
font-size: 13px;
color: #8c8c8c;
white-space: nowrap;
}
&__id {
font-size: 12px;
color: #595959;
font-family: 'Consolas', 'Monaco', monospace;
word-break: break-all;
}
&__selector {
display: flex;
align-items: center;
gap: 8px;
}
&__vs {
font-weight: 600;
color: #faad14;
font-size: 14px;
}
}
.anty-img-wrap > img {
max-height: 100%;
.compare-stats {
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
&__total {
font-size: 12px;
color: #8c8c8c;
margin-left: 4px;
}
}
.marginCss {
margin-top: 20px;
.compare-table {
border: 1px solid #f0f0f0;
border-radius: 6px;
overflow: hidden;
&__inner {
width: 100%;
border-collapse: collapse;
font-size: 13px;
thead {
tr {
background: #fafafa;
}
th {
padding: 10px 12px;
font-weight: 500;
color: #595959;
border-bottom: 1px solid #f0f0f0;
text-align: left;
}
}
tbody {
tr {
transition: background 0.2s;
&:hover {
background: #fafafa;
}
&:not(:last-child) td {
border-bottom: 1px solid #f5f5f5;
}
}
td {
padding: 8px 12px;
color: #333;
vertical-align: top;
}
}
}
}
.col-field {
width: 140px;
min-width: 120px;
}
.col-value {
width: 40%;
}
.col-status {
width: 36px;
text-align: center !important;
}
.field-name {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
color: #1890ff;
}
.cell-text {
word-break: break-all;
font-size: 12px;
line-height: 1.5;
}
.row-diff {
.field-name {
font-weight: 600;
}
}
.cell-diff {
background: #fff7e6;
.cell-text {
color: #d46b08;
font-weight: 500;
}
}
.diff-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 50%;
background: #fff1f0;
color: #ff4d4f;
font-size: 12px;
font-weight: 700;
}
.same-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 50%;
background: #f6ffed;
color: #52c41a;
font-size: 12px;
font-weight: 700;
}
.version-tag {
display: inline-block;
padding: 2px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&--left {
background: #e6f7ff;
color: #1890ff;
}
&--right {
background: #f6ffed;
color: #52c41a;
}
}
</style>

View File

@@ -1,31 +1,80 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
import { h } from 'vue';
import { Tag, Tooltip } from 'ant-design-vue';
export const columns: BasicColumn[] = [
{
title: '表名',
dataIndex: 'dataTable',
width: 150,
width: 120,
align: 'left',
customRender: ({ text }) => {
return h(Tag, { color: 'blue' }, () => text);
},
},
{
title: '数据ID',
dataIndex: 'dataId',
width: 350,
width: 260,
align: 'left',
ellipsis: true,
customRender: ({ text }) => {
return h(
'span',
{ style: 'font-family: Consolas, Monaco, monospace; font-size: 12px; color: #595959' },
text
);
},
},
{
title: '版本号',
dataIndex: 'dataVersion',
width: 100,
width: 70,
align: 'center',
customRender: ({ text }) => {
return h(Tag, { color: 'green' }, () => 'V' + text);
},
},
{
title: '数据内容',
dataIndex: 'dataContent',
ellipsis: true,
customRender: ({ text }) => {
if (!text) return '--';
// 尝试格式化 JSON 显示关键字段
try {
const obj = JSON.parse(text);
const keys = Object.keys(obj);
const preview = keys
.slice(0, 3)
.map((k) => {
const v = obj[k];
const val = v === null || v === undefined || v === '' ? '--' : String(v);
return `${k}: ${val.length > 20 ? val.substring(0, 20) + '...' : val}`;
})
.join(' | ');
const suffix = keys.length > 3 ? ` (+${keys.length - 3} 字段)` : '';
return h(
Tooltip,
{ title: JSON.stringify(obj, null, 2), overlayStyle: { maxWidth: '500px', whiteSpace: 'pre-wrap', fontFamily: 'Consolas, monospace', fontSize: '12px' } },
() => h('span', { style: 'font-size: 12px; color: #595959' }, preview + suffix)
);
} catch {
return text;
}
},
},
{
title: '创建人',
dataIndex: 'createBy',
sorter: true,
width: 200,
width: 90,
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 120,
sorter: true,
},
];
@@ -34,12 +83,27 @@ export const searchFormSchema: FormSchema[] = [
field: 'dataTable',
label: '表名',
component: 'Input',
colProps: { span: 8 },
componentProps: {
placeholder: '请输入表名',
},
colProps: { span: 6 },
},
{
field: 'dataId',
label: '数据ID',
component: 'Input',
colProps: { span: 8 },
componentProps: {
placeholder: '请输入数据ID',
},
colProps: { span: 6 },
},
{
field: 'createBy',
label: '创建人',
component: 'Input',
componentProps: {
placeholder: '请输入创建人',
},
colProps: { span: 6 },
},
];

View File

@@ -2,34 +2,38 @@
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button preIcon="ant-design:plus-outlined" type="primary" @click="handleCompare" style="margin-right: 5px">数据比较</a-button>
<a-button preIcon="ant-design:swap-outlined" type="primary" size="small" @click="handleCompare">数据比较</a-button>
<span v-if="selectedRowKeys.length === 0" class="compare-tip">请勾选两条相同数据ID的记录</span>
<a-tag v-else-if="selectedRowKeys.length === 1" color="warning" style="margin-left: 10px">再选一条相同数据ID的记录</a-tag>
<a-tag v-else-if="selectedRowKeys.length === 2 && !isSameDataId" color="error" style="margin-left: 10px">数据ID不一致无法比较</a-tag>
<a-tag v-else-if="selectedRowKeys.length === 2 && isSameDataId" color="success" style="margin-left: 10px">可以比较 V{{ selectedRows[0]?.dataVersion }} vs V{{ selectedRows[1]?.dataVersion }}</a-tag>
<a-tag v-else color="error" style="margin-left: 10px">只能选择两条记录</a-tag>
</template>
</BasicTable>
<DataLogCompareModal @register="registerModal" @success="reload" />
</div>
</template>
<script lang="ts" name="monitor-datalog" setup>
import { ref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { computed } from 'vue';
import { BasicTable } from '/@/components/Table';
import DataLogCompareModal from './DataLogCompareModal.vue';
const [registerModal, { openModal }] = useModal();
import { getDataLogList } from './datalog.api';
import { columns, searchFormSchema } from './datalog.data';
import { useMessage } from '/@/hooks/web/useMessage';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
const { createMessage } = useMessage();
const checkedRows = ref<Array<object | number>>([]);
// 列表页面公共参数、方法
const { prefixCls, tableContext } = useListPage({
const [registerModal, { openModal }] = useModal();
const { createMessage } = useMessage();
const { tableContext } = useListPage({
designScope: 'datalog-template',
tableProps: {
title: '数据日志列表',
api: getDataLogList,
columns: columns,
formConfig: {
labelWidth: 120,
labelWidth: 80,
schemas: searchFormSchema,
},
actionColumn: false,
@@ -38,20 +42,32 @@
const [registerTable, { reload }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
const isSameDataId = computed(() => {
const rows = selectedRows.value;
if (!rows || rows.length !== 2) return false;
return rows[0].dataId === rows[1].dataId;
});
function handleCompare() {
let obj = selectedRows.value;
console.info('sfsfsf', obj);
if (!obj || obj.length != 2) {
createMessage.warning('请选择两条数据!');
return false;
const rows = selectedRows.value;
if (!rows || rows.length !== 2) {
createMessage.warning('请选择两条数据进行比较!');
return;
}
if (obj[0].dataId != obj[1].dataId) {
createMessage.warning('请选择相同的数据库表和数据ID进行比较!');
return false;
if (rows[0].dataId !== rows[1].dataId) {
createMessage.warning('请选择相同数据ID的记录进行比较');
return;
}
openModal(true, {
selectedRows,
selectedRows: rows,
isUpdate: true,
});
}
</script>
<style lang="less" scoped>
.compare-tip {
margin-left: 10px;
font-size: 12px;
color: #999;
}
</style>

View File

@@ -27,9 +27,9 @@
>
</div>
<div v-if="searchInfo.logType == 4">
<div style="margin-bottom: 5px">
<a-badge status="success" style="vertical-align: middle" />
<span class="error-box" style="vertical-align: middle">异常堆栈:{{ record.requestParam }}</span>
<div class="error-section">
<div class="error-label"><a-badge status="error" /> 异常堆栈:</div>
<pre class="error-box">{{ record.requestParam }}</pre>
</div>
</div>
</template>
@@ -105,19 +105,42 @@
}
</script>
<style lang="less" scoped>
.error-box {
white-space: break-spaces;
.error-section {
.error-label {
font-weight: 500;
margin-bottom: 8px;
color: #ff4d4f;
}
}
.error-box {
margin: 0;
padding: 12px 16px;
background: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow-y: auto;
color: #595959;
}
.table-title-bar {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.export-btn {
margin-left: auto;
}
:deep(.jeecg-basic-table-header__toolbar){
width:100px !important;
:deep(.jeecg-basic-table-header__toolbar) {
width: 100px !important;
}
</style>

View File

@@ -4,8 +4,9 @@ export const columns: BasicColumn[] = [
{
title: '日志内容',
dataIndex: 'logContent',
width: 100,
width: 200,
align: 'left',
ellipsis: true,
},
{
title: '操作人ID',
@@ -20,12 +21,12 @@ export const columns: BasicColumn[] = [
{
title: 'IP',
dataIndex: 'ip',
width: 80,
width: 60,
},
{
title: '耗时(毫秒)',
dataIndex: 'costTime',
width: 80,
width: 50,
},
{
title: '创建时间',
@@ -36,7 +37,7 @@ export const columns: BasicColumn[] = [
{
title: '客户端类型',
dataIndex: 'clientType_dictText',
width: 60,
width: 50,
},
];
@@ -56,30 +57,35 @@ export const exceptionColumns: BasicColumn[] = [
{
title: '异常标题',
dataIndex: 'logContent',
width: 100,
width: 200,
align: 'left',
ellipsis: true,
},
{
title: '请求地址',
dataIndex: 'requestUrl',
width: 100,
width: 140,
align: 'left',
ellipsis: true,
},
{
title: '请求参数',
title: '请求方法',
dataIndex: 'method',
width: 60,
width: 120,
align: 'left',
ellipsis: true,
},
{
title: '操作人',
dataIndex: 'username',
width: 60,
width: 80,
customRender: ({ record }) => {
let pname = record.username;
let pid = record.userid;
if(!pname && !pid){
return "";
const pname = record.username;
const pid = record.userid;
if (!pname && !pid) {
return '';
}
return pname + " (账号: "+ pid + " )";
return pname + ' (' + pid + ')';
},
},
{
@@ -91,12 +97,12 @@ export const exceptionColumns: BasicColumn[] = [
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
width: 60,
width: 80,
},
{
title: '客户端类型',
dataIndex: 'clientType_dictText',
width: 60,
width: 50,
},
];

View File

@@ -33,7 +33,7 @@
</template>
</a-card-meta>
<a-divider />
<div v-html="content.msgContent" class="article-content"></div>
<div v-html="removeSpecialTags(content.msgContent)" class="article-content"></div>
<div>
<a-button v-if="hasHref" @click="jumpToHandlePage">前往办理<ArrowRightOutlined /></a-button>
</div>
@@ -81,6 +81,7 @@
import { getToken } from '@/utils/auth';
import {defHttp} from "@/utils/http/axios";
import {$electron} from "@/electron";
import { removeSpecialTags } from '@/utils/index';
const router = useRouter();
const glob = useGlobSetting();
const isUpdate = ref(true);
@@ -279,7 +280,9 @@
function handleViewFile(filePath) {
if (filePath) {
console.log('glob.onlineUrl', glob.viewUrl);
let url = encodeURIComponent(encryptByBase64(filePath));
//update-begin-author:scott---date:2026-04-16--for: 【Github #8855】修复文件预览路径处理问题filePath需要先拼接完整URL再编码
let url = encodeURIComponent(encryptByBase64(getFileAccessHttpUrl(filePath)));
//update-end-author:scott---date:2026-04-16--for: 【Github #8855】修复文件预览路径处理问题filePath需要先拼接完整URL再编码
let previewUrl = `${glob.viewUrl}?url=` + url;
//update-begin-author:liusq---date:2025-12-16--for: JHHB-1139桌面端 文件预览统一修改
if($electron.isElectron()){
@@ -377,6 +380,20 @@
max-width: 100%;
height: auto;
}
/* 修复 Word 复制内容中表格边框丢失和间隔问题 */
.article-content {
:deep(table) {
border-collapse: collapse !important;
border-spacing: 0 !important;
}
:deep(table td),
:deep(table th) {
border: 1px solid #d0d0d0;
padding: 4px 8px;
min-width: 20px;
word-break: break-word;
}
}
.basic-title{
position: relative;
display: flex;

View File

@@ -19,10 +19,10 @@ export const options = {
a: ['style', 'target', 'href', 'title', 'rel'],
img: ['style', 'src', 'title','width','height'],
div: ['style'],
table: ['style', 'width', 'border', 'height'],
tr: ['style'],
td: ['style', 'width', 'colspan'],
th: ['style', 'width', 'colspan'],
table: ['style', 'width', 'border', 'height', 'cellspacing', 'cellpadding'],
tr: ['style', 'valign', 'align'],
td: ['style', 'width', 'colspan', 'rowspan', 'border', 'valign', 'align'],
th: ['style', 'width', 'colspan', 'rowspan', 'border', 'valign', 'align'],
tbody: ['style'],
ul: ['style'],
li: ['style'],

View File

@@ -1,6 +1,6 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
import { render } from '/@/utils/common/renderUtils';
import { JCronValidator } from '/@/components/Form';
import JCronValidator from '/@/components/Form/src/jeecg/components/JEasyCron/validator';
export const columns: BasicColumn[] = [
{

View File

@@ -1,130 +1,188 @@
<template>
<div class="p-4">
<a-card>
<!-- Redis 信息实时监控 -->
<a-row :gutter="8">
<a-col :sm="24" :xl="12">
<div ref="chartRef" style="width: 100%; height: 300px"></div>
</a-col>
<a-col :sm="24" :xl="12">
<div ref="chartRef2" style="width: 100%; height: 300px"></div>
</a-col>
</a-row>
</a-card>
<div class="redis-monitor p-4">
<!-- 顶部概览卡片 -->
<a-row :gutter="16" class="overview-row">
<a-col :sm="12" :md="6" v-for="item in overviewCards" :key="item.label">
<div class="overview-card" :style="{ borderTopColor: item.color }">
<div class="overview-card__value" :style="{ color: item.color }">{{ item.value }}</div>
<div class="overview-card__label">{{ item.label }}</div>
</div>
</a-col>
</a-row>
<BasicTable @register="registerTable" :api="getInfo"></BasicTable>
<!-- Redis 信息实时监控 -->
<a-row :gutter="16" class="chart-row">
<a-col :sm="24" :xl="12">
<a-card :bordered="false" class="chart-card">
<div ref="chartRef" style="width: 100%; height: 300px"></div>
</a-card>
</a-col>
<a-col :sm="24" :xl="12">
<a-card :bordered="false" class="chart-card">
<div ref="chartRef2" style="width: 100%; height: 300px"></div>
</a-card>
</a-col>
</a-row>
<!-- Redis 详细信息表格 -->
<a-card :bordered="false" class="table-card" title="Redis 配置详情">
<BasicTable @register="registerTable" :api="getInfo" :canResize="false"></BasicTable>
</a-card>
</div>
</template>
<script lang="ts" name="monitor-redis" setup>
import { onMounted, ref, reactive, Ref, onUnmounted } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { onMounted, ref, reactive, Ref, onUnmounted, computed } from 'vue';
import { BasicTable, useTable } from '/@/components/Table';
import { getInfo, getRedisInfo, getMetricsHistory } from './redis.api';
import dayjs from 'dayjs';
import { columns } from './redis.data';
import { useMessage } from '/@/hooks/web/useMessage';
import { useECharts } from '/@/hooks/web/useECharts';
const dataSource = ref([]);
const chartRef = ref<HTMLDivElement | null>(null);
const chartRef2 = ref<HTMLDivElement | null>(null);
const { setOptions, echarts } = useECharts(chartRef as Ref<HTMLDivElement>);
const { setOptions: setOptions2, echarts: echarts2 } = useECharts(chartRef2 as Ref<HTMLDivElement>);
const loading = ref(false);
let timer = null;
const { createMessage } = useMessage();
const key = reactive({
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
const { setOptions: setOptions2 } = useECharts(chartRef2 as Ref<HTMLDivElement>);
let timer: any = null;
// 概览数据
const currentMemory = ref('--');
const currentKeys = ref('--');
const currentUptime = ref('--');
const currentPort = ref('--');
const overviewCards = computed(() => [
{ label: '已用内存', value: currentMemory.value, color: '#1890ff' },
{ label: 'Key 数量', value: currentKeys.value, color: '#52c41a' },
{ label: '运行时间', value: currentUptime.value, color: '#faad14' },
{ label: '监听端口', value: currentPort.value, color: '#722ed1' },
]);
const memoryOption = reactive({
title: {
text: 'Redis Key 实时数量(个',
text: 'Redis 内存实时占用KB',
textStyle: { fontSize: 14, fontWeight: 500, color: '#333' },
},
xAxis: {
type: 'category',
boundaryGap: false,
data: [],
},
yAxis: {
type: 'value',
},
series: [
{
data: [],
type: 'line',
areaStyle: {
color: '#ff6987',
},
lineStyle: {
color: '#dc143c',
width: 10,
type: 'solid',
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255,255,255,0.96)',
borderColor: '#e8e8e8',
borderWidth: 1,
textStyle: { color: '#333', fontSize: 12 },
formatter(params) {
const p = params[0];
return `<div style="font-weight:500;margin-bottom:4px">${p.axisValue}</div>
<span style="color:#1890ff">● 内存</span>${p.value} KB`;
},
],
});
const memory = reactive({
title: {
text: 'Redis 内存实时占用情况KB',
},
grid: { top: 50, right: 20, bottom: 30, left: 60 },
xAxis: {
type: 'category',
boundaryGap: false,
data: [],
axisLine: { lineStyle: { color: '#d9d9d9' } },
axisTick: { show: false },
axisLabel: { color: '#8c8c8c', fontSize: 11 },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } },
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { color: '#8c8c8c', fontSize: 11 },
},
series: [
{
data: [],
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 4,
showSymbol: false,
lineStyle: { color: '#1890ff', width: 2 },
itemStyle: { color: '#1890ff' },
areaStyle: {
color: '#74bcff',
},
lineStyle: {
color: '#1890ff',
width: 10,
type: 'solid',
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(24,144,255,0.25)' },
{ offset: 1, color: 'rgba(24,144,255,0.02)' },
],
},
},
},
],
});
const [registerTable, { reload }] = useTable({
const keyOption = reactive({
title: {
text: 'Redis Key 实时数量(个)',
textStyle: { fontSize: 14, fontWeight: 500, color: '#333' },
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255,255,255,0.96)',
borderColor: '#e8e8e8',
borderWidth: 1,
textStyle: { color: '#333', fontSize: 12 },
formatter(params) {
const p = params[0];
return `<div style="font-weight:500;margin-bottom:4px">${p.axisValue}</div>
<span style="color:#52c41a">● Key 数量</span>${p.value}`;
},
},
grid: { top: 50, right: 20, bottom: 30, left: 60 },
xAxis: {
type: 'category',
boundaryGap: false,
data: [],
axisLine: { lineStyle: { color: '#d9d9d9' } },
axisTick: { show: false },
axisLabel: { color: '#8c8c8c', fontSize: 11 },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } },
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { color: '#8c8c8c', fontSize: 11 },
},
series: [
{
data: [],
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 4,
showSymbol: false,
lineStyle: { color: '#52c41a', width: 2 },
itemStyle: { color: '#52c41a' },
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(82,196,26,0.25)' },
{ offset: 1, color: 'rgba(82,196,26,0.02)' },
],
},
},
},
],
});
const [registerTable] = useTable({
columns,
showIndexColumn: false,
pagination: false,
bordered: true,
canResize: false,
showTableSetting: false,
});
// 获取一组数据中最大和最小的值
function getMaxAndMin(dataSource, field) {
let maxValue = null,
minValue = null;
dataSource.forEach((item) => {
let value = Number.parseInt(item[field]);
// max
if (maxValue == null) {
maxValue = value;
} else if (value > maxValue) {
maxValue = value;
}
// min
if (minValue == null) {
minValue = value;
} else if (value < minValue) {
minValue = value;
}
});
return [maxValue, minValue];
}
function loadRedisInfo() {
getInfo().then((res) => {
dataSource.value = res.result;
});
}
function initCharts() {
setOptions(memory);
setOptions2(key);
setOptions(memoryOption);
setOptions2(keyOption);
}
/** 开启定时器 */
@@ -141,70 +199,127 @@
if (timer) clearInterval(timer);
}
/**
* 加载历史监控数据
*/
/** 加载历史监控数据 */
function loadHistoryData() {
getMetricsHistory().then((res) => {
let dbSizes = res.dbSize;
let memories = res.memory;
const dbSizes = res.dbSize;
const memories = res.memory;
dbSizes.forEach((dbSize) => {
key.xAxis.data.push(dayjs(dbSize.create_time).format('hh:mm:ss'));
key.series[0].data.push(dbSize.dbSize);
keyOption.xAxis.data.push(dayjs(dbSize.create_time).format('HH:mm:ss'));
keyOption.series[0].data.push(dbSize.dbSize);
});
memories.forEach((memoryData) => {
memory.xAxis.data.push(dayjs(memoryData.create_time).format('hh:mm:ss'));
memory.series[0].data.push(memoryData.used_memory / 1000);
memoryOption.xAxis.data.push(dayjs(memoryData.create_time).format('HH:mm:ss'));
memoryOption.series[0].data.push(memoryData.used_memory / 1000);
});
setOptions(memory, false);
setOptions2(key, false);
// 更新概览卡片
if (memories.length > 0) {
const lastMem = memories[memories.length - 1].used_memory / 1000;
currentMemory.value = lastMem.toFixed(0) + ' KB';
}
if (dbSizes.length > 0) {
currentKeys.value = dbSizes[dbSizes.length - 1].dbSize + '';
}
setOptions(memoryOption, false);
setOptions2(keyOption, false);
});
// 加载详细信息获取端口和运行时间
getInfo().then((res) => {
const list = res.result || res;
if (Array.isArray(list)) {
list.forEach((item) => {
if (item.key === 'tcp_port') currentPort.value = item.value;
if (item.key === 'uptime_in_days') currentUptime.value = item.value + ' 天';
});
}
});
}
function loadData() {
getRedisInfo()
.then((res) => {
let time = dayjs().format('hh:mm:ss');
let [{ dbSize: currentSize }, memoryInfo] = res;
let currentMemory = memoryInfo.used_memory / 1000;
// push 数据
key.xAxis.data.push(time);
key.series[0].data.push(currentSize);
memory.xAxis.data.push(time);
memory.series[0].data.push(currentMemory);
const time = dayjs().format('HH:mm:ss');
const [{ dbSize: curSize }, memInfo] = res;
const curMem = memInfo.used_memory / 1000;
keyOption.xAxis.data.push(time);
keyOption.series[0].data.push(curSize);
memoryOption.xAxis.data.push(time);
memoryOption.series[0].data.push(curMem);
// 更新概览
currentMemory.value = curMem.toFixed(0) + ' KB';
currentKeys.value = curSize + '';
// 最大长度为80
if (key.series[0].data.length > 80) {
key.xAxis.data.splice(0, 1);
key.series[0].data.splice(0, 1);
memory.xAxis.data.splice(0, 1);
memory.series[0].data.splice(0, 1);
if (keyOption.series[0].data.length > 80) {
keyOption.xAxis.data.splice(0, 1);
keyOption.series[0].data.splice(0, 1);
memoryOption.xAxis.data.splice(0, 1);
memoryOption.series[0].data.splice(0, 1);
}
setOptions(memory, false);
setOptions2(key, false);
// 计算 Key 最大最小值
//let keyPole = getMaxAndMin(key.dataSource, 'y');
//key.max = Math.floor(keyPole[0]) + 10;
//key.min = Math.floor(keyPole[1]) - 10;
//if (key.min < 0) this.key.min = 0;
// 计算 Memory 最大最小值
//let memoryPole = getMaxAndMin(memory.dataSource, 'y');
//memory.max = Math.floor(memoryPole[0]) + 100;
//memory.min = Math.floor(memoryPole[1]) - 100;
//if (memory.min < 0) memory.min = 0;
setOptions(memoryOption, false);
setOptions2(keyOption, false);
})
.catch((e) => {
//closeTimer()
});
.catch(() => {});
}
onMounted(() => {
initCharts();
openTimer();
});
// 代码逻辑说明: 【issues-615】系统监控中的REDIS监控页面打开再关闭后没有关闭计时器
onUnmounted(() => {
closeTimer();
});
</script>
<style lang="less" scoped>
.redis-monitor {
.overview-row {
margin-bottom: 16px;
}
.overview-card {
background: #fff;
border-radius: 6px;
padding: 20px 24px;
border-top: 3px solid #1890ff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.3s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
&__value {
font-size: 28px;
font-weight: 600;
line-height: 1.2;
margin-bottom: 4px;
}
&__label {
font-size: 13px;
color: #8c8c8c;
}
}
.chart-row {
margin-bottom: 16px;
}
.chart-card {
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
:deep(.ant-card-body) {
padding: 16px;
}
}
.table-card {
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
}
</style>

View File

@@ -2,18 +2,25 @@ import { BasicColumn } from '/@/components/Table';
export const columns: BasicColumn[] = [
{
title: 'Key',
title: '配置项',
dataIndex: 'key',
width: 100,
width: 120,
align: 'left',
customRender: ({ text }) => {
return text;
},
},
{
title: 'Description',
title: '说明',
dataIndex: 'description',
width: 80,
width: 200,
align: 'left',
ellipsis: true,
},
{
title: 'Value',
title: '',
dataIndex: 'value',
width: 80,
align: 'right',
},
];

View File

@@ -11,35 +11,34 @@ export const columns: BasicColumn[] = [
align:"center",
dataIndex: 'name'
},
{
title: '请求方法',
align:"center",
dataIndex: 'requestMethod'
},
{
title: '接口地址',
align:"center",
dataIndex: 'requestUrl'
dataIndex: 'requestUrl',
width: 120,
},
{
title: 'IP 黑名单',
title: '请求方式',
align:"center",
dataIndex: 'blackList'
},
// {
// title: '状态',
// align:"center",
// dataIndex: 'status'
// },
{
title: '创建人',
align:"center",
dataIndex: 'createBy'
dataIndex: 'requestMethod',
width: 100,
},
{
title: '创建时间',
title: '原始接口',
align:"center",
dataIndex: 'createTime'
dataIndex: 'originUrl',
ellipsis: true,
},
{
title: 'IP 白名单',
align:"center",
dataIndex: 'whiteList',
ellipsis: true,
customRender: ({ text }) => {
if (!text) return '不限制';
const count = text.split(/[,\n]/).filter(item => item.trim()).length;
return count + ' 条规则';
}
},
];
//查询数据
@@ -50,8 +49,8 @@ export const searchFormSchema: FormSchema[] = [
component: 'JInput',
},
{
label: "创建人",
field: "createBy",
label: "接口地址",
field: "requestUrl",
component: 'JInput',
},
];
@@ -68,12 +67,35 @@ export const formSchema: FormSchema[] = [
},
},
{
label: '原始地址',
label: '原始接口',
field: 'originUrl',
component: 'Input',
componentProps: {
placeholder: '当前系统的原始接口地址,如 /sys/user/list',
},
helpMessage: '当前系统中被代理的原始接口路径',
dynamicRules: () => {
return [
{ required: true, message: '请输入原始接口路径!' },
{
validator: (_, value) => {
if (value && !value.startsWith('/')) {
return Promise.reject('原始接口路径必须以 / 开头');
}
if (value && value.includes('//')) {
return Promise.reject('原始接口路径不能包含 //');
}
if (value && value.includes('..')) {
return Promise.reject('原始接口路径不能包含 ..');
}
return Promise.resolve();
},
},
];
},
},
{
label: '请求方',
label: '请求方',
field: 'requestMethod',
component: 'JSearchSelect',
componentProps:{
@@ -112,7 +134,7 @@ export const formSchema: FormSchema[] = [
},
dynamicRules: ({model,schema}) => {
return [
{ required: true, message: '请输入请求方!'},
{ required: true, message: '请输入请求方!'},
];
},
},
@@ -123,14 +145,36 @@ export const formSchema: FormSchema[] = [
dynamicDisabled:true
},
{
label: 'IP 名单',
field: 'blackList',
component: 'Input',
label: 'IP 名单',
field: 'whiteList',
helpMessage: '支持精确IP、CIDR网段如192.168.1.0/24、通配符如10.2.3.*),每行一个或逗号分隔,为空则不限制',
component: 'InputTextArea',
slot: 'whiteListSlot',
componentProps: {
rows: 5,
placeholder: '示例:\n192.168.1.100\n10.0.0.0/8\n172.16.*.*',
},
colProps: { span: 24 },
},
{
label: '请求体内容',
component:"Input",
field: 'body'
label: '备注',
field: 'comment',
component: 'InputTextArea',
componentProps: {
rows: 2,
placeholder: '请输入白名单备注说明',
},
colProps: { span: 24 },
},
{
label: '接口描述',
field: 'description',
component: 'InputTextArea',
componentProps: {
rows: 3,
placeholder: '请输入接口描述',
},
colProps: { span: 24 },
},
{
label: '删除标识',
@@ -240,6 +284,21 @@ export const openApiHeaderJVxeColumns: JVxeColumn[] = [
defaultValue:'',
customValue: ['1','0']
},
{
title: '参数类型',
key: 'paramType',
type: JVxeTypes.select,
width: '120px',
options: [
{ title: 'string', value: 'string' },
{ title: 'integer', value: 'integer' },
{ title: 'number', value: 'number' },
{ title: 'boolean', value: 'boolean' },
{ title: 'array', value: 'array' },
{ title: 'object', value: 'object' },
],
defaultValue: 'string',
},
{
title: '默认值',
key: 'defaultValue',
@@ -248,6 +307,14 @@ export const openApiHeaderJVxeColumns: JVxeColumn[] = [
placeholder: '请输入${title}',
defaultValue:'',
},
{
title: '示例值',
key: 'example',
type: JVxeTypes.input,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '备注',
key: 'note',
@@ -284,6 +351,21 @@ export const openApiParamJVxeColumns: JVxeColumn[] = [
defaultValue:'',
customValue: ['1','0']
},
{
title: '参数类型',
key: 'paramType',
type: JVxeTypes.select,
width: '120px',
options: [
{ title: 'string', value: 'string' },
{ title: 'integer', value: 'integer' },
{ title: 'number', value: 'number' },
{ title: 'boolean', value: 'boolean' },
{ title: 'array', value: 'array' },
{ title: 'object', value: 'object' },
],
defaultValue: 'string',
},
{
title: '默认值',
key: 'defaultValue',
@@ -292,6 +374,14 @@ export const openApiParamJVxeColumns: JVxeColumn[] = [
placeholder: '请输入${title}',
defaultValue:'',
},
{
title: '示例值',
key: 'example',
type: JVxeTypes.input,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '备注',
key: 'note',
@@ -301,12 +391,45 @@ export const openApiParamJVxeColumns: JVxeColumn[] = [
},
]
export const responseFieldJVxeColumns: JVxeColumn[] = [
{
title: '字段名',
key: 'fieldName',
type: JVxeTypes.input,
width: '200px',
placeholder: '请输入${title}',
defaultValue: '',
},
{
title: '类型',
key: 'fieldType',
type: JVxeTypes.select,
width: '120px',
options: [
{ title: 'string', value: 'string' },
{ title: 'integer', value: 'integer' },
{ title: 'number', value: 'number' },
{ title: 'boolean', value: 'boolean' },
{ title: 'array', value: 'array' },
{ title: 'object', value: 'object' },
],
defaultValue: 'string',
},
{
title: '说明',
key: 'fieldDesc',
type: JVxeTypes.input,
placeholder: '请输入${title}',
defaultValue: '',
},
];
// 高级查询数据
export const superQuerySchema = {
name: {title: '接口名称',order: 0,view: 'text', type: 'string',},
requestMethod: {title: '请求方',order: 1,view: 'list', type: 'string',dictCode: '',},
requestMethod: {title: '请求方',order: 1,view: 'list', type: 'string',dictCode: '',},
requestUrl: {title: '接口地址',order: 2,view: 'text', type: 'string',},
blackList: {title: 'IP 名单',order: 3,view: 'text', type: 'string',},
whiteList: {title: 'IP 名单',order: 3,view: 'text', type: 'string',},
status: {title: '状态',order: 5,view: 'number', type: 'number',},
createBy: {title: '创建人',order: 6,view: 'text', type: 'string',},
createTime: {title: '创建时间',order: 7,view: 'datetime', type: 'string',},

View File

@@ -1,48 +1,84 @@
import {BasicColumn} from '/@/components/Table';
import {FormSchema} from '/@/components/Table';
import { rules} from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import { getWeekMonthQuarterYear } from '/@/utils';
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
//列表数据
export const columns: BasicColumn[] = [
{
title: '授权名称',
align: "center",
dataIndex: 'name'
title: '授权对象',
align: 'center',
dataIndex: 'name',
},
{
title: 'AK',
align: "center",
dataIndex: 'ak'
},
{
title: 'SK',
align: "center",
dataIndex: 'sk'
title: '访问密钥(AK',
align: 'center',
dataIndex: 'ak',
ellipsis: true,
},
{
title: '创建人',
align: "center",
dataIndex: 'createBy'
align: 'center',
dataIndex: 'createBy',
},
{
title: '创建时间',
align: "center",
dataIndex: 'createTime'
align: 'center',
dataIndex: 'createTime',
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [
{
label: '授权对象',
field: 'name',
component: 'JInput',
},
{
label: '访问密钥',
field: 'ak',
component: 'JInput',
},
];
//授权表单数据
export const authFormSchema: FormSchema[] = [
{
label: '授权对象',
field: 'name',
component: 'Input',
required: true,
},
{
label: '',
field: 'ak',
component: 'Input',
show: false,
},
{
label: '',
field: 'sk',
component: 'Input',
show: false,
},
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
{
label: '',
field: 'systemUserId',
component: 'Input',
show: false,
},
// {
// title: '关联系统用户名',
// align: "center",
// dataIndex: 'createBy',
// },
];
// 高级查询数据
export const superQuerySchema = {
name: {title: '授权名称',order: 0,view: 'text', type: 'string',},
ak: {title: 'AK',order: 1,view: 'text', type: 'string',},
sk: {title: 'SK',order: 2,view: 'text', type: 'string',},
createBy: {title: '关联系统用户名',order: 3,view: 'text', type: 'string',},
createTime: {title: '创建时间',order: 4,view: 'datetime', type: 'string',},
// systemUserId: {title: '关联系统用户名',order: 5,view: 'text', type: 'string',},
name: { title: '授权对象', order: 0, view: 'text', type: 'string' },
ak: { title: '访问密钥(AK', order: 1, view: 'text', type: 'string' },
sk: { title: '签名密钥(SK', order: 2, view: 'text', type: 'string' },
createBy: { title: '创建人', order: 3, view: 'text', type: 'string' },
createTime: { title: '创建时间', order: 4, view: 'datetime', type: 'string' },
};

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