初始化版本,引入智慧食堂模块,第三方模块

This commit is contained in:
zjf committed 2024-12-06 18:48:18 +08:00
commit d22f88efd3
677 files changed
+69341

No files matched your search

@@ -0,0 +1,56 @@
<script lang="ts">
export default {
title: '审计日志',
icon: 'DocumentCopy',
description: '审计日志列表',
};
</script>
<template>
<el-card class="box-card h-96">
<template #header>
<div class="card-header">
<span>{{ $t('home.auditLogsTip') }}</span>
<el-button link class="button" text @click="handleRoutr">{{ $t('home.moreTip') }}</el-button>
</div>
</template>
<el-timeline v-if="auditState.dataList.length > 0">
<el-timeline-item v-for="(item, index) in auditState.dataList" :key="index" :timestamp="item.createTime">
{{ item.createBy }} : {{ item.auditField }} {{ item.afterVal }} => {{ item.beforeVal }}
</el-timeline-item>
</el-timeline>
<el-empty :image-size="120" v-else />
</el-card>
</template>
<script setup lang="ts" name="SysAuditLogDashboard">
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList } from '/@/api/admin/audit';
const router = useRouter();
// 创建基本表格参数对象
const auditState: BasicTableProps = reactive({
queryForm: {},
pageList: fetchList,
descs: ['create_time'],
pagination: {
size: 4, // 每页显示数据量
},
});
// 使用实例
useTable(auditState);
// 跳转路由
const handleRoutr = () => {
router.push('/admin/audit/index');
};
</script>
<style scoped lang="scss">
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
@@ -0,0 +1,186 @@
<script lang="ts">
export default {
title: '日程管理',
icon: 'Calendar',
description: '日历组件展示',
};
</script>
<template>
<el-card class="h-96 box-card">
<Calendar
view="weekly"
:locale="locale"
:attributes="reminders"
ref="calendar"
title-position="center"
@did-move="weeknumberClick"
@dayclick="dayClick"
:masks="masks"
v-bind="themeConfig?.isDark ? { 'is-dark': true } : {}"
transparent
borderless
expanded
/>
<div v-if="calendar" class="py-4 px-6 w-full h-[18rem] overflow-y-auto">
<template v-for="{ day, cells } in Object.values(dayCells)">
<ul v-if="cells.length > 0" class="py-2 space-y-2" :key="day">
<li v-for="cell in cells" :key="cell">
<div class="flex items-center space-x-4">
<!--Icon-->
<div class="flex-grow-0 flex-shrink-0">
<div :class="`flex justify-center items-center w-10 h-10 rounded-lg bg-blue-100 text-blue-500 dark:bg-blue-400 dark:text-white`">
<el-icon>
<CircleCheck />
</el-icon>
</div>
</div>
<div class="flex items-center justify-between flex-grow">
<div>
<p class="font-medium">
{{ cell.data.customData.title }}
</p>
<p class="text-xs font-medium text-gray-400 dark:text-gray-400 leading-2">
{{ cell.data.customData.scheduleDate }} {{ cell.data.customData.scheduleTime }}
</p>
</div>
</div>
<el-switch @change="changeSwitch(cell.data.customData.id)"></el-switch>
</div>
</li>
</ul>
</template>
<el-empty :image-size="120" v-if="reminders.length === 0" class="text-center" />
</div>
<!-- 新增日程的表单 -->
<schedule-form ref="scheduleFormRef" @refresh="initscheduleList()" />
<!-- 日程查询 -->
<schedule ref="scheduleRef" @refresh="initscheduleList()" />
</el-card>
</template>
<script setup lang="ts" name="scheduleCalendar">
import { Calendar } from 'v-calendar';
import 'v-calendar/style.css';
import { useThemeConfig } from '/@/stores/themeConfig';
import { parseDate } from '/@/utils/formatTime';
import { list, putObj } from '/@/api/admin/schedule';
const ScheduleForm = defineAsyncComponent(() => import('/@/views/home/schedule/form.vue'));
const Schedule = defineAsyncComponent(() => import('/@/views/home/schedule/index.vue'));
// 获取当前国际化方言
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const locale = computed(() => {
return themeConfig.value.globalI18n;
});
const masks = ref({
weekdays: 'WWW',
});
const reminders = ref([]);
const calendar = ref<any>(null);
const scheduleFormRef = ref();
const scheduleRef = ref();
// 添加类型定义
interface DayCell {
day: string;
cells: Array<{
data: {
customData: {
id: string;
title: string;
scheduleDate: string;
scheduleTime: string;
};
};
}>;
}
// 修改 dayCells 的类型定义
const dayCells = computed<Record<string, DayCell>>(() => {
if (!calendar.value) return {};
return calendar.value.dayCells || {};
});
// 日期范围的开始日期引用
const startDateRef = ref();
// 日期范围的结束日期引用
const endDateRef = ref();
// 点击周数的回调函数
const weeknumberClick = (page: any) => {
// 获取当前周的第一天和最后一天的日期
const startDate = page[0].viewDays[0].id;
const endDate = page[0].viewDays[6].id;
// 更新开始日期引用和结束日期引用
startDateRef.value = startDate;
endDateRef.value = endDate;
// 初始化日程列表
initscheduleList();
};
//处理日程安排事件,若当前日期下没有日程则打开表单对话框,否则打开日程详情页面
const dayClick = (day: any) => {
if (filterCellSelected(day.id)) {
scheduleRef.value.open({ date: parseDate(day.id, null) });
} else {
scheduleFormRef.value.openDialog(null, { date: parseDate(day.id, null) });
}
};
// 修改开关状态
const changeSwitch = async (id: string) => {
// 修改对象的状态为'3'
await putObj({ id: id, scheduleState: '3' });
// 初始化调度列表
initscheduleList();
};
const initscheduleList = () => {
// 初始化日程列表
list({
startDate: startDateRef.value,
endDate: endDateRef.value,
}).then((res) => {
// 获取返回结果的数据并转换为合适的格式
reminders.value = res.data.map((item: any) => {
return {
key: item.id,
highlight: {
color: 'primary',
fillMode: 'outline',
},
dates: item.scheduleDate,
customData: item,
};
});
});
};
// 过滤日历中选中的单元格是否有日程
const filterCellSelected = (day: string) => {
return (
reminders.value.filter((item: any) => {
return item.dates.includes(day);
}).length > 0
);
};
onMounted(() => {
initscheduleList();
});
</script>
<style scoped lang="scss">
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
@@ -0,0 +1,78 @@
<script lang="ts">
export default {
title: '当前用户',
icon: 'User',
description: '获取当前用户信息展示',
};
</script>
<template>
<el-card class="flex items-center justify-center h-[191px]">
<div class="flex items-center justify-center">
<div class="relative">
<img class="object-cover w-16 h-16 rounded-full" :src="userData.avatar" alt="Avatar" />
<div class="absolute inset-0 rounded-full shadow-inner"></div>
</div>
<div class="ml-8">
<h2 class="text-lg font-bold dark:text-gray-100">{{ userData.name }}</h2>
<p class="text-gray-600 dark:text-gray-400">{{ userData.deptName }} | {{ userData.postName }}</p>
</div>
</div>
</el-card>
</template>
<script setup lang="ts" name="currentUser">
import { useUserInfo } from '/@/stores/userInfo';
import { getObj } from '/@/api/admin/user';
import { getCurrentInstance } from 'vue';
const instance = getCurrentInstance();
const proxy = instance?.proxy;
if (!proxy) {
throw new Error('Failed to get component instance');
}
const userData = ref({
postName: '',
name: '',
username: '',
userId: '',
avatar: '',
deptName: '',
} as any);
const loading = ref(false);
onMounted(() => {
const data = useUserInfo().userInfos;
initUserInfo(data.user.userId);
});
/**
* 根据用户 ID 初始化用户信息。
* @param {any} userId - 要查询的用户 ID。
* @returns {Promise<void>} - 初始化用户信息的 Promise 实例。
*/
const initUserInfo = async (userId: any): Promise<void> => {
try {
loading.value = true; // 显示加载状态
const res = await getObj(userId); // 执行查询操作
userData.value = res.data; // 将查询到的数据保存到 userData 变量中
userData.value.postName = res.data?.postList?.map((item: any) => item.postName).join(',') || ''; // 将 postList 中的 postName 合并成字符串并保存到 userData 变量中
// 文件上传增加后端前缀
userData.value.avatar = proxy.baseURL + res.data.avatar;
} finally {
loading.value = false; // 结束加载状态
}
};
</script>
<style scoped>
.info {
margin-left: 8px;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>
@@ -0,0 +1,99 @@
<script lang="ts">
export default {
title: '示例图表1',
icon: 'DocumentCopy',
description: '示例图表无意义,可删除',
};
</script>
<template>
<el-card class="relative h-full">
<v-chart class="w-full h-80" :option="option"/>
</el-card>
</template>
<script setup lang="ts" name="demo-chart1">
import VChart from 'vue-echarts';
import {use} from 'echarts/core';
import {PieChart} from 'echarts/charts';
import {TooltipComponent, LegendComponent} from 'echarts/components';
import {CanvasRenderer} from 'echarts/renderers';
import {useTagsViewRoutes} from "/@/stores/tagsViewRoutes";
import {useMessage} from "/@/hooks/message";
use([TooltipComponent, LegendComponent, PieChart, CanvasRenderer]);
const option = reactive({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
},
legend: {
data: ['Java17', 'Java8', 'Other'],
},
series: [
{
name: 'Java',
type: 'pie',
selectedMode: 'single',
radius: [0, '30%'],
label: {
position: 'inner',
fontSize: 14,
},
labelLine: {
show: false,
},
data: [
{value: 1548, name: 'Java17'},
{value: 775, name: 'Java8'},
{value: 679, name: 'Other', selected: true},
],
},
{
name: '使用率',
type: 'pie',
radius: ['45%', '60%'],
labelLine: {
length: 30,
},
label: {
formatter: '{a|{a}}{abg|}\n{hr|}\n {b|{b}:}{c} {per|{d}%} ',
backgroundColor: '#F6F8FC',
borderColor: '#8C8D8E',
borderWidth: 1,
borderRadius: 4,
rich: {
a: {
color: '#6E7079',
lineHeight: 22,
align: 'center',
},
hr: {
borderColor: '#8C8D8E',
width: '100%',
borderWidth: 1,
height: 0,
},
b: {
color: '#4C5058',
fontSize: 14,
fontWeight: 'bold',
lineHeight: 33,
},
per: {
color: '#fff',
backgroundColor: '#4C5058',
padding: [3, 4],
borderRadius: 4,
},
},
},
data: [
{value: 1048, name: 'Java17'},
{value: 335, name: 'Java8'},
{value: 310, name: 'Other'},
],
},
],
});
</script>
@@ -0,0 +1,55 @@
<script lang="ts">
export default {
title: '示例图表2',
icon: 'DocumentCopy',
description: '示例图表无意义,可删除',
};
</script>
<template>
<el-card class="relative h-full">
<v-chart class="w-full h-80" :option="option" />
</el-card>
</template>
<script setup lang="ts" name="log-line-chart">
import VChart from 'vue-echarts';
import { use } from 'echarts/core';
import { ScatterChart } from 'echarts/charts';
import { TitleComponent, GridComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
use([TitleComponent, ScatterChart, CanvasRenderer, GridComponent]);
const option = reactive({
xAxis: {},
yAxis: {},
series: [
{
symbolSize: 20,
data: [
[10.0, 8.04],
[8.07, 6.95],
[13.0, 7.58],
[9.05, 8.81],
[11.0, 8.33],
[14.0, 7.66],
[13.4, 6.81],
[10.0, 6.33],
[14.0, 8.96],
[12.5, 6.82],
[9.15, 7.2],
[11.5, 7.2],
[3.03, 4.23],
[12.2, 7.83],
[2.02, 4.47],
[1.05, 3.33],
[4.05, 4.96],
[6.03, 7.24],
[12.0, 6.26],
[12.0, 8.84],
[7.08, 5.82],
[5.02, 5.68],
],
type: 'scatter',
},
],
});
</script>
@@ -0,0 +1,14 @@
<script lang="ts">
export default {
title: '常用流程',
icon: 'Star',
description: '常用流程收藏',
};
</script>
<template>
<Shortcut :title="$t('home.quickNavigationFlowsTip')" :empty-description="$t('home.addFlowsRoutesTip')" type="flow"></Shortcut>
</template>
<script setup lang="ts" name="SysFavoriteDashboard">
const Shortcut = defineAsyncComponent(() => import('/@/views/home/shortcut/index.vue'));
</script>
@@ -0,0 +1,14 @@
<script lang="ts">
export default {
title: '常用功能',
icon: 'Star',
description: '常用功能收藏',
};
</script>
<template>
<Shortcut :title="$t('home.quickNavigationToolsTip')" :empty-description="$t('home.addFavoriteRoutesTip')" type="menu"></Shortcut>
</template>
<script setup lang="ts" name="SysFavoriteDashboard">
const Shortcut = defineAsyncComponent(() => import('/@/views/home/shortcut/index.vue'));
</script>
@@ -0,0 +1,95 @@
<script lang="ts">
export default {
title: '工作流信息',
icon: 'PieChart',
description: '获取工作流信息展示',
};
</script>
<template>
<el-card class="flex items-center justify-center h-[191px]">
<div class="relative flex items-center justify-center">
<!-- 待办任务 -->
<div class="flex-row items-center w-auto">
<div class="p-3 rounded-full bg-lightPrimary dark:bg-navy-700">
<span class="flex items-center text-brand-500 dark:text-white">
<svg
t="1697994355915"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="8441"
width="32"
height="32"
>
<path
d="M404.266667 85.333333v78.933334h236.8V85.333333h78.933333v78.933334h157.866667A39.466667 39.466667 0 0 1 917.333333 203.733333v631.466667A39.466667 39.466667 0 0 1 877.866667 874.666667H167.466667A39.466667 39.466667 0 0 1 128 835.2V203.733333A39.466667 39.466667 0 0 1 167.466667 164.266667h157.866666V85.333333h78.933334zM838.4 361.6H206.933333V795.733333h631.466667V361.6z m-195.904 84.309333l55.808 55.786667-195.370667 195.370667-139.562666-139.562667 55.893333-55.808 83.712 83.754667 139.541333-139.541334h-0.021333z"
fill="#8a8a8a"
p-id="8442"
></path>
</svg>
</span>
</div>
</div>
<router-link to="/flow/task/pending">
<div class="flex flex-col justify-center w-auto ml-4 h-50">
<p class="text-sm font-medium text-gray-600 font-dm">{{ $t('home.pendingTask') }}</p>
<h4 class="text-xl font-bold text-navy-700 dark:text-white">{{ state.pendingNum }}</h4>
</div>
</router-link>
<!-- 抄送任务 -->
<div class="flex-row items-center w-auto ml-8">
<div class="p-3 rounded-full bg-lightPrimary dark:bg-navy-700">
<span class="flex items-center text-brand-500 dark:text-white">
<svg
t="1697994410210"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="9746"
width="32"
height="32"
>
<path
d="M382.293 692.497a37.956 37.956 0 0 1-32.29 7.373v0.205l-278.118-84.31a39.39 39.39 0 0 1-5.803-73.113l862.14-420.045a39.39 39.39 0 0 1 55.432 45.056L823.228 800.358a39.39 39.39 0 0 1-49.63 27.99l-295.595-89.566a15.428 15.428 0 0 1-12.083-14.814c0-4.437 1.98-8.397 5.12-11.196l356.762-401.135-445.44 380.792h-0.069z m98.031 93.867l54 17.886a15.428 15.428 0 0 1 7.986 23.142l-53.93 81.92a15.428 15.428 0 0 1-28.33-8.533v-99.806a15.428 15.428 0 0 1 20.274-14.61z"
p-id="9747"
fill="#8a8a8a"
></path>
</svg>
</span>
</div>
</div>
<router-link to="/flow/task/cc">
<div class="flex flex-col justify-center w-auto ml-4 h-50">
<p class="text-sm font-medium text-gray-600 font-dm">{{ $t('home.copyTask') }}</p>
<h4 class="text-xl font-bold text-navy-700 dark:text-white">{{ state.copyNum }}</h4>
</div>
</router-link>
</div>
</el-card>
</template>
<script setup lang="ts" name="flowData">
import { queryTaskData } from '/@/api/flow/task';
const state = reactive({
pendingNum: 0,
copyNum: 0,
});
onMounted(async () => {
try {
const { data } = await queryTaskData();
state.pendingNum = Number.parseInt(data?.pendingNum || 0);
state.copyNum = Number.parseInt(data?.copyNum || 0);
} catch (error) {
// 避免没有启动 flow模块 vue 组件渲染 warning
}
});
</script>
<style scoped>
.el-col {
text-align: center;
}
</style>
@@ -0,0 +1,12 @@
const resultComps = {};
const files = import.meta.glob('./*.vue', { import: 'default', eager: true });
Object.keys(files).forEach((fileName) => {
if (fileName) {
// @ts-ignore
resultComps[fileName.replace(/^\.\/(.*)\.\w+$/, '$1')] = files[fileName];
}
});
export default markRaw(resultComps);
@@ -0,0 +1,73 @@
<script lang="ts">
export default {
title: '系统公告',
icon: 'MuteNotification',
description: '系统公告展示',
};
</script>
<template>
<el-card class="h-96">
<template #header>
<div class="card-header">
<span>{{ $t('home.newsletterTip') }}</span>
<el-button link class="button" text @click="openList">{{ $t('home.moreTip') }}</el-button>
</div>
</template>
<el-timeline v-if="newsList.length > 0">
<div class="relative">
<el-timeline-item v-for="(item, index) in newsList" :key="index" :timestamp="item.createTime" @click="contentRef.openDialog(item)">
<button class="absolute right-0 px-3 rotate-[10deg] -top-2 border bg-primary text-white font-bold">
{{ item.readFlag === '1' ? $t('msg.readed') : $t('msg.unread') }}
</button>
<div class="p-2 border border-gray-400 purple_border">
{{ item.title }}
</div>
</el-timeline-item>
</div>
</el-timeline>
<el-empty :image-size="120" v-else />
</el-card>
<!-- 消息列表 -->
<news-lists ref="listRef"/>
<!-- 消息内容 -->
<news-content ref="contentRef" @refresh="getUserMessage" />
</template>
<script setup lang="ts" name="newsLetter">
import { fetchUserMessageList } from '/@/api/admin/message';
const NewsContent = defineAsyncComponent(() => import('/@/views/home/news/content.vue'));
const NewsLists = defineAsyncComponent(() => import('/@/views/home/news/list.vue'));
const listRef = ref();
const contentRef = ref();
const visible = ref(false);
const showList = ref(false);
const newsList = ref([]);
// 获取用户的信息
const getUserMessage = () => {
// 取前三条数据
return fetchUserMessageList({ current: 1, size: 3, category: '0' }).then((res) => {
newsList.value = res.data.records;
});
};
const openList = () => {
showList.value = true;
listRef.value.openDialog('0');
};
onMounted(() => {
getUserMessage();
});
</script>
<style scoped lang="scss">
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
@@ -0,0 +1,15 @@
<script lang="ts">
export default {
title: '请求折线图',
icon: 'DocumentCopy',
description: '折线图示例',
};
</script>
<template>
<el-card class="relative h-full">
<log-line-chart />
</el-card>
</template>
<script setup lang="ts" name="line-chart">
const LogLineChart = defineAsyncComponent(() => import('/@/views/admin/log/line-chart.vue'));
</script>
@@ -0,0 +1,57 @@
<script lang="ts">
export default {
title: '系统日志',
icon: 'Monitor',
description: '系统日志列表',
};
</script>
<template>
<el-card class="h-96 box-card">
<template #header>
<div class="card-header">
<span>{{ $t('home.systemLogsTip') }}</span>
<el-button link class="button" text @click="handleRoutr">{{ $t('home.moreTip') }}</el-button>
</div>
</template>
<el-timeline v-if="logState.dataList.length > 0">
<el-timeline-item v-for="(item, index) in logState.dataList" :key="index" :timestamp="item.createTime">
{{ item.title }} - {{ item.remoteAddr }}
</el-timeline-item>
</el-timeline>
</el-card>
</template>
<script setup lang="ts" name="SysLogDashboard">
import { BasicTableProps, useTable } from '/@/hooks/table';
import { pageList } from '/@/api/admin/log';
const router = useRouter();
// 创建基本表格参数对象
const logState: BasicTableProps = reactive({
pageList, // 分页列表数据
descs: ['create_time'], // 排序方式
pagination: {
size: 4, // 每页显示数据量
},
});
// 使用实例
useTable(logState);
/**
* 处理路由跳转事件
* @function
*/
const handleRoutr = () => {
router.push('/admin/log/index'); // 跳转到日志管理页面
};
</script>
<style scoped lang="scss">
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
+484
View File
@@ -0,0 +1,484 @@
<!--
基于 SCUI 重构适配 vite 加载和适配相关业务页面。https://gitee.com/lolicode/scui/tree/master/src/views/home
-->
<template>
<div>
<div :class="['widgets-home', customizing ? 'customizing' : '']" ref="main">
<div class="widgets-content">
<div class="widgets-top">
<div class="flex justify-end custom_btn">
<el-button v-if="customizing" type="primary" round @click="save">完成</el-button>
<el-button v-else type="primary" round @click="custom">自定义</el-button>
</div>
</div>
<div class="widgets" ref="widgets">
<div class="widgets-wrapper">
<div v-if="nowCompsList.length <= 0" class="no-widgets">
<el-empty description="没有部件啦" :image-size="280"></el-empty>
</div>
<el-row :gutter="2">
<el-col :gutter="2" v-for="(item, index) in grid.layout" v-bind:key="index" :md="item" :xs="24">
<draggable
v-model="grid.copmsList[index]"
animation="200"
handle=".customize-overlay"
group="people"
item-key="com"
dragClass="aaaaa"
force-fallback
fallbackOnBody
class="draggable-box"
>
<template #item="{ element }">
<div class="widgets-item">
<component :is="allComps[element]"></component>
<div v-if="customizing" class="customize-overlay">
<el-button class="close" type="danger" plain icon="Close" size="small" @click="remove(element)"></el-button>
<label>
<el-icon>
<component :is="allComps[element].icon" />
</el-icon>
{{ allComps[element].title }}</label
>
</div>
</div>
</template>
</draggable>
</el-col>
</el-row>
</div>
</div>
</div>
<div v-if="customizing" class="widgets-aside">
<el-container>
<el-header>
<div class="widgets-aside-title">添加部件</div>
<div class="widgets-aside-close" @click="close()">
<el-icon><Close /></el-icon>
</div>
</el-header>
<el-header style="height: auto">
<div class="selectLayout">
<div class="selectLayout-item item01" :class="{ active: grid.layout.join(',') === '12,6,6' }" @click="setLayout([12, 6, 6])">
<el-row :gutter="2">
<el-col :span="7"><span></span></el-col>
<el-col :span="7"><span></span></el-col>
<el-col :span="10"><span></span></el-col>
</el-row>
</div>
<div class="selectLayout-item item02" :class="{ active: grid.layout.join(',') === '24,16,8' }" @click="setLayout([24, 16, 8])">
<el-row :gutter="2">
<el-col :span="24"><span></span></el-col>
<el-col :span="16"><span></span></el-col>
<el-col :span="8"><span></span></el-col>
</el-row>
</div>
<div class="selectLayout-item item03" :class="{ active: grid.layout.join(',') === '24' }" @click="setLayout([24])">
<el-row :gutter="2">
<el-col :span="24"><span></span></el-col>
<el-col :span="24"><span></span></el-col>
<el-col :span="24"><span></span></el-col>
</el-row>
</div>
</div>
</el-header>
<el-main class="nopadding">
<div class="widgets-list">
<div v-if="myCompsList.length <= 0" class="widgets-list-nodata">
<el-empty description="没有部件啦" :image-size="60"></el-empty>
</div>
<div v-for="item in myCompsList" :key="item.title" class="widgets-list-item">
<div class="item-logo">
<el-icon>
<component :is="item.icon" />
</el-icon>
</div>
<div class="item-info">
<h2>{{ item.title }}</h2>
<p>{{ item.description }}</p>
</div>
<div class="item-actions">
<el-button type="primary" icon="el-icon-plus" size="small" @click="push(item)"></el-button>
</div>
</div>
</div>
</el-main>
<el-footer style="height: 51px">
<el-button size="small" @click="backDefaul()">恢复默认</el-button>
</el-footer>
</el-container>
</div>
</div>
</div>
</template>
<script lang="ts" name="widgets" setup>
import draggable from 'vuedraggable';
import allComps from './components/index';
import { Local } from '/@/utils/storage';
import { useUserInfo } from '/@/stores/userInfo';
// 默认布局设置
const defaultGrid = ref({
layout: [7, 7, 10],
copmsList: [
['current-user', 'flow-data', 'audit-log', 'sys-log-line'],
['news', 'sys-log', 'demo-chart1'],
['calendar', 'favorite-menu','favorite-flow', 'demo-chart2'],
],
});
const customizing = ref(false);
const widgets = ref();
const widgetsKey = ref('widgets');
const grid = ref(JSON.parse(JSON.stringify(defaultGrid.value)));
const allCompsList = computed(() => {
const list = [];
for (const [key, { title, icon, description }] of Object.entries(allComps)) {
list.push({ key, title, icon, description });
}
const myCopmsList = grid.value.copmsList.flat();
list.forEach((comp) => {
const existingItem = myCopmsList.find((item) => item === comp.key);
comp.disabled = !!existingItem;
});
return list;
});
const myCompsList = computed(() => {
// 支持列表
const myGrid = [
'calendar',
'current-user',
'news',
'audit-log',
'sys-log',
'flow-data',
'favorite-menu',
'favorite-flow',
'sys-log-line',
'demo-chart1',
'demo-chart2',
];
return allCompsList.value.filter((item) => !item.disabled && myGrid.includes(item.key));
});
const nowCompsList = computed(() => grid.value.copmsList.flat());
const custom = () => {
customizing.value = true;
nextTick(() => {
const oldWidth = widgets.value.offsetWidth;
const scale = widgets.value.offsetWidth / oldWidth;
widgets.value.style.transform = `scale(${scale})`;
});
};
const setLayout = (layout: Array<number>) => {
grid.value.layout = layout;
if (layout.join(',') === '24') {
if (grid.value.copmsList[1]) {
grid.value.copmsList[0].push(...grid.value.copmsList[1]);
}
if (grid.value.copmsList[2]) {
grid.value.copmsList[0].push(...grid.value.copmsList[2]);
}
grid.value.copmsList[1] = [];
grid.value.copmsList[2] = [];
}
};
const push = (item: any) => {
grid.value.copmsList[0].push(item.key);
};
const remove = (item: any) => {
grid.value.copmsList = grid.value.copmsList.map((obj) => obj.filter((o) => o !== item));
};
const save = () => {
customizing.value = false;
widgets.value.style.removeProperty('transform');
Local.set(widgetsKey.value, JSON.stringify(grid.value));
};
const backDefaul = () => {
customizing.value = false;
widgets.value.style.removeProperty('transform');
grid.value = defaultGrid.value;
Local.remove(widgetsKey.value);
// 重新加载页面
window.location.reload();
};
const close = () => {
customizing.value = false;
widgets.value.style.removeProperty('transform');
};
onMounted(() => {
// 初始化key
const data = useUserInfo().userInfos;
widgetsKey.value = `${window.location.host}-${data.user.userId}-widgets}`;
let widgets = Local.get(widgetsKey.value);
grid.value = widgets ? JSON.parse(widgets) : defaultGrid.value;
});
</script>
<style scoped lang="scss">
.custom_btn {
position: absolute;
top: 7px;
right: 5px;
z-index: 9;
color: white;
}
.widgets-home {
display: flex;
flex-direction: row;
flex: 1;
height: 100%;
}
.widgets-content {
flex: 1;
overflow: auto;
overflow-x: hidden;
padding: 5px;
}
.widgets-aside {
width: 360px;
background: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
position: relative;
overflow: auto;
}
.widgets-aside-title {
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.widgets-aside-title i {
margin-right: 10px;
font-size: 18px;
}
.widgets-aside-close {
font-size: 18px;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
cursor: pointer;
}
.widgets-aside-close:hover {
background: rgba(180, 180, 180, 0.1);
}
.widgets-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.widgets-top-title {
font-size: 18px;
font-weight: bold;
}
.widgets {
transform-origin: top left;
transition: transform 0.15s;
}
.draggable-box {
height: 100%;
}
.customizing .widgets-wrapper {
margin-right: -360px;
width: 100%;
}
.customizing .widgets-wrapper .el-col {
padding-bottom: 15px;
}
.customizing .widgets-wrapper .draggable-box {
border: 1px dashed var(--el-color-primary);
padding: 15px;
}
.customizing .widgets-wrapper .no-widgets {
display: none;
}
.widgets-item {
position: relative;
margin-bottom: 2px;
}
.customize-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.9);
cursor: move;
}
.customize-overlay label {
background: var(--el-color-primary);
color: #fff;
height: 40px;
padding: 0 30px;
border-radius: 40px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
cursor: move;
}
.customize-overlay label i {
margin-right: 15px;
font-size: 24px;
}
.customize-overlay .close {
position: absolute;
top: 15px;
left: 15px;
}
.widgets-list-item {
display: flex;
flex-direction: row;
padding: 15px;
align-items: center;
}
.widgets-list-item .item-logo {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(180, 180, 180, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
margin-right: 15px;
color: #6a8bad;
}
.widgets-list-item .item-info {
flex: 1;
}
.widgets-list-item .item-info h2 {
font-size: 16px;
font-weight: normal;
cursor: default;
}
.widgets-list-item .item-info p {
font-size: 12px;
color: #999;
cursor: default;
}
.widgets-list-item:hover {
background: rgba(180, 180, 180, 0.1);
}
.widgets-wrapper .sortable-ghost {
opacity: 0.5;
}
.selectLayout {
width: 100%;
display: flex;
}
.selectLayout-item {
width: 60px;
height: 60px;
border: 2px solid var(--el-border-color-light);
padding: 5px;
cursor: pointer;
margin-right: 15px;
}
.selectLayout-item span {
display: block;
background: var(--el-border-color-light);
height: 46px;
}
.selectLayout-item.item02 span {
height: 30px;
}
.selectLayout-item.item02 .el-col:nth-child(1) span {
height: 14px;
margin-bottom: 2px;
}
.selectLayout-item.item03 span {
height: 14px;
margin-bottom: 2px;
}
.selectLayout-item:hover {
border-color: var(--el-color-primary);
}
.selectLayout-item.active {
border-color: var(--el-color-primary);
}
.selectLayout-item.active span {
background: var(--el-color-primary);
}
.dark {
.widgets-aside {
background: #2b2b2b;
}
.customize-overlay {
background: rgba(43, 43, 43, 0.9);
}
}
@media (max-width: 992px) {
.customizing .widgets {
transform: scale(1) !important;
}
.customizing .widgets-aside {
width: 100%;
position: absolute;
top: 50%;
right: 0;
bottom: 0;
}
.customizing .widgets-wrapper {
margin-right: 0;
}
}
</style>