This commit is contained in:
吴红兵
2025-12-02 10:37:49 +08:00
commit 1f645dad3e
1183 changed files with 147673 additions and 0 deletions

50
src/views/home/i18n/en.ts Normal file
View File

@@ -0,0 +1,50 @@
export default {
home: {
addFavoriteRoutesTip: 'no data , right click on the tab to favorite',
addFlowsRoutesTip: 'no data, please select in Process management to save',
quickNavigationToolsTip: 'quick navigation bar',
quickNavigationFlowsTip: 'quick flow bar',
systemLogsTip: 'system logs',
auditLogsTip: 'audit logs',
moreTip: 'more',
newsletterTip: 'news letter',
pendingTask: 'pending task✍',
copyTask: 'copy task🔖',
schedule: 'schedule',
reminders: 'Reminders',
},
msg: {
readed: 'readed',
unread: 'unread',
},
schedule: {
index: '#',
importsysScheduleTip: 'import SysSchedule',
id: 'id',
title: 'title',
type: 'type',
state: 'state',
content: 'content',
time: 'time',
date: 'date',
createBy: 'createBy',
createTime: 'createTime',
updateBy: 'updateBy',
updateTime: 'updateTime',
delFlag: 'delFlag',
tenantId: 'tenantId',
inputIdTip: 'input id',
inputTitleTip: 'input title',
inputTypeTip: 'input type',
inputStateTip: 'input state',
inputContentTip: 'input content',
inputTimeTip: 'input time',
inputDateTip: 'input date',
inputCreateByTip: 'input createBy',
inputCreateTimeTip: 'input createTime',
inputUpdateByTip: 'input updateBy',
inputUpdateTimeTip: 'input updateTime',
inputDelFlagTip: 'input delFlag',
inputTenantIdTip: 'input tenantId',
},
};

View File

@@ -0,0 +1,50 @@
export default {
home: {
addFavoriteRoutesTip: '当前无数据,请在标签页上右键单击以收藏',
addFlowsRoutesTip: '当前无数据,请在流程管理选择以收藏',
quickNavigationToolsTip: '常用功能',
quickNavigationFlowsTip: '常用流程',
systemLogsTip: '系统日志',
auditLogsTip: '审计日志',
moreTip: '更多',
newsletterTip: '公告',
pendingTask: '待办任务✍️',
copyTask: '抄送任务🔖',
schedule: '日程',
reminders: '提醒',
},
msg: {
readed: '已读',
unread: '未读',
},
schedule: {
index: '#',
importsysScheduleTip: '导入日程',
id: 'id',
title: '标题',
type: '类型',
state: '状态',
content: '内容',
time: '时间',
date: '日期',
createBy: '创建人',
createTime: '创建时间',
updateBy: '修改人',
updateTime: '更新时间',
delFlag: '删除标记',
tenantId: '租户ID',
inputIdTip: '请输入id',
inputTitleTip: '请输入标题',
inputTypeTip: '请输入日程类型',
inputStateTip: '请输入状态',
inputContentTip: '请输入内容',
inputTimeTip: '请输入时间',
inputDateTip: '请输入日期',
inputCreateByTip: '请输入创建人',
inputCreateTimeTip: '请输入创建时间',
inputUpdateByTip: '请输入修改人',
inputUpdateTimeTip: '请输入更新时间',
inputDelFlagTip: '请输入删除标记',
inputTenantIdTip: '请输入租户ID',
},
};

24
src/views/home/index.vue Normal file
View File

@@ -0,0 +1,24 @@
<template>
<div>
<div v-if="pageLoading">
<el-main>
<el-card shadow="never">
<el-skeleton :rows="1"></el-skeleton>
</el-card>
<el-card shadow="never" style="margin-top: 15px;">
<el-skeleton></el-skeleton>
</el-card>
</el-main>
</div>
<widgets/>
</div>
</template>
<script setup lang="ts" name="dashboard">
const Widgets = defineAsyncComponent(() => import('./widgets/index.vue'));
const pageLoading = ref(true);
onMounted(() => {
pageLoading.value = false;
});
</script>

View File

@@ -0,0 +1,43 @@
<template>
<!-- 消息内容 -->
<el-drawer v-model="visible" size="40%">
<div class="flex items-center justify-center -mt-8">
<div class="max-w-md w-full p-6 bg-white rounded-lg shadow-lg">
<h1 class="text-2xl font-semibold text-center text-gray-500 mb-6">{{ currentNew.title }}</h1>
<div class="text-center">
<p class="text-xs text-gray-600 text-center mt-8">&copy; {{ currentNew.createBy }} {{ currentNew.createTime }}</p>
</div>
<p class="text-sm text-gray-600 text-justify mt-8 mb-6" v-html="currentNew.content"></p>
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts" name="newsLetter">
import {readUserMessage} from "/@/api/admin/message";
const emit = defineEmits(['refresh']);
const currentNew = ref()
const visible = ref(false)
// 打开信息内容
const openDialog = (item: any) => {
visible.value = true
currentNew.value = item;
readMessage(item)
}
// 阅读事件
const readMessage = async (item: any) => {
if (item.readFlag === '1') {
return
}
await readUserMessage({id: item.id})
emit('refresh')
}
defineExpose({
openDialog
})
</script>

View File

@@ -0,0 +1,53 @@
<template>
<el-drawer v-model="visible" size="40%">
<el-table :data="state.dataList" v-loading="state.loading" style="width: 100%" @sort-change="sortChangeHandle"
@cell-click="cellClick">
<el-table-column type="index" label="序号" width="60"/>
<el-table-column prop="title" label="标题" show-overflow-tooltip/>
<el-table-column prop="readFlag" label="状态" show-overflow-tooltip>
<template #default="scope">
<el-tag>{{ scope.row.readFlag === '1' ? $t('msg.readed') : $t('msg.unread') }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="时间"/>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination"/>
<!-- 消息内容 -->
<news-content ref="contentRef" @refresh="getDataList"/>
</el-drawer>
</template>
<script setup lang="ts" name="newsList">
import {BasicTableProps, useTable} from '/@/hooks/table';
import {fetchUserMessageList} from '/@/api/admin/message';
const NewsContent = defineAsyncComponent(() => import('./content.vue'));
// 搜索变量
const contentRef = ref()
const visible = ref(false);
// table hook
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {category: '0'},
pageList: fetchUserMessageList,
});
const {getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle} = useTable(state);
// 打开弹窗
const openDialog = (type: string) => {
state.queryForm.category = type
getDataList();
visible.value = true
};
const cellClick = (row: any) => {
contentRef.value.openDialog(row)
}
// 暴露变量
defineExpose({
openDialog
});
</script>

View File

@@ -0,0 +1,155 @@
<template>
<el-dialog :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" v-model="visible">
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item :label="t('schedule.title')" prop="title">
<el-input v-model="form.title" :placeholder="t('schedule.inputTitleTip')" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('schedule.type')" prop="scheduleType">
<el-select v-model="form.scheduleType" :placeholder="t('schedule.inputTypeTip')" clearable default-first-option>
<el-option v-for="item in schedule_type" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('schedule.state')" prop="scheduleState">
<el-select v-model="form.scheduleState" :placeholder="t('schedule.inputStateTip')" clearable default-first-option>
<el-option v-for="item in schedule_status" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('schedule.date')" prop="scheduleDate">
<el-date-picker type="date" :placeholder="t('schedule.inputDateTip')" v-model="form.scheduleDate" :value-format="dateStr" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('schedule.time')" prop="scheduleTime">
<el-time-picker v-model="form.scheduleTime" arrow-control :placeholder="t('schedule.inputTimeTip')" :value-format="timeStr" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('schedule.content')" prop="content">
<editor v-model:get-html="form.content" :placeholder="t('schedule.inputContentTip')" :disable="form.id !== ''" v-if="visible" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="SysScheduleDialog">
import {useMessage} from '/@/hooks/message';
import {addObj, getObj, putObj} from '/@/api/admin/schedule';
import {useI18n} from 'vue-i18n';
import {useDict} from '/@/hooks/dict';
const emit = defineEmits(['refresh']);
// 获取字典内容
const { schedule_type, schedule_status } = useDict('schedule_type', 'schedule_status');
// 使用国际化实例
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 提交表单数据
const form = reactive({
id: '',
title: '',
scheduleType: 'record',
scheduleState: '0',
content: '',
scheduleTime: '',
scheduleDate: '',
});
// 定义校验规则
const dataRules = ref({
title: [{ required: true, message: '标题不能为空', trigger: 'blur' }],
scheduleType: [{ required: true, message: '日程类型不能为空', trigger: 'blur' }],
scheduleState: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
content: [{ required: true, message: '内容不能为空', trigger: 'blur' }],
scheduleTime: [{ required: true, message: '时间不能为空', trigger: 'blur' }],
scheduleDate: [{ required: true, message: '日期不能为空', trigger: 'blur' }],
});
/**
* 打开日程表单弹窗
* @function
* @param {string} id - 日程ID
* @param {Object} row - 行数据
*/
const openDialog = (id: string, row: any) => {
visible.value = true;
form.id = '';
// 重置表单数据
nextTick(() => dataFormRef.value?.resetFields());
if (row?.date) {
form.date = row.date;
}
// 获取sysSchedule信息
if (id) {
form.id = id;
getsysScheduleData(id);
}
};
/**
* 提交表单数据
* @function
* @async
*/
const onSubmit = async () => {
// 验证表单是否符合规则
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
loading.value = true;
await (form.id ? putObj(form) : addObj(form));
useMessage().success(t(form.id ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
/**
* 获取sysSchedule数据
* @function
* @param {string} id - 日程ID
*/
const getsysScheduleData = (id: string) => {
// 获取数据
loading.value = true;
getObj(id)
.then((res: any) => Object.assign(form, res.data))
.finally(() => (loading.value = false));
};
// 暴露变量
defineExpose({ openDialog });
</script>

View File

@@ -0,0 +1,170 @@
<template>
<el-drawer v-model="visible" title="日程管理" size="80%" @close="handleClose">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item :label="t('schedule.date')" prop="scheduleDate">
<el-date-picker
type="date"
:placeholder="t('schedule.inputDateTip')"
v-model="state.queryForm.scheduleDate"
:value-format="dateStr"
></el-date-picker>
</el-form-item>
<el-form-item :label="$t('schedule.title')" prop="title">
<el-input :placeholder="t('schedule.inputTitleTip')" v-model="state.queryForm.title" style="max-width: 180px" />
</el-form-item>
<el-form-item>
<el-button formDialogRef icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button icon="Refresh" formDialogRef @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button formDialogRef icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog(null, state.queryForm)">
{{ $t('common.addBtn') }}
</el-button>
<el-button formDialogRef :disabled="multiple" icon="Delete" type="primary" class="ml10" @click="handleDelete(selectObjs)">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="true"
@exportExcel="exportExcel"
v-model:showSearch="showSearch"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
style="width: 100%"
@selection-change="handleSelectionChange"
@sort-change="sortChangeHandle"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" :label="t('schedule.index')" width="80" />
<el-table-column prop="title" :label="t('schedule.title')" show-overflow-tooltip />
<el-table-column prop="scheduleType" :label="t('schedule.type')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="schedule_type" :value="scope.row.scheduleType"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="scheduleState" :label="t('schedule.state')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="schedule_status" :value="scope.row.scheduleState"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="scheduleDate" :label="t('schedule.date')" show-overflow-tooltip />
<el-table-column prop="scheduleTime" :label="t('schedule.time')" show-overflow-tooltip />
<el-table-column prop="createBy" :label="t('schedule.createBy')" show-overflow-tooltip />
<el-table-column prop="createTime" :label="t('schedule.createTime')" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" @click="formDialogRef.openDialog(scope.row.id)">{{ $t('common.editBtn') }}</el-button>
<el-button icon="delete" text type="primary" @click="handleDelete([scope.row.id])">{{ $t('common.delBtn') }}</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
</el-drawer>
</template>
<script setup lang="ts" name="systemSysSchedule">
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList, delObjs } from '/@/api/admin/schedule';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
import { useI18n } from 'vue-i18n';
import DictTag from '/@/components/DictTag/index.vue';
const { schedule_type, schedule_status } = useDict('schedule_type', 'schedule_status');
const emit = defineEmits(['refresh']);
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义变量内容
const formDialogRef = ref();
const visible = ref(false);
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
// table hook
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
createdIsNeed: false,
pageList: fetchList,
});
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields();
state.queryForm.date = '';
// 清空多选
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/job/schedule/export', state.queryForm, 'schedule.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObjs(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
//关闭日程刷新首页日程数据
const handleClose = () => {
emit('refresh');
};
const open = (row: any) => {
state.queryForm.date = row.date;
getDataList();
visible.value = true;
};
// 暴露变量
defineExpose({
open,
});
</script>

View File

@@ -0,0 +1,86 @@
<template>
<el-card class="h-[191px] box-card">
<template #header>
<div class="card-header">
<span>{{ props.title }}</span>
</div>
</template>
<el-row :gutter="10" v-if="showRoutes.length > 0">
<el-col class="shortcutCard" :span="6" :key="shortcut.id" v-for="shortcut in showRoutes">
<SvgIcon name="ele-Close" :size="12" class="shortcutCardClose" @click="handleCloseFavorite(shortcut)" />
<shortcutCard :icon="shortcut.meta?.icon" :label="shortcut.name" @click="handleRoute(shortcut.path)" />
</el-col>
</el-row>
<el-empty :image-size="48" :description="props.emptyDescription" v-else />
</el-card>
</template>
<script setup lang="ts" name="Shortcut">
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import shortcutCard from '/@/components/ShortcutCard/index.vue';
const props = defineProps({
title: {
type: String,
},
type: {
type: String,
default: true,
},
emptyDescription:{
type: String,
default: true,
}
});
/**
* 获取路由对象的实例。
*/
const router = useRouter();
/**
* 获取 tagsView 路由列表 store 对象的实例。
*/
const storesTagsViewRoutes = useTagsViewRoutes();
const { favoriteRoutes } = storeToRefs(storesTagsViewRoutes); // 将 tagView 路由列表转换为 Ref 对象
/**
* 点击跳转链接触发事件的回调函数。
* @param path - 需要跳转的路径。
*/
const handleRoute = (path: string) => {
router.push(path); // 跳转到指定路由页面
};
/**
* 关闭收藏路由的事件回调函数。
* @param item - 需要删除的路由信息。
*/
const handleCloseFavorite = (item: any) => {
storesTagsViewRoutes.delFavoriteRoutes(item); // 从收藏路由列表中删除指定路由
};
const showRoutes = computed(() => {
if (props.type === 'flow') {
return favoriteRoutes.value.filter((item) => item.path.includes('/flow/list/index?flowId'));
} else {
return favoriteRoutes.value.filter((item) => !item.path.includes('/flow/list/index?flowId'));
}
});
</script>
<style lang="scss" scoped>
.shortcutCard {
position: relative;
.shortcutCardClose {
position: absolute;
top: 0;
right: 30%;
font-weight: 700;
font-size: 20px;
cursor: pointer;
color: #6d6b6b;
}
}
</style>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,77 @@
<script lang="ts">
export default {
title: '当前用户',
icon: 'User',
description: '获取当前用户信息展示',
};
</script>
<template>
<el-card class="user-card h-[191px] hover:shadow-lg transition-shadow duration-300 ease-in-out">
<div class="flex justify-center items-center p-4 h-full">
<div class="relative">
<img class="object-cover w-20 h-20 rounded-full shadow-md" :src="userData.avatar" alt="Avatar" />
<div class="absolute inset-0 rounded-full shadow-inner"></div>
</div>
<div class="ml-6">
<h2 class="text-2xl font-semibold text-gray-800 dark:text-gray-100">{{ userData.name }}</h2>
<p class="mt-1 text-xs text-gray-500 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>
.user-card {
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,60 @@
<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 && logState.dataList.length > 0">
<el-timeline-item v-for="(item, index) in logState.dataList" :key="index">
<div>{{ item.title }} - {{ item.remoteAddr }}</div>
<div style="font-size: 0.8rem; color: #909399">{{ item.createTime }}</div>
</el-timeline-item>
</el-timeline>
<el-empty v-else :description="$t('common.noDataText')" />
</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({
dataList: [], // Explicitly initialize dataList
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>

View File

@@ -0,0 +1,518 @@
<!--
基于 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="p-5 text-center no-widgets">
<el-empty description='您的仪表盘是空的!点击右上角的"自定义"按钮添加小组件吧。' :image-size="200"></el-empty>
</div>
<el-row :gutter="0">
<el-col 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 as keyof typeof allComps]"></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 v-if="allComps[element as keyof typeof allComps]">
<el-icon v-if="allComps[element as keyof typeof allComps].icon">
<component :is="allComps[element as keyof typeof allComps].icon" />
</el-icon>
{{ allComps[element as keyof typeof allComps].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="p-3 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="p-5 text-center widgets-list-nodata">
<el-empty description="所有可用小组件都已添加。" :image-size="100"></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';
import type { Component } from 'vue';
interface WidgetComponent {
title: string;
icon: Component | string; // Can be a component or an icon name string
description: string;
[key: string]: any; // Allow other props
}
interface WidgetListItem {
key: string;
title: string;
icon: Component | string;
description: string;
disabled?: boolean;
}
// Default layout settings
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: WidgetListItem[] = [];
for (const [key, compDetails] of Object.entries(allComps as Record<string, WidgetComponent>)) {
list.push({ key, title: compDetails.title, icon: compDetails.icon, description: compDetails.description });
}
const myCopmsList = grid.value.copmsList.flat();
list.forEach((comp: WidgetListItem) => {
const existingItem = myCopmsList.find((item: string) => item === comp.key);
comp.disabled = !!existingItem;
});
return list;
});
const myCompsList = computed(() => {
// Support list
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: WidgetListItem) => !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: WidgetListItem) => {
grid.value.copmsList[0].push(item.key);
};
const remove = (itemKey: string) => {
grid.value.copmsList = grid.value.copmsList.map((obj: string[]) => obj.filter((o: string) => o !== itemKey));
};
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: 10px;
right: 10px;
z-index: 9;
}
.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 2px 12px 0 rgba(0, 0, 0, 0.05);
border-radius: 8px;
margin-bottom: 5px;
min-height: 100px; /* Ensure a minimum height for better drag experience */
position: relative; /* Needed for customize-overlay positioning */
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%;
width: 100%;
}
.customizing .widgets-wrapper {
margin-right: -360px;
width: 100%;
}
.customizing .widgets-wrapper .el-col {
padding-bottom: 10px;
}
.customizing .widgets-wrapper .draggable-box {
border: 1px dashed var(--el-color-primary);
padding: 15px;
}
.customizing .widgets-wrapper .no-widgets {
display: none;
}
.widgets-item {
background: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color-light);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
border-radius: 8px;
margin-bottom: 0px; /* Reduced for tighter vertical spacing */
min-height: 100px; /* Ensure a minimum height for better drag experience */
position: relative; /* Needed for customize-overlay positioning */
}
.widgets-item > :deep(div) {
border-radius: 8px; /* Ensure component content also has rounded corners if it fills the card */
}
/* Ensure components inside widgets-item are not creating extra margins or borders if not needed */
.widgets-item > :deep(.el-card) {
border: none !important;
box-shadow: none !important;
}
.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>