mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-08-26 06:38:55 +00:00
v3.9.5版本发布 vue3
This commit is contained in:
parent
6f90858bcb
commit
2d401c923d
@ -12,6 +12,7 @@
|
||||
*.iws
|
||||
*.ipr
|
||||
out/
|
||||
.playwright-mcp
|
||||
|
||||
# Eclipse
|
||||
.classpath
|
||||
@ -38,6 +39,7 @@ logs/
|
||||
# Node (frontend artifacts if any)
|
||||
node_modules/
|
||||
dist/
|
||||
mock/
|
||||
|
||||
# Docker volumes / data
|
||||
docker/data/
|
||||
|
||||
@ -29,7 +29,7 @@ VITE_APP_SUB_jeecg-app-1 = '//localhost:8092'
|
||||
# 全局隐藏哪些布局。可选属性: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
|
||||
|
||||
# icon组件的iconify图标库使用在线还是本地。可选属性:online, local
|
||||
|
||||
@ -11,7 +11,7 @@ VITE_GLOB_DOMAIN_URL=https://api3.boot.jeecg.com
|
||||
# 接口父路径前缀
|
||||
VITE_GLOB_API_URL_PREFIX=
|
||||
|
||||
# 在线文档编辑版本。可选属性:wps, offlineWps(离线版), onlyoffice
|
||||
# 在线文档编辑版本。可选属性:wps, newWps(新版本wps), offlineWps(离线版), onlyoffice
|
||||
VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps
|
||||
|
||||
# 全局隐藏哪些布局。可选属性:sider,header,multi-tabs;多个用逗号隔开
|
||||
|
||||
@ -30,7 +30,7 @@ VITE_GLOB_API_URL_PREFIX=
|
||||
# 全局隐藏哪些布局。可选属性: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
|
||||
|
||||
# icon组件的iconify图标库使用在线还是本地。可选属性:online, local
|
||||
|
||||
59
jeecgboot-vue3/.ignore
Normal file
59
jeecgboot-vue3/.ignore
Normal 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
158
jeecgboot-vue3/AGENTS.md
Normal 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.
|
||||
@ -1,9 +1,9 @@
|
||||
import { generate } from '@ant-design/colors';
|
||||
import setting from '../../src/settings/projectSetting';
|
||||
// 构建期默认主题色/主题模式:从 src/settings/projectSetting.ts 动态读取,保持与运行时默认值一致
|
||||
// 运行时会从 store 拿真实值并显式传入,这里只做 build 阶段的占位与默认色板生成
|
||||
// 注意:projectSetting.ts 及其依赖链(enums、designSetting、getConfigByMenuType)均已改为相对路径导入,
|
||||
// 因为 vite 8 的 config 文件打包(rolldown)不解析 tsconfig 路径别名(/@/、@/ 等)
|
||||
|
||||
// 代码逻辑说明: 【JHHB-579】去掉写死的主题色,根据导航栏模式确定主题色
|
||||
export const primaryColor = setting.themeColor;
|
||||
export const darkMode = setting.themeMode;
|
||||
type Fn = (...arg: any) => any;
|
||||
|
||||
@ -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-14806】antd采用unplugin-vue-components实现按需加载
|
||||
// unplugin-vue-components: ant-design-vue 组件自动按需导入
|
||||
vitePlugins.push(
|
||||
|
||||
@ -24,15 +24,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@ant-design/colors": "^7.2.1",
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@vue/shared": "^3.5.39",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@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",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.18.1",
|
||||
@ -84,6 +84,7 @@
|
||||
"vxe-table-plugin-antd": "4.0.8",
|
||||
"xe-utils": "3.5.26",
|
||||
"xss": "^1.0.15",
|
||||
"@onlyoffice/document-editor-vue": "^1.6.1",
|
||||
"vue-grid-layout-v3": "^3.1.2",
|
||||
"lunar-javascript": "^1.7.7",
|
||||
"perfect-scrollbar": "^1.5.6",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -45,6 +45,8 @@ export interface GetUserInfoModel {
|
||||
userInfo?: any;
|
||||
// 缓存字典项
|
||||
sysAllDictItems?: any;
|
||||
// 三级等保强密码开关
|
||||
enableStrongPwd?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -11,10 +11,14 @@ const { uploadUrl = '' } = useGlobSetting();
|
||||
export function uploadApi(params: UploadFileParams, onUploadProgress: (progressEvent: ProgressEvent) => void) {
|
||||
return defHttp.uploadFile<UploadApiResult>(
|
||||
{
|
||||
url: uploadUrl,
|
||||
url: `${uploadUrl}/sys/common/upload`,
|
||||
onUploadProgress,
|
||||
},
|
||||
params
|
||||
params,
|
||||
// update-begin--author:liaozhiyang---date:20260804---for:【LHZP-1385】修复上传示例报错
|
||||
// 返回上传接口完整响应,避免调用方拿到 undefined 被当成失败
|
||||
{ isReturnResponse: true }
|
||||
// update-end--author:liaozhiyang---date:20260804---for:【LHZP-1385】修复上传示例报错
|
||||
);
|
||||
}
|
||||
/**
|
||||
|
||||
@ -24,6 +24,10 @@
|
||||
* @default: #ffffff
|
||||
*/
|
||||
color: { type: String, default: '#ffffff' },
|
||||
/**
|
||||
* Tooltip background color
|
||||
*/
|
||||
backgroundColor: { type: String },
|
||||
/**
|
||||
* Help text font size
|
||||
* @default: 14px
|
||||
@ -77,6 +81,7 @@
|
||||
<Tooltip
|
||||
overlayClassName={`${prefixCls}__wrap`}
|
||||
title={<div style={unref(getTooltipStyle)}>{renderTitle()}</div>}
|
||||
color={props.backgroundColor}
|
||||
autoAdjustOverflow={true}
|
||||
overlayStyle={unref(getOverlayStyle)}
|
||||
placement={props.placement as 'right'}
|
||||
|
||||
@ -91,6 +91,8 @@ componentMap.set('RangeDate', createAsyncComponent(() => import('./jeecg/compone
|
||||
componentMap.set('RangeTime', createAsyncComponent(() => import('./jeecg/components/JRangeTime.vue')));
|
||||
componentMap.set('RoleSelect', createAsyncComponent(() => import('./jeecg/components/roleSelect/RoleSelectInput.vue')));
|
||||
componentMap.set('JInputSelect', createAsyncComponent(() => import('./jeecg/components/JInputSelect.vue')));
|
||||
componentMap.set('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('JSelectUserByDeptPost', createAsyncComponent(() => import('./jeecg/components/JSelectUserByDeptPost.vue')));
|
||||
|
||||
|
||||
@ -4,10 +4,11 @@
|
||||
v-model:value="state"
|
||||
:options="getOptions"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
:filter-option="getFilterOption"
|
||||
@change="handleChange"
|
||||
@dropdownVisibleChange="handleFetch"
|
||||
@popupScroll="handlePopupScroll"
|
||||
@search="handleSearch"
|
||||
>
|
||||
<template #[item]="data" v-for="item in Object.keys($slots)">
|
||||
<slot :name="item" v-bind="data || {}"></slot>
|
||||
@ -66,6 +67,7 @@
|
||||
labelField: propTypes.string.def('label'),
|
||||
valueField: propTypes.string.def('value'),
|
||||
immediate: propTypes.bool.def(true),
|
||||
searchDebounce: propTypes.number.def(300),
|
||||
},
|
||||
emits: ['options-change', 'change'],
|
||||
setup(props, { emit }) {
|
||||
@ -74,6 +76,8 @@
|
||||
const isFirstLoad = ref(true);
|
||||
const emitData = ref<any[]>([]);
|
||||
const attrs = useAttrs();
|
||||
const searchValue = ref('');
|
||||
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const { t } = useI18n();
|
||||
// 代码逻辑说明: 【QQYUN-11831】ApiSelect 分页下拉方案 #7883
|
||||
const hasMore = ref(true);
|
||||
@ -82,7 +86,14 @@
|
||||
pageSize: 10,
|
||||
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---for:【QQYUN-11831】ApiSelect 分页下拉方案 #7883
|
||||
// Embedded in the form, just use the hook binding to perform form verification
|
||||
const [state, setState] = useRuleFormItem(props, 'value', 'change', emitData);
|
||||
@ -159,6 +170,22 @@
|
||||
const filterOption = (input: string, option: any) => {
|
||||
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() {
|
||||
const api = props.api;
|
||||
if (!api || !isFunction(api)) return;
|
||||
@ -168,10 +195,16 @@
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
let { isPage, pageField, pageSizeField, totalField, listField } = { ...defPageConfig, ...props.pageConfig };
|
||||
let { isPage, pageField, pageSizeField, totalField, listField, searchField } = mergedPageConfig.value;
|
||||
let params = isPage
|
||||
? { ...props.params, [pageField]: pagination.value.pageNo, [pageSizeField]: pagination.value.pageSize }
|
||||
: { ...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);
|
||||
if (isPage) {
|
||||
// 代码逻辑说明: 【QQYUN-11831】ApiSelect 分页下拉方案 #7883
|
||||
@ -236,7 +269,7 @@
|
||||
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>
|
||||
|
||||
@ -164,24 +164,31 @@
|
||||
return { isShow, isIfShow };
|
||||
}
|
||||
// 代码逻辑说明: 【TV360X-434】validator校验执行两次
|
||||
//update-begin---author:liaozhiyang ---date:20260518 for:[issues/9638]新增prevValueArr,值发生变化时强制重新执行validator,避免上传/输入后必填红色提示不消失-----------
|
||||
let vSwitchArr: any = [],
|
||||
prevValidatorArr: any = [];
|
||||
prevValidatorArr: any = [],
|
||||
prevValueArr: any = [];
|
||||
const hijackValidator = (rules) => {
|
||||
vSwitchArr = [];
|
||||
prevValidatorArr = [];
|
||||
prevValueArr = [];
|
||||
rules.forEach((item, index) => {
|
||||
const fn = item.validator;
|
||||
vSwitchArr.push(true);
|
||||
prevValidatorArr.push(null);
|
||||
prevValueArr.push(undefined);
|
||||
if (isFunction(fn)) {
|
||||
item.validator = (rule, value, callback) => {
|
||||
if (vSwitchArr[index]) {
|
||||
// 100ms内不允许多次调用,除非值变了
|
||||
const valueChanged = !Object.is(prevValueArr[index], value);
|
||||
if (vSwitchArr[index] || valueChanged) {
|
||||
vSwitchArr[index] = false;
|
||||
setTimeout(() => {
|
||||
vSwitchArr[index] = true;
|
||||
}, 100);
|
||||
const result = fn(rule, value, callback);
|
||||
prevValidatorArr[index] = result;
|
||||
prevValueArr[index] = value;
|
||||
return result;
|
||||
} else {
|
||||
return prevValidatorArr[index];
|
||||
@ -190,6 +197,7 @@
|
||||
}
|
||||
});
|
||||
};
|
||||
//update-end---author:liaozhiyang ---date:20260518 for:[issues/9638]新增prevValueArr,值发生变化时强制重新执行validator,避免上传/输入后必填红色提示不消失-----------
|
||||
function handleRules(): ValidationRule[] {
|
||||
const { rules: defRules = [], component, rulesMessageJoinLabel, label, dynamicRules, required, auth, field } = props.schema;
|
||||
// 代码逻辑说明: 【TV360X-857】解决禁用状态下触发校验
|
||||
|
||||
@ -26,6 +26,11 @@ export function createPlaceholderMessage(component: ComponentType) {
|
||||
// return `请选择${label}`;
|
||||
return t('common.chooseText');
|
||||
}
|
||||
//update-begin---author:liaozhiyang ---date:20260518 for:[issues/9638]BasicForm里使用JUpload且必填时验证总是提示未上传,补Upload类必填提示前缀-----------
|
||||
if (component.includes('Upload')) {
|
||||
return t('common.uploadText');
|
||||
}
|
||||
//update-end---author:liaozhiyang ---date:20260518 for:[issues/9638]BasicForm里使用JUpload且必填时验证总是提示未上传,补Upload类必填提示前缀-----------
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
<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>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watchEffect, computed } from 'vue';
|
||||
@ -23,6 +30,9 @@
|
||||
displayLevel: propTypes.oneOf(['province', 'city', 'region', 'all']),
|
||||
//是否显示区县
|
||||
showArea: propTypes.bool.def(true),
|
||||
// update-begin--author:liaozhiyang---date:20260804---for:【LHZP-707】修复省市区禁用状态下还可以点击x删除
|
||||
disabled: propTypes.bool.def(false),
|
||||
// update-end--author:liaozhiyang---date:20260804---for:【LHZP-707】修复省市区禁用状态下还可以点击x删除
|
||||
// 存储数据 (all时:传递到外面的是数组;province, city, region传递外面的是字符串)
|
||||
saveCode: propTypes.oneOf(['province', 'city', 'region', 'all']).def('all'),
|
||||
},
|
||||
|
||||
@ -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>
|
||||
@ -4,8 +4,15 @@
|
||||
-->
|
||||
<template>
|
||||
<div class="cascader" ref="rootRef">
|
||||
<AInput :value="displayText" :placeholder="inputPlaceholder" readonly :disabled="inputDisabled" v-bind="attrs"
|
||||
:class="{ 'cascader-input-open': visible }" @click="toggle">
|
||||
<AInput
|
||||
:value="displayText"
|
||||
:placeholder="inputPlaceholder"
|
||||
readonly
|
||||
:disabled="inputDisabled"
|
||||
v-bind="inputAttrs"
|
||||
:class="{ 'cascader-input-open': visible }"
|
||||
@click="toggle"
|
||||
>
|
||||
<template #suffix>
|
||||
<span v-if="displayText && !inputDisabled" class="ant-input-clear-icon ant-input-clear-icon-has-suffix"
|
||||
role="button" tabindex="-1" @click.stop="handleClear" @mousedown.prevent>
|
||||
@ -36,7 +43,7 @@
|
||||
</template>
|
||||
|
||||
<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 { DownOutlined, RightOutlined, CloseCircleFilled } from '@ant-design/icons-vue';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
@ -81,8 +88,15 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const attrs = useAttrs();
|
||||
const inputPlaceholder = computed(() => String((attrs as Record<string, unknown>).placeholder ?? '请选择'));
|
||||
const inputDisabled = computed(() => Boolean((attrs as Record<string, unknown>).disabled));
|
||||
const inputPlaceholder = computed(() => String(unref(attrs as Record<string, unknown>).placeholder ?? '请选择'));
|
||||
const inputDisabled = computed(() => Boolean(unref(attrs as Record<string, unknown>).disabled));
|
||||
// update-begin--author:liaozhiyang---date:20260804---for:【LHZP-707】修复省市区禁用状态下还可以点击x删除
|
||||
/** 排除 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---for:【LHZP-707】修复省市区禁用状态下还可以点击x删除
|
||||
const rootRef = ref<HTMLElement>();
|
||||
const dropdownRef = ref<HTMLElement>();
|
||||
/** 下拉是否展开 */
|
||||
@ -179,6 +193,7 @@ function toggle() {
|
||||
|
||||
/** 清除选中:与 UI 库 readonly 下不显示清除按钮的补偿,在 suffix 中手动实现 */
|
||||
function handleClear() {
|
||||
if (inputDisabled.value) return;
|
||||
emit('change', []);
|
||||
emit('update:value', []);
|
||||
visible.value = false;
|
||||
@ -197,12 +212,40 @@ function handleClickOutside(e: MouseEvent) {
|
||||
function updatePopupPosition() {
|
||||
if (!rootRef.value) return;
|
||||
const rect = rootRef.value.getBoundingClientRect();
|
||||
dropdownPlaceStyle.value = {
|
||||
left: `${rect.left}px`,
|
||||
top: `${rect.bottom + DROPDOWN_GAP}px`,
|
||||
};
|
||||
// update-begin--author:liaozhiyang---date:20260714---for【LHZP-100】省市区在页面边缘时,最后一级没法选
|
||||
const margin = 8;
|
||||
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---for:【LHZP-100】省市区在页面边缘时,最后一级没法选
|
||||
}
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20260714---for:【LHZP-100】省市区在页面边缘时,最后一级没法选
|
||||
watch(columns, () => {
|
||||
if (visible.value) {
|
||||
nextTick(updatePopupPosition);
|
||||
}
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20260714---for:【LHZP-100】省市区在页面边缘时,最后一级没法选
|
||||
|
||||
/** 展开时:用当前 value 初始化 editingPath、计算定位、注册点击外部关闭;收起时移除监听 */
|
||||
watch(visible, (v) => {
|
||||
if (v) {
|
||||
@ -248,6 +291,19 @@ onBeforeUnmount(() => {
|
||||
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 {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
</template>
|
||||
|
||||
<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 { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
// 引入全局实例
|
||||
@ -73,7 +73,7 @@
|
||||
// 代码提示
|
||||
keywords: propTypes.array.def([]),
|
||||
},
|
||||
emits: ['change', 'update:value'],
|
||||
emits: ['change', 'update:value', 'ready'],
|
||||
setup(props, { emit }) {
|
||||
const { getDarkMode } = useRootSetting();
|
||||
const containerRef = ref(null);
|
||||
@ -161,6 +161,8 @@
|
||||
// 代码逻辑说明: 【QQYUN-8473】代码编辑器首次加载会有遮挡
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
// 初始化 + refresh 全部完成后通知父组件
|
||||
emit('ready');
|
||||
}, 150);
|
||||
});
|
||||
|
||||
@ -221,6 +223,11 @@
|
||||
// 切换全屏状态
|
||||
function onToggleFullScreen() {
|
||||
isFullScreen.value = !isFullScreen.value;
|
||||
// update-begin--author:liaozhiyang---date:20260713---for:【LHZP-236】onlinejs增强查看历史里面的数据,放大查看,内容无法滚动
|
||||
nextTick(() => {
|
||||
setTimeout(() => refresh(), 100);
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20260713---for:【LHZP-236】onlinejs增强查看历史里面的数据,放大查看,内容无法滚动
|
||||
}
|
||||
|
||||
// 代码逻辑说明: codeEditor禁用功能
|
||||
@ -342,7 +349,9 @@
|
||||
|
||||
.full-screen-child,
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
// update-begin--author:liaozhiyang---date:20260713---for:【LHZP-236】onlinejs增强查看历史里面的数据,放大查看,内容无法滚动
|
||||
height: 100% !important;
|
||||
// update-end--author:liaozhiyang---date:20260713---for:【LHZP-236】onlinejs增强查看历史里面的数据,放大查看,内容无法滚动
|
||||
max-height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@ -1,17 +1,64 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicModal v-bind="$attrs" @register="register" title="导入EXCEL" :width="600" @cancel="handleClose" :confirmLoading="uploading" destroyOnClose>
|
||||
<!--是否校验-->
|
||||
<div style="margin: 0 5px 5px" v-if="online">
|
||||
<span style="display: inline-block; height: 32px; line-height: 32px; vertical-align: middle">是否开启校验:</span>
|
||||
<span style="margin-left: 6px">
|
||||
<BasicModal
|
||||
v-bind="$attrs"
|
||||
@register="register"
|
||||
title="导入 Excel"
|
||||
: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="否" />
|
||||
</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>
|
||||
<!--上传-->
|
||||
<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>
|
||||
<a-button @click="handleClose">关闭</a-button>
|
||||
@ -24,7 +71,8 @@
|
||||
</template>
|
||||
|
||||
<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 { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
@ -36,6 +84,10 @@
|
||||
name: 'JImportModal',
|
||||
components: {
|
||||
BasicModal,
|
||||
CheckCircleFilled,
|
||||
DeleteOutlined,
|
||||
FileExcelOutlined,
|
||||
InboxOutlined,
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
@ -71,7 +123,7 @@
|
||||
const uploadAction = ref('');
|
||||
const foreignKeys = ref('');
|
||||
//校验状态
|
||||
const validateStatus = ref(0);
|
||||
const validateStatus = ref(1);
|
||||
const getBindValue = Object.assign({}, unref(props), unref(attrs));
|
||||
//监听url
|
||||
watchEffect(() => {
|
||||
@ -123,8 +175,8 @@
|
||||
if (!!online) {
|
||||
formData.append('validateStatus', unref(validateStatus));
|
||||
}
|
||||
unref(fileList).forEach((file) => {
|
||||
formData.append('files[]', file);
|
||||
unref(fileList).forEach((file, index) => {
|
||||
formData.append(`files[${index}]`, file);
|
||||
});
|
||||
uploading.value = true;
|
||||
|
||||
@ -153,15 +205,29 @@
|
||||
|
||||
//错误信息提示
|
||||
function errorTip(tipMessage, fileUrl) {
|
||||
let href = glob.uploadUrl + fileUrl;
|
||||
//update-begin---author:scott ---date:20260813 for:优化Excel导入弹窗并修复错误提示HTML注入风险-----------
|
||||
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({
|
||||
title: '导入成功,但是有错误数据!',
|
||||
centered: false,
|
||||
content: `<div>
|
||||
<span>${tipMessage}</span><br/>
|
||||
<span>具体详情请<a href = ${href} target="_blank"> 点击下载 </a> </span>
|
||||
</div>`,
|
||||
title: '导入完成,部分数据未导入',
|
||||
centered: true,
|
||||
width: 480,
|
||||
okText: '知道了',
|
||||
content: h('div', { class: 'j-import-result-content' }, [
|
||||
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 for:优化Excel导入弹窗并修复错误提示HTML注入风险-----------
|
||||
}
|
||||
|
||||
//重置
|
||||
@ -169,7 +235,7 @@
|
||||
fileList.value = [];
|
||||
uploading.value = false;
|
||||
foreignKeys.value = arg;
|
||||
validateStatus.value = 0;
|
||||
validateStatus.value = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
@ -188,3 +254,239 @@
|
||||
},
|
||||
});
|
||||
</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>
|
||||
|
||||
@ -85,7 +85,10 @@
|
||||
}
|
||||
switch (props.type) {
|
||||
case JInputTypeEnum.JINPUT_QUERY_LIKE:
|
||||
text = '*' + text + '*';
|
||||
// 如果输入以 ! 开头,表示走 NE(不等于)查询,不再拼 * 走 LIKE
|
||||
if (!text.startsWith('!')) {
|
||||
text = '*' + text + '*';
|
||||
}
|
||||
break;
|
||||
case JInputTypeEnum.JINPUT_QUERY_NE:
|
||||
text = '!' + text;
|
||||
|
||||
@ -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>
|
||||
@ -35,7 +35,8 @@
|
||||
</a-row>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@ -69,11 +70,14 @@
|
||||
detail: propTypes.bool.def(false),
|
||||
// 图片字段
|
||||
imageField: propTypes.string.def(''),
|
||||
// 查询模式:'online'(仅限 Online 表单配置的表)| 'table'(数据库中的所有表)
|
||||
queryMode: propTypes.string.def('online'),
|
||||
},
|
||||
components: {
|
||||
PlusOutlined,
|
||||
MinusCircleFilled,
|
||||
LinkTableListModal: createAsyncComponent(() => import('./components/LinkTableListModal.vue'), { loading: true }),
|
||||
AllTableListModal: createAsyncComponent(() => import('./components/AllTableListModal.vue'), { loading: true }),
|
||||
},
|
||||
emits: ['change', 'update:value'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
@ -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>
|
||||
@ -12,306 +12,549 @@
|
||||
<a-button :disabled="submitDisabled" key="submit" type="primary" @click="handleSubmit" :loading="submitLoading">确定</a-button>
|
||||
</template>
|
||||
|
||||
<BasicTable ref="tableRef" @register="registerTable" :rowSelection="rowSelection">
|
||||
<template #tableTitle>
|
||||
<a-input-search v-model:value="searchText" @search="onSearch" placeholder="请输入关键词,按回车搜索" style="width: 240px" />
|
||||
</template>
|
||||
<!-- 工具栏:搜索 + 已选提示 -->
|
||||
<div class="ltm-toolbar">
|
||||
<a-input-search
|
||||
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 }">
|
||||
<TableAction :actions="getTableAction(record)"> </TableAction>
|
||||
</template>
|
||||
<!-- 卡片视图 -->
|
||||
<div v-if="cardLoading" class="atm-spin-wrap">
|
||||
<a-spin />
|
||||
</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>
|
||||
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download" size="small" @click="downloadRowFile(text)"> 下载 </a-button>
|
||||
</template>
|
||||
<!-- 内容区 -->
|
||||
<div class="atm-card-inner" :class="{ 'atm-has-image': hasCardImage(record) }">
|
||||
<div class="atm-card-title">{{ getCardMainContent(record) }}</div>
|
||||
<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>
|
||||
<img v-else :src="getImgView(text)" alt="图片不存在" class="online-cell-image" @click="viewOnlineCellImage(text)" />
|
||||
</template>
|
||||
<!-- 图片区 -->
|
||||
<div v-if="hasCardImage(record)" class="atm-card-image">
|
||||
<img :src="getCardImageSrc(record)" alt="" @error="handleCardImageError" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #htmlSlot="{ text }">
|
||||
<div v-html="text"></div>
|
||||
</template>
|
||||
|
||||
<template #pcaSlot="{ text, column }">
|
||||
<div :title="getPcaText(text, column)">{{ getPcaText(text, column) }}</div>
|
||||
</template>
|
||||
|
||||
<template #dateSlot="{ text, column }">
|
||||
<span>{{ getFormatDate(text, column) }}</span>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<!-- 卡片分页 -->
|
||||
<div class="atm-pagination">
|
||||
<a-pagination
|
||||
v-model:current="cardCurrentPage"
|
||||
:total="cardTotal"
|
||||
:pageSize="cardPageSize"
|
||||
:show-total="(t) => `共 ${t} 条`"
|
||||
show-size-changer
|
||||
@change="handleCardPageChange"
|
||||
@showSizeChange="handleCardSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch, ref, toRaw, computed, nextTick } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useListPage } from '/@/hooks/system/useListPage';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useTableColumns } from '@/views/super/online/cgform/hooks/auto/useTableColumns';
|
||||
import { createAsyncComponent } from '@/utils/factory/createAsyncComponent';
|
||||
import { useFixedHeightModal } from '../hooks/useLinkTable';
|
||||
import { defineComponent, watch, ref, toRaw, computed } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useTableColumns } from '@/views/super/online/cgform/hooks/auto/useTableColumns';
|
||||
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';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LinkTableListModal',
|
||||
props: {
|
||||
/**可以是表名 可以是ID*/
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
multi: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
addAuth: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
export default defineComponent({
|
||||
name: 'LinkTableListModal',
|
||||
props: {
|
||||
/**可以是表名 可以是ID*/
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
components: {
|
||||
BasicModal,
|
||||
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), { loading: true, delay: 1000 }),
|
||||
TableAction: createAsyncComponent(() => import('/@/components/Table/src/components/TableAction.vue'), { loading: true, delay: 1000 }),
|
||||
multi: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const { createMessage: $message } = useMessage();
|
||||
const tableRef = ref(null);
|
||||
// 弹窗高度控制
|
||||
const { popModalFixedWidth, resetBodyStyle, popBodyStyle } = useFixedHeightModal();
|
||||
const searchText = ref('');
|
||||
const modalWidth = ref(800);
|
||||
//useModalInner
|
||||
const [registerModal, { closeModal }] = useModalInner((data) => {
|
||||
searchText.value = '';
|
||||
selectedRowKeys.value = data.selectedRowKeys;
|
||||
selectedRows.value = data.selectedRows;
|
||||
setTimeout(async () => {
|
||||
await setPagination({ current: 1 });
|
||||
await reload(); // 等待表格加载
|
||||
resetBodyStyle();
|
||||
}, 100);
|
||||
});
|
||||
addAuth: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
BasicModal,
|
||||
CheckSquareFilled,
|
||||
BorderOutlined,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const { createMessage: $message } = useMessage();
|
||||
// 弹窗高度控制
|
||||
const { popModalFixedWidth, resetBodyStyle, popBodyStyle } = useFixedHeightModal();
|
||||
const searchText = ref('');
|
||||
|
||||
function handleCancel() {
|
||||
closeModal();
|
||||
}
|
||||
const submitDisabled = computed(() => {
|
||||
const arr = selectedRowKeys.value;
|
||||
if (arr && arr.length > 0) {
|
||||
return false;
|
||||
// ==================== 卡片模式状态 ====================
|
||||
const cardLoading = ref(false);
|
||||
const cardDataList = ref<any[]>([]);
|
||||
const cardTotal = ref(0);
|
||||
const cardCurrentPage = ref(1);
|
||||
const cardPageSize = ref(10);
|
||||
|
||||
// 卡片模式选中状态
|
||||
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;
|
||||
});
|
||||
const submitLoading = ref(false);
|
||||
function handleSubmit() {
|
||||
submitLoading.value = true;
|
||||
let arr = toRaw(selectedRows.value);
|
||||
if (arr && arr.length > 0) {
|
||||
emit('success', arr);
|
||||
closeModal();
|
||||
}
|
||||
setTimeout(() => {
|
||||
submitLoading.value = false;
|
||||
}, 200);
|
||||
} else {
|
||||
cardSelectedKeys.value = [key];
|
||||
cardSelectedRecordsMap.value = { [key]: record };
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------列表------------------------
|
||||
function queryTableData(params) {
|
||||
const url = '/online/cgform/api/getData/' + props.id;
|
||||
return defHttp.get({ url, params });
|
||||
}
|
||||
function clearCardSelection() {
|
||||
cardSelectedKeys.value = [];
|
||||
cardSelectedRecordsMap.value = {};
|
||||
}
|
||||
|
||||
function list(params) {
|
||||
params['column'] = 'id';
|
||||
return new Promise(async (resolve, _reject) => {
|
||||
const aa = await queryTableData(params);
|
||||
resolve(aa);
|
||||
// ==================== 卡片字段计算(基于 columns) ====================
|
||||
// 主显示字段(第一个非图片/文件列)
|
||||
const cardMainField = computed(() => {
|
||||
const cols = columns.value || [];
|
||||
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 = {
|
||||
isPopList: true,
|
||||
reloadTable() {
|
||||
console.log('reloadTable');
|
||||
},
|
||||
isTree() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
const extConfigJson = ref<any>({});
|
||||
function handleCardPageChange(page: number) {
|
||||
cardCurrentPage.value = page;
|
||||
loadCardData();
|
||||
}
|
||||
|
||||
// 处理 BasicTable 的配置
|
||||
const { columns, downloadRowFile, getImgView, getPcaText, getFormatDate, handleColumnResult, hrefComponent, viewOnlineCellImage } =
|
||||
useTableColumns(onlineTableContext, extConfigJson);
|
||||
function handleCardSizeChange(_current: number, size: number) {
|
||||
cardPageSize.value = size;
|
||||
cardCurrentPage.value = 1;
|
||||
loadCardData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询table列信息 及其他配置
|
||||
*/
|
||||
function getColumnList() {
|
||||
const url = '/online/cgform/api/getColumns/' + props.id;
|
||||
return new Promise((resolve, reject) => {
|
||||
defHttp.get({ url }, { isTransformResponse: false }).then((res) => {
|
||||
if (res.success) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
$message.warning(res.message);
|
||||
reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// ==================== 弹窗 ====================
|
||||
const [registerModal, { closeModal }] = useModalInner((data) => {
|
||||
searchText.value = '';
|
||||
|
||||
const modalTitle = ref('');
|
||||
watch(
|
||||
() => props.id,
|
||||
async () => {
|
||||
let columnResult: any = await getColumnList();
|
||||
handleColumnResult(columnResult);
|
||||
modalTitle.value = columnResult.description;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const { tableContext } = useListPage({
|
||||
designScope: 'process-design',
|
||||
pagination: true,
|
||||
tableProps: {
|
||||
title: '',
|
||||
api: list,
|
||||
clickToRowSelect: true,
|
||||
columns: columns,
|
||||
showTableSetting: false,
|
||||
immediate: false,
|
||||
//showIndexColumn: true,
|
||||
canResize: false,
|
||||
showActionColumn: false,
|
||||
actionColumn: {
|
||||
dataIndex: 'action',
|
||||
slots: { customRender: 'action' },
|
||||
},
|
||||
useSearchForm: false,
|
||||
beforeFetch: (params) => {
|
||||
return addQueryParams(params);
|
||||
},
|
||||
},
|
||||
});
|
||||
const [registerTable, { reload, setPagination }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
|
||||
watch(
|
||||
() => props.multi,
|
||||
(val) => {
|
||||
if (val == true) {
|
||||
rowSelection.type = 'checkbox';
|
||||
} else {
|
||||
rowSelection.type = 'radio';
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 操作栏
|
||||
*/
|
||||
function getTableAction(record) {
|
||||
return [
|
||||
{
|
||||
label: '编辑',
|
||||
onClick: handleUpdate.bind(null, record),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function handleUpdate(record) {
|
||||
console.log('handleUpdate', record);
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
reload();
|
||||
}
|
||||
const eqConditonTypes = ['int', 'double', 'Date', 'Datetime', 'BigDecimal'];
|
||||
function addQueryParams(params) {
|
||||
let text = searchText.value;
|
||||
if (!text) {
|
||||
params['superQueryMatchType'] = 'or';
|
||||
params['superQueryParams'] = '';
|
||||
return params;
|
||||
// 同步卡片选中状态
|
||||
cardSelectedKeys.value = (data.selectedRowKeys || []).map(String);
|
||||
cardSelectedRecordsMap.value = {};
|
||||
for (const row of data.selectedRows || []) {
|
||||
if (row && row['id'] != null) {
|
||||
cardSelectedRecordsMap.value[String(row['id'])] = row;
|
||||
}
|
||||
let arr = columns.value;
|
||||
let conditions: any[] = [];
|
||||
if (arr && arr.length > 0) {
|
||||
for (let item of arr) {
|
||||
if (item.dbType) {
|
||||
if (item.dbType == 'string') {
|
||||
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'like', val: text });
|
||||
} else if (item.dbType == 'Date') {
|
||||
if (text.length == '2020-10-10'.length) {
|
||||
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
|
||||
}
|
||||
} else if (item.dbType == 'Datetime') {
|
||||
if (text.length == '2020-10-10 10:10:10'.length) {
|
||||
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
|
||||
}
|
||||
} else if (eqConditonTypes.indexOf(item.dbType)) {
|
||||
conditions.push({ field: item.dataIndex, type: item.dbType.toLowerCase(), rule: 'eq', val: text });
|
||||
} else {
|
||||
//text blob不做处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
params['superQueryMatchType'] = 'or';
|
||||
params['superQueryParams'] = encodeURI(JSON.stringify(conditions));
|
||||
return params;
|
||||
}
|
||||
|
||||
// modal数据新增完成 直接关闭list,将新增的数据带回表单
|
||||
function handleDataSave(data) {
|
||||
console.log('handleDateSave', data);
|
||||
// 代码逻辑说明: 【issues/8163】关联记录新增丢失
|
||||
let arr = [data, ...selectedRows.value];
|
||||
setTimeout(async () => {
|
||||
cardCurrentPage.value = 1;
|
||||
await loadCardData();
|
||||
resetBodyStyle();
|
||||
}, 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);
|
||||
closeModal();
|
||||
//reload();
|
||||
}
|
||||
setTimeout(() => {
|
||||
submitLoading.value = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
modalWidth,
|
||||
handleCancel,
|
||||
submitDisabled,
|
||||
submitLoading,
|
||||
handleSubmit,
|
||||
// ==================== 数据请求 ====================
|
||||
function queryTableData(params) {
|
||||
const url = '/online/cgform/api/getData/' + props.id;
|
||||
return defHttp.get({ url, params });
|
||||
}
|
||||
|
||||
registerTable,
|
||||
getTableAction,
|
||||
searchText,
|
||||
onSearch,
|
||||
const onlineTableContext = {
|
||||
isPopList: true,
|
||||
reloadTable() {},
|
||||
isTree() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
const extConfigJson = ref<any>({});
|
||||
|
||||
downloadRowFile,
|
||||
getImgView,
|
||||
getPcaText,
|
||||
getFormatDate,
|
||||
hrefComponent,
|
||||
viewOnlineCellImage,
|
||||
rowSelection,
|
||||
modalTitle,
|
||||
const { columns, handleColumnResult } = useTableColumns(onlineTableContext, extConfigJson);
|
||||
|
||||
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,
|
||||
popBodyStyle,
|
||||
handleDataSave,
|
||||
const modalTitle = ref('');
|
||||
watch(
|
||||
() => 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>
|
||||
|
||||
<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>
|
||||
|
||||
@ -13,6 +13,39 @@ function queryTableColumns(tableName, params) {
|
||||
const url = '/online/cgform/api/getColumns/' + tableName;
|
||||
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) {
|
||||
//TODO 目前只支持查询第一页的数据,可以输入关键字搜索
|
||||
@ -50,6 +83,18 @@ export function useLinkTable(props) {
|
||||
watchEffect(async () => {
|
||||
const table = props.tableName;
|
||||
if (table) {
|
||||
if (props.queryMode === 'table') {
|
||||
// table 模式:跳过 Online 接口,根据 textField/imageField 配置初始化展示列
|
||||
const textFields = (props.textField || '').split(',').filter(Boolean);
|
||||
mainContentField.value = textFields[0] || '';
|
||||
// otherColumns:textField 中第一个之后的字段,用于卡片副内容展示
|
||||
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 textField = props.textField || '';
|
||||
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() {
|
||||
const params = baseParam.value;
|
||||
@ -210,6 +251,34 @@ export function useLinkTable(props) {
|
||||
if (!value) {
|
||||
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 params = {
|
||||
...baseParam.value,
|
||||
|
||||
@ -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);
|
||||
// table属性scroll
|
||||
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---for:【QQYUN-4161】列支持固定功能
|
||||
if (column.fieldExtendJson) {
|
||||
const json = JSON.parse(column.fieldExtendJson);
|
||||
if (!!json.isFixed) {
|
||||
column.fixed = 'left';
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20230818---for:【QQYUN-4161】列支持固定功能
|
||||
// update-begin--author:liaozhiyang---date:20240517---for:【TV360X-129】增加富文本控件配置href跳转
|
||||
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---for:【TV360X-129】增加富文本控件配置href跳转
|
||||
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---for:【issues/7930】表格列表中支持关联记录配置是否只读
|
||||
const fieldExtendJson = column.fieldExtendJson ?? '{}';
|
||||
const json = JSON.parse(fieldExtendJson);
|
||||
// update-end--author:liaozhiyang---date:20250318---for:【issues/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-4286【online表单】主子表开启联合查询 功能测试报错打不开
|
||||
let tempLabelArray = [];
|
||||
if(record[column.dataIndex+"_dictText"]){
|
||||
tempLabelArray = record[column.dataIndex+"_dictText"].split(',');
|
||||
}
|
||||
//update-end-author:taoyan date:2023-2-15 for: QQYUN-4286【online表单】主子表开启联合查询 功能测试报错打不开
|
||||
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---for:【QQYUN-8833】JPopupDict的列表翻译
|
||||
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---for:【QQYUN-8833】JPopupDict的列表翻译
|
||||
} else {
|
||||
if (!hrefSlotName && column.scopedSlots && column.scopedSlots.customRender) {
|
||||
//【Online报表】字典和href互斥 这里通过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\-\.\?\,\'\/\\\+&%\$#_]*)?/;
|
||||
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---for:【QQYUN-6390】eval替换成new Function,解决build警告
|
||||
return _eval(s0);
|
||||
// update-end--author:liaozhiyang---date:20230904---for:【QQYUN-6390】eval替换成new Function,解决build警告
|
||||
} 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---for:【QQYUN-6366】升级到antd4.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---for:【QQYUN-6366】升级到antd4.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---for:【QQYUN-6366】升级到antd4.x
|
||||
hrefComponent.model.open = true;
|
||||
// update-end--author:liaozhiyang---date:20231218---for:【QQYUN-6366】升级到antd4.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---for:【QQYUN-8020】online 表单有多个文件走下载接口
|
||||
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---for:【QQYUN-8020】online 表单有多个文件走下载接口
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片
|
||||
* @param text
|
||||
*/
|
||||
function getImgView(text) {
|
||||
if (text && text.indexOf(',') > 0) {
|
||||
// update-begin--author:liaozhiyang---date:20250325---for:【issues/7990】图片参数中包含逗号会错误的识别成多张图
|
||||
text = split(text)[0];
|
||||
// update-end--author:liaozhiyang---date:20250325---for:【issues/7990】图片参数中包含逗号会错误的识别成多张图
|
||||
}
|
||||
return getFileAccessHttpUrl(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据编码获取省市区文本
|
||||
* @param code
|
||||
*/
|
||||
function getPcaText(code, column) {
|
||||
if (!code) {
|
||||
return '';
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20260204---for:【QQYUN-14694】online支持配置独立的省、市、县
|
||||
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-14694】online支持配置独立的省、市、县
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期格式化
|
||||
* @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---for:【issues/6094】online 日期(年月日)控件增加年、年月,年周,年季度等格式
|
||||
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---for:【issues/6094】online 日期(年月日)控件增加年、年月,年周,年季度等格式
|
||||
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---for:【issues/7990】图片参数中包含逗号会错误的识别成多张图
|
||||
const arr = split(text);
|
||||
// update-end--author:liaozhiyang---date:20250325---for:【issues/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---for:【issues/7930】表格列表中支持关联记录配置是否只读
|
||||
disableSubmit: isListReadOnly ? true : false,
|
||||
// update-end--author:liaozhiyang---date:20250318---for:【issues/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,
|
||||
};
|
||||
}
|
||||
@ -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>
|
||||
@ -2,9 +2,10 @@
|
||||
<template>
|
||||
<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>
|
||||
<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 #suffix>
|
||||
<Icon icon="ant-design:close-circle-outlined" @click="handleEmpty" title="清空" v-if="showText"></Icon>
|
||||
@ -34,6 +35,13 @@
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
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({
|
||||
name: 'JPopup',
|
||||
@ -71,6 +79,9 @@
|
||||
const avalid = ref(true);
|
||||
const showText = ref('');
|
||||
const innerShowText = ref('')
|
||||
const loading = ref(false);
|
||||
const isQueryingShowText = ref(false);
|
||||
const cgRpConfigId = ref('');
|
||||
//注册model
|
||||
const [regModal, { openModal }] = useModal();
|
||||
//表单值
|
||||
@ -92,11 +103,101 @@
|
||||
watch(
|
||||
() => props.value,
|
||||
(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 }
|
||||
);
|
||||
|
||||
/**
|
||||
* 根据 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弹出框
|
||||
*/
|
||||
@ -155,6 +256,8 @@
|
||||
innerShowText,
|
||||
avalid,
|
||||
uniqGroupId,
|
||||
loading,
|
||||
isQueryingShowText,
|
||||
attrs,
|
||||
regModal,
|
||||
handleOpen,
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
<template>
|
||||
<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>
|
||||
<a-form-item>
|
||||
@ -54,21 +55,25 @@
|
||||
dictCode: propTypes.string.def(''),
|
||||
value: propTypes.string.def(''),
|
||||
sorter: propTypes.string.def(''),
|
||||
disabled: propTypes.bool.def(false),
|
||||
multi: propTypes.bool.def(false),
|
||||
param: propTypes.object.def({}),
|
||||
spliter: propTypes.string.def(','),
|
||||
getFormValues: propTypes.func,
|
||||
getContainer: propTypes.func,
|
||||
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 }) {
|
||||
const { createMessage } = useMessage();
|
||||
const attrs = useAttrs();
|
||||
const showText = ref<any>(props.multi ? [] : '');
|
||||
const showLabel = ref<string>('');
|
||||
const options = ref<any>([]);
|
||||
const cgRpConfigId = ref('');
|
||||
const loading = ref(false);
|
||||
let syncingFromProps = false;
|
||||
const code = props.dictCode.split(',')[0];
|
||||
const labelFiled = props.dictCode.split(',')[1];
|
||||
const valueFiled = props.dictCode.split(',')[2];
|
||||
@ -84,29 +89,69 @@
|
||||
*/
|
||||
function handleOpen() {
|
||||
// 代码逻辑说明: 【TV360X-317】禁用后JPopup和JPopupdic还可以点击出弹窗
|
||||
!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数值
|
||||
*/
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
() => {
|
||||
const callBack = () => {
|
||||
syncingFromProps = true;
|
||||
// update-begin--author:liusq---date:20260720---for:【LHZP-1003】单表pop字典,设置后,编辑打开,回显数据陷入循环查询的死循环
|
||||
// 代码逻辑说明: 必须使用实时的props.value而非闭包捕获的val,否则在异步回调时
|
||||
// defaultValue与记录值不同时,多个callBack会交替覆盖formModel,导致无限刷新
|
||||
const currentValue = props.value;
|
||||
if (props.multi) {
|
||||
showText.value = val && val.length > 0 ? val.split(props.spliter) : [];
|
||||
showText.value = currentValue && currentValue.length > 0 ? currentValue.split(props.spliter) : [];
|
||||
} else {
|
||||
showText.value = val ?? '';
|
||||
showText.value = currentValue ?? '';
|
||||
}
|
||||
// update-end--author:liusq---date:20260720---for:【LHZP-1003】单表pop字典,设置后,编辑打开,回显数据陷入循环查询的死循环
|
||||
// update-begin--author:liaozhiyang---date:20260714---for:【LHZP-392】pop字典外面删除了,弹窗里面还是勾选
|
||||
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---for:【LHZP-392】pop字典外面删除了,弹窗里面还是勾选
|
||||
syncShowLabel();
|
||||
nextTick(() => {
|
||||
syncingFromProps = false;
|
||||
});
|
||||
};
|
||||
// 先回显已保存值,接口返回后再用加载到的 options 替换为显示文本,避免编辑弹窗短暂空白
|
||||
callBack();
|
||||
if (props.value || props.defaultValue) {
|
||||
if (cgRpConfigId.value) {
|
||||
loadData({ callBack });
|
||||
} else {
|
||||
loadColumnsInfo({ callBack });
|
||||
}
|
||||
} else {
|
||||
callBack();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@ -114,6 +159,8 @@
|
||||
watch(
|
||||
() => showText.value,
|
||||
(val) => {
|
||||
// props 回显属于单向同步,不能再作为用户操作反向清空或覆盖表单值
|
||||
if (syncingFromProps) return;
|
||||
let result;
|
||||
if (props.multi) {
|
||||
result = val.join(',');
|
||||
@ -182,18 +229,30 @@
|
||||
if (props.multi) {
|
||||
showText.value = dataValue;
|
||||
result = dataValue.join(props.spliter);
|
||||
//update-begin author:liaozhiyang date:20260522 for:【QQYUN-15493】新增a-input模式下
|
||||
if (props.useInput) {
|
||||
showLabel.value = dataOptions.map((o: any) => o.text).join(props.spliter);
|
||||
}
|
||||
//update-end author:liaozhiyang date:20260522 for:【QQYUN-15493】新增a-input模式下
|
||||
} else {
|
||||
showText.value = dataValue[0];
|
||||
result = dataValue[0];
|
||||
//update-begin author:liaozhiyang date:20260522 for:【QQYUN-15493】新增a-input模式下
|
||||
if (props.useInput) {
|
||||
showLabel.value = dataOptions[0]?.text || '';
|
||||
}
|
||||
//update-end author:liaozhiyang date:20260522 for:【QQYUN-15493】新增a-input模式下
|
||||
}
|
||||
nextTick(() => {
|
||||
emit('change', result);
|
||||
emit('update:value', result);
|
||||
emit('options-change', options.value);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
showText,
|
||||
showLabel,
|
||||
attrs,
|
||||
regModal,
|
||||
handleOpen,
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
<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>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, watch, computed } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
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 = ['开始日期', '结束日期']
|
||||
/**
|
||||
@ -17,6 +23,9 @@
|
||||
value: propTypes.string.def(''),
|
||||
datetime: propTypes.bool.def(false),
|
||||
placeholder: propTypes.string.def(''),
|
||||
picker: propTypes.string.def('date'),
|
||||
format: propTypes.string.def(''),
|
||||
valueFormat: propTypes.string.def(''),
|
||||
},
|
||||
emits:['change', 'update:value'],
|
||||
setup(props, {emit}){
|
||||
@ -32,6 +41,9 @@
|
||||
}, {immediate: true});
|
||||
|
||||
const valueFormat = computed(()=>{
|
||||
if(props.valueFormat){
|
||||
return props.valueFormat
|
||||
}
|
||||
if(props.datetime === true){
|
||||
return 'YYYY-MM-DD HH:mm:ss'
|
||||
}else{
|
||||
@ -43,12 +55,33 @@
|
||||
let str = ''
|
||||
if(arr && arr.length>0){
|
||||
// 代码逻辑说明: [issues/6368] rangeDate去掉判断允许起始项或结束项为空兼容allowEmpty
|
||||
str = arr.join(',')
|
||||
// update-begin--author:wangshuai---date:20260819---for:【LHZP-322】【表单设计器】支持周、季度
|
||||
str = arr.map(normalizePickerValue).join(',')
|
||||
// update-end--author:wangshuai---date:20260819---for:【LHZP-322】【表单设计器】支持周、季度
|
||||
}
|
||||
emit('change', str);
|
||||
emit('update:value', str);
|
||||
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 {
|
||||
rangeValue,
|
||||
placeholder,
|
||||
|
||||
@ -52,9 +52,13 @@
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
if (val && val.length == 2) {
|
||||
beginValue.value = val[0];
|
||||
endValue.value = val[1];
|
||||
let parseVal = val;
|
||||
if (typeof val === 'string') {
|
||||
parseVal = val.split(',');
|
||||
}
|
||||
if (Array.isArray(parseVal) && parseVal.length === 2) {
|
||||
beginValue.value = parseVal[0];
|
||||
endValue.value = parseVal[1];
|
||||
} else {
|
||||
beginValue.value = '';
|
||||
endValue.value = '';
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<!-- 字典下拉单选 -->
|
||||
<!-- 字典下拉单选 / 远程加载下拉 -->
|
||||
<template>
|
||||
<a-select
|
||||
:value="innerValue"
|
||||
:mode="mode"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
:filter-option="filterOptionComputed"
|
||||
@ -10,7 +11,7 @@
|
||||
:notFoundContent="loading ? undefined : null"
|
||||
allowClear
|
||||
@change="handleChange"
|
||||
@search="handleSearch"
|
||||
:onSearch="showSearch ? handleSearch : undefined"
|
||||
@popupScroll="handlePopupScroll"
|
||||
@dropdownVisibleChange="handleDropdownVisibleChange"
|
||||
v-bind="$attrs"
|
||||
@ -51,7 +52,6 @@
|
||||
style: { backgroundColor: string };
|
||||
}
|
||||
|
||||
/** 下拉单选组件的值类型(可被 change / update:value / input 等复用) */
|
||||
export type SelectSingleValue = string | number | undefined;
|
||||
|
||||
const props = withDefaults(
|
||||
@ -59,6 +59,7 @@
|
||||
value?: SelectSingleValue;
|
||||
placeholder?: string;
|
||||
readOnly?: boolean;
|
||||
mode?: 'multiple' | 'tags' | 'combobox';
|
||||
options?: DictOption[];
|
||||
triggerChange?: boolean;
|
||||
popContainer?: string;
|
||||
@ -67,6 +68,12 @@
|
||||
useDicColor?: boolean;
|
||||
pageSize?: number;
|
||||
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: '请选择',
|
||||
@ -78,6 +85,8 @@
|
||||
useDicColor: false,
|
||||
pageSize: 10,
|
||||
scrollLoad: false,
|
||||
showSearch: false,
|
||||
remoteLoad: false,
|
||||
}
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
@ -88,17 +97,19 @@
|
||||
}>();
|
||||
|
||||
// --- 选项数据 ---
|
||||
const innerValue = ref<SelectSingleValue>(props.value ?? undefined);
|
||||
const innerValue = ref<SelectSingleValue>();
|
||||
const dictOptions = ref<DictOption[]>([]);
|
||||
const loading = ref(false);
|
||||
const searchKeyword = ref('');
|
||||
|
||||
// --- 字典加载(分页滚动) ---
|
||||
// --- 分页滚动状态 ---
|
||||
const scrollState = {
|
||||
pageNo: 1,
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
};
|
||||
/** 缓存上次滚动的目标元素,关闭下拉时复位其滚动条 */
|
||||
let lastScrollTarget: HTMLElement | null = null;
|
||||
|
||||
const isDictTable = computed(() => {
|
||||
if (!props.dictCode) return false;
|
||||
@ -107,6 +118,10 @@
|
||||
|
||||
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[]>(() => {
|
||||
if (!Array.isArray(dictOptions.value)) return [];
|
||||
return dictOptions.value.map((item, index) => {
|
||||
@ -121,6 +136,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
// --- 字典加载 ---
|
||||
function fetchDictPage(pageNo: number, append: boolean) {
|
||||
return defHttp
|
||||
.get({
|
||||
@ -128,48 +144,66 @@
|
||||
params: { pageNo, pageSize: props.pageSize, keyword: searchKeyword.value, order: 'asc' },
|
||||
})
|
||||
.then((res: any[]) => {
|
||||
const items: DictOption[] = (res ?? []).map((it) => ({
|
||||
const items: DictOption[] = (res ?? []).map((it: any) => ({
|
||||
value: it.value,
|
||||
label: it.text ?? it.label,
|
||||
}));
|
||||
if (items.length === 0) {
|
||||
if (!append) dictOptions.value = [];
|
||||
scrollState.hasMore = false;
|
||||
return;
|
||||
}
|
||||
if (append) {
|
||||
const existValues = new Set(dictOptions.value.map((o) => String(o.value)));
|
||||
const newItems = items.filter((it) => !existValues.has(String(it.value)));
|
||||
if (newItems.length > 0) {
|
||||
dictOptions.value = [...dictOptions.value, ...newItems];
|
||||
}
|
||||
} else {
|
||||
// 有选中的值且optinos中存在时,需要把选中的值在options中存在且不在新数据中的项保留(防止多次请求)
|
||||
if (unref(innerValue) && unref(dictOptions).length) {
|
||||
const existItem = unref(dictOptions).find((item) => String(item.value) === String(unref(innerValue)));
|
||||
if (existItem && !items.some((item) => String(item.value) === String(existItem.value))) {
|
||||
items.push(existItem);
|
||||
}
|
||||
}
|
||||
dictOptions.value = items;
|
||||
}
|
||||
scrollState.pageNo = pageNo + 1;
|
||||
processPageResult(items, pageNo, append);
|
||||
});
|
||||
}
|
||||
|
||||
// --- 远程加载 ---
|
||||
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() {
|
||||
if (useLoadDict.value) {
|
||||
scrollState.pageNo = 1;
|
||||
scrollState.hasMore = true;
|
||||
loading.value = true;
|
||||
fetchDictPage(1, false)
|
||||
.catch(() => {
|
||||
dictOptions.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
ensureValueInOptions();
|
||||
});
|
||||
.catch(() => { dictOptions.value = []; })
|
||||
.finally(() => { loading.value = false; ensureValueInOptions(); });
|
||||
} else if (useRemoteLoad.value) {
|
||||
scrollState.pageNo = 1;
|
||||
scrollState.hasMore = true;
|
||||
loadRemoteOptions();
|
||||
} else {
|
||||
let code = props.dictCode ?? '';
|
||||
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> {
|
||||
if (val == null) return Promise.resolve(null);
|
||||
return defHttp
|
||||
.get({ url: `/sys/dict/loadDictItem/${props.dictCode}`, params: { key: val } })
|
||||
.then((res: any) => {
|
||||
if (Array.isArray(res)) {
|
||||
return { value: val, label: res[0] };
|
||||
}
|
||||
if (Array.isArray(res)) return { value: val, label: res[0] };
|
||||
return null;
|
||||
})
|
||||
.catch(() => null);
|
||||
}
|
||||
|
||||
/** 确保当前 value 在选项中(仅 useLoadDict 时:编辑时当前值不在已加载列表中则拉取单条并插入) */
|
||||
function ensureValueInOptions() {
|
||||
if (!useLoadDict.value) return;
|
||||
const val = innerValue.value;
|
||||
@ -209,53 +248,72 @@
|
||||
}
|
||||
|
||||
const handleSearch = debounce((keyword: string) => {
|
||||
if (!useLoadDict.value) return;
|
||||
searchKeyword.value = keyword ?? '';
|
||||
scrollState.pageNo = 1;
|
||||
scrollState.hasMore = true;
|
||||
loading.value = true;
|
||||
fetchDictPage(1, false)
|
||||
.catch(() => {
|
||||
dictOptions.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
ensureValueInOptions();
|
||||
});
|
||||
if (useLoadDict.value) {
|
||||
searchKeyword.value = keyword ?? '';
|
||||
scrollState.pageNo = 1;
|
||||
scrollState.hasMore = true;
|
||||
loading.value = true;
|
||||
fetchDictPage(1, false)
|
||||
.catch(() => { dictOptions.value = []; })
|
||||
.finally(() => { loading.value = false; ensureValueInOptions(); });
|
||||
} else if (useRemoteLoad.value) {
|
||||
searchKeyword.value = keyword ?? '';
|
||||
scrollState.pageNo = 1;
|
||||
scrollState.hasMore = true;
|
||||
loading.value = true;
|
||||
fetchRemotePage(1, false)
|
||||
.finally(() => { loading.value = false; });
|
||||
} else if (props.onSearch) {
|
||||
props.onSearch(keyword ?? '');
|
||||
}
|
||||
}, 300);
|
||||
|
||||
function handleDropdownVisibleChange(open: boolean) {
|
||||
if (!open || !useLoadDict.value) return;
|
||||
scrollState.pageNo = 1;
|
||||
scrollState.hasMore = true;
|
||||
searchKeyword.value = '';
|
||||
loading.value = true;
|
||||
fetchDictPage(1, false)
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
ensureValueInOptions();
|
||||
});
|
||||
if (!open) {
|
||||
// 关闭时:复位缓存的滚动元素,下次打开从顶部开始
|
||||
if (lastScrollTarget) {
|
||||
lastScrollTarget.scrollTop = 0;
|
||||
lastScrollTarget = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 打开时:清空缓存
|
||||
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) {
|
||||
if (!useLoadDict.value || scrollState.loading || !scrollState.hasMore) return;
|
||||
const target = e.target as HTMLElement;
|
||||
const { scrollTop, scrollHeight, clientHeight } = target;
|
||||
if (scrollTop + clientHeight < scrollHeight - 10) return;
|
||||
scrollState.loading = true;
|
||||
fetchDictPage(scrollState.pageNo, true)
|
||||
.finally(() => {
|
||||
scrollState.loading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
if (scrollState.pageNo > 1) {
|
||||
scrollState.pageNo--;
|
||||
}
|
||||
});
|
||||
lastScrollTarget = e.target as HTMLElement;
|
||||
if (useLoadDict.value) {
|
||||
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;
|
||||
fetchDictPage(scrollState.pageNo, true)
|
||||
.finally(() => { scrollState.loading = false; })
|
||||
.catch(() => { if (scrollState.pageNo > 1) scrollState.pageNo--; });
|
||||
} else if (useRemoteLoad.value) {
|
||||
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) {
|
||||
return props.popContainer ? setPopContainer(node, props.popContainer) : node?.parentNode;
|
||||
}
|
||||
@ -266,8 +324,8 @@
|
||||
return text.indexOf(input.toLowerCase()) >= 0;
|
||||
}
|
||||
const filterOptionComputed = computed(() => (input: string, option: any) => {
|
||||
if (useLoadDict.value) return true;
|
||||
return filterOption(input, option);
|
||||
if (useLoadDict.value || useRemoteLoad.value) return true;
|
||||
return props.filterOption?.(input, option) ?? filterOption(input, option);
|
||||
});
|
||||
|
||||
// --- 选择变更 ---
|
||||
@ -283,7 +341,7 @@
|
||||
|
||||
// --- 初始化与监听 ---
|
||||
function syncOptionsFromProps() {
|
||||
if (props.dictCode) {
|
||||
if (props.dictCode || props.remoteLoad) {
|
||||
loadDictOptions();
|
||||
} else {
|
||||
dictOptions.value = props.options ?? [];
|
||||
@ -294,15 +352,23 @@
|
||||
watch(
|
||||
() => props.value,
|
||||
(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();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.options,
|
||||
() => {
|
||||
if (!props.dictCode) dictOptions.value = props.options ?? [];
|
||||
if (!props.dictCode && !props.remoteLoad) dictOptions.value = props.options ?? [];
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
import { unref } from 'vue';
|
||||
import UserSelectModal from './modal/UserSelectModal.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 { propTypes } from '/@/utils/propTypes';
|
||||
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
|
||||
@ -147,7 +147,10 @@
|
||||
selectValues.value = 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】用户选择组件取消和关闭会把选择数据带入
|
||||
const handleClose = () => {
|
||||
if (tempSave.length) {
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
:multiple="multiple"
|
||||
v-bind="attrs"
|
||||
@change="onChange"
|
||||
@search="onSearch"
|
||||
:tree-checkable="treeCheckAble"
|
||||
:tagRender="tagRender"
|
||||
>
|
||||
@ -31,7 +30,6 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { TreeSelect } from 'ant-design-vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { isObject, isArray } from '/@/utils/is';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
@ -71,7 +69,7 @@
|
||||
//树形下拉数据
|
||||
const treeData = ref<any[]>([]);
|
||||
//选择数据
|
||||
const treeValue = ref<any>(null);
|
||||
const treeValue = ref<any>();
|
||||
const tableName = ref<any>('');
|
||||
const text = ref<any>('');
|
||||
const code = ref<any>('');
|
||||
@ -131,7 +129,7 @@
|
||||
if(props.multiple){
|
||||
treeValue.value = [];
|
||||
}else{
|
||||
treeValue.value = { label: null, value: null };
|
||||
treeValue.value = undefined;
|
||||
}
|
||||
} else {
|
||||
// 代码逻辑说明: issues/4173 Online JTreeSelect控件changeOptions方法未生效
|
||||
@ -224,7 +222,7 @@
|
||||
* @param text
|
||||
*/
|
||||
function translateTitle(text) {
|
||||
if (text.includes("t('") && t) {
|
||||
if (text?.includes("t('") && t) {
|
||||
return new Function('t', `return ${text}`)(t);
|
||||
}
|
||||
return text;
|
||||
@ -319,13 +317,6 @@
|
||||
emit('update:value', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本框值变化
|
||||
*/
|
||||
function onSearch(value) {
|
||||
console.log(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验条件配置是否有误
|
||||
*/
|
||||
@ -352,7 +343,7 @@
|
||||
}
|
||||
|
||||
// 代码逻辑说明: issues/4173 Online JTreeSelect控件changeOptions方法未生效
|
||||
watch(()=>props.url, async (val)=>{
|
||||
watch(()=> [props.url, props.params], async (val)=>{
|
||||
if(val){
|
||||
await loadRootByUrl();
|
||||
}
|
||||
@ -367,8 +358,8 @@
|
||||
let res = await defHttp.get({ url, params }, { isTransformResponse: false });
|
||||
if (res.success && res.result) {
|
||||
for (let i of res.result) {
|
||||
i.title = translateTitle(i.title);
|
||||
i.key = i.value;
|
||||
i.title = translateTitle(i.title ?? i.name);
|
||||
i.key = i.value ?? i.id;
|
||||
i.isLeaf = !!i.leaf;
|
||||
}
|
||||
// 代码逻辑说明: 【TV360X-87】树表编辑时不可选自己及子孙节点当父节点
|
||||
|
||||
@ -11,6 +11,22 @@
|
||||
@change="onFileChange"
|
||||
@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">
|
||||
<div v-if="!isMaxCount">
|
||||
<Icon icon="ant-design:plus-outlined" />
|
||||
@ -26,9 +42,8 @@
|
||||
</template>
|
||||
|
||||
<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 { getToken } from '/@/utils/auth';
|
||||
import { uploadUrl } from '/@/api/common/api';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
@ -40,7 +55,7 @@
|
||||
import UploadItemActions from './components/UploadItemActions.vue';
|
||||
import { split } from '/@/utils/index';
|
||||
|
||||
const { createMessage, createConfirm } = useMessage();
|
||||
const { createMessage } = useMessage();
|
||||
const { prefixCls } = useDesign('j-upload');
|
||||
const attrs = useAttrs();
|
||||
const emit = defineEmits(['change', 'update:value']);
|
||||
@ -76,7 +91,8 @@
|
||||
const fileList = ref<any[]>([]);
|
||||
const uploadGoOn = ref<boolean>(true);
|
||||
// refs
|
||||
const containerRef = ref();
|
||||
const containerRef = ref<HTMLElement>();
|
||||
const confirmedRemoveUid = ref<string | number>();
|
||||
// 是否达到了最大上传数量
|
||||
const isMaxCount = computed(() => props.maxCount > 0 && fileList.value.length >= props.maxCount);
|
||||
// 当前是否是上传图片模式
|
||||
@ -252,19 +268,26 @@
|
||||
}
|
||||
|
||||
// 删除处理事件
|
||||
function onRemove() {
|
||||
if (props.removeConfirm) {
|
||||
return new Promise((resolve) => {
|
||||
createConfirm({
|
||||
title: '删除',
|
||||
content: `确定要删除这${isImageMode.value ? '张图片' : '个文件'}吗?`,
|
||||
iconType: 'warning',
|
||||
onOk: () => resolve(true),
|
||||
onCancel: () => resolve(false),
|
||||
});
|
||||
});
|
||||
function onRemove(file) {
|
||||
if (!props.removeConfirm) {
|
||||
return true;
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
// upload组件change事件
|
||||
@ -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详情界面 图片下载按钮显示不全*/
|
||||
.upload-download-handler {
|
||||
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>
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<BasicModal @register="registerModal" :title="modalTitle" :width="width" @ok="handleOk" v-bind="$attrs">
|
||||
<JUpload ref="uploadRef" :value="value" v-bind="uploadBinds.props" @change="emitValue" />
|
||||
<BasicModal @register="registerModal" :title="modalTitle" :width="width" :minHeight="160" @ok="handleOk" v-bind="$attrs">
|
||||
<div class="j-upload-modal-content">
|
||||
<JUpload ref="uploadRef" :value="value" v-bind="uploadBinds.props" @change="emitValue" />
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, unref, reactive, computed, nextTick } from 'vue';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import JUpload from './JUpload.vue';
|
||||
import { UploadTypeEnum } from './upload.data';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
const emit = defineEmits(['change', 'update:value', 'register']);
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
|
||||
width: propTypes.number.def(520),
|
||||
});
|
||||
@ -43,3 +43,54 @@
|
||||
emit('update:value', value);
|
||||
}
|
||||
</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>
|
||||
|
||||
@ -23,16 +23,24 @@
|
||||
:maxTagCount="maxTagCount"
|
||||
@change="handleChange"
|
||||
style="width: 100%"
|
||||
@click="!disabled && openModal(false)"
|
||||
@click="handleSelectClick"
|
||||
v-bind="attrs"
|
||||
:class="{ 'j-select-biz-wrap': isMultipleMode, 'j-select-biz-has-omitted-tags': hasOmittedTags }"
|
||||
>
|
||||
<template v-if="isCustomRenderTag" #tagRender="{ label, value, option}">
|
||||
<a-tag class="ant-select-selection-item" style="margin-right: 4px">
|
||||
<span class="ant-select-selection-item-content" style="font-size: 14px;max-width: 300px" :title="tagRender(label, value, false)">{{ tagRender(label, value, true) }}</span>
|
||||
<span class="ant-select-selection-item-remove">
|
||||
<!-- update-begin--author:liaozhiyang---date:20260804---for:【LHZP-602】岗位层级过深时右侧文字完整、左侧省略;多选换行展开 -->
|
||||
<a-tag class="ant-select-selection-item j-select-biz-tag" style="margin-right: 4px">
|
||||
<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>
|
||||
</span>
|
||||
</a-tag>
|
||||
<!-- update-end--author:liaozhiyang---date:20260804---for:【LHZP-602】岗位层级过深时右侧文字完整、左侧省略;多选换行展开 -->
|
||||
</template>
|
||||
</a-select>
|
||||
</a-col>
|
||||
@ -48,15 +56,16 @@
|
||||
</div>
|
||||
</template>
|
||||
<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 { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
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({
|
||||
name: 'JSelectBiz',
|
||||
components: { LoadingOutlined },
|
||||
components: { LoadingOutlined, JLeftEllipsis },
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
showButton: propTypes.bool.def(true),
|
||||
@ -94,7 +103,18 @@
|
||||
|
||||
//存放部门名称
|
||||
const departNamePath = ref<Record<string, string>>({});
|
||||
|
||||
|
||||
const isMultipleMode = computed(() => props.multiple === 'multiple' || props.multiple === 'tags');
|
||||
// update-begin--author:liaozhiyang---date:20260811---for:【LHZP-513】online一对多部门的渲染样式有重叠
|
||||
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---for:【LHZP-513】online一对多部门的渲染样式有重叠
|
||||
|
||||
/**
|
||||
* 打开弹出框
|
||||
*/
|
||||
@ -117,49 +137,84 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* 多选tag自定义渲染
|
||||
* 多选tag自定义渲染(完整路径,省略交给 JLeftEllipsis)
|
||||
*
|
||||
* @param label
|
||||
* @param value
|
||||
* @param isEllipsis 是否省略
|
||||
* @param _isEllipsis 兼容旧调用
|
||||
*/
|
||||
function tagRender(label, value, isEllipsis) {
|
||||
if (departNamePath.value[value]) {
|
||||
//是否需要省略显示
|
||||
if(!isEllipsis){
|
||||
return departNamePath.value[value];
|
||||
} else {
|
||||
let departName = departNamePath.value[value];
|
||||
//超过20则截取后20位的字符,前面加省略号
|
||||
if(departName && departName.length >= 20){
|
||||
const name:any = departName.substring(departName.length - 20);
|
||||
return '...' + name;
|
||||
} else {
|
||||
return departName;
|
||||
}
|
||||
}
|
||||
function tagRender(label, value, _isEllipsis) {
|
||||
return getTagFullText(label, value);
|
||||
}
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20260804---for:【LHZP-602】岗位层级过深时右侧文字完整、左侧省略;多选换行展开
|
||||
// 岗位层级过深时右侧文字完整、左侧省略
|
||||
function toDisplayText(val) {
|
||||
if (val == null || val === '') {
|
||||
return '';
|
||||
}
|
||||
//判断rowKey是否为orgCode
|
||||
if(props?.rowKey && props?.rowKey === 'orgCode'){
|
||||
getDepartPathNameByOrgCode(value, label, '').then((data) => {
|
||||
departNamePath.value[value] = data;
|
||||
if (typeof val === 'string') {
|
||||
return val;
|
||||
}
|
||||
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 {
|
||||
//否则按照id处理
|
||||
getDepartPathNameByOrgCode('', label, value).then((data) => {
|
||||
departNamePath.value[value] = data;
|
||||
getDepartPathNameByOrgCode('', fallback, value).then((data) => {
|
||||
departNamePath.value[value] = toDisplayText(data) || fallback;
|
||||
});
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20260804---for:【LHZP-602】岗位层级过深时右侧文字完整、左侧省略;多选换行展开
|
||||
|
||||
/**
|
||||
* 点击选择框时打开弹窗
|
||||
* 排除点击清除按钮的情况,避免清除时误触发弹窗
|
||||
*/
|
||||
function handleSelectClick(event) {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
// 点击清除按钮(内置clear或tag移除按钮)时不打开弹窗
|
||||
if (event.target.closest('.ant-select-selection-item-remove') || event.target.closest('.ant-select-clear')) {
|
||||
return;
|
||||
}
|
||||
openModal(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* tag删除
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function handleRemoveClick(value) {
|
||||
if(selectValues?.value){
|
||||
let values = selectValues?.value.filter(item => item !== value);
|
||||
// update-begin--author:liaozhiyang---date:20260714---for:【LHZP-583】部门组件设置为只读之后还可以删除
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20260714---for:【LHZP-583】部门组件设置为只读之后还可以删除
|
||||
if (selectValues?.value) {
|
||||
let values = selectValues?.value.filter((item) => item !== value);
|
||||
handleChange(values);
|
||||
}
|
||||
}
|
||||
@ -186,7 +241,12 @@
|
||||
openModal,
|
||||
detailStr,
|
||||
tagRender,
|
||||
getTagFullText,
|
||||
handleRemoveClick,
|
||||
handleSelectClick,
|
||||
isMultipleMode,
|
||||
hasOmittedTags,
|
||||
tagReserveWidth,
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -194,22 +254,95 @@
|
||||
<style lang="less" scoped>
|
||||
.j-select-row {
|
||||
@width: 82px;
|
||||
flex-wrap: nowrap;
|
||||
align-items: flex-start;
|
||||
|
||||
.left {
|
||||
// update-begin--author:liaozhiyang---date:20260804---for:【LHZP-602】岗位层级过深时右侧文字完整、左侧省略;多选换行展开
|
||||
flex: 1 1 auto;
|
||||
width: calc(100% - @width - 8px);
|
||||
max-width: calc(100% - @width - 8px);
|
||||
min-width: 0;
|
||||
// update-end--author:liaozhiyang---date:20260804---for:【LHZP-602】岗位层级过深时右侧文字完整、左侧省略;多选换行展开
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 0 0 @width;
|
||||
width: @width;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-select-search__field) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20260804---for:【LHZP-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---for:【LHZP-513】online一对多部门的渲染样式有重叠
|
||||
: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---for:【LHZP-513】online一对多部门的渲染样式有重叠
|
||||
|
||||
: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---for:【LHZP-602】岗位层级过深时右侧文字完整、左侧省略;多选换行展开
|
||||
}
|
||||
.detailStr {
|
||||
margin: 0;
|
||||
|
||||
@ -282,7 +282,7 @@
|
||||
// 代码逻辑说明: 【issues/3656】popupdict回显
|
||||
watchEffect(() => {
|
||||
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]);
|
||||
selectRows!.value = selected;
|
||||
}
|
||||
|
||||
@ -313,9 +313,17 @@
|
||||
breadcrumb.value = [];
|
||||
}
|
||||
};
|
||||
// 同步当前部门下所有子部门的勾选状态
|
||||
const setDepartChildrenChecked = (depart, checked: boolean) => {
|
||||
depart.children?.forEach((child) => {
|
||||
child.checked = checked;
|
||||
setDepartChildrenChecked(child, checked);
|
||||
});
|
||||
};
|
||||
// 点击部门树复选框触发
|
||||
const handleDepartTreeCheck = (e, item) => {
|
||||
const { target } = e;
|
||||
setDepartChildrenChecked(item, target.checked);
|
||||
if (target.checked) {
|
||||
// 选中
|
||||
getUsersByDeptId(item['id']).then((users) => {
|
||||
@ -678,25 +686,39 @@
|
||||
ol {
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
/* 美化滚动条 */
|
||||
/* 预留滚动条空间,避免 hover 变粗时布局跳动 */
|
||||
padding-bottom: 6px;
|
||||
/* 美化滚动条:默认细,悬停变粗方便拖动 */
|
||||
// update-begin--author:liaozhiyang---date:20260804---for:【LHZP-869】部门组件面包屑有滚动条时悬停加粗便于拖动
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 3px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 2px;
|
||||
height: 4px;
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #bfbfbf;
|
||||
border-radius: 3px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
&:hover {
|
||||
background: #999;
|
||||
}
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f5f5f5;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #999;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20260804---for:【LHZP-869】部门组件面包屑有滚动条时悬停加粗便于拖动
|
||||
}
|
||||
li {
|
||||
flex: none;
|
||||
|
||||
@ -83,6 +83,7 @@
|
||||
component: 'Input',
|
||||
},
|
||||
],
|
||||
autoSubmitOnEnter: true,
|
||||
};
|
||||
//定义表格列
|
||||
const columns = [
|
||||
|
||||
@ -114,6 +114,7 @@
|
||||
size: 'small',
|
||||
formConfig: {
|
||||
//labelWidth: 200,
|
||||
autoSubmitOnEnter: true,
|
||||
baseColProps: {
|
||||
xs: 24,
|
||||
sm: 8,
|
||||
|
||||
@ -134,14 +134,23 @@
|
||||
size: 'small',
|
||||
};
|
||||
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,
|
||||
getBindValue,
|
||||
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】右侧选中列表删除无效
|
||||
watch(rowSelection.selectedRowKeys, (newVal) => {
|
||||
watch(checkedKeys, (newVal) => {
|
||||
// 代码逻辑说明: null指针异常导致控制台报错页面不显示------------
|
||||
if(tableRef.value){
|
||||
tableRef.value.setSelectedRowKeys(newVal);
|
||||
|
||||
@ -1,44 +1,56 @@
|
||||
<template>
|
||||
<BasicModal
|
||||
@register="register"
|
||||
:getContainer="getContainer"
|
||||
:canFullscreen="false"
|
||||
:title="title"
|
||||
:width="500"
|
||||
destroyOnClose
|
||||
@ok="handleOk"
|
||||
wrapClassName="j-user-select-modal2" >
|
||||
<BasicModal
|
||||
@register="register"
|
||||
:getContainer="getContainer"
|
||||
:canFullscreen="false"
|
||||
:title="title"
|
||||
width="min(500px, 92vw)"
|
||||
destroyOnClose
|
||||
@ok="handleOk"
|
||||
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 style="width: 100%">
|
||||
<a-input v-model:value="searchText" allowClear style="width: 100%" placeholder="搜索">
|
||||
<template #prefix>
|
||||
<SearchOutlined style="color: #c0c0c0" />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
<div class="list-caption">
|
||||
<span>角色列表</span>
|
||||
<span>共 {{ showDataList.length }} 项</span>
|
||||
</div>
|
||||
|
||||
<!-- tabs -->
|
||||
<div class="modal-select-list-container">
|
||||
<div class="scroll">
|
||||
<div class="content" style="right: -10px">
|
||||
|
||||
<label class="item" v-for="item in showDataList" @click="(e)=>onSelect(e, item)">
|
||||
<a-checkbox v-model:checked="item.checked">
|
||||
<span class="text">{{ item.name }}</span>
|
||||
</a-checkbox>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- 角色列表 -->
|
||||
<div class="modal-select-list-container">
|
||||
<template v-if="showDataList.length > 0">
|
||||
<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>
|
||||
<span class="role-info">
|
||||
<span class="role-name">{{ item.name }}</span>
|
||||
<span v-if="item.code" class="role-code">{{ item.code }}</span>
|
||||
</span>
|
||||
<a-checkbox v-model:checked="item.checked" class="role-checkbox" />
|
||||
</label>
|
||||
</template>
|
||||
<a-empty v-else description="暂无角色" :image-style="{ height: '60px' }" style="margin-top: 70px" />
|
||||
</div>
|
||||
|
||||
<!-- 选中用户 -->
|
||||
<div class="selected-users" style="width: 100%; overflow-x: hidden">
|
||||
<SelectedUserItem v-for="item in selectedList" :info="item" @unSelect="unSelect" />
|
||||
</div>
|
||||
<!-- 已选角色 -->
|
||||
<div v-if="selectedList.length > 0" class="selected-users-wrapper">
|
||||
<div class="selected-title">已选角色</div>
|
||||
<div class="selected-users">
|
||||
<SelectedUserItem v-for="item in selectedList" :key="item.id" :info="item" @unSelect="unSelect" />
|
||||
</div>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</div>
|
||||
<!--update-end---author:wangshuai---date:20260807---for:【LHZP-141】【表单设计器】筛选 角色选择的弹框效果不太好-->
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@ -104,6 +116,9 @@
|
||||
if(!list || list.length ==0 ){
|
||||
}else{
|
||||
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){
|
||||
if(selectedIdList.indexOf(item.id)>=0){
|
||||
item.checked = true;
|
||||
@ -236,83 +251,223 @@
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.modal-select-list-container{
|
||||
height: 352px;
|
||||
margin-top: 12px;
|
||||
overflow: auto;
|
||||
.scroll{
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
.content{
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
.item{
|
||||
padding: 7px 5px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
&:hover{
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//update-begin---author:wangshuai---date:20260807---for:【LHZP-141】【表单设计器】筛选 角色选择的弹框效果不太好
|
||||
.role-modal-body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.role-search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, fade(@primary-color, 8%), fade(@primary-color, 2%));
|
||||
border: 1px solid fade(@primary-color, 12%);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.role-search {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
border-radius: 7px;
|
||||
background-color: #fff;
|
||||
border-color: #e5e7eb;
|
||||
box-shadow: none;
|
||||
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 lang="less">
|
||||
.j-user-select-modal2 {
|
||||
.depart-select {
|
||||
.ant-select-selector {
|
||||
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%;
|
||||
}
|
||||
.j-user-select-modal2 {
|
||||
.scroll-container {
|
||||
padding-bottom: 0 !important;
|
||||
|
||||
.anticon {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
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;
|
||||
}
|
||||
// 去掉内容底部多余的 18px 外边距,弹窗底部不再空一大块
|
||||
.scrollbar__wrap {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -126,11 +126,15 @@
|
||||
}
|
||||
|
||||
const maxHeight = ref(300);
|
||||
maxHeight.value = window.innerHeight - 300;
|
||||
//update-begin---wangshuai---date:20260810 for:【LHZP-1106】审批角色 按角色添加人员 高度太大了------------
|
||||
maxHeight.value = window.innerHeight - 500;
|
||||
//update-end---wangshuai---date:20260810 for:【LHZP-1106】审批角色 按角色添加人员 高度太大了------------
|
||||
const containerStyle = computed(() => {
|
||||
return {
|
||||
'overflow-y': 'auto',
|
||||
'max-height': maxHeight.value + 'px',
|
||||
//update-begin---wangshuai---date:20260810 for:【LHZP-1106】审批角色 按角色添加人员 高度太大了------------
|
||||
'max-height': (maxHeight.value >= 300 ? maxHeight.value : 300) + 'px',
|
||||
//update-end---author:wangshuai ---date:20260810 for:【LHZP-1106】审批角色 按角色添加人员 高度太大了------------
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -162,6 +162,7 @@
|
||||
if (props.izExcludeMy) {
|
||||
excludeUserIdList.value.push(userStore.getUserInfo.id);
|
||||
}
|
||||
resetFilterState();
|
||||
//加载用户列表
|
||||
loadUserList();
|
||||
});
|
||||
@ -180,6 +181,7 @@
|
||||
selectedDepart.value = '';
|
||||
}
|
||||
function onDepartChange() {
|
||||
pageNo.value = 1;
|
||||
loadUserList();
|
||||
}
|
||||
/*--------------部门下拉框,用于筛选用户---------------*/
|
||||
@ -187,6 +189,17 @@
|
||||
/*--------------第一页 搜索框---------------*/
|
||||
const searchInputStatus = ref(false);
|
||||
const searchText = ref('');
|
||||
|
||||
/**
|
||||
* 清空数据
|
||||
*/
|
||||
function resetFilterState() {
|
||||
selectedDepart.value = '';
|
||||
searchText.value = '';
|
||||
searchInputStatus.value = false;
|
||||
pageNo.value = 1;
|
||||
}
|
||||
|
||||
function showSearchInput(e) {
|
||||
e && prevent(e);
|
||||
searchInputStatus.value = true;
|
||||
|
||||
@ -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 { isEmpty } from '@/utils/is';
|
||||
|
||||
@ -61,17 +61,16 @@ export function useSelectBiz(getList, props, emit?) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择列配置
|
||||
* 选择列配置(响应式:当 isRadioSelection prop 变化时,type 自动切换 radio/checkbox)
|
||||
*/
|
||||
const rowSelection = {
|
||||
// 代码逻辑说明: 动态设置rowSelection的type值,默认是'checkbox' ---
|
||||
const rowSelection = computed(() => ({
|
||||
type: props.isRadioSelection ? 'radio' : 'checkbox',
|
||||
columnWidth: 20,
|
||||
selectedRowKeys: checkedKeys,
|
||||
onChange: onSelectChange,
|
||||
//table4.4.0新增属性选中之后是否清空上一页下一页的数据,默认false
|
||||
preserveSelectedRowKeys:true,
|
||||
};
|
||||
}));
|
||||
|
||||
/**
|
||||
* 序号列配置
|
||||
|
||||
@ -219,6 +219,8 @@ export interface HelpComponentProps {
|
||||
text: any;
|
||||
// colour
|
||||
color: string;
|
||||
// Tooltip background colour
|
||||
backgroundColor?: string;
|
||||
// font size
|
||||
fontSize: string;
|
||||
icon: string;
|
||||
|
||||
@ -154,6 +154,7 @@ export type ComponentType =
|
||||
| 'LinkTableSelect'
|
||||
| 'LinkTableForQuery'
|
||||
| 'CascaderPcaForQuery'
|
||||
| 'JCategoryLinkage'
|
||||
| 'CascaderPcaInFilter'
|
||||
| 'UserSelect'
|
||||
| 'RoleSelect'
|
||||
@ -163,5 +164,5 @@ export type ComponentType =
|
||||
| 'RangeTime'
|
||||
| 'JRangeNumber'
|
||||
| 'JLinkTableCard'
|
||||
| 'JInputSelect';
|
||||
|
||||
| 'JInputSelect'
|
||||
| 'JAuthRuleValue';
|
||||
|
||||
@ -9,6 +9,10 @@
|
||||
import { isString } from '/@/utils/is';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
const SVG_END_WITH_FLAG = '|svg';
|
||||
// update-begin--author:liaozhiyang---date:20260713---for:【LHZP-143】online新增字段图标会闪烁
|
||||
// 缓存Iconify实例,避免每次动态import造成图标插入延迟(闪烁)
|
||||
let iconifyInstance: any = null;
|
||||
// update-end--author:liaozhiyang---date:20260713---for:【LHZP-143】online新增字段图标会闪烁
|
||||
export default defineComponent({
|
||||
name: 'Icon',
|
||||
components: { SvgIcon },
|
||||
@ -44,17 +48,21 @@
|
||||
// update-begin--author:liaozhiyang---date:20260304---for:【QQYUN-14802】新增unplugin-icons插件,及icon支持online和local两种模式
|
||||
try {
|
||||
let svg: SVGElement | null = null;
|
||||
if (import.meta.env.VITE_GLOB_ICONIFY_USE_TYPE === 'local') {
|
||||
// update-begin--author:liaozhiyang---date:20260713---for:【LHZP-143】online新增字段图标会闪烁
|
||||
if (iconifyInstance) {
|
||||
svg = iconifyInstance.renderSVG(icon, {});
|
||||
} else if (import.meta.env.VITE_GLOB_ICONIFY_USE_TYPE === 'local') {
|
||||
// 使用本地 purge-icons 图标(离线,图标已打包进产物)
|
||||
const iconifyModule = await import('@purge-icons/generated');
|
||||
const Iconify = iconifyModule.default;
|
||||
svg = Iconify.renderSVG(icon, {});
|
||||
iconifyInstance = iconifyModule.default;
|
||||
svg = iconifyInstance.renderSVG(icon, {});
|
||||
} else {
|
||||
// 使用 @iconify/iconify 在线按需加载
|
||||
const iconifyModule = await import('@iconify/iconify');
|
||||
const Iconify = iconifyModule.default;
|
||||
svg = Iconify.renderSVG(icon, {});
|
||||
iconifyInstance = iconifyModule.default;
|
||||
svg = iconifyInstance.renderSVG(icon, {});
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20260713---for:【LHZP-143】online新增字段图标会闪烁
|
||||
if (svg) {
|
||||
el.textContent = '';
|
||||
el.appendChild(svg);
|
||||
|
||||
@ -41,10 +41,16 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<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 {useDesign} from '/@/hooks/web/useDesign';
|
||||
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();
|
||||
|
||||
// 日期范围选项
|
||||
@ -98,12 +104,30 @@ watch(innerValue, (val) => {
|
||||
val = ''
|
||||
openDatePicker()
|
||||
}
|
||||
emit('change', val)
|
||||
emit('update:value', val)
|
||||
// update-begin--author:wangshuai---date:20260819---for:【LHZP-322】【表单设计器】online周、季度没转化成功
|
||||
const normalizedValue = normalizePickerValue(val)
|
||||
emit('change', normalizedValue)
|
||||
emit('update:value', normalizedValue)
|
||||
// update-end--author:wangshuai---date:20260819---for:【LHZP-322】【表单设计器】online周、季度没转化成功
|
||||
// 代码逻辑说明: 【QQYUN-9227】日期校验没清空
|
||||
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) => {
|
||||
// 如果不允许选择范围,且当前值为范围选项,则清空值
|
||||
if (!allow && isRange.value) {
|
||||
|
||||
@ -10,6 +10,10 @@ export async function registerJVxeCustom() {
|
||||
|
||||
// 注册【Popup】(普通封装方式)
|
||||
await registerAsyncComponent(JVxeTypes.popup, import('./src/components/JVxePopupCell.vue'));
|
||||
// update-begin--author:liaozhiyang---date:20260507---for:【issues/7617】vxetable子表支持popup字典
|
||||
// 注册【Popup字典】
|
||||
await registerAsyncComponent(JVxeTypes.popupDict, import('./src/components/JVxePopupDictCell.vue'));
|
||||
// update-end--author:liaozhiyang---date:20260507---for:【issues/7617】vxetable子表支持popup字典
|
||||
|
||||
// 注册【字典搜索下拉】组件(高级封装方式)
|
||||
registerComponent(JVxeTypes.selectDictSearch, DictSearchInputCell, DictSearchSpanCell);
|
||||
|
||||
@ -1,57 +1,70 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="hasFile" v-for="(file, fileKey) of [innerFile || {}]" :key="fileKey">
|
||||
<div style="position: relative">
|
||||
<template v-if="hasFile">
|
||||
<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)}%)`">
|
||||
<LoadingOutlined />
|
||||
<span style="margin-left: 5px">上传中…</span>
|
||||
<span class="j-vxe-file-content">
|
||||
<LoadingOutlined />
|
||||
<span class="j-vxe-file-name">上传中…</span>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip v-else-if="file.status === 'done'" :title="file.name">
|
||||
<Icon icon="ant-design:paper-clip" />
|
||||
<span style="margin-left: 5px">{{ ellipsisFileName }}</span>
|
||||
<a-tooltip v-else-if="file.status === 'done'" :title="file.name" :mouseEnterDelay="0.4">
|
||||
<button
|
||||
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 v-else :title="file.message || '上传失败'">
|
||||
<Icon icon="ant-design:exclamation-circle" style="color: red" />
|
||||
<span style="margin-left: 5px">{{ ellipsisFileName }}</span>
|
||||
<span class="j-vxe-file-content">
|
||||
<Icon icon="ant-design:exclamation-circle" style="color: red" />
|
||||
<span class="j-vxe-file-name">{{ shortFileName }}</span>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
|
||||
<Dropdown :trigger="['click']" placement="bottomRight" style="margin-left: 10px">
|
||||
<Dropdown v-if="file.status !== 'uploading'" :trigger="['click']" placement="bottomRight">
|
||||
<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>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item :disabled="cellProps.disabled" @click="handleMoreOperation">
|
||||
<span><Icon icon="ant-design:bars" /> 管理</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="originColumn.allowDownload !== false" @click="handleClickDownloadFile">
|
||||
<span><Icon icon="ant-design:download" /> 下载</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item :disabled="cellProps.disabled" v-if="originColumn.allowRemove !== false" @click="handleClickDeleteFile">
|
||||
<span><Icon icon="ant-design:delete" /> 删除</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item :disabled="cellProps.disabled" @click="handleMoreOperation">
|
||||
<span><Icon icon="ant-design:bars" /> 更多</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-upload
|
||||
<a-button
|
||||
v-if="!cellProps.disabledTable"
|
||||
v-show="!hasFile"
|
||||
name="file"
|
||||
:data="{ isup: 1 }"
|
||||
:multiple="false"
|
||||
:action="uploadAction"
|
||||
:headers="uploadHeaders"
|
||||
:showUploadList="false"
|
||||
v-bind="cellProps"
|
||||
@change="handleChangeUpload"
|
||||
class="j-vxe-upload-trigger"
|
||||
size="small"
|
||||
preIcon="ant-design:upload"
|
||||
:disabled="cellProps.disabled"
|
||||
@click="handleMoreOperation"
|
||||
>
|
||||
<a-button preIcon="ant-design:upload">{{ originColumn.btnText || '点击上传' }}</a-button>
|
||||
</a-upload>
|
||||
{{ originColumn.btnText || '附件' }}
|
||||
</a-button>
|
||||
<JUploadModal :value="modalValue" @register="registerModel" @change="onModalChange" />
|
||||
</div>
|
||||
</template>
|
||||
@ -75,4 +88,94 @@
|
||||
});
|
||||
</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>
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
<a-upload
|
||||
:accept="acceptFileType"
|
||||
name="file"
|
||||
:data="{ isup: 1 }"
|
||||
:data="uploadData"
|
||||
:multiple="false"
|
||||
:action="uploadAction"
|
||||
:headers="uploadHeaders"
|
||||
@ -29,7 +29,7 @@
|
||||
v-bind="cellProps"
|
||||
@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="">
|
||||
<Icon icon="ant-design:plus" />
|
||||
</div>
|
||||
|
||||
@ -8,98 +8,124 @@
|
||||
:linkFields="originColumn.linkFields || []"
|
||||
:imageField="originColumn.imageField || ''"
|
||||
:editBtnShow="!originColumn.editBtnShow"
|
||||
:queryMode="originColumn.queryMode || 'online'"
|
||||
v-bind="cellProps"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
import { vModel } from '/@/components/jeecg/JVxeTable/utils';
|
||||
import { isEmpty } from '/@/utils/is';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { defineComponent } from 'vue';
|
||||
import { JVxeComponent } from '/@/components/jeecg/JVxeTable/types';
|
||||
import { useJVxeComponent, useJVxeCompProps } from '/@/components/jeecg/JVxeTable/hooks';
|
||||
import { vModel } from '/@/components/jeecg/JVxeTable/utils';
|
||||
import LinkTableInput from '../../../../views/super/online/cgform/extend/linkTable/LinkTableInput.vue';
|
||||
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({
|
||||
name: 'JVxeLinkTableCell',
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const { innerValue, cellProps, originColumn, scrolling, handleChangeCommon, row } = useJVxeComponent(props);
|
||||
function handleChange(value, otherValues) {
|
||||
if (!isEmpty(value)) {
|
||||
Object.keys(otherValues).forEach((key) => {
|
||||
let currentValue = otherValues[key];
|
||||
vModel(currentValue, row, key);
|
||||
});
|
||||
handleChangeCommon(value);
|
||||
}
|
||||
export default defineComponent({
|
||||
name: 'JVxeLinkTableCell',
|
||||
components: { LinkTableInput },
|
||||
props: useJVxeCompProps(),
|
||||
setup(props: JVxeComponent.Props) {
|
||||
const { innerValue, cellProps, originColumn, scrolling, handleChangeCommon, row } = useJVxeComponent(props);
|
||||
function handleChange(value, otherValues) {
|
||||
if (!isEmpty(value)) {
|
||||
Object.keys(otherValues).forEach((key) => {
|
||||
let currentValue = otherValues[key];
|
||||
vModel(currentValue, row, key);
|
||||
});
|
||||
handleChangeCommon(value);
|
||||
}
|
||||
return {
|
||||
innerValue,
|
||||
cellProps,
|
||||
originColumn,
|
||||
scrolling,
|
||||
handleChange,
|
||||
row,
|
||||
};
|
||||
}
|
||||
return {
|
||||
innerValue,
|
||||
cellProps,
|
||||
originColumn,
|
||||
scrolling,
|
||||
handleChange,
|
||||
row,
|
||||
};
|
||||
},
|
||||
enhanced: {
|
||||
aopEvents: {
|
||||
},
|
||||
enhanced: {
|
||||
aopEvents: {
|
||||
interceptor: {
|
||||
'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) {
|
||||
if (className.includes('jeecg-online-pop-list-modal') || className.includes('jeecg-online-pop-modal')) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
translate: {
|
||||
enabled: true,
|
||||
async handler(value, ctx) {
|
||||
if (!value) return '';
|
||||
if (!ctx) return value;
|
||||
const { originColumn, row } = ctx.context;
|
||||
const col = originColumn.value;
|
||||
const tableName = col.tableName;
|
||||
const textField = col.textField || '';
|
||||
const valueField = col.valueField || 'id';
|
||||
// 关联记录相关的他表字段
|
||||
const linkFields: string[] = col.linkFields || [];
|
||||
if (!tableName) return value;
|
||||
// 将他表字段对应的关联表字段也一并查询出来
|
||||
},
|
||||
translate: {
|
||||
enabled: true,
|
||||
async handler(value, ctx) {
|
||||
if (!value) return '';
|
||||
if (!ctx) return value;
|
||||
const { originColumn, row } = ctx.context;
|
||||
const col = originColumn.value;
|
||||
const tableName = col.tableName;
|
||||
const textField = col.textField || '';
|
||||
const valueField = col.valueField || 'id';
|
||||
// 关联记录相关的他表字段
|
||||
const linkFields: string[] = col.linkFields || [];
|
||||
if (!tableName) return value;
|
||||
const vals = (value + '').split(',');
|
||||
const textKey = textField.split(',')[0];
|
||||
|
||||
let records: any[] = [];
|
||||
if (col.queryMode === 'table') {
|
||||
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 allSelectFields = [valueField, textField, ...extraTableFields].filter(Boolean);
|
||||
const selectFields = [...new Set(allSelectFields)].join(',');
|
||||
const params = {
|
||||
linkTableSelectFields: selectFields,
|
||||
pageSize: value.split(',').length,
|
||||
pageNo: 1,
|
||||
superQueryMatchType: 'and',
|
||||
superQueryParams: encodeURI(JSON.stringify([{ field: valueField, rule: 'in', val: value }])),
|
||||
};
|
||||
const data = await defHttp.get({ url: '/online/cgform/api/getData/' + tableName, params });
|
||||
const records = data?.records || [];
|
||||
const textKey = textField.split(',')[0];
|
||||
const vals = (value + '').split(',');
|
||||
const labels = vals.map((v) => {
|
||||
const record = records.find((r) => String(r[valueField]) === String(v));
|
||||
return record ? record[textKey] : v;
|
||||
});
|
||||
// 将他表字段的值同步写入行数据,使对应列能正确显示
|
||||
if (linkFields.length > 0 && row) {
|
||||
linkFields.forEach((lf) => {
|
||||
const [formField, tableField] = lf.split(',');
|
||||
const fieldValues = vals.map((v) => {
|
||||
const record = records.find((r) => String(r[valueField]) === String(v));
|
||||
return record ? (record[tableField] ?? '') : '';
|
||||
});
|
||||
vModel(fieldValues.join(','), row, formField);
|
||||
const [data, columns] = await Promise.all([
|
||||
defHttp.get({url: '/online/cgform/api/getData/' + tableName,
|
||||
params: {
|
||||
linkTableSelectFields: selectFields,
|
||||
pageSize: vals.length,
|
||||
pageNo: 1,
|
||||
superQueryMatchType: 'and',
|
||||
superQueryParams: encodeURI(JSON.stringify([{ field: valueField, rule: 'in', val: value }])),
|
||||
},
|
||||
}),
|
||||
getLinkTableColumns(tableName, selectFields),
|
||||
]);
|
||||
records = data?.records || [];
|
||||
records.forEach((record) => transLinkTableRecord(record, columns?.columns, columns?.dictOptions));
|
||||
}
|
||||
|
||||
const labels = vals.map((v) => {
|
||||
const record = records.find((r) => String(r[valueField]) === String(v));
|
||||
return record ? record[textKey] : v;
|
||||
});
|
||||
// 将他表字段的值同步写入行数据,使对应列能正确显示
|
||||
if (linkFields.length > 0 && row) {
|
||||
linkFields.forEach((lf) => {
|
||||
const [formField, tableField] = lf.split(',');
|
||||
const fieldValues = vals.map((v) => {
|
||||
const record = records.find((r) => String(r[valueField]) === String(v));
|
||||
return record ? (record[tableField] ?? '') : '';
|
||||
});
|
||||
}
|
||||
return labels.join(',');
|
||||
},
|
||||
vModel(fieldValues.join(','), row, formField);
|
||||
});
|
||||
}
|
||||
return labels.join(',');
|
||||
},
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
},
|
||||
} as JVxeComponent.EnhancedPartial,
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -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);
|
||||
|
||||
// 仅支持 fieldConfig:cgreport报表code,显示字段(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>
|
||||
@ -7,6 +7,7 @@ import { JVxeComponent } from '/@/components/jeecg/JVxeTable/src/types/JVxeCompo
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { Dropdown } from 'ant-design-vue';
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
import { split } from '/@/utils';
|
||||
|
||||
export function useFileCell(props, fileType: UploadTypeEnum, 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 ellipsisFileName = computed(() => {
|
||||
let length = 5;
|
||||
let file = innerFile.value;
|
||||
const shortFileName = computed(() => {
|
||||
const length = 5;
|
||||
const file = innerFile.value;
|
||||
if (!file || !file.name) {
|
||||
return '';
|
||||
}
|
||||
if (file.name.length > length) {
|
||||
return file.name.substr(0, length) + '…';
|
||||
return file.name.substr(0, length);
|
||||
}
|
||||
return file.name;
|
||||
});
|
||||
@ -38,14 +39,19 @@ export function useFileCell(props, fileType: UploadTypeEnum, options?) {
|
||||
return '';
|
||||
});
|
||||
|
||||
const uploadCount = computed(() => {
|
||||
const value = modalValue.value;
|
||||
return value ? split(value).filter(Boolean).length : 0;
|
||||
});
|
||||
|
||||
const maxCount = computed(() => {
|
||||
let maxCount = originColumn.value.maxCount;
|
||||
// online 扩展JSON
|
||||
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;
|
||||
}
|
||||
return maxCount ?? 0;
|
||||
return Number(maxCount ?? 0);
|
||||
});
|
||||
|
||||
// 点击更多按钮
|
||||
@ -56,19 +62,26 @@ export function useFileCell(props, fileType: UploadTypeEnum, options?) {
|
||||
download: true,
|
||||
...originColumn.value.props,
|
||||
maxCount: maxCount.value,
|
||||
multiple: originColumn.value.props?.multiple ?? maxCount.value !== 1,
|
||||
fileType: fileType,
|
||||
action: originColumn.value.action ?? void 0,
|
||||
// update-begin--author:liaozhiyang---date:20250526---for:【issues/9657】jvxetable组件的图片上传编辑时再次上传报错
|
||||
action: originColumn.value.action ?? setup.uploadAction,
|
||||
// update-end--author:liaozhiyang---date:20250526---for:【issues/9657】jvxetable组件的图片上传编辑时再次上传报错
|
||||
// update-begin--author:liaozhiyang---date:20250526---for:【issues/9652】vxetable上传、上传文件、上传图片加上自定义路径
|
||||
bizPath: originColumn.value.bizPath ?? 'temp',
|
||||
// update-end--author:liaozhiyang---date:20250526---for:【issues/9652】vxetable上传、上传文件、上传图片加上自定义路径
|
||||
});
|
||||
}
|
||||
|
||||
// 更多上传回调
|
||||
function onModalChange(path) {
|
||||
if (path) {
|
||||
// 代码逻辑说明: 【TV360X-235】富文本禁用状态下图片上传按钮文字看不清
|
||||
if (innerFile.value === null) {
|
||||
innerFile.value = {};
|
||||
// 从弹窗首次上传时同步补齐文件名和完成状态
|
||||
innerFile.value = fileSetValue(path);
|
||||
} else {
|
||||
innerFile.value.path = path;
|
||||
}
|
||||
innerFile.value.path = path;
|
||||
handleChangeCommon(innerFile.value);
|
||||
} else {
|
||||
// 代码逻辑说明: [issues/530]JVxeTable 的JVxeTypes.image类型,无法全部删除上传图片
|
||||
@ -79,8 +92,9 @@ export function useFileCell(props, fileType: UploadTypeEnum, options?) {
|
||||
return {
|
||||
...setup,
|
||||
modalValue,
|
||||
uploadCount,
|
||||
maxCount,
|
||||
ellipsisFileName,
|
||||
shortFileName,
|
||||
registerModel,
|
||||
onModalChange,
|
||||
handleMoreOperation,
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
import 'vditor/dist/index.css';
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { ThemeEnum } from '/@/enums/appEnum';
|
||||
import { sanitizeRichText } from '/@/utils/htmlSanitizer';
|
||||
|
||||
const converter = new showdown.Converter();
|
||||
converter.setOption('tables', true);
|
||||
@ -19,7 +20,7 @@
|
||||
value: { type: String },
|
||||
class: { type: String },
|
||||
});
|
||||
const getHtmlData = computed(() => converter.makeHtml(props.value || ''));
|
||||
const getHtmlData = computed(() => sanitizeRichText(converter.makeHtml(props.value || '')));
|
||||
|
||||
// 代码逻辑说明: 【issues/918】MarkdownViewer加上暗黑主题
|
||||
const isDarkTheme = ref(false);
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
:maxHeight="getProps.maxHeight"
|
||||
:height="getWrapperHeight"
|
||||
:visible="visibleRef"
|
||||
:modalHeaderHeight="getProps.modalHeaderHeight"
|
||||
:modalHeaderHeight="isNoTitle ? 0 : getProps.modalHeaderHeight"
|
||||
:modalFooterHeight="footer !== undefined && !footer ? 0 : getProps.modalFooterHeight"
|
||||
v-bind="omit(getProps.wrapperProps, 'visible', 'height', 'modalFooterHeight')"
|
||||
@ext-height="handleExtHeight"
|
||||
@ -266,6 +266,13 @@
|
||||
emit('fullScreen',val);
|
||||
});
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20260805---for:【LHZP-865】basicModal无标题时不预留 header 高度,有标题时重新计算内容区高度
|
||||
watch(isNoTitle, () => {
|
||||
nextTick(() => {
|
||||
modalMethods.redoModalHeight();
|
||||
});
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20260805---for:【LHZP-865】basicModal无标题时不预留 header 高度,有标题时重新计算内容区高度
|
||||
return {
|
||||
handleCancel,
|
||||
getBindValue,
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
const spinRef = ref<ElRef>(null);
|
||||
const realHeightRef = ref(0);
|
||||
const minRealHeightRef = ref(0);
|
||||
const availableHeightRef = ref(0);
|
||||
|
||||
let realHeight = 0;
|
||||
|
||||
@ -116,7 +117,7 @@
|
||||
return {
|
||||
minHeight: `${props.minHeight === null ? defaultMiniHeight : props.minHeight}px`,
|
||||
// 代码逻辑说明: 【QQYUN-7641】basicModal组件添加MaxHeight属性
|
||||
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();
|
||||
|
||||
try {
|
||||
const modalDom = bodyDom.parentElement && bodyDom.parentElement.parentElement;
|
||||
const modalDom = bodyDom.closest('.ant-modal');
|
||||
if (!modalDom) return;
|
||||
//update-begin---author:scott ---date:20260814 for:修复弹窗内容高度自适应及底部操作按钮遮挡问题-----------
|
||||
const spinEl = unref(spinRef);
|
||||
if (!spinEl) return;
|
||||
|
||||
const modalRect = getComputedStyle(modalDom as Element).top;
|
||||
const modalTop = Number.parseInt(modalRect);
|
||||
let maxHeight = window.innerHeight - modalTop * 2 + (props.footerOffset! || 0) - props.modalFooterHeight - props.modalHeaderHeight;
|
||||
// 使用 CSS 布局位置计算可用高度,避免打开动画导致位置读取不稳定
|
||||
const modalTop = Number.parseFloat(getComputedStyle(modalDom).top) || 0;
|
||||
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) {
|
||||
maxHeight -= 26;
|
||||
}
|
||||
availableHeightRef.value = maxHeight;
|
||||
await nextTick();
|
||||
const spinEl = unref(spinRef);
|
||||
|
||||
if (!spinEl) return;
|
||||
await nextTick();
|
||||
// if (!realHeight) {
|
||||
realHeight = spinEl.scrollHeight;
|
||||
|
||||
@ -145,7 +145,7 @@
|
||||
width: 0;
|
||||
height: 0;
|
||||
cursor: pointer;
|
||||
background-color: rgba(144, 147, 153, 0.3);
|
||||
background-color: rgba(144, 147, 153, 0.6);
|
||||
border-radius: inherit;
|
||||
transition: 0.3s background-color;
|
||||
|
||||
|
||||
@ -18,7 +18,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
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 'tinymce/themes/silver';
|
||||
import 'tinymce/icons/default/icons';
|
||||
@ -31,6 +33,7 @@
|
||||
import 'tinymce/plugins/lists';
|
||||
import 'tinymce/plugins/preview';
|
||||
import 'tinymce/plugins/image';
|
||||
import 'tinymce/plugins/media';
|
||||
import { defineComponent, computed, nextTick, ref, unref, watch, onDeactivated, onBeforeUnmount, onMounted } from 'vue';
|
||||
import ImgUpload from './ImgUpload.vue';
|
||||
import ProcessMask from './ProcessMask.vue';
|
||||
@ -205,6 +208,38 @@
|
||||
};
|
||||
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',
|
||||
...options,
|
||||
setup: (editor: any) => {
|
||||
|
||||
@ -4,16 +4,16 @@
|
||||
// colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration
|
||||
|
||||
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 =
|
||||
'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 = [
|
||||
'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';
|
||||
|
||||
@ -168,7 +168,7 @@
|
||||
}
|
||||
try {
|
||||
item.status = UploadResultStatus.UPLOADING;
|
||||
const { data } = await props.api?.(
|
||||
const res = await props.api?.(
|
||||
{
|
||||
data: {
|
||||
...(props.uploadParams || {}),
|
||||
@ -182,8 +182,16 @@
|
||||
item.percent = complete;
|
||||
}
|
||||
);
|
||||
// update-begin--author:liaozhiyang---date:20260804---for:【LHZP-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.responseData = data;
|
||||
item.responseData = { ...data, url };
|
||||
// update-end--author:liaozhiyang---date:20260804---for:【LHZP-1385】修复上传示例报错
|
||||
return {
|
||||
success: true,
|
||||
error: null,
|
||||
|
||||
@ -434,6 +434,7 @@
|
||||
async function queryUserList(params) {
|
||||
params['column'] = 'sort';
|
||||
params['order'] = 'ASC';
|
||||
params['status'] = '1';
|
||||
if(departInfo.selectedKeys.length == 0 && positionInfo.selectedKeys.length == 0 && userGroupInfo.selectedKeys.length == 0 && params.realname){
|
||||
params['realname'] = `*${params.realname.trim()}*`;
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
:treeData="treeData"
|
||||
:checkStrictly="true"
|
||||
@select="onSelectDepart"
|
||||
style="height: calc(100vh - 390px); min-height: 150px; overflow: auto"
|
||||
class="post-rank-tree"
|
||||
></BasicTree>
|
||||
</template>
|
||||
<a-empty v-else description="无岗位信息" />
|
||||
@ -73,7 +73,38 @@
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.depart-rule-tree :deep(.scrollbar__bar) {
|
||||
pointer-events: none;
|
||||
// update-begin--author:liaozhiyang---date:20260811---for:【LHZP-1694】选中用户组件切换到岗位时滚动条拖动不了
|
||||
.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---for:【LHZP-1694】选中用户组件切换到岗位时滚动条拖动不了
|
||||
</style>
|
||||
|
||||
@ -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' } });
|
||||
|
||||
/**
|
||||
* 获取指定部门用户列表
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
<a-button v-if="addBtnCfg.enabled && showAdd" type="primary" :preIcon="addBtnCfg.buttonIcon" :disabled="disabled" :loading="deleting" @click="trigger('add')">
|
||||
<span>{{ addBtnCfg.buttonName }}</span>
|
||||
</a-button>
|
||||
<slot name="toolbarAfterAdd" :size="btnSize" :selectedRowIds="selectedRowIds" />
|
||||
<a-button v-if="showSave" preIcon="ant-design:save-outlined" :disabled="disabled" @click="trigger('save')">
|
||||
<span>保存</span>
|
||||
</a-button>
|
||||
@ -21,7 +22,7 @@
|
||||
<a-button preIcon="ant-design:delete-outlined" @click="trigger('clearSelection')">清空选择</a-button>
|
||||
</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">
|
||||
<span>{{ collapsed ? '展开' : '收起' }}</span>
|
||||
<Icon :icon="collapsed ? 'ant-design:down-outlined' : 'ant-design:up-outlined'" />
|
||||
|
||||
@ -54,7 +54,10 @@
|
||||
function handleChange(_mom, dateStr) {
|
||||
// 代码逻辑说明: 【QQYUN-9205】一对多(jVxetable组件date)支持年,年月,年度度,年周
|
||||
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 {
|
||||
handleChangeCommon(dateStr);
|
||||
}
|
||||
|
||||
@ -4,10 +4,29 @@
|
||||
<LoadingOutlined />
|
||||
<span> 加载中…</span>
|
||||
</template>
|
||||
<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>
|
||||
<!-- 分组模式:options 中含有 group 字段时,按分组渲染 -->
|
||||
<template v-if="hasGroup">
|
||||
<template v-for="(groupOptions, groupName) of groupedSelectOptions" :key="groupName">
|
||||
<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>
|
||||
</a-select>
|
||||
</template>
|
||||
@ -79,6 +98,24 @@
|
||||
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 ---------
|
||||
|
||||
// 多选、搜索type
|
||||
@ -169,6 +206,8 @@
|
||||
innerValue,
|
||||
selectProps,
|
||||
selectOptions,
|
||||
hasGroup,
|
||||
groupedSelectOptions,
|
||||
handleChange,
|
||||
handleBlur,
|
||||
};
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
v-if="!cellProps.disabledTable"
|
||||
v-show="!hasFile"
|
||||
name="file"
|
||||
:data="{ isup: 1 }"
|
||||
:data="uploadData"
|
||||
:multiple="false"
|
||||
:action="originColumn.action"
|
||||
:headers="uploadHeaders"
|
||||
|
||||
@ -37,7 +37,16 @@ export function useJVxeUploadCell(props: JVxeComponent.Props, options?) {
|
||||
});
|
||||
const hasFile = computed(() => innerFile.value != null);
|
||||
const responseName = computed(() => originColumn.value.responseName ?? 'message');
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20250526---for:【issues/9652】vxetable上传、上传文件、上传图片加上自定义路径
|
||||
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---for:【issues/9652】vxetable上传、上传文件、上传图片加上自定义路径
|
||||
watch(
|
||||
innerValue,
|
||||
(val) => {
|
||||
@ -110,6 +119,7 @@ export function useJVxeUploadCell(props: JVxeComponent.Props, options?) {
|
||||
innerFile,
|
||||
uploadAction,
|
||||
uploadHeaders,
|
||||
uploadData,
|
||||
hasFile,
|
||||
responseName,
|
||||
handleChangeUpload,
|
||||
|
||||
@ -141,7 +141,9 @@ function customComponentAddStar(columns) {
|
||||
].includes(type)
|
||||
) {
|
||||
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】用户、部门、图片、文件、开关的必填标识显示非红色
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,6 +186,28 @@ export function useJVxeComponent(props: JVxeComponent.Props) {
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20250526---for:【QQYUN-15511】一对多子表的options通过js修改后,刷新后编辑回显的不是label
|
||||
// 监听 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---for:【QQYUN-15511】一对多子表的options通过js修改后,刷新后编辑回显的不是label
|
||||
|
||||
/** 通用处理 change 事件 */
|
||||
function handleChangeCommon($value, force = false) {
|
||||
const newValue = enhanced.getValue($value, ctx);
|
||||
|
||||
@ -61,6 +61,7 @@ export function useToolbar(props: JVxeTableProps, data: JVxeDataProps, methods:
|
||||
},
|
||||
{
|
||||
toolbarPrefix: $slots.toolbarPrefix,
|
||||
toolbarAfterAdd: $slots.toolbarAfterAdd,
|
||||
toolbarSuffix: $slots.toolbarSuffix,
|
||||
}
|
||||
);
|
||||
|
||||
@ -22,12 +22,18 @@ export function useValidateRules(args: HandleArgs) {
|
||||
delete rule.pattern;
|
||||
} else {
|
||||
// 兼容Online表单的特殊规则
|
||||
let reg: any = rule.pattern;
|
||||
for (let foo of fooPatterns) {
|
||||
if (foo.value === rule.pattern) {
|
||||
rule.pattern = foo.pattern;
|
||||
reg = foo.pattern;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20260714---for:【LHZP-172】行编辑pattern校验空值时不应触发,仅有值时校验(非空由required规则负责)
|
||||
const regExp = reg instanceof RegExp ? reg : new RegExp(reg);
|
||||
delete rule.pattern;
|
||||
rule.validator = emptySkipPatternValidator(regExp);
|
||||
// update-end--author:liaozhiyang---date:20260714---for:【LHZP-172】行编辑pattern校验空值时不应触发,仅有值时校验(非空由required规则负责)
|
||||
}
|
||||
} else if (typeof rule.handler === 'function') {
|
||||
// 自定义函数校验
|
||||
@ -78,6 +84,19 @@ function handlerConvertToValidator(event) {
|
||||
});
|
||||
}
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20260714---for:【LHZP-172】行编辑pattern校验空值时不应触发,仅有值时校验(非空由required规则负责)
|
||||
/** 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---for:【LHZP-172】行编辑pattern校验空值时不应触发,仅有值时校验(非空由required规则负责)
|
||||
|
||||
// 兼容 online 的规则
|
||||
const fooPatterns = [
|
||||
{ title: '非空', value: '*', pattern: /^.+$/ },
|
||||
|
||||
@ -70,7 +70,14 @@ function preventClosingPopUp(this: any, params) {
|
||||
return false;
|
||||
}
|
||||
// 点击的标签是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;
|
||||
}
|
||||
// 点击的是日期选择器
|
||||
|
||||
@ -7,6 +7,13 @@
|
||||
border-color: #606266;
|
||||
}
|
||||
|
||||
// 图片、附件上传按钮在行内保持紧凑,避免抢占表格内容的视觉层级
|
||||
.j-vxe-upload-trigger.ant-btn {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sort--active {
|
||||
border-color: @primary-color;
|
||||
}
|
||||
@ -111,5 +118,17 @@
|
||||
.vxe-header--column.col--ellipsis>.vxe-cell .vxe-cell--title{
|
||||
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】用户、部门、图片、文件、开关的必填标识显示非红色
|
||||
|
||||
}
|
||||
|
||||
@ -40,6 +40,9 @@ export enum JVxeTypes {
|
||||
|
||||
// 以下为自定义组件
|
||||
popup = 'popup',
|
||||
// update-begin--author:liaozhiyang---date:20260507---for:【QQYUN-7961】vxetable子表支持popup字典
|
||||
popupDict = 'popupDict',
|
||||
// update-end--author:liaozhiyang---date:20260507---for:【QQYUN-7961】vxetable子表支持popup字典
|
||||
selectDictSearch = 'selectDictSearch',
|
||||
radio = 'radio',
|
||||
image = 'image',
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
valueFormat="YYYY-MM-DD"
|
||||
:placeholder="'请选择' + item.label"
|
||||
v-model:value="queryParam[item.field]"
|
||||
style="width: 100%"
|
||||
></a-date-picker>
|
||||
</template>
|
||||
<template v-else>
|
||||
@ -40,6 +41,7 @@
|
||||
:show-time="true"
|
||||
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||
v-model:value="queryParam[item.field]"
|
||||
style="width: 100%"
|
||||
></a-date-picker>
|
||||
</template>
|
||||
<template v-else>
|
||||
@ -66,7 +68,7 @@
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</template>
|
||||
<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 v-else>
|
||||
<a-date-picker
|
||||
@ -115,6 +117,7 @@
|
||||
:pidField="item.pidField"
|
||||
:pidValue="item.pidValue"
|
||||
:hasChildField="item.hasChildField"
|
||||
:converIsLeafVal="item.converIsLeafVal"
|
||||
load-triggle-change
|
||||
>
|
||||
</JTreeSelect>
|
||||
@ -131,7 +134,12 @@
|
||||
<template #label>
|
||||
<span :title="item.label" class="label-text">{{ item.label }}</span>
|
||||
</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 v-else-if="item.view === CompTypeEnum.SelUser" :labelCol="labelCol" :class="'jeecg-online-search'">
|
||||
@ -324,4 +332,10 @@
|
||||
text-overflow: ellipsis;
|
||||
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>
|
||||
|
||||
@ -255,9 +255,17 @@ export function usePopBiz(ob, tableRef?) {
|
||||
if (query) {
|
||||
url = url + query
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20260804---for:【LHZP-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---for:【LHZP-1296】地址栏有参数时首次请求即带上,避免再触发两次请求
|
||||
//缓存key
|
||||
let groupIdKey = props.groupId ? `${props.groupId}${url}` : '';
|
||||
httpGroupRequest(() => defHttp.get({ url }, { isTransformResponse: false, successMessageMode: 'none' }), groupIdKey).then((res) => {
|
||||
let groupIdKey = props.groupId ? `${props.groupId}${url}${JSON.stringify(params)}` : '';
|
||||
httpGroupRequest(() => defHttp.get({ url, params }, { isTransformResponse: false, successMessageMode: 'none' }), groupIdKey).then((res) => {
|
||||
if (res.success) {
|
||||
initDictOptionData(res.result.dictOptions);
|
||||
cgRpConfigId.value = props.id;
|
||||
@ -621,15 +629,14 @@ export function usePopBiz(ob, tableRef?) {
|
||||
dynamicParamHandler(res.result);
|
||||
queryInfo.value = res.result;
|
||||
console.log('queryInfo==>', queryInfo.value);
|
||||
//查询条件加载后再请求数据
|
||||
// update-begin--author:liaozhiyang---date:20260804---for:【JPopupOnlReport】已有列表数据时不再二次 loadData,避免地址栏带参重复请求
|
||||
// 查询条件加载后再请求数据;若 loadColumnsAndData 已带回数据,直接回填,不再重复请求
|
||||
if (data) {
|
||||
setDataSource(data);
|
||||
//传递路由参数和动态参数,不生效,
|
||||
loadData(1);
|
||||
} else {
|
||||
//没有传递data时查询数据
|
||||
loadData(1);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20260804---for:【JPopupOnlReport】已有列表数据时不再二次 loadData,避免地址栏带参重复请求
|
||||
} else {
|
||||
createMessage.warning(res.message);
|
||||
}
|
||||
@ -695,7 +702,7 @@ export function usePopBiz(ob, tableRef?) {
|
||||
}
|
||||
dataSource.value = data.records;
|
||||
// 代码逻辑说明: issues/426 修复356时候引入的回归错误 JPopupOnlReportModal.vue 中未修改
|
||||
tableRef?.value && tableRef?.value?.setPagination({
|
||||
tableRef?.value && tableRef?.value?.setPagination?.({
|
||||
total: Number(data.total)
|
||||
})
|
||||
} else {
|
||||
|
||||
@ -17,7 +17,8 @@
|
||||
<span>{{ item.toUserId_dictText }}</span>
|
||||
<Tooltip class="comment-last-content" @openChange="(v)=>visibleChange(v, item)">
|
||||
<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>
|
||||
<message-outlined />
|
||||
</Tooltip>
|
||||
@ -85,7 +86,7 @@
|
||||
// dayjs.extend(customParseFormat);
|
||||
|
||||
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 MyComment from './MyComment.vue';
|
||||
import { list, saveOne, deleteOne, useCommentWithFile, useEmojiHtml, queryById, getGloablEmojiIndex } from './useComment';
|
||||
@ -115,21 +116,21 @@
|
||||
setup(props) {
|
||||
const { createMessage } = useMessage();
|
||||
const dataList = ref([]);
|
||||
const { userInfo } = useUserStore();
|
||||
const userStore = useUserStore();
|
||||
const dayjs = inject('$dayjs')
|
||||
const listRef = ref(null);
|
||||
/**
|
||||
* 获取当前用户名称
|
||||
*/
|
||||
function getMyname() {
|
||||
if (userInfo.realname) {
|
||||
return userInfo.realname.substr(0, 2);
|
||||
if (userStore.getUserInfo.realname) {
|
||||
return userStore.getUserInfo.realname.substr(0, 2);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getMyAvatar(){
|
||||
return getFileAccessHttpUrl(userInfo.avatar);
|
||||
return getFileAccessHttpUrl(userStore.getUserInfo.avatar);
|
||||
}
|
||||
|
||||
// 获取头像
|
||||
@ -201,7 +202,7 @@
|
||||
async function replyComment(item, content, fileList) {
|
||||
console.log(content, item);
|
||||
let obj = {
|
||||
fromUserId: userInfo.id,
|
||||
fromUserId: userStore.getUserInfo.id,
|
||||
toUserId: item.fromUserId,
|
||||
commentId: item.id,
|
||||
commentContent: content
|
||||
@ -213,7 +214,7 @@
|
||||
//评论
|
||||
async function sendComment(content, fileList) {
|
||||
let obj = {
|
||||
fromUserId: userInfo.id,
|
||||
fromUserId: userStore.getUserInfo.id,
|
||||
commentContent: content
|
||||
}
|
||||
await saveCommentAndFiles(obj, fileList)
|
||||
@ -270,6 +271,7 @@
|
||||
async function visibleChange(v, item){
|
||||
if(v==true){
|
||||
if(!item.commentId_dictText){
|
||||
item.commentIdLoading = true;
|
||||
const data = await queryById(item.commentId);
|
||||
if(data.success == true){
|
||||
item.commentId_dictText = data.result.commentContent
|
||||
@ -277,11 +279,13 @@
|
||||
console.error(data.message)
|
||||
item.commentId_dictText='该评论已被删除';
|
||||
}
|
||||
item.commentIdLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 代码逻辑说明: 【TV360X-932】评论加上换行
|
||||
const lineFeed = (content) => {
|
||||
if (!content) return '';
|
||||
return content.replace(/\n/g, '<br>');
|
||||
};
|
||||
|
||||
@ -365,3 +369,14 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
|
||||
@ -9,8 +9,8 @@
|
||||
<edit-outlined v-else/>
|
||||
</span>
|
||||
<span class="log-item-content">
|
||||
<a @click="handleClickPerson">@{{item.createName || item.createBy}}</a>
|
||||
{{ item.dataContent }}
|
||||
<span @click="handleClickPerson">@{{item.createName || item.createBy}}</span>
|
||||
{{ item.dataContent }}
|
||||
</span>
|
||||
<div class="log-item-date">
|
||||
<Tooltip :title="item.createTime">
|
||||
@ -158,6 +158,9 @@
|
||||
width: 100%;
|
||||
word-break: break-word;
|
||||
box-sizing: border-box;
|
||||
span {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
.log-item-date{
|
||||
word-wrap: break-word;
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {ref, watch, computed, inject} from 'vue';
|
||||
import { ref, watch, computed, nextTick } from 'vue';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { UserAddOutlined, PaperClipOutlined, SmileOutlined } from '@ant-design/icons-vue';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
@ -130,7 +130,7 @@
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
const commentRef = ref();
|
||||
const commentRef = ref<HTMLTextAreaElement | null>(null);
|
||||
watch(
|
||||
() => props.inputFocus,
|
||||
(val) => {
|
||||
@ -275,9 +275,14 @@
|
||||
function handleBlur() {
|
||||
showHtml.value = true;
|
||||
// 代码逻辑说明: 解决多行获取焦点和失去焦点时滚动位置不一致
|
||||
setTimeout(() => {
|
||||
commentContentRef.value!.scrollTop = commentRef.value.scrollTop;
|
||||
}, 0);
|
||||
nextTick(() => {
|
||||
const commentContentElement = commentContentRef.value;
|
||||
const commentElement = commentRef.value;
|
||||
if (!commentContentElement || !commentElement) {
|
||||
return;
|
||||
}
|
||||
commentContentElement.scrollTop = commentElement.scrollTop;
|
||||
});
|
||||
}
|
||||
|
||||
const commentActive = ref(false);
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
<div class="complex">
|
||||
<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)+')'}">
|
||||
<!-- <img style="height: 100%;" :src="getImageSrc(item)">-->
|
||||
</div>
|
||||
@ -25,7 +25,7 @@
|
||||
<!-- 文件 -->
|
||||
<template v-else>
|
||||
<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 class="content-bottom" :title="item.name">
|
||||
<span>{{ item.name }}</span>
|
||||
@ -78,7 +78,7 @@
|
||||
},
|
||||
emits:['select'],
|
||||
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();
|
||||
|
||||
@ -126,6 +126,7 @@
|
||||
clear,
|
||||
isImage,
|
||||
getImageSrc,
|
||||
getImageTypeIcon,
|
||||
viewImage,
|
||||
registerModel,
|
||||
showFileModal,
|
||||
|
||||
@ -17,6 +17,7 @@ import { createImgPreview } from '/@/components/Preview';
|
||||
import data from "emoji-mart-vue-fast/data/apple.json";
|
||||
import { EmojiIndex } from "emoji-mart-vue-fast/src";
|
||||
import { encryptByBase64 } from '/@/utils/cipher';
|
||||
import { sanitizeCommentHtml } from '/@/utils/htmlSanitizer';
|
||||
|
||||
enum Api {
|
||||
list = '/sys/comment/listByForm',
|
||||
@ -422,7 +423,7 @@ export function useEmojiHtml(globalEmojiIndex){
|
||||
if(!text){
|
||||
return ''
|
||||
}
|
||||
return text.replace(COLONS_REGEX, function (match, p1, p2) {
|
||||
const emojiHtml = text.replace(COLONS_REGEX, function (match, p1, p2) {
|
||||
const before = p1 || ''
|
||||
if (endsWith(before, 'alt="') || endsWith(before, 'data-text="')) {
|
||||
return match
|
||||
@ -433,7 +434,7 @@ export function useEmojiHtml(globalEmojiIndex){
|
||||
}
|
||||
return before + emoji2Html(emoji)
|
||||
})
|
||||
return text;
|
||||
return sanitizeCommentHtml(emojiHtml);
|
||||
}
|
||||
|
||||
function endsWith(str, temp){
|
||||
|
||||
@ -555,7 +555,7 @@
|
||||
const [registerHighlightColorModal, { openModal: openHighlightColorModal }] = useModal();
|
||||
|
||||
function initEditor() {
|
||||
setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
const fontDom: any = document.querySelector('.jeecg-menu-item-font');
|
||||
fontOptionDom.value = fontDom.querySelector('.options');
|
||||
//字体大小
|
||||
@ -594,7 +594,13 @@
|
||||
searchCollapseDom.value = document.querySelector('.jeecg-menu-item-search-collapse');
|
||||
|
||||
setTableStyle()
|
||||
createEditor(props.content);
|
||||
// update-begin--author:liaozhiyang---date:20270724---for:【LHZP-1452】修复word目录需要修改下正文才显示
|
||||
await createEditor(props.content);
|
||||
// 初始化完成后主动渲染一次目录,避免需要点击目录按钮才显示
|
||||
if (formState.showCataLogDom) {
|
||||
updateCatalog();
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20270724---for:【LHZP-1452】修复word目录需要修改下正文才显示
|
||||
// 组件挂载后添加事件监听器
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
//监听Ctrl或Cmd键和S键事件
|
||||
|
||||
@ -56,7 +56,8 @@ export function useThirdLogin() {
|
||||
bindingPhoneModal.value = true;
|
||||
let strings = token.split(',');
|
||||
thirdUserUuid.value = strings[1];
|
||||
} else {
|
||||
} else if (token.split('.').length === 3) {
|
||||
// JWT 格式校验:必须包含两个 '.',过滤飞书 SDK 等第三方 postMessage(如 [tea-sdk]ready)
|
||||
doThirdLogin(token);
|
||||
}
|
||||
} else if (typeof token === 'object') {
|
||||
|
||||
@ -44,6 +44,16 @@ export function useDragNotice() {
|
||||
console.log('仪表盘监听点击事件参数', record);
|
||||
//1.路径的话,判断外部链接还是内部路由跳转
|
||||
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') {
|
||||
url = record[url];
|
||||
@ -145,7 +155,7 @@ export function useDragNotice() {
|
||||
const iframes: any = document.getElementsByClassName('jeecg-iframe-page__main');
|
||||
// 将 HTMLCollection 转换为数组
|
||||
const iframeArray = Array.from(iframes);
|
||||
if (currentRoute.value?.meta?.frameSrc && currentRoute.value?.meta?.frameSrc.indexOf('/drag/view?pageId=') >= 0) {
|
||||
if (currentRoute.value?.meta?.frameSrc && isDragFrameSrc(currentRoute.value.meta.frameSrc)) {
|
||||
const targetIframe: any = iframeArray.find((iframe: any) => iframe.src == currentRoute.value?.meta?.frameSrc);
|
||||
console.log('targetIframe', targetIframe);
|
||||
if (targetIframe) {
|
||||
@ -153,6 +163,16 @@ export function useDragNotice() {
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 判断 frameSrc 是否为仪表盘页面
|
||||
* 支持两种 URL 形式:
|
||||
* 1. /drag/view?pageId=xxx(query 形式,老版本约定)
|
||||
* 2. /drag/view/xxx(path 形式,新版本约定)
|
||||
*/
|
||||
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 {
|
||||
initDragWebSocket: initWebSocket,
|
||||
handleOpenType,
|
||||
|
||||
@ -6,6 +6,17 @@ import { validateCasLogin } from '/@/api/sys/user';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
const globSetting = useGlobSetting();
|
||||
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() {
|
||||
// 代码逻辑说明: 【QQYUN-7805】SSO登录强制用http #957---
|
||||
let locationUrl = document.location.protocol +"//" + window.location.host + '/';
|
||||
@ -18,17 +29,18 @@ export function useSso() {
|
||||
let token = getToken();
|
||||
let ticket = getUrlParam('ticket');
|
||||
if (!token) {
|
||||
const loginServiceUrl = getCasServiceUrl();
|
||||
if (ticket) {
|
||||
await validateCasLogin({
|
||||
ticket: ticket,
|
||||
service: locationUrl,
|
||||
service: loginServiceUrl,
|
||||
}).then((res) => {
|
||||
const userStore = useUserStore();
|
||||
userStore.setToken(res.token);
|
||||
return userStore.afterLoginAction(true, {});
|
||||
});
|
||||
} else {
|
||||
window.location.href = globSetting.casBaseUrl + '/login?service=' + encodeURIComponent(locationUrl);
|
||||
window.location.href = globSetting.casBaseUrl + '/login?service=' + encodeURIComponent(loginServiceUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
import { BasicForm, useForm } from '/@/components/Form';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
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 { useUserStore } from '@/store/modules/user';
|
||||
|
||||
@ -36,27 +36,8 @@
|
||||
const { createMessage } = useMessage();
|
||||
const confirmLoading = ref(false);
|
||||
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({
|
||||
labelWidth: 100,
|
||||
@ -73,60 +54,11 @@
|
||||
required: true,
|
||||
message: t('layout.changePassword.pleaseEnterNewPassword'),
|
||||
},
|
||||
//update-begin---author:wangshuai ---date:2026-06-29 for:【QQYUN-16619】根据三级等保开关动态切换校验规则-----------
|
||||
{
|
||||
validator:(_, 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();
|
||||
}
|
||||
validator: createPasswordValidator(oldPassword.value),
|
||||
},
|
||||
/* {
|
||||
// 使用一个函数来自定义验证逻辑,客户使用这个
|
||||
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为以上的数字或字母');
|
||||
}
|
||||
//不能相同字符(如111、aaa)连续3位或3位以上
|
||||
const reg2 = /(\w)*(\w)\2{2}(\w)*!/g
|
||||
if (reg2.test(value)) {
|
||||
console.log('密码不能出现相同字符连续3位或3位以上')
|
||||
return false
|
||||
}
|
||||
// 如果所有规则都通过,返回 resolve()
|
||||
return Promise.resolve();
|
||||
}
|
||||
}*/
|
||||
//update-end---author:wangshuai ---date:2026-06-29 for:【QQYUN-16619】根据三级等保开关动态切换校验规则-----------
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -149,7 +81,6 @@
|
||||
});
|
||||
|
||||
const getTitle = computed(() => '修改密码');
|
||||
const userStore = useUserStore();
|
||||
const { createMessage: $message } = useMessage();
|
||||
|
||||
|
||||
|
||||
@ -108,10 +108,13 @@
|
||||
* 弹窗打开前处理
|
||||
*/
|
||||
async function show() {
|
||||
//加载部门
|
||||
await loadDepartList();
|
||||
//加载租户
|
||||
await loadTenantList();
|
||||
// update-begin--author:liaozhiyang---date:20260812---for:【LHZP-1359】部门和租户任一有数据都需要展示出来,除非都没数据时则提示
|
||||
const [canSwitchDepart, canSwitchTenant] = await Promise.all([loadDepartList(), loadTenantList()]);
|
||||
if (!canSwitchDepart && !canSwitchTenant) {
|
||||
createMessage.info('当前没有可切换的租户或部门');
|
||||
return;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20260812---for:【LHZP-1359】部门和租户任一有数据都需要展示出来,除非都没数据时则提示
|
||||
//标题配置
|
||||
if (unref(isMultiTenant) && unref(isMultiDepart)) {
|
||||
currTitle.value = '切换租户和部门';
|
||||
@ -132,18 +135,20 @@
|
||||
*/
|
||||
async function loadDepartList() {
|
||||
const result = await getUserDeparts();
|
||||
if (!result.list || result.list.length == 0) {
|
||||
return;
|
||||
// update-begin--author:liaozhiyang---date:20260812---for:【LHZP-1359】部门和租户任一有数据都需要展示出来,除非都没数据时则提示
|
||||
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);
|
||||
//TODO 筛选出用户的部门信息(排除公司或者岗位配置)后期回滚
|
||||
const userDeparts = result.list.filter((item) => item.orgCategory == '2');
|
||||
// update-end--author:liaozhiyang---date:20260812---for:【LHZP-1359】部门和租户任一有数据都需要展示出来,除非都没数据时则提示
|
||||
let currentDepart = userDeparts.filter((item) => item.orgCode == result.orgCode);
|
||||
departList.value = userDeparts;
|
||||
// 代码逻辑说明: JHHB-790 用户部门变更,会出现这个情况(因为之前设置的这里只切换部门,过滤了公司和岗位信息)
|
||||
const hasCurrentDepart = userDeparts.some(item => item.orgCode == result.orgCode);
|
||||
departSelected.value = hasCurrentDepart && currentDepart && currentDepart.length > 0 ? result.orgCode : '';
|
||||
currentDepartName.value = currentDepart && currentDepart.length > 0 ? currentDepart[0].departName : '';
|
||||
isMultiDepart.value = true;
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
*加载租户信息
|
||||
@ -151,7 +156,7 @@
|
||||
async function loadTenantList() {
|
||||
const result = await getUserTenants();
|
||||
if (!result.list || result.list.length == 0) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
let tenantId = userStore.getTenant;
|
||||
let currentTenant = result.list.filter((item) => item.id == tenantId);
|
||||
@ -159,6 +164,7 @@
|
||||
tenantList.value = result.list;
|
||||
tenantSelected.value = tenantId;
|
||||
isMultiTenant.value = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -198,23 +204,24 @@
|
||||
*/
|
||||
function departResolve() {
|
||||
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();
|
||||
} else {
|
||||
const result = await selectDepart({
|
||||
username: userStore.getUserInfo.username,
|
||||
orgCode: unref(departSelected),
|
||||
loginTenantId: unref(tenantSelected),
|
||||
});
|
||||
if (result.userInfo) {
|
||||
const userInfo = result.userInfo;
|
||||
userStore.setUserInfo(userInfo);
|
||||
resolve();
|
||||
} else {
|
||||
requestFailed(result);
|
||||
userStore.logout();
|
||||
reject();
|
||||
}
|
||||
requestFailed(result);
|
||||
userStore.logout();
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
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 { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import BasicForm from '/@/components/Form/src/BasicForm.vue';
|
||||
@ -43,10 +43,11 @@
|
||||
required: true,
|
||||
message: t('layout.changePassword.pleaseEnterNewPassword'),
|
||||
},
|
||||
//update-begin---author:wangshuai ---date:2026-06-29 for:【QQYUN-16619】根据三级等保开关动态切换校验规则-----------
|
||||
{
|
||||
pattern: /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[~!@#$%^&*()_+`\-={}:";'<>?,./]).{8,}$/,
|
||||
message: '密码由 8 位及以上数字、大小写字母和特殊符号组成!',
|
||||
},
|
||||
validator: createPasswordValidator(),
|
||||
},
|
||||
//update-end---author:wangshuai ---date:2026-06-29 for:【QQYUN-16619】根据三级等保开关动态切换校验规则-----------
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
import { removeAuthCache, setAuthCache } from '/src/utils/auth';
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
import { getRefPromise } from '/@/utils/index';
|
||||
import { refreshDragCache } from "@/api/common/api";
|
||||
import { refreshDragCache, refreshHomeCache } from "@/api/common/api";
|
||||
|
||||
type MenuEvent = 'logout' | 'doc' | 'lock' | 'cache' | 'depart' | 'defaultHomePage' | 'password' | 'account';
|
||||
const { createMessage } = useMessage();
|
||||
@ -124,6 +124,8 @@
|
||||
const result = await refreshCache();
|
||||
const dragRes = await refreshDragCache();
|
||||
console.log('dragRes', dragRes);
|
||||
await refreshHomeCache();
|
||||
await userStore.getUserInfoAction();
|
||||
if (result.success) {
|
||||
const res = await queryAllDictItems();
|
||||
removeAuthCache(DB_DICT_DATA_KEY);
|
||||
|
||||
@ -11,6 +11,7 @@ export default {
|
||||
|
||||
inputText: 'Please enter',
|
||||
chooseText: 'Please choose',
|
||||
uploadText: 'Please upload',
|
||||
|
||||
redo: 'Refresh',
|
||||
back: 'Back',
|
||||
|
||||
@ -11,6 +11,7 @@ export default {
|
||||
|
||||
inputText: '请输入',
|
||||
chooseText: '请选择',
|
||||
uploadText: '请上传',
|
||||
|
||||
redo: '刷新',
|
||||
back: '返回',
|
||||
|
||||
@ -128,7 +128,7 @@ function dynamicImport(dynamicViewsModules: Record<string, () => Promise<Recorda
|
||||
return;
|
||||
}
|
||||
// online/aiflow 本地未找到,尝试从懒加载包中按需加载
|
||||
if (component.startsWith('/super/airag/aiflow')) {
|
||||
if (component.startsWith('/super/airag/aiflow/')) {
|
||||
return () => {
|
||||
return loadPackageComponent(component).then((factory) => (factory ? factory() : Promise.reject(`组件 ${component} 未找到`)));
|
||||
};
|
||||
|
||||
@ -7,6 +7,21 @@ import {createRouter as createVueRouter, destroyRouter, router} from './router'
|
||||
|
||||
// 白名单应该包含基本静态路由
|
||||
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[]) =>
|
||||
array.forEach((item) => {
|
||||
WHITE_NAME_LIST.push(item.name);
|
||||
@ -40,7 +55,7 @@ export function createRouter() {
|
||||
export function resetRouter() {
|
||||
router.getRoutes().forEach((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);
|
||||
}
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user