企业级通用业务 Header 处理方案

news2024/11/20 20:34:00

目录

01: 处理 PC 端基础架构 

02: 通用组件:search 搜索框能力分析

03: 通用组件:search 搜索框样式处理

04: 通用组件:Button 按钮能力分析 

05: 通用组件:Button 按钮功能实现 

06: 通用组件:完善 search 基本能力

07: 通用组件:popover 气泡卡片能力分析

08: 通用组件:popover 气泡卡片基础功能实现 

09: 通用组件:popover 功能延伸,控制气泡展示位置 

10: 通用组件:处理慢速移动时,气泡消失问题 


01: 处理 PC 端基础架构 

- layout
- - components
- - - header
- - - - index.vue
- - - floating.vue
- - - main.vue
- - index.vue
// 设置 header 和 main 区域高度

// tailwind.config.js
module.exports = {
    ……
    theme: {
        extend: {
            ……
            height: {
                header: '72px',
                main: 'calc(100vh - 72px)'
            }
        }
    }
}


// 使用
// l-white => shadow-l-white
// height  => h-header

<header-vue class="h-header"></>
<main-vue class="h-main"></>

02: 通用组件:search 搜索框能力分析

        既然是通用组件,就需要分析它的能力,它应该具备什么样的功能:

        1. 输入内容实现双向数据绑定

        2. 鼠标移入与获取焦点时的动画

        3. 一键清空文本功能

        4. 搜索触发功能

        5. 可控制,可填充的下拉展示区

        6. 监听到以下事件列表:

                1. clear:删除所有文本事件

                2. input:输入事件

                3. focus:获取焦点事件

                4. blur:失去焦点事件

                5. search:触发搜索(点击或回车)事件

03: 通用组件:search 搜索框样式处理

 

- libs
- - search
- - - index.vue
<template>
  <div
    ref="containerTarget"
    class="group relative p-0.5 rounded-xl border-white duration-500 hover:bg-red-100/40"
  >
    <div>
      <!-- 搜索图标 -->
      <m-svg-icon
        class="w-1.5 h-1.5 absolute translate-y-[-50%] top-[50%] left-2"
        name="search"
        color="#707070"
      />
      <!-- 输入框 -->
      <input
        class="block w-full h-[44px] pl-4 text-sm outline-0 bg-zinc-100 dark:bg-zinc-800 caret-zinc-400 rounded-xl text-zinc-900 dark:text-zinc-200 tracking-wide font-semibold border border-zinc-100 dark:border-zinc-700 duration-500 group-hover:bg-white dark:group-hover:bg-zinc-900 group-hover:border-zinc-200 dark:group-hover:border-zinc-700 focus:border-red-300"
        type="text"
        placeholder="搜索"
        v-model="inputValue"
        @focus="onFocusHandler"
        @blur="onBlurHandler"
        @keyup.enter="onSearchHandlder"
      />
      <!-- 删除按钮 -->
      <m-svg-icon
        v-show="inputValue"
        name="input-delete"
        class="h-1.5 w-1.5 absolute translate-y-[-50%] top-[50%] right-9 duration-500 cursor-pointer"
        @click="onClearClick"
      ></m-svg-icon>
      <!-- 分割线 -->
      <div
        class="opacity-0 h-1.5 w-[1px] absolute translate-y-[-50%] top-[50%] right-[62px] duration-500 bg-zinc-200 group-hover:opacity-100"
      ></div>
      <!-- TODO: 搜索按钮(通用组件) -->
      <m-button
        class="absolute translate-y-[-50%] top-[50%] right-1 rounded-xl duration-500 opacity-0 group-hover:opacity-100"
        icon="search"
        iconColor="#ffffff"
        @click="onSearchHandlder"
      ></m-button>
    </div>
    <!-- 下拉区 -->
    <transition name="slide">
      <div
        v-if="$slots.dropdown"
        v-show="isFocus"
        class="max-h-[368px] w-full text-base overflow-auto bg-white dark:bg-zinc-800 absolute z-20 left-0 top-[56px] p-2 rounded border border-zinc-200 dark:border-zinc-600 duration-200 hover:shadow-3xl scrollbar-thin scrollbar-thumb-zinc-200 dark:scrollbar-thumb-zinc-900 scrollbar-track-transparent"
      >
        <slot name="dropdown" />
      </div>
    </transition>
  </div>
</template>

<script>
// 更新事件
const EMIT_UPDATE_MODELVALUE = 'update:modelValue'
// 触发搜索(点击或回车)事件
const EMIT_SEARCH = 'search'
// 删除所有文本事件
const EMIT_CLEAR = 'clear'
// 输入事件
const EMIT_INPUT = 'input'
// 获取焦点事件
const EMIT_FOCUS = 'focus'
// 失去焦点事件
const EMIT_BLUR = 'blur'
</script>

<script setup>
import { watch, ref } from 'vue'
import { useVModel, onClickOutside } from '@vueuse/core'

const props = defineProps({
  modelValue: {
    type: String,
    required: true
  }
})

const emits = defineEmits([
  EMIT_UPDATE_MODELVALUE,
  EMIT_CLEAR,
  EMIT_INPUT,
  EMIT_FOCUS,
  EMIT_BLUR,
  EMIT_SEARCH
])

// 输入文本
const inputValue = useVModel(props)

/**
 * 清空文本
 */
const onClearClick = () => {
  inputValue.value = ''
  emits(EMIT_CLEAR, '')
}

/**
 * 触发搜索
 */
const onSearchHandlder = () => {
  emits(EMIT_SEARCH, inputValue.value)
}

/**
 * 监听焦点行为
 */
const isFocus = ref(false)
const onFocusHandler = () => {
  isFocus.value = true
  emits(EMIT_FOCUS)
}

/**
 * 失去焦点
 */
const onBlurHandler = () => {
  emits(EMIT_BLUR)
}

/**
 * 点击区域外隐藏 dropdown
 */
const containerTarget = ref(null)
onClickOutside(containerTarget, () => {
  isFocus.value = false
})

/**
 * 监听输入行为
 */
watch(inputValue, (val) => {
  emits(EMIT_INPUT, val)
})
</script>

<style lang="scss" scoped>
.slide-enter-active {
  transition: all 0.5s;
}

.slide-leave-active {
  transition: all 0.5s;
}

.slide-enter-from,
.slide-leave-to {
  transform: translateY(40px);
  opacity: 0;
}
</style>

04: 通用组件:Button 按钮能力分析 

对于这个按钮来说,我们期望拥有以下能力:

        1. 可以显示文字按钮,并提供 loading 功能

        2. 可以显示 icon 按钮,并可以任意指定 icon 颜色

        3. 可以开关的点击动画

        4. 可以指定各种风格和大小

        5. 当指定的风格或大小不符合预设时,需要给开发者以提示消息

05: 通用组件:Button 按钮功能实现 

- libs
- - button
- - - index.vue
/**
 * 实现步骤:
 * 1. 构建 type 风格可选项 和 size 大小可选项
 * 2. 通过 props 让开发者控制按钮
 * 3. 区分 icon button 和 text button
 * 4. 依据当前数据,实现视图
 * 5. 处理点击事件
 */

 书写习惯:setup 是写逻辑的地方,不希望在这里写大量的常量。可以在 <script setup> 上面再去创建一个 <script>

// 定义 main 颜色
// tailwind.config.js
module.exports = {
    theme: {
        extend: {
            colors: {
                main: '#f44c58',
                'hover-main': '#F2F9EC',
            }
        }
    }
}

// 使用
class = "bg-main"
<script>
// type 可选项:表示按钮风格
const typeEnum = {
  primary:
    'text-white  bg-zinc-800 dark:bg-zinc-900  hover:bg-zinc-900 dark:hover:bg-zinc-700 active:bg-zinc-800 dark:active:bg-zinc-700',
  main: 'text-white  bg-main dark:bg-zinc-900  hover:bg-hover-main dark:hover:bg-zinc-700 active:bg-main dark:active:bg-zinc-700',
  info: 'text-zinc-800 dark:text-zinc-300  bg-zinc-200 dark:bg-zinc-700 hover:bg-zinc-300 dark:hover:bg-zinc-600 active:bg-zinc-200 dark:active:bg-zinc-700 '
}
// size 可选项:表示按钮大小。区分文字按钮和icon按钮
const sizeEnum = {
  default: {
    button: 'w-8 h-4 text-base',
    icon: ''
  },
  'icon-default': {
    button: 'w-4 h-4',
    icon: 'w-1.5 h-1.5'
  },
  small: {
    button: 'w-7 h-3 text-base',
    icon: ''
  },
  'icon-small': {
    button: 'w-3 h-3',
    icon: 'w-1.5 h-1.5'
  }
}
</script>
// 通过 props 让开发者控制按钮
<script setup>
const props = defineProps({
  // icon 图标名字
  icon: {
    type: String
  },
  // icon 图标颜色
  iconColor: {
    type: String
  },
  // icon 图标类名(匹配 tailwind)
  iconClass: {
    type: String
  },
  // 按钮风格
  type: {
    type: String,
    default: 'main',
    validator(val) {
      // 获取所有的可选的按钮风格
      const keys = Object.keys(typeEnum)
      // 开发者指定风格是否在可选风格中
      const result = keys.includes(val)
      // 如果不在则给开发者提示
      if (!result) {
        throw new Error(`你的 type 必须是 ${keys.join('、')} 中的一个`)
      }
      // 返回校验结果
      return result
    }
  },
  // 大小风格
  size: {
    type: String,
    default: 'default',
    validator(val) {
      // 获取所有的可选的大小(注意剔除 icon 开头的元素,因为我们期望开发者输入 size="default",但不期望开发者输入 size="icon-default")
      const keys = Object.keys(sizeEnum).filter((key) => !key.includes('icon'))
      // 开发者指定大小是否在可选大小中
      const result = keys.includes(val)
      // 如果不在则给开发者提示
      if (!result) {
        throw new Error(`你的 size 必须是 ${keys.join('、')} 中的一个`)
      }
      // 返回校验结果
      return result
    }
  },
  // 按钮在点击时是否需要动画
  isActiveAnim: {
    type: Boolean,
    default: true
  },
  // 加载状态
  loading: {
    type: Boolean,
    default: false
  }
})
</script>
// 区分 icon button 和 text button
// 传递了 icon props 则默认按钮类型为 icon button

// 处理大小的 key 值
const sizeKey = computed(() => {
  return props.icon ? 'icon-' + props.size : props.size
})
// 依据当前的数据,实现视图
<template>
  <button
    class="text-sm text-center rounded duration-150 flex justify-center items-center"
    :class="[
      typeEnum[type],
      sizeEnum[sizeKey].button,
      { 'active:scale-105': isActiveAnim }
    ]"
    @click.stop="onBtnClick"
  >
    <!-- 展示 loading -->
    <m-svg-icon
      v-if="loading"
      name="loading"
      class="w-2 h-2 animate-spin mr-1"
    ></m-svg-icon>
    <!-- icon 按钮 -->
    <m-svg-icon
      v-if="icon"
      :name="icon"
      class="m-auto"
      :class="sizeEnum[sizeKey].icon"
      :color="iconColor"
      :fillClass="iconClass"
    ></m-svg-icon>
    <!-- 文字按钮 -->
    <slot v-else />
  </button>
</template>
// 处理点击事件
const EMITS_CLICK = 'click'
const emits = defineEmits([EMITS_CLICK])
/**
 * 按钮点击事件处理
 */
const onBtnClick = () => {
  if (props.loading) {
    return
  }
  emits(EMITS_CLICK)
}

06: 通用组件:完善 search 基本能力

/**
 * 1. 输入内容实现双向数据绑定
 * 2. 搜索按钮在 hover 时展示
 * 3. 一键清空文本功能
 * 4. 触发搜索
 * 5. 控制下拉展示区的展示
 * 6. 事件处理
 */
// 事件处理:
//     双向绑定
//     search 搜索
//     删除所有文本
//     输入事件
//     获取焦点事件
//     失去焦点事件

07: 通用组件:popover 气泡卡片能力分析

/**
 * 具备两个插槽。
 *     第一个插槽描述触发弹出层的视图。这个视图可以定为具名插槽。
 *     第二个插槽描述弹出层内容。这个内容可以定为匿名插槽。
 * 弹出层气泡可以在指定位置弹出。
 */

08: 通用组件:popover 气泡卡片基础功能实现 

- libs
- - popover
- - - index.vue
<template>
  <div class="relative" @mouseleave="onMouseleave" @mouseenter="onMouseenter">
    <div ref="referenceTarget">
      <!-- 具名插槽 -->
      <slot name="reference" />
    </div>
    <!-- 气泡展示动画 -->
    <transition name="slide">
      <div
        v-show="isVisable"
        ref="contentTarget"
        class="absolute p-1 z-20 bg-white dark:bg-zinc-900 border rounded-md dark:border-zinc-700"
        :style="contentStyle"
      >
        <!-- 匿名插槽 -->
        <slot />
      </div>
    </transition>
  </div>
</template>

<script>
// 延迟关闭时长
const DELAY_TIME = 100

const PROP_TOP_LEFT = 'top-left'
const PROP_TOP_RIGHT = 'top-right'
const PROP_BOTTOM_LEFT = 'bottom-left'
const PROP_BOTTOM_RIGHT = 'bottom-right'

// 定义指定位置的 Enum
const placementEnum = [
  PROP_TOP_LEFT,
  PROP_TOP_RIGHT,
  PROP_BOTTOM_LEFT,
  PROP_BOTTOM_RIGHT
]
</script>

<script setup>
import { ref, watch, nextTick } from 'vue'

const props = defineProps({
  // 控制气泡弹出位置,并给出开发者错误的提示
  placement: {
    type: String,
    default: 'bottom-left',
    validator(val) {
      const result = placementEnum.includes(val)
      if (!result) {
        throw new Error(
          `你的 placement 必须是 ${placementEnum.join('、')} 中的一个`
        )
      }
      return result
    }
  }
})

// 控制 menu 展示
const isVisable = ref(false)

// 控制延迟关闭
let timeout = null
/**
 * 鼠标移入的触发行为
 */
const onMouseenter = () => {
  isVisable.value = true
  // 再次触发时,清理延时装置
  if (timeout) {
    clearTimeout(timeout)
  }
}
/**
 * 鼠标移出的触发行为
 */
const onMouseleave = () => {
  // 延时装置
  timeout = setTimeout(() => {
    isVisable.value = false
    timeout = null
  }, DELAY_TIME)
}

/**
 * 计算元素尺寸
 */
const referenceTarget = ref(null)
const contentTarget = ref(null)
const useElementSize = (target) => {
  if (!target) return {}
  return {
    width: target.offsetWidth,
    height: target.offsetHeight
  }
}

/**
 * 计算弹层位置
 */
const contentStyle = ref({
  top: 0,
  left: 0
})

/**
 * 监听展示的变化,在展示时计算气泡位置
 */
watch(isVisable, (val) => {
  if (!val) {
    return
  }
  // 等待渲染成功之后
  nextTick(() => {
    switch (props.placement) {
      // 左上
      case PROP_TOP_LEFT:
        contentStyle.value.top = 0
        contentStyle.value.left =
          -useElementSize(contentTarget.value).width + 'px'
        break
      // 右上
      case PROP_TOP_RIGHT:
        contentStyle.value.top = 0
        contentStyle.value.left =
          useElementSize(referenceTarget.value).width + 'px'
        break
      // 左下
      case PROP_BOTTOM_LEFT:
        contentStyle.value.top =
          useElementSize(referenceTarget.value).height + 'px'
        contentStyle.value.left =
          -useElementSize(contentTarget.value).width + 'px'
        break
      // 右下
      case PROP_BOTTOM_RIGHT:
        contentStyle.value.top =
          useElementSize(referenceTarget.value).height + 'px'
        contentStyle.value.left =
          useElementSize(referenceTarget.value).width + 'px'
        break
    }
  })
})
</script>

<style lang="scss" scoped>
// slide 展示动画
.slide-enter-active {
  transition: opacity 0.3s, transform 0.3s;
}

.slide-leave-active {
  transition: opacity 0.3s, transform 0.3s;
}

.slide-enter-from,
.slide-leave-to {
  transform: translateY(20px);
  opacity: 0;
}
</style>

09: 通用组件:popover 功能延伸,控制气泡展示位置 

/**
 * 步骤:
 * 1. 指定所有可选位置的常量,并生成 enum
 * 2. 通过 prop 控制指定位置
 * 3. 获取元素的 DOM;创建读取元素尺寸的方法
 * 4. 生成气泡的样式对象,用来控制每个位置对应的样式
 * 5. 根据 prop,计算样式对象
 */

10: 通用组件:处理慢速移动时,气泡消失问题 

        想要解决这个问题,可以利用 类似于防抖(debounce)的概念。

        也就是:鼠标刚离开时,不去立刻修改 isVisible,而是延迟一段时间,如果在这段时间之内,再次触发了鼠标移入事件,则不再修改 isVisible。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1657011.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【漏洞复现】GB28181摄像头管理平台api接口处存在未授权漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…

【北京迅为】《iTOP-3588开发板nfstftp烧写手册》

RK3588是一款低功耗、高性能的处理器&#xff0c;适用于基于arm的PC和Edge计算设备、个人移动互联网设备等数字多媒体应用&#xff0c;RK3588支持8K视频编解码&#xff0c;内置GPU可以完全兼容OpenGLES 1.1、2.0和3.2。RK3588引入了新一代完全基于硬件的最大4800万像素ISP&…

力扣HOT100 - 4. 寻找两个正序数组的中位数

解题思路&#xff1a; 两个数组合并&#xff0c;然后根据奇偶返回中位数。 class Solution {public double findMedianSortedArrays(int[] nums1, int[] nums2) {int m nums1.length;int n nums2.length;int[] nums new int[m n];if (m 0) {if (n % 2 0) return (nums2…

若依集成mybatis-plus 超详细教程(亲测可用)

文章目录 简介步骤第一步第二步第三步第四步第五步第六步 使用QueryWrapperservice层impl 实现接口类层Mapper层 简介 话不多说 直接跟着下面的教程操作&#xff0c;如果有报错私信我&#xff0c;或者通过博文下面的微信名片加我微信&#xff0c;免费解答哦&#xff01; 步骤 …

代码随想录刷题随记31-贪心5

代码随想录刷题随记31-贪心5 435. 无重叠区间 leetcode链接 按照右边界排序&#xff0c;从左向右记录非交叉区间的个数。 此时问题就是要求非交叉区间的最大个数。 这里记录非交叉区间的个数还是有技巧的&#xff0c;如图&#xff1a; 左边界排序可不可以呢&#xff1f; 也是…

Unity数据持久化之XML

目录 数据持久化XML概述XML文件格式XML基本语法XML属性 C#读取存储XMLXML文件存放位置C#读取XML文件C#存储XML文件 实践小项目必备知识点XML序列化&#xff08;不支持字典&#xff09;XML反序列化IXmlSerializable接口让Dictionary支持序列化反序列化 数据持久化XML概述 什么是…

Docker入门指南:Docker镜像的使用(二)

&#x1f340; 前言 博客地址&#xff1a; CSDN&#xff1a;https://blog.csdn.net/powerbiubiu &#x1f44b; 简介 在本章节中&#xff0c;将深入探讨 Docker 镜像的概念&#xff0c;以及如何使用镜像的一系列操作。 &#x1f4d6; 正文 1 什么是镜像 1.1 Docker镜像的简…

cocos=》带你全面、系统的了解周期函数(含源码分析)

目录 简介 第一 初步了解周期函数 第二 进一步认识周期函数 一、结合节点树来了解一下周期函数 二、节点激活、脚本组件启用 三、node.parent、setParent、addChild 与 周期函数 四、addComponent 与周期函数 五、exectionOrder 与 周期函数 第三 从源码中 学习周期函…

科学碳目标(SBTI)认证是什么?

科学碳目标&#xff08;SBTI&#xff09;认证是一种基于科学的减排目标认证和监测体系&#xff0c;旨在确保企业和国家制定的减排目标符合科学标准&#xff0c;并且能够实现全球气候目标的减缓效应。这个认证体系由全球碳项目和世界资源研究所&#xff08;WRI&#xff09;共同开…

如何判断自己是不是强迫型人格障碍?

什么是强迫型人格障碍&#xff1f; 强迫型人格&#xff0c;也叫强迫固执型人格&#xff0c;当某些强迫型行为严重影响到正常的生活&#xff0c;工作和人际关系&#xff0c;且具有长期稳定的持续性特征&#xff0c;即是强迫型人格障碍。 这类思维和行为特征可以概括为&#x…

leetcode 1235

leetcode 1235 代码 class Solution { public:int jobScheduling(vector<int>& startTime, vector<int>& endTime, vector<int>& profit) {int n startTime.size();vector<vector<int>> jobs(n);for(int i0; i<n; i){jobs[i] …

【Anaconda】升级Anaconda Navigator提示JSONDecoderError,删除.condarc文件后搞定

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、报错&#xff1a;JSONDecoderError二、错误原因三、解决问题总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 时间长未升级Ana…

本地搭建springboot服务并实现公网远程调试本地接口

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

2024/5/9 QTday4

完成定时器制作 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);connect(&timer2, &QTimer::timeout, this, &Widget::label_begin);connect(&…

数据库(MySQL)—— 索引

数据库&#xff08;MySQL&#xff09;—— 索引 什么是索引创建索引使用 CREATE INDEX 语句使用 ALTER TABLE 语句在创建表时定义索引特殊类型索引注意事项 举个例子无索引的情况有索引的情况为什么索引快索引的结构 今天我们来看看MySQL中的索引&#xff1a; 什么是索引 MyS…

0509_IO4

练习1&#xff1a; 创建一对父子进程&#xff1a; 父进程负责向文件中写入 长方形的长和宽 子进程负责读取文件中的长宽信息后&#xff0c;计算长方形的面积 1 #include <stdio.h>2 #include <string.h>3 #include <stdlib.h>4 #include <sys/types.h>…

PyCharm安装详细教程

PyCharm安装详细教程 PyCharm简介及其下载网站 PyCharm是由JetBrains打造的一款Python IDE(Integrated Development Environment&#xff0c;集成开发环境)&#xff0c;带有一整套可以帮助用户在使用Python语言开发时提高其效率的工具。PyCharm提供了代码编辑、调试、语法高亮…

【BUUCTF】[RoarCTF 2019]Easy Java1

工具&#xff1a;hackbar发包&#xff0c;bp抓包。 解题步骤&#xff1a;【该网站有时候send不了数据&#xff0c;只能销毁靶机重试】 这里的登录界面是个天坑【迷魂弹】 直接点击help&#xff0c;然后进行打开hackbar——通过post请求&#xff0c;再通过bp抓包&#xff0c;…

DetCLIPv3:面向多功能生成开放词汇的目标检测

DetCLIPv3:面向多功能生成开放词汇的目标检测 摘要IntroductionRelated worksMethod DetCLIPv3: Towards Versatile Generative Open-vocabulary Object Detection 摘要 现有的开词汇目标检测器通常需要用户预设一组类别&#xff0c;这大大限制了它们的应用场景。在本文中&…

长难句打卡5.9

For example, the Long Now Foundation has as its flagship project a mechanical clock that is designed to still be marking time thousands of years hence. 例如,今日永存资金会将机械钟表视为旗舰项目,因此该钟表旨在为未来几千年保持计时。 Foundation n.基金会flag…