v3.9.5版本发布 vue3

This commit is contained in:
JEECG 2026-08-21 17:01:39 +08:00
parent 6f90858bcb
commit 2d401c923d
336 changed files with 13150 additions and 2410 deletions

View File

@ -12,6 +12,7 @@
*.iws *.iws
*.ipr *.ipr
out/ out/
.playwright-mcp
# Eclipse # Eclipse
.classpath .classpath
@ -38,6 +39,7 @@ logs/
# Node (frontend artifacts if any) # Node (frontend artifacts if any)
node_modules/ node_modules/
dist/ dist/
mock/
# Docker volumes / data # Docker volumes / data
docker/data/ docker/data/

View File

@ -29,7 +29,7 @@ VITE_APP_SUB_jeecg-app-1 = '//localhost:8092'
# 全局隐藏哪些布局。可选属性sider,header,multi-tabs多个用逗号隔开 # 全局隐藏哪些布局。可选属性sider,header,multi-tabs多个用逗号隔开
#VITE_GLOB_HIDE_LAYOUT_TYPES=sider,header,multi-tabs #VITE_GLOB_HIDE_LAYOUT_TYPES=sider,header,multi-tabs
# 在线文档编辑版本。可选属性wps, offlineWps(离线版) ,onlyoffice # 在线文档编辑版本。可选属性wps, newWps(新版本wps), offlineWps(离线版) ,onlyoffice
VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps
# icon组件的iconify图标库使用在线还是本地。可选属性online, local # icon组件的iconify图标库使用在线还是本地。可选属性online, local

View File

@ -11,7 +11,7 @@ VITE_GLOB_DOMAIN_URL=https://api3.boot.jeecg.com
# 接口父路径前缀 # 接口父路径前缀
VITE_GLOB_API_URL_PREFIX= VITE_GLOB_API_URL_PREFIX=
# 在线文档编辑版本。可选属性wps, offlineWps(离线版), onlyoffice # 在线文档编辑版本。可选属性wps, newWps(新版本wps), offlineWps(离线版), onlyoffice
VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps
# 全局隐藏哪些布局。可选属性sider,header,multi-tabs多个用逗号隔开 # 全局隐藏哪些布局。可选属性sider,header,multi-tabs多个用逗号隔开

View File

@ -30,7 +30,7 @@ VITE_GLOB_API_URL_PREFIX=
# 全局隐藏哪些布局。可选属性sider,header,multi-tabs多个用逗号隔开 # 全局隐藏哪些布局。可选属性sider,header,multi-tabs多个用逗号隔开
#VITE_GLOB_HIDE_LAYOUT_TYPES=sider,header,multi-tabs #VITE_GLOB_HIDE_LAYOUT_TYPES=sider,header,multi-tabs
# 在线文档编辑版本。可选属性wps, offlineWps(离线版), onlyoffice # 在线文档编辑版本。可选属性wps, newWps(新版本wps), offlineWps(离线版), onlyoffice
VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps
# icon组件的iconify图标库使用在线还是本地。可选属性online, local # icon组件的iconify图标库使用在线还是本地。可选属性online, local

59
jeecgboot-vue3/.ignore Normal file
View File

@ -0,0 +1,59 @@
# Git
.git/
.gitignore
.gitmodules
# SVN
.svn/
# IntelliJ IDEA
.idea/
*.iml
*.iws
*.ipr
out/
.playwright-mcp
# 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/
mock/
# Docker volumes / data
docker/data/
# Compiled classes
*.class
# Custom
*.qqy
代码修改.log
代码修改日志
*.zip
backup/
.history/
.cursor/
doc/
docs/

158
jeecgboot-vue3/AGENTS.md Normal file
View File

@ -0,0 +1,158 @@
# AGENTS.md
This file provides repository guidance for Codex when working in this project.
## Communication
- Always answer in Simplified Chinese first, unless the user explicitly requests another language.
## Project Overview
JeecgBoot Vue3 (v3.8.3) is an enterprise low-code admin platform frontend. It is built on Vue 3, TypeScript, Vite, Ant Design Vue 4, and Pinia. It was originally forked from Vben Admin and customized with JeecgBoot-specific components such as online forms, code generation, workflow, and related low-code modules.
This workspace uses SVN for version control. Do not assume Git is available in this directory.
Backend: Spring Boot (JeecgBoot), expected at `http://127.0.0.1:8080/jeecg-boot` during development.
## Related Projects
| Project | Path |
| --- | --- |
| Frontend (Vue3, this repo) | `E:\workspace-cc-jeecg\jeecgboot-vue3-2026` |
| Backend (Spring Boot 3) | `E:\workspace-cc-jeecg\jeecg-boot-framework-2026` |
## Commands
```bash
pnpm install
pnpm dev
pnpm build
pnpm batch:prettier
```
- `pnpm dev` starts the dev server on port 3100.
- `pnpm build` creates the production build in `dist/`.
- `pnpm batch:prettier` formats all matching files under `src/`; run it only when broad formatting is intended.
- No regular test script is configured in `package.json`; a Jest config exists, but there is no `test` script.
## Path Aliases
- `/@/` or `@/` maps to `src/`.
- `/#/` or `#/` maps to `types/`.
- Prefer the `/@/` prefix with the leading slash, which is the convention used throughout this codebase.
## Source Structure
| Directory | Purpose |
| --- | --- |
| `src/api/` | HTTP request functions organized by domain, such as `sys/` and `common/`. |
| `src/components/` | Reusable components, including generic components and JeecgBoot-specific components under `jeecg/`. |
| `src/hooks/` | Composition API hooks, including `web/`, `setting/`, and `system/`. |
| `src/layouts/` | App layouts, including `default/`, `iframe/`, and `page/`. |
| `src/router/` | Vue Router setup with dynamic route registration based on backend permissions. |
| `src/store/` | Pinia stores for app, user, permission, tabs, locale, and lock state. |
| `src/settings/` | Global project, component, and design settings. |
| `src/utils/` | HTTP client, auth/token management, encryption, dict helpers, and shared utilities. |
| `src/views/` | Feature pages, including `system/`, `dashboard/`, `monitor/`, and `super/`. |
| `src/locales/` | i18n files such as `zh_CN` and `en`. |
| `src/enums/` | TypeScript enums for constants. |
| `src/directives/` | Custom directives such as `v-auth`, `v-loading`, `v-click-outside`, and `v-ripple`. |
## View Conventions
Each feature module in `src/views/` typically includes:
- `index.vue` for the main list page.
- `*.data.ts` for table column definitions and form schemas.
- `*.api.ts` for API endpoint calls.
- `*Modal.vue` or `*Drawer.vue` for detail and edit components.
## Component Patterns
Use `BasicTable` and `BasicForm` for list pages:
```ts
import { BasicTable, useTable } from '/@/components/Table';
const [registerTable] = useTable({ api, columns, formConfig: { schemas } });
```
Use `useModal` and `useDrawer` for detail panels:
```ts
const [registerModal, { openModal }] = useModal();
openModal(true, { isUpdate: true, record });
```
Use `useListPage` from `src/hooks/system/` for standard CRUD pages that combine table, form, modal, and drawer behavior.
Form schemas use `FormSchema[]` with `component` specifying Ant Design Vue or custom components such as `Input`, `Select`, `JDictSelectTag`, and `JSearchSelect`.
## HTTP Client
`defHttp` in `src/utils/http/axios/` wraps Axios with:
- Token injection through interceptors.
- MD5 request signing for API security.
- Multi-tenant header support.
- Standard response unwrapping through the `result` field.
API functions usually follow this pattern:
```ts
enum Api {
List = '/sys/user/list',
}
export const list = (params) => defHttp.get({ url: Api.List, params });
```
## Authentication And Permissions
- Tokens are stored in localStorage through auth utilities.
- `src/router/guard/permissionGuard.ts` fetches user info and dynamic menus from the backend.
- Permission mode is `BACK`, meaning routes and button permissions are backend-driven.
- Button-level auth uses the `v-auth="'system:user:add'"` directive or the `usePermission()` hook.
## Component Settings
Table pagination uses `pageNo` and `pageSize` params and expects `records` and `total` in the response. This is configured in `src/settings/componentSetting.ts`.
## Environment Variables
- `.env` contains base config such as port, app title, SSO, and qiankun toggles.
- `.env.development` contains dev proxy, mock toggle, and backend URL.
- `.env.production` contains production API URL and gzip config.
- Key variables include `VITE_GLOB_DOMAIN_URL`, `VITE_GLOB_API_URL`, and `VITE_PROXY`.
## Build System
`vite.config.ts` delegates plugin setup to `build/vite/plugin/`. Plugins include HTML template handling, mock data, gzip compression, SVG sprites, dynamic theme, qiankun micro-frontend, and PWA. Build scripts under `build/script/` handle post-build tasks.
## Online Forms
Online forms are a JeecgBoot low-code core feature. They generate CRUD pages from configuration. The full schema reference is in `src/views/super/online/cgform/online-form-schema.md` and covers table metadata, field configuration, control types, validation rules, and JavaScript enhancements.
## Code Style
- Prettier uses 150 character line width, single quotes, trailing commas for ES5, semicolons, and 2-space indentation.
- ESLint uses `vue/vue3-recommended`, `@typescript-eslint/recommended`, and Prettier with Prettier rules disabled.
- Unused variables prefixed with `_` are allowed.
- Prefer `<script setup>` for new Vue components.
- Use Less for styling.
## Working Exclusions
Treat the existing `.claudeignore` as the source for routine Codex work exclusions.
Avoid broad searches, reads, or edits in these paths unless the task explicitly targets them or verification requires them:
- `.git/`, `.svn/`, `.idea/`, `.vscode/`
- `node_modules/`, `dist/`, `target/`, `docker/data/`
- `backup/`, `.history/`, `.cursor/`
- `doc/`, `docs/`
- IDE metadata such as `*.iml`, `*.iws`, `*.ipr`, `.classpath`, `.project`, `.settings/`, and `out/`
- OS files such as `.DS_Store`, `Thumbs.db`, and `desktop.ini`
- Logs and generated artifacts such as `*.log`, `logs/`, `*.class`, `*.zip`, `*.qqy`, `代码修改.log`, and `代码修改日志`
The `.claudeignore` file lists `build/`, but this frontend uses `build/` for Vite plugins and build scripts. Avoid broad scans there, but allow targeted reads and edits when working on build tooling.

View File

@ -1,9 +1,9 @@
import { generate } from '@ant-design/colors'; import { generate } from '@ant-design/colors';
import setting from '../../src/settings/projectSetting'; import setting from '../../src/settings/projectSetting';
// / src/settings/projectSetting.ts
// store build
// projectSetting.ts enumsdesignSettinggetConfigByMenuType // projectSetting.ts enumsdesignSettinggetConfigByMenuType
// vite 8 config (rolldown) tsconfig (/@/@/ ) // vite 8 config (rolldown) tsconfig (/@/@/ )
// : JHHB-579
export const primaryColor = setting.themeColor; export const primaryColor = setting.themeColor;
export const darkMode = setting.themeMode; export const darkMode = setting.themeMode;
type Fn = (...arg: any) => any; type Fn = (...arg: any) => any;

View File

@ -62,7 +62,17 @@ export async function createVitePlugins(
}), }),
]; ];
vitePlugins.push(UnoCSS({ presets: [presetUno(), presetTypography()] })); // update-begin--author:copilot---date:20260711---for: UnoCSS /
// '[orderby:...]''[09:00]' / UnoCSS [prop:value]
// CSS CSS JS esbuild/lightningcss minify
// vite8 lightningcss blocklist
vitePlugins.push(
UnoCSS({
presets: [presetUno(), presetTypography()],
blocklist: [/^\[orderby:.*\]$/, /^\[uploading:.*\]$/, /^\[\d{2}:\d{2}\]$/],
})
);
// update-end--author:copilot---date:20260711---for: UnoCSS /
// update-begin--author:liaozhiyang---date:20260302---for:QQYUN-14806antdunplugin-vue-components // update-begin--author:liaozhiyang---date:20260302---for:QQYUN-14806antdunplugin-vue-components
// unplugin-vue-components: ant-design-vue // unplugin-vue-components: ant-design-vue
vitePlugins.push( vitePlugins.push(

View File

@ -24,15 +24,15 @@
}, },
"dependencies": { "dependencies": {
"@jeecg/aiflow": "3.9.3", "@jeecg/aiflow": "3.9.3",
"@logicflow/core": "^2.2.4",
"@logicflow/extension": "^2.3.0",
"@logicflow/vue-node-registry": "^1.2.4",
"@iconify/iconify": "^3.1.1", "@iconify/iconify": "^3.1.1",
"@ant-design/colors": "^7.2.1", "@ant-design/colors": "^7.2.1",
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "^7.0.1",
"@vue/shared": "^3.5.39", "@vue/shared": "^3.5.39",
"@vueuse/core": "^10.11.1", "@vueuse/core": "^10.11.1",
"@tinymce/tinymce-vue": "4.0.7", "@tinymce/tinymce-vue": "4.0.7",
"@logicflow/core": "^2.2.4",
"@logicflow/extension": "^2.3.0",
"@logicflow/vue-node-registry": "^1.2.4",
"@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/core": "^3.0.4",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"axios": "^1.18.1", "axios": "^1.18.1",
@ -84,6 +84,7 @@
"vxe-table-plugin-antd": "4.0.8", "vxe-table-plugin-antd": "4.0.8",
"xe-utils": "3.5.26", "xe-utils": "3.5.26",
"xss": "^1.0.15", "xss": "^1.0.15",
"@onlyoffice/document-editor-vue": "^1.6.1",
"vue-grid-layout-v3": "^3.1.2", "vue-grid-layout-v3": "^3.1.2",
"lunar-javascript": "^1.7.7", "lunar-javascript": "^1.7.7",
"perfect-scrollbar": "^1.5.6", "perfect-scrollbar": "^1.5.6",

File diff suppressed because one or more lines are too long

View File

@ -45,6 +45,8 @@ export interface GetUserInfoModel {
userInfo?: any; userInfo?: any;
// //
sysAllDictItems?: any; sysAllDictItems?: any;
//
enableStrongPwd?: boolean;
} }
/** /**

View File

@ -11,10 +11,14 @@ const { uploadUrl = '' } = useGlobSetting();
export function uploadApi(params: UploadFileParams, onUploadProgress: (progressEvent: ProgressEvent) => void) { export function uploadApi(params: UploadFileParams, onUploadProgress: (progressEvent: ProgressEvent) => void) {
return defHttp.uploadFile<UploadApiResult>( return defHttp.uploadFile<UploadApiResult>(
{ {
url: uploadUrl, url: `${uploadUrl}/sys/common/upload`,
onUploadProgress, onUploadProgress,
}, },
params params,
// update-begin--author:liaozhiyang---date:20260804---forLHZP-1385
// undefined
{ isReturnResponse: true }
// update-end--author:liaozhiyang---date:20260804---forLHZP-1385
); );
} }
/** /**

View File

@ -24,6 +24,10 @@
* @default: #ffffff * @default: #ffffff
*/ */
color: { type: String, default: '#ffffff' }, color: { type: String, default: '#ffffff' },
/**
* Tooltip background color
*/
backgroundColor: { type: String },
/** /**
* Help text font size * Help text font size
* @default: 14px * @default: 14px
@ -77,6 +81,7 @@
<Tooltip <Tooltip
overlayClassName={`${prefixCls}__wrap`} overlayClassName={`${prefixCls}__wrap`}
title={<div style={unref(getTooltipStyle)}>{renderTitle()}</div>} title={<div style={unref(getTooltipStyle)}>{renderTitle()}</div>}
color={props.backgroundColor}
autoAdjustOverflow={true} autoAdjustOverflow={true}
overlayStyle={unref(getOverlayStyle)} overlayStyle={unref(getOverlayStyle)}
placement={props.placement as 'right'} placement={props.placement as 'right'}

View File

@ -91,6 +91,8 @@ componentMap.set('RangeDate', createAsyncComponent(() => import('./jeecg/compone
componentMap.set('RangeTime', createAsyncComponent(() => import('./jeecg/components/JRangeTime.vue'))); componentMap.set('RangeTime', createAsyncComponent(() => import('./jeecg/components/JRangeTime.vue')));
componentMap.set('RoleSelect', createAsyncComponent(() => import('./jeecg/components/roleSelect/RoleSelectInput.vue'))); componentMap.set('RoleSelect', createAsyncComponent(() => import('./jeecg/components/roleSelect/RoleSelectInput.vue')));
componentMap.set('JInputSelect', createAsyncComponent(() => import('./jeecg/components/JInputSelect.vue'))); componentMap.set('JInputSelect', createAsyncComponent(() => import('./jeecg/components/JInputSelect.vue')));
componentMap.set('JAuthRuleValue', createAsyncComponent(() => import('./jeecg/components/JAuthRuleValue.vue')));
componentMap.set('JMultiTextTagInput', createAsyncComponent(() => import('./jeecg/components/JMultiTextTagInput.vue')));
componentMap.set('JSelectDepartPost', createAsyncComponent(() => import('./jeecg/components/JSelectDepartPost.vue'))); componentMap.set('JSelectDepartPost', createAsyncComponent(() => import('./jeecg/components/JSelectDepartPost.vue')));
componentMap.set('JSelectUserByDeptPost', createAsyncComponent(() => import('./jeecg/components/JSelectUserByDeptPost.vue'))); componentMap.set('JSelectUserByDeptPost', createAsyncComponent(() => import('./jeecg/components/JSelectUserByDeptPost.vue')));

View File

@ -4,10 +4,11 @@
v-model:value="state" v-model:value="state"
:options="getOptions" :options="getOptions"
show-search show-search
:filter-option="filterOption" :filter-option="getFilterOption"
@change="handleChange" @change="handleChange"
@dropdownVisibleChange="handleFetch" @dropdownVisibleChange="handleFetch"
@popupScroll="handlePopupScroll" @popupScroll="handlePopupScroll"
@search="handleSearch"
> >
<template #[item]="data" v-for="item in Object.keys($slots)"> <template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot> <slot :name="item" v-bind="data || {}"></slot>
@ -66,6 +67,7 @@
labelField: propTypes.string.def('label'), labelField: propTypes.string.def('label'),
valueField: propTypes.string.def('value'), valueField: propTypes.string.def('value'),
immediate: propTypes.bool.def(true), immediate: propTypes.bool.def(true),
searchDebounce: propTypes.number.def(300),
}, },
emits: ['options-change', 'change'], emits: ['options-change', 'change'],
setup(props, { emit }) { setup(props, { emit }) {
@ -74,6 +76,8 @@
const isFirstLoad = ref(true); const isFirstLoad = ref(true);
const emitData = ref<any[]>([]); const emitData = ref<any[]>([]);
const attrs = useAttrs(); const attrs = useAttrs();
const searchValue = ref('');
let searchTimer: ReturnType<typeof setTimeout> | null = null;
const { t } = useI18n(); const { t } = useI18n();
// : QQYUN-11831ApiSelect #7883 // : QQYUN-11831ApiSelect #7883
const hasMore = ref(true); const hasMore = ref(true);
@ -82,7 +86,14 @@
pageSize: 10, pageSize: 10,
total: 0, total: 0,
}); });
const defPageConfig = { isPage: false, pageField: 'pageNo', pageSizeField: 'pageSize', totalField: 'total', listField: 'records' }; // update-begin--author:liaozhiyang---date:20260617---for:issue/9689 + ,
const defPageConfig = { isPage: false, pageField: 'pageNo', pageSizeField: 'pageSize', totalField: 'total', listField: 'records', searchField: '' };
const mergedPageConfig = computed(() => ({ ...defPageConfig, ...props.pageConfig }));
const isRemoteSearch = computed(() => {
const { isPage, searchField } = mergedPageConfig.value;
return !!(isPage && searchField);
});
// update-end--author:liaozhiyang---date:20260617---for:issue/9689 + ,
// update-end--author:liusq---date:20250407---forQQYUN-11831ApiSelect #7883 // update-end--author:liusq---date:20250407---forQQYUN-11831ApiSelect #7883
// Embedded in the form, just use the hook binding to perform form verification // Embedded in the form, just use the hook binding to perform form verification
const [state, setState] = useRuleFormItem(props, 'value', 'change', emitData); const [state, setState] = useRuleFormItem(props, 'value', 'change', emitData);
@ -159,6 +170,22 @@
const filterOption = (input: string, option: any) => { const filterOption = (input: string, option: any) => {
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 || option.label.indexOf(input) >= 0; return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 || option.label.indexOf(input) >= 0;
}; };
// update-begin--author:liaozhiyang---date:20260617---for:issue/9689 + ,
const getFilterOption = computed(() => {
return isRemoteSearch.value ? false : filterOption;
});
function handleSearch(input: string) {
if (!isRemoteSearch.value) return;
if (searchTimer) clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
if (searchValue.value === input) return;
searchValue.value = input;
pagination.value.pageNo = 1;
hasMore.value = true;
fetch();
}, props.searchDebounce);
}
// update-end--author:liaozhiyang---date:20260617---for:issue/9689 + ,
async function fetch() { async function fetch() {
const api = props.api; const api = props.api;
if (!api || !isFunction(api)) return; if (!api || !isFunction(api)) return;
@ -168,10 +195,16 @@
} }
try { try {
loading.value = true; loading.value = true;
let { isPage, pageField, pageSizeField, totalField, listField } = { ...defPageConfig, ...props.pageConfig }; let { isPage, pageField, pageSizeField, totalField, listField, searchField } = mergedPageConfig.value;
let params = isPage let params = isPage
? { ...props.params, [pageField]: pagination.value.pageNo, [pageSizeField]: pagination.value.pageSize } ? { ...props.params, [pageField]: pagination.value.pageNo, [pageSizeField]: pagination.value.pageSize }
: { ...props.params }; : { ...props.params };
// update-begin--author:liaozhiyang---date:20260617---for:issue/9689 + ,
//
if (isPage && searchField && searchValue.value) {
params[searchField] = searchValue.value;
}
// update-end--author:liaozhiyang---date:20260617---for:issue/9689 + ,
const res = await api(params); const res = await api(params);
if (isPage) { if (isPage) {
// : QQYUN-11831ApiSelect #7883 // : QQYUN-11831ApiSelect #7883
@ -236,7 +269,7 @@
fetch(); fetch();
} }
} }
return { state, attrs_, attrs, getOptions, loading, t, handleFetch, handleChange, handlePopupScroll,filterOption }; return { state, attrs_, attrs, getOptions, loading, t, handleFetch, handleChange, handlePopupScroll, filterOption, getFilterOption, handleSearch };
}, },
}); });
</script> </script>

View File

@ -164,24 +164,31 @@
return { isShow, isIfShow }; return { isShow, isIfShow };
} }
// : TV360X-434validator // : TV360X-434validator
//update-begin---author:liaozhiyang ---date:20260518 for[issues/9638]prevValueArrvalidator/-----------
let vSwitchArr: any = [], let vSwitchArr: any = [],
prevValidatorArr: any = []; prevValidatorArr: any = [],
prevValueArr: any = [];
const hijackValidator = (rules) => { const hijackValidator = (rules) => {
vSwitchArr = []; vSwitchArr = [];
prevValidatorArr = []; prevValidatorArr = [];
prevValueArr = [];
rules.forEach((item, index) => { rules.forEach((item, index) => {
const fn = item.validator; const fn = item.validator;
vSwitchArr.push(true); vSwitchArr.push(true);
prevValidatorArr.push(null); prevValidatorArr.push(null);
prevValueArr.push(undefined);
if (isFunction(fn)) { if (isFunction(fn)) {
item.validator = (rule, value, callback) => { item.validator = (rule, value, callback) => {
if (vSwitchArr[index]) { // 100ms
const valueChanged = !Object.is(prevValueArr[index], value);
if (vSwitchArr[index] || valueChanged) {
vSwitchArr[index] = false; vSwitchArr[index] = false;
setTimeout(() => { setTimeout(() => {
vSwitchArr[index] = true; vSwitchArr[index] = true;
}, 100); }, 100);
const result = fn(rule, value, callback); const result = fn(rule, value, callback);
prevValidatorArr[index] = result; prevValidatorArr[index] = result;
prevValueArr[index] = value;
return result; return result;
} else { } else {
return prevValidatorArr[index]; return prevValidatorArr[index];
@ -190,6 +197,7 @@
} }
}); });
}; };
//update-end---author:liaozhiyang ---date:20260518 for[issues/9638]prevValueArrvalidator/-----------
function handleRules(): ValidationRule[] { function handleRules(): ValidationRule[] {
const { rules: defRules = [], component, rulesMessageJoinLabel, label, dynamicRules, required, auth, field } = props.schema; const { rules: defRules = [], component, rulesMessageJoinLabel, label, dynamicRules, required, auth, field } = props.schema;
// : TV360X-857 // : TV360X-857

View File

@ -26,6 +26,11 @@ export function createPlaceholderMessage(component: ComponentType) {
// return `${label}`; // return `${label}`;
return t('common.chooseText'); return t('common.chooseText');
} }
//update-begin---author:liaozhiyang ---date:20260518 for[issues/9638]BasicForm使JUploadUpload-----------
if (component.includes('Upload')) {
return t('common.uploadText');
}
//update-end---author:liaozhiyang ---date:20260518 for[issues/9638]BasicForm使JUploadUpload-----------
return ''; return '';
} }

View File

@ -1,5 +1,12 @@
<template> <template>
<JCascader v-bind="attrs" :value="cascaderValue" :showLastLevelOnly="showLastLevelOnly" :options="getOptions" @change="handleChange" /> <JCascader
v-bind="attrs"
:disabled="disabled"
:value="cascaderValue"
:showLastLevelOnly="showLastLevelOnly"
:options="getOptions"
@change="handleChange"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, watchEffect, computed } from 'vue'; import { defineComponent, ref, watchEffect, computed } from 'vue';
@ -23,6 +30,9 @@
displayLevel: propTypes.oneOf(['province', 'city', 'region', 'all']), displayLevel: propTypes.oneOf(['province', 'city', 'region', 'all']),
// //
showArea: propTypes.bool.def(true), showArea: propTypes.bool.def(true),
// update-begin--author:liaozhiyang---date:20260804---forLHZP-707x
disabled: propTypes.bool.def(false),
// update-end--author:liaozhiyang---date:20260804---forLHZP-707x
// allprovince, city, region // allprovince, city, region
saveCode: propTypes.oneOf(['province', 'city', 'region', 'all']).def('all'), saveCode: propTypes.oneOf(['province', 'city', 'region', 'all']).def('all'),
}, },

View File

@ -0,0 +1,230 @@
<template>
<div class="JAuthRuleValue">
<a-input-group compact>
<!-- 系统变量选择器使用 Dropdown 而非 Select避免被外层 FormItem 二次收集 -->
<a-dropdown class="sys-var-dropdown" :trigger="['click']" :getPopupContainer="getPopupContainer" v-model:open="dropdownVisible">
<a-button type="default" size="middle" :class="isSysVarMode ? 'sys-var-button-active' : 'sys-var-button-inactive'">
<span class="sys-var-label">{{ selectedSysVarLabel || selectPlaceholder }}</span>
<DownOutlined />
</a-button>
<template #overlay>
<a-menu @click="handleMenuClick">
<a-menu-item v-for="item in options" :key="item.value">{{ item.label }}</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<!-- 默认文本输入未配置专业组件 当前为系统变量模式 -->
<a-input
v-if="!innerComponent || isSysVarMode"
allow-clear
:placeholder="inputPlaceholder"
v-model:value="inputVal"
:getPopupContainer="getPopupContainer"
@change="handleInputChange"
/>
<!-- 字典选择 -->
<JDictSelectTag v-else-if="innerComponent === 'JDictSelectTag'" v-bind="innerProps" :value="inputVal" @change="handleCustomChange" />
<!-- 关联记录 -->
<div v-else-if="innerComponent === 'JLinkTableCard'" class="link-table-rule-value">
<JLinkTableCard v-bind="innerProps" :value="inputVal" @change="handleCustomChange" />
</div>
<!-- Popup 字典 -->
<JPopupDict v-else-if="innerComponent === 'JPopupDict'" v-bind="innerProps" :value="inputVal" @change="handleCustomChange" />
<!-- 用户选择 -->
<JSelectUser v-else-if="innerComponent === 'JSelectUser'" v-bind="innerProps" :value="inputVal" @change="handleCustomChange" />
<!-- 部门选择 -->
<JSelectDept v-else-if="innerComponent === 'JSelectDept'" v-bind="innerProps" :value="inputVal" @change="handleCustomChange" />
<!-- 省市区 -->
<JAreaLinkage v-else-if="innerComponent === 'JAreaLinkage'" v-bind="innerProps" :value="inputVal" @change="handleCustomChange" />
<!-- 日期 -->
<DatePicker
v-else-if="innerComponent === 'DatePicker'"
v-bind="innerProps"
:value="datePickerValue"
v-model:open="datePickerOpen"
@change="handleDatePickerChange"
/>
<!-- 时间 -->
<TimePicker v-else-if="innerComponent === 'TimePicker'" v-bind="innerProps" :value="inputVal" @change="handleCustomChange" />
</a-input-group>
</div>
</template>
<script setup name="JAuthRuleValue" lang="ts">
import { ref, watchEffect, computed } from 'vue';
import { propTypes } from '/@/utils/propTypes';
import { DownOutlined } from '@ant-design/icons-vue';
import JDictSelectTag from './JDictSelectTag.vue';
import JLinkTableCard from './JLinkTableCard/JLinkTableCard.vue';
import JPopupDict from './JPopupDict.vue';
import JSelectUser from './JSelectUser.vue';
import JSelectDept from './JSelectDept.vue';
import JAreaLinkage from './JAreaLinkage.vue';
import { DatePicker, TimePicker } from 'ant-design-vue';
import dayjs from 'dayjs';
import weekOfYear from 'dayjs/plugin/weekOfYear';
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
dayjs.extend(weekOfYear);
dayjs.extend(quarterOfYear);
const props = defineProps({
value: propTypes.oneOfType([propTypes.string, propTypes.number, propTypes.array]),
// { component: 'JSelectUser', componentProps: {...} }
customComponent: propTypes.object.def(null),
//
options: propTypes.array.def([]),
selectPlaceholder: propTypes.string.def('选择系统变量'),
inputPlaceholder: propTypes.string.def('请输入'),
getPopupContainer: {
type: Function,
default: () => document.body,
},
});
const emit = defineEmits(['update:value', 'change']);
const inputVal = ref<any>();
const dropdownVisible = ref(false);
const datePickerOpen = ref(false);
const innerComponent = computed(() => props.customComponent?.component);
const innerProps = computed(() => props.customComponent?.componentProps || {});
// /// DatePicker YYYY-MM-DD
const datePickerValue = computed(() => {
const value = inputVal.value;
const picker = innerProps.value?.picker;
if (!picker || typeof value !== 'string' || /^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
if (picker === 'year' && /^\d{4}$/.test(value)) return `${value}-01-01`;
if (picker === 'month' && /^\d{4}-\d{2}$/.test(value)) return `${value}-01`;
const quarterMatch = value.match(/^(\d{4})-[Qq]([1-4])$/);
if (picker === 'quarter' && quarterMatch) {
return dayjs(`${quarterMatch[1]}-01-01`).quarter(Number(quarterMatch[2])).startOf('quarter').format('YYYY-MM-DD');
}
const weekMatch = value.match(/^(\d{4})-(\d+)/);
if (picker === 'week' && weekMatch) {
return dayjs(`${weekMatch[1]}-01-01`).week(Number(weekMatch[2])).startOf('week').format('YYYY-MM-DD');
}
return value;
});
const isSysVar = (val) => val != null && props.options.some((item) => item.value === val);
// 退 #{...}
const isSysVarMode = computed(() => isSysVar(inputVal.value));
const selectedSysVarLabel = computed(() => {
const found = props.options.find((item) => item.value === inputVal.value);
return found ? found.label : '';
});
const handleInputChange = (e) => {
emits(e.target.value);
};
const handleMenuClick = ({ key }) => {
inputVal.value = key;
emits(key);
dropdownVisible.value = false;
};
const handleCustomChange = (val) => {
inputVal.value = val;
emits(val);
};
const handleDatePickerChange = (val) => {
handleCustomChange(val);
datePickerOpen.value = false;
};
const emits = (val) => {
emit('update:value', val);
emit('change', val);
};
watchEffect(() => {
inputVal.value = props.value;
});
</script>
<style lang="less" scoped>
.JAuthRuleValue {
.ant-input-group {
display: flex;
width: 100%;
}
// a-dropdown class 使
.sys-var-dropdown.ant-btn {
flex: 0 0 auto;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
box-shadow: none;
transition:
background-color 0.2s,
border-color 0.2s,
color 0.2s;
}
.sys-var-dropdown.sys-var-button-inactive {
border-color: @border-color-base !important;
background: fade(@text-color, 2%) !important;
color: fade(@text-color, 35%) !important;
font-weight: 400;
&:hover,
&:focus,
&:active {
border-color: @border-color-base !important;
background: fade(@text-color, 5%) !important;
color: fade(@text-color, 50%) !important;
}
}
.sys-var-dropdown.sys-var-button-active {
border-color: @primary-color !important;
background: fade(@primary-color, 8%) !important;
color: @primary-color !important;
&:hover,
&:focus,
&:active {
background: fade(@primary-color, 12%) !important;
color: @primary-color !important;
}
}
.sys-var-label {
display: inline-block;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.ant-input-group > .ant-input,
.ant-input-group > :deep(.ant-select),
.ant-input-group > :deep(.ant-radio-group),
.ant-input-group > .JselectUser,
.ant-input-group > .JSelectDept,
.ant-input-group > .JAreaLinkage,
.ant-input-group > :deep(.JPopupDict),
.ant-input-group > .link-table-rule-value,
.ant-input-group > .ant-picker {
flex: 1;
min-width: 0;
}
:deep(.JPopupDict > .ant-select),
:deep(.JPopupDict > .ant-input) {
width: 100%;
}
:deep(.JPopupDict > .ant-select .ant-select-selector),
:deep(.JPopupDict > .ant-input) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.link-table-rule-value {
padding-left: 8px;
}
}
</style>

View File

@ -4,8 +4,15 @@
--> -->
<template> <template>
<div class="cascader" ref="rootRef"> <div class="cascader" ref="rootRef">
<AInput :value="displayText" :placeholder="inputPlaceholder" readonly :disabled="inputDisabled" v-bind="attrs" <AInput
:class="{ 'cascader-input-open': visible }" @click="toggle"> :value="displayText"
:placeholder="inputPlaceholder"
readonly
:disabled="inputDisabled"
v-bind="inputAttrs"
:class="{ 'cascader-input-open': visible }"
@click="toggle"
>
<template #suffix> <template #suffix>
<span v-if="displayText && !inputDisabled" class="ant-input-clear-icon ant-input-clear-icon-has-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> role="button" tabindex="-1" @click.stop="handleClear" @mousedown.prevent>
@ -36,7 +43,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onBeforeUnmount, nextTick } from 'vue'; import { ref, computed, watch, onBeforeUnmount, nextTick, unref } from 'vue';
import { Input as AInput } from 'ant-design-vue'; import { Input as AInput } from 'ant-design-vue';
import { DownOutlined, RightOutlined, CloseCircleFilled } from '@ant-design/icons-vue'; import { DownOutlined, RightOutlined, CloseCircleFilled } from '@ant-design/icons-vue';
import { useAttrs } from '/@/hooks/core/useAttrs'; import { useAttrs } from '/@/hooks/core/useAttrs';
@ -81,8 +88,15 @@ const emit = defineEmits<{
}>(); }>();
const attrs = useAttrs(); const attrs = useAttrs();
const inputPlaceholder = computed(() => String((attrs as Record<string, unknown>).placeholder ?? '请选择')); const inputPlaceholder = computed(() => String(unref(attrs as Record<string, unknown>).placeholder ?? '请选择'));
const inputDisabled = computed(() => Boolean((attrs as Record<string, unknown>).disabled)); const inputDisabled = computed(() => Boolean(unref(attrs as Record<string, unknown>).disabled));
// update-begin--author:liaozhiyang---date:20260804---forLHZP-707x
/** 排除 allowClear/disabled清空按钮由 suffix 自定义实现 */
const inputAttrs = computed(() => {
const { allowClear: _allowClear, disabled: _disabled, ...rest } = unref(attrs as Record<string, unknown>);
return rest;
});
// update-end--author:liaozhiyang---date:20260804---forLHZP-707x
const rootRef = ref<HTMLElement>(); const rootRef = ref<HTMLElement>();
const dropdownRef = ref<HTMLElement>(); const dropdownRef = ref<HTMLElement>();
/** 下拉是否展开 */ /** 下拉是否展开 */
@ -179,6 +193,7 @@ function toggle() {
/** 清除选中:与 UI 库 readonly 下不显示清除按钮的补偿,在 suffix 中手动实现 */ /** 清除选中:与 UI 库 readonly 下不显示清除按钮的补偿,在 suffix 中手动实现 */
function handleClear() { function handleClear() {
if (inputDisabled.value) return;
emit('change', []); emit('change', []);
emit('update:value', []); emit('update:value', []);
visible.value = false; visible.value = false;
@ -197,12 +212,40 @@ function handleClickOutside(e: MouseEvent) {
function updatePopupPosition() { function updatePopupPosition() {
if (!rootRef.value) return; if (!rootRef.value) return;
const rect = rootRef.value.getBoundingClientRect(); const rect = rootRef.value.getBoundingClientRect();
dropdownPlaceStyle.value = { // update-begin--author:liaozhiyang---date:20260714---forLHZP-100
left: `${rect.left}px`, const margin = 8;
top: `${rect.bottom + DROPDOWN_GAP}px`, const viewportWidth = document.documentElement.clientWidth;
}; const viewportHeight = document.documentElement.clientHeight;
// 退
const dropdownWidth = dropdownRef.value?.offsetWidth ?? 0;
const dropdownHeight = dropdownRef.value?.offsetHeight ?? 0;
// 使
let left = rect.left;
if (dropdownWidth && left + dropdownWidth > viewportWidth - margin) {
left = viewportWidth - dropdownWidth - margin;
}
if (left < margin) left = margin;
//
let top = rect.bottom + DROPDOWN_GAP;
if (dropdownHeight && top + dropdownHeight > viewportHeight - margin) {
const aboveTop = rect.top - DROPDOWN_GAP - dropdownHeight;
top = aboveTop >= margin ? aboveTop : Math.max(margin, viewportHeight - dropdownHeight - margin);
}
dropdownPlaceStyle.value = { left: `${left}px`, top: `${top}px` };
// update-end--author:liaozhiyang---date:20260714---forLHZP-100
} }
// update-begin--author:liaozhiyang---date:20260714---forLHZP-100
watch(columns, () => {
if (visible.value) {
nextTick(updatePopupPosition);
}
});
// update-end--author:liaozhiyang---date:20260714---forLHZP-100
/** 展开时:用当前 value 初始化 editingPath、计算定位、注册点击外部关闭收起时移除监听 */ /** 展开时:用当前 value 初始化 editingPath、计算定位、注册点击外部关闭收起时移除监听 */
watch(visible, (v) => { watch(visible, (v) => {
if (v) { if (v) {
@ -248,6 +291,19 @@ onBeforeUnmount(() => {
opacity: 0; opacity: 0;
} }
} }
&.ant-input-affix-wrapper-disabled,
&.ant-input-affix-wrapper-disabled:hover {
cursor: not-allowed;
.ant-input-clear-icon {
display: none !important;
}
.cascader-arrow {
opacity: 1;
}
}
} }
.cascader-arrow { .cascader-arrow {

View File

@ -7,7 +7,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, reactive, ref, watch, unref, computed } from 'vue'; import { defineComponent, onMounted, reactive, ref, watch, unref, computed, nextTick } from 'vue';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
import { useRuleFormItem } from '/@/hooks/component/useFormItem'; import { useRuleFormItem } from '/@/hooks/component/useFormItem';
// //
@ -73,7 +73,7 @@
// //
keywords: propTypes.array.def([]), keywords: propTypes.array.def([]),
}, },
emits: ['change', 'update:value'], emits: ['change', 'update:value', 'ready'],
setup(props, { emit }) { setup(props, { emit }) {
const { getDarkMode } = useRootSetting(); const { getDarkMode } = useRootSetting();
const containerRef = ref(null); const containerRef = ref(null);
@ -161,6 +161,8 @@
// : QQYUN-8473 // : QQYUN-8473
setTimeout(() => { setTimeout(() => {
refresh(); refresh();
// + refresh
emit('ready');
}, 150); }, 150);
}); });
@ -221,6 +223,11 @@
// //
function onToggleFullScreen() { function onToggleFullScreen() {
isFullScreen.value = !isFullScreen.value; isFullScreen.value = !isFullScreen.value;
// update-begin--author:liaozhiyang---date:20260713---for:LHZP-236onlinejs
nextTick(() => {
setTimeout(() => refresh(), 100);
});
// update-end--author:liaozhiyang---date:20260713---for:LHZP-236onlinejs
} }
// : codeEditor // : codeEditor
@ -342,7 +349,9 @@
.full-screen-child, .full-screen-child,
.CodeMirror { .CodeMirror {
height: 100%; // update-begin--author:liaozhiyang---date:20260713---for:LHZP-236onlinejs
height: 100% !important;
// update-end--author:liaozhiyang---date:20260713---for:LHZP-236onlinejs
max-height: 100%; max-height: 100%;
min-height: 100%; min-height: 100%;
} }

View File

@ -1,17 +1,64 @@
<template> <template>
<div> <div>
<BasicModal v-bind="$attrs" @register="register" title="导入EXCEL" :width="600" @cancel="handleClose" :confirmLoading="uploading" destroyOnClose> <BasicModal
<!--是否校验--> v-bind="$attrs"
<div style="margin: 0 5px 5px" v-if="online"> @register="register"
<span style="display: inline-block; height: 32px; line-height: 32px; vertical-align: middle">是否开启校验:</span> title="导入 Excel"
<span style="margin-left: 6px"> :width="620"
:minHeight="0"
:bodyStyle="{ padding: '20px 24px 16px' }"
:canFullscreen="false"
@cancel="handleClose"
:confirmLoading="uploading"
destroyOnClose
>
<div class="j-import-modal">
<!--是否校验-->
<div v-if="online" class="validate-setting">
<div class="validate-setting__content">
<div class="validate-setting__title">导入数据校验</div>
<div class="validate-setting__description">开启后将校验文件内容并反馈不符合规则的数据</div>
</div>
<a-switch :checked="validateStatus == 1" @change="handleChangeValidateStatus" checked-children="" un-checked-children="" /> <a-switch :checked="validateStatus == 1" @change="handleChangeValidateStatus" checked-children="" un-checked-children="" />
</span> </div>
<!--上传-->
<a-upload-dragger
class="import-uploader"
name="file"
accept=".xls,.xlsx"
:multiple="true"
:fileList="fileList"
:showUploadList="false"
@remove="handleRemove"
:beforeUpload="beforeUpload"
>
<template v-if="fileList.length === 0">
<div class="upload-icon">
<InboxOutlined />
</div>
<div class="upload-title">点击或拖拽 Excel 文件到此处</div>
<div class="upload-description">支持 .xls.xlsx 格式可一次选择多个文件</div>
</template>
<div v-else class="selected-files">
<div class="selected-files__header">
<div>
<div class="selected-files__title"><CheckCircleFilled /> 已选择 {{ fileList.length }} 个文件</div>
<div class="selected-files__description">点击此区域可继续添加文件</div>
</div>
<span class="selected-files__add">继续添加</span>
</div>
<div class="selected-files__list">
<div v-for="(file, index) in fileList" :key="file.uid || `${file.name}-${index}`" class="selected-file" @click.stop>
<span class="selected-file__icon"><FileExcelOutlined /></span>
<span class="selected-file__name" :title="file.name">{{ file.name }}</span>
<a-button type="text" class="selected-file__remove" title="移除文件" @click.stop="handleRemove(file)">
<DeleteOutlined />
</a-button>
</div>
</div>
</div>
</a-upload-dragger>
</div> </div>
<!--上传-->
<a-upload name="file" accept=".xls,.xlsx" :multiple="true" :fileList="fileList" @remove="handleRemove" :beforeUpload="beforeUpload">
<a-button preIcon="ant-design:upload-outlined">选择导入文件</a-button>
</a-upload>
<!--页脚--> <!--页脚-->
<template #footer> <template #footer>
<a-button @click="handleClose">关闭</a-button> <a-button @click="handleClose">关闭</a-button>
@ -24,7 +71,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, unref, watchEffect, computed } from 'vue'; import { CheckCircleFilled, DeleteOutlined, FileExcelOutlined, InboxOutlined } from '@ant-design/icons-vue';
import { defineComponent, ref, unref, watchEffect, computed, h } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal'; import { BasicModal, useModalInner } from '/@/components/Modal';
import { useAttrs } from '/@/hooks/core/useAttrs'; import { useAttrs } from '/@/hooks/core/useAttrs';
import { defHttp } from '/@/utils/http/axios'; import { defHttp } from '/@/utils/http/axios';
@ -36,6 +84,10 @@
name: 'JImportModal', name: 'JImportModal',
components: { components: {
BasicModal, BasicModal,
CheckCircleFilled,
DeleteOutlined,
FileExcelOutlined,
InboxOutlined,
}, },
props: { props: {
url: { url: {
@ -71,7 +123,7 @@
const uploadAction = ref(''); const uploadAction = ref('');
const foreignKeys = ref(''); const foreignKeys = ref('');
// //
const validateStatus = ref(0); const validateStatus = ref(1);
const getBindValue = Object.assign({}, unref(props), unref(attrs)); const getBindValue = Object.assign({}, unref(props), unref(attrs));
//url //url
watchEffect(() => { watchEffect(() => {
@ -123,8 +175,8 @@
if (!!online) { if (!!online) {
formData.append('validateStatus', unref(validateStatus)); formData.append('validateStatus', unref(validateStatus));
} }
unref(fileList).forEach((file) => { unref(fileList).forEach((file, index) => {
formData.append('files[]', file); formData.append(`files[${index}]`, file);
}); });
uploading.value = true; uploading.value = true;
@ -153,15 +205,29 @@
// //
function errorTip(tipMessage, fileUrl) { function errorTip(tipMessage, fileUrl) {
let href = glob.uploadUrl + fileUrl; //update-begin---author:scott ---date:20260813 forExcelHTML-----------
const resolvedUrl = new URL(`${glob.uploadUrl}${fileUrl}`, window.location.origin);
const href = ['http:', 'https:'].includes(resolvedUrl.protocol) ? resolvedUrl.href : '#';
const resultMessage = String(tipMessage ?? '')
.replace(/,/g, '')
.replace(/:/g, '');
createWarningModal({ createWarningModal({
title: '导入成功,但是有错误数据!', title: '导入完成,部分数据未导入',
centered: false, centered: true,
content: `<div> width: 480,
<span>${tipMessage}</span><br/> okText: '知道了',
<span>具体详情请<a href = ${href} target="_blank"> 点击下载 </a> </span> content: h('div', { class: 'j-import-result-content' }, [
</div>`, h('div', { class: 'j-import-result-summary' }, [
h('div', { class: 'j-import-result-summary__label' }, '导入结果'),
h('div', { class: 'j-import-result-summary__message' }, resultMessage),
]),
h('a', { class: 'j-import-result-download', href, target: '_blank', rel: 'noopener noreferrer' }, [
h('span', { class: 'j-import-result-download__main' }, [h(FileExcelOutlined), h('span', '下载错误数据明细')]),
h('span', { class: 'j-import-result-download__meta' }, 'Excel'),
]),
]),
}); });
//update-end---author:scott ---date:20260813 forExcelHTML-----------
} }
// //
@ -169,7 +235,7 @@
fileList.value = []; fileList.value = [];
uploading.value = false; uploading.value = false;
foreignKeys.value = arg; foreignKeys.value = arg;
validateStatus.value = 0; validateStatus.value = 1;
} }
return { return {
@ -188,3 +254,239 @@
}, },
}); });
</script> </script>
<style scoped lang="less">
.j-import-modal {
.validate-setting {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
margin-bottom: 16px;
padding: 12px 16px;
border: 1px solid #edf0f5;
border-radius: 8px;
background: #f7f9fc;
}
.validate-setting__content {
min-width: 0;
}
.validate-setting__title {
color: #1f2937;
font-size: 14px;
font-weight: 500;
line-height: 22px;
}
.validate-setting__description {
margin-top: 2px;
color: #8c8c8c;
font-size: 12px;
line-height: 20px;
}
.import-uploader {
:deep(.ant-upload-drag) {
border-color: #cbd5e1;
border-radius: 10px;
background: #fbfcfe;
transition:
border-color 0.2s ease,
background-color 0.2s ease;
}
:deep(.ant-upload-drag:hover) {
border-color: @primary-color;
background: fade(@primary-color, 3%);
}
:deep(.ant-upload-btn) {
padding: 28px 16px 26px;
}
}
.upload-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
margin-bottom: 12px;
border-radius: 14px;
color: @primary-color;
background: fade(@primary-color, 10%);
font-size: 26px;
}
.upload-title {
color: #262626;
font-size: 15px;
font-weight: 500;
line-height: 24px;
}
.upload-description {
margin-top: 4px;
color: #8c8c8c;
font-size: 13px;
line-height: 22px;
}
.selected-files {
width: 100%;
text-align: left;
}
.selected-files__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.selected-files__title {
color: #262626;
font-size: 14px;
font-weight: 500;
line-height: 22px;
:deep(.anticon) {
margin-right: 6px;
color: #52c41a;
}
}
.selected-files__description {
margin-top: 2px;
color: #8c8c8c;
font-size: 12px;
line-height: 20px;
}
.selected-files__add {
flex: none;
color: @primary-color;
font-size: 13px;
}
.selected-files__list {
max-height: 116px;
overflow-y: auto;
}
.selected-file {
display: flex;
align-items: center;
min-height: 38px;
padding: 4px 6px;
border: 1px solid #e8edf3;
border-radius: 6px;
background: #f5f7fa;
transition:
border-color 0.2s ease,
background-color 0.2s ease;
& + & {
margin-top: 4px;
}
&:hover {
border-color: fade(@primary-color, 18%);
background: fade(@primary-color, 6%);
.selected-file__remove {
opacity: 1;
}
}
}
.selected-file__icon {
flex: none;
margin-right: 8px;
color: #22a06b;
font-size: 18px;
}
.selected-file__name {
min-width: 0;
flex: 1;
overflow: hidden;
color: #4b5563;
font-size: 13px;
line-height: 22px;
text-overflow: ellipsis;
white-space: nowrap;
}
.selected-file__remove {
flex: none;
color: #7a8491;
opacity: 0.75;
&:hover {
color: #ff4d4f;
}
}
}
:global(.j-import-result-content) {
margin-top: 4px;
}
:global(.j-import-result-summary) {
padding: 12px 14px;
border: 1px solid #ffe7a3;
border-radius: 8px;
background: #fffaf0;
}
:global(.j-import-result-summary__label) {
margin-bottom: 4px;
color: #ad6800;
font-size: 12px;
font-weight: 500;
line-height: 20px;
}
:global(.j-import-result-summary__message) {
color: #595959;
font-size: 14px;
line-height: 22px;
}
:global(.j-import-result-download) {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
padding: 10px 12px;
border: 1px solid #d9eaf7;
border-radius: 8px;
background: #f7fbff;
transition:
border-color 0.2s ease,
background-color 0.2s ease;
&:hover {
border-color: fade(@primary-color, 35%);
background: fade(@primary-color, 6%);
}
}
:global(.j-import-result-download__main) {
display: inline-flex;
align-items: center;
gap: 8px;
color: @primary-color;
font-size: 14px;
font-weight: 500;
}
:global(.j-import-result-download__meta) {
color: #8c8c8c;
font-size: 12px;
}
</style>

View File

@ -85,7 +85,10 @@
} }
switch (props.type) { switch (props.type) {
case JInputTypeEnum.JINPUT_QUERY_LIKE: case JInputTypeEnum.JINPUT_QUERY_LIKE:
text = '*' + text + '*'; // ! NE * LIKE
if (!text.startsWith('!')) {
text = '*' + text + '*';
}
break; break;
case JInputTypeEnum.JINPUT_QUERY_NE: case JInputTypeEnum.JINPUT_QUERY_NE:
text = '!' + text; text = '!' + text;

View File

@ -0,0 +1,138 @@
<template>
<span ref="rootRef" class="j-left-ellipsis" :class="contentClass" :title="fullText">{{ displayText }}</span>
</template>
<script lang="ts" setup>
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue';
import { propTypes } from '/@/utils/propTypes';
/**
* 左侧省略空间不足时显示 "...右侧文字"保证末尾内容完整可见
* 按指定容器默认 .ant-select-selector的实际宽度测量避免被文案自身撑开导致测不准
*/
defineOptions({ name: 'JLeftEllipsis' });
const props = defineProps({
/** 完整文案 */
text: propTypes.string.def(''),
/** 用于测量可用宽度的容器选择器 */
containerSelector: propTypes.string.def('.ant-select-selector'),
/** 额外预留宽度关闭按钮、padding 等),单位 px */
reserveWidth: propTypes.number.def(40),
/** 附加到根节点的 class */
contentClass: propTypes.string.def(''),
});
const rootRef = ref<HTMLElement | null>(null);
const displayText = ref(props.text || '');
let resizeObserver: ResizeObserver | null = null;
let measureCtx: CanvasRenderingContext2D | null = null;
const fullText = computed(() => (props.text == null ? '' : String(props.text)));
function getMeasureCtx(el: HTMLElement) {
if (!measureCtx) {
measureCtx = document.createElement('canvas').getContext('2d');
}
if (measureCtx) {
const style = getComputedStyle(el);
measureCtx.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
}
return measureCtx;
}
function getMaxTextWidth(el: HTMLElement) {
const box = (el.closest(props.containerSelector) || el.parentElement) as HTMLElement | null;
if (!box) {
return 0;
}
const boxStyle = getComputedStyle(box);
const paddingX = (parseFloat(boxStyle.paddingLeft) || 0) + (parseFloat(boxStyle.paddingRight) || 0);
return Math.max(box.clientWidth - paddingX - (props.reserveWidth || 0), 24);
}
function updateDisplay() {
const el = rootRef.value;
const full = fullText.value;
if (!el || !full) {
displayText.value = full;
return;
}
const maxWidth = getMaxTextWidth(el);
if (maxWidth <= 0) {
displayText.value = full;
return;
}
const ctx = getMeasureCtx(el);
if (!ctx) {
displayText.value = full;
return;
}
if (ctx.measureText(full).width <= maxWidth) {
displayText.value = full;
return;
}
const ellipsis = '...';
const ellipsisWidth = ctx.measureText(ellipsis).width;
let lo = 0;
let hi = full.length;
while (lo < hi) {
const mid = Math.ceil((lo + hi) / 2);
const suffix = full.slice(-mid);
if (ellipsisWidth + ctx.measureText(suffix).width <= maxWidth) {
lo = mid;
} else {
hi = mid - 1;
}
}
displayText.value = lo > 0 ? ellipsis + full.slice(-lo) : ellipsis;
}
function bindObserver() {
resizeObserver?.disconnect();
resizeObserver = null;
if (typeof ResizeObserver === 'undefined' || !rootRef.value) {
return;
}
const el = rootRef.value;
const box = (el.closest(props.containerSelector) || el.parentElement) as HTMLElement | null;
resizeObserver = new ResizeObserver(() => updateDisplay());
if (box) {
resizeObserver.observe(box);
} else {
resizeObserver.observe(el);
}
}
watch(
() => [props.text, props.reserveWidth, props.containerSelector],
() => {
nextTick(() => {
requestAnimationFrame(updateDisplay);
});
}
);
onMounted(() => {
nextTick(() => {
updateDisplay();
requestAnimationFrame(updateDisplay);
bindObserver();
});
});
onBeforeUnmount(() => {
resizeObserver?.disconnect();
resizeObserver = null;
});
</script>
<style lang="less" scoped>
.j-left-ellipsis {
display: block;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: clip;
}
</style>

View File

@ -35,7 +35,8 @@
</a-row> </a-row>
</div> </div>
</div> </div>
<LinkTableListModal @register="registerListModal" :multi="multi" :id="popTableName" @success="addCard" /> <LinkTableListModal v-if="queryMode === 'online'" @register="registerListModal" :multi="multi" :id="popTableName" @success="addCard" />
<AllTableListModal v-else @register="registerListModal" :tableName="tableName" :textField="textField" :valueField="valueField" :imageField="imageField" :multi="multi" @success="addCard" />
</div> </div>
</template> </template>
@ -69,11 +70,14 @@
detail: propTypes.bool.def(false), detail: propTypes.bool.def(false),
// //
imageField: propTypes.string.def(''), imageField: propTypes.string.def(''),
// 'online' Online | 'table'
queryMode: propTypes.string.def('online'),
}, },
components: { components: {
PlusOutlined, PlusOutlined,
MinusCircleFilled, MinusCircleFilled,
LinkTableListModal: createAsyncComponent(() => import('./components/LinkTableListModal.vue'), { loading: true }), LinkTableListModal: createAsyncComponent(() => import('./components/LinkTableListModal.vue'), { loading: true }),
AllTableListModal: createAsyncComponent(() => import('./components/AllTableListModal.vue'), { loading: true }),
}, },
emits: ['change', 'update:value'], emits: ['change', 'update:value'],
setup(props, { emit }) { setup(props, { emit }) {

View File

@ -0,0 +1,528 @@
<template>
<BasicModal
@register="registerModal"
:width="popModalFixedWidth"
:dialogStyle="{ top: '70px' }"
:bodyStyle="modalBodyStyle"
title="选择记录"
wrapClassName="jeecg-all-table-pop-list-modal"
>
<template #footer>
<a-button key="back" @click="handleCancel">关闭</a-button>
<a-button :disabled="submitDisabled" key="submit" type="primary" @click="handleSubmit" :loading="submitLoading">确定</a-button>
</template>
<a-form-item-rest>
<a-form-item class="atm-form-item">
<div class="atm-layout">
<!-- 搜索栏 + 已选提示 -->
<div class="atm-toolbar">
<a-input-search
v-model:value="searchText"
placeholder="请输入关键词,按回车搜索"
style="width: 240px"
@search="doSearch"
@pressEnter="doSearch"
/>
<span v-if="selectedKeys.length > 0" class="atm-selected-tip">
已选中 {{ selectedKeys.length }} 条记录
<a class="atm-clear-link" @click="clearSelection">清空</a>
</span>
</div>
<div class="atm-content">
<!-- 加载中 -->
<div v-if="loading" class="atm-spin-wrap">
<a-spin />
</div>
<!-- 无数据 -->
<a-empty v-else-if="dataList.length === 0" class="atm-empty" description="暂无数据" />
<!-- 卡片网格 -->
<div v-else class="atm-card-grid">
<div
v-for="record in dataList"
:key="getRecordKey(record)"
class="atm-card"
:class="{ 'atm-card--selected': isSelected(record) }"
@click="handleToggle(record)"
>
<!-- 多选模式才显示右上角选中图标 -->
<span v-if="multi" class="atm-card-check">
<CheckSquareFilled v-if="isSelected(record)" class="atm-check-icon--on" />
<BorderOutlined v-else class="atm-check-icon--off" />
</span>
<!-- 内容区 -->
<div class="atm-card-inner" :class="{ 'atm-has-image': hasImage(record) }">
<div class="atm-card-title">{{ getMainContent(record) }}</div>
<div class="atm-card-fields">
<div v-for="field in otherTextFields" :key="field" class="atm-card-field-row">
<span class="atm-field-label">{{ field }}</span>
<span class="atm-field-value">{{ record[field] }}</span>
</div>
</div>
</div>
<!-- 图片区 -->
<div v-if="hasImage(record)" class="atm-card-image">
<img :src="getImageSrc(record)" alt="" @error="handleImageError" />
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="atm-pagination">
<a-pagination
v-model:current="currentPage"
:total="total"
:pageSize="pageSize"
:show-total="(t) => `共 ${t} 条`"
show-size-changer
@change="handlePageChange"
@showSizeChange="handleSizeChange"
/>
</div>
</div>
</a-form-item>
</a-form-item-rest>
</BasicModal>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { defHttp } from '/@/utils/http/axios';
import { useFixedHeightModal } from '../hooks/useLinkTable';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { CheckSquareFilled, BorderOutlined } from '@ant-design/icons-vue';
import placeholderImage from '/@/assets/images/placeholderImage.png';
/** 调用后端 /sys/dict/queryTableDataForLinkRecord 接口 */
function queryTableData(params: Record<string, any>) {
return defHttp.get({ url: '/sys/dict/queryTableDataForLinkRecord', params });
}
export default defineComponent({
name: 'AllTableListModal',
components: { BasicModal, CheckSquareFilled, BorderOutlined },
props: {
tableName: { type: String, default: '' },
textField: { type: String, default: '' },
valueField: { type: String, default: '' },
imageField: { type: String, default: '' },
multi: { type: Boolean, default: false },
},
emits: ['success', 'register'],
setup(props, { emit }) {
const { popModalFixedWidth, resetBodyStyle, popBodyStyle } = useFixedHeightModal();
const modalBodyStyle = computed(() => ({ ...popBodyStyle.value, overflow: 'hidden', padding: '16px 20px' }));
// ---------- ----------
const loading = ref(false);
const dataList = ref<any[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
const searchText = ref('');
// ---------- ----------
const selectedKeys = ref<string[]>([]);
const selectedRecordsMap = ref<Record<string, any>>({});
function getRecordKey(record: any): string {
return String(record[props.valueField] ?? '');
}
function isSelected(record: any): boolean {
return selectedKeys.value.includes(getRecordKey(record));
}
function handleToggle(record: any) {
const key = getRecordKey(record);
if (props.multi) {
const idx = selectedKeys.value.indexOf(key);
if (idx >= 0) {
selectedKeys.value.splice(idx, 1);
delete selectedRecordsMap.value[key];
} else {
selectedKeys.value.push(key);
selectedRecordsMap.value[key] = record;
}
} else {
selectedKeys.value = [key];
selectedRecordsMap.value = { [key]: record };
}
}
function clearSelection() {
selectedKeys.value = [];
selectedRecordsMap.value = {};
}
// ---------- ----------
const mainTextField = computed(() => {
return (props.textField || '').split(',').filter(Boolean)[0] || props.valueField;
});
const otherTextFields = computed(() => {
return (props.textField || '').split(',').filter(Boolean).slice(1);
});
function getMainContent(record: any): string {
return record[mainTextField.value] ?? '';
}
function hasImage(record: any): boolean {
return !!(props.imageField && record[props.imageField]);
}
function getImageSrc(record: any): string {
if (!props.imageField) return '';
let url = record[props.imageField];
if (typeof url === 'string') url = url.split(',')[0];
return getFileAccessHttpUrl(url);
}
function handleImageError(e: Event) {
(e.target as HTMLImageElement).src = placeholderImage;
}
// ---------- ----------
async function loadData() {
if (!props.tableName || !props.valueField || !props.textField) return;
const showFieldSet = new Set((props.textField || '').split(',').filter(Boolean));
if (props.imageField) showFieldSet.add(props.imageField);
const showFields = Array.from(showFieldSet).join(',');
loading.value = true;
try {
const res = await queryTableData({
tableName: props.tableName,
showFields,
valueField: props.valueField,
keyword: searchText.value || undefined,
pageNo: currentPage.value,
pageSize: pageSize.value,
});
dataList.value = res?.records || [];
total.value = res?.total || 0;
} catch (e) {
console.error('[AllTableListModal] 加载数据失败', e);
dataList.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
function doSearch() {
currentPage.value = 1;
loadData();
}
function handlePageChange(page: number) {
currentPage.value = page;
loadData();
}
function handleSizeChange(_current: number, size: number) {
pageSize.value = size;
currentPage.value = 1;
loadData();
}
// ---------- ----------
const [registerModal, { closeModal }] = useModalInner((data) => {
//
searchText.value = '';
currentPage.value = 1;
dataList.value = [];
total.value = 0;
//
selectedKeys.value = (data.selectedRowKeys || []).map(String);
selectedRecordsMap.value = {};
for (const row of data.selectedRows || []) {
if (row && row[props.valueField] != null) {
selectedRecordsMap.value[String(row[props.valueField])] = row;
}
}
//
setTimeout(() => {
loadData();
resetBodyStyle();
}, 100);
});
// ---------- ----------
const submitLoading = ref(false);
const submitDisabled = computed(() => selectedKeys.value.length === 0);
function handleCancel() {
closeModal();
}
function handleSubmit() {
submitLoading.value = true;
const rows = Object.values(selectedRecordsMap.value);
if (rows.length > 0) {
emit('success', rows);
closeModal();
}
setTimeout(() => {
submitLoading.value = false;
}, 200);
}
return {
registerModal,
popModalFixedWidth,
modalBodyStyle,
loading,
dataList,
total,
currentPage,
pageSize,
searchText,
selectedKeys,
getRecordKey,
isSelected,
handleToggle,
clearSelection,
otherTextFields,
getMainContent,
hasImage,
getImageSrc,
handleImageError,
doSearch,
handlePageChange,
handleSizeChange,
handleCancel,
handleSubmit,
submitDisabled,
submitLoading,
};
},
});
</script>
<style scoped lang="less">
.atm-form-item {
height: 100%;
margin-bottom: 0;
:deep(.ant-form-item-row),
:deep(.ant-form-item-control),
:deep(.ant-form-item-control-input),
:deep(.ant-form-item-control-input-content) {
height: 100%;
}
}
.atm-layout {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.atm-toolbar {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 12px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.atm-selected-tip {
padding: 4px 10px;
font-size: 13px;
color: #1890ff;
background: #e6f7ff;
border-radius: 4px;
}
.atm-clear-link {
margin-left: 6px;
color: #ff4d4f;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.atm-spin-wrap {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.atm-content {
flex: 1;
min-height: 0;
padding: 16px 2px;
overflow-y: auto;
}
.atm-empty {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
margin: 0;
}
.atm-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
align-content: start;
gap: 12px;
}
/* 卡片 */
.atm-card {
position: relative;
display: inline-flex;
flex-direction: row;
width: 100%;
background: #fff;
min-height: 104px;
overflow: hidden;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
cursor: pointer;
transition:
box-shadow 0.2s ease,
border-color 0.2s ease,
transform 0.2s ease;
&:hover {
border-color: #91caff;
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.1);
transform: translateY(-1px);
}
&.atm-card--selected {
border-color: #1890ff;
background: #f6fbff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.12);
}
}
/* 选中图标(右上角,位于卡片内部) */
.atm-card-check {
position: absolute;
top: 10px;
right: 10px;
z-index: 1;
line-height: 1;
font-size: 18px;
}
.atm-check-icon--on {
color: #1890ff;
}
.atm-check-icon--off {
color: #d9d9d9;
}
/* 内容区 */
.atm-card-inner {
flex: 1;
min-width: 0;
padding: 16px 34px 14px 16px;
overflow: hidden;
&.atm-has-image {
width: calc(100% - 80px);
}
}
.atm-card-title {
margin-bottom: 8px;
font-size: 15px;
font-weight: 600;
color: #262626;
line-height: 22px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.atm-card-fields {
display: flex;
flex-direction: column;
gap: 3px;
}
.atm-card-field-row {
display: flex;
align-items: center;
gap: 6px;
line-height: 20px;
overflow: hidden;
}
.atm-field-label {
font-size: 12px;
color: #8c8c8c;
white-space: nowrap;
flex-shrink: 0;
}
.atm-field-value {
font-size: 12px;
color: #595959;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 图片区 */
.atm-card-image {
width: 88px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px 12px 0;
overflow: hidden;
img {
width: 64px;
height: 64px;
border-radius: 50%;
background: #f5f5f5;
border: 1px solid #f0f0f0;
object-fit: cover;
}
}
/* 分页 */
.atm-pagination {
display: flex;
justify-content: flex-end;
flex-shrink: 0;
padding-top: 14px;
border-top: 1px solid #f0f0f0;
}
@media (max-width: 992px) {
.atm-card-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.atm-toolbar {
flex-wrap: wrap;
}
}
</style>

View File

@ -12,306 +12,549 @@
<a-button :disabled="submitDisabled" key="submit" type="primary" @click="handleSubmit" :loading="submitLoading">确定</a-button> <a-button :disabled="submitDisabled" key="submit" type="primary" @click="handleSubmit" :loading="submitLoading">确定</a-button>
</template> </template>
<BasicTable ref="tableRef" @register="registerTable" :rowSelection="rowSelection"> <!-- 工具栏搜索 + 已选提示 -->
<template #tableTitle> <div class="ltm-toolbar">
<a-input-search v-model:value="searchText" @search="onSearch" placeholder="请输入关键词,按回车搜索" style="width: 240px" /> <a-input-search
</template> v-model:value="searchText"
@search="onSearch"
@pressEnter="onSearch"
placeholder="请输入关键词,按回车搜索"
style="width: 240px"
/>
<span v-if="cardSelectedKeys.length > 0" class="atm-selected-tip">
已选中 {{ cardSelectedKeys.length }} 条记录
<a class="atm-clear-link" @click="clearCardSelection">清空</a>
</span>
</div>
<!--操作栏--> <!-- 卡片视图 -->
<template #action="{ record }"> <div v-if="cardLoading" class="atm-spin-wrap">
<TableAction :actions="getTableAction(record)"> </TableAction> <a-spin />
</template> </div>
<a-empty v-else-if="cardDataList.length === 0" style="margin: 40px 0" description="暂无数据" />
<div v-else class="atm-card-grid">
<div
v-for="record in cardDataList"
:key="getCardKey(record)"
class="atm-card"
:class="{ 'atm-card--selected': isCardSelected(record) }"
@click="handleCardToggle(record)"
>
<!-- 多选模式右上角图标 -->
<span v-if="multi" class="atm-card-check">
<CheckSquareFilled v-if="isCardSelected(record)" class="atm-check-icon--on" />
<BorderOutlined v-else class="atm-check-icon--off" />
</span>
<template #fileSlot="{ text }"> <!-- 内容区 -->
<span v-if="!text" style="font-size: 12px; font-style: italic">无文件</span> <div class="atm-card-inner" :class="{ 'atm-has-image': hasCardImage(record) }">
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download" size="small" @click="downloadRowFile(text)"> 下载 </a-button> <div class="atm-card-title">{{ getCardMainContent(record) }}</div>
</template> <div class="atm-card-fields">
<div v-for="field in cardOtherDisplayFields" :key="field.key" class="atm-card-field-row">
<span class="atm-field-label">{{ field.title }}</span>
<span class="atm-field-value">{{ record[field.key] }}</span>
</div>
</div>
</div>
<template #imgSlot="{ text }"> <!-- 图片区 -->
<span v-if="!text" style="font-size: 12px; font-style: italic">无图片</span> <div v-if="hasCardImage(record)" class="atm-card-image">
<img v-else :src="getImgView(text)" alt="图片不存在" class="online-cell-image" @click="viewOnlineCellImage(text)" /> <img :src="getCardImageSrc(record)" alt="" @error="handleCardImageError" />
</template> </div>
</div>
</div>
<template #htmlSlot="{ text }"> <!-- 卡片分页 -->
<div v-html="text"></div> <div class="atm-pagination">
</template> <a-pagination
v-model:current="cardCurrentPage"
<template #pcaSlot="{ text, column }"> :total="cardTotal"
<div :title="getPcaText(text, column)">{{ getPcaText(text, column) }}</div> :pageSize="cardPageSize"
</template> :show-total="(t) => `共 ${t} 条`"
show-size-changer
<template #dateSlot="{ text, column }"> @change="handleCardPageChange"
<span>{{ getFormatDate(text, column) }}</span> @showSizeChange="handleCardSizeChange"
</template> />
</BasicTable> </div>
</BasicModal> </BasicModal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, watch, ref, toRaw, computed, nextTick } from 'vue'; import { defineComponent, watch, ref, toRaw, computed } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal'; import { BasicModal, useModalInner } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage'; import { useMessage } from '/@/hooks/web/useMessage';
import { useMessage } from '/@/hooks/web/useMessage'; import { defHttp } from '/@/utils/http/axios';
import { defHttp } from '/@/utils/http/axios'; import { useTableColumns } from '@/views/super/online/cgform/hooks/auto/useTableColumns';
import { useTableColumns } from '@/views/super/online/cgform/hooks/auto/useTableColumns'; import { useFixedHeightModal } from '../hooks/useLinkTable';
import { createAsyncComponent } from '@/utils/factory/createAsyncComponent'; import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { useFixedHeightModal } from '../hooks/useLinkTable'; import { CheckSquareFilled, BorderOutlined } from '@ant-design/icons-vue';
import placeholderImage from '/@/assets/images/placeholderImage.png';
export default defineComponent({ export default defineComponent({
name: 'LinkTableListModal', name: 'LinkTableListModal',
props: { props: {
/**可以是表名 可以是ID*/ /**可以是表名 可以是ID*/
id: { id: {
type: String, type: String,
default: '', default: '',
},
multi: {
type: Boolean,
default: false,
},
addAuth: {
type: Boolean,
default: true,
},
}, },
components: { multi: {
BasicModal, type: Boolean,
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), { loading: true, delay: 1000 }), default: false,
TableAction: createAsyncComponent(() => import('/@/components/Table/src/components/TableAction.vue'), { loading: true, delay: 1000 }),
}, },
emits: ['success', 'register'], addAuth: {
setup(props, { emit }) { type: Boolean,
const { createMessage: $message } = useMessage(); default: true,
const tableRef = ref(null); },
// },
const { popModalFixedWidth, resetBodyStyle, popBodyStyle } = useFixedHeightModal(); components: {
const searchText = ref(''); BasicModal,
const modalWidth = ref(800); CheckSquareFilled,
//useModalInner BorderOutlined,
const [registerModal, { closeModal }] = useModalInner((data) => { },
searchText.value = ''; emits: ['success', 'register'],
selectedRowKeys.value = data.selectedRowKeys; setup(props, { emit }) {
selectedRows.value = data.selectedRows; const { createMessage: $message } = useMessage();
setTimeout(async () => { //
await setPagination({ current: 1 }); const { popModalFixedWidth, resetBodyStyle, popBodyStyle } = useFixedHeightModal();
await reload(); // const searchText = ref('');
resetBodyStyle();
}, 100);
});
function handleCancel() { // ==================== ====================
closeModal(); const cardLoading = ref(false);
} const cardDataList = ref<any[]>([]);
const submitDisabled = computed(() => { const cardTotal = ref(0);
const arr = selectedRowKeys.value; const cardCurrentPage = ref(1);
if (arr && arr.length > 0) { const cardPageSize = ref(10);
return false;
//
const cardSelectedKeys = ref<string[]>([]);
const cardSelectedRecordsMap = ref<Record<string, any>>({});
function getCardKey(record: any): string {
return String(record['id'] ?? '');
}
function isCardSelected(record: any): boolean {
return cardSelectedKeys.value.includes(getCardKey(record));
}
function handleCardToggle(record: any) {
const key = getCardKey(record);
if (props.multi) {
const idx = cardSelectedKeys.value.indexOf(key);
if (idx >= 0) {
cardSelectedKeys.value.splice(idx, 1);
delete cardSelectedRecordsMap.value[key];
} else {
cardSelectedKeys.value.push(key);
cardSelectedRecordsMap.value[key] = record;
} }
return true; } else {
}); cardSelectedKeys.value = [key];
const submitLoading = ref(false); cardSelectedRecordsMap.value = { [key]: record };
function handleSubmit() {
submitLoading.value = true;
let arr = toRaw(selectedRows.value);
if (arr && arr.length > 0) {
emit('success', arr);
closeModal();
}
setTimeout(() => {
submitLoading.value = false;
}, 200);
} }
}
//--------------------------------------------- function clearCardSelection() {
function queryTableData(params) { cardSelectedKeys.value = [];
const url = '/online/cgform/api/getData/' + props.id; cardSelectedRecordsMap.value = {};
return defHttp.get({ url, params }); }
}
function list(params) { // ==================== columns ====================
params['column'] = 'id'; // /
return new Promise(async (resolve, _reject) => { const cardMainField = computed(() => {
const aa = await queryTableData(params); const cols = columns.value || [];
resolve(aa); const first = cols.find((c) => !isImageOrFileColumn(c) && c.dataIndex !== 'action');
return first?.dataIndex || '';
});
// 3
const cardOtherDisplayFields = computed(() => {
const cols = columns.value || [];
const mainKey = cardMainField.value;
return cols
.filter((c) => c.dataIndex !== mainKey && c.dataIndex !== 'action' && !isImageOrFileColumn(c))
.slice(0, 3)
.map((c) => ({ key: c.dataIndex, title: c.title || c.dataIndex }));
});
// imgSlot
const cardImageField = computed(() => {
const cols = columns.value || [];
const imgCol = cols.find((c) => getColumnSlot(c) === 'imgSlot');
return imgCol?.dataIndex || '';
});
function getColumnSlot(col: any): string {
return col?.slots?.customRender || col?.customRender || '';
}
function isImageOrFileColumn(col: any): boolean {
const slot = getColumnSlot(col);
return slot === 'imgSlot' || slot === 'fileSlot';
}
function getCardMainContent(record: any): string {
return record[cardMainField.value] ?? '';
}
function hasCardImage(record: any): boolean {
const field = cardImageField.value;
return !!(field && record[field]);
}
function getCardImageSrc(record: any): string {
const field = cardImageField.value;
if (!field) return '';
let url = record[field];
if (typeof url === 'string') url = url.split(',')[0];
return getFileAccessHttpUrl(url);
}
function handleCardImageError(e: Event) {
(e.target as HTMLImageElement).src = placeholderImage;
}
// ==================== ====================
async function loadCardData() {
if (!props.id) return;
cardLoading.value = true;
try {
const params = addQueryParams({
column: 'id',
pageNo: cardCurrentPage.value,
pageSize: cardPageSize.value,
}); });
const res = await queryTableData(params);
cardDataList.value = res?.records || [];
cardTotal.value = res?.total || 0;
} catch (e) {
console.error('[LinkTableListModal] 卡片数据加载失败', e);
cardDataList.value = [];
cardTotal.value = 0;
} finally {
cardLoading.value = false;
} }
}
const onlineTableContext = { function handleCardPageChange(page: number) {
isPopList: true, cardCurrentPage.value = page;
reloadTable() { loadCardData();
console.log('reloadTable'); }
},
isTree() {
return false;
},
};
const extConfigJson = ref<any>({});
// BasicTable function handleCardSizeChange(_current: number, size: number) {
const { columns, downloadRowFile, getImgView, getPcaText, getFormatDate, handleColumnResult, hrefComponent, viewOnlineCellImage } = cardPageSize.value = size;
useTableColumns(onlineTableContext, extConfigJson); cardCurrentPage.value = 1;
loadCardData();
}
/** // ==================== ====================
* 查询table列信息 及其他配置 const [registerModal, { closeModal }] = useModalInner((data) => {
*/ searchText.value = '';
function getColumnList() {
const url = '/online/cgform/api/getColumns/' + props.id;
return new Promise((resolve, reject) => {
defHttp.get({ url }, { isTransformResponse: false }).then((res) => {
if (res.success) {
resolve(res.result);
} else {
$message.warning(res.message);
reject();
}
});
});
}
const modalTitle = ref(''); //
watch( cardSelectedKeys.value = (data.selectedRowKeys || []).map(String);
() => props.id, cardSelectedRecordsMap.value = {};
async () => { for (const row of data.selectedRows || []) {
let columnResult: any = await getColumnList(); if (row && row['id'] != null) {
handleColumnResult(columnResult); cardSelectedRecordsMap.value[String(row['id'])] = row;
modalTitle.value = columnResult.description;
},
{ immediate: true }
);
const { tableContext } = useListPage({
designScope: 'process-design',
pagination: true,
tableProps: {
title: '',
api: list,
clickToRowSelect: true,
columns: columns,
showTableSetting: false,
immediate: false,
//showIndexColumn: true,
canResize: false,
showActionColumn: false,
actionColumn: {
dataIndex: 'action',
slots: { customRender: 'action' },
},
useSearchForm: false,
beforeFetch: (params) => {
return addQueryParams(params);
},
},
});
const [registerTable, { reload, setPagination }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
watch(
() => props.multi,
(val) => {
if (val == true) {
rowSelection.type = 'checkbox';
} else {
rowSelection.type = 'radio';
}
},
{ immediate: true }
);
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleUpdate.bind(null, record),
},
];
}
function handleUpdate(record) {
console.log('handleUpdate', record);
}
function onSearch() {
reload();
}
const eqConditonTypes = ['int', 'double', 'Date', 'Datetime', 'BigDecimal'];
function addQueryParams(params) {
let text = searchText.value;
if (!text) {
params['superQueryMatchType'] = 'or';
params['superQueryParams'] = '';
return params;
} }
let arr = columns.value;
let conditions: any[] = [];
if (arr && arr.length > 0) {
for (let item of arr) {
if (item.dbType) {
if (item.dbType == 'string') {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'like', val: text });
} else if (item.dbType == 'Date') {
if (text.length == '2020-10-10'.length) {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
}
} else if (item.dbType == 'Datetime') {
if (text.length == '2020-10-10 10:10:10'.length) {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
}
} else if (eqConditonTypes.indexOf(item.dbType)) {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
} else {
//text blob
}
}
}
}
params['superQueryMatchType'] = 'or';
params['superQueryParams'] = encodeURI(JSON.stringify(conditions));
return params;
} }
// modal list setTimeout(async () => {
function handleDataSave(data) { cardCurrentPage.value = 1;
console.log('handleDateSave', data); await loadCardData();
// : issues/8163 resetBodyStyle();
let arr = [data, ...selectedRows.value]; }, 100);
});
function handleCancel() {
closeModal();
}
const submitDisabled = computed(() => cardSelectedKeys.value.length === 0);
const submitLoading = ref(false);
function handleSubmit() {
submitLoading.value = true;
const arr = Object.values(cardSelectedRecordsMap.value);
if (arr && arr.length > 0) {
emit('success', arr); emit('success', arr);
closeModal(); closeModal();
//reload();
} }
setTimeout(() => {
submitLoading.value = false;
}, 200);
}
return { // ==================== ====================
registerModal, function queryTableData(params) {
modalWidth, const url = '/online/cgform/api/getData/' + props.id;
handleCancel, return defHttp.get({ url, params });
submitDisabled, }
submitLoading,
handleSubmit,
registerTable, const onlineTableContext = {
getTableAction, isPopList: true,
searchText, reloadTable() {},
onSearch, isTree() {
return false;
},
};
const extConfigJson = ref<any>({});
downloadRowFile, const { columns, handleColumnResult } = useTableColumns(onlineTableContext, extConfigJson);
getImgView,
getPcaText,
getFormatDate,
hrefComponent,
viewOnlineCellImage,
rowSelection,
modalTitle,
reload, function getColumnList() {
const url = '/online/cgform/api/getColumns/' + props.id;
return new Promise((resolve, reject) => {
defHttp.get({ url }, { isTransformResponse: false }).then((res) => {
if (res.success) {
resolve(res.result);
} else {
$message.warning(res.message);
reject();
}
});
});
}
popModalFixedWidth, const modalTitle = ref('');
popBodyStyle, watch(
handleDataSave, () => props.id,
async () => {
let columnResult: any = await getColumnList();
handleColumnResult(columnResult);
modalTitle.value = columnResult.description;
},
{ immediate: true }
);
tableRef, function onSearch() {
}; cardCurrentPage.value = 1;
}, loadCardData();
}); }
const eqConditonTypes = ['int', 'double', 'Date', 'Datetime', 'BigDecimal'];
function addQueryParams(params) {
let text = searchText.value;
if (!text) {
params['superQueryMatchType'] = 'or';
params['superQueryParams'] = '';
return params;
}
let arr = columns.value;
let conditions: any[] = [];
if (arr && arr.length > 0) {
for (let item of arr) {
if (item.dbType) {
if (item.dbType == 'string') {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'like', val: text });
} else if (item.dbType == 'Date') {
if (text.length == '2020-10-10'.length) {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
}
} else if (item.dbType == 'Datetime') {
if (text.length == '2020-10-10 10:10:10'.length) {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
}
} else if (eqConditonTypes.indexOf(item.dbType)) {
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
}
}
}
}
params['superQueryMatchType'] = 'or';
params['superQueryParams'] = encodeURI(JSON.stringify(conditions));
return params;
}
return {
registerModal,
handleCancel,
submitDisabled,
submitLoading,
handleSubmit,
searchText,
onSearch,
modalTitle,
popModalFixedWidth,
popBodyStyle,
//
cardLoading,
cardDataList,
cardTotal,
cardCurrentPage,
cardPageSize,
cardSelectedKeys,
getCardKey,
isCardSelected,
handleCardToggle,
clearCardSelection,
cardOtherDisplayFields,
getCardMainContent,
hasCardImage,
getCardImageSrc,
handleCardImageError,
handleCardPageChange,
handleCardSizeChange,
};
},
});
</script> </script>
<style scoped></style> <style scoped lang="less">
/* ===== 工具栏 ===== */
.ltm-toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
/* ===== 卡片通用样式 ===== */
.atm-selected-tip {
font-size: 13px;
color: #1890ff;
}
.atm-clear-link {
margin-left: 6px;
color: #ff4d4f;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.atm-spin-wrap {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.atm-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 10px;
padding: 0 4px;
min-height: 100px;
}
.atm-card {
position: relative;
display: inline-flex;
flex-direction: row;
width: 100%;
background: #fff;
border-radius: 4px;
border: 2px solid transparent;
box-shadow:
0 1px 4px rgba(0, 0, 0, 0.12),
0 0 2px rgba(0, 0, 0, 0.08);
cursor: pointer;
transition:
box-shadow 0.2s,
border-color 0.2s;
&:hover {
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
}
&.atm-card--selected {
border-color: #1890ff;
}
}
.atm-card-check {
position: absolute;
top: 6px;
right: 8px;
z-index: 1;
line-height: 1;
font-size: 16px;
}
.atm-check-icon--on {
color: #1890ff;
}
.atm-check-icon--off {
color: #d9d9d9;
}
.atm-card-inner {
flex: 1;
padding: 12px 28px 12px 14px;
overflow: hidden;
&.atm-has-image {
width: calc(100% - 80px);
}
}
.atm-card-title {
font-size: 14px;
font-weight: 500;
color: #333;
line-height: 20px;
margin-bottom: 6px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.atm-card-fields {
display: flex;
flex-direction: column;
gap: 2px;
}
.atm-card-field-row {
display: flex;
align-items: center;
gap: 4px;
line-height: 20px;
overflow: hidden;
}
.atm-field-label {
font-size: 12px;
color: #9e9e9e;
white-space: nowrap;
flex-shrink: 0;
}
.atm-field-value {
font-size: 12px;
color: #555;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.atm-card-image {
width: 72px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 8px 8px 8px 0;
overflow: hidden;
img {
width: 56px;
height: 56px;
border-radius: 50%;
object-fit: cover;
}
}
.atm-pagination {
display: flex;
justify-content: flex-end;
margin-top: 14px;
}
</style>

View File

@ -13,6 +13,39 @@ function queryTableColumns(tableName, params) {
const url = '/online/cgform/api/getColumns/' + tableName; const url = '/online/cgform/api/getColumns/' + tableName;
return defHttp.get({ url, params }); return defHttp.get({ url, params });
} }
//
const tableColumns = ref<any[]>([]);
//
const selectOptions = ref<any[]>([]);
//
const dictOptions = ref<any>({});
/**
* table 模式调用 /sys/dict/queryTableDataForLinkRecord 接口一次请求获取多字段数据
*/
function queryAllTableData(params: {
tableName: string;
showFields: string;
valueField: string;
keyword?: string;
searchFields?: string;
condition?: string;
keyValues?: string;
pageNo?: number;
pageSize?: number;
}) {
return defHttp.get({ url: '/sys/dict/queryTableDataForLinkRecord', params });
}
/**
* table 模式获取需要查询的所有展示字段textFields + imageField去重
*/
function getAllTableShowFields(props): string {
const textFields = (props.textField || '').split(',').filter(Boolean);
const imageField = props.imageField;
const fieldSet = new Set<string>(textFields);
if (imageField) fieldSet.add(imageField);
return Array.from(fieldSet).join(',');
}
export function useLinkTable(props) { export function useLinkTable(props) {
//TODO //TODO
@ -50,6 +83,18 @@ export function useLinkTable(props) {
watchEffect(async () => { watchEffect(async () => {
const table = props.tableName; const table = props.tableName;
if (table) { if (table) {
if (props.queryMode === 'table') {
// table Online textField/imageField
const textFields = (props.textField || '').split(',').filter(Boolean);
mainContentField.value = textFields[0] || '';
// otherColumnstextField
otherColumns.value = textFields.slice(1).map((f) => ({ title: f, dataIndex: f }));
// tableColumns transData 使allTable no-op
tableColumns.value = textFields.map((f) => ({ title: f, dataIndex: f }));
dictOptions.value = {};
return;
}
const valueField = props.valueField || ''; const valueField = props.valueField || '';
const textField = props.textField || ''; const textField = props.textField || '';
const arr: any[] = []; const arr: any[] = [];
@ -94,10 +139,6 @@ export function useLinkTable(props) {
}; };
}); });
//
const selectOptions = ref<any[]>([]);
const tableColumns = ref<any[]>([]);
const dictOptions = ref<any>({});
async function resetTableColumns() { async function resetTableColumns() {
const params = baseParam.value; const params = baseParam.value;
@ -210,6 +251,34 @@ export function useLinkTable(props) {
if (!value) { if (!value) {
return []; return [];
} }
// table keyValues
if (props.queryMode === 'table') {
const showFields = getAllTableShowFields(props);
if (!showFields) return [];
try {
const data = await queryAllTableData({
tableName: props.tableName,
showFields,
valueField: props.valueField,
keyValues: value,
pageNo: 1,
pageSize: 200,
});
const records: any[] = data?.records || [];
if (records.length > 0) return records;
// value
const firstTextField = (props.textField || '').split(',').filter(Boolean)[0] || props.valueField;
return value
.split(',')
.filter(Boolean)
.map((v: string) => ({ id: v, [props.valueField]: v, [firstTextField]: v }));
} catch (e) {
console.error('[useLinkTable] table loadOne 失败', e);
return [];
}
}
let valueFieldName = props.valueField; let valueFieldName = props.valueField;
let params = { let params = {
...baseParam.value, ...baseParam.value,

View File

@ -0,0 +1,648 @@
import type { Ref } from 'vue';
import type { ExtConfigType } from '../../types';
import { HrefSlots, OnlineColumn } from '/@/components/jeecg/OnLine/types/onlineConfig';
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 { getAreaTextByCodeAnyLevel } from '/@/components/Form/src/utils/Area';
import { createImgPreview } from '@/components/Preview';
import { importViewsFile, _eval } from '/@/utils';
import { useModal } from '/@/components/Modal';
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<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)
//
const dictOptionInfo = ref<any>({});
//
const selectedKeys = ref<any[]>([]);
//
//const selectRows = ref<Array<any>>([]);
// --computed
const rowSelection = ref<any>(null);
//
let enableScrollBar = ref(true);
// tablescroll
let tableScroll = computed(() => {
if (enableScrollBar.value == true) {
return undefined;
} else {
// X
return { x: false };
}
});
// online -
const [registerOnlineHrefModal, { openModal: openOnlineHrefModal }] = useModal();
const hrefMainTableId = ref('')
// online
const [registerPopModal, { openModal: openPopModal }] = useModal();
const popTableId = ref('')
//
function handleColumnResult(result, type = 'checkbox') {
//
dictOptionInfo.value = result.dictOptions;
// rowSelection
if (result.checkboxFlag == 'Y') {
rowSelection.value = {
selectedRowKeys: selectedKeys,
onChange: onSelectChange,
type,
};
} else {
rowSelection.value = null;
}
//
enableScrollBar.value = result.scrollFlag == 1;
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);
if (!!json.isFixed) {
column.fixed = 'left';
}
}
// update-end--author:liaozhiyang---date:20230818---forQQYUN-4161
// update-begin--author:liaozhiyang---date:20240517---forTV360X-129href
if (column.hrefSlotName && column.scopedSlots) {
const obj = result.fieldHrefSlots?.find((item) => item.slotName === column.hrefSlotName);
if (obj) {
column.fieldHref = obj;
}
}
// update-end--author:liaozhiyang---date:20240517---forTV360X-129href
Object.keys(column).map((key) => {
// ('') 0
if (column[key] == null) {
delete column[key];
}
});
});
// href
let fieldHrefSlots: HrefSlots[] = result.fieldHrefSlots;
const fieldHrefSlotKeysMap = {};
fieldHrefSlots.forEach((item) => (fieldHrefSlotKeysMap[item.slotName] = item));
let tableColumns: OnlineColumn[] = [];
// href dict 使
tableColumns = handleColumnHrefAndDict(dataColumns, fieldHrefSlotKeysMap);
// bpm_status
bpmStatusFilter(tableColumns);
console.log('-----列表列配置----', tableColumns);
// align
if (onlineTableContext.isTree() === true) {
//
let firstField = result.textField;
let index = -1;
for (let i = 0; i < tableColumns.length; i++) {
if (tableColumns[i].dataIndex == firstField) {
index = i;
break;
}
}
if (index > 0) {
//0-1
let deleteColumns = tableColumns.splice(index, 1);
tableColumns.unshift(deleteColumns[0]);
}
//
if (tableColumns.length > 0) {
tableColumns[0].align = 'left';
}
}
columns.value = tableColumns;
//
onlineTableContext.reloadTable();
}
/**
* 表格选择事件 [expose]
* @param selectedRowKeys
* @param selectRow
*/
function onSelectChange(selectedRowKeys, selectedRows) {
selectedKeys.value = selectedRowKeys;
onlineTableContext['selectedRows'] = toRaw(selectedRows);
onlineTableContext['selectedRowKeys'] = toRaw(selectedRowKeys);
}
/**
* 处理列的href和字典翻译
*/
function handleColumnHrefAndDict(columns: OnlineColumn[], fieldHrefSlotKeysMap: {}): OnlineColumn[] {
for (let column of columns) {
let { customRender, hrefSlotName, fieldType } = column;
// online yyyy-MM-dd (yyyy-MM-dd HH:mm:ss) issues/3042
if (fieldType == 'date' || fieldType == 'Date') {
column.customRender = ({ text }) => {
if (!text) {
return '';
}
if (text.length > 10) {
return text.substring(0, 10);
}
return text;
};
} else if (fieldType == 'link_table') {
//
// update-begin--author:liaozhiyang---date:20250318---forissues/7930
const fieldExtendJson = column.fieldExtendJson ?? '{}';
const json = JSON.parse(fieldExtendJson);
// update-end--author:liaozhiyang---date:20250318---forissues/7930
column.customRender = ({ text, record }) => {
if (!text) {
return '';
}
if(onlineTableContext.isPopList===true){
//
return record[column.dataIndex+"_dictText"]
}else{
let tempIdArray = (text+'').split(',');
//update-begin-author:taoyan date:2023-2-15 for: QQYUN-4286online
let tempLabelArray = [];
if(record[column.dataIndex+"_dictText"]){
tempLabelArray = record[column.dataIndex+"_dictText"].split(',');
}
//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 ''
}
//display: flex;width: 100%;flex-wrap: wrap;flex-direction: row;
return h('div',{style:{'overflow':'hidden'}}, renderResult);
}
};
} else if (fieldType === 'popup_dict') {
// update-begin--author:liaozhiyang---date:20240402---forQQYUN-8833JPopupDict
column.customRender = ({ text, record }) => {
const dict = record[column.dataIndex + '_dictText'];
if (dict != undefined) {
return record[column.dataIndex + '_dictText'];
}
return text;
};
// update-end--author:liaozhiyang---date:20240402---forQQYUN-8833JPopupDict
} else {
if (!hrefSlotName && column.scopedSlots && column.scopedSlots.customRender) {
//Onlinehref fieldHrefSlotKeysMap href
if (fieldHrefSlotKeysMap.hasOwnProperty(column.scopedSlots.customRender)) {
hrefSlotName = column.scopedSlots.customRender;
}
}
// customRender 使
// hrefSlotName 使href
// href
if (customRender || hrefSlotName) {
let dictCode = customRender as string;
let replaceFlag = '_replace_text_';
// ellipsis
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 {
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(',');
}
}
//
if (column.showLength) {
if (value && value.length > column.showLength) {
value = value.substr(0, column.showLength) + '...';
}
}
// hrefSlotName a
if (hrefSlotName) {
let field = fieldHrefSlotKeysMap[hrefSlotName];
if (field) {
return h(
'a',
{
onClick: () => handleClickFieldHref(field, record),
},
getValue(),
);
}
}
return h('span', {}, getValue());
};
}
// scopedSlots slots
if (column.scopedSlots) {
// slot ellipsis
column.ellipsis = true;
let slots = column.scopedSlots;
column['slots'] = slots;
delete column.scopedSlots;
}
}
}
return columns;
}
/**
* href 点击事件
* @param field
* @param record
*/
function handleClickFieldHref(field, record) {
let href = field.href;
let urlPattern = /(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&amp;%\$#_]*)?/;
let compPattern = /\.vue(\?.*)?$/;
let jsPattern = /{{([^}]+)}}/g; // {{ xxx }}
if (typeof href === 'string') {
if(href.startsWith('ONLINE:')){
// ONLINE:tableId:fieldName
let arr = href.split(':')
hrefMainTableId.value = arr[1];
let fieldName = arr[2];
openOnlineHrefModal(true, {
isUpdate: true,
disableSubmit: true,
hideSub: true,
record:{id: record[fieldName]},
})
}else{
href = href.trim().replace(/\${([^}]+)?}/g, (_s1, s2) => record[s2]);
// {{...}} JS
if (jsPattern.test(href)) {
href = href.replace(jsPattern, function (text, s0) {
try {
// {{ ACCESS_TOKEN }}
if (s0.trim() === 'ACCESS_TOKEN') {
return getToken()
}
// update-begin--author:liaozhiyang---date:20230904---forQQYUN-6390evalnew Functionbuild
return _eval(s0);
// update-end--author:liaozhiyang---date:20230904---forQQYUN-6390evalnew Functionbuild
} catch (e) {
console.error(e);
return text;
}
});
}
if (urlPattern.test(href)) {
window.open(href, '_blank');
} else if (compPattern.test(href)) {
//
openHrefCompModal(href);
} else {
router.push(href);
}
}
}
}
//
const dialogStyle = {
top: 0,
left: 0,
height: '100%',
margin: 0,
padding: 0,
};
// update-begin--author:liaozhiyang---date:20231218---forQQYUN-6366antd4.x
//
const hrefComponent = reactive({
model: {
title: '',
okText: '关闭',
width: '100%',
open: false,
destroyOnClose: true,
style: dialogStyle,
// dialogStyle: dialogStyle,
bodyStyle: { padding: '8px', height: 'calc(100vh - 108px)', overflow: 'auto', overflowX: 'hidden' },
//
cancelButtonProps: { style: { display: 'none' } },
},
on: {
ok: () => (hrefComponent.model.open = false),
cancel: () => (hrefComponent.model.open = false),
},
is: <any>null,
params: {},
});
// update-end--author:liaozhiyang---date:20231218---forQQYUN-6366antd4.x
// --> modal
function openHrefCompModal(href) {
// href
let index = href.indexOf('?');
let path = href;
if (index !== -1) {
path = href.substring(0, index);
let paramString = href.substring(index + 1, href.length);
let paramArray = paramString.split('&');
let params = {};
paramArray.forEach((paramObject) => {
let paramItem = paramObject.split('=');
params[paramItem[0]] = paramItem[1];
});
hrefComponent.params = params;
} else {
hrefComponent.params = {};
}
// update-begin--author:liaozhiyang---date:20231218---forQQYUN-6366antd4.x
hrefComponent.model.open = true;
// update-end--author:liaozhiyang---date:20231218---forQQYUN-6366antd4.x
hrefComponent.model.title = '操作';
hrefComponent.is = markRaw(defineAsyncComponent(() => importViewsFile(path)));
}
//
let fixedAction:any = 'right';
if(onlineTableContext.isTree()){
fixedAction = 'right'
}
const actionColumn = reactive<OnlineColumn>({
title: '操作',
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: fixedAction,
align: 'center',
width: 150,
});
//
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 {
let flag = false;
for (let i = 0; i < tableColumns.length; i++) {
let item = tableColumns[i];
let fieldName = item.dataIndex;
if (fieldName!.toLowerCase() == 'bpm_status') {
flag = true;
break;
}
}
onlineTableContext['hasBpmStatus'] = flag;
return flag;
}
/**
* 文件
* @param text
*/
function downloadRowFile(text, record, column, id) {
if (!text) {
return;
}
// update-begin--author:liaozhiyang---date:20240124---forQQYUN-8020online
if (text.indexOf(',') > 0) {
downloadFile(`/online/cgform/field/download/${id}/${record.id}/${column.dataIndex}`, `文件_${record.id}.zip`);
} else {
const url = getFileAccessHttpUrl(text);
window.open(url);
}
// update-end--author:liaozhiyang---date:20240124---forQQYUN-8020online
}
/**
* 图片
* @param text
*/
function getImgView(text) {
if (text && text.indexOf(',') > 0) {
// update-begin--author:liaozhiyang---date:20250325---forissues/7990
text = split(text)[0];
// update-end--author:liaozhiyang---date:20250325---forissues/7990
}
return getFileAccessHttpUrl(text);
}
/**
* 根据编码获取省市区文本
* @param code
*/
function getPcaText(code, column) {
if (!code) {
return '';
}
// 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
}
/**
* 日期格式化
* @param text
*/
function getFormatDate(text, column) {
if (!text) {
return '';
}
let a = text;
if (a.length > 10) {
a = a.substring(0, 10);
}
// update-begin--author:liaozhiyang---date:20240430---forissues/6094online ()
let fieldExtendJson = column?.fieldExtendJson;
if (fieldExtendJson) {
fieldExtendJson = JSON.parse(fieldExtendJson);
if (fieldExtendJson.picker && fieldExtendJson.picker != 'default') {
const result = getWeekMonthQuarterYear(a);
return result[fieldExtendJson.picker];
}
}
// update-end--author:liaozhiyang---date:20240430---forissues/6094online ()
return a;
}
watch(selectedKeys, () => {
onlineTableContext['selectedRowKeys'] = toRaw(selectedKeys.value);
});
onlineTableContext['clearSelectedRow'] = () => {
selectedKeys.value = [];
onlineTableContext['selectedRows'] = [];
onlineTableContext['selectedRowKeys'] = [];
};
/**
* 预览列表 cell 图片
* @param text
*/
function viewOnlineCellImage(text) {
if (text) {
let imgList: any = [];
// update-begin--author:liaozhiyang---date:20250325---forissues/7990
const arr = split(text);
// update-end--author:liaozhiyang---date:20250325---forissues/7990
for (let str of arr) {
if (str) {
imgList.push(getFileAccessHttpUrl(str));
}
}
createImgPreview({ imageList: imgList });
}
}
/**
* link table控件在列表上显示 支持点击跳转表单
* @param id
* @param hrefTableName
*/
const onlinePopModalRef = ref();
async function handleClickLinkTable(id, hrefTableName, isListReadOnly){
popTableId.value = hrefTableName;
let formStatus = await onlinePopModalRef.value.getFormStatus();
//
if(formStatus==true){
hrefMainTableId.value = hrefTableName;
openOnlineHrefModal(true, {
isUpdate: true,
disableSubmit: true,
hideSub: true,
record:{id: id},
})
}else{
openPopModal(true, {
isUpdate: true,
// update-begin--author:liaozhiyang---date:20250318---forissues/7930
disableSubmit: isListReadOnly ? true : false,
// update-end--author:liaozhiyang---date:20250318---forissues/7930
record: {
id: id
}
});
}
}
return {
columns,
actionColumn,
selectedKeys,
rowSelection,
enableScrollBar,
tableScroll,
downloadRowFile,
getImgView,
getPcaText,
getFormatDate,
handleColumnResult,
onSelectChange,
hrefComponent,
viewOnlineCellImage,
hrefMainTableId,
registerOnlineHrefModal,
registerPopModal,
openPopModal,
openOnlineHrefModal,
onlinePopModalRef,
popTableId,
handleClickFieldHref,
};
}

View File

@ -0,0 +1,298 @@
<template>
<div class="multi-text-tag-input" :class="{ 'is-focused': isFocused }" @click="focusInput">
<span v-for="(tag, index) in tags" :key="index" class="mtti-tag">
<span class="mtti-tag-text">{{ tag }}</span>
<span class="mtti-tag-close" @click.stop="removeTag(index)">×</span>
</span>
<input
ref="inputRef"
v-model="inputValue"
class="mtti-input"
:placeholder="tags.length === 0 ? placeholder : ''"
@focus="onFocus"
@blur="onBlur"
@keydown.enter.prevent="handleEnter"
@keydown="handleKeydown"
@compositionstart="isComposing = true"
@compositionend="isComposing = false"
/>
<div v-if="showDropdown" class="mtti-dropdown">
<div class="mtti-dropdown-item" @mousedown.prevent="handleDropdownClick">
使用"{{ inputValue }}"
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, nextTick } from 'vue';
export default defineComponent({
name: 'JMultiTextTagInput',
props: {
value: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '输入后按回车添加,逗号分隔多个关键词',
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ['change', 'update:value'],
setup(props, {emit}) {
const inputRef = ref<HTMLInputElement | null>(null);
const inputValue = ref('');
const isFocused = ref(false);
const isComposing = ref(false);
//
const tags = computed(() => {
if (!props.value || props.value.trim() === '') {
return [];
}
return props.value.split(',').map(s => s.trim()).filter(s => s !== '');
});
const showDropdown = computed(() => {
return isFocused.value && inputValue.value && inputValue.value.trim() !== '';
});
//
function emitChange(newTags: string[]) {
const val = newTags.filter(s => s.trim() !== '').join(',');
emit('update:value', val);
emit('change', val);
}
//
function addTags(values: string[]) {
const existing = [...tags.value];
for (const v of values) {
const trimmed = v.trim();
if (trimmed && !existing.includes(trimmed)) {
existing.push(trimmed);
}
}
emitChange(existing);
}
//
function addTag(value: string) {
addTags([value]);
}
//
function removeTag(index: number) {
const newTags = [...tags.value];
newTags.splice(index, 1);
emitChange(newTags);
}
//
function handleEnter() {
if (isComposing.value) {
return;
}
const text = inputValue.value.trim();
if (text) {
//
if (text.includes(',')) {
const parts = text.split(',').map(s => s.trim()).filter(s => s !== '');
addTags(parts);
} else {
addTag(text);
}
inputValue.value = '';
}
}
//
function handleKeydown(e: KeyboardEvent) {
if (isComposing.value) {
return;
}
//
if (e.key === ',' || e.key === '') {
e.preventDefault();
const text = inputValue.value.trim();
if (text) {
addTag(text);
inputValue.value = '';
}
}
// 退
if (e.key === 'Backspace' && inputValue.value === '' && tags.value.length > 0) {
removeTag(tags.value.length - 1);
}
}
//
function handleDropdownClick() {
const text = inputValue.value.trim();
if (text) {
addTag(text);
inputValue.value = '';
nextTick(() => {
inputRef.value?.focus();
});
}
}
function onFocus() {
isFocused.value = true;
}
function onBlur() {
// mousedown
setTimeout(() => {
isFocused.value = false;
//
const text = inputValue.value.trim();
if (text) {
if (text.includes(',')) {
const parts = text.split(',').map(s => s.trim()).filter(s => s !== '');
addTags(parts);
} else {
addTag(text);
}
inputValue.value = '';
}
}, 200);
}
function focusInput() {
inputRef.value?.focus();
}
return {
inputRef,
inputValue,
isFocused,
isComposing,
tags,
showDropdown,
handleEnter,
handleKeydown,
handleDropdownClick,
onFocus,
onBlur,
focusInput,
removeTag,
};
},
});
</script>
<style scoped lang="less">
.multi-text-tag-input {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
min-height: 32px;
padding: 2px 8px;
border: 1px solid #d9d9d9;
border-radius: 6px;
background: #fff;
cursor: text;
transition: border-color 0.3s;
gap: 4px;
&:hover {
border-color: #4096ff;
}
&.is-focused {
border-color: #4096ff;
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
}
}
.mtti-tag {
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 6px;
background: #f5f5f5;
border: 1px solid #e8e8e8;
border-radius: 4px;
font-size: 13px;
color: #333;
max-width: 200px;
overflow: hidden;
}
.mtti-tag-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 160px;
}
.mtti-tag-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
margin-left: 4px;
cursor: pointer;
color: #999;
font-size: 12px;
line-height: 1;
border-radius: 50%;
transition: all 0.2s;
&:hover {
color: #fff;
background: #ff4d4f;
}
}
.mtti-input {
flex: 1;
min-width: 60px;
height: 24px;
border: none;
outline: none;
font-size: 13px;
color: #333;
background: transparent;
padding: 0 2px;
&::placeholder {
color: #bfbfbf;
}
}
.mtti-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
background: #fff;
border: 1px solid #d9d9d9;
border-radius: 6px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08);
z-index: 1050;
overflow: hidden;
}
.mtti-dropdown-item {
padding: 6px 12px;
font-size: 13px;
color: #333;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: #f5f5f5;
}
}
</style>

View File

@ -2,9 +2,10 @@
<template> <template>
<div class="JPopup components-input-demo-presuffix" v-if="avalid"> <div class="JPopup components-input-demo-presuffix" v-if="avalid">
<!--输入框--> <!--输入框-->
<a-input @click="handleOpen" :value="innerShowText || showText" :placeholder="placeholder" readOnly v-bind="attrs"> <a-input @click="handleOpen" :value="isQueryingShowText ? '加载中...' : (innerShowText || showText)" :placeholder="placeholder" readOnly v-bind="attrs">
<template #prefix> <template #prefix>
<Icon icon="ant-design:cluster-outlined"></Icon> <Icon v-if="!isQueryingShowText" icon="ant-design:cluster-outlined"></Icon>
<Icon v-else icon="ant-design:loading-outlined" spin></Icon>
</template> </template>
<!-- <template #suffix> <!-- <template #suffix>
<Icon icon="ant-design:close-circle-outlined" @click="handleEmpty" title="清空" v-if="showText"></Icon> <Icon icon="ant-design:close-circle-outlined" @click="handleEmpty" title="清空" v-if="showText"></Icon>
@ -34,6 +35,13 @@
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs'; import { useAttrs } from '/@/hooks/core/useAttrs';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { defHttp } from '/@/utils/http/axios';
// url
const configUrl = reactive({
getColumns: '/online/cgreport/api/getRpColumns/',
getData: '/online/cgreport/api/getData/',
});
export default defineComponent({ export default defineComponent({
name: 'JPopup', name: 'JPopup',
@ -71,6 +79,9 @@
const avalid = ref(true); const avalid = ref(true);
const showText = ref(''); const showText = ref('');
const innerShowText = ref('') const innerShowText = ref('')
const loading = ref(false);
const isQueryingShowText = ref(false);
const cgRpConfigId = ref('');
//model //model
const [regModal, { openModal }] = useModal(); const [regModal, { openModal }] = useModal();
// //
@ -92,11 +103,101 @@
watch( watch(
() => props.value, () => props.value,
(val) => { (val) => {
showText.value = val && val.length > 0 ? val.split(props.spliter).join(',') : ''; // id退 value
if (!isQueryingShowText.value) {
showText.value = val && val.length > 0 ? val.split(props.spliter).join(',') : '';
}
// innerShowText value online
if (props.inSearch && val && val.length > 0 && !innerShowText.value) {
queryShowTextByValue(val);
}
}, },
{ immediate: true } { immediate: true }
); );
/**
* 根据 value 反查显示文本
*/
function queryShowTextByValue(val) {
if (!props.code || props.fieldConfig.length === 0) {
return;
}
const sourceField = props.fieldConfig[0]['source'];
const labelField = props.fieldConfig[0]['label'];
if (!sourceField || !labelField) {
return;
}
// loading showText id
isQueryingShowText.value = true;
showText.value = '';
const doQuery = () => {
loadDataForShowText(val, sourceField, labelField);
};
if (cgRpConfigId.value) {
doQuery();
} else {
loadColumnsInfo({ callBack: doQuery });
}
}
/**
* 加载列信息获取 cgRpConfigId
*/
function loadColumnsInfo({ callBack }) {
loading.value = true;
let url = `${configUrl.getColumns}${props.code}`;
defHttp
.get({ url }, { isTransformResponse: false, successMessageMode: 'none' })
.then((res) => {
if (res.success) {
cgRpConfigId.value = res.result.cgRpConfigId;
callBack?.();
}
})
.catch((err) => {
console.error('【JPopup】加载报表列信息失败', err);
})
.finally(() => {
loading.value = false;
// value loading
if (!cgRpConfigId.value) {
isQueryingShowText.value = false;
showText.value = props.value && props.value.length > 0 ? props.value.split(props.spliter).join(',') : '';
}
});
}
/**
* 根据 value 加载显示文本
*/
function loadDataForShowText(val, sourceField, labelField) {
loading.value = true;
let url = `${configUrl.getData}${unref(cgRpConfigId)}`;
defHttp
.get(
{ url, params: { ['force_' + sourceField]: val } },
{ isTransformResponse: false, successMessageMode: 'none' }
)
.then((res) => {
let data = res.result;
if (data.records?.length) {
const labels = data.records.map((item) => item[labelField]);
innerShowText.value = labels.join(',');
}
})
.catch((err) => {
console.error('【JPopup】根据 value 反查显示文本失败', err);
})
.finally(() => {
loading.value = false;
// loading退 value
isQueryingShowText.value = false;
if (!innerShowText.value) {
showText.value = props.value && props.value.length > 0 ? props.value.split(props.spliter).join(',') : '';
}
});
}
/** /**
* 打开pop弹出框 * 打开pop弹出框
*/ */
@ -155,6 +256,8 @@
innerShowText, innerShowText,
avalid, avalid,
uniqGroupId, uniqGroupId,
loading,
isQueryingShowText,
attrs, attrs,
regModal, regModal,
handleOpen, handleOpen,

View File

@ -2,7 +2,8 @@
<template> <template>
<div class="JPopupDict components-input-demo-presuffix"> <div class="JPopupDict components-input-demo-presuffix">
<!--输入框--> <!--输入框-->
<a-select v-model:value="showText" v-bind="attrs" :mode="multi ? 'multiple' : ''" @click="handleOpen" readOnly :loading="loading"> <a-input v-if="useInput" v-model:value="showLabel" v-bind="attrs" :disabled="disabled" @click="handleOpen" readonly :loading="loading" />
<a-select v-else v-model:value="showText" v-bind="attrs" :disabled="disabled" :mode="multi ? 'multiple' : ''" @click="handleOpen" readOnly :loading="loading" :open="false">
<a-select-option v-for="item in options" :value="item.value">{{ item.text }}</a-select-option> <a-select-option v-for="item in options" :value="item.value">{{ item.text }}</a-select-option>
</a-select> </a-select>
<a-form-item> <a-form-item>
@ -54,21 +55,25 @@
dictCode: propTypes.string.def(''), dictCode: propTypes.string.def(''),
value: propTypes.string.def(''), value: propTypes.string.def(''),
sorter: propTypes.string.def(''), sorter: propTypes.string.def(''),
disabled: propTypes.bool.def(false),
multi: propTypes.bool.def(false), multi: propTypes.bool.def(false),
param: propTypes.object.def({}), param: propTypes.object.def({}),
spliter: propTypes.string.def(','), spliter: propTypes.string.def(','),
getFormValues: propTypes.func, getFormValues: propTypes.func,
getContainer: propTypes.func, getContainer: propTypes.func,
showAdvancedButton: propTypes.bool.def(true), showAdvancedButton: propTypes.bool.def(true),
useInput: propTypes.bool.def(false),
}, },
emits: ['update:value', 'register', 'change'], emits: ['update:value', 'register', 'change', 'options-change'],
setup(props, { emit }) { setup(props, { emit }) {
const { createMessage } = useMessage(); const { createMessage } = useMessage();
const attrs = useAttrs(); const attrs = useAttrs();
const showText = ref<any>(props.multi ? [] : ''); const showText = ref<any>(props.multi ? [] : '');
const showLabel = ref<string>('');
const options = ref<any>([]); const options = ref<any>([]);
const cgRpConfigId = ref(''); const cgRpConfigId = ref('');
const loading = ref(false); const loading = ref(false);
let syncingFromProps = false;
const code = props.dictCode.split(',')[0]; const code = props.dictCode.split(',')[0];
const labelFiled = props.dictCode.split(',')[1]; const labelFiled = props.dictCode.split(',')[1];
const valueFiled = props.dictCode.split(',')[2]; const valueFiled = props.dictCode.split(',')[2];
@ -84,29 +89,69 @@
*/ */
function handleOpen() { function handleOpen() {
// : TV360X-317JPopupJPopupdic // : TV360X-317JPopupJPopupdic
!attrs.value.disabled && openModal(true); //update-begin---wangshuai---date:20260104 for[issue/1560]------------
//update-end---author:wangshuai ---date:20260104 for[issue/1560]------------
if (props.disabled) {
return;
}
openModal(true);
}
/**
* liaozhiyang
* 20260522
* QQYUN-15493新增a-input模式下
* */
function syncShowLabel() {
if (!props.useInput) return;
const currentValue = showText.value;
if (props.multi) {
const values = Array.isArray(currentValue) ? currentValue : currentValue ? currentValue.split(props.spliter) : [];
showLabel.value = values
.map((v) => {
const opt = options.value.find((o: any) => String(o.value) === String(v));
return opt?.text || v;
})
.join(props.spliter);
} else {
const opt = options.value.find((o: any) => String(o.value) === String(currentValue));
showLabel.value = opt?.text || currentValue || '';
}
} }
/** /**
* 监听value数值 * 监听value数值
*/ */
watch( watch(
() => props.value, () => props.value,
(val) => { () => {
const callBack = () => { const callBack = () => {
syncingFromProps = true;
// update-begin--author:liusq---date:20260720---forLHZP-1003pop
// : 使props.valueval
// defaultValuecallBackformModel
const currentValue = props.value;
if (props.multi) { if (props.multi) {
showText.value = val && val.length > 0 ? val.split(props.spliter) : []; showText.value = currentValue && currentValue.length > 0 ? currentValue.split(props.spliter) : [];
} else { } else {
showText.value = val ?? ''; showText.value = currentValue ?? '';
} }
// update-end--author:liusq---date:20260720---forLHZP-1003pop
// update-begin--author:liaozhiyang---date:20260714---forLHZP-392pop
const currentValues = (props.multi ? showText.value : showText.value ? [showText.value] : []).map((v: any) => String(v));
selected.value = (selected.value || []).filter((r: any) => currentValues.includes(String(r[valueFiled])));
// update-end--author:liaozhiyang---date:20260714---forLHZP-392pop
syncShowLabel();
nextTick(() => {
syncingFromProps = false;
});
}; };
// options
callBack();
if (props.value || props.defaultValue) { if (props.value || props.defaultValue) {
if (cgRpConfigId.value) { if (cgRpConfigId.value) {
loadData({ callBack }); loadData({ callBack });
} else { } else {
loadColumnsInfo({ callBack }); loadColumnsInfo({ callBack });
} }
} else {
callBack();
} }
}, },
{ immediate: true } { immediate: true }
@ -114,6 +159,8 @@
watch( watch(
() => showText.value, () => showText.value,
(val) => { (val) => {
// props
if (syncingFromProps) return;
let result; let result;
if (props.multi) { if (props.multi) {
result = val.join(','); result = val.join(',');
@ -182,18 +229,30 @@
if (props.multi) { if (props.multi) {
showText.value = dataValue; showText.value = dataValue;
result = dataValue.join(props.spliter); result = dataValue.join(props.spliter);
//update-begin author:liaozhiyang date:20260522 forQQYUN-15493a-input
if (props.useInput) {
showLabel.value = dataOptions.map((o: any) => o.text).join(props.spliter);
}
//update-end author:liaozhiyang date:20260522 forQQYUN-15493a-input
} else { } else {
showText.value = dataValue[0]; showText.value = dataValue[0];
result = dataValue[0]; result = dataValue[0];
//update-begin author:liaozhiyang date:20260522 forQQYUN-15493a-input
if (props.useInput) {
showLabel.value = dataOptions[0]?.text || '';
}
//update-end author:liaozhiyang date:20260522 forQQYUN-15493a-input
} }
nextTick(() => { nextTick(() => {
emit('change', result); emit('change', result);
emit('update:value', result); emit('update:value', result);
emit('options-change', options.value);
}); });
} }
return { return {
showText, showText,
showLabel,
attrs, attrs,
regModal, regModal,
handleOpen, handleOpen,

View File

@ -1,11 +1,17 @@
<template> <template>
<a-range-picker v-model:value="rangeValue" @change="handleChange" :show-time="datetime" :placeholder="placeholder" :valueFormat="valueFormat"/> <a-range-picker v-model:value="rangeValue" @change="handleChange" :show-time="datetime" :placeholder="placeholder" :picker="picker" :format="format || valueFormat" :valueFormat="valueFormat"/>
</template> </template>
<script> <script>
import { defineComponent, ref, watch, computed } from 'vue'; import { defineComponent, ref, watch, computed } from 'vue';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
import { Form } from 'ant-design-vue'; import { Form } from 'ant-design-vue';
import dayjs from 'dayjs';
import isoWeek from 'dayjs/plugin/isoWeek';
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
dayjs.extend(isoWeek);
dayjs.extend(quarterOfYear);
const placeholder = ['开始日期', '结束日期'] const placeholder = ['开始日期', '结束日期']
/** /**
@ -17,6 +23,9 @@
value: propTypes.string.def(''), value: propTypes.string.def(''),
datetime: propTypes.bool.def(false), datetime: propTypes.bool.def(false),
placeholder: propTypes.string.def(''), placeholder: propTypes.string.def(''),
picker: propTypes.string.def('date'),
format: propTypes.string.def(''),
valueFormat: propTypes.string.def(''),
}, },
emits:['change', 'update:value'], emits:['change', 'update:value'],
setup(props, {emit}){ setup(props, {emit}){
@ -32,6 +41,9 @@
}, {immediate: true}); }, {immediate: true});
const valueFormat = computed(()=>{ const valueFormat = computed(()=>{
if(props.valueFormat){
return props.valueFormat
}
if(props.datetime === true){ if(props.datetime === true){
return 'YYYY-MM-DD HH:mm:ss' return 'YYYY-MM-DD HH:mm:ss'
}else{ }else{
@ -43,12 +55,33 @@
let str = '' let str = ''
if(arr && arr.length>0){ if(arr && arr.length>0){
// : [issues/6368] rangeDateallowEmpty // : [issues/6368] rangeDateallowEmpty
str = arr.join(',') // update-begin--author:wangshuai---date:20260819---forLHZP-322
str = arr.map(normalizePickerValue).join(',')
// update-end--author:wangshuai---date:20260819---forLHZP-322
} }
emit('change', str); emit('change', str);
emit('update:value', str); emit('update:value', str);
formItemContext.onFieldChange(); formItemContext.onFieldChange();
} }
/**
* 返回季度 格式化之后的代码
* @param value
* @returns {*|string}
*/
function normalizePickerValue(value){
if(!value){
return value
}
if(props.picker === 'quarter'){
return dayjs(value).startOf('quarter').format('YYYY-MM-DD')
}
if(props.picker === 'week'){
return dayjs(value).startOf('isoWeek').format('YYYY-MM-DD')
}
return value
}
return { return {
rangeValue, rangeValue,
placeholder, placeholder,

View File

@ -52,9 +52,13 @@
watch( watch(
() => props.value, () => props.value,
(val) => { (val) => {
if (val && val.length == 2) { let parseVal = val;
beginValue.value = val[0]; if (typeof val === 'string') {
endValue.value = val[1]; parseVal = val.split(',');
}
if (Array.isArray(parseVal) && parseVal.length === 2) {
beginValue.value = parseVal[0];
endValue.value = parseVal[1];
} else { } else {
beginValue.value = ''; beginValue.value = '';
endValue.value = ''; endValue.value = '';

View File

@ -1,7 +1,8 @@
<!-- 字典下拉单选 --> <!-- 字典下拉单选 / 远程加载下拉 -->
<template> <template>
<a-select <a-select
:value="innerValue" :value="innerValue"
:mode="mode"
:disabled="disabled" :disabled="disabled"
:placeholder="placeholder" :placeholder="placeholder"
:filter-option="filterOptionComputed" :filter-option="filterOptionComputed"
@ -10,7 +11,7 @@
:notFoundContent="loading ? undefined : null" :notFoundContent="loading ? undefined : null"
allowClear allowClear
@change="handleChange" @change="handleChange"
@search="handleSearch" :onSearch="showSearch ? handleSearch : undefined"
@popupScroll="handlePopupScroll" @popupScroll="handlePopupScroll"
@dropdownVisibleChange="handleDropdownVisibleChange" @dropdownVisibleChange="handleDropdownVisibleChange"
v-bind="$attrs" v-bind="$attrs"
@ -51,7 +52,6 @@
style: { backgroundColor: string }; style: { backgroundColor: string };
} }
/** 下拉单选组件的值类型(可被 change / update:value / input 等复用) */
export type SelectSingleValue = string | number | undefined; export type SelectSingleValue = string | number | undefined;
const props = withDefaults( const props = withDefaults(
@ -59,6 +59,7 @@
value?: SelectSingleValue; value?: SelectSingleValue;
placeholder?: string; placeholder?: string;
readOnly?: boolean; readOnly?: boolean;
mode?: 'multiple' | 'tags' | 'combobox';
options?: DictOption[]; options?: DictOption[];
triggerChange?: boolean; triggerChange?: boolean;
popContainer?: string; popContainer?: string;
@ -67,6 +68,12 @@
useDicColor?: boolean; useDicColor?: boolean;
pageSize?: number; pageSize?: number;
scrollLoad?: boolean; scrollLoad?: boolean;
showSearch?: boolean;
filterOption?: (input: string, option: any) => boolean;
onSearch?: (keyword: string) => void;
/** 远程加载模式:启用滚动加载+远程搜索,通过 onLoad 回调获取分页数据 */
remoteLoad?: boolean;
onLoad?: (params: { keyword: string; pageNo: number; pageSize: number }) => Promise<DictOption[]>;
}>(), }>(),
{ {
placeholder: '请选择', placeholder: '请选择',
@ -78,6 +85,8 @@
useDicColor: false, useDicColor: false,
pageSize: 10, pageSize: 10,
scrollLoad: false, scrollLoad: false,
showSearch: false,
remoteLoad: false,
} }
); );
const emit = defineEmits<{ const emit = defineEmits<{
@ -88,17 +97,19 @@
}>(); }>();
// --- --- // --- ---
const innerValue = ref<SelectSingleValue>(props.value ?? undefined); const innerValue = ref<SelectSingleValue>();
const dictOptions = ref<DictOption[]>([]); const dictOptions = ref<DictOption[]>([]);
const loading = ref(false); const loading = ref(false);
const searchKeyword = ref(''); const searchKeyword = ref('');
// --- --- // --- ---
const scrollState = { const scrollState = {
pageNo: 1, pageNo: 1,
hasMore: true, hasMore: true,
loading: false, loading: false,
}; };
/** 缓存上次滚动的目标元素,关闭下拉时复位其滚动条 */
let lastScrollTarget: HTMLElement | null = null;
const isDictTable = computed(() => { const isDictTable = computed(() => {
if (!props.dictCode) return false; if (!props.dictCode) return false;
@ -107,6 +118,10 @@
const useLoadDict = computed(() => props.scrollLoad && isDictTable.value); const useLoadDict = computed(() => props.scrollLoad && isDictTable.value);
const useRemoteLoad = computed(() => props.remoteLoad && typeof props.onLoad === 'function');
const showSearch = computed(() => useLoadDict.value || useRemoteLoad.value || props.showSearch);
const selectOptions = computed<SelectOptionItem[]>(() => { const selectOptions = computed<SelectOptionItem[]>(() => {
if (!Array.isArray(dictOptions.value)) return []; if (!Array.isArray(dictOptions.value)) return [];
return dictOptions.value.map((item, index) => { return dictOptions.value.map((item, index) => {
@ -121,6 +136,7 @@
}); });
}); });
// --- ---
function fetchDictPage(pageNo: number, append: boolean) { function fetchDictPage(pageNo: number, append: boolean) {
return defHttp return defHttp
.get({ .get({
@ -128,48 +144,66 @@
params: { pageNo, pageSize: props.pageSize, keyword: searchKeyword.value, order: 'asc' }, params: { pageNo, pageSize: props.pageSize, keyword: searchKeyword.value, order: 'asc' },
}) })
.then((res: any[]) => { .then((res: any[]) => {
const items: DictOption[] = (res ?? []).map((it) => ({ const items: DictOption[] = (res ?? []).map((it: any) => ({
value: it.value, value: it.value,
label: it.text ?? it.label, label: it.text ?? it.label,
})); }));
if (items.length === 0) { processPageResult(items, pageNo, append);
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 fetchRemotePage(pageNo: number, append: boolean) {
if (!props.onLoad) return;
try {
const items = await props.onLoad({
keyword: searchKeyword.value,
pageNo,
pageSize: props.pageSize,
});
processPageResult(items, pageNo, append);
} catch {
dictOptions.value = [];
}
}
function processPageResult(items: DictOption[], pageNo: number, append: boolean) {
if (!items || 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 {
if (unref(innerValue) && unref(dictOptions).length && !searchKeyword.value) {
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;
scrollState.hasMore = items.length >= (props.pageSize || 10);
}
async function loadDictOptions() { async function loadDictOptions() {
if (useLoadDict.value) { if (useLoadDict.value) {
scrollState.pageNo = 1; scrollState.pageNo = 1;
scrollState.hasMore = true; scrollState.hasMore = true;
loading.value = true; loading.value = true;
fetchDictPage(1, false) fetchDictPage(1, false)
.catch(() => { .catch(() => { dictOptions.value = []; })
dictOptions.value = []; .finally(() => { loading.value = false; ensureValueInOptions(); });
}) } else if (useRemoteLoad.value) {
.finally(() => { scrollState.pageNo = 1;
loading.value = false; scrollState.hasMore = true;
ensureValueInOptions(); loadRemoteOptions();
});
} else { } else {
let code = props.dictCode ?? ''; let code = props.dictCode ?? '';
try { try {
@ -182,21 +216,26 @@
} }
} }
/** 根据 value 拉取单条字典项用于回显(编辑时当前值不在已加载选项中时) */ async function loadRemoteOptions() {
loading.value = true;
searchKeyword.value = '';
scrollState.pageNo = 1;
scrollState.hasMore = true;
fetchRemotePage(1, false)
.finally(() => { loading.value = false; });
}
function fetchDictItemByValue(val: SelectSingleValue): Promise<DictOption | null> { function fetchDictItemByValue(val: SelectSingleValue): Promise<DictOption | null> {
if (val == null) return Promise.resolve(null); if (val == null) return Promise.resolve(null);
return defHttp return defHttp
.get({ url: `/sys/dict/loadDictItem/${props.dictCode}`, params: { key: val } }) .get({ url: `/sys/dict/loadDictItem/${props.dictCode}`, params: { key: val } })
.then((res: any) => { .then((res: any) => {
if (Array.isArray(res)) { if (Array.isArray(res)) return { value: val, label: res[0] };
return { value: val, label: res[0] };
}
return null; return null;
}) })
.catch(() => null); .catch(() => null);
} }
/** 确保当前 value 在选项中(仅 useLoadDict 时:编辑时当前值不在已加载列表中则拉取单条并插入) */
function ensureValueInOptions() { function ensureValueInOptions() {
if (!useLoadDict.value) return; if (!useLoadDict.value) return;
const val = innerValue.value; const val = innerValue.value;
@ -209,53 +248,72 @@
} }
const handleSearch = debounce((keyword: string) => { const handleSearch = debounce((keyword: string) => {
if (!useLoadDict.value) return; if (useLoadDict.value) {
searchKeyword.value = keyword ?? ''; searchKeyword.value = keyword ?? '';
scrollState.pageNo = 1; scrollState.pageNo = 1;
scrollState.hasMore = true; scrollState.hasMore = true;
loading.value = true; loading.value = true;
fetchDictPage(1, false) fetchDictPage(1, false)
.catch(() => { .catch(() => { dictOptions.value = []; })
dictOptions.value = []; .finally(() => { loading.value = false; ensureValueInOptions(); });
}) } else if (useRemoteLoad.value) {
.finally(() => { searchKeyword.value = keyword ?? '';
loading.value = false; scrollState.pageNo = 1;
ensureValueInOptions(); scrollState.hasMore = true;
}); loading.value = true;
fetchRemotePage(1, false)
.finally(() => { loading.value = false; });
} else if (props.onSearch) {
props.onSearch(keyword ?? '');
}
}, 300); }, 300);
function handleDropdownVisibleChange(open: boolean) { function handleDropdownVisibleChange(open: boolean) {
if (!open || !useLoadDict.value) return; if (!open) {
scrollState.pageNo = 1; //
scrollState.hasMore = true; if (lastScrollTarget) {
searchKeyword.value = ''; lastScrollTarget.scrollTop = 0;
loading.value = true; lastScrollTarget = null;
fetchDictPage(1, false) }
.finally(() => { return;
loading.value = false; }
ensureValueInOptions(); //
}); lastScrollTarget = null;
if (useLoadDict.value) {
scrollState.pageNo = 1;
scrollState.hasMore = true;
searchKeyword.value = '';
loading.value = true;
fetchDictPage(1, false)
.finally(() => { loading.value = false; ensureValueInOptions(); });
} else if (useRemoteLoad.value) {
loadRemoteOptions();
}
} }
function handlePopupScroll(e: Event) { function handlePopupScroll(e: Event) {
if (!useLoadDict.value || scrollState.loading || !scrollState.hasMore) return; lastScrollTarget = e.target as HTMLElement;
const target = e.target as HTMLElement; if (useLoadDict.value) {
const { scrollTop, scrollHeight, clientHeight } = target; if (scrollState.loading || !scrollState.hasMore) return;
if (scrollTop + clientHeight < scrollHeight - 10) return; const target = e.target as HTMLElement;
scrollState.loading = true; const { scrollTop, scrollHeight, clientHeight } = target;
fetchDictPage(scrollState.pageNo, true) if (scrollTop + clientHeight < scrollHeight - 10) return;
.finally(() => { scrollState.loading = true;
scrollState.loading = false; fetchDictPage(scrollState.pageNo, true)
}) .finally(() => { scrollState.loading = false; })
.catch(() => { .catch(() => { if (scrollState.pageNo > 1) scrollState.pageNo--; });
if (scrollState.pageNo > 1) { } else if (useRemoteLoad.value) {
scrollState.pageNo--; if (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;
fetchRemotePage(scrollState.pageNo, true)
.finally(() => { scrollState.loading = false; });
}
} }
// --- & --- // --- & ---
const showSearch = computed(() => useLoadDict.value);
function getPopupContainer(node: HTMLElement) { function getPopupContainer(node: HTMLElement) {
return props.popContainer ? setPopContainer(node, props.popContainer) : node?.parentNode; return props.popContainer ? setPopContainer(node, props.popContainer) : node?.parentNode;
} }
@ -266,8 +324,8 @@
return text.indexOf(input.toLowerCase()) >= 0; return text.indexOf(input.toLowerCase()) >= 0;
} }
const filterOptionComputed = computed(() => (input: string, option: any) => { const filterOptionComputed = computed(() => (input: string, option: any) => {
if (useLoadDict.value) return true; if (useLoadDict.value || useRemoteLoad.value) return true;
return filterOption(input, option); return props.filterOption?.(input, option) ?? filterOption(input, option);
}); });
// --- --- // --- ---
@ -283,7 +341,7 @@
// --- --- // --- ---
function syncOptionsFromProps() { function syncOptionsFromProps() {
if (props.dictCode) { if (props.dictCode || props.remoteLoad) {
loadDictOptions(); loadDictOptions();
} else { } else {
dictOptions.value = props.options ?? []; dictOptions.value = props.options ?? [];
@ -294,15 +352,23 @@
watch( watch(
() => props.value, () => props.value,
(val) => { (val) => {
innerValue.value = val ?? undefined; let parsedVal = val ?? undefined;
let isMulti = props.mode === 'multiple' || props.mode === 'tags';
if (isMulti && typeof parsedVal === 'string') {
parsedVal = parsedVal.split(',').map((v) => v.trim()).filter((v) => v !== '');
}
innerValue.value = parsedVal;
if (useLoadDict.value) ensureValueInOptions(); if (useLoadDict.value) ensureValueInOptions();
},
{
immediate: true,
} }
); );
watch( watch(
() => props.options, () => props.options,
() => { () => {
if (!props.dictCode) dictOptions.value = props.options ?? []; if (!props.dictCode && !props.remoteLoad) dictOptions.value = props.options ?? [];
} }
); );

View File

@ -18,7 +18,7 @@
import { unref } from 'vue'; import { unref } from 'vue';
import UserSelectModal from './modal/UserSelectModal.vue'; import UserSelectModal from './modal/UserSelectModal.vue';
import JSelectBiz from './base/JSelectBiz.vue'; import JSelectBiz from './base/JSelectBiz.vue';
import { defineComponent, ref, reactive, watchEffect, watch, provide } from 'vue'; import { defineComponent, ref, reactive, watchEffect, watch, provide, computed } from 'vue';
import { useModal } from '/@/components/Modal'; import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
import { useRuleFormItem } from '/@/hooks/component/useFormItem'; import { useRuleFormItem } from '/@/hooks/component/useFormItem';
@ -147,7 +147,10 @@
selectValues.value = values; selectValues.value = values;
send(values); send(values);
} }
const getBindValue = Object.assign({}, unref(props), unref(attrs)); // setup Object.assign
// updateSchema props v-bind="getBindValue"
// computed props/attrs UserSelectModal
const getBindValue = computed(() => Object.assign({}, unref(props), unref(attrs)));
// : QQYUN-9366 // : QQYUN-9366
const handleClose = () => { const handleClose = () => {
if (tempSave.length) { if (tempSave.length) {

View File

@ -13,7 +13,6 @@
:multiple="multiple" :multiple="multiple"
v-bind="attrs" v-bind="attrs"
@change="onChange" @change="onChange"
@search="onSearch"
:tree-checkable="treeCheckAble" :tree-checkable="treeCheckAble"
:tagRender="tagRender" :tagRender="tagRender"
> >
@ -31,7 +30,6 @@
import { defHttp } from '/@/utils/http/axios'; import { defHttp } from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs'; import { useAttrs } from '/@/hooks/core/useAttrs';
import { TreeSelect } from 'ant-design-vue';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { isObject, isArray } from '/@/utils/is'; import { isObject, isArray } from '/@/utils/is';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '/@/hooks/web/useI18n';
@ -71,7 +69,7 @@
// //
const treeData = ref<any[]>([]); const treeData = ref<any[]>([]);
// //
const treeValue = ref<any>(null); const treeValue = ref<any>();
const tableName = ref<any>(''); const tableName = ref<any>('');
const text = ref<any>(''); const text = ref<any>('');
const code = ref<any>(''); const code = ref<any>('');
@ -131,7 +129,7 @@
if(props.multiple){ if(props.multiple){
treeValue.value = []; treeValue.value = [];
}else{ }else{
treeValue.value = { label: null, value: null }; treeValue.value = undefined;
} }
} else { } else {
// : issues/4173 Online JTreeSelectchangeOptions // : issues/4173 Online JTreeSelectchangeOptions
@ -224,7 +222,7 @@
* @param text * @param text
*/ */
function translateTitle(text) { function translateTitle(text) {
if (text.includes("t('") && t) { if (text?.includes("t('") && t) {
return new Function('t', `return ${text}`)(t); return new Function('t', `return ${text}`)(t);
} }
return text; return text;
@ -319,13 +317,6 @@
emit('update:value', value); emit('update:value', value);
} }
/**
* 文本框值变化
*/
function onSearch(value) {
console.log(value);
}
/** /**
* 校验条件配置是否有误 * 校验条件配置是否有误
*/ */
@ -352,7 +343,7 @@
} }
// : issues/4173 Online JTreeSelectchangeOptions // : issues/4173 Online JTreeSelectchangeOptions
watch(()=>props.url, async (val)=>{ watch(()=> [props.url, props.params], async (val)=>{
if(val){ if(val){
await loadRootByUrl(); await loadRootByUrl();
} }
@ -367,8 +358,8 @@
let res = await defHttp.get({ url, params }, { isTransformResponse: false }); let res = await defHttp.get({ url, params }, { isTransformResponse: false });
if (res.success && res.result) { if (res.success && res.result) {
for (let i of res.result) { for (let i of res.result) {
i.title = translateTitle(i.title); i.title = translateTitle(i.title ?? i.name);
i.key = i.value; i.key = i.value ?? i.id;
i.isLeaf = !!i.leaf; i.isLeaf = !!i.leaf;
} }
// : TV360X-87 // : TV360X-87

View File

@ -11,6 +11,22 @@
@change="onFileChange" @change="onFileChange"
@preview="onFilePreview" @preview="onFilePreview"
> >
<template #removeIcon="{ file }">
<a-popconfirm
v-if="removeConfirm"
:title="`删除这${isImageMode ? '张图片' : '个文件'}`"
placement="topRight"
overlayClassName="j-upload-remove-popconfirm"
okText="删除"
cancelText="取消"
@confirm="confirmRemove(file)"
>
<span class="j-upload-remove-trigger" :data-file-uid="file.uid" @click.stop>
<Icon icon="ant-design:delete-outlined" />
</span>
</a-popconfirm>
<Icon v-else icon="ant-design:delete-outlined" />
</template>
<template v-if="isImageMode"> <template v-if="isImageMode">
<div v-if="!isMaxCount"> <div v-if="!isMaxCount">
<Icon icon="ant-design:plus-outlined" /> <Icon icon="ant-design:plus-outlined" />
@ -26,9 +42,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed, watch, nextTick, createApp,unref } from 'vue'; import { ref, computed, watch, nextTick, createApp,unref } from 'vue';
import { Icon } from '/@/components/Icon'; import { Icon } from '/@/components/Icon';
import { getToken } from '/@/utils/auth';
import { uploadUrl } from '/@/api/common/api'; import { uploadUrl } from '/@/api/common/api';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
@ -40,7 +55,7 @@
import UploadItemActions from './components/UploadItemActions.vue'; import UploadItemActions from './components/UploadItemActions.vue';
import { split } from '/@/utils/index'; import { split } from '/@/utils/index';
const { createMessage, createConfirm } = useMessage(); const { createMessage } = useMessage();
const { prefixCls } = useDesign('j-upload'); const { prefixCls } = useDesign('j-upload');
const attrs = useAttrs(); const attrs = useAttrs();
const emit = defineEmits(['change', 'update:value']); const emit = defineEmits(['change', 'update:value']);
@ -76,7 +91,8 @@
const fileList = ref<any[]>([]); const fileList = ref<any[]>([]);
const uploadGoOn = ref<boolean>(true); const uploadGoOn = ref<boolean>(true);
// refs // refs
const containerRef = ref(); const containerRef = ref<HTMLElement>();
const confirmedRemoveUid = ref<string | number>();
// //
const isMaxCount = computed(() => props.maxCount > 0 && fileList.value.length >= props.maxCount); const isMaxCount = computed(() => props.maxCount > 0 && fileList.value.length >= props.maxCount);
// //
@ -252,19 +268,26 @@
} }
// //
function onRemove() { function onRemove(file) {
if (props.removeConfirm) { if (!props.removeConfirm) {
return new Promise((resolve) => { return true;
createConfirm({
title: '删除',
content: `确定要删除这${isImageMode.value ? '张图片' : '个文件'}吗?`,
iconType: 'warning',
onOk: () => resolve(true),
onCancel: () => resolve(false),
});
});
} }
return true; if (confirmedRemoveUid.value === file.uid) {
confirmedRemoveUid.value = undefined;
return true;
}
nextTick(() => getRemoveTrigger(file)?.click());
return false;
}
function confirmRemove(file) {
confirmedRemoveUid.value = file.uid;
getRemoveTrigger(file)?.closest('button')?.click();
}
function getRemoveTrigger(file): HTMLElement | undefined {
const triggers = containerRef.value?.querySelectorAll<HTMLElement>('.j-upload-remove-trigger') || [];
return Array.from(triggers).find((trigger) => trigger.dataset.fileUid === String(file.uid));
} }
// uploadchange // uploadchange
@ -402,6 +425,29 @@
} }
} }
.ant-upload-list-item-actions {
display: block !important;
a,
a.anticon,
.anticon,
.anticon-eye,
.anticon-download,
.upload-action-icon,
.app-iconify {
pointer-events: auto !important;
cursor: pointer !important;
color: #fff !important;
opacity: 1 !important;
}
svg,
svg path {
color: #fff !important;
fill: #fff !important;
}
}
/* update-begin-author:taoyan date:2022-5-24 for:VUEN-1093详情界面 图片下载按钮显示不全*/ /* update-begin-author:taoyan date:2022-5-24 for:VUEN-1093详情界面 图片下载按钮显示不全*/
.upload-download-handler { .upload-download-handler {
right: 6px !important; right: 6px !important;
@ -458,4 +504,28 @@
} }
} }
} }
.j-upload-remove-popconfirm {
.ant-popover-inner {
min-width: 148px;
padding: 8px 10px;
}
.ant-popconfirm-message-icon {
display: none;
}
.ant-popconfirm-message-title {
margin-inline-start: 0;
}
.ant-popconfirm-buttons {
margin-top: 6px;
.ant-btn {
padding-inline: 6px;
box-shadow: none;
}
}
}
</style> </style>

View File

@ -1,20 +1,20 @@
<template> <template>
<BasicModal @register="registerModal" :title="modalTitle" :width="width" @ok="handleOk" v-bind="$attrs"> <BasicModal @register="registerModal" :title="modalTitle" :width="width" :minHeight="160" @ok="handleOk" v-bind="$attrs">
<JUpload ref="uploadRef" :value="value" v-bind="uploadBinds.props" @change="emitValue" /> <div class="j-upload-modal-content">
<JUpload ref="uploadRef" :value="value" v-bind="uploadBinds.props" @change="emitValue" />
</div>
</BasicModal> </BasicModal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, unref, reactive, computed, nextTick } from 'vue'; import { ref, unref, reactive, computed, nextTick } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal'; import { BasicModal, useModalInner } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import JUpload from './JUpload.vue'; import JUpload from './JUpload.vue';
import { UploadTypeEnum } from './upload.data'; import { UploadTypeEnum } from './upload.data';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
const { createMessage } = useMessage();
const emit = defineEmits(['change', 'update:value', 'register']); const emit = defineEmits(['change', 'update:value', 'register']);
const props = defineProps({ defineProps({
value: propTypes.oneOfType([propTypes.string, propTypes.array]), value: propTypes.oneOfType([propTypes.string, propTypes.array]),
width: propTypes.number.def(520), width: propTypes.number.def(520),
}); });
@ -43,3 +43,54 @@
emit('update:value', value); emit('update:value', value);
} }
</script> </script>
<style lang="less">
.j-upload-modal-content {
.ant-upload-list-text {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 12px;
.ant-upload-list-item-container {
margin: 0;
}
.ant-upload-list-item {
min-height: 38px;
margin: 0;
padding: 6px 10px;
border: 1px solid #f0f0f0;
border-radius: 6px;
background-color: #fafafa;
transition:
border-color 0.2s,
background-color 0.2s;
&:hover {
border-color: fade(@primary-color, 25%);
background-color: fade(@primary-color, 4%);
}
}
.ant-upload-list-item-name {
min-width: 0;
overflow: hidden;
color: rgba(0, 0, 0, 0.65);
text-overflow: ellipsis;
white-space: nowrap;
}
.ant-upload-text-icon {
color: rgba(0, 0, 0, 0.45);
}
.ant-upload-list-item:hover {
.ant-upload-list-item-name,
.ant-upload-text-icon {
color: @primary-color;
}
}
}
}
</style>

View File

@ -23,16 +23,24 @@
:maxTagCount="maxTagCount" :maxTagCount="maxTagCount"
@change="handleChange" @change="handleChange"
style="width: 100%" style="width: 100%"
@click="!disabled && openModal(false)" @click="handleSelectClick"
v-bind="attrs" v-bind="attrs"
:class="{ 'j-select-biz-wrap': isMultipleMode, 'j-select-biz-has-omitted-tags': hasOmittedTags }"
> >
<template v-if="isCustomRenderTag" #tagRender="{ label, value, option}"> <template v-if="isCustomRenderTag" #tagRender="{ label, value, option}">
<a-tag class="ant-select-selection-item" style="margin-right: 4px"> <!-- update-begin--author:liaozhiyang---date:20260804---forLHZP-602岗位层级过深时右侧文字完整左侧省略多选换行展开 -->
<span class="ant-select-selection-item-content" style="font-size: 14px;max-width: 300px" :title="tagRender(label, value, false)">{{ tagRender(label, value, true) }}</span> <a-tag class="ant-select-selection-item j-select-biz-tag" style="margin-right: 4px">
<span class="ant-select-selection-item-remove"> <JLeftEllipsis
content-class="ant-select-selection-item-content j-select-biz-tag-content"
:text="getTagFullText(label, value)"
container-selector=".ant-select-selector"
:reserve-width="tagReserveWidth"
/>
<span v-if="!disabled" class="ant-select-selection-item-remove">
<Icon icon="ant-design:close-outlined" size="12" @click="handleRemoveClick(value)"></Icon> <Icon icon="ant-design:close-outlined" size="12" @click="handleRemoveClick(value)"></Icon>
</span> </span>
</a-tag> </a-tag>
<!-- update-end--author:liaozhiyang---date:20260804---forLHZP-602岗位层级过深时右侧文字完整左侧省略多选换行展开 -->
</template> </template>
</a-select> </a-select>
</a-col> </a-col>
@ -48,15 +56,16 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, inject, reactive, watch } from 'vue'; import { computed, defineComponent, ref, inject, watch } from 'vue';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs'; import { useAttrs } from '/@/hooks/core/useAttrs';
import { LoadingOutlined } from '@ant-design/icons-vue'; import { LoadingOutlined } from '@ant-design/icons-vue';
import { getDepartPathNameByOrgCode } from "@/utils/common/compUtils"; import { getDepartPathNameByOrgCode } from '@/utils/common/compUtils';
import JLeftEllipsis from '../JLeftEllipsis.vue';
export default defineComponent({ export default defineComponent({
name: 'JSelectBiz', name: 'JSelectBiz',
components: { LoadingOutlined }, components: { LoadingOutlined, JLeftEllipsis },
inheritAttrs: false, inheritAttrs: false,
props: { props: {
showButton: propTypes.bool.def(true), showButton: propTypes.bool.def(true),
@ -94,7 +103,18 @@
// //
const departNamePath = ref<Record<string, string>>({}); const departNamePath = ref<Record<string, string>>({});
const isMultipleMode = computed(() => props.multiple === 'multiple' || props.multiple === 'tags');
// update-begin--author:liaozhiyang---date:20260811---forLHZP-513online
const hasOmittedTags = computed(
() => typeof props.maxTagCount === 'number' && Array.isArray(selectValues.value) && selectValues.value.length > props.maxTagCount
);
const tagReserveWidth = computed(() => {
const baseWidth = props.disabled ? 24 : 44;
return hasOmittedTags.value ? baseWidth + 40 : baseWidth;
});
// update-end--author:liaozhiyang---date:20260811---forLHZP-513online
/** /**
* 打开弹出框 * 打开弹出框
*/ */
@ -117,49 +137,84 @@
} }
/** /**
* 多选tag自定义渲染 * 多选tag自定义渲染完整路径省略交给 JLeftEllipsis
* *
* @param label * @param label
* @param value * @param value
* @param isEllipsis 是否省略 * @param _isEllipsis 兼容旧调用
*/ */
function tagRender(label, value, isEllipsis) { function tagRender(label, value, _isEllipsis) {
if (departNamePath.value[value]) { return getTagFullText(label, value);
// }
if(!isEllipsis){
return departNamePath.value[value]; // update-begin--author:liaozhiyang---date:20260804---forLHZP-602
} else { //
let departName = departNamePath.value[value]; function toDisplayText(val) {
//2020 if (val == null || val === '') {
if(departName && departName.length >= 20){ return '';
const name:any = departName.substring(departName.length - 20);
return '...' + name;
} else {
return departName;
}
}
} }
//rowKeyorgCode if (typeof val === 'string') {
if(props?.rowKey && props?.rowKey === 'orgCode'){ return val;
getDepartPathNameByOrgCode(value, label, '').then((data) => { }
departNamePath.value[value] = data; if (typeof val === 'number' || typeof val === 'boolean') {
return String(val);
}
if (Array.isArray(val)) {
return val.map((item) => toDisplayText(item)).filter(Boolean).join('');
}
if (typeof val === 'object' && typeof val.children === 'string') {
return val.children;
}
return '';
}
/** 获取完整部门/岗位路径(异步回填后响应式更新) */
function getTagFullText(label, value) {
if (departNamePath.value[value]) {
return toDisplayText(departNamePath.value[value]);
}
const fallback = toDisplayText(label);
if (props?.rowKey && props?.rowKey === 'orgCode') {
getDepartPathNameByOrgCode(value, fallback, '').then((data) => {
departNamePath.value[value] = toDisplayText(data) || fallback;
}); });
} else { } else {
//id getDepartPathNameByOrgCode('', fallback, value).then((data) => {
getDepartPathNameByOrgCode('', label, value).then((data) => { departNamePath.value[value] = toDisplayText(data) || fallback;
departNamePath.value[value] = data;
}); });
} }
return fallback;
}
// update-end--author:liaozhiyang---date:20260804---forLHZP-602
/**
* 点击选择框时打开弹窗
* 排除点击清除按钮的情况避免清除时误触发弹窗
*/
function handleSelectClick(event) {
if (props.disabled) {
return;
}
// cleartag
if (event.target.closest('.ant-select-selection-item-remove') || event.target.closest('.ant-select-clear')) {
return;
}
openModal(false);
} }
/** /**
* tag删除 * tag删除
* *
* @param value * @param value
*/ */
function handleRemoveClick(value) { function handleRemoveClick(value) {
if(selectValues?.value){ // update-begin--author:liaozhiyang---date:20260714---forLHZP-583
let values = selectValues?.value.filter(item => item !== value); if (props.disabled) {
return;
}
// update-end--author:liaozhiyang---date:20260714---forLHZP-583
if (selectValues?.value) {
let values = selectValues?.value.filter((item) => item !== value);
handleChange(values); handleChange(values);
} }
} }
@ -186,7 +241,12 @@
openModal, openModal,
detailStr, detailStr,
tagRender, tagRender,
getTagFullText,
handleRemoveClick, handleRemoveClick,
handleSelectClick,
isMultipleMode,
hasOmittedTags,
tagReserveWidth,
}; };
}, },
}); });
@ -194,22 +254,95 @@
<style lang="less" scoped> <style lang="less" scoped>
.j-select-row { .j-select-row {
@width: 82px; @width: 82px;
flex-wrap: nowrap;
align-items: flex-start;
.left { .left {
// update-begin--author:liaozhiyang---date:20260804---forLHZP-602
flex: 1 1 auto;
width: calc(100% - @width - 8px); width: calc(100% - @width - 8px);
max-width: calc(100% - @width - 8px);
min-width: 0;
// update-end--author:liaozhiyang---date:20260804---forLHZP-602
} }
.right { .right {
flex: 0 0 @width;
width: @width; width: @width;
} }
.full { .full {
width: 100%; width: 100%;
max-width: 100%;
} }
:deep(.ant-select-search__field) { :deep(.ant-select-search__field) {
display: none !important; display: none !important;
} }
// update-begin--author:liaozhiyang---date:20260804---forLHZP-602
:deep(.ant-select) {
max-width: 100%;
}
:deep(.ant-select-selector) {
max-width: 100%;
height: auto !important;
min-height: 32px;
overflow: hidden;
}
/* 多选tag 换行展开,不挤在一行 */
:deep(.j-select-biz-wrap .ant-select-selection-overflow) {
flex-wrap: wrap;
overflow: visible;
max-width: 100%;
}
:deep(.ant-select-selection-overflow-item) {
max-width: 100%;
min-width: 0;
}
// update-begin--author:liaozhiyang---date:20260811---forLHZP-513online
:deep(.j-select-biz-has-omitted-tags .ant-select-selection-overflow) {
flex-wrap: nowrap;
}
:deep(.j-select-biz-has-omitted-tags .ant-select-selection-overflow-item) {
max-width: calc(100% - 48px);
}
:deep(.j-select-biz-has-omitted-tags .ant-select-selection-overflow-item-rest),
:deep(.j-select-biz-has-omitted-tags .ant-select-selection-overflow-item-suffix) {
max-width: none;
}
// update-end--author:liaozhiyang---date:20260811---forLHZP-513online
:deep(.j-select-biz-tag) {
display: inline-flex !important;
align-items: center;
max-width: 100%;
margin-inline-end: 4px;
margin-bottom: 2px;
overflow: hidden;
box-sizing: border-box;
}
:deep(.j-select-biz-tag-content) {
flex: 1 1 0 !important;
min-width: 0 !important;
display: block !important;
font-size: 14px;
overflow: hidden;
white-space: nowrap !important;
text-overflow: clip !important;
}
:deep(.j-select-biz-tag .ant-select-selection-item-remove) {
flex-shrink: 0;
}
// update-end--author:liaozhiyang---date:20260804---forLHZP-602
} }
.detailStr { .detailStr {
margin: 0; margin: 0;

View File

@ -282,7 +282,7 @@
// : issues/3656popupdict // : issues/3656popupdict
watchEffect(() => { watchEffect(() => {
if (props.selected && props.rowkey && unref(visible)) { if (props.selected && props.rowkey && unref(visible)) {
const selected = props.multi ? props.selected : [props.selected]; const selected = Array.isArray(props.selected) ? props.selected : [props.selected];
checkedKeys!.value = selected.map((item) => item[props.rowkey]); checkedKeys!.value = selected.map((item) => item[props.rowkey]);
selectRows!.value = selected; selectRows!.value = selected;
} }

View File

@ -313,9 +313,17 @@
breadcrumb.value = []; breadcrumb.value = [];
} }
}; };
//
const setDepartChildrenChecked = (depart, checked: boolean) => {
depart.children?.forEach((child) => {
child.checked = checked;
setDepartChildrenChecked(child, checked);
});
};
// //
const handleDepartTreeCheck = (e, item) => { const handleDepartTreeCheck = (e, item) => {
const { target } = e; const { target } = e;
setDepartChildrenChecked(item, target.checked);
if (target.checked) { if (target.checked) {
// //
getUsersByDeptId(item['id']).then((users) => { getUsersByDeptId(item['id']).then((users) => {
@ -678,25 +686,39 @@
ol { ol {
flex-wrap: nowrap; flex-wrap: nowrap;
overflow-x: auto; overflow-x: auto;
/* 美化滚动条 */ /* 预留滚动条空间,避免 hover 变粗时布局跳动 */
padding-bottom: 6px;
/* 美化滚动条:默认细,悬停变粗方便拖动 */
// update-begin--author:liaozhiyang---date:20260804---forLHZP-869便
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 4px; width: 4px;
height: 3px; height: 4px;
background: #f5f5f5; background: transparent;
border-radius: 2px;
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: #bfbfbf; background: #bfbfbf;
border-radius: 3px; border-radius: 4px;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
&:hover { &:hover {
background: #999; background: #999;
} }
} }
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: #f5f5f5; background: transparent;
border-radius: 3px; border-radius: 4px;
} }
&:hover {
&::-webkit-scrollbar {
height: 10px;
}
&::-webkit-scrollbar-thumb {
background: #999;
}
&::-webkit-scrollbar-track {
background: #f0f0f0;
}
}
// update-end--author:liaozhiyang---date:20260804---forLHZP-869便
} }
li { li {
flex: none; flex: none;

View File

@ -83,6 +83,7 @@
component: 'Input', component: 'Input',
}, },
], ],
autoSubmitOnEnter: true,
}; };
// //
const columns = [ const columns = [

View File

@ -114,6 +114,7 @@
size: 'small', size: 'small',
formConfig: { formConfig: {
//labelWidth: 200, //labelWidth: 200,
autoSubmitOnEnter: true,
baseColProps: { baseColProps: {
xs: 24, xs: 24,
sm: 8, sm: 8,

View File

@ -134,14 +134,23 @@
size: 'small', size: 'small',
}; };
const getBindValue = Object.assign({}, unref(props), unref(attrs), config); const getBindValue = Object.assign({}, unref(props), unref(attrs), config);
const [{ rowSelection, visibleChange, selectValues, indexColumnProps, getSelectResult, handleDeleteSelected, selectRows }] = useSelectBiz( const [{ rowSelection, visibleChange, selectValues, indexColumnProps, getSelectResult, handleDeleteSelected, selectRows, checkedKeys }] = useSelectBiz(
getUserList, getUserList,
getBindValue, getBindValue,
emit emit
); );
const searchInfo = ref(props.params); // params props watch props.params
// searchInfo
const searchInfo = ref<Recordable>(props.params ?? {});
watch(
() => props.params,
(newParams) => {
searchInfo.value = newParams ?? {};
},
{ deep: true, immediate: true }
);
// : issues/657 // : issues/657
watch(rowSelection.selectedRowKeys, (newVal) => { watch(checkedKeys, (newVal) => {
// : null------------ // : null------------
if(tableRef.value){ if(tableRef.value){
tableRef.value.setSelectedRowKeys(newVal); tableRef.value.setSelectedRowKeys(newVal);

View File

@ -1,44 +1,56 @@
<template> <template>
<BasicModal <BasicModal
@register="register" @register="register"
:getContainer="getContainer" :getContainer="getContainer"
:canFullscreen="false" :canFullscreen="false"
:title="title" :title="title"
:width="500" width="min(500px, 92vw)"
destroyOnClose destroyOnClose
@ok="handleOk" @ok="handleOk"
wrapClassName="j-user-select-modal2" > wrapClassName="j-user-select-modal2"
>
<!--update-begin---author:wangshuai---date:20260807---for:LHZP-141表单设计器筛选 角色选择的弹框效果不太好-->
<div class="role-modal-body">
<!-- 搜索框 -->
<div class="role-search-bar">
<a-input v-model:value="searchText" class="role-search" allowClear placeholder="搜索角色名称">
<template #prefix>
<SearchOutlined class="search-icon" />
</template>
</a-input>
<span class="selected-count">已选 {{ selectedList.length }}</span>
</div>
<div style="position: relative; min-height: 350px"> <div class="list-caption">
<div style="width: 100%"> <span>角色列表</span>
<a-input v-model:value="searchText" allowClear style="width: 100%" placeholder="搜索"> <span> {{ showDataList.length }} </span>
<template #prefix> </div>
<SearchOutlined style="color: #c0c0c0" />
</template>
</a-input>
</div>
<!-- tabs --> <!-- 角色列表 -->
<div class="modal-select-list-container"> <div class="modal-select-list-container">
<div class="scroll"> <template v-if="showDataList.length > 0">
<div class="content" style="right: -10px"> <label class="item" :class="{ checked: item.checked }" v-for="item in showDataList" :key="item.id" @click="(e) => onSelect(e, item)">
<span class="role-mark">{{ item.name?.slice(0, 1) || '角' }}</span>
<label class="item" v-for="item in showDataList" @click="(e)=>onSelect(e, item)"> <span class="role-info">
<a-checkbox v-model:checked="item.checked"> <span class="role-name">{{ item.name }}</span>
<span class="text">{{ item.name }}</span> <span v-if="item.code" class="role-code">{{ item.code }}</span>
</a-checkbox> </span>
</label> <a-checkbox v-model:checked="item.checked" class="role-checkbox" />
</div> </label>
</template>
</div> <a-empty v-else description="暂无角色" :image-style="{ height: '60px' }" style="margin-top: 70px" />
</div> </div>
<!-- 选中用户 --> <!-- 已选角色 -->
<div class="selected-users" style="width: 100%; overflow-x: hidden"> <div v-if="selectedList.length > 0" class="selected-users-wrapper">
<SelectedUserItem v-for="item in selectedList" :info="item" @unSelect="unSelect" /> <div class="selected-title">已选角色</div>
</div> <div class="selected-users">
<SelectedUserItem v-for="item in selectedList" :key="item.id" :info="item" @unSelect="unSelect" />
</div> </div>
</BasicModal> </div>
</div>
<!--update-end---author:wangshuai---date:20260807---for:LHZP-141表单设计器筛选 角色选择的弹框效果不太好-->
</BasicModal>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -104,6 +116,9 @@
if(!list || list.length ==0 ){ if(!list || list.length ==0 ){
}else{ }else{
let selectedIdList = data.list || []; let selectedIdList = data.list || [];
//update-begin---author:wangshuai---date:20260807---for:LHZP-141
selectedKeys.value = [...selectedIdList];
//update-end---author:wangshuai---date:20260807---for:LHZP-141
for(let item of list){ for(let item of list){
if(selectedIdList.indexOf(item.id)>=0){ if(selectedIdList.indexOf(item.id)>=0){
item.checked = true; item.checked = true;
@ -236,83 +251,223 @@
}; };
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.modal-select-list-container{ //update-begin---author:wangshuai---date:20260807---for:LHZP-141
height: 352px; .role-modal-body {
margin-top: 12px; position: relative;
overflow: auto; }
.scroll{
height: 100%; .role-search-bar {
position: relative; display: flex;
width: 100%; align-items: center;
overflow: hidden; gap: 12px;
.content{ padding: 12px;
bottom: 0; background: linear-gradient(135deg, fade(@primary-color, 8%), fade(@primary-color, 2%));
left: 0; border: 1px solid fade(@primary-color, 12%);
overflow: scroll; border-radius: 10px;
overflow-x: hidden; }
position: absolute;
right: 0; .role-search {
top: 0; flex: 1;
.item{ height: 36px;
padding: 7px 5px; border-radius: 7px;
cursor: pointer; background-color: #fff;
display: block; border-color: #e5e7eb;
&:hover{ box-shadow: none;
background-color: #f5f5f5; transition:
} border-color 0.2s,
} box-shadow 0.2s;
} &:hover {
border-color: fade(@primary-color, 55%);
}
&:focus,
&.ant-input-affix-wrapper-focused {
background-color: #fff;
border-color: @primary-color;
box-shadow: 0 0 0 2px fade(@primary-color, 12%);
}
:deep(.ant-input) {
background-color: transparent;
}
}
.search-icon {
color: #9ca3af;
}
.selected-count {
flex: none;
min-width: 58px;
color: @primary-color;
font-size: 12px;
font-weight: 500;
text-align: right;
white-space: nowrap;
}
.list-caption {
display: flex;
align-items: center;
justify-content: space-between;
margin: 14px 2px 8px;
color: #1f2937;
font-size: 13px;
font-weight: 500;
span:last-child {
color: #9ca3af;
font-size: 12px;
font-weight: 400;
}
}
.modal-select-list-container {
height: 360px;
overflow-y: auto;
border: 1px solid #e8eaed;
border-radius: 10px;
padding: 6px;
scrollbar-width: thin;
scrollbar-color: #d8dce3 transparent;
.item {
position: relative;
display: flex;
align-items: center;
min-height: 54px;
padding: 7px 12px 7px 10px;
border: 1px solid transparent;
border-radius: 8px;
cursor: pointer;
transition:
background-color 0.2s,
border-color 0.2s;
& + .item {
margin-top: 4px;
}
&:hover {
background-color: #f7f9fc;
border-color: #edf0f4;
}
&.checked {
background-color: fade(@primary-color, 7%);
border-color: fade(@primary-color, 22%);
&::before {
position: absolute;
top: 12px;
bottom: 12px;
left: -1px;
width: 3px;
background: @primary-color;
border-radius: 0 3px 3px 0;
content: '';
} }
.role-mark {
color: #fff;
background: @primary-color;
}
}
} }
}
.role-mark {
display: inline-flex;
flex: none;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
margin-right: 11px;
color: @primary-color;
font-size: 14px;
font-weight: 600;
background: fade(@primary-color, 10%);
border-radius: 9px;
transition:
color 0.2s,
background-color 0.2s;
}
.role-info {
display: flex;
flex: 1;
min-width: 0;
flex-direction: column;
}
.role-name,
.role-code {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.role-name {
color: #20242a;
font-size: 14px;
line-height: 20px;
}
.role-code {
margin-top: 1px;
color: #9aa1ab;
font-size: 11px;
line-height: 16px;
}
.role-checkbox {
flex: none;
margin-left: 12px;
}
.selected-users-wrapper {
margin-top: 12px;
padding: 10px 12px 8px;
background: #f8f9fb;
border: 1px solid #eef0f3;
border-radius: 8px;
.selected-title {
margin-bottom: 6px;
color: #6b7280;
font-size: 12px;
}
.selected-users {
display: flex;
flex-wrap: wrap;
flex-direction: row;
align-items: center;
gap: 4px;
padding-top: 0;
// 30px
:deep(.user-selected-item) {
height: 26px;
line-height: 26px;
margin-bottom: 2px;
}
}
}
//update-end---author:wangshuai---date:20260807---for:LHZP-141
</style> </style>
<style lang="less"> <style lang="less">
.j-user-select-modal2 { .j-user-select-modal2 {
.depart-select { .scroll-container {
.ant-select-selector { padding-bottom: 0 !important;
color: #fff !important;
background-color: #409eff !important;
border-radius: 5px !important;
}
.ant-select-selection-item,
.ant-select-arrow {
color: #fff !important;
}
}
.my-search {
position: absolute;
top: 14px;
z-index: 1;
&.all-width {
width: 100%;
}
.anticon { // 18px
cursor: pointer; .scrollbar__wrap {
&:hover { margin-bottom: 0 !important;
color: #0a8fe9 !important; }
}
}
.hidden {
display: none;
}
}
.my-tabs {
}
.selected-users {
display: flex;
flex-wrap: wrap;
flex-direction: row;
padding-top: 15px;
}
.scroll-container {
padding-bottom: 0 !important;
}
} }
}
</style> </style>

View File

@ -126,11 +126,15 @@
} }
const maxHeight = ref(300); const maxHeight = ref(300);
maxHeight.value = window.innerHeight - 300; //update-begin---wangshuai---date:20260810 forLHZP-1106 ------------
maxHeight.value = window.innerHeight - 500;
//update-end---wangshuai---date:20260810 forLHZP-1106 ------------
const containerStyle = computed(() => { const containerStyle = computed(() => {
return { return {
'overflow-y': 'auto', 'overflow-y': 'auto',
'max-height': maxHeight.value + 'px', //update-begin---wangshuai---date:20260810 forLHZP-1106 ------------
'max-height': (maxHeight.value >= 300 ? maxHeight.value : 300) + 'px',
//update-end---author:wangshuai ---date:20260810 forLHZP-1106 ------------
}; };
}); });

View File

@ -162,6 +162,7 @@
if (props.izExcludeMy) { if (props.izExcludeMy) {
excludeUserIdList.value.push(userStore.getUserInfo.id); excludeUserIdList.value.push(userStore.getUserInfo.id);
} }
resetFilterState();
// //
loadUserList(); loadUserList();
}); });
@ -180,6 +181,7 @@
selectedDepart.value = ''; selectedDepart.value = '';
} }
function onDepartChange() { function onDepartChange() {
pageNo.value = 1;
loadUserList(); loadUserList();
} }
/*--------------部门下拉框,用于筛选用户---------------*/ /*--------------部门下拉框,用于筛选用户---------------*/
@ -187,6 +189,17 @@
/*--------------第一页 搜索框---------------*/ /*--------------第一页 搜索框---------------*/
const searchInputStatus = ref(false); const searchInputStatus = ref(false);
const searchText = ref(''); const searchText = ref('');
/**
* 清空数据
*/
function resetFilterState() {
selectedDepart.value = '';
searchText.value = '';
searchInputStatus.value = false;
pageNo.value = 1;
}
function showSearchInput(e) { function showSearchInput(e) {
e && prevent(e); e && prevent(e);
searchInputStatus.value = true; searchInputStatus.value = true;

View File

@ -1,4 +1,4 @@
import { inject, reactive, ref, watch, unref, Ref } from 'vue'; import { inject, reactive, ref, watch, unref, Ref, computed } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { isEmpty } from '@/utils/is'; import { isEmpty } from '@/utils/is';
@ -61,17 +61,16 @@ export function useSelectBiz(getList, props, emit?) {
} }
/** /**
* 选择列配置 * 选择列配置响应式 isRadioSelection prop 变化时type 自动切换 radio/checkbox
*/ */
const rowSelection = { const rowSelection = computed(() => ({
// : rowSelectiontype,'checkbox' ---
type: props.isRadioSelection ? 'radio' : 'checkbox', type: props.isRadioSelection ? 'radio' : 'checkbox',
columnWidth: 20, columnWidth: 20,
selectedRowKeys: checkedKeys, selectedRowKeys: checkedKeys,
onChange: onSelectChange, onChange: onSelectChange,
//table4.4.0false //table4.4.0false
preserveSelectedRowKeys:true, preserveSelectedRowKeys:true,
}; }));
/** /**
* 序号列配置 * 序号列配置

View File

@ -219,6 +219,8 @@ export interface HelpComponentProps {
text: any; text: any;
// colour // colour
color: string; color: string;
// Tooltip background colour
backgroundColor?: string;
// font size // font size
fontSize: string; fontSize: string;
icon: string; icon: string;

View File

@ -154,6 +154,7 @@ export type ComponentType =
| 'LinkTableSelect' | 'LinkTableSelect'
| 'LinkTableForQuery' | 'LinkTableForQuery'
| 'CascaderPcaForQuery' | 'CascaderPcaForQuery'
| 'JCategoryLinkage'
| 'CascaderPcaInFilter' | 'CascaderPcaInFilter'
| 'UserSelect' | 'UserSelect'
| 'RoleSelect' | 'RoleSelect'
@ -163,5 +164,5 @@ export type ComponentType =
| 'RangeTime' | 'RangeTime'
| 'JRangeNumber' | 'JRangeNumber'
| 'JLinkTableCard' | 'JLinkTableCard'
| 'JInputSelect'; | 'JInputSelect'
| 'JAuthRuleValue';

View File

@ -9,6 +9,10 @@
import { isString } from '/@/utils/is'; import { isString } from '/@/utils/is';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
const SVG_END_WITH_FLAG = '|svg'; const SVG_END_WITH_FLAG = '|svg';
// update-begin--author:liaozhiyang---date:20260713---forLHZP-143online
// Iconifyimport
let iconifyInstance: any = null;
// update-end--author:liaozhiyang---date:20260713---forLHZP-143online
export default defineComponent({ export default defineComponent({
name: 'Icon', name: 'Icon',
components: { SvgIcon }, components: { SvgIcon },
@ -44,17 +48,21 @@
// update-begin--author:liaozhiyang---date:20260304---for:QQYUN-14802unplugin-iconsicononlinelocal // update-begin--author:liaozhiyang---date:20260304---for:QQYUN-14802unplugin-iconsicononlinelocal
try { try {
let svg: SVGElement | null = null; let svg: SVGElement | null = null;
if (import.meta.env.VITE_GLOB_ICONIFY_USE_TYPE === 'local') { // update-begin--author:liaozhiyang---date:20260713---forLHZP-143online
if (iconifyInstance) {
svg = iconifyInstance.renderSVG(icon, {});
} else if (import.meta.env.VITE_GLOB_ICONIFY_USE_TYPE === 'local') {
// 使 purge-icons 线 // 使 purge-icons 线
const iconifyModule = await import('@purge-icons/generated'); const iconifyModule = await import('@purge-icons/generated');
const Iconify = iconifyModule.default; iconifyInstance = iconifyModule.default;
svg = Iconify.renderSVG(icon, {}); svg = iconifyInstance.renderSVG(icon, {});
} else { } else {
// 使 @iconify/iconify 线 // 使 @iconify/iconify 线
const iconifyModule = await import('@iconify/iconify'); const iconifyModule = await import('@iconify/iconify');
const Iconify = iconifyModule.default; iconifyInstance = iconifyModule.default;
svg = Iconify.renderSVG(icon, {}); svg = iconifyInstance.renderSVG(icon, {});
} }
// update-end--author:liaozhiyang---date:20260713---forLHZP-143online
if (svg) { if (svg) {
el.textContent = ''; el.textContent = '';
el.appendChild(svg); el.appendChild(svg);

View File

@ -41,10 +41,16 @@ export default defineComponent({
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import {ref, watch, computed, nextTick, useAttrs, defineProps} from 'vue' import {ref, watch, computed, nextTick, useAttrs} from 'vue'
import {DatePicker} from 'ant-design-vue' import {DatePicker} from 'ant-design-vue'
import {useDesign} from '/@/hooks/web/useDesign'; import {useDesign} from '/@/hooks/web/useDesign';
import { Form } from 'ant-design-vue'; import { Form } from 'ant-design-vue';
import dayjs from 'dayjs';
import isoWeek from 'dayjs/plugin/isoWeek';
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
dayjs.extend(isoWeek);
dayjs.extend(quarterOfYear);
const formItemContext = Form.useInjectFormItemContext(); const formItemContext = Form.useInjectFormItemContext();
// //
@ -98,12 +104,30 @@ watch(innerValue, (val) => {
val = '' val = ''
openDatePicker() openDatePicker()
} }
emit('change', val) // update-begin--author:wangshuai---date:20260819---forLHZP-322online
emit('update:value', val) const normalizedValue = normalizePickerValue(val)
emit('change', normalizedValue)
emit('update:value', normalizedValue)
// update-end--author:wangshuai---date:20260819---forLHZP-322online
// : QQYUN-9227 // : QQYUN-9227
formItemContext?.onFieldChange(); formItemContext?.onFieldChange();
}) })
/** 季度取季度第一天,周取 ISO 周的周一。 */
function normalizePickerValue(value) {
if (!value || RANGE_OPTION_KEYS.includes(value)) {
return value
}
const picker = attrs.picker
if (picker === 'quarter') {
return dayjs(value).startOf('quarter').format('YYYY-MM-DD')
}
if (picker === 'week') {
return dayjs(value).startOf('isoWeek').format('YYYY-MM-DD')
}
return value
}
watch(() => props.allowSelectRange, (allow) => { watch(() => props.allowSelectRange, (allow) => {
// //
if (!allow && isRange.value) { if (!allow && isRange.value) {

View File

@ -10,6 +10,10 @@ export async function registerJVxeCustom() {
// Popup // Popup
await registerAsyncComponent(JVxeTypes.popup, import('./src/components/JVxePopupCell.vue')); await registerAsyncComponent(JVxeTypes.popup, import('./src/components/JVxePopupCell.vue'));
// update-begin--author:liaozhiyang---date:20260507---forissues/7617vxetablepopup
// Popup
await registerAsyncComponent(JVxeTypes.popupDict, import('./src/components/JVxePopupDictCell.vue'));
// update-end--author:liaozhiyang---date:20260507---forissues/7617vxetablepopup
// //
registerComponent(JVxeTypes.selectDictSearch, DictSearchInputCell, DictSearchSpanCell); registerComponent(JVxeTypes.selectDictSearch, DictSearchInputCell, DictSearchSpanCell);

View File

@ -1,57 +1,70 @@
<template> <template>
<div> <div>
<template v-if="hasFile" v-for="(file, fileKey) of [innerFile || {}]" :key="fileKey"> <template v-if="hasFile">
<div style="position: relative"> <div v-for="(file, fileKey) of [innerFile || {}]" :key="fileKey" class="j-vxe-file-item">
<a-tooltip v-if="file.status === 'uploading'" :title="`上传中(${Math.floor(file.percent)}%)`"> <a-tooltip v-if="file.status === 'uploading'" :title="`上传中(${Math.floor(file.percent)}%)`">
<LoadingOutlined /> <span class="j-vxe-file-content">
<span style="margin-left: 5px">上传中</span> <LoadingOutlined />
<span class="j-vxe-file-name">上传中</span>
</span>
</a-tooltip> </a-tooltip>
<a-tooltip v-else-if="file.status === 'done'" :title="file.name"> <a-tooltip v-else-if="file.status === 'done'" :title="file.name" :mouseEnterDelay="0.4">
<Icon icon="ant-design:paper-clip" /> <button
<span style="margin-left: 5px">{{ ellipsisFileName }}</span> type="button"
class="j-vxe-file-content j-vxe-file-content--action"
title=""
:disabled="cellProps.disabled"
:aria-label="`管理附件,共 ${uploadCount} 个`"
@click="handleMoreOperation"
>
<Icon icon="ant-design:paper-clip" />
<span class="j-vxe-file-name">{{ shortFileName }}</span>
<span v-if="uploadCount > 1" class="j-vxe-file-count">{{ uploadCount }}</span>
</button>
</a-tooltip> </a-tooltip>
<a-tooltip v-else :title="file.message || '上传失败'"> <a-tooltip v-else :title="file.message || '上传失败'">
<Icon icon="ant-design:exclamation-circle" style="color: red" /> <span class="j-vxe-file-content">
<span style="margin-left: 5px">{{ ellipsisFileName }}</span> <Icon icon="ant-design:exclamation-circle" style="color: red" />
<span class="j-vxe-file-name">{{ shortFileName }}</span>
</span>
</a-tooltip> </a-tooltip>
<Dropdown :trigger="['click']" placement="bottomRight" style="margin-left: 10px"> <Dropdown v-if="file.status !== 'uploading'" :trigger="['click']" placement="bottomRight">
<a-tooltip title="操作"> <a-tooltip title="操作">
<Icon v-if="file.status !== 'uploading'" icon="ant-design:setting" style="cursor: pointer" /> <button type="button" class="j-vxe-file-action" aria-label="文件操作">
<Icon icon="ant-design:ellipsis-outlined" />
</button>
</a-tooltip> </a-tooltip>
<template #overlay> <template #overlay>
<a-menu> <a-menu>
<a-menu-item :disabled="cellProps.disabled" @click="handleMoreOperation">
<span><Icon icon="ant-design:bars" />&nbsp;管理</span>
</a-menu-item>
<a-menu-item v-if="originColumn.allowDownload !== false" @click="handleClickDownloadFile"> <a-menu-item v-if="originColumn.allowDownload !== false" @click="handleClickDownloadFile">
<span><Icon icon="ant-design:download" />&nbsp;下载</span> <span><Icon icon="ant-design:download" />&nbsp;下载</span>
</a-menu-item> </a-menu-item>
<a-menu-item :disabled="cellProps.disabled" v-if="originColumn.allowRemove !== false" @click="handleClickDeleteFile"> <a-menu-item :disabled="cellProps.disabled" v-if="originColumn.allowRemove !== false" @click="handleClickDeleteFile">
<span><Icon icon="ant-design:delete" />&nbsp;删除</span> <span><Icon icon="ant-design:delete" />&nbsp;删除</span>
</a-menu-item> </a-menu-item>
<a-menu-item :disabled="cellProps.disabled" @click="handleMoreOperation">
<span><Icon icon="ant-design:bars" />&nbsp;更多</span>
</a-menu-item>
</a-menu> </a-menu>
</template> </template>
</Dropdown> </Dropdown>
</div> </div>
</template> </template>
<a-upload <a-button
v-if="!cellProps.disabledTable" v-if="!cellProps.disabledTable"
v-show="!hasFile" v-show="!hasFile"
name="file" class="j-vxe-upload-trigger"
:data="{ isup: 1 }" size="small"
:multiple="false" preIcon="ant-design:upload"
:action="uploadAction" :disabled="cellProps.disabled"
:headers="uploadHeaders" @click="handleMoreOperation"
:showUploadList="false"
v-bind="cellProps"
@change="handleChangeUpload"
> >
<a-button preIcon="ant-design:upload">{{ originColumn.btnText || '点击上传' }}</a-button> {{ originColumn.btnText || '附件' }}
</a-upload> </a-button>
<JUploadModal :value="modalValue" @register="registerModel" @change="onModalChange" /> <JUploadModal :value="modalValue" @register="registerModel" @change="onModalChange" />
</div> </div>
</template> </template>
@ -75,4 +88,94 @@
}); });
</script> </script>
<style scoped lang="less"></style> <style scoped lang="less">
.j-vxe-file-item {
display: inline-flex;
align-items: center;
max-width: 100%;
height: 28px;
overflow: hidden;
color: var(--vxe-ui-font-color);
vertical-align: middle;
background-color: var(--vxe-ui-layout-background-color);
border: 1px solid var(--vxe-ui-table-border-color);
border-radius: 4px;
}
.j-vxe-file-content {
display: inline-flex;
align-items: center;
min-width: 0;
height: 100%;
padding: 0 8px;
color: inherit;
background: transparent;
border: 0;
column-gap: 6px;
}
.j-vxe-file-content--action:not(:disabled) {
cursor: pointer;
&:hover,
&:focus-visible {
color: var(--vxe-ui-font-primary-color);
background-color: rgba(64, 158, 255, 0.08);
}
&:focus-visible {
outline: 2px solid var(--vxe-ui-font-primary-color) !important;
outline-offset: -2px;
}
}
.j-vxe-file-name {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.j-vxe-file-count {
flex: none;
min-width: 16px;
height: 16px;
padding: 0 4px;
color: #fff;
font-size: 10px;
line-height: 16px;
text-align: center;
background-color: var(--vxe-ui-font-primary-color);
border-radius: 8px;
}
.j-vxe-file-action {
display: inline-flex;
align-items: center;
justify-content: center;
align-self: stretch;
flex: 0 0 28px;
width: 28px;
padding: 0;
color: var(--vxe-ui-font-color);
cursor: pointer;
background: transparent;
border: 0;
border-left: 1px solid var(--vxe-ui-table-border-color);
transition:
color 0.2s,
background-color 0.2s;
&:hover,
&:focus-visible {
color: var(--vxe-ui-font-primary-color);
background-color: rgba(64, 158, 255, 0.08);
}
&:focus-visible {
outline: 2px solid var(--vxe-ui-font-primary-color) !important;
outline-offset: -2px;
}
}
</style>

View File

@ -21,7 +21,7 @@
<a-upload <a-upload
:accept="acceptFileType" :accept="acceptFileType"
name="file" name="file"
:data="{ isup: 1 }" :data="uploadData"
:multiple="false" :multiple="false"
:action="uploadAction" :action="uploadAction"
:headers="uploadHeaders" :headers="uploadHeaders"
@ -29,7 +29,7 @@
v-bind="cellProps" v-bind="cellProps"
@change="handleChangeUpload" @change="handleChangeUpload"
> >
<a-button v-if="!hasFile" preIcon="ant-design:upload">{{ originColumn.btnText || '上传图片' }}</a-button> <a-button v-if="!hasFile" class="j-vxe-upload-trigger" size="small" preIcon="ant-design:upload">{{ originColumn.btnText || '图片' }}</a-button>
<div v-if="hasFile && imgList.length < maxCount" class="j-vxe-plus" @click=""> <div v-if="hasFile && imgList.length < maxCount" class="j-vxe-plus" @click="">
<Icon icon="ant-design:plus" /> <Icon icon="ant-design:plus" />
</div> </div>

View File

@ -8,98 +8,124 @@
:linkFields="originColumn.linkFields || []" :linkFields="originColumn.linkFields || []"
:imageField="originColumn.imageField || ''" :imageField="originColumn.imageField || ''"
:editBtnShow="!originColumn.editBtnShow" :editBtnShow="!originColumn.editBtnShow"
:queryMode="originColumn.queryMode || 'online'"
v-bind="cellProps" v-bind="cellProps"
@change="handleChange" @change="handleChange"
/> />
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types'; import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks'; import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
import { vModel } from '/@/components/jeecg/JVxeTable/utils'; import { vModel } from '/@/components/jeecg/JVxeTable/utils';
import { isEmpty } from '/@/utils/is'; import LinkTableInput from '../../../../views/super/online/cgform/extend/linkTable/LinkTableInput.vue';
import { defHttp } from '/@/utils/http/axios'; import { getLinkTableColumns, transLinkTableRecord } from '../../../../views/super/online/cgform/extend/linkTable/useLinkTable';
import { isEmpty } from '/@/utils/is';
import { defHttp } from '/@/utils/http/axios';
export default defineComponent({ export default defineComponent({
name: 'JVxeLinkTableCell', name: 'JVxeLinkTableCell',
props: useJVxeCompProps(), components: { LinkTableInput },
setup(props: JVxeComponent.Props) { props: useJVxeCompProps(),
const { innerValue, cellProps, originColumn, scrolling, handleChangeCommon, row } = useJVxeComponent(props); setup(props: JVxeComponent.Props) {
function handleChange(value, otherValues) { const { innerValue, cellProps, originColumn, scrolling, handleChangeCommon, row } = useJVxeComponent(props);
if (!isEmpty(value)) { function handleChange(value, otherValues) {
Object.keys(otherValues).forEach((key) => { if (!isEmpty(value)) {
let currentValue = otherValues[key]; Object.keys(otherValues).forEach((key) => {
vModel(currentValue, row, key); let currentValue = otherValues[key];
}); vModel(currentValue, row, key);
handleChangeCommon(value); });
} handleChangeCommon(value);
} }
return { }
innerValue, return {
cellProps, innerValue,
originColumn, cellProps,
scrolling, originColumn,
handleChange, scrolling,
row, handleChange,
}; row,
};
},
enhanced: {
aopEvents: {
}, },
enhanced: { interceptor: {
aopEvents: { 'event.clearActived.className'(className) {
if (
className.includes('jeecg-online-pop-list-modal') ||
className.includes('jeecg-online-pop-modal') ||
className.includes('jeecg-all-table-pop-list-modal')
) {
return false;
}
}, },
interceptor: { },
'event.clearActived.className'(className) { translate: {
if (className.includes('jeecg-online-pop-list-modal') || className.includes('jeecg-online-pop-modal')) { enabled: true,
return false; async handler(value, ctx) {
} if (!value) return '';
}, if (!ctx) return value;
}, const { originColumn, row } = ctx.context;
translate: { const col = originColumn.value;
enabled: true, const tableName = col.tableName;
async handler(value, ctx) { const textField = col.textField || '';
if (!value) return ''; const valueField = col.valueField || 'id';
if (!ctx) return value; //
const { originColumn, row } = ctx.context; const linkFields: string[] = col.linkFields || [];
const col = originColumn.value; if (!tableName) return value;
const tableName = col.tableName; const vals = (value + '').split(',');
const textField = col.textField || ''; const textKey = textField.split(',')[0];
const valueField = col.valueField || 'id';
// let records: any[] = [];
const linkFields: string[] = col.linkFields || []; if (col.queryMode === 'table') {
if (!tableName) return value; const extraTableFields = linkFields.map((lf) => lf.split(',')[1]).filter(Boolean);
// const fieldSet = new Set([valueField, ...textField.split(',').filter(Boolean), ...extraTableFields]);
const showFields = Array.from(fieldSet).join(',');
const data = await defHttp.get({
url: '/sys/dict/queryTableDataForLinkRecord',
params: { tableName, showFields, valueField, keyValues: value, pageNo: 1, pageSize: vals.length },
});
records = data?.records || [];
} else {
const extraTableFields = linkFields.map((lf) => lf.split(',')[1]).filter(Boolean); const extraTableFields = linkFields.map((lf) => lf.split(',')[1]).filter(Boolean);
const allSelectFields = [valueField, textField, ...extraTableFields].filter(Boolean); const allSelectFields = [valueField, textField, ...extraTableFields].filter(Boolean);
const selectFields = [...new Set(allSelectFields)].join(','); const selectFields = [...new Set(allSelectFields)].join(',');
const params = { const [data, columns] = await Promise.all([
linkTableSelectFields: selectFields, defHttp.get({url: '/online/cgform/api/getData/' + tableName,
pageSize: value.split(',').length, params: {
pageNo: 1, linkTableSelectFields: selectFields,
superQueryMatchType: 'and', pageSize: vals.length,
superQueryParams: encodeURI(JSON.stringify([{ field: valueField, rule: 'in', val: value }])), pageNo: 1,
}; superQueryMatchType: 'and',
const data = await defHttp.get({ url: '/online/cgform/api/getData/' + tableName, params }); superQueryParams: encodeURI(JSON.stringify([{ field: valueField, rule: 'in', val: value }])),
const records = data?.records || []; },
const textKey = textField.split(',')[0]; }),
const vals = (value + '').split(','); getLinkTableColumns(tableName, selectFields),
const labels = vals.map((v) => { ]);
const record = records.find((r) => String(r[valueField]) === String(v)); records = data?.records || [];
return record ? record[textKey] : v; records.forEach((record) => transLinkTableRecord(record, columns?.columns, columns?.dictOptions));
}); }
// 使
if (linkFields.length > 0 && row) { const labels = vals.map((v) => {
linkFields.forEach((lf) => { const record = records.find((r) => String(r[valueField]) === String(v));
const [formField, tableField] = lf.split(','); return record ? record[textKey] : v;
const fieldValues = vals.map((v) => { });
const record = records.find((r) => String(r[valueField]) === String(v)); // 使
return record ? (record[tableField] ?? '') : ''; if (linkFields.length > 0 && row) {
}); linkFields.forEach((lf) => {
vModel(fieldValues.join(','), row, formField); 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(','); });
}, }
return labels.join(',');
}, },
} as JVxeComponent.EnhancedPartial, },
}); } as JVxeComponent.EnhancedPartial,
});
</script> </script>

View File

@ -0,0 +1,138 @@
<template>
<JPopupDict v-bind="popupProps" @change="onChange" @options-change="onOptionsChange" />
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import JPopupDict from '/@/components/Form/src/jeecg/components/JPopupDict.vue';
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
import { dispatchEvent } from '/@/components/jeecg/JVxeTable/utils';
import { defHttp } from '/@/utils/http/axios';
// dictCode"code,labelField,valueField"
function parseDictCode(dictCode: string | undefined) {
const arr = (dictCode || '').split(',');
return { code: arr[0] || '', labelField: arr[1] || '', valueField: arr[2] || '' };
}
let resultOptions = [];
const CG_RP_CONFIG_ID_SESSION_PREFIX = 'jeecg:popupDict:cgRpConfigId:';
const cgRpConfigIdCache = new Map<string, string>();
function safeSessionGet(key: string) {
return sessionStorage.getItem(key) || '';
}
function safeSessionSet(key: string, value: string) {
sessionStorage.setItem(key, value);
}
// cgRpConfigId +
async function getCgRpConfigId(code: string) {
if (!code) return '';
const sessionKey = `${CG_RP_CONFIG_ID_SESSION_PREFIX}${code}`;
const cached = cgRpConfigIdCache.get(code) || safeSessionGet(sessionKey);
if (cached) {
if (!cgRpConfigIdCache.has(code)) cgRpConfigIdCache.set(code, cached);
return cached;
}
const res = await defHttp.get({ url: `/online/cgreport/api/getRpColumns/${code}` }, { isTransformResponse: false, successMessageMode: 'none' });
const id = (res.success && res.result?.cgRpConfigId ? String(res.result.cgRpConfigId) : '') || '';
if (id) {
cgRpConfigIdCache.set(code, id);
safeSessionSet(sessionKey, id);
}
return id;
}
export default defineComponent({
name: 'JVxePopupDictCell',
components: { JPopupDict },
props: useJVxeCompProps(),
setup(props: JVxeComponent.Props) {
const { innerValue, originColumn, cellProps, handleChangeCommon } = useJVxeComponent(props);
// fieldConfigcgreportcode,(label),(value)
const computedFieldConfig = computed(() => {
const col = originColumn.value;
return (col as any).fieldConfig || '';
});
const popupProps = computed(() => ({
...cellProps.value,
value: innerValue.value ?? '',
dictCode: computedFieldConfig.value,
multi: originColumn.value.multi ?? originColumn.value.popupMulti ?? false,
param: originColumn.value.params ?? originColumn.value.param ?? {},
sorter: originColumn.value.sorter,
useInput: true,
}));
function onChange(val) {
setTimeout(() => {
handleChangeCommon(val ?? '');
}, 0);
}
function onOptionsChange(options) {
resultOptions = options;
}
return { popupProps, onChange, onOptionsChange };
},
// value(username) label(realname)
enhanced: {
switches: { editRender: true, visible: false },
aopEvents: {
editActived({ $event }) {
dispatchEvent({
$event,
props: this.props,
className: '.ant-select',
isClick: true,
});
},
},
translate: {
enabled: true,
async handler(value, ctx) {
if (value == null || value === '') return '';
const col = ctx?.context.originColumn.value;
const fieldConfig = (col as any)?.fieldConfig || '';
const { code, labelField, valueField } = parseDictCode(fieldConfig);
if (!code || !labelField || !valueField) return value;
if (resultOptions?.length) {
// options
const result = resultOptions.map((item: any) => item.text);
resultOptions = [];
return result.join(',');
}
const ids = String(value).split(',');
try {
const cgRpConfigId = await getCgRpConfigId(code);
if (!cgRpConfigId) return value;
const params = {
[`force_${valueField}`]: value,
};
const res = await defHttp.get(
{ url: `/online/cgreport/api/getData/${cgRpConfigId}`, params },
{ isTransformResponse: false, successMessageMode: 'none' }
);
const records = res?.result?.records;
if (records?.length) {
const labels = ids.map((id) => {
const found = records.find((r) => String(r[valueField]) === id);
return found?.[labelField] ?? id;
});
return labels.join(',');
}
} catch {}
return value;
},
},
} as JVxeComponent.EnhancedPartial,
});
</script>
<style lang="less" scoped>
.JPopupDict {
:deep(.ant-select) {
width: 100%;
}
}
</style>

View File

@ -7,6 +7,7 @@ import { JVxeComponent } from '/@/components/jeecg/JVxeTable/src/types/JVxeCompo
import { Icon } from '/@/components/Icon'; import { Icon } from '/@/components/Icon';
import { Dropdown } from 'ant-design-vue'; import { Dropdown } from 'ant-design-vue';
import { LoadingOutlined } from '@ant-design/icons-vue'; import { LoadingOutlined } from '@ant-design/icons-vue';
import { split } from '/@/utils';
export function useFileCell(props, fileType: UploadTypeEnum, options?) { export function useFileCell(props, fileType: UploadTypeEnum, options?) {
const setup = useJVxeUploadCell(props, { token: true, action: uploadUrl, ...options }); const setup = useJVxeUploadCell(props, { token: true, action: uploadUrl, ...options });
@ -15,14 +16,14 @@ export function useFileCell(props, fileType: UploadTypeEnum, options?) {
const [registerModel, { openModal }] = useModal(); const [registerModel, { openModal }] = useModal();
// //
const ellipsisFileName = computed(() => { const shortFileName = computed(() => {
let length = 5; const length = 5;
let file = innerFile.value; const file = innerFile.value;
if (!file || !file.name) { if (!file || !file.name) {
return ''; return '';
} }
if (file.name.length > length) { if (file.name.length > length) {
return file.name.substr(0, length) + '…'; return file.name.substr(0, length);
} }
return file.name; return file.name;
}); });
@ -38,14 +39,19 @@ export function useFileCell(props, fileType: UploadTypeEnum, options?) {
return ''; return '';
}); });
const uploadCount = computed(() => {
const value = modalValue.value;
return value ? split(value).filter(Boolean).length : 0;
});
const maxCount = computed(() => { const maxCount = computed(() => {
let maxCount = originColumn.value.maxCount; let maxCount = originColumn.value.maxCount;
// online JSON // online JSON
if (originColumn.value && originColumn.value.fieldExtendJson) { if (originColumn.value && originColumn.value.fieldExtendJson) {
let json = JSON.parse(originColumn.value.fieldExtendJson); const json = JSON.parse(originColumn.value.fieldExtendJson);
maxCount = json.uploadnum ? json.uploadnum : 0; maxCount = json.uploadnum ? json.uploadnum : 0;
} }
return maxCount ?? 0; return Number(maxCount ?? 0);
}); });
// //
@ -56,19 +62,26 @@ export function useFileCell(props, fileType: UploadTypeEnum, options?) {
download: true, download: true,
...originColumn.value.props, ...originColumn.value.props,
maxCount: maxCount.value, maxCount: maxCount.value,
multiple: originColumn.value.props?.multiple ?? maxCount.value !== 1,
fileType: fileType, fileType: fileType,
action: originColumn.value.action ?? void 0, // update-begin--author:liaozhiyang---date:20250526---forissues/9657jvxetable
action: originColumn.value.action ?? setup.uploadAction,
// update-end--author:liaozhiyang---date:20250526---forissues/9657jvxetable
// update-begin--author:liaozhiyang---date:20250526---forissues/9652vxetable
bizPath: originColumn.value.bizPath ?? 'temp',
// update-end--author:liaozhiyang---date:20250526---forissues/9652vxetable
}); });
} }
// //
function onModalChange(path) { function onModalChange(path) {
if (path) { if (path) {
// : TV360X-235
if (innerFile.value === null) { if (innerFile.value === null) {
innerFile.value = {}; //
innerFile.value = fileSetValue(path);
} else {
innerFile.value.path = path;
} }
innerFile.value.path = path;
handleChangeCommon(innerFile.value); handleChangeCommon(innerFile.value);
} else { } else {
// : [issues/530]JVxeTable JVxeTypes.image // : [issues/530]JVxeTable JVxeTypes.image
@ -79,8 +92,9 @@ export function useFileCell(props, fileType: UploadTypeEnum, options?) {
return { return {
...setup, ...setup,
modalValue, modalValue,
uploadCount,
maxCount, maxCount,
ellipsisFileName, shortFileName,
registerModel, registerModel,
onModalChange, onModalChange,
handleMoreOperation, handleMoreOperation,

View File

@ -11,6 +11,7 @@
import 'vditor/dist/index.css'; import 'vditor/dist/index.css';
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { ThemeEnum } from '/@/enums/appEnum'; import { ThemeEnum } from '/@/enums/appEnum';
import { sanitizeRichText } from '/@/utils/htmlSanitizer';
const converter = new showdown.Converter(); const converter = new showdown.Converter();
converter.setOption('tables', true); converter.setOption('tables', true);
@ -19,7 +20,7 @@
value: { type: String }, value: { type: String },
class: { type: String }, class: { type: String },
}); });
const getHtmlData = computed(() => converter.makeHtml(props.value || '')); const getHtmlData = computed(() => sanitizeRichText(converter.makeHtml(props.value || '')));
// : issues/918MarkdownViewer // : issues/918MarkdownViewer
const isDarkTheme = ref(false); const isDarkTheme = ref(false);

View File

@ -29,7 +29,7 @@
:maxHeight="getProps.maxHeight" :maxHeight="getProps.maxHeight"
:height="getWrapperHeight" :height="getWrapperHeight"
:visible="visibleRef" :visible="visibleRef"
:modalHeaderHeight="getProps.modalHeaderHeight" :modalHeaderHeight="isNoTitle ? 0 : getProps.modalHeaderHeight"
:modalFooterHeight="footer !== undefined && !footer ? 0 : getProps.modalFooterHeight" :modalFooterHeight="footer !== undefined && !footer ? 0 : getProps.modalFooterHeight"
v-bind="omit(getProps.wrapperProps, 'visible', 'height', 'modalFooterHeight')" v-bind="omit(getProps.wrapperProps, 'visible', 'height', 'modalFooterHeight')"
@ext-height="handleExtHeight" @ext-height="handleExtHeight"
@ -266,6 +266,13 @@
emit('fullScreen',val); emit('fullScreen',val);
}); });
// update-begin--author:liaozhiyang---date:20260805---for:LHZP-865basicModal header
watch(isNoTitle, () => {
nextTick(() => {
modalMethods.redoModalHeight();
});
});
// update-end--author:liaozhiyang---date:20260805---for:LHZP-865basicModal header
return { return {
handleCancel, handleCancel,
getBindValue, getBindValue,

View File

@ -38,6 +38,7 @@
const spinRef = ref<ElRef>(null); const spinRef = ref<ElRef>(null);
const realHeightRef = ref(0); const realHeightRef = ref(0);
const minRealHeightRef = ref(0); const minRealHeightRef = ref(0);
const availableHeightRef = ref(0);
let realHeight = 0; let realHeight = 0;
@ -116,7 +117,7 @@
return { return {
minHeight: `${props.minHeight === null ? defaultMiniHeight : props.minHeight}px`, minHeight: `${props.minHeight === null ? defaultMiniHeight : props.minHeight}px`,
// : QQYUN-7641basicModalMaxHeight // : QQYUN-7641basicModalMaxHeight
maxHeight: `${props.maxHeight ? props.maxHeight : unref(realHeightRef)}px`, maxHeight: `${props.maxHeight ? Math.min(props.maxHeight, unref(availableHeightRef) || props.maxHeight) : unref(realHeightRef)}px`,
}; };
} }
} }
@ -172,21 +173,24 @@
await nextTick(); await nextTick();
try { try {
const modalDom = bodyDom.parentElement && bodyDom.parentElement.parentElement; const modalDom = bodyDom.closest('.ant-modal');
if (!modalDom) return; if (!modalDom) return;
//update-begin---author:scott ---date:20260814 for:-----------
const spinEl = unref(spinRef);
if (!spinEl) return;
const modalRect = getComputedStyle(modalDom as Element).top; // 使 CSS
const modalTop = Number.parseInt(modalRect); const modalTop = Number.parseFloat(getComputedStyle(modalDom).top) || 0;
let maxHeight = window.innerHeight - modalTop * 2 + (props.footerOffset! || 0) - props.modalFooterHeight - props.modalHeaderHeight; const modalExtHeight = (modalDom as HTMLElement).offsetHeight - spinEl.clientHeight;
let maxHeight = window.innerHeight - modalTop + (props.footerOffset! || 0) - modalExtHeight;
//update-end---author:scott ---date:20260814 for:-----------
// //
if (modalTop < 40) { if (modalTop < 40) {
maxHeight -= 26; maxHeight -= 26;
} }
availableHeightRef.value = maxHeight;
await nextTick(); await nextTick();
const spinEl = unref(spinRef);
if (!spinEl) return;
await nextTick(); await nextTick();
// if (!realHeight) { // if (!realHeight) {
realHeight = spinEl.scrollHeight; realHeight = spinEl.scrollHeight;

View File

@ -145,7 +145,7 @@
width: 0; width: 0;
height: 0; height: 0;
cursor: pointer; cursor: pointer;
background-color: rgba(144, 147, 153, 0.3); background-color: rgba(144, 147, 153, 0.6);
border-radius: inherit; border-radius: inherit;
transition: 0.3s background-color; transition: 0.3s background-color;

View File

@ -18,7 +18,9 @@
<script lang="ts"> <script lang="ts">
import type { RawEditorOptions } from 'tinymce'; import type { RawEditorOptions } from 'tinymce';
import tinymce from 'tinymce/tinymce'; // update-begin--author:copilot---date:20260711---for: Rolldown CJS tinymce
import tinymce from '/@/utils/tinymceGlobalShim';
// update-end--author:copilot---date:20260711---for: Rolldown CJS tinymce
import Editor from '@tinymce/tinymce-vue' import Editor from '@tinymce/tinymce-vue'
import 'tinymce/themes/silver'; import 'tinymce/themes/silver';
import 'tinymce/icons/default/icons'; import 'tinymce/icons/default/icons';
@ -31,6 +33,7 @@
import 'tinymce/plugins/lists'; import 'tinymce/plugins/lists';
import 'tinymce/plugins/preview'; import 'tinymce/plugins/preview';
import 'tinymce/plugins/image'; import 'tinymce/plugins/image';
import 'tinymce/plugins/media';
import { defineComponent, computed, nextTick, ref, unref, watch, onDeactivated, onBeforeUnmount, onMounted } from 'vue'; import { defineComponent, computed, nextTick, ref, unref, watch, onDeactivated, onBeforeUnmount, onMounted } from 'vue';
import ImgUpload from './ImgUpload.vue'; import ImgUpload from './ImgUpload.vue';
import ProcessMask from './ProcessMask.vue'; import ProcessMask from './ProcessMask.vue';
@ -205,6 +208,38 @@
}; };
uploadFile(params, uploadSuccess); uploadFile(params, uploadSuccess);
}), }),
// update-begin--author:liaozhiyang---date:20260617---for:JHHB-1433
media_live_embeds: true,
media_alt_source: false,
media_poster: false,
media_dimensions: true,
file_picker_types: 'media',
file_picker_callback: (callback, value, meta) => {
if (meta.filetype !== 'media') return;
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'video/*');
input.onchange = () => {
const file = (input.files && input.files[0]) as File | undefined;
if (!file) return;
const params = {
file,
filename: file.name,
data: { biz: 'jeditor', jeditor: '1' },
};
const uploadSuccess = (res: any) => {
if (res && res.success) {
const videoUrl = res.message === 'local'
? URL.createObjectURL(file)
: getFileAccessHttpUrl(res.message);
callback(videoUrl, { title: file.name });
}
};
uploadFile(params, uploadSuccess);
};
input.click();
},
// update-end--author:liaozhiyang---date:20260617---for:JHHB-1433
content_css: publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css', content_css: publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css',
...options, ...options,
setup: (editor: any) => { setup: (editor: any) => {

View File

@ -4,16 +4,16 @@
// colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration // colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration
export const plugins = [ export const plugins = [
'advlist anchor autolink autosave code codesample directionality fullscreen hr insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace tabfocus template textpattern visualblocks visualchars wordcount image', 'advlist anchor autolink autosave code codesample directionality fullscreen hr insertdatetime link lists media nonbreaking noneditable pagebreak preview print save searchreplace tabfocus template textpattern visualblocks visualchars wordcount image',
]; ];
export const toolbar = export const toolbar =
'fullscreen code preview | undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent lineheight|subscript superscript blockquote| numlist bullist checklist | forecolor backcolor casechange permanentpen formatpainter removeformat | pagebreak | charmap emoticons | insertfile image media pageembed link anchor codesample insertdatetime hr| a11ycheck ltr rtl'; 'fullscreen code preview | undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent lineheight|subscript superscript blockquote| numlist bullist checklist | forecolor backcolor casechange permanentpen formatpainter removeformat | pagebreak | charmap emoticons | insertfile image media pageembed link anchor codesample insertdatetime hr| a11ycheck ltr rtl';
export const simplePlugins = 'lists image link fullscreen paste'; export const simplePlugins = 'lists image link fullscreen media';
export const simpleToolbar = [ export const simpleToolbar = [
'undo redo styles forecolor fontsize bold italic alignleft aligncenter alignright alignjustify bullist numlist outdent indent lists image link fullscreen', 'undo redo styles forecolor fontsize bold italic alignleft aligncenter alignright alignjustify bullist numlist outdent indent lists image media link fullscreen',
]; ];
export const menubar = 'file edit insert view format table'; export const menubar = 'file edit insert view format table';

View File

@ -168,7 +168,7 @@
} }
try { try {
item.status = UploadResultStatus.UPLOADING; item.status = UploadResultStatus.UPLOADING;
const { data } = await props.api?.( const res = await props.api?.(
{ {
data: { data: {
...(props.uploadParams || {}), ...(props.uploadParams || {}),
@ -182,8 +182,16 @@
item.percent = complete; item.percent = complete;
} }
); );
// update-begin--author:liaozhiyang---date:20260804---forLHZP-1385
const data = res?.data ?? res;
const isSuccess = data && (data.success === true || data.code === 0);
const url = data?.url || data?.result || data?.message;
if (!isSuccess || !url) {
throw new Error(data?.message || 'upload failed');
}
item.status = UploadResultStatus.SUCCESS; item.status = UploadResultStatus.SUCCESS;
item.responseData = data; item.responseData = { ...data, url };
// update-end--author:liaozhiyang---date:20260804---forLHZP-1385
return { return {
success: true, success: true,
error: null, error: null,

View File

@ -434,6 +434,7 @@
async function queryUserList(params) { async function queryUserList(params) {
params['column'] = 'sort'; params['column'] = 'sort';
params['order'] = 'ASC'; params['order'] = 'ASC';
params['status'] = '1';
if(departInfo.selectedKeys.length == 0 && positionInfo.selectedKeys.length == 0 && userGroupInfo.selectedKeys.length == 0 && params.realname){ if(departInfo.selectedKeys.length == 0 && positionInfo.selectedKeys.length == 0 && userGroupInfo.selectedKeys.length == 0 && params.realname){
params['realname'] = `*${params.realname.trim()}*`; params['realname'] = `*${params.realname.trim()}*`;
} }

View File

@ -8,7 +8,7 @@
:treeData="treeData" :treeData="treeData"
:checkStrictly="true" :checkStrictly="true"
@select="onSelectDepart" @select="onSelectDepart"
style="height: calc(100vh - 390px); min-height: 150px; overflow: auto" class="post-rank-tree"
></BasicTree> ></BasicTree>
</template> </template>
<a-empty v-else description="无岗位信息" /> <a-empty v-else description="无岗位信息" />
@ -73,7 +73,38 @@
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.depart-rule-tree :deep(.scrollbar__bar) { // update-begin--author:liaozhiyang---date:20260811---forLHZP-1694
pointer-events: none; .post-rank-tree {
width: 100%;
min-width: 0;
height: calc(100vh - 390px);
min-height: 150px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-gutter: stable;
} }
.post-rank-tree :deep(.scrollbar__bar) {
pointer-events: auto;
}
.post-rank-tree :deep(.scrollbar__view),
.post-rank-tree :deep(.ant-tree),
.post-rank-tree :deep(.ant-tree-list-holder-inner) {
width: 100%;
min-width: 0;
}
.post-rank-tree :deep(.ant-tree-treenode) {
box-sizing: border-box;
width: 100%;
min-width: 0;
overflow: hidden;
}
.post-rank-tree :deep(.ant-tree-node-content-wrapper) {
min-width: 0;
overflow: hidden;
}
// update-end--author:liaozhiyang---date:20260811---forLHZP-1694
</style> </style>

View File

@ -20,7 +20,7 @@ export const getDepartTreeData = (params?) => defHttp.get({ url: Api.departList,
/** /**
* 获取用户列表 * 获取用户列表
*/ */
export const getUserList = (params?) => defHttp.get({ url: Api.userList, params }); export const getUserList = (params?) => defHttp.get({ url: Api.userList, params: { ...params, status: '1' } });
/** /**
* 获取指定部门用户列表 * 获取指定部门用户列表

View File

@ -8,6 +8,7 @@
<a-button v-if="addBtnCfg.enabled && showAdd" type="primary" :preIcon="addBtnCfg.buttonIcon" :disabled="disabled" :loading="deleting" @click="trigger('add')"> <a-button v-if="addBtnCfg.enabled && showAdd" type="primary" :preIcon="addBtnCfg.buttonIcon" :disabled="disabled" :loading="deleting" @click="trigger('add')">
<span>{{ addBtnCfg.buttonName }}</span> <span>{{ addBtnCfg.buttonName }}</span>
</a-button> </a-button>
<slot name="toolbarAfterAdd" :size="btnSize" :selectedRowIds="selectedRowIds" />
<a-button v-if="showSave" preIcon="ant-design:save-outlined" :disabled="disabled" @click="trigger('save')"> <a-button v-if="showSave" preIcon="ant-design:save-outlined" :disabled="disabled" @click="trigger('save')">
<span>保存</span> <span>保存</span>
</a-button> </a-button>
@ -21,7 +22,7 @@
<a-button preIcon="ant-design:delete-outlined" @click="trigger('clearSelection')">清空选择</a-button> <a-button preIcon="ant-design:delete-outlined" @click="trigger('clearSelection')">清空选择</a-button>
</template> </template>
</template> </template>
<slot v-if="showSuffix" name="toolbarSuffix" :size="btnSize" /> <slot v-if="showSuffix" name="toolbarSuffix" :size="btnSize" :selectedRowIds="selectedRowIds" />
<a v-if="showCollapse" style="margin-left: 4px" @click="toggleCollapse"> <a v-if="showCollapse" style="margin-left: 4px" @click="toggleCollapse">
<span>{{ collapsed ? '展开' : '收起' }}</span> <span>{{ collapsed ? '展开' : '收起' }}</span>
<Icon :icon="collapsed ? 'ant-design:down-outlined' : 'ant-design:up-outlined'" /> <Icon :icon="collapsed ? 'ant-design:down-outlined' : 'ant-design:up-outlined'" />

View File

@ -54,7 +54,10 @@
function handleChange(_mom, dateStr) { function handleChange(_mom, dateStr) {
// : QQYUN-9205(jVxetabledate) // : QQYUN-9205(jVxetabledate)
if (picker.value) { if (picker.value) {
handleChangeCommon(_mom); //update-begin-author:liusq---date:2026-08-06--for: LHZP-1052 jvxe
const dateValue = _mom && dayjs(_mom).isValid() ? dayjs(_mom).format(dateFormat.value) : null;
handleChangeCommon(dateValue);
//update-end-author:liusq---date:2026-08-06--for: LHZP-1052 jvxe
} else { } else {
handleChangeCommon(dateStr); handleChangeCommon(dateStr);
} }

View File

@ -4,10 +4,29 @@
<LoadingOutlined /> <LoadingOutlined />
<span>&nbsp;加载中</span> <span>&nbsp;加载中</span>
</template> </template>
<template v-for="option of selectOptions" :key="option.value"> <!-- 分组模式options 中含有 group 字段时按分组渲染 -->
<a-select-option :value="option.value" :title="option.text || option.label || option.title" :disabled="option.disabled"> <template v-if="hasGroup">
<span>{{ option.text || option.label || option.title || option.value }}</span> <template v-for="(groupOptions, groupName) of groupedSelectOptions" :key="groupName">
</a-select-option> <a-select-opt-group :label="groupName">
<a-select-option
v-for="option of groupOptions"
:key="option.value"
:value="option.value"
:title="option.text || option.label || option.title"
:disabled="option.disabled"
>
<span>{{ option.text || option.label || option.title || option.value }}</span>
</a-select-option>
</a-select-opt-group>
</template>
</template>
<!-- 非分组模式原有扁平渲染 -->
<template v-else>
<template v-for="option of selectOptions" :key="option.value">
<a-select-option :value="option.value" :title="option.text || option.label || option.title" :disabled="option.disabled">
<span>{{ option.text || option.label || option.title || option.value }}</span>
</a-select-option>
</template>
</template> </template>
</a-select> </a-select>
</template> </template>
@ -79,6 +98,24 @@
return originColumn.value.options; return originColumn.value.options;
}); });
//
const hasGroup = computed(() => {
const opts = selectOptions.value;
return opts && opts.length > 0 && opts.some((o) => o.group);
});
// group
const groupedSelectOptions = computed(() => {
const opts = selectOptions.value || [];
const groups: Record<string, any[]> = {};
for (const opt of opts) {
const g = opt.group || '';
if (!groups[g]) groups[g] = [];
groups[g].push(opt);
}
return groups;
});
// --------- created --------- // --------- created ---------
// type // type
@ -169,6 +206,8 @@
innerValue, innerValue,
selectProps, selectProps,
selectOptions, selectOptions,
hasGroup,
groupedSelectOptions,
handleChange, handleChange,
handleBlur, handleBlur,
}; };

View File

@ -37,7 +37,7 @@
v-if="!cellProps.disabledTable" v-if="!cellProps.disabledTable"
v-show="!hasFile" v-show="!hasFile"
name="file" name="file"
:data="{ isup: 1 }" :data="uploadData"
:multiple="false" :multiple="false"
:action="originColumn.action" :action="originColumn.action"
:headers="uploadHeaders" :headers="uploadHeaders"

View File

@ -37,7 +37,16 @@ export function useJVxeUploadCell(props: JVxeComponent.Props, options?) {
}); });
const hasFile = computed(() => innerFile.value != null); const hasFile = computed(() => innerFile.value != null);
const responseName = computed(() => originColumn.value.responseName ?? 'message'); const responseName = computed(() => originColumn.value.responseName ?? 'message');
// update-begin--author:liaozhiyang---date:20250526---forissues/9652vxetable
const uploadData = computed(() => {
const bizPath = originColumn.value.bizPath;
const result: any = { isup: 1 };
if (bizPath) {
result.biz = bizPath;
}
return result;
});
// update-end--author:liaozhiyang---date:20250526---forissues/9652vxetable
watch( watch(
innerValue, innerValue,
(val) => { (val) => {
@ -110,6 +119,7 @@ export function useJVxeUploadCell(props: JVxeComponent.Props, options?) {
innerFile, innerFile,
uploadAction, uploadAction,
uploadHeaders, uploadHeaders,
uploadData,
hasFile, hasFile,
responseName, responseName,
handleChangeUpload, handleChangeUpload,

View File

@ -141,7 +141,9 @@ function customComponentAddStar(columns) {
].includes(type) ].includes(type)
) { ) {
if (validateRules.find((item) => item.required)) { if (validateRules.find((item) => item.required)) {
column.title = ` * ${column.title}`; // update-begin--author:liaozhiyang---date:20260807---for:LHZP-703
column.headerClassName = 'jvxe-col--required';
// update-end--author:liaozhiyang---date:20260807---for:LHZP-703
} }
} }
} }

View File

@ -186,6 +186,28 @@ export function useJVxeComponent(props: JVxeComponent.Props) {
{ immediate: true } { immediate: true }
); );
// update-begin--author:liaozhiyang---date:20250526---forQQYUN-15511optionsjslabel
// options options
watch(
() => originColumn.value?.options,
() => {
if (props.renderType === JVxeRenderType.spaner && enhanced.translate.enabled === true) {
if (isFunction(enhanced.translate.handler)) {
const newValue = value.value;
if (newValue === undefined || newValue === null) return;
let res = enhanced.translate.handler(newValue, ctx);
if (isPromise(res)) {
res.then((v) => (innerValue.value = v));
} else {
innerValue.value = res;
}
}
}
},
{ deep: true }
);
// update-end--author:liaozhiyang---date:20250526---forQQYUN-15511optionsjslabel
/** 通用处理 change 事件 */ /** 通用处理 change 事件 */
function handleChangeCommon($value, force = false) { function handleChangeCommon($value, force = false) {
const newValue = enhanced.getValue($value, ctx); const newValue = enhanced.getValue($value, ctx);

View File

@ -61,6 +61,7 @@ export function useToolbar(props: JVxeTableProps, data: JVxeDataProps, methods:
}, },
{ {
toolbarPrefix: $slots.toolbarPrefix, toolbarPrefix: $slots.toolbarPrefix,
toolbarAfterAdd: $slots.toolbarAfterAdd,
toolbarSuffix: $slots.toolbarSuffix, toolbarSuffix: $slots.toolbarSuffix,
} }
); );

View File

@ -22,12 +22,18 @@ export function useValidateRules(args: HandleArgs) {
delete rule.pattern; delete rule.pattern;
} else { } else {
// Online // Online
let reg: any = rule.pattern;
for (let foo of fooPatterns) { for (let foo of fooPatterns) {
if (foo.value === rule.pattern) { if (foo.value === rule.pattern) {
rule.pattern = foo.pattern; reg = foo.pattern;
break; break;
} }
} }
// update-begin--author:liaozhiyang---date:20260714---forLHZP-172patternrequired
const regExp = reg instanceof RegExp ? reg : new RegExp(reg);
delete rule.pattern;
rule.validator = emptySkipPatternValidator(regExp);
// update-end--author:liaozhiyang---date:20260714---forLHZP-172patternrequired
} }
} else if (typeof rule.handler === 'function') { } else if (typeof rule.handler === 'function') {
// //
@ -78,6 +84,19 @@ function handlerConvertToValidator(event) {
}); });
} }
// update-begin--author:liaozhiyang---date:20260714---forLHZP-172patternrequired
/** pattern 校验器:空值不校验(非空交给 required 规则),仅当有值时做正则校验 */
function emptySkipPatternValidator(regExp: RegExp) {
return function (event) {
const { cellValue, rule } = event;
if (cellValue === '' || cellValue === null || cellValue === undefined) {
return Promise.resolve();
}
return regExp.test(String(cellValue)) ? Promise.resolve() : Promise.reject(new Error(rule.message));
};
}
// update-end--author:liaozhiyang---date:20260714---forLHZP-172patternrequired
// online // online
const fooPatterns = [ const fooPatterns = [
{ title: '非空', value: '*', pattern: /^.+$/ }, { title: '非空', value: '*', pattern: /^.+$/ },

View File

@ -70,7 +70,14 @@ function preventClosingPopUp(this: any, params) {
return false; return false;
} }
// JPopup // JPopup
if (className.includes('j-popup-modal') || className.includes('j-depart-select-modal') || className.includes('j-user-select-modal')) { if (
className.includes('j-popup-modal') ||
className.includes('j-depart-select-modal') ||
className.includes('j-user-select-modal') ||
className.includes('jeecg-online-pop-list-modal') ||
className.includes('jeecg-online-pop-modal') ||
className.includes('jeecg-all-table-pop-list-modal')
) {
return false; return false;
} }
// //

View File

@ -7,6 +7,13 @@
border-color: #606266; border-color: #606266;
} }
// 图片、附件上传按钮在行内保持紧凑,避免抢占表格内容的视觉层级
.j-vxe-upload-trigger.ant-btn {
height: 28px;
padding: 0 10px;
box-shadow: none;
}
.sort--active { .sort--active {
border-color: @primary-color; border-color: @primary-color;
} }
@ -111,5 +118,17 @@
.vxe-header--column.col--ellipsis>.vxe-cell .vxe-cell--title{ .vxe-header--column.col--ellipsis>.vxe-cell .vxe-cell--title{
font-size: 13px; font-size: 13px;
} }
// update-begin--author:liaozhiyang---date:20260807---for:【LHZP-703】用户、部门、图片、文件、开关的必填标识显示非红色
// 不可编辑组件cellRender必填标识样式与 vxe-table 原生 .vxe-cell--required-icon 保持一致
.vxe-header--column.jvxe-col--required > .vxe-cell .vxe-cell--title::before {
content: '*';
display: inline-block;
color: var(--vxe-ui-table-validate-error-color, #f5222d);
width: 0.8em;
margin-right: 2px;
font-family: Verdana, Arial, Tahoma;
vertical-align: middle;
}
// update-end--author:liaozhiyang---date:20260807---for:【LHZP-703】用户、部门、图片、文件、开关的必填标识显示非红色
} }

View File

@ -40,6 +40,9 @@ export enum JVxeTypes {
// //
popup = 'popup', popup = 'popup',
// update-begin--author:liaozhiyang---date:20260507---forQQYUN-7961vxetablepopup
popupDict = 'popupDict',
// update-end--author:liaozhiyang---date:20260507---forQQYUN-7961vxetablepopup
selectDictSearch = 'selectDictSearch', selectDictSearch = 'selectDictSearch',
radio = 'radio', radio = 'radio',
image = 'image', image = 'image',

View File

@ -9,6 +9,7 @@
valueFormat="YYYY-MM-DD" valueFormat="YYYY-MM-DD"
:placeholder="'请选择' + item.label" :placeholder="'请选择' + item.label"
v-model:value="queryParam[item.field]" v-model:value="queryParam[item.field]"
style="width: 100%"
></a-date-picker> ></a-date-picker>
</template> </template>
<template v-else> <template v-else>
@ -40,6 +41,7 @@
:show-time="true" :show-time="true"
valueFormat="YYYY-MM-DD HH:mm:ss" valueFormat="YYYY-MM-DD HH:mm:ss"
v-model:value="queryParam[item.field]" v-model:value="queryParam[item.field]"
style="width: 100%"
></a-date-picker> ></a-date-picker>
</template> </template>
<template v-else> <template v-else>
@ -66,7 +68,7 @@
<span :title="item.label" class="label-text">{{ item.label }}</span> <span :title="item.label" class="label-text">{{ item.label }}</span>
</template> </template>
<template v-if="single_mode === item.mode"> <template v-if="single_mode === item.mode">
<a-date-picker :placeholder="'请选择' + item.label" mode="time" valueFormat="HH:mm:ss" v-model:value="queryParam[item.field]"></a-date-picker> <a-date-picker :placeholder="'请选择' + item.label" mode="time" valueFormat="HH:mm:ss" v-model:value="queryParam[item.field]" style="width: 100%"></a-date-picker>
</template> </template>
<template v-else> <template v-else>
<a-date-picker <a-date-picker
@ -115,6 +117,7 @@
:pidField="item.pidField" :pidField="item.pidField"
:pidValue="item.pidValue" :pidValue="item.pidValue"
:hasChildField="item.hasChildField" :hasChildField="item.hasChildField"
:converIsLeafVal="item.converIsLeafVal"
load-triggle-change load-triggle-change
> >
</JTreeSelect> </JTreeSelect>
@ -131,7 +134,12 @@
<template #label> <template #label>
<span :title="item.label" class="label-text">{{ item.label }}</span> <span :title="item.label" class="label-text">{{ item.label }}</span>
</template> </template>
<JOnlineSearchSelect v-model:value="queryParam[item.field]" :placeholder="'请选择'+item.label" :fieldId="item.fieldId"/> <JOnlineSearchSelect
v-model:value="queryParam[item.field]"
:placeholder="'请选择' + item.label"
:fieldId="item.fieldId"
:reportId="item.reportId"
/>
</a-form-item> </a-form-item>
<a-form-item v-else-if="item.view === CompTypeEnum.SelUser" :labelCol="labelCol" :class="'jeecg-online-search'"> <a-form-item v-else-if="item.view === CompTypeEnum.SelUser" :labelCol="labelCol" :class="'jeecg-online-search'">
@ -324,4 +332,10 @@
text-overflow: ellipsis; text-overflow: ellipsis;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
/* update-begin--author:liaozhiyang---date:20260804---for【LHZP-983】查询日期/日期时间组件宽度撑满 */
.jeecg-online-search :deep(.ant-picker) {
width: 100%;
}
/* update-end--author:liaozhiyang---date:20260804---for【LHZP-983】查询日期/日期时间组件宽度撑满 */
</style> </style>

View File

@ -255,9 +255,17 @@ export function usePopBiz(ob, tableRef?) {
if (query) { if (query) {
url = url + query url = url + query
} }
// update-begin--author:liaozhiyang---date:20260804---forLHZP-1296
//
if (props.routeQuery && Object.keys(props.routeQuery).length > 0) {
queryParam.value = Object.assign({}, queryParam.value, props.routeQuery);
}
let params = getQueryParams();
params['onlRepUrlParamStr'] = getUrlParamString();
// update-end--author:liaozhiyang---date:20260804---forLHZP-1296
//key //key
let groupIdKey = props.groupId ? `${props.groupId}${url}` : ''; let groupIdKey = props.groupId ? `${props.groupId}${url}${JSON.stringify(params)}` : '';
httpGroupRequest(() => defHttp.get({ url }, { isTransformResponse: false, successMessageMode: 'none' }), groupIdKey).then((res) => { httpGroupRequest(() => defHttp.get({ url, params }, { isTransformResponse: false, successMessageMode: 'none' }), groupIdKey).then((res) => {
if (res.success) { if (res.success) {
initDictOptionData(res.result.dictOptions); initDictOptionData(res.result.dictOptions);
cgRpConfigId.value = props.id; cgRpConfigId.value = props.id;
@ -621,15 +629,14 @@ export function usePopBiz(ob, tableRef?) {
dynamicParamHandler(res.result); dynamicParamHandler(res.result);
queryInfo.value = res.result; queryInfo.value = res.result;
console.log('queryInfo==>', queryInfo.value); console.log('queryInfo==>', queryInfo.value);
// // update-begin--author:liaozhiyang---date:20260804---forJPopupOnlReport loadData
// loadColumnsAndData
if (data) { if (data) {
setDataSource(data); setDataSource(data);
//
loadData(1);
} else { } else {
//data
loadData(1); loadData(1);
} }
// update-end--author:liaozhiyang---date:20260804---forJPopupOnlReport loadData
} else { } else {
createMessage.warning(res.message); createMessage.warning(res.message);
} }
@ -695,7 +702,7 @@ export function usePopBiz(ob, tableRef?) {
} }
dataSource.value = data.records; dataSource.value = data.records;
// : issues/426 356 JPopupOnlReportModal.vue // : issues/426 356 JPopupOnlReportModal.vue
tableRef?.value && tableRef?.value?.setPagination({ tableRef?.value && tableRef?.value?.setPagination?.({
total: Number(data.total) total: Number(data.total)
}) })
} else { } else {

View File

@ -17,7 +17,8 @@
<span>{{ item.toUserId_dictText }}</span> <span>{{ item.toUserId_dictText }}</span>
<Tooltip class="comment-last-content" @openChange="(v)=>visibleChange(v, item)"> <Tooltip class="comment-last-content" @openChange="(v)=>visibleChange(v, item)">
<template #title> <template #title>
<div v-html="getHtml(lineFeed(item.commentId_dictText))"></div> <a-spin v-if="item.commentIdLoading" size="small" tip="加载中..." class="jeecg-comment-tooltip-spin" />
<div v-else v-html="getHtml(lineFeed(item.commentId_dictText))"></div>
</template> </template>
<message-outlined /> <message-outlined />
</Tooltip> </Tooltip>
@ -85,7 +86,7 @@
// dayjs.extend(customParseFormat); // dayjs.extend(customParseFormat);
import { MessageOutlined } from '@ant-design/icons-vue'; import { MessageOutlined } from '@ant-design/icons-vue';
import { Comment, Tooltip } from 'ant-design-vue'; import { Comment, Spin, Tooltip } from 'ant-design-vue';
import { useUserStore } from '/@/store/modules/user'; import { useUserStore } from '/@/store/modules/user';
import MyComment from './MyComment.vue'; import MyComment from './MyComment.vue';
import { list, saveOne, deleteOne, useCommentWithFile, useEmojiHtml, queryById, getGloablEmojiIndex } from './useComment'; import { list, saveOne, deleteOne, useCommentWithFile, useEmojiHtml, queryById, getGloablEmojiIndex } from './useComment';
@ -115,21 +116,21 @@
setup(props) { setup(props) {
const { createMessage } = useMessage(); const { createMessage } = useMessage();
const dataList = ref([]); const dataList = ref([]);
const { userInfo } = useUserStore(); const userStore = useUserStore();
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
const listRef = ref(null); const listRef = ref(null);
/** /**
* 获取当前用户名称 * 获取当前用户名称
*/ */
function getMyname() { function getMyname() {
if (userInfo.realname) { if (userStore.getUserInfo.realname) {
return userInfo.realname.substr(0, 2); return userStore.getUserInfo.realname.substr(0, 2);
} }
return ''; return '';
} }
function getMyAvatar(){ function getMyAvatar(){
return getFileAccessHttpUrl(userInfo.avatar); return getFileAccessHttpUrl(userStore.getUserInfo.avatar);
} }
// //
@ -201,7 +202,7 @@
async function replyComment(item, content, fileList) { async function replyComment(item, content, fileList) {
console.log(content, item); console.log(content, item);
let obj = { let obj = {
fromUserId: userInfo.id, fromUserId: userStore.getUserInfo.id,
toUserId: item.fromUserId, toUserId: item.fromUserId,
commentId: item.id, commentId: item.id,
commentContent: content commentContent: content
@ -213,7 +214,7 @@
// //
async function sendComment(content, fileList) { async function sendComment(content, fileList) {
let obj = { let obj = {
fromUserId: userInfo.id, fromUserId: userStore.getUserInfo.id,
commentContent: content commentContent: content
} }
await saveCommentAndFiles(obj, fileList) await saveCommentAndFiles(obj, fileList)
@ -270,6 +271,7 @@
async function visibleChange(v, item){ async function visibleChange(v, item){
if(v==true){ if(v==true){
if(!item.commentId_dictText){ if(!item.commentId_dictText){
item.commentIdLoading = true;
const data = await queryById(item.commentId); const data = await queryById(item.commentId);
if(data.success == true){ if(data.success == true){
item.commentId_dictText = data.result.commentContent item.commentId_dictText = data.result.commentContent
@ -277,11 +279,13 @@
console.error(data.message) console.error(data.message)
item.commentId_dictText='该评论已被删除'; item.commentId_dictText='该评论已被删除';
} }
item.commentIdLoading = false;
} }
} }
} }
// : TV360X-932 // : TV360X-932
const lineFeed = (content) => { const lineFeed = (content) => {
if (!content) return '';
return content.replace(/\n/g, '<br>'); return content.replace(/\n/g, '<br>');
}; };
@ -365,3 +369,14 @@
} }
} }
</style> </style>
<style lang="less">
.jeecg-comment-tooltip-spin {
.ant-spin-dot-item {
background-color: rgba(255, 255, 255, 0.85);
}
.ant-spin-text {
color: rgba(255, 255, 255, 0.85);
}
}
</style>

View File

@ -9,8 +9,8 @@
<edit-outlined v-else/> <edit-outlined v-else/>
</span> </span>
<span class="log-item-content"> <span class="log-item-content">
<a @click="handleClickPerson">@{{item.createName || item.createBy}}</a> <span @click="handleClickPerson">@{{item.createName || item.createBy}}</span>
{{ item.dataContent }} {{ item.dataContent }}
</span> </span>
<div class="log-item-date"> <div class="log-item-date">
<Tooltip :title="item.createTime"> <Tooltip :title="item.createTime">
@ -158,6 +158,9 @@
width: 100%; width: 100%;
word-break: break-word; word-break: break-word;
box-sizing: border-box; box-sizing: border-box;
span {
color: #333;
}
} }
.log-item-date{ .log-item-date{
word-wrap: break-word; word-wrap: break-word;

View File

@ -45,7 +45,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {ref, watch, computed, inject} from 'vue'; import { ref, watch, computed, nextTick } from 'vue';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
import { UserAddOutlined, PaperClipOutlined, SmileOutlined } from '@ant-design/icons-vue'; import { UserAddOutlined, PaperClipOutlined, SmileOutlined } from '@ant-design/icons-vue';
import { Tooltip } from 'ant-design-vue'; import { Tooltip } from 'ant-design-vue';
@ -130,7 +130,7 @@
emit('cancel'); emit('cancel');
} }
const commentRef = ref(); const commentRef = ref<HTMLTextAreaElement | null>(null);
watch( watch(
() => props.inputFocus, () => props.inputFocus,
(val) => { (val) => {
@ -275,9 +275,14 @@
function handleBlur() { function handleBlur() {
showHtml.value = true; showHtml.value = true;
// : // :
setTimeout(() => { nextTick(() => {
commentContentRef.value!.scrollTop = commentRef.value.scrollTop; const commentContentElement = commentContentRef.value;
}, 0); const commentElement = commentRef.value;
if (!commentContentElement || !commentElement) {
return;
}
commentContentElement.scrollTop = commentElement.scrollTop;
});
} }
const commentActive = ref(false); const commentActive = ref(false);

View File

@ -17,7 +17,7 @@
<div class="complex"> <div class="complex">
<div class="content"> <div class="content">
<!-- 图片 --> <!-- 图片 -->
<div v-if="isImage(item)" class="content-top" style="height: 100%"> <div v-if="isImage(item) && !item.exist" class="content-top" style="height: 100%">
<div class="content-image" :style="{'height':'100%', 'backgroundImage': 'url('+getImageSrc(item)+')'}"> <div class="content-image" :style="{'height':'100%', 'backgroundImage': 'url('+getImageSrc(item)+')'}">
<!-- <img style="height: 100%;" :src="getImageSrc(item)">--> <!-- <img style="height: 100%;" :src="getImageSrc(item)">-->
</div> </div>
@ -25,7 +25,7 @@
<!-- 文件 --> <!-- 文件 -->
<template v-else> <template v-else>
<div class="content-top"> <div class="content-top">
<div class="content-icon" :style="{ background: 'url(' + getBackground(item) + ') no-repeat' }"></div> <div class="content-icon" :style="{ background: 'url(' + (isImage(item) ? getImageTypeIcon() : getBackground(item)) + ') no-repeat' }"></div>
</div> </div>
<div class="content-bottom" :title="item.name"> <div class="content-bottom" :title="item.name">
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
@ -78,7 +78,7 @@
}, },
emits:['select'], emits:['select'],
setup(_p, {emit}) { setup(_p, {emit}) {
const { selectFileList, beforeUpload, handleRemove, getBackground, isImage, getImageSrc, viewImage } = useFileList(); const { selectFileList, beforeUpload, handleRemove, getBackground, isImage, getImageSrc, viewImage, getImageTypeIcon } = useFileList();
const [registerModel, { openModal }] = useModal(); const [registerModel, { openModal }] = useModal();
@ -126,6 +126,7 @@
clear, clear,
isImage, isImage,
getImageSrc, getImageSrc,
getImageTypeIcon,
viewImage, viewImage,
registerModel, registerModel,
showFileModal, showFileModal,

View File

@ -17,6 +17,7 @@ import { createImgPreview } from '/@/components/Preview';
import data from "emoji-mart-vue-fast/data/apple.json"; import data from "emoji-mart-vue-fast/data/apple.json";
import { EmojiIndex } from "emoji-mart-vue-fast/src"; import { EmojiIndex } from "emoji-mart-vue-fast/src";
import { encryptByBase64 } from '/@/utils/cipher'; import { encryptByBase64 } from '/@/utils/cipher';
import { sanitizeCommentHtml } from '/@/utils/htmlSanitizer';
enum Api { enum Api {
list = '/sys/comment/listByForm', list = '/sys/comment/listByForm',
@ -422,7 +423,7 @@ export function useEmojiHtml(globalEmojiIndex){
if(!text){ if(!text){
return '' return ''
} }
return text.replace(COLONS_REGEX, function (match, p1, p2) { const emojiHtml = text.replace(COLONS_REGEX, function (match, p1, p2) {
const before = p1 || '' const before = p1 || ''
if (endsWith(before, 'alt="') || endsWith(before, 'data-text="')) { if (endsWith(before, 'alt="') || endsWith(before, 'data-text="')) {
return match return match
@ -433,7 +434,7 @@ export function useEmojiHtml(globalEmojiIndex){
} }
return before + emoji2Html(emoji) return before + emoji2Html(emoji)
}) })
return text; return sanitizeCommentHtml(emojiHtml);
} }
function endsWith(str, temp){ function endsWith(str, temp){

View File

@ -555,7 +555,7 @@
const [registerHighlightColorModal, { openModal: openHighlightColorModal }] = useModal(); const [registerHighlightColorModal, { openModal: openHighlightColorModal }] = useModal();
function initEditor() { function initEditor() {
setTimeout(() => { setTimeout(async () => {
const fontDom: any = document.querySelector('.jeecg-menu-item-font'); const fontDom: any = document.querySelector('.jeecg-menu-item-font');
fontOptionDom.value = fontDom.querySelector('.options'); fontOptionDom.value = fontDom.querySelector('.options');
// //
@ -594,7 +594,13 @@
searchCollapseDom.value = document.querySelector('.jeecg-menu-item-search-collapse'); searchCollapseDom.value = document.querySelector('.jeecg-menu-item-search-collapse');
setTableStyle() setTableStyle()
createEditor(props.content); // update-begin--author:liaozhiyang---date:20270724---for:LHZP-1452word
await createEditor(props.content);
//
if (formState.showCataLogDom) {
updateCatalog();
}
// update-end--author:liaozhiyang---date:20270724---for:LHZP-1452word
// //
document.addEventListener('click', handleClickOutside); document.addEventListener('click', handleClickOutside);
//CtrlCmdS //CtrlCmdS

View File

@ -56,7 +56,8 @@ export function useThirdLogin() {
bindingPhoneModal.value = true; bindingPhoneModal.value = true;
let strings = token.split(','); let strings = token.split(',');
thirdUserUuid.value = strings[1]; thirdUserUuid.value = strings[1];
} else { } else if (token.split('.').length === 3) {
// JWT '.' SDK postMessage [tea-sdk]ready
doThirdLogin(token); doThirdLogin(token);
} }
} else if (typeof token === 'object') { } else if (typeof token === 'object') {

View File

@ -44,6 +44,16 @@ export function useDragNotice() {
console.log('仪表盘监听点击事件参数', record); console.log('仪表盘监听点击事件参数', record);
//1. //1.
if (action == 'url') { if (action == 'url') {
if (record?.type && record?.type === 'file') {
const fileUrl = getFileAccessHttpUrl(record.fileUrl);
const fileName = record.fileUrl?.split('/')?.pop()?.split('?')?.[0] || record.title;
downloadFile(fileUrl, fileName);
return;
}
if (record?.type && record.type === 'url') {
window.open(record.linkUrl, '_blank');
return;
}
// //
if (url == 'fileUrl') { if (url == 'fileUrl') {
url = record[url]; url = record[url];
@ -145,7 +155,7 @@ export function useDragNotice() {
const iframes: any = document.getElementsByClassName('jeecg-iframe-page__main'); const iframes: any = document.getElementsByClassName('jeecg-iframe-page__main');
// HTMLCollection // HTMLCollection
const iframeArray = Array.from(iframes); const iframeArray = Array.from(iframes);
if (currentRoute.value?.meta?.frameSrc && currentRoute.value?.meta?.frameSrc.indexOf('/drag/view?pageId=') >= 0) { if (currentRoute.value?.meta?.frameSrc && isDragFrameSrc(currentRoute.value.meta.frameSrc)) {
const targetIframe: any = iframeArray.find((iframe: any) => iframe.src == currentRoute.value?.meta?.frameSrc); const targetIframe: any = iframeArray.find((iframe: any) => iframe.src == currentRoute.value?.meta?.frameSrc);
console.log('targetIframe', targetIframe); console.log('targetIframe', targetIframe);
if (targetIframe) { if (targetIframe) {
@ -153,6 +163,16 @@ export function useDragNotice() {
} }
} }
} }
/**
* 判断 frameSrc 是否为仪表盘页面
* 支持两种 URL 形式
* 1. /drag/view?pageId=xxxquery 形式老版本约定
* 2. /drag/view/xxxpath 形式新版本约定
*/
function isDragFrameSrc(frameSrc: string): boolean {
return /\/(drag|jimubi)\/view(\?|\/|$)/.test(frameSrc || '');
}
// update-end--author:liusq---date:20260804---for: tab /drag/view?pageId= /drag/view/:id path iframe
return { return {
initDragWebSocket: initWebSocket, initDragWebSocket: initWebSocket,
handleOpenType, handleOpenType,

View File

@ -6,6 +6,17 @@ import { validateCasLogin } from '/@/api/sys/user';
import { useUserStore } from '/@/store/modules/user'; import { useUserStore } from '/@/store/modules/user';
const globSetting = useGlobSetting(); const globSetting = useGlobSetting();
const openSso = globSetting.openSso; const openSso = globSetting.openSso;
/**
* 获取 CAS 登录回调地址保留当前页面路径并移除 CAS 追加的 ticket 参数
* for: issues/9797开启CAS验证回跳地址错误
*/
export function getCasServiceUrl() {
const serviceUrl = new URL(window.location.href);
serviceUrl.searchParams.delete('ticket');
return serviceUrl.href;
}
export function useSso() { export function useSso() {
// : QQYUN-7805SSOhttp #957--- // : QQYUN-7805SSOhttp #957---
let locationUrl = document.location.protocol +"//" + window.location.host + '/'; let locationUrl = document.location.protocol +"//" + window.location.host + '/';
@ -18,17 +29,18 @@ export function useSso() {
let token = getToken(); let token = getToken();
let ticket = getUrlParam('ticket'); let ticket = getUrlParam('ticket');
if (!token) { if (!token) {
const loginServiceUrl = getCasServiceUrl();
if (ticket) { if (ticket) {
await validateCasLogin({ await validateCasLogin({
ticket: ticket, ticket: ticket,
service: locationUrl, service: loginServiceUrl,
}).then((res) => { }).then((res) => {
const userStore = useUserStore(); const userStore = useUserStore();
userStore.setToken(res.token); userStore.setToken(res.token);
return userStore.afterLoginAction(true, {}); return userStore.afterLoginAction(true, {});
}); });
} else { } else {
window.location.href = globSetting.casBaseUrl + '/login?service=' + encodeURIComponent(locationUrl); window.location.href = globSetting.casBaseUrl + '/login?service=' + encodeURIComponent(loginServiceUrl);
} }
} }
} }

View File

@ -26,7 +26,7 @@
import { BasicForm, useForm } from '/@/components/Form'; import { BasicForm, useForm } from '/@/components/Form';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '/@/hooks/web/useI18n';
import { rules } from '@/utils/helper/validator'; import { rules, createPasswordValidator } from '@/utils/helper/validator';
import { defHttp } from '@/utils/http/axios'; import { defHttp } from '@/utils/http/axios';
import { useUserStore } from '@/store/modules/user'; import { useUserStore } from '@/store/modules/user';
@ -36,27 +36,8 @@
const { createMessage } = useMessage(); const { createMessage } = useMessage();
const confirmLoading = ref(false); const confirmLoading = ref(false);
const oldPassword = ref(''); const oldPassword = ref('');
const userStore = useUserStore();
//
function lxStr (password) {
let arr = password.split('');
let flag = true;
for (let i = 1; i < arr.length - 1; i++) {
let firstIndex = arr[i - 1].charCodeAt();
let secondIndex = arr[i].charCodeAt();
let thirdIndex = arr[i + 1].charCodeAt();
thirdIndex - secondIndex == 1;
secondIndex - firstIndex == 1;
if ((thirdIndex - secondIndex === 1) && (secondIndex - firstIndex === 1)) {
flag = false;
}
}
if (!flag) {
return flag
}
return flag
}
// //
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({ const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 100, labelWidth: 100,
@ -73,60 +54,11 @@
required: true, required: true,
message: t('layout.changePassword.pleaseEnterNewPassword'), message: t('layout.changePassword.pleaseEnterNewPassword'),
}, },
//update-begin---author:wangshuai ---date:2026-06-29 forQQYUN-16619-----------
{ {
validator:(_, value)=>{ validator: createPasswordValidator(oldPassword.value),
if(!value){
return Promise.resolve();
}
//使
if(value === oldPassword.value){
return Promise.reject('不能使用系统密码作为新密码!');
}
//8
let reg = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[~!@#$%^&*()_+`\-={}:";'<>?,./]).{8,}$/;
if(!reg.test(value)){
return Promise.reject('密码由 8 位及以上数字、大小写字母和特殊符号组成!');
}
return Promise.resolve();
}
}, },
/* { //update-end---author:wangshuai ---date:2026-06-29 forQQYUN-16619-----------
// 使使
validator: (_, value) => {
// resolve() required
if (!value) {
return Promise.resolve();
}
//使
if(value === oldPassword.value){
return Promise.reject('不能使用系统密码作为新密码!');
}
// 1. 8
if (value.length < 8) {
return Promise.reject('密码长度不能少于8位');
}
// 2.
// 使
const hasNumber = /[0-9]/.test(value);
const hasLowercase = /[a-z]/.test(value);
const hasUppercase = /[A-Z]/.test(value);
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`]/.test(value);
if (!(hasNumber && hasLowercase && hasUppercase && hasSpecialChar)) {
return Promise.reject('密码必须包含数字、大小写字母和特殊字符');
}
if(!lxStr(value)){
return Promise.reject('密码不能出现3位及3为以上的数字或字母');
}
//111aaa33
const reg2 = /(\w)*(\w)\2{2}(\w)*!/g
if (reg2.test(value)) {
console.log('密码不能出现相同字符连续3位或3位以上')
return false
}
// resolve()
return Promise.resolve();
}
}*/
], ],
}, },
{ {
@ -149,7 +81,6 @@
}); });
const getTitle = computed(() => '修改密码'); const getTitle = computed(() => '修改密码');
const userStore = useUserStore();
const { createMessage: $message } = useMessage(); const { createMessage: $message } = useMessage();

View File

@ -108,10 +108,13 @@
* 弹窗打开前处理 * 弹窗打开前处理
*/ */
async function show() { async function show() {
// // update-begin--author:liaozhiyang---date:20260812---for:LHZP-1359
await loadDepartList(); const [canSwitchDepart, canSwitchTenant] = await Promise.all([loadDepartList(), loadTenantList()]);
// if (!canSwitchDepart && !canSwitchTenant) {
await loadTenantList(); createMessage.info('当前没有可切换的租户或部门');
return;
}
// update-end--author:liaozhiyang---date:20260812---for:LHZP-1359
// //
if (unref(isMultiTenant) && unref(isMultiDepart)) { if (unref(isMultiTenant) && unref(isMultiDepart)) {
currTitle.value = '切换租户和部门'; currTitle.value = '切换租户和部门';
@ -132,18 +135,20 @@
*/ */
async function loadDepartList() { async function loadDepartList() {
const result = await getUserDeparts(); const result = await getUserDeparts();
if (!result.list || result.list.length == 0) { // update-begin--author:liaozhiyang---date:20260812---for:LHZP-1359
return; const userDeparts = Array.isArray(result.list) ? result.list.filter((item) => item.orgCategory == '2') : [];
if (userDeparts.length == 0) {
return false;
} }
let currentDepart = result.list.filter((item) => item.orgCode == result.orgCode); // update-end--author:liaozhiyang---date:20260812---for:LHZP-1359
//TODO let currentDepart = userDeparts.filter((item) => item.orgCode == result.orgCode);
const userDeparts = result.list.filter((item) => item.orgCategory == '2');
departList.value = userDeparts; departList.value = userDeparts;
// : JHHB-790 // : JHHB-790
const hasCurrentDepart = userDeparts.some(item => item.orgCode == result.orgCode); const hasCurrentDepart = userDeparts.some(item => item.orgCode == result.orgCode);
departSelected.value = hasCurrentDepart && currentDepart && currentDepart.length > 0 ? result.orgCode : ''; departSelected.value = hasCurrentDepart && currentDepart && currentDepart.length > 0 ? result.orgCode : '';
currentDepartName.value = currentDepart && currentDepart.length > 0 ? currentDepart[0].departName : ''; currentDepartName.value = currentDepart && currentDepart.length > 0 ? currentDepart[0].departName : '';
isMultiDepart.value = true; isMultiDepart.value = true;
return true;
} }
/** /**
*加载租户信息 *加载租户信息
@ -151,7 +156,7 @@
async function loadTenantList() { async function loadTenantList() {
const result = await getUserTenants(); const result = await getUserTenants();
if (!result.list || result.list.length == 0) { if (!result.list || result.list.length == 0) {
return; return false;
} }
let tenantId = userStore.getTenant; let tenantId = userStore.getTenant;
let currentTenant = result.list.filter((item) => item.id == tenantId); let currentTenant = result.list.filter((item) => item.id == tenantId);
@ -159,6 +164,7 @@
tenantList.value = result.list; tenantList.value = result.list;
tenantSelected.value = tenantId; tenantSelected.value = tenantId;
isMultiTenant.value = true; isMultiTenant.value = true;
return true;
} }
/** /**
@ -198,23 +204,24 @@
*/ */
function departResolve() { function departResolve() {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
if (!unref(isMultiDepart) || unref(departList).length == 0) { const hasDepart = unref(isMultiDepart) && unref(departList).length > 0;
const hasTenant = unref(isMultiTenant);
if (!hasDepart && !hasTenant) {
resolve();
return;
}
const result = await selectDepart({
username: userStore.getUserInfo.username,
orgCode: hasDepart ? unref(departSelected) : undefined,
loginTenantId: hasTenant ? unref(tenantSelected) : undefined,
});
if (result.userInfo) {
userStore.setUserInfo(result.userInfo);
resolve(); resolve();
} else { } else {
const result = await selectDepart({ requestFailed(result);
username: userStore.getUserInfo.username, userStore.logout();
orgCode: unref(departSelected), reject();
loginTenantId: unref(tenantSelected),
});
if (result.userInfo) {
const userInfo = result.userInfo;
userStore.setUserInfo(userInfo);
resolve();
} else {
requestFailed(result);
userStore.logout();
reject();
}
} }
}); });
} }

View File

@ -5,7 +5,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, unref } from 'vue'; import { ref, unref } from 'vue';
import { rules } from '/@/utils/helper/validator'; import { rules, createPasswordValidator } from '/@/utils/helper/validator';
import { defHttp } from '/@/utils/http/axios'; import { defHttp } from '/@/utils/http/axios';
import { BasicModal, useModalInner } from '/@/components/Modal'; import { BasicModal, useModalInner } from '/@/components/Modal';
import BasicForm from '/@/components/Form/src/BasicForm.vue'; import BasicForm from '/@/components/Form/src/BasicForm.vue';
@ -43,10 +43,11 @@
required: true, required: true,
message: t('layout.changePassword.pleaseEnterNewPassword'), message: t('layout.changePassword.pleaseEnterNewPassword'),
}, },
//update-begin---author:wangshuai ---date:2026-06-29 forQQYUN-16619-----------
{ {
pattern: /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[~!@#$%^&*()_+`\-={}:";'<>?,./]).{8,}$/, validator: createPasswordValidator(),
message: '密码由 8 位及以上数字、大小写字母和特殊符号组成!', },
}, //update-end---author:wangshuai ---date:2026-06-29 forQQYUN-16619-----------
], ],
}, },
{ {

View File

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

View File

@ -11,6 +11,7 @@ export default {
inputText: 'Please enter', inputText: 'Please enter',
chooseText: 'Please choose', chooseText: 'Please choose',
uploadText: 'Please upload',
redo: 'Refresh', redo: 'Refresh',
back: 'Back', back: 'Back',

View File

@ -11,6 +11,7 @@ export default {
inputText: '请输入', inputText: '请输入',
chooseText: '请选择', chooseText: '请选择',
uploadText: '请上传',
redo: '刷新', redo: '刷新',
back: '返回', back: '返回',

View File

@ -128,7 +128,7 @@ function dynamicImport(dynamicViewsModules: Record<string, () => Promise<Recorda
return; return;
} }
// online/aiflow // online/aiflow
if (component.startsWith('/super/airag/aiflow')) { if (component.startsWith('/super/airag/aiflow/')) {
return () => { return () => {
return loadPackageComponent(component).then((factory) => (factory ? factory() : Promise.reject(`组件 ${component} 未找到`))); return loadPackageComponent(component).then((factory) => (factory ? factory() : Promise.reject(`组件 ${component} 未找到`)));
}; };

View File

@ -7,6 +7,21 @@ import {createRouter as createVueRouter, destroyRouter, router} from './router'
// //
const WHITE_NAME_LIST: string[] = []; const WHITE_NAME_LIST: string[] = [];
// Online 退 404
const ONLINE_ROUTE_NAMES = [
'onl-auto-cgform-router',
'OnlineAutoList',
'DefaultOnlineList',
'CgformErpList',
'OnlCgformInnerTableList',
'OnlCgformTabList',
'onl-auto-cgreport-router',
'OnlCgReportList',
'onl-auto-graphreport-router',
'GraphreportAutoChart',
'onl-auto-desform-router',
'AutoDesformDataList'
];
const getRouteNames = (array: any[]) => const getRouteNames = (array: any[]) =>
array.forEach((item) => { array.forEach((item) => {
WHITE_NAME_LIST.push(item.name); WHITE_NAME_LIST.push(item.name);
@ -40,7 +55,7 @@ export function createRouter() {
export function resetRouter() { export function resetRouter() {
router.getRoutes().forEach((route) => { router.getRoutes().forEach((route) => {
const { name } = route; const { name } = route;
if (name && !WHITE_NAME_LIST.includes(name as string)) { if (name && !WHITE_NAME_LIST.includes(name as string) && !ONLINE_ROUTE_NAMES.includes(name as string)) {
router.hasRoute(name) && router.removeRoute(name); router.hasRoute(name) && router.removeRoute(name);
} }
}); });

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