HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

蒙牛×每日互动合作获评中国信通院2023“数据+”行业应用优秀案例

消息推送

当前在数字营销领域,品牌广告主越来越追求品效协同。针对品牌主更注重营销转化的切实需求,数据智能上市企业每日互动(股票代码:300766)发挥自身数据和技术能力优势,为垂直行业的品牌客户提供专业的数字化营销解决方案,颇受行业认可。就在不久前举办的“2024中国信通院ICT深度观察报告会”上,每日互动(个推)申报的「大数据赋能蒙牛乳制品品牌精准投放」案例获评中国信通院2023年“数据+”行业应用优秀案例。


图:中国信通院2023“数据+”行业应用优秀案例证书
蒙牛集团是乳制品行业的龙头企业,旗下产品线丰富、消费群体广泛、营销活动多元。在与蒙牛集团的合作中,每日互动基于海量的数据沉淀,通过大数据联合计算平台,在安全、合规的前提下,为蒙牛集团提供消费者洞察、广告投放定向、联合建模等服务,助力蒙牛集团营销策略优化,更精准、有效地链接不同消费人群。

比如,每日互动和蒙牛旗下品牌特仑苏合作,通过联合建模实现「人群精准定向」。每日互动对蒙牛特仑苏的一方样本数据进行了深度清洗和特征挖掘,精细化完成特征筛选和模型调参等,为蒙牛特仑苏构建了专属的智能预测模型,来实现高潜力消费者的精准定向。效果数据表明,特仑苏使用智能预测模型定向找出的投放人群表现出明显的转化优势,整体投放的ROI相比行业定向人群和历史均值均实现了大幅提升,有效加强了品牌种草和下单转化的效果。

随着以大模型为代表的新技术给各行业带来新红利,每日互动也正结合大模型的能力与理念,持续创新品牌营销服务模式,推出了全新的大模型营销应用——“AITA智选人群”工具。蒙牛在11月的营销项目中,首次使用了该工具,无需样本数据和复杂的建模过程,只需要简单的自然语言对话,就能快速实现目标人群定向,不仅使品牌营销过程更安全、更智能,也取得了突出的营销效果。投放测试数据显示,蒙牛通过AITA定向生成的投放人群表现优异,在微博平台投放的CTR相比行业定向人群得到显著提升。

蒙牛集团通过与每日互动合作,以联合建模、AI定向人群等方式在品牌数字化营销中实现了降本增效。未来,每日互动(个推)也将继续积极拥抱新理念、新技术,以持续创新的品牌营销服务,更好地为品牌主业务的高质量发展提供支撑,让更多垂直行业的品牌客户享受到数据智能、大模型等科技带来的红利。

继续阅读 »

当前在数字营销领域,品牌广告主越来越追求品效协同。针对品牌主更注重营销转化的切实需求,数据智能上市企业每日互动(股票代码:300766)发挥自身数据和技术能力优势,为垂直行业的品牌客户提供专业的数字化营销解决方案,颇受行业认可。就在不久前举办的“2024中国信通院ICT深度观察报告会”上,每日互动(个推)申报的「大数据赋能蒙牛乳制品品牌精准投放」案例获评中国信通院2023年“数据+”行业应用优秀案例。


图:中国信通院2023“数据+”行业应用优秀案例证书
蒙牛集团是乳制品行业的龙头企业,旗下产品线丰富、消费群体广泛、营销活动多元。在与蒙牛集团的合作中,每日互动基于海量的数据沉淀,通过大数据联合计算平台,在安全、合规的前提下,为蒙牛集团提供消费者洞察、广告投放定向、联合建模等服务,助力蒙牛集团营销策略优化,更精准、有效地链接不同消费人群。

比如,每日互动和蒙牛旗下品牌特仑苏合作,通过联合建模实现「人群精准定向」。每日互动对蒙牛特仑苏的一方样本数据进行了深度清洗和特征挖掘,精细化完成特征筛选和模型调参等,为蒙牛特仑苏构建了专属的智能预测模型,来实现高潜力消费者的精准定向。效果数据表明,特仑苏使用智能预测模型定向找出的投放人群表现出明显的转化优势,整体投放的ROI相比行业定向人群和历史均值均实现了大幅提升,有效加强了品牌种草和下单转化的效果。

随着以大模型为代表的新技术给各行业带来新红利,每日互动也正结合大模型的能力与理念,持续创新品牌营销服务模式,推出了全新的大模型营销应用——“AITA智选人群”工具。蒙牛在11月的营销项目中,首次使用了该工具,无需样本数据和复杂的建模过程,只需要简单的自然语言对话,就能快速实现目标人群定向,不仅使品牌营销过程更安全、更智能,也取得了突出的营销效果。投放测试数据显示,蒙牛通过AITA定向生成的投放人群表现优异,在微博平台投放的CTR相比行业定向人群得到显著提升。

蒙牛集团通过与每日互动合作,以联合建模、AI定向人群等方式在品牌数字化营销中实现了降本增效。未来,每日互动(个推)也将继续积极拥抱新理念、新技术,以持续创新的品牌营销服务,更好地为品牌主业务的高质量发展提供支撑,让更多垂直行业的品牌客户享受到数据智能、大模型等科技带来的红利。

收起阅读 »

uniapp 项目开发经验总结

uniapp 教程

前言

总结 uniapp 多端项目三个月开发维护的经验,遇到并解决了什么困难,收获了什么。多端指:

  • Andorid: H5、微信 H5、App
  • iOS: H5、微信 H5、App

issues

Android 拍照闪退问题

部分机型中调用 uniapp 提供的 Api 进行拍照会偶现应用闪退,具体分析在 应用闪退分析与 uniapp 安卓原生插件开发,已经确认是系统回收资源,结束了应用进程导致的(调用系统相机进行拍摄,应用处于后台)。

解决方案:

  • 如果项目有需要保活的相关功能,可以做保活来提升应用优先级,避免拍照时进入后台被系统回收资源。
  • 如果项目没有保活的业务,做了保活很大概率无法通过应用商店的审核,此时可以考虑通过自定义应用相机来实现。

目前 uniapp 插件市场有很多自定义相机可以使用,或者可以基于 uniapp 官方的 live-pusher 组件和 nvue 结合自定义自己的相机;这里我是写了个仿微信应用内相机的原生插件:

cameraX gif

主要使用了三个依赖库来完成拍照、编辑、裁切功能:

感兴趣的可以去 u-android-native-plugin-camera 看看,或者下载 demo 体验。

搞这个还是花了不少时间的,毕竟不是专业的 Android 开发,后续可能会就实现这个功能写一篇文章作为经验分享。

应用内通知

项目是 C 端项目,有 C 到 C 的通讯需求,也接入了各大平台的推送通知功能;系统自带的通知在应用处于前台时是不会弹出提示的,而项目要尽量跟微信对齐,需要能够在应用内弹出通知,也就是这种效果:

QQ图片20240108021430

这里我对比了微信 Android 端和 iOS 端的应用内通知,区别如下:

  • Android 端使用的是系统通知接口,与推送通知是相同的(需要通知权限)。
  • iOS 端,微信是自定义了一个通知视图,与系统推送通知不同,猜测可能是 iOS 没有提供相关接口,无法创建系统通知。

这个需求在项目中是使用自定义组件来完成的,这种解决方式有两个很明显的缺点:

  1. App 端无法使用全局组件,自定义组件必须在所有页面中引入一遍。
  2. App 端,切换页面时,即使在新的页面继续弹出通知,也会出现闪烁的情况。

我尝试使用原生插件的方式来解决,参考了 NotificationToast 的实现方式,使用 Andorid 中的 Toast 类并加上一些黑科技来模拟系统通知,如下:

notify

因为只是一个 Toast 轻提示, 所以不需要通知权限,可以做到的功能大概如下(PS:不建议使用这种方式代替系统通知,当做应用内通知使用还是可以的):

  • 自定义视图
  • 自定义触摸、点击事件,以此可以完成清除通知、跳转页面等功能
  • 应用内、应用外弹出通知

想做到但没有做到的功能:自定义弹出动画,尝试了网上的一些文章都不能很好的完成效果。

iOS 端,因为没有 iOS 的开发经验,所以不知道有什么方式可以解决,不过也就是添加一个视图,肯定也是能够实现的。

感兴趣的可以去 u-android-native-plugin-notify 看看,或者下载 demo 体验。

iOS 侧滑返回

这个问题在 入职一个月,总结下 uniapp 多端项目遇到的一些问题与解决方案 中提到过,iOS 侧滑返回是无法阻止的,如果我们在导航守卫中阻止用户离开,而用户使用了侧滑进行了强制返回,此时会出现这种情况:页面卡顿,一段时间后重新回到那个阻止离开的页面,如下图:

slide

用户通过侧滑强制返回时,不能阻止,所以需要知道当前是否是侧滑;经过查找资料和测试,侧滑返回可以靠以下特征来标识:

  1. touchstart 的触摸坐标 x 点在指定区域内(比如 0-50px 的范围)
  2. touchmove 时,触摸坐标 x 点变为负值

基于这些特征我写了个判断是否是侧滑返回的工具:

/*  
 * @Author: yuanyxh   
 * @Date: 2023-12-27 12:04:11   
 * @Last Modified by:   yuanyxh   
 * @Last Modified time: 2023-12-27 12:04:11   
 */  

import Vue from "vue";  

const ZERO = 0;  

/** 最小左侧触摸开始位置 */  
const MINIMUM_LEFT_DISTANCE = 100;  
/** 最小移动距离 */  
const MINIMUM_MOVE_DISTANCE = 50;  
/** 最小间隔事件 */  
const MINIMUM_TIME_INTERVAL = 100;  

/**  
 *   
 * @callback OnSideSlipListener  
 * @param {number} sideTime 上次侧滑事件  
*/  

/**  
 *  
 * @description 侦听 ios 侧滑事件, 通过此工具配合 beforeRouteLeave 导航守卫判断是否侧滑返回  
 */  
const sideSlip = (function sideSlipListener(global) {  
  let startX = 0,  
    endX = 0;  
  let sideTime = 0;  

  const vm = Vue.prototype;  

  /** @type {OnSideSlipListener[]} */  
  const callbackList = [];  

  function start(e) {  
    const point = e.touches?.length ? e.touches[0] : e;  

    startX = point.pageX;  
  }  

  function end(e) {  
    const point = e.changedTouched?.length ? e.changedTouched[0] : e;  

    endX = point.pageX;  

    if (  
      startX >= ZERO &&  
      startX <= MINIMUM_LEFT_DISTANCE &&  
      endX < ZERO &&  
      vm.windowWidth - Math.abs(endX) > MINIMUM_MOVE_DISTANCE  
    ) {  
      sideTime = Date.now();  

      callbackList.forEach((callback) => callback(sideTime));  
    }  
  }  

  function isWithinValidityPeriod(time) {  
    const interval = Date.now() - time;  

    return interval >= ZERO && interval < MINIMUM_TIME_INTERVAL;  
  }  

  if (vm.isIos) {  
    global.addEventListener("touchstart", start);  
    global.addEventListener("touchend", end);  
    global.addEventListener("touchcancel", end);  
  }  

  return {  
    /**  
     * @description 是否左滑  
     * @readonly  
     */  
    get isSideSlip() {  
      if (vm.isIos) {  
        return isWithinValidityPeriod(sideTime);  
      }  

      return false;  
    },  
    /**  
     * @description 判断侧滑时间是否在有效期内  
     * @param {number} time 侧滑时间  
     * @returns {boolean} 是否有效  
     */  
    isWithinValidityPeriod: isWithinValidityPeriod,  
    /**  
     *   
     * @description 侦听侧滑  
     * @param {OnSideSlipListener} callback   
     */  
    on(callback) {  
      if (typeof callback === 'function') {  
        callbackList.push(callback);  
      }  
    },  
    /**  
     * @description 取消事件侦听  
     * @param {OnSideSlipListener} callback   
     */  
    off(callback) {  
      const i = callbackList.indexOf(callback);  

      if (i === -1) return;  

      callbackList.splice(i, 1);  
    },  
  };  
})(  
  (function() {  
    let global = {};  

    if (typeof window !== "undefined") {  
      return (global = window);  
    }  

    global = {  
      addEventListener() {},  
      removeEventListener() {},  
    };  

    return global;  
  })()  
);  

export default sideSlip;

具体逻辑为:判断是否侧滑返回,touchend 时记录当前时间,在指定时间间隔内触发了诸如 onUnloadbeforeRouteLeave 等钩子时就可以认为是侧滑返回。

小米浏览器滑动冲突

不知道从什么版本开始,小米浏览器加了一个让人恼火的功能:左右滑屏手势。通过这个功能可以让左右滑动手势控制网页的前进后退,如下:

lr-slide

看起来很美好,但却与很多项目中的滑动手势冲突,比如 Element 中的滑块功能:

g2ye1-52lb3

这时候一般就会想到阻止冒泡、阻止默认事件来禁用左右滑动手势了,但恼火的点就在于没有效果,由此可以猜测这个功能的实现没有在浏览器内核中或者没有适配我们熟知的那一套 js 事件体系。

目前还没有研究出解决方案,有知道如何解决的大佬欢迎讨论。

后台定时器

因为内核的限制,网页和基于 webview 实现的框架下使用定时器时,进入后台一段时间后定时器会被挂起,回到前台时才继续执行,导致程序的运行和我们预想的有偏差。

解决方案:

  • 一种比较简单的解决方案是定时器 + 系统时间的结合,但是系统时间可以被更改,这种方式适用于不是那么重要的定时任务。
  • H5 中也可以尝试使用定时器 + performance.now() 结合来完成,performance.now() 返回的是距 时间源 过去的时间,且不会被系统时间影响。
  • 以上两种方式都不能在定时器被挂起时继续执行任务,如果需要在后台时继续执行任务,可以考虑使用 worker 的方式。

这里给出个人认为的最佳实践:

  1. 如果使用了定时器来完成持续的动画效果或视图变化,在程序进入后台时主动停止,回到前台时恢复
  2. 较简单的定时任务,可以使用定时器 + 系统时间的组合,能够在多端生效
  3. 需要保证任务的执行逻辑,H5 可以使用定时器 + performance.now(),App 可以使用 worker 或原生插件
  4. 对于需要持续执行,不影响视图的任务,使用 worker

软键盘高度的获取

App 端软键盘高度的获取还是比较简单的,H5 端就比较难受了,因为不同浏览器对于软键盘弹出时的页面模式是不能确认的,导致我们不能简单的通过 window.onresize 事件来判断软键盘是否弹出,好在目前主流浏览器都支持了 Window.visualViewport 接口,也能兼容到大部分的主流浏览器版本。

我们可以通过 Window.visualViewport 接口的 resize 事件,接收到可视窗口变化的事件,以此计算出软键盘的高度。

import Vue from "vue";  

const DELAY_TIME = 300;  

let callbackList = [];  
let unimplementedChangeList = [];  

function emit(target, payload) {  
  if (target.length) {  
    for (let i = 0; i < target.length; i++) {  
      target[i](payload);  
    }  
  }  
}  

// 页面原始高度  
let windowHeight = 0;  
function onKeyboardHeightChangeWithH5() {  
  let extraHeight = 0;  
  let hasFocus = false;  
  let originScrollY = 0;  

  let cancheHeight = 0;  

  let keyboardChangeTimer = null;  

  function exec(height) {  
    const keyboardHeight = Math.max(0, windowHeight - height);  

    if (cancheHeight === keyboardHeight) {  
      return;  
    }  

    cancheHeight = keyboardHeight;  

    const { isIos } = Vue.prototype;  

    emit(callbackList, {  
      extra: isIos ? extraHeight : 0,  
      height: keyboardHeight,  
    });  
  }  

  window.addEventListener(  
    "focus",  
    (e) => {  
      if (  
        e instanceof FocusEvent &&  
        (e.target instanceof HTMLInputElement ||  
          e.target instanceof HTMLTextAreaElement ||  
          e.target.contenteditable)  
      ) {  
        hasFocus = true;  

        originScrollY = window.scrollY;  

        setTimeout(() => {  
          hasFocus = false;  
        }, 600);  
      }  
    },  
    { capture: true }  
  );  

  window.addEventListener(  
    "blur",  
    (e) => {  
      if (  
        e instanceof FocusEvent &&  
        (e.target instanceof HTMLInputElement ||  
          e.target instanceof HTMLTextAreaElement ||  
          e.target.contenteditable)  
      ) {  
        hasFocus = true;  

        setTimeout(() => {  
          hasFocus = false;  
        }, DELAY_TIME);  
      }  
    },  
    { capture: true }  
  );  

  window.addEventListener(  
    "scroll",  
    () => {  
      if (hasFocus) {  
        extraHeight = window.scrollY - originScrollY;  
      }  
    },  
    { capture: true }  
  );  

  if (typeof window.visualViewport !== "undefined") {  
    return window.visualViewport.addEventListener("resize", (e) => {  
      if (hasFocus === false) {  
        return emit(unimplementedChangeList);  
      }  

      if (keyboardChangeTimer) {  
        clearTimeout(keyboardChangeTimer);  
      }  

      keyboardChangeTimer = setTimeout(() => {  
        exec(e.target.height);  
      }, DELAY_TIME);  
    });  
  }  

  window.addEventListener("resize", () => {  
    if (hasFocus === false) {  
      return emit(unimplementedChangeList);  
    }  

    if (keyboardChangeTimer) {  
      clearTimeout(keyboardChangeTimer);  
    }  

    keyboardChangeTimer = setTimeout(() => {  
      exec(window.innerHeight);  
    }, DELAY_TIME);  
  });  
}  

let isInited = false;  

function initKeyboardHeightChangeListener() {  
  // #ifdef APP-PLUS  
  uni.onKeyboardHeightChange((res) => {  
    emit(callbackList, { extra: 0, height: res.height });  
  });  
  // #endif  

  // #ifdef H5  

  windowHeight = Vue.prototype.windowHeight;  

  onKeyboardHeightChangeWithH5();  
  // #endif  
}  

/**  
 * @callback OnKeyboardHeightChangeCallback  
 * @param {{ extra: number; height: number }} 键盘高度(ios 中还有底部块的高度)  
 */  
/**  
 * @typedef Options  
 * @type {Object}  
 * @property {boolean} reset app 端对 uni.onKeyboardHeightChange 重置侦听  
 */  
/**  
 *  
 * @description 侦听键盘高度变化事件, 对多端做兼容处理  
 * @param {OnKeyboardHeightChangeCallback} callback 键盘高度变化事件回调  
 * @param {Options} options 额外参数  
 */  
function onKeyboardHeightChange(callback, options = {}) {  
  const { isPc } = Vue.prototype;  

  if (isPc) {  
    return console.error(  
      "PC devices do not need to listen for soft keyboard height"  
    );  
  }  

  // #ifdef APP-PLUS  
  if (options.reset) {  
    isInited = false;  

    callbackList = [];  
  }  
  // #endif  

  if (typeof callback === "function") {  
    callbackList.push(callback);  
  }  

  if (isInited === false) {  
    isInited = true;  

    initKeyboardHeightChangeListener();  
  }  
}  

/**  
 *  
 * @description 移除事件侦听函数  
 * @param {OnKeyboardHeightChangeCallback} callback 需要移除的已注册函数  
 */  
function offKeyboardHeightChange(callback) {  
  if (typeof callback === "function") {  
    const index = callbackList.indexOf(callback);  

    if (index >= 0) callbackList.splice(index, 1);  
  }  
}  

/**  
 *  
 * @description H5 页面高度变化但未执行键盘高度变化事件时触发  
 * @param {OnKeyboardHeightChangeCallback} callback 需要添加的注册函数  
 */  
function onUnimplementedChange(callback) {  
  const { isPc } = Vue.prototype;  

  if (isPc) {  
    return console.error(  
      "PC devices do not need to listen for soft keyboard height"  
    );  
  }  

  if (typeof callback === "function") {  
    unimplementedChangeList.push(callback);  
  }  

  if (isInited === false) {  
    isInited = true;  

    initKeyboardHeightChangeListener();  
  }  
}  

/**  
 *  
 * @description H5 页面高度变化但未执行键盘高度变化事件时触发  
 * @param {OnKeyboardHeightChangeCallback} callback 需要添加的注册函数  
 */  
function offUnimplementedChange(callback) {  
  if (typeof callback === "function") {  
    const index = unimplementedChangeList.indexOf(callback);  

    if (index >= 0) unimplementedChangeList.splice(index, 1);  
  }  
}  

export default {  
  onKeyboardHeightChange,  
  offKeyboardHeightChange,  
  onUnimplementedChange,  
  offUnimplementedChange,  
};

上述代码对于软键盘高度的获取做了统一的封装,对于 App 端使用 uni.onKeyboardHeightChange 接口;H5 端利用 Window.visualViewport,同时使用 window.onresize 兜底。

思路与看法

全局弹窗

用过 uniapp 的人应该都对全局这个词很敏感,因为很实用但却无法实现(除 H5 外)。接触这个项目后我也感到了局部组件带来的一些问题:

  • 书写相同的结构代码,做同一件事
  • 一个页面多个实例,状态不能统一(如一个通用的弹窗组件,在一个页面引入了多次,代码逻辑同时弹出了其中两个弹窗,弹窗重叠在一起;如果使用全局弹窗就能在内部维护,关闭第一个后再弹出第二个)
  • 。。。

这些问题看起来很小,但对项目的影响巨大,所以我也在找能够实现全局弹窗的办法。

看过插件市场的一些全局弹窗实现,基本思路都是使用一个页面作为全局弹窗的载体,需要弹出时跳转至这个页面即可。

App 端中一个页面就是一个 webview,其实我们可以通过 H5+ Api 的方式创建 webview 来作为全局弹窗的载体,会比页面的形式更好一点,查看 uni-app-picker 组件的源码也可以发现,App 端的 picker 就是依赖于 H5+ webview 接口实现的,且经过实验,创建的 webview 是可以做到全局重用的。

我们还需要考虑这个全局弹窗的可扩展、可重用性:

  • 考虑结构的可扩展性,是否可以传递组件实例在 webview 中创建
  • 考虑事件的传递
  • 考虑对外接口的封装

对项目的看法

这个项目是一个比较老的项目了,给我的感觉就是乱,很乱,没有一个统一的规范,你想写什么样的代码都可以,导致经过多个开发人员的手之后,各种风格的代码夹杂其中,给后续的开发维护造成很大的影响。

项目的初期没有规划好,在开发过程中也没有考虑封装统一,比如:

  • 输入框的封装
    • 目前关于输入框和软键盘的问题还有很多,如果初期考虑了封装的话能够统一解决,会轻松很多
  • 弹窗的封装
  • 录音功能的封装
    • 多端中录音实现都不同,H5 使用 Recorder、微信 H5 使用 wx-jssdk、App 使用 uniapp 提供的接口;直接在页面中内嵌代码,而没有考虑封装为一个模块,内部区分,对外提供一致接口
  • 全局事件的封装
    • 在 H5 中经常使用 window.onresize 之类的接口完成功能,可能一个组件就添加一个事件,应该封装为模块,内部注册一次事件,提供对外接口,事件触发时再分发出去

重构

一开始接手这个项目时就感觉项目问题很大,天天喊着需要重构重构,需要几个月的时间对项目进行大的重构,后来需求要将项目中使用 scroll-view 的列表替换为三方组件,在完成这个需求时就发现了所谓大的重构是不切实际的。

在进行这个需求时出现的比较明显的问题:

  1. 组件与 uniapp scroll-view 的属性、逻辑不兼容,替换需要格外小心,耗时是很漫长的
  2. 替换过程中,可能遗漏了部分逻辑或结构出现错误;如果列表功能复杂,可能很长时间内不会发现这个问题

从这个需求中也更能体会到 《重构2》 中作者所践行的 一步一测试 的必要性了,也更深刻的理解了作者表达的:当你有时间且你认为这段代码需要重构时才进行重构,对于那些永远不会碰到的,不要去碰它(让营地比你来时更干净)

项目进度

一个星期前是试用期结束,告知我试用期没有通过,理由是进度缓慢,我也接受了,这里不是为了吐槽,只是谈谈对于项目进度的看法。

上面也说了,这是一个 C 端的老项目,且存在着比较大的问题,对于我而言,我会让营地比我来时更干净,这是为了后续的项目维护考虑,同时尽量保证项目的逻辑在我的可接受范围内,毕竟连自己都不能接受的程序怎么保证用户的体验呢?

--end

继续阅读 »

前言

总结 uniapp 多端项目三个月开发维护的经验,遇到并解决了什么困难,收获了什么。多端指:

  • Andorid: H5、微信 H5、App
  • iOS: H5、微信 H5、App

issues

Android 拍照闪退问题

部分机型中调用 uniapp 提供的 Api 进行拍照会偶现应用闪退,具体分析在 应用闪退分析与 uniapp 安卓原生插件开发,已经确认是系统回收资源,结束了应用进程导致的(调用系统相机进行拍摄,应用处于后台)。

解决方案:

  • 如果项目有需要保活的相关功能,可以做保活来提升应用优先级,避免拍照时进入后台被系统回收资源。
  • 如果项目没有保活的业务,做了保活很大概率无法通过应用商店的审核,此时可以考虑通过自定义应用相机来实现。

目前 uniapp 插件市场有很多自定义相机可以使用,或者可以基于 uniapp 官方的 live-pusher 组件和 nvue 结合自定义自己的相机;这里我是写了个仿微信应用内相机的原生插件:

cameraX gif

主要使用了三个依赖库来完成拍照、编辑、裁切功能:

感兴趣的可以去 u-android-native-plugin-camera 看看,或者下载 demo 体验。

搞这个还是花了不少时间的,毕竟不是专业的 Android 开发,后续可能会就实现这个功能写一篇文章作为经验分享。

应用内通知

项目是 C 端项目,有 C 到 C 的通讯需求,也接入了各大平台的推送通知功能;系统自带的通知在应用处于前台时是不会弹出提示的,而项目要尽量跟微信对齐,需要能够在应用内弹出通知,也就是这种效果:

QQ图片20240108021430

这里我对比了微信 Android 端和 iOS 端的应用内通知,区别如下:

  • Android 端使用的是系统通知接口,与推送通知是相同的(需要通知权限)。
  • iOS 端,微信是自定义了一个通知视图,与系统推送通知不同,猜测可能是 iOS 没有提供相关接口,无法创建系统通知。

这个需求在项目中是使用自定义组件来完成的,这种解决方式有两个很明显的缺点:

  1. App 端无法使用全局组件,自定义组件必须在所有页面中引入一遍。
  2. App 端,切换页面时,即使在新的页面继续弹出通知,也会出现闪烁的情况。

我尝试使用原生插件的方式来解决,参考了 NotificationToast 的实现方式,使用 Andorid 中的 Toast 类并加上一些黑科技来模拟系统通知,如下:

notify

因为只是一个 Toast 轻提示, 所以不需要通知权限,可以做到的功能大概如下(PS:不建议使用这种方式代替系统通知,当做应用内通知使用还是可以的):

  • 自定义视图
  • 自定义触摸、点击事件,以此可以完成清除通知、跳转页面等功能
  • 应用内、应用外弹出通知

想做到但没有做到的功能:自定义弹出动画,尝试了网上的一些文章都不能很好的完成效果。

iOS 端,因为没有 iOS 的开发经验,所以不知道有什么方式可以解决,不过也就是添加一个视图,肯定也是能够实现的。

感兴趣的可以去 u-android-native-plugin-notify 看看,或者下载 demo 体验。

iOS 侧滑返回

这个问题在 入职一个月,总结下 uniapp 多端项目遇到的一些问题与解决方案 中提到过,iOS 侧滑返回是无法阻止的,如果我们在导航守卫中阻止用户离开,而用户使用了侧滑进行了强制返回,此时会出现这种情况:页面卡顿,一段时间后重新回到那个阻止离开的页面,如下图:

slide

用户通过侧滑强制返回时,不能阻止,所以需要知道当前是否是侧滑;经过查找资料和测试,侧滑返回可以靠以下特征来标识:

  1. touchstart 的触摸坐标 x 点在指定区域内(比如 0-50px 的范围)
  2. touchmove 时,触摸坐标 x 点变为负值

基于这些特征我写了个判断是否是侧滑返回的工具:

/*  
 * @Author: yuanyxh   
 * @Date: 2023-12-27 12:04:11   
 * @Last Modified by:   yuanyxh   
 * @Last Modified time: 2023-12-27 12:04:11   
 */  

import Vue from "vue";  

const ZERO = 0;  

/** 最小左侧触摸开始位置 */  
const MINIMUM_LEFT_DISTANCE = 100;  
/** 最小移动距离 */  
const MINIMUM_MOVE_DISTANCE = 50;  
/** 最小间隔事件 */  
const MINIMUM_TIME_INTERVAL = 100;  

/**  
 *   
 * @callback OnSideSlipListener  
 * @param {number} sideTime 上次侧滑事件  
*/  

/**  
 *  
 * @description 侦听 ios 侧滑事件, 通过此工具配合 beforeRouteLeave 导航守卫判断是否侧滑返回  
 */  
const sideSlip = (function sideSlipListener(global) {  
  let startX = 0,  
    endX = 0;  
  let sideTime = 0;  

  const vm = Vue.prototype;  

  /** @type {OnSideSlipListener[]} */  
  const callbackList = [];  

  function start(e) {  
    const point = e.touches?.length ? e.touches[0] : e;  

    startX = point.pageX;  
  }  

  function end(e) {  
    const point = e.changedTouched?.length ? e.changedTouched[0] : e;  

    endX = point.pageX;  

    if (  
      startX >= ZERO &&  
      startX <= MINIMUM_LEFT_DISTANCE &&  
      endX < ZERO &&  
      vm.windowWidth - Math.abs(endX) > MINIMUM_MOVE_DISTANCE  
    ) {  
      sideTime = Date.now();  

      callbackList.forEach((callback) => callback(sideTime));  
    }  
  }  

  function isWithinValidityPeriod(time) {  
    const interval = Date.now() - time;  

    return interval >= ZERO && interval < MINIMUM_TIME_INTERVAL;  
  }  

  if (vm.isIos) {  
    global.addEventListener("touchstart", start);  
    global.addEventListener("touchend", end);  
    global.addEventListener("touchcancel", end);  
  }  

  return {  
    /**  
     * @description 是否左滑  
     * @readonly  
     */  
    get isSideSlip() {  
      if (vm.isIos) {  
        return isWithinValidityPeriod(sideTime);  
      }  

      return false;  
    },  
    /**  
     * @description 判断侧滑时间是否在有效期内  
     * @param {number} time 侧滑时间  
     * @returns {boolean} 是否有效  
     */  
    isWithinValidityPeriod: isWithinValidityPeriod,  
    /**  
     *   
     * @description 侦听侧滑  
     * @param {OnSideSlipListener} callback   
     */  
    on(callback) {  
      if (typeof callback === 'function') {  
        callbackList.push(callback);  
      }  
    },  
    /**  
     * @description 取消事件侦听  
     * @param {OnSideSlipListener} callback   
     */  
    off(callback) {  
      const i = callbackList.indexOf(callback);  

      if (i === -1) return;  

      callbackList.splice(i, 1);  
    },  
  };  
})(  
  (function() {  
    let global = {};  

    if (typeof window !== "undefined") {  
      return (global = window);  
    }  

    global = {  
      addEventListener() {},  
      removeEventListener() {},  
    };  

    return global;  
  })()  
);  

export default sideSlip;

具体逻辑为:判断是否侧滑返回,touchend 时记录当前时间,在指定时间间隔内触发了诸如 onUnloadbeforeRouteLeave 等钩子时就可以认为是侧滑返回。

小米浏览器滑动冲突

不知道从什么版本开始,小米浏览器加了一个让人恼火的功能:左右滑屏手势。通过这个功能可以让左右滑动手势控制网页的前进后退,如下:

lr-slide

看起来很美好,但却与很多项目中的滑动手势冲突,比如 Element 中的滑块功能:

g2ye1-52lb3

这时候一般就会想到阻止冒泡、阻止默认事件来禁用左右滑动手势了,但恼火的点就在于没有效果,由此可以猜测这个功能的实现没有在浏览器内核中或者没有适配我们熟知的那一套 js 事件体系。

目前还没有研究出解决方案,有知道如何解决的大佬欢迎讨论。

后台定时器

因为内核的限制,网页和基于 webview 实现的框架下使用定时器时,进入后台一段时间后定时器会被挂起,回到前台时才继续执行,导致程序的运行和我们预想的有偏差。

解决方案:

  • 一种比较简单的解决方案是定时器 + 系统时间的结合,但是系统时间可以被更改,这种方式适用于不是那么重要的定时任务。
  • H5 中也可以尝试使用定时器 + performance.now() 结合来完成,performance.now() 返回的是距 时间源 过去的时间,且不会被系统时间影响。
  • 以上两种方式都不能在定时器被挂起时继续执行任务,如果需要在后台时继续执行任务,可以考虑使用 worker 的方式。

这里给出个人认为的最佳实践:

  1. 如果使用了定时器来完成持续的动画效果或视图变化,在程序进入后台时主动停止,回到前台时恢复
  2. 较简单的定时任务,可以使用定时器 + 系统时间的组合,能够在多端生效
  3. 需要保证任务的执行逻辑,H5 可以使用定时器 + performance.now(),App 可以使用 worker 或原生插件
  4. 对于需要持续执行,不影响视图的任务,使用 worker

软键盘高度的获取

App 端软键盘高度的获取还是比较简单的,H5 端就比较难受了,因为不同浏览器对于软键盘弹出时的页面模式是不能确认的,导致我们不能简单的通过 window.onresize 事件来判断软键盘是否弹出,好在目前主流浏览器都支持了 Window.visualViewport 接口,也能兼容到大部分的主流浏览器版本。

我们可以通过 Window.visualViewport 接口的 resize 事件,接收到可视窗口变化的事件,以此计算出软键盘的高度。

import Vue from "vue";  

const DELAY_TIME = 300;  

let callbackList = [];  
let unimplementedChangeList = [];  

function emit(target, payload) {  
  if (target.length) {  
    for (let i = 0; i < target.length; i++) {  
      target[i](payload);  
    }  
  }  
}  

// 页面原始高度  
let windowHeight = 0;  
function onKeyboardHeightChangeWithH5() {  
  let extraHeight = 0;  
  let hasFocus = false;  
  let originScrollY = 0;  

  let cancheHeight = 0;  

  let keyboardChangeTimer = null;  

  function exec(height) {  
    const keyboardHeight = Math.max(0, windowHeight - height);  

    if (cancheHeight === keyboardHeight) {  
      return;  
    }  

    cancheHeight = keyboardHeight;  

    const { isIos } = Vue.prototype;  

    emit(callbackList, {  
      extra: isIos ? extraHeight : 0,  
      height: keyboardHeight,  
    });  
  }  

  window.addEventListener(  
    "focus",  
    (e) => {  
      if (  
        e instanceof FocusEvent &&  
        (e.target instanceof HTMLInputElement ||  
          e.target instanceof HTMLTextAreaElement ||  
          e.target.contenteditable)  
      ) {  
        hasFocus = true;  

        originScrollY = window.scrollY;  

        setTimeout(() => {  
          hasFocus = false;  
        }, 600);  
      }  
    },  
    { capture: true }  
  );  

  window.addEventListener(  
    "blur",  
    (e) => {  
      if (  
        e instanceof FocusEvent &&  
        (e.target instanceof HTMLInputElement ||  
          e.target instanceof HTMLTextAreaElement ||  
          e.target.contenteditable)  
      ) {  
        hasFocus = true;  

        setTimeout(() => {  
          hasFocus = false;  
        }, DELAY_TIME);  
      }  
    },  
    { capture: true }  
  );  

  window.addEventListener(  
    "scroll",  
    () => {  
      if (hasFocus) {  
        extraHeight = window.scrollY - originScrollY;  
      }  
    },  
    { capture: true }  
  );  

  if (typeof window.visualViewport !== "undefined") {  
    return window.visualViewport.addEventListener("resize", (e) => {  
      if (hasFocus === false) {  
        return emit(unimplementedChangeList);  
      }  

      if (keyboardChangeTimer) {  
        clearTimeout(keyboardChangeTimer);  
      }  

      keyboardChangeTimer = setTimeout(() => {  
        exec(e.target.height);  
      }, DELAY_TIME);  
    });  
  }  

  window.addEventListener("resize", () => {  
    if (hasFocus === false) {  
      return emit(unimplementedChangeList);  
    }  

    if (keyboardChangeTimer) {  
      clearTimeout(keyboardChangeTimer);  
    }  

    keyboardChangeTimer = setTimeout(() => {  
      exec(window.innerHeight);  
    }, DELAY_TIME);  
  });  
}  

let isInited = false;  

function initKeyboardHeightChangeListener() {  
  // #ifdef APP-PLUS  
  uni.onKeyboardHeightChange((res) => {  
    emit(callbackList, { extra: 0, height: res.height });  
  });  
  // #endif  

  // #ifdef H5  

  windowHeight = Vue.prototype.windowHeight;  

  onKeyboardHeightChangeWithH5();  
  // #endif  
}  

/**  
 * @callback OnKeyboardHeightChangeCallback  
 * @param {{ extra: number; height: number }} 键盘高度(ios 中还有底部块的高度)  
 */  
/**  
 * @typedef Options  
 * @type {Object}  
 * @property {boolean} reset app 端对 uni.onKeyboardHeightChange 重置侦听  
 */  
/**  
 *  
 * @description 侦听键盘高度变化事件, 对多端做兼容处理  
 * @param {OnKeyboardHeightChangeCallback} callback 键盘高度变化事件回调  
 * @param {Options} options 额外参数  
 */  
function onKeyboardHeightChange(callback, options = {}) {  
  const { isPc } = Vue.prototype;  

  if (isPc) {  
    return console.error(  
      "PC devices do not need to listen for soft keyboard height"  
    );  
  }  

  // #ifdef APP-PLUS  
  if (options.reset) {  
    isInited = false;  

    callbackList = [];  
  }  
  // #endif  

  if (typeof callback === "function") {  
    callbackList.push(callback);  
  }  

  if (isInited === false) {  
    isInited = true;  

    initKeyboardHeightChangeListener();  
  }  
}  

/**  
 *  
 * @description 移除事件侦听函数  
 * @param {OnKeyboardHeightChangeCallback} callback 需要移除的已注册函数  
 */  
function offKeyboardHeightChange(callback) {  
  if (typeof callback === "function") {  
    const index = callbackList.indexOf(callback);  

    if (index >= 0) callbackList.splice(index, 1);  
  }  
}  

/**  
 *  
 * @description H5 页面高度变化但未执行键盘高度变化事件时触发  
 * @param {OnKeyboardHeightChangeCallback} callback 需要添加的注册函数  
 */  
function onUnimplementedChange(callback) {  
  const { isPc } = Vue.prototype;  

  if (isPc) {  
    return console.error(  
      "PC devices do not need to listen for soft keyboard height"  
    );  
  }  

  if (typeof callback === "function") {  
    unimplementedChangeList.push(callback);  
  }  

  if (isInited === false) {  
    isInited = true;  

    initKeyboardHeightChangeListener();  
  }  
}  

/**  
 *  
 * @description H5 页面高度变化但未执行键盘高度变化事件时触发  
 * @param {OnKeyboardHeightChangeCallback} callback 需要添加的注册函数  
 */  
function offUnimplementedChange(callback) {  
  if (typeof callback === "function") {  
    const index = unimplementedChangeList.indexOf(callback);  

    if (index >= 0) unimplementedChangeList.splice(index, 1);  
  }  
}  

export default {  
  onKeyboardHeightChange,  
  offKeyboardHeightChange,  
  onUnimplementedChange,  
  offUnimplementedChange,  
};

上述代码对于软键盘高度的获取做了统一的封装,对于 App 端使用 uni.onKeyboardHeightChange 接口;H5 端利用 Window.visualViewport,同时使用 window.onresize 兜底。

思路与看法

全局弹窗

用过 uniapp 的人应该都对全局这个词很敏感,因为很实用但却无法实现(除 H5 外)。接触这个项目后我也感到了局部组件带来的一些问题:

  • 书写相同的结构代码,做同一件事
  • 一个页面多个实例,状态不能统一(如一个通用的弹窗组件,在一个页面引入了多次,代码逻辑同时弹出了其中两个弹窗,弹窗重叠在一起;如果使用全局弹窗就能在内部维护,关闭第一个后再弹出第二个)
  • 。。。

这些问题看起来很小,但对项目的影响巨大,所以我也在找能够实现全局弹窗的办法。

看过插件市场的一些全局弹窗实现,基本思路都是使用一个页面作为全局弹窗的载体,需要弹出时跳转至这个页面即可。

App 端中一个页面就是一个 webview,其实我们可以通过 H5+ Api 的方式创建 webview 来作为全局弹窗的载体,会比页面的形式更好一点,查看 uni-app-picker 组件的源码也可以发现,App 端的 picker 就是依赖于 H5+ webview 接口实现的,且经过实验,创建的 webview 是可以做到全局重用的。

我们还需要考虑这个全局弹窗的可扩展、可重用性:

  • 考虑结构的可扩展性,是否可以传递组件实例在 webview 中创建
  • 考虑事件的传递
  • 考虑对外接口的封装

对项目的看法

这个项目是一个比较老的项目了,给我的感觉就是乱,很乱,没有一个统一的规范,你想写什么样的代码都可以,导致经过多个开发人员的手之后,各种风格的代码夹杂其中,给后续的开发维护造成很大的影响。

项目的初期没有规划好,在开发过程中也没有考虑封装统一,比如:

  • 输入框的封装
    • 目前关于输入框和软键盘的问题还有很多,如果初期考虑了封装的话能够统一解决,会轻松很多
  • 弹窗的封装
  • 录音功能的封装
    • 多端中录音实现都不同,H5 使用 Recorder、微信 H5 使用 wx-jssdk、App 使用 uniapp 提供的接口;直接在页面中内嵌代码,而没有考虑封装为一个模块,内部区分,对外提供一致接口
  • 全局事件的封装
    • 在 H5 中经常使用 window.onresize 之类的接口完成功能,可能一个组件就添加一个事件,应该封装为模块,内部注册一次事件,提供对外接口,事件触发时再分发出去

重构

一开始接手这个项目时就感觉项目问题很大,天天喊着需要重构重构,需要几个月的时间对项目进行大的重构,后来需求要将项目中使用 scroll-view 的列表替换为三方组件,在完成这个需求时就发现了所谓大的重构是不切实际的。

在进行这个需求时出现的比较明显的问题:

  1. 组件与 uniapp scroll-view 的属性、逻辑不兼容,替换需要格外小心,耗时是很漫长的
  2. 替换过程中,可能遗漏了部分逻辑或结构出现错误;如果列表功能复杂,可能很长时间内不会发现这个问题

从这个需求中也更能体会到 《重构2》 中作者所践行的 一步一测试 的必要性了,也更深刻的理解了作者表达的:当你有时间且你认为这段代码需要重构时才进行重构,对于那些永远不会碰到的,不要去碰它(让营地比你来时更干净)

项目进度

一个星期前是试用期结束,告知我试用期没有通过,理由是进度缓慢,我也接受了,这里不是为了吐槽,只是谈谈对于项目进度的看法。

上面也说了,这是一个 C 端的老项目,且存在着比较大的问题,对于我而言,我会让营地比我来时更干净,这是为了后续的项目维护考虑,同时尽量保证项目的逻辑在我的可接受范围内,毕竟连自己都不能接受的程序怎么保证用户的体验呢?

--end

收起阅读 »

Uni-app 学习、开发交流微信群,欢迎大家加入

大家好,为了方便交流沟通uniapp的开发交流,新建了一个微信群,欢迎大家加入沟通

大家好,为了方便交流沟通uniapp的开发交流,新建了一个微信群,欢迎大家加入沟通

uni-app+vue3+pinia+Nvue+vite4多端直播App实例|uniapp仿微信直播商城

前段时间有给大家分享一个Electron27+react18跨平台mateOs桌面系统后台框架,这次带来全新研发的uniapp+vue3+Nvue+pinia跨端仿抖音直播商城项目uni-welive。

https://ask.dcloud.net.cn/article/40867

编译到H5+小程序+App端效果

img

uniapp-vue3-welive全新基于uniapp+vue3+pinia+nvue+uv-ui等技术跨多端仿制抖音/微信直播商城App项目。支持全屏上下滑动切换小视频,支持编译到兼容H5+小程序+App端。

img

使用技术

  • 开发工具:HbuilderX 3.98
  • 技术框架:Uniapp+Vue3+Vite4+Nvue+Pinia
  • UI组件库:uv-ui+vk-uview
  • 弹框组件:uaPopup(uniapp封装多端弹框组件)
  • 自定义组件:uaNavbar+uaTabbar组件
  • 本地缓存:pinia-plugin-unistorage
  • 编译支持:H5+小程序+APP端

img

img

项目结构目录

img

![img](https://image-static.segmentfault.com/420/845/4208453913-6593668e44dd0_fix7320

img

img

img

入口main.js

/**  
 * 入口配置  
 */  

import { createSSRApp } from 'vue'  
import App from './App'  

// 引入Pinia状态管理  
import Pinia from '@/store'  

// 引入vk-uview-ui组件库  
import VKuview from '@/uni_modules/vk-uview-ui'  

export function createApp() {  
    const app = createSSRApp(App)  
    app.use(Pinia)  
    app.use(VKuview)  
    return {  
        app,  
        Pinia  
    }  
}

App.vue模板

<script>  
    export default {  
        globalData: {  
            // 全局设置状态栏和导航栏高度  
            statusBarH: 0,  
            customBarH: 0,  
            screenWidth: 0,  
            screenHeight: 0,  
            menuBar: null  
        },  
        onLaunch: function() {  
            console.log('App Launch')  

            // 隐藏系统tabbar  
            uni.hideTabBar()  
            this.appInit()  
        },  
        onShow: function() {  
            console.log('App Show')  
        },  
        onHide: function() {  
            console.log('App Hide')  
        },  
        onPageNotFound: function() {  
            console.log('Page Not Found', e.path)  
            uni.redirectTo({  
                url: '/pages/index/index'  
            })  
        },  
        methods: {  
            appInit: function() {  
                uni.getSystemInfo({  
                    success: (e) => {  
                        // 获取手机状态栏信息  
                        let statusBar = e.statusBarHeight || 0  
                        let customBar  
                        let menuBar  

                        // #ifndef MP  
                        customBar = statusBar + (e.osName === 'android' ? 50 : 45)  
                        // #endif  

                        // #ifdef MP-WEIXIN  
                        // 获取微信小程序胶囊按钮信息  
                        let menu = wx.getMenuButtonBoundingClientRect()  
                        // 导航栏高度 = 胶囊下距离 + 胶囊上距离 - 状态栏高度  
                        customBar = menu.bottom + menu.top - statusBar  
                        menuBar = menu  
                        // #endif  

                        // // #ifdef MP-ALIPAY  
                        customBar = statusBar + e.titleBarHeight  
                        // #endif  

                        // 兼容nvue写法(H5/小程序/APP/APP-Nvue)  
                        this.globalData.statusBarH = statusBar  
                        this.globalData.customBarH = customBar  
                        this.globalData.screenWidth = e.screenWidth  
                        this.globalData.screenHeight = e.screenHeight  
                        this.globalData.menuBar = menuBar  
                    }  
                })  
            }  
        }  
    }  
</script>  

<style>  
    /* #ifndef APP-NVUE */  
    @import 'static/fonts/iconfont.css';  
    /* #endif */  
    .nvueicon {font-family: nvueicon;}  
</style>  
<style lang="scss">  
    // 引入vk-uview-ui基础样式  
    @import 'uni_modules/vk-uview-ui/index.scss';  

    @import 'styles/reset.scss';  
    @import 'styles/layout.scss';  
</style>

img

img

img

img

img

img

img

img

img

img

img

img

uniapp+vue3直播模板

img

<ua-layout>  
    <view class="ua__swipervideo flex1">  
        <swiper  
            class="ua__swipervideo-wrap flex1"  
            :current="currentLive"  
            vertical  
            @change="handleChange"  
        >  
            <swiper-item v-for="(item, index) in liveList" :key="index">  
                <video  
                    class="ua__swipervideo-player flex1"  
                    :id="'uplayer' + index"  
                    :src="item.src"  
                    :controls="false"  
                    :loop="true"  
                    :autoplay="index == currentLive"  
                    :show-center-play-btn="false"  
                    object-fit="contain"  
                    :style="{'width': `${winWidth}px`, 'height': `${winHeight}px`}"  
                >  
                </video>  

                <!-- 浮层模块 -->  
                <swiper class="ulive__swiperscreen flex1" :current="1">  
                    <swiper-item>  
                        第一屏  
                    </swiper-item>  
                    <swiper-item>  
                        <!-- 顶部区域 -->  
                        <view class="ulive__headlayer" :style="{'top': menuBarT+'px'}">  
                            <!-- logo+关注 -->  
                            <view class="ulive__hd-liveinfo flexbox flex-row flex-alignc">  
                                <view class="ulive__hd-avatar ulive__mask flex-alignc">  
                                    <image class="logo" :src="item.logo" mode="widthFix" />  
                                    <view class="flex1 flexbox flex-col ml-10">  
                                        <text class="name">{{item.name}}</text>  
                                        <text class="zan">{{item.likeNum}}本场点赞</text>  
                                    </view>  
                                    <view class="btn flexbox flex-row flex-alignc" :class="{'active': item.isFollow}" @click="handleFollow(index)"><text class="btntext" :class="{'active': item.isFollow}">{{item.isFollow ? '已关注' : '关注'}}</text></view>  
                                </view>  
                                <view class="ulive__hd-onlineuser flex1">  
                                    <uv-icon name="close" color="#fff" @click="handleLiveQuit" />  
                                </view>  
                            </view>  
                            <view class="ulive__hd-livewrap flexbox flex-row">  
                                <view class="ulive__hd-livewrap__left flex1 flexbox flex-col">  
                                    <view class="ulive__hd-livewrap__tags flexbox flex-row">  
                                        <view class="ulive__roundwrap ulive__mask">  
                                            <uv-icon name="shopping-cart" color="#ffdd1a" /><text class="ulive__roundtext">服饰鞋包榜第1名</text>  
                                        </view>  
                                        <view class="ulive__roundwrap ulive__mask ml-10">  
                                            <uv-icon name="level" color="#ffdd1a" /><text class="ulive__roundtext">小时榜</text>  
                                        </view>  
                                    </view>  
                                    <!-- 红包+福袋倒计时 -->  
                                    <view class="ulive__hd-livewrap__redpacket flexbox flex-row">  
                                        <view class="ulive__redpacket-item ulive__mask" @click="handleOpenRedpacket(1)">  
                                            <image class="ulive__redpacket-image" src="/static/icon-fudai.png" mode="widthFix" /><text class="ulive__redpacket-time">04:49</text>  
                                        </view>  
                                        <view class="ulive__redpacket-item ulive__mask" @click="handleOpenRedpacket(2)">  
                                            <image class="ulive__redpacket-image" src="/static/icon-hb.png" mode="widthFix" /><text class="ulive__redpacket-time">04:49</text>  
                                        </view>  
                                        <view class="ulive__redpacket-item ulive__mask center">  
                                            <image class="ulive__redpacket-image" src="/static/icon-rotate.png" mode="widthFix" /><text class="ulive__redpacket-time">04:49</text>  
                                        </view>  
                                    </view>  
                                </view>  
                                <view class="ulive__hd-livewrap__right flexbox flex-col">  
                                    <view class="ulive__roundwrap ulive__mask mr-20">  
                                        <uv-icon name="kefu-ermai" color="#fff" /><text class="ulive__roundtext ml-5">后台</text>  
                                    </view>  
                                </view>  
                            </view>  
                        </view>  

                        <!-- 底部区域 -->  
                        <view class="ulive__footlayer">  
                            <!-- 商品提示层 -->  
                            <view class="ulive__ft-livewrap-placeholder animated fadeIn">  
                                <view class="ulive__ft-livewrap-hotbuy flexbox flex-row">  
                                    <image class="gimg" :src="item.poster" mode="aspectFill" />  
                                    <view class="ginfo flex1">  
                                        <view class="flexbox flex-row"><text class="user c-ffdd1a">Andy</text><text class="c-fff">等{{item.saleNum}}人在购买</text></view>  
                                        <text class="gdesc clamp1">{{item.desc}}</text>  
                                    </view>  
                                    <view class="btn"><text class="btntext">去购买</text></view>  
                                </view>  
                            </view>  
                            <!-- 加入直播间/送礼物提示 -->  
                            <view class="ulive__ft-livewrap-animateview flexbox flex-col">  
                                <view class="ulive__ft-livewrap-animatejoin ulive__ft-livewrap-placeholder">  
                                    <view v-if="joinRoomData" class="ulive__ft-livewrap-joinroom"><text class="ulive__ft-livewrap-joinroom__text">欢迎{{joinRoomData}}加入了直播间</text></view>  
                                </view>  

                                <!-- 送礼物 -->  
                                <view class="ulive__ft-livewrap-animategift ulive__ft-livewrap-placeholder">  
                                    <view v-if="!isEmpty(sendGiftData)" class="ulive__ft-livewrap-activegift flexbox flex-row flex-alignc">  
                                        <image class="avatar" :src="sendGiftData.avatar" />  
                                        <view class="info flex1"><text class="name">{{sendGiftData.user}}</text><text class="desc">送出</text></view>  
                                        <image class="gift" :src="sendGiftData.pic" />  
                                    </view>  
                                </view>  
                            </view>  
                            <!-- 聊天浮层+商品讲解 -->  
                            <view class="ulive__ft-livewrap-mixinview flexbox flex-row">  
                                <!-- 聊天消息 -->  
                                <view class="ulive__ft-livewrap-chats flex1">  
                                    <scroll-view class="ulive__ft-livewrap-chats__scrollview flex1" scroll-y show-scrollbar="false" :scroll-into-view="scrollToView" :lower-threshold="5" @scroll="handleMsgScroll" @scrolltolower="handleMsgScrollLower">  
                                        <block v-for="(msgitem, msgidx) in item.message" :key="msgidx">  
                                            <view v-if="msgitem.type == 'notice'" class="notice" :id="`msg-${msgitem.id}`"><view class="item"><text class="noticetext">{{msgitem.content}}</text></view></view>  
                                            <view v-else-if="msgitem.type == 'gift'" class="gift" :id="`msg-${msgitem.id}`">  
                                                <view class="item">  
                                                    <text class="giftuser">{{msgitem.user}}</text>  
                                                    <text class="gifttext">送出了{{msgitem.content}}</text>  
                                                    <image class="giftimg" :src="msgitem.img" mode="widthFix" />  
                                                    <text class="giftnum">x{{msgitem.num}}</text>  
                                                </view>  
                                            </view>  
                                            <view v-else class="msg" :id="`msg-${msgitem.id}`">  
                                                <view class="item">  
                                                    <text v-if="msgitem.tag" class="tag">{{msgitem.tag}}</text>  
                                                    <text class="user">{{msgitem.user}}</text>  
                                                    <text class="text" :style="[fixTextStyle]">{{msgitem.isbuy ? '正在购买' : msgitem.content}}</text>  
                                                    <text v-if="msgitem.isbuy" class="tag tag-buy">去购买</text>  
                                                </view>  
                                            </view>  
                                        </block>  
                                    </scroll-view>  
                                    <view v-if="!isEmpty(msgUnread)" class="ulive__ft-livewrap-chats__unread" @click="handleMsgIsRead"><text class="c-eb4868 fs-24">{{msgUnread.length}}条新消息</text></view>  
                                </view>  
                                <!-- 商品讲解 -->  
                                <view v-if="isVisibleGoodsTalk" class="ulive__ft-livewrap-activegoods animated fadeInRight" id="goodsTalkID">  
                                    <view class="ulive__ft-livewrap-activegoods__hotsale flexbox flex-row">  
                                        <image class="fimg" src="/static/icon-hot.png" mode="widthFix" /><text class="c-fff fs-32">热卖 x{{item.saleNum}}</text>  
                                    </view>  
                                    <swiper class="ulive__ft-livewrap-activegoods__swiper">  
                                        <swiper-item>  
                                            <view class="ulive__ft-livewrap-activegoods__card">  
                                                <view class="gwrap" @click="toGoodsDetail">  
                                                    <image class="gimg" :src="item.poster" mode="aspectFill" />  
                                                    <view class="waves"><text class="c-fff fs-24">讲解中</text></view>  
                                                    <view class="close" @click.stop="isVisibleGoodsTalk=false"><uv-icon name="close-circle-fill" color="rgba(0, 0, 0, .3)" size="14" /></view>  
                                                </view>  
                                                <view class="ginfo flexbox flex-col">  
                                                    <text class="clamp1 fs-24">{{item.desc}}</text>  
                                                    <text class="clamp1 fs-24 c-eb4868">7天无理由退货</text>  
                                                </view>  
                                                <view class="btn flexbox flex-row"><text class="flex1 c-fff fs-28">¥79.00</text><text class="qiang">抢</text></view>  
                                            </view>  
                                        </swiper-item>  
                                    </swiper>  
                                </view>  
                            </view>  
                            <!-- 工具栏 -->  
                            <view class="ulive__ft-livewrap-toolbar flexbox flex-row">  
                                <view class="editorwrap flex1 flexbox flex-row flex-alignc">  
                                    <view class="flex1" @click="handleOpenChatbox"><text class="editorwrap-text">说点什么...</text></view>  
                                </view>  
                                <view class="btnwrap flexbox flex-row">  
                                    <view class="btn flexbox" @click="handleOpenMenus"><uv-icon name="grid" color="#3c9cff" size="22" /></view>  
                                    <view class="btn flexbox" @click="handleOpenGoods(item)"><uv-icon name="shopping-cart-fill" color="#ffaa00" size="24" /></view>  
                                    <view class="btn flexbox" @click="handleOpenGifts"><uv-icon name="gift" color="#ff0ad3" size="22" /></view>  
                                    <view class="btn flexbox"><uv-icon name="more-dot-fill" color="#efe9ff" size="18" /></view>  
                                </view>  
                            </view>  
                        </view>  
                    </swiper-item>  
                </swiper>  
            </swiper-item>  
        </swiper>  
    </view>  
</ua-layout>

OK,综上就是uni-app+vue3+nvue开发多端直播商城实例的一些分享。

作者:xiaoyan2015
链接: https://segmentfault.com/a/1190000044519351
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

继续阅读 »

前段时间有给大家分享一个Electron27+react18跨平台mateOs桌面系统后台框架,这次带来全新研发的uniapp+vue3+Nvue+pinia跨端仿抖音直播商城项目uni-welive。

https://ask.dcloud.net.cn/article/40867

编译到H5+小程序+App端效果

img

uniapp-vue3-welive全新基于uniapp+vue3+pinia+nvue+uv-ui等技术跨多端仿制抖音/微信直播商城App项目。支持全屏上下滑动切换小视频,支持编译到兼容H5+小程序+App端。

img

使用技术

  • 开发工具:HbuilderX 3.98
  • 技术框架:Uniapp+Vue3+Vite4+Nvue+Pinia
  • UI组件库:uv-ui+vk-uview
  • 弹框组件:uaPopup(uniapp封装多端弹框组件)
  • 自定义组件:uaNavbar+uaTabbar组件
  • 本地缓存:pinia-plugin-unistorage
  • 编译支持:H5+小程序+APP端

img

img

项目结构目录

img

![img](https://image-static.segmentfault.com/420/845/4208453913-6593668e44dd0_fix7320

img

img

img

入口main.js

/**  
 * 入口配置  
 */  

import { createSSRApp } from 'vue'  
import App from './App'  

// 引入Pinia状态管理  
import Pinia from '@/store'  

// 引入vk-uview-ui组件库  
import VKuview from '@/uni_modules/vk-uview-ui'  

export function createApp() {  
    const app = createSSRApp(App)  
    app.use(Pinia)  
    app.use(VKuview)  
    return {  
        app,  
        Pinia  
    }  
}

App.vue模板

<script>  
    export default {  
        globalData: {  
            // 全局设置状态栏和导航栏高度  
            statusBarH: 0,  
            customBarH: 0,  
            screenWidth: 0,  
            screenHeight: 0,  
            menuBar: null  
        },  
        onLaunch: function() {  
            console.log('App Launch')  

            // 隐藏系统tabbar  
            uni.hideTabBar()  
            this.appInit()  
        },  
        onShow: function() {  
            console.log('App Show')  
        },  
        onHide: function() {  
            console.log('App Hide')  
        },  
        onPageNotFound: function() {  
            console.log('Page Not Found', e.path)  
            uni.redirectTo({  
                url: '/pages/index/index'  
            })  
        },  
        methods: {  
            appInit: function() {  
                uni.getSystemInfo({  
                    success: (e) => {  
                        // 获取手机状态栏信息  
                        let statusBar = e.statusBarHeight || 0  
                        let customBar  
                        let menuBar  

                        // #ifndef MP  
                        customBar = statusBar + (e.osName === 'android' ? 50 : 45)  
                        // #endif  

                        // #ifdef MP-WEIXIN  
                        // 获取微信小程序胶囊按钮信息  
                        let menu = wx.getMenuButtonBoundingClientRect()  
                        // 导航栏高度 = 胶囊下距离 + 胶囊上距离 - 状态栏高度  
                        customBar = menu.bottom + menu.top - statusBar  
                        menuBar = menu  
                        // #endif  

                        // // #ifdef MP-ALIPAY  
                        customBar = statusBar + e.titleBarHeight  
                        // #endif  

                        // 兼容nvue写法(H5/小程序/APP/APP-Nvue)  
                        this.globalData.statusBarH = statusBar  
                        this.globalData.customBarH = customBar  
                        this.globalData.screenWidth = e.screenWidth  
                        this.globalData.screenHeight = e.screenHeight  
                        this.globalData.menuBar = menuBar  
                    }  
                })  
            }  
        }  
    }  
</script>  

<style>  
    /* #ifndef APP-NVUE */  
    @import 'static/fonts/iconfont.css';  
    /* #endif */  
    .nvueicon {font-family: nvueicon;}  
</style>  
<style lang="scss">  
    // 引入vk-uview-ui基础样式  
    @import 'uni_modules/vk-uview-ui/index.scss';  

    @import 'styles/reset.scss';  
    @import 'styles/layout.scss';  
</style>

img

img

img

img

img

img

img

img

img

img

img

img

uniapp+vue3直播模板

img

<ua-layout>  
    <view class="ua__swipervideo flex1">  
        <swiper  
            class="ua__swipervideo-wrap flex1"  
            :current="currentLive"  
            vertical  
            @change="handleChange"  
        >  
            <swiper-item v-for="(item, index) in liveList" :key="index">  
                <video  
                    class="ua__swipervideo-player flex1"  
                    :id="'uplayer' + index"  
                    :src="item.src"  
                    :controls="false"  
                    :loop="true"  
                    :autoplay="index == currentLive"  
                    :show-center-play-btn="false"  
                    object-fit="contain"  
                    :style="{'width': `${winWidth}px`, 'height': `${winHeight}px`}"  
                >  
                </video>  

                <!-- 浮层模块 -->  
                <swiper class="ulive__swiperscreen flex1" :current="1">  
                    <swiper-item>  
                        第一屏  
                    </swiper-item>  
                    <swiper-item>  
                        <!-- 顶部区域 -->  
                        <view class="ulive__headlayer" :style="{'top': menuBarT+'px'}">  
                            <!-- logo+关注 -->  
                            <view class="ulive__hd-liveinfo flexbox flex-row flex-alignc">  
                                <view class="ulive__hd-avatar ulive__mask flex-alignc">  
                                    <image class="logo" :src="item.logo" mode="widthFix" />  
                                    <view class="flex1 flexbox flex-col ml-10">  
                                        <text class="name">{{item.name}}</text>  
                                        <text class="zan">{{item.likeNum}}本场点赞</text>  
                                    </view>  
                                    <view class="btn flexbox flex-row flex-alignc" :class="{'active': item.isFollow}" @click="handleFollow(index)"><text class="btntext" :class="{'active': item.isFollow}">{{item.isFollow ? '已关注' : '关注'}}</text></view>  
                                </view>  
                                <view class="ulive__hd-onlineuser flex1">  
                                    <uv-icon name="close" color="#fff" @click="handleLiveQuit" />  
                                </view>  
                            </view>  
                            <view class="ulive__hd-livewrap flexbox flex-row">  
                                <view class="ulive__hd-livewrap__left flex1 flexbox flex-col">  
                                    <view class="ulive__hd-livewrap__tags flexbox flex-row">  
                                        <view class="ulive__roundwrap ulive__mask">  
                                            <uv-icon name="shopping-cart" color="#ffdd1a" /><text class="ulive__roundtext">服饰鞋包榜第1名</text>  
                                        </view>  
                                        <view class="ulive__roundwrap ulive__mask ml-10">  
                                            <uv-icon name="level" color="#ffdd1a" /><text class="ulive__roundtext">小时榜</text>  
                                        </view>  
                                    </view>  
                                    <!-- 红包+福袋倒计时 -->  
                                    <view class="ulive__hd-livewrap__redpacket flexbox flex-row">  
                                        <view class="ulive__redpacket-item ulive__mask" @click="handleOpenRedpacket(1)">  
                                            <image class="ulive__redpacket-image" src="/static/icon-fudai.png" mode="widthFix" /><text class="ulive__redpacket-time">04:49</text>  
                                        </view>  
                                        <view class="ulive__redpacket-item ulive__mask" @click="handleOpenRedpacket(2)">  
                                            <image class="ulive__redpacket-image" src="/static/icon-hb.png" mode="widthFix" /><text class="ulive__redpacket-time">04:49</text>  
                                        </view>  
                                        <view class="ulive__redpacket-item ulive__mask center">  
                                            <image class="ulive__redpacket-image" src="/static/icon-rotate.png" mode="widthFix" /><text class="ulive__redpacket-time">04:49</text>  
                                        </view>  
                                    </view>  
                                </view>  
                                <view class="ulive__hd-livewrap__right flexbox flex-col">  
                                    <view class="ulive__roundwrap ulive__mask mr-20">  
                                        <uv-icon name="kefu-ermai" color="#fff" /><text class="ulive__roundtext ml-5">后台</text>  
                                    </view>  
                                </view>  
                            </view>  
                        </view>  

                        <!-- 底部区域 -->  
                        <view class="ulive__footlayer">  
                            <!-- 商品提示层 -->  
                            <view class="ulive__ft-livewrap-placeholder animated fadeIn">  
                                <view class="ulive__ft-livewrap-hotbuy flexbox flex-row">  
                                    <image class="gimg" :src="item.poster" mode="aspectFill" />  
                                    <view class="ginfo flex1">  
                                        <view class="flexbox flex-row"><text class="user c-ffdd1a">Andy</text><text class="c-fff">等{{item.saleNum}}人在购买</text></view>  
                                        <text class="gdesc clamp1">{{item.desc}}</text>  
                                    </view>  
                                    <view class="btn"><text class="btntext">去购买</text></view>  
                                </view>  
                            </view>  
                            <!-- 加入直播间/送礼物提示 -->  
                            <view class="ulive__ft-livewrap-animateview flexbox flex-col">  
                                <view class="ulive__ft-livewrap-animatejoin ulive__ft-livewrap-placeholder">  
                                    <view v-if="joinRoomData" class="ulive__ft-livewrap-joinroom"><text class="ulive__ft-livewrap-joinroom__text">欢迎{{joinRoomData}}加入了直播间</text></view>  
                                </view>  

                                <!-- 送礼物 -->  
                                <view class="ulive__ft-livewrap-animategift ulive__ft-livewrap-placeholder">  
                                    <view v-if="!isEmpty(sendGiftData)" class="ulive__ft-livewrap-activegift flexbox flex-row flex-alignc">  
                                        <image class="avatar" :src="sendGiftData.avatar" />  
                                        <view class="info flex1"><text class="name">{{sendGiftData.user}}</text><text class="desc">送出</text></view>  
                                        <image class="gift" :src="sendGiftData.pic" />  
                                    </view>  
                                </view>  
                            </view>  
                            <!-- 聊天浮层+商品讲解 -->  
                            <view class="ulive__ft-livewrap-mixinview flexbox flex-row">  
                                <!-- 聊天消息 -->  
                                <view class="ulive__ft-livewrap-chats flex1">  
                                    <scroll-view class="ulive__ft-livewrap-chats__scrollview flex1" scroll-y show-scrollbar="false" :scroll-into-view="scrollToView" :lower-threshold="5" @scroll="handleMsgScroll" @scrolltolower="handleMsgScrollLower">  
                                        <block v-for="(msgitem, msgidx) in item.message" :key="msgidx">  
                                            <view v-if="msgitem.type == 'notice'" class="notice" :id="`msg-${msgitem.id}`"><view class="item"><text class="noticetext">{{msgitem.content}}</text></view></view>  
                                            <view v-else-if="msgitem.type == 'gift'" class="gift" :id="`msg-${msgitem.id}`">  
                                                <view class="item">  
                                                    <text class="giftuser">{{msgitem.user}}</text>  
                                                    <text class="gifttext">送出了{{msgitem.content}}</text>  
                                                    <image class="giftimg" :src="msgitem.img" mode="widthFix" />  
                                                    <text class="giftnum">x{{msgitem.num}}</text>  
                                                </view>  
                                            </view>  
                                            <view v-else class="msg" :id="`msg-${msgitem.id}`">  
                                                <view class="item">  
                                                    <text v-if="msgitem.tag" class="tag">{{msgitem.tag}}</text>  
                                                    <text class="user">{{msgitem.user}}</text>  
                                                    <text class="text" :style="[fixTextStyle]">{{msgitem.isbuy ? '正在购买' : msgitem.content}}</text>  
                                                    <text v-if="msgitem.isbuy" class="tag tag-buy">去购买</text>  
                                                </view>  
                                            </view>  
                                        </block>  
                                    </scroll-view>  
                                    <view v-if="!isEmpty(msgUnread)" class="ulive__ft-livewrap-chats__unread" @click="handleMsgIsRead"><text class="c-eb4868 fs-24">{{msgUnread.length}}条新消息</text></view>  
                                </view>  
                                <!-- 商品讲解 -->  
                                <view v-if="isVisibleGoodsTalk" class="ulive__ft-livewrap-activegoods animated fadeInRight" id="goodsTalkID">  
                                    <view class="ulive__ft-livewrap-activegoods__hotsale flexbox flex-row">  
                                        <image class="fimg" src="/static/icon-hot.png" mode="widthFix" /><text class="c-fff fs-32">热卖 x{{item.saleNum}}</text>  
                                    </view>  
                                    <swiper class="ulive__ft-livewrap-activegoods__swiper">  
                                        <swiper-item>  
                                            <view class="ulive__ft-livewrap-activegoods__card">  
                                                <view class="gwrap" @click="toGoodsDetail">  
                                                    <image class="gimg" :src="item.poster" mode="aspectFill" />  
                                                    <view class="waves"><text class="c-fff fs-24">讲解中</text></view>  
                                                    <view class="close" @click.stop="isVisibleGoodsTalk=false"><uv-icon name="close-circle-fill" color="rgba(0, 0, 0, .3)" size="14" /></view>  
                                                </view>  
                                                <view class="ginfo flexbox flex-col">  
                                                    <text class="clamp1 fs-24">{{item.desc}}</text>  
                                                    <text class="clamp1 fs-24 c-eb4868">7天无理由退货</text>  
                                                </view>  
                                                <view class="btn flexbox flex-row"><text class="flex1 c-fff fs-28">¥79.00</text><text class="qiang">抢</text></view>  
                                            </view>  
                                        </swiper-item>  
                                    </swiper>  
                                </view>  
                            </view>  
                            <!-- 工具栏 -->  
                            <view class="ulive__ft-livewrap-toolbar flexbox flex-row">  
                                <view class="editorwrap flex1 flexbox flex-row flex-alignc">  
                                    <view class="flex1" @click="handleOpenChatbox"><text class="editorwrap-text">说点什么...</text></view>  
                                </view>  
                                <view class="btnwrap flexbox flex-row">  
                                    <view class="btn flexbox" @click="handleOpenMenus"><uv-icon name="grid" color="#3c9cff" size="22" /></view>  
                                    <view class="btn flexbox" @click="handleOpenGoods(item)"><uv-icon name="shopping-cart-fill" color="#ffaa00" size="24" /></view>  
                                    <view class="btn flexbox" @click="handleOpenGifts"><uv-icon name="gift" color="#ff0ad3" size="22" /></view>  
                                    <view class="btn flexbox"><uv-icon name="more-dot-fill" color="#efe9ff" size="18" /></view>  
                                </view>  
                            </view>  
                        </view>  
                    </swiper-item>  
                </swiper>  
            </swiper-item>  
        </swiper>  
    </view>  
</ua-layout>

OK,综上就是uni-app+vue3+nvue开发多端直播商城实例的一些分享。

作者:xiaoyan2015
链接: https://segmentfault.com/a/1190000044519351
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

FreePlus-二次美化

FreePlus-二次美化
这是一款适用于论坛、软件库、圈子、社群等用途,功能也是十分强大


继续阅读 »

FreePlus-二次美化
这是一款适用于论坛、软件库、圈子、社群等用途,功能也是十分强大


收起阅读 »

FreePlus-二次美化版

论坛

这是一款APP程序源码,适用于论坛、软件库、圈子、社群等用途,功能也是十分强大


继续阅读 »

这是一款APP程序源码,适用于论坛、软件库、圈子、社群等用途,功能也是十分强大


收起阅读 »

FreePlus-二次美化版

这是一款APP程序源码,适用于论坛、软件库、圈子、社群等用途,功能也是十分强大
全新UI
板块需增加的出格文章
主页去掉了多余的功能

  • 邮箱联系方式uffyfctvu@163.com
继续阅读 »

这是一款APP程序源码,适用于论坛、软件库、圈子、社群等用途,功能也是十分强大
全新UI
板块需增加的出格文章
主页去掉了多余的功能

  • 邮箱联系方式uffyfctvu@163.com
收起阅读 »

ios厂商推送的P12证书过期后,重新生成的推送证书上传失败

推送 iOS证书 unipush

ios厂商推送的P12证书过期后,重新生成的推送证书上传失败
附图:

  1. 上传失败
  2. 控制台信息
  3. 网络响应信息
继续阅读 »

ios厂商推送的P12证书过期后,重新生成的推送证书上传失败
附图:

  1. 上传失败
  2. 控制台信息
  3. 网络响应信息
收起阅读 »

一定要用真的ChatGPT

ChatGPT

各种AI乌烟瘴气,大部分打着ChatGPT的幌子,全都是假的,验证很简单,你只需要提问:‘你是ChatGPT吗’

各种AI乌烟瘴气,大部分打着ChatGPT的幌子,全都是假的,验证很简单,你只需要提问:‘你是ChatGPT吗’

User Friendly大会 | 每日互动刘宇分享AIGC时代的数智营销变革

近日,第十九届暨2023年User Friendly国际用户体验大会在深圳召开。本次大会以“开智启能,体验无界”为主题,邀请了各行业领袖精英齐聚,分享前沿新观点,碰撞体验新思潮。每日互动高级副总裁刘宇作为业内资深专家,受邀出席大会并在数据智能论坛发表主题演讲,分享AIGC时代的数智营销变革及发展机遇。


▲每日互动高级副总裁刘宇现场发表主题演讲

                                                                                                                               1  

                                                                                                           AIGC带来全新用户体验变革  

以AIGC和大模型为代表的新一代技术浪潮,给很多行业和领域都带了巨大变革。以用户的人机交互体验举例,历经点击时代、触摸时代,现在我们已经进入到了全新的自然语言时代。我们能够和Siri等AI应用以及chatGPT等大模型产品直接进行自然语言对话,开启智能、高效的人机协作模式。

同样,AIGC和大模型也给营销行业带来了深刻的体验变革。AI创意生成、数字人直播、AI智能外呼……这些创新应用使得整个营销行业的生产效率有了质的飞跃。刘宇表示,“虽然AI生成的创意素材可能是快餐式的,内容的质量目前还比不上专业的设计师和创意人员,但是AI生成的创意内容是真正意义上的‘数字原生’内容,这个意义是巨大的”。

刘宇讲到,“以前,我们要探究某个广告创意为什么点击率很高,需要投入非常复杂的工作,首先需要对该广告创意进行标签化,通过OCR技术、识图软件等进行模式识别,分析该广告创意在相应的维度上具备哪些属性,根据这些属性构建出一个数据空间,然后我们再从这个数据空间中挖掘规律,分析原因,根据这些规律指导后续广告创意的优化和制作。而现在AI生成的创意内容由于其本身是‘数字原生’的,因此我们能够非常容易地使用算法、模型等数据化的方式来分析其点击率高低背后的原因,再加上AIGC带来的指数级效率提升,都有力驱动着数智营销行业的深层次变革”。

                                                                                                                                2  

                                                                                                                AI不止GC,更有TA  

“AIGC和大模型给数智营销行业带来的红利,不仅体现在广告创意的生成方面”。活动现场,刘宇表示,“每日互动最新推出的一款叫做“AITA智选人群”的大模型应用产品,让AI不仅能够GC,还能TA”。

“TA即Targeting Audience”。刘宇介绍,在品牌营销领域,人群定向一直是备受关注的课题。如何从茫茫人海中定向找到目标消费者,把广告投给对的TA?针对品牌主的切实需求,每日互动发挥自身能力优势,从2019年起就面向品牌主提供第三方DMP数智营销服务。依托海量的三方数据积累,每日互动帮助品牌广告主进行人群洞察和广告投放的前验,通过标签、联合建模等方式,帮助品牌主智能圈选和定向目标TA。

但是,在具体的实施过程中,这两种人群定向的方式都存在一定门槛。“标签定向”方式,需要由品牌营销人员根据自己对消费者的认知以及对每日互动数据的理解,来进行标签的勾选、组合,对于营销人员的专业性有一定要求,而且还可能涉及到较多与咨询团队、数据分析团队的人工沟通。

另一种“联合建模”的方式则需要品牌提供一方种子数据作为前提,每日互动通过机器学习为其进行分析并扩量。然而在新品推广等一些特定的营销场景下,品牌主通常缺少种子用户,也并不清楚自己的目标消费人群。

“AITA就能够很好地解决这一问题”。刘宇介绍,在AITA这款产品中,每日互动使用了大模型去赋予它对自然语义的理解,并使用了数据编织技术去实现数据空间的语义化,让市场营销等领域从业者的奇思妙想,都能够通过与大模型对话的方式,转化到数据空间的数据实体、数据标签,进而对应到目标人群,实现了“无种子洞察”和“对话式提人”等创新功能。

刘宇介绍,“与AI大模型的使用体验类似,用户通过自然语言对话就可以快速上手,通过AITA进入到数据智能的世界。AITA同时也融入了更为严谨的人群定义和预估功能,让客户在使用AITA时,既有轻松活泼的对话过程,也有严谨高效的定量部分,能够更为丝滑地完成市场分析、洞察和媒体对接等过程,也让营销领域能够更早地享受到AI大模型带来的红利”。

刘宇讲到,“因此,不仅仅是在产品和体验本身,用户体验设计从更大的社会层面和人文层面来讲,都有重大意义”。如今,数据成为重要的生产要素,AI极大提升了社会生产力,而用户体验更加关注人本身,三者的有机融合将更加有效地促进经济社会发展,帮助企业赢得商业成功。每日互动也将持续探索AI大模型和数据智能在行业里的应用落地,用数据和技术的力量推动社会持续进步。

继续阅读 »

近日,第十九届暨2023年User Friendly国际用户体验大会在深圳召开。本次大会以“开智启能,体验无界”为主题,邀请了各行业领袖精英齐聚,分享前沿新观点,碰撞体验新思潮。每日互动高级副总裁刘宇作为业内资深专家,受邀出席大会并在数据智能论坛发表主题演讲,分享AIGC时代的数智营销变革及发展机遇。


▲每日互动高级副总裁刘宇现场发表主题演讲

                                                                                                                               1  

                                                                                                           AIGC带来全新用户体验变革  

以AIGC和大模型为代表的新一代技术浪潮,给很多行业和领域都带了巨大变革。以用户的人机交互体验举例,历经点击时代、触摸时代,现在我们已经进入到了全新的自然语言时代。我们能够和Siri等AI应用以及chatGPT等大模型产品直接进行自然语言对话,开启智能、高效的人机协作模式。

同样,AIGC和大模型也给营销行业带来了深刻的体验变革。AI创意生成、数字人直播、AI智能外呼……这些创新应用使得整个营销行业的生产效率有了质的飞跃。刘宇表示,“虽然AI生成的创意素材可能是快餐式的,内容的质量目前还比不上专业的设计师和创意人员,但是AI生成的创意内容是真正意义上的‘数字原生’内容,这个意义是巨大的”。

刘宇讲到,“以前,我们要探究某个广告创意为什么点击率很高,需要投入非常复杂的工作,首先需要对该广告创意进行标签化,通过OCR技术、识图软件等进行模式识别,分析该广告创意在相应的维度上具备哪些属性,根据这些属性构建出一个数据空间,然后我们再从这个数据空间中挖掘规律,分析原因,根据这些规律指导后续广告创意的优化和制作。而现在AI生成的创意内容由于其本身是‘数字原生’的,因此我们能够非常容易地使用算法、模型等数据化的方式来分析其点击率高低背后的原因,再加上AIGC带来的指数级效率提升,都有力驱动着数智营销行业的深层次变革”。

                                                                                                                                2  

                                                                                                                AI不止GC,更有TA  

“AIGC和大模型给数智营销行业带来的红利,不仅体现在广告创意的生成方面”。活动现场,刘宇表示,“每日互动最新推出的一款叫做“AITA智选人群”的大模型应用产品,让AI不仅能够GC,还能TA”。

“TA即Targeting Audience”。刘宇介绍,在品牌营销领域,人群定向一直是备受关注的课题。如何从茫茫人海中定向找到目标消费者,把广告投给对的TA?针对品牌主的切实需求,每日互动发挥自身能力优势,从2019年起就面向品牌主提供第三方DMP数智营销服务。依托海量的三方数据积累,每日互动帮助品牌广告主进行人群洞察和广告投放的前验,通过标签、联合建模等方式,帮助品牌主智能圈选和定向目标TA。

但是,在具体的实施过程中,这两种人群定向的方式都存在一定门槛。“标签定向”方式,需要由品牌营销人员根据自己对消费者的认知以及对每日互动数据的理解,来进行标签的勾选、组合,对于营销人员的专业性有一定要求,而且还可能涉及到较多与咨询团队、数据分析团队的人工沟通。

另一种“联合建模”的方式则需要品牌提供一方种子数据作为前提,每日互动通过机器学习为其进行分析并扩量。然而在新品推广等一些特定的营销场景下,品牌主通常缺少种子用户,也并不清楚自己的目标消费人群。

“AITA就能够很好地解决这一问题”。刘宇介绍,在AITA这款产品中,每日互动使用了大模型去赋予它对自然语义的理解,并使用了数据编织技术去实现数据空间的语义化,让市场营销等领域从业者的奇思妙想,都能够通过与大模型对话的方式,转化到数据空间的数据实体、数据标签,进而对应到目标人群,实现了“无种子洞察”和“对话式提人”等创新功能。

刘宇介绍,“与AI大模型的使用体验类似,用户通过自然语言对话就可以快速上手,通过AITA进入到数据智能的世界。AITA同时也融入了更为严谨的人群定义和预估功能,让客户在使用AITA时,既有轻松活泼的对话过程,也有严谨高效的定量部分,能够更为丝滑地完成市场分析、洞察和媒体对接等过程,也让营销领域能够更早地享受到AI大模型带来的红利”。

刘宇讲到,“因此,不仅仅是在产品和体验本身,用户体验设计从更大的社会层面和人文层面来讲,都有重大意义”。如今,数据成为重要的生产要素,AI极大提升了社会生产力,而用户体验更加关注人本身,三者的有机融合将更加有效地促进经济社会发展,帮助企业赢得商业成功。每日互动也将持续探索AI大模型和数据智能在行业里的应用落地,用数据和技术的力量推动社会持续进步。

收起阅读 »

安卓系统获取海拔高度

function getAltitudeFromAndroidSystem() {  
    let LocationManager = plus.android.importClass("android.location.LocationManager");  
    let Context = plus.android.importClass("android.content.Context");  
    let main = plus.android.runtimeMainActivity();  
    let locationManager = main.getSystemService(Context.LOCATION_SERVICE);  
    let location = locationManager.getLastKnownLocation(LocationManager  
        .GPS_PROVIDER);  
    let altitude = plus.android.invoke(location, "getAltitude");  
    let hasAltitude = plus.android.invoke(location, "hasAltitude");  
    plus.android.deleteObject(LocationManager);  
    plus.android.deleteObject(Context);  
    if(hasAltitude) {  
        return altitude;  
    } else {  
        return 0;  
    }  
}
继续阅读 »
function getAltitudeFromAndroidSystem() {  
    let LocationManager = plus.android.importClass("android.location.LocationManager");  
    let Context = plus.android.importClass("android.content.Context");  
    let main = plus.android.runtimeMainActivity();  
    let locationManager = main.getSystemService(Context.LOCATION_SERVICE);  
    let location = locationManager.getLastKnownLocation(LocationManager  
        .GPS_PROVIDER);  
    let altitude = plus.android.invoke(location, "getAltitude");  
    let hasAltitude = plus.android.invoke(location, "hasAltitude");  
    plus.android.deleteObject(LocationManager);  
    plus.android.deleteObject(Context);  
    if(hasAltitude) {  
        return altitude;  
    } else {  
        return 0;  
    }  
}
收起阅读 »