| | |
| | | <template> |
| | | <div |
| | | ref="floatDrag" |
| | | class="float-position" |
| | | id="float-box" |
| | | :style="{ |
| | | left: left + 'px', |
| | | top: top + 'px', |
| | | right: right + 'px !important', |
| | | zIndex: zIndex, |
| | | ref="floatBall" |
| | | class="float-ball" |
| | | :class="{ |
| | | 'float-ball-hidden': isHidden && !isHovering, |
| | | 'float-ball-expanded': isExpanded, |
| | | }" |
| | | @touchmove.prevent |
| | | @mousemove.prevent |
| | | @mousedown="mouseDown" |
| | | @mouseup="mouseUp" |
| | | :style="{ |
| | | left: position.x + 'px', |
| | | top: position.y + 'px', |
| | | '--primary-color': primaryColor, |
| | | '--hover-color': hoverColor, |
| | | }" |
| | | @mouseenter="handleMouseEnter" |
| | | @mouseleave="handleMouseLeave" |
| | | > |
| | | <div class="drag"> |
| | | <svg |
| | | t="1682058484158" |
| | | class="icon" |
| | | viewBox="0 0 1024 1024" |
| | | version="1.1" |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | p-id="2023" |
| | | width="32" |
| | | height="32" |
| | | > |
| | | <path |
| | | d="M556.297 172.715a42.407 42.407 0 0 1 42.426 42.398l0.837 267.69c-0.118 1.703 0.63 2.737 1.408 2.737 0.63 0 1.29-0.699 1.506-2.284l37.74-208.953c3.732-20.672 21.844-36.166 42.162-36.166a40.074 40.074 0 0 1 7.136 0.64c23.064 4.164 38.391 27.562 34.217 50.587l-33.656 244.529c0 2.559 0.483 4.478 1.32 4.478 0.58 0 1.328-0.935 2.175-3.218l50.144-134.063c6.27-17.65 23.034-29.403 40.793-29.403A39.798 39.798 0 0 1 797.892 374c22.08 7.875 33.626 33.41 25.78 55.47l-87.904 287.191c-0.453 1.585-0.984 3.16-1.437 4.725l-0.187 0.591v0.128a187.031 187.031 0 0 1-177.847 129.1c-53.156 0-108.42-18.752-150.472-51-45.419-27.336-190.968-183.783-190.968-183.783-22.09-22.07-18.792-55.882 3.297-77.962 11.537-11.537 25.919-17.6 40.173-17.6 13.033 0 25.967 5.05 36.51 15.592l63.138 63.157c8.603 8.594 18.132 12.699 26.922 12.699a26.952 26.952 0 0 0 20.88-9.893c7.658-9.037 4.635-36.914 2.49-54.594l-31.668-260.259c-2.825-23.26 13.781-45.724 37.003-48.549a40.497 40.497 0 0 1 4.853-0.295c21.282 0 39.749 16.98 42.387 38.597l34.926 204.425c0.905 2.54 2.342 4.036 3.602 4.036s2.353-1.496 2.58-4.922l11.88-265.741a42.417 42.417 0 0 1 42.467-42.398m0-70.875a113.36 113.36 0 0 0-104.344 69.153c-0.246 0.57-0.482 1.152-0.718 1.732a111.234 111.234 0 0 0-90.022 10.976 113.597 113.597 0 0 0-32.415 29.207 115.23 115.23 0 0 0-19.067 38.489 113.843 113.843 0 0 0-3.465 44.68l21.36 175.77a120.842 120.842 0 0 0-69.3-21.863c-33.468 0-65.549 13.614-90.286 38.332-23.212 23.202-36.993 53.363-38.863 84.952a120.92 120.92 0 0 0 34.502 92.216c5.532 5.906 39.64 42.407 79.203 82.412 74.586 75.422 105.328 99.648 122.702 110.771 53.973 40.36 123.254 63.414 190.674 63.414A257.906 257.906 0 0 0 801.14 745.1c0.247-0.709 0.483-1.417 0.7-2.136l0.117-0.374a178.56 178.56 0 0 0 1.723-5.64l87.413-285.578a113.203 113.203 0 0 0 5.729-42.86 115.585 115.585 0 0 0-35.772-77.135 111.431 111.431 0 0 0-67.45-30.19l0.148-0.985a113.676 113.676 0 0 0-1.201-43.155 115.408 115.408 0 0 0-16.872-39.523 113.774 113.774 0 0 0-30.703-30.968 111.077 111.077 0 0 0-84.981-17.06 113.203 113.203 0 0 0-103.694-67.656z" |
| | | fill="#ffffff" |
| | | p-id="2024" |
| | | ></path> |
| | | </svg> |
| | | </div> |
| | | <div class="content" id="content" @click="handelFlex"> |
| | | <!-- <img src="../../../../assets/image/alarm.png" alt="" /> --> |
| | | <div class="label"> |
| | | <div v-if="flag">展开</div> |
| | | <div v-else>收起</div> |
| | | </div> |
| | | <div class="item-container"> |
| | | <div |
| | | v-for="(item, index) in powerList" |
| | | :key="index" |
| | | @click.stop="activeHandle(index,item.url)" |
| | | <!-- 主球体 --> |
| | | <div |
| | | class="ball-main" |
| | | :class="{ 'ball-main-expanded': isExpanded }" |
| | | @click="toggleExpand" |
| | | @mousedown="startDrag" |
| | | @touchstart="startDrag" |
| | | > |
| | | <!-- 折叠状态图标 --> |
| | | <div v-if="!isExpanded" class="ball-icon"> |
| | | <svg |
| | | class="fold-icon" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | > |
| | | <path d="M4 6h16M4 12h16M4 18h16" /> |
| | | </svg> |
| | | </div> |
| | | |
| | | <!-- 展开状态关闭按钮 --> |
| | | <div v-else class="close-btn" @click.stop="toggleExpand"> |
| | | <svg |
| | | class="close-icon" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | > |
| | | <path d="M6 18L18 6M6 6l12 12" /> |
| | | </svg> |
| | | </div> |
| | | |
| | | <!-- 角标提示(有未读数时显示) --> |
| | | <div v-if="totalUnread > 0" class="ball-badge"> |
| | | {{ totalUnread > 99 ? "99+" : totalUnread }} |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 展开的内容面板 --> |
| | | <transition name="ball-expand"> |
| | | <div v-if="isExpanded" class="ball-content"> |
| | | <div class="content-header"> |
| | | <h3>随访工作台</h3> |
| | | <div class="update-time">更新于 {{ updateTime }}</div> |
| | | </div> |
| | | |
| | | <div class="stats-grid"> |
| | | <div |
| | | :class="activeIndex == index ? 'active power-item' : 'power-item'" |
| | | v-for="(item, index) in statsItems" |
| | | :key="index" |
| | | class="stat-item" |
| | | :class="{ 'stat-item-highlight': item.highlight }" |
| | | @click="handleItemClick(item)" |
| | | > |
| | | <img :src="item.path" alt="" style="width: 26px" /> |
| | | <div class="stat-icon"> |
| | | <svg |
| | | v-if="item.icon === 'IconUsers'" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | > |
| | | <path |
| | | d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13-7.157a4 4 0 11-8 0 4 4 0 018 0z" |
| | | /> |
| | | </svg> |
| | | <svg |
| | | v-else-if="item.icon === 'IconAlertCircle'" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | > |
| | | <path d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> |
| | | </svg> |
| | | <svg |
| | | v-else-if="item.icon === 'IconTask'" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | > |
| | | <path |
| | | d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" |
| | | /> |
| | | </svg> |
| | | </div> |
| | | <div class="stat-info"> |
| | | <div class="stat-label">{{ item.label }}</div> |
| | | <div class="stat-value">{{ item.value }}</div> |
| | | <div |
| | | v-if="item.trend" |
| | | class="stat-trend" |
| | | :class="'trend-' + item.trend.type" |
| | | > |
| | | <span class="trend-arrow">{{ item.trend.arrow }}</span> |
| | | <span class="trend-value">{{ item.trend.value }}</span> |
| | | </div> |
| | | </div> |
| | | <div v-if="item.unread > 0" class="stat-badge"> |
| | | {{ item.unread > 99 ? "99+" : item.unread }} |
| | | </div> |
| | | </div> |
| | | <div :class="activeIndex == index ? 'active-des des' : 'des'"> |
| | | {{ item.label }} |
| | | </div> |
| | | |
| | | <div class="quick-actions"> |
| | | <div |
| | | v-for="(action, index) in quickActions" |
| | | :key="index" |
| | | class="action-item" |
| | | @click="handleActionClick(action)" |
| | | > |
| | | <div class="action-icon"> |
| | | <svg |
| | | v-if="action.icon === 'IconMessageCircle'" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | > |
| | | <path |
| | | d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" |
| | | /> |
| | | </svg> |
| | | <svg |
| | | v-else-if="action.icon === 'IconPhone'" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | > |
| | | <path |
| | | d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" |
| | | /> |
| | | </svg> |
| | | </div> |
| | | <div class="action-label">{{ action.label }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </transition> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import { getCurrentUserServiceSubtaskCount } from "@/api/AiCentre/index"; |
| | | export default { |
| | | name: "DragBall", |
| | | name: "FloatBall", |
| | | |
| | | props: { |
| | | distanceRight: { |
| | | type: Number, |
| | | default: 36, |
| | | // 初始位置 |
| | | initialPosition: { |
| | | type: Object, |
| | | default: () => ({ x: 20, y: 100 }), |
| | | }, |
| | | distanceBottom: { |
| | | type: Number, |
| | | default: 600, |
| | | }, |
| | | isScrollHidden: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | isCanDraggable: { |
| | | // 是否自动隐藏 |
| | | autoHide: { |
| | | type: Boolean, |
| | | default: true, |
| | | }, |
| | | zIndex: { |
| | | // 隐藏延迟(毫秒) |
| | | hideDelay: { |
| | | type: Number, |
| | | default: 50, |
| | | default: 2000, |
| | | }, |
| | | value: { |
| | | // 主题颜色 |
| | | primaryColor: { |
| | | type: String, |
| | | default: "悬浮球!", |
| | | default: "#4f46e5", |
| | | }, |
| | | // 悬停颜色 |
| | | hoverColor: { |
| | | type: String, |
| | | default: "#4338ca", |
| | | }, |
| | | // 数据源(可从外部传入) |
| | | statsData: { |
| | | type: Object, |
| | | default: null, |
| | | }, |
| | | }, |
| | | |
| | | data() { |
| | | return { |
| | | clientWidth: null, |
| | | clientHeight: null, |
| | | left: null, |
| | | top: null, |
| | | right: null, |
| | | timer: null, |
| | | currentTop: 0, |
| | | mousedownX: 0, |
| | | mousedownY: 0, |
| | | isExpanded: false, |
| | | isHovering: false, |
| | | isHidden: false, |
| | | isDragging: false, |
| | | position: { ...this.initialPosition }, |
| | | dragStart: { x: 0, y: 0 }, |
| | | hideTimer: null, |
| | | updateTime: "", |
| | | roles: null, |
| | | // 统计数据 |
| | | statsItems: [ |
| | | { |
| | | id: "pending", |
| | | label: "待随访", |
| | | value: "0", |
| | | unread: 0, |
| | | urltype: 2, |
| | | icon: "IconUsers", |
| | | url: "/followvisit/discharge", |
| | | trend: { type: "up", arrow: "", value: "" }, |
| | | highlight: true, |
| | | }, |
| | | { |
| | | id: "failed", |
| | | label: "随访失败", |
| | | value: "0", |
| | | unread: 0, |
| | | urltype: 3, |
| | | icon: "IconAlertCircle", |
| | | url: "/followvisit/discharge", |
| | | trend: { type: "down", arrow: "", value: "" }, |
| | | }, |
| | | { |
| | | id: "abnormal", |
| | | label: "任务异常", |
| | | value: "0", |
| | | unread: 0, |
| | | urltype: 4, |
| | | icon: "IconAlertCircle", |
| | | url: "/followvisit/discharge", |
| | | trend: { type: "up", arrow: "", value: "" }, |
| | | }, |
| | | { |
| | | id: "myTasks", |
| | | label: "我的任务", |
| | | value: "0", |
| | | unread: 0, |
| | | urltype: 5, |
| | | icon: "IconTask", |
| | | url: "/followvisit/discharge", |
| | | trend: { type: "stable", arrow: "", value: "" }, |
| | | }, |
| | | ], |
| | | |
| | | flag: true, // 控制悬浮框是否展开 |
| | | box: "", // 悬浮球的dom |
| | | activeIndex: 0, //高亮显示 |
| | | powerList: [ |
| | | // 快捷操作 |
| | | quickActions: [ |
| | | { |
| | | path: require("@/assets/images/huanzheliebiao.png"), |
| | | url:'/patient/patient/', |
| | | label: "患者", |
| | | id: "sms", |
| | | label: "创建问卷任务", |
| | | icon: "IconMessageCircle", |
| | | url: "/followvisit/QuestionnaireTask?type=2&serviceType=2", |
| | | }, |
| | | { |
| | | path: require("@/assets/images/fwwu.png"), |
| | | url:'/followvisit/tasklist/', |
| | | label: "任务", |
| | | id: "call", |
| | | label: "创建语音任务", |
| | | icon: "IconPhone", |
| | | url: "/followvisit/particty?type=1&serviceType=2", |
| | | }, |
| | | { |
| | | path: require("@/assets/images/duanxinjilu.png"), |
| | | url:'', |
| | | label: "短信", |
| | | }, |
| | | { |
| | | path: require("@/assets/images/dianhua.png"), |
| | | url:'', |
| | | label: "电话", |
| | | }, |
| | | { |
| | | path: require("@/assets/images/zxlt.png"), |
| | | url:'', |
| | | label: "在线聊天", |
| | | }, |
| | | // { |
| | | // id: 'chat', |
| | | // label: '在线聊天', |
| | | // icon: 'IconMessageCircle', |
| | | // url: '/chat' |
| | | // } |
| | | ], |
| | | }; |
| | | }, |
| | | created() { |
| | | this.clientWidth = document.documentElement.clientWidth; |
| | | this.clientHeight = document.documentElement.clientHeight; |
| | | |
| | | computed: { |
| | | totalUnread() { |
| | | return this.statsItems.reduce((sum, item) => sum + item.unread, 0); |
| | | }, |
| | | }, |
| | | |
| | | mounted() { |
| | | this.isCanDraggable && |
| | | this.$nextTick(() => { |
| | | this.floatDrag = this.$refs.floatDrag; |
| | | // 获取元素位置属性 |
| | | this.floatDragDom = this.floatDrag.getBoundingClientRect(); |
| | | // 设置初始位置 |
| | | this.left = this.clientWidth - this.floatDragDom.width - this.distanceRight; |
| | | // this.right = 0; |
| | | this.top = |
| | | this.clientHeight - this.floatDragDom.height - this.distanceBottom; |
| | | this.initDraggable(); |
| | | }); |
| | | // this.isScrollHidden && window.addEventListener('scroll', this.handleScroll); |
| | | this.roles = this.$store.state.user.roles; |
| | | this.loadPosition(); |
| | | |
| | | if (this.autoHide) { |
| | | this.startAutoHide(); |
| | | } |
| | | |
| | | // 点击外部关闭 |
| | | document.addEventListener("click", this.handleClickOutside); |
| | | |
| | | // 窗口大小变化时重新定位 |
| | | window.addEventListener("resize", this.handleResize); |
| | | |
| | | this.box = document.getElementById("float-box"); |
| | | }, |
| | | beforeUnmount() { |
| | | window.removeEventListener("scroll", this.handleScroll); |
| | | |
| | | beforeDestroy() { |
| | | document.removeEventListener("click", this.handleClickOutside); |
| | | window.removeEventListener("resize", this.handleResize); |
| | | clearTimeout(this.hideTimer); |
| | | }, |
| | | methods: { |
| | | // 伸缩悬浮球 |
| | | handelFlex() { |
| | | if (this.flag) { |
| | | this.buffer(this.box, "height", 600); |
| | | } else { |
| | | this.buffer(this.box, "height", 70); |
| | | } |
| | | this.flag = !this.flag; |
| | | console.log("是否展开", this.flag); |
| | | }, |
| | | // 点击哪个power |
| | | activeHandle(index,url) { |
| | | //把我们自定义的下标赋值 |
| | | this.activeIndex = index; |
| | | this.$router.push({ |
| | | path: url, |
| | | }) |
| | | console.log("HHHH", index); |
| | | }, |
| | | // 获取要改变得样式属性 |
| | | getStyleAttr(obj, attr) { |
| | | if (obj.currentStyle) { |
| | | // IE 和 opera |
| | | return obj.currentStyle[attr]; |
| | | } else { |
| | | return window.getComputedStyle(obj, null)[attr]; |
| | | } |
| | | }, |
| | | // 动画函数 |
| | | buffer(eleObj, attr, target) { |
| | | // setInterval方式开启动画 |
| | | //先清后设 |
| | | // clearInterval(eleObj.timer); |
| | | // let speed = 0 |
| | | // let begin = 0 |
| | | // //设置定时器 |
| | | // eleObj.timer = setInterval(() => { |
| | | // //获取动画属性的初始值 |
| | | // begin = parseInt(this.getStyleAttr(eleObj, attr)); |
| | | // speed = (target - begin) * 0.2; |
| | | // speed = target > begin ? Math.ceil(speed) : Math.floor(speed); |
| | | // eleObj.style[attr] = begin + speed + "px"; |
| | | // if (begin === target) { |
| | | // clearInterval(eleObj.timer); |
| | | // } |
| | | // }, 20); |
| | | // cancelAnimationFrame开启动画 |
| | | // 先清后设 |
| | | cancelAnimationFrame(eleObj.timer); |
| | | let speed = 0; |
| | | let begin = 0; |
| | | let _this = this; |
| | | eleObj.timer = requestAnimationFrame(function fn() { |
| | | begin = parseInt(_this.getStyleAttr(eleObj, attr)); |
| | | // 动画速度 |
| | | speed = (target - begin) * 0.9; |
| | | speed = target > begin ? Math.ceil(speed) : Math.floor(speed); |
| | | eleObj.style[attr] = begin + speed + "px"; |
| | | eleObj.timer = requestAnimationFrame(fn); |
| | | if (begin === target) { |
| | | cancelAnimationFrame(eleObj.timer); |
| | | } |
| | | }); |
| | | }, |
| | | /** |
| | | * 窗口resize监听 |
| | | */ |
| | | handleResize() { |
| | | // this.clientWidth = document.documentElement.clientWidth; |
| | | // this.clientHeight = document.documentElement.clientHeight; |
| | | // console.log(window.innerWidth); |
| | | // console.log(document.documentElement.clientWidth); |
| | | |
| | | this.checkDraggablePosition(); |
| | | }, |
| | | /** |
| | | * 初始化draggable |
| | | */ |
| | | initDraggable() { |
| | | this.floatDrag.addEventListener("touchstart", this.toucheStart); |
| | | this.floatDrag.addEventListener("touchmove", (e) => this.touchMove(e)); |
| | | this.floatDrag.addEventListener("touchend", this.touchEnd); |
| | | }, |
| | | mouseDown(e) { |
| | | const event = e || window.event; |
| | | this.mousedownX = event.screenX; |
| | | this.mousedownY = event.screenY; |
| | | const that = this; |
| | | let floatDragWidth = this.floatDragDom.width / 2; |
| | | let floatDragHeight = this.floatDragDom.height / 2; |
| | | if (event.preventDefault) { |
| | | event.preventDefault(); |
| | | methods: { |
| | | toggleExpand() { |
| | | this.isExpanded = !this.isExpanded; |
| | | if (this.isExpanded) { |
| | | this.isHidden = false; |
| | | clearTimeout(this.hideTimer); |
| | | this.updateStats(); |
| | | } |
| | | this.canClick = false; |
| | | this.floatDrag.style.transition = "none"; |
| | | document.onmousemove = function (e) { |
| | | var event = e || window.event; |
| | | that.left = event.clientX - floatDragWidth; |
| | | that.top = event.clientY - floatDragHeight; |
| | | if (that.left < 0) that.left = 0; |
| | | if (that.top < 0) that.top = 0; |
| | | // 鼠标移出可视区域后给按钮还原 |
| | | if ( |
| | | event.clientY < 0 || |
| | | event.clientY > Number(this.clientHeight) || |
| | | event.clientX > Number(this.clientWidth) || |
| | | event.clientX < 0 |
| | | ) { |
| | | this.right = 0; |
| | | this.top = |
| | | this.clientHeight - this.floatDragDom.height - this.distanceBottom; |
| | | document.onmousemove = null; |
| | | this.floatDrag.style.transition = "all 0.3s"; |
| | | return; |
| | | }, |
| | | |
| | | handleMouseEnter() { |
| | | this.isHovering = true; |
| | | if (this.autoHide) { |
| | | clearTimeout(this.hideTimer); |
| | | this.isHidden = false; |
| | | } |
| | | }, |
| | | |
| | | handleMouseLeave() { |
| | | this.isHovering = false; |
| | | if (this.autoHide && !this.isExpanded) { |
| | | this.startAutoHide(); |
| | | } |
| | | }, |
| | | |
| | | startAutoHide() { |
| | | this.hideTimer = setTimeout(() => { |
| | | if (!this.isExpanded && !this.isHovering) { |
| | | this.isHidden = true; |
| | | } |
| | | if ( |
| | | that.left >= |
| | | document.documentElement.clientWidth - floatDragWidth * 2 |
| | | ) { |
| | | that.left = document.documentElement.clientWidth - floatDragWidth * 2; |
| | | }, this.hideDelay); |
| | | }, |
| | | |
| | | startDrag(e) { |
| | | e.preventDefault(); |
| | | e.stopPropagation(); |
| | | this.isDragging = true; |
| | | |
| | | const clientX = e.type.includes("touch") |
| | | ? e.touches[0].clientX |
| | | : e.clientX; |
| | | const clientY = e.type.includes("touch") |
| | | ? e.touches[0].clientY |
| | | : e.clientY; |
| | | |
| | | this.dragStart = { |
| | | x: clientX - this.position.x, |
| | | y: clientY - this.position.y, |
| | | }; |
| | | |
| | | const onMove = (moveEvent) => { |
| | | if (!this.isDragging) return; |
| | | |
| | | const moveX = moveEvent.type.includes("touch") |
| | | ? moveEvent.touches[0].clientX |
| | | : moveEvent.clientX; |
| | | const moveY = moveEvent.type.includes("touch") |
| | | ? moveEvent.touches[0].clientY |
| | | : moveEvent.clientY; |
| | | |
| | | const newX = moveX - this.dragStart.x; |
| | | const newY = moveY - this.dragStart.y; |
| | | |
| | | // 边界检查 |
| | | const maxX = window.innerWidth - 60; |
| | | const maxY = window.innerHeight - 60; |
| | | |
| | | this.position.x = Math.max(0, Math.min(newX, maxX)); |
| | | this.position.y = Math.max(0, Math.min(newY, maxY)); |
| | | }; |
| | | |
| | | const onEnd = () => { |
| | | this.isDragging = false; |
| | | document.removeEventListener("mousemove", onMove); |
| | | document.removeEventListener("mouseup", onEnd); |
| | | document.removeEventListener("touchmove", onMove); |
| | | document.removeEventListener("touchend", onEnd); |
| | | |
| | | // 如果靠近边缘,自动吸附 |
| | | if (this.position.x < 20) { |
| | | this.position.x = 0; |
| | | } else if (this.position.x > window.innerWidth - 80) { |
| | | this.position.x = window.innerWidth - 60; |
| | | } |
| | | if (that.top >= that.clientHeight - floatDragHeight * 2) { |
| | | that.top = that.clientHeight - floatDragHeight * 2; |
| | | |
| | | // 保存位置到本地存储 |
| | | try { |
| | | localStorage.setItem( |
| | | "floatBallPosition", |
| | | JSON.stringify(this.position) |
| | | ); |
| | | } catch (e) { |
| | | console.error("保存位置失败:", e); |
| | | } |
| | | }; |
| | | |
| | | document.addEventListener("mousemove", onMove); |
| | | document.addEventListener("mouseup", onEnd); |
| | | document.addEventListener("touchmove", onMove, { passive: false }); |
| | | document.addEventListener("touchend", onEnd); |
| | | }, |
| | | mouseUp(e) { |
| | | const event = e || window.event; |
| | | //判断只是单纯的点击,没有拖拽 |
| | | |
| | | handleItemClick(item) { |
| | | if (item.url) { |
| | | console.log(item.url, "item.url"); |
| | | |
| | | // this.$router.push(item.url); |
| | | this.$router.replace({ |
| | | path: item.url, |
| | | query: { |
| | | errtype: item.urltype, |
| | | }, |
| | | }); |
| | | this.toggleExpand(); |
| | | } |
| | | }, |
| | | |
| | | handleActionClick(action) { |
| | | console.log(this.roles, "this.roles"); |
| | | if ( |
| | | this.mousedownY == event.screenY && |
| | | this.mousedownX == event.screenX |
| | | action.url && |
| | | (this.roles.includes("admin") || this.roles.includes("sysadmin")) |
| | | ) { |
| | | this.$emit("handlepaly"); |
| | | } |
| | | document.onmousemove = null; |
| | | this.checkDraggablePosition(); |
| | | this.floatDrag.style.transition = "all 0.3s"; |
| | | }, |
| | | toucheStart() { |
| | | this.canClick = false; |
| | | this.floatDrag.style.transition = "none"; |
| | | }, |
| | | touchMove(e) { |
| | | this.canClick = true; |
| | | if (e.targetTouches.length === 1) { |
| | | // 单指拖动 |
| | | let touch = event.targetTouches[0]; |
| | | this.left = touch.clientX - this.floatDragDom.width / 2; |
| | | this.top = touch.clientY - this.floatDragDom.height / 2; |
| | | } |
| | | }, |
| | | touchEnd() { |
| | | if (!this.canClick) return; // 解决点击事件和touch事件冲突的问题 |
| | | this.floatDrag.style.transition = "all 0.3s"; |
| | | this.checkDraggablePosition(); |
| | | }, |
| | | /** |
| | | * 判断元素显示位置 |
| | | * 在窗口改变和move end时调用 |
| | | */ |
| | | checkDraggablePosition() { |
| | | this.clientWidth = document.documentElement.clientWidth; |
| | | this.clientHeight = document.documentElement.clientHeight; |
| | | if (this.left + this.floatDragDom.width / 2 >= this.clientWidth / 2) { |
| | | // 判断位置是往左往右滑动 |
| | | this.left = this.clientWidth - this.floatDragDom.width; |
| | | this.$router.replace(action.url); |
| | | this.toggleExpand(); |
| | | } else { |
| | | this.left = 0; |
| | | this.$modal.msgError("非管理员用户暂无创建任务权限"); |
| | | } |
| | | if (this.top < 0) { |
| | | // 判断是否超出屏幕上沿 |
| | | this.top = 0; |
| | | }, |
| | | |
| | | async updateStats() { |
| | | try { |
| | | // 这里可以替换为实际的 API 调用 |
| | | // const response = await this.$api.getFollowupStats() |
| | | // this.statsItems = response.data |
| | | |
| | | // 模拟数据更新 |
| | | const mockData = { |
| | | pending: { |
| | | value: "128", |
| | | unread: null, |
| | | trend: { type: "up", arrow: "↑", value: "5" }, |
| | | }, |
| | | failed: { |
| | | value: "24", |
| | | unread: null, |
| | | trend: { type: "down", arrow: "↓", value: "2" }, |
| | | }, |
| | | abnormal: { |
| | | value: "8", |
| | | unread: null, |
| | | trend: { type: "up", arrow: "↑", value: "3" }, |
| | | }, |
| | | myTasks: { |
| | | value: "156", |
| | | unread: null, |
| | | trend: { type: "stable", arrow: "→", value: "0" }, |
| | | }, |
| | | }; |
| | | const response = await getCurrentUserServiceSubtaskCount(); |
| | | mockData.pending.value = response.pendingVisitCount; |
| | | mockData.failed.value = response.failedVisitCount; |
| | | mockData.abnormal.value = response.abnormalVisitCount; |
| | | mockData.myTasks.value = response.allVisitCount; |
| | | this.statsItems = this.statsItems.map((item) => { |
| | | const data = mockData[item.id] || {}; |
| | | return { |
| | | ...item, |
| | | value: data.value || item.value, |
| | | unread: data.unread || item.unread, |
| | | trend: data.trend || item.trend, |
| | | }; |
| | | }); |
| | | |
| | | // 更新时间 |
| | | const now = new Date(); |
| | | this.updateTime = `${now.getHours().toString().padStart(2, "0")}:${now |
| | | .getMinutes() |
| | | .toString() |
| | | .padStart(2, "0")}`; |
| | | } catch (error) { |
| | | console.error("更新统计数据失败:", error); |
| | | } |
| | | if (this.top + this.floatDragDom.height >= this.clientHeight) { |
| | | // 判断是否超出屏幕下沿 |
| | | this.top = this.clientHeight - this.floatDragDom.height; |
| | | }, |
| | | |
| | | loadPosition() { |
| | | try { |
| | | const savedPosition = localStorage.getItem("floatBallPosition"); |
| | | if (savedPosition) { |
| | | const parsed = JSON.parse(savedPosition); |
| | | this.position = parsed; |
| | | } |
| | | } catch (e) { |
| | | console.error("加载位置失败:", e); |
| | | } |
| | | }, |
| | | |
| | | handleClickOutside(e) { |
| | | if ( |
| | | this.isExpanded && |
| | | this.$refs.floatBall && |
| | | !this.$refs.floatBall.contains(e.target) |
| | | ) { |
| | | this.toggleExpand(); |
| | | } |
| | | }, |
| | | |
| | | handleResize() { |
| | | const maxX = window.innerWidth - 60; |
| | | const maxY = window.innerHeight - 60; |
| | | |
| | | this.position.x = Math.min(this.position.x, maxX); |
| | | this.position.y = Math.min(this.position.y, maxY); |
| | | }, |
| | | }, |
| | | }; |
| | | </script> |
| | | <style> |
| | | html, |
| | | body { |
| | | overflow: hidden; |
| | | } |
| | | </style> |
| | | <style scoped lang="scss"> |
| | | .float-position { |
| | | |
| | | <style scoped> |
| | | .float-ball { |
| | | position: fixed; |
| | | z-index: 10003 !important; |
| | | left: 0; |
| | | top: 20%; |
| | | width: 70px; |
| | | height: 70px; |
| | | border-radius: 32px; |
| | | z-index: 9999; |
| | | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| | | pointer-events: auto; |
| | | } |
| | | |
| | | .float-ball-hidden { |
| | | opacity: 0.3; |
| | | transform: translateX(10px); |
| | | } |
| | | |
| | | .float-ball-hidden:hover { |
| | | opacity: 1; |
| | | transform: translateX(0); |
| | | } |
| | | |
| | | .ball-main { |
| | | width: 60px; |
| | | height: 60px; |
| | | border-radius: 50%; |
| | | background: linear-gradient(135deg, var(--primary-color), #7c3aed); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | cursor: move; |
| | | box-shadow: 0 4px 20px rgba(79, 70, 229, 0.3); |
| | | transition: all 0.3s ease; |
| | | position: relative; |
| | | z-index: 10000; |
| | | } |
| | | |
| | | .ball-main:hover { |
| | | background: linear-gradient(135deg, var(--hover-color), #6d28d9); |
| | | box-shadow: 0 6px 25px rgba(79, 70, 229, 0.4); |
| | | transform: scale(1.05); |
| | | } |
| | | |
| | | .ball-main-expanded { |
| | | background: linear-gradient(135deg, #6366f1, #8b5cf6); |
| | | } |
| | | |
| | | .ball-icon { |
| | | width: 24px; |
| | | height: 24px; |
| | | color: white; |
| | | } |
| | | |
| | | .fold-icon { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .close-btn { |
| | | width: 24px; |
| | | height: 24px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | cursor: pointer; |
| | | overflow: hidden; |
| | | user-select: none; |
| | | color: white; |
| | | transition: transform 0.2s ease; |
| | | } |
| | | |
| | | display: block; |
| | | background: black; |
| | | background: -webkit-radial-gradient(100px 100px, circle, #5788FE, #292929); |
| | | // background: -moz-radial-gradient(100px 100px, circle, #35a1a1, #000);Firefox 浏览器的实现 |
| | | // background: radial-gradient(100px 100px, circle, #35a1a1, #000);标准 HTML5 属性 |
| | | margin: 0; |
| | | .drag { |
| | | width: 70px; |
| | | height: 35px; |
| | | // background: #f2e96a; |
| | | text-align: center; |
| | | line-height: 35px; |
| | | border-bottom: 1px solid #fff; |
| | | .close-btn:hover { |
| | | transform: rotate(90deg); |
| | | } |
| | | |
| | | .close-icon { |
| | | width: 20px; |
| | | height: 20px; |
| | | } |
| | | |
| | | .ball-badge { |
| | | position: absolute; |
| | | top: -5px; |
| | | right: -5px; |
| | | min-width: 20px; |
| | | height: 20px; |
| | | padding: 0 6px; |
| | | background: #ef4444; |
| | | color: white; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | border-radius: 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border: 2px solid white; |
| | | animation: pulse 2s infinite; |
| | | } |
| | | |
| | | @keyframes pulse { |
| | | 0%, |
| | | 100% { |
| | | transform: scale(1); |
| | | } |
| | | .content { |
| | | width: 70px; |
| | | height: 35px; |
| | | // background: #716af2; |
| | | .label { |
| | | width: 70px; |
| | | height: 35px; |
| | | text-align: center; |
| | | line-height: 35px; |
| | | color: white; |
| | | } |
| | | .label:hover { |
| | | color: rgb(19, 217, 243); |
| | | transition: all 0.5; |
| | | } |
| | | |
| | | .item-container { |
| | | margin-top: 10px; |
| | | width: 70px; |
| | | height: 500px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | flex-direction: column; |
| | | |
| | | .power-item { |
| | | width: 40px; |
| | | height: 40px; |
| | | border-radius: 50%; |
| | | background-color: #f1f7ff; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | flex-direction: column; |
| | | } |
| | | .des { |
| | | width: 40px; |
| | | text-align: center; |
| | | margin-bottom: 5px; |
| | | font-size: 10px; |
| | | color: #fff; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .close { |
| | | width: 20px; |
| | | height: 20px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #fff; |
| | | background: rgba(0, 0, 0, 0.6); |
| | | position: absolute; |
| | | right: -10px; |
| | | top: -12px; |
| | | cursor: pointer; |
| | | 50% { |
| | | transform: scale(1.1); |
| | | } |
| | | } |
| | | |
| | | .cart { |
| | | border-radius: 50%; |
| | | width: 5em; |
| | | height: 5em; |
| | | .ball-content { |
| | | position: absolute; |
| | | top: 70px; |
| | | left: 0; |
| | | width: 320px; |
| | | background: white; |
| | | border-radius: 16px; |
| | | box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); |
| | | overflow: hidden; |
| | | z-index: 9999; |
| | | } |
| | | |
| | | .ball-expand-enter-active, |
| | | .ball-expand-leave-active { |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .ball-expand-enter, |
| | | .ball-expand-leave-to { |
| | | opacity: 0; |
| | | transform: translateY(-10px); |
| | | } |
| | | |
| | | .content-header { |
| | | padding: 20px 20px 16px; |
| | | background: linear-gradient(135deg, var(--primary-color), #7c3aed); |
| | | color: white; |
| | | } |
| | | |
| | | .content-header h3 { |
| | | margin: 0 0 8px 0; |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .update-time { |
| | | font-size: 12px; |
| | | opacity: 0.9; |
| | | } |
| | | |
| | | .stats-grid { |
| | | padding: 16px; |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .stat-item { |
| | | padding: 16px; |
| | | background: #f8fafc; |
| | | border-radius: 12px; |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | position: relative; |
| | | border: 2px solid transparent; |
| | | } |
| | | |
| | | .stat-item:hover { |
| | | background: #f1f5f9; |
| | | border-color: #e2e8f0; |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | .stat-item-highlight { |
| | | border-color: var(--primary-color); |
| | | background: linear-gradient(to bottom right, #f0f9ff, #f8fafc); |
| | | } |
| | | |
| | | .stat-icon { |
| | | width: 32px; |
| | | height: 32px; |
| | | background: white; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-bottom: 12px; |
| | | color: var(--primary-color); |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .stat-icon svg { |
| | | width: 18px; |
| | | height: 18px; |
| | | } |
| | | |
| | | .stat-label { |
| | | font-size: 12px; |
| | | color: #64748b; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 20px; |
| | | font-weight: 700; |
| | | color: #1e293b; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .stat-trend { |
| | | font-size: 11px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 2px; |
| | | } |
| | | |
| | | .trend-up { |
| | | color: #10b981; |
| | | } |
| | | |
| | | .trend-down { |
| | | color: #ef4444; |
| | | } |
| | | |
| | | .trend-stable { |
| | | color: #64748b; |
| | | } |
| | | |
| | | .trend-arrow { |
| | | font-size: 10px; |
| | | } |
| | | |
| | | .stat-badge { |
| | | position: absolute; |
| | | top: 12px; |
| | | right: 12px; |
| | | min-width: 18px; |
| | | height: 18px; |
| | | padding: 0 4px; |
| | | background: #ef4444; |
| | | color: white; |
| | | font-size: 10px; |
| | | font-weight: 600; |
| | | border-radius: 9px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .header-notice { |
| | | display: inline-block; |
| | | transition: all 0.3s; |
| | | |
| | | span { |
| | | vertical-align: initial; |
| | | } |
| | | |
| | | .notice-badge { |
| | | color: inherit; |
| | | |
| | | .header-notice-icon { |
| | | font-size: 16px; |
| | | padding: 4px; |
| | | } |
| | | } |
| | | .quick-actions { |
| | | padding: 12px 20px 20px; |
| | | border-top: 1px solid #f1f5f9; |
| | | display: flex; |
| | | gap: 12px; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .drag-ball .drag-content { |
| | | overflow-wrap: break-word; |
| | | font-size: 14px; |
| | | color: #fff; |
| | | letter-spacing: 2px; |
| | | .action-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 8px; |
| | | cursor: pointer; |
| | | padding: 12px; |
| | | border-radius: 8px; |
| | | transition: all 0.2s ease; |
| | | flex: 1; |
| | | } |
| | | |
| | | .active { |
| | | background-color: #f9f1db !important; |
| | | .action-item:hover { |
| | | background: #f8fafc; |
| | | } |
| | | .active-des { |
| | | color: #71dcfa !important; |
| | | font-size: 20px !important; |
| | | font-weight: 500 !important; |
| | | |
| | | .action-icon { |
| | | width: 24px; |
| | | height: 24px; |
| | | color: var(--primary-color); |
| | | } |
| | | |
| | | .action-icon svg { |
| | | width: 20px; |
| | | height: 20px; |
| | | } |
| | | |
| | | .action-label { |
| | | font-size: 12px; |
| | | color: #475569; |
| | | font-weight: 500; |
| | | } |
| | | </style> |