first commit
This commit is contained in:
95
src/components/Breadcrumb/index.vue
Normal file
95
src/components/Breadcrumb/index.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<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 = '/') => {
|
||||
let index = str.indexOf(char);
|
||||
let num = 0;
|
||||
while (index !== -1) {
|
||||
num++;
|
||||
index = str.indexOf(char, index + 1);
|
||||
}
|
||||
return num;
|
||||
};
|
||||
const getMatched = (pathList, routeList, matched) => {
|
||||
let 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>
|
61
src/components/BuildCode/index.vue
Normal file
61
src/components/BuildCode/index.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<!-- 代码构建 -->
|
||||
<div>
|
||||
<v-form-designer
|
||||
ref="buildRef"
|
||||
class="build"
|
||||
:designer-config="{ importJsonButton: true, exportJsonButton: true, exportCodeButton: true, generateSFCButton: true, formTemplates: true }"
|
||||
>
|
||||
<template v-if="showBtn" #customToolButtons>
|
||||
<el-button link type="primary" icon="Select" @click="getJson">保存</el-button>
|
||||
</template>
|
||||
</v-form-designer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
showBtn: boolean;
|
||||
formJson: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showBtn: true,
|
||||
formJson: ''
|
||||
});
|
||||
|
||||
const buildRef = ref();
|
||||
const emits = defineEmits(['reJson', 'saveDesign']);
|
||||
|
||||
//获取表单json
|
||||
const getJson = () => {
|
||||
const formJson = JSON.stringify(buildRef.value.getFormJson());
|
||||
const fieldJson = JSON.stringify(buildRef.value.getFieldWidgets());
|
||||
let data = {
|
||||
formJson,
|
||||
fieldJson
|
||||
};
|
||||
emits('saveDesign', data);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.formJson) {
|
||||
buildRef.value.setFormJson(props.formJson);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.build {
|
||||
margin: 0 !important;
|
||||
overflow-y: auto !important;
|
||||
|
||||
& header.main-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .right-toolbar-con {
|
||||
text-align: right !important;
|
||||
}
|
||||
}
|
||||
</style>
|
57
src/components/BuildCode/render.vue
Normal file
57
src/components/BuildCode/render.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<v-form-render ref="vFormRef" :form-json="formJson" :form-data="formData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 动态表单渲染 -->
|
||||
<script setup name="Render" lang="ts">
|
||||
interface Props {
|
||||
formJson: string | object;
|
||||
formData: string | object;
|
||||
isView: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
formJson: '',
|
||||
formData: '',
|
||||
isView: false
|
||||
});
|
||||
|
||||
const vFormRef = ref();
|
||||
// 获取表单数据-异步
|
||||
const getFormData = () => {
|
||||
return vFormRef.value.getFormData();
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置表单内容
|
||||
* @param {表单配置} formConf
|
||||
* formConfig:{ formTemplate:表单模板,formData:表单数据,hiddenField:需要隐藏的字段字符串集合,disabledField:需要禁用的自读字符串集合}
|
||||
*/
|
||||
const initForm = (formConf: any) => {
|
||||
const { formTemplate, formData, hiddenField, disabledField } = toRaw(formConf);
|
||||
if (formTemplate) {
|
||||
vFormRef.value.setFormJson(formTemplate);
|
||||
if (formData) {
|
||||
vFormRef.value.setFormData(formData);
|
||||
}
|
||||
if (disabledField && disabledField.length > 0) {
|
||||
setTimeout(() => {
|
||||
vFormRef.value.disableWidgets(disabledField);
|
||||
}, 200);
|
||||
}
|
||||
if (hiddenField && hiddenField.length > 0) {
|
||||
setTimeout(() => {
|
||||
vFormRef.value.hideWidgets(hiddenField);
|
||||
}, 200);
|
||||
}
|
||||
if (props.isView) {
|
||||
setTimeout(() => {
|
||||
vFormRef.value.disableForm();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
defineExpose({ getFormData, initForm });
|
||||
</script>
|
94
src/components/DictTag/index.vue
Normal file
94
src/components/DictTag/index.vue
Normal 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 scoped>
|
||||
.el-tag + .el-tag {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
244
src/components/Editor/index.vue
Normal file
244
src/components/Editor/index.vue
Normal 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(() => {
|
||||
let 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) {
|
||||
// 获取富文本实例
|
||||
let quill = toRaw(quillEditorRef.value).getQuill();
|
||||
// 获取光标位置
|
||||
let 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>
|
490
src/components/FileUpload/index.vue
Normal file
490
src/components/FileUpload/index.vue
Normal file
@ -0,0 +1,490 @@
|
||||
<template>
|
||||
<div class="upload-file">
|
||||
<el-upload
|
||||
ref="fileUploadRef"
|
||||
multiple
|
||||
:action="realUploadUrl"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:file-list="fileList"
|
||||
:limit="limit"
|
||||
:on-error="handleUploadError"
|
||||
:on-exceed="handleExceed"
|
||||
:on-success="handleUploadSuccess"
|
||||
:show-file-list="showFileList"
|
||||
:headers="headers"
|
||||
class="upload-file-uploader"
|
||||
:list-type="isConstruction ? 'picture-card' : 'text'"
|
||||
:accept="accept"
|
||||
:drag="isDarg"
|
||||
:data="data"
|
||||
:auto-upload="autoUpload"
|
||||
:on-change="handleChange"
|
||||
:on-remove="handleRemove"
|
||||
:method="method"
|
||||
:http-request="customUpload"
|
||||
>
|
||||
<slot>
|
||||
<div>
|
||||
<!-- 上传按钮 -->
|
||||
<el-button v-if="!isConstruction && !isImportInfo && !isDarg" type="primary">选取文件</el-button>
|
||||
|
||||
<!-- 上传提示 -->
|
||||
<el-icon v-if="isDarg" class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div v-if="showTip" class="el-upload__tip" @click.stop>
|
||||
请上传
|
||||
<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
|
||||
v-if="!isConstruction && !isImportInfo"
|
||||
class="upload-file-list el-upload-list el-upload-list--text"
|
||||
name="el-fade-in-linear"
|
||||
tag="ul"
|
||||
@click.stop
|
||||
>
|
||||
<li
|
||||
style="margin-top: 10px;"
|
||||
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" link @click="handleDelete(index)">删除</el-button>
|
||||
</div>
|
||||
</li>
|
||||
</transition-group>
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<el-icon v-if="isConstruction"><Plus /></el-icon>
|
||||
<template #file="{ file }">
|
||||
<div class="pdf" v-if="isConstruction">
|
||||
<img src="@/assets/icons/svg/pdf.png" alt="" />
|
||||
<el-text class="w-148px text-center" truncated>
|
||||
<span>{{ file.name }}</span>
|
||||
</el-text>
|
||||
<div class="Shadow">
|
||||
<a :href="file.url" target="_blank">
|
||||
<el-icon class="mr"><View /></el-icon>
|
||||
</a>
|
||||
<a href="#">
|
||||
<el-icon @click="handleDelete((file as any).ossId, 'ossId')"><Delete /></el-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
import { delOss, listByIds } from '@/api/system/oss';
|
||||
import { globalHeaders } from '@/utils/request';
|
||||
import axios from 'axios';
|
||||
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', 'xls', 'ppt', 'txt', 'pdf', 'png', 'jpg', 'jpeg', 'zip']),
|
||||
// 是否显示提示
|
||||
isShowTip: propTypes.bool.def(true),
|
||||
//是否为施工人员上传
|
||||
isConstruction: propTypes.bool.def(false),
|
||||
//是否为上传zip文件
|
||||
isImportInfo: propTypes.bool.def(false),
|
||||
//ip地址
|
||||
uploadUrl: propTypes.string.def('/resource/oss/upload'),
|
||||
//可拖拽上传
|
||||
isDarg: propTypes.bool.def(false),
|
||||
// 是否自动上传
|
||||
autoUpload: propTypes.bool.def(true),
|
||||
// 是否显示文件列表
|
||||
showFileList: propTypes.bool.def(false),
|
||||
// 其他参数
|
||||
data: propTypes.object.def({}),
|
||||
// 成功回调
|
||||
onUploadSuccess: {
|
||||
type: Function as PropType<(files: any[], res: any) => void>,
|
||||
default: undefined
|
||||
},
|
||||
// 上传方法
|
||||
method: propTypes.string.def('post'),
|
||||
|
||||
// 失败回调
|
||||
onUploadError: {
|
||||
type: Function as PropType<(err: any, file: any, fileList: any) => void>,
|
||||
default: undefined
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const emit = defineEmits(['update:modelValue', 'handleChange', 'handleRemove']);
|
||||
|
||||
const number = ref(0);
|
||||
const uploadList = ref<any[]>([]);
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API;
|
||||
const uploadFileUrl = ref(baseUrl + props.uploadUrl); // 上传文件服务器地址
|
||||
const headers = ref(globalHeaders());
|
||||
const pendingFiles = ref<UploadFile[]>([]);
|
||||
|
||||
const realUploadUrl = computed(() => {
|
||||
const search = new URLSearchParams(props.params).toString();
|
||||
return search ? `${baseUrl}${props.uploadUrl}?${search}` : `${baseUrl}${props.uploadUrl}`;
|
||||
});
|
||||
const fileList = ref<any[]>([]);
|
||||
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
|
||||
|
||||
const fileUploadRef = ref<ElUploadInstance>();
|
||||
|
||||
const accept = computed(() => {
|
||||
return props.fileType.map((value) => `.${value}`).join(',');
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (val) => {
|
||||
if (props.isImportInfo) return;
|
||||
if (val) {
|
||||
let temp = 1;
|
||||
// 首先将值转为数组
|
||||
let list: any[] = [];
|
||||
if (Array.isArray(val)) {
|
||||
list = val;
|
||||
} else {
|
||||
const res = await listByIds(val as any);
|
||||
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('上传文件失败');
|
||||
};
|
||||
|
||||
// 上传成功回调
|
||||
interface UploadFileWithOssId extends UploadFile {
|
||||
ossId?: string;
|
||||
}
|
||||
|
||||
const handleUploadSuccess = (res: any, file: UploadFileWithOssId) => {
|
||||
if (res.code === 200) {
|
||||
console.log('上传成功');
|
||||
// 上传成功,不管 data 是否为空
|
||||
uploadList.value.push({
|
||||
name: file.name,
|
||||
url: (res.data && res.data.url) || '',
|
||||
ossId: (res.data && res.data.ossId) || ''
|
||||
});
|
||||
} else {
|
||||
console.log('失败', res);
|
||||
|
||||
number.value--;
|
||||
proxy?.$modal.closeLoading();
|
||||
proxy?.$modal.msgError(res.msg || '上传失败');
|
||||
fileUploadRef.value?.handleRemove(file);
|
||||
return;
|
||||
}
|
||||
|
||||
uploadedSuccessfully(res);
|
||||
};
|
||||
|
||||
const handleChange = (file: any, fileList: any) => {
|
||||
// 记录 status = 'ready' 的文件
|
||||
if (file.status === 'ready') {
|
||||
pendingFiles.value.push(file);
|
||||
}
|
||||
|
||||
emit('handleChange', file, fileList);
|
||||
};
|
||||
|
||||
// 删除文件
|
||||
const handleRemove = (file: any, fileList: any) => {
|
||||
console.log(11);
|
||||
|
||||
emit('handleRemove', file, fileList);
|
||||
};
|
||||
|
||||
// 删除文件
|
||||
const handleDelete = async (index: string | number, type?: string) => {
|
||||
await proxy?.$modal.confirm('是否确认删除此文件?').finally();
|
||||
try {
|
||||
if (type === 'ossId') {
|
||||
delOss(index);
|
||||
fileList.value = fileList.value.filter((f) => f.ossId !== index);
|
||||
} else {
|
||||
let ossId = fileList.value[index].ossId;
|
||||
delOss(ossId);
|
||||
index = parseInt(index as string);
|
||||
fileList.value.splice(index, 1);
|
||||
}
|
||||
} finally {
|
||||
emit('handleRemove');
|
||||
emit('update:modelValue', listToString(fileList.value));
|
||||
}
|
||||
};
|
||||
|
||||
// 上传结束处理
|
||||
const uploadedSuccessfully = (res: any) => {
|
||||
if (props.isImportInfo) {
|
||||
emit('update:modelValue', 'ok');
|
||||
fileUploadRef.value?.clearFiles();
|
||||
proxy?.$modal.closeLoading();
|
||||
proxy?.$modal.msgSuccess('导入成功');
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
if (props.autoUpload && props.limit === fileList.value.length) {
|
||||
fileUploadRef.value?.clearFiles();
|
||||
fileList.value = [];
|
||||
emit('update:modelValue', ''); // 同步到外部 v-model
|
||||
}
|
||||
props.onUploadSuccess?.(fileList.value, res);
|
||||
};
|
||||
|
||||
// 获取文件名称
|
||||
const getFileName = (name: string) => {
|
||||
// 如果是url那么取最后的名字 如果不是直接返回
|
||||
if (!name) return '';
|
||||
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) : '';
|
||||
};
|
||||
|
||||
// 改造后的 customUpload
|
||||
const customUpload = async (options: any) => {
|
||||
if (props.autoUpload) {
|
||||
// 自动上传,单文件请求
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', options.file);
|
||||
Object.entries(props.data).forEach(([k, v]) => {
|
||||
if (v !== null && v !== undefined) formData.append(k, v as any);
|
||||
});
|
||||
const res = await axios?.({
|
||||
url: realUploadUrl.value,
|
||||
method: props.method,
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data', ...headers.value }
|
||||
});
|
||||
handleUploadSuccess(res.data, options.file);
|
||||
} catch (err) {
|
||||
handleUploadError();
|
||||
}
|
||||
} else {
|
||||
// 手动上传,不发请求,只缓存
|
||||
pendingFiles.value.push(options.file);
|
||||
}
|
||||
};
|
||||
|
||||
// 改造后的 submitUpload
|
||||
const submitUpload = async () => {
|
||||
if (props.autoUpload) {
|
||||
fileUploadRef.value?.submit();
|
||||
return;
|
||||
}
|
||||
if (!pendingFiles.value.length) {
|
||||
return 'noFile';
|
||||
}
|
||||
try {
|
||||
proxy?.$modal.loading('正在上传文件,请稍候...');
|
||||
const formData = new FormData();
|
||||
pendingFiles.value.forEach((f) => {
|
||||
if (f.raw) formData.append('file', f.raw as File);
|
||||
});
|
||||
Object.entries(props.data).forEach(([k, v]) => {
|
||||
if (v !== null && v !== undefined) formData.append(k, v as any);
|
||||
});
|
||||
const res = await axios?.({
|
||||
url: realUploadUrl.value,
|
||||
method: props.method,
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data', ...headers.value }
|
||||
});
|
||||
handleUploadSuccess(res.data, {} as any);
|
||||
pendingFiles.value = [];
|
||||
fileUploadRef.value?.clearFiles();
|
||||
} catch (err) {
|
||||
handleUploadError();
|
||||
} finally {
|
||||
proxy?.$modal.closeLoading();
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ submitUpload });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pdf {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
img {
|
||||
width: 40%;
|
||||
}
|
||||
&:hover {
|
||||
.Shadow {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
> span {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-file-list {
|
||||
margin: 0;
|
||||
.el-upload-list__item {
|
||||
border: 1px solid #e4e7ed;
|
||||
line-height: 2;
|
||||
margin-bottom: 0;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-file-list .ele-upload-list__item-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
}
|
||||
.Shadow {
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
cursor: default;
|
||||
display: inline-flex;
|
||||
font-size: 20px;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: opacity 0.3s;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ele-upload-list__item-content-action .el-link {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.el-icon.avatar-uploader-icon {
|
||||
border: 1px dashed #cdd0d6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.el-icon.avatar-uploader-icon:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.el-icon.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 200px;
|
||||
height: 178px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
35
src/components/Hamburger/index.vue
Normal file
35
src/components/Hamburger/index.vue
Normal 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>
|
195
src/components/HeaderSearch/index.vue
Normal file
195
src/components/HeaderSearch/index.vue
Normal file
@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div :class="{ show: show }" class="header-search">
|
||||
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
|
||||
<el-select
|
||||
ref="headerSearchSelectRef"
|
||||
v-model="search"
|
||||
:remote-method="querySearch"
|
||||
filterable
|
||||
default-first-option
|
||||
remote
|
||||
placeholder="Search"
|
||||
class="header-search-select"
|
||||
@change="change"
|
||||
>
|
||||
<el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="HeaderSearch">
|
||||
import Fuse from 'fuse.js';
|
||||
import { getNormalPath } from '@/utils/ruoyi';
|
||||
import { isHttp } from '@/utils/validate';
|
||||
import usePermissionStore from '@/store/modules/permission';
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
type Router = Array<{
|
||||
path: string;
|
||||
title: string[];
|
||||
}>;
|
||||
|
||||
const search = ref('');
|
||||
const options = ref<any>([]);
|
||||
const searchPool = ref<Router>([]);
|
||||
const show = ref(false);
|
||||
const fuse = ref();
|
||||
const headerSearchSelectRef = ref<ElSelectInstance>();
|
||||
const router = useRouter();
|
||||
const routes = computed(() => usePermissionStore().getRoutes());
|
||||
|
||||
const click = () => {
|
||||
show.value = !show.value;
|
||||
if (show.value) {
|
||||
headerSearchSelectRef.value && headerSearchSelectRef.value.focus();
|
||||
}
|
||||
};
|
||||
const close = () => {
|
||||
headerSearchSelectRef.value && headerSearchSelectRef.value.blur();
|
||||
options.value = [];
|
||||
show.value = false;
|
||||
};
|
||||
const change = (val: any) => {
|
||||
const path = val.path;
|
||||
const query = val.query;
|
||||
if (isHttp(path)) {
|
||||
// http(s):// 路径新窗口打开
|
||||
const pindex = path.indexOf('http');
|
||||
window.open(path.substr(pindex, path.length), '_blank');
|
||||
} else {
|
||||
if (query) {
|
||||
router.push({ path: path, query: JSON.parse(query) });
|
||||
} else {
|
||||
router.push(path);
|
||||
}
|
||||
}
|
||||
search.value = '';
|
||||
options.value = [];
|
||||
nextTick(() => {
|
||||
show.value = false;
|
||||
});
|
||||
};
|
||||
const initFuse = (list: Router) => {
|
||||
fuse.value = new Fuse(list, {
|
||||
shouldSort: true,
|
||||
threshold: 0.4,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
minMatchCharLength: 1,
|
||||
keys: [
|
||||
{
|
||||
name: 'title',
|
||||
weight: 0.7
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
weight: 0.3
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
// Filter out the routes that can be displayed in the sidebar
|
||||
// And generate the internationalized title
|
||||
const generateRoutes = (routes: RouteRecordRaw[], basePath = '', prefixTitle: string[] = []) => {
|
||||
let res: Router = [];
|
||||
routes.forEach((r) => {
|
||||
// skip hidden router
|
||||
if (!r.hidden) {
|
||||
const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
|
||||
const data = {
|
||||
path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
|
||||
title: [...prefixTitle],
|
||||
query: ''
|
||||
};
|
||||
if (r.meta && r.meta.title) {
|
||||
data.title = [...data.title, r.meta.title];
|
||||
if (r.redirect !== 'noRedirect') {
|
||||
// only push the routes with title
|
||||
// special case: need to exclude parent router without redirect
|
||||
res.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
if (r.query) {
|
||||
data.query = r.query;
|
||||
}
|
||||
|
||||
// recursive child routes
|
||||
if (r.children) {
|
||||
const tempRoutes = generateRoutes(r.children, data.path, data.title);
|
||||
if (tempRoutes.length >= 1) {
|
||||
res = [...res, ...tempRoutes];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
const querySearch = (query: string) => {
|
||||
if (query !== '') {
|
||||
options.value = fuse.value.search(query);
|
||||
} else {
|
||||
options.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
searchPool.value = generateRoutes(routes.value);
|
||||
});
|
||||
|
||||
// watchEffect(() => {
|
||||
// searchPool.value = generateRoutes(routes.value)
|
||||
// })
|
||||
|
||||
watch(show, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', close);
|
||||
} else {
|
||||
document.body.removeEventListener('click', close);
|
||||
}
|
||||
});
|
||||
|
||||
watch(searchPool, (list: Router) => {
|
||||
initFuse(list);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-search {
|
||||
font-size: 0 !important;
|
||||
|
||||
.search-icon {
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-search-select {
|
||||
font-size: 18px;
|
||||
transition: width 0.2s;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
box-shadow: none !important;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.show {
|
||||
.header-search-select {
|
||||
width: 210px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
104
src/components/IconSelect/index.vue
Normal file
104
src/components/IconSelect/index.vue
Normal 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 scoped lang="scss">
|
||||
.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>
|
7
src/components/IconSelect/requireIcons.ts
Normal file
7
src/components/IconSelect/requireIcons.ts
Normal 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;
|
80
src/components/ImagePreview/index.vue
Normal file
80
src/components/ImagePreview/index.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<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';
|
||||
import { PictureFilled } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
src: propTypes.string.def(''),
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const realSrc = computed(() => {
|
||||
if (!props.src) {
|
||||
return;
|
||||
}
|
||||
let real_src = props.src.split(',')[0];
|
||||
return real_src;
|
||||
});
|
||||
|
||||
const realSrcList = computed(() => {
|
||||
if (!props.src) {
|
||||
return [];
|
||||
}
|
||||
let real_src_list = props.src.split(',');
|
||||
let 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>
|
239
src/components/ImageUpload/index.vue
Normal file
239
src/components/ImageUpload/index.vue
Normal file
@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="component-upload-image">
|
||||
<el-upload
|
||||
ref="imageUpload"
|
||||
multiple
|
||||
:action="uploadImgUrl"
|
||||
list-type="picture-card"
|
||||
:on-success="handleUploadSuccess"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:limit="limit"
|
||||
: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 }"
|
||||
accept="image/png, image/jpeg, image/jpg"
|
||||
>
|
||||
<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>();
|
||||
|
||||
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) {
|
||||
let 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 (let 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 scoped lang="scss">
|
||||
// .el-upload--picture-card 控制加号部分
|
||||
:deep(.hide .el-upload--picture-card) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
39
src/components/LangSelect/index.vue
Normal file
39
src/components/LangSelect/index.vue
Normal 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>
|
94
src/components/Pagination/index.vue
Normal file
94
src/components/Pagination/index.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div :class="{ hidden: hidden }" class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:size="size"
|
||||
:background="background"
|
||||
:layout="layout"
|
||||
:page-sizes="pageSizes"
|
||||
:pager-count="pagerCount"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Pagination'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup 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'),
|
||||
size: propTypes.any
|
||||
});
|
||||
|
||||
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 {
|
||||
padding: 32px 16px;
|
||||
|
||||
.el-pagination {
|
||||
float: v-bind(float);
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
3
src/components/ParentView/index.vue
Normal file
3
src/components/ParentView/index.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
59
src/components/Process/approvalButton.vue
Normal file
59
src/components/Process/approvalButton.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<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 route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
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(route);
|
||||
router.go(-1);
|
||||
};
|
||||
</script>
|
131
src/components/Process/approvalRecord.vue
Normal file
131
src/components/Process/approvalRecord.vue
Normal file
@ -0,0 +1,131 @@
|
||||
<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) => {
|
||||
console.log(323232);
|
||||
|
||||
visible.value = true;
|
||||
loading.value = true;
|
||||
tabActiveName.value = 'image';
|
||||
historyList.value = [];
|
||||
console.log('🚀 ~ init ~ businessId:', businessId);
|
||||
|
||||
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>
|
41
src/components/Process/flowChart.vue
Normal file
41
src/components/Process/flowChart.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<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;
|
||||
console.log('🚀 ~ iframeUrl.value:', iframeUrl.value);
|
||||
});
|
||||
</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>
|
154
src/components/Process/flowChartImg.vue
Normal file
154
src/components/Process/flowChartImg.vue
Normal 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>
|
211
src/components/Process/processMeddle.vue
Normal file
211
src/components/Process/processMeddle.vue
Normal 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>
|
584
src/components/Process/submitVerify.vue
Normal file
584
src/components/Process/submitVerify.vue
Normal file
@ -0,0 +1,584 @@
|
||||
<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="isDrawing ? '800px' : '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>
|
||||
<div class="box" v-if="isDrawing">
|
||||
<span>设计验证</span>
|
||||
<detailForm ref="detailFormRef"></detailForm>
|
||||
</div>
|
||||
<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 v-model="isShowTermination" title="终止任务" width="800px" draggable :close-on-click-modal="false">
|
||||
<detailForm ref="detailFormTeRef"></detailForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer" style="float: right; padding-bottom: 20px">
|
||||
<el-button type="primary" @click="handleTermination">确认</el-button>
|
||||
<el-button @click="isShowTermination = false">取消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</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';
|
||||
import detailForm from '@/views/design/drawingreview/detailForm.vue';
|
||||
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 detailFormRef = ref<InstanceType<typeof detailForm>>();
|
||||
const detailFormTeRef = ref<InstanceType<typeof detailForm>>();
|
||||
|
||||
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 isDrawing = ref(false); //图纸评审标志
|
||||
const businessId = ref(''); //业务id
|
||||
const isShowTermination = ref(false); //显示终止
|
||||
//节点编码
|
||||
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, Drawing?: boolean, Id?: string) => {
|
||||
businessId.value = Id || '';
|
||||
isDrawing.value = Drawing;
|
||||
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;
|
||||
if (isDrawing.value) {
|
||||
detailFormRef.value.submit(businessId.value);
|
||||
}
|
||||
});
|
||||
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 handleTermination = async () => {
|
||||
// 终止任务
|
||||
const params = {
|
||||
taskId: taskId.value,
|
||||
comment: form.value.message
|
||||
};
|
||||
await terminationTask(params).finally(() => {
|
||||
// 提交设计验证
|
||||
detailFormTeRef.value.submit(businessId.value);
|
||||
loading.value = false;
|
||||
buttonDisabled.value = false;
|
||||
});
|
||||
dialog.visible = false;
|
||||
emits('submitCallback');
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
};
|
||||
//终止任务
|
||||
const handleTerminationTask = async () => {
|
||||
if (isDrawing.value) {
|
||||
isShowTermination.value = true;
|
||||
return;
|
||||
}
|
||||
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>
|
||||
<style lang="scss">
|
||||
.box {
|
||||
border: 1px solid #f1f1f1;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
> span {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #0090f9;
|
||||
}
|
||||
}
|
||||
</style>
|
108
src/components/ProjectSelector/index.vue
Normal file
108
src/components/ProjectSelector/index.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="select-container">
|
||||
<label for="projectSelect" class="select-label">项目列表:</label>
|
||||
<el-select
|
||||
id="projectSelect"
|
||||
v-model="selectedProjectId"
|
||||
placeholder="全部工程项目"
|
||||
clearable
|
||||
filterable
|
||||
@change="handleSelect"
|
||||
style="width: 150px; margin-right: 20px"
|
||||
>
|
||||
<el-option v-for="project in projects" :key="project.id" :label="project.name" :value="project.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { getProjectTeam } from '@/utils/projectTeam';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const projects = computed(() => [
|
||||
// { id: '', name: '全部工程项目' }, // 添加空选项
|
||||
...userStore.projects
|
||||
]);
|
||||
|
||||
const selectedProjectId = ref(userStore.selectedProject?.id || '');
|
||||
|
||||
// 监听 userStore.selectedProject 变化,更新 selectedProjectId
|
||||
watch(
|
||||
() => userStore.selectedProject,
|
||||
(newProject) => {
|
||||
selectedProjectId.value = newProject?.id ?? ''; // 避免 undefined 导致 placeholder 显示
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const handleSelect = (projectId: string) => {
|
||||
const selectedProject = projects.value.find((p) => p.id === projectId);
|
||||
if (selectedProject) {
|
||||
userStore.setSelectedProject(selectedProject);
|
||||
console.log(userStore.selectedProject); // 打印选中的项目
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.select-container {
|
||||
display: flex;
|
||||
align-items: center; // 上下居中对齐
|
||||
gap: 10px; // label 和 select 之间的间距
|
||||
}
|
||||
|
||||
.select-label {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
white-space: nowrap; // 防止 label 换行
|
||||
font-size: 14px; // 设置字体大小
|
||||
}
|
||||
|
||||
#projectSelect {
|
||||
.el-select {
|
||||
width: 400px; // 保持宽度
|
||||
|
||||
.el-input__inner {
|
||||
height: 38px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #dcdfe6;
|
||||
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 0 5px rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.el-input__icon {
|
||||
line-height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-focus .el-input__inner {
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计(可选)
|
||||
@media (max-width: 768px) {
|
||||
.select-container {
|
||||
flex-direction: column; // 栈式布局
|
||||
align-items: flex-start; // 左对齐
|
||||
|
||||
.select-label {
|
||||
margin-bottom: 5px; // label 和 select 之间的垂直间距
|
||||
}
|
||||
|
||||
#projectSelect .el-select {
|
||||
width: 100%; // 在小屏幕上占满宽度
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
102
src/components/RightToolbar/index.vue
Normal file
102
src/components/RightToolbar/index.vue
Normal 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' }"
|
||||
@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>
|
250
src/components/RoleSelect/index.vue
Normal file
250
src/components/RoleSelect/index.vue
Normal 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>
|
9
src/components/Screenfull/index.vue
Normal file
9
src/components/Screenfull/index.vue
Normal 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>
|
41
src/components/SizeSelect/index.vue
Normal file
41
src/components/SizeSelect/index.vue
Normal 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>
|
40
src/components/SvgIcon/index.vue
Normal file
40
src/components/SvgIcon/index.vue
Normal 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 scope lang="scss">
|
||||
.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>
|
200
src/components/TopNav/index.vue
Normal file
200
src/components/TopNav/index.vue
Normal 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(() => {
|
||||
let topMenus: RouteRecordRaw[] = [];
|
||||
routers.value.map((menu) => {
|
||||
if (menu.hidden !== true) {
|
||||
// 兼容顶部栏一级菜单内部跳转
|
||||
if (menu.path === '/') {
|
||||
topMenus.push(menu.children ? menu.children[0] : menu);
|
||||
} else {
|
||||
topMenus.push(menu);
|
||||
}
|
||||
}
|
||||
});
|
||||
return topMenus;
|
||||
});
|
||||
|
||||
// 设置子路由
|
||||
const childrenMenus = computed(() => {
|
||||
let 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) {
|
||||
let 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) => {
|
||||
let 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>
|
147
src/components/TreeSelect/index.vue
Normal file
147
src/components/TreeSelect/index.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="el-tree-select">
|
||||
<el-select
|
||||
ref="treeSelect"
|
||||
v-model="valueId"
|
||||
style="width: 100%"
|
||||
:filterable="true"
|
||||
:clearable="true"
|
||||
:filter-method="selectFilterData"
|
||||
:placeholder="placeholder"
|
||||
@clear="clearHandle"
|
||||
>
|
||||
<el-option :value="valueId" :label="valueTitle">
|
||||
<el-tree
|
||||
id="tree-option"
|
||||
ref="selectTree"
|
||||
:accordion="accordion"
|
||||
:data="options"
|
||||
:props="objMap"
|
||||
:node-key="objMap.value"
|
||||
:expand-on-click-node="false"
|
||||
:default-expanded-keys="defaultExpandedKey"
|
||||
:filter-node-method="filterNode"
|
||||
@node-click="handleNodeClick"
|
||||
></el-tree>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface ObjMap {
|
||||
value: string;
|
||||
label: string;
|
||||
children: string;
|
||||
}
|
||||
interface Props {
|
||||
objMap: ObjMap;
|
||||
accordion: boolean;
|
||||
value: string | number;
|
||||
options: any[];
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
objMap: () => {
|
||||
return {
|
||||
value: 'id',
|
||||
label: 'label',
|
||||
children: 'children'
|
||||
};
|
||||
},
|
||||
accordion: false,
|
||||
value: '',
|
||||
options: () => [],
|
||||
placeholder: ''
|
||||
});
|
||||
|
||||
const selectTree = ref<ElTreeSelectInstance>();
|
||||
|
||||
const emit = defineEmits(['update:value']);
|
||||
|
||||
const valueId = computed({
|
||||
get: () => props.value,
|
||||
set: (val) => {
|
||||
emit('update:value', val);
|
||||
}
|
||||
});
|
||||
const valueTitle = ref('');
|
||||
const defaultExpandedKey = ref<any[]>([]);
|
||||
|
||||
const initHandle = () => {
|
||||
nextTick(() => {
|
||||
const selectedValue = valueId.value;
|
||||
if (selectedValue !== null && typeof selectedValue !== 'undefined') {
|
||||
const node = selectTree.value?.getNode(selectedValue);
|
||||
if (node) {
|
||||
valueTitle.value = node.data[props.objMap.label];
|
||||
selectTree.value?.setCurrentKey(selectedValue); // 设置默认选中
|
||||
defaultExpandedKey.value = [selectedValue]; // 设置默认展开
|
||||
}
|
||||
} else {
|
||||
clearHandle();
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleNodeClick = (node: any) => {
|
||||
valueTitle.value = node[props.objMap.label];
|
||||
valueId.value = node[props.objMap.value];
|
||||
defaultExpandedKey.value = [];
|
||||
selectTree.value?.blur();
|
||||
selectFilterData('');
|
||||
};
|
||||
const selectFilterData = (val: any) => {
|
||||
selectTree.value?.filter(val);
|
||||
};
|
||||
const filterNode = (value: any, data: any) => {
|
||||
if (!value) return true;
|
||||
return data[props.objMap['label']].indexOf(value) !== -1;
|
||||
};
|
||||
const clearHandle = () => {
|
||||
valueTitle.value = '';
|
||||
valueId.value = '';
|
||||
defaultExpandedKey.value = [];
|
||||
clearSelected();
|
||||
};
|
||||
const clearSelected = () => {
|
||||
const allNode = document.querySelectorAll('#tree-option .el-tree-node');
|
||||
allNode.forEach((element) => element.classList.remove('is-current'));
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initHandle();
|
||||
});
|
||||
|
||||
watch(valueId, () => {
|
||||
initHandle();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/variables.module.scss';
|
||||
|
||||
.el-scrollbar .el-scrollbar__view .el-select-dropdown__item {
|
||||
padding: 0;
|
||||
background-color: #fff;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.el-select-dropdown__item.selected {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
ul li .el-tree .el-tree-node__content {
|
||||
height: auto;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content:hover),
|
||||
:deep(.el-tree-node__content:active),
|
||||
:deep(.is-current > div:first-child),
|
||||
:deep(.el-tree-node__content:focus) {
|
||||
background-color: mix(#fff, $--color-primary, 90%);
|
||||
color: $--color-primary;
|
||||
}
|
||||
</style>
|
306
src/components/UserSelect/index.vue
Normal file
306
src/components/UserSelect/index.vue
Normal file
@ -0,0 +1,306 @@
|
||||
<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' }"
|
||||
: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;
|
||||
}
|
||||
const prop = withDefaults(defineProps<PropType>(), {
|
||||
multiple: true,
|
||||
modelValue: undefined,
|
||||
data: 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: ''
|
||||
});
|
||||
|
||||
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;
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped></style>
|
136
src/components/amap/index.vue
Normal file
136
src/components/amap/index.vue
Normal file
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="map">
|
||||
<input type="text" placeholder="请输入地址" v-model="searchValue" />
|
||||
<button @click="onSearch">搜索</button>
|
||||
<div id="container" :style="{ 'height': mapProps.height }"></div>
|
||||
|
||||
<div id="my-panel" @listElementClick="selectPostion"></div>
|
||||
<div class="flex justify-end">
|
||||
<el-button type="primary" @click="submit"> 确定 </el-button>
|
||||
<el-button @click="emit('setLocation')">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import AMapLoader from '@amap/amap-jsapi-loader';
|
||||
const { proxy } = getCurrentInstance();
|
||||
//props参数
|
||||
const mapProps = defineProps({
|
||||
height: {
|
||||
type: String,
|
||||
default: '800px'
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['setLocation']);
|
||||
const center = ref([116.397428, 39.90923]);
|
||||
const map = ref(null);
|
||||
const placeSearch = ref(null);
|
||||
const geocoder = ref(null);
|
||||
const searchValue = ref('');
|
||||
const lnglat = ref([]);
|
||||
onMounted(() => {
|
||||
window._AMapSecurityConfig = {
|
||||
securityJsCode: '3f418182f27c907265f69a708c5fa41c'
|
||||
};
|
||||
AMapLoader.load({
|
||||
key: 'ed8d05ca57affee582e2be654bac5baf', // 申请好的Web端开发者Key,首次调用 load 时必填
|
||||
version: '2.0', // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
|
||||
plugins: ['AMap.Scale', 'AMap.AutoComplete', 'AMap.PlaceSearch', 'AMap.Geolocation', 'AMap.Geocoder'] //需要使用的的插件列表,如比例尺'AMap.Scale',支持添加多个如:['...','...']
|
||||
})
|
||||
.then((AMap) => {
|
||||
map.value = new AMap.Map('container', {
|
||||
// 设置地图容器id
|
||||
viewMode: '3D', // 是否为3D地图模式
|
||||
zoom: 8, // 初始化地图级别
|
||||
center: center.value // 初始化地图中心点位置
|
||||
});
|
||||
//初始化搜索
|
||||
placeSearch.value = new AMap.PlaceSearch({
|
||||
pageSize: 5, //单页显示结果条数
|
||||
// pageIndex: 1, //页码
|
||||
// city: '010', //兴趣点城市
|
||||
// citylimit: true, //是否强制限制在设置的城市内搜索
|
||||
panel: 'my-panel',
|
||||
map: map.value, //展现结果的地图实例
|
||||
autoFitView: true //是否自动调整地图视野使绘制的 Marker 点都处于视口的可见范围
|
||||
});
|
||||
// 初始化Geocoder
|
||||
geocoder.value = new AMap.Geocoder({
|
||||
radius: 1000 //范围,默认:500
|
||||
});
|
||||
|
||||
// 定位
|
||||
const geolocation = new AMap.Geolocation({
|
||||
enableHighAccuracy: true, //是否使用高精度定位,默认:true
|
||||
timeout: 10000, //超过10秒后停止定位,默认:无穷大
|
||||
maximumAge: 0, //定位结果缓存0毫秒,默认:0
|
||||
convert: true, //自动偏移坐标,偏移后的坐标为高德坐标,默认:true
|
||||
showButton: true, //显示定位按钮,默认:true
|
||||
buttonPosition: 'LB', //定位按钮停靠位置,默认:'LB',左下角
|
||||
buttonOffset: new AMap.Pixel(10, 20), //定位按钮与设置的停靠位置的偏移量,默认:Pixel(10, 20)
|
||||
showMarker: true, //定位成功后在定位到的位置显示点标记,默认:true
|
||||
showCircle: true, //定位成功后用圆圈表示定位精度范围,默认:true
|
||||
panToLocation: true, //定位成功后将定位到的位置作为地图中心点,默认:true
|
||||
zoomToAccuracy: true //定位成功后调整地图视野范围使定位位置及精度范围视野内可见,默认:false
|
||||
});
|
||||
map.value.addControl(geolocation);
|
||||
|
||||
//定位到当前位置
|
||||
geolocation.getCurrentPosition((status, result) => {
|
||||
console.log(status, result);
|
||||
});
|
||||
placeSearch.value.on('selectChanged', (e) => {
|
||||
let { lng, lat } = e.selected.data.location;
|
||||
lnglat.value = [lng, lat];
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
});
|
||||
const onSearch = () => {
|
||||
//搜索地址
|
||||
placeSearch.value.search(searchValue.value, (status, result) => {
|
||||
if (result.info !== 'OK') return;
|
||||
let { lng, lat } = result.poiList.pois[0].location;
|
||||
lnglat.value = [lng, lat];
|
||||
});
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
if (!lnglat.value.length) {
|
||||
proxy?.$modal.msgWarning('请选择正确地址');
|
||||
return;
|
||||
}
|
||||
geocoder.value.getAddress(lnglat.value, function (status, result) {
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
// result为对应的地理位置详细信息
|
||||
const position = {
|
||||
lng: lnglat.value[0],
|
||||
lat: lnglat.value[1],
|
||||
projectSite: result.regeocode.formattedAddress
|
||||
};
|
||||
emit('setLocation', position);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
map.value?.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
#my-panel {
|
||||
position: absolute;
|
||||
top: 103px;
|
||||
z-index: 1;
|
||||
left: 10px;
|
||||
}
|
||||
</style>
|
26
src/components/iFrame/index.vue
Normal file
26
src/components/iFrame/index.vue
Normal 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>
|
597
src/components/openLayersMap/index.vue
Normal file
597
src/components/openLayersMap/index.vue
Normal file
@ -0,0 +1,597 @@
|
||||
<template>
|
||||
<div class="flex justify-between" v-loading="treeLoading">
|
||||
<el-tree-v2
|
||||
style="width: 340px; overflow: auto"
|
||||
show-checkbox
|
||||
:data="jsonData"
|
||||
:height="500"
|
||||
@check-change="handleCheckChange"
|
||||
:props="treeProps"
|
||||
@node-contextmenu="showMenu"
|
||||
ref="treeRef"
|
||||
@node-click="isMenuVisible = false"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span @dblclick="handlePosition(data, node)">{{ data.name }}</span>
|
||||
</template>
|
||||
</el-tree-v2>
|
||||
<div>
|
||||
<div class="ol-map" id="olMap"></div>
|
||||
<div class="h15 mt-2" v-if="!selectLayer.length"></div>
|
||||
<div class="m-0 c-white text-3 flex w-237.5 mt-2 flex-wrap" v-else>
|
||||
<p
|
||||
v-for="(item, index) in selectLayer"
|
||||
class="pl-xl border-rd pr p-3 w-111 mr-1 bg-#909399 flex items-center cursor-pointer justify-between"
|
||||
@click="delLayer(index, item.option)"
|
||||
>
|
||||
{{ item.location.name + '被选中为' + item.option }}
|
||||
<el-icon>
|
||||
<Close />
|
||||
</el-icon>
|
||||
</p>
|
||||
</div>
|
||||
<!-- <el-form-item label="类型" class="items-center">
|
||||
<el-radio-group v-model="layerType">
|
||||
<el-radio :value="1" size="large">光伏板</el-radio>
|
||||
<el-radio :value="2" size="large">桩点/支架</el-radio>
|
||||
<el-radio :value="3" size="large">方阵</el-radio>
|
||||
<el-radio :value="4" size="large">逆变器</el-radio>
|
||||
<el-radio :value="5" size="large">箱变</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item> -->
|
||||
</div>
|
||||
|
||||
<div v-if="isMenuVisible" :style="{ left: menuX + 'px', top: menuY + 'px' }" class="fixed bg-white shadow-md rounded-md overflow-hidden">
|
||||
<ul class="py-1 pl-0">
|
||||
<li
|
||||
v-for="(item, index) in layerTypeList"
|
||||
class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
|
||||
@click="handleMenuItemClick(item, index + 1)"
|
||||
>
|
||||
<i class="fa-solid fa-check mr-2"></i>{{ item }}
|
||||
</li>
|
||||
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('名称', null)">
|
||||
<i class="fa-solid fa-times mr-2"></i>名称
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="float-right">
|
||||
<el-button @click="emit('close')">取消</el-button>
|
||||
<el-button type="primary" @click="addFacilities" :loading="loading">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Map from 'ol/Map'; // OpenLayers的主要类,用于创建和管理地图
|
||||
import View from 'ol/View'; // OpenLayers的视图类,定义地图的视图属性
|
||||
import { Tile as TileLayer } from 'ol/layer'; // OpenLayers的瓦片图层类
|
||||
import { XYZ, OSM } from 'ol/source'; // OpenLayers的瓦片数据源,包括XYZ格式和OpenStreetMap专用的数据源
|
||||
import { fromLonLat, toLonLat } from 'ol/proj'; // OpenLayers的投影转换函数,用于经纬度坐标和投影坐标之间的转换
|
||||
import { defaults as defaultInteractions, DragRotateAndZoom } from 'ol/interaction'; // OpenLayers的交互类,包括默认的交互集合和特定的旋转缩放交互
|
||||
import { defaults as defaultControls, defaults, FullScreen, MousePosition, ScaleLine } from 'ol/control'; // OpenLayers的控件类,包括默认的控件集合和特定的全屏、鼠标位置、比例尺控件
|
||||
import Feature from 'ol/Feature'; // OpenLayers的要素类,表示地图上的一个对象或实体
|
||||
import Point from 'ol/geom/Point'; // OpenLayers的点几何类,用于表示点状的地理数据
|
||||
import { Vector as VectorLayer } from 'ol/layer'; // OpenLayers的矢量图层类,用于显示矢量数据
|
||||
import { Vector as VectorSource } from 'ol/source'; // OpenLayers的矢量数据源类,用于管理和提供矢量数据
|
||||
import { Circle, Style, Stroke, Fill, Icon, Text } from 'ol/style'; // OpenLayers的样式类,用于定义图层的样式,包括圆形样式、基本样式、边框、填充和图标
|
||||
import LineString from 'ol/geom/LineString'; // OpenLayers的线几何类,用于表示线状的地理数据
|
||||
import Polygon from 'ol/geom/Polygon'; // OpenLayers的多边形几何类,用于表示面状的地理数据
|
||||
import GeoJSON from 'ol/format/GeoJSON';
|
||||
import * as turf from '@turf/turf';
|
||||
import { FeatureCollection, Geometry } from 'geojson';
|
||||
import { MapViewFitter } from '@/utils/setMapCenter';
|
||||
import { TreeInstance } from 'element-plus';
|
||||
import { addProjectFacilities, addProjectPilePoint, addProjectSquare, listDXFProject, addInverter, addBoxTransformer } from '@/api/project/project';
|
||||
import { BatchUploader } from '@/utils/batchUpload';
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const props = defineProps({
|
||||
projectId: String,
|
||||
designId: String
|
||||
});
|
||||
const treeData = ref<any>([]);
|
||||
const layerType = ref(null);
|
||||
const layerTypeList = ref(['光伏板', '桩点/支架', '方阵', '逆变器', '箱变']);
|
||||
const contextMenu = ref(null);
|
||||
const selectLayer = ref([]);
|
||||
const treeRef = ref<TreeInstance>();
|
||||
const treeProps = {
|
||||
value: 'name'
|
||||
};
|
||||
const loading = ref(false);
|
||||
const treeLoading = ref(false);
|
||||
const emit = defineEmits(['handleCheckChange', 'close']);
|
||||
let map: any = null;
|
||||
|
||||
const layerData = reactive<any>({});
|
||||
const centerPosition = ref(fromLonLat([107.13761560163239, 23.80480003743964]));
|
||||
|
||||
const jsonData = computed(() => {
|
||||
let id = 0;
|
||||
let arr = [];
|
||||
treeData.value.forEach((item: any, index: any) => {
|
||||
arr.push({
|
||||
name: item.name,
|
||||
index
|
||||
});
|
||||
for (const itm of item.features) {
|
||||
if (itm.geometry.id) {
|
||||
break;
|
||||
}
|
||||
itm.geometry.id = ++id;
|
||||
itm.geometry.coordinates = convertStrToNum(itm.geometry.coordinates);
|
||||
}
|
||||
});
|
||||
return arr; // treeData.value;
|
||||
});
|
||||
const handlePosition = (data: any, node: any) => {
|
||||
const fitter = new MapViewFitter(map); // 传入你的 OpenLayers 地图实例
|
||||
const features = treeData.value[data.index]?.features; //features数组
|
||||
console.log('🚀 ~ handlePosition ~ features:', features);
|
||||
|
||||
if (features?.length) {
|
||||
const featureCollection: FeatureCollection<Geometry> = {
|
||||
type: 'FeatureCollection',
|
||||
features
|
||||
};
|
||||
|
||||
fitter.fit(featureCollection);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckChange = (data: any, bool: boolean) => {
|
||||
if (isMenuVisible.value) isMenuVisible.value = false;
|
||||
|
||||
const features = treeData.value[data.index].features;
|
||||
|
||||
if (bool) {
|
||||
features.forEach((item: any) => {
|
||||
const fid = item.geometry.id;
|
||||
|
||||
// 没创建过就先创建
|
||||
if (!featureMap[fid]) {
|
||||
creatPoint(item.geometry.coordinates, item.geometry.type, fid, item.properties.text);
|
||||
}
|
||||
|
||||
// 添加到共享 source 中(避免重复添加)
|
||||
const feature = featureMap[fid];
|
||||
if (!sharedSource.hasFeature(feature)) {
|
||||
sharedSource.addFeature(feature);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
features.forEach((item: any) => {
|
||||
const fid = item.geometry.id;
|
||||
const feature = featureMap[fid];
|
||||
if (feature && sharedSource.hasFeature(feature)) {
|
||||
sharedSource.removeFeature(feature);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function initOLMap() {
|
||||
// 创造地图实例
|
||||
map = new Map({
|
||||
// 设置地图容器的ID
|
||||
target: 'olMap',
|
||||
// 定义地图的图层列表,用于显示特定的地理信息。
|
||||
layers: [
|
||||
// 高德地图
|
||||
// TileLayer表示一个瓦片图层,它由一系列瓦片(通常是图片)组成,用于在地图上显示地理数据。
|
||||
new TileLayer({
|
||||
// 设置图层的数据源为XYZ类型。XYZ是一个通用的瓦片图层源,它允许你通过URL模板来获取瓦片
|
||||
source: new XYZ({
|
||||
url: 'https://webrd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}'
|
||||
})
|
||||
})
|
||||
],
|
||||
// 设置地图的视图参数
|
||||
// View表示地图的视图,它定义了地图的中心点、缩放级别、旋转角度等参数。
|
||||
view: new View({
|
||||
// fromLonLat是一个函数,用于将经纬度坐标转换为地图的坐标系统。
|
||||
center: centerPosition.value, //地图中心点
|
||||
zoom: 15, // 缩放级别
|
||||
minZoom: 0, // 最小缩放级别
|
||||
// maxZoom: 18, // 最大缩放级别
|
||||
constrainResolution: true // 因为存在非整数的缩放级别,所以设置该参数为true来让每次缩放结束后自动缩放到距离最近的一个整数级别,这个必须要设置,当缩放在非整数级别时地图会糊
|
||||
// projection: 'EPSG:4326' // 投影坐标系,默认是3857
|
||||
}),
|
||||
// 按住shift进行旋转
|
||||
// interactions: defaultInteractions().extend([new DragRotateAndZoom()]),
|
||||
// 控件
|
||||
// controls: defaults().extend([
|
||||
// new FullScreen(), // 全屏
|
||||
// new MousePosition(), // 显示鼠标当前位置的地图坐标
|
||||
// new ScaleLine() // 显示比例尺
|
||||
|
||||
// ])
|
||||
|
||||
//加载控件到地图容器中
|
||||
controls: defaultControls({
|
||||
zoom: false,
|
||||
rotate: false,
|
||||
attribution: false
|
||||
}).extend([
|
||||
new FullScreen() // 全屏
|
||||
])
|
||||
});
|
||||
|
||||
// 事件
|
||||
// map.on('moveend', (e: any) => {
|
||||
// // console.log('地图移动', e);
|
||||
// // 获取当前缩放级别
|
||||
// var zoomLevel = map.getView().getZoom();
|
||||
// // console.log('当前缩放级别:', zoomLevel);
|
||||
// });
|
||||
// map.on('rendercomplete', () => {
|
||||
// // console.log('渲染完成');
|
||||
// });
|
||||
// map.on('click', (e: any) => {
|
||||
// var coordinate = e.coordinate;
|
||||
|
||||
// // 将投影坐标转换为经纬度坐标
|
||||
// var lonLatCoordinate = toLonLat(coordinate);
|
||||
// // 输出转换后的经纬度坐标
|
||||
// console.log('经纬度坐标:', lonLatCoordinate);
|
||||
// });
|
||||
}
|
||||
|
||||
//递归字符串数组变成数字
|
||||
function convertStrToNum(arr) {
|
||||
if (typeof arr === 'string') {
|
||||
const num = Number(arr);
|
||||
return isNaN(num) ? arr : num;
|
||||
} else if (Array.isArray(arr)) {
|
||||
return arr.map((item) => convertStrToNum(item));
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图层
|
||||
* @param {*} pointObj 坐标数组
|
||||
* @param {*} type 类型
|
||||
* @param {*} id 唯一id
|
||||
* @param {*} name 名称
|
||||
* */
|
||||
// 共享 source 和图层(全局一次性创建)
|
||||
const sharedSource = new VectorSource();
|
||||
const sharedLayer = new VectorLayer({
|
||||
source: sharedSource,
|
||||
renderMode: 'image' // 提高渲染性能
|
||||
} as any);
|
||||
|
||||
// id => Feature 映射表
|
||||
const featureMap: Record<string, Feature> = {};
|
||||
|
||||
const creatPoint = (pointObj: Array<any>, type: string, id: string, name?: string) => {
|
||||
let geometry;
|
||||
|
||||
if (type === 'Point') {
|
||||
geometry = new Point(fromLonLat(pointObj));
|
||||
} else if (type === 'LineString') {
|
||||
const coords = pointObj.map((arr: any) => fromLonLat(arr));
|
||||
// 注意:这里虽然是 LineString 类型,但数据实际表示的是闭合面
|
||||
geometry = new Polygon([coords]);
|
||||
} else {
|
||||
const coords = pointObj.map((arr: any) => arr.map((i: any) => fromLonLat(i)));
|
||||
geometry = new Polygon(coords);
|
||||
}
|
||||
|
||||
const feature = new Feature({ geometry });
|
||||
|
||||
const pointStyle = new Style({
|
||||
image: new Circle({
|
||||
radius: 2,
|
||||
fill: new Fill({ color: 'red' })
|
||||
}),
|
||||
text: new Text({
|
||||
font: '12px Microsoft YaHei',
|
||||
text: name,
|
||||
scale: 1,
|
||||
fill: new Fill({ color: '#7bdd63' })
|
||||
})
|
||||
});
|
||||
|
||||
const polygonStyle = new Style({
|
||||
stroke: new Stroke({
|
||||
color: type === 'LineString' ? 'skyblue' : 'purple',
|
||||
width: 2
|
||||
}),
|
||||
fill: new Fill({ color: 'transparent' })
|
||||
});
|
||||
|
||||
feature.setStyle(type === 'Point' ? pointStyle : polygonStyle);
|
||||
|
||||
// 缓存 feature(用于后续判断)
|
||||
featureMap[id] = feature;
|
||||
};
|
||||
|
||||
// 控制菜单是否显示
|
||||
const isMenuVisible = ref(false);
|
||||
// 菜单的 x 坐标
|
||||
const menuX = ref(0);
|
||||
// 菜单的 y 坐标
|
||||
const menuY = ref(0);
|
||||
|
||||
// 显示菜单的方法
|
||||
const showMenu = (event: MouseEvent, data) => {
|
||||
console.log('🚀 ~ showMenu ~ data:', data, treeData.value[data.index]);
|
||||
contextMenu.value = data;
|
||||
isMenuVisible.value = true;
|
||||
menuX.value = event.clientX;
|
||||
menuY.value = event.clientY;
|
||||
};
|
||||
|
||||
// 处理菜单项点击事件的方法
|
||||
const handleMenuItemClick = (option: string, index: number) => {
|
||||
isMenuVisible.value = false;
|
||||
|
||||
if (selectLayer.value.some((item) => item.location.name === contextMenu.value.name)) {
|
||||
return proxy?.$modal.msgError('已选择该图层,请勿重复选择');
|
||||
}
|
||||
if (selectLayer.value.some((item) => item.option !== '名称' && item.option !== '箱变' && item.option !== '光伏板')) {
|
||||
if (option !== '名称' && option !== '箱变') return proxy?.$modal.msgError('只能选择一个类型');
|
||||
}
|
||||
selectLayer.value.push({ location: contextMenu.value, option });
|
||||
layerType.value = index ? index : layerType.value; // 设置 layerType 为对应的索引值
|
||||
|
||||
emit('handleCheckChange', selectLayer.value);
|
||||
};
|
||||
|
||||
//删除菜单
|
||||
const delLayer = (index, option) => {
|
||||
selectLayer.value.splice(index, 1);
|
||||
if (option != '名称') {
|
||||
if (selectLayer.value.every((item) => item.option == '名称')) layerType.value = null;
|
||||
}
|
||||
emit('handleCheckChange', selectLayer.value);
|
||||
};
|
||||
|
||||
// 点击页面其他区域隐藏菜单
|
||||
const closeMenuOnClickOutside = (event: MouseEvent) => {
|
||||
if (isMenuVisible.value) {
|
||||
const menuElement = document.querySelector('.fixed.bg-white');
|
||||
if (menuElement && !menuElement.contains(event.target as Node)) {
|
||||
isMenuVisible.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 添加全局点击事件监听器
|
||||
window.addEventListener('click', closeMenuOnClickOutside);
|
||||
const getTreeData = async () => {
|
||||
treeLoading.value = true;
|
||||
try {
|
||||
const res = await listDXFProject(props.designId);
|
||||
treeData.value = res.data.layers;
|
||||
treeLoading.value = false;
|
||||
} catch (err) {
|
||||
treeLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件卸载时移除事件监听器
|
||||
const onUnmounted = () => {
|
||||
window.removeEventListener('click', closeMenuOnClickOutside);
|
||||
};
|
||||
|
||||
type LayerConfig = {
|
||||
optionB: string;
|
||||
apiFunc: (data: any) => Promise<any>;
|
||||
};
|
||||
|
||||
const LAYER_CONFIG: Record<number, LayerConfig> = {
|
||||
1: { optionB: '光伏板', apiFunc: addProjectFacilities },
|
||||
3: { optionB: '方阵', apiFunc: addProjectSquare },
|
||||
4: { optionB: '逆变器', apiFunc: addInverter },
|
||||
5: { optionB: '箱变', apiFunc: addBoxTransformer }
|
||||
};
|
||||
|
||||
const showError = (msg: string) => proxy?.$modal.msgError(msg);
|
||||
const showSuccess = (msg: string) => proxy?.$modal.msgSuccess(msg);
|
||||
|
||||
const getGeoJsonData = (nameOption = '名称', secondOption: string): { nameGeoJson: any[]; locationGeoJson: any | null } | null => {
|
||||
const nameLayers = selectLayer.value.filter((item) => item.option === nameOption);
|
||||
const secondLayer = selectLayer.value.filter((item) => item.option === secondOption);
|
||||
|
||||
if (!nameLayers.length || !secondLayer) {
|
||||
showError(`请选择${nameOption}和${secondOption}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const nameGeoJson = nameLayers.map((item) => treeData.value[item.location.index]);
|
||||
const locationGeoJson = secondLayer.map((item) => treeData.value[item.location.index]);
|
||||
|
||||
return { nameGeoJson, locationGeoJson };
|
||||
};
|
||||
|
||||
const handleTwoLayerUpload = async (optionB: string, apiFunc: (data: any) => Promise<any>) => {
|
||||
const geoJson = getGeoJsonData('名称', optionB);
|
||||
if (!geoJson) return;
|
||||
if (optionB == '光伏板') return uploadPhotovoltaic(geoJson, apiFunc);
|
||||
const data = {
|
||||
projectId: props.projectId,
|
||||
nameGeoJson: geoJson.nameGeoJson,
|
||||
locationGeoJson: geoJson.locationGeoJson,
|
||||
pointGeoJson: null
|
||||
};
|
||||
|
||||
loading.value = true;
|
||||
await apiFunc(data);
|
||||
await showSuccess('添加成功');
|
||||
};
|
||||
//上传光伏板
|
||||
const uploadPhotovoltaic = async (geoJson: { nameGeoJson: any[]; locationGeoJson: any }, apiFunc: (data: any) => Promise<any>) => {
|
||||
// 提取原始 features
|
||||
let rawNameFeatures = geoJson.nameGeoJson || [];
|
||||
let rawLocationFeatures = geoJson.locationGeoJson || [];
|
||||
|
||||
console.log('🚀 nameGeoJson:', rawNameFeatures);
|
||||
console.log('🚀 locationGeoJson:', rawLocationFeatures);
|
||||
|
||||
// 扁平化处理 FeatureCollection
|
||||
const nameFeatures = rawNameFeatures.flatMap((fc) => fc.features || []).map((f) => ({ ...f, __source: 'name' }));
|
||||
const locationFeatures = rawLocationFeatures.flatMap((fc) => fc.features).map((f) => ({ ...f, __source: 'location' }));
|
||||
// 配对成上传单元
|
||||
type FeaturePair = { nameFeature: any; locationFeature: any };
|
||||
const pairedFeatures: FeaturePair[] = nameFeatures.map((name, i) => ({
|
||||
nameFeature: name,
|
||||
locationFeature: locationFeatures[i]
|
||||
}));
|
||||
|
||||
// 启动上传
|
||||
loading.value = true;
|
||||
|
||||
const sessionId = new Date().getTime().toString(36) + Math.random().toString(36).substring(2, 15);
|
||||
|
||||
const uploader = new BatchUploader({
|
||||
dataList: pairedFeatures,
|
||||
chunkSize: 3000, // 一次上传3000对
|
||||
delay: 200,
|
||||
uploadFunc: async (chunk, batchNum, totalBatch) => {
|
||||
const chunkNameFeatures = chunk.map((pair) => pair.nameFeature);
|
||||
const chunkLocationFeatures = chunk.map((pair) => pair.locationFeature);
|
||||
|
||||
console.log(`🚀 上传第 ${batchNum}/${totalBatch} 批,条数:`, chunk.length);
|
||||
|
||||
await apiFunc({
|
||||
projectId: props.projectId,
|
||||
nameGeoJson: [{ type: 'FeatureCollection', features: chunkNameFeatures }],
|
||||
locationGeoJson: [{ type: 'FeatureCollection', features: chunkLocationFeatures }],
|
||||
pointGeoJson: null,
|
||||
sessionId,
|
||||
totalBatch,
|
||||
batchNum
|
||||
});
|
||||
},
|
||||
onComplete: () => {
|
||||
showSuccess('图层上传完成');
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
await uploader.start();
|
||||
};
|
||||
|
||||
const handlePointUpload = async () => {
|
||||
if (selectLayer.value.length > 1) return showError('最多选择一个桩点/支架');
|
||||
if (selectLayer.value[0].option !== '桩点/支架') return showError('请选择类型为桩点/支架');
|
||||
|
||||
const features = treeData.value[selectLayer.value[0].location.index]?.features || [];
|
||||
if (!features.length) return showError('桩点数据为空');
|
||||
|
||||
loading.value = true;
|
||||
const sessionId = new Date().getTime().toString(36) + Math.random().toString(36).substring(2, 15);
|
||||
const uploader = new BatchUploader({
|
||||
dataList: features,
|
||||
chunkSize: 15000,
|
||||
delay: 200,
|
||||
uploadFunc: async (chunk, batchNum, totalBatch) => {
|
||||
await addProjectPilePoint({
|
||||
projectId: props.projectId,
|
||||
locationGeoJson: {
|
||||
type: 'FeatureCollection',
|
||||
features: chunk
|
||||
},
|
||||
sessionId,
|
||||
totalBatch,
|
||||
batchNum
|
||||
});
|
||||
},
|
||||
onComplete: () => {
|
||||
showSuccess('桩点上传完成');
|
||||
reset();
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
await uploader.start();
|
||||
};
|
||||
|
||||
const addFacilities = async () => {
|
||||
if (!layerType.value) return showError('请选择图层类型');
|
||||
if (!selectLayer.value.length) return showError('请选择需要上传的图层');
|
||||
|
||||
const config = LAYER_CONFIG[layerType.value];
|
||||
|
||||
try {
|
||||
if (layerType.value == 2) {
|
||||
await handlePointUpload();
|
||||
} else if (config) {
|
||||
await handleTwoLayerUpload(config.optionB, config.apiFunc);
|
||||
} else {
|
||||
showError('不支持的图层类型');
|
||||
}
|
||||
} finally {
|
||||
reset();
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
selectLayer.value = [];
|
||||
treeRef.value?.setCheckedKeys([]);
|
||||
sharedSource.clear(); // 清空共享 source 中的所有要素
|
||||
layerType.value = null;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.designId,
|
||||
(newId, oldId) => {
|
||||
if (newId !== oldId) {
|
||||
reset();
|
||||
getTreeData();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
// 地图初始化
|
||||
initOLMap();
|
||||
map.addLayer(sharedLayer);
|
||||
// creatPoint(
|
||||
// [
|
||||
// [
|
||||
// [107.13205125908726, 23.806785824010216],
|
||||
// [107.13218187963494, 23.806867960389773],
|
||||
// [107.13215698891558, 23.806902336258318],
|
||||
// [107.13202636835067, 23.8068201998575],
|
||||
// [107.13205125908726, 23.806785824010216]
|
||||
// ]
|
||||
// ],
|
||||
// 'Polygon',
|
||||
// '1',
|
||||
// '测试方阵'
|
||||
// );
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.ol-map {
|
||||
height: 450px;
|
||||
width: 950px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.ol-custome-full-screen {
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
padding: 5px 11px;
|
||||
height: 24px;
|
||||
background-color: #409eff;
|
||||
color: #fff;
|
||||
border: 1px solid #409eff;
|
||||
&:active {
|
||||
background-color: #337ecc;
|
||||
border-color: #66b1ff;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #79bbff;
|
||||
border-color: #79bbff;
|
||||
}
|
||||
}
|
||||
li {
|
||||
list-style-type: none;
|
||||
}
|
||||
</style>
|
57
src/components/webrtc/index.vue
Normal file
57
src/components/webrtc/index.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<video class="rtc_media_player" width="100%" height="100%" autoplay muted playsinline ref="rtcMediaPlayer"></video>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SrsPlayer",
|
||||
data() {
|
||||
return {
|
||||
webrtc: null, // Instance of SRS SDK
|
||||
sessionId: null,
|
||||
simulatorUrl: null,
|
||||
playerVisible: false,
|
||||
url: ""
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
hideInfo() {
|
||||
document.querySelector(".alert").style.display = "none";
|
||||
},
|
||||
async startPlay(url) {
|
||||
this.url = url;
|
||||
this.playerVisible = true;
|
||||
if (this.webrtc) {
|
||||
this.webrtc.close();
|
||||
}
|
||||
this.webrtc = new SrsRtcWhipWhepAsync();
|
||||
this.$refs.rtcMediaPlayer.srcObject = this.webrtc.stream;
|
||||
console.log('stream tracks:', this.webrtc.stream.getTracks());
|
||||
try {
|
||||
const session = await this.webrtc.play(url);
|
||||
console.log('after play, stream tracks:', this.webrtc.stream.getTracks());
|
||||
this.sessionId = session.sessionid;
|
||||
this.simulatorUrl = `${session.simulator}?drop=1&username=${session.sessionid}`;
|
||||
} catch (error) {
|
||||
console.error("Error playing stream:", error);
|
||||
this.webrtc.close();
|
||||
this.playerVisible = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
// Cleanup the SDK instance on component destroy
|
||||
if (window[this.url]) {
|
||||
window[this.url].close();
|
||||
window[this.url] = null
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rtc_media_player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
BIN
src/components/wordDetial/icon/down.png
Normal file
BIN
src/components/wordDetial/icon/down.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 922 B |
270
src/components/wordDetial/index.vue
Normal file
270
src/components/wordDetial/index.vue
Normal file
@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<el-dialog v-model="isShowDialog" title="变更单详情" draggable width="60vw" :close-on-click-modal="false" :destroy-on-close="true">
|
||||
<el-card :body-style="{ padding: '20px' }" style="border: none; box-shadow: none">
|
||||
<div class="dialog-footer">
|
||||
<div class="btn-item" @click="onLoad">
|
||||
<img src="./icon/down.png" />
|
||||
<span>导出</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-form ref="formRef" :model="formData" label-width="100px" id="formContent" style="width: 75%; margin-left: 10%">
|
||||
<div class="table-content" id="table-content">
|
||||
<el-row class="mb20" style="display: flex; justify-content: center">
|
||||
<h2>设计变更申请单(总承包)</h2>
|
||||
</el-row>
|
||||
<el-row class="mb10" style="display: flex; justify-content: space-between">
|
||||
<div class="head-text">
|
||||
<span>NO:</span>
|
||||
<span>{{ formData.formNo }}</span>
|
||||
</div>
|
||||
<!-- <div class="head-text">
|
||||
<span>填报时间:</span>
|
||||
<span>{{ formData.createdAt }}</span>
|
||||
</div> -->
|
||||
</el-row>
|
||||
<table style="width: 100%" border="1" cellspacing="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="150">工程名称</th>
|
||||
<td class="th-bg">{{ formData.projectName }}</td>
|
||||
<th width="150">提出单位</th>
|
||||
<td class="th-bg">{{ formData.submitUnit }}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th width="150">专业</th>
|
||||
<td class="th-bg">{{ formData.specialty }}</td>
|
||||
<th width="150">提出日期</th>
|
||||
<td class="th-bg">{{ parseTime(formData.submitDate, '{y}-{m}-{d}') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="150">卷册名称</th>
|
||||
<td class="th-bg">{{ formData.volumeName }}</td>
|
||||
<th width="150">附图</th>
|
||||
<td class="th-bg">
|
||||
<el-image
|
||||
v-for="(item, i) of formData.attachmentPicList"
|
||||
:key="i"
|
||||
style="width: 100px; height: 100px"
|
||||
:src="item.url"
|
||||
fit="cover"
|
||||
:preview-src-list="[item.url]"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th width="150">卷册号</th>
|
||||
<td colspan="3">{{ formData.volumeNo }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="150">变更原因</th>
|
||||
<td colspan="3">{{ formData.changeReason }}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th width="150">变更内容</th>
|
||||
<td colspan="3">{{ formData.changeContent }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th width="150">变更费用估算(附计算表)</th>
|
||||
<td colspan="3">
|
||||
<div>
|
||||
<span
|
||||
v-for="(item, i) of formData.costEstimationFileList"
|
||||
:key="i"
|
||||
style="color: rgb(41 145 255);cursor: pointer"
|
||||
@click="onOpen(item.url)"
|
||||
>{{item.originalName}}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { listDesignChange, getDesignChange, delDesignChange, addDesignChange, updateDesignChange } from '@/api/design/designChange';
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
import { downLoadOss } from '@/api/system/oss';
|
||||
// 响应式状态
|
||||
const isShowDialog = ref(false);
|
||||
const initFormData: DesignChangeForm = {
|
||||
id: undefined,
|
||||
formNo: undefined,
|
||||
projectName: undefined,
|
||||
submitUnit: undefined,
|
||||
specialty: undefined,
|
||||
submitDate: undefined,
|
||||
volumeName: undefined,
|
||||
volumeNo: undefined,
|
||||
attachmentPic: undefined,
|
||||
changeReason: [],
|
||||
changeContent: undefined,
|
||||
costEstimation: undefined,
|
||||
costEstimationFile: undefined,
|
||||
fileId: undefined,
|
||||
status: undefined,
|
||||
remark: undefined
|
||||
};
|
||||
const data = reactive<PageData<DesignChangeForm, DesignChangeQuery>>({
|
||||
formData: { ...initFormData }
|
||||
});
|
||||
const design_change_reason_type = ref([]);
|
||||
const { formData } = toRefs(data);
|
||||
// 打开弹窗
|
||||
const openDialog = (row?: any, types) => {
|
||||
resetForm();
|
||||
design_change_reason_type.value = types;
|
||||
if (row?.id) {
|
||||
getInfos(row.id, types);
|
||||
}
|
||||
isShowDialog.value = true;
|
||||
};
|
||||
// 获取详情数据
|
||||
const getInfos = async (id: string, types) => {
|
||||
const res = await getDesignChange(id);
|
||||
Object.assign(formData.value, res.data);
|
||||
// 数据处理
|
||||
if (formData.value.changeReason) {
|
||||
let arr = formData.value.changeReason.split(',');
|
||||
var changeReason = types.filter((item) => arr.includes(item.value.toString())).map((item) => item.label);
|
||||
formData.value.changeReason = changeReason.join(',');
|
||||
}
|
||||
};
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.keys(formData.value).forEach((key) => {
|
||||
formData[key] = undefined;
|
||||
});
|
||||
};
|
||||
// 下载文件
|
||||
const onOpen = (path: string) => {
|
||||
window.open(path, '_blank');
|
||||
};
|
||||
// 导出
|
||||
const onLoad = async () => {
|
||||
await downLoadOss({ id: formData.value.id }, '/design/designChange/export/word', '设计变更单.zip');
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const closeDialog = () => {
|
||||
isShowDialog.value = false;
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
openDialog,
|
||||
closeDialog
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pic-block {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.file-block {
|
||||
width: 100%;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
margin-bottom: 5px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
.ml-2 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
::v-deep .el-icon svg {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
::v-deep .el-step__icon-inner {
|
||||
font-size: 14px !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
.dialog-footer {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
top: 14%;
|
||||
right: 10%;
|
||||
background: #fff;
|
||||
box-shadow: 0px 0px 10px #ddd;
|
||||
text-align: center;
|
||||
padding: 20px 10px;
|
||||
.btn-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
> span {
|
||||
padding-top: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: rgba(51, 51, 51, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse; //合并为一个单一的边框
|
||||
border-color: rgba(199, 199, 199, 1); //边框颜色按实际自定义即可
|
||||
}
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
background-color: rgba(247, 247, 247, 1); //设置表格标题背景色
|
||||
height: 35px; //设置单元格最小高度
|
||||
text-align: center;
|
||||
letter-spacing: 5px;
|
||||
padding: 15px;
|
||||
}
|
||||
td {
|
||||
text-align: left;
|
||||
height: 35px; //设置单元格最小高度
|
||||
padding: 15px;
|
||||
}
|
||||
.th-bg {
|
||||
background-color: rgba(247, 247, 247, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
text-align: left;
|
||||
height: 40px; //设置单元格最小高度
|
||||
padding: 15px;
|
||||
}
|
||||
th {
|
||||
height: 35px; //设置单元格最小高度
|
||||
text-align: center;
|
||||
letter-spacing: 5px;
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.table-content {
|
||||
box-shadow: 0px 0px 10px #ddd;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user