前端日常记录问题2

320 阅读5分钟

解决打开取色器页面抖动问题

此类问题一般再抖动过程中左右会出现滑动条,抖动的原因其实是因为页面滑动条的显示与隐藏,所以解决的方式找到滑动条所属的元素添加 overflow: hidden

通过js的方式设置元素的样式

 const newNode = document.createElement('img');
 newNode.setAttribute('class', 'upload-icon');
 newNode.setAttribute('src', 'https://d322uc7y3fcjjx.cloudfront.net/test/easy-email/upload-icon.svg');

实现拖拽组件形成flow

image.png

实现手机短信样式

image.png

          <div className={styles['sme-preview']}>
            <div className={styles['sme-phone']}>
              <div className={styles['sme-phone-header']}>
                <div className={styles['sme-phone-header-input']}>
                  +19786794257
                </div>
              </div>
              <div className={styles['sme-phone-content']}>
                <div className={styles['sme-phone-message']} dangerouslySetInnerHTML={{
                  __html: segmentInfo.previewHtml
                }}>
                </div>
              </div>
              <div className={styles['sme-phone-bottom']}>
                <div className={styles['sme-phone-bottom-item-1']}></div>
                <div className={styles['sme-phone-bottom-item-2']}></div>
              </div>
            </div>
          </div>
          
          
          
.sme-preview {
  width: 50%;
  height: 100%;
  background: #F6F5FF;
  overflow-y: auto;
}
.sme-phone {
  margin: 24px auto;
  width: 360px;
  height: 710px;
  border: 10px solid #7C7B85;
  overflow: hidden;
  border-radius: 50px;
  box-sizing: border-box;
}
.sme-phone-header {
  width: 100%;
  height: 72px;
  padding: 20px 50px;
  background: #E4E3EB;
  box-sizing: border-box;
}
.sme-phone-header-input {
  background: #F5F5F7;
  text-align: center;
  line-height: 32px;
  overflow: hidden;
  color: #51505A;
  font-weight: 500;
  font-size: 16px;
  border-radius: 20px;
}
.sme-phone-content {
  width: 100%;
  height: 538px;
  background-color: #FFFFFF;
  overflow: auto;
  box-sizing: border-box;
}
.sme-phone-message {
  white-space: pre-wrap;
  position: relative;
  margin: 32px 44px 32px 16px;
  padding: 16px 8px 16px 22px;
  min-height: 72px;
  text-align: left;
  border-radius: 14px;
  background-color: #EDEBFF;
  line-height: 20px;
  color: #21202A;
  font-size: 14px;
  overflow-wrap: break-word;
  word-break: break-word;
}
// 此处为实现对话框左边的那个左尖角
.sme-phone-message::before {
  content: ' ';
  position: absolute;
  top: 32px;
  left: -4px;
  width: 8px;
  height: 8px;
  background-color: #EDEBFF;
  transform: rotate(45deg);
}
.sme-phone-bottom {
  padding: 24px 32px;
  width: 100%;
  height: 80px;
  background: #E4E3EB;
  box-sizing: border-box;
}
.sme-phone-bottom-item-1 {
  display: inline-block;
  width: 32px;
  height: 32px;
  border-radius: 16px;
  background-color: #F9F9FC;
}
.sme-phone-bottom-item-2 {
  margin-left: 24px;
  display: inline-block;
  width: 220px;
  height: 32px;
  border-radius: 16px;
  background-color: #F9F9FC;
}

实现下拉小箭头的上下翻转

image.png

image.png

通过点击然后改变transform的旋转属性即可

<Button
    className={styles['header-button-fold']}
    type="text"
    style={{ transform: height === '0' ? 'rotate(180deg)' : '' }}
    onClick={() => {
      height === '0' ? setHeight('inherit') : setHeight('0');
    }}

前端埋点脚本

三种事件 1.页面访问(Site/Visit) 2. 浏览商品(Site/ViewProduct) 3.加购(Cart/Add)

! function (w,c, u) {
  u = 'https://59b517704ce43f0f.cartx.cloud/cartxtrack';
  c = w.cartq;
  c.params = {};

  c.helper = {
    getCookie: function(c_name) {
      if (document.cookie.length>0) {
        var c_start=document.cookie.indexOf(c_name + "=")
        if (c_start!==-1) {
          c_start=c_start + c_name.length+1
          var c_end=document.cookie.indexOf(";",c_start)
          if (c_end===-1) c_end=document.cookie.length
          return unescape(document.cookie.substring(c_start,c_end))
        }
      }
      return ""
    },
    getQueryVariable(variable) {
      const urlSearchParams = new URLSearchParams(location.search.replace("+", "%2B"));
      return urlSearchParams.get(variable) || '';
    },
    S4: function() {
      return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
    },
    guid: function(S4) {
      S4 = c.helper.S4;
      return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
    },
    ajax: {
      get: function(data, fn, errFn) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', data.url, true);
        xhr.onreadystatechange = function() {
          if (xhr.readyState === 4){
            var responseText;
            try {
              responseText = JSON.parse(xhr.responseText)
            } catch (e) {
              responseText = xhr.responseText;
            }
            if(xhr.status === 200 || xhr.status === 304) {
              fn && fn.call(this, responseText);
            } else {
              errFn && errFn.call(this, responseText);
            }
          }
        };
        xhr.send();
      },
      post: function (data, fn) {
        var xhr = new XMLHttpRequest();
        xhr.open("POST", data.url, true);
        xhr.setRequestHeader("Content-Type", data.contentType||"application/json");
        xhr.onreadystatechange = function() {
          if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 304)) {
            var responseText;
            try {
              responseText = JSON.parse(xhr.responseText)
            } catch (e) {
              responseText = xhr.responseText;
            }
            fn && fn.call(this, responseText);
          }
        };
        xhr.send(data.data);
      }
    },
    getCart: function (fn) {
      c.helper.ajax.get({
        // todo 获取购物车信息
        url: location.origin + '/cart.js?promoter=cartrack'
      },function(res) {
        // todo 购物车信息返回值
        res && (c.params.cart = res);
        fn && fn();
      })
    },
    getProduct: function (fn) {
      if(c.params.product) {
        fn();
      } else {
        c.helper.ajax.get({
          // todo 获取产品信息
          url: location.origin.concat(location.pathname, ".js")
        },function (res) {
          // todo 产品信息返回值
          res && (c.params.product = res);
          fn && fn();
        },function () {
          fn && fn();
        })
      }
    },
    handleAddCart: function () {
      if(sessionStorage.getItem('carttrackAddCard') === 'false') {
        c('track', 'Cart/Add', {
          email: c.params?.customer?.email || '',
          phone: c.params?.customer?.phone || '',
          pathName: location.href,
          productId: String(c.params?.product?.id || ''),
          productName: c.params?.product?.title || '',
          price: c.params?.product?.price || null,
          compareAtPrice: c.params?.product?.compare_at_price || '',
          variantId: sessionStorage.getItem('carttrackVariantId') || null,
          // todo 获取购物车token
          cartToken: c.params?.cart?.token || '', // 购物车token
          cartJson: c.params?.cart || null // 购物车
        })
        sessionStorage.setItem('carttrackAddCard', 'true')
      }
    }
  }

  c.eventList = {
    init: function(e,id,email) {
      c.id = id;
      c.params = {...c.params, customer: {...c.params?.customer, email: email}};
    },
    set: function(e,key,value) {
      c.params = {...c.params, [key]: value};
    },
    track: function (e,v,d) {
      const p = {
        // todo 唯一用户标识
        uid: c.helper.getCookie("_shopify_y"),  // 唯一用户标识
        requestId: c.helper.guid(),  // 每次request的uuid
        timestamp: Date.now(),
        eventType: v,
        companyId: c.id||'',
        // todo 集成id
        integrationId: 1,
        cid: localStorage.getItem('cid')||null,
        customerId: String(c.params?.customer?.id||''), // shopify用户id,在已登录情形下才会有
        data: d
      };
      c.helper.ajax.post({
        url: u,
        contentType: "application/json",
        data: JSON.stringify(p)
      });
    }
  }

  if(c.helper.getQueryVariable('cid')) {
    localStorage.setItem('cid', c.helper.getQueryVariable('cid'))
  }

  c.callMethod = function() {
    if (arguments.length === 0 || !Object.keys(c.eventList).includes(arguments[0])) return console.warn('no event');
    c.eventList[arguments[0]].apply(c,arguments);
  }
  c.queue.forEach(function(a) {
    c.apply(c,a);
  })
  c.queue = [];

  // 站点活跃Active on Site: 用户进入站点任意页面
  c('track', 'Site/Visit', {
    pathName: location.href,
    email: c.params?.customer?.email||'',
    phone: c.params?.customer?.phone||''
  })

  c.helper.getCart(null);

  // todo 商品页面链接
  if (location.pathname.indexOf('/products') !== -1) {
    c.helper.getProduct(function () {
      // 浏览商品Viewed Product: 用户进入集成站点product详情页
      c('track', 'Site/ViewProduct', {
        email: c.params?.customer?.email||'',
        phone: c.params?.customer?.phone||'',
        pathName: location.href,
        productId: String(c.params?.product?.id || ''),
        productName: c.params?.product?.title || '',
        price: c.params?.product?.price || null,
        compareAtPrice: c.params?.product?.compare_at_price || ''
      })
    })
  }

  c.listenAdd = function() {
    if (document.readyState === 'complete') {
      const button = document.getElementsByTagName("button");
      const input = document.getElementsByTagName("input");
      c.handleEle(button);
      c.handleEle(input);
    }
  }

  c.handleEle = function(elements) {
    for (let i = 0; i < elements.length; i++) {
      const element = elements[i];
      const name = element.getAttribute("name");
      const type = element.getAttribute("type");
      const cssClass = element.getAttribute("class");
      const id = element.getAttribute("id");
      if(
        (id && id.toLowerCase().indexOf("addtocart") !== -1)
        || (id && id.indexOf('add_to_cart') !== -1)
        || (id && id.indexOf('add-to-cart') !== -1)
        || (cssClass && cssClass.toLowerCase().indexOf('addtocart') !== -1)
        || (cssClass && cssClass.indexOf('add_to_cart') !== -1)
        || (cssClass && cssClass.indexOf('add-to-cart') !== -1)
        || (name && name === "add" && type && type === "submit")
      ) {
        element.addEventListener("click", c.listenerEvent);
      }
    }
  }

  c.listenerEvent = function () {
    sessionStorage.setItem('carttrackAddCard', 'false')
    // todo 获取sku信息
    sessionStorage.setItem('carttrackVariantId', c.helper.getQueryVariable('variant') || c.params?.product?.variants?.[0]?.id || '',)
    let times = 0;
    const fn = function () {
      times += 1;
      // todo 获取购物车产品数量
      if (c?.params?.cart?.item_count) {
        c.helper.handleAddCart();
      } else if(times < 10){
        setTimeout(() => {c.helper.getCart(fn)}, 2000);
      }
    }
    c.helper.getCart(fn);
  }

  // todo 购物车链接
  if (location.pathname.indexOf('/cart') !== -1) {
    c.helper.getCart(c.helper.handleAddCart);
  }

  // 默认事件获取
  c.listenAdd();
  if(document.onreadystatechange){
    const oldFn = document.onreadystatechange;
    document.onreadystatechange = function (...args){
      oldFn(...args);
      c.listenAdd();
    }
  } else {
    document.onreadystatechange = c.listenAdd;
  }
}(window);

Arco design Select组件实现分组

image.png

利用其dropdownRender自定义下拉项菜单,参数menu就是当前的所有Select.option的子项的虚拟dom对象,所以对其分组只需要新建两个对象然后对两组数据进行分类,这里一定要新建两个对象进行构造。

<Select
  style={{ width: '320px', height: '32px' }}
  dropdownRender={
    (menu) => {
      if (menu) {
        const group1 = [];
        const group2 = [];
        menu?.props?.data?.forEach((item) =>
          !item?.key.includes('bottom') ? group1.push(item) : group2.push(item))
        // 这里不可直接循环menu去修改,因为menu是一个immutable对象,直接修改会报错
        // 自定义的dom结构
        const customermenu = {
          ...menu,
          props: {
            ...menu.props,
            data: group1
          }
        }
        // 已存在的dom结构
        const exsitMenu = {
          ...menu,
          props: {
            ...menu.props,
            data: group2
          }
        }
        return (
          <div>
          {customermenu}
            <Button style={{ margin: '0px 100px' }}
              type='text'
              icon={<i className='iconfont icon-bianji'></i>}
              onClick={() => { setIsCustomModal(true) }}
            >自定义添加
            </Button>
            <Divider style={{ margin: 0 }} />
            {exsitMenu}
          </div>
        )
      }
    }
  }
  onVisibleChange={(visible) => {
    visible && getUtmCodeList('flow')
  }}
  renderFormat={(option, value) => {
    return (
      <div className={styles.defineOptions}>
        {value}
      </div>
    )
  }}
>
  {utmCodeList?.map((item, index) => {
    if (item === 'CartSee' || item === 'SMS' || item === 'Email' || item === 'Message' || item === 'Flow_ID' || item === 'FlowSMS' || item === 'FlowEmail') {
      return <Select.Option key={`${item}-bottom`} value={item}>{item}</Select.Option>
    } else {
    // 这一组每一项添加删除按钮
      return <Select.Option key={index} value={item}>
        <div className={styles.defineOptions}>
          {item}
          {
            <i className="iconfont icon-guanbi" onClick={(e) => {
              e.stopPropagation();
              // featRemoveEmail(item.email)
            }} style={{ float: 'right' }} />
          }
        </div>
      </Select.Option>
    }
    //
  })}
</Select>

实现可变色的评分组件(Rate)

需求:选择不同的星时需要有不通的颜色。

20231018-105921的副本.gif

实现思路: 监听不通的input标签的点击和鼠标hover 切换不同的图片


  const [src, setSrc] = useState(
    'https://cdn.trustpilot.net/brand-assets/4.1.0/stars/stars-0.svg'
  );
  const [index, setIndex] = useState(null);
  // 鼠标离开时 判断上一次点击的是第几个radio
  const onMouseLeave = () => {
    switch (index) {
      case 0:
        setSrc(
          'https://cdn.trustpilot.net/brand-assets/4.1.0/stars/stars-1.svg'
        );
        break;
      case 1:
        setSrc(
          'https://cdn.trustpilot.net/brand-assets/4.1.0/stars/stars-2.svg'
        );
        break;
      case 2:
        setSrc(
          'https://cdn.trustpilot.net/brand-assets/4.1.0/stars/stars-3.svg'
        );
        break;
      case 3:
        setSrc(
          'https://cdn.trustpilot.net/brand-assets/4.1.0/stars/stars-4.svg'
        );
        break;
      case 4:
        setSrc(
          'https://cdn.trustpilot.net/brand-assets/4.1.0/stars/stars-5.svg'
        );
        break;
      default:
        setSrc(
          'https://cdn.trustpilot.net/brand-assets/4.1.0/stars/stars-0.svg'
        );
        break;
      // 可以继续添加更多的情况
    }
  };



            <div>
              <div
                style={{ position: 'absolute', width: '216px' }}
                id="rate-c"
                onMouseLeave={onMouseLeave}
              >
                <input
                  type="radio"
                  name="star-selector"
                  style={{
                    width: '42px',
                    display: 'inline-block',
                    margin: 0,
                    border: 0,
                    height: '40px',
                    zIndex: 1,
                    cursor: 'pointer',
                    appearance: 'none',
                    writingMode: 'horizontal-tb',
                  }}
                  aria-label="1 star: Bad"
                  data-star-selector-star-1="true"
                  tabIndex={0}
                  value="1"
                  onClick={() => {
                    setIndex(0);
                    setPlaceholder(
                      'What went wrong this time?How can this company imporve?Remember to be honest, helpful, and constructive!'
                    );
                  }}
                  onMouseOver={() => {
                    setSrc(
                      'https://cdn.trustpilot.net/brand-assets/4.1.0/stars/stars-1.svg'
                    );
                  }}
                />
                <input
                  type="radio"
                  name="star-selector"
                  style={{
                    width: '42px',
                    display: 'inline-block',
                    margin: 0,
                    border: 0,
                    height: '40px',
                    zIndex: 1,
                    cursor: 'pointer',
                    appearance: 'none',
                    writingMode: 'horizontal-tb',
                  }}
                  aria-label="2 stars: Poor"
                  data-star-selector-star-2="true"
                  tabIndex={0}
                  value="2"
                  onClick={() => {
                    setIndex(1);
                    setPlaceholder(
                      'What went wrong this time?How can this company imporve?Remember to be honest, helpful, and constructive!'
                    );
                  }}
                  onMouseOver={() => {
                    setSrc(
                      'https://cdn.trustpilot.net/brand-assets/4.1.0/stars/stars-2.svg'
                    );
                  }}
                />
                <input
                  type="radio"
                  name="star-selector"
                  style={{
                    width: '42px',
                    display: 'inline-block',
                    margin: 0,
                    border: 0,
                    height: '40px',
                    zIndex: 1,
                    cursor: 'pointer',
                    appearance: 'none',
                    writingMode: 'horizontal-tb',
                  }}
                  aria-label="3 stars: Average"
                  data-star-selector-star-3="true"
                  tabIndex={0}
                  value="3"
                  onClick={() => {
                    setIndex(2);
                    setPlaceholder(
                      'What did you like or dislike?What is this company doing well, or how can they improve? Remember to be honest, helpful, and constructive!'
                    );
                  }}
                  onMouseOver={() => {
                    setSrc(
                      'https://cdn.trustpilot.net/brand-assets/4.1.0/stars/stars-3.svg'
                    );
                  }}
                />
                <input
                  type="radio"
                  name="star-selector"
                  style={{
                    width: '42px',
                    display: 'inline-block',
                    margin: 0,
                    border: 0,
                    height: '40px',
                    zIndex: 1,
                    cursor: 'pointer',
                    appearance: 'none',
                    writingMode: 'horizontal-tb',
                  }}
                  aria-label="4 stars: Great"
                  data-star-selector-star-4="true"
                  tabIndex={0}
                  value="4"
                  onClick={() => {
                    setIndex(3);
                    setPlaceholder(
                      'What made your experience great? What is this company doing well?Remember to be honest, helpful, and constructive!'
                    );
                  }}
                  onMouseOver={() => {
                    setSrc(
                      'https://cdn.trustpilot.net/brand-assets/4.1.0/stars/stars-4.svg'
                    );
                  }}
                />
                <input
                  type="radio"
                  name="star-selector"
                  style={{
                    width: '42px',
                    display: 'inline-block',
                    margin: 0,
                    border: 0,
                    height: '40px',
                    zIndex: 1,
                    cursor: 'pointer',
                    appearance: 'none',
                    writingMode: 'horizontal-tb',
                  }}
                  aria-label="5 stars: Excellent"
                  data-star-selector-star-5="true"
                  tabIndex={0}
                  value="5"
                  onClick={() => {
                    setIndex(4);
                    setPlaceholder(
                      'What made your experience great? What is this company doing well?Remember to be honest, helpful, and constructive!'
                    );
                  }}
                  onMouseOver={() => {
                    setSrc(
                      'https://cdn.trustpilot.net/brand-assets/4.1.0/stars/stars-5.svg'
                    );
                  }}
                />
              </div>
              <div
                style={{
                  width: '216px',
                  height: '40px',
                  display: 'flex',
                  minWidth: '90px',
                }}
              >
                <img alt="" src={src} />
              </div>
            </div>

解决页面闪烁问题

以下这种情况就是常见的页面闪烁的情况,因为初始状态isOpenSetting为true,在第二次渲染完以后又把isOpenSetting 设置为了false, 这种情况的解决办法就是加一个loading的状态,之后isOpenSetting 的判断要为isOpenSetting && loading ,也就是等state最终确定了(loading为false)以后再去用它判断

// 效果提升的折叠默认关闭
const [isOpenSetting, setIsOpenSetting] = useState(true);
// 增加loading 状态解决 页面闪烁问题
const [loading, setLoading] = useState(false);
useEffect(() => {
// 在一个请求后 set这个依赖项
setLoading(true);
getGuideStatus({}).then((res) => {
  setLoading(false);
  if (res.data) {
    setEffectNum(eNum);
  }
    });

}, [effectNum, settingNum]);

// 这里会在第一次渲染完后第二次渲染settingNum达到了4这个条件 把isOpenSetting 从 true 变为 false
useEffect(() => {
if (settingNum === 4) {
  setIsOpenSetting(false);
}

}, [effectNum, settingNum]);

return isOpenSetting && loading <渲染的内容>

css篇

通过设置BFC创建自适应布局(利用流体特性)

1.当设置position:absolute 会触发BFC且 当对立属性同时存在比如left: 1px right:1px 该元素就具有流体特性即元素的宽度自适应于父元素,随着父元素变大变小 他的content-wdith也会变大变小

2.设置overflow: hidden 触发BFC 也能使得块元素自适应布局,里面的子元素的宽度自适应于父元素

还有就是可以设置float absolute inline-block 只是这三个属性具有包裹性(即它的宽度会默认为内部元素的大小)

zindex生效条件

zindex 只有在定位元素和flex元素上才会生效

关系选择器

  • 空格表示后代选择器,选择所有后代
  • 表示直接子元素选择器

  • ~ 选择当前元素后面的相邻的兄弟元素
    • 仅和当前元素相邻的的兄弟元素

深藏不露的width:auto

width 的默认值是 auto,包含4种

  • 充分利用可用空间 比如div默认宽度为父级的100%
  • 收缩与包裹 比如浮动、绝对定位、inline-block 元素或 table 的auto表现为内容的宽度,也就是包裹性
  • 收缩到最小 个最容易出现在 table-layout 为 auto 的表格中,也就是min-content
  • 超出容器限制 ,上面 3 种情况尺寸都不会主动超过父级容器宽度的,比如设置了max-content 就是超出父元素的宽度

流体特性

1.正常流宽度

所谓流动性,并不是看上去的宽度 100%显示这么简单,而是一种 margin/border/padding 和 content 内容区域自动分配水平空间的机制,块级元素一旦设置了宽度,流动性就丢失了

为流体而生的 min-width/max-width

min-width/max-width 出现的场景一定是自适应布局或者流体布局中,如果是那种 width/height 定死的砖头式布局,min-width/max-width 就没有任何出现的价值,因为它们是具有边界行为的属性,所以没有变化自然无法触发,也就没有使用价值。

比如限制图片的样式为 img {max-width: 100%; height: auto!important;} ,height:auto 是必需的,否则,如果原始图片有设定 height,max-width 生效的时候,图片就会被水平压缩

margin:auto 用于块级元素居中

overflow 与滚动条

HTML 中有两个标签是默认可以产生滚动条的,一个是根元素<html>,另一个是文本域<textarea>,因为他们默认的属性值是auto而不是visible。

这里分享一个可以让页面滚动条不发生晃动的小技巧

html {
overflow-y: scroll; /* for IE8 */
}
:root {
overflow-y: auto;
overflow-x: hidden;
}
:root body {
position: absolute;
}
body {
width: 100vw;
overflow: hidden;
}

Table组件的column如何根据条件动态渲染

根据表达式判断列时候显示,最后columns数组用filter(Boolean)方法来移除 columns 数组中的 false 值,以确保只有有效的列会被渲染到表格中

import React from 'react';
import { Table } from '@arco-design/arco-ui';

function MyTable({ data, showAge }) {
  const columns = [
    { title: '姓名', dataIndex: 'name' },
    { title: '性别', dataIndex: 'gender' },
    // 条件渲染的列
    showAge && { title: '年龄', dataIndex: 'age' },
  ].filter(Boolean); // 移除值为 false 的列

  return <Table dataSource={data} columns={columns} />;
}

抓取三方网站所有请求(参数)

可以使用基于Porxy的库,ajax-proxy

import ajaxProxy from '@lazyduke/ajax-proxy'
const { proxyAjax, unProxyAjax } = ajaxProxy

try {
    proxyAjax({
        open: function(args, xhr) {
          console.log('case 1.拦截 open 方法: ', args[0]);
          console.log(typeof args, 'typeof args');
          if (args[0] === 'GET') {
            checkUrlAndParams(args[1], '');
          }
        },
        send: function(data, xhr) {
            console.log('case 2.拦截 open 方法: ', data);
            console.log(JSON.parse(data[0]).formUid, 'data?.email');
            console.log(typeof JSON.parse(data[0]), 'typeof data');
            checkUrlAndParams('', JSON.parse(data[0]));
        }
      })

} catch (error) {
    console.log(error, 'proxyAjax  -- error');
}

css确保图片始终为正方形(自适应屏幕的变化)

<div class="demo">
    <div class="item">
        <div class="img-wrap"><img /></div>
    </div>
    <div class="item">
        <div class="img-wrap"><img /></div>
    </div>
    ....
</div>
* {
    box-sizing: border-box;
}
.demo {
    display: flex;
    width: 100%;
    flex-wrap: wrap;
    padding: 5px;
}
.item {
    position: relative;
    width: 25%;
    padding-bottom: 25%;
}
.img-wrap {
    position: absolute;
    top: 5px;
    left: 5px;
    right: 5px;
    bottom: 5px;
}
img {
    width: 100%;
    height: 100%;
}
  • padding-bottom 设置 25% 即为宽度的 25%,这样使宽高动态保持一致。
  • padding 占据了空间,使用绝对定位铺满父元素,从而使内容能正常展示,还可以通过 top/right/bottom/left 来设置图片之前的间距。

那就是当 margin/padding 设置百分比的值时,无论是左右间距,还是上下间距,它始终相对的是宽度,利用这个知识点就可以完美解决了。

参考文章:www.qinshenxue.com/article/css…

svg图标如何改变颜色

import Edit from '@/assets/img/flow/bianji-temp.svg';
<Edit
  className={styles['custom-icon']}
 ></Edit>
.custom-icon:hover {
  filter: drop-shadow(0 0 0 #6B5BFF); // 通过drop-shadow属性可以实现hoversvg图标变色的需求
}

移动端自适应

1.如果index.html 中有这样的代码 ,就需要先把他注释掉 2.给最外面的元素比如#app加这么一段代码

 #app {
        min-width: 1440px;  //设计稿是多大就写多大
        overflow: hidden;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
    }

然后就可以适配任何屏幕大小的设备,包括微信和飞书内置的浏览器

微信风向链接呈现卡片形式

以vue为例

import {getSing} from '../services/index';
created() {
    // 调用微信分型的事件
    this.getShareInfo();
 },
 getShareInfo() {
  let res1;
  //获取url链接
  const url = window.location.href.split('#')[0]
  getSing(url).then(res => {
    wx.config({
      debug: false, // 开启调试模式,调用的所有 api 的返回值会在客户端 alert 出来,若要查看传入的参数,可以在 pc 端打开,参数信息会通过 log 打出,仅在 pc 端时才会打印。
      appId: res.data.data.appId, // 必填,公众号的唯一标识
      timestamp: parseInt(res.data.data.timestamp), // 必填,生成签名的时间戳
      nonceStr: res.data.data.nonceStr, // 必填,生成签名的随机串
      signature: res.data.data.signature, // 必填,签名
      jsApiList: [
        "updateAppMessageShareData",
        "updateTimelineShareData"
      ] // 必填,需要使用的 JS 接口列表
    });
    wx.ready(() => {
      const shareData = {
        title: "更适合中国海外卖家的营销自动化引擎",
        desc: "选择更适合中国卖家的Cartsee 意味着不费吹灰之力,即可提高你的营收",
        // link: window.location.href,
        link: window.location.href.split('#')[0],
        imgUrl: "https://image.cartx.cloud/cartsee-website/20240117-180705.jpeg",
      };
      //自定义“分享给朋友”及“分享到QQ”按钮的分享内容
      wx.updateAppMessageShareData(shareData);
      //自定义“分享到朋友圈”及“分享到 QQ 空间”按钮的分享内容(1.4.0)
      wx.updateTimelineShareData(shareData);
    });
    //错误了会走 这里
    wx.error(function (res) {
      res1 = res;
    });
  });
},
 

这里的getSing 是后端接口,需要后端去对接微信获取签名参数

注意:1。用微信开发者工具在本地调试是看不出效果的,因为要求环境的域名也必须和url的域名一样(本地是localhost)

2、微信分享卡片现微信有限制,只能将链接先收藏,然后从收藏里分享才可以(ios收藏后分享也会失效)

手动实现elements组件中的tabs

以vue为例,实现的原理就是下面标识哪一项选中是通过移动下面的横线,然后再给横线的移动增加动画

<div
  style="
    display: flex;
    width: 1170px;
    justify-content: space-between;
    margin-top: 60px;
    padding: 0px 35px;
    border-bottom: 1px solid #ccc;
  "
>
  <div
    style="width: 200px; cursor: pointer"
    :class="{ selected: selectedOption === '1' }"
    @click="selectOption('1')"
  >
    <div
      style="
        color: #292930;
        font-family: Noto Sans;
        font-size: 10px;
        font-style: normal;
        font-weight: 500;
        line-height: 156.5%;
        letter-spacing: 1.515px;
        text-transform: uppercase;
        width: 200px;
      "
    >
      // 01 . user segmentation
    </div>
    <div
      style="
        font-family: PingFang SC;
        font-size: 28px;
        font-style: normal;
        font-weight: 600;
        line-height: 110.5%;
        margin-top: 10px;
      "
      :style="{ color: selectedOption === '1' ? '#333af4' : '#292930' }"
    >
      智能用户分层
    </div>
    <transition name="fade">
      <div
      style="width: 168px; margin-top: 24px;"
      :style="{ transform: `translateX(${underlinePosition}px)` }"
      class="underline"
    >
      <div
        style="
          width: 74px;
          height: 4px;
          flex-shrink: 0;
          background: #333af4;
          margin: 0 auto;
        "
      ></div>
    </div>

    </transition>

  </div>

  <div
    style="width: 200px; cursor: pointer"
    :class="{ selected: selectedOption === '2' }"
    @click="selectOption('2')"
  >
    <div
      style="
        color: #292930;
        font-family: Noto Sans;
        font-size: 10px;
        font-style: normal;
        font-weight: 500;
        line-height: 156.5%;
        letter-spacing: 1.515px;
        text-transform: uppercase;
        width: 200px;
      "
    >
      // 02 . Product PUSH
    </div>
    <div
      style="
        font-family: PingFang SC;
        font-size: 28px;
        font-style: normal;
        font-weight: 600;
        line-height: 110.5%;
        margin-top: 10px;
      "
      :style="{ color: selectedOption === '2' ? '#333af4' : '#292930' }"
    >
      商品精准推荐
    </div>
    <!-- <transition name="fade">
      <div style="width: 168px; margin-top: 24px;"  :style="{ transform: `translateX(${underlinePosition}px)` }">
      <div
        style="
          width: 74px;
          height: 4px;
          flex-shrink: 0;
          background: #333af4;
          margin: 0 auto;
        "
      ></div>
    </div>
    </transition> -->


  </div>

  <div
    style="width: 200px; cursor: pointer"
    :class="{ selected: selectedOption === '3' }"
    @click="selectOption('3')"
  >
    <div
      style="
        color: #292930;
        font-family: Noto Sans;
        font-size: 10px;
        font-style: normal;
        font-weight: 500;
        line-height: 156.5%;
        letter-spacing: 1.515px;
        text-transform: uppercase;
        width: 200px;
      "
    >
      // 03 . Email Composition
    </div>
    <div
      style="
        font-family: PingFang SC;
        font-size: 28px;
        font-style: normal;
        font-weight: 600;
        line-height: 110.5%;
        margin-top: 10px;
      "
      :style="{ color: selectedOption === '3' ? '#333af4' : '#292930' }"
    >
      高效邮件创作
    </div>
    <!-- <transition name="fade">
      <div style="width: 168px; margin-top: 24px" v-if="selectedOption === '3'">
      <div
        style="
          width: 74px;
          height: 4px;
          flex-shrink: 0;
          background: #333af4;
          margin: 0 auto;
        "
      ></div>
    </div>
    </transition> -->

  </div>

  <div
    style="width: 200px; cursor: pointer"
    :class="{ selected: selectedOption === '4' }"
    @click="selectOption('4')"
  >
    <div
      style="
        color: #292930;
        font-family: Noto Sans;
        font-size: 10px;
        font-style: normal;
        font-weight: 500;
        line-height: 156.5%;
        letter-spacing: 1.515px;
        text-transform: uppercase;
        width: 200px;
      "
    >
      // 04 . Excellent Channels
    </div>
    <div
      style="
        font-family: PingFang SC;
        font-size: 28px;
        font-style: normal;
        font-weight: 600;
        line-height: 110.5%;
        margin-top: 10px;
      "
      :style="{ color: selectedOption === '4' ? '#333af4' : '#292930' }"
    >
      卓越通道效果
    </div>
    <!-- <transition name="fade">
      <div style="width: 168px; margin-top: 24px" v-if="selectedOption === '4'">
      <div
        style="
          width: 74px;
          height: 4px;
          flex-shrink: 0;
          background: #333af4;
          margin: 0 auto;
        "
      ></div>
    </div>
    </transition> -->

  </div>
</div>

<script>
updateUnderlinePosition() {
  if (this.selectedOption === '1') {
    this.underlinePosition = 0;
  } else if (this.selectedOption === '2') {
    this.underlinePosition = (Number(this.selectedOption) - 1) * 300; // Assuming each option has a width of 200px
  } else if (this.selectedOption === '3') {
    this.underlinePosition = (Number(this.selectedOption) - 1) * 300;
  } else if (this.selectedOption === '4') {
    this.underlinePosition = (Number(this.selectedOption) - 1) * 300;
  }
},
</script>
        
<style>
.fade-enter-active {
  transition: all .3s ease;
}
.fade-leave-active {
  transition: all .1s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.fade-enter, .fade-leave-to
/* .slide-fade-leave-active for below version 2.1.8 */ {
  transform: translateX(10px);
  opacity: 0;
}
.underline {
  transition: transform 0.3s;
}
</style>

前端国际化

如果项目很大手动翻译需要大量时间的话,可以使用自动化翻译插件实现 vite-plugin-auto-i18n

具体配置如下,以react项目 vite为例

vite.config.ts

import vuePluginsAutoI18n from "vite-plugin-auto-i18n";
vuePluginsAutoI18n({
  option:{
    globalPath: './lang',
    namespace: 'lang',
    distPath: './dist/assets',
    distKey: 'index',
    langKey: ['zh-cn', 'en'],
    originLang: 'zh-cn',
    excludedPath:[/node_modules/,],
    includePath:[/src/,],
  }
})

然后只要运行项目就会开始翻译,然后在lang/index.json中生成翻译的内容, 只要翻译过的部分就不会再重新翻译 可以在本地执行npm run build 然后一次性全部翻译,这样就不需要后续在运行阶段翻译了(可能会网络失败,换个节点比如美国节点就可以成功)

手动实现tab 点击锚点定位到具体区域,及 tab 栏吸顶的效果

  const handleChange = (value) => {
    const cardEle = document.getElementById(`${value}`)
    if (cardEle) {
      cardEle.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }
  }


  {/* tab 组合 */}
  <div className={styles.tabColumn}>
    <Tabs defaultActiveKey="1" onChange={handleChange}>
      {cardTitles.map(item => <TabPane tab={item.title} key={item.value} style={{ height: 200 }}>
      </TabPane>)}
    </Tabs>
  </div>
  
  
  .tabColumn {
  :global(.ant-tabs-content) {
    display: none !important;
  }

  :global(.ant-tabs-bar) {
    margin-bottom: 0px !important;
  }


  position: sticky;
  top: 0;
  z-index: 10000;
  background-color: #FFFFFF;
}

// 解决锚点定位 tab 栏遮挡问题
.customerInfo,
.houseInfo,
.recommendInfo,
.demandInfo,
.intergralDecoraProject,
.organizationInfo,
.serviceInfo {
  margin-top: 9px;
  scroll-margin-top: 40px;
}