v3.9.2 王炸!大版本前端

This commit is contained in:
JEECG 2026-04-28 15:28:00 +08:00
parent 06955e4ad5
commit c32e59cf64
157 changed files with 11253 additions and 1324 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

@ -31,3 +31,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

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

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

@ -168,13 +168,15 @@
<script type="module" src="/src/main.ts"></script>
<!-- 百度统计 -->
<script>
window.addEventListener('load', function() {
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?0febd9e3cacb3f627ddac64d52caac39";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?0febd9e3cacb3f627ddac64d52caac39";
hm.async = true;
hm.defer = true;
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
});
</script>
</body>

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

@ -114,7 +114,9 @@
if (next) {
const value = next[valueField];
prev.push({
...omit(next, [labelField, valueField]),
// update-begin--author:liaozhiyang---date:20260226---for:issues/9326ApiSelect options
...omit(next, [labelField, valueField, 'options']),
// update-end--author:liaozhiyang---date:20260226---for:issues/9326ApiSelect options
label: next[labelField],
value: numberToString ? `${value}` : value,
});

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),
// allprovince, 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-14694online
const showLastLevelOnly = computed(() => {
return props.displayLevel === 'province' || props.displayLevel === 'city' || props.displayLevel === 'region';
});
// update-end--author:liaozhiyang---date:20260204---for:QQYUN-14694online
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-14694online
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-14694online
});
/**
* 监听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-14694online
const code = arr[0];
cascaderValue.value = buildDisplayPathFromCode(code, props.displayLevel);
// update-end--author:liaozhiyang---date:20260204---for:QQYUN-14694online
} 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-14694online
// 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-14694online
} 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 }">

View File

@ -1,27 +1,52 @@
import type { Ref } from 'vue';
import type { ExtConfigType } from '../../types';
import { HrefSlots, OnlineColumn } from '/@/components/jeecg/OnLine/types/onlineConfig';
import { filterMultiDictText } from '/@/utils/dict/JDictSelectUtil';
import { filterMultiDictObjs } from '/@/utils/dict/JDictSelectUtil';
import { computed, defineAsyncComponent, h, reactive, ref, toRaw, unref, watch, markRaw } from 'vue';
import { useRouter } from 'vue-router';
import { Tag as ATag } from 'ant-design-vue';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { getAreaTextByCode } from '/@/components/Form/src/utils/Area';
import { createImgPreview } from '/@/components/Preview/index';
import { getAreaTextByCodeAnyLevel } from '/@/components/Form/src/utils/Area';
import { createImgPreview } from '@/components/Preview';
import { importViewsFile, _eval } from '/@/utils';
import { useModal } from '/@/components/Modal';
import { getToken } from '/@/utils/auth';
import LinkTableListPiece from '../../extend/linkTable/LinkTableListPiece.vue'
import { getToken } from "/@/utils/auth";
import { downloadFile } from '/@/api/common/api';
import { getWeekMonthQuarterYear, split } from '/@/utils';
import { getItemColor } from "@/utils/dict/DictColors";
/**
* 获取实际列表需要的column配置
* @param onlineTableContext 从数据库中查出来的数据
* @param extConfigJson 扩展配置JSON
*/
export function useTableColumns(onlineTableContext, extConfigJson: Ref<any | undefined>) {
export function useTableColumns(onlineTableContext, extConfigJson: Ref<ExtConfigType | undefined>) {
// href
let router = useRouter();
//
const columns = ref<Array<OnlineColumn>>([]);
/**
* 20260309
* liaozhiyang
* issues/9336列宽拖动不了
* */
function applyResizableColumns(cols: OnlineColumn[]) {
cols.forEach((column) => {
if (!column.width) {
if (column.fieldType === 'date' || column.fieldType === 'Date') {
column.width = 120;
} else if (column.fieldType === 'link_table') {
column.width = 180;
} else {
column.width = 150;
}
}
column.resizable = true;
});
}
// bpm_status
//const hasBpmStatus = ref<boolean>(false)
//
@ -70,6 +95,12 @@ export function useTableColumns(onlineTableContext, extConfigJson: Ref<any | und
let dataColumns = result.columns;
dataColumns.forEach((column) => {
if (extConfigJson?.value?.canResizeColumn === 1) {
// update-begin--author:liaozhiyang---date:20260309---for:issues/9336
applyResizableColumns([column]);
// update-end--author:liaozhiyang---date:20260309---for:issues/9336
}
// update-begin--author:liaozhiyang---date:20230818---forQQYUN-4161
if (column.fieldExtendJson) {
const json = JSON.parse(column.fieldExtendJson);
@ -182,6 +213,17 @@ export function useTableColumns(onlineTableContext, extConfigJson: Ref<any | und
}
//update-end-author:taoyan date:2023-2-15 for: QQYUN-4286online
let renderResult:any = []
for(let i=0;i<tempIdArray.length;i++){
let renderObj = h(
LinkTableListPiece,
{
id: tempIdArray[i],
text: tempLabelArray[i],
onTab:(id)=>handleClickLinkTable(id, hrefSlotName, json.isListReadOnly)
}
);
renderResult.push(renderObj)
}
if(renderResult.length==0){
return ''
}
@ -216,13 +258,29 @@ export function useTableColumns(onlineTableContext, extConfigJson: Ref<any | und
column.ellipsis = true;
column.customRender = ({ text, record }) => {
let value = text;
const valueSpan: any[] = [];
const getValue = () => valueSpan.length ? valueSpan : value;
// dictCode
if (dictCode) {
if (dictCode.startsWith(replaceFlag)) {
let textFieldName = dictCode.replace(replaceFlag, '');
value = record[textFieldName];
} else {
value = filterMultiDictText(unref(dictOptionInfo)[dictCode], text + '');
const dictItems = filterMultiDictObjs(unref(dictOptionInfo)[dictCode], text);
value = dictItems.map((item) => {
if (item.hasColor) {
//
const fontColor = getItemColor(item.color);
valueSpan.push(h(ATag, {
color: item.color,
style: {
'color': fontColor,
'margin-left': '5px',
},
}, () => item.text))
}
return item.text;
}).join(',');
}
}
//
@ -240,11 +298,11 @@ export function useTableColumns(onlineTableContext, extConfigJson: Ref<any | und
{
onClick: () => handleClickFieldHref(field, record),
},
value
getValue(),
);
}
}
return value;
return h('span', {}, getValue());
};
}
@ -374,7 +432,7 @@ export function useTableColumns(onlineTableContext, extConfigJson: Ref<any | und
}
//
let fixedAction:any = 'left';
let fixedAction:any = 'right';
if(onlineTableContext.isTree()){
fixedAction = 'right'
}
@ -388,15 +446,20 @@ export function useTableColumns(onlineTableContext, extConfigJson: Ref<any | und
});
//
watch(() => extConfigJson?.value, () => {
if (extConfigJson?.value?.tableFixedAction === 1) {
actionColumn.fixed = extConfigJson?.value?.tableFixedActionType || 'right';
//
if(onlineTableContext.isTree()){
actionColumn.fixed = 'right'
}
watch(() => extConfigJson?.value, () => {
if (extConfigJson?.value?.tableFixedAction === 1) {
actionColumn.fixed = extConfigJson?.value?.tableFixedActionType || 'right';
if (onlineTableContext.isTree()) {
actionColumn.fixed = 'right';
}
});
}
// update-begin--author:liaozhiyang---date:20260309---for:issues/9336
if (extConfigJson?.value?.canResizeColumn === 1 && columns.value.length > 0) {
applyResizableColumns(columns.value);
onlineTableContext.reloadTable();
}
// update-end--author:liaozhiyang---date:20260309---for:issues/9336
});
//
function bpmStatusFilter(tableColumns: OnlineColumn[]): boolean {
@ -448,11 +511,29 @@ export function useTableColumns(onlineTableContext, extConfigJson: Ref<any | und
* 根据编码获取省市区文本
* @param code
*/
function getPcaText(code) {
function getPcaText(code, column) {
if (!code) {
return '';
}
return getAreaTextByCode(code);
// update-begin--author:liaozhiyang---date:20260204---for:QQYUN-14694online
let includeParent = true;
let fieldExtendJson = column?.fieldExtendJson;
let level = 3;
if (fieldExtendJson) {
fieldExtendJson = JSON.parse(fieldExtendJson);
if (['province', 'city', 'region'].includes(fieldExtendJson.displayLevel)) {
if (fieldExtendJson.displayLevel === 'province') {
level = 1;
} else if (fieldExtendJson.displayLevel === 'city') {
level = 2;
} else if (fieldExtendJson.displayLevel === 'region') {
level = 3;
}
includeParent = false;
}
}
return getAreaTextByCodeAnyLevel(code, includeParent, level as 1 | 2 | 3);
// update-end--author:liaozhiyang---date:20260204---for:QQYUN-14694online
}
/**

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/9307online
// 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/9307online
},
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/9307online
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/9307online
//
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-15021online js
// online jsoptions
dictOptions.value = props.options;
// update-end--author:liaozhiyang---date:20260325---for:QQYUN-15021online 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/9307online
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/9307online
}
// : 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 {
// optinosoptions
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 {
// optinosoptions
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-14694online支持配置独立的省 根据 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

@ -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-9441online
//
await registerAsyncComponent(JVxeTypes.linkTable, import('./src/components/JVxeLinkTableCell.vue'));
// update-end--author:liaozhiyang---date:20260317---for:QQYUN-9441online
//
// await registerAsyncComponent(JVxeTypes.pca, import('./src/components/JVxePcaCell.vue'));
// : QQYUN-8241china-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/9212JVxeTypes.popupparam
param: originColumn.value.params ?? originColumn.value.param,
// update-end--author:liaozhiyang---date:20260203---for:issues/9212JVxeTypes.popupparam
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---forissues/9405,
//
if (item.path && !item.hideMenu) {
return item;
}
// update-end--author:liaozhiyang---date:20250401---forissues/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/9448tinymce
// tinymcebody
bindScrollCloseMenu(editor);
// update-end--author:liaozhiyang---date:20260306---for:issues/9448tinymce
});
//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 = () => {
// bodymousedowntinymce
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);
// changebasicForm
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);
// changebasicForm
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) {
// valueselectedUser
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---forTV360X-1050Safari
.ant-select {
vertical-align: middle;
}
// update-end--author:liaozhiyang---date:20240605---forTV360X-1050Safari
//
.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,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-13751jVxetable
// 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-13751jVxetable
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/7633online
addComponent(JVxeTypes.treeSelect, JVxeTreeSelectCell);
addComponent(JVxeTypes.catTreeSelect, JVxeCategorySelectCell);
// update-end--author:liaozhiyang---date:20260413---for:issues/7633online
}
/**
* 清空注册的组件
*/
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 idtranslate.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-13751jVxetable
// reactData.validErrorMaps={}
validConfig: {
autoClear: false,
},
// update-end--author:liaozhiyang---date:20260316---for:QQYUN-13751jVxetable
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-13751jVxetable
data.vxeDataSource.value = props.dataSource.map(row => ({ ...row }));
// update-end--author:liaozhiyang---date:20260316---for:QQYUN-13751jVxetable
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-15134jvxetable使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-8785onlineidid
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---forTV360X-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.clonee.itemDOMremove
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---forTV360X-585使
// -update-end--author:liaozhiyang---date:20240619---forTV360X-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-15134jvxetable使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---forissues/7497
// TODO
enhanced = getEnhanced(props.type);
// enhanced = getEnhanced(props.type);
// -update-end--author:liaozhiyang---date:20241210---forissues/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-13751jVxetable
let item = { ...row };
// update-end--author:liaozhiyang---date:20260316---for:QQYUN-13751jVxetable
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-13751jVxetable
records.push({ ...row });
// update-end--author:liaozhiyang---date:20260316---for:QQYUN-13751jVxetable
}
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/7633online
//
treeSelect = 'sel-tree',
//
catTreeSelect = 'cat-tree',
// update-end--author:liaozhiyang---date:20260413---for:issues/7633online
}
// 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-13751jVxetable
// 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-13751jVxetable
return [
h(component, {
key: cellKey,
type: type,
params: params,
renderOptions: renderOptions,

View File

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

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/8709LayoutContent1px
// 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) {
// // 10500
// 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) {
// // qiankunmeta
// to.meta.isQiankunRoute = true;
// } else {
// // qiankunmeta
// delete to.meta.isQiankunRoute;
// }
// next();
// });
// }
//
//
// return {
// registerQiankunRouter,
// }
// })();

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-13658Jvxetablevxetable
// 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-13658Jvxetablevxetable
//---------------------------------------------------------------------
//
// : QQYUN-8241emoji-mart-vue-fast

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

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

@ -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;
// vxetablekey
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 #8855filePathURL
let url = encodeURIComponent(encryptByBase64(getFileAccessHttpUrl(filePath)));
//update-end-author:scott---date:2026-04-16--for: Github #8855filePathURL
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-615REDIS
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' },
};

View File

@ -1,44 +1,12 @@
<template>
<div class="p-2">
<!--查询区域-->
<div class="jeecg-basic-table-form-container">
<a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-row :gutter="24">
<a-col :lg="6">
<a-form-item name="name">
<template #label><span title="授权名称">授权名称</span></template>
<a-input placeholder="请输入授权名称" v-model:value="queryParam.name" allow-clear ></a-input>
</a-form-item>
</a-col>
<a-col :lg="6">
<a-form-item name="createBy">
<template #label><span title="关联系统用户名">关联系统用户名</span></template>
<JSearchSelect dict="sys_user,username,username" v-model:value="queryParam.createBy" placeholder="请输入关联系统用户名" allow-clear ></JSearchSelect>
<!-- <a-input placeholder="请输入关联系统用户名" v-model:value="queryParam.systemUserId" allow-clear ></a-input>-->
</a-form-item>
</a-col>
<a-col :xl="6" :lg="7" :md="8" :sm="24">
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
<a-col :lg="6">
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
<a @click="toggleSearchStatus = !toggleSearchStatus" style="margin-left: 8px">
{{ toggleSearchStatus ? '收起' : '展开' }}
<Icon :icon="toggleSearchStatus ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" />
</a>
</a-col>
</span>
</a-col>
</a-row>
</a-form>
</div>
<div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'openapi:open_api_auth:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="'openapi:open_api_auth:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" v-auth="'openapi:open_api_auth:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-button type="primary" v-auth="'openapi:open_api_auth:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="'openapi:open_api_auth:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" v-auth="'openapi:open_api_auth:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
@ -57,15 +25,15 @@
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }">
</template>
</BasicTable>
<!-- 表单区域 -->
<OpenApiAuthModal ref="registerModal" @success="handleSuccess"></OpenApiAuthModal>
<AuthModal ref="authModal" @success="handleSuccess"></AuthModal>
<OpenApiAuthDrawer @register="registerAuthDrawer" @success="handleSuccess" />
<AuthDrawer @register="registerPermDrawer" @success="handleSuccess" />
</div>
</template>
@ -73,63 +41,51 @@
import { ref, reactive } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { columns, superQuerySchema } from './OpenApiAuth.data';
import {
list,
deleteOne,
batchDelete,
getImportUrl,
getExportUrl,
getGenAKSK, saveOrUpdate
} from "./OpenApiAuth.api";
import OpenApiAuthModal from './components/OpenApiAuthModal.vue'
import AuthModal from './components/AuthModal.vue'
import { useUserStore } from '/@/store/modules/user';
import JSearchSelect from "../../components/Form/src/jeecg/components/JSearchSelect.vue";
import { useDrawer } from '/@/components/Drawer';
import { useMessage } from '/@/hooks/web/useMessage';
import { columns, searchFormSchema, superQuerySchema } from './OpenApiAuth.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl, getGenAKSK, saveOrUpdate } from './OpenApiAuth.api';
import OpenApiAuthDrawer from './components/OpenApiAuthDrawer.vue';
import AuthDrawer from './components/AuthDrawer.vue';
const formRef = ref();
const queryParam = reactive<any>({});
const toggleSearchStatus = ref<boolean>(false);
const registerModal = ref();
const authModal = ref();
const userStore = useUserStore();
const { createMessage } = useMessage();
const [registerAuthDrawer, { openDrawer: openAuthDrawer }] = useDrawer();
const [registerPermDrawer, { openDrawer: openPermDrawer }] = useDrawer();
//table
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '授权管理',
api: list,
columns,
canResize:false,
useSearchForm: false,
canResize: false,
formConfig: {
schemas: searchFormSchema,
autoSubmitOnEnter: true,
showAdvancedButton: true,
fieldMapToNumber: [],
fieldMapToTime: [],
},
actionColumn: {
width: 200,
width: 220,
fixed: 'right',
},
beforeFetch: async (params) => {
beforeFetch: (params) => {
return Object.assign(params, queryParam);
},
},
exportConfig: {
name: "授权管理",
name: '授权管理',
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess
},
});
const [registerTable, { reload, updateTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext;
const labelCol = reactive({
xs:24,
sm:10,
xl:6,
xxl:10
});
const wrapperCol = reactive({
xs: 24,
sm: 20,
importConfig: {
url: getImportUrl,
success: handleSuccess,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
//
const superQueryConfig = reactive(superQuerySchema);
@ -141,163 +97,144 @@
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
searchQuery();
reload();
}
/**
* 新增事件
*/
function handleAdd() {
registerModal.value.disableSubmit = false;
registerModal.value.add();
}
/**
* 编辑事件
*/
function handleAuth(record: Recordable) {
authModal.value.disableSubmit = false;
authModal.value.edit(record);
openAuthDrawer(true, {
isUpdate: false,
showFooter: true,
});
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
registerModal.value.disableSubmit = false;
registerModal.value.authDrawerOpen = true;
registerModal.value.edit(record);
openAuthDrawer(true, {
record,
isUpdate: true,
showFooter: true,
});
}
/**
* 重置事件
* @param record
* 授权事件
*/
function handleAuth(record: Recordable) {
openPermDrawer(true, { record });
}
/**
* 重置AK/SK
*/
async function handleReset(record: Recordable) {
const AKSKObj = await getGenAKSK({});
record.ak = AKSKObj[0];
record.sk = AKSKObj[1];
saveOrUpdate(record,true);
// handleSuccess;
await saveOrUpdate(record, true);
reload();
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
registerModal.value.disableSubmit = true;
registerModal.value.edit(record);
openAuthDrawer(true, {
record,
isUpdate: true,
showFooter: false,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 操作栏
*/
/**
* 复制密钥
*/
async function handleCopyKeys(record: Recordable) {
const text = `访问密钥AK: ${record.ak}\n签名密钥SK: ${record.sk}`;
try {
await navigator.clipboard.writeText(text);
createMessage.success('密钥已复制到剪贴板');
} catch (_e) {
createMessage.error('复制失败,请手动复制');
}
}
function getTableAction(record) {
return [
{
label: '授权',
onClick: handleAuth.bind(null, record),
auth: 'openapi:open_api_auth:edit'
label: '复制密钥',
onClick: handleCopyKeys.bind(null, record),
},
{
label: '重置',
popConfirm: {
title: '是否重置AK,SK',
confirm: handleReset.bind(null, record),
placement: 'topLeft',
},
auth: 'openapi:open_api_auth:edit'
label: '分配接口',
onClick: handleAuth.bind(null, record),
auth: 'openapi:open_api_auth:edit',
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
}, {
label: '修改对象',
onClick: handleEdit.bind(null, record),
auth: 'openapi:open_api_auth:edit',
},
{
label: '重置密钥',
popConfirm: {
title: '原密钥将失效,确认重置?',
confirm: handleReset.bind(null, record),
placement: 'topLeft',
},
auth: 'openapi:open_api_auth:edit',
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
auth: 'openapi:open_api_auth:delete'
}
]
auth: 'openapi:open_api_auth:delete',
},
];
}
/**
* 查询
*/
function searchQuery() {
reload();
}
/**
* 重置
*/
function searchReset() {
formRef.value.resetFields();
selectedRowKeys.value = [];
//
reload();
}
</script>
<style lang="less" scoped>
.jeecg-basic-table-form-container {
padding: 0;
.table-page-search-submitButtons {
display: block;
margin-bottom: 24px;
white-space: nowrap;
}
.query-group-cust{
min-width: 100px !important;
}
.query-group-split-cust{
width: 30px;
display: inline-block;
text-align: center
}
.ant-form-item:not(.ant-form-item-with-help){
margin-bottom: 16px;
height: 32px;
}
:deep(.ant-picker),:deep(.ant-input-number){
width: 100%;
}
:deep(.ant-picker),:deep(.ant-input-number) {
width: 100%;
}
</style>

View File

@ -41,31 +41,30 @@
</template>
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }">
<template v-if="column.dataIndex === 'requestUrl'">
<a @click="handleCopyUrl(record)" title="点击复制完整接口地址">{{ text }}</a>
</template>
</template>
</BasicTable>
<!-- 表单区域 -->
<OpenApiModal @register="registerModal" @success="handleSuccess"></OpenApiModal>
<OpenApiDrawer @register="registerDrawer" @success="handleSuccess" />
</div>
</template>
<script lang="ts" name="openapi-openApi" setup>
import {ref, reactive, computed, unref} from 'vue';
import {BasicTable, useTable, TableAction} from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage'
import {useModal} from '/@/components/Modal';
import OpenApiModal from './components/OpenApiModal.vue'
import OpenApiHeaderSubTable from './subTables/OpenApiHeaderSubTable.vue'
import OpenApiParamSubTable from './subTables/OpenApiParamSubTable.vue'
import {columns, searchFormSchema, superQuerySchema} from './OpenApi.data';
import {list, deleteOne, batchDelete, getImportUrl,getExportUrl} from './OpenApi.api';
import {downloadFile} from '/@/utils/common/renderUtils';
import { useUserStore } from '/@/store/modules/user';
import { ref, reactive } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { useDrawer } from '/@/components/Drawer';
import { useMessage } from '/@/hooks/web/useMessage';
import OpenApiDrawer from './components/OpenApiDrawer.vue';
import { columns, searchFormSchema, superQuerySchema } from './OpenApi.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './OpenApi.api';
const queryParam = reactive<any>({});
// key
const expandedRowKeys = ref<any[]>([]);
//model
const [registerModal, {openModal}] = useModal();
const userStore = useUserStore();
const { createMessage } = useMessage();
const API_DOMAIN = import.meta.env.VITE_GLOB_DOMAIN_URL;
const [registerDrawer, { openDrawer }] = useDrawer();
//table
const { prefixCls,tableContext,onExportXls,onImportXls } = useListPage({
tableProps:{
@ -84,7 +83,7 @@
],
},
actionColumn: {
width: 120,
width: 200,
fixed:'right'
},
beforeFetch: (params) => {
@ -130,7 +129,7 @@
* 新增事件
*/
function handleAdd() {
openModal(true, {
openDrawer(true, {
isUpdate: false,
showFooter: true,
});
@ -139,7 +138,7 @@
* 编辑事件
*/
function handleEdit(record: Recordable) {
openModal(true, {
openDrawer(true, {
record,
isUpdate: true,
showFooter: true,
@ -149,7 +148,7 @@
* 详情
*/
function handleDetail(record: Recordable) {
openModal(true, {
openDrawer(true, {
record,
isUpdate: true,
showFooter: false,
@ -176,13 +175,26 @@
/**
* 操作栏
*/
/**
* 复制接口地址
*/
async function handleCopyUrl(record: Recordable) {
const url = API_DOMAIN + '/openapi/call/' + record.requestUrl;
try {
await navigator.clipboard.writeText(url);
createMessage.success('接口地址已复制');
} catch (_e) {
createMessage.error('复制失败,请手动复制');
}
}
function getTableAction(record){
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'openapi:open_api:edit'
}
},
]
}

View File

@ -0,0 +1,185 @@
<template>
<BasicDrawer
v-bind="$attrs"
@register="registerDrawer"
title="接口授权"
width="720px"
destroyOnClose
@ok="handleSubmit"
>
<a-spin :spinning="confirmLoading">
<a-row :gutter="[12, 12]">
<a-col :span="12" v-for="item in apiList" :key="item.id">
<a-card
:class="['auth-api-card', { 'auth-api-card--checked': item.checked }]"
hoverable
:body-style="{ padding: '12px' }"
@click="handleSelect(item)"
>
<div style="display: flex; justify-content: space-between; align-items: center">
<span class="auth-api-name">{{ item.name }}</span>
<a-checkbox v-model:checked="item.checked" @click.stop @change="(e) => handleChange(e, item)" />
</div>
<div style="margin-top: 6px; color: #888; font-size: 12px">
<a-tag :color="getMethodColor(item.requestMethod)">{{ item.requestMethod }}</a-tag>
<span style="margin-left: 4px">{{ item.requestUrl }}</span>
</div>
</a-card>
</a-col>
</a-row>
<div v-if="apiList.length === 0 && !confirmLoading" style="text-align: center; padding: 40px 0; color: #999">
暂无接口数据
</div>
<div v-if="total > 0" style="margin-top: 16px; text-align: right">
<a-pagination
:current="pageNo"
:page-size="pageSize"
:page-size-options="['10', '20', '30']"
:total="total"
show-quick-jumper
show-size-changer
size="small"
@change="handlePageChange"
/>
</div>
</a-spin>
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { getApiList, getPermissionList, permissionAddFunction } from '../OpenApiAuth.api';
import { useMessage } from '/@/hooks/web/useMessage';
const emit = defineEmits(['register', 'success']);
const { createMessage } = useMessage();
const confirmLoading = ref(false);
const apiAuthId = ref('');
const apiList = ref<any[]>([]);
const selectedRowKeys = ref<string[]>([]);
const pageNo = ref(1);
const pageSize = ref(10);
const total = ref(0);
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
selectedRowKeys.value = [];
apiList.value = [];
pageNo.value = 1;
pageSize.value = 10;
total.value = 0;
apiAuthId.value = data.record?.id || '';
// Load existing permissions
try {
const permRes = await getPermissionList({ apiAuthId: apiAuthId.value });
if (permRes && permRes.length > 0) {
permRes.forEach((item) => {
if (item.ifCheckBox == '1') {
selectedRowKeys.value.push(item.id);
}
});
}
} catch (_e) {
// ignore
}
await reload();
});
async function reload() {
confirmLoading.value = true;
try {
const res = await getApiList({
pageNo: pageNo.value,
pageSize: pageSize.value,
column: 'createTime',
order: 'desc',
});
if (res.success) {
const records = res.result.records || [];
records.forEach((item) => {
item.checked = selectedRowKeys.value.includes(item.id);
});
apiList.value = records;
total.value = res.result.total || 0;
} else {
apiList.value = [];
total.value = 0;
}
} finally {
confirmLoading.value = false;
}
}
function handleSelect(item) {
item.checked = !item.checked;
toggleSelection(item.id, item.checked);
}
function handleChange(e, item) {
toggleSelection(item.id, e.target.checked);
}
function toggleSelection(id: string, checked: boolean) {
const idx = selectedRowKeys.value.indexOf(id);
if (checked && idx === -1) {
selectedRowKeys.value.push(id);
} else if (!checked && idx !== -1) {
selectedRowKeys.value.splice(idx, 1);
}
}
function handlePageChange(page, current) {
pageNo.value = page;
pageSize.value = current;
reload();
}
function getMethodColor(method: string) {
const map = { GET: 'green', POST: 'blue', PUT: 'orange', DELETE: 'red', PATCH: 'purple' };
return map[method] || 'default';
}
async function handleSubmit() {
confirmLoading.value = true;
try {
setDrawerProps({ confirmLoading: true });
const res = await permissionAddFunction({
apiId: selectedRowKeys.value.join(','),
apiAuthId: apiAuthId.value,
});
if (res.success) {
createMessage.success(res.message);
closeDrawer();
emit('success');
} else {
createMessage.warning(res.message);
}
} finally {
confirmLoading.value = false;
setDrawerProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped>
.auth-api-card {
transition: all 0.2s;
border: 1px solid #d9d9d9;
&--checked {
border-color: #1890ff;
background-color: #e6f7ff;
}
}
.auth-api-name {
font-weight: 500;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 200px;
display: inline-block;
}
</style>

View File

@ -0,0 +1,75 @@
<template>
<BasicDrawer
v-bind="$attrs"
@register="registerDrawer"
:title="title"
width="600px"
destroyOnClose
@ok="handleSubmit"
:showFooter="showFooter"
>
<BasicForm @register="registerForm" />
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { BasicForm, useForm } from '/@/components/Form/index';
import { authFormSchema } from '../OpenApiAuth.data';
import { saveOrUpdate } from '../OpenApiAuth.api';
import { useMessage } from '/@/hooks/web/useMessage';
import { USER_INFO_KEY } from '/@/enums/cacheEnum';
import { getAuthCache } from '/@/utils/auth';
const emit = defineEmits(['register', 'success']);
const { createMessage } = useMessage();
const isUpdate = ref(false);
const formDisabled = ref(false);
const showFooter = ref(true);
const [registerForm, { resetFields, setFieldsValue, validate, setProps }] = useForm({
labelWidth: 100,
schemas: authFormSchema,
showActionButtonGroup: false,
baseColProps: { span: 24 },
});
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
await resetFields();
showFooter.value = !!data?.showFooter;
setDrawerProps({ confirmLoading: false, showFooter: showFooter.value });
isUpdate.value = !!data?.isUpdate;
formDisabled.value = !data?.showFooter;
if (unref(isUpdate)) {
await setFieldsValue({ ...data.record });
} else {
// New record: set current user
const userData = getAuthCache(USER_INFO_KEY) as any;
await setFieldsValue({
systemUserId: userData?.id || '',
});
}
setProps({ disabled: !data?.showFooter });
});
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(formDisabled) ? '编辑' : '详情'));
async function handleSubmit() {
try {
const values = await validate();
setDrawerProps({ confirmLoading: true });
const res = await saveOrUpdate(values, isUpdate.value);
if (res.success) {
createMessage.success(res.message);
closeDrawer();
emit('success');
} else {
createMessage.warning(res.message);
}
} finally {
setDrawerProps({ confirmLoading: false });
}
}
</script>

View File

@ -5,18 +5,18 @@
<a-form ref="formRef" class="antd-modal-form" :labelCol="labelCol" :wrapperCol="wrapperCol" name="OpenApiAuthForm">
<a-row>
<a-col :span="24">
<a-form-item label="授权名称" v-bind="validateInfos.name" id="OpenApiAuthForm-name" name="name">
<a-input v-model:value="formData.name" placeholder="请输入授权名称" allow-clear ></a-input>
<a-form-item label="授权对象" v-bind="validateInfos.name" id="OpenApiAuthForm-name" name="name">
<a-input v-model:value="formData.name" placeholder="请输入授权对象" allow-clear ></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="AK" v-bind="validateInfos.ak" id="OpenApiAuthForm-ak" name="ak">
<a-input v-model:value="formData.ak" placeholder="请输入AK" disabled allow-clear ></a-input>
<a-form-item label="访问密钥(AK" v-bind="validateInfos.ak" id="OpenApiAuthForm-ak" name="ak">
<a-input v-model:value="formData.ak" placeholder="自动生成" disabled allow-clear ></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="SK" v-bind="validateInfos.sk" id="OpenApiAuthForm-sk" name="sk">
<a-input v-model:value="formData.sk" placeholder="请输入SK" disabled allow-clear ></a-input>
<a-form-item label="签名密钥(SK" v-bind="validateInfos.sk" id="OpenApiAuthForm-sk" name="sk">
<a-input v-model:value="formData.sk" placeholder="自动生成" disabled allow-clear ></a-input>
</a-form-item>
</a-col>
<!-- <a-col :span="24">-->
@ -63,7 +63,7 @@
const confirmLoading = ref<boolean>(false);
//
const validatorRules = reactive({
name:[{ required: true, message: '请输入授权名称!'},],
name:[{ required: true, message: '请输入授权对象!'},],
systemUserId:[{ required: true, message: '请输入关联系统用户名!'},],
});
const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: false });

View File

@ -0,0 +1,269 @@
<template>
<BasicDrawer
v-bind="$attrs"
@register="registerDrawer"
:title="title"
width="90%"
destroyOnClose
@ok="handleSubmit"
:showFooter="showFooter"
>
<!-- 上部基本信息表单 -->
<BasicForm @register="registerForm" ref="formRef">
<template #whiteListSlot="{ model, field }">
<a-textarea
v-model:value="model[field]"
:rows="5"
placeholder="示例:&#10;192.168.1.100&#10;10.0.0.0/8&#10;172.16.*.*"
:disabled="formDisabled"
/>
<!-- 标签预览 -->
<div v-if="model[field]" style="margin-top: 8px">
<a-tag
v-for="item in parseWhiteList(model[field])"
:key="item"
color="green"
style="margin-bottom: 4px"
>
{{ item }}
</a-tag>
</div>
<!-- 整理按钮 -->
<div v-if="model[field] && !formDisabled" style="margin-top: 4px; text-align: right">
<a-button size="small" @click="formatWhiteList(model, field)"> </a-button>
</div>
</template>
</BasicForm>
<!-- 下部Tabs -->
<a-tabs v-model:activeKey="activeTab" style="margin-top: 16px">
<a-tab-pane key="headers" tab="请求头">
<JVxeTable
keep-source
ref="openApiHeader"
:loading="openApiHeaderTable.loading"
:columns="openApiHeaderTable.columns"
:dataSource="openApiHeaderTable.dataSource"
:height="240"
:disabled="formDisabled"
:rowNumber="true"
:rowSelection="true"
:toolbar="true"
/>
</a-tab-pane>
<a-tab-pane key="params" tab="请求参数">
<JVxeTable
keep-source
ref="openApiParam"
:loading="openApiParamTable.loading"
:columns="openApiParamTable.columns"
:dataSource="openApiParamTable.dataSource"
:height="240"
:disabled="formDisabled"
:rowNumber="true"
:rowSelection="true"
:toolbar="true"
/>
</a-tab-pane>
<a-tab-pane key="body" tab="请求体">
<div style="border: 1px solid #d9d9d9; border-radius: 4px; min-height: 300px">
<CodeEditor v-model:value="bodyContent" mode="application/json" :readonly="formDisabled" />
</div>
</a-tab-pane>
<a-tab-pane key="response" tab="响应配置">
<div style="margin-bottom: 16px">
<h4 style="margin-bottom: 8px">响应示例</h4>
<div style="border: 1px solid #d9d9d9; border-radius: 4px; min-height: 200px">
<CodeEditor v-model:value="responseExample" mode="application/json" :readonly="formDisabled" />
</div>
</div>
<div>
<h4 style="margin-bottom: 8px">响应字段说明</h4>
<JVxeTable
keep-source
ref="responseField"
:loading="responseFieldTable.loading"
:columns="responseFieldTable.columns"
:dataSource="responseFieldTable.dataSource"
:height="240"
:disabled="formDisabled"
:rowNumber="true"
:rowSelection="true"
:toolbar="true"
/>
</div>
</a-tab-pane>
</a-tabs>
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, computed, unref, reactive } from 'vue';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { BasicForm, useForm } from '/@/components/Form/index';
import { CodeEditor } from '/@/components/CodeEditor';
import {
formSchema,
openApiHeaderJVxeColumns,
openApiParamJVxeColumns,
responseFieldJVxeColumns,
} from '../OpenApi.data';
import { saveOrUpdate, getGenPath } from '../OpenApi.api';
import { useMessage } from '/@/hooks/web/useMessage';
const emit = defineEmits(['register', 'success']);
const $message = useMessage();
const isUpdate = ref(true);
const formDisabled = ref(false);
const showFooter = ref(true);
const activeTab = ref('headers');
const bodyContent = ref('');
const responseExample = ref('');
const openApiHeader = ref();
const openApiParam = ref();
const responseField = ref();
const openApiHeaderTable = reactive({
loading: false,
dataSource: [] as any[],
columns: openApiHeaderJVxeColumns,
});
const openApiParamTable = reactive({
loading: false,
dataSource: [] as any[],
columns: openApiParamJVxeColumns,
});
const responseFieldTable = reactive({
loading: false,
dataSource: [] as any[],
columns: responseFieldJVxeColumns,
});
const [registerForm, { setProps, resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 100,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: { span: 12 },
});
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
await reset();
showFooter.value = !!data?.showFooter;
setDrawerProps({ confirmLoading: false, showFooter: showFooter.value });
isUpdate.value = !!data?.isUpdate;
formDisabled.value = !data?.showFooter;
if (unref(isUpdate)) {
await setFieldsValue({
...data.record,
});
openApiHeaderTable.dataSource = data.record.headersJson ? JSON.parse(data.record.headersJson) : [];
openApiParamTable.dataSource = data.record.paramsJson ? JSON.parse(data.record.paramsJson) : [];
bodyContent.value = data.record.body || '';
responseExample.value = data.record.responseExample || '';
responseFieldTable.dataSource = data.record.responseFieldsJson ? JSON.parse(data.record.responseFieldsJson) : [];
} else {
const requestUrlObj = await getGenPath({});
await setFieldsValue({
requestUrl: requestUrlObj.result,
});
}
setProps({ disabled: !data?.showFooter });
});
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(formDisabled) ? '编辑' : '详情'));
/** 解析白名单文本为条目数组 */
function parseWhiteList(text: string): string[] {
if (!text) return [];
return text
.split(/[,\n]/)
.map((s) => s.trim())
.filter(Boolean);
}
/** 整理白名单:去空行、去重、每行一个 */
function formatWhiteList(model: any, field: string) {
const items = parseWhiteList(model[field]);
const unique = [...new Set(items)];
model[field] = unique.join('\n');
}
async function reset() {
await resetFields();
activeTab.value = 'headers';
openApiHeaderTable.dataSource = [];
openApiParamTable.dataSource = [];
responseFieldTable.dataSource = [];
bodyContent.value = '';
responseExample.value = '';
}
async function handleSubmit() {
try {
const values = await validate();
setDrawerProps({ confirmLoading: true });
// Collect JVxeTable data
const headerData = await openApiHeader.value?.getTableData();
const paramData = await openApiParam.value?.getTableData();
const responseFieldData = await responseField.value?.getTableData();
const headersJson = headerData?.tableData?.length ? JSON.stringify(headerData.tableData) : null;
const paramsJson = paramData?.tableData?.length ? JSON.stringify(paramData.tableData) : null;
const responseFieldsJson = responseFieldData?.tableData?.length ? JSON.stringify(responseFieldData.tableData) : null;
// Validate body JSON
if (bodyContent.value) {
try {
if (typeof JSON.parse(bodyContent.value) != 'object') {
$message.createMessage.error('JSON格式化错误,请检查输入数据');
return;
}
} catch (e) {
$message.createMessage.error('JSON格式化错误,请检查输入数据');
return;
}
}
// Validate response example JSON
if (responseExample.value) {
try {
JSON.parse(responseExample.value);
} catch (e) {
$message.createMessage.error('响应示例JSON格式错误,请检查输入数据');
return;
}
}
const submitValues = {
...values,
headersJson,
paramsJson,
body: bodyContent.value || null,
responseExample: responseExample.value || null,
responseFieldsJson,
};
await saveOrUpdate(submitValues, isUpdate.value);
closeDrawer();
emit('success');
} finally {
setDrawerProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped>
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>

View File

@ -46,7 +46,6 @@
import { ref, computed, unref, reactive } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { JVxeTable } from '/@/components/jeecg/JVxeTable';
import { useJvxeMethod } from '/@/hooks/system/useJvxeMethods.ts';
import { formSchema, openApiHeaderJVxeColumns, openApiParamJVxeColumns } from '../OpenApi.data';
import { saveOrUpdate, queryOpenApiHeader, queryOpenApiParam, getGenPath } from '../OpenApi.api';

View File

@ -131,7 +131,7 @@ export const generateMemoryByAppId = (params) => {
url: Api.generateMemoryByAppId+'?variables='+ params.variables + '&memoryId='+ params.memoryId,
adapter: 'fetch',
responseType: 'stream',
timeout: 5 * 60 * 1000,
timeout: 60 * 60 * 1000,
},
{
isTransformResponse: false,

View File

@ -333,6 +333,7 @@
initChartData(params.appId);
} else {
initChartData();
appData.value.metadata = { izDraw: '1', defaultSelect: '0' }
quickCommandData.value = [
{ name: '请介绍一下JeecgBoot', descr: "请介绍一下JeecgBoot" },
{ name: 'JEECG有哪些优势', descr: "JEECG有哪些优势" },

View File

@ -360,8 +360,8 @@
const isThinking = ref<boolean>(false);
//
const enableSearch = ref<boolean>(false);
//
const showWebSearch = ref<boolean>(false);
//
const showWebSearch = ref<boolean>(true);
//provider
const modelProvider = ref<string>('');
//( deepsee-reason )
@ -565,7 +565,7 @@
//
const handleStop = () => {
console.log('ai 聊天:::---停止响应');
console.log('ai 聊天:::---停止响应, 当前loading:', loading.value, ', 调用栈:', new Error().stack?.split('\n').slice(1,4).join(' <- '));
if (loading.value) {
loading.value = false;
}
@ -666,7 +666,7 @@
params: param,
adapter: 'fetch',
responseType: 'stream',
timeout: 5 * 60 * 1000,
timeout: 60 * 60 * 1000,
},
{
isTransformResponse: false,
@ -1037,59 +1037,34 @@
}
//update-begin---author:wangshuai---date:2025-03-12---for:QQYUN-11555---
let result = decoder.decode(value, { stream: true });
result = buffer + result;
const lines = result.split('\n\n');
for (let line of lines) {
if (line.startsWith('data:')) {
let content = line.replace('data:', '').trim();
if(!content){
continue;
}
if(!content.endsWith('}')){
buffer = buffer + content;
continue;
}
buffer = "";
try {
//update-begin---author:wangshuai---date:2025-03-13---for:QQYUN-11572线---
if(content.indexOf(":::card:::") !== -1){
content = content.replace(/\s+/g, '');
}
let parse = JSON.parse(content);
await renderText(parse,conversationId,text,options).then((res)=>{
text = res.returnText;
conversationId = res.conversationId;
});
//update-end---author:wangshuai---date:2025-03-13---for:QQYUN-11572线---
} catch (error) {
console.log('Error parsing update:', error);
}
//update-end---author:wangshuai---date:2025-03-12---for:QQYUN-11555---
}else{
if(!line){
continue;
}
if(!line.endsWith('}')){
buffer = buffer + line;
continue;
}
buffer = "";
//update-begin---author:wangshuai---date:2025-03-13---for:QQYUN-11572线---
try {
if(line.indexOf(":::card:::") !== -1){
line = line.replace(/\s+/g, '');
}
let parse = JSON.parse(line);
await renderText(parse, conversationId, text, options).then((res) => {
text = res.returnText;
conversationId = res.conversationId;
});
} catch (error) {
console.log('Error parsing update:', error);
}
//update-end---author:wangshuai---date:2025-03-13---for:QQYUN-11572线---
buffer += result;
// SSE \n\n buffer
const parts = buffer.split('\n\n');
buffer = parts.pop() || '';
for (let part of parts) {
if (!part || !part.trim()) {
continue;
}
let content = part.startsWith('data:') ? part.replace('data:', '').trim() : part.trim();
if (!content) {
continue;
}
//update-begin---author:wangshuai---date:2025-03-13---for:QQYUN-11572线---
try {
if(content.indexOf(":::card:::") !== -1){
content = content.replace(/\s+/g, '');
}
let parse = JSON.parse(content);
await renderText(parse, conversationId, text, options).then((res) => {
text = res.returnText;
conversationId = res.conversationId;
});
} catch (error) {
console.log('JSON解析失败, content长度:', content.length, ', error:', error);
}
//update-end---author:wangshuai---date:2025-03-13---for:QQYUN-11572线---
}
//update-end---author:wangshuai---date:2025-03-12---for:QQYUN-11555---
}
//update-begin---author:wangshuai---date:2025-11-05---for: 线---
if(!text && isReConnect && chatData.value.length >1){
@ -1118,7 +1093,7 @@
const result = await defHttp.get({ url: '/airag/chat/receive/' + requestId ,
adapter: 'fetch',
responseType: 'stream',
timeout: 5 * 60 * 1000
timeout: 60 * 60 * 1000
}, { isTransformResponse: false }).catch(async (err)=>{
loading.value = false;
localStorage.removeItem('chat_requestId_' + uuid.value);
@ -1240,29 +1215,28 @@
//
showDraw.value = metadata.izDraw === '1';
//
enableDraw.value = metadata.izDraw === '1';
//defaultSelect 0
const defaultSelect = metadata.defaultSelect || metadata.izDraw;
enableDraw.value = defaultSelect === '1';
drawModelId.value = metadata.drawModelId;
if (metadata && metadata.modelInfo) {
modelProvider.value = metadata.modelInfo.provider || '';
modelName.value = metadata.modelInfo.modelName || '';
//
showWebSearch.value = modelProvider.value === 'QWEN';
showThink.value = modelName.value === 'deepseek-reasoner';
} else {
showWebSearch.value = false;
showThink.value = false;
}
} catch (e) {
console.error('解析模型信息失败', e);
showWebSearch.value = false;
showThink.value = false;
enableDraw.value = false;
}
} else {
showWebSearch.value = false;
showThink.value = false;
showDraw.value = false;
enableDraw.value = false;
}
}

View File

@ -1,5 +1,5 @@
<template>
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']" v-if="getText || (props.presetQuestion && props.presetQuestion.length>0)">
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']" v-if="getText || props.error || (props.presetQuestion && props.presetQuestion.length>0)">
<div class="avatar" v-if="showAvatar !== 'no'">
<img v-if="inversion === 'user'" :src="avatar()" />
<img v-else :src="getAiImg()" />

View File

@ -1,5 +1,5 @@
<template>
<div v-if="parsedText != ''" class="textWrap" :class="[inversion === 'user' ? 'self' : (isOnlyImage ? 'chatgpt-image' : 'chatgpt')]" ref="textRef">
<div v-if="parsedText != '' || error" class="textWrap" :class="[inversion === 'user' ? 'self' : (isOnlyImage ? 'chatgpt-image' : 'chatgpt')]" ref="textRef">
<div v-if="inversion != 'user'" :style="{ width: getIsMobile? screenWidth : 'auto' }">
<div ref="markdownBodyRef" class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="parsedText" />
<template v-if="showRefKnow">

View File

@ -0,0 +1,43 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
sqlPageExecute = '/airag/mcp/database/sqlPageExecute',
sqlExportXls = '/airag/mcp/database/sqlExportXls',
}
/**
* 分页执行 SQL 查询
*/
export function sqlPageExecute(params: { sql: string; dbSource?: string; pageNo: number; pageSize: number }) {
return defHttp.post<Recordable>(
{
url: Api.sqlPageExecute,
params: {
sql: params.sql,
dbSourceKey: params.dbSource || '',
pageNo: params.pageNo,
pageSize: params.pageSize,
},
},
{ isTransformResponse: false }
);
}
/**
* 导出图表原始数据为 Excel
*/
export function sqlExportXls(params: { sql: string; dbSource?: string; columns?: Recordable }) {
return defHttp.post(
{
url: Api.sqlExportXls,
params: {
sql: params.sql,
dbSourceKey: params.dbSource || '',
columns: params.columns || {},
},
responseType: 'blob',
timeout: 5 * 60 * 1000,
},
{ isTransformResponse: false, isReturnNativeResponse: true }
);
}

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