将开源版本的修改打补丁应用到商业版

This commit is contained in:
wsm
2026-04-09 19:36:52 +08:00
parent d8e3a63df1
commit 7cf7a03983
105 changed files with 18321 additions and 2386 deletions
+2499 -2337
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -75,11 +75,13 @@ import JRangeNumber from './jeecg/components/JRangeNumber.vue';
import LinkTableForQuery from '/@/components/jeecg/super/desform/LinkTableForQuery.vue';
import CascaderPcaForQuery from '/@/components/jeecg/super/desform/CascaderPcaForQuery.vue';
import UserSelect from './jeecg/components/userSelect/index.vue';
import JRangeDate from './jeecg/components/JRangeDate.vue'
import JRangeTime from './jeecg/components/JRangeTime.vue'
import JInputSelect from './jeecg/components/JInputSelect.vue'
import JRangeDate from './jeecg/components/JRangeDate.vue';
import JRangeTime from './jeecg/components/JRangeTime.vue';
import JInputSelect from './jeecg/components/JInputSelect.vue';
import RoleSelectInput from './jeecg/components/roleSelect/RoleSelectInput.vue';
import {DatePickerInFilter, CascaderPcaInFilter} from "@/components/InFilter";
import SzSelectUser from '@/components/semri/userComponent/SzUserSelect.vue';
import SzSelectUserByDept from '@/components/semri/userComponent/SzSelectUserByDept.vue';
const componentMap = new Map<ComponentType, Component>();
@@ -178,8 +180,8 @@ componentMap.set('RangeDate', JRangeDate);
componentMap.set('RangeTime', JRangeTime);
componentMap.set('RoleSelect', RoleSelectInput);
componentMap.set('JInputSelect', JInputSelect);
componentMap.set('SzSelectUser', SzSelectUser);
componentMap.set('SzSelectUserByDept',SzSelectUserByDept)
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component);
@@ -50,10 +50,10 @@
},
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
//排除用户id的集合
excludeUserIdList:{
excludeUserIdList: {
type: Array,
default: () => [],
}
},
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
},
emits: ['options-change', 'change', 'update:value'],
@@ -114,9 +114,13 @@
/**
* 需要监听一下excludeUserIdList,否则modal获取不到
*/
watch(()=>props.excludeUserIdList,(data)=>{
excludeUserIdList.value = data;
},{ immediate: true })
watch(
() => props.excludeUserIdList,
(data) => {
excludeUserIdList.value = data;
},
{ immediate: true }
);
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
/**
@@ -169,7 +173,7 @@
send(tempSave);
};
const send = (values) => {
let result = typeof props.value == "string" ? values.join(',') : values;
let result = typeof props.value == 'string' ? values.join(',') : values;
emit('update:value', result);
emit('change', result);
};
@@ -163,6 +163,11 @@
field: 'realname',
component: 'JInput',
},
{
label: '密级',
field: 'userSecurityLevel',
component: 'JInput',
},
],
};
//定义表格列
+3 -1
View File
@@ -158,5 +158,7 @@ export type ComponentType =
| 'RangeTime'
| 'JRangeNumber'
| 'JLinkTableCard'
| 'JInputSelect';
| 'JInputSelect'
| 'SzSelectUser'
| 'SzSelectUserByDept';
@@ -0,0 +1,158 @@
<template>
<div class="JSelectDept">
<JSelectBiz @change="handleSelectChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs" />
<a-form-item v-show="false">
<SzDeptSelectModal
@register="regModal"
@getSelectResult="setValue"
v-bind="getBindValue"
:rowKey="rowKey"
:labelKey="labelKey"
@close="handleClose"
/>
</a-form-item>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, watchEffect, watch, provide, unref, toRaw } from 'vue';
import SzDeptSelectModal from './modal/SzDeptSelectModal.vue';
import JSelectBiz from '@/components/Form/src/jeecg/components/base/JSelectBiz.vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
import { cloneDeep } from 'lodash-es';
export default defineComponent({
name: 'SzSelectDept',
components: {
SzDeptSelectModal,
JSelectBiz,
},
inheritAttrs: false,
props: {
// 外部 v-model 绑定的值(ID字符串或数组)
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
// 显示文本的字段名
labelKey: {
type: String,
default: 'departName',
},
// 存储值的字段名
rowKey: {
type: String,
default: 'id',
},
// 是否允许多选
multiple: propTypes.bool.def(true),
},
emits: ['options-change', 'change', 'update:value', 'select'],
setup(props, { emit }) {
const [regModal, { openModal }] = useModal();
const attrs = useAttrs();
const selectOptions = ref<SelectValue>([]); // 存储 [{id, departName}]
const selectValues = reactive<Recordable>({ value: [] }); // 存储 [id]
const loadingEcho = ref<boolean>(false);
let tempSave: any = [];
// 依赖注入给 JSelectBiz 组件使用,用于显示标签和加载状态
provide('selectOptions', selectOptions);
provide('selectValues', selectValues);
provide('loadingEcho', loadingEcho);
/**
* 监听外部 value 变化进行初始化
*/
watchEffect(() => {
tempSave = [];
if (props.value) {
initValue();
} else {
selectValues.value = [];
}
});
/**
* 初始化值:将字符串转为数组
*/
function initValue() {
let val = props.value ? props.value : [];
if (typeof val === 'string' && val !== 'null' && val !== 'undefined') {
selectValues.value = val.split(',');
tempSave = val.split(',');
} else {
selectValues.value = Array.isArray(val) ? val : [];
tempSave = cloneDeep(selectValues.value);
}
}
/**
* 【关键】接收弹窗传回的精简数据
* @param options 格式:[{id: 'xxx', departName: 'xxx'}]
* @param values 格式:['xxx']
*/
function setValue(options, values) {
// 将后端字段 [id, departName] 转换为 a-select 认识的 [value, label]
selectOptions.value = options.map((item) => ({
label: item.departName, // 这一步让框里显示出名字
value: item.id, // 这一步让值对应上
}));
selectValues.value = values;
send(values);
}
/**
* 发送数据给父组件
*/
const send = (values) => {
let result = typeof props.value === 'string' ? values.join(',') : values;
emit('update:value', result);
emit('change', result);
};
function handleOpen() {
openModal(true, { isUpdate: false });
}
function handleClose() {
if (tempSave.length) {
selectValues.value = cloneDeep(tempSave);
} else {
send(tempSave);
}
}
function handleSelectChange(values) {
tempSave = cloneDeep(values);
send(tempSave);
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
return {
attrs,
selectOptions,
selectValues,
loadingEcho,
getBindValue,
regModal,
setValue,
handleOpen,
handleClose,
handleSelectChange,
};
},
});
</script>
<style lang="less" scoped>
.JSelectDept {
> .ant-form-item {
display: none;
}
}
</style>
@@ -0,0 +1,12 @@
// 1. 修改引入路径,Jeecg 使用的是 defHttp
import { defHttp } from '@/utils/http/axios';
// 根据部门+角色+密级筛选用户
export function queryDept(params) {
return defHttp.get({
// 2. 注意:Jeecg 默认配置了 baseUrl,通常不需要写 /jeecg-boot 前缀
url: '/sys/sysDepart/querySecondLevelDepts',
// 3. GET 请求的参数 key 是 params,而不是 data
params,
});
}
@@ -0,0 +1,245 @@
<template>
<div>
<BasicModal
v-bind="$attrs"
@register="register"
:title="modalTitle"
:width="showSelected ? '1200px' : '900px'"
wrapClassName="j-user-select-modal"
@ok="handleOk"
@cancel="handleCancel"
:maxHeight="maxHeight"
:centered="true"
destroyOnClose
@visible-change="visibleChange"
>
<a-row>
<a-col :span="showSelected ? 18 : 24">
<BasicTable
ref="tableRef"
:columns="columns"
:scroll="tableScroll"
v-bind="getBindValue"
:useSearchForm="true"
:formConfig="formConfig"
:api="queryDept"
:searchInfo="searchInfo"
:rowSelection="rowSelection"
:indexColumnProps="indexColumnProps"
:afterFetch="afterFetch"
:beforeFetch="beforeFetch"
>
<template #tableTitle></template>
</BasicTable>
</a-col>
<a-col :span="showSelected ? 6 : 0" v-if="showSelected" style="padding-left: 10px">
<BasicTable v-bind="selectedTable" :dataSource="selectRows">
<template #action="{ record }">
<a href="javascript:void(0)" @click="handleDeleteSelected(record)">
<Icon icon="ant-design:delete-outlined" style="color: #ff4d4f"></Icon>
</a>
</template>
</BasicTable>
</a-col>
</a-row>
</BasicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, unref, ref, watch, h } from 'vue';
import { BasicModal, useModalInner } from '@/components/Modal';
import { queryDept } from '../dept';
import { createAsyncComponent } from '@/utils/factory/createAsyncComponent';
import { useSelectBiz } from '@/components/Form/src/jeecg/hooks/useSelectBiz';
import { useAttrs } from '@/hooks/core/useAttrs';
import { selectProps } from '@/components/Form/src/jeecg/props/props';
import { Icon } from '@/components/Icon';
import { JDictSelectTag } from "@/components/Form";
import { render } from "@/utils/common/renderUtils";
export default defineComponent({
name: 'SzDeptSelectModal',
components: {
BasicModal,
Icon,
BasicTable: createAsyncComponent(() => import('/src/components/Table/src/BasicTable.vue'), {
loading: true,
}),
},
props: {
...selectProps,
modalTitle: {
type: String,
default: '选择部门',
},
},
emits: ['register', 'getSelectResult', 'close'],
setup(props, { emit }) {
const tableScroll = ref<any>({ x: false });
const tableRef = ref();
const maxHeight = ref(600);
// 内部状态注册
const [register, { closeModal }] = useModalInner(() => {
if (window.innerWidth < 900) {
tableScroll.value = { x: 900 };
} else {
tableScroll.value = { x: false };
}
// 延迟设置选中行,确保 table 渲染完成
setTimeout(() => {
if (tableRef.value) {
tableRef.value.setSelectedRowKeys(selectValues['value'] || []);
}
}, 300);
});
const attrs = useAttrs();
// 基础配置
const config = {
canResize: false,
bordered: true,
size: 'small',
rowKey: 'id', // 必须确保后端返回 id 字段
labelKey: 'departName',
};
const getBindValue = Object.assign({}, unref(props), unref(attrs), config);
// 使用 Jeecg 核心 Hook
const [{ rowSelection, visibleChange, selectValues, indexColumnProps, getSelectResult, handleDeleteSelected, selectRows, showSelected }] =
useSelectBiz(queryDept, getBindValue, emit);
const searchInfo = ref(props.params);
// 监听外部 keys 变化同步给 table
watch(rowSelection.selectedRowKeys, (newVal) => {
if (tableRef.value) {
tableRef.value.setSelectedRowKeys(newVal);
}
});
// 搜索表单配置
const formConfig = {
// 在这里增加 labelWidth
labelWidth: 65,
baseColProps: { xs: 24, sm: 12, md: 8, lg: 8, xl: 8, xxl: 8 },
schemas: [
{
label: '部门类别',
field: 'departType',
// 也可以在具体的 schema 里单独控制
// labelWidth: 100,
component: 'JDictSelectTag',
componentProps: {
dictCode: 'depart_type',
placeholder: '请选择部门类别',
stringToNumber: true,
'onUpdate:value': (val) => {
// 这里的 val 就是选中的值
console.log('监听到值改变:', val);
// 触发 Table 刷新
tableRef.value?.reload();
},
},
},
],
};
// 主表列定义
const columns = [
{ title: '部门名称', dataIndex: 'departName', width: 180, align: 'left' },
{ title: '机构编码', dataIndex: 'orgCode', width: 120 },
{
title: '机构类型',
dataIndex: 'departType',
width: 100,
customRender: ({ text }) => {
return render.renderDict(text, 'depart_type');
},
},
{ title: '排序', dataIndex: 'departOrder', width: 80 },
];
// 右侧已选表配置
const selectedTable = {
pagination: false,
showIndexColumn: false,
scroll: { y: 390 },
size: 'small',
canResize: false,
bordered: true,
rowKey: 'id',
columns: [
{ title: '部门名称', dataIndex: 'departName', width: 100 },
{ title: '操作', dataIndex: 'action', align: 'center', width: 40, slots: { customRender: 'action' } },
],
};
/**
* 【修改后的 handleOk】
* 只提取 id 和 departName
*/
function handleOk() {
// 直接从 tableRef 获取选中的原始行数据,避免 label 为 undefined 的问题
const rows = tableRef.value.getSelectRows();
// 1. 提取精简的对象列表
const options = rows.map((item) => ({
id: item.id,
departName: item.departName,
}));
// 2. 提取 ID 列表
const values = rows.map((item) => item.id);
console.log('确定选择内容:', options);
// 3. 向上抛出事件
emit('getSelectResult', options, values);
closeModal();
}
function afterFetch(record) {
return record;
}
function beforeFetch(params) {
return Object.assign({ column: 'createTime', order: 'desc' }, params);
}
const handleCancel = () => {
emit('close');
};
return {
handleOk,
searchInfo,
register,
indexColumnProps,
visibleChange,
getBindValue,
queryDept, // 必须暴露给 template 中的 :api
formConfig,
columns,
rowSelection,
selectRows,
showSelected,
selectedTable,
handleDeleteSelected,
tableScroll,
tableRef,
afterFetch,
handleCancel,
maxHeight,
beforeFetch,
};
},
});
</script>
@@ -0,0 +1,257 @@
<!-- 文件上传弹窗组件 -->
<template>
<a-modal
:visible="visible"
title="选择文件密级并上传"
:width="600"
:confirm-loading="uploading"
@ok="handleUpload"
@cancel="handleCancel"
:destroyOnClose="true"
>
<a-form layout="vertical" style="padding: 0 20px">
<a-form-item label="文件密级" required>
<a-radio-group v-model:value="selectedSecretLevel">
<a-radio
v-for="option in secretLevelOptions"
:key="option.value"
:value="option.value"
>
<a-tag :color="getSecretLevelColor(option.value)">
{{ option.label }}
</a-tag>
</a-radio>
</a-radio-group>
<!-- <div class="secret-tip">-->
<!-- <Icon icon="ant-design:info-circle-outlined" />-->
<!-- 文件密级将决定谁能查看此文件请谨慎选择-->
<!-- </div>-->
</a-form-item>
<a-form-item label="选择文件" required>
<a-upload
:before-upload="beforeUpload"
:file-list="tempFileList"
@remove="handleRemoveFile"
:multiple="multiple"
:max-count="multiple ? undefined : 1"
>
<a-button>
<Icon icon="ant-design:file-add-outlined" />
{{ multiple ? '选择文件(可多选)' : '选择文件' }}
</a-button>
</a-upload>
</a-form-item>
<a-alert
v-if="selectedFiles.length > 0"
:message="`已选择 ${selectedFiles.length} 个文件`"
type="info"
show-icon
>
<template #description>
<div class="selected-files-summary">
<div v-for="(f, idx) in selectedFiles" :key="f.uid" class="summary-item">
{{ idx + 1}}. {{ f.name }}{{ formatFileSize(f.size) }}
</div>
</div>
</template>
</a-alert>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { Icon } from '/src/components/Icon';
import { useMessage } from '/src/hooks/web/useMessage';
import { uploadMyFile } from '/src/api/common/api';
const { createMessage } = useMessage();
const props = defineProps({
visible: {
type: Boolean,
required: true
},
secretLevelOptions: {
type: Array,
default: () => []
},
businessId: {
type: String,
default: ''
},
multiple: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['update:visible', 'upload-success', 'upload-error']);
const selectedSecretLevel = ref<number>(1);
const selectedFiles = ref<any[]>([]);
const tempFileList = ref<any[]>([]);
const uploading = ref<boolean>(false);
function getSecretLevelColor(level: number) {
const colors = {
1: 'success',
2: 'processing',
3: 'warning',
4: 'error'
};
return colors[level] || 'default';
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
function beforeUpload(file) {
const isValidSize = file.size / 1024 / 1024 < 500;
if (!isValidSize) {
createMessage.error(`文件"${file.name}"大小超过500MB,已跳过`);
return false;
}
if (!props.multiple) {
selectedFiles.value = [file];
tempFileList.value = [{
uid: file.uid,
name: file.name,
status: 'done',
size: file.size
}];
} else {
selectedFiles.value = [...selectedFiles.value, file];
tempFileList.value = [...tempFileList.value, {
uid: file.uid,
name: file.name,
status: 'done',
size: file.size
}];
}
return false;
}
function handleRemoveFile(file) {
selectedFiles.value = selectedFiles.value.filter(f => f.uid !== file.uid);
tempFileList.value = tempFileList.value.filter(f => f.uid !== file.uid);
}
async function handleUpload() {
if (selectedFiles.value.length === 0) {
createMessage.warning('请先选择文件');
return;
}
if (!selectedSecretLevel.value) {
createMessage.warning('请选择文件密级');
return;
}
uploading.value = true;
try {
const uploadPromises = selectedFiles.value.map(file => {
const formData = new FormData();
formData.append('file', file);
formData.append('secretLevel', selectedSecretLevel.value.toString());
if (props.businessId) {
formData.append('business_id', props.businessId);
}
return uploadMyFile('/sys/file/upload', formData).then(res => ({
rawFile: file,
response: res
}));
});
const results = await Promise.allSettled(uploadPromises);
let successCount = 0;
let failCount = 0;
const successResults: any[] = [];
results.forEach((r) => {
if (r.status === 'fulfilled') {
const result = r.value?.response?.data;
if (result && result.success) {
successCount++;
successResults.push({
fileId: result.result.fileId,
fileName: r.value.rawFile.name,
filePath: result.result.savePath,
fileSize: r.value.rawFile.size,
secretLevel: selectedSecretLevel.value,
fileUploadType: result.result.fileUploadType,
bound: !!props.businessId
});
} else {
failCount++;
}
} else {
failCount++;
}
});
if (successCount > 0) {
createMessage.success(`成功上传 ${successCount} 个文件${failCount > 0 ? `${failCount} 个失败` : ''}`);
emit('upload-success', { success: true, results: successResults });
handleCancel();
} else {
createMessage.error('所有文件上传失败');
emit('upload-error', new Error('所有文件上传失败'));
}
} catch (error) {
console.error('上传失败:', error);
createMessage.error('文件上传失败');
emit('upload-error', error);
} finally {
uploading.value = false;
}
}
function handleCancel() {
emit('update:visible', false);
setTimeout(() => {
selectedSecretLevel.value = 1;
selectedFiles.value = [];
tempFileList.value = [];
}, 300);
}
watch(() => props.visible, (newVal) => {
if (newVal && props.secretLevelOptions.length > 0) {
selectedSecretLevel.value = props.secretLevelOptions[0].value;
}
});
</script>
<style lang="less" scoped>
.secret-tip {
margin-top: 8px;
color: #faad14;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
.selected-files-summary {
max-height: 150px;
overflow-y: auto;
.summary-item {
padding: 2px 0;
font-size: 12px;
color: #595959;
}
}
</style>
@@ -0,0 +1,467 @@
<!-- 文件上传下载删除组件 -->
<template>
<div class="s-upload-file-container">
<!-- 上传按钮区域 -->
<div class="upload-btn-wrapper" v-if="!disabled">
<a-button type="primary" @click="handleOpenUploadModal">
<Icon icon="ant-design:upload-outlined" />
选择文件上传
</a-button>
</div>
<!-- 文件列表区域 -->
<div class="file-list-wrapper">
<div v-if="displayFileList.length > 0" class="file-list-header">
<span class="file-count"> {{ displayFileList.length }} 个文件</span>
<a-button
type="link"
size="small"
:loading="downloadingAll"
@click="handleDownloadAll"
>
<Icon icon="ant-design:download-outlined" />
下载全部
</a-button>
</div>
<a-spin :spinning="loading">
<a-empty v-if="!loading && displayFileList.length === 0" description="暂无文件" />
<div v-else class="file-list">
<div
v-for="file in displayFileList"
:key="file.fileId || file.id"
class="file-item"
>
<!-- 文件信息 -->
<div class="file-info">
<Icon icon="ant-design:file-outlined" class="file-icon" />
<span class="file-name">{{ file.fileName }}</span>
<a-tag :color="getSecretLevelColor(file.secretLevel)" class="secret-tag">
{{ getSecretLevelLabel(file.secretLevel) }}
</a-tag>
<a-tag v-if="file.pending" color="orange" class="secret-tag">
待保存
</a-tag>
<span v-if="file.fileSize" class="file-size">{{ formatFileSize(file.fileSize) }}</span>
</div>
<!-- 操作按钮 -->
<div class="file-actions">
<a-button
type="link"
size="small"
@click="handleDownload(file)"
>
<Icon icon="ant-design:download-outlined" />
下载
</a-button>
<a-button
v-if="!disabled"
type="link"
size="small"
danger
@click="handleDelete(file)"
>
<Icon icon="ant-design:delete-outlined" />
删除
</a-button>
</div>
</div>
</div>
</a-spin>
</div>
<!-- 上传弹窗 -->
<SFileUploadModal
v-model:visible="uploadModalVisible"
:secret-level-options="secretLevelOptions"
:business-id="bussinessId"
:multiple="multiple"
@upload-success="handleUploadSuccess"
@upload-error="handleUploadError"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { Icon } from '/src/components/Icon';
import { defHttp } from '/src/utils/http/axios';
import { useMessage } from '/src/hooks/web/useMessage';
import SFileUploadModal from './SFileUploadModal.vue';
const { createMessage, createConfirm } = useMessage();
const props = defineProps({
bussinessId: {
type: String,
default: ''
},
bussinessSecretLevel: {
type: Number,
default: 1,
validator: (value: number) => [1, 2, 3, 4].includes(value)
},
disabled: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['upload-success', 'delete-success', 'upload-error']);
const serverFileList = ref<any[]>([]);
const pendingFileList = ref<any[]>([]);
const loading = ref<boolean>(false);
const uploadModalVisible = ref<boolean>(false);
const binding = ref<boolean>(false);
const downloadingAll = ref<boolean>(false);
const displayFileList = computed(() => {
return [
...serverFileList.value.map(f => ({ ...f, pending: false })),
...pendingFileList.value.map(f => ({ ...f, pending: true }))
];
});
const SECRET_LEVEL_CONFIG = {
1: { label: '非密', color: 'success' },
2: { label: '内部', color: 'processing' },
3: { label: '秘密', color: 'warning' },
4: { label: '机密', color: 'error' }
};
function getSecretLevelInfo(level: number) {
return SECRET_LEVEL_CONFIG[level] || SECRET_LEVEL_CONFIG[1];
}
function getSecretLevelLabel(level: number) {
return getSecretLevelInfo(level).label;
}
function getSecretLevelColor(level: number) {
return getSecretLevelInfo(level).color;
}
function formatFileSize(bytes: number): string {
if (!bytes || bytes === 0) return '';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
const secretLevelOptions = computed(() => {
const level = props.bussinessSecretLevel;
const options = [];
options.push({ label: '非密', value: 1 });
if (level >= 2) options.push({ label: '内部', value: 2 });
if (level >= 3) options.push({ label: '秘密', value: 3 });
if (level >= 4) options.push({ label: '机密', value: 4 });
return options;
});
function handleOpenUploadModal() {
uploadModalVisible.value = true;
}
async function handleUploadSuccess(result: any) {
if (result.results && Array.isArray(result.results)) {
result.results.forEach((fileInfo: any) => {
if (!fileInfo.bound) {
pendingFileList.value.push({
fileId: fileInfo.fileId,
fileName: fileInfo.fileName,
filePath: fileInfo.filePath,
fileSize: fileInfo.fileSize,
secretLevel: fileInfo.secretLevel,
fileUploadType: fileInfo.fileUploadType
});
}
});
}
if (props.bussinessId) {
await loadFileList();
}
emit('upload-success', result);
}
function handleUploadError(error: any) {
emit('upload-error', error);
}
async function handleDownload(file: any) {
try {
const fileUrl = file.filePath;
const fileName = file.fileName || fileUrl.substring(fileUrl.lastIndexOf('/') + 1);
const domainUrl = import.meta.env.VITE_GLOB_DOMAIN_URL;
const data = await defHttp.get(
{ url: `${domainUrl}/sys/file/download`, params: { fileUrl }, responseType: 'blob' },
{ isTransformResponse: false }
);
if (!data || data.size === 0) {
createMessage.warning('文件下载失败');
return;
}
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(new Blob([data]), fileName);
} else {
const blobUrl = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement('a');
link.style.display = 'none';
link.href = blobUrl;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
}
} catch (error) {
console.error('下载失败:', error);
createMessage.error('文件下载失败');
}
}
async function handleDownloadAll() {
const files = displayFileList.value;
if (files.length === 0) return;
downloadingAll.value = true;
let successCount = 0;
let failCount = 0;
for (const file of files) {
try {
await handleDownload(file);
successCount++;
} catch (error) {
failCount++;
console.error(`下载文件"${file.fileName}"失败:`, error);
}
}
downloadingAll.value = false;
if (successCount > 0) {
createMessage.success(`成功下载 ${successCount} 个文件${failCount > 0 ? `${failCount} 个失败` : ''}`);
} else {
createMessage.error('所有文件下载失败');
}
}
function handleDelete(file: any) {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: `确定要删除文件"${file.fileName}"吗?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
loading.value = true;
await defHttp.get({
url: '/sys/file/delete',
params: { id: file.fileId || file.id }
});
createMessage.success('文件删除成功');
if (file.pending) {
pendingFileList.value = pendingFileList.value.filter(f => f.fileId !== file.fileId);
} else {
serverFileList.value = serverFileList.value.filter(f => (f.id || f.fileId) !== (file.id || file.fileId));
}
emit('delete-success', file.fileId || file.id);
} catch (error) {
console.error('删除失败:', error);
createMessage.error('文件删除失败');
} finally {
loading.value = false;
}
}
});
}
async function loadFileList() {
if (!props.bussinessId) {
return;
}
try {
loading.value = true;
const result = await defHttp.get({
url: '/sys/file/getFileInfoByBussinessId',
params: { bussinessId: props.bussinessId }
});
if (Array.isArray(result)) {
serverFileList.value = result;
} else {
serverFileList.value = [];
}
} catch (error) {
console.error('加载文件列表失败:', error);
serverFileList.value = [];
createMessage.error('加载文件列表失败');
} finally {
loading.value = false;
}
}
//这个是在表单id还没有生成之前上传了附件,附件表没有存储bussinessId。当业务表保存后有id了,再把bussinessId更新到这些附件上去
async function bindBussinessId(newBussinessId: string) {
if (!newBussinessId) {
createMessage.warning('bussinessId 不能为空');
return false;
}
if (pendingFileList.value.length === 0) {
return true;
}
const fileIds = pendingFileList.value.map(f => f.fileId).join(',');
try {
binding.value = true;
await defHttp.get({
url: '/sys/file/updateFilesInfo',
params: {
bussinessId: newBussinessId,
fileIds: fileIds
}
});
createMessage.success(`成功关联 ${pendingFileList.value.length} 个文件`);
pendingFileList.value = [];
await loadFileList();
return true;
} catch (error) {
console.error('关联文件失败:', error);
createMessage.error('关联文件失败,请重试');
return false;
} finally {
binding.value = false;
}
}
function getPendingFileIds(): string[] {
return pendingFileList.value.map(f => f.fileId);
}
function hasPendingFiles(): boolean {
return pendingFileList.value.length > 0;
}
defineExpose({
bindBussinessId,
getPendingFileIds,
hasPendingFiles,
loadFileList
});
watch(() => props.bussinessId, (newVal) => {
if (newVal && newVal.length > 0) {
loadFileList();
} else {
serverFileList.value = [];
}
}, { immediate: true });
</script>
<style lang="less" scoped>
.s-upload-file-container {
width: 100%;
max-width: 100%;
.upload-btn-wrapper {
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.file-list-wrapper {
width: 100%;
min-height: 100px;
padding: 12px;
background: #fafafa;
border-radius: 4px;
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
.file-count {
font-size: 13px;
color: #8c8c8c;
}
}
}
.file-list {
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
margin-bottom: 8px;
transition: all 0.3s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&:last-child {
margin-bottom: 0;
}
.file-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
overflow: hidden;
.file-icon {
font-size: 16px;
color: #1890ff;
flex-shrink: 0;
}
.file-name {
font-size: 14px;
color: #262626;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.secret-tag {
margin-left: 4px;
font-weight: 500;
flex-shrink: 0;
}
.file-size {
font-size: 12px;
color: #8c8c8c;
flex-shrink: 0;
}
}
.file-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
}
}
}
</style>
@@ -0,0 +1,231 @@
# SUploadFile 文件上传组件使用指南
## 组件介绍
`SUploadFile` 是一个支持密级管理的通用文件上传下载删除组件,适用于 JeecgBoot 项目。
## 组件位置
```
jeecgboot-vue3/src/components/semri/fileComponent/SUploadFile.vue
```
## 功能特性
- ✅ 文件加密上传(支持密级选择)
- ✅ 文件解密下载
- ✅ 文件删除
- ✅ 密级权限控制(文件密级 <= 业务密级)
- ✅ 支持禁用状态(禁用后不能上传和删除)
- ✅ 文件列表展示(带密级标签)
- ✅ 支持业务关联(通过 businessId
## Props 参数
| 参数 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| bussinessId | String | '' | 否 | 业务ID,用于关联和查询文件 |
| bussinessSecretLevel | Number | 3 | 否 | 业务密级(1-非密,2-内部,3-秘密,4-机密) |
| disabled | Boolean | false | 否 | 是否禁用(禁用后不能上传和删除,但可以下载) |
## Events 事件
| 事件名 | 参数 | 说明 |
|--------|------|------|
| upload-success | result | 上传成功时触发,返回上传结果 |
| delete-success | fileId | 删除成功时触发,返回文件ID |
| upload-error | error | 上传失败时触发,返回错误信息 |
## 使用示例
### 基础用法
```vue
<template>
<a-form>
<a-form-item label="项目文件">
<SUploadFile
v-model:bussinessId="formData.id"
:bussiness-secret-level="formData.secretLevel"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import { ref } from 'vue';
import SUploadFile from '/@/semri/fileComponent/SUploadFile.vue';
const formData = ref({
id: '',
secretLevel: 3 // 秘密
});
</script>
```
### 带事件监听的用法
```vue
<template>
<a-form>
<a-form-item label="项目文件">
<SUploadFile
v-model:bussinessId="formData.id"
:bussiness-secret-level="formData.secretLevel"
:disabled="formData.readOnly"
@upload-success="handleUploadSuccess"
@delete-success="handleDeleteSuccess"
@upload-error="handleUploadError"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import { ref } from 'vue';
import { createMessage } from '/@/hooks/web/useMessage';
import SUploadFile from '/@/semri/fileComponent/SUploadFile.vue';
const formData = ref({
id: '123456',
secretLevel: 3, // 秘密
readOnly: false
});
// 上传成功处理
function handleUploadSuccess(result) {
console.log('上传成功:', result);
createMessage.success('文件上传完成');
}
// 删除成功处理
function handleDeleteSuccess(fileId) {
console.log('删除成功,文件ID:', fileId);
createMessage.success('文件删除完成');
}
// 上传失败处理
function handleUploadError(error) {
console.error('上传失败:', error);
}
</script>
```
### 不同密级的使用
```vue
<template>
<div>
<!-- 业务密级为非密只能上传非密文件 -->
<SUploadFile
bussiness-id="001"
:bussiness-secret-level="1"
/>
<!-- 业务密级为内部可以上传非密内部文件 -->
<SUploadFile
bussiness-id="002"
:bussiness-secret-level="2"
/>
<!-- 业务密级为秘密可以上传非密内部秘密文件 -->
<SUploadFile
bussiness-id="003"
:bussiness-secret-level="3"
/>
<!-- 业务密级为机密可以上传所有密级的文件 -->
<SUploadFile
bussiness-id="004"
:bussiness-secret-level="4"
/>
</div>
</template>
<script setup>
import SUploadFile from '/@/semri/fileComponent/SUploadFile.vue';
</script>
```
### 禁用状态(只读)
```vue
<template>
<a-form>
<a-form-item label="项目文件">
<SUploadFile
bussiness-id="123456"
:bussiness-secret-level="3"
:disabled="true"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import SUploadFile from '/@/semri/fileComponent/SUploadFile.vue';
</script>
```
## 密级规则
### 文件密级选项规则
组件会根据业务密级自动生成可选的文件密级:
| 业务密级 | 可选文件密级 |
|---------|------------|
| 1-非密 | 1-非密 |
| 2-内部 | 1-非密,2-内部 |
| 3-秘密 | 1-非密,2-内部,3-秘密 |
| 4-机密 | 1-非密,2-内部,3-秘密,4-机密 |
### 密级显示颜色
- **非密**:绿色 (success)
- **内部**:蓝色 (processing)
- **秘密**:橙色 (warning)
- **机密**:红色 (error)
## API 接口
组件内部调用的后端接口:
- **上传文件**: `POST /sys/file/upload`
- 参数:file, secretLevel, business_id
- **获取文件列表**: `GET /sys/file/getFileInfoByBussinessId`
- 参数:bussinessId
- **下载文件**: `GET /sys/file/download`
- 参数:fileUrl
- **删除文件**: `GET /sys/file/delete`
- 参数:id
## 注意事项
1. **文件大小限制**:单个文件不超过 100MB
2. **密级校验**:文件密级必须 <= 业务密级
3. **禁用状态**:禁用时不显示上传和删除按钮,但可以下载文件
4. **业务ID**:如果提供了业务ID,组件会自动加载该业务关联的文件列表
5. **Token 处理**:组件自动处理 token,无需手动添加
## 常见问题
### Q: 为什么上传按钮不显示?
A: 检查是否设置了 `disabled="true"`,禁用状态下不会显示上传按钮。
### Q: 为什么只能选择某些密级?
A: 文件密级受业务密级限制,只能选择小于等于业务密级的文件密级。
### Q: 如何获取上传后文件的ID?
A: 监听 `upload-success` 事件,返回的结果中包含 `fileId`
### Q: 删除文件时如何自定义确认文案?
A: 当前版本删除确认文案是固定的,如需自定义请修改组件源码。
## 更新日志
### v1.0.0 (2026-03-27)
- 初始版本发布
- 支持文件上传、下载、删除功能
- 支持密级管理
- 支持禁用状态
@@ -0,0 +1,171 @@
<!--用户选择组件-->
<template>
<div>
<JSelectBiz @change="handleChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
<UserSelectByDepModal
:rowKey="rowKey"
@register="regModal"
@getSelectResult="setValue"
v-bind="getBindValue"
:userSecurityLevel="interUserSecurityLevel"
></UserSelectByDepModal>
</div>
</template>
<script lang="ts">
import { unref } from 'vue';
import UserSelectByDepModal from '@/components/semri/userComponent/modal/SzUserSelectByDepModal.vue';
import JSelectBiz from '@/components/Form/src/jeecg/components/base/JSelectBiz.vue';
import { defineComponent, ref, reactive, watchEffect, watch, provide } from 'vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
import SzUserSelectModal from '@/components/semri/userComponent/modal/SzUserSelectModal.vue';
export default defineComponent({
name: 'SzSelectUserByDept',
components: {
SzUserSelectModal,
UserSelectByDepModal,
JSelectBiz,
},
inheritAttrs: false,
props: {
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
rowKey: {
type: String,
default: 'username',
},
labelKey: {
type: String,
default: 'realname',
},
userSecurityLevel: {
type: Number,
default: 3,
},
},
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit, refs }) {
const emitData = ref<any[]>();
//注册model
const [regModal, { openModal }] = useModal();
//表单值
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
//下拉框选项值
const selectOptions = ref<SelectValue>([]);
//下拉框选中值
let selectValues = reactive<object>({
value: [],
change: false,
});
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
//下发 selectOptions,xxxBiz组件接收
provide('selectOptions', selectOptions);
//下发 selectValues,xxxBiz组件接收
provide('selectValues', selectValues);
//下发 loadingEcho,xxxBiz组件接收
provide('loadingEcho', loadingEcho);
const tag = ref(false);
const attrs = useAttrs();
const interUserSecurityLevel = ref<number>(props.userSecurityLevel);
/**
* 监听组件值
*/
watchEffect(() => {
initValue();
});
/**
* 监听selectValues变化
*/
watch(selectValues, () => {
if (selectValues) {
// update-begin--author:liaozhiyang---date:20250616---for:【QQYUN-12869】通过部门选择用户组件,必填状态下选择用户后,点击重置后,会出校验信息
if (props.value === undefined && selectValues.value?.length == 0) {
return;
}
// update-end--author:liaozhiyang---date:20250616---for:【QQYUN-12869】通过部门选择用户组件,必填状态下选择用户后,点击重置后,会出校验信息
state.value = selectValues.value;
}
});
/**
* 打卡弹出框
*/
function handleOpen() {
tag.value = true;
openModal(true, {
isUpdate: false,
});
}
/**
* 将字符串值转化为数组
*/
function initValue() {
let value = props.value ? props.value : [];
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
state.value = value.split(',');
selectValues.value = value.split(',');
} else {
selectValues.value = value;
}
}
/**
* 设置下拉框的值
*/
function setValue(options, values) {
selectOptions.value = options;
//emitData.value = values.join(",");
state.value = values;
selectValues.value = values;
emit('update:value', values);
emit('options-change', options);
}
function handleChange(values) {
emit('update:value', values);
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
return {
state,
attrs,
selectOptions,
getBindValue,
selectValues,
loadingEcho,
tag,
regModal,
setValue,
handleOpen,
handleChange,
};
},
});
</script>
<style lang="less" scoped>
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>
@@ -0,0 +1,234 @@
<!--用户选择组件-->
<template>
<div class="JselectUser">
<JSelectBiz @change="handleSelectChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
<!-- update-begin--author:liaozhiyang---date:20240515---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
<a-form-item>
<SzUserSelectModal
:rowKey="rowKey"
@register="regModal"
@getSelectResult="setValue"
v-bind="getBindValue"
:excludeUserIdList="excludeUserIdList"
:userSecurityLevel="interUserSecurityLevel"
@close="handleClose"
/>
</a-form-item>
<!-- update-end--author:liaozhiyang---date:20240515---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
</div>
</template>
<script lang="ts">
import { unref } from 'vue';
import SzUserSelectModal from '@/components/semri/userComponent/modal/SzUserSelectModal.vue';
import JSelectBiz from '@/components/Form/src/jeecg/components/base/JSelectBiz.vue';
import { defineComponent, ref, reactive, watchEffect, watch, provide } from 'vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
import { cloneDeep } from 'lodash-es';
import { SzUserSelectByDeptRoleSecurityModal } from '@/components/semri/userComponent/modal/SzUserSelectByDeptRoleSecurityModal.vue';
export default defineComponent({
name: 'SzSelectUser',
components: {
SzUserSelectModal,
JSelectBiz,
},
inheritAttrs: false,
props: {
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
labelKey: {
type: String,
default: 'realname',
},
rowKey: {
type: String,
default: 'username',
},
params: {
type: Object,
default: () => {},
},
userSecurityLevel: {
type: Number,
default: 3,
},
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
//排除用户id的集合
excludeUserIdList: {
type: Array,
default: () => [],
},
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
},
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit }) {
const emitData = ref<any[]>();
//注册model
const [regModal, { openModal }] = useModal();
//表单值
// const [state] = useRuleFormItem(props, 'value', 'change', emitData);
//下拉框选项值
const selectOptions = ref<SelectValue>([]);
//下拉框选中值
let selectValues = reactive<Recordable>({
value: [],
change: false,
});
const interUserSecurityLevel = ref<number>(props.userSecurityLevel);
let tempSave: any = [];
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
//下发 selectOptions,xxxBiz组件接收
provide('selectOptions', selectOptions);
//下发 selectValues,xxxBiz组件接收
provide('selectValues', selectValues);
//下发 loadingEcho,xxxBiz组件接收
provide('loadingEcho', loadingEcho);
const tag = ref(false);
const attrs = useAttrs();
/**
* 监听组件值
*/
watchEffect(() => {
// update-begin--author:liaozhiyang---date:20240611---for:【TV360X-576】已选中了数据,再次选择打开弹窗点击取消,数据清空了
//update-begin-author:liusq---date:2024-06-03--for: [TV360X-840]用户授权,没有选择,点取消,也会回显一个选过的用户
tempSave = [];
//update-end-author:liusq---date:2024-06-03--for:[TV360X-840]用户授权,没有选择,点取消,也会回显一个选过的用户
// update-end--author:liaozhiyang---date:20240611---for:【TV360X-576】已选中了数据,再次选择打开弹窗点击取消,数据清空了
props.value && initValue();
// 查询条件重置的时候 界面显示未清空
if (!props.value) {
selectValues.value = [];
}
});
/**
* 监听selectValues变化
*/
// watch(selectValues, () => {
// if (selectValues) {
// state.value = selectValues.value;
// }
// });
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
const excludeUserIdList = ref<any>([]);
/**
* 需要监听一下excludeUserIdList,否则modal获取不到
*/
watch(
() => props.excludeUserIdList,
(data) => {
excludeUserIdList.value = data;
},
{ immediate: true }
);
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
/**
* 打卡弹出框
*/
function handleOpen() {
tag.value = true;
openModal(true, {
isUpdate: false,
});
}
/**
* 将字符串值转化为数组
*/
function initValue() {
let value = props.value ? props.value : [];
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
// state.value = value.split(',');
selectValues.value = value.split(',');
tempSave = value.split(',');
} else {
// 【VUEN-857】兼容数组(行编辑的用法问题)
selectValues.value = value;
tempSave = cloneDeep(value);
}
}
/**
* 设置下拉框的值
*/
function setValue(options, values) {
selectOptions.value = options;
//emitData.value = values.join(",");
// state.value = values;
selectValues.value = values;
send(values);
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
const handleClose = () => {
if (tempSave.length) {
selectValues.value = cloneDeep(tempSave);
} else {
send(tempSave);
}
};
const handleSelectChange = (values) => {
tempSave = cloneDeep(values);
send(tempSave);
};
const send = (values) => {
let result = typeof props.value == 'string' ? values.join(',') : values;
emit('update:value', result);
emit('change', result);
};
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
return {
// state,
attrs,
selectOptions,
getBindValue,
selectValues,
loadingEcho,
tag,
regModal,
setValue,
handleOpen,
excludeUserIdList,
handleClose,
handleSelectChange,
};
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.JselectUser {
> .ant-form-item {
display: none;
}
}
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>
@@ -0,0 +1,12 @@
// 1. 修改引入路径,Jeecg 使用的是 defHttp
import { defHttp } from '@/utils/http/axios';
// 根据部门+角色+密级筛选用户
export function queryDepartRoleUserPageList(params) {
return defHttp.get({
// 2. 注意:Jeecg 默认配置了 baseUrl,通常不需要写 /jeecg-boot 前缀
url: '/sys/user/queryUserRoleComponentData',
// 3. GET 请求的参数 key 是 params,而不是 data
params,
});
}
@@ -0,0 +1,278 @@
<!--通过部门选择用户-->
<template>
<BasicModal v-bind="$attrs" @register="register" :title="modalTitle" width="1200px" @ok="handleOk" destroyOnClose @visible-change="visibleChange">
<a-row :gutter="10">
<a-col :md="7" :sm="24">
<a-card :style="{ minHeight: '613px', overflow: 'auto' }">
<!--组织机构-->
<BasicTree
ref="treeRef"
:style="{ minWidth: '250px' }"
selectable
@select="onDepSelect"
:load-data="loadChildrenTreeData"
:treeData="departTree"
:selectedKeys="selectedDepIds"
:expandedKeys="expandedKeys"
:clickRowToExpand="false"
></BasicTree>
</a-card>
</a-col>
<a-col :md="17" :sm="24">
<a-card :style="{ minHeight: '613px', overflow: 'auto' }">
<!--用户列表-->
<BasicTable ref="tableRef" v-bind="getBindValue" :searchInfo="searchInfo" :api="getTableList" :rowSelection="rowSelection"></BasicTable>
</a-card>
</a-col>
</a-row>
</BasicModal>
</template>
<script lang="ts">
import { defineComponent, unref, ref } from 'vue';
import { BasicModal, useModalInner } from '/src/components/Modal';
import { BasicTree } from '/src/components/Tree';
import { queryTreeList, getTableList as getTableListOrigin } from '/src/api/common/api';
import { createAsyncComponent } from '/src/utils/factory/createAsyncComponent';
import { useSelectBiz } from '/src/components/Form/src/jeecg/hooks/useSelectBiz';
import { useAttrs } from '/src/hooks/core/useAttrs';
import { queryDepartTreeSync as queryDepartTreeSyncOrigin } from '/src/views/system/depart/depart.api';
import { selectProps } from '/src/components/Form/src/jeecg/props/props';
export default defineComponent({
name: 'SzUserSelectByDepModal',
components: {
//此处需要异步加载BasicTable
BasicModal,
BasicTree,
BasicTable: createAsyncComponent(() => import('/src/components/Table/src/BasicTable.vue'), {
loading: true,
}),
},
props: {
...selectProps,
//选择框标题
modalTitle: {
type: String,
default: '部门用户选择',
},
userSecurityLevel: {
type: Number,
default: 3,
},
},
emits: ['register', 'getSelectResult'],
setup(props, { emit, refs }) {
const tableRef = ref();
const treeRef = ref();
//注册弹框
const [register, { closeModal }] = useModalInner(async (data) => {
await queryDepartTree();
});
const attrs = useAttrs();
const departTree = ref([]);
const selectedDepIds = ref([]);
const expandedKeys = ref([]);
const searchInfo = {};
/**
*表格配置
*/
const tableProps = {
columns: [
{
title: '用户账号',
dataIndex: 'username',
width: 180,
},
{
title: '用户姓名',
dataIndex: 'realname',
width: 180,
},
{
title: '性别',
dataIndex: 'sex_dictText',
width: 80,
},
{
title: '手机号码',
dataIndex: 'phone',
// width: 50,
},
{
title: '手机号码',
dataIndex: 'phone',
// width: 50,
},
{
title: '密级',
dataIndex: 'userSecurityLevel_dictText',
},
],
useSearchForm: true,
canResize: false,
showIndexColumn: false,
striped: true,
bordered: true,
size: 'small',
formConfig: {
//labelWidth: 200,
baseColProps: {
xs: 24,
sm: 8,
md: 6,
lg: 8,
xl: 6,
xxl: 10,
},
//update-begin-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
actionColOptions: {
xs: 24,
sm: 12,
md: 12,
lg: 12,
xl: 8,
xxl: 8,
},
//update-end-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
schemas: [
{
label: '账号',
field: 'username',
component: 'Input',
},
{
label: '姓名',
field: 'realname',
component: 'JInput',
},
{
label: '密级',
field: 'searchSecurityLevel',
component: 'JDictSelectTag',
componentProps: {
defaultValue: 3,
dictCode: 'user_security_level',
placeholder: '请选择密级',
stringToNumber: true,
},
},
],
resetFunc: customResetFunc,
},
};
const getBindValue = Object.assign({}, unref(props), unref(attrs), tableProps);
const [{ rowSelection, visibleChange, indexColumnProps, getSelectResult, reset }] = useSelectBiz(getTableList, getBindValue);
function getTableList(params) {
params = parseParams(params);
return getTableListOrigin({ ...params });
}
function queryDepartTreeSync(params) {
params = parseParams(params);
return queryDepartTreeSyncOrigin({ ...params });
}
/**
* 解析参数
* @param params
*/
function parseParams(params) {
if (props?.params) {
return {
...params,
...props.params,
};
}
return params;
}
/**
* 加载树形数据
*/
function queryDepartTree() {
queryDepartTreeSync().then((res) => {
if (res) {
departTree.value = res;
// 默认展开父节点
//expandedKeys.value = unref(departTree).map(item => item.id)
}
});
}
/**
* 加载子级部门
*/
async function loadChildrenTreeData(treeNode) {
try {
const result = await queryDepartTreeSync({
pid: treeNode.eventKey,
});
const asyncTreeAction = unref(treeRef);
if (asyncTreeAction) {
asyncTreeAction.updateNodeByKey(treeNode.eventKey, { children: result });
asyncTreeAction.setExpandedKeys([treeNode.eventKey, ...asyncTreeAction.getExpandedKeys()]);
}
} catch (e) {
console.error(e);
}
return Promise.resolve();
}
/**
* 点击树节点,筛选出对应的用户
*/
function onDepSelect(keys) {
if (keys[0] != null) {
if (unref(selectedDepIds)[0] !== keys[0]) {
selectedDepIds.value = [keys[0]];
}
searchInfo['departId'] = unref(selectedDepIds).join(',');
tableRef.value.reload();
}
}
/**
* 自定义重置方法
* */
async function customResetFunc() {
console.log('自定义查询');
//树节点清空
selectedDepIds.value = [];
//查询条件清空
searchInfo['departId'] = '';
//选择项清空
reset();
}
/**
* 确定选择
*/
function handleOk() {
getSelectResult((options, values) => {
//回传选项和已选择的值
emit('getSelectResult', options, values);
//关闭弹窗
closeModal();
});
}
return {
//config,
handleOk,
searchInfo,
register,
indexColumnProps,
visibleChange,
getBindValue,
rowSelection,
departTree,
selectedDepIds,
expandedKeys,
treeRef,
tableRef,
getTableList,
onDepSelect,
loadChildrenTreeData,
};
},
});
</script>
<style scoped lang="less"></style>
@@ -0,0 +1,196 @@
<template>
<a-modal :open="open" title="选择用户(部门+角色+密级)" :width="1000" :mask-closable="false" @cancel="handleCancel" @ok="handleOk" destroyOnClose>
<BasicForm @register="registerForm" />
<a-table
ref="tableRef"
row-key="id"
size="middle"
:columns="columns"
:data-source="userList"
:loading="loading"
:pagination="pagination"
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
@change="handleTableChange"
:scroll="{ y: 400 }"
/>
</a-modal>
</template>
<script setup>
import { ref, reactive, watch, nextTick } from 'vue';
import { BasicForm, useForm } from '@/components/Form/index';
import { queryDepartRoleUserPageList } from '../api/user';
defineOptions({ name: 'SzUserSelectByDeptRoleSecurityModal' });
const props = defineProps({
open: { type: Boolean, default: false },
deptId: { type: String, default: null },
roleId: { type: String, default: null },
securityValue: { type: Number, default: null },
});
const emit = defineEmits(['update:open', 'confirm']);
// --- 状态定义 ---
const loading = ref(false);
const userList = ref([]);
const selectedRowKeys = ref([]);
const selectedRows = ref([]);
const innerDeptRoleId = ref(props.deptId);
const innerRoleId = ref(props.roleId);
const innerSecurityValue = ref(props.securityValue);
// 分页参数:注意 a-table 期待的属性名是 total, current, pageSize
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`,
});
// --- 表单配置 ---
const [registerForm, { getFieldsValue, resetFields, validate }] = useForm({
labelWidth: 80,
// 增加 baseColProps 让布局更紧凑
baseColProps: { span: 8 },
schemas: [
{
field: 'departId',
label: '部门',
component: 'JSelectDept',
componentProps: {
showButton: false,
checkStrictly: true, // 如果需要选子部门不级联
},
},
{
field: 'roleId',
label: '角色',
component: 'JSelectRole',
},
{
field: 'searchSecurityLevel',
label: '密级',
component: 'JDictSelectTag',
componentProps: { dictCode: 'user_security_level' },
},
],
showActionButtonGroup: true,
showAdvancedButton: false,
actionColOptions: { span: 24 }, // 按钮行占满全屏,右对齐
submitButtonOptions: { text: '查询' },
resetButtonOptions: { text: '重置' },
// 查询逻辑
submitFunc: async () => {
pagination.current = 1;
await loadData();
},
// 重置逻辑
resetFunc: async () => {
selectedRowKeys.value = [];
selectedRows.value = [];
pagination.current = 1;
await loadData();
},
});
const columns = [
{ title: '用户名', dataIndex: 'username', key: 'username', width: 120 },
{ title: '真实姓名', dataIndex: 'realname', key: 'realname', width: 120 },
{ title: '部门', dataIndex: 'orgCodeTxt', key: 'orgCodeTxt', width: 200 },
{ title: '密级', dataIndex: 'userSecurityLevel_dictText', key: 'userSecurityLevel', width: 100 },
];
// --- 核心数据加载逻辑 ---
async function loadData() {
loading.value = true;
try {
const values = getFieldsValue() || {};
const params = {
...values,
pageNo: pagination.current,
pageSize: pagination.pageSize,
};
const res = await queryDepartRoleUserPageList(params);
// --- 核心修复:兼容性处理 ---
// 检查 res 本身是否包含 records (你现在的情况)
// 或者 res.result 包含 records (之前的预期)
let dataSource = [];
let totalCount = 0;
if (res && res.records) {
// 对应你当前的情况
dataSource = res.records;
totalCount = res.total;
} else if (res && res.result && res.result.records) {
// 对应之前的预期
dataSource = res.result.records;
totalCount = res.result.total;
}
userList.value = dataSource || [];
pagination.total = parseInt(totalCount || 0);
console.log('解析后的数据:', userList.value); // 调试用
} catch (e) {
console.error('加载用户列表失败:', e);
} finally {
loading.value = false;
}
}
// --- 事件处理 ---
function onSelectChange(keys, rows) {
selectedRowKeys.value = keys;
selectedRows.value = rows;
}
function handleTableChange(p) {
pagination.current = p.current;
pagination.pageSize = p.pageSize;
loadData();
}
function handleCancel() {
emit('update:open', false);
}
function handleOk() {
if (selectedRows.value.length === 0) {
// 如果没有选择,可以加个提示,或者直接关闭
}
emit('confirm', selectedRows.value);
emit('update:open', false);
}
// 监听 Modal 打开
watch(
() => props.open,
async (val) => {
if (val) {
// 必须在 nextTick 后执行,确保 BasicForm 实例已挂载,getFieldsValue 可用
await nextTick();
loadData();
} else {
// 弹窗关闭时建议重置表单和选中项
resetFields();
selectedRowKeys.value = [];
selectedRows.value = [];
}
},
{ immediate: true }
);
</script>
<style scoped>
/* 调整表单底部间距 */
:deep(.ant-form) {
margin-bottom: 16px;
}
</style>
@@ -0,0 +1,325 @@
<!--用户选择框-->
<template>
<div>
<BasicModal
v-bind="$attrs"
@register="register"
:title="modalTitle"
:width="showSelected ? '1200px' : '900px'"
wrapClassName="j-user-select-modal"
@ok="handleOk"
@cancel="handleCancel"
:maxHeight="maxHeight"
:centered="true"
destroyOnClose
@visible-change="visibleChange"
>
<a-row>
<a-col :span="showSelected ? 18 : 24">
<BasicTable
ref="tableRef"
:columns="columns"
:scroll="tableScroll"
v-bind="getBindValue"
:useSearchForm="true"
:formConfig="formConfig"
:api="getUserList"
:searchInfo="searchInfo"
:rowSelection="rowSelection"
:indexColumnProps="indexColumnProps"
:afterFetch="afterFetch"
:beforeFetch="beforeFetch"
>
<!-- update-begin-author:taoyan date:2022-5-25 for: VUEN-1112一对多 用户选择 未显示选择条数及清空 -->
<template #tableTitle></template>
<!-- update-end-author:taoyan date:2022-5-25 for: VUEN-1112一对多 用户选择 未显示选择条数及清空 -->
</BasicTable>
</a-col>
<a-col :span="showSelected ? 6 : 0">
<BasicTable
v-bind="selectedTable"
:dataSource="selectRows"
:useSearchForm="true"
:formConfig="{ showActionButtonGroup: false, baseRowStyle: { minHeight: '40px' } }"
>
<!--操作栏-->
<template #action="{ record }">
<a href="javascript:void(0)" @click="handleDeleteSelected(record)"><Icon icon="ant-design:delete-outlined"></Icon></a>
</template>
</BasicTable>
</a-col>
</a-row>
</BasicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, unref, ref, watch } from 'vue';
import { BasicModal, useModalInner } from '/src/components/Modal';
import { getUserList } from '/src/api/common/api';
import { createAsyncComponent } from '/src/utils/factory/createAsyncComponent';
import { useSelectBiz } from '/src/components/Form/src/jeecg/hooks/useSelectBiz';
import { useAttrs } from '/src/hooks/core/useAttrs';
import { selectProps } from '/src/components/Form/src/jeecg/props/props';
export default defineComponent({
name: 'SzUserSelectModal',
components: {
//此处需要异步加载BasicTable
BasicModal,
BasicTable: createAsyncComponent(() => import('/src/components/Table/src/BasicTable.vue'), {
loading: true,
}),
},
props: {
...selectProps,
//选择框标题
modalTitle: {
type: String,
default: '选择用户',
},
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
//排除用户id的集合
excludeUserIdList: {
type: Array,
default: [],
},
userSecurityLevel: {
type: Number,
default: 3,
},
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
},
emits: ['register', 'getSelectResult', 'close'],
setup(props, { emit, refs }) {
// update-begin-author:taoyan date:2022-5-24 for: VUEN-1086 【移动端】用户选择 查询按钮 效果不好 列表展示没有滚动条
const tableScroll = ref<any>({ x: false });
const tableRef = ref();
const maxHeight = ref(600);
//注册弹框
const [register, { closeModal }] = useModalInner(() => {
if (window.innerWidth < 900) {
tableScroll.value = { x: 900 };
} else {
tableScroll.value = { x: false };
}
//update-begin-author:taoyan date:2022-6-2 for: VUEN-1112 一对多 用户选择 未显示选择条数,及清空
setTimeout(() => {
if (tableRef.value) {
tableRef.value.setSelectedRowKeys(selectValues['value'] || []);
}
}, 800);
//update-end-author:taoyan date:2022-6-2 for: VUEN-1112 一对多 用户选择 未显示选择条数,及清空
});
// update-end-author:taoyan date:2022-5-24 for: VUEN-1086 【移动端】用户选择 查询按钮 效果不好 列表展示没有滚动条
const attrs = useAttrs();
//表格配置
const config = {
canResize: false,
bordered: true,
size: 'small',
};
const getBindValue = Object.assign({}, unref(props), unref(attrs), config);
const [{ rowSelection, visibleChange, selectValues, indexColumnProps, getSelectResult, handleDeleteSelected, selectRows }] = useSelectBiz(
getUserList,
getBindValue,
emit
);
const searchInfo = ref(props.params);
// update-begin--author:liaozhiyang---date:20230811---for:【issues/657】右侧选中列表删除无效
watch(rowSelection.selectedRowKeys, (newVal) => {
//update-begin---author:wangshuai ---date: 20230829 fornull指针异常导致控制台报错页面不显示------------
if (tableRef.value) {
tableRef.value.setSelectedRowKeys(newVal);
}
//update-end---author:wangshuai ---date: 20230829 fornull指针异常导致控制台报错页面不显示------------
});
// update-end--author:liaozhiyang---date:20230811---for:【issues/657】右侧选中列表删除无效
//查询form
const formConfig = {
baseColProps: {
xs: 24,
sm: 8,
md: 6,
lg: 8,
xl: 6,
xxl: 6,
},
//update-begin-author:taoyan date:2022-5-24 for: VUEN-1086 【移动端】用户选择 查询按钮 效果不好 列表展示没有滚动条---查询表单按钮的栅格布局和表单的保持一致
actionColOptions: {
xs: 24,
sm: 8,
md: 8,
lg: 8,
xl: 8,
xxl: 8,
},
//update-end-author:taoyan date:2022-5-24 for: VUEN-1086 【移动端】用户选择 查询按钮 效果不好 列表展示没有滚动条---查询表单按钮的栅格布局和表单的保持一致
schemas: [
{
label: '账号',
field: 'username',
component: 'JInput',
},
{
label: '姓名',
field: 'realname',
component: 'JInput',
},
{
label: '密级',
field: 'searchSecurityLevel',
component: 'JDictSelectTag',
defaultValue: 3,
componentProps: {
dictCode: 'user_security_level',
placeholder: '请选择密级',
stringToNumber: true,
},
},
],
};
//定义表格列
const columns = [
{
title: '用户账号',
dataIndex: 'username',
width: 120,
align: 'left',
},
{
title: '用户姓名',
dataIndex: 'realname',
width: 120,
},
{
title: '密级',
dataIndex: 'userSecurityLevel_dictText',
width: 120,
},
{
title: '性别',
dataIndex: 'sex_dictText',
width: 50,
},
{
title: '手机号码',
dataIndex: 'phone',
width: 120,
},
{
title: '邮箱',
dataIndex: 'email',
// width: 40,
},
{
title: '状态',
dataIndex: 'status_dictText',
width: 80,
},
];
//已选择的table信息
const selectedTable = {
pagination: false,
showIndexColumn: false,
scroll: { y: 390 },
size: 'small',
canResize: false,
bordered: true,
rowKey: 'id',
columns: [
{
title: '用户姓名',
dataIndex: 'realname',
width: 40,
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 40,
slots: { customRender: 'action' },
},
],
};
/**
* 确定选择
*/
function handleOk() {
getSelectResult((options, values) => {
//回传选项和已选择的值
emit('getSelectResult', options, values);
//关闭弹窗
closeModal();
});
}
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
/**
* 用户返回结果逻辑查询
*/
function afterFetch(record) {
let excludeList = props.excludeUserIdList;
if (!excludeList) {
return record;
}
let arr: any[] = [];
//如果存在过滤用户id集合,并且后台返回的数据不为空
if (excludeList.length > 0 && record && record.length > 0) {
for (let item of record) {
if (excludeList.indexOf(item.id) < 0) {
arr.push({ ...item });
}
}
return arr;
}
return record;
}
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
const handleCancel = () => {
emit('close');
};
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
// update-begin--author:liaozhiyang---date:20240607---for:【TV360X-305】小屏幕展示10条
const clientHeight = document.documentElement.clientHeight * 200;
maxHeight.value = clientHeight > 600 ? 600 : clientHeight;
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-305】小屏幕展示10条
//update-begin---author:wangshuai---date:2024-07-03---for:【TV360X-1629】用户选择组件不是根据创建时间正序排序的---
/**
* 请求之前根据创建时间排序
*
* @param params
*/
function beforeFetch(params) {
return Object.assign({ column: 'createTime', order: 'desc' }, params);
}
//update-end---author:wangshuai---date:2024-07-03---for:【TV360X-1629】用户选择组件不是根据创建时间正序排序的---
return {
//config,
handleOk,
searchInfo,
register,
indexColumnProps,
visibleChange,
getBindValue,
getUserList,
formConfig,
columns,
rowSelection,
selectRows,
selectedTable,
handleDeleteSelected,
tableScroll,
tableRef,
afterFetch,
handleCancel,
maxHeight,
beforeFetch,
};
},
});
</script>
@@ -0,0 +1,158 @@
<template>
<div class="JSelectDept">
<JSelectBiz @change="handleSelectChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs" />
<a-form-item v-show="false">
<SzDeptSelectModal
@register="regModal"
@getSelectResult="setValue"
v-bind="getBindValue"
:rowKey="rowKey"
:labelKey="labelKey"
@close="handleClose"
/>
</a-form-item>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, watchEffect, watch, provide, unref, toRaw } from 'vue';
import SzDeptSelectModal from './modal/SzDeptSelectModal.vue';
import JSelectBiz from '@/components/Form/src/jeecg/components/base/JSelectBiz.vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
import { cloneDeep } from 'lodash-es';
export default defineComponent({
name: 'SzSelectDept',
components: {
SzDeptSelectModal,
JSelectBiz,
},
inheritAttrs: false,
props: {
// 外部 v-model 绑定的值(ID字符串或数组)
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
// 显示文本的字段名
labelKey: {
type: String,
default: 'departName',
},
// 存储值的字段名
rowKey: {
type: String,
default: 'id',
},
// 是否允许多选
multiple: propTypes.bool.def(true),
},
emits: ['options-change', 'change', 'update:value', 'select'],
setup(props, { emit }) {
const [regModal, { openModal }] = useModal();
const attrs = useAttrs();
const selectOptions = ref<SelectValue>([]); // 存储 [{id, departName}]
const selectValues = reactive<Recordable>({ value: [] }); // 存储 [id]
const loadingEcho = ref<boolean>(false);
let tempSave: any = [];
// 依赖注入给 JSelectBiz 组件使用,用于显示标签和加载状态
provide('selectOptions', selectOptions);
provide('selectValues', selectValues);
provide('loadingEcho', loadingEcho);
/**
* 监听外部 value 变化进行初始化
*/
watchEffect(() => {
tempSave = [];
if (props.value) {
initValue();
} else {
selectValues.value = [];
}
});
/**
* 初始化值:将字符串转为数组
*/
function initValue() {
let val = props.value ? props.value : [];
if (typeof val === 'string' && val !== 'null' && val !== 'undefined') {
selectValues.value = val.split(',');
tempSave = val.split(',');
} else {
selectValues.value = Array.isArray(val) ? val : [];
tempSave = cloneDeep(selectValues.value);
}
}
/**
* 【关键】接收弹窗传回的精简数据
* @param options 格式:[{id: 'xxx', departName: 'xxx'}]
* @param values 格式:['xxx']
*/
function setValue(options, values) {
// 将后端字段 [id, departName] 转换为 a-select 认识的 [value, label]
selectOptions.value = options.map((item) => ({
label: item.departName, // 这一步让框里显示出名字
value: item.id, // 这一步让值对应上
}));
selectValues.value = values;
send(values);
}
/**
* 发送数据给父组件
*/
const send = (values) => {
let result = typeof props.value === 'string' ? values.join(',') : values;
emit('update:value', result);
emit('change', result);
};
function handleOpen() {
openModal(true, { isUpdate: false });
}
function handleClose() {
if (tempSave.length) {
selectValues.value = cloneDeep(tempSave);
} else {
send(tempSave);
}
}
function handleSelectChange(values) {
tempSave = cloneDeep(values);
send(tempSave);
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
return {
attrs,
selectOptions,
selectValues,
loadingEcho,
getBindValue,
regModal,
setValue,
handleOpen,
handleClose,
handleSelectChange,
};
},
});
</script>
<style lang="less" scoped>
.JSelectDept {
> .ant-form-item {
display: none;
}
}
</style>
@@ -0,0 +1,12 @@
// 1. 修改引入路径,Jeecg 使用的是 defHttp
import { defHttp } from '@/utils/http/axios';
// 根据部门+角色+密级筛选用户
export function queryDept(params) {
return defHttp.get({
// 2. 注意:Jeecg 默认配置了 baseUrl,通常不需要写 /jeecg-boot 前缀
url: '/sys/sysDepart/querySecondLevelDepts',
// 3. GET 请求的参数 key 是 params,而不是 data
params,
});
}
@@ -0,0 +1,245 @@
<template>
<div>
<BasicModal
v-bind="$attrs"
@register="register"
:title="modalTitle"
:width="showSelected ? '1200px' : '900px'"
wrapClassName="j-user-select-modal"
@ok="handleOk"
@cancel="handleCancel"
:maxHeight="maxHeight"
:centered="true"
destroyOnClose
@visible-change="visibleChange"
>
<a-row>
<a-col :span="showSelected ? 18 : 24">
<BasicTable
ref="tableRef"
:columns="columns"
:scroll="tableScroll"
v-bind="getBindValue"
:useSearchForm="true"
:formConfig="formConfig"
:api="queryDept"
:searchInfo="searchInfo"
:rowSelection="rowSelection"
:indexColumnProps="indexColumnProps"
:afterFetch="afterFetch"
:beforeFetch="beforeFetch"
>
<template #tableTitle></template>
</BasicTable>
</a-col>
<a-col :span="showSelected ? 6 : 0" v-if="showSelected" style="padding-left: 10px">
<BasicTable v-bind="selectedTable" :dataSource="selectRows">
<template #action="{ record }">
<a href="javascript:void(0)" @click="handleDeleteSelected(record)">
<Icon icon="ant-design:delete-outlined" style="color: #ff4d4f"></Icon>
</a>
</template>
</BasicTable>
</a-col>
</a-row>
</BasicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, unref, ref, watch, h } from 'vue';
import { BasicModal, useModalInner } from '@/components/Modal';
import { queryDept } from '../dept';
import { createAsyncComponent } from '@/utils/factory/createAsyncComponent';
import { useSelectBiz } from '@/components/Form/src/jeecg/hooks/useSelectBiz';
import { useAttrs } from '@/hooks/core/useAttrs';
import { selectProps } from '@/components/Form/src/jeecg/props/props';
import { Icon } from '@/components/Icon';
import { JDictSelectTag } from "@/components/Form";
import { render } from "@/utils/common/renderUtils";
export default defineComponent({
name: 'SzDeptSelectModal',
components: {
BasicModal,
Icon,
BasicTable: createAsyncComponent(() => import('/src/components/Table/src/BasicTable.vue'), {
loading: true,
}),
},
props: {
...selectProps,
modalTitle: {
type: String,
default: '选择部门',
},
},
emits: ['register', 'getSelectResult', 'close'],
setup(props, { emit }) {
const tableScroll = ref<any>({ x: false });
const tableRef = ref();
const maxHeight = ref(600);
// 内部状态注册
const [register, { closeModal }] = useModalInner(() => {
if (window.innerWidth < 900) {
tableScroll.value = { x: 900 };
} else {
tableScroll.value = { x: false };
}
// 延迟设置选中行,确保 table 渲染完成
setTimeout(() => {
if (tableRef.value) {
tableRef.value.setSelectedRowKeys(selectValues['value'] || []);
}
}, 300);
});
const attrs = useAttrs();
// 基础配置
const config = {
canResize: false,
bordered: true,
size: 'small',
rowKey: 'id', // 必须确保后端返回 id 字段
labelKey: 'departName',
};
const getBindValue = Object.assign({}, unref(props), unref(attrs), config);
// 使用 Jeecg 核心 Hook
const [{ rowSelection, visibleChange, selectValues, indexColumnProps, getSelectResult, handleDeleteSelected, selectRows, showSelected }] =
useSelectBiz(queryDept, getBindValue, emit);
const searchInfo = ref(props.params);
// 监听外部 keys 变化同步给 table
watch(rowSelection.selectedRowKeys, (newVal) => {
if (tableRef.value) {
tableRef.value.setSelectedRowKeys(newVal);
}
});
// 搜索表单配置
const formConfig = {
// 在这里增加 labelWidth
labelWidth: 65,
baseColProps: { xs: 24, sm: 12, md: 8, lg: 8, xl: 8, xxl: 8 },
schemas: [
{
label: '部门类别',
field: 'departType',
// 也可以在具体的 schema 里单独控制
// labelWidth: 100,
component: 'JDictSelectTag',
componentProps: {
dictCode: 'depart_type',
placeholder: '请选择部门类别',
stringToNumber: true,
'onUpdate:value': (val) => {
// 这里的 val 就是选中的值
console.log('监听到值改变:', val);
// 触发 Table 刷新
tableRef.value?.reload();
},
},
},
],
};
// 主表列定义
const columns = [
{ title: '部门名称', dataIndex: 'departName', width: 180, align: 'left' },
{ title: '机构编码', dataIndex: 'orgCode', width: 120 },
{
title: '机构类型',
dataIndex: 'departType',
width: 100,
customRender: ({ text }) => {
return render.renderDict(text, 'depart_type');
},
},
{ title: '排序', dataIndex: 'departOrder', width: 80 },
];
// 右侧已选表配置
const selectedTable = {
pagination: false,
showIndexColumn: false,
scroll: { y: 390 },
size: 'small',
canResize: false,
bordered: true,
rowKey: 'id',
columns: [
{ title: '部门名称', dataIndex: 'departName', width: 100 },
{ title: '操作', dataIndex: 'action', align: 'center', width: 40, slots: { customRender: 'action' } },
],
};
/**
* 【修改后的 handleOk】
* 只提取 id 和 departName
*/
function handleOk() {
// 直接从 tableRef 获取选中的原始行数据,避免 label 为 undefined 的问题
const rows = tableRef.value.getSelectRows();
// 1. 提取精简的对象列表
const options = rows.map((item) => ({
id: item.id,
departName: item.departName,
}));
// 2. 提取 ID 列表
const values = rows.map((item) => item.id);
console.log('确定选择内容:', options);
// 3. 向上抛出事件
emit('getSelectResult', options, values);
closeModal();
}
function afterFetch(record) {
return record;
}
function beforeFetch(params) {
return Object.assign({ column: 'createTime', order: 'desc' }, params);
}
const handleCancel = () => {
emit('close');
};
return {
handleOk,
searchInfo,
register,
indexColumnProps,
visibleChange,
getBindValue,
queryDept, // 必须暴露给 template 中的 :api
formConfig,
columns,
rowSelection,
selectRows,
showSelected,
selectedTable,
handleDeleteSelected,
tableScroll,
tableRef,
afterFetch,
handleCancel,
maxHeight,
beforeFetch,
};
},
});
</script>
@@ -0,0 +1,257 @@
<!-- 文件上传弹窗组件 -->
<template>
<a-modal
:visible="visible"
title="选择文件密级并上传"
:width="600"
:confirm-loading="uploading"
@ok="handleUpload"
@cancel="handleCancel"
:destroyOnClose="true"
>
<a-form layout="vertical" style="padding: 0 20px">
<a-form-item label="文件密级" required>
<a-radio-group v-model:value="selectedSecretLevel">
<a-radio
v-for="option in secretLevelOptions"
:key="option.value"
:value="option.value"
>
<a-tag :color="getSecretLevelColor(option.value)">
{{ option.label }}
</a-tag>
</a-radio>
</a-radio-group>
<!-- <div class="secret-tip">-->
<!-- <Icon icon="ant-design:info-circle-outlined" />-->
<!-- 文件密级将决定谁能查看此文件请谨慎选择-->
<!-- </div>-->
</a-form-item>
<a-form-item label="选择文件" required>
<a-upload
:before-upload="beforeUpload"
:file-list="tempFileList"
@remove="handleRemoveFile"
:multiple="multiple"
:max-count="multiple ? undefined : 1"
>
<a-button>
<Icon icon="ant-design:file-add-outlined" />
{{ multiple ? '选择文件(可多选)' : '选择文件' }}
</a-button>
</a-upload>
</a-form-item>
<a-alert
v-if="selectedFiles.length > 0"
:message="`已选择 ${selectedFiles.length} 个文件`"
type="info"
show-icon
>
<template #description>
<div class="selected-files-summary">
<div v-for="(f, idx) in selectedFiles" :key="f.uid" class="summary-item">
{{ idx + 1}}. {{ f.name }}{{ formatFileSize(f.size) }}
</div>
</div>
</template>
</a-alert>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { Icon } from '/src/components/Icon';
import { useMessage } from '/src/hooks/web/useMessage';
import { uploadMyFile } from '/src/api/common/api';
const { createMessage } = useMessage();
const props = defineProps({
visible: {
type: Boolean,
required: true
},
secretLevelOptions: {
type: Array,
default: () => []
},
businessId: {
type: String,
default: ''
},
multiple: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['update:visible', 'upload-success', 'upload-error']);
const selectedSecretLevel = ref<number>(1);
const selectedFiles = ref<any[]>([]);
const tempFileList = ref<any[]>([]);
const uploading = ref<boolean>(false);
function getSecretLevelColor(level: number) {
const colors = {
1: 'success',
2: 'processing',
3: 'warning',
4: 'error'
};
return colors[level] || 'default';
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
function beforeUpload(file) {
const isValidSize = file.size / 1024 / 1024 < 500;
if (!isValidSize) {
createMessage.error(`文件"${file.name}"大小超过500MB,已跳过`);
return false;
}
if (!props.multiple) {
selectedFiles.value = [file];
tempFileList.value = [{
uid: file.uid,
name: file.name,
status: 'done',
size: file.size
}];
} else {
selectedFiles.value = [...selectedFiles.value, file];
tempFileList.value = [...tempFileList.value, {
uid: file.uid,
name: file.name,
status: 'done',
size: file.size
}];
}
return false;
}
function handleRemoveFile(file) {
selectedFiles.value = selectedFiles.value.filter(f => f.uid !== file.uid);
tempFileList.value = tempFileList.value.filter(f => f.uid !== file.uid);
}
async function handleUpload() {
if (selectedFiles.value.length === 0) {
createMessage.warning('请先选择文件');
return;
}
if (!selectedSecretLevel.value) {
createMessage.warning('请选择文件密级');
return;
}
uploading.value = true;
try {
const uploadPromises = selectedFiles.value.map(file => {
const formData = new FormData();
formData.append('file', file);
formData.append('secretLevel', selectedSecretLevel.value.toString());
if (props.businessId) {
formData.append('business_id', props.businessId);
}
return uploadMyFile('/sys/file/upload', formData).then(res => ({
rawFile: file,
response: res
}));
});
const results = await Promise.allSettled(uploadPromises);
let successCount = 0;
let failCount = 0;
const successResults: any[] = [];
results.forEach((r) => {
if (r.status === 'fulfilled') {
const result = r.value?.response?.data;
if (result && result.success) {
successCount++;
successResults.push({
fileId: result.result.fileId,
fileName: r.value.rawFile.name,
filePath: result.result.savePath,
fileSize: r.value.rawFile.size,
secretLevel: selectedSecretLevel.value,
fileUploadType: result.result.fileUploadType,
bound: !!props.businessId
});
} else {
failCount++;
}
} else {
failCount++;
}
});
if (successCount > 0) {
createMessage.success(`成功上传 ${successCount} 个文件${failCount > 0 ? `${failCount} 个失败` : ''}`);
emit('upload-success', { success: true, results: successResults });
handleCancel();
} else {
createMessage.error('所有文件上传失败');
emit('upload-error', new Error('所有文件上传失败'));
}
} catch (error) {
console.error('上传失败:', error);
createMessage.error('文件上传失败');
emit('upload-error', error);
} finally {
uploading.value = false;
}
}
function handleCancel() {
emit('update:visible', false);
setTimeout(() => {
selectedSecretLevel.value = 1;
selectedFiles.value = [];
tempFileList.value = [];
}, 300);
}
watch(() => props.visible, (newVal) => {
if (newVal && props.secretLevelOptions.length > 0) {
selectedSecretLevel.value = props.secretLevelOptions[0].value;
}
});
</script>
<style lang="less" scoped>
.secret-tip {
margin-top: 8px;
color: #faad14;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
.selected-files-summary {
max-height: 150px;
overflow-y: auto;
.summary-item {
padding: 2px 0;
font-size: 12px;
color: #595959;
}
}
</style>
@@ -0,0 +1,467 @@
<!-- 文件上传下载删除组件 -->
<template>
<div class="s-upload-file-container">
<!-- 上传按钮区域 -->
<div class="upload-btn-wrapper" v-if="!disabled">
<a-button type="primary" @click="handleOpenUploadModal">
<Icon icon="ant-design:upload-outlined" />
选择文件上传
</a-button>
</div>
<!-- 文件列表区域 -->
<div class="file-list-wrapper">
<div v-if="displayFileList.length > 0" class="file-list-header">
<span class="file-count"> {{ displayFileList.length }} 个文件</span>
<a-button
type="link"
size="small"
:loading="downloadingAll"
@click="handleDownloadAll"
>
<Icon icon="ant-design:download-outlined" />
下载全部
</a-button>
</div>
<a-spin :spinning="loading">
<a-empty v-if="!loading && displayFileList.length === 0" description="暂无文件" />
<div v-else class="file-list">
<div
v-for="file in displayFileList"
:key="file.fileId || file.id"
class="file-item"
>
<!-- 文件信息 -->
<div class="file-info">
<Icon icon="ant-design:file-outlined" class="file-icon" />
<span class="file-name">{{ file.fileName }}</span>
<a-tag :color="getSecretLevelColor(file.secretLevel)" class="secret-tag">
{{ getSecretLevelLabel(file.secretLevel) }}
</a-tag>
<a-tag v-if="file.pending" color="orange" class="secret-tag">
待保存
</a-tag>
<span v-if="file.fileSize" class="file-size">{{ formatFileSize(file.fileSize) }}</span>
</div>
<!-- 操作按钮 -->
<div class="file-actions">
<a-button
type="link"
size="small"
@click="handleDownload(file)"
>
<Icon icon="ant-design:download-outlined" />
下载
</a-button>
<a-button
v-if="!disabled"
type="link"
size="small"
danger
@click="handleDelete(file)"
>
<Icon icon="ant-design:delete-outlined" />
删除
</a-button>
</div>
</div>
</div>
</a-spin>
</div>
<!-- 上传弹窗 -->
<SFileUploadModal
v-model:visible="uploadModalVisible"
:secret-level-options="secretLevelOptions"
:business-id="bussinessId"
:multiple="multiple"
@upload-success="handleUploadSuccess"
@upload-error="handleUploadError"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { Icon } from '/src/components/Icon';
import { defHttp } from '/src/utils/http/axios';
import { useMessage } from '/src/hooks/web/useMessage';
import SFileUploadModal from './SFileUploadModal.vue';
const { createMessage, createConfirm } = useMessage();
const props = defineProps({
bussinessId: {
type: String,
default: ''
},
bussinessSecretLevel: {
type: Number,
default: 1,
validator: (value: number) => [1, 2, 3, 4].includes(value)
},
disabled: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['upload-success', 'delete-success', 'upload-error']);
const serverFileList = ref<any[]>([]);
const pendingFileList = ref<any[]>([]);
const loading = ref<boolean>(false);
const uploadModalVisible = ref<boolean>(false);
const binding = ref<boolean>(false);
const downloadingAll = ref<boolean>(false);
const displayFileList = computed(() => {
return [
...serverFileList.value.map(f => ({ ...f, pending: false })),
...pendingFileList.value.map(f => ({ ...f, pending: true }))
];
});
const SECRET_LEVEL_CONFIG = {
1: { label: '非密', color: 'success' },
2: { label: '内部', color: 'processing' },
3: { label: '秘密', color: 'warning' },
4: { label: '机密', color: 'error' }
};
function getSecretLevelInfo(level: number) {
return SECRET_LEVEL_CONFIG[level] || SECRET_LEVEL_CONFIG[1];
}
function getSecretLevelLabel(level: number) {
return getSecretLevelInfo(level).label;
}
function getSecretLevelColor(level: number) {
return getSecretLevelInfo(level).color;
}
function formatFileSize(bytes: number): string {
if (!bytes || bytes === 0) return '';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
const secretLevelOptions = computed(() => {
const level = props.bussinessSecretLevel;
const options = [];
options.push({ label: '非密', value: 1 });
if (level >= 2) options.push({ label: '内部', value: 2 });
if (level >= 3) options.push({ label: '秘密', value: 3 });
if (level >= 4) options.push({ label: '机密', value: 4 });
return options;
});
function handleOpenUploadModal() {
uploadModalVisible.value = true;
}
async function handleUploadSuccess(result: any) {
if (result.results && Array.isArray(result.results)) {
result.results.forEach((fileInfo: any) => {
if (!fileInfo.bound) {
pendingFileList.value.push({
fileId: fileInfo.fileId,
fileName: fileInfo.fileName,
filePath: fileInfo.filePath,
fileSize: fileInfo.fileSize,
secretLevel: fileInfo.secretLevel,
fileUploadType: fileInfo.fileUploadType
});
}
});
}
if (props.bussinessId) {
await loadFileList();
}
emit('upload-success', result);
}
function handleUploadError(error: any) {
emit('upload-error', error);
}
async function handleDownload(file: any) {
try {
const fileUrl = file.filePath;
const fileName = file.fileName || fileUrl.substring(fileUrl.lastIndexOf('/') + 1);
const domainUrl = import.meta.env.VITE_GLOB_DOMAIN_URL;
const data = await defHttp.get(
{ url: `${domainUrl}/sys/file/download`, params: { fileUrl }, responseType: 'blob' },
{ isTransformResponse: false }
);
if (!data || data.size === 0) {
createMessage.warning('文件下载失败');
return;
}
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(new Blob([data]), fileName);
} else {
const blobUrl = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement('a');
link.style.display = 'none';
link.href = blobUrl;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
}
} catch (error) {
console.error('下载失败:', error);
createMessage.error('文件下载失败');
}
}
async function handleDownloadAll() {
const files = displayFileList.value;
if (files.length === 0) return;
downloadingAll.value = true;
let successCount = 0;
let failCount = 0;
for (const file of files) {
try {
await handleDownload(file);
successCount++;
} catch (error) {
failCount++;
console.error(`下载文件"${file.fileName}"失败:`, error);
}
}
downloadingAll.value = false;
if (successCount > 0) {
createMessage.success(`成功下载 ${successCount} 个文件${failCount > 0 ? `${failCount} 个失败` : ''}`);
} else {
createMessage.error('所有文件下载失败');
}
}
function handleDelete(file: any) {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: `确定要删除文件"${file.fileName}"吗?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
loading.value = true;
await defHttp.get({
url: '/sys/file/delete',
params: { id: file.fileId || file.id }
});
createMessage.success('文件删除成功');
if (file.pending) {
pendingFileList.value = pendingFileList.value.filter(f => f.fileId !== file.fileId);
} else {
serverFileList.value = serverFileList.value.filter(f => (f.id || f.fileId) !== (file.id || file.fileId));
}
emit('delete-success', file.fileId || file.id);
} catch (error) {
console.error('删除失败:', error);
createMessage.error('文件删除失败');
} finally {
loading.value = false;
}
}
});
}
async function loadFileList() {
if (!props.bussinessId) {
return;
}
try {
loading.value = true;
const result = await defHttp.get({
url: '/sys/file/getFileInfoByBussinessId',
params: { bussinessId: props.bussinessId }
});
if (Array.isArray(result)) {
serverFileList.value = result;
} else {
serverFileList.value = [];
}
} catch (error) {
console.error('加载文件列表失败:', error);
serverFileList.value = [];
createMessage.error('加载文件列表失败');
} finally {
loading.value = false;
}
}
//这个是在表单id还没有生成之前上传了附件,附件表没有存储bussinessId。当业务表保存后有id了,再把bussinessId更新到这些附件上去
async function bindBussinessId(newBussinessId: string) {
if (!newBussinessId) {
createMessage.warning('bussinessId 不能为空');
return false;
}
if (pendingFileList.value.length === 0) {
return true;
}
const fileIds = pendingFileList.value.map(f => f.fileId).join(',');
try {
binding.value = true;
await defHttp.get({
url: '/sys/file/updateFilesInfo',
params: {
bussinessId: newBussinessId,
fileIds: fileIds
}
});
createMessage.success(`成功关联 ${pendingFileList.value.length} 个文件`);
pendingFileList.value = [];
await loadFileList();
return true;
} catch (error) {
console.error('关联文件失败:', error);
createMessage.error('关联文件失败,请重试');
return false;
} finally {
binding.value = false;
}
}
function getPendingFileIds(): string[] {
return pendingFileList.value.map(f => f.fileId);
}
function hasPendingFiles(): boolean {
return pendingFileList.value.length > 0;
}
defineExpose({
bindBussinessId,
getPendingFileIds,
hasPendingFiles,
loadFileList
});
watch(() => props.bussinessId, (newVal) => {
if (newVal && newVal.length > 0) {
loadFileList();
} else {
serverFileList.value = [];
}
}, { immediate: true });
</script>
<style lang="less" scoped>
.s-upload-file-container {
width: 100%;
max-width: 100%;
.upload-btn-wrapper {
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.file-list-wrapper {
width: 100%;
min-height: 100px;
padding: 12px;
background: #fafafa;
border-radius: 4px;
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
.file-count {
font-size: 13px;
color: #8c8c8c;
}
}
}
.file-list {
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
margin-bottom: 8px;
transition: all 0.3s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&:last-child {
margin-bottom: 0;
}
.file-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
overflow: hidden;
.file-icon {
font-size: 16px;
color: #1890ff;
flex-shrink: 0;
}
.file-name {
font-size: 14px;
color: #262626;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.secret-tag {
margin-left: 4px;
font-weight: 500;
flex-shrink: 0;
}
.file-size {
font-size: 12px;
color: #8c8c8c;
flex-shrink: 0;
}
}
.file-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
}
}
}
</style>
@@ -0,0 +1,231 @@
# SUploadFile 文件上传组件使用指南
## 组件介绍
`SUploadFile` 是一个支持密级管理的通用文件上传下载删除组件,适用于 JeecgBoot 项目。
## 组件位置
```
jeecgboot-vue3/src/components/semri/fileComponent/SUploadFile.vue
```
## 功能特性
- ✅ 文件加密上传(支持密级选择)
- ✅ 文件解密下载
- ✅ 文件删除
- ✅ 密级权限控制(文件密级 <= 业务密级)
- ✅ 支持禁用状态(禁用后不能上传和删除)
- ✅ 文件列表展示(带密级标签)
- ✅ 支持业务关联(通过 businessId
## Props 参数
| 参数 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| bussinessId | String | '' | 否 | 业务ID,用于关联和查询文件 |
| bussinessSecretLevel | Number | 3 | 否 | 业务密级(1-非密,2-内部,3-秘密,4-机密) |
| disabled | Boolean | false | 否 | 是否禁用(禁用后不能上传和删除,但可以下载) |
## Events 事件
| 事件名 | 参数 | 说明 |
|--------|------|------|
| upload-success | result | 上传成功时触发,返回上传结果 |
| delete-success | fileId | 删除成功时触发,返回文件ID |
| upload-error | error | 上传失败时触发,返回错误信息 |
## 使用示例
### 基础用法
```vue
<template>
<a-form>
<a-form-item label="项目文件">
<SUploadFile
v-model:bussinessId="formData.id"
:bussiness-secret-level="formData.secretLevel"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import { ref } from 'vue';
import SUploadFile from '/@/semri/fileComponent/SUploadFile.vue';
const formData = ref({
id: '',
secretLevel: 3 // 秘密
});
</script>
```
### 带事件监听的用法
```vue
<template>
<a-form>
<a-form-item label="项目文件">
<SUploadFile
v-model:bussinessId="formData.id"
:bussiness-secret-level="formData.secretLevel"
:disabled="formData.readOnly"
@upload-success="handleUploadSuccess"
@delete-success="handleDeleteSuccess"
@upload-error="handleUploadError"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import { ref } from 'vue';
import { createMessage } from '/@/hooks/web/useMessage';
import SUploadFile from '/@/semri/fileComponent/SUploadFile.vue';
const formData = ref({
id: '123456',
secretLevel: 3, // 秘密
readOnly: false
});
// 上传成功处理
function handleUploadSuccess(result) {
console.log('上传成功:', result);
createMessage.success('文件上传完成');
}
// 删除成功处理
function handleDeleteSuccess(fileId) {
console.log('删除成功,文件ID:', fileId);
createMessage.success('文件删除完成');
}
// 上传失败处理
function handleUploadError(error) {
console.error('上传失败:', error);
}
</script>
```
### 不同密级的使用
```vue
<template>
<div>
<!-- 业务密级为非密只能上传非密文件 -->
<SUploadFile
bussiness-id="001"
:bussiness-secret-level="1"
/>
<!-- 业务密级为内部可以上传非密内部文件 -->
<SUploadFile
bussiness-id="002"
:bussiness-secret-level="2"
/>
<!-- 业务密级为秘密可以上传非密内部秘密文件 -->
<SUploadFile
bussiness-id="003"
:bussiness-secret-level="3"
/>
<!-- 业务密级为机密可以上传所有密级的文件 -->
<SUploadFile
bussiness-id="004"
:bussiness-secret-level="4"
/>
</div>
</template>
<script setup>
import SUploadFile from '/@/semri/fileComponent/SUploadFile.vue';
</script>
```
### 禁用状态(只读)
```vue
<template>
<a-form>
<a-form-item label="项目文件">
<SUploadFile
bussiness-id="123456"
:bussiness-secret-level="3"
:disabled="true"
/>
</a-form-item>
</a-form>
</template>
<script setup>
import SUploadFile from '/@/semri/fileComponent/SUploadFile.vue';
</script>
```
## 密级规则
### 文件密级选项规则
组件会根据业务密级自动生成可选的文件密级:
| 业务密级 | 可选文件密级 |
|---------|------------|
| 1-非密 | 1-非密 |
| 2-内部 | 1-非密,2-内部 |
| 3-秘密 | 1-非密,2-内部,3-秘密 |
| 4-机密 | 1-非密,2-内部,3-秘密,4-机密 |
### 密级显示颜色
- **非密**:绿色 (success)
- **内部**:蓝色 (processing)
- **秘密**:橙色 (warning)
- **机密**:红色 (error)
## API 接口
组件内部调用的后端接口:
- **上传文件**: `POST /sys/file/upload`
- 参数:file, secretLevel, business_id
- **获取文件列表**: `GET /sys/file/getFileInfoByBussinessId`
- 参数:bussinessId
- **下载文件**: `GET /sys/file/download`
- 参数:fileUrl
- **删除文件**: `GET /sys/file/delete`
- 参数:id
## 注意事项
1. **文件大小限制**:单个文件不超过 100MB
2. **密级校验**:文件密级必须 <= 业务密级
3. **禁用状态**:禁用时不显示上传和删除按钮,但可以下载文件
4. **业务ID**:如果提供了业务ID,组件会自动加载该业务关联的文件列表
5. **Token 处理**:组件自动处理 token,无需手动添加
## 常见问题
### Q: 为什么上传按钮不显示?
A: 检查是否设置了 `disabled="true"`,禁用状态下不会显示上传按钮。
### Q: 为什么只能选择某些密级?
A: 文件密级受业务密级限制,只能选择小于等于业务密级的文件密级。
### Q: 如何获取上传后文件的ID?
A: 监听 `upload-success` 事件,返回的结果中包含 `fileId`
### Q: 删除文件时如何自定义确认文案?
A: 当前版本删除确认文案是固定的,如需自定义请修改组件源码。
## 更新日志
### v1.0.0 (2026-03-27)
- 初始版本发布
- 支持文件上传、下载、删除功能
- 支持密级管理
- 支持禁用状态
@@ -0,0 +1,171 @@
<!--用户选择组件-->
<template>
<div>
<JSelectBiz @change="handleChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
<UserSelectByDepModal
:rowKey="rowKey"
@register="regModal"
@getSelectResult="setValue"
v-bind="getBindValue"
:userSecurityLevel="interUserSecurityLevel"
></UserSelectByDepModal>
</div>
</template>
<script lang="ts">
import { unref } from 'vue';
import UserSelectByDepModal from '@/components/semri/userComponent/modal/SzUserSelectByDepModal.vue';
import JSelectBiz from '@/components/Form/src/jeecg/components/base/JSelectBiz.vue';
import { defineComponent, ref, reactive, watchEffect, watch, provide } from 'vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
import SzUserSelectModal from '@/components/semri/userComponent/modal/SzUserSelectModal.vue';
export default defineComponent({
name: 'SzSelectUserByDept',
components: {
SzUserSelectModal,
UserSelectByDepModal,
JSelectBiz,
},
inheritAttrs: false,
props: {
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
rowKey: {
type: String,
default: 'username',
},
labelKey: {
type: String,
default: 'realname',
},
userSecurityLevel: {
type: Number,
default: 3,
},
},
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit, refs }) {
const emitData = ref<any[]>();
//注册model
const [regModal, { openModal }] = useModal();
//表单值
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
//下拉框选项值
const selectOptions = ref<SelectValue>([]);
//下拉框选中值
let selectValues = reactive<object>({
value: [],
change: false,
});
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
//下发 selectOptions,xxxBiz组件接收
provide('selectOptions', selectOptions);
//下发 selectValues,xxxBiz组件接收
provide('selectValues', selectValues);
//下发 loadingEcho,xxxBiz组件接收
provide('loadingEcho', loadingEcho);
const tag = ref(false);
const attrs = useAttrs();
const interUserSecurityLevel = ref<number>(props.userSecurityLevel);
/**
* 监听组件值
*/
watchEffect(() => {
initValue();
});
/**
* 监听selectValues变化
*/
watch(selectValues, () => {
if (selectValues) {
// update-begin--author:liaozhiyang---date:20250616---for:【QQYUN-12869】通过部门选择用户组件,必填状态下选择用户后,点击重置后,会出校验信息
if (props.value === undefined && selectValues.value?.length == 0) {
return;
}
// update-end--author:liaozhiyang---date:20250616---for:【QQYUN-12869】通过部门选择用户组件,必填状态下选择用户后,点击重置后,会出校验信息
state.value = selectValues.value;
}
});
/**
* 打卡弹出框
*/
function handleOpen() {
tag.value = true;
openModal(true, {
isUpdate: false,
});
}
/**
* 将字符串值转化为数组
*/
function initValue() {
let value = props.value ? props.value : [];
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
state.value = value.split(',');
selectValues.value = value.split(',');
} else {
selectValues.value = value;
}
}
/**
* 设置下拉框的值
*/
function setValue(options, values) {
selectOptions.value = options;
//emitData.value = values.join(",");
state.value = values;
selectValues.value = values;
emit('update:value', values);
emit('options-change', options);
}
function handleChange(values) {
emit('update:value', values);
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
return {
state,
attrs,
selectOptions,
getBindValue,
selectValues,
loadingEcho,
tag,
regModal,
setValue,
handleOpen,
handleChange,
};
},
});
</script>
<style lang="less" scoped>
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>
@@ -0,0 +1,234 @@
<!--用户选择组件-->
<template>
<div class="JselectUser">
<JSelectBiz @change="handleSelectChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
<!-- update-begin--author:liaozhiyang---date:20240515---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
<a-form-item>
<SzUserSelectModal
:rowKey="rowKey"
@register="regModal"
@getSelectResult="setValue"
v-bind="getBindValue"
:excludeUserIdList="excludeUserIdList"
:userSecurityLevel="interUserSecurityLevel"
@close="handleClose"
/>
</a-form-item>
<!-- update-end--author:liaozhiyang---date:20240515---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
</div>
</template>
<script lang="ts">
import { unref } from 'vue';
import SzUserSelectModal from '@/components/semri/userComponent/modal/SzUserSelectModal.vue';
import JSelectBiz from '@/components/Form/src/jeecg/components/base/JSelectBiz.vue';
import { defineComponent, ref, reactive, watchEffect, watch, provide } from 'vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
import { cloneDeep } from 'lodash-es';
import { SzUserSelectByDeptRoleSecurityModal } from '@/components/semri/userComponent/modal/SzUserSelectByDeptRoleSecurityModal.vue';
export default defineComponent({
name: 'SzSelectUser',
components: {
SzUserSelectModal,
JSelectBiz,
},
inheritAttrs: false,
props: {
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
labelKey: {
type: String,
default: 'realname',
},
rowKey: {
type: String,
default: 'username',
},
params: {
type: Object,
default: () => {},
},
userSecurityLevel: {
type: Number,
default: 3,
},
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
//排除用户id的集合
excludeUserIdList: {
type: Array,
default: () => [],
},
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
},
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit }) {
const emitData = ref<any[]>();
//注册model
const [regModal, { openModal }] = useModal();
//表单值
// const [state] = useRuleFormItem(props, 'value', 'change', emitData);
//下拉框选项值
const selectOptions = ref<SelectValue>([]);
//下拉框选中值
let selectValues = reactive<Recordable>({
value: [],
change: false,
});
const interUserSecurityLevel = ref<number>(props.userSecurityLevel);
let tempSave: any = [];
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
//下发 selectOptions,xxxBiz组件接收
provide('selectOptions', selectOptions);
//下发 selectValues,xxxBiz组件接收
provide('selectValues', selectValues);
//下发 loadingEcho,xxxBiz组件接收
provide('loadingEcho', loadingEcho);
const tag = ref(false);
const attrs = useAttrs();
/**
* 监听组件值
*/
watchEffect(() => {
// update-begin--author:liaozhiyang---date:20240611---for:【TV360X-576】已选中了数据,再次选择打开弹窗点击取消,数据清空了
//update-begin-author:liusq---date:2024-06-03--for: [TV360X-840]用户授权,没有选择,点取消,也会回显一个选过的用户
tempSave = [];
//update-end-author:liusq---date:2024-06-03--for:[TV360X-840]用户授权,没有选择,点取消,也会回显一个选过的用户
// update-end--author:liaozhiyang---date:20240611---for:【TV360X-576】已选中了数据,再次选择打开弹窗点击取消,数据清空了
props.value && initValue();
// 查询条件重置的时候 界面显示未清空
if (!props.value) {
selectValues.value = [];
}
});
/**
* 监听selectValues变化
*/
// watch(selectValues, () => {
// if (selectValues) {
// state.value = selectValues.value;
// }
// });
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
const excludeUserIdList = ref<any>([]);
/**
* 需要监听一下excludeUserIdList,否则modal获取不到
*/
watch(
() => props.excludeUserIdList,
(data) => {
excludeUserIdList.value = data;
},
{ immediate: true }
);
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
/**
* 打卡弹出框
*/
function handleOpen() {
tag.value = true;
openModal(true, {
isUpdate: false,
});
}
/**
* 将字符串值转化为数组
*/
function initValue() {
let value = props.value ? props.value : [];
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
// state.value = value.split(',');
selectValues.value = value.split(',');
tempSave = value.split(',');
} else {
// 【VUEN-857】兼容数组(行编辑的用法问题)
selectValues.value = value;
tempSave = cloneDeep(value);
}
}
/**
* 设置下拉框的值
*/
function setValue(options, values) {
selectOptions.value = options;
//emitData.value = values.join(",");
// state.value = values;
selectValues.value = values;
send(values);
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
const handleClose = () => {
if (tempSave.length) {
selectValues.value = cloneDeep(tempSave);
} else {
send(tempSave);
}
};
const handleSelectChange = (values) => {
tempSave = cloneDeep(values);
send(tempSave);
};
const send = (values) => {
let result = typeof props.value == 'string' ? values.join(',') : values;
emit('update:value', result);
emit('change', result);
};
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
return {
// state,
attrs,
selectOptions,
getBindValue,
selectValues,
loadingEcho,
tag,
regModal,
setValue,
handleOpen,
excludeUserIdList,
handleClose,
handleSelectChange,
};
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.JselectUser {
> .ant-form-item {
display: none;
}
}
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>
@@ -0,0 +1,12 @@
// 1. 修改引入路径,Jeecg 使用的是 defHttp
import { defHttp } from '@/utils/http/axios';
// 根据部门+角色+密级筛选用户
export function queryDepartRoleUserPageList(params) {
return defHttp.get({
// 2. 注意:Jeecg 默认配置了 baseUrl,通常不需要写 /jeecg-boot 前缀
url: '/sys/user/queryUserRoleComponentData',
// 3. GET 请求的参数 key 是 params,而不是 data
params,
});
}
@@ -0,0 +1,278 @@
<!--通过部门选择用户-->
<template>
<BasicModal v-bind="$attrs" @register="register" :title="modalTitle" width="1200px" @ok="handleOk" destroyOnClose @visible-change="visibleChange">
<a-row :gutter="10">
<a-col :md="7" :sm="24">
<a-card :style="{ minHeight: '613px', overflow: 'auto' }">
<!--组织机构-->
<BasicTree
ref="treeRef"
:style="{ minWidth: '250px' }"
selectable
@select="onDepSelect"
:load-data="loadChildrenTreeData"
:treeData="departTree"
:selectedKeys="selectedDepIds"
:expandedKeys="expandedKeys"
:clickRowToExpand="false"
></BasicTree>
</a-card>
</a-col>
<a-col :md="17" :sm="24">
<a-card :style="{ minHeight: '613px', overflow: 'auto' }">
<!--用户列表-->
<BasicTable ref="tableRef" v-bind="getBindValue" :searchInfo="searchInfo" :api="getTableList" :rowSelection="rowSelection"></BasicTable>
</a-card>
</a-col>
</a-row>
</BasicModal>
</template>
<script lang="ts">
import { defineComponent, unref, ref } from 'vue';
import { BasicModal, useModalInner } from '/src/components/Modal';
import { BasicTree } from '/src/components/Tree';
import { queryTreeList, getTableList as getTableListOrigin } from '/src/api/common/api';
import { createAsyncComponent } from '/src/utils/factory/createAsyncComponent';
import { useSelectBiz } from '/src/components/Form/src/jeecg/hooks/useSelectBiz';
import { useAttrs } from '/src/hooks/core/useAttrs';
import { queryDepartTreeSync as queryDepartTreeSyncOrigin } from '/src/views/system/depart/depart.api';
import { selectProps } from '/src/components/Form/src/jeecg/props/props';
export default defineComponent({
name: 'SzUserSelectByDepModal',
components: {
//此处需要异步加载BasicTable
BasicModal,
BasicTree,
BasicTable: createAsyncComponent(() => import('/src/components/Table/src/BasicTable.vue'), {
loading: true,
}),
},
props: {
...selectProps,
//选择框标题
modalTitle: {
type: String,
default: '部门用户选择',
},
userSecurityLevel: {
type: Number,
default: 3,
},
},
emits: ['register', 'getSelectResult'],
setup(props, { emit, refs }) {
const tableRef = ref();
const treeRef = ref();
//注册弹框
const [register, { closeModal }] = useModalInner(async (data) => {
await queryDepartTree();
});
const attrs = useAttrs();
const departTree = ref([]);
const selectedDepIds = ref([]);
const expandedKeys = ref([]);
const searchInfo = {};
/**
*表格配置
*/
const tableProps = {
columns: [
{
title: '用户账号',
dataIndex: 'username',
width: 180,
},
{
title: '用户姓名',
dataIndex: 'realname',
width: 180,
},
{
title: '性别',
dataIndex: 'sex_dictText',
width: 80,
},
{
title: '手机号码',
dataIndex: 'phone',
// width: 50,
},
{
title: '手机号码',
dataIndex: 'phone',
// width: 50,
},
{
title: '密级',
dataIndex: 'userSecurityLevel_dictText',
},
],
useSearchForm: true,
canResize: false,
showIndexColumn: false,
striped: true,
bordered: true,
size: 'small',
formConfig: {
//labelWidth: 200,
baseColProps: {
xs: 24,
sm: 8,
md: 6,
lg: 8,
xl: 6,
xxl: 10,
},
//update-begin-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
actionColOptions: {
xs: 24,
sm: 12,
md: 12,
lg: 12,
xl: 8,
xxl: 8,
},
//update-end-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
schemas: [
{
label: '账号',
field: 'username',
component: 'Input',
},
{
label: '姓名',
field: 'realname',
component: 'JInput',
},
{
label: '密级',
field: 'searchSecurityLevel',
component: 'JDictSelectTag',
componentProps: {
defaultValue: 3,
dictCode: 'user_security_level',
placeholder: '请选择密级',
stringToNumber: true,
},
},
],
resetFunc: customResetFunc,
},
};
const getBindValue = Object.assign({}, unref(props), unref(attrs), tableProps);
const [{ rowSelection, visibleChange, indexColumnProps, getSelectResult, reset }] = useSelectBiz(getTableList, getBindValue);
function getTableList(params) {
params = parseParams(params);
return getTableListOrigin({ ...params });
}
function queryDepartTreeSync(params) {
params = parseParams(params);
return queryDepartTreeSyncOrigin({ ...params });
}
/**
* 解析参数
* @param params
*/
function parseParams(params) {
if (props?.params) {
return {
...params,
...props.params,
};
}
return params;
}
/**
* 加载树形数据
*/
function queryDepartTree() {
queryDepartTreeSync().then((res) => {
if (res) {
departTree.value = res;
// 默认展开父节点
//expandedKeys.value = unref(departTree).map(item => item.id)
}
});
}
/**
* 加载子级部门
*/
async function loadChildrenTreeData(treeNode) {
try {
const result = await queryDepartTreeSync({
pid: treeNode.eventKey,
});
const asyncTreeAction = unref(treeRef);
if (asyncTreeAction) {
asyncTreeAction.updateNodeByKey(treeNode.eventKey, { children: result });
asyncTreeAction.setExpandedKeys([treeNode.eventKey, ...asyncTreeAction.getExpandedKeys()]);
}
} catch (e) {
console.error(e);
}
return Promise.resolve();
}
/**
* 点击树节点,筛选出对应的用户
*/
function onDepSelect(keys) {
if (keys[0] != null) {
if (unref(selectedDepIds)[0] !== keys[0]) {
selectedDepIds.value = [keys[0]];
}
searchInfo['departId'] = unref(selectedDepIds).join(',');
tableRef.value.reload();
}
}
/**
* 自定义重置方法
* */
async function customResetFunc() {
console.log('自定义查询');
//树节点清空
selectedDepIds.value = [];
//查询条件清空
searchInfo['departId'] = '';
//选择项清空
reset();
}
/**
* 确定选择
*/
function handleOk() {
getSelectResult((options, values) => {
//回传选项和已选择的值
emit('getSelectResult', options, values);
//关闭弹窗
closeModal();
});
}
return {
//config,
handleOk,
searchInfo,
register,
indexColumnProps,
visibleChange,
getBindValue,
rowSelection,
departTree,
selectedDepIds,
expandedKeys,
treeRef,
tableRef,
getTableList,
onDepSelect,
loadChildrenTreeData,
};
},
});
</script>
<style scoped lang="less"></style>
@@ -0,0 +1,196 @@
<template>
<a-modal :open="open" title="选择用户(部门+角色+密级)" :width="1000" :mask-closable="false" @cancel="handleCancel" @ok="handleOk" destroyOnClose>
<BasicForm @register="registerForm" />
<a-table
ref="tableRef"
row-key="id"
size="middle"
:columns="columns"
:data-source="userList"
:loading="loading"
:pagination="pagination"
:row-selection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }"
@change="handleTableChange"
:scroll="{ y: 400 }"
/>
</a-modal>
</template>
<script setup>
import { ref, reactive, watch, nextTick } from 'vue';
import { BasicForm, useForm } from '@/components/Form/index';
import { queryDepartRoleUserPageList } from '../api/user';
defineOptions({ name: 'SzUserSelectByDeptRoleSecurityModal' });
const props = defineProps({
open: { type: Boolean, default: false },
deptId: { type: String, default: null },
roleId: { type: String, default: null },
securityValue: { type: Number, default: null },
});
const emit = defineEmits(['update:open', 'confirm']);
// --- 状态定义 ---
const loading = ref(false);
const userList = ref([]);
const selectedRowKeys = ref([]);
const selectedRows = ref([]);
const innerDeptRoleId = ref(props.deptId);
const innerRoleId = ref(props.roleId);
const innerSecurityValue = ref(props.securityValue);
// 分页参数:注意 a-table 期待的属性名是 total, current, pageSize
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total}`,
});
// --- 表单配置 ---
const [registerForm, { getFieldsValue, resetFields, validate }] = useForm({
labelWidth: 80,
// 增加 baseColProps 让布局更紧凑
baseColProps: { span: 8 },
schemas: [
{
field: 'departId',
label: '部门',
component: 'JSelectDept',
componentProps: {
showButton: false,
checkStrictly: true, // 如果需要选子部门不级联
},
},
{
field: 'roleId',
label: '角色',
component: 'JSelectRole',
},
{
field: 'searchSecurityLevel',
label: '密级',
component: 'JDictSelectTag',
componentProps: { dictCode: 'user_security_level' },
},
],
showActionButtonGroup: true,
showAdvancedButton: false,
actionColOptions: { span: 24 }, // 按钮行占满全屏,右对齐
submitButtonOptions: { text: '查询' },
resetButtonOptions: { text: '重置' },
// 查询逻辑
submitFunc: async () => {
pagination.current = 1;
await loadData();
},
// 重置逻辑
resetFunc: async () => {
selectedRowKeys.value = [];
selectedRows.value = [];
pagination.current = 1;
await loadData();
},
});
const columns = [
{ title: '用户名', dataIndex: 'username', key: 'username', width: 120 },
{ title: '真实姓名', dataIndex: 'realname', key: 'realname', width: 120 },
{ title: '部门', dataIndex: 'orgCodeTxt', key: 'orgCodeTxt', width: 200 },
{ title: '密级', dataIndex: 'userSecurityLevel_dictText', key: 'userSecurityLevel', width: 100 },
];
// --- 核心数据加载逻辑 ---
async function loadData() {
loading.value = true;
try {
const values = getFieldsValue() || {};
const params = {
...values,
pageNo: pagination.current,
pageSize: pagination.pageSize,
};
const res = await queryDepartRoleUserPageList(params);
// --- 核心修复:兼容性处理 ---
// 检查 res 本身是否包含 records (你现在的情况)
// 或者 res.result 包含 records (之前的预期)
let dataSource = [];
let totalCount = 0;
if (res && res.records) {
// 对应你当前的情况
dataSource = res.records;
totalCount = res.total;
} else if (res && res.result && res.result.records) {
// 对应之前的预期
dataSource = res.result.records;
totalCount = res.result.total;
}
userList.value = dataSource || [];
pagination.total = parseInt(totalCount || 0);
console.log('解析后的数据:', userList.value); // 调试用
} catch (e) {
console.error('加载用户列表失败:', e);
} finally {
loading.value = false;
}
}
// --- 事件处理 ---
function onSelectChange(keys, rows) {
selectedRowKeys.value = keys;
selectedRows.value = rows;
}
function handleTableChange(p) {
pagination.current = p.current;
pagination.pageSize = p.pageSize;
loadData();
}
function handleCancel() {
emit('update:open', false);
}
function handleOk() {
if (selectedRows.value.length === 0) {
// 如果没有选择,可以加个提示,或者直接关闭
}
emit('confirm', selectedRows.value);
emit('update:open', false);
}
// 监听 Modal 打开
watch(
() => props.open,
async (val) => {
if (val) {
// 必须在 nextTick 后执行,确保 BasicForm 实例已挂载,getFieldsValue 可用
await nextTick();
loadData();
} else {
// 弹窗关闭时建议重置表单和选中项
resetFields();
selectedRowKeys.value = [];
selectedRows.value = [];
}
},
{ immediate: true }
);
</script>
<style scoped>
/* 调整表单底部间距 */
:deep(.ant-form) {
margin-bottom: 16px;
}
</style>
@@ -0,0 +1,325 @@
<!--用户选择框-->
<template>
<div>
<BasicModal
v-bind="$attrs"
@register="register"
:title="modalTitle"
:width="showSelected ? '1200px' : '900px'"
wrapClassName="j-user-select-modal"
@ok="handleOk"
@cancel="handleCancel"
:maxHeight="maxHeight"
:centered="true"
destroyOnClose
@visible-change="visibleChange"
>
<a-row>
<a-col :span="showSelected ? 18 : 24">
<BasicTable
ref="tableRef"
:columns="columns"
:scroll="tableScroll"
v-bind="getBindValue"
:useSearchForm="true"
:formConfig="formConfig"
:api="getUserList"
:searchInfo="searchInfo"
:rowSelection="rowSelection"
:indexColumnProps="indexColumnProps"
:afterFetch="afterFetch"
:beforeFetch="beforeFetch"
>
<!-- update-begin-author:taoyan date:2022-5-25 for: VUEN-1112一对多 用户选择 未显示选择条数及清空 -->
<template #tableTitle></template>
<!-- update-end-author:taoyan date:2022-5-25 for: VUEN-1112一对多 用户选择 未显示选择条数及清空 -->
</BasicTable>
</a-col>
<a-col :span="showSelected ? 6 : 0">
<BasicTable
v-bind="selectedTable"
:dataSource="selectRows"
:useSearchForm="true"
:formConfig="{ showActionButtonGroup: false, baseRowStyle: { minHeight: '40px' } }"
>
<!--操作栏-->
<template #action="{ record }">
<a href="javascript:void(0)" @click="handleDeleteSelected(record)"><Icon icon="ant-design:delete-outlined"></Icon></a>
</template>
</BasicTable>
</a-col>
</a-row>
</BasicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, unref, ref, watch } from 'vue';
import { BasicModal, useModalInner } from '/src/components/Modal';
import { getUserList } from '/src/api/common/api';
import { createAsyncComponent } from '/src/utils/factory/createAsyncComponent';
import { useSelectBiz } from '/src/components/Form/src/jeecg/hooks/useSelectBiz';
import { useAttrs } from '/src/hooks/core/useAttrs';
import { selectProps } from '/src/components/Form/src/jeecg/props/props';
export default defineComponent({
name: 'SzUserSelectModal',
components: {
//此处需要异步加载BasicTable
BasicModal,
BasicTable: createAsyncComponent(() => import('/src/components/Table/src/BasicTable.vue'), {
loading: true,
}),
},
props: {
...selectProps,
//选择框标题
modalTitle: {
type: String,
default: '选择用户',
},
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
//排除用户id的集合
excludeUserIdList: {
type: Array,
default: [],
},
userSecurityLevel: {
type: Number,
default: 3,
},
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
},
emits: ['register', 'getSelectResult', 'close'],
setup(props, { emit, refs }) {
// update-begin-author:taoyan date:2022-5-24 for: VUEN-1086 【移动端】用户选择 查询按钮 效果不好 列表展示没有滚动条
const tableScroll = ref<any>({ x: false });
const tableRef = ref();
const maxHeight = ref(600);
//注册弹框
const [register, { closeModal }] = useModalInner(() => {
if (window.innerWidth < 900) {
tableScroll.value = { x: 900 };
} else {
tableScroll.value = { x: false };
}
//update-begin-author:taoyan date:2022-6-2 for: VUEN-1112 一对多 用户选择 未显示选择条数,及清空
setTimeout(() => {
if (tableRef.value) {
tableRef.value.setSelectedRowKeys(selectValues['value'] || []);
}
}, 800);
//update-end-author:taoyan date:2022-6-2 for: VUEN-1112 一对多 用户选择 未显示选择条数,及清空
});
// update-end-author:taoyan date:2022-5-24 for: VUEN-1086 【移动端】用户选择 查询按钮 效果不好 列表展示没有滚动条
const attrs = useAttrs();
//表格配置
const config = {
canResize: false,
bordered: true,
size: 'small',
};
const getBindValue = Object.assign({}, unref(props), unref(attrs), config);
const [{ rowSelection, visibleChange, selectValues, indexColumnProps, getSelectResult, handleDeleteSelected, selectRows }] = useSelectBiz(
getUserList,
getBindValue,
emit
);
const searchInfo = ref(props.params);
// update-begin--author:liaozhiyang---date:20230811---for:【issues/657】右侧选中列表删除无效
watch(rowSelection.selectedRowKeys, (newVal) => {
//update-begin---author:wangshuai ---date: 20230829 fornull指针异常导致控制台报错页面不显示------------
if (tableRef.value) {
tableRef.value.setSelectedRowKeys(newVal);
}
//update-end---author:wangshuai ---date: 20230829 fornull指针异常导致控制台报错页面不显示------------
});
// update-end--author:liaozhiyang---date:20230811---for:【issues/657】右侧选中列表删除无效
//查询form
const formConfig = {
baseColProps: {
xs: 24,
sm: 8,
md: 6,
lg: 8,
xl: 6,
xxl: 6,
},
//update-begin-author:taoyan date:2022-5-24 for: VUEN-1086 【移动端】用户选择 查询按钮 效果不好 列表展示没有滚动条---查询表单按钮的栅格布局和表单的保持一致
actionColOptions: {
xs: 24,
sm: 8,
md: 8,
lg: 8,
xl: 8,
xxl: 8,
},
//update-end-author:taoyan date:2022-5-24 for: VUEN-1086 【移动端】用户选择 查询按钮 效果不好 列表展示没有滚动条---查询表单按钮的栅格布局和表单的保持一致
schemas: [
{
label: '账号',
field: 'username',
component: 'JInput',
},
{
label: '姓名',
field: 'realname',
component: 'JInput',
},
{
label: '密级',
field: 'searchSecurityLevel',
component: 'JDictSelectTag',
defaultValue: 3,
componentProps: {
dictCode: 'user_security_level',
placeholder: '请选择密级',
stringToNumber: true,
},
},
],
};
//定义表格列
const columns = [
{
title: '用户账号',
dataIndex: 'username',
width: 120,
align: 'left',
},
{
title: '用户姓名',
dataIndex: 'realname',
width: 120,
},
{
title: '密级',
dataIndex: 'userSecurityLevel_dictText',
width: 120,
},
{
title: '性别',
dataIndex: 'sex_dictText',
width: 50,
},
{
title: '手机号码',
dataIndex: 'phone',
width: 120,
},
{
title: '邮箱',
dataIndex: 'email',
// width: 40,
},
{
title: '状态',
dataIndex: 'status_dictText',
width: 80,
},
];
//已选择的table信息
const selectedTable = {
pagination: false,
showIndexColumn: false,
scroll: { y: 390 },
size: 'small',
canResize: false,
bordered: true,
rowKey: 'id',
columns: [
{
title: '用户姓名',
dataIndex: 'realname',
width: 40,
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 40,
slots: { customRender: 'action' },
},
],
};
/**
* 确定选择
*/
function handleOk() {
getSelectResult((options, values) => {
//回传选项和已选择的值
emit('getSelectResult', options, values);
//关闭弹窗
closeModal();
});
}
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
/**
* 用户返回结果逻辑查询
*/
function afterFetch(record) {
let excludeList = props.excludeUserIdList;
if (!excludeList) {
return record;
}
let arr: any[] = [];
//如果存在过滤用户id集合,并且后台返回的数据不为空
if (excludeList.length > 0 && record && record.length > 0) {
for (let item of record) {
if (excludeList.indexOf(item.id) < 0) {
arr.push({ ...item });
}
}
return arr;
}
return record;
}
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
const handleCancel = () => {
emit('close');
};
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
// update-begin--author:liaozhiyang---date:20240607---for:【TV360X-305】小屏幕展示10条
const clientHeight = document.documentElement.clientHeight * 200;
maxHeight.value = clientHeight > 600 ? 600 : clientHeight;
// update-end--author:liaozhiyang---date:20240607---for:【TV360X-305】小屏幕展示10条
//update-begin---author:wangshuai---date:2024-07-03---for:【TV360X-1629】用户选择组件不是根据创建时间正序排序的---
/**
* 请求之前根据创建时间排序
*
* @param params
*/
function beforeFetch(params) {
return Object.assign({ column: 'createTime', order: 'desc' }, params);
}
//update-end---author:wangshuai---date:2024-07-03---for:【TV360X-1629】用户选择组件不是根据创建时间正序排序的---
return {
//config,
handleOk,
searchInfo,
register,
indexColumnProps,
visibleChange,
getBindValue,
getUserList,
formConfig,
columns,
rowSelection,
selectRows,
selectedTable,
handleDeleteSelected,
tableScroll,
tableRef,
afterFetch,
handleCancel,
maxHeight,
beforeFetch,
};
},
});
</script>
@@ -0,0 +1,226 @@
<!--用户选择组件-->
<template>
<div class="JselectUser">
<JSelectBiz @change="handleSelectChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
<!-- update-begin--author:liaozhiyang---date:20240515---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
<a-form-item>
<UserSelectModal
:rowKey="rowKey"
@register="regModal"
@getSelectResult="setValue"
v-bind="getBindValue"
:excludeUserIdList="excludeUserIdList"
@close="handleClose"
/>
</a-form-item>
<!-- update-end--author:liaozhiyang---date:20240515---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
</div>
</template>
<script lang="ts">
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 { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
import { cloneDeep } from 'lodash-es';
export default defineComponent({
name: 'SzSelectUser',
components: {
UserSelectModal,
JSelectBiz,
},
inheritAttrs: false,
props: {
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
labelKey: {
type: String,
default: 'realname',
},
rowKey: {
type: String,
default: 'username',
},
params: {
type: Object,
default: () => {},
},
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
//排除用户id的集合
excludeUserIdList: {
type: Array,
default: () => [],
},
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
},
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit }) {
const emitData = ref<any[]>();
//注册model
const [regModal, { openModal }] = useModal();
//表单值
// const [state] = useRuleFormItem(props, 'value', 'change', emitData);
//下拉框选项值
const selectOptions = ref<SelectValue>([]);
//下拉框选中值
let selectValues = reactive<Recordable>({
value: [],
change: false,
});
let tempSave: any = [];
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
//下发 selectOptions,xxxBiz组件接收
provide('selectOptions', selectOptions);
//下发 selectValues,xxxBiz组件接收
provide('selectValues', selectValues);
//下发 loadingEcho,xxxBiz组件接收
provide('loadingEcho', loadingEcho);
const tag = ref(false);
const attrs = useAttrs();
/**
* 监听组件值
*/
watchEffect(() => {
// update-begin--author:liaozhiyang---date:20240611---for:【TV360X-576】已选中了数据,再次选择打开弹窗点击取消,数据清空了
//update-begin-author:liusq---date:2024-06-03--for: [TV360X-840]用户授权,没有选择,点取消,也会回显一个选过的用户
tempSave = [];
//update-end-author:liusq---date:2024-06-03--for:[TV360X-840]用户授权,没有选择,点取消,也会回显一个选过的用户
// update-end--author:liaozhiyang---date:20240611---for:【TV360X-576】已选中了数据,再次选择打开弹窗点击取消,数据清空了
props.value && initValue();
// 查询条件重置的时候 界面显示未清空
if (!props.value) {
selectValues.value = [];
}
});
/**
* 监听selectValues变化
*/
// watch(selectValues, () => {
// if (selectValues) {
// state.value = selectValues.value;
// }
// });
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
const excludeUserIdList = ref<any>([]);
/**
* 需要监听一下excludeUserIdList,否则modal获取不到
*/
watch(
() => props.excludeUserIdList,
(data) => {
excludeUserIdList.value = data;
},
{ immediate: true }
);
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
/**
* 打卡弹出框
*/
function handleOpen() {
tag.value = true;
openModal(true, {
isUpdate: false,
});
}
/**
* 将字符串值转化为数组
*/
function initValue() {
let value = props.value ? props.value : [];
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
// state.value = value.split(',');
selectValues.value = value.split(',');
tempSave = value.split(',');
} else {
// 【VUEN-857】兼容数组(行编辑的用法问题)
selectValues.value = value;
tempSave = cloneDeep(value);
}
}
/**
* 设置下拉框的值
*/
function setValue(options, values) {
selectOptions.value = options;
//emitData.value = values.join(",");
// state.value = values;
selectValues.value = values;
send(values);
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
// update-begin--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
const handleClose = () => {
if (tempSave.length) {
selectValues.value = cloneDeep(tempSave);
} else {
send(tempSave);
}
};
const handleSelectChange = (values) => {
tempSave = cloneDeep(values);
send(tempSave);
};
const send = (values) => {
let result = typeof props.value == 'string' ? values.join(',') : values;
emit('update:value', result);
emit('change', result);
};
// update-end--author:liaozhiyang---date:20240517---for:【QQYUN-9366】用户选择组件取消和关闭会把选择数据带入
return {
// state,
attrs,
selectOptions,
getBindValue,
selectValues,
loadingEcho,
tag,
regModal,
setValue,
handleOpen,
excludeUserIdList,
handleClose,
handleSelectChange,
};
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.JselectUser {
> .ant-form-item {
display: none;
}
}
// update-end--author:liaozhiyang---date:20240515---for:【QQYUN-9260】必填模式下会影响到弹窗内antd组件的样式
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>
@@ -0,0 +1,132 @@
<template>
<div style="padding: 30px; background: #fff; min-height: 500px">
<a-card title="自定义组件测试页面" :bordered="false">
<a-form layout="vertical">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="已选择的用户ID">
<a-input v-model:value="formData.userIds" readonly placeholder="选择后自动填充" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="选择用户">
<a-input-search v-model:value="formData.userNames" placeholder="请选择用户" enter-button="选择" readonly @search="openUserModal" />
</a-form-item>
</a-col>
</a-row>
<div style="margin-top: 20px">
<p><strong>当前选中详情JSON格式</strong></p>
<pre>{{ JSON.stringify(selectedRows, null, 2) }}</pre>
</div>
</a-form>
</a-card>
<SzUserSelectByDeptRoleSecurityModal :open="modalVisible" @confirm="handleConfirm" @cancel="handleCancel" />
<a-card title="根据部门,角色选满足对应条件用户组件" :bordered="false">
<a-form layout="vertical">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="已选择的用户">
<ApiSelect
placeholder="请选择用户(可多选)"
mode="multiple"
:api="getUserListApi"
:params="queryParam"
labelField="username"
valueField="id"
v-model:value="selectedUsers"
@change="handleUserChange"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card title="改造后的选部门组件,根据部门类别返回二级部门" :bordered="false">
<a-form layout="vertical">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="已选择的部门">
<SzSelectDept v-model:value="formDeptData.deptIds" labelKey="departName" rowKey="id" />
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
import SzUserSelectByDeptRoleSecurityModal from '../../components/semri/userComponent/modal/SzUserSelectByDeptRoleSecurityModal.vue';
import { ApiSelect } from '@/components/Form';
import { defHttp } from '@/utils/http/axios';
import SzSelectDept from '@/components/semri/deptComponent/SzSelectDept.vue';
const selectedUsers = ref([]);
const modalVisible = ref(false);
const selectedRows = ref([]);
const formData = reactive({ userIds: '', userNames: '' });
const formDeptData = reactive({
deptIds: '', // 如果是字符串,send函数会自动join成 "id1,id2"
});
const queryParam = reactive({
deptId: '1582683631414632450',
roleId: '2039894374372970498',
secretLevel: 3,
});
// 3. 定义 API 请求函数(对接你刚才写的 Controller 接口)
const getUserListApi = (params) => {
return defHttp.get({
url: '/sys/user/queryUserByDeptAndROle', // 替换为你真实的接口路径
params,
});
};
function handleUserChange(val) {
console.log('当前选中的值:', val);
}
function openUserModal() {
modalVisible.value = true;
}
// 【关键改进点 2】确认后关闭弹窗
function handleConfirm(rows) {
console.log('测试页面接收到数据:', rows);
selectedRows.value = rows;
if (rows && rows.length > 0) {
formData.userIds = rows.map((item) => item.id).join(',');
formData.userNames = rows.map((item) => item.realname).join(',');
} else {
formData.userIds = '';
formData.userNames = '';
}
// 成功接收数据后关闭窗口
modalVisible.value = false;
}
// 【关键改进点 3】处理取消按钮逻辑
function handleCancel() {
console.log('用户取消了选择');
modalVisible.value = false;
}
</script>
<style scoped>
pre {
background: #f5f5f5;
padding: 10px;
border-radius: 4px;
font-size: 12px;
max-height: 200px;
overflow: auto;
}
</style>
+20 -14
View File
@@ -34,10 +34,16 @@
<BasicDragVerify v-model:value="model[field]" />
</template>
<template #superQuery="{ model, field }">
<super-query :config="superQueryConfig" @search="(value)=>handleSuperQuery(value, model, field)"/>
<super-query :config="superQueryConfig" @search="(value) => handleSuperQuery(value, model, field)" />
</template>
<template #superQuery1="{ model, field }">
<super-query :config="superQueryConfig" @search="(value)=>handleSuperQuery(value, model, field)" :isCustomSave="true" :saveSearchData="saveSearchData" :save="handleSuperQuerySave"/>
<super-query
:config="superQueryConfig"
@search="(value) => handleSuperQuery(value, model, field)"
:isCustomSave="true"
:saveSearchData="saveSearchData"
:save="handleSuperQuerySave"
/>
</template>
</BasicForm>
</template>
@@ -82,17 +88,17 @@
}
const superQueryConfig = {
name:{ title: "名称", view: "text", type: "string", order: 1 },
birthday:{ title: "生日", view: "date", type: "string", order: 2 },
age:{ title: "年龄", view: "number", type: "number", order: 4 },
sex:{ title: "性别", view: "list", type: "string", dictCode: "sex", order: 5 },
bpmStatus:{ title: "流程状态", view: "list_multi", type: "string", dictCode: "bpm_status", order: 6 },
}
function handleSuperQuery(value, model, field){
if(value){
let str = decodeURI(value.superQueryParams)
console.log(str)
model[field] = str
name: { title: '名称', view: 'text', type: 'string', order: 1 },
birthday: { title: '生日', view: 'date', type: 'string', order: 2 },
age: { title: '年龄', view: 'number', type: 'number', order: 4 },
sex: { title: '性别', view: 'list', type: 'string', dictCode: 'sex', order: 5 },
bpmStatus: { title: '流程状态', view: 'list_multi', type: 'string', dictCode: 'bpm_status', order: 6 },
};
function handleSuperQuery(value, model, field) {
if (value) {
let str = decodeURI(value.superQueryParams);
console.log(str);
model[field] = str;
}
}
const saveSearchData = ref([
@@ -122,7 +128,7 @@
}
}, 1e3);
});
}
};
return {
schemas,
formElRef,
+40 -16
View File
@@ -197,22 +197,6 @@ export const schemas: FormSchema[] = [
span: 12,
},
},
{
field: 'depart3',
component: 'JEllipsis',
label: '选中部门',
colProps: { span: 12 },
},
{
field: 'depart2',
component: 'JSelectDept',
label: '选择部门',
helpMessage: ['component模式'],
componentProps: { showButton: false },
colProps: {
span: 12,
},
},
{
field: 'depart2',
component: 'JEllipsis',
@@ -239,6 +223,27 @@ export const schemas: FormSchema[] = [
label: '选中用户',
colProps: { span: 12 },
},
{
field: 'szuser1',
component: 'SzSelectUser',
label: 'Sz用户选择组件',
helpMessage: ['component模式'],
componentProps: {
labelKey: 'realname',
rowKey: 'id',
showSelected: true,
userSecurityLevel: 3,
},
colProps: {
span: 12,
},
},
{
field: 'user2',
component: 'JEllipsis',
label: '选中用户',
colProps: { span: 12 },
},
{
field: 'user3',
component: 'JSelectUserByDept',
@@ -258,6 +263,25 @@ export const schemas: FormSchema[] = [
label: '选中用户',
colProps: { span: 12 },
},
{
field: 'szuser3',
component: 'SzSelectUserByDept',
label: 'Sz部门选择用户',
helpMessage: ['component模式'],
componentProps: {
labelKey: 'realname',
rowKey: 'username',
},
colProps: {
span: 12,
},
},
{
field: 'szuser3',
component: 'JEllipsis',
label: '选中用户',
colProps: { span: 12 },
},
{
field: 'user4',
component: 'JSelectUserByDepartment',
+64
View File
@@ -0,0 +1,64 @@
import {defHttp} from '/@/utils/http/axios';
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/check/djCheck/list',
save='/check/djCheck/add',
edit='/check/djCheck/edit',
deleteOne = '/check/djCheck/delete',
deleteBatch = '/check/djCheck/deleteBatch',
importExcel = '/check/djCheck/importExcel',
exportXls = '/check/djCheck/exportXls',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口
* @param params
*/
export const list = (params) =>
defHttp.get({url: Api.list, params});
/**
* 删除单个
*/
export const deleteOne = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
* 批量删除
* @param params
*/
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
});
}
/**
* 保存或者更新
* @param params
*/
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({url: url, params});
}
+170
View File
@@ -0,0 +1,170 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { rules } from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '序号',
align: 'center',
dataIndex: 'sortNo',
},
{
title: '一级指标',
align: 'center',
dataIndex: 'primaryIndex',
},
{
title: '评价要点',
align: 'center',
dataIndex: 'evalPoints',
},
{
title: '评价内容',
align: 'center',
dataIndex: 'evalContent',
},
{
title: '问题表现',
align: 'center',
dataIndex: 'problemManifestation',
},
{
title: '整改措施',
align: 'center',
dataIndex: 'rectificationMeasures',
},
{
title: '完成时限',
align: 'center',
dataIndex: 'completionDeadline',
customRender: ({ text }) => {
text = !text ? '' : text.length > 10 ? text.substr(0, 10) : text;
return text;
},
},
{
title: '责任部门',
align: 'center',
dataIndex: 'responsibleDept_dictText',
},
{
title: '完成情况',
align: 'center',
dataIndex: 'completionStatus',
},
{
title: '是否可发起',
width: 150,
dataIndex: 'isLaunchable_dictText',
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [];
//表单数据
export const formSchema: FormSchema[] = [
{
label: '序号',
field: 'sortNo',
component: 'InputNumber',
},
{
label: '一级指标',
field: 'primaryIndex',
component: 'Input',
},
{
label: '评价要点',
field: 'evalPoints',
component: 'Input',
},
{
label: '评价内容',
field: 'evalContent',
component: 'Input',
},
{
label: '问题表现',
field: 'problemManifestation',
component: 'Input',
},
{
label: '整改措施',
field: 'rectificationMeasures',
component: 'Input',
},
{
label: '完成时限',
field: 'completionDeadline',
component: 'DatePicker',
componentProps: {
valueFormat: 'YYYY-MM-DD',
},
},
{
label: '责任部门',
field: 'responsibleDept',
component: 'JSelectDept',
componentProps: ({ formActionType, formModel }) => {
return {
sync: false,
checkStrictly: true,
defaultExpandLevel: 2,
onSelect: (options, values) => {
const { updateSchema } = formActionType;
//所属部门修改后更新负责部门下拉框数据
updateSchema([
{
field: 'departIds',
componentProps: { options },
},
]);
//update-begin---author:wangshuai---date:2024-05-11---for:【issues/1222】用户编辑界面“所属部门”与“负责部门”联动出错整---
if (!values) {
formModel.departIds = [];
return;
}
//update-end---author:wangshuai---date:2024-05-11---for:【issues/1222】用户编辑界面“所属部门”与“负责部门”联动出错整---
//所属部门修改后更新负责部门数据
formModel.departIds && (formModel.departIds = formModel.departIds.filter((item) => values.value.indexOf(item) > -1));
},
};
},
},
{
label: '完成情况',
field: 'completionStatus',
component: 'Input',
},
// TODO 主键隐藏字段,目前写死为ID
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
];
// 高级查询数据
export const superQuerySchema = {
sortNo: { title: '序号', order: 0, view: 'number', type: 'number' },
primaryIndex: { title: '一级指标', order: 1, view: 'text', type: 'string' },
evalPoints: { title: '评价要点', order: 2, view: 'text', type: 'string' },
evalContent: { title: '评价内容', order: 3, view: 'text', type: 'string' },
problemManifestation: { title: '问题表现', order: 4, view: 'text', type: 'string' },
rectificationMeasures: { title: '整改措施', order: 5, view: 'text', type: 'string' },
completionDeadline: { title: '完成时限', order: 6, view: 'date', type: 'string' },
responsibleDept: { title: '责任部门', order: 7, view: 'text', type: 'string' },
completionStatus: { title: '完成情况', order: 8, view: 'text', type: 'string' },
};
/**
* 流程表单调用这个方法获取formSchema
* @param param
*/
export function getBpmFormSchema(_formData): FormSchema[] {
// 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema
return formSchema;
}
+223
View File
@@ -0,0 +1,223 @@
<template>
<div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'check:dj_check:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增 </a-button>
<a-button type="primary" v-auth="'check:dj_check:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出 </a-button>
<j-upload-button type="primary" v-auth="'check:dj_check:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls"
>导入
</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'check:dj_check:deleteBatch'"
>批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
<!-- 高级查询 -->
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }"> </template>
</BasicTable>
<!-- 表单区域 -->
<DjCheckModal @register="registerModal" @success="handleSuccess"></DjCheckModal>
</div>
</template>
<script lang="ts" name="check-djCheck" setup>
import { ref, reactive, computed, unref } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import DjCheckModal from './components/DjCheckModal.vue';
import { columns, searchFormSchema, superQuerySchema } from './DjCheck.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './DjCheck.api';
import { downloadFile } from '/@/utils/common/renderUtils';
import { useUserStore } from '/@/store/modules/user';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
//日期个性化选择
const fieldPickers = reactive({});
const queryParam = reactive<any>({});
const checkedKeys = ref<Array<string | number>>([]);
const userStore = useUserStore();
const { createMessage } = useMessage();
//注册model
const [registerModal, { openModal }] = useModal();
//注册table数据
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '党建检查台账',
api: list,
columns,
canResize: true,
formConfig: {
//labelWidth: 120,
schemas: searchFormSchema,
autoSubmitOnEnter: true,
showAdvancedButton: true,
fieldMapToNumber: [],
fieldMapToTime: [],
},
actionColumn: {
width: 120,
fixed: 'right',
},
beforeFetch: (params) => {
if (params && fieldPickers) {
for (let key in fieldPickers) {
if (params[key]) {
params[key] = getDateByPicker(params[key], fieldPickers[key]);
}
}
}
return Object.assign(params, queryParam);
},
},
exportConfig: {
name: '党建检查台账',
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
reload();
}
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
showFooter: true,
});
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: true,
});
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: false,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'check:dj_check:edit',
},
{
label: '发起流程',
onClick: handleProcessLaunch(),
disabled: record.isLaunch !== 1,
//auth: 'rectifylist:dj_inspection_rectify_list:edit',
},
];
}
/**
* 发起流程
*/
function handleProcessLaunch() {
return null;
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
auth: 'check:dj_check:delete',
},
];
}
</script>
<style lang="less" scoped>
:deep(.ant-picker),
:deep(.ant-input-number) {
width: 100%;
}
</style>
@@ -0,0 +1,70 @@
<template>
<div style="min-height: 400px">
<BasicForm @register="registerForm"></BasicForm>
<div style="width: 100%; text-align: center" v-if="!formDisabled">
<a-button @click="submitForm" pre-icon="ant-design:check" type="primary"> </a-button>
</div>
</div>
</template>
<script lang="ts">
import { BasicForm, useForm } from '/@/components/Form/index';
import { computed, defineComponent } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes';
import { getBpmFormSchema } from '../DjCheck.data';
import { saveOrUpdate } from '../DjCheck.api';
export default defineComponent({
name: 'DjCheckForm',
components: {
BasicForm,
},
props: {
formData: propTypes.object.def({}),
formBpm: propTypes.bool.def(true),
},
setup(props) {
const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
labelWidth: 150,
schemas: getBpmFormSchema(props.formData),
showActionButtonGroup: false,
baseColProps: { span: 24 },
});
const formDisabled = computed(() => {
if (props.formData.disabled === false) {
return false;
}
return true;
});
let formData = {};
const queryByIdUrl = '/check/djCheck/queryById';
async function initFormData() {
let params = { id: props.formData.dataId };
const data = await defHttp.get({ url: queryByIdUrl, params });
formData = { ...data };
//设置表单的值
await setFieldsValue(formData);
//默认是禁用
await setProps({ disabled: formDisabled.value });
}
async function submitForm() {
let data = getFieldsValue();
let params = Object.assign({}, formData, data);
console.log('表单数据', params);
await saveOrUpdate(params, true);
}
initFormData();
return {
registerForm,
formDisabled,
submitForm,
};
},
});
</script>
@@ -0,0 +1,97 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
<BasicForm @register="registerForm" name="DjCheckForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref, reactive } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from '../DjCheck.data';
import { saveOrUpdate } from '../DjCheck.api';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
const { createMessage } = useMessage();
// Emits声明
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
const isDetail = ref(false);
//表单配置
const [registerForm, { setProps, resetFields, setFieldsValue, validate, scrollToField }] = useForm({
labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: { span: 24 },
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
isUpdate.value = !!data?.isUpdate;
isDetail.value = !!data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter });
});
//日期个性化选择
const fieldPickers = reactive({});
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
// 预处理日期数据
changeDateValue(values);
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} catch ({ errorFields }) {
if (errorFields) {
const firstField = errorFields[0];
if (firstField) {
scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
return Promise.reject(errorFields);
} finally {
setModalProps({ confirmLoading: false });
}
}
/**
* 处理日期值
* @param formData 表单数据
*/
const changeDateValue = (formData) => {
if (formData && fieldPickers) {
for (let key in fieldPickers) {
if (formData[key]) {
formData[key] = getDateByPicker(formData[key], fieldPickers[key]);
}
}
}
};
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>
@@ -0,0 +1,64 @@
import {defHttp} from '/@/utils/http/axios';
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/demoMetting/djDemoLifeMeeting/list',
save='/demoMetting/djDemoLifeMeeting/add',
edit='/demoMetting/djDemoLifeMeeting/edit',
deleteOne = '/demoMetting/djDemoLifeMeeting/delete',
deleteBatch = '/demoMetting/djDemoLifeMeeting/deleteBatch',
importExcel = '/demoMetting/djDemoLifeMeeting/importExcel',
exportXls = '/demoMetting/djDemoLifeMeeting/exportXls',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口
* @param params
*/
export const list = (params) =>
defHttp.get({url: Api.list, params});
/**
* 删除单个
*/
export const deleteOne = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
* 批量删除
* @param params
*/
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
});
}
/**
* 保存或者更新
* @param params
*/
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({url: url, params});
}
@@ -0,0 +1,200 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { rules } from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '序号',
align: 'center',
dataIndex: 'sortNo',
},
{
title: '问题分类',
align: 'center',
dataIndex: 'problemType',
},
{
title: '问题清单',
align: 'center',
dataIndex: 'problemList',
},
{
title: '问题表现',
align: 'center',
dataIndex: 'problemManifestation',
},
{
title: '责任领导',
align: 'center',
dataIndex: 'responsibleLeader',
},
{
title: '责任部门',
width: 150,
dataIndex: 'responsibleDept_dictText',
},
{
title: '整改措施',
align: 'center',
dataIndex: 'rectificationMeasures',
},
{
title: '完成时限',
align: 'center',
dataIndex: 'completionDeadline',
customRender: ({ text }) => {
text = !text ? '' : text.length > 10 ? text.substr(0, 10) : text;
return text;
},
},
{
title: '完成时间',
align: 'center',
dataIndex: 'completionDate',
customRender: ({ text }) => {
text = !text ? '' : text.length > 10 ? text.substr(0, 10) : text;
return text;
},
},
{
title: '输出成果',
align: 'center',
dataIndex: 'outputResults',
},
{
title: '完成情况',
align: 'center',
dataIndex: 'completionStatus',
},
{
title: '是否可发起',
align: 'center',
width: 150,
dataIndex: 'isLaunchable_dictText',
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [];
//表单数据
export const formSchema: FormSchema[] = [
{
label: '序号',
field: 'sortNo',
component: 'InputNumber',
},
{
label: '问题分类',
field: 'problemType',
component: 'InputNumber',
},
{
label: '问题清单',
field: 'problemList',
component: 'Input',
},
{
label: '问题表现',
field: 'problemManifestation',
component: 'Input',
},
{
label: '责任领导',
field: 'responsibleLeader',
component: 'Input',
},
{
label: '责任部门',
field: 'responsibleDept',
component: 'JSelectDept',
componentProps: ({ formActionType, formModel }) => {
return {
sync: false,
checkStrictly: true,
defaultExpandLevel: 2,
onSelect: (options, values) => {
const { updateSchema } = formActionType;
//所属部门修改后更新负责部门下拉框数据
updateSchema([
{
field: 'departIds',
componentProps: { options },
},
]);
//update-begin---author:wangshuai---date:2024-05-11---for:【issues/1222】用户编辑界面“所属部门”与“负责部门”联动出错整---
if (!values) {
formModel.departIds = [];
return;
}
//update-end---author:wangshuai---date:2024-05-11---for:【issues/1222】用户编辑界面“所属部门”与“负责部门”联动出错整---
//所属部门修改后更新负责部门数据
formModel.departIds && (formModel.departIds = formModel.departIds.filter((item) => values.value.indexOf(item) > -1));
},
};
},
},
{
label: '整改措施',
field: 'rectificationMeasures',
component: 'Input',
},
{
label: '完成时限',
field: 'completionDeadline',
component: 'DatePicker',
componentProps: {
valueFormat: 'YYYY-MM-DD',
},
},
{
label: '完成时间',
field: 'completionDate',
component: 'DatePicker',
componentProps: {
valueFormat: 'YYYY-MM-DD',
},
},
{
label: '输出成果',
field: 'outputResults',
component: 'Input',
},
{
label: '完成情况',
field: 'completionStatus',
component: 'Input',
},
// TODO 主键隐藏字段,目前写死为ID
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
];
// 高级查询数据
export const superQuerySchema = {
sortNo: { title: '序号', order: 0, view: 'number', type: 'number' },
problemType: { title: '问题分类', order: 1, view: 'number', type: 'number' },
problemList: { title: '问题清单', order: 2, view: 'text', type: 'string' },
problemManifestation: { title: '问题表现', order: 3, view: 'text', type: 'string' },
responsibleLeader: { title: '责任领导', order: 4, view: 'text', type: 'string' },
responsibleDept: { title: '责任部门', order: 5, view: 'text', type: 'string' },
rectificationMeasures: { title: '整改措施', order: 6, view: 'text', type: 'string' },
completionDeadline: { title: '完成时限', order: 7, view: 'date', type: 'string' },
completionDate: { title: '完成时间', order: 8, view: 'date', type: 'string' },
outputResults: { title: '输出成果', order: 9, view: 'text', type: 'string' },
completionStatus: { title: '完成情况', order: 10, view: 'text', type: 'string' },
};
/**
* 流程表单调用这个方法获取formSchema
* @param param
*/
export function getBpmFormSchema(_formData): FormSchema[] {
// 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema
return formSchema;
}
@@ -0,0 +1,223 @@
<template>
<div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'demoMetting:dj_demo_life_meeting:add'" @click="handleAdd" preIcon="ant-design:plus-outlined">
新增</a-button
>
<a-button type="primary" v-auth="'demoMetting:dj_demo_life_meeting:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls">
导出</a-button
>
<j-upload-button
type="primary"
v-auth="'demoMetting:dj_demo_life_meeting:importExcel'"
preIcon="ant-design:import-outlined"
@click="onImportXls"
>导入</j-upload-button
>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'demoMetting:dj_demo_life_meeting:deleteBatch'"
>批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
<!-- 高级查询 -->
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }"> </template>
</BasicTable>
<!-- 表单区域 -->
<DjDemoLifeMeetingModal @register="registerModal" @success="handleSuccess"></DjDemoLifeMeetingModal>
</div>
</template>
<script lang="ts" name="demoMetting-djDemoLifeMeeting" setup>
import { ref, reactive, computed, unref } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import DjDemoLifeMeetingModal from './components/DjDemoLifeMeetingModal.vue';
import { columns, searchFormSchema, superQuerySchema } from './DjDemoLifeMeeting.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './DjDemoLifeMeeting.api';
import { downloadFile } from '/@/utils/common/renderUtils';
import { useUserStore } from '/@/store/modules/user';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
//日期个性化选择
const fieldPickers = reactive({});
const queryParam = reactive<any>({});
const checkedKeys = ref<Array<string | number>>([]);
const userStore = useUserStore();
const { createMessage } = useMessage();
//注册model
const [registerModal, { openModal }] = useModal();
//注册table数据
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '所领导板子民主生活会问题台账',
api: list,
columns,
canResize: true,
formConfig: {
//labelWidth: 120,
schemas: searchFormSchema,
autoSubmitOnEnter: true,
showAdvancedButton: true,
fieldMapToNumber: [],
fieldMapToTime: [],
},
actionColumn: {
width: 120,
fixed: 'right',
},
beforeFetch: (params) => {
if (params && fieldPickers) {
for (let key in fieldPickers) {
if (params[key]) {
params[key] = getDateByPicker(params[key], fieldPickers[key]);
}
}
}
return Object.assign(params, queryParam);
},
},
exportConfig: {
name: '所领导板子民主生活会问题台账',
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
reload();
}
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
showFooter: true,
});
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: true,
});
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: false,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 发起流程
*/
function handleProcessLaunch() {
return null;
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'demoMetting:dj_demo_life_meeting:edit',
},
{
label: '发起流程',
onClick: handleProcessLaunch(),
disabled: record.isLaunch === 0,
//auth: 'rectifylist:dj_inspection_rectify_list:edit',
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
auth: 'demoMetting:dj_demo_life_meeting:delete',
},
];
}
</script>
<style lang="less" scoped>
:deep(.ant-picker),
:deep(.ant-input-number) {
width: 100%;
}
</style>
@@ -0,0 +1,70 @@
<template>
<div style="min-height: 400px">
<BasicForm @register="registerForm"></BasicForm>
<div style="width: 100%;text-align: center" v-if="!formDisabled">
<a-button @click="submitForm" pre-icon="ant-design:check" type="primary"> </a-button>
</div>
</div>
</template>
<script lang="ts">
import {BasicForm, useForm} from '/@/components/Form/index';
import {computed, defineComponent} from 'vue';
import {defHttp} from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes';
import {getBpmFormSchema} from '../DjDemoLifeMeeting.data';
import {saveOrUpdate} from '../DjDemoLifeMeeting.api';
export default defineComponent({
name: "DjDemoLifeMeetingForm",
components:{
BasicForm
},
props:{
formData: propTypes.object.def({}),
formBpm: propTypes.bool.def(true),
},
setup(props){
const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
labelWidth: 150,
schemas: getBpmFormSchema(props.formData),
showActionButtonGroup: false,
baseColProps: {span: 24}
});
const formDisabled = computed(()=>{
if(props.formData.disabled === false){
return false;
}
return true;
});
let formData = {};
const queryByIdUrl = '/demoMetting/djDemoLifeMeeting/queryById';
async function initFormData(){
let params = {id: props.formData.dataId};
const data = await defHttp.get({url: queryByIdUrl, params});
formData = {...data}
//设置表单的值
await setFieldsValue(formData);
//默认是禁用
await setProps({disabled: formDisabled.value})
}
async function submitForm() {
let data = getFieldsValue();
let params = Object.assign({}, formData, data);
console.log('表单数据', params)
await saveOrUpdate(params, true)
}
initFormData();
return {
registerForm,
formDisabled,
submitForm,
}
}
});
</script>
@@ -0,0 +1,99 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
<BasicForm @register="registerForm" name="DjDemoLifeMeetingForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import {ref, computed, unref, reactive} from 'vue';
import {BasicModal, useModalInner} from '/@/components/Modal';
import {BasicForm, useForm} from '/@/components/Form/index';
import {formSchema} from '../DjDemoLifeMeeting.data';
import {saveOrUpdate} from '../DjDemoLifeMeeting.api';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
const { createMessage } = useMessage();
// Emits声明
const emit = defineEmits(['register','success']);
const isUpdate = ref(true);
const isDetail = ref(false);
//表单配置
const [registerForm, { setProps,resetFields, setFieldsValue, validate, scrollToField }] = useForm({
labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: {span: 24}
});
//表单赋值
const [registerModal, {setModalProps, closeModal}] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({confirmLoading: false,showCancelBtn:!!data?.showFooter,showOkBtn:!!data?.showFooter});
isUpdate.value = !!data?.isUpdate;
isDetail.value = !!data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter })
});
//日期个性化选择
const fieldPickers = reactive({
});
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
// 预处理日期数据
changeDateValue(values);
setModalProps({confirmLoading: true});
//提交表单
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} catch ({ errorFields }) {
if (errorFields) {
const firstField = errorFields[0];
if (firstField) {
scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
return Promise.reject(errorFields);
} finally {
setModalProps({confirmLoading: false});
}
}
/**
* 处理日期值
* @param formData 表单数据
*/
const changeDateValue = (formData) => {
if (formData && fieldPickers) {
for (let key in fieldPickers) {
if (formData[key]) {
formData[key] = getDateByPicker(formData[key], fieldPickers[key]);
}
}
}
};
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>
@@ -0,0 +1,64 @@
import {defHttp} from '/@/utils/http/axios';
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/peopleService/djPeopleService/list',
save='/peopleService/djPeopleService/add',
edit='/peopleService/djPeopleService/edit',
deleteOne = '/peopleService/djPeopleService/delete',
deleteBatch = '/peopleService/djPeopleService/deleteBatch',
importExcel = '/peopleService/djPeopleService/importExcel',
exportXls = '/peopleService/djPeopleService/exportXls',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口
* @param params
*/
export const list = (params) =>
defHttp.get({url: Api.list, params});
/**
* 删除单个
*/
export const deleteOne = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
* 批量删除
* @param params
*/
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
});
}
/**
* 保存或者更新
* @param params
*/
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({url: url, params});
}
@@ -0,0 +1,138 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { rules } from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '序号',
align: 'center',
dataIndex: 'sortNo',
},
{
title: '问题分类',
align: 'center',
dataIndex: 'problemType',
},
{
title: '具体措施',
align: 'center',
dataIndex: 'specificMeasure',
},
{
title: '完成时限',
align: 'center',
dataIndex: 'completionDeadline',
customRender: ({ text }) => {
text = !text ? '' : text.length > 10 ? text.substr(0, 10) : text;
return text;
},
},
{
title: '责任部门',
align: 'center',
dataIndex: 'responsibleDept_dictText',
},
{
title: '完成情况',
align: 'center',
dataIndex: 'completionStatus',
},
{
title: '是否可发起',
align: 'center',
width: 150,
dataIndex: 'isLaunchable_dictText',
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [];
//表单数据
export const formSchema: FormSchema[] = [
{
label: '序号',
field: 'sortNo',
component: 'InputNumber',
},
{
label: '问题分类',
field: 'problemType',
component: 'Input',
},
{
label: '具体措施',
field: 'specificMeasure',
component: 'Input',
},
{
label: '完成时限',
field: 'completionDeadline',
component: 'DatePicker',
componentProps: {
valueFormat: 'YYYY-MM-DD',
},
},
{
label: '责任部门',
field: 'responsibleDept',
component: 'JSelectDept',
componentProps: ({ formActionType, formModel }) => {
return {
sync: false,
checkStrictly: true,
defaultExpandLevel: 2,
onSelect: (options, values) => {
const { updateSchema } = formActionType;
//所属部门修改后更新负责部门下拉框数据
updateSchema([
{
field: 'departIds',
componentProps: { options },
},
]);
//update-begin---author:wangshuai---date:2024-05-11---for:【issues/1222】用户编辑界面“所属部门”与“负责部门”联动出错整---
if (!values) {
formModel.departIds = [];
return;
}
//update-end---author:wangshuai---date:2024-05-11---for:【issues/1222】用户编辑界面“所属部门”与“负责部门”联动出错整---
//所属部门修改后更新负责部门数据
formModel.departIds && (formModel.departIds = formModel.departIds.filter((item) => values.value.indexOf(item) > -1));
},
};
},
},
{
label: '完成情况',
field: 'completionStatus',
component: 'Input',
},
// TODO 主键隐藏字段,目前写死为ID
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
];
// 高级查询数据
export const superQuerySchema = {
sortNo: { title: '序号', order: 0, view: 'number', type: 'number' },
problemType: { title: '问题分类', order: 1, view: 'text', type: 'string' },
specificMeasure: { title: '具体措施', order: 2, view: 'text', type: 'string' },
completionDeadline: { title: '完成时限', order: 3, view: 'date', type: 'string' },
responsibleDept: { title: '责任部门', order: 4, view: 'text', type: 'string' },
completionStatus: { title: '完成情况', order: 5, view: 'text', type: 'string' },
};
/**
* 流程表单调用这个方法获取formSchema
* @param param
*/
export function getBpmFormSchema(_formData): FormSchema[] {
// 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema
return formSchema;
}
@@ -0,0 +1,221 @@
<template>
<div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'peopleService:dj_people_service:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="'peopleService:dj_people_service:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls">
导出</a-button
>
<j-upload-button
type="primary"
v-auth="'peopleService:dj_people_service:importExcel'"
preIcon="ant-design:import-outlined"
@click="onImportXls"
>导入</j-upload-button
>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'peopleService:dj_people_service:deleteBatch'"
>批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
<!-- 高级查询 -->
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }"> </template>
</BasicTable>
<!-- 表单区域 -->
<DjPeopleServiceModal @register="registerModal" @success="handleSuccess"></DjPeopleServiceModal>
</div>
</template>
<script lang="ts" name="peopleService-djPeopleService" setup>
import { ref, reactive, computed, unref } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import DjPeopleServiceModal from './components/DjPeopleServiceModal.vue';
import { columns, searchFormSchema, superQuerySchema } from './DjPeopleService.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './DjPeopleService.api';
import { downloadFile } from '/@/utils/common/renderUtils';
import { useUserStore } from '/@/store/modules/user';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
//日期个性化选择
const fieldPickers = reactive({});
const queryParam = reactive<any>({});
const checkedKeys = ref<Array<string | number>>([]);
const userStore = useUserStore();
const { createMessage } = useMessage();
//注册model
const [registerModal, { openModal }] = useModal();
//注册table数据
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '我为群众办实事台账',
api: list,
columns,
canResize: true,
formConfig: {
//labelWidth: 120,
schemas: searchFormSchema,
autoSubmitOnEnter: true,
showAdvancedButton: true,
fieldMapToNumber: [],
fieldMapToTime: [],
},
actionColumn: {
width: 120,
fixed: 'right',
},
beforeFetch: (params) => {
if (params && fieldPickers) {
for (let key in fieldPickers) {
if (params[key]) {
params[key] = getDateByPicker(params[key], fieldPickers[key]);
}
}
}
return Object.assign(params, queryParam);
},
},
exportConfig: {
name: '我为群众办实事台账',
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
reload();
}
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
showFooter: true,
});
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: true,
});
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: false,
});
}
/**
* 发起流程
*/
function handleProcessLaunch() {
return null;
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'peopleService:dj_people_service:edit',
},
{
label: '发起流程',
onClick: handleProcessLaunch(),
disabled: record.isLaunch === 0,
//auth: 'rectifylist:dj_inspection_rectify_list:edit',
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
auth: 'peopleService:dj_people_service:delete',
},
];
}
</script>
<style lang="less" scoped>
:deep(.ant-picker),
:deep(.ant-input-number) {
width: 100%;
}
</style>
@@ -0,0 +1,70 @@
<template>
<div style="min-height: 400px">
<BasicForm @register="registerForm"></BasicForm>
<div style="width: 100%;text-align: center" v-if="!formDisabled">
<a-button @click="submitForm" pre-icon="ant-design:check" type="primary"> </a-button>
</div>
</div>
</template>
<script lang="ts">
import {BasicForm, useForm} from '/@/components/Form/index';
import {computed, defineComponent} from 'vue';
import {defHttp} from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes';
import {getBpmFormSchema} from '../DjPeopleService.data';
import {saveOrUpdate} from '../DjPeopleService.api';
export default defineComponent({
name: "DjPeopleServiceForm",
components:{
BasicForm
},
props:{
formData: propTypes.object.def({}),
formBpm: propTypes.bool.def(true),
},
setup(props){
const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
labelWidth: 150,
schemas: getBpmFormSchema(props.formData),
showActionButtonGroup: false,
baseColProps: {span: 24}
});
const formDisabled = computed(()=>{
if(props.formData.disabled === false){
return false;
}
return true;
});
let formData = {};
const queryByIdUrl = '/peopleService/djPeopleService/queryById';
async function initFormData(){
let params = {id: props.formData.dataId};
const data = await defHttp.get({url: queryByIdUrl, params});
formData = {...data}
//设置表单的值
await setFieldsValue(formData);
//默认是禁用
await setProps({disabled: formDisabled.value})
}
async function submitForm() {
let data = getFieldsValue();
let params = Object.assign({}, formData, data);
console.log('表单数据', params)
await saveOrUpdate(params, true)
}
initFormData();
return {
registerForm,
formDisabled,
submitForm,
}
}
});
</script>
@@ -0,0 +1,99 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
<BasicForm @register="registerForm" name="DjPeopleServiceForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import {ref, computed, unref, reactive} from 'vue';
import {BasicModal, useModalInner} from '/@/components/Modal';
import {BasicForm, useForm} from '/@/components/Form/index';
import {formSchema} from '../DjPeopleService.data';
import {saveOrUpdate} from '../DjPeopleService.api';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
const { createMessage } = useMessage();
// Emits声明
const emit = defineEmits(['register','success']);
const isUpdate = ref(true);
const isDetail = ref(false);
//表单配置
const [registerForm, { setProps,resetFields, setFieldsValue, validate, scrollToField }] = useForm({
labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: {span: 24}
});
//表单赋值
const [registerModal, {setModalProps, closeModal}] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({confirmLoading: false,showCancelBtn:!!data?.showFooter,showOkBtn:!!data?.showFooter});
isUpdate.value = !!data?.isUpdate;
isDetail.value = !!data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter })
});
//日期个性化选择
const fieldPickers = reactive({
});
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
// 预处理日期数据
changeDateValue(values);
setModalProps({confirmLoading: true});
//提交表单
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} catch ({ errorFields }) {
if (errorFields) {
const firstField = errorFields[0];
if (firstField) {
scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
return Promise.reject(errorFields);
} finally {
setModalProps({confirmLoading: false});
}
}
/**
* 处理日期值
* @param formData 表单数据
*/
const changeDateValue = (formData) => {
if (formData && fieldPickers) {
for (let key in fieldPickers) {
if (formData[key]) {
formData[key] = getDateByPicker(formData[key], fieldPickers[key]);
}
}
}
};
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>
@@ -0,0 +1,85 @@
import {defHttp} from "/@/utils/http/axios";
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/rectifylist/djInspectionRectifyList/rootList',
save='/rectifylist/djInspectionRectifyList/add',
edit='/rectifylist/djInspectionRectifyList/edit',
deleteDjInspectionRectifyList = '/rectifylist/djInspectionRectifyList/delete',
importExcel = '/rectifylist/djInspectionRectifyList/importExcel',
exportXls = '/rectifylist/djInspectionRectifyList/exportXls',
loadTreeData = '/rectifylist/djInspectionRectifyList/loadTreeRoot',
getChildList = '/rectifylist/djInspectionRectifyList/childList',
getChildListBatch = '/rectifylist/djInspectionRectifyList/getChildListBatch',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
* @param params
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口
* @param params
*/
export const list = (params) =>
defHttp.get({url: Api.list, params});
/**
* 删除
*/
export const deleteDjInspectionRectifyList = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteDjInspectionRectifyList, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
* 批量删除
* @param params
*/
export const batchDeleteDjInspectionRectifyList = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({url: Api.deleteDjInspectionRectifyList, data: params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
});
}
/**
* 保存或者更新
* @param params
*/
export const saveOrUpdateDict = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({url: url, params});
}
/**
* 查询全部树形节点数据
* @param params
*/
export const loadTreeData = (params) =>
defHttp.get({url: Api.loadTreeData,params});
/**
* 查询子节点数据
* @param params
*/
export const getChildList = (params) =>
defHttp.get({url: Api.getChildList, params});
/**
* 批量查询子节点数据
* @param params
*/
export const getChildListBatch = (params) =>
defHttp.get({url: Api.getChildListBatch, params},{isTransformResponse:false});
@@ -0,0 +1,222 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { rules } from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '问题名称',
align: 'left',
dataIndex: 'issueName',
},
{
title: '问题分类',
align: 'center',
dataIndex: 'problemType',
},
{
title: '面上问题',
align: 'center',
dataIndex: 'msIssue',
},
{
title: '具体问题',
align: 'center',
dataIndex: 'specificIssue',
},
{
title: '整改措施',
align: 'center',
dataIndex: 'rectificationMeasures',
},
{
title: '完成时限',
align: 'center',
dataIndex: 'completionDeadline',
customRender: ({ text }) => {
text = !text ? '' : text.length > 10 ? text.substr(0, 10) : text;
return text;
},
},
{
title: '责任部门',
align: 'center',
dataIndex: 'responsibleDept_dictText',
},
{
title: '分管所领导',
align: 'center',
dataIndex: 'supervisingLeader',
},
{
title: '标志性成果',
align: 'center',
dataIndex: 'landmarkAchievement',
},
{
title: '完成时间',
align: 'center',
dataIndex: 'completionDate',
customRender: ({ text }) => {
text = !text ? '' : text.length > 10 ? text.substr(0, 10) : text;
return text;
},
},
{
title: '监督意见',
align: 'center',
dataIndex: 'supervisionOpinion',
},
{
title: '措施数量',
align: 'center',
dataIndex: 'measureCount',
},
{
title: '是否可发起',
align: 'center',
width: 150,
dataIndex: 'isLaunchable_dictText',
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [];
//表单数据
export const formSchema: FormSchema[] = [
{
label: '父级节点',
field: 'pid',
component: 'JTreeSelect',
componentProps: {
dict: 'dj_inspection_rectify_list,issue_name,id',
pidField: 'pid',
pidValue: '0',
hasChildField: 'has_child',
},
},
{
label: '问题名称',
field: 'issueName',
component: 'Input',
},
{
label: '问题分类',
field: 'problemType',
component: 'InputNumber',
},
{
label: '面上问题',
field: 'msIssue',
component: 'Input',
},
{
label: '具体问题',
field: 'specificIssue',
component: 'Input',
},
{
label: '整改措施',
field: 'rectificationMeasures',
component: 'Input',
},
{
label: '完成时限',
field: 'completionDeadline',
component: 'DatePicker',
componentProps: {
valueFormat: 'YYYY-MM-DD',
},
},
{
label: '责任部门',
field: 'responsibleDept',
component: 'JSelectDept',
componentProps: ({ formActionType, formModel }) => {
return {
sync: false,
checkStrictly: true,
defaultExpandLevel: 2,
onSelect: (options, values) => {
const { updateSchema } = formActionType;
//所属部门修改后更新负责部门下拉框数据
updateSchema([
{
field: 'departIds',
componentProps: { options },
},
]);
//update-begin---author:wangshuai---date:2024-05-11---for:【issues/1222】用户编辑界面“所属部门”与“负责部门”联动出错整---
if (!values) {
formModel.departIds = [];
return;
}
//update-end---author:wangshuai---date:2024-05-11---for:【issues/1222】用户编辑界面“所属部门”与“负责部门”联动出错整---
//所属部门修改后更新负责部门数据
formModel.departIds && (formModel.departIds = formModel.departIds.filter((item) => values.value.indexOf(item) > -1));
},
};
},
},
{
label: '分管所领导',
field: 'supervisingLeader',
component: 'Input',
},
{
label: '标志性成果',
field: 'landmarkAchievement',
component: 'Input',
},
{
label: '完成时间',
field: 'completionDate',
component: 'DatePicker',
componentProps: {
valueFormat: 'YYYY-MM-DD',
},
},
{
label: '监督意见',
field: 'supervisionOpinion',
component: 'Input',
},
{
label: '措施数量',
field: 'measureCount',
component: 'InputNumber',
},
// TODO 主键隐藏字段,目前写死为ID
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
];
// 高级查询数据
export const superQuerySchema = {
issueName: { title: '问题名称', order: 1, view: 'text', type: 'string' },
problemType: { title: '问题分类', order: 2, view: 'number', type: 'number' },
msIssue: { title: '面上问题', order: 3, view: 'text', type: 'string' },
specificIssue: { title: '具体问题', order: 4, view: 'text', type: 'string' },
rectificationMeasures: { title: '整改措施', order: 5, view: 'text', type: 'string' },
completionDeadline: { title: '完成时限', order: 6, view: 'date', type: 'string' },
responsibleDept: { title: '责任部门', order: 7, view: 'text', type: 'string' },
supervisingLeader: { title: '分管所领导', order: 8, view: 'text', type: 'string' },
landmarkAchievement: { title: '标志性成果', order: 9, view: 'text', type: 'string' },
completionDate: { title: '完成时间', order: 10, view: 'date', type: 'string' },
supervisionOpinion: { title: '监督意见', order: 11, view: 'text', type: 'string' },
measureCount: { title: '措施数量', order: 12, view: 'number', type: 'number' },
};
/**
* 流程表单调用这个方法获取formSchema
* @param param
*/
export function getBpmFormSchema(_formData): FormSchema[] {
// 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema
return formSchema;
}
@@ -0,0 +1,387 @@
<template>
<div>
<!--引用表格-->
<BasicTable
@register="registerTable"
:rowSelection="rowSelection"
:expandedRowKeys="expandedRowKeys"
@expand="handleExpand"
@fetch-success="onFetchSuccess"
>
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'rectifylist:dj_inspection_rectify_list:add'" @click="handleCreate" preIcon="ant-design:plus-outlined">
新增</a-button
>
<a-button
type="primary"
v-auth="'rectifylist:dj_inspection_rectify_list:exportXls'"
preIcon="ant-design:export-outlined"
@click="onExportXls"
>
导出</a-button
>
<j-upload-button
type="primary"
v-auth="'rectifylist:dj_inspection_rectify_list:importExcel'"
preIcon="ant-design:import-outlined"
@click="onImportXls"
>导入</j-upload-button
>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'rectifylist:dj_inspection_rectify_list:deleteBatch'"
>批量操作
<Icon icon="ant-design:down-outlined"></Icon>
</a-button>
</a-dropdown>
<!-- 高级查询 -->
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }"> </template>
</BasicTable>
<!--字典弹窗-->
<DjInspectionRectifyListModal @register="registerModal" @success="handleSuccess" />
</div>
</template>
<script lang="ts" name="rectifylist-djInspectionRectifyList" setup>
//ts语法
import { ref, reactive, computed, unref, toRaw, nextTick } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import DjInspectionRectifyListModal from './components/DjInspectionRectifyListModal.vue';
import { columns, searchFormSchema, superQuerySchema } from './DjInspectionRectifyList.data';
import { downloadFile } from '/@/utils/common/renderUtils';
import { useMessage } from '/@/hooks/web/useMessage';
import {
list,
deleteDjInspectionRectifyList,
batchDeleteDjInspectionRectifyList,
getExportUrl,
getImportUrl,
getChildList,
getChildListBatch,
} from './DjInspectionRectifyList.api';
import { getDateByPicker } from '/@/utils';
//日期个性化选择
const fieldPickers = reactive({});
const { createMessage } = useMessage();
const queryParam = reactive<any>({});
const expandedRowKeys = ref([]);
//字典model
const [registerModal, { openModal }] = useModal();
//注册table数据
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
api: list,
title: '党建巡视整改提升工作台账',
columns,
canResize: true,
isTreeTable: true,
formConfig: {
//labelWidth: 120,
schemas: searchFormSchema,
autoSubmitOnEnter: true,
showAdvancedButton: true,
fieldMapToNumber: [],
fieldMapToTime: [],
},
actionColumn: {
width: 240,
fixed: 'right',
},
beforeFetch: (params) => {
if (params && fieldPickers) {
for (let key in fieldPickers) {
if (params[key]) {
params[key] = getDateByPicker(params[key], fieldPickers[key]);
}
}
}
params.hasQuery = 'true';
return Object.assign(params, queryParam);
},
},
exportConfig: {
name: '党建巡视整改提升工作台账',
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: importSuccess,
},
});
const [registerTable, { reload, collapseAll, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] =
tableContext;
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
reload();
}
/**
* 新增事件
*/
function handleCreate() {
openModal(true, {
isUpdate: false,
});
}
/**
* 发起流程
*/
function handleProcessLaunch() {
return null;
}
/**
* 编辑事件
*/
async function handleEdit(record) {
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 详情
*/
async function handleDetail(record) {
openModal(true, {
record,
isUpdate: true,
hideFooter: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteDjInspectionRectifyList({ id: record.id }, importSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
const ids = selectedRowKeys.value.filter((item) => !item.includes('loadChild'));
await batchDeleteDjInspectionRectifyList({ id: ids }, importSuccess);
}
/**
* 导入
*/
function importSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 添加下级
*/
function handleAddSub(record) {
openModal(true, {
record,
isUpdate: false,
});
}
/**
* 成功回调
*/
async function handleSuccess({ isUpdate, values, expandedArr, changeParent }) {
if (isUpdate) {
if (changeParent) {
reload();
} else {
let data = await list({ id: values.id, pageSize: 1, pageNo: 1, pid: values['pid'] });
if (data && data.records && data.records.length > 0) {
// 编辑回调
updateTableDataRecord(values.id, data.records[0]);
} else {
updateTableDataRecord(values.id, values);
}
}
} else {
if (!values['id'] || !values['pid']) {
//新增根节点
reload();
} else {
//新增子集
expandedRowKeys.value = [];
for (let key of unref(expandedArr)) {
await expandTreeNode(key);
}
}
}
}
/**
* 接口请求成功后回调
*/
function onFetchSuccess(result) {
getDataByResult(result.items) && loadDataByExpandedRows();
}
/**
* 根据已展开的行查询数据(用于保存后刷新时异步加载子级的数据)
*/
async function loadDataByExpandedRows() {
if (unref(expandedRowKeys).length > 0) {
const res = await getChildListBatch({ parentIds: unref(expandedRowKeys).join(',') });
if (res.success && res.result.records.length > 0) {
//已展开的数据批量子节点
let records = res.result.records;
const listMap = new Map();
for (let item of records) {
let pid = item['pid'];
if (unref(expandedRowKeys).includes(pid)) {
let mapList = listMap.get(pid);
if (mapList == null) {
mapList = [];
}
mapList.push(item);
listMap.set(pid, mapList);
}
}
let childrenMap = listMap;
let fn = (list) => {
if (list) {
list.forEach((data) => {
if (unref(expandedRowKeys).includes(data.id)) {
data.children = getDataByResult(childrenMap.get(data.id));
fn(data.children);
}
});
}
};
fn(getDataSource());
}
}
}
/**
* 处理数据集
*/
function getDataByResult(result) {
if (result && result.length > 0) {
return result.map((item) => {
//判断是否标记了带有子节点
if (item['hasChild'] == '1') {
let loadChild = { id: item.id + '_loadChild', name: 'loading...', isLoading: true };
item.children = [loadChild];
}
return item;
});
}
}
/**
*树节点展开合并
* */
async function handleExpand(expanded, record) {
// 判断是否是展开状态,展开状态(expanded)并且存在子集(children)并且未加载过(isLoading)的就去查询子节点数据
if (expanded) {
expandedRowKeys.value.push(record.id);
if (record.children.length > 0 && !!record.children[0].isLoading) {
let result = await getChildList({ pid: record.id });
result = result.records ? result.records : result;
if (result && result.length > 0) {
record.children = getDataByResult(result);
} else {
record.children = null;
record.hasChild = '0';
}
}
} else {
let keyIndex = expandedRowKeys.value.indexOf(record.id);
if (keyIndex >= 0) {
expandedRowKeys.value.splice(keyIndex, 1);
}
}
}
/**
*操作表格后处理树节点展开合并
* */
async function expandTreeNode(key) {
let record = findTableDataRecord(key);
expandedRowKeys.value.push(key);
let result = await getChildList({ pid: key });
if (result && result.length > 0) {
record.children = getDataByResult(result);
} else {
record.children = null;
record.hasChild = '0';
}
updateTableDataRecord(key, record);
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'rectifylist:dj_inspection_rectify_list:edit',
},
{
label: '发起流程',
onClick: handleProcessLaunch(),
disabled: record.isLaunch === 0,
//auth: 'rectifylist:dj_inspection_rectify_list:edit',
},
{
label: '添加下级',
onClick: handleAddSub.bind(null, { pid: record.id }),
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '确定删除吗?',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
auth: 'rectifylist:dj_inspection_rectify_list:delete',
},
];
}
</script>
<style lang="less" scoped>
:deep(.ant-picker),
:deep(.ant-input-number) {
width: 100%;
}
</style>
@@ -0,0 +1,70 @@
<template>
<div style="min-height: 400px">
<BasicForm @register="registerForm"></BasicForm>
<div style="width: 100%;text-align: center" v-if="!formDisabled">
<a-button @click="submitForm" pre-icon="ant-design:check" type="primary"> </a-button>
</div>
</div>
</template>
<script lang="ts">
import {BasicForm, useForm} from '/@/components/Form/index';
import {computed, defineComponent} from 'vue';
import {defHttp} from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes';
import {getBpmFormSchema} from '../DjInspectionRectifyList.data';
import {saveOrUpdateDict} from '../DjInspectionRectifyList.api';
export default defineComponent({
name: "DjInspectionRectifyListForm",
components:{
BasicForm
},
props:{
formData: propTypes.object.def({}),
formBpm: propTypes.bool.def(true),
},
setup(props){
const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
labelWidth: 150,
schemas: getBpmFormSchema(props.formData),
showActionButtonGroup: false,
baseColProps: {span: 24},
});
const formDisabled = computed(()=>{
if(props.formData.disabled === false){
return false;
}
return true;
});
let formData = {};
const queryByIdUrl = '/rectifylist/djInspectionRectifyList/queryById';
async function initFormData(){
let params = {id: props.formData.dataId};
const data = await defHttp.get({url: queryByIdUrl, params});
formData = {...data}
//设置表单的值
await setFieldsValue(formData);
//默认是禁用
await setProps({disabled: formDisabled.value})
}
async function submitForm() {
let data = getFieldsValue();
let params = Object.assign({}, formData, data);
console.log('表单数据', params)
await saveOrUpdateDict(params, true)
}
initFormData();
return {
registerForm,
formDisabled,
submitForm,
}
}
});
</script>
@@ -0,0 +1,140 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :width="800" :title="getTitle" @ok="handleSubmit">
<BasicForm @register="registerForm" name="DjInspectionRectifyListForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import {ref, computed, unref, reactive} from 'vue';
import {BasicModal, useModalInner} from '/@/components/Modal';
import {BasicForm, useForm} from '/@/components/Form';
import {formSchema} from '../DjInspectionRectifyList.data';
import {loadTreeData, saveOrUpdateDict} from '../DjInspectionRectifyList.api';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
//日期个性化选择
const fieldPickers = reactive({
});
const { createMessage } = useMessage();
// 获取emit
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
const isDetail = ref(false);
const expandedRowKeys = ref([]);
const treeData = ref([]);
// 当前编辑的数据
let model:Nullable<Recordable> = null;
//表单配置
const [registerForm, { setProps,resetFields, setFieldsValue, validate, updateSchema, scrollToField }] = useForm({
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: {span: 24},
labelCol: {
xs: { span: 24 },
sm: { span: 4 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 18 },
},
});
//表单赋值
const [registerModal, {setModalProps, closeModal}] = useModalInner(async (data) => {
//重置表单
await resetFields();
expandedRowKeys.value = [];
setModalProps({confirmLoading: false, minHeight: 80 ,showOkBtn: !!!data?.hideFooter});
isUpdate.value = !!data?.isUpdate;
isDetail.value = !!data?.showFooter;
if (data?.record) {
model = data.record;
//表单赋值
await setFieldsValue({
...data.record,
});
} else {
model = null;
}
//父级节点树信息
treeData.value = await loadTreeData({'async': false,'pcode':''});
// 隐藏底部时禁用整个表单
setProps({ disabled: !!data?.hideFooter })
});
//设置标题
const getTitle = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
/**
* 根据pid获取展开的节点
* @param pid
* @param arr
*/
function getExpandKeysByPid(pid,arr){
if(pid && arr && arr.length>0){
for(let i=0;i<arr.length;i++){
if(arr[i].key==pid && unref(expandedRowKeys).indexOf(pid)<0){
expandedRowKeys.value.push(arr[i].key);
getExpandKeysByPid(arr[i]['parentId'],unref(treeData))
}else{
getExpandKeysByPid(pid,arr[i].children)
}
}
}
}
//表单提交事件
async function handleSubmit() {
try {
let values = await validate();
// 预处理日期数据
changeDateValue(values);
setModalProps({confirmLoading: true});
//提交表单
await saveOrUpdateDict(values, isUpdate.value);
//关闭弹窗
closeModal();
//展开的节点信息
await getExpandKeysByPid(values['pid'],unref(treeData))
//刷新列表(isUpdate:是否编辑;values:表单信息;expandedArr:展开的节点信息)
emit('success', {
isUpdate: unref(isUpdate),
values: {...values},
expandedArr: unref(expandedRowKeys).reverse(),
// 是否更改了父级节点
changeParent: model != null && (model['pid'] != values['pid']),
});
} catch ({ errorFields }) {
if (errorFields) {
const firstField = errorFields[0];
if (firstField) {
scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
return Promise.reject(errorFields);
} finally {
setModalProps({confirmLoading: false});
}
}
/**
* 处理日期值
* @param formData 表单数据
*/
const changeDateValue = (formData) => {
if (formData && fieldPickers) {
for (let key in fieldPickers) {
if (formData[key]) {
formData[key] = getDateByPicker(formData[key], fieldPickers[key]);
}
}
}
};
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>
@@ -0,0 +1,63 @@
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
const { createConfirm } = useMessage();
enum Api {
list = '/tzMetting/djMeetingRecord/list',
save = '/tzMetting/djMeetingRecord/add',
edit = '/tzMetting/djMeetingRecord/edit',
deleteOne = '/tzMetting/djMeetingRecord/delete',
deleteBatch = '/tzMetting/djMeetingRecord/deleteBatch',
importExcel = '/tzMetting/djMeetingRecord/importExcel',
exportXls = '/tzMetting/djMeetingRecord/exportXls',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 删除单个
*/
export const deleteOne = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 批量删除
* @param params
*/
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 保存或者更新
* @param params
*/
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
@@ -0,0 +1,138 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { rules } from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '序号',
align: 'center',
dataIndex: 'sortNo',
},
{
title: '问题分类',
align: 'center',
dataIndex: 'problemType',
},
{
title: '具体措施',
align: 'center',
dataIndex: 'specificMeasure',
},
{
title: '完成时限',
align: 'center',
dataIndex: 'completionDeadline',
customRender: ({ text }) => {
text = !text ? '' : text.length > 10 ? text.substr(0, 10) : text;
return text;
},
},
{
title: '责任部门',
align: 'center',
dataIndex: 'responsibleDept_dictText',
},
{
title: '完成情况',
align: 'center',
dataIndex: 'completionStatus',
},
{
title: '是否可发起',
align: 'center',
width: 150,
dataIndex: 'isLaunchable_dictText',
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [];
//表单数据
export const formSchema: FormSchema[] = [
{
label: '序号',
field: 'sortNo',
component: 'InputNumber',
},
{
label: '问题分类',
field: 'problemType',
component: 'InputNumber',
},
{
label: '具体措施',
field: 'specificMeasure',
component: 'Input',
},
{
label: '完成时限',
field: 'completionDeadline',
component: 'DatePicker',
componentProps: {
valueFormat: 'YYYY-MM-DD',
},
},
{
label: '责任部门',
field: 'responsibleDept',
component: 'JSelectDept',
componentProps: ({ formActionType, formModel }) => {
return {
sync: false,
checkStrictly: true,
defaultExpandLevel: 2,
onSelect: (options, values) => {
const { updateSchema } = formActionType;
//所属部门修改后更新负责部门下拉框数据
updateSchema([
{
field: 'departIds',
componentProps: { options },
},
]);
//update-begin---author:wangshuai---date:2024-05-11---for:【issues/1222】用户编辑界面“所属部门”与“负责部门”联动出错整---
if (!values) {
formModel.departIds = [];
return;
}
//update-end---author:wangshuai---date:2024-05-11---for:【issues/1222】用户编辑界面“所属部门”与“负责部门”联动出错整---
//所属部门修改后更新负责部门数据
formModel.departIds && (formModel.departIds = formModel.departIds.filter((item) => values.value.indexOf(item) > -1));
},
};
},
},
{
label: '完成情况',
field: 'completionStatus',
component: 'Input',
},
// TODO 主键隐藏字段,目前写死为ID
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
];
// 高级查询数据
export const superQuerySchema = {
sortNo: { title: '序号', order: 0, view: 'number', type: 'number' },
problemType: { title: '问题分类', order: 1, view: 'number', type: 'number' },
specificMeasure: { title: '具体措施', order: 2, view: 'text', type: 'string' },
completionDeadline: { title: '完成时限', order: 3, view: 'date', type: 'string' },
responsibleDept: { title: '责任部门', order: 4, view: 'text', type: 'string' },
completionStatus: { title: '完成情况', order: 5, view: 'text', type: 'string' },
};
/**
* 流程表单调用这个方法获取formSchema
* @param param
*/
export function getBpmFormSchema(_formData): FormSchema[] {
// 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema
return formSchema;
}
@@ -0,0 +1,217 @@
<template>
<div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'tzMetting:dj_meeting_record:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="'tzMetting:dj_meeting_record:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls">
导出</a-button
>
<j-upload-button type="primary" v-auth="'tzMetting:dj_meeting_record:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls"
>导入</j-upload-button
>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'tzMetting:dj_meeting_record:deleteBatch'"
>批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
<!-- 高级查询 -->
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }"> </template>
</BasicTable>
<!-- 表单区域 -->
<DjMeetingRecordModal @register="registerModal" @success="handleSuccess"></DjMeetingRecordModal>
</div>
</template>
<script lang="ts" name="tzMetting-djMeetingRecord" setup>
import { ref, reactive, computed, unref } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import DjMeetingRecordModal from './components/DjMeetingRecordModal.vue';
import { columns, searchFormSchema, superQuerySchema } from './DjMeetingRecord.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './DjMeetingRecord.api';
import { downloadFile } from '/@/utils/common/renderUtils';
import { useUserStore } from '/@/store/modules/user';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
//日期个性化选择
const fieldPickers = reactive({});
const queryParam = reactive<any>({});
const checkedKeys = ref<Array<string | number>>([]);
const userStore = useUserStore();
const { createMessage } = useMessage();
//注册model
const [registerModal, { openModal }] = useModal();
//注册table数据
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '统战座谈会意见台账及督办反馈',
api: list,
columns,
canResize: true,
formConfig: {
//labelWidth: 120,
schemas: searchFormSchema,
autoSubmitOnEnter: true,
showAdvancedButton: true,
fieldMapToNumber: [],
fieldMapToTime: [],
},
actionColumn: {
width: 120,
fixed: 'right',
},
beforeFetch: (params) => {
if (params && fieldPickers) {
for (let key in fieldPickers) {
if (params[key]) {
params[key] = getDateByPicker(params[key], fieldPickers[key]);
}
}
}
return Object.assign(params, queryParam);
},
},
exportConfig: {
name: '统战座谈会意见台账及督办反馈',
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
reload();
}
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
showFooter: true,
});
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: true,
});
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: false,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 发起流程
*/
function handleProcessLaunch() {
return null;
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'tzMetting:dj_meeting_record:edit',
},
{
label: '发起流程',
onClick: handleProcessLaunch(),
disabled: record.isLaunch === 0,
//auth: 'rectifylist:dj_inspection_rectify_list:edit',
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
auth: 'tzMetting:dj_meeting_record:delete',
},
];
}
</script>
<style lang="less" scoped>
:deep(.ant-picker),
:deep(.ant-input-number) {
width: 100%;
}
</style>
@@ -0,0 +1,70 @@
<template>
<div style="min-height: 400px">
<BasicForm @register="registerForm"></BasicForm>
<div style="width: 100%;text-align: center" v-if="!formDisabled">
<a-button @click="submitForm" pre-icon="ant-design:check" type="primary"> </a-button>
</div>
</div>
</template>
<script lang="ts">
import {BasicForm, useForm} from '/@/components/Form/index';
import {computed, defineComponent} from 'vue';
import {defHttp} from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes';
import {getBpmFormSchema} from '../DjMeetingRecord.data';
import {saveOrUpdate} from '../DjMeetingRecord.api';
export default defineComponent({
name: "DjMeetingRecordForm",
components:{
BasicForm
},
props:{
formData: propTypes.object.def({}),
formBpm: propTypes.bool.def(true),
},
setup(props){
const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
labelWidth: 150,
schemas: getBpmFormSchema(props.formData),
showActionButtonGroup: false,
baseColProps: {span: 24}
});
const formDisabled = computed(()=>{
if(props.formData.disabled === false){
return false;
}
return true;
});
let formData = {};
const queryByIdUrl = '/tzMetting/djMeetingRecord/queryById';
async function initFormData(){
let params = {id: props.formData.dataId};
const data = await defHttp.get({url: queryByIdUrl, params});
formData = {...data}
//设置表单的值
await setFieldsValue(formData);
//默认是禁用
await setProps({disabled: formDisabled.value})
}
async function submitForm() {
let data = getFieldsValue();
let params = Object.assign({}, formData, data);
console.log('表单数据', params)
await saveOrUpdate(params, true)
}
initFormData();
return {
registerForm,
formDisabled,
submitForm,
}
}
});
</script>
@@ -0,0 +1,99 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
<BasicForm @register="registerForm" name="DjMeetingRecordForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import {ref, computed, unref, reactive} from 'vue';
import {BasicModal, useModalInner} from '/@/components/Modal';
import {BasicForm, useForm} from '/@/components/Form/index';
import {formSchema} from '../DjMeetingRecord.data';
import {saveOrUpdate} from '../DjMeetingRecord.api';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
const { createMessage } = useMessage();
// Emits声明
const emit = defineEmits(['register','success']);
const isUpdate = ref(true);
const isDetail = ref(false);
//表单配置
const [registerForm, { setProps,resetFields, setFieldsValue, validate, scrollToField }] = useForm({
labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: {span: 24}
});
//表单赋值
const [registerModal, {setModalProps, closeModal}] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({confirmLoading: false,showCancelBtn:!!data?.showFooter,showOkBtn:!!data?.showFooter});
isUpdate.value = !!data?.isUpdate;
isDetail.value = !!data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter })
});
//日期个性化选择
const fieldPickers = reactive({
});
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
// 预处理日期数据
changeDateValue(values);
setModalProps({confirmLoading: true});
//提交表单
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} catch ({ errorFields }) {
if (errorFields) {
const firstField = errorFields[0];
if (firstField) {
scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
return Promise.reject(errorFields);
} finally {
setModalProps({confirmLoading: false});
}
}
/**
* 处理日期值
* @param formData 表单数据
*/
const changeDateValue = (formData) => {
if (formData && fieldPickers) {
for (let key in fieldPickers) {
if (formData[key]) {
formData[key] = getDateByPicker(formData[key], fieldPickers[key]);
}
}
}
};
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>
+235
View File
@@ -0,0 +1,235 @@
<template>
<div class="j-upload-demo-container">
<a-card title="SUploadFile 文件上传组件示例" style="margin-bottom: 16px">
<a-form layout="vertical">
<a-form-item label="业务名称">
<a-input v-model:value="formData.name" placeholder="请输入业务名称" />
</a-form-item>
<a-form-item label="业务密级">
<a-radio-group v-model:value="formData.secretLevel">
<a-radio :value="1">非密</a-radio>
<a-radio :value="2">内部</a-radio>
<a-radio :value="3">秘密</a-radio>
<a-radio :value="4">机密</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="多文件上传">
<a-switch v-model:checked="multipleEnabled" />
</a-form-item>
<a-form-item label="文件上传区域">
<SUploadFile
ref="uploadFileRef"
v-model:bussinessId="formData.id"
:bussiness-secret-level="formData.secretLevel"
:disabled="formData.readOnly"
:multiple="multipleEnabled"
@upload-success="handleUploadSuccess"
@delete-success="handleDeleteSuccess"
@upload-error="handleUploadError"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSave" :loading="saving">
保存表单模拟
</a-button>
<a-button @click="handleReset">重置</a-button>
<a-button type="link" @click="formData.readOnly = !formData.readOnly">
{{ formData.readOnly ? '解除禁用' : '禁用组件' }}
</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
<a-card title="操作日志" style="margin-bottom: 16px">
<div class="state-display">
<a-descriptions :column="2" size="small" bordered>
<a-descriptions-item label="业务ID">{{ formData.id || '(空 - 新增模式)' }}</a-descriptions-item>
<a-descriptions-item label="待关联文件数">
<a-badge :count="pendingCount" :number-style="{ backgroundColor: '#faad14' }" />
</a-descriptions-item>
<a-descriptions-item label="业务模式">
<a-tag :color="formData.id ? 'green' : 'orange'">
{{ formData.id ? '编辑模式(已有ID' : '新增模式(无ID' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="组件状态">
<a-tag :color="formData.readOnly ? 'red' : 'green'">
{{ formData.readOnly ? '已禁用' : '可操作' }}
</a-tag>
</a-descriptions-item>
</a-descriptions>
</div>
</a-card>
<a-card title="事件日志">
<div class="event-log-wrapper">
<a-button size="small" @click="eventLogs = []" style="margin-bottom: 8px">清空日志</a-button>
<div class="event-log-list">
<div v-for="(log, idx) in eventLogs" :key="idx" class="event-log-item">
<a-tag :color="log.type === 'success' ? 'green' : log.type === 'error' ? 'red' : 'blue'" size="small">
{{ log.type }}
</a-tag>
<span class="log-time">{{ log.time }}</span>
<span class="log-msg">{{ log.message }}</span>
</div>
<div v-if="eventLogs.length === 0" class="empty-log">暂无日志</div>
</div>
</div>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage';
import SUploadFile from '/@/components/semri/fileComponent/SUploadFile.vue';
const { createMessage } = useMessage();
const uploadFileRef = ref<InstanceType<typeof SUploadFile> | null>(null);
const multipleEnabled = ref(true);
const saving = ref(false);
const pendingCount = ref(0);
const eventLogs = ref<any[]>([]);
const formData = ref({
id: '',
name: '',
secretLevel: 3,
readOnly: false,
});
function addLog(type: string, message: string) {
const now = new Date();
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
eventLogs.value.unshift({ type, message, time });
if (eventLogs.value.length > 50) eventLogs.value.pop();
}
async function handleSave() {
if (!formData.value.name) {
createMessage.warning('请输入业务名称');
return;
}
saving.value = true;
addLog('info', '开始保存表单...');
try {
await new Promise(resolve => setTimeout(resolve, 800));
const isNew = !formData.value.id;
if (isNew) {
formData.value.id = 'BIZ_' + Date.now();
addLog('success', `表单保存成功,生成业务ID: ${formData.value.id}`);
} else {
addLog('success', `表单更新成功,业务ID: ${formData.value.id}`);
}
if (uploadFileRef.value) {
const hasPending = uploadFileRef.value.hasPendingFiles();
pendingCount.value = hasPending ? uploadFileRef.value.getPendingFileIds().length : 0;
if (hasPending) {
addLog('info', `发现 ${pendingCount.value} 个待关联文件,开始绑定...`);
const bindSuccess = await uploadFileRef.value.bindBussinessId(formData.value.id);
if (bindSuccess) {
pendingCount.value = 0;
addLog('success', '文件关联成功!');
} else {
addLog('error', '文件关联失败!');
}
}
}
} catch (error) {
addLog('error', `保存失败: ${error.message}`);
} finally {
saving.value = false;
}
}
function handleReset() {
formData.value = {
id: '',
name: '',
secretLevel: 3,
readOnly: false,
};
pendingCount.value = 0;
eventLogs.value = [];
addLog('info', '表单已重置');
}
function handleUploadSuccess(result: any) {
addLog('success', `上传成功,${result.results?.length || 0} 个文件`);
if (uploadFileRef.value) {
pendingCount.value = uploadFileRef.value.hasPendingFiles()
? uploadFileRef.value.getPendingFileIds().length
: 0;
}
}
function handleDeleteSuccess(fileId: any) {
addLog('success', `删除成功,文件ID: ${fileId}`);
}
function handleUploadError(error: any) {
addLog('error', `上传失败: ${error.message || '未知错误'}`);
}
</script>
<style lang="less" scoped>
.j-upload-demo-container {
padding: 16px;
}
.state-display {
margin-bottom: 12px;
}
.event-log-wrapper {
.event-log-list {
max-height: 300px;
overflow-y: auto;
background: #fafafa;
border-radius: 4px;
padding: 8px;
}
.event-log-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.log-time {
font-size: 12px;
color: #8c8c8c;
flex-shrink: 0;
}
.log-msg {
font-size: 13px;
color: #262626;
}
}
.empty-log {
text-align: center;
color: #8c8c8c;
font-size: 13px;
padding: 16px 0;
}
}
</style>
@@ -0,0 +1,64 @@
import {defHttp} from '/@/utils/http/axios';
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/supervisiontest/supervisionTest/list',
save='/supervisiontest/supervisionTest/add',
edit='/supervisiontest/supervisionTest/edit',
deleteOne = '/supervisiontest/supervisionTest/delete',
deleteBatch = '/supervisiontest/supervisionTest/deleteBatch',
importExcel = '/supervisiontest/supervisionTest/importExcel',
exportXls = '/supervisiontest/supervisionTest/exportXls',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口
* @param params
*/
export const list = (params) =>
defHttp.get({url: Api.list, params});
/**
* 删除单个
*/
export const deleteOne = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
* 批量删除
* @param params
*/
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
});
}
/**
* 保存或者更新
* @param params
*/
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({url: url, params});
}
@@ -0,0 +1,120 @@
import {BasicColumn} from '/@/components/Table';
import {FormSchema} from '/@/components/Table';
import { rules} from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '附件1',
align:"center",
dataIndex: 'fileurl1',
},
{
title: '附件2',
align:"center",
dataIndex: 'fileurl2',
},
{
title: '督办标题',
align:"center",
dataIndex: 'supervisionTitle'
},
{
title: '督办人',
align:"center",
dataIndex: 'supervisionPerson_dictText'
},
{
title: '督办部门',
align:"center",
dataIndex: 'supervisionDepart_dictText'
},
{
title: '督办类型',
align:"center",
dataIndex: 'supervitionType'
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [
];
//表单数据
export const formSchema: FormSchema[] = [
{
label: '附件1',
field: 'fileurl1',
component: 'JUpload',
componentProps:{
},
},
{
label: '附件2',
field: 'fileurl2',
component: 'JUpload',
componentProps:{
},
},
{
label: '督办标题',
field: 'supervisionTitle',
component: 'Input',
},
{
label: '督办人',
field: 'supervisionPerson',
component: 'JSelectUser',
componentProps:{
},
},
{
label: '督办部门',
field: 'supervisionDepart',
component: 'JSelectDept',
componentProps:{
},
},
{
label: '督办类型',
field: 'supervitionType',
component: 'JPopup',
componentProps: ({ formActionType }) => {
const {setFieldsValue} = formActionType;
return{
setFieldsValue:setFieldsValue,
code:"",
fieldConfig: [
{ source: '', target: '' },
],
multi:true
}
},
},
// TODO 主键隐藏字段,目前写死为ID
{
label: '',
field: 'id',
component: 'Input',
show: false
},
];
// 高级查询数据
export const superQuerySchema = {
fileurl1: {title: '附件1',order: 0,view: 'file', type: 'string',},
fileurl2: {title: '附件2',order: 1,view: 'file', type: 'string',},
supervisionTitle: {title: '督办标题',order: 2,view: 'text', type: 'string',},
supervisionPerson: {title: '督办人',order: 3,view: 'sel_user', type: 'string',},
supervisionDepart: {title: '督办部门',order: 4,view: 'sel_depart', type: 'string',},
supervitionType: {title: '督办类型',order: 5,view: 'popup', type: 'string',code: '', orgFields: '', destFields: 'supervitionType', popupMulti: false,},
};
/**
* 流程表单调用这个方法获取formSchema
* @param param
*/
export function getBpmFormSchema(_formData): FormSchema[]{
// 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema
return formSchema;
}
@@ -0,0 +1,216 @@
<template>
<div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'supervisiontest:supervision_test:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="'supervisiontest:supervision_test:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" v-auth="'supervisiontest:supervision_test:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'supervisiontest:supervision_test:deleteBatch'">批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
<!-- 高级查询 -->
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
</template>
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }">
<template v-if="column.dataIndex==='fileurl1'">
<!--文件字段回显插槽-->
<span v-if="!text" style="font-size: 12px;font-style: italic;">无文件</span>
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download-outlined" size="small" @click="downloadFile(text)">下载</a-button>
</template>
<template v-if="column.dataIndex==='fileurl2'">
<!--文件字段回显插槽-->
<span v-if="!text" style="font-size: 12px;font-style: italic;">无文件</span>
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download-outlined" size="small" @click="downloadFile(text)">下载</a-button>
</template>
</template>
</BasicTable>
<!-- 表单区域 -->
<SupervisionTestModal @register="registerModal" @success="handleSuccess"></SupervisionTestModal>
</div>
</template>
<script lang="ts" name="supervisiontest-supervisionTest" setup>
import {ref, reactive, computed, unref} from 'vue';
import {BasicTable, useTable, TableAction} from '/@/components/Table';
import {useModal} from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage'
import SupervisionTestModal from './components/SupervisionTestModal.vue'
import {columns, searchFormSchema, superQuerySchema} from './SupervisionTest.data';
import {list, deleteOne, batchDelete, getImportUrl,getExportUrl} from './SupervisionTest.api';
import { downloadFile } from '/@/utils/common/renderUtils';
import { useUserStore } from '/@/store/modules/user';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
//日期个性化选择
const fieldPickers = reactive({
});
const queryParam = reactive<any>({});
const checkedKeys = ref<Array<string | number>>([]);
const userStore = useUserStore();
const { createMessage } = useMessage();
//注册model
const [registerModal, {openModal}] = useModal();
//注册table数据
const { prefixCls,tableContext,onExportXls,onImportXls } = useListPage({
tableProps:{
title: 'supervision_test',
api: list,
columns,
canResize:true,
formConfig: {
//labelWidth: 120,
schemas: searchFormSchema,
autoSubmitOnEnter:true,
showAdvancedButton:true,
fieldMapToNumber: [
],
fieldMapToTime: [
],
},
actionColumn: {
width: 120,
fixed:'right'
},
beforeFetch: (params) => {
if (params && fieldPickers) {
for (let key in fieldPickers) {
if (params[key]) {
params[key] = getDateByPicker(params[key], fieldPickers[key]);
}
}
}
return Object.assign(params, queryParam);
},
},
exportConfig: {
name:"supervision_test",
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess
},
})
const [registerTable, {reload},{ rowSelection, selectedRowKeys }] = tableContext
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
reload();
}
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
showFooter: true,
});
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: true,
});
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: false,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({id: record.id}, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ids: selectedRowKeys.value}, handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 操作栏
*/
function getTableAction(record){
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'supervisiontest:supervision_test:edit'
}
]
}
/**
* 下拉操作栏
*/
function getDropDownAction(record){
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
}, {
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
auth: 'supervisiontest:supervision_test:delete'
}
]
}
</script>
<style lang="less" scoped>
:deep(.ant-picker),:deep(.ant-input-number){
width: 100%;
}
</style>
@@ -0,0 +1,70 @@
<template>
<div style="min-height: 400px">
<BasicForm @register="registerForm"></BasicForm>
<div style="width: 100%;text-align: center" v-if="!formDisabled">
<a-button @click="submitForm" pre-icon="ant-design:check" type="primary"> </a-button>
</div>
</div>
</template>
<script lang="ts">
import {BasicForm, useForm} from '/@/components/Form/index';
import {computed, defineComponent} from 'vue';
import {defHttp} from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes';
import {getBpmFormSchema} from '../SupervisionTest.data';
import {saveOrUpdate} from '../SupervisionTest.api';
export default defineComponent({
name: "SupervisionTestForm",
components:{
BasicForm
},
props:{
formData: propTypes.object.def({}),
formBpm: propTypes.bool.def(true),
},
setup(props){
const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
labelWidth: 150,
schemas: getBpmFormSchema(props.formData),
showActionButtonGroup: false,
baseColProps: {span: 24}
});
const formDisabled = computed(()=>{
if(props.formData.disabled === false){
return false;
}
return true;
});
let formData = {};
const queryByIdUrl = '/supervisiontest/supervisionTest/queryById';
async function initFormData(){
let params = {id: props.formData.dataId};
const data = await defHttp.get({url: queryByIdUrl, params});
formData = {...data}
//设置表单的值
await setFieldsValue(formData);
//默认是禁用
await setProps({disabled: formDisabled.value})
}
async function submitForm() {
let data = getFieldsValue();
let params = Object.assign({}, formData, data);
console.log('表单数据', params)
await saveOrUpdate(params, true)
}
initFormData();
return {
registerForm,
formDisabled,
submitForm,
}
}
});
</script>
@@ -0,0 +1,99 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
<BasicForm @register="registerForm" name="SupervisionTestForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import {ref, computed, unref, reactive} from 'vue';
import {BasicModal, useModalInner} from '/@/components/Modal';
import {BasicForm, useForm} from '/@/components/Form/index';
import {formSchema} from '../SupervisionTest.data';
import {saveOrUpdate} from '../SupervisionTest.api';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
const { createMessage } = useMessage();
// Emits声明
const emit = defineEmits(['register','success']);
const isUpdate = ref(true);
const isDetail = ref(false);
//表单配置
const [registerForm, { setProps,resetFields, setFieldsValue, validate, scrollToField }] = useForm({
labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: {span: 24}
});
//表单赋值
const [registerModal, {setModalProps, closeModal}] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({confirmLoading: false,showCancelBtn:!!data?.showFooter,showOkBtn:!!data?.showFooter});
isUpdate.value = !!data?.isUpdate;
isDetail.value = !!data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter })
});
//日期个性化选择
const fieldPickers = reactive({
});
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
// 预处理日期数据
changeDateValue(values);
setModalProps({confirmLoading: true});
//提交表单
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} catch ({ errorFields }) {
if (errorFields) {
const firstField = errorFields[0];
if (firstField) {
scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
return Promise.reject(errorFields);
} finally {
setModalProps({confirmLoading: false});
}
}
/**
* 处理日期值
* @param formData 表单数据
*/
const changeDateValue = (formData) => {
if (formData && fieldPickers) {
for (let key in fieldPickers) {
if (formData[key]) {
formData[key] = getDateByPicker(formData[key], fieldPickers[key]);
}
}
}
};
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>
@@ -0,0 +1,184 @@
<template>
<BasicTable @register="registerTable">
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate"> 新增</a-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
<div style="margin-left: 10px;margin-top: 5px">当前登录租户: <span class="tenant-name">{{loginTenantName}}</span> </div>
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<!--角色用户表格-->
<RoleUserTable @register="roleUserDrawer" :disableUserEdit="true"/>
<!--角色编辑抽屉-->
<RoleDrawer @register="registerDrawer" @success="reload" :showFooter="showFooter" />
<!--角色详情-->
<RoleDesc @register="registerDesc"></RoleDesc>
</template>
<script lang="ts" name="tenant-role-list" setup>
import { onMounted, ref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useDrawer } from '/@/components/Drawer';
import { useModal } from '/@/components/Modal';
import RoleDesc from './components/RoleDesc.vue';
import RoleDrawer from './components/RoleDrawer.vue';
import RoleUserTable from './components/RoleUserTable.vue';
import { columns, searchFormSchema } from './role.data';
import { listByTenant, deleteRole, batchDeleteRole, getExportUrl, getImportUrl } from './role.api';
import { useListPage } from '/@/hooks/system/useListPage';
import { getLoginTenantName } from "/@/views/system/tenant/tenant.api";
import { tenantSaasMessage } from "@/utils/common/compUtils";
const showFooter = ref(true);
const [roleUserDrawer, { openDrawer: openRoleUserDrawer }] = useDrawer();
const [registerDrawer, { openDrawer }] = useDrawer();
const [registerModal, { openModal }] = useModal();
const [registerDesc, { openDrawer: openRoleDesc }] = useDrawer();
// 列表页面公共参数、方法
const { prefixCls, tableContext, onImportXls, onExportXls } = useListPage({
designScope: 'role-template',
tableProps: {
title: '租户角色列表',
api: listByTenant,
columns: columns,
formConfig: {
schemas: searchFormSchema,
},
actionColumn: {
width: 120,
},
rowSelection: null,
//自定义默认排序
defSort: {
column: 'id',
order: 'desc',
},
},
exportConfig: {
name: '角色列表',
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
/**
* 新增
*/
function handleCreate() {
showFooter.value = true;
openDrawer(true, {
isUpdate: false,
});
}
/**
* 编辑
*/
function handleEdit(record: Recordable) {
showFooter.value = true;
openDrawer(true, {
record,
isUpdate: true,
});
}
/**
* 详情
*/
function handleDetail(record) {
showFooter.value = false;
openRoleDesc(true, {
record,
isUpdate: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteRole({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDeleteRole({ ids: selectedRowKeys.value }, reload);
}
/**
* 角色用户
*/
function handleUser(record) {
//onSelectChange(selectedRowKeys)
openRoleUserDrawer(true, record);
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '用户',
onClick: handleUser.bind(null, record),
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
const loginTenantName = ref<string>('');
getTenantName();
async function getTenantName(){
loginTenantName.value = await getLoginTenantName();
}
onMounted(()=>{
tenantSaasMessage('租户角色')
})
</script>
<style scoped lang="less">
.tenant-name{
text-decoration:underline;
margin: 5px;
font-size: 15px;
}
</style>
@@ -0,0 +1,59 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="添加所属部门" width="400px" @ok="handleSubmit" destroyOnClose>
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicModal, useModalInner } from '/src/components/Modal';
import { BasicForm, useForm } from '/src/components/Form';
const emit = defineEmits(['success', 'register']);
const rowRecord = ref<any>(null);
const [registerForm, { validate, resetFields, updateSchema }] = useForm({
labelWidth: 80,
schemas: [
{
field: 'deptIds',
label: '选择部门',
component: 'Select',
required: true,
componentProps: {
mode: 'multiple',
options: [], // 初始为空,由父组件注入
},
},
],
showActionButtonGroup: false,
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
rowRecord.value = data.record;
// ✅ 关键:适配你说的“预加载后作为props传入”
if (data.deptOptions) {
updateSchema({
field: 'deptIds',
componentProps: { options: data.deptOptions },
});
}
});
async function handleSubmit() {
try {
const values = await validate();
console.log('当前选到的部门Id为', values.deptIds);
console.log('接收到的来自table的record为', rowRecord.value);
emit('success', {
deptIds: values.deptIds,
userId: rowRecord.value.userId,
roleId: rowRecord.value.roleId,
});
// closeModal();
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
@@ -0,0 +1,85 @@
<template>
<BasicDrawer v-bind="$attrs" @register="registerDrawer" title="数据规则配置" width="450px" destroyOnClose>
<a-tabs defaultActiveKey="1">
<a-tab-pane tab="数据规则" key="1">
<a-checkbox-group v-model:value="dataRuleChecked" v-if="dataRuleList.length > 0">
<a-row>
<a-col :span="24" v-for="(item, index) in dataRuleList" :key="'dr' + index">
<a-checkbox :value="item.id">{{ item.ruleName }}</a-checkbox>
</a-col>
<a-col :span="24">
<div style="width: 100%; margin-top: 15px">
<a-button @click="saveDataRuleForRole" type="primary" size="small"> <Icon icon="ant-design:save-outlined"></Icon>点击保存</a-button>
</div>
</a-col>
</a-row>
</a-checkbox-group>
<div v-else><h3>无配置信息!</h3></div>
</a-tab-pane>
</a-tabs>
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicDrawer, useDrawerInner } from '/src/components/Drawer';
import { useMessage } from '/src/hooks/web/useMessage';
import { queryDataRule, saveDataRule } from '../role.api';
// 声明Emits
const emit = defineEmits(['success', 'register']);
const { createMessage } = useMessage();
// 声明数据
const functionId = ref('');
const roleId = ref('');
const dataRuleList = ref([]);
const dataRuleChecked = ref([]);
/**
* 数据
*/
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
await reset();
setDrawerProps({ confirmLoading: false });
//权限的id
functionId.value = data.functionId;
//角色的id
roleId.value = data.roleId;
//查询数据
const res = await queryDataRule({ functionId: unref(functionId), roleId: unref(roleId) });
if (res.success) {
dataRuleList.value = res.result.datarule;
if (res.result.drChecked) {
dataRuleChecked.value = res.result.drChecked.split(',');
}
}
});
/**
* 重置
*/
function reset() {
functionId.value = '';
roleId.value = '';
dataRuleList.value = [];
dataRuleChecked.value = [];
}
/**
* 提交
*/
async function saveDataRuleForRole() {
if (!unref(dataRuleChecked) || unref(dataRuleChecked).length == 0) {
createMessage.warning('请注意,现未勾选任何数据权限!');
}
let params = {
permissionId: unref(functionId),
roleId: unref(roleId),
dataRuleIds: unref(dataRuleChecked).join(','),
};
await saveDataRule(params);
//关闭弹窗
closeDrawer();
//刷新列表
emit('success');
}
</script>
@@ -0,0 +1,18 @@
<template>
<BasicDrawer v-bind="$attrs" @register="registerDrawer" title="角色详情" width="500px" destroyOnClose>
<Description :column="1" :data="roleData" :schema="formDescSchema" />
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, useAttrs } from 'vue';
import { BasicDrawer, useDrawerInner } from '/src/components/Drawer';
import { formDescSchema } from '../role.data';
import { Description, useDescription } from '/@/components/Description/index';
const emit = defineEmits(['register']);
const attrs = useAttrs();
const roleData = ref({});
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
setDrawerProps({ confirmLoading: false });
roleData.value = data.record;
});
</script>
@@ -0,0 +1,53 @@
<template>
<BasicDrawer v-bind="$attrs" @register="registerDrawer" :title="getTitle" width="500px" @ok="handleSubmit" destroyOnClose>
<BasicForm @register="registerForm" />
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, computed, unref, useAttrs } from 'vue';
import { BasicForm, useForm } from '/src/components/Form';
import { BasicDrawer, useDrawerInner } from '/src/components/Drawer';
import { BasicTree, TreeItem } from '/src/components/Tree';
import { formSchema } from '../role.data';
import { saveOrUpdateRole } from '../role.api';
// 声明Emits
const emit = defineEmits(['success', 'register']);
const attrs = useAttrs();
const isUpdate = ref(true);
const [registerForm, { setProps, resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 90,
schemas: formSchema,
showActionButtonGroup: false,
});
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
resetFields();
isUpdate.value = !!data?.isUpdate;
setDrawerProps({ confirmLoading: false });
if (unref(isUpdate)) {
setFieldsValue({
...data.record,
});
}
//禁用表单
setProps({ disabled: !attrs.showFooter });
});
/**
* 标题
*/
const getTitle = computed(() => (!unref(isUpdate) ? '新增角色' : '编辑角色'));
/**
* 提交
*/
async function handleSubmit() {
try {
const values = await validate();
setDrawerProps({ confirmLoading: true });
//提交表单
await saveOrUpdateRole(values, isUpdate.value);
closeDrawer();
emit('success');
} finally {
setDrawerProps({ confirmLoading: false });
}
}
</script>
@@ -0,0 +1,55 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="首页配置" @ok="handleSubmit" width="40%">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { roleIndexFormSchema } from '../role.data';
import { saveOrUpdateRoleIndex, queryIndexByCode } from '../role.api';
// Emits声明
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 150,
schemas: roleIndexFormSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
setFieldsValue({ roleCode: data.roleCode });
let res = await queryIndexByCode({ roleCode: data.roleCode });
isUpdate.value = !!res.result?.id;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...res.result,
});
}
});
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdateRoleIndex(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success', { isUpdate: isUpdate.value, values });
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,309 @@
<template>
<BasicDrawer v-bind="$attrs" @register="registerDrawer" width="650px" destroyOnClose showFooter>
<template #title>
角色权限配置
<a-dropdown>
<Icon icon="ant-design:more-outlined" class="more-icon" />
<template #overlay>
<a-menu @click="treeMenuClick">
<a-menu-item key="checkAll">选择全部</a-menu-item>
<a-menu-item key="cancelCheck">取消选择</a-menu-item>
<div class="line"></div>
<a-menu-item key="openAll">展开全部</a-menu-item>
<a-menu-item key="closeAll">折叠全部</a-menu-item>
<div class="line"></div>
<a-menu-item key="relation">层级关联</a-menu-item>
<a-menu-item key="standAlone">层级独立</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<BasicTree
ref="treeRef"
checkable
:treeData="treeData"
:checkedKeys="checkedKeys"
:expandedKeys="expandedKeys"
:selectedKeys="selectedKeys"
:clickRowToExpand="false"
:checkStrictly="true"
title="所拥有的的权限"
@check="onCheck"
@select="onTreeNodeSelect"
>
<template #title="{ slotTitle, ruleFlag }">
{{ slotTitle }}
<Icon v-if="ruleFlag" icon="ant-design:align-left-outlined" style="margin-left: 5px; color: red"></Icon>
</template>
</BasicTree>
<!--右下角按钮-->
<template #footer>
<!-- <PopConfirmButton title="确定放弃编辑?" @confirm="closeDrawer" okText="确定" cancelText="取消"></PopConfirmButton> -->
<a-button @click="closeDrawer">取消</a-button>
<a-button @click="handleSubmit(false)" type="primary" :loading="loading" ghost style="margin-right: 0.8rem">仅保存</a-button>
<a-button @click="handleSubmit(true)" type="primary" :loading="loading">保存并关闭</a-button>
</template>
<RoleDataRuleDrawer @register="registerDrawer1" />
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, computed, unref, onMounted } from 'vue';
import { BasicDrawer, useDrawer, useDrawerInner } from '/@/components/Drawer';
import { BasicTree, TreeItem } from '/@/components/Tree';
import { PopConfirmButton } from '/@/components/Button';
import RoleDataRuleDrawer from './RoleDataRuleDrawer.vue';
import { queryTreeListForRole, queryRolePermission, saveRolePermission, queryScopeRolePermission } from '../role.api';
import { useI18n } from '/@/hooks/web/useI18n';
import { ROLE_AUTH_CONFIG_KEY } from '/@/enums/cacheEnum';
import {ROLE_SCOPE_CONFIG} from '../config/rolePermession'
const emit = defineEmits(['register']);
//树的信息
const treeData = ref<TreeItem[]>([]);
//树的全部节点信息
const allTreeKeys = ref([]);
//树的选择节点信息
const checkedKeys = ref<any>([]);
const defaultCheckedKeys = ref([]);
//树的选中的节点信息
const selectedKeys = ref([]);
const roleId = ref('');
//树的实例
const treeRef = ref(null);
const loading = ref(false);
//展开折叠的key
const expandedKeys = ref<any>([]);
//父子节点选中状态是否关联 true不关联,false关联
const checkStrictly = ref<boolean>(false);
const [registerDrawer1, { openDrawer: openDataRuleDrawer }] = useDrawer();
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
await reset();
setDrawerProps({ confirmLoading: false, loading: true });
roleId.value = data.roleId;
//初始化数据
const roleResult = await queryScopeRolePermission({ ids: ROLE_SCOPE_CONFIG.DEFAULT_PERMESSION_IDS });
// update-begin--author:liaozhiyang---date:20240228---for:【QQYUN-8355】角色权限配置的菜单翻译
treeData.value = translateTitle(roleResult.treeList);
// update-end--author:liaozhiyang---date:20240228---for:【QQYUN-8355】角色权限配置的菜单翻译
allTreeKeys.value = roleResult.ids;
// update-begin--author:liaozhiyang---date:20240531---for:【TV360X-590】角色授权弹窗操作缓存
const localData = localStorage.getItem(ROLE_AUTH_CONFIG_KEY);
if (localData) {
const obj = JSON.parse(localData);
obj.level && treeMenuClick({ key: obj.level });
obj.expand && treeMenuClick({ key: obj.expand });
} else {
expandedKeys.value = roleResult.ids;
}
// update-end--author:liaozhiyang---date:20240531---for:【TV360X-590】角色授权弹窗操作缓存
//初始化角色菜单数据
const permResult = await queryRolePermission({ roleId: unref(roleId) });
checkedKeys.value = permResult;
defaultCheckedKeys.value = permResult;
setDrawerProps({ loading: false });
});
/**
* 2024-02-28
* liaozhiyang
* 翻译菜单名称
*/
function translateTitle(data) {
if (data?.length) {
data.forEach((item) => {
if (item.slotTitle) {
const { t } = useI18n();
if (item.slotTitle.includes("t('") && t) {
item.slotTitle = new Function('t', `return ${item.slotTitle}`)(t);
}
}
if (item.children?.length) {
translateTitle(item.children);
}
});
}
return data;
}
/**
* 点击选中
* 2024-04-26
* liaozhiyang
*/
function onCheck(o, e) {
// checkStrictly: true=>层级独立,false=>层级关联.
if (checkStrictly.value) {
checkedKeys.value = o.checked ? o.checked : o;
} else {
const keys = getNodeAllKey(e.node, 'children', 'key');
if (e.checked) {
// 反复操作下可能会有重复的keys,得用new Set去重下
checkedKeys.value = [...new Set([...checkedKeys.value, ...keys])];
} else {
const result = removeMatchingItems(checkedKeys.value, keys);
checkedKeys.value = result;
}
}
}
/**
* 2024-04-26
* liaozhiyang
* 删除相匹配数组的项
*/
function removeMatchingItems(arr1, arr2) {
// 使用哈希表记录 arr2 中的元素
const hashTable = {};
for (const item of arr2) {
hashTable[item] = true;
}
// 使用 filter 方法遍历第一个数组,过滤出不在哈希表中存在的项
return arr1.filter((item) => !hashTable[item]);
}
/**
* 2024-04-26
* liaozhiyang
* 获取当前节点及以下所有子孙级的key
*/
function getNodeAllKey(node: any, children: any, key: string) {
const result: any = [];
result.push(node[key]);
const recursion = (data) => {
data.forEach((item: any) => {
result.push(item[key]);
if (item[children]?.length) {
recursion(item[children]);
}
});
};
node[children]?.length && recursion(node[children]);
return result;
}
/**
* 选中节点,打开数据权限抽屉
*/
function onTreeNodeSelect(key) {
if (key && key.length > 0) {
selectedKeys.value = key;
}
openDataRuleDrawer(true, { functionId: unref(selectedKeys)[0], roleId: unref(roleId) });
}
/**
* 数据重置
*/
function reset() {
treeData.value = [];
allTreeKeys.value = [];
checkedKeys.value = [];
defaultCheckedKeys.value = [];
selectedKeys.value = [];
roleId.value = '';
}
/**
* 获取tree实例
*/
function getTree() {
const tree = unref(treeRef);
if (!tree) {
throw new Error('tree is null!');
}
return tree;
}
/**
* 提交
*/
async function handleSubmit(exit) {
let params = {
roleId: unref(roleId),
permissionIds: unref(getTree().getCheckedKeys()).join(','),
lastpermissionIds: unref(defaultCheckedKeys).join(','),
};
//update-begin-author:taoyan date:2023-2-11 for: issues/352 VUE角色授权重复保存
if (loading.value === false) {
await doSave(params);
} else {
console.log('请等待上次执行完毕!');
}
if (exit) {
// 如果关闭
closeDrawer();
} else {
// 没有关闭需要重新获取选中数据
const permResult = await queryRolePermission({ roleId: unref(roleId) });
defaultCheckedKeys.value = permResult;
}
}
// VUE角色授权重复保存 #352
async function doSave(params) {
loading.value = true;
try {
await saveRolePermission(params);
} catch (e) {
loading.value = false;
}
setTimeout(() => {
loading.value = false;
}, 500);
}
//update-end-author:taoyan date:2023-2-11 for: issues/352 VUE角色授权重复保存
/**
* 树菜单选择
* @param key
*/
function treeMenuClick({ key }) {
if (key === 'checkAll') {
checkedKeys.value = allTreeKeys.value;
} else if (key === 'cancelCheck') {
checkedKeys.value = [];
} else if (key === 'openAll') {
expandedKeys.value = allTreeKeys.value;
saveLocalOperation('expand', 'openAll');
} else if (key === 'closeAll') {
expandedKeys.value = [];
saveLocalOperation('expand', 'closeAll');
} else if (key === 'relation') {
checkStrictly.value = false;
saveLocalOperation('level', 'relation');
} else {
checkStrictly.value = true;
saveLocalOperation('level', 'standAlone');
}
}
/**
* 2024-05-31
* liaozhiyang
* 【TV360X-590】角色授权弹窗操作缓存
* */
const saveLocalOperation = (key, value) => {
const localData = localStorage.getItem(ROLE_AUTH_CONFIG_KEY);
const obj = localData ? JSON.parse(localData) : {};
obj[key] = value;
localStorage.setItem(ROLE_AUTH_CONFIG_KEY, JSON.stringify(obj));
};
</script>
<style lang="less" scoped>
/** 固定操作按钮 */
.jeecg-basic-tree {
position: absolute;
width: 618px;
}
//update-begin---author:wangshuai ---date:20230202 for:抽屉弹窗标题图标下拉样式------------
.line {
height: 1px;
width: 100%;
border-bottom: 1px solid #f0f0f0;
}
.more-icon {
font-size: 20px !important;
color: black;
display: inline-flex;
float: right;
margin-right: 2px;
cursor: pointer;
}
:deep(.jeecg-tree-header) {
border-bottom: none;
}
//update-end---author:wangshuai ---date:20230202 for:抽屉弹窗标题图标下拉样式------------
</style>
@@ -0,0 +1,268 @@
<template>
<BasicDrawer @register="registerBaseDrawer" title="角色用户" width="800" destroyOnClose>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<!-- <a-button type="primary" @click="handleCreate" v-if="!disableUserEdit"> 新增用户</a-button>-->
<a-button type="primary" @click="handleSelect"> 已有用户</a-button>
<a-dropdown v-if="checkedKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="bx:bx-unlink"></Icon>
取消关联
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="ant-design:down-outlined"></Icon>
</a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
<!--用户操作抽屉-->
<UserDrawer @register="registerDrawer" @success="handleSuccess" />
<!--用户选择弹窗-->
<UseSelectModal @register="registerModal" @select="selectOk" />
<DeptSelectModal @register="registerDeptModal" @success="handleDeptSuccess" />
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, watch, unref } from 'vue';
import { BasicTable, useTable, TableAction } from '/src/components/Table';
import { BasicDrawer, useDrawer, useDrawerInner } from '/src/components/Drawer';
import { useModal } from '/src/components/Modal';
import UserDrawer from '@/views/system/user/UserDrawer.vue';
import UseSelectModal from './UseSelectModal.vue';
import { userList, userListWithDepts, deleteUserRole, batchDeleteUserRole, addUserRole, addDepts, userDepts, saveUserDeptsApi } from '../role.api';
import DeptSelectModal from './DeptSelectModal.vue'; // 引入新组件
import { userColumns, searchUserFormSchema } from '../role.data';
import { getUserRoles } from '@/views/system/user/user.api';
import { options } from 'axios';
const emit = defineEmits(['register', 'hideUserList']);
const props = defineProps({
disableUserEdit: { type: Boolean, default: false },
});
const checkedKeys = ref<Array<string | number>>([]);
const roleId = ref('');
const [registerBaseDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
roleId.value = data.id;
setProps({ searchInfo: { roleId: data.id } });
reload();
});
//注册drawer
const [registerDrawer, { openDrawer }] = useDrawer();
//注册drawer
const [registerModal, { openModal }] = useModal();
// 2. 注册第二个 Modal (部门选择)
const [registerDeptModal, { openModal: openDeptModal, closeModal: closeDeptModal }] = useModal();
const [registerTable, { reload, updateTableDataRecord, setProps }] = useTable({
title: '用户列表',
api: userListWithDepts,
columns: userColumns,
formConfig: {
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】3、租户角色下,查询居左显示
labelWidth: 60,
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】3、租户角色下,查询居左显示
schemas: searchUserFormSchema,
autoSubmitOnEnter: true,
},
striped: true,
useSearchForm: true,
showTableSetting: true,
clickToRowSelect: false,
bordered: true,
showIndexColumn: false,
// 【issues/1064】列设置的 cacheKey
tableSetting: { fullScreen: true, cacheKey: 'role_user_table' },
canResize: false,
rowKey: 'id',
actionColumn: {
width: 180,
title: '操作',
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: undefined,
},
});
/**
* 选择列配置
*/
const rowSelection = {
type: 'checkbox',
columnWidth: 50,
selectedRowKeys: checkedKeys,
onChange: onSelectChange,
};
/**
* 选择事件
*/
function onSelectChange(selectedRowKeys: (string | number)[], selectionRows) {
checkedKeys.value = selectedRowKeys;
}
/**
* 新增
*/
function handleCreate() {
openDrawer(true, {
isUpdate: false,
selectedroles: [roleId.value],
isRole: true,
});
}
/**
* 编辑
*/
async function handleEdit(record: Recordable) {
try {
const userRoles = await getUserRoles({ userid: record.id });
if (userRoles && userRoles.length > 0) {
record.selectedroles = userRoles;
}
} catch (e) {
console.log(e);
}
openDrawer(true, {
record,
isUpdate: true,
isRole: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteUserRole({ userId: record.id, roleId: roleId.value }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDeleteUserRole({ userIds: checkedKeys.value.join(','), roleId: roleId.value }, () => {
// update-begin--author:liaozhiyang---date:20240701---for:【TV360X-1655】批量取消关联之后清空选中记录
reload();
checkedKeys.value = [];
// update-end--author:liaozhiyang---date:20240701---for:【TV360X-1655】批量取消关联之后清空选中记录
});
}
/**
* 成功回调
*/
function handleSuccess({ isUpdate, values }) {
isUpdate ? updateTableDataRecord(values.id, values) : reload();
}
/**
* 选择已有用户
*/
function handleSelect() {
openModal(true);
}
/**
* 添加所属部门按钮点击
*/
async function handleAddDepts(record: Recordable) {
// 1. 在打开前预加载 options,确保渲染时数据已存在
// 这里可以根据 record.id 或 roleId 灵活传参
console.log('record为', record);
// const rId = unref(roleId) ?? '';
const userName = record?.username ?? '';
// 然后进行业务判定
// if (!rId || !userName) {
// console.warn('参数不完整,停止请求');
// return;
// }
try {
const res = await userDepts({ userName: userName });
console.log('加载出来的res', res);
// 2. 格式化为 Select 需要的结构
const options = (res.records || res || []).map((item) => ({
label: item.departName,
value: item.id,
}));
// 请求成功才打开弹窗
if (options && options.length > 0) {
// 3. 打开新 Modal 并传入数据
openDeptModal(true, {
record: record,
deptOptions: options,
});
}
} catch (error) {
// 请求失败,给用户一个提示
console.error('加载部门数据失败,请稍后再试');
console.error('接口报错详情:', error);
}
}
/**
* 添加已有用户
*/
async function selectOk(val) {
await addUserRole({ roleId: roleId.value, userIdList: val }, reload);
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
// {
// label: '编辑',
// onClick: handleEdit.bind(null, record),
// ifShow: () => !props.disableUserEdit,
// },
{
label: '添加所属部门',
onClick: handleAddDepts.bind(null, record),
ifShow: () => !props.disableUserEdit,
},
{
label: '取消关联',
popConfirm: {
title: '是否确认取消关联',
confirm: handleDelete.bind(null, record),
},
},
];
}
// 响应子组件的 emit('success', ...)
async function handleDeptSuccess(data) {
try {
console.log('提交的data为', data);
data.roleId = roleId.value;
// 1. 调用后端接口
await saveUserDeptsApi(data);
// 3. 刷新表格数据
reload();
// 4. 关键:由父组件决定什么时候关闭弹窗
closeDeptModal();
} catch (error) {
// 如果接口报错,由于没有执行 closeModal,弹窗会保持开启,
// 子组件的 setModalProps({ confirmLoading: false }) 也会停止转圈,允许用户重试
console.error('保存失败:', error);
}
}
</script>
<style scoped>
/*update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】3、租户角色下,查询居左显示*/
:deep(.ant-form-item-control-input-content) {
text-align: left;
}
/*update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】3、租户角色下,查询居左显示*/
</style>
@@ -0,0 +1,76 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
title="用户选择列表"
width="1000px"
@ok="handleSubmit"
destroyOnClose
@openChange="handleOpenChange"
>
<BasicTable @register="registerTable" :rowSelection="rowSelection" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, unref, toRaw } from 'vue';
import { BasicModal, useModal, useModalInner } from '/src/components/Modal';
import { BasicTable, useTable, TableAction } from '/src/components/Table';
import { userColumnsWithoutDept, searchUserFormSchema } from '../role.data';
import { listNoCareTenant } from '../../three-admin-user/user.api';
// 声明Emits
const emit = defineEmits(['select', 'register']);
const checkedKeys = ref<Array<string | number>>([]);
const [registerModal, { setModalProps, closeModal }] = useModalInner();
//注册table数据
const [registerTable, { reload }] = useTable({
api: listNoCareTenant,
rowKey: 'id',
columns: userColumnsWithoutDept,
formConfig: {
labelWidth: 60,
schemas: searchUserFormSchema,
baseRowStyle: { maxHeight: '20px' },
autoSubmitOnEnter: true,
},
striped: true,
useSearchForm: true,
showTableSetting: false,
bordered: true,
showIndexColumn: false,
canResize: false,
});
const [registerDeptModal, { openModal: openDeptModal }] = useModal(); // 注册新 Modal
/**
* 选择列配置
*/
const rowSelection = {
type: 'checkbox',
columnWidth: 50,
selectedRowKeys: checkedKeys,
onChange: onSelectChange,
};
/**
* 选择事件
*/
function onSelectChange(selectedRowKeys: (string | number)[]) {
checkedKeys.value = selectedRowKeys;
}
const handleOpenChange = (visible) => {
// -update-begin--author:liaozhiyang---date:20240702---for:【TV360X-1679】系统角色-角色用户再次打开弹窗重置之前选中的状态
if (visible) {
checkedKeys.value = [];
}
// -update-end--author:liaozhiyang---date:20240702---for:【TV360X-1679】系统角色-角色用户再次打开弹窗重置之前选中的状态
};
//提交事件
function handleSubmit() {
setModalProps({ confirmLoading: true });
//关闭弹窗
closeModal();
//刷新列表
emit('select', toRaw(unref(checkedKeys)));
setModalProps({ confirmLoading: false });
}
</script>
@@ -0,0 +1,3 @@
export const ROLE_SCOPE_CONFIG = {
DEFAULT_PERMESSION_IDS: '2035916880060223489',
};
+193
View File
@@ -0,0 +1,193 @@
<template>
<BasicTable @register="registerTable">
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate"> 新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<!--角色用户表格-->
<RoleUserTable @register="roleUserDrawer" />
<!--角色编辑抽屉-->
<RoleDrawer @register="registerDrawer" @success="reload" :showFooter="showFooter" />
<!--角色详情-->
<RoleDesc @register="registerDesc"></RoleDesc>
<!--角色菜单授权抽屉-->
<RolePermissionDrawer @register="rolePermissionDrawer" />
<!--角色首页配置-->
<RoleIndexModal @register="registerIndexModal" />
</template>
<script lang="ts" name="system-role" setup>
import { ref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useDrawer } from '/@/components/Drawer';
import { useModal } from '/@/components/Modal';
import RoleDrawer from './components/RoleDrawer.vue';
import RoleDesc from './components/RoleDesc.vue';
import RolePermissionDrawer from './components/RolePermissionDrawer.vue';
import RoleIndexModal from './components/RoleIndexModal.vue';
import RoleUserTable from './components/RoleUserTable.vue';
import { columns, searchFormSchema } from './role.data';
import { list, deleteRole, batchDeleteRole, getExportUrl, getImportUrl } from './role.api';
import { useListPage } from '/@/hooks/system/useListPage';
const showFooter = ref(true);
const [roleUserDrawer, { openDrawer: openRoleUserDrawer }] = useDrawer();
const [registerDrawer, { openDrawer }] = useDrawer();
const [registerIndexModal, { openModal: openIndexModal }] = useModal();
const [rolePermissionDrawer, { openDrawer: openRolePermissionDrawer }] = useDrawer();
const [registerDesc, { openDrawer: openRoleDesc }] = useDrawer();
// 列表页面公共参数、方法
const { prefixCls, tableContext, onImportXls, onExportXls } = useListPage({
designScope: 'role-template',
tableProps: {
title: '系统角色列表',
api: list,
columns: columns,
formConfig: {
// update-begin--author:liaozhiyang---date:20230803---for:【QQYUN-5873】查询区域lablel默认居左
labelWidth:65,
rowProps: { gutter: 24 },
// update-end--author:liaozhiyang---date:20230803---for:【QQYUN-5873】查询区域lablel默认居左
schemas: searchFormSchema,
},
actionColumn: {
width: 120,
},
rowSelection: null,
//自定义默认排序
defSort: {
column: 'id',
order: 'desc',
},
},
exportConfig: {
name: '角色列表',
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
/**
* 新增
*/
function handleCreate() {
showFooter.value = true;
openDrawer(true, {
isUpdate: false,
});
}
/**
* 编辑
*/
function handleEdit(record: Recordable) {
showFooter.value = true;
openDrawer(true, {
record,
isUpdate: true,
});
}
/**
* 详情
*/
function handleDetail(record) {
showFooter.value = false;
openRoleDesc(true, {
record,
isUpdate: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteRole({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDeleteRole({ ids: selectedRowKeys.value }, reload);
}
/**
* 角色授权弹窗
*/
function handlePerssion(record) {
openRolePermissionDrawer(true, { roleId: record.id });
}
/**
* 首页配置弹窗
*/
function handleIndexConfig(roleCode) {
openIndexModal(true, { roleCode });
}
/**
* 角色用户
*/
function handleUser(record) {
//onSelectChange(selectedRowKeys)
openRoleUserDrawer(true, record);
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '用户',
onClick: handleUser.bind(null, record),
},
{
label: '授权',
onClick: handlePerssion.bind(null, record),
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
// {
// label: '首页配置',
// onClick: handleIndexConfig.bind(null, record.roleCode),
// },
];
}
</script>
+220
View File
@@ -0,0 +1,220 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/sys/role/list',
listByTenant = '/sys/role/listByTenant',
save = '/sys/role/add',
edit = '/sys/role/edit',
deleteRole = '/sys/role/delete',
deleteBatch = '/sys/role/deleteBatch',
exportXls = '/sys/role/exportXls',
importExcel = '/sys/role/importExcel',
isRoleExist = '/sys/role/checkRoleCode',
queryTreeListForRole = '/sys/role/queryTreeList',
queryRolePermission = '/sys/permission/queryRolePermission',
queryScopeRolePermission = '/sys/role/queryTreeListByIds',
saveRolePermission = '/sys/permission/saveRolePermission',
saveDeptForUserRole = '/sys/permission/saveDeptForUserRole',
queryUserDeptsList = '/sys/sysDepart/queryAffiliatedDepts',
queryDataRule = '/sys/role/datarule',
getParentDesignList = '/act/process/extActDesignFlowData/getDesFormFlows',
getRoleDegisnList = '/joa/designform/designFormCommuse/getRoleDegisnList',
saveRoleDesign = '/joa/designform/designFormCommuse/sysRoleDesignAdd',
userList = '/sys/user/userRoleList',
userListWithDepts = '/sys/user/userRoleListWithDepts',
saveDeptForRoleUser = '/sys/role/saveDeptsForUser',
deleteUserRole = '/sys/user/deleteUserRole',
batchDeleteUserRole = '/sys/user/deleteUserRoleBatch',
addUserRole = '/sys/user/addSysUserRole',
saveRoleIndex = '/sys/sysRoleIndex/add',
editRoleIndex = '/sys/sysRoleIndex/edit',
queryIndexByCode = '/sys/sysRoleIndex/queryByCode',
}
/**
* 导出api
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 系统角色列表
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 租户角色列表
* @param params
*/
export const listByTenant = (params) => defHttp.get({ url: Api.listByTenant, params });
/**
* 删除角色
*/
export const deleteRole = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteRole, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 批量删除角色
* @param params
*/
export const batchDeleteRole = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 保存或者更新角色
* @param params
*/
export const saveOrUpdateRole = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
/**
* 编码校验
* @param params
*/
// update-begin--author:liaozhiyang---date:20231215---for:【QQYUN-7415】表单调用接口进行校验的添加防抖
let timer;
export const isRoleExist = (params) => {
return new Promise((resolve, rejected) => {
clearTimeout(timer);
timer = setTimeout(() => {
defHttp
.get({ url: Api.isRoleExist, params }, { isTransformResponse: false })
.then((res) => {
resolve(res);
})
.catch((error) => {
rejected(error);
});
}, 500);
});
};
// update-end--author:liaozhiyang---date:20231215---for:【QQYUN-7415】表单调用接口进行校验的添加防抖
/**
* 根据角色查询树信息
*/
export const queryTreeListForRole = () => defHttp.get({ url: Api.queryTreeListForRole });
/**
* 查询角色权限
*/
export const queryRolePermission = (params) => defHttp.get({ url: Api.queryRolePermission, params });
/**
* 查询特定权限下的子孙权限
*/
export const queryScopeRolePermission = (params) => defHttp.get({ url: Api.queryScopeRolePermission, params });
/**
* 保存角色权限
*/
export const saveRolePermission = (params) => defHttp.post({ url: Api.saveRolePermission, params });
/**
* 查询角色数据规则
*/
export const queryDataRule = (params) =>
defHttp.get({ url: `${Api.queryDataRule}/${params.functionId}/${params.roleId}` }, { isTransformResponse: false });
/**
* 保存角色数据规则
*/
export const saveDataRule = (params) => defHttp.post({ url: Api.queryDataRule, params });
/**
* 获取表单数据
* @return List<Map>
*/
export const getParentDesignList = () => defHttp.get({ url: Api.getParentDesignList });
/**
* 获取角色表单数据
* @return List<Map>
*/
export const getRoleDegisnList = (params) => defHttp.get({ url: Api.getRoleDegisnList, params });
/**
* 提交角色工单信息
*/
export const saveRoleDesign = (params) => defHttp.post({ url: Api.saveRoleDesign, params });
/**
* 角色列表接口
* @param params
*/
export const userList = (params) => defHttp.get({ url: Api.userList, params });
/**
* 角色列表接口
* @param params
*/
export const userListWithDepts = (params) => defHttp.get({ url: Api.userListWithDepts, params });
/**
* 保存用户角色的应用部门
*/
export const saveUserDeptsApi = (params) => defHttp.post({ url: Api.saveDeptForRoleUser, params });
/**
* 获取用户所在全部部门接口
* @param params
*/
export const userDepts = (params) => defHttp.get({ url: Api.queryUserDeptsList, params });
/**
* 删除角色用户
*/
export const deleteUserRole = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteUserRole, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 批量删除角色用户
* @param params
*/
export const batchDeleteUserRole = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.batchDeleteUserRole, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 添加已有用户
*/
export const addUserRole = (params, handleSuccess) => {
return defHttp.post({ url: Api.addUserRole, params }).then(() => {
handleSuccess();
});
};
/**
* 保存或者更新
* @param params
* @param isUpdate 是否是更新数据
*/
export const saveOrUpdateRoleIndex = (params, isUpdate) => {
let url = isUpdate ? Api.editRoleIndex : Api.saveRoleIndex;
return defHttp.post({ url: url, params });
};
/**
* 为用户的角色指定应用部门
* @param params
*/
export const addDepts = (params) => {
return defHttp.post({ url: Api.saveDeptForUserRole, params });
};
/**
* 根据code查询首页配置
* @param params
*/
export const queryIndexByCode = (params) => defHttp.get({ url: Api.queryIndexByCode, params }, { isTransformResponse: false });
+227
View File
@@ -0,0 +1,227 @@
import { FormSchema } from '/@/components/Table';
import { isRoleExist } from './role.api';
import { JDictSelectTag, JSelectDept } from '@/components/Form';
import { h } from 'vue';
export const columns = [
{
title: '角色名称',
dataIndex: 'roleName',
width: 100,
},
{
title: '角色编码',
dataIndex: 'roleCode',
width: 100,
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 100,
},
];
/**
* 角色用户Columns
*/
export const userColumns = [
{
title: '用户id',
dataIndex: 'userId',
},
{
title: '用户账号',
dataIndex: 'username',
},
{
title: '用户姓名',
dataIndex: 'realname',
},
{
title: '状态',
dataIndex: 'status_dictText',
width: 80,
},
{
title: '所属部门',
dataIndex: 'deptId', // 确保这是你后端返回的逗号分隔字符串,如 "1,2,3"
width: 250,
ellipsis: true,
customRender: ({ text }) => {
// 如果没有数据,直接返回空
if (!text) return '';
// 使用 h 函数渲染部门选择组件
return h(JSelectDept, {
value: text, // 传入 "1,2,3"
disabled: true, // 必须禁用,否则用户在表格里能点开选
rowKey: 'id', // 对应数据库主键
checkStrictly: true, // 父子不互相关联
multi: true, // 开启多选支持,处理逗号分隔
// 如果你希望显示成简单的文本而不是输入框样式,可以根据组件支持情况调整
});
},
},
];
export const userColumnsWithoutDept = [
{
title: '用户账号',
dataIndex: 'username',
},
{
title: '用户姓名',
dataIndex: 'realname',
},
{
title: '状态',
dataIndex: 'status_dictText',
width: 80,
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'roleName',
label: '角色名称',
component: 'Input',
colProps: { span: 6 },
},
{
field: 'roleCode',
label: '角色编码',
component: 'Input',
colProps: { span: 6 },
},
];
/**
* 角色用户搜索form
*/
export const searchUserFormSchema: FormSchema[] = [
{
field: 'username',
label: '用户账号',
component: 'Input',
colProps: { span: 12 },
labelWidth: 74,
},
];
export const formSchema: FormSchema[] = [
{
field: 'id',
label: '',
component: 'Input',
show: false,
},
{
field: 'roleName',
label: '角色名称',
required: true,
component: 'Input',
},
{
field: 'roleCode',
label: '角色编码',
required: true,
component: 'Input',
dynamicDisabled: ({ values }) => {
return !!values.id;
},
dynamicRules: ({ values, model }) => {
console.log('values:', values);
return [
{
required: true,
validator: (_, value) => {
if (!value) {
return Promise.reject('请输入角色编码');
}
if (values) {
return new Promise((resolve, reject) => {
isRoleExist({ id: model.id, roleCode: value })
.then((res) => {
res.success ? resolve() : reject(res.message || '校验失败');
})
.catch((err) => {
reject(err.message || '验证失败');
});
});
}
return Promise.resolve();
},
},
];
},
},
{
label: '备注',
field: 'description',
component: 'InputTextArea',
},
];
export const formDescSchema = [
{
field: 'roleName',
label: '角色名称',
},
{
field: 'roleCode',
label: '角色编码',
},
{
label: '备注',
field: 'description',
},
];
export const roleIndexFormSchema: FormSchema[] = [
{
field: 'id',
label: '',
component: 'Input',
show: false,
},
{
label: '角色编码',
field: 'roleCode',
component: 'Input',
dynamicDisabled: true,
},
{
label: '首页路由',
field: 'url',
component: 'Input',
required: true,
helpMessage: '首页路由的访问地址',
},
{
label: '组件地址',
field: 'component',
component: 'Input',
helpMessage: '首页路由的组件地址',
componentProps: {
placeholder: '请输入前端组件',
},
required: true,
},
{
field: 'route',
label: '是否路由菜单',
helpMessage: '非路由菜单设置成首页,需开启',
component: 'Switch',
defaultValue: true,
},
{
label: '优先级',
field: 'priority',
component: 'InputNumber',
},
{
label: '是否开启',
field: 'status',
component: 'JSwitch',
componentProps: {
options: ['1', '0'],
},
},
];
@@ -0,0 +1,78 @@
<template>
<BasicDrawer title="数据规则/按钮权限配置" :width="365" @close="onClose" @register="registerDrawer">
<a-spin :spinning="loading">
<a-tabs defaultActiveKey="1">
<a-tab-pane tab="数据规则" key="1">
<a-checkbox-group v-model:value="dataRuleChecked" v-if="dataRuleList.length > 0">
<a-row>
<a-col :span="24" v-for="(item, index) in dataRuleList" :key="'dr' + index">
<a-checkbox :value="item.id">{{ item.ruleName }}</a-checkbox>
</a-col>
<a-col :span="24">
<div style="width: 100%; margin-top: 15px">
<a-button type="primary" :loading="loading" :size="'small'" preIcon="ant-design:save-filled" @click="saveDataRuleForRole">
<span>点击保存</span>
</a-button>
</div>
</a-col>
</a-row>
</a-checkbox-group>
<a-empty v-else description="无配置信息" />
</a-tab-pane>
</a-tabs>
</a-spin>
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { queryDepartDataRule, saveDepartDataRule } from '../depart.api';
defineEmits(['register']);
const loading = ref<boolean>(false);
const departId = ref('');
const functionId = ref('');
const dataRuleList = ref<Array<any>>([]);
const dataRuleChecked = ref<Array<any>>([]);
// 注册抽屉组件
const [registerDrawer, { closeDrawer }] = useDrawerInner((data) => {
departId.value = unref(data.departId);
functionId.value = unref(data.functionId);
loadData();
});
async function loadData() {
try {
loading.value = true;
const { datarule, drChecked } = await queryDepartDataRule(functionId, departId);
dataRuleList.value = datarule;
if (drChecked) {
dataRuleChecked.value = drChecked.split(',');
}
} finally {
loading.value = false;
}
}
function saveDataRuleForRole() {
let params = {
departId: departId.value,
permissionId: functionId.value,
dataRuleIds: dataRuleChecked.value.join(','),
};
saveDepartDataRule(params);
}
function onClose() {
doReset();
}
function doReset() {
functionId.value = '';
dataRuleList.value = [];
dataRuleChecked.value = [];
}
</script>
@@ -0,0 +1,92 @@
<template>
<BasicModal :title="title" :width="800" v-bind="$attrs" @ok="handleOk" @register="registerModal">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { watch, computed, inject, ref, unref, onMounted } from 'vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { saveOrUpdateDepart } from '../depart.api';
import { useBasicFormSchema, orgCategoryOptions } from '../depart.data';
const emit = defineEmits(['success', 'register']);
const props = defineProps({
rootTreeData: { type: Array, default: () => [] },
});
const prefixCls = inject('prefixCls');
// 当前是否是更新模式
const isUpdate = ref<boolean>(false);
// 当前的弹窗数据
const model = ref<object>({});
const title = computed(() => (isUpdate.value ? '编辑' : '新增'));
//注册表单
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema }] = useForm({
schemas: useBasicFormSchema().basicFormSchema,
showActionButtonGroup: false,
});
// 注册弹窗
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
isUpdate.value = unref(data?.isUpdate);
// 当前是否为添加子级
let isChild = unref(data?.isChild);
let categoryOptions = isChild ? orgCategoryOptions.child : orgCategoryOptions.root;
// 隐藏不需要展示的字段
updateSchema([
{
field: 'parentId',
show: isChild,
componentProps: {
// 如果是添加子部门,就禁用该字段
disabled: isChild,
treeData: props.rootTreeData,
},
},
{
field: 'orgCode',
show: false,
},
{
field: 'orgCategory',
componentProps: { options: categoryOptions },
},
]);
let record = unref(data?.record);
if (typeof record !== 'object') {
record = {};
}
// 赋默认值
record = Object.assign(
{
departOrder: 0,
orgCategory: categoryOptions[0].value,
},
record
);
model.value = record;
await setFieldsValue({ ...record });
});
// 提交事件
async function handleOk() {
try {
setModalProps({ confirmLoading: true });
let values = await validate();
//提交表单
await saveOrUpdateDepart(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
@@ -0,0 +1,128 @@
<template>
<a-spin :spinning="loading">
<BasicForm @register="registerForm" />
<div class="j-box-bottom-button offset-20" style="margin-top: 30px">
<div class="j-box-bottom-button-float" :class="[`${prefixCls}`]">
<a-button preIcon="ant-design:sync-outlined" @click="onReset">重置</a-button>
<a-button type="primary" preIcon="ant-design:save-filled" @click="onSubmit">保存</a-button>
</div>
</div>
</a-spin>
</template>
<script lang="ts" setup>
import { watch, computed, inject, ref, unref, onMounted } from 'vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { saveOrUpdateDepart } from '../depart.api';
import { useBasicFormSchema, orgCategoryOptions } from '../depart.data';
import { useDesign } from '/@/hooks/web/useDesign';
const { prefixCls } = useDesign('j-depart-form-content');
const emit = defineEmits(['success']);
const props = defineProps({
data: { type: Object, default: () => ({}) },
rootTreeData: { type: Array, default: () => [] },
});
const loading = ref<boolean>(false);
// 当前是否是更新模式
const isUpdate = ref<boolean>(true);
// 当前的弹窗数据
const model = ref<object>({});
//注册表单
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema }] = useForm({
schemas: useBasicFormSchema().basicFormSchema,
showActionButtonGroup: false,
});
const categoryOptions = computed(() => {
if (!!props?.data?.parentId) {
return orgCategoryOptions.child;
} else {
return orgCategoryOptions.root;
}
});
onMounted(() => {
// 禁用字段
updateSchema([
{ field: 'parentId', componentProps: { disabled: true } },
{ field: 'orgCode', componentProps: { disabled: true } },
]);
// data 变化,重填表单
watch(
() => props.data,
async () => {
let record = unref(props.data);
if (typeof record !== 'object') {
record = {};
}
model.value = record;
await resetFields();
await setFieldsValue({ ...record });
},
{ deep: true, immediate: true }
);
// 更新 父部门 选项
watch(
() => props.rootTreeData,
async () => {
updateSchema([
{
field: 'parentId',
componentProps: { treeData: props.rootTreeData },
},
]);
},
{ deep: true, immediate: true }
);
// 监听并更改 orgCategory options
watch(
categoryOptions,
async () => {
updateSchema([
{
field: 'orgCategory',
componentProps: { options: categoryOptions.value },
},
]);
},
{ immediate: true }
);
});
// 重置表单
async function onReset() {
await resetFields();
await setFieldsValue({ ...model.value });
}
// 提交事件
async function onSubmit() {
try {
loading.value = true;
let values = await validate();
values = Object.assign({}, model.value, values);
//提交表单
await saveOrUpdateDepart(values, isUpdate.value);
//刷新列表
emit('success');
Object.assign(model.value, values);
} finally {
loading.value = false;
}
}
</script>
<style lang="less">
// update-begin-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
@prefix-cls: ~'@{namespace}-j-depart-form-content';
/*begin 兼容暗夜模式*/
.@{prefix-cls} {
background: @component-background;
border-top: 1px solid @border-color-base;
}
/*end 兼容暗夜模式*/
// update-end-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
</style>
@@ -0,0 +1,338 @@
<template>
<a-card :bordered="false" style="height: 100%">
<div class="j-table-operator" style="width: 100%">
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="onAddDepart">新增</a-button>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="onAddChildDepart()">添加下级</a-button>
<a-upload name="file" :showUploadList="false" :customRequest="onImportXls">
<a-button type="primary" preIcon="ant-design:import-outlined">导入</a-button>
</a-upload>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
<!-- <a-button type="primary" preIcon="ant-design:sync-outlined">同步企微?</a-button>-->
<!-- <a-button type="primary" preIcon="ant-design:sync-outlined">同步钉钉?</a-button>-->
<template v-if="checkedKeys.length > 0">
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="onDeleteBatch">
<icon icon="ant-design:delete-outlined" />
<span>删除</span>
</a-menu-item>
</a-menu>
</template>
<a-button>
<span>批量操作 </span>
<icon icon="akar-icons:chevron-down" />
</a-button>
</a-dropdown>
</template>
</div>
<a-alert type="info" show-icon class="alert" style="margin-bottom: 8px">
<template #message>
<template v-if="checkedKeys.length > 0">
<span>已选中 {{ checkedKeys.length }} 条记录</span>
<a-divider type="vertical" />
<a @click="checkedKeys = []">清空</a>
</template>
<template v-else>
<span>未选中任何数据</span>
</template>
</template>
</a-alert>
<a-spin :spinning="loading">
<a-input-search placeholder="按部门名称搜索…" style="margin-bottom: 10px" @search="onSearch" />
<!--组织机构树-->
<template v-if="treeData.length > 0">
<a-tree
v-if="!treeReloading"
checkable
:clickRowToExpand="false"
:treeData="treeData"
:selectedKeys="selectedKeys"
:checkStrictly="checkStrictly"
:load-data="loadChildrenTreeData"
:checkedKeys="checkedKeys"
v-model:expandedKeys="expandedKeys"
@check="onCheck"
@select="onSelect"
>
<template #title="{ key: treeKey, title, dataRef }">
<a-dropdown :trigger="['contextmenu']">
<Popconfirm
:open="visibleTreeKey === treeKey"
title="确定要删除吗?"
ok-text="确定"
cancel-text="取消"
placement="rightTop"
@confirm="onDelete(dataRef)"
@openChange="onVisibleChange"
>
<span>{{ title }}</span>
</Popconfirm>
<template #overlay>
<a-menu @click="">
<a-menu-item key="1" @click="onAddChildDepart(dataRef)">添加子级</a-menu-item>
<a-menu-item key="2" @click="visibleTreeKey = treeKey">
<span style="color: red">删除</span>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-tree>
</template>
<a-empty v-else description="暂无数据" />
</a-spin>
<DepartFormModal :rootTreeData="treeData" @register="registerModal" @success="loadRootTreeData" />
</a-card>
</template>
<script lang="ts" setup>
import { inject, nextTick, ref, unref } from 'vue';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { useMethods } from '/@/hooks/system/useMethods';
import { Api, deleteBatchDepart, queryDepartTreeSync } from '../depart.api';
import { searchByKeywords } from '/@/views/system/departUser/depart.user.api';
import DepartFormModal from './DepartFormModal.vue';
import { Popconfirm } from 'ant-design-vue';
const prefixCls = inject('prefixCls');
const emit = defineEmits(['select', 'rootTreeData']);
const { createMessage } = useMessage();
const { handleImportXls, handleExportXls } = useMethods();
const loading = ref<boolean>(false);
// 部门树列表数据
const treeData = ref<any[]>([]);
// 当前选中的项
const checkedKeys = ref<any[]>([]);
// 当前展开的项
const expandedKeys = ref<any[]>([]);
// 当前选中的项
const selectedKeys = ref<any[]>([]);
// 树组件重新加载
const treeReloading = ref<boolean>(false);
// 树父子是否关联
const checkStrictly = ref<boolean>(true);
// 当前选中的部门
const currentDepart = ref<any>(null);
// 控制确认删除提示框是否显示
const visibleTreeKey = ref<any>(null);
// 搜索关键字
const searchKeyword = ref('');
// 注册 modal
const [registerModal, { openModal }] = useModal();
// 加载顶级部门信息
async function loadRootTreeData() {
try {
loading.value = true;
treeData.value = [];
const result = await queryDepartTreeSync();
if (Array.isArray(result)) {
treeData.value = result;
}
if (expandedKeys.value.length === 0) {
autoExpandParentNode();
} else {
if (selectedKeys.value.length === 0) {
let item = treeData.value[0];
if (item) {
// 默认选中第一个
setSelectedKey(item.id, item);
}
} else {
emit('select', currentDepart.value);
}
}
emit('rootTreeData', treeData.value);
} finally {
loading.value = false;
}
}
loadRootTreeData();
// 加载子级部门信息
async function loadChildrenTreeData(treeNode) {
try {
const result = await queryDepartTreeSync({
pid: treeNode.dataRef.id,
});
if (result.length == 0) {
treeNode.dataRef.isLeaf = true;
} else {
treeNode.dataRef.children = result;
if (expandedKeys.value.length > 0) {
// 判断获取的子级是否有当前展开的项
let subKeys: any[] = [];
for (let key of expandedKeys.value) {
if (result.findIndex((item) => item.id === key) !== -1) {
subKeys.push(key);
}
}
if (subKeys.length > 0) {
expandedKeys.value = [...expandedKeys.value];
}
}
}
treeData.value = [...treeData.value];
emit('rootTreeData', treeData.value);
} catch (e) {
console.error(e);
}
return Promise.resolve();
}
// 自动展开父节点,只展开一级
function autoExpandParentNode() {
let item = treeData.value[0];
if (item) {
if (!item.isLeaf) {
expandedKeys.value = [item.key];
}
// 默认选中第一个
setSelectedKey(item.id, item);
reloadTree();
} else {
emit('select', null);
}
}
// 重新加载树组件,防止无法默认展开数据
async function reloadTree() {
await nextTick();
treeReloading.value = true;
await nextTick();
treeReloading.value = false;
}
/**
* 设置当前选中的行
*/
function setSelectedKey(key: string, data?: object) {
selectedKeys.value = [key];
if (data) {
currentDepart.value = data;
emit('select', data);
}
}
// 添加一级部门
function onAddDepart() {
openModal(true, { isUpdate: false, isChild: false });
}
// 添加子级部门
function onAddChildDepart(data = currentDepart.value) {
if (data == null) {
createMessage.warning('请先选择一个部门');
return;
}
const record = { parentId: data.id };
openModal(true, { isUpdate: false, isChild: true, record });
}
// 搜索事件
async function onSearch(value: string) {
if (value) {
try {
loading.value = true;
treeData.value = [];
let result = await searchByKeywords({ keyWord: value });
if (Array.isArray(result)) {
treeData.value = result;
}
autoExpandParentNode();
} finally {
loading.value = false;
}
} else {
loadRootTreeData();
}
searchKeyword.value = value;
}
// 树复选框选择事件
function onCheck(e) {
if (Array.isArray(e)) {
checkedKeys.value = e;
} else {
checkedKeys.value = e.checked;
}
}
// 树选择事件
function onSelect(selKeys, event) {
console.log('select: ', selKeys, event);
if (selKeys.length > 0 && selectedKeys.value[0] !== selKeys[0]) {
setSelectedKey(selKeys[0], event.selectedNodes[0]);
} else {
// 这样可以防止用户取消选择
setSelectedKey(selectedKeys.value[0]);
}
}
/**
* 根据 ids 删除部门
* @param idListRef array
* @param confirm 是否显示确认提示框
*/
async function doDeleteDepart(idListRef, confirm = true) {
const idList = unref(idListRef);
if (idList.length > 0) {
try {
loading.value = true;
await deleteBatchDepart({ ids: idList.join(',') }, confirm);
await loadRootTreeData();
} finally {
loading.value = false;
}
}
}
// 删除单个部门
async function onDelete(data) {
if (data) {
onVisibleChange(false);
doDeleteDepart([data.id], false);
}
}
// 批量删除部门
async function onDeleteBatch() {
try {
await doDeleteDepart(checkedKeys);
checkedKeys.value = [];
} finally {
}
}
function onVisibleChange(visible) {
if (!visible) {
visibleTreeKey.value = null;
}
}
function onImportXls(d) {
handleImportXls(d, Api.importExcelUrl, () => {
loadRootTreeData();
});
}
function onExportXls() {
//update-begin---author:wangshuai---date:2024-07-05---for:【TV360X-1671】部门管理不支持选中的记录导出---
let params = {}
if(checkedKeys.value && checkedKeys.value.length > 0) {
params['selections'] = checkedKeys.value.join(',')
}
handleExportXls('部门信息', Api.exportXlsUrl,params);
//update-end---author:wangshuai---date:2024-07-05---for:【TV360X-1671】部门管理不支持选中的记录导出---
}
defineExpose({
loadRootTreeData,
});
</script>
@@ -0,0 +1,273 @@
<template>
<a-spin :spinning="loading">
<template v-if="treeData.length > 0">
<BasicTree
ref="basicTree"
class="depart-rule-tree"
checkable
:treeData="treeData"
:checkedKeys="checkedKeys"
:selectedKeys="selectedKeys"
:expandedKeys="expandedKeys"
:checkStrictly="true"
style="height: 500px; overflow: auto"
@check="onCheck"
@expand="onExpand"
@select="onSelect"
>
<template #title="{ slotTitle, ruleFlag }">
<span>{{ slotTitle }}</span>
<Icon v-if="ruleFlag" icon="ant-design:align-left-outlined" style="margin-left: 5px; color: red" />
</template>
</BasicTree>
</template>
<a-empty v-else description="无可配置部门权限" />
<div class="j-box-bottom-button offset-20" style="margin-top: 30px">
<div class="j-box-bottom-button-float" :class="[`${prefixCls}`]">
<a-dropdown :trigger="['click']" placement="top">
<template #overlay>
<a-menu>
<a-menu-item key="3" @click="toggleCheckALL(true)">{{ t('component.tree.selectAll') }}</a-menu-item>
<a-menu-item key="4" @click="toggleCheckALL(false)">{{ t('component.tree.unSelectAll') }}</a-menu-item>
<a-menu-item key="5" @click="toggleExpandAll(true)">{{ t('component.tree.expandAll') }}</a-menu-item>
<a-menu-item key="6" @click="toggleExpandAll(false)">{{ t('component.tree.unExpandAll') }}</a-menu-item>
<a-menu-item key="7" @click="toggleRelationAll(false)">{{ t('component.tree.checkStrictly') }}</a-menu-item>
<a-menu-item key="8" @click="toggleRelationAll(true)">{{ t('component.tree.checkUnStrictly') }}</a-menu-item>
</a-menu>
</template>
<a-button style="float: left">
树操作
<Icon icon="ant-design:up-outlined" />
</a-button>
</a-dropdown>
<a-button type="primary" preIcon="ant-design:save-filled" @click="onSubmit">保存</a-button>
</div>
</div>
</a-spin>
<DepartDataRuleDrawer @register="registerDataRuleDrawer" />
</template>
<script lang="ts" setup>
import { watch, computed, inject, ref, nextTick } from 'vue';
import { useDrawer } from '/@/components/Drawer';
import { BasicTree } from '/@/components/Tree/index';
import DepartDataRuleDrawer from './DepartDataRuleDrawer.vue';
import { queryRoleTreeList, queryDepartPermission, saveDepartPermission } from '../depart.api';
import { useDesign } from '/@/hooks/web/useDesign';
import { translateTitle } from '/@/utils/common/compUtils';
import { DEPART_MANGE_AUTH_CONFIG_KEY } from '/@/enums/cacheEnum';
import { useI18n } from '/@/hooks/web/useI18n';
const { prefixCls } = useDesign('j-depart-form-content');
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
// 当前选中的部门ID,可能会为空,代表未选择部门
const departId = computed(() => props.data?.id);
const basicTree = ref();
const loading = ref<boolean>(false);
//树的全部节点信息
const allTreeKeys = ref([]);
const treeData = ref<any[]>([]);
const expandedKeys = ref<Array<any>>([]);
const selectedKeys = ref<Array<any>>([]);
const checkedKeys = ref<Array<any>>([]);
const lastCheckedKeys = ref<Array<any>>([]);
const checkStrictly = ref(false);
const { t } = useI18n();
// 注册数据规则授权弹窗抽屉
const [registerDataRuleDrawer, dataRuleDrawer] = useDrawer();
// onCreated
loadData({
success: (ids) => {
// update-begin--author:liaozhiyang---date:20240704---for:【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
const localData = localStorage.getItem(DEPART_MANGE_AUTH_CONFIG_KEY);
if (localData) {
const obj = JSON.parse(localData);
obj.level && toggleRelationAll(obj.level == 'relation' ? false : true);
obj.expand && toggleExpandAll(obj.expand == 'openAll' ? true :false);
} else {
// expandedKeys.value = ids;
}
// update-end--author:liaozhiyang---date:20240704---for:【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
}
});
watch(departId, () => loadDepartPermission(), { immediate: true });
async function loadData(options: any = {}) {
try {
loading.value = true;
let { treeList, ids } = await queryRoleTreeList();
//update-begin---author:wangshuai---date:2024-04-08---for:【issues/1169】部门管理功能中的【部门权限】中未翻译 t('') 多语言---
treeData.value = translateTitle(treeList);
//update-end---author:wangshuai---date:2024-04-08---for:【issues/1169】部门管理功能中的【部门权限】中未翻译 t('') 多语言---
// update-begin--author:liaozhiyang---date:20240704---for:【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
allTreeKeys.value = ids;
options.success?.(ids);
// update-end--author:liaozhiyang---date:20240704---for:【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
} finally {
loading.value = false;
}
}
async function loadDepartPermission() {
if (departId.value) {
try {
loading.value = true;
let keys = await queryDepartPermission({ departId: departId.value });
checkedKeys.value = keys;
lastCheckedKeys.value = [...keys];
} finally {
loading.value = false;
}
}
}
async function onSubmit() {
try {
loading.value = true;
await saveDepartPermission({
departId: departId.value,
permissionIds: checkedKeys.value.join(','),
lastpermissionIds: lastCheckedKeys.value.join(','),
});
await loadData();
await loadDepartPermission();
} finally {
loading.value = false;
}
}
/**
* 点击选中
* 2024-07-04
* liaozhiyang
*/
function onCheck(o, e) {
// checkStrictly: true=>层级独立,false=>层级关联.
if (checkStrictly.value) {
checkedKeys.value = o.checked ? o.checked : o;
} else {
const keys = getNodeAllKey(e.node, 'children', 'key');
if (e.checked) {
// 反复操作下可能会有重复的keys,得用new Set去重下
checkedKeys.value = [...new Set([...checkedKeys.value, ...keys])];
} else {
const result = removeMatchingItems(checkedKeys.value, keys);
checkedKeys.value = result;
}
}
}
/**
* 2024-07-04
* liaozhiyang
* 删除相匹配数组的项
*/
function removeMatchingItems(arr1, arr2) {
// 使用哈希表记录 arr2 中的元素
const hashTable = {};
for (const item of arr2) {
hashTable[item] = true;
}
// 使用 filter 方法遍历第一个数组,过滤出不在哈希表中存在的项
return arr1.filter((item) => !hashTable[item]);
}
/**
* 2024-07-04
* liaozhiyang
* 获取当前节点及以下所有子孙级的key
*/
function getNodeAllKey(node: any, children: any, key: string) {
const result: any = [];
result.push(node[key]);
const recursion = (data) => {
data.forEach((item: any) => {
result.push(item[key]);
if (item[children]?.length) {
recursion(item[children]);
}
});
};
node[children]?.length && recursion(node[children]);
return result;
}
// tree展开事件
function onExpand($expandedKeys) {
expandedKeys.value = $expandedKeys;
}
// tree选中事件
function onSelect($selectedKeys, { selectedNodes }) {
if (selectedNodes[0]?.ruleFlag) {
let functionId = $selectedKeys[0];
dataRuleDrawer.openDrawer(true, { departId, functionId });
}
selectedKeys.value = [];
}
// 切换父子关联
async function toggleCheckStrictly(flag) {
checkStrictly.value = flag;
await nextTick();
checkedKeys.value = basicTree.value.getCheckedKeys();
}
// 切换展开收起
async function toggleExpandAll(flag) {
// update-begin--author:liaozhiyang---date:20240704---for:【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
if (flag) {
expandedKeys.value = allTreeKeys.value;
saveLocalOperation('expand', 'openAll');
} else {
expandedKeys.value = [];
saveLocalOperation('expand', 'closeAll');
}
// update-end--author:liaozhiyang---date:20240704---for:【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
}
// 切换全选
async function toggleCheckALL(flag) {
// update-begin--author:liaozhiyang---date:20240704---for:【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
if (flag) {
checkedKeys.value = allTreeKeys.value;
} else {
checkedKeys.value = [];
}
// update-end--author:liaozhiyang---date:20240704---for:【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
}
// 切换层级关联(独立)
const toggleRelationAll = (flag) => {
// update-begin--author:liaozhiyang---date:20240704---for:【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
checkStrictly.value = flag;
if (flag) {
saveLocalOperation('level', 'standAlone');
} else {
saveLocalOperation('level', 'relation');
}
// update-end--author:liaozhiyang---date:20240704---for:【TV360X-1689】同步系统角色改法加上缓存层级关联等功能
};
/**
* 2024-07-04
* liaozhiyang
* 缓存
* */
const saveLocalOperation = (key, value) => {
const localData = localStorage.getItem(DEPART_MANGE_AUTH_CONFIG_KEY);
const obj = localData ? JSON.parse(localData) : {};
obj[key] = value;
localStorage.setItem(DEPART_MANGE_AUTH_CONFIG_KEY, JSON.stringify(obj))
};
</script>
<style lang="less" scoped>
// 【VUEN-188】解决滚动条不灵敏的问题
.depart-rule-tree :deep(.scrollbar__bar) {
pointer-events: none;
}
</style>
@@ -0,0 +1,122 @@
import { unref } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
const { createConfirm } = useMessage();
export enum Api {
queryDepartTreeSync = '/sys/sysDepart/queryDepartTreeSync',
save = '/sys/sysDepart/add',
edit = '/sys/sysDepart/edit',
delete = '/sys/sysDepart/delete',
deleteBatch = '/sys/sysDepart/deleteBatch',
exportXlsUrl = '/sys/sysDepart/exportXls',
importExcelUrl = '/sys/sysDepart/importExcel',
roleQueryTreeList = '/sys/role/queryTreeList',
queryDepartPermission = '/sys/permission/queryDepartPermission',
saveDepartPermission = '/sys/permission/saveDepartPermission',
dataRule = '/sys/sysDepartPermission/datarule',
getCurrentUserDeparts = '/sys/user/getCurrentUserDeparts',
selectDepart = '/sys/selectDepart',
getUpdateDepartInfo = '/sys/user/getUpdateDepartInfo',
doUpdateDepartInfo = '/sys/user/doUpdateDepartInfo',
changeDepartChargePerson = '/sys/user/changeDepartChargePerson',
}
/**
* 获取部门树列表
*/
export const queryDepartTreeSync = (params?) => defHttp.get({ url: Api.queryDepartTreeSync, params });
/**
* 保存或者更新部门角色
*/
export const saveOrUpdateDepart = (params, isUpdate) => {
if (isUpdate) {
return defHttp.put({ url: Api.edit, params });
} else {
return defHttp.post({ url: Api.save, params });
}
};
/**
* 批量删除部门角色
*/
export const deleteBatchDepart = (params, confirm = false) => {
return new Promise((resolve, reject) => {
const doDelete = () => {
resolve(defHttp.delete({ url: Api.deleteBatch, params }, { joinParamsToUrl: true }));
};
if (confirm) {
createConfirm({
iconType: 'warning',
title: '删除',
content: '确定要删除吗?',
onOk: () => doDelete(),
onCancel: () => reject(),
});
} else {
doDelete();
}
});
};
/**
* 获取权限树列表
*/
export const queryRoleTreeList = (params?) => defHttp.get({ url: Api.roleQueryTreeList, params });
/**
* 查询部门权限
*/
export const queryDepartPermission = (params?) => defHttp.get({ url: Api.queryDepartPermission, params });
/**
* 保存部门权限
*/
export const saveDepartPermission = (params) => defHttp.post({ url: Api.saveDepartPermission, params });
/**
* 查询部门数据权限列表
*/
export const queryDepartDataRule = (functionId, departId, params?) => {
let url = `${Api.dataRule}/${unref(functionId)}/${unref(departId)}`;
return defHttp.get({ url, params });
};
/**
* 保存部门数据权限
*/
export const saveDepartDataRule = (params) => defHttp.post({ url: Api.dataRule, params });
/**
* 获取登录用户部门信息
*/
export const getUserDeparts = (params?) => defHttp.get({ url: Api.getCurrentUserDeparts, params });
/**
* 切换选择部门
*/
export const selectDepart = (params?) => defHttp.put({ url: Api.selectDepart, params });
/**
* 编辑部门前获取部门相关信息
* @param id
*/
export const getUpdateDepartInfo = (id) => defHttp.get({ url: Api.getUpdateDepartInfo, params: {id} });
/**
* 编辑部门
* @param params
*/
export const doUpdateDepartInfo = (params) => defHttp.put({ url: Api.doUpdateDepartInfo, params });
/**
* 删除部门
* @param id
*/
export const deleteDepart = (id) => defHttp.delete({ url: Api.delete, params:{ id } }, { joinParamsToUrl: true });
/**
* 设置负责人 取消负责人
* @param params
*/
export const changeDepartChargePerson = (params) => defHttp.put({ url: Api.changeDepartChargePerson, params });
@@ -0,0 +1,105 @@
import { FormSchema } from '/@/components/Form';
// 部门基础表单
export function useBasicFormSchema() {
const basicFormSchema: FormSchema[] = [
{
field: 'departName',
label: '机构名称',
component: 'Input',
componentProps: {
placeholder: '请输入机构/部门名称',
},
rules: [{ required: true, message: '机构名称不能为空' }],
},
{
field: 'parentId',
label: '上级部门',
component: 'TreeSelect',
componentProps: {
treeData: [],
placeholder: '无',
dropdownStyle: { maxHeight: '200px', overflow: 'auto' },
},
},
{
field: 'orgCode',
label: '机构编码',
component: 'Input',
componentProps: {
placeholder: '请输入机构编码',
},
},
{
field: 'orgCategory',
label: '机构类型',
component: 'RadioButtonGroup',
componentProps: { options: [] },
},
{
field: 'departOrder',
label: '排序',
component: 'InputNumber',
componentProps: {},
},
// {
// field: 'mobile',
// label: '电话',
// component: 'Input',
// componentProps: {
// placeholder: '请输入电话',
// },
// },
// {
// field: 'fax',
// label: '传真',
// component: 'Input',
// componentProps: {
// placeholder: '请输入传真',
// },
// },
// {
// field: 'address',
// label: '地址',
// component: 'Input',
// componentProps: {
// placeholder: '请输入地址',
// },
// },
// {
// field: 'memo',
// label: '备注',
// component: 'InputTextArea',
// componentProps: {
// placeholder: '请输入备注',
// },
// },
{
field: 'id',
label: 'ID',
component: 'Input',
show: false,
},
{
label: '部门类别',
field: 'departType',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'depart_type',
placeholder: '请选择部门类别',
},
},
];
return { basicFormSchema };
}
// 机构类型选项
export const orgCategoryOptions = {
// 一级部门
root: [{ value: '1', label: '公司' }],
// 子级部门
child: [
{ value: '2', label: '部门' },
{ value: '3', label: '岗位' },
],
};
@@ -0,0 +1,14 @@
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-depart-manage';
.@{prefix-cls} {
// update-begin-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
background: @component-background;
// update-end-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
&--box {
.ant-tabs-nav {
padding: 0 20px;
}
}
}
@@ -0,0 +1,63 @@
<template>
<a-row :class="['p-4', `${prefixCls}--box`]" type="flex" :gutter="10">
<a-col :xl="12" :lg="24" :md="24" style="margin-bottom: 10px">
<DepartLeftTree ref="leftTree" @select="onTreeSelect" @rootTreeData="onRootTreeData" />
</a-col>
<a-col :xl="12" :lg="24" :md="24" style="margin-bottom: 10px">
<div style="height: 100%;" :class="[`${prefixCls}`]">
<a-tabs v-show="departData != null" defaultActiveKey="base-info">
<a-tab-pane tab="基本信息" key="base-info" forceRender style="position: relative">
<div style="padding: 20px">
<DepartFormTab :data="departData" :rootTreeData="rootTreeData" @success="onSuccess" />
</div>
</a-tab-pane>
<!-- <a-tab-pane tab="部门权限" key="role-info">-->
<!-- <div style="padding: 0 20px 20px">-->
<!-- <DepartRuleTab :data="departData" />-->
<!-- </div>-->
<!-- </a-tab-pane>-->
</a-tabs>
<div v-show="departData == null" style="padding-top: 40px">
<a-empty description="尚未选择部门" />
</div>
</div>
</a-col>
</a-row>
</template>
<script lang="ts" setup name="system-depart">
import { provide, ref } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import DepartLeftTree from './components/DepartLeftTree.vue';
import DepartFormTab from './components/DepartFormTab.vue';
import DepartRuleTab from './components/DepartRuleTab.vue';
const { prefixCls } = useDesign('depart-manage');
provide('prefixCls', prefixCls);
// 给子组件定义一个ref变量
const leftTree = ref();
// 当前选中的部门信息
const departData = ref({});
const rootTreeData = ref<any[]>([]);
// 左侧树选择后触发
function onTreeSelect(data) {
console.log('onTreeSelect: ', data);
departData.value = data;
}
// 左侧树rootTreeData触发
function onRootTreeData(data) {
rootTreeData.value = data;
}
function onSuccess() {
leftTree.value.loadRootTreeData();
}
</script>
<style lang="less">
@import './index.less';
</style>
@@ -0,0 +1,42 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="修改密码" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" name="PassWordModal" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formPasswordSchema } from './user.data';
import { changePassword } from './user.api';
// 声明Emits
const emit = defineEmits(['success', 'register']);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
schemas: formPasswordSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
//表单赋值
await setFieldsValue({ ...data });
});
//表单提交事件
async function handleSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await changePassword(values);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
@@ -0,0 +1,45 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :width="800" title="用户代理" @ok="handleSubmit" destroyOnClose>
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formAgentSchema } from './user.data';
import { getUserAgent, saveOrUpdateAgent } from './user.api';
// 声明Emits
const emit = defineEmits(['success', 'register']);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
schemas: formAgentSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
//查询获取表单数据
const res = await getUserAgent({ userName: data.userName });
data = res.result ? res.result : data;
//表单赋值
await setFieldsValue({ ...data });
});
//表单提交事件
async function handleSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdateAgent(values);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
@@ -0,0 +1,178 @@
<template>
<BasicDrawer
v-bind="$attrs"
@register="registerDrawer"
:title="getTitle"
:width="adaptiveWidth"
@ok="handleSubmit"
:showFooter="showFooter"
destroyOnClose
>
<BasicForm @register="registerForm" />
</BasicDrawer>
</template>
<script lang="ts" setup>
import { defineComponent, ref, computed, unref, useAttrs } from 'vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from './user.data';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { saveOrUpdateUser, getUserRoles, getUserDepartList, getAllRolesListNoByTenant, getAllRolesList } from './user.api';
import { useDrawerAdaptiveWidth } from '/@/hooks/jeecg/useAdaptiveWidth';
import { getTenantId } from "/@/utils/auth";
// 声明Emits
const emit = defineEmits(['success', 'register']);
const attrs = useAttrs();
const isUpdate = ref(true);
const rowId = ref('');
const departOptions = ref([]);
let isFormDepartUser = false;
//表单配置
const [registerForm, { setProps, resetFields, setFieldsValue, validate, updateSchema }] = useForm({
labelWidth: 90,
schemas: formSchema,
showActionButtonGroup: false,
});
// TODO [VUEN-527] https://www.teambition.com/task/6239beb894b358003fe93626
const showFooter = ref(true);
//表单赋值
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
await resetFields();
showFooter.value = data?.showFooter ?? true;
setDrawerProps({ confirmLoading: false, showFooter: showFooter.value });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
rowId.value = data.record.id;
//租户信息定义成数组
/* if (data.record.relTenantIds && !Array.isArray(data.record.relTenantIds)) {
data.record.relTenantIds = data.record.relTenantIds.split(',');
} else {
//【issues/I56C5I】用户管理中连续点两次编辑租户配置就丢失了
//data.record.relTenantIds = [];
}*/
//查角色/赋值/try catch 处理,不然编辑有问题
try {
const userRoles = await getUserRoles({ userid: data.record.id });
if (userRoles && userRoles.length > 0) {
data.record.selectedroles = userRoles;
}
} catch (error) {}
//查所属部门/赋值
const userDepart = await getUserDepartList({ userId: data.record.id });
if (userDepart && userDepart.length > 0) {
data.record.selecteddeparts = userDepart;
let selectDepartKeys = Array.from(userDepart, ({ key }) => key);
data.record.selecteddeparts = selectDepartKeys.join(',');
departOptions.value = userDepart.map((item) => {
return { label: item.title, value: item.key };
});
}
//负责部门/赋值
data.record.departIds && !Array.isArray(data.record.departIds) && (data.record.departIds = data.record.departIds.split(','));
//update-begin---author:zyf Date:20211210 for:避免空值显示异常------------
//update-begin---author:liusq Date:20231008 for[issues/772]避免空值显示异常------------
data.record.departIds = (!data.record.departIds || data.record.departIds == '') ? [] : data.record.departIds;
//update-end-----author:liusq Date:20231008 for[issues/772]避免空值显示异常------------
//update-begin---author:zyf Date:20211210 for:避免空值显示异常------------
}
//处理角色用户列表情况(和角色列表有关系)
data.selectedroles && (await setFieldsValue({ selectedroles: data.selectedroles }));
// -update-begin--author:liaozhiyang---date:20240702---for:【TV360X-1737】部门用户编辑接口,增加参数updateFromPage:"deptUsers"
isFormDepartUser = data?.departDisabled === true ? true : false;
// -update-end--author:liaozhiyang---date:20240702---for:【TV360X-1737】部门用户编辑接口,增加参数updateFromPage:"deptUsers"
//编辑时隐藏密码/角色列表隐藏角色信息/我的部门时隐藏所属部门
updateSchema([
{
field: 'password',
// 【QQYUN-8324】
ifShow: !unref(isUpdate),
},
{
field: 'confirmPassword',
ifShow: !unref(isUpdate),
},
{
field: 'selectedroles',
show: !data.isRole,
},
{
field: 'departIds',
componentProps: { options: departOptions },
},
{
field: 'selecteddeparts',
show: !data?.departDisabled,
},
{
field: 'selectedroles',
show: !data?.departDisabled,
//update-begin---author:wangshuai ---date:20230424 for:【issues/4844】多租户模式下,新增或编辑用户,选择角色一栏,角色选项没有做租户隔离------------
//判断是否为多租户模式
componentProps:{
api: data.tenantSaas?getAllRolesList:getAllRolesListNoByTenant
}
//update-end---author:wangshuai ---date:20230424 for:【issues/4844】多租户模式下,新增或编辑用户,选择角色一栏,角色选项没有做租户隔离------------
},
//update-begin---author:wangshuai ---date:20230522 for:【issues/4935】租户用户编辑界面中租户下拉框未过滤,显示当前系统所有的租户------------
{
field: 'relTenantIds',
componentProps:{
disabled: !!data.tenantSaas,
},
},
//update-end---author:wangshuai ---date:20230522 for:【issues/4935】租户用户编辑界面中租户下拉框未过滤,显示当前系统所有的租户------------
]);
//update-begin---author:wangshuai ---date:20230522 for:【issues/4935】租户用户编辑界面中租户下拉框未过滤,显示当前系统所有的租户------------
if(!unref(isUpdate) && data.tenantSaas){
await setFieldsValue({ relTenantIds: getTenantId().toString() })
}
//update-end---author:wangshuai ---date:20230522 for:【issues/4935】租户用户编辑界面中租户下拉框未过滤,显示当前系统所有的租户------------
// 无论新增还是编辑,都可以设置表单值
if (typeof data.record === 'object') {
setFieldsValue({
...data.record,
});
}
// 隐藏底部时禁用整个表单
//update-begin-author:taoyan date:2022-5-24 for: VUEN-1117【issue】0523周开源问题
setProps({ disabled: !showFooter.value });
//update-end-author:taoyan date:2022-5-24 for: VUEN-1117【issue】0523周开源问题
});
//获取标题
const getTitle = computed(() => {
// update-begin--author:liaozhiyang---date:20240306---for:【QQYUN-8389】系统用户详情抽屉title更改
if (!unref(isUpdate)) {
return '新增用户';
} else {
return unref(showFooter) ? '编辑用户' : '用户详情';
}
// update-end--author:liaozhiyang---date:20240306---for:【QQYUN-8389】系统用户详情抽屉title更改
});
const { adaptiveWidth } = useDrawerAdaptiveWidth();
//提交事件
async function handleSubmit() {
try {
let values = await validate();
setDrawerProps({ confirmLoading: true });
values.userIdentity === 1 && (values.departIds = '');
let isUpdateVal = unref(isUpdate);
// -update-begin--author:liaozhiyang---date:20240702---for:【TV360X-1737】部门用户编辑接口,增加参数updateFromPage:"deptUsers"
let params = values;
if (isFormDepartUser) {
params = { ...params, updateFromPage: 'deptUsers' };
}
// -update-end--author:liaozhiyang---date:20240702---for:【TV360X-1737】部门用户编辑接口,增加参数updateFromPage:"deptUsers"
//提交表单
await saveOrUpdateUser(params, isUpdateVal);
//关闭弹窗
closeDrawer();
//刷新列表
emit('success',{isUpdateVal ,values});
} finally {
setDrawerProps({ confirmLoading: false });
}
}
</script>
@@ -0,0 +1,81 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :width="800" title="离职交接" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup name="user-quit-agent-modal">
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formQuitAgentSchema } from './user.data';
import { getUserAgent, userQuitAgent } from './user.api';
import dayjs from 'dayjs';
// 声明Emits
const emit = defineEmits(['success', 'register']);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate, clearValidate, updateSchema }] = useForm({
schemas: formQuitAgentSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: true });
let userId = data.userId;
//查询获取表单数据
const res = await getUserAgent({ userName: data.userName });
data = res.result ? res.result : data;
let date = new Date();
if (!data.startTime) {
data.startTime = dayjs(date).format('YYYY-MM-DD HH:mm:ss');
}
if (!data.endTime) {
data.endTime = getYear(date);
}
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
await updateSchema(
[{
field:'agentUserName',
componentProps:{
excludeUserIdList:[userId]
}
}]
)
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
//表单赋值
await setFieldsValue({ ...data });
setModalProps({ confirmLoading: false });
});
//表单提交事件
async function handleSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await userQuitAgent(values);
//关闭弹窗
closeModal();
//刷新列表
emit('success',values.userName);
} finally {
setModalProps({ confirmLoading: false });
}
}
/**
* 获取后30年
*/
function getYear(date) {
//update-begin---author:wangshuai ---date:20221207 for[QQYUN-3285]交接人设置 结束时间有问题------------
//这是一个数值
let y = date.getFullYear() + 30;
let m = dayjs(date).format('MM');
let d = dayjs(date).format('DD');
let hour = dayjs(date).format('HH:mm:ss');
console.log('年月日', y + '-' + m + '-' + d);
return dayjs(y + '-' + m + '-' + d + ' ' + hour).format('YYYY-MM-DD HH:mm:ss');
//update-end---author:wangshuai ---date:20221207 for[QQYUN-3285]交接人设置 结束时间有问题--------------
}
</script>
@@ -0,0 +1,110 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="离职人员信息" :showOkBtn="false" width="1000px" destroyOnClose>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleRevert">
<Icon icon="ant-design:redo-outlined"></Icon>
批量取消
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="ant-design:down-outlined"></Icon>
</a-button>
</a-dropdown>
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
</BasicModal>
</template>
<script lang="ts" setup name="user-quit-modal">
import { ref, toRaw, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { recycleColumns } from './user.data';
import { getQuitList, putCancelQuit, deleteRecycleBin } from './user.api';
import { useMessage } from '/@/hooks/web/useMessage';
import { useListPage } from '/@/hooks/system/useListPage';
import { Modal } from 'ant-design-vue';
import { defHttp } from '/@/utils/http/axios';
const { createConfirm } = useMessage();
// 声明Emits
const emit = defineEmits(['success', 'register']);
const checkedKeys = ref<Array<string | number>>([]);
const [registerModal] = useModalInner(() => {
checkedKeys.value = [];
});
//注册table数据
const { prefixCls, tableContext } = useListPage({
tableProps: {
api: getQuitList,
columns: recycleColumns,
rowKey: 'id',
canResize: false,
useSearchForm: false,
actionColumn: {
width: 120,
},
},
});
//注册table数据
const [registerTable, { reload }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
/**
* 取消离职事件
* @param record
*/
async function handleCancelQuit(record) {
await putCancelQuit({ userIds: record.id, usernames: record.username }, reload);
emit('success');
}
/**
* 批量取消离职事件
*/
function batchHandleRevert() {
Modal.confirm({
title: '取消离职',
content: '取消离职交接人也会清空',
okText: '确认',
cancelText: '取消',
onOk: () => {
let rowValue = selectedRows.value;
let rowData: any = [];
for (const value of rowValue) {
rowData.push(value.username);
}
handleCancelQuit({ id: toRaw(unref(selectedRowKeys)).join(','), username: rowData.join(',') });
},
});
}
//获取操作栏事件
function getTableAction(record) {
return [
{
label: '取消离职',
icon: 'ant-design:redo-outlined',
popConfirm: {
title: '是否取消离职,取消离职交接人也会清空',
confirm: handleCancelQuit.bind(null, record),
},
},
];
}
</script>
<style scoped lang="less">
:deep(.ant-popover-inner-content){
width: 185px !important;
}
</style>
@@ -0,0 +1,166 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="用户回收站" :showOkBtn="false" width="1000px" destroyOnClose @fullScreen="handleFullScreen">
<BasicTable @register="registerTable" :rowSelection="rowSelection" :scroll="scroll">
<!--插槽:table标题-->
<template #tableTitle>
<a-dropdown v-if="checkedKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
批量删除
</a-menu-item>
<a-menu-item key="1" @click="batchHandleRevert">
<Icon icon="ant-design:redo-outlined"></Icon>
批量还原
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="ant-design:down-outlined"></Icon>
</a-button>
</a-dropdown>
</template>
<!--操作栏-->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<TableAction :actions="getTableAction(record)" />
</template>
</template>
</BasicTable>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, toRaw, unref, watch } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { recycleColumns } from './user.data';
import { getRecycleBinList, putRecycleBin, deleteRecycleBin } from './user.api';
import { useMessage } from '/@/hooks/web/useMessage';
const { createConfirm } = useMessage();
// 声明Emits
const emit = defineEmits(['success', 'register']);
const checkedKeys = ref<Array<string | number>>([]);
const [registerModal] = useModalInner(() => {
checkedKeys.value = [];
});
const scroll = ref({ y: 0 });
//注册table数据
const [registerTable, { reload }] = useTable({
api: getRecycleBinList,
columns: recycleColumns,
rowKey: 'id',
striped: true,
useSearchForm: false,
showTableSetting: false,
clickToRowSelect: false,
bordered: true,
showIndexColumn: false,
pagination: true,
tableSetting: { fullScreen: true },
canResize: false,
actionColumn: {
width: 150,
title: '操作',
dataIndex: 'action',
// slots: { customRender: 'action' },
fixed: undefined,
},
});
// update-begin--author:liaozhiyang---date:20240704---for:【TV360X-1657】系统用户回收站弹窗分页展示在可视区内
const handleFullScreen = (maximize) => {
setTableHeight(maximize);
};
const setTableHeight = (maximize) => {
const clientHeight = document.documentElement.clientHeight;
scroll.value = {
y: clientHeight - (maximize ? 300 : 500),
};
};
setTableHeight(false);
watch(
checkedKeys,
(newValue, oldValue) => {
if (checkedKeys.value.length && oldValue.length == 0) {
scroll.value = {
y: scroll.value.y - 50,
};
} else if (checkedKeys.value.length == 0 && oldValue.length) {
scroll.value = {
y: scroll.value.y + 50,
};
}
},
{ deep: true }
);
// update-end--author:liaozhiyang---date:20240704---for:【TV360X-1657】系统用户回收站弹窗分页展示在可视区内
/**
* 选择列配置
*/
const rowSelection = {
type: 'checkbox',
columnWidth: 50,
selectedRowKeys: checkedKeys,
onChange: onSelectChange,
};
/**
* 选择事件
*/
function onSelectChange(selectedRowKeys: (string | number)[]) {
checkedKeys.value = selectedRowKeys;
}
/**
* 还原事件
*/
async function handleRevert(record) {
await putRecycleBin({ userIds: record.id }, reload);
emit('success');
}
/**
* 批量还原事件
*/
function batchHandleRevert() {
handleRevert({ id: toRaw(unref(checkedKeys)).join(',') });
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteRecycleBin({ userIds: record.id }, reload);
}
/**
* 批量删除事件
*/
function batchHandleDelete() {
createConfirm({
iconType: 'warning',
title: '删除',
content: '确定要永久删除吗?删除后将不可恢复!',
onOk: () => handleDelete({ id: toRaw(unref(checkedKeys)).join(',') }),
onCancel() {},
});
}
//获取操作栏事件
function getTableAction(record) {
return [
{
label: '取回',
icon: 'ant-design:redo-outlined',
popConfirm: {
title: '是否确认还原',
confirm: handleRevert.bind(null, record),
},
},
{
label: '彻底删除',
icon: 'ant-design:scissor-outlined',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
</script>
@@ -0,0 +1,299 @@
<template>
<div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate"> 新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls" :disabled="isDisabledAuth('system:user:export')"> 导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-button type="primary" @click="openModal(true, {})" preIcon="ant-design:hdd-outlined"> 回收站</a-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
<a-menu-item key="2" @click="batchFrozen(2)">
<Icon icon="ant-design:lock-outlined"></Icon>
冻结
</a-menu-item>
<a-menu-item key="3" @click="batchFrozen(1)">
<Icon icon="ant-design:unlock-outlined"></Icon>
解冻
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<!--用户抽屉-->
<UserDrawer @register="registerDrawer" @success="handleSuccess" />
<!--修改密码-->
<PasswordModal @register="registerPasswordModal" @success="reload" />
<!--用户代理-->
<UserAgentModal @register="registerAgentModal" @success="reload" />
<!--回收站-->
<UserRecycleBinModal @register="registerModal" @success="reload" />
<!-- 离职受理人弹窗 -->
<UserQuitAgentModal @register="registerQuitAgentModal" @success="reload" />
<!-- 离职人员列弹窗 -->
<UserQuitModal @register="registerQuitModal" @success="reload" />
</div>
</template>
<script lang="ts" name="system-user" setup>
//ts语法
import { ref, computed, unref } from 'vue';
import { BasicTable, TableAction, ActionItem } from '/@/components/Table';
import UserDrawer from './UserDrawer.vue';
import UserRecycleBinModal from './UserRecycleBinModal.vue';
import PasswordModal from './PasswordModal.vue';
import UserAgentModal from './UserAgentModal.vue';
import JThirdAppButton from '/@/components/jeecg/thirdApp/JThirdAppButton.vue';
import UserQuitAgentModal from './UserQuitAgentModal.vue';
import UserQuitModal from './UserQuitModal.vue';
import { useDrawer } from '/@/components/Drawer';
import { useListPage } from '/@/hooks/system/useListPage';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { columns, searchFormSchema } from './user.data';
import { listNoCareTenant, deleteUser, batchDeleteUser, getImportUrl, getExportUrl, frozenBatch } from './user.api';
import {usePermission} from "/@/hooks/web/usePermission";
const { createMessage, createConfirm } = useMessage();
const { isDisabledAuth } = usePermission();
//注册drawer
const [registerDrawer, { openDrawer }] = useDrawer();
//回收站model
const [registerModal, { openModal }] = useModal();
//密码model
const [registerPasswordModal, { openModal: openPasswordModal }] = useModal();
//代理人model
const [registerAgentModal, { openModal: openAgentModal }] = useModal();
//离职代理人model
const [registerQuitAgentModal, { openModal: openQuitAgentModal }] = useModal();
//离职用户列表model
const [registerQuitModal, { openModal: openQuitModal }] = useModal();
// 列表页面公共参数、方法
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
designScope: 'user-list',
tableProps: {
title: '用户列表',
api: listNoCareTenant,
columns: columns,
size: 'small',
formConfig: {
// labelWidth: 200,
schemas: searchFormSchema,
},
actionColumn: {
width: 120,
},
beforeFetch: (params) => {
return Object.assign({ column: 'createTime', order: 'desc' }, params);
},
},
exportConfig: {
name: '用户列表',
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
},
});
//注册table数据
const [registerTable, { reload, updateTableDataRecord }, { rowSelection, selectedRows, selectedRowKeys }] = tableContext;
/**
* 新增事件
*/
function handleCreate() {
openDrawer(true, {
isUpdate: false,
showFooter: true,
tenantSaas: false,
});
}
/**
* 编辑事件
*/
async function handleEdit(record: Recordable) {
openDrawer(true, {
record,
isUpdate: true,
showFooter: true,
tenantSaas: false,
});
}
/**
* 详情
*/
async function handleDetail(record: Recordable) {
openDrawer(true, {
record,
isUpdate: true,
showFooter: false,
tenantSaas: false,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
if ('admin' == record.username) {
createMessage.warning('管理员账号不允许此操作!');
return;
}
await deleteUser({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
let hasAdmin = unref(selectedRows).filter((item) => item.username == 'admin');
if (unref(hasAdmin).length > 0) {
createMessage.warning('管理员账号不允许此操作!');
return;
}
await batchDeleteUser({ ids: selectedRowKeys.value }, () => {
selectedRowKeys.value = [];
reload();
});
}
/**
* 成功回调
*/
function handleSuccess() {
reload();
}
/**
* 打开修改密码弹窗
*/
function handleChangePassword(username) {
openPasswordModal(true, { username });
}
/**
* 打开代理人弹窗
*/
function handleAgentSettings(userName) {
openAgentModal(true, { userName });
}
/**
* 冻结解冻
*/
async function handleFrozen(record, status) {
if ('admin' == record.username) {
createMessage.warning('管理员账号不允许此操作!');
return;
}
await frozenBatch({ ids: record.id, status: status }, reload);
}
/**
* 批量冻结解冻
*/
function batchFrozen(status) {
let hasAdmin = selectedRows.value.filter((item) => item.username == 'admin');
if (unref(hasAdmin).length > 0) {
createMessage.warning('管理员账号不允许此操作!');
return;
}
createConfirm({
iconType: 'warning',
title: '确认操作',
content: '是否' + (status == 1 ? '解冻' : '冻结') + '选中账号?',
onOk: async () => {
await frozenBatch({ ids: unref(selectedRowKeys).join(','), status: status }, reload);
},
});
}
/**
*同步钉钉和微信回调
*/
function onSyncFinally({ isToLocal }) {
// 同步到本地时刷新下数据
if (isToLocal) {
reload();
}
}
/**
* 操作栏
*/
function getTableAction(record): ActionItem[] {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
// ifShow: () => hasPermission('system:user:edit'),
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record): ActionItem[] {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '密码',
//auth: 'user:changepwd',
onClick: handleChangePassword.bind(null, record.username),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
{
label: '冻结',
ifShow: record.status == 1,
popConfirm: {
title: '确定冻结吗?',
confirm: handleFrozen.bind(null, record, 2),
},
},
{
label: '解冻',
ifShow: record.status == 2,
popConfirm: {
title: '确定解冻吗?',
confirm: handleFrozen.bind(null, record, 1),
},
},
{
label: '代理人',
onClick: handleAgentSettings.bind(null, record.username),
},
];
}
/**
* 离职
* @param userName
*/
function handleQuit(userName) {
//打开离职代理人弹窗
openQuitAgentModal(true, { userName });
}
</script>
<style scoped></style>
@@ -0,0 +1,251 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
import { isObject } from '/@/utils/is';
enum Api {
listNoCareTenant = '/sys/user/listAllExcludeAdmin',
list = '/sys/user/list',
save = '/sys/user/add',
edit = '/sys/user/edit',
agentSave = '/sys/sysUserAgent/add',
agentEdit = '/sys/sysUserAgent/edit',
getUserRole = '/sys/user/queryUserRole',
duplicateCheck = '/sys/duplicate/check',
deleteUser = '/sys/user/delete',
deleteBatch = '/sys/user/deleteBatch',
importExcel = '/sys/user/importExcel',
exportXls = '/sys/user/exportXls',
recycleBinList = '/sys/user/recycleBin',
putRecycleBin = '/sys/user/putRecycleBin',
deleteRecycleBin = '/sys/user/deleteRecycleBin',
allRolesList = '/sys/role/queryall',
allRolesListNoByTenant = '/sys/role/queryallNoByTenant',
allTenantList = '/sys/tenant/queryList',
allPostList = '/sys/position/list',
userDepartList = '/sys/user/userDepartList',
changePassword = '/sys/user/changePassword',
frozenBatch = '/sys/user/frozenBatch',
getUserAgent = '/sys/sysUserAgent/queryByUserName',
userQuitAgent = '/sys/user/userQuitAgent',
getQuitList = '/sys/user/getQuitList',
putCancelQuit = '/sys/user/putCancelQuit',
updateUserTenantStatus='/sys/tenant/updateUserTenantStatus',
getUserTenantPageList='/sys/tenant/getUserTenantPageList',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口(查询用户,通过租户隔离)
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 列表接口(查询全部用户,不通过租户隔离)
* @param params
*/
export const listNoCareTenant = (params) => defHttp.get({ url: Api.listNoCareTenant, params });
/**
* 用户角色接口
* @param params
*/
export const getUserRoles = (params) => defHttp.get({ url: Api.getUserRole, params }, { errorMessageMode: 'none' });
/**
* 删除用户
*/
export const deleteUser = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteUser, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 批量删除用户
* @param params
*/
export const batchDeleteUser = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 保存或者更新用户
* @param params
*/
export const saveOrUpdateUser = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
/**
* 唯一校验
* @param params
*/
export const duplicateCheck = (params) => defHttp.get({ url: Api.duplicateCheck, params }, { isTransformResponse: false });
/**
* 20231215
* liaozhiyang
* 唯一校验( 延迟【防抖】)
* @param params
*/
const timer = {};
export const duplicateCheckDelay = (params) => {
return new Promise((resove, rejected) => {
// -update-begin--author:liaozhiyang---date:20240619---for:【TV360X-1380】表单中使用多个duplicateCheckDelayvalidate方法调用时会导致promise被挂起保存不了
let key;
if (isObject(params)) {
key = `${params.tableName}_${params.fieldName}`;
} else {
key = params;
}
clearTimeout(timer[key]);
// -update-end--author:liaozhiyang---date:20240619---for:【TV360X-1380】表单中使用多个duplicateCheckDelayvalidate方法调用时会导致promise被挂起保存不了
timer[key] = setTimeout(() => {
defHttp
.get({ url: Api.duplicateCheck, params }, { isTransformResponse: false })
.then((res: any) => {
resove(res as any);
})
.catch((error) => {
rejected(error);
});
delete timer[key];
}, 500);
});
};
/**
* 获取全部角色(租户隔离)
* @param params
*/
export const getAllRolesList = (params) => defHttp.get({ url: Api.allRolesList, params });
/**
* 获取全部角色(不租户隔离)
* @param params
*/
export const getAllRolesListNoByTenant = (params) => defHttp.get({ url: Api.allRolesListNoByTenant, params });
/**
* 获取全部租户
*/
export const getAllTenantList = (params) => defHttp.get({ url: Api.allTenantList, params });
/**
* 获取指定用户负责部门
*/
export const getUserDepartList = (params) => defHttp.get({ url: Api.userDepartList, params }, { successMessageMode: 'none' });
/**
* 获取全部职务
*/
export const getAllPostList = (params) => {
return new Promise((resolve) => {
defHttp.get({ url: Api.allPostList, params }).then((res) => {
resolve(res.records);
});
});
};
/**
* 回收站列表
* @param params
*/
export const getRecycleBinList = (params) => defHttp.get({ url: Api.recycleBinList, params });
/**
* 回收站还原
* @param params
*/
export const putRecycleBin = (params, handleSuccess) => {
return defHttp.put({ url: Api.putRecycleBin, params }).then(() => {
handleSuccess();
});
};
/**
* 回收站删除
* @param params
*/
export const deleteRecycleBin = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteRecycleBin, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 修改密码
* @param params
*/
export const changePassword = (params) => {
return defHttp.put({ url: Api.changePassword, params });
};
/**
* 冻结解冻
* @param params
*/
export const frozenBatch = (params, handleSuccess) => {
return defHttp.put({ url: Api.frozenBatch, params }).then(() => {
handleSuccess();
});
};
/**
* 获取用户代理
* @param params
*/
export const getUserAgent = (params) => defHttp.get({ url: Api.getUserAgent, params }, { isTransformResponse: false });
/**
* 保存或者更新用户代理
* @param params
*/
export const saveOrUpdateAgent = (params) => {
let url = params.id ? Api.agentEdit : Api.agentSave;
return defHttp.post({ url: url, params });
};
/**
* 用户离职(新增代理人和用户状态变更操作)
* @param params
*/
export const userQuitAgent = (params) => {
return defHttp.put({ url: Api.userQuitAgent, params });
};
/**
* 用户离职列表
* @param params
*/
export const getQuitList = (params) => {
return defHttp.get({ url: Api.getQuitList, params });
};
/**
* 取消离职
* @param params
*/
export const putCancelQuit = (params, handleSuccess) => {
return defHttp.put({ url: Api.putCancelQuit, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 待审批获取列表数据
*/
export const getUserTenantPageList = (params) => {
return defHttp.get({ url: Api.getUserTenantPageList, params });
};
/**
* 更新租户状态
* @param params
*/
export const updateUserTenantStatus = (params) => {
return defHttp.put({ url: Api.updateUserTenantStatus, params }, { joinParamsToUrl: true, isTransformResponse: false });
};
@@ -0,0 +1,618 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { getAllRolesListNoByTenant, getAllTenantList } from './user.api';
import { rules } from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
export const columns: BasicColumn[] = [
{
title: '用户账号',
dataIndex: 'username',
width: 120,
},
{
title: '用户姓名',
dataIndex: 'realname',
width: 100,
},
// {
// title: '头像',
// dataIndex: 'avatar',
// width: 120,
// customRender: render.renderAvatar,
// },
{
title: '性别',
dataIndex: 'sex',
width: 80,
sorter: true,
customRender: ({ text }) => {
return render.renderDict(text, 'sex');
},
},
{
title: '生日',
dataIndex: 'birthday',
width: 100,
},
// {
// title: '手机号',
// dataIndex: 'phone',
// width: 100,
// },
{
title: '部门',
width: 150,
dataIndex: 'orgCodeTxt',
},
{
title: '负责部门',
width: 150,
dataIndex: 'departIds_dictText',
},
{
title: '状态',
dataIndex: 'status_dictText',
width: 80,
},
{
title: '密级',
width: 150,
dataIndex: 'userSecurityLevel_dictText',
},
{
title: '是否专项组成员',
width: 150,
dataIndex: 'isSpecial_dictText',
},
];
export const recycleColumns: BasicColumn[] = [
{
title: '用户账号',
dataIndex: 'username',
width: 100,
},
{
title: '用户姓名',
dataIndex: 'realname',
width: 100,
},
// {
// title: '头像',
// dataIndex: 'avatar',
// width: 80,
// customRender: render.renderAvatar,
// },
{
title: '性别',
dataIndex: 'sex',
width: 80,
sorter: true,
customRender: ({ text }) => {
return render.renderDict(text, 'sex');
},
},
];
export const searchFormSchema: FormSchema[] = [
{
label: '账号',
field: 'username',
component: 'JInput',
//colProps: { span: 6 },
},
{
label: '名字',
field: 'realname',
component: 'JInput',
//colProps: { span: 6 },
},
{
label: '性别',
field: 'sex',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'sex',
placeholder: '请选择性别',
stringToNumber: true,
},
//colProps: { span: 6 },
},
// {
// label: '手机号码',
// field: 'phone',
// component: 'Input',
// //colProps: { span: 6 },
// },
{
label: '用户状态',
field: 'status',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'user_status',
placeholder: '请选择状态',
stringToNumber: true,
},
},
//colProps: { span: 6 },
{
label: '密级',
field: 'phone',
component: 'Input',
//colProps: { span: 6 },
},
{
label: '手机号码',
field: 'phone',
component: 'Input',
//colProps: { span: 6 },
},
];
export const formSchema: FormSchema[] = [
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
{
label: '用户账号',
field: 'username',
component: 'Input',
required: true,
dynamicDisabled: ({ values }) => {
return !!values.id;
},
dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_user', 'username', model, schema, true),
},
{
label: '登录密码',
field: 'password',
component: 'StrengthMeter',
componentProps: {
autocomplete: 'new-password',
},
rules: [
{
required: true,
message: '请输入登录密码',
},
{
pattern: /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[~!@#$%^&*()_+`\-={}:";'<>?,./]).{8,}$/,
message: '密码由8位数字、大小写字母和特殊符号组成!',
},
],
},
{
label: '确认密码',
field: 'confirmPassword',
component: 'InputPassword',
dynamicRules: ({ values }) => rules.confirmPassword(values, true),
},
{
label: '用户姓名',
field: 'realname',
required: true,
component: 'Input',
},
{
label: '工号',
field: 'workNo',
required: true,
component: 'Input',
dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_user', 'work_no', model, schema, true),
},
{
label: '职务',
field: 'post',
required: false,
component: 'JSelectPosition',
componentProps: {
labelKey: 'name',
},
},
{
label: '角色',
field: 'selectedroles',
component: 'ApiSelect',
componentProps: {
mode: 'multiple',
api: getAllRolesListNoByTenant,
labelField: 'roleName',
valueField: 'id',
immediate: false,
},
},
{
label: '所属部门',
field: 'selecteddeparts',
component: 'JSelectDept',
componentProps: ({ formActionType, formModel }) => {
return {
sync: false,
checkStrictly: true,
defaultExpandLevel: 2,
onSelect: (options, values) => {
const { updateSchema } = formActionType;
//所属部门修改后更新负责部门下拉框数据
updateSchema([
{
field: 'departIds',
componentProps: { options },
},
]);
//update-begin---author:wangshuai---date:2024-05-11---for:【issues/1222】用户编辑界面“所属部门”与“负责部门”联动出错整---
if (!values) {
formModel.departIds = [];
return;
}
//update-end---author:wangshuai---date:2024-05-11---for:【issues/1222】用户编辑界面“所属部门”与“负责部门”联动出错整---
//所属部门修改后更新负责部门数据
formModel.departIds && (formModel.departIds = formModel.departIds.filter((item) => values.value.indexOf(item) > -1));
},
};
},
},
{
label: '租户',
field: 'relTenantIds',
component: 'JSearchSelect',
componentProps: {
dict: 'sys_tenant,name,id',
async: true,
multiple: true,
},
},
{
label: '身份',
field: 'userIdentity',
component: 'RadioGroup',
defaultValue: 1,
componentProps: ({ formModel }) => {
return {
options: [
{ label: '普通用户', value: 1, key: '1' },
{ label: '上级', value: 2, key: '2' },
],
onChange: () => {
formModel.userIdentity == 1 && (formModel.departIds = []);
},
};
},
},
{
label: '负责部门',
field: 'departIds',
component: 'Select',
componentProps: {
mode: 'multiple',
},
ifShow: ({ values }) => values.userIdentity == 2,
},
// {
// label: '头像',
// field: 'avatar',
// component: 'JImageUpload',
// componentProps: {
// fileMax: 1,
// },
// },
{
label: '生日',
field: 'birthday',
component: 'DatePicker',
},
{
label: '性别',
field: 'sex',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'sex',
placeholder: '请选择性别',
stringToNumber: true,
},
},
// {
// label: '邮箱',
// field: 'email',
// component: 'Input',
// required: true,
// dynamicRules: ({ model, schema }) => {
// return [
// { ...rules.duplicateCheckRule('sys_user', 'email', model, schema, true)[0], trigger: 'blur' },
// { ...rules.rule('email', false)[0], trigger: 'blur' },
// ];
// },
// },
// {
// label: '手机号码',
// field: 'phone',
// component: 'Input',
// required: true,
// dynamicRules: ({ model, schema }) => {
// return [
// { ...rules.duplicateCheckRule('sys_user', 'phone', model, schema, true)[0], trigger: 'blur' },
// { pattern: /^1[3456789]\d{9}$/, message: '手机号码格式有误', trigger: 'blur' },
// ];
// },
// },
{
label: '座机',
field: 'telephone',
component: 'Input',
rules: [{ pattern: /^0\d{2,3}-[1-9]\d{6,7}$/, message: '请输入正确的座机号码' }],
},
{
label: '工作流引擎',
field: 'activitiSync',
defaultValue: 1,
component: 'JDictSelectTag',
componentProps: {
dictCode: 'activiti_sync',
type: 'radio',
stringToNumber: true,
},
},
{
label: '密级',
field: 'user_security_level',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'user_security_level',
placeholder: '请选择密级',
stringToNumber: true,
},
},
{
label: '专项组成员',
field: 'is_special',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'is_special',
placeholder: '请选择是否为专项组成员',
stringToNumber: true,
},
},
];
export const formPasswordSchema: FormSchema[] = [
{
label: '用户账号',
field: 'username',
component: 'Input',
componentProps: { readOnly: true },
},
{
label: '登录密码',
field: 'password',
component: 'StrengthMeter',
componentProps: {
placeholder: '请输入登录密码',
},
rules: [
{
required: true,
message: '请输入登录密码',
},
{
pattern: /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[~!@#$%^&*()_+`\-={}:";'<>?,./]).{8,}$/,
message: '密码由8位数字、大小写字母和特殊符号组成!',
},
],
},
{
label: '确认密码',
field: 'confirmPassword',
component: 'InputPassword',
dynamicRules: ({ values }) => rules.confirmPassword(values, true),
},
];
export const formAgentSchema: FormSchema[] = [
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
{
field: 'userName',
label: '用户名',
component: 'Input',
componentProps: {
readOnly: true,
allowClear: false,
},
},
{
field: 'agentUserName',
label: '代理人用户名',
required: true,
component: 'JSelectUser',
componentProps: {
rowKey: 'username',
labelKey: 'realname',
maxSelectCount: 10,
},
},
{
field: 'startTime',
label: '代理开始时间',
component: 'DatePicker',
required: true,
componentProps: {
showTime: true,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
placeholder: '请选择代理开始时间',
getPopupContainer: () => document.body,
},
},
{
field: 'endTime',
label: '代理结束时间',
component: 'DatePicker',
required: true,
componentProps: {
showTime: true,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
placeholder: '请选择代理结束时间',
getPopupContainer: () => document.body,
},
},
{
field: 'status',
label: '状态',
component: 'JDictSelectTag',
defaultValue: '1',
componentProps: {
dictCode: 'valid_status',
type: 'radioButton',
},
},
];
export const formQuitAgentSchema: FormSchema[] = [
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
{
field: 'userName',
label: '用户名',
component: 'Input',
componentProps: {
readOnly: true,
allowClear: false,
},
},
{
field: 'agentUserName',
label: '交接人员',
//required: true,
component: 'JSelectUser',
componentProps: {
rowKey: 'username',
labelKey: 'realname',
maxSelectCount: 1,
},
},
{
field: 'startTime',
label: '交接开始时间',
component: 'DatePicker',
//required: true,
componentProps: {
showTime: true,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
placeholder: '请选择交接开始时间',
getPopupContainer: () => document.body,
},
},
{
field: 'endTime',
label: '交接结束时间',
component: 'DatePicker',
//required: true,
componentProps: {
showTime: true,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
placeholder: '请选择交接结束时间',
getPopupContainer: () => document.body,
},
},
{
field: 'status',
label: '状态',
component: 'JDictSelectTag',
defaultValue: '1',
componentProps: {
dictCode: 'valid_status',
type: 'radioButton',
},
},
];
//租户用户列表
export const userTenantColumns: BasicColumn[] = [
{
title: '用户账号',
dataIndex: 'username',
width: 120,
},
{
title: '用户姓名',
dataIndex: 'realname',
width: 100,
},
{
title: '头像',
dataIndex: 'avatar',
width: 120,
customRender: render.renderAvatar,
},
{
title: '手机号',
dataIndex: 'phone',
width: 100,
},
{
title: '部门',
width: 150,
dataIndex: 'orgCodeTxt',
},
{
title: '状态',
dataIndex: 'status',
width: 80,
customRender: ({ text }) => {
if (text === '1') {
return '正常';
} else if (text === '3') {
return '审批中';
} else {
return '已拒绝';
}
},
},
{
title: '密级',
width: 150,
dataIndex: 'userSecurityLevel_dictText',
},
{
title: '是否专项组成员',
width: 150,
dataIndex: 'isSpecial_dictText',
},
];
//用户租户搜索表单
export const userTenantFormSchema: FormSchema[] = [
{
label: '账号',
field: 'username',
component: 'Input',
colProps: { span: 6 },
},
{
label: '名字',
field: 'realname',
component: 'Input',
colProps: { span: 6 },
},
{
label: '性别',
field: 'sex',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'sex',
placeholder: '请选择性别',
stringToNumber: true,
},
colProps: { span: 6 },
},
];
@@ -0,0 +1,54 @@
<template>
<Description @register="register" class="mt-4" />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Description, DescItem, useDescription } from '/@/components/Description/index';
const mockData = {
username: 'test',
nickName: 'VB',
age: '123',
phone: '15695909xxx',
email: '190848757@qq.com',
addr: '厦门市思明区',
sex: '男',
certy: '3504256199xxxxxxxxx',
tag: 'orange',
};
const schema: DescItem[] = [
{
field: 'username',
label: '用户名',
},
{
field: 'nickName',
label: '昵称',
render: (curVal, data) => {
return `${data.username}-${curVal}`;
},
},
{
field: 'phone',
label: '联系电话',
},
{
field: 'email',
label: '邮箱',
},
{
field: 'addr',
label: '地址',
},
];
export default defineComponent({
components: { Description },
setup() {
const [register] = useDescription({
title: 'useDescription',
data: mockData,
schema: schema,
});
return { mockData, schema, register };
},
});
</script>
@@ -0,0 +1,42 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="修改密码" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" name="PassWordModal" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formPasswordSchema } from './user.data';
import { changePassword } from './user.api';
// 声明Emits
const emit = defineEmits(['success', 'register']);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
schemas: formPasswordSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
//表单赋值
await setFieldsValue({ ...data });
});
//表单提交事件
async function handleSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await changePassword(values);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
@@ -0,0 +1,45 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :width="800" title="用户代理" @ok="handleSubmit" destroyOnClose>
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formAgentSchema } from './user.data';
import { getUserAgent, saveOrUpdateAgent } from './user.api';
// 声明Emits
const emit = defineEmits(['success', 'register']);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
schemas: formAgentSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
//查询获取表单数据
const res = await getUserAgent({ userName: data.userName });
data = res.result ? res.result : data;
//表单赋值
await setFieldsValue({ ...data });
});
//表单提交事件
async function handleSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdateAgent(values);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
@@ -0,0 +1,178 @@
<template>
<BasicDrawer
v-bind="$attrs"
@register="registerDrawer"
:title="getTitle"
:width="adaptiveWidth"
@ok="handleSubmit"
:showFooter="showFooter"
destroyOnClose
>
<BasicForm @register="registerForm" />
</BasicDrawer>
</template>
<script lang="ts" setup>
import { defineComponent, ref, computed, unref, useAttrs } from 'vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from './user.data';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { saveOrUpdateUser, getUserRoles, getUserDepartList, getAllRolesListNoByTenant, getAllRolesList } from './user.api';
import { useDrawerAdaptiveWidth } from '/@/hooks/jeecg/useAdaptiveWidth';
import { getTenantId } from "/@/utils/auth";
// 声明Emits
const emit = defineEmits(['success', 'register']);
const attrs = useAttrs();
const isUpdate = ref(true);
const rowId = ref('');
const departOptions = ref([]);
let isFormDepartUser = false;
//表单配置
const [registerForm, { setProps, resetFields, setFieldsValue, validate, updateSchema }] = useForm({
labelWidth: 90,
schemas: formSchema,
showActionButtonGroup: false,
});
// TODO [VUEN-527] https://www.teambition.com/task/6239beb894b358003fe93626
const showFooter = ref(true);
//表单赋值
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
await resetFields();
showFooter.value = data?.showFooter ?? true;
setDrawerProps({ confirmLoading: false, showFooter: showFooter.value });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
rowId.value = data.record.id;
//租户信息定义成数组
/* if (data.record.relTenantIds && !Array.isArray(data.record.relTenantIds)) {
data.record.relTenantIds = data.record.relTenantIds.split(',');
} else {
//【issues/I56C5I】用户管理中连续点两次编辑租户配置就丢失了
//data.record.relTenantIds = [];
}*/
//查角色/赋值/try catch 处理,不然编辑有问题
try {
const userRoles = await getUserRoles({ userid: data.record.id });
if (userRoles && userRoles.length > 0) {
data.record.selectedroles = userRoles;
}
} catch (error) {}
//查所属部门/赋值
const userDepart = await getUserDepartList({ userId: data.record.id });
if (userDepart && userDepart.length > 0) {
data.record.selecteddeparts = userDepart;
let selectDepartKeys = Array.from(userDepart, ({ key }) => key);
data.record.selecteddeparts = selectDepartKeys.join(',');
departOptions.value = userDepart.map((item) => {
return { label: item.title, value: item.key };
});
}
//负责部门/赋值
data.record.departIds && !Array.isArray(data.record.departIds) && (data.record.departIds = data.record.departIds.split(','));
//update-begin---author:zyf Date:20211210 for:避免空值显示异常------------
//update-begin---author:liusq Date:20231008 for[issues/772]避免空值显示异常------------
data.record.departIds = (!data.record.departIds || data.record.departIds == '') ? [] : data.record.departIds;
//update-end-----author:liusq Date:20231008 for[issues/772]避免空值显示异常------------
//update-begin---author:zyf Date:20211210 for:避免空值显示异常------------
}
//处理角色用户列表情况(和角色列表有关系)
data.selectedroles && (await setFieldsValue({ selectedroles: data.selectedroles }));
// -update-begin--author:liaozhiyang---date:20240702---for:【TV360X-1737】部门用户编辑接口,增加参数updateFromPage:"deptUsers"
isFormDepartUser = data?.departDisabled === true ? true : false;
// -update-end--author:liaozhiyang---date:20240702---for:【TV360X-1737】部门用户编辑接口,增加参数updateFromPage:"deptUsers"
//编辑时隐藏密码/角色列表隐藏角色信息/我的部门时隐藏所属部门
updateSchema([
{
field: 'password',
// 【QQYUN-8324】
ifShow: !unref(isUpdate),
},
{
field: 'confirmPassword',
ifShow: !unref(isUpdate),
},
{
field: 'selectedroles',
show: !data.isRole,
},
{
field: 'departIds',
componentProps: { options: departOptions },
},
{
field: 'selecteddeparts',
show: !data?.departDisabled,
},
{
field: 'selectedroles',
show: !data?.departDisabled,
//update-begin---author:wangshuai ---date:20230424 for:【issues/4844】多租户模式下,新增或编辑用户,选择角色一栏,角色选项没有做租户隔离------------
//判断是否为多租户模式
componentProps:{
api: data.tenantSaas?getAllRolesList:getAllRolesListNoByTenant
}
//update-end---author:wangshuai ---date:20230424 for:【issues/4844】多租户模式下,新增或编辑用户,选择角色一栏,角色选项没有做租户隔离------------
},
//update-begin---author:wangshuai ---date:20230522 for:【issues/4935】租户用户编辑界面中租户下拉框未过滤,显示当前系统所有的租户------------
{
field: 'relTenantIds',
componentProps:{
disabled: !!data.tenantSaas,
},
},
//update-end---author:wangshuai ---date:20230522 for:【issues/4935】租户用户编辑界面中租户下拉框未过滤,显示当前系统所有的租户------------
]);
//update-begin---author:wangshuai ---date:20230522 for:【issues/4935】租户用户编辑界面中租户下拉框未过滤,显示当前系统所有的租户------------
if(!unref(isUpdate) && data.tenantSaas){
await setFieldsValue({ relTenantIds: getTenantId().toString() })
}
//update-end---author:wangshuai ---date:20230522 for:【issues/4935】租户用户编辑界面中租户下拉框未过滤,显示当前系统所有的租户------------
// 无论新增还是编辑,都可以设置表单值
if (typeof data.record === 'object') {
setFieldsValue({
...data.record,
});
}
// 隐藏底部时禁用整个表单
//update-begin-author:taoyan date:2022-5-24 for: VUEN-1117【issue】0523周开源问题
setProps({ disabled: !showFooter.value });
//update-end-author:taoyan date:2022-5-24 for: VUEN-1117【issue】0523周开源问题
});
//获取标题
const getTitle = computed(() => {
// update-begin--author:liaozhiyang---date:20240306---for:【QQYUN-8389】系统用户详情抽屉title更改
if (!unref(isUpdate)) {
return '新增用户';
} else {
return unref(showFooter) ? '编辑用户' : '用户详情';
}
// update-end--author:liaozhiyang---date:20240306---for:【QQYUN-8389】系统用户详情抽屉title更改
});
const { adaptiveWidth } = useDrawerAdaptiveWidth();
//提交事件
async function handleSubmit() {
try {
let values = await validate();
setDrawerProps({ confirmLoading: true });
values.userIdentity === 1 && (values.departIds = '');
let isUpdateVal = unref(isUpdate);
// -update-begin--author:liaozhiyang---date:20240702---for:【TV360X-1737】部门用户编辑接口,增加参数updateFromPage:"deptUsers"
let params = values;
if (isFormDepartUser) {
params = { ...params, updateFromPage: 'deptUsers' };
}
// -update-end--author:liaozhiyang---date:20240702---for:【TV360X-1737】部门用户编辑接口,增加参数updateFromPage:"deptUsers"
//提交表单
await saveOrUpdateUser(params, isUpdateVal);
//关闭弹窗
closeDrawer();
//刷新列表
emit('success',{isUpdateVal ,values});
} finally {
setDrawerProps({ confirmLoading: false });
}
}
</script>
@@ -0,0 +1,81 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :width="800" title="离职交接" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup name="user-quit-agent-modal">
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formQuitAgentSchema } from './user.data';
import { getUserAgent, userQuitAgent } from './user.api';
import dayjs from 'dayjs';
// 声明Emits
const emit = defineEmits(['success', 'register']);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate, clearValidate, updateSchema }] = useForm({
schemas: formQuitAgentSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: true });
let userId = data.userId;
//查询获取表单数据
const res = await getUserAgent({ userName: data.userName });
data = res.result ? res.result : data;
let date = new Date();
if (!data.startTime) {
data.startTime = dayjs(date).format('YYYY-MM-DD HH:mm:ss');
}
if (!data.endTime) {
data.endTime = getYear(date);
}
//update-begin---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
await updateSchema(
[{
field:'agentUserName',
componentProps:{
excludeUserIdList:[userId]
}
}]
)
//update-end---author:wangshuai ---date:20230703 for:【QQYUN-5685】5、离职人员可以选自己------------
//表单赋值
await setFieldsValue({ ...data });
setModalProps({ confirmLoading: false });
});
//表单提交事件
async function handleSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await userQuitAgent(values);
//关闭弹窗
closeModal();
//刷新列表
emit('success',values.userName);
} finally {
setModalProps({ confirmLoading: false });
}
}
/**
* 获取后30年
*/
function getYear(date) {
//update-begin---author:wangshuai ---date:20221207 for[QQYUN-3285]交接人设置 结束时间有问题------------
//这是一个数值
let y = date.getFullYear() + 30;
let m = dayjs(date).format('MM');
let d = dayjs(date).format('DD');
let hour = dayjs(date).format('HH:mm:ss');
console.log('年月日', y + '-' + m + '-' + d);
return dayjs(y + '-' + m + '-' + d + ' ' + hour).format('YYYY-MM-DD HH:mm:ss');
//update-end---author:wangshuai ---date:20221207 for[QQYUN-3285]交接人设置 结束时间有问题--------------
}
</script>

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