You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1026 lines
26 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="body">
<div class="dashboard-container">
<div class="row row1">
<div class="weather">
<div class="left">
<h3>工作台</h3>
<div class="d-flex">
<div>欢迎回来{{ name }}</div>
</div>
</div>
<div class="center">
<el-button
style="padding: 6px 15px"
type="primary"
size="small"
round
@click="$router.push('/attendance')"
>去打卡</el-button
>
</div>
<div class="right">
<div class="weather__icon">
<i :class="weatherIcon.get(weather.weather)"></i>
</div>
<div class="weather__weather">
<div>{{ $moment().format("dddd") }}</div>
<div>{{ weather.weather }}</div>
</div>
<div class="weather__temperature">
<div class="celsius">
<span>
{{ weather.temperature }}
</span>
<span>°C</span>
</div>
<div class="wind">
<span>{{ weather.winddirection }}</span>
<span> {{ weather.windpower }}</span>
</div>
</div>
</div>
</div>
<div class="mycard calendar">
<div class="mycard__title calendar__title d-flex">
<div>工作日历</div>
<el-date-picker
v-model="calendar"
type="month"
size="small"
style="
width: 160px;
border-radius: 10px;
background-color: #ffffff;
border: 1px solid #d1d1d1;
"
></el-date-picker>
</div>
<div class="mycard__body calendar__body">
<el-calendar v-model="calendar">
<template #dateCell="{ date, data }">
<div
class="sign-status"
:class="
(getTodayAttendance(data.day) && (getTodayAttendance(data.day).sign_in_at || getTodayAttendance(data.day).sign_out_at)) ? 'has-attendance' : ''
"
>
{{ data.day.split("-")[2] }}
<template v-if="getTodayAttendance(data.day)">
<div
class="work-overtime"
v-if="
getTodayAttendance(data.day) && getTodayAttendance(data.day).jiaban instanceof Array && getTodayAttendance(data.day).jiaban.filter(i => i).length > 0
"
>
加班
</div>
<div
class="sign-duty"
v-if="
getTodayAttendance(data.day, 'on_duty_schedules')
"
>
{{
getTodayAttendance(data.day, 'on_duty_schedules').status ? '已值班' : '待值班'
}}
</div>
<div
class="sign-in"
v-if="
getTodayAttendance(data.day) &&
getTodayAttendance(data.day).sign_in_at
"
>
{{
$moment(getTodayAttendance(data.day).sign_in_at).format(
"HH:mm"
)
}}
</div>
<div
class="sign-out"
v-if="
getTodayAttendance(data.day) &&
getTodayAttendance(data.day).sign_out_at
"
>
{{
$moment(
getTodayAttendance(data.day).sign_out_at
).format("HH:mm")
}}
</div>
<div
class="sign-away"
v-if="getTodayAttendance(data.day) && getTodayAttendance(data.day).chuchai && getTodayAttendance(data.day).chuchai instanceof Array && (getTodayAttendance(data.day).chuchai[0] || getTodayAttendance(data.day).chuchai[1])"
>
出差
</div>
<div
class="sign-leave"
v-if="getTodayAttendance(data.day) && getTodayAttendance(data.day).qingxiujia && getTodayAttendance(data.day).qingxiujia instanceof Array && getTodayAttendance(data.day).qingxiujia[0]"
>
上午请假
</div>
<div
class="sign-leave"
v-if="getTodayAttendance(data.day) && getTodayAttendance(data.day).qingxiujia && getTodayAttendance(data.day).qingxiujia instanceof Array && getTodayAttendance(data.day).qingxiujia[1]"
>
下午请假
</div>
</template>
</div>
</template>
</el-calendar>
</div>
</div>
</div>
<div class="row row2">
<div class="mycard quick-menu">
<div class="mycard__title">
<span>快捷导航</span>
</div>
<div class="mycard__body quick-menu__body">
<el-badge :value="item.num"
:max="99"
:hidden="!item.num"
class="item"
v-for="item in quickMenus"
:key="item.key">
<router-link
:to="item.url"
tag="div"
>
<div>
{{ item.name }}
</div>
</router-link>
</el-badge>
</div>
</div>
<div class="mycard flow">
<div class="mycard__title">
<span>常用流程</span>
</div>
<div class="mycard__body flow__body">
<div
v-for="item in usualFlows"
:key="item.key"
@click="toCreate(item)">
<div class="flow-cover">
<template v-if="item.icon">
<Icon class="flow__body--icon" :icon="item.icon"></Icon>
</template>
<template v-else>
<i class="el-icon-edit flow__body--icon"></i>
</template>
</div>
<span class="flow__body--name">{{ item.name }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="todo mycard">
<div class="mycard__title">
<span>待办事项</span>
</div>
<div class="mycard__body">
<el-table
v-loading="noticeLoading"
style="width: 100%"
size="mini"
:header-cell-style="{
'font-weight': '600',
background: '#eaf2ff',
color: '#515a6e',
}"
element-loading-text="拼命加载中"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.8)"
:data="notice"
>
<el-table-column
type="index"
width="46"
label="序号"
align="center"
/>
<el-table-column
v-for="(item, index) in noticeTable"
:key="index"
:width="item.width"
:label="item.title"
:prop="item.key"
:show-overflow-tooltip="item['show-overflow-tooltip']"
header-align="center"
:align="item.align"
:formatter="item.formatter"
/>
<el-table-column
label="操作"
header-align="center"
width="150"
fixed="right"
>
<template #default="{ row }">
<el-button
size="mini"
style="padding: 5px 10px"
@click="read(row)"
>设为已读</el-button
>
<el-button
size="mini"
type="primary"
style="padding: 5px 10px"
@click="handle(row)"
>办理</el-button
>
</template>
</el-table-column>
</el-table>
<el-pagination
style="display: flex;justify-content: center;"
layout="prev, pager, next"
:total="noticeTotal"
:page-size.sync="noticeSelect.page_size"
@current-change="e => {
noticeSelect.page = e;
getNotices();
}"
/>
</div>
</div>
<!-- 办理-->
<vxe-modal
v-model="isShowModal"
:z-index="zIndex"
transfer
show-zoom
resize
:fullscreen="$store.getters.device === 'mobile'"
title="办理"
:width="defaultModalSize.width"
:height="defaultModalSize.height"
esc-closable
:padding="false"
>
<iframe
:src="modalUrl"
style="display: block;width: 100%;height: 100%;border: 0;"
frameborder="0"
/>
</vxe-modal>
</div>
</template>
<script>
import Icon from '@/layout/components/Sidebar/Icon.vue'
import { PopupManager } from "element-ui/lib/utils/popup";
import { mapGetters } from "vuex";
import axios from "axios";
import { getToken } from "@/utils/auth";
import { flow } from "@/api/flow";
import { index as configIndex } from "@/api/config";
import { index } from "@/api/attendance";
import {isExternal} from "@/utils/validate";
import { defaultModalSize } from "@/settings";
export default {
name: "Dashboard",
components: {
Icon
},
data() {
return {
defaultModalSize,
modalUrl: "",
zIndex: PopupManager.nextZIndex(),
isShowModal: false,
weatherIcon: new Map([
["晴", "el-icon-sunny"],
["阴", "el-icon-cloudy"],
["多云", "el-icon-cloudy-and-sunny"],
["大雨", "el-icon-heavy-rain"],
["雷阵雨", "el-icon-lightning"],
["小雨", "el-icon-light-rain"],
["中雨", "el-icon-light-rain"],
]),
weather: {},
calendar: new Date(),
attendanceData: {
attendances: [],
},
quickMenus: [
{
name: "我收藏的",
url: "/flow/list/fav",
num: 0
},
{
name: "我办理过的",
url: "/flow/list/handled",
num: 0
},
{
name: "我发起的",
url: "/flow/list/my",
num: 0
},
{
name: "所有待办",
url: "/flow/list/todo",
num: 0
},
{
name: "抄送给我的",
url: "/flow/list/cc",
num: 0
},
],
flows: [],
usualFlows: [],
noticeTimer: null,
noticeLoading: false,
notice: [],
noticeSelect: {
page: 1,
page_size: 20,
from: window.MODULE_NAME || process.env.VUE_APP_MODULE_NAME,
},
noticeTotal: 0,
noticeTable: [
{
title: "下发时间",
key: "created_at",
width: 156,
align: "center",
formatter: (row, column, cellValue) => {
return this.$moment(cellValue).format("YYYY-MM-DD HH:mm:ss");
},
},
{
title: "内容",
key: "data.title",
align: "left",
"show-overflow-tooltip": true,
minWidth: 240,
},
],
};
},
computed: {
...mapGetters(["name"]),
},
watch: {
isShowModal(newVal) {
if(newVal) {
this.zIndex = PopupManager.nextZIndex()
} else {
this.modalUrl = ''
}
},
calendar(newVal, oldVal) {
if (this.$moment(oldVal).format('YYYY-MM') !== this.$moment(newVal).format('YYYY-MM')) {
this.getAttendance()
}
}
},
methods: {
toCreate(flow, cate) {
if(isExternal(flow.url)) {
const url = new URL(flow.url)
window.open(flow.url,'_blank')
} else if (flow.url) {
if (/\?.+/g.test(flow.url)) {
window.open(window.location.origin + flow.url,'_blank')
}
} else {
this.$router.push({
path: '/flow/create',
query: {
module_id: flow.id
}
})
}
},
getTodayAttendance(date,key = 'attendance') {
let temp = this.attendanceData.attendances.find((i) => i.date === date) ?? {}
return temp[key]
},
async getAttendance() {
try {
const res = await index({
month: this.$moment(this.calendar).format('YYYY-MM'),
});
this.attendanceData = res;
} catch (err) {
console.error(err);
}
},
async getUsualFlows() {
try {
await this.getFlows();
const res = await configIndex({
filter: [
{
key: "key",
op: "eq",
value: "dashboard_menus",
},
],
});
this.usualFlows = this.flows.filter(
(i) => res.data[0]?.value?.indexOf(i.id.toString()) !== -1
);
console.log(this.usualFlows);
} catch (err) {
console.error(err);
}
},
async getFlows() {
try {
const res = await flow();
this.flows = res.cates?.map((i) => i.customer_models).flat();
} catch (err) {
console.error(err);
}
},
async getTotal() {
try {
const res = await axios.get(`${process.env.VUE_APP_BASE_API}/api/oa/statistics/notifications`,{
headers: {
Authorization: `Bearer ${getToken()}`,
}
});
if (res.status === 200) {
console.log(res)
this.quickMenus[3].num = res.data.data?.count_todo ?? 0
this.quickMenus[4].num = res.data.data?.count_unread_ccs ?? 0
}
} catch (err) {
console.error(err);
}
},
async getWeather() {
try {
const res = await axios.get(
`https://restapi.amap.com/v3/weather/weatherInfo?city=320400&key=15ecd1e7de61e684959f43d8965a89f0`
);
if (res.status === 200) {
this.weather = res.data.lives[0];
}
} catch (err) {
console.error(err);
}
},
async getNotices(loading = false) {
try {
if (loading) {
this.noticeLoading = true;
}
const res = await axios.get(
`${process.env.VUE_APP_BASE_API}/api/notification/todo`,
{
headers: {
Authorization: `Bearer ${getToken()}`,
},
params: this.noticeSelect,
}
);
if (res.status === 200) {
this.notice = res.data.data?.result;
this.noticeTotal = res.data.data?.total;
}
this.noticeLoading = false;
} catch (err) {
console.error(err);
this.noticeLoading = false;
}
},
async read(row) {
try {
this.loading = true;
const res = await axios.get(
`${process.env.VUE_APP_BASE_API}/api/notification/read`,
{
headers: {
Authorization: `Bearer ${getToken()}`,
},
params: {
id: row.id,
},
}
);
if (res.status === 200) {
await this.getNotices();
}
this.loading = false;
} catch (err) {
console.error(err);
this.loading = false;
}
},
handle(row) {
try {
const data = row.data;
if (data.from === "ht") {
this.modalUrl = `/${
data.from
}/#/contract/contractList?auth_token=${window.encodeURIComponent(
getToken()
)}&module_name=${data.from}&keyword=${
/\[(.*?)]/.exec(data.title) ? /\[(.*?)]/.exec(data.title)[1] : ""
}&isSinglePage=1`;
} else if (data.from === "oa") {
this.modalUrl = `/${
data.from
}/#/flow/create?auth_token=${window.encodeURIComponent(
getToken()
)}&module_name=${data.from}&flow_id=${
data.other?.flow_id
}&isSinglePage=1`;
}
console.log(this.modalUrl);
this.isShowModal = true;
} catch (err) {
this.$message.warning("未找到流程");
console.error(err);
}
},
},
created() {
this.getWeather();
this.getNotices(true);
this.noticeTimer = setInterval(this.getNotices, 5000)
this.getTotal();
this.getUsualFlows();
this.getAttendance();
},
beforeDestroy() {
clearInterval(this.noticeTimer)
}
};
</script>
<style lang="scss" scoped>
$btn-colors: linear-gradient(90deg, #d4bbfd 0%, #af7bff 100%),
linear-gradient(90deg, #8dddf7 0%, #3fbde2 100%),
linear-gradient(90deg, #aec1fd 0%, #81a0ea 100%),
linear-gradient(90deg, #f5c0b8 0%, #ee9b98 100%),
linear-gradient(90deg, #ffd2a6 0%, #ffb46d 100%);
::v-deep .el-calendar__header {
display: none;
}
::v-deep .el-input__inner {
border-color: transparent;
border-radius: 10px;
}
::v-deep .el-calendar-table {
border-collapse: separate;
border-spacing: 4px;
}
::v-deep .el-calendar__body table thead th:nth-last-child(1),
::v-deep .el-calendar__body table thead th:nth-last-child(2) {
color: #dd383d;
}
::v-deep .el-calendar-table td,
::v-deep .el-calendar-table tr:first-child td,
::v-deep .el-calendar-table tr td:first-child {
border: none;
}
::v-deep .el-calendar-table__row .current, ::v-deep .el-calendar-table__row .next, ::v-deep .el-calendar-table__row .prev{
min-height: 68px;
position: relative;
}
::v-deep .el-calendar-table .el-calendar-day {
border-radius: 10px;
min-height: 68px;
height: 100%;
transition: all 0.2s;
}
::v-deep .el-calendar-table td.is-selected {
background: var(--theme-color) !important;
color: #fff !important;
border-radius: 10px;
}
::v-deep .el-calendar-table .el-calendar-day:hover {
color: var(--theme-color);
}
::v-deep .el-calendar {
border-radius: 0 0 10px 10px;
}
::v-deep .el-badge__content.is-fixed {
z-index: 1;
}
::v-deep .el-calendar-table .is-today {
&::after {
content: "今";
color: #fff;
background: var(--theme-color);
filter: sepia(0.5);
transform: translate(50%, -50%);
border-radius: 4px;
padding: 2px;
font-size: 10px;
position: absolute;
right: 0;
top: 0;
}
}
.d-flex {
display: flex;
}
.body {
padding: 10px;
}
.mycard {
border-radius: 10px;
filter: drop-shadow(0 0 5px rgba(51, 51, 51, 0.1));
background-color: #ffffff;
padding: 22px;
&__title {
font-size: 20px;
color: #333333;
font-weight: bold;
padding-left: 20px;
position: relative;
&::before {
content: "";
height: 100%;
border-radius: 5px;
background: var(--theme-color);
width: 5px;
position: absolute;
top: 0;
left: 0;
}
}
&__body {
padding-top: 20px;
}
}
.dashboard {
&-container {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
& > * {
width: calc(50% - 10px);
}
.row {
& > * {
margin-bottom: 20px;
}
}
.weather {
display: flex;
align-items: flex-end;
.center {
margin-left: 20px;
}
.right {
display: flex;
align-items: center;
margin-left: auto;
}
&__icon {
color: var(--theme-color);
font-size: 40px;
}
&__weather {
font-size: 14px;
line-height: 1.5;
text-transform: uppercase;
color: #595959;
text-align: center;
margin-left: 8px;
}
&__temperature {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 8px;
.celsius {
& > span:nth-child(1) {
font-size: 44px;
color: #595959;
}
& > span:nth-child(2) {
font-size: 30px;
color: #595959;
}
}
.wind {
background: var(--theme-color);
border-radius: 8px;
font-size: 12px;
padding: 2px 12px;
color: #fff;
text-align: center;
}
}
}
.calendar {
padding: 0;
&__title {
justify-content: space-between;
align-items: center;
color: #fff;
font-size: 20px;
font-weight: bold;
border-radius: 10px 10px 0 0;
background: var(--theme-color);
padding: 22px;
&::before {
display: none;
}
}
&__body {
background: #fff;
border-radius: 0 0 10px 10px;
}
}
.flow {
&__body {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 20px;
& > div {
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
& > * {
@for $index from 1 through length($btn-colors) {
&:nth-child(#{$index}n) .flow-cover {
background: nth($btn-colors, $index);
}
}
}
.flow-cover {
padding: 10px;
border-radius: 6px;
}
&--icon {
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
width: 30px !important;
height: 30px !important;
color: #fff;
margin-right: 0 !important;
}
&--name {
color: #5a576f;
font-weight: bold;
font-size: 14px;
padding-top: 20px;
text-align: center;
}
}
}
.quick-menu {
&__body {
display: flex;
flex-wrap: wrap;
& > div {
font-weight: bold;
font-size: 16px;
color: #fff;
padding: 14px 20px;
cursor: pointer;
border-radius: 10px;
margin: 10px 10px 0 0;
// overflow: hidden;
position: relative;
z-index: 3;
&::before {
content: "";
width: 86%;
height: 114%;
background: radial-gradient(
100% 100% at 50% 50%,
#0000 0,
#0000 13%,
#fff2 44%,
#0000 44%
)
no-repeat;
z-index: -1;
position: absolute;
left: -33%;
bottom: -64%;
}
&::after {
content: "";
width: 116%;
height: 152%;
background: radial-gradient(
100% 100% at 50% 50%,
#0000 0,
#0000 13%,
#fff2 44%,
#0000 44%
)
no-repeat;
z-index: -1;
position: absolute;
right: -56%;
bottom: -79%;
}
}
@for $index from 1 through length($btn-colors) {
& > div:nth-child(#{$index}) {
background: nth($btn-colors, $index);
}
}
}
}
}
}
.todo {
margin-top: 20px;
}
@media (max-width: 992px) {
.body {
padding: 10px;
}
.dashboard {
&-container {
flex-direction: column;
padding: 0;
& > * {
width: 100%;
}
.weather {
display: flex;
&__icon {
font-size: 32px;
}
&__temperature {
.celsius {
& > span:nth-child(1) {
font-size: 32px;
}
& > span:nth-child(2) {
font-size: 20px;
}
}
.wind {
font-size: 11px;
}
}
.center {
margin-left: 0;
}
}
}
}
.todo {
margin-top: 10px;
}
}
</style>
<style lang="scss">
.el-calendar-day:has(.has-attendance)::before{
content: "";
border-radius: 10px;
background: var(--theme-color);
opacity: 0.4;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.el-calendar-day:has(.has-attendance) .sign-status {
display: flex;
flex-direction: column;
justify-content: flex-end;
position: relative;
}
.work-overtime {
margin-top: 4px;
color: #fff;
font-size: 10px;
border-radius: 3px;
background: #9f2222;
padding: 2px 4px;
margin-left: auto;
text-align: center;
}
.sign-duty {
margin-top: 4px;
color: #fff;
font-size: 10px;
border-radius: 3px;
background: #f56c6c;
padding: 2px 4px;
margin-left: auto;
text-align: center;
}
.sign-in {
margin-top: 4px;
color: #fff;
font-size: 10px;
border-radius: 3px;
background: var(--theme-color);
padding: 2px 4px;
margin-left: auto;
text-align: center;
}
.sign-out {
margin-top: 4px;
color: #fff;
font-size: 10px;
border-radius: 3px;
background-color: #251f83;
padding: 2px 4px;
margin-left: auto;
text-align: center;
}
.sign-leave {
margin-top: 4px;
color: #fff;
font-size: 8px;
border-radius: 3px;
background-color: #e6a23c;
padding: 2px 4px;
margin-left: auto;
text-align: center;
}
.sign-away {
margin-top: 4px;
color: #fff;
font-size: 8px;
border-radius: 3px;
background-color: #909399;
padding: 2px 4px;
margin-left: auto;
text-align: center;
}
@media (max-width: 992px) {
.sign-status {
display: block !important;
}
.work-overtime,
.sign-in,
.sign-out,
.sign-leave,
.sign-out {
float: none !important;
}
.el-calendar-table .el-calendar-day {
padding: 2px;
}
}
</style>