WXL (wul)
21 小时以前 0c7cc21d8a51e164dd2fe4ce73ab566b3a9081a9
src/components/Assistant/index.vue
@@ -1,479 +1,809 @@
<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>