初始版本提交

This commit is contained in:
yaoyn
2024-02-05 09:15:37 +08:00
parent b52d4414be
commit 445292105f
1848 changed files with 236859 additions and 75 deletions

View File

@ -0,0 +1,31 @@
import { withInstall } from '/@/utils';
import resizeFixedLayout from './src/ResizeFixedLayout.vue';
import ModalPanelVue from './src/ModalPanel.vue';
import SearchBoxVue from './src/SearchBox.vue';
import EmptyBoxVue from './src/EmptyBox.vue';
import NodeHeadVue from './src/NodeHead.vue';
import designLogo from './src/DesignLogo.vue';
import loadingBox from './src/LoadingBox.vue';
import fewerLeft from './src/FewerLeft.vue';
import fewerRight from './src/FewerRight.vue';
import zoomInOrOut from './src/ZoomInOrOut.vue';
import pageLayout from './src/PageLayout.vue';
import desktopDesignEmpty from './src/DesktopDesignEmpty.vue';
export const ResizeFixedLayout = withInstall(resizeFixedLayout);
export const DesignLogo = withInstall(designLogo); // 设计页面公司LOGO图标
export const DesktopDesignEmpty = withInstall(desktopDesignEmpty); // 桌面设计空数据显示图片
export const ModalPanel = withInstall(ModalPanelVue);
export const SearchBox = withInstall(SearchBoxVue);
export const EmptyBox = withInstall(EmptyBoxVue);
export const NodeHead = withInstall(NodeHeadVue); // 标题
export const LoadingBox = withInstall(loadingBox); // Loading
export const FewerLeft = withInstall(fewerLeft); //收取左边图标
export const FewerRight = withInstall(fewerRight); //收取右边图标
export const ZoomInOrOut = withInstall(zoomInOrOut); //放大缩小
export const PageLayout = withInstall(pageLayout); // 布局

View File

@ -0,0 +1,11 @@
<template>
<img :src="logoImg" width="208" style="height: 64px" />
</template>
<script lang="ts" setup>
import logo from '/@/assets/images/design/logo.png';
import { useAppStore } from '/@/store/modules/app';
const appStore = useAppStore();
const logoConfig = appStore.getLogoConfig;
const logoImg = logoConfig.designerLogoUrl || logo;
</script>

View File

@ -0,0 +1,3 @@
<template>
<img src="./../../../assets/desktop/empty.png" />
</template>

View File

@ -0,0 +1,56 @@
<template>
<div class="wrap">
<div class="empty-box">
<IconFontSymbol v-if="hasIcon" icon="tishi" class="empty-icon" />
<div class="msg">{{ msg }}</div>
</div>
</div>
</template>
<script lang="ts">
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const defaultMsg = t('暂无数据');
export default {};
</script>
<script setup lang="ts">
import IconFontSymbol from '/@/components/IconFontSymbol/Index.vue';
defineProps({
msg: {
type: String,
default: defaultMsg,
},
hasIcon: {
type: Boolean,
default: true,
},
});
</script>
<style scoped>
.wrap {
width: 100%;
height: 100%;
min-height: 80px;
display: flex;
justify-content: center;
align-items: center;
}
.empty-box {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.empty-icon {
font-size: 36px;
}
.msg {
font-size: 14px;
color: #9e9d9d;
padding: 10px 40px;
}
</style>

View File

@ -0,0 +1,9 @@
<template>
<img src="./../../../assets/workflow/fewer-left.png" class="img" />
</template>
<style lang="less" scoped>
.img {
width: 16px;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,9 @@
<template>
<img src="./../../../assets/workflow/fewer-right.png" class="img" />
</template>
<style lang="less" scoped>
.img {
width: 16px;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,22 @@
<template>
<div class="fix">
<a-spin size="large" />
</div>
</template>
<script setup></script>
<style lang="less" scoped>
.fix {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.6);
z-index: 2000;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<a-modal
:visible="visible"
:title="title"
:maskClosable="false"
:width="hasLeftSlot ? 1200 : width || 600"
:okText="t('确定')"
:cancelText="t('取消')"
@ok="$emit('submit')"
@cancel="$emit('close')"
>
<slot name="header"></slot>
<div class="content">
<div :class="['left', isDeptSelect ? 'left-box' : '']" v-if="hasLeftSlot">
<slot name="left"></slot>
</div>
<div class="right">
<slot></slot>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { computed, useSlots } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
defineEmits(['submit', 'close']);
defineProps({
title: String,
width: Number,
visible: { type: Boolean, default: false },
isDeptSelect: { type: Boolean, default: false },
});
const hasLeftSlot = computed(() => {
return !!useSlots().left;
});
</script>
<style lang="less" scoped>
.content {
height: 520px;
display: flex;
margin: 10px;
overflow: auto;
.left {
flex-basis: 310px;
margin-right: 10px;
}
.right {
flex: 1;
}
}
:deep(.search-box) {
padding: 10px 20px;
}
:deep(.list-box) {
display: flex;
flex-wrap: wrap;
overflow-y: auto;
padding: 10px 0;
}
:deep(.list-page-box) {
display: flex;
flex-wrap: wrap;
overflow-y: auto;
padding: 10px 0;
}
:deep(.page-box) {
position: absolute;
bottom: 80px;
right: 20px;
}
:deep(.ant-spin-nested-loading) {
height: 480px;
margin-top: 10px;
}
:deep(.title) {
font-size: 16px;
color: #333;
padding: 0 0 10px 10px;
margin: 0 20px;
border-bottom: 1px solid #f0f0f0;
}
:deep(.ant-checkbox-inner) {
border-radius: 50%;
}
:deep(.ant-checkbox-checked .ant-checkbox-inner) {
border-radius: 50%;
}
.left-box {
margin-left: calc(50% - 155px);
}
</style>

View File

@ -0,0 +1,21 @@
<template>
<div class="node-head">
<div class="node-title">{{ nodeName }}</div>
<slot></slot>
</div>
</template>
<script setup lang="ts">
defineProps(['nodeName']);
</script>
<style scoped>
.node-head {
display: flex;
align-items: center;
}
.node-title {
font-size: 16px;
}
</style>

View File

@ -0,0 +1,182 @@
<template>
<div class="layout" :class="layoutClass || ''">
<div class="left-width overflow-hidden" v-if="hasLeftSlot">
<slot name="left"></slot>
<!-- 左侧树 -->
</div>
<div
class="p-3 bg-color"
:class="[hasLeftSlot ? 'right-width' : 'full-width']"
v-if="hasRightSlot"
>
<slot name="search"></slot>
<!-- 搜索栏 -->
<div v-if="layoutOne">
<div v-if="props.title" class=" ">
<div v-if="props.title" class="l-panel--title">{{ props.title }}</div>
<div class="flex justify-between">
<SearchBox
class="w-full"
v-if="hasSearchSlot"
:searchConfig="props.searchConfig"
@search="search"
@scroll-height="$emit('scrollHeight')"
/>
<slot name="operation"></slot>
</div>
<!-- 搜索下操作按钮 -->
</div>
</div>
<div v-else>
<div v-if="hasOperationSlot || props.title || hasSearchSlot" class="node-box">
<NodeHead v-if="props.title" class="mb-3 mt-3" :nodeName="props.title" />
<div class="flex">
<SearchBox
v-if="hasSearchSlot"
:searchConfig="props.searchConfig"
@search="search"
@scroll-height="$emit('scrollHeight')"
/>
<slot name="operation"></slot>
</div>
<!-- 搜索下操作按钮 -->
</div>
</div>
<slot name="right"></slot>
<!-- 右边 table列表 -->
</div>
<div v-if="hasFullSlot" :class="hasLeftSlot ? 'right-width' : 'full-width'">
<slot name="full"></slot>
<!-- 列表<无搜索按钮> -->
</div>
</div>
</template>
<script setup lang="ts">
import { computed, useSlots } from 'vue';
import { NodeHead } from '/@/components/ModalPanel/index';
import SearchBox from './WorkflowSearchBox.vue';
const props = defineProps(['title', 'searchConfig', 'layoutClass', 'layoutOne']);
const emits = defineEmits(['search', 'scrollHeight']);
const hasLeftSlot = computed(() => {
return !!useSlots().left;
});
const hasSearchSlot = computed(() => {
return !!useSlots().search;
});
const hasOperationSlot = computed(() => {
return !!useSlots().operation;
});
const hasRightSlot = computed(() => {
return !!useSlots().right;
});
const hasFullSlot = computed(() => {
return !!useSlots().full;
});
function search(params: any) {
emits('search', params);
}
</script>
<style lang="less" scoped>
.layout {
display: flex;
height: 100%;
padding: 10px;
}
.left-width {
flex-basis: 20%;
border-right: 1px solid #e5e7eb;
}
.right-width {
flex-basis: 80%;
}
.full-width {
flex-basis: 100%;
}
.bg-color {
background-color: #fff;
}
.node-box {
display: flex;
justify-content: space-between;
align-items: center;
}
:deep(.button-box) {
button {
margin-right: 6px;
}
}
// 操作列
:deep(.delete-icon) {
cursor: pointer;
color: @clear-color;
}
:deep(.edit-icon) {
cursor: pointer;
color: @primary-color;
margin-right: 10px;
}
// 左侧树结构样式
:deep(.vben-tree-header) {
height: 60px;
border-bottom: 1px solid #f1f1f1;
}
:deep(.ant-tree) {
padding: 0;
margin-top: 10px;
border-top: 0;
color: #666;
}
:deep(.ant-tree-switcher) {
width: 0;
}
:deep(.ant-tree-title .anticon) {
padding-left: 10px;
}
:deep(.ant-tree .ant-tree-treenode) {
height: 40px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
// 表
:deep(.ant-table-pagination.ant-pagination) {
position: fixed;
bottom: 10px;
right: 30px;
}
.l-panel--title {
align-items: center;
color: #262626;
font-size: 16px;
padding-left: 8px;
padding-right: 8px;
border-bottom: 1px solid #d9d9d9;
height: 24px;
line-height: 24px;
padding-bottom: 32px;
margin-bottom: 8px;
}
</style>
<style>
html[data-theme='dark'] {
.bg-color {
background-color: #151515 !important;
}
}
</style>

View File

@ -0,0 +1,160 @@
<template>
<div class="flow-containers">
<div class="resize-head"><slot name="head"></slot></div>
<div class="resize-layout">
<div class="resize-left" ref="left"><slot name="left"></slot></div>
<div class="resize-shrink-sidebar" title="收缩侧边栏">
<span class="shrink-sidebar-text" v-if="showPanel"></span>
</div>
<div class="resize-right" ref="right">
<div class="right-box">
<slot name="right"></slot>
</div>
</div>
<div class="fewer-panel-box" @click="changeShowPanel">
<component :is="fewerPanelComponent" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import FewerLeft from './FewerLeft.vue';
import FewerRight from './FewerRight.vue';
let left = ref();
let right = ref();
let showPanel = ref(true);
let fewerPanelComponent = computed(() => {
return showPanel.value ? FewerRight : FewerLeft;
});
onMounted(() => {
dragControllerDiv();
});
function changeShowPanel() {
showPanel.value = !showPanel.value;
if (showPanel.value) {
showRightBox();
} else {
hideRightBox();
}
}
function showRightBox() {
left.value.style.width = '65%';
right.value.style.width = '35%';
}
function hideRightBox() {
left.value.style.width = '100%';
right.value.style.width = '0';
}
function changeWidth(moveLen: number, boxClientWidth: number) {
left.value.style.width = moveLen + 'px';
right.value.style.width = boxClientWidth - moveLen - 10 + 'px';
}
function dragControllerDiv() {
let resize = document.getElementsByClassName('resize-shrink-sidebar') as unknown as Array<any>;
let box = document.getElementsByClassName('resize-layout');
for (let i = 0; i < resize.length; i++) {
resize[i].onmousedown = function (e: { clientX: any }) {
let startX = e.clientX;
resize[i].left = resize[i].offsetLeft;
document.onmousemove = function (e) {
let endX = e.clientX;
let moveLen = resize[i].left + (endX - startX);
let maxT = box[i].clientWidth - resize[i].offsetWidth;
if (moveLen < 32) moveLen = 32;
if (moveLen > maxT - 400) moveLen = maxT - 400;
resize[i].style.left = moveLen;
changeWidth(moveLen, box[i].clientWidth);
};
document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
resize[i].releaseCapture && resize[i].releaseCapture();
};
resize[i].setCapture && resize[i].setCapture();
return false;
};
}
}
</script>
<style scoped>
.flow-containers {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
background-color: #fff;
}
[data-theme='dark'] .flow-containers {
background-color: #151515;
}
.head {
height: 50px;
}
.resize-layout {
display: flex;
height: calc(100% - 50px);
width: 100%;
}
.resize-left {
width: 65%;
padding: 0;
overflow: hidden;
}
.resize-shrink-sidebar {
cursor: col-resize;
display: flex;
justify-content: center;
align-items: center;
}
.shrink-sidebar-text {
border-radius: 4px;
/* color: #fff; */
}
.shrink-sidebar:hover {
color: #666;
}
.resize-right {
width: 35%; /*右侧初始化宽度*/
}
.right-box {
height: 100%;
box-shadow: -8px 2px 4px rgb(0 0 0 / 10%);
}
.right-box:hover {
box-shadow: -4px 2px 4px rgb(0 0 0 / 10%);
border-left: 8px solid #fff;
margin-top: -2px;
padding-top: 2px;
margin-left: -8px;
}
[data-theme='dark'] .right-box:hover {
border-left: 8px solid #151515;
}
.fewer-panel-box {
width: 20px;
position: fixed;
top: 80px;
right: 10px;
z-index: 3;
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<div class="search-box">
<a-input v-model:value="keyword" :placeholder="t('请填写搜索内容')" style="width: 300px" />
<a-button type="primary" @click="search" class="serach-btn">{{ t('搜索') }}</a-button>
<a-button @click="cancel" class="serach-btn">{{ t('重置') }}</a-button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const emits = defineEmits(['search']);
let keyword = ref('');
function search() {
emits('search', keyword.value);
}
function cancel() {
keyword.value = '';
emits('search', keyword.value);
}
</script>
<style scoped>
.search-box {
padding: 10px 20px;
}
.serach-btn {
margin-left: 10px;
}
</style>

View File

@ -0,0 +1,217 @@
<template>
<div class="search">
<div
v-for="(item, index) in searchData.list"
:key="index"
class="search-item"
:style="item.type == 'date' ? 'flex-basis: 48%' : ''"
>
<!-- 搜索按钮 -->
<div class="item" v-if="item.type == 'search'">
<a-button type="primary" @click="search" class="search-btn">{{ t('搜索') }}</a-button>
<a-button @click="reset" class="search-btn">{{ t('重置') }}</a-button>
<span @click="expansionOrContraction" class="show-icon" v-if="searchData.showSearchIcon"
>{{ searchData.showAll ? t('收缩') : t('展开') }}
<Icon icon="ion:chevron-forward" :class="searchData.showAll ? 'show' : 'hide'"
/></span>
</div>
<!-- 时间范围 -->
<div class="item" v-else-if="item.type == 'date'">
<div class="label" v-if="item.label">{{ item.label }}</div>
<a-range-picker
v-model:value="searchData.searchForm[item.field]"
:format="dateFormat"
:valueFormat="dateFormat"
style="width: 100%"
/>
</div>
<!-- 用户 -->
<div class="item" v-else-if="item.type == 'user'">
<div class="label" v-if="item.label">{{ item.label }}</div>
<SelectUser
:selectedIds="searchData.searchForm[item.field]"
:multiple="true"
@change="
(ids) => {
searchData.searchForm[item.field] = ids;
}
"
@change-names="
(names) => {
searchData.showData[item.field] = names;
}
"
>
<a-input
:value="searchData.showData[item.field]"
:placeholder="item.label"
style="width: 100%"
>
<template #addonAfter>
<Icon icon="ant-design:user-outlined" />
</template>
</a-input>
</SelectUser>
</div>
<div class="item" v-else>
<div class="label" v-if="item.label">{{ item.label }}</div>
<a-input
v-model:value="searchData.searchForm[item.field]"
:placeholder="t('请输入') + item.label"
style="width: 100%"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive } from 'vue';
import { Icon } from '/@/components/Icon/index';
import { SelectUser } from '/@/components/SelectOrganizational/index';
import { cloneDeep } from 'lodash-es';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
const props = defineProps(['searchConfig']);
const emits = defineEmits(['search', 'scrollHeight']);
// 搜索
onMounted(() => {
initSearch();
});
let searchData: {
showAll: boolean;
showSearchIcon: boolean;
searchForm: any;
showData: any;
originalData: Array<{ field: string; label: string; value: string; type: string }>;
list: Array<{ field: string; label: string; value: string; type: string }>;
} = reactive({
showAll: false,
showSearchIcon: true,
originalData: [],
list: [],
searchForm: {},
showData: {},
});
function initSearch() {
if (props.searchConfig) {
searchData.showAll = true;
searchData.originalData = cloneDeep(props.searchConfig);
searchData.originalData.forEach((element) => {
if (element.field === 'originator' || element.field === 'userID') {
searchData.searchForm[element.field] = [];
searchData.showData[element.field] = '';
} else if (element.field === 'searchDate') {
searchData.searchForm[element.field] = [null, null];
} else {
searchData.searchForm[element.field] = element.value ? element.value : '';
}
});
expansionOrContraction();
}
}
function search() {
let params = {};
for (const key in searchData.searchForm) {
if (Object.prototype.hasOwnProperty.call(searchData.searchForm, key)) {
const value = searchData.searchForm[key];
if (key === 'originator') {
//发起人
if (Array.isArray(value)) {
params['originator'] = value.join(',');
}
} else if (key === 'searchDate') {
if (Array.isArray(value)) {
params['startTime'] = value[0];
params['endTime'] = value[1];
}
} else {
params[key] = value;
}
}
}
emits('search', params);
}
function reset() {
initSearch();
search();
}
// 展开或者收缩
function expansionOrContraction() {
searchData.showAll = !searchData.showAll;
if (searchData.originalData.length > 3) {
searchData.showSearchIcon = true;
searchData.list = cloneDeep(searchData.originalData).splice(0, 3);
searchData.list.push({
field: '',
label: '',
type: 'search',
value: '',
});
if (searchData.showAll) {
searchData.list.push(
...cloneDeep(searchData.originalData).splice(3, searchData.originalData.length),
);
}
} else {
searchData.showSearchIcon = false;
searchData.list = cloneDeep(searchData.originalData);
searchData.list.push({
field: '',
label: '',
type: 'search',
value: '',
});
}
emits('scrollHeight');
}
</script>
<style lang="less" scoped>
// 搜索
.search {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
.search-item {
display: flex;
align-items: center;
padding: 0 4px;
}
.item {
display: flex;
align-items: center;
width: 100%;
}
.search-btn {
margin-left: 4px;
}
.show-icon {
color: @primary-color;
margin: 0 10px;
align-self: center;
}
.show {
transform: rotate(-90deg);
}
.hide {
transform: rotate(90deg);
}
.label {
min-width: 70px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div class="fixed-bottom-item">
<div class="fixed-common-btn" @click="$emit('out')">
<IconFontSymbol icon="fangda" />
</div>
<div class="fixed-common-btn" @click="$emit('in')">
<IconFontSymbol icon="suoxiao" />
</div>
</div>
</template>
<script lang="ts" setup>
import IconFontSymbol from '/@/components/IconFontSymbol/Index.vue';
defineEmits(['in', 'out']);
</script>
<style lang="less" scoped>
.fixed-bottom-item {
display: flex;
box-shadow: 0px 2px 2px 2px rgb(0 0 0 / 4%);
border-radius: 4px;
padding: 4px;
margin-right: 10px;
height: 40px;
}
.fixed-common-btn {
display: flex;
justify-content: center;
align-items: center;
margin-right: 6px;
cursor: pointer;
font-size: 24px;
}
</style>