This commit is contained in:
2025-08-07 09:12:03 +08:00
commit 7a3af012b4
356 changed files with 27999 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta?.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta?.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script setup lang="ts">
import { RouteLocationMatched } from 'vue-router';
import { usePermissionStore } from '@/store/modules/permission';
const route = useRoute();
const router = useRouter();
const permissionStore = usePermissionStore();
const levelList = ref<RouteLocationMatched[]>([]);
const getBreadcrumb = () => {
// only show routes with meta.title
let matched = [];
const pathNum = findPathNum(route.path);
// multi-level menu
if (pathNum > 2) {
const reg = /\/\w+/gi;
const pathList = route.path.match(reg).map((item, index) => {
if (index !== 0) item = item.slice(1);
return item;
});
getMatched(pathList, permissionStore.defaultRoutes, matched);
} else {
matched = route.matched.filter((item) => item.meta && item.meta.title);
}
// 判断是否为首页
if (!isDashboard(matched[0])) {
matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched);
}
levelList.value = matched.filter((item) => item.meta && item.meta.title && item.meta.breadcrumb !== false);
};
const findPathNum = (str, char = '/') => {
if (typeof str !== 'string' || str.length === 0) return 0;
return str.split(char).length - 1;
};
const getMatched = (pathList, routeList, matched) => {
const data = routeList.find((item) => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0]);
if (data) {
matched.push(data);
if (data.children && pathList.length) {
pathList.shift();
getMatched(pathList, data.children, matched);
}
}
};
const isDashboard = (route: RouteLocationMatched) => {
const name = route && (route.name as string);
if (!name) {
return false;
}
return name.trim() === 'Index';
};
const handleLink = (item) => {
const { redirect, path } = item;
redirect ? router.push(redirect) : router.push(path);
};
watchEffect(() => {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith('/redirect/')) return;
getBreadcrumb();
});
onMounted(() => {
getBreadcrumb();
});
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<div>
<template v-for="(item, index) in options">
<template v-if="values.includes(item.value)">
<span
v-if="(item.elTagType === 'default' || item.elTagType === '') && (item.elTagClass === '' || item.elTagClass == null)"
:key="item.value"
:index="index"
:class="item.elTagClass"
>
{{ item.label + ' ' }}
</span>
<el-tag
v-else
:key="item.value + ''"
:disable-transitions="true"
:index="index"
:type="
item.elTagType === 'primary' ||
item.elTagType === 'success' ||
item.elTagType === 'info' ||
item.elTagType === 'warning' ||
item.elTagType === 'danger'
? item.elTagType
: 'primary'
"
:class="item.elTagClass"
>
{{ item.label + ' ' }}
</el-tag>
</template>
</template>
<template v-if="unmatch && showValue">
{{ unmatchArray }}
</template>
</div>
</template>
<script setup lang="ts">
interface Props {
options: Array<DictDataOption>;
value: number | string | Array<number | string>;
showValue?: boolean;
separator?: string;
}
const props = withDefaults(defineProps<Props>(), {
showValue: true,
separator: ','
});
const values = computed(() => {
if (props.value === '' || props.value === null || typeof props.value === 'undefined') return [];
return Array.isArray(props.value) ? props.value.map((item) => '' + item) : String(props.value).split(props.separator);
});
const unmatch = computed(() => {
if (props.options?.length == 0 || props.value === '' || props.value === null || typeof props.value === 'undefined') return false;
// 传入值为非数组
let unmatch = false; // 添加一个标志来判断是否有未匹配项
values.value.forEach((item) => {
if (!props.options.some((v) => v.value === item)) {
unmatch = true; // 如果有未匹配项将标志设置为true
}
});
return unmatch; // 返回标志的值
});
const unmatchArray = computed(() => {
// 记录未匹配的项
const itemUnmatchArray: Array<string | number> = [];
if (props.value !== '' && props.value !== null && typeof props.value !== 'undefined') {
values.value.forEach((item) => {
if (!props.options.some((v) => v.value === item)) {
itemUnmatchArray.push(item);
}
});
}
// 没有value不显示
return handleArray(itemUnmatchArray);
});
const handleArray = (array: Array<string | number>) => {
if (array.length === 0) return '';
return array.reduce((pre, cur) => {
return pre + ' ' + cur;
});
};
</script>
<style lang="scss" scoped>
.el-tag + .el-tag {
margin-left: 10px;
}
</style>

View File

@@ -0,0 +1,244 @@
<template>
<div>
<el-upload
v-if="type === 'url'"
:action="upload.url"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
class="editor-img-uploader"
name="file"
:show-file-list="false"
:headers="upload.headers"
>
<i ref="uploadRef"></i>
</el-upload>
</div>
<div class="editor">
<quill-editor
ref="quillEditorRef"
v-model:content="content"
content-type="html"
:options="options"
:style="styles"
@text-change="(e: any) => $emit('update:modelValue', content)"
/>
</div>
</template>
<script setup lang="ts">
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { QuillEditor, Quill } from '@vueup/vue-quill';
import { propTypes } from '@/utils/propTypes';
import { globalHeaders } from '@/utils/request';
defineEmits(['update:modelValue']);
const props = defineProps({
/* 编辑器的内容 */
modelValue: propTypes.string,
/* 高度 */
height: propTypes.number.def(400),
/* 最小高度 */
minHeight: propTypes.number.def(400),
/* 只读 */
readOnly: propTypes.bool.def(false),
/* 上传文件大小限制(MB) */
fileSize: propTypes.number.def(5),
/* 类型base64格式、url格式 */
type: propTypes.string.def('url')
});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const upload = reactive<UploadOption>({
headers: globalHeaders(),
url: import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'
});
const quillEditorRef = ref();
const uploadRef = ref<HTMLDivElement>();
const options = ref<any>({
theme: 'snow',
bounds: document.body,
debug: 'warn',
modules: {
// 工具栏配置
toolbar: {
container: [
['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
['blockquote', 'code-block'], // 引用 代码块
[{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
[{ indent: '-1' }, { indent: '+1' }], // 缩进
[{ size: ['small', false, 'large', 'huge'] }], // 字体大小
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
[{ align: [] }], // 对齐方式
['clean'], // 清除文本格式
['link', 'image', 'video'] // 链接、图片、视频
],
handlers: {
image: (value: boolean) => {
if (value) {
// 调用element图片上传
uploadRef.value.click();
} else {
Quill.format('image', true);
}
}
}
}
},
placeholder: '请输入内容',
readOnly: props.readOnly
});
const styles = computed(() => {
const style: any = {};
if (props.minHeight) {
style.minHeight = `${props.minHeight}px`;
}
if (props.height) {
style.height = `${props.height}px`;
}
return style;
});
const content = ref('');
watch(
() => props.modelValue,
(v: string) => {
if (v !== content.value) {
content.value = v || '<p></p>';
}
},
{ immediate: true }
);
// 图片上传成功返回图片地址
const handleUploadSuccess = (res: any) => {
// 如果上传成功
if (res.code === 200) {
// 获取富文本实例
const quill = toRaw(quillEditorRef.value).getQuill();
// 获取光标位置
const length = quill.selection.savedRange.index;
// 插入图片res为服务器返回的图片链接地址
quill.insertEmbed(length, 'image', res.data.url);
// 调整光标到最后
quill.setSelection(length + 1);
proxy?.$modal.closeLoading();
} else {
proxy?.$modal.msgError('图片插入失败');
proxy?.$modal.closeLoading();
}
};
// 图片上传前拦截
const handleBeforeUpload = (file: any) => {
const type = ['image/jpeg', 'image/jpg', 'image/png', 'image/svg'];
const isJPG = type.includes(file.type);
//检验文件格式
if (!isJPG) {
proxy?.$modal.msgError(`图片格式错误!`);
return false;
}
// 校检文件大小
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
proxy?.$modal.loading('正在上传文件,请稍候...');
return true;
};
// 图片失败拦截
const handleUploadError = (err: any) => {
proxy?.$modal.msgError('上传文件失败');
};
</script>
<style>
.editor-img-uploader {
display: none;
}
.editor,
.ql-toolbar {
white-space: pre-wrap !important;
line-height: normal !important;
}
.quill-img {
display: none;
}
.ql-snow .ql-tooltip[data-mode='link']::before {
content: '请输入链接地址:';
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
border-right: 0;
content: '保存';
padding-right: 0;
}
.ql-snow .ql-tooltip[data-mode='video']::before {
content: '请输入视频地址:';
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: '14px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
content: '10px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
content: '18px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
content: '32px';
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: '文本';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
content: '标题1';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
content: '标题2';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
content: '标题3';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
content: '标题4';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
content: '标题5';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
content: '标题6';
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: '标准字体';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
content: '衬线字体';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
content: '等宽字体';
}
</style>

View File

@@ -0,0 +1,241 @@
<template>
<div class="upload-file">
<el-upload
ref="fileUploadRef"
multiple
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
:limit="limit"
:accept="fileAccept"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:on-success="handleUploadSuccess"
:show-file-list="false"
:headers="headers"
class="upload-file-uploader"
v-if="!disabled"
>
<!-- 上传按钮 -->
<el-button type="primary">选取文件</el-button>
</el-upload>
<!-- 上传提示 -->
<div v-if="showTip && !disabled" class="el-upload__tip">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
</template>
的文件
</div>
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item ele-upload-list__item-content">
<el-link :href="`${file.url}`" :underline="false" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
<el-button type="danger" v-if="!disabled" link @click="handleDelete(index)">删除</el-button>
</div>
</li>
</transition-group>
</div>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
import { delOss, listByIds } from '@/api/system/oss';
import { globalHeaders } from '@/utils/request';
const props = defineProps({
modelValue: {
type: [String, Object, Array],
default: () => []
},
// 数量限制
limit: propTypes.number.def(5),
// 大小限制(MB)
fileSize: propTypes.number.def(5),
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: propTypes.array.def(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'pdf']),
// 是否显示提示
isShowTip: propTypes.bool.def(true),
// 禁用组件(仅查看文件)
disabled: propTypes.bool.def(false)
});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emit = defineEmits(['update:modelValue']);
const number = ref(0);
const uploadList = ref<any[]>([]);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(baseUrl + '/resource/oss/upload'); // 上传文件服务器地址
const headers = ref(globalHeaders());
const fileList = ref<any[]>([]);
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
const fileUploadRef = ref<ElUploadInstance>();
// 监听 fileType 变化,更新 fileAccept
const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','));
watch(
() => props.modelValue,
async (val) => {
if (val) {
let temp = 1;
// 首先将值转为数组
let list: any[] = [];
if (Array.isArray(val)) {
list = val;
} else {
const res = await listByIds(val);
list = res.data.map((oss) => {
return {
name: oss.originalName,
url: oss.url,
ossId: oss.ossId
};
});
}
// 然后将数组转为对象数组
fileList.value = list.map((item) => {
item = { name: item.name, url: item.url, ossId: item.ossId };
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
} else {
fileList.value = [];
return [];
}
},
{ deep: true, immediate: true }
);
// 上传前校检格式和大小
const handleBeforeUpload = (file: any) => {
// 校检文件类型
if (props.fileType.length) {
const fileName = file.name.split('.');
const fileExt = fileName[fileName.length - 1];
const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
if (!isTypeOk) {
proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`);
return false;
}
}
// 校检文件名是否包含特殊字符
if (file.name.includes(',')) {
proxy?.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false;
}
// 校检文件大小
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
proxy?.$modal.loading('正在上传文件,请稍候...');
number.value++;
return true;
};
// 文件个数超出
const handleExceed = () => {
proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
};
// 上传失败
const handleUploadError = () => {
proxy?.$modal.msgError('上传文件失败');
};
// 上传成功回调
const handleUploadSuccess = (res: any, file: UploadFile) => {
if (res.code === 200) {
uploadList.value.push({
name: res.data.fileName,
url: res.data.url,
ossId: res.data.ossId
});
uploadedSuccessfully();
} else {
number.value--;
proxy?.$modal.closeLoading();
proxy?.$modal.msgError(res.msg);
fileUploadRef.value?.handleRemove(file);
uploadedSuccessfully();
}
};
// 删除文件
const handleDelete = (index: number) => {
const ossId = fileList.value[index].ossId;
delOss(ossId);
fileList.value.splice(index, 1);
emit('update:modelValue', listToString(fileList.value));
};
// 上传结束处理
const uploadedSuccessfully = () => {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit('update:modelValue', listToString(fileList.value));
proxy?.$modal.closeLoading();
}
};
// 获取文件名称
const getFileName = (name: string) => {
// 如果是url那么取最后的名字 如果不是直接返回
if (name.lastIndexOf('/') > -1) {
return name.slice(name.lastIndexOf('/') + 1);
} else {
return name;
}
};
// 对象转成指定字符串分隔
const listToString = (list: any[], separator?: string) => {
let strs = '';
separator = separator || ',';
list.forEach((item) => {
if (item.ossId) {
strs += item.ossId + separator;
}
});
return strs != '' ? strs.substring(0, strs.length - 1) : '';
};
</script>
<style lang="scss" scoped>
.upload-file-uploader {
margin-bottom: 5px;
}
.upload-file-list .el-upload-list__item {
border: 1px solid #e4e7ed;
line-height: 2;
margin-bottom: 10px;
position: relative;
}
.upload-file-list .ele-upload-list__item-content {
display: flex;
justify-content: space-between;
align-items: center;
color: inherit;
}
.ele-upload-list__item-content-action .el-link {
margin-right: 10px;
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<div style="padding: 0 15px" @click="toggleClick">
<svg :class="{ 'is-active': isActive }" class="hamburger" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="64" height="64">
<path
d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
/>
</svg>
</div>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
defineProps({
isActive: propTypes.bool.def(false)
});
const emit = defineEmits(['toggleClick']);
const toggleClick = () => {
emit('toggleClick');
};
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div class="relative" :style="{ 'width': width }">
<el-input v-model="modelValue" readonly placeholder="点击选择图标" @click="visible = !visible">
<template #prepend>
<svg-icon :icon-class="modelValue" />
</template>
</el-input>
<el-popover shadow="none" :visible="visible" placement="bottom-end" trigger="click" :width="450">
<template #reference>
<div class="cursor-pointer text-[#999] absolute right-[10px] top-0 height-[32px] leading-[32px]" @click="visible = !visible">
<i-ep-caret-top v-show="visible"></i-ep-caret-top>
<i-ep-caret-bottom v-show="!visible"></i-ep-caret-bottom>
</div>
</template>
<el-input v-model="filterValue" class="p-2" placeholder="搜索图标" clearable @input="filterIcons" />
<el-scrollbar height="w-[200px]">
<ul class="icon-list">
<el-tooltip v-for="(iconName, index) in iconNames" :key="index" :content="iconName" placement="bottom" effect="light">
<li :class="['icon-item', { active: modelValue == iconName }]" @click="selectedIcon(iconName)">
<svg-icon color="var(--el-text-color-regular)" :icon-class="iconName" />
</li>
</el-tooltip>
</ul>
</el-scrollbar>
</el-popover>
</div>
</template>
<script setup lang="ts">
import icons from '@/components/IconSelect/requireIcons';
import { propTypes } from '@/utils/propTypes';
const props = defineProps({
modelValue: propTypes.string.isRequired,
width: propTypes.string.def('400px')
});
const emit = defineEmits(['update:modelValue']);
const visible = ref(false);
const { modelValue, width } = toRefs(props);
const iconNames = ref<string[]>(icons);
const filterValue = ref('');
/**
* 筛选图标
*/
const filterIcons = () => {
if (filterValue.value) {
iconNames.value = icons.filter((iconName) => iconName.includes(filterValue.value));
} else {
iconNames.value = icons;
}
};
/**
* 选择图标
* @param iconName 选择的图标名称
*/
const selectedIcon = (iconName: string) => {
emit('update:modelValue', iconName);
visible.value = false;
};
</script>
<style lang="scss" scoped>
.el-scrollbar {
max-height: calc(50vh - 100px) !important;
overflow-y: auto;
}
.el-divider--horizontal {
margin: 10px auto !important;
}
.icon-list {
display: flex;
flex-wrap: wrap;
padding-left: 10px;
margin-top: 10px;
.icon-item {
cursor: pointer;
width: 10%;
margin: 0 10px 10px 0;
padding: 5px;
display: flex;
flex-direction: column;
justify-items: center;
align-items: center;
border: 1px solid #ccc;
&:hover {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
transition: all 0.2s;
transform: scaleX(1.1);
}
}
.active {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
}
</style>

View File

@@ -0,0 +1,7 @@
const icons: string[] = [];
const modules = import.meta.glob('./../../assets/icons/svg/*.svg');
for (const path in modules) {
const p = path.split('assets/icons/svg/')[1].split('.svg')[0];
icons.push(p);
}
export default icons;

View File

@@ -0,0 +1,79 @@
<template>
<el-image :src="`${realSrc}`" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="realSrcList" preview-teleported>
<template #error>
<div class="image-slot">
<el-icon><picture-filled /></el-icon>
</div>
</template>
</el-image>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
const props = defineProps({
src: propTypes.string.def(''),
width: {
type: [Number, String],
default: ''
},
height: {
type: [Number, String],
default: ''
}
});
const realSrc = computed(() => {
if (!props.src) {
return;
}
const real_src = props.src.split(',')[0];
return real_src;
});
const realSrcList = computed(() => {
if (!props.src) {
return [];
}
const real_src_list = props.src.split(',');
const srcList: string[] = [];
real_src_list.forEach((item: string) => {
if (item.trim() === '') {
return;
}
return srcList.push(item);
});
return srcList;
});
const realWidth = computed(() => (typeof props.width == 'string' ? props.width : `${props.width}px`));
const realHeight = computed(() => (typeof props.height == 'string' ? props.height : `${props.height}px`));
</script>
<style lang="scss" scoped>
.el-image {
border-radius: 5px;
background-color: #ebeef5;
box-shadow: 0 0 5px 1px #ccc;
:deep(.el-image__inner) {
transition: all 0.3s;
cursor: pointer;
&:hover {
transform: scale(1.2);
}
}
:deep(.image-slot) {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
color: #909399;
font-size: 30px;
}
}
</style>

View File

@@ -0,0 +1,242 @@
<template>
<div class="component-upload-image">
<el-upload
ref="imageUploadRef"
multiple
:action="uploadImgUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:limit="limit"
:accept="fileAccept"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:before-remove="handleDelete"
:show-file-list="true"
:headers="headers"
:file-list="fileList"
:on-preview="handlePictureCardPreview"
:class="{ hide: fileList.length >= limit }"
>
<el-icon class="avatar-uploader-icon">
<plus />
</el-icon>
</el-upload>
<!-- 上传提示 -->
<div v-if="showTip" class="el-upload__tip">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
</template>
的文件
</div>
<el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
<img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { listByIds, delOss } from '@/api/system/oss';
import { OssVO } from '@/api/system/oss/types';
import { propTypes } from '@/utils/propTypes';
import { globalHeaders } from '@/utils/request';
import { compressAccurately } from 'image-conversion';
const props = defineProps({
modelValue: {
type: [String, Object, Array],
default: () => []
},
// 图片数量限制
limit: propTypes.number.def(5),
// 大小限制(MB)
fileSize: propTypes.number.def(5),
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: propTypes.array.def(['png', 'jpg', 'jpeg']),
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
},
// 是否支持压缩,默认否
compressSupport: {
type: Boolean,
default: false
},
// 压缩目标大小单位KB。默认300KB以上文件才压缩并压缩至300KB以内
compressTargetSize: propTypes.number.def(300)
});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emit = defineEmits(['update:modelValue']);
const number = ref(0);
const uploadList = ref<any[]>([]);
const dialogImageUrl = ref('');
const dialogVisible = ref(false);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadImgUrl = ref(baseUrl + '/resource/oss/upload'); // 上传的图片服务器地址
const headers = ref(globalHeaders());
const fileList = ref<any[]>([]);
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
const imageUploadRef = ref<ElUploadInstance>();
// 监听 fileType 变化,更新 fileAccept
const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','));
watch(
() => props.modelValue,
async (val: string) => {
if (val) {
// 首先将值转为数组
let list: OssVO[] = [];
if (Array.isArray(val)) {
list = val as OssVO[];
} else {
const res = await listByIds(val);
list = res.data;
}
// 然后将数组转为对象数组
fileList.value = list.map((item) => {
// 字符串回显处理 如果此处存的是url可直接回显 如果存的是id需要调用接口查出来
let itemData;
if (typeof item === 'string') {
itemData = { name: item, url: item };
} else {
// 此处name使用ossId 防止删除出现重名
itemData = { name: item.ossId, url: item.url, ossId: item.ossId };
}
return itemData;
});
} else {
fileList.value = [];
return [];
}
},
{ deep: true, immediate: true }
);
/** 上传前loading加载 */
const handleBeforeUpload = (file: any) => {
let isImg = false;
if (props.fileType.length) {
let fileExtension = '';
if (file.name.lastIndexOf('.') > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1);
}
isImg = props.fileType.some((type: any) => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
} else {
isImg = file.type.indexOf('image') > -1;
}
if (!isImg) {
proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`);
return false;
}
if (file.name.includes(',')) {
proxy?.$modal.msgError('文件名不正确,不能包含英文逗号!');
return false;
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
//压缩图片,开启压缩并且大于指定的压缩大小时才压缩
if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
proxy?.$modal.loading('正在上传图片,请稍候...');
number.value++;
return compressAccurately(file, props.compressTargetSize);
} else {
proxy?.$modal.loading('正在上传图片,请稍候...');
number.value++;
}
};
// 文件个数超出
const handleExceed = () => {
proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
};
// 上传成功回调
const handleUploadSuccess = (res: any, file: UploadFile) => {
if (res.code === 200) {
uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
uploadedSuccessfully();
} else {
number.value--;
proxy?.$modal.closeLoading();
proxy?.$modal.msgError(res.msg);
imageUploadRef.value?.handleRemove(file);
uploadedSuccessfully();
}
};
// 删除图片
const handleDelete = (file: UploadFile): boolean => {
const findex = fileList.value.map((f) => f.name).indexOf(file.name);
if (findex > -1 && uploadList.value.length === number.value) {
const ossId = fileList.value[findex].ossId;
delOss(ossId);
fileList.value.splice(findex, 1);
emit('update:modelValue', listToString(fileList.value));
return false;
}
return true;
};
// 上传结束处理
const uploadedSuccessfully = () => {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit('update:modelValue', listToString(fileList.value));
proxy?.$modal.closeLoading();
}
};
// 上传失败
const handleUploadError = () => {
proxy?.$modal.msgError('上传图片失败');
proxy?.$modal.closeLoading();
};
// 预览
const handlePictureCardPreview = (file: any) => {
dialogImageUrl.value = file.url;
dialogVisible.value = true;
};
// 对象转成指定字符串分隔
const listToString = (list: any[], separator?: string) => {
let strs = '';
separator = separator || ',';
for (const i in list) {
if (undefined !== list[i].ossId && list[i].url.indexOf('blob:') !== 0) {
strs += list[i].ossId + separator;
}
}
return strs != '' ? strs.substring(0, strs.length - 1) : '';
};
</script>
<style lang="scss" scoped>
// .el-upload--picture-card 控制加号部分
:deep(.hide .el-upload--picture-card) {
display: none;
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<el-dropdown trigger="click" @command="handleLanguageChange">
<div class="lang-select--style">
<svg-icon icon-class="language" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :disabled="appStore.language === 'zh_CN'" command="zh_CN"> 中文 </el-dropdown-item>
<el-dropdown-item :disabled="appStore.language === 'en_US'" command="en_US"> English </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useAppStore } from '@/store/modules/app';
import SvgIcon from '@/components/SvgIcon/index.vue';
const appStore = useAppStore();
const { locale } = useI18n();
const message: any = {
zh_CN: '切换语言成功!',
en_US: 'Switch Language Successful!'
};
const handleLanguageChange = (lang: any) => {
locale.value = lang;
appStore.changeLanguage(lang);
ElMessage.success(message[lang] || '切换语言成功!');
};
</script>
<style lang="scss" scoped>
.lang-select--style {
font-size: 18px;
line-height: 50px;
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<div :class="{ hidden: hidden }" class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:background="background"
:layout="layout"
:page-sizes="pageSizes"
:pager-count="pagerCount"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup name="Pagination" lang="ts">
import { scrollTo } from '@/utils/scroll-to';
import { propTypes } from '@/utils/propTypes';
const props = defineProps({
total: propTypes.number,
page: propTypes.number.def(1),
limit: propTypes.number.def(20),
pageSizes: { type: Array<number>, default: () => [10, 20, 30, 50] },
// 移动端页码按钮的数量端默认值5
pagerCount: propTypes.number.def(document.body.clientWidth < 992 ? 5 : 7),
layout: propTypes.string.def('total, sizes, prev, pager, next, jumper'),
background: propTypes.bool.def(true),
autoScroll: propTypes.bool.def(true),
hidden: propTypes.bool.def(false),
float: propTypes.string.def('right')
});
const emit = defineEmits(['update:page', 'update:limit', 'pagination']);
const currentPage = computed({
get() {
return props.page;
},
set(val) {
emit('update:page', val);
}
});
const pageSize = computed({
get() {
return props.limit;
},
set(val) {
emit('update:limit', val);
}
});
function handleSizeChange(val: number) {
if (currentPage.value * val > props.total) {
currentPage.value = 1;
}
emit('pagination', { page: currentPage.value, limit: val });
if (props.autoScroll) {
scrollTo(0, 800);
}
}
function handleCurrentChange(val: number) {
emit('pagination', { page: val, limit: pageSize.value });
if (props.autoScroll) {
scrollTo(0, 800);
}
}
</script>
<style lang="scss" scoped>
.pagination-container {
.el-pagination {
float: v-bind(float);
}
}
.pagination-container.hidden {
display: none;
}
</style>

View File

@@ -0,0 +1,3 @@
<template>
<router-view />
</template>

View File

@@ -0,0 +1,56 @@
<template>
<div style="display: flex; justify-content: space-between">
<div>
<el-button v-if="submitButtonShow" :loading="props.buttonLoading" type="info" @click="submitForm('draft')">暂存</el-button>
<el-button v-if="submitButtonShow" :loading="props.buttonLoading" type="primary" @click="submitForm('submit')"> </el-button>
<el-button v-if="approvalButtonShow" :loading="props.buttonLoading" type="primary" @click="approvalVerifyOpen">审批</el-button>
<el-button v-if="props.id && props.status !== 'draft'" type="primary" @click="handleApprovalRecord">流程进度</el-button>
<slot />
</div>
<div>
<el-button style="float: right" @click="goBack()">返回</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const props = defineProps({
status: propTypes.string.def(''),
pageType: propTypes.string.def(''),
buttonLoading: propTypes.bool.def(false),
id: propTypes.string.def('') || propTypes.number.def()
});
const emits = defineEmits(['submitForm', 'approvalVerifyOpen', 'handleApprovalRecord']);
//暂存,提交
const submitForm = async (type) => {
emits('submitForm', type);
};
//审批
const approvalVerifyOpen = async () => {
emits('approvalVerifyOpen');
};
//审批记录
const handleApprovalRecord = () => {
emits('handleApprovalRecord');
};
//校验提交按钮是否显示
const submitButtonShow = computed(() => {
return (
props.pageType === 'add' ||
(props.pageType === 'update' && props.status && (props.status === 'draft' || props.status === 'cancel' || props.status === 'back'))
);
});
//校验审批按钮是否显示
const approvalButtonShow = computed(() => {
return props.pageType === 'approval' && props.status && props.status === 'waiting';
});
//返回
const goBack = () => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.go(-1);
};
</script>

View File

@@ -0,0 +1,127 @@
<template>
<div class="container">
<el-dialog v-model="visible" draggable title="审批记录" :width="props.width" :height="props.height" :close-on-click-modal="false">
<el-tabs v-model="tabActiveName" class="demo-tabs">
<el-tab-pane v-loading="loading" label="流程图" name="image" style="height: 68vh">
<flowChart :ins-id="insId" v-if="insId" />
</el-tab-pane>
<el-tab-pane v-loading="loading" label="审批信息" name="info">
<div>
<el-table :data="historyList" style="width: 100%" border fit>
<el-table-column type="index" label="序号" align="center" width="60"></el-table-column>
<el-table-column prop="nodeName" label="任务名称" sortable align="center"></el-table-column>
<el-table-column prop="approveName" :show-overflow-tooltip="true" label="办理人" sortable align="center">
<template #default="scope">
<template v-if="scope.row.approveName">
<el-tag v-for="(item, index) in scope.row.approveName.split(',')" :key="index" type="success">{{ item }}</el-tag>
</template>
<template v-else> <el-tag type="success"></el-tag></template>
</template>
</el-table-column>
<el-table-column prop="flowStatus" label="状态" width="80" sortable align="center">
<template #default="scope">
<dict-tag :options="wf_task_status" :value="scope.row.flowStatus"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="message" label="审批意见" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
<el-table-column prop="createTime" label="开始时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
<el-table-column prop="updateTime" label="结束时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
<el-table-column
prop="runDuration"
label="运行时长"
width="140"
:show-overflow-tooltip="true"
sortable
align="center"
></el-table-column>
<el-table-column prop="attachmentList" width="120" label="附件" align="center">
<template #default="scope">
<el-popover v-if="scope.row.attachmentList && scope.row.attachmentList.length > 0" placement="right" :width="310" trigger="click">
<template #reference>
<el-button type="primary" style="margin-right: 16px">附件</el-button>
</template>
<el-table border :data="scope.row.attachmentList">
<el-table-column prop="originalName" width="202" :show-overflow-tooltip="true" label="附件名称"></el-table-column>
<el-table-column prop="name" width="80" align="center" :show-overflow-tooltip="true" label="操作">
<template #default="tool">
<el-button type="text" @click="handleDownload(tool.row.ossId)">下载</el-button>
</template>
</el-table-column>
</el-table>
</el-popover>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { flowHisTaskList } from '@/api/workflow/instance';
import { propTypes } from '@/utils/propTypes';
import { listByIds } from '@/api/system/oss';
import FlowChart from '@/components/Process/flowChart.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wf_task_status } = toRefs<any>(proxy?.useDict('wf_task_status'));
const props = defineProps({
width: propTypes.string.def('80%'),
height: propTypes.string.def('100%')
});
const loading = ref(false);
const visible = ref(false);
const historyList = ref<Array<any>>([]);
const tabActiveName = ref('image');
const insId = ref(null);
//初始化查询审批记录
const init = async (businessId: string | number) => {
visible.value = true;
loading.value = true;
tabActiveName.value = 'image';
historyList.value = [];
flowHisTaskList(businessId).then((resp) => {
if (resp.data) {
historyList.value = resp.data.list;
insId.value = resp.data.instanceId;
if (historyList.value.length > 0) {
historyList.value.forEach((item) => {
if (item.ext) {
getIds(item.ext).then((res) => {
item.attachmentList = res.data;
});
} else {
item.attachmentList = [];
}
});
}
loading.value = false;
}
});
};
const getIds = async (ids: string | number) => {
const res = await listByIds(ids);
return res;
};
/** 下载按钮操作 */
const handleDownload = (ossId: string) => {
proxy?.$download.oss(ossId);
};
/**
* 对外暴露子组件方法
*/
defineExpose({
init
});
</script>
<style lang="scss" scoped>
.container {
:deep(.el-dialog .el-dialog__body) {
max-height: calc(100vh - 170px) !important;
min-height: calc(100vh - 170px) !important;
}
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<div>
<div style="height: 68vh" class="iframe-wrapper">
<iframe :src="iframeUrl" style="width: 100%; height: 100%" frameborder="0" scrolling="no" class="custom-iframe" />
</div>
</div>
</template>
<script setup lang="ts">
import { getToken } from '@/utils/auth';
// Props 定义方式变化
const props = defineProps({
insId: {
type: [String, Number],
default: null
}
});
const iframeUrl = ref('');
const baseUrl = import.meta.env.VITE_APP_BASE_API;
onMounted(async () => {
const url = baseUrl + `/warm-flow-ui/index.html?id=${props.insId}&type=FlowChart&t=${Date.now()}`;
iframeUrl.value = url + '&Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID;
});
</script>
<style scoped>
.iframe-wrapper {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.custom-iframe {
width: 100%;
border: none;
background: transparent;
}
</style>

View File

@@ -0,0 +1,154 @@
<template>
<div
ref="imageWrapperRef"
class="image-wrapper"
@wheel="handleMouseWheel"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseLeave"
@dblclick="resetTransform"
:style="transformStyle"
>
<el-card class="box-card">
<el-image :src="props.imgUrl" class="scalable-image" />
</el-card>
</div>
</template>
<script setup lang="ts">
// Props 定义方式变化
const props = defineProps({
imgUrl: {
type: String,
default: () => ''
}
});
const imageWrapperRef = ref<HTMLElement | null>(null);
const scale = ref(1); // 初始缩放比例
const maxScale = 3; // 最大缩放比例
const minScale = 0.5; // 最小缩放比例
let isDragging = false;
let startX = 0;
let startY = 0;
let currentTranslateX = 0;
let currentTranslateY = 0;
const handleMouseWheel = (event: WheelEvent) => {
event.preventDefault();
let newScale = scale.value - event.deltaY / 1000;
newScale = Math.max(minScale, Math.min(newScale, maxScale));
if (newScale !== scale.value) {
scale.value = newScale;
resetDragPosition(); // 重置拖拽位置,使图片居中
}
};
const handleMouseDown = (event: MouseEvent) => {
if (scale.value > 1) {
event.preventDefault(); // 阻止默认行为,防止拖拽
isDragging = true;
startX = event.clientX;
startY = event.clientY;
}
};
const handleMouseMove = (event: MouseEvent) => {
if (!isDragging || !imageWrapperRef.value) return;
const deltaX = event.clientX - startX;
const deltaY = event.clientY - startY;
startX = event.clientX;
startY = event.clientY;
currentTranslateX += deltaX;
currentTranslateY += deltaY;
// 边界检测,防止图片被拖出容器
const bounds = getBounds();
if (currentTranslateX > bounds.maxTranslateX) {
currentTranslateX = bounds.maxTranslateX;
} else if (currentTranslateX < bounds.minTranslateX) {
currentTranslateX = bounds.minTranslateX;
}
if (currentTranslateY > bounds.maxTranslateY) {
currentTranslateY = bounds.maxTranslateY;
} else if (currentTranslateY < bounds.minTranslateY) {
currentTranslateY = bounds.minTranslateY;
}
applyTransform();
};
const handleMouseUp = () => {
isDragging = false;
};
const handleMouseLeave = () => {
isDragging = false;
};
const resetTransform = () => {
scale.value = 1;
currentTranslateX = 0;
currentTranslateY = 0;
applyTransform();
};
const resetDragPosition = () => {
currentTranslateX = 0;
currentTranslateY = 0;
applyTransform();
};
const applyTransform = () => {
if (imageWrapperRef.value) {
imageWrapperRef.value.style.transform = `translate(${currentTranslateX}px, ${currentTranslateY}px) scale(${scale.value})`;
}
};
const getBounds = () => {
if (!imageWrapperRef.value) return { minTranslateX: 0, maxTranslateX: 0, minTranslateY: 0, maxTranslateY: 0 };
const imgRect = imageWrapperRef.value.getBoundingClientRect();
const containerRect = imageWrapperRef.value.parentElement?.getBoundingClientRect() ?? imgRect;
const minTranslateX = (containerRect.width - imgRect.width * scale.value) / 2;
const maxTranslateX = -(containerRect.width - imgRect.width * scale.value) / 2;
const minTranslateY = (containerRect.height - imgRect.height * scale.value) / 2;
const maxTranslateY = -(containerRect.height - imgRect.height * scale.value) / 2;
return { minTranslateX, maxTranslateX, minTranslateY, maxTranslateY };
};
const transformStyle = computed(() => ({
transition: isDragging ? 'none' : 'transform 0.2s ease'
}));
</script>
<style scoped>
.image-wrapper {
width: 100%;
overflow: hidden;
position: relative;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
user-select: none; /* 禁用文本选择 */
cursor: grab; /* 设置初始鼠标指针为可拖动 */
}
.image-wrapper:active {
cursor: grabbing; /* 当正在拖动时改变鼠标指针 */
}
.scalable-image {
object-fit: contain;
width: 100%;
padding: 15px;
}
</style>

View File

@@ -0,0 +1,211 @@
<template>
<el-dialog v-model="visible" draggable title="流程干预" :width="props.width" :height="props.height" :close-on-click-modal="false">
<el-descriptions v-loading="loading" class="margin-top" :title="`${task.flowName}(${task.flowCode})`" :column="2" border>
<el-descriptions-item label="任务名称">{{ task.nodeName }}</el-descriptions-item>
<el-descriptions-item label="节点编码">{{ task.nodeCode }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ task.createTime }}</el-descriptions-item>
<el-descriptions-item label="流程实例ID">{{ task.instanceId }}</el-descriptions-item>
<el-descriptions-item label="版本号">{{ task.version }}.0</el-descriptions-item>
<el-descriptions-item label="业务ID">{{ task.businessId }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<span class="dialog-footer">
<el-button v-if="task.flowStatus === 'waiting'" :disabled="buttonDisabled" type="primary" @click="openTransferTask"> 转办 </el-button>
<el-button
v-if="task.flowStatus === 'waiting' && Number(task.nodeRatio) > 0"
:disabled="buttonDisabled"
type="primary"
@click="openMultiInstanceUser"
>
加签
</el-button>
<el-button
v-if="task.flowStatus === 'waiting' && Number(task.nodeRatio) > 0"
:disabled="buttonDisabled"
type="primary"
@click="handleTaskUser"
>
减签
</el-button>
<el-button v-if="task.flowStatus === 'waiting'" :disabled="buttonDisabled" type="danger" @click="handleTerminationTask"> 终止 </el-button>
</span>
</template>
<!-- 转办 -->
<UserSelect ref="transferTaskRef" :multiple="false" @confirm-call-back="handleTransferTask"></UserSelect>
<!-- 加签组件 -->
<UserSelect ref="multiInstanceUserRef" :multiple="true" @confirm-call-back="addMultiInstanceUser"></UserSelect>
<el-dialog v-model="deleteSignatureVisible" draggable title="减签人员" width="700px" height="400px" append-to-body :close-on-click-modal="false"
><div>
<el-table :data="deleteUserList" border>
<el-table-column prop="nodeName" label="任务名称" />
<el-table-column prop="nickName" label="办理人" />
<el-table-column label="操作" align="center" width="160">
<template #default="scope">
<el-button type="danger" size="small" icon="Delete" @click="deleteMultiInstanceUser(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</el-dialog>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
import { FlowTaskVO, TaskOperationBo } from '@/api/workflow/task/types';
import UserSelect from '@/components/UserSelect';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { getTask, taskOperation, currentTaskAllUser, terminationTask } from '@/api/workflow/task';
const props = defineProps({
width: propTypes.string.def('50%'),
height: propTypes.string.def('100%')
});
const emits = defineEmits(['submitCallback']);
const transferTaskRef = ref<InstanceType<typeof UserSelect>>();
const multiInstanceUserRef = ref<InstanceType<typeof UserSelect>>();
//遮罩层
const loading = ref(true);
//按钮
const buttonDisabled = ref(true);
const visible = ref(false);
//减签弹窗
const deleteSignatureVisible = ref(false);
//可减签的人员
const deleteUserList = ref<any>([]);
//任务
const task = ref<FlowTaskVO>({
id: undefined,
createTime: undefined,
updateTime: undefined,
tenantId: undefined,
definitionId: undefined,
instanceId: undefined,
flowName: undefined,
businessId: undefined,
nodeCode: undefined,
nodeName: undefined,
flowCode: undefined,
flowStatus: undefined,
formCustom: undefined,
formPath: undefined,
nodeType: undefined,
nodeRatio: undefined,
version: undefined,
applyNode: undefined,
buttonList: []
});
const open = (taskId: string) => {
visible.value = true;
getTask(taskId).then((response) => {
loading.value = false;
buttonDisabled.value = false;
task.value = response.data;
});
};
//打开转办
const openTransferTask = () => {
transferTaskRef.value.open();
};
//转办
const handleTransferTask = async (data) => {
if (data && data.length > 0) {
const taskOperationBo = reactive<TaskOperationBo>({
userId: data[0].userId,
taskId: task.value.id,
message: ''
});
await proxy?.$modal.confirm('是否确认提交?');
loading.value = true;
buttonDisabled.value = true;
await taskOperation(taskOperationBo, 'transferTask').finally(() => {
loading.value = false;
buttonDisabled.value = false;
});
visible.value = false;
emits('submitCallback');
proxy?.$modal.msgSuccess('操作成功');
} else {
proxy?.$modal.msgWarning('请选择用户!');
}
};
//加签
const openMultiInstanceUser = async () => {
multiInstanceUserRef.value.open();
};
//加签
const addMultiInstanceUser = async (data) => {
if (data && data.length > 0) {
const taskOperationBo = reactive<TaskOperationBo>({
userIds: data.map((e) => e.userId),
taskId: task.value.id,
message: ''
});
await proxy?.$modal.confirm('是否确认提交?');
loading.value = true;
buttonDisabled.value = true;
await taskOperation(taskOperationBo, 'addSignature').finally(() => {
loading.value = false;
buttonDisabled.value = false;
});
visible.value = false;
emits('submitCallback');
proxy?.$modal.msgSuccess('操作成功');
} else {
proxy?.$modal.msgWarning('请选择用户!');
}
};
//减签
const deleteMultiInstanceUser = async (row) => {
await proxy?.$modal.confirm('是否确认提交?');
loading.value = true;
buttonDisabled.value = true;
const taskOperationBo = reactive<TaskOperationBo>({
userIds: [row.userId],
taskId: task.value.id,
message: ''
});
await taskOperation(taskOperationBo, 'reductionSignature').finally(() => {
loading.value = false;
buttonDisabled.value = false;
});
visible.value = false;
emits('submitCallback');
proxy?.$modal.msgSuccess('操作成功');
};
//获取办理人
const handleTaskUser = async () => {
const data = await currentTaskAllUser(task.value.id);
deleteUserList.value = data.data;
if (deleteUserList.value && deleteUserList.value.length > 0) {
deleteUserList.value.forEach((e) => {
e.nodeName = task.value.nodeName;
});
}
deleteSignatureVisible.value = true;
};
//终止任务
const handleTerminationTask = async () => {
const params = {
taskId: task.value.id,
comment: ''
};
await proxy?.$modal.confirm('是否确认终止?');
loading.value = true;
buttonDisabled.value = true;
await terminationTask(params).finally(() => {
loading.value = false;
buttonDisabled.value = false;
});
visible.value = false;
emits('submitCallback');
proxy?.$modal.msgSuccess('操作成功');
};
/**
* 对外暴露子组件方法
*/
defineExpose({
open
});
</script>

View File

@@ -0,0 +1,529 @@
<template>
<el-dialog v-model="dialog.visible" :title="dialog.title" width="50%" draggable :before-close="cancel" center :close-on-click-modal="false">
<el-form v-loading="loading" :model="form" label-width="120px">
<el-form-item label="消息提醒">
<el-checkbox-group v-model="form.messageType">
<el-checkbox value="1" name="type" disabled>站内信</el-checkbox>
<el-checkbox value="2" name="type">邮件</el-checkbox>
<el-checkbox value="3" name="type">短信</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="附件">
<fileUpload v-model="form.fileId" :file-type="['png', 'jpg', 'jpeg', 'doc', 'docx', 'xlsx', 'xls', 'ppt', 'txt', 'pdf']" :file-size="20" />
</el-form-item>
<el-form-item label="抄送" v-if="buttonObj.copy">
<el-button type="primary" icon="Plus" circle @click="openUserSelectCopy" />
<el-tag v-for="user in selectCopyUserList" :key="user.userId" closable style="margin: 2px" @close="handleCopyCloseTag(user)">
{{ user.nickName }}
</el-tag>
</el-form-item>
<el-form-item v-if="buttonObj.pop && nestNodeList && nestNodeList.length > 0" label="下一步审批人" prop="assigneeMap">
<div v-for="(item, index) in nestNodeList" :key="index" style="margin-bottom: 5px; width: 500px">
<span>{{ item.nodeName }}</span>
<el-input v-if="false" v-model="form.assigneeMap[item.nodeCode]" />
<el-input placeholder="请选择审批人" readonly v-model="nickName[item.nodeCode]">
<template v-slot:append>
<el-button @click="choosePeople(item)" icon="search">选择</el-button>
</template>
</el-input>
</div>
</el-form-item>
<el-form-item v-if="task.flowStatus === 'waiting'" label="审批意见">
<el-input v-model="form.message" type="textarea" resize="none" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="buttonDisabled" type="primary" @click="handleCompleteTask"> 提交 </el-button>
<el-button v-if="task.flowStatus === 'waiting' && buttonObj.trust" :disabled="buttonDisabled" type="primary" @click="openDelegateTask">
委托
</el-button>
<el-button v-if="task.flowStatus === 'waiting' && buttonObj.transfer" :disabled="buttonDisabled" type="primary" @click="openTransferTask">
转办
</el-button>
<el-button
v-if="task.flowStatus === 'waiting' && Number(task.nodeRatio) > 0 && buttonObj.addSign"
:disabled="buttonDisabled"
type="primary"
@click="openMultiInstanceUser"
>
加签
</el-button>
<el-button
v-if="task.flowStatus === 'waiting' && Number(task.nodeRatio) > 0 && buttonObj.subSign"
:disabled="buttonDisabled"
type="primary"
@click="handleTaskUser"
>
减签
</el-button>
<el-button
v-if="task.flowStatus === 'waiting' && buttonObj.termination"
:disabled="buttonDisabled"
type="danger"
@click="handleTerminationTask"
>
终止
</el-button>
<el-button v-if="task.flowStatus === 'waiting' && buttonObj.back" :disabled="buttonDisabled" type="danger" @click="handleBackProcessOpen">
退回
</el-button>
<el-button :disabled="buttonDisabled" @click="cancel">取消</el-button>
</span>
</template>
<!-- 抄送 -->
<UserSelect ref="userSelectCopyRef" :multiple="true" :data="selectCopyUserIds" @confirm-call-back="userSelectCopyCallBack"></UserSelect>
<!-- 转办 -->
<UserSelect ref="transferTaskRef" :multiple="false" @confirm-call-back="handleTransferTask"></UserSelect>
<!-- 委托 -->
<UserSelect ref="delegateTaskRef" :multiple="false" @confirm-call-back="handleDelegateTask"></UserSelect>
<!-- 加签组件 -->
<UserSelect ref="multiInstanceUserRef" :multiple="true" @confirm-call-back="addMultiInstanceUser"></UserSelect>
<!-- 弹窗选人 -->
<UserSelect ref="porUserRef" :multiple="true" :userIds="popUserIds" @confirm-call-back="handlePopUser"></UserSelect>
<!-- 驳回开始 -->
<el-dialog v-model="backVisible" draggable title="驳回" width="40%" :close-on-click-modal="false">
<el-form v-if="task.flowStatus === 'waiting'" v-loading="backLoading" :model="backForm" label-width="120px">
<el-form-item label="驳回节点">
<el-select v-model="backForm.nodeCode" clearable placeholder="请选择" style="width: 300px">
<el-option v-for="item in taskNodeList" :key="item.nodeCode" :label="item.nodeName" :value="item.nodeCode" />
</el-select>
</el-form-item>
<el-form-item label="消息提醒">
<el-checkbox-group v-model="backForm.messageType">
<el-checkbox label="1" name="type" disabled>站内信</el-checkbox>
<el-checkbox label="2" name="type">邮件</el-checkbox>
<el-checkbox label="3" name="type">短信</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item v-if="task.flowStatus === 'waiting'" label="附件">
<fileUpload
v-model="backForm.fileId"
:file-type="['png', 'jpg', 'jpeg', 'doc', 'docx', 'xlsx', 'xls', 'ppt', 'txt', 'pdf']"
:file-size="20"
/>
</el-form-item>
<el-form-item label="审批意见">
<el-input v-model="backForm.message" type="textarea" resize="none" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer" style="float: right; padding-bottom: 20px">
<el-button :disabled="backButtonDisabled" type="primary" @click="handleBackProcess">确认</el-button>
<el-button :disabled="backButtonDisabled" @click="backVisible = false">取消</el-button>
</div>
</template>
</el-dialog>
<!-- 驳回结束 -->
<el-dialog v-model="deleteSignatureVisible" draggable title="减签人员" width="700px" height="400px" append-to-body :close-on-click-modal="false">
<div>
<el-table :data="deleteUserList" border>
<el-table-column prop="nodeName" label="任务名称" />
<el-table-column prop="nickName" label="办理人" />
<el-table-column label="操作" align="center" width="160">
<template #default="scope">
<el-button type="danger" size="small" icon="Delete" @click="deleteMultiInstanceUser(scope.row)">删除 </el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ComponentInternalInstance } from 'vue';
import { ElForm } from 'element-plus';
import {
completeTask,
backProcess,
getTask,
taskOperation,
terminationTask,
getBackTaskNode,
currentTaskAllUser,
getNextNodeList
} from '@/api/workflow/task';
import UserSelect from '@/components/UserSelect';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { UserVO } from '@/api/system/user/types';
import { FlowTaskVO, TaskOperationBo } from '@/api/workflow/task/types';
const userSelectCopyRef = ref<InstanceType<typeof UserSelect>>();
const transferTaskRef = ref<InstanceType<typeof UserSelect>>();
const delegateTaskRef = ref<InstanceType<typeof UserSelect>>();
const multiInstanceUserRef = ref<InstanceType<typeof UserSelect>>();
const porUserRef = ref<InstanceType<typeof UserSelect>>();
const props = defineProps({
taskVariables: {
type: Object as () => Record<string, any>,
default: () => {}
}
});
//遮罩层
const loading = ref(true);
//按钮
const buttonDisabled = ref(true);
//任务id
const taskId = ref<string>('');
//抄送人
const selectCopyUserList = ref<UserVO[]>([]);
//抄送人id
const selectCopyUserIds = ref<string>(undefined);
//可减签的人员
const deleteUserList = ref<any>([]);
//弹窗可选择的人员id
const popUserIds = ref<any>([]);
//驳回是否显示
const backVisible = ref(false);
const backLoading = ref(true);
const backButtonDisabled = ref(true);
// 可驳回得任务节点
const taskNodeList = ref([]);
const nickName = ref({});
//节点编码
const nodeCode = ref<string>('');
const buttonObj = ref<any>({
pop: false,
trust: false,
transfer: false,
addSign: false,
subSign: false,
termination: false,
back: false
});
//下一节点列表
const nestNodeList = ref([]);
//任务
const task = ref<FlowTaskVO>({
id: undefined,
createTime: undefined,
updateTime: undefined,
tenantId: undefined,
definitionId: undefined,
instanceId: undefined,
flowName: undefined,
businessId: undefined,
nodeCode: undefined,
nodeName: undefined,
flowCode: undefined,
flowStatus: undefined,
formCustom: undefined,
formPath: undefined,
nodeType: undefined,
nodeRatio: undefined,
applyNode: false,
buttonList: []
});
const dialog = reactive<DialogOption>({
visible: false,
title: '提示'
});
//减签弹窗
const deleteSignatureVisible = ref(false);
const form = ref<Record<string, any>>({
taskId: undefined,
message: undefined,
assigneeMap: {},
variables: {},
messageType: ['1'],
flowCopyList: []
});
const backForm = ref<Record<string, any>>({
taskId: undefined,
nodeCode: undefined,
message: undefined,
variables: {},
messageType: ['1']
});
//打开弹窗
const openDialog = async (id?: string) => {
selectCopyUserIds.value = undefined;
selectCopyUserList.value = [];
form.value.fileId = undefined;
taskId.value = id;
form.value.message = undefined;
dialog.visible = true;
loading.value = true;
buttonDisabled.value = true;
const response = await getTask(taskId.value);
task.value = response.data;
buttonObj.value = {};
task.value.buttonList.forEach((e) => {
buttonObj.value[e.code] = e.show;
});
buttonDisabled.value = false;
const data = {
taskId: taskId.value,
variables: props.taskVariables
};
const nextData = await getNextNodeList(data);
nestNodeList.value = nextData.data;
loading.value = false;
};
onMounted(() => {});
const emits = defineEmits(['submitCallback', 'cancelCallback']);
/** 办理流程 */
const handleCompleteTask = async () => {
form.value.taskId = taskId.value;
form.value.variables = props.taskVariables;
let verify = false;
if (buttonObj.value.pop && nestNodeList.value && nestNodeList.value.length > 0) {
nestNodeList.value.forEach((e) => {
if (
Object.keys(form.value.assigneeMap).length === 0 ||
form.value.assigneeMap[e.nodeCode] === '' ||
form.value.assigneeMap[e.nodeCode] === null ||
form.value.assigneeMap[e.nodeCode] === undefined
) {
verify = true;
}
});
if (verify) {
proxy?.$modal.msgWarning('请选择审批人!');
return false;
}
} else {
form.value.assigneeMap = {};
}
if (selectCopyUserList.value && selectCopyUserList.value.length > 0) {
const flowCopyList = [];
selectCopyUserList.value.forEach((e) => {
const copyUser = {
userId: e.userId,
userName: e.nickName
};
flowCopyList.push(copyUser);
});
form.value.flowCopyList = flowCopyList;
}
await proxy?.$modal.confirm('是否确认提交?');
loading.value = true;
buttonDisabled.value = true;
try {
await completeTask(form.value);
dialog.visible = false;
emits('submitCallback');
proxy?.$modal.msgSuccess('操作成功');
} finally {
loading.value = false;
buttonDisabled.value = false;
}
};
/** 驳回弹窗打开 */
const handleBackProcessOpen = async () => {
backForm.value = {};
backForm.value.messageType = ['1'];
backVisible.value = true;
backLoading.value = true;
backButtonDisabled.value = true;
const data = await getBackTaskNode(task.value.definitionId, task.value.nodeCode);
taskNodeList.value = data.data;
backLoading.value = false;
backButtonDisabled.value = false;
backForm.value.nodeCode = taskNodeList.value[0].nodeCode;
};
/** 驳回流程 */
const handleBackProcess = async () => {
backForm.value.taskId = taskId.value;
await proxy?.$modal.confirm('是否确认驳回到申请人?');
loading.value = true;
backLoading.value = true;
backButtonDisabled.value = true;
await backProcess(backForm.value).finally(() => {
loading.value = false;
buttonDisabled.value = false;
});
dialog.visible = false;
backLoading.value = false;
backButtonDisabled.value = false;
emits('submitCallback');
proxy?.$modal.msgSuccess('操作成功');
};
//取消
const cancel = async () => {
dialog.visible = false;
buttonDisabled.value = false;
nickName.value = {};
form.value.assigneeMap = {};
emits('cancelCallback');
};
//打开抄送人员
const openUserSelectCopy = () => {
userSelectCopyRef.value.open();
};
//确认抄送人员
const userSelectCopyCallBack = (data: UserVO[]) => {
if (data && data.length > 0) {
selectCopyUserList.value = data;
selectCopyUserIds.value = selectCopyUserList.value.map((item) => item.userId).join(',');
}
};
//删除抄送人员
const handleCopyCloseTag = (user: UserVO) => {
const userId = user.userId;
// 使用split删除用户
const index = selectCopyUserList.value.findIndex((item) => item.userId === userId);
selectCopyUserList.value.splice(index, 1);
selectCopyUserIds.value = selectCopyUserList.value.map((item) => item.userId).join(',');
};
//加签
const openMultiInstanceUser = async () => {
multiInstanceUserRef.value.open();
};
//加签
const addMultiInstanceUser = async (data) => {
if (data && data.length > 0) {
const taskOperationBo = reactive<TaskOperationBo>({
userIds: data.map((e) => e.userId),
taskId: taskId.value,
message: form.value.message
});
await proxy?.$modal.confirm('是否确认提交?');
loading.value = true;
buttonDisabled.value = true;
await taskOperation(taskOperationBo, 'addSignature').finally(() => {
loading.value = false;
buttonDisabled.value = false;
});
dialog.visible = false;
emits('submitCallback');
proxy?.$modal.msgSuccess('操作成功');
} else {
proxy?.$modal.msgWarning('请选择用户!');
}
};
//减签
const deleteMultiInstanceUser = async (row) => {
await proxy?.$modal.confirm('是否确认提交?');
loading.value = true;
buttonDisabled.value = true;
const taskOperationBo = reactive<TaskOperationBo>({
userIds: [row.userId],
taskId: taskId.value,
message: form.value.message
});
await taskOperation(taskOperationBo, 'reductionSignature').finally(() => {
loading.value = false;
buttonDisabled.value = false;
});
dialog.visible = false;
emits('submitCallback');
proxy?.$modal.msgSuccess('操作成功');
};
//打开转办
const openTransferTask = () => {
transferTaskRef.value.open();
};
//转办
const handleTransferTask = async (data) => {
if (data && data.length > 0) {
const taskOperationBo = reactive<TaskOperationBo>({
userId: data[0].userId,
taskId: taskId.value,
message: form.value.message
});
await proxy?.$modal.confirm('是否确认提交?');
loading.value = true;
buttonDisabled.value = true;
await taskOperation(taskOperationBo, 'transferTask').finally(() => {
loading.value = false;
buttonDisabled.value = false;
});
dialog.visible = false;
emits('submitCallback');
proxy?.$modal.msgSuccess('操作成功');
} else {
proxy?.$modal.msgWarning('请选择用户!');
}
};
//打开委托
const openDelegateTask = () => {
delegateTaskRef.value.open();
};
//委托
const handleDelegateTask = async (data) => {
if (data && data.length > 0) {
const taskOperationBo = reactive<TaskOperationBo>({
userId: data[0].userId,
taskId: taskId.value,
message: form.value.message
});
await proxy?.$modal.confirm('是否确认提交?');
loading.value = true;
buttonDisabled.value = true;
await taskOperation(taskOperationBo, 'delegateTask').finally(() => {
loading.value = false;
buttonDisabled.value = false;
});
dialog.visible = false;
emits('submitCallback');
proxy?.$modal.msgSuccess('操作成功');
} else {
proxy?.$modal.msgWarning('请选择用户!');
}
};
//终止任务
const handleTerminationTask = async () => {
const params = {
taskId: taskId.value,
comment: form.value.message
};
await proxy?.$modal.confirm('是否确认终止?');
loading.value = true;
buttonDisabled.value = true;
await terminationTask(params).finally(() => {
loading.value = false;
buttonDisabled.value = false;
});
dialog.visible = false;
emits('submitCallback');
proxy?.$modal.msgSuccess('操作成功');
};
const handleTaskUser = async () => {
const data = await currentTaskAllUser(taskId.value);
deleteUserList.value = data.data;
if (deleteUserList.value && deleteUserList.value.length > 0) {
deleteUserList.value.forEach((e) => {
e.nodeName = task.value.nodeName;
});
}
deleteSignatureVisible.value = true;
};
// 选择人员
const choosePeople = async (data) => {
if (!data.permissionFlag) {
proxy?.$modal.msgError('没有可选择的人员,请联系管理员!');
}
popUserIds.value = data.permissionFlag;
nodeCode.value = data.nodeCode;
porUserRef.value.open();
};
//确认选择
const handlePopUser = async (userList) => {
const userIds = userList.map((item) => {
return item.userId;
});
const nickNames = userList.map((item) => {
return item.nickName;
});
form.value.assigneeMap[nodeCode.value] = userIds.join(',');
nickName.value[nodeCode.value] = nickNames.join(',');
};
/**
* 对外暴露子组件方法
*/
defineExpose({
openDialog
});
</script>

View File

@@ -0,0 +1,102 @@
<template>
<div class="top-right-btn" :style="style">
<el-row>
<el-tooltip v-if="search" class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top">
<el-button circle icon="Search" @click="toggleSearch()" />
</el-tooltip>
<el-tooltip class="item" effect="dark" content="刷新" placement="top">
<el-button circle icon="Refresh" @click="refresh()" />
</el-tooltip>
<el-tooltip v-if="columns" class="item" effect="dark" content="显示/隐藏列" placement="top">
<div class="show-btn">
<el-popover placement="bottom" trigger="click">
<div class="tree-header">显示/隐藏列</div>
<el-tree
ref="columnRef"
:data="columns"
show-checkbox
node-key="key"
:props="{ label: 'label', children: 'children' } as any"
@check="columnChange"
></el-tree>
<template #reference>
<el-button circle icon="Menu" />
</template>
</el-popover>
</div>
</el-tooltip>
</el-row>
</div>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
const props = defineProps({
showSearch: propTypes.bool.def(true),
columns: propTypes.fieldOption,
search: propTypes.bool.def(true),
gutter: propTypes.number.def(10)
});
const columnRef = ref<ElTreeInstance>();
const emits = defineEmits(['update:showSearch', 'queryTable']);
const style = computed(() => {
const ret: any = {};
if (props.gutter) {
ret.marginRight = `${props.gutter / 2}px`;
}
return ret;
});
// 搜索
function toggleSearch() {
emits('update:showSearch', !props.showSearch);
}
// 刷新
function refresh() {
emits('queryTable');
}
// 更改数据列的显示和隐藏
function columnChange(...args: any[]) {
props.columns?.forEach((item) => {
item.visible = args[1].checkedKeys.includes(item.key);
});
}
// 显隐列初始默认隐藏列
onMounted(() => {
props.columns?.forEach((item) => {
if (item.visible) {
columnRef.value?.setChecked(item.key, true, false);
// value.value.push(item.key);
}
});
});
</script>
<style lang="scss" scoped>
:deep(.el-transfer__button) {
border-radius: 50%;
display: block;
margin-left: 0px;
}
:deep(.el-transfer__button:first-child) {
margin-bottom: 10px;
}
.my-el-transfer {
text-align: center;
}
.tree-header {
width: 100%;
line-height: 24px;
text-align: center;
}
.show-btn {
margin-left: 12px;
}
</style>

View File

@@ -0,0 +1,250 @@
<template>
<div>
<el-dialog v-model="roleDialog.visible.value" :title="roleDialog.title.value" width="80%" append-to-body>
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="queryParams.roleName" placeholder="请输入角色名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="权限字符" prop="roleKey">
<el-input v-model="queryParams.roleKey" placeholder="请输入权限字符" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="hover">
<template #header>
<el-tag v-for="role in selectRoleList" :key="role.roleId" closable style="margin: 2px" @close="handleCloseTag(role)">
{{ role.roleName }}
</el-tag>
</template>
<vxe-table
ref="tableRef"
height="400px"
border
show-overflow
:data="roleList"
:loading="loading"
:row-config="{ keyField: 'roleId' }"
:checkbox-config="{ reserve: true, checkRowKeys: defaultSelectRoleIds }"
highlight-current-row
@checkbox-all="handleCheckboxAll"
@checkbox-change="handleCheckboxChange"
>
<vxe-column type="checkbox" width="50" align="center" />
<vxe-column v-if="false" key="roleId" label="角色编号" />
<vxe-column field="roleName" title="角色名称" />
<vxe-column field="roleKey" title="权限字符" />
<vxe-column field="roleSort" title="显示顺序" width="100" />
<vxe-column title="状态" align="center" width="100">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status"></dict-tag>
</template>
</vxe-column>
<vxe-column field="createTime" title="创建时间" align="center">
<template #default="scope">
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
</template>
</vxe-column>
</vxe-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="pageList"
/>
</el-card>
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="confirm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { RoleVO, RoleQuery } from '@/api/system/role/types';
import { VxeTableInstance } from 'vxe-table';
import useDialog from '@/hooks/useDialog';
import api from '@/api/system/role';
interface PropType {
modelValue?: RoleVO[] | RoleVO | undefined;
multiple?: boolean;
data?: string | number | (string | number)[];
}
const prop = withDefaults(defineProps<PropType>(), {
multiple: true,
modelValue: undefined,
data: undefined
});
const emit = defineEmits(['update:modelValue', 'confirmCallBack']);
const router = useRouter();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
const roleList = ref<RoleVO[]>();
const loading = ref(true);
const showSearch = ref(true);
const total = ref(0);
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
const selectRoleList = ref<RoleVO[]>([]);
const roleDialog = useDialog({
title: '角色选择'
});
const queryFormRef = ref<ElFormInstance>();
const tableRef = ref<VxeTableInstance<RoleVO>>();
const queryParams = ref<RoleQuery>({
pageNum: 1,
pageSize: 10,
roleName: '',
roleKey: '',
status: ''
});
const defaultSelectRoleIds = computed(() => computedIds(prop.data));
const confirm = () => {
emit('update:modelValue', selectRoleList.value);
emit('confirmCallBack', selectRoleList.value);
roleDialog.closeDialog();
};
const computedIds = (data) => {
if (data instanceof Array) {
return [...data];
} else if (typeof data === 'string') {
return data.split(',');
} else if (typeof data === 'number') {
return [data];
} else {
console.warn('<RoleSelect> The data type of data should be array or string or number, but I received other');
return [];
}
};
/**
* 查询角色列表
*/
const getList = () => {
loading.value = true;
api.listRole(proxy?.addDateRange(queryParams.value, dateRange.value)).then((res) => {
roleList.value = res.rows;
total.value = res.total;
loading.value = false;
});
};
const pageList = async () => {
await getList();
const roles = roleList.value.filter((item) => {
return selectRoleList.value.some((role) => role.roleId === item.roleId);
});
await tableRef.value.setCheckboxRow(roles, true);
};
/**
* 搜索按钮操作
*/
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置 */
const resetQuery = () => {
dateRange.value = ['', ''];
queryFormRef.value?.resetFields();
handleQuery();
};
const handleCheckboxChange = (checked) => {
if (!prop.multiple && checked.checked) {
tableRef.value.setCheckboxRow(selectRoleList.value, false);
selectRoleList.value = [];
}
const row = checked.row;
if (checked.checked) {
selectRoleList.value.push(row);
} else {
selectRoleList.value = selectRoleList.value.filter((item) => {
return item.roleId !== row.roleId;
});
}
};
const handleCheckboxAll = (checked) => {
const rows = roleList.value;
if (checked.checked) {
rows.forEach((row) => {
if (!selectRoleList.value.some((item) => item.roleId === row.roleId)) {
selectRoleList.value.push(row);
}
});
} else {
selectRoleList.value = selectRoleList.value.filter((item) => {
return !rows.some((row) => row.roleId === item.roleId);
});
}
};
const handleCloseTag = (user: RoleVO) => {
const roleId = user.roleId;
// 使用split删除用户
const index = selectRoleList.value.findIndex((item) => item.roleId === roleId);
const rows = selectRoleList.value[index];
tableRef.value?.setCheckboxRow(rows, false);
selectRoleList.value.splice(index, 1);
};
/**
* 初始化选中数据
*/
const initSelectRole = async () => {
if (defaultSelectRoleIds.value.length > 0) {
const { data } = await api.optionSelect(defaultSelectRoleIds.value);
selectRoleList.value = data;
const users = roleList.value.filter((item) => {
return defaultSelectRoleIds.value.includes(String(item.roleId));
});
await nextTick(() => {
tableRef.value.setCheckboxRow(users, true);
});
}
};
const close = () => {
roleDialog.closeDialog();
};
watch(
() => roleDialog.visible.value,
(newValue: boolean) => {
if (newValue) {
initSelectRole();
} else {
tableRef.value.clearCheckboxReserve();
tableRef.value.clearCheckboxRow();
resetQuery();
selectRoleList.value = [];
}
}
);
onMounted(() => {
getList(); // 初始化列表数据
});
defineExpose({
open: roleDialog.openDialog,
close: roleDialog.closeDialog
});
</script>

View File

@@ -0,0 +1,13 @@
<template>
<div>
<svg-icon icon-class="question" @click="goto" />
</div>
</template>
<script setup lang="ts">
const url = ref('https://plus-doc.dromara.org/');
function goto() {
window.open(url.value);
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<div>
<svg-icon icon-class="github" @click="goto" />
</div>
</template>
<script setup lang="ts">
const url = ref('https://gitee.com/dromara/RuoYi-Vue-Plus');
function goto() {
window.open(url.value);
}
</script>

View File

@@ -0,0 +1,9 @@
<template>
<div>
<svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
</div>
</template>
<script setup lang="ts">
const { isFullscreen, toggle } = useFullscreen();
</script>

View File

@@ -0,0 +1,41 @@
<template>
<div>
<el-dropdown trigger="click" @command="handleSetSize">
<div class="size-icon--style">
<svg-icon class-name="size-icon" icon-class="size" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script setup lang="ts">
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const size = computed(() => appStore.size);
const sizeOptions = ref([
{ label: '较大', value: 'large' },
{ label: '默认', value: 'default' },
{ label: '稍小', value: 'small' }
]);
const handleSetSize = (size: 'large' | 'default' | 'small') => {
appStore.setSize(size);
};
</script>
<style lang="scss" scoped>
.size-icon--style {
font-size: 18px;
line-height: 50px;
padding-right: 7px;
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<svg :class="svgClass" aria-hidden="true">
<use :xlink:href="iconName" :fill="color" />
</svg>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
const props = defineProps({
iconClass: propTypes.string.isRequired,
className: propTypes.string.def(''),
color: propTypes.string.def('')
});
const iconName = computed(() => `#icon-${props.iconClass}`);
const svgClass = computed(() => {
if (props.className) {
return `svg-icon ${props.className}`;
}
return 'svg-icon';
});
</script>
<style lang="scss" scoped>
.sub-el-icon,
.nav-icon {
display: inline-block;
font-size: 15px;
margin-right: 12px;
position: relative;
}
.svg-icon {
width: 1em;
height: 1em;
position: relative;
fill: currentColor;
vertical-align: -2px;
}
</style>

View File

@@ -0,0 +1,200 @@
<template>
<el-menu :default-active="activeMenu" mode="horizontal" :ellipsis="false" @select="handleSelect">
<template v-for="(item, index) in topMenus">
<el-menu-item v-if="index < visibleNumber" :key="index" :style="{ '--theme': theme }" :index="item.path"
><svg-icon v-if="item.meta && item.meta.icon && item.meta.icon !== '#'" :icon-class="item.meta ? item.meta.icon : ''" />
{{ item.meta?.title }}</el-menu-item
>
</template>
<!-- 顶部菜单超出数量折叠 -->
<el-sub-menu v-if="topMenus.length > visibleNumber" :style="{ '--theme': theme }" index="more">
<template #title>更多菜单</template>
<template v-for="(item, index) in topMenus">
<el-menu-item v-if="index >= visibleNumber" :key="index" :index="item.path"
><svg-icon :icon-class="item.meta ? item.meta.icon : ''" /> {{ item.meta?.title }}</el-menu-item
>
</template>
</el-sub-menu>
</el-menu>
</template>
<script setup lang="ts">
import { constantRoutes } from '@/router';
import { isHttp } from '@/utils/validate';
import { useAppStore } from '@/store/modules/app';
import { useSettingsStore } from '@/store/modules/settings';
import { usePermissionStore } from '@/store/modules/permission';
import { RouteRecordRaw } from 'vue-router';
// 顶部栏初始数
const visibleNumber = ref<number>(-1);
// 当前激活菜单的 index
const currentIndex = ref<string>();
// 隐藏侧边栏路由
const hideList = ['/index', '/user/profile'];
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const route = useRoute();
const router = useRouter();
// 主题颜色
const theme = computed(() => settingsStore.theme);
// 所有的路由信息
const routers = computed(() => permissionStore.getTopbarRoutes());
// 顶部显示菜单
const topMenus = computed(() => {
const topMenus: RouteRecordRaw[] = [];
routers.value.map((menu) => {
if (menu.hidden !== true) {
// 兼容顶部栏一级菜单内部跳转
if (menu.path === '/' && menu.children) {
topMenus.push(menu.children ? menu.children[0] : menu);
} else {
topMenus.push(menu);
}
}
});
return topMenus;
});
// 设置子路由
const childrenMenus = computed(() => {
const childrenMenus: RouteRecordRaw[] = [];
routers.value.map((router) => {
router.children?.forEach((item) => {
if (item.parentPath === undefined) {
if (router.path === '/') {
item.path = '/' + item.path;
} else {
if (!isHttp(item.path)) {
item.path = router.path + '/' + item.path;
}
}
item.parentPath = router.path;
}
childrenMenus.push(item);
});
});
return constantRoutes.concat(childrenMenus);
});
// 默认激活的菜单
const activeMenu = computed(() => {
let path = route.path;
if (path === '/index') {
path = '/system/user';
}
let activePath = path;
if (path !== undefined && path.lastIndexOf('/') > 0 && hideList.indexOf(path) === -1) {
const tmpPath = path.substring(1, path.length);
if (!route.meta.link) {
activePath = '/' + tmpPath.substring(0, tmpPath.indexOf('/'));
appStore.toggleSideBarHide(false);
}
} else if (!route.children) {
activePath = path;
appStore.toggleSideBarHide(true);
}
activeRoutes(activePath);
return activePath;
});
const setVisibleNumber = () => {
const width = document.body.getBoundingClientRect().width / 3;
visibleNumber.value = parseInt(String(width / 85));
};
const handleSelect = (key: string) => {
currentIndex.value = key;
const route = routers.value.find((item) => item.path === key);
if (isHttp(key)) {
// http(s):// 路径新窗口打开
window.open(key, '_blank');
} else if (!route || !route.children) {
// 没有子路由路径内部打开
const routeMenu = childrenMenus.value.find((item) => item.path === key);
if (routeMenu && routeMenu.query) {
const query = JSON.parse(routeMenu.query);
router.push({ path: key, query: query });
} else {
router.push({ path: key });
}
appStore.toggleSideBarHide(true);
} else {
// 显示左侧联动菜单
activeRoutes(key);
appStore.toggleSideBarHide(false);
}
};
const activeRoutes = (key: string) => {
const routes: RouteRecordRaw[] = [];
if (childrenMenus.value && childrenMenus.value.length > 0) {
childrenMenus.value.map((item) => {
if (key == item.parentPath || (key == 'index' && '' == item.path)) {
routes.push(item);
}
});
}
if (routes.length > 0) {
permissionStore.setSidebarRouters(routes);
} else {
appStore.toggleSideBarHide(true);
}
return routes;
};
onMounted(() => {
window.addEventListener('resize', setVisibleNumber);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber);
});
onMounted(() => {
setVisibleNumber();
});
</script>
<style lang="scss">
.topmenu-container.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #999093 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active,
.el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
border-bottom: 2px solid #{'var(--theme)'} !important;
color: #303133;
}
/* sub-menu item */
.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #999093 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
/* 背景色隐藏 */
.topmenu-container.el-menu--horizontal > .el-menu-item:not(.is-disabled):focus,
.topmenu-container.el-menu--horizontal > .el-menu-item:not(.is-disabled):hover,
.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title:hover {
background-color: #ffffff !important;
}
/* 图标右间距 */
.topmenu-container .svg-icon {
margin-right: 4px;
}
</style>

View File

@@ -0,0 +1,308 @@
<template>
<div>
<el-dialog v-model="userDialog.visible.value" :title="userDialog.title.value" width="80%" append-to-body>
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" style="">
<el-card shadow="hover">
<el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
<el-tree
ref="deptTreeRef"
class="mt-2"
node-key="id"
:data="deptOptions"
:props="{ label: 'label', children: 'children' } as any"
:expand-on-click-node="false"
:filter-node-method="filterNode"
highlight-current
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</el-col>
<el-col :lg="20" :xs="24">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="用户名称" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="() => resetQuery()">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="hover">
<template v-if="prop.multiple" #header>
<el-tag v-for="user in selectUserList" :key="user.userId" closable style="margin: 2px" @close="handleCloseTag(user)">
{{ user.nickName }}
</el-tag>
</template>
<vxe-table
ref="tableRef"
height="400px"
border
show-overflow
:data="userList"
:loading="loading"
:row-config="{ keyField: 'userId', isHover: true }"
:checkbox-config="{ reserve: true, trigger: 'row', highlight: true, showHeader: prop.multiple }"
@checkbox-all="handleCheckboxAll"
@checkbox-change="handleCheckboxChange"
>
<vxe-column type="checkbox" width="50" align="center" />
<vxe-column key="userId" title="用户编号" align="center" field="userId" />
<vxe-column key="userName" title="用户名称" align="center" field="userName" />
<vxe-column key="nickName" title="用户昵称" align="center" field="nickName" />
<vxe-column key="deptName" title="部门" align="center" field="deptName" />
<vxe-column key="phonenumber" title="手机号码" align="center" field="phonenumber" width="120" />
<vxe-column key="status" title="状态" align="center">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status"></dict-tag>
</template>
</vxe-column>
<vxe-column title="创建时间" align="center" width="160">
<template #default="scope">
<span>{{ scope.row.createTime }}</span>
</template>
</vxe-column>
</vxe-table>
<pagination
v-show="total > 0"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
:total="total"
@pagination="pageList"
/>
</el-card>
</el-col>
</el-row>
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="confirm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import api from '@/api/system/user';
import { UserQuery, UserVO } from '@/api/system/user/types';
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
import { VxeTableInstance } from 'vxe-table';
import useDialog from '@/hooks/useDialog';
interface PropType {
modelValue?: UserVO[] | UserVO | undefined;
multiple?: boolean;
data?: string | number | (string | number)[] | undefined;
userIds?: string | number | (string | number)[] | undefined;
}
const prop = withDefaults(defineProps<PropType>(), {
multiple: true,
modelValue: undefined,
data: undefined,
userIds: undefined
});
const emit = defineEmits(['update:modelValue', 'confirmCallBack']);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
const userList = ref<UserVO[]>();
const loading = ref(true);
const showSearch = ref(true);
const total = ref(0);
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
const deptName = ref('');
const deptOptions = ref<DeptTreeVO[]>([]);
const selectUserList = ref<UserVO[]>([]);
const deptTreeRef = ref<ElTreeInstance>();
const queryFormRef = ref<ElFormInstance>();
const tableRef = ref<VxeTableInstance<UserVO>>();
const userDialog = useDialog({
title: '用户选择'
});
const queryParams = ref<UserQuery>({
pageNum: 1,
pageSize: 10,
userName: '',
phonenumber: '',
status: '',
deptId: '',
roleId: '',
userIds: ''
});
const defaultSelectUserIds = computed(() => computedIds(prop.data));
/** 根据名称筛选部门树 */
watchEffect(
() => {
deptTreeRef.value?.filter(deptName.value);
},
{
flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
}
);
const confirm = () => {
emit('update:modelValue', selectUserList.value);
emit('confirmCallBack', selectUserList.value);
userDialog.closeDialog();
};
const computedIds = (data) => {
if (data instanceof Array) {
return data.map((item) => String(item));
} else if (typeof data === 'string') {
return data.split(',');
} else if (typeof data === 'number') {
return [data];
} else {
console.warn('<UserSelect> The data type of data should be array or string or number, but I received other');
return [];
}
};
/** 通过条件过滤节点 */
const filterNode = (value: string, data: any) => {
if (!value) return true;
return data.label.indexOf(value) !== -1;
};
/** 查询部门下拉树结构 */
const getTreeSelect = async () => {
const res = await api.deptTreeSelect();
deptOptions.value = res.data;
};
/** 查询用户列表 */
const getList = async () => {
loading.value = true;
queryParams.value.userIds = prop.userIds;
const res = await api.listUser(proxy?.addDateRange(queryParams.value, dateRange.value));
loading.value = false;
userList.value = res.rows;
total.value = res.total;
};
const pageList = async () => {
await getList();
const users = userList.value.filter((item) => {
return selectUserList.value.some((user) => user.userId === item.userId);
});
await tableRef.value.setCheckboxRow(users, true);
};
/** 节点单击事件 */
const handleNodeClick = (data: DeptVO) => {
queryParams.value.deptId = data.id;
handleQuery();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = (refresh = true) => {
dateRange.value = ['', ''];
queryFormRef.value?.resetFields();
queryParams.value.pageNum = 1;
queryParams.value.deptId = undefined;
deptTreeRef.value?.setCurrentKey(undefined);
refresh && handleQuery();
};
const handleCheckboxChange = (checked) => {
if (!prop.multiple && checked.checked) {
tableRef.value.setCheckboxRow(selectUserList.value, false);
selectUserList.value = [];
}
const row = checked.row;
if (checked.checked) {
selectUserList.value.push(row);
} else {
selectUserList.value = selectUserList.value.filter((item) => {
return item.userId !== row.userId;
});
}
};
const handleCheckboxAll = (checked) => {
const rows = userList.value;
if (checked.checked) {
rows.forEach((row) => {
if (!selectUserList.value.some((item) => item.userId === row.userId)) {
selectUserList.value.push(row);
}
});
} else {
selectUserList.value = selectUserList.value.filter((item) => {
return !rows.some((row) => row.userId === item.userId);
});
}
};
const handleCloseTag = (user: UserVO) => {
const userId = user.userId;
// 使用split删除用户
const index = selectUserList.value.findIndex((item) => item.userId === userId);
const rows = selectUserList.value[index];
tableRef.value?.setCheckboxRow(rows, false);
selectUserList.value.splice(index, 1);
};
const initSelectUser = async () => {
if (defaultSelectUserIds.value.length > 0) {
const { data } = await api.optionSelect(defaultSelectUserIds.value);
selectUserList.value = data;
const users = userList.value.filter((item) => {
return defaultSelectUserIds.value.includes(String(item.userId));
});
await nextTick(() => {
tableRef.value.setCheckboxRow(users, true);
});
}
};
const close = () => {
userDialog.closeDialog();
};
watch(
() => userDialog.visible.value,
async (newValue: boolean) => {
if (newValue) {
await getTreeSelect(); // 初始化部门数据
await getList(); // 初始化列表数据
await initSelectUser();
} else {
tableRef.value.clearCheckboxReserve();
tableRef.value.clearCheckboxRow();
resetQuery(false);
selectUserList.value = [];
}
}
);
defineExpose({
open: userDialog.openDialog,
close: userDialog.closeDialog
});
</script>

View File

@@ -0,0 +1,26 @@
<template>
<div v-loading="loading" :style="'height:' + height">
<iframe :src="url" frameborder="no" style="width: 100%; height: 100%" scrolling="auto" />
</div>
</template>
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes';
const props = defineProps({
src: propTypes.string.isRequired
});
const height = ref(document.documentElement.clientHeight - 94.5 + 'px;');
const loading = ref(true);
const url = computed(() => props.src);
onMounted(() => {
setTimeout(() => {
loading.value = false;
}, 300);
window.onresize = function temp() {
height.value = document.documentElement.clientHeight - 94.5 + 'px;';
};
});
</script>