Write the Code. Change the World.

分类目录
5月 30

百度小程序第一步

获取 access_token https://smartprogram.baidu.com/docs/develop/serverapi/power_exp/

获取 session_key https://smartprogram.baidu.com/docs/develop/api/open/getSessionKey/

登录流程 https://smartprogram.baidu.com/docs/develop/function/login_process/

单登录 https://smartprogram.baidu.com/docs/develop/function/login/

获取手机号码 https://smartprogram.baidu.com/docs/develop/function/login_union/

获取用户信息 https://smartprogram.baidu.com/docs/develop/function/getuserinfo/

5月 27

用 blender 这样的 3d 建模软件,不用快捷键是不行的。有时候录一些教程,显示操作的快捷键就很有必要。screencast keys 这个插件刚好能满足需求。

下载地址: https://github.com/nutti/Screencast-Keys/releases

效果图如下图所示

这里我按了一组 shift + a 快捷键,弹出了添加面板。同时在右下角这里,显示出了我按了 shift + a 快捷键。按 n 快捷键弹出选项面板,选项面板中就包含有 screencast keys 插件的设置信息。你可以设置字号、字体、位置等。

安装方法

  1. 打开下载地址,下载最新版本的 zip 文件。
  2. 打开 blender,依次点击菜单的 编辑->偏好设置->插件,在弹出面板中点击安装,选择之前下载的 zip 文件。
  3. 然后在该面包找到该插件打钩够。

上边三步就 ok 了。你通过 n 快捷键,就可以从弹出的选项中找到该插件的设置面板。

5月 24

babylonjs + blender 弄一个最简单的 3D效果出来。从来没有做过,那就试试吧。

最终效果: https://www.xiangrong.pro/web3d

步骤

  1. 使用 blender 画一个小椅子。
  2. 给立方体上色。
  3. 导出 glb 文件。
  4. 创建 babylongjs 项目。
  5. 加载使用 glb 文件。

先用 blender 画出小椅子并导出 glb 文件

花了好几个小时,看着 B 站的视频,用 blender 画了一个椅子。如下图所示。

椅背本来是粉色的。为了让 logo 可以显示出来,换成了这个色。

创建项目加载渲染 glb

这里,我们使用 pnpm 创建 vite 项目

pnpm crate vite

# 输入项目名 web3d
# 选择框架,这里选择最高效,最悠久的框架 Vanilla
# 选择语言, 这些选择 JavaScript
# 最后,回车。项目就建立好了

创建好项目后,我习惯性安装依赖包,并建立 git 版本控制,初始化项目

cd web3d
pnpm install
git init -b main
git add .
git commit -m 'initialize web'

我喜欢干干净净的干事情,删掉不需要的文件,并创建 src 目录,将 main.jsstyle.css 文件移到该目录中。

rm -rf counter.js
rm -rf javascript.svg
mkdir src
mv main.js ./src
mv style.css ./src

修改 index.html

  <body>
    <canvas id="canvas"></canvas>
    <script type="module" src="/src/main.js"></script>
  </body>

修改 style.css

# 删除 style.css 中所有代码,加入下边代码
* {
  margin: 0;
  padding: 0;
}

canvas {
  width: 100vw;
  height: 100vh;
  position: fixed;
  left: 0;
  top: 0;
}

修改 main.js

# 删除 main.js 中所有代码,加入下边代码
import './style.css'

到此,已经很干净了。运行起来,再提交版本控制。

pnpm dev

git add .
git commit -m '删除不必要的代码'

这个时候,只有一个白白的网页。下边正式开学写 babylonjs 代码。

先安装 babylonjs 和 babylonjs-loaders

pnpm add babylonjs
pnpm add babylonjs-loaders

main.js 中修改为以下代码(之前只有一行代码)

import * as BABYLON from 'babylonjs'
import 'babylonjs-loaders'
import './style.css'

const canvas = document.getElementById('canvas');

// 创建引擎, 第二个参数为抗锯齿
const engine = new BABYLON.Engine(canvas, true)

// 创建场景
const scene = new BABYLON.Scene(engine)

/**
 * 创建相机
 * @name 名字
 * @alpha 相机的 alpha 值,水平旋转角度
 * @beta  相机的 bata 值,垂直旋转角度
 * @radius 相机的半径
 * @target 相机的目标点
 * @scene 相机所在的场景
 */
const camera = new BABYLON.ArcRotateCamera('camera', 0, 0, 0, BABYLON.Vector3.Zero(), scene)

// 设置相机的位置
camera.setPosition(new BABYLON.Vector3(0, 5, 10))

// 将相机的目标指向场景的原点
camera.setTarget(BABYLON.Vector3.Zero());

// 把相机附加到画布上, true 阻止默认操作
camera.attachControl(canvas, true)

// 顶部球形光
const hemisphericLight1 = new BABYLON.HemisphericLight(
    'light',  //光源的名称
    new BABYLON.Vector3(1, 1, 0),  //光源的方向
    scene //光源所在的场景
)

// 底部球形光
const hemisphericLight2 = new BABYLON.HemisphericLight(
    'light',  //光源的名称
    new BABYLON.Vector3(1, -1, 0),  //光源的方向
    scene //光源所在的场景
)
hemisphericLight2.intensity = 0.72

// 隐藏 loading 画面
BABYLON.SceneLoader.ShowLoadingScreen = false

BABYLON.SceneLoader.Append('models/', 'yizi.glb', scene, (glb) => {
    console.log(glb)
}, null)

// 渲染场景
engine.runRenderLoop(() => {
    scene.render()
})

// 监听尺寸更改
window.addEventListener('resize', function() {
    engine.resize()
})

到此,代码是完成了。椅子文件还没放到项目中。我们在 public 目录中创建 models文件夹,并将 blender 导出的 yizi.glb 文件放到 models文件夹下。

于是,效果如下图所示。

不知道啥原因,用镜像做的部分,显示不出来。使用官方的沙盒也是这样。

提交代码。

git add .
git commit -m '安装 babylonjs 以及 loader,并编写创建加载代码'

相关

glb 文件下载

链接: https://pan.baidu.com/s/1DnkSQaa1z_JLAYCsXeXmqw 提取码: u8p4

椅子教程

https://www.bilibili.com/video/BV1kX4y1m7G5?p=1

最后

椅子缺失的问题找到了。导出的时候将网格下的应用修改器打钩就可以了。之前镜像就是修改器之一。

代码 https://github.com/vinistudy/web3d

5月 18

微信、百度小程序都支持常量 env,来自适应有圆弧屏幕的手机。头条小程序却不可以(抖音开发者官网也没有,只手搜适配,自适应等关键词没找到,看了部分文档也没找到)。头条自己的官方示范的小程序自己都没适配。这就是他们的态度。好吧,别人不做,自己总得做做好,要不体验肯定会差的。

常规处理

像苹果这种棱角是圆弧的手机,底部顶部都是要做自适应处理,才可以有更好的体验。常规操作是加一个 margin 或 一个 padding 或补一个 div。而这些都会用到这个常量: env(safe-area-inset-bottom)

比如可以这样做:

    .safe-bottom {
        margin-bottom: env(safe-area-inset-bottom);
    }

    # 或
    .safe-bottom {
        box-sizing: box-border;
        padding-bottom: env(safe-area-inset-bottom);
    }

    # 或多加一个 div
    .safe-bottom {
        height: env(safe-area-inset-bottom);
    }

甚至可以和 calc 组合使用, 比如:

.footer {
    width: 100%;
    height: calc(88rpx + env(safe-area-inset-bottom));
    box-sizing: box-border;
    padding-bottom: env(safe-area-inset-bottom);
}

可是,头条小程序中 env(safe-area-inset-bottom) 这玩意不支持。

头条小程序自适应

既然一条路走不通,那就选其他的路。找找思路。比如只要能知道圆弧区域的高度不就可以了吗。在系统信息中,会携带这些信息(叫安全区域信息)。拿到了高度,一样可以 margin,一样可以 padding 哈。

那梳理下流程。

  1. 在 main.js 中,拿到安全区域信息,并将底部的高度数据定义到 globalProperties 中。之所以这样做,是因为需要自适应场景的页面有很多个,不想每个页面都再重复去获取这些信息。
  2. 在有需要的地方使用这个高度。
# main.js
import App from './App'
import { createPinia } from 'pinia'
import {
    createSSRApp
} from 'vue'

export function createApp() {
    const app = createSSRApp(App)

    app.use(createPinia())

    // 头条自适应
    // #ifdef MP-TOUTIAO
    const systemInfo = uni.getSystemInfoSync();
    if (systemInfo && systemInfo.safeAreaInsets) {
        app.config.globalProperties.$safeTop = systemInfo.safeAreaInsets.top;
        app.config.globalProperties.$safeBottom = systemInfo.safeAreaInsets.bottom;
    }
    // #endif

    return {
        app
    }
}

# 使用的地方
<view  hover-class="hover" :style="{'margin-bottom': $safeBottom + 'px'}">
</view>

好吧,就这样完事了。

字节小程序官方实例

滑到底部了,不仅不自适应,底部还有一部分被挡住了。

其实,并不赞成官方实例的搞法。底部导航栏非原生,会对整个项目带来影响。页面滚动不丝滑是其中之一,滚动条会滚动到底部不好看。要不就是使用 scroolview 隐藏了滚动条。反正体验还是不怎么好。

5月 17

初步了解了下 web3d 相关的信息。 three.js 或 babylon.js 要学一个(它们会封装 WebGL 和 WebGPU 的Api)。然后 blender 也要学一下。如果做虚拟城市这块的建模,building tools 这个插件很多人在用。那就用吧。

下载地址

https://ranjian0.github.io/building_tools/

下载好后,打开 blender ,点击菜单栏的编辑,再点插件,再添加,把这个插件添加进去。

5月 17

随着社会的发展,技术的发展,市场的发展,平面展示已不能满足需要了。商品可以 3D 化,道路建筑可以 3D 化。而 web 又是最通用的一种承载形式,web3D 的的必要性就突出来了。其实,很早就有了。我是萌新,可不能不去了解,甚至学习。

WebGL

https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API

WebGL(Web 图形库)是一个 JavaScript API,可在任何兼容的 Web 浏览器中渲染高性能的交互式 3D 和 2D 图形,而无需使用插件。WebGL 通过引入一个与 OpenGL ES 2.0 非常一致的 API 来做到这一点,该 API 可以在 HTML5 canvas 元素中使用。这种一致性使 API 可以利用用户设备提供的硬件图形加速。

目前支持 WebGL 的浏览器有:Firefox 4+, Google Chrome 9+, Opera 12+, Safari 5.1+, Internet Explorer 11+ 和 Microsoft Edge build 10240+;然而,WebGL 一些特性也需要用户的硬件设备支持。

WebGL 2 API 引入了对大部分的 OpenGL ES 3.0 功能集的支持; 它是通过WebGL2RenderingContext界面提供的。

WebGL 是基于 OpenGL 演进的。

WebGPU

https://developer.mozilla.org/zh-CN/docs/Web/API/WebGPU_API

WebGPU 是 WebGL 的继任者,为现代 GPU 提供更好的兼容、支持更通用的 GPU 计算、更快的操作以及能够访问到更高级的 GPU 特性。

web3D 渲染

web3D应用 -> 引擎 -> web端图形API -> 本地端图形 API -> 显卡

WebGL 和 WebGPU 在这个环节中,本地图形 API 是不一样的。 WebGL 使用的是 OpenGL 或 DirectX。WebGPU 使用的是 Direct3D 或 Metal 或 Vulkan。

对于用户,只会关心所看到的 3D 效果。对于实现者,要了解知道渲染的流程还有熟悉 web 端图形 API 的使用。直接使用 WebGL API 或 WebGPU API 是能够实现 3D 效果。但会有第三方工具,比如 three.js 和 babylon.js 。它们会对 API 进行封装转换,可以省很多很多事。

blender 也是工具。
https://www.blender.org/
https://zhuanlan.zhihu.com/p/59575701
https://www.bilibili.com/video/BV13M41137Ki/?p=2&spm_id_from=pageDriver&vd_source=6993ce0cd88c9947ac7681b6864a99d0

相关文章

https://zhuanlan.zhihu.com/p/369632693

https://juejin.cn/post/6966584226758000648

https://juejin.cn/post/7184099035018428472

https://juejin.cn/post/7053137406463049742

5月 09

头条、抖音、字节啥的都叫抖音小程序。没开始之前,以为和其他小程序一样,看这文档,噼里啪啦就能做下来。发现不是那么会事。不是不好开发,是一开始抖音就限制了你开发的能力。比如,你想自定义顶部导航栏不允许,比如企业级账号想获取手机号码不允许,比如获取定位不允许。作为一个小程序,一个常规的社交 app,这些是最基本的能力。这都不允许,怎么好把小程序体验开发到极致呢。
不是不允许,是要你满足它特定的条件,才渐渐的开放给你。不明白这是要搞啥子。好吧,只能妥协,开始开发出不是很满意的小程序出来。

顺便吐槽小各家小程序的顶部的胶囊。个人觉得还是微信和支付宝的好看一些。有些家不仅不好看,感觉就毫无设计可言。包括抖音的。胶囊丑,顶部导航栏也丑。

https://developer.open-douyin.com/

5月 06

前边那个弹窗还有一些瑕疵,看得到的瑕疵。比如,使用贝塞尔曲线来模拟弹簧那种伸缩效果时,会露底。如果说是变魔术,用土话讲,那就是穿帮了。还有就是弹窗的动画的时间没有定义的地方。这次都加上了。先看看效果。

Code

popupLayout.vue

<template>
    <view v-if="controller.open" class="popup-layer" catchtouchmove="true">
        <view v-if="direction == 'top'" class="top-bg-color" :class="{ 'top-bg-show': controller.bg, 'top-bg-hide': !controller.bg }" :style="{ opacity: controller.bgOpacity }" @tap="close"></view>

        <view v-if="direction == 'bottom'" class="bottom-bg-color" :class="{ 'bottom-bg-show': controller.bg, 'bottom-bg-hide': !controller.bg }" :style="{ opacity: controller.bgOpacity }" @tap="close"></view>

        <view v-if="direction == 'left'" class="left-bg-color" :class="{ 'left-bg-show': controller.bg, 'left-bg-hide': !controller.bg }" :style="{ opacity: controller.bgOpacity }" @tap="close"></view>

        <view v-if="direction == 'right'" class="right-bg-color" :class="{ 'right-bg-show': controller.bg, 'right-bg-hide': !controller.bg }" :style="{ opacity: controller.bgOpacity }" @tap="close"></view>

        <view class="popup-content" :style="_location">
            <slot></slot>

            <view v-if="timingFun == 'cubic-bezier' && contentBgColor" :style="{background:contentBgColor}" :class="'content-bg-' + direction"></view>
        </view>
    </view>
</template>

<script setup>
import { reactive, defineProps, defineEmits, defineExpose, computed, onMounted, watch } from 'vue';

const controller = reactive({
    open: false,
    lock: false,
    bg: false,
    bgOpacity: 0,
    translate: -100,
    time: null
});

const emits = defineEmits(['onClose']);

const timingFuns = {
    'cubic-bezier': 'transition-timing-function: cubic-bezier(.15,-0.88,.41,1.68);',
    ease: 'transition-timing-function: easy;',
    'ease-in': 'transition-timing-function: ease-in;',
    'ease-out': 'transition-timing-function: ease-out;',
    'ease-in-out': 'transition-timing-function: ease-in-out;',
    linear: 'transition-timing-function: linear;'
};

const props = defineProps({
    direction: {
        type: String,
        default: 'top' // 方向  top,bottom,left,right
    },
    bgOpacity: {
        type: Number,
        default: 0.38
    },
    timingFun: {
        type: String,
        default: 'cubic-bezier'
    },
    contentBgColor: {
        type: String,
        default: ''
    },
    transitionDuration: {
        type: String,
        default: '0.5s'
    }
});

watch(() => props.timingFun, (newValue, oldValue) => {}, {
    immediate: true
});

const _translate = computed(() => {
    const transformObj = {
        top: `transform:translateY(${-controller.translate}%)`,
        bottom: `transform:translateY(${controller.translate}%)`,
        left: `transform:translateX(${-controller.translate}%)`,
        right: `transform:translateX(${controller.translate}%)`
    };
    return transformObj[props.direction];
});

const _location = computed(() => {
    const positionValue = {
        top: 'bottom:0px;width:100%;',
        bottom: 'top:0px;width:100%;',
        left: 'right:0px;top:0px;height:100%;',
        right: 'left:0px;top:0px;height:100%;'
    };

    let style = `transition-duration:${props.transitionDuration};`;
    if (timingFuns[props.timingFun]) {
        style += timingFuns[props.timingFun];
    }
    return style + positionValue[props.direction] + _translate.value;
});

const open = function() {
    if (controller.open) {
        return;
    }

    controller.open = true;

    let openTimer = setTimeout(() => {
        clearTimeout(openTimer);
        controller.lock = true;
        controller.bg = true;
        controller.bgOpacity = props.bgOpacity;
        controller.translate = -0.1;
        openTimer = null;
    }, 100);

    // 防止误触
    let lockTimer = setTimeout(() => {
        clearTimeout(lockTimer);
        controller.lock = false;
        lockTimer = null;
    }, 500);
};

const close = function() {
    if (controller.timer || controller.lock) {
        return;
    }

    controller.bgOpacity = 0;
    controller.bg = false;
    controller.translate = -100;

    controller.timer = setTimeout(() => {
        clearTimeout(controller.timer);
        controller.open = false;
        controller.timer = null;
        emits('onClose');
    }, 500);
};

defineExpose({
    open,
    close
});
</script>

<style lang="scss" scoped>
.popup-layer {
    width: 100vw;
    height: 100vh;
    overflow: hidden;
    top: 0px;
    left: 0px;
    position: fixed;
    z-index: 9999;

    .popup-content {
        position: absolute;
        z-index: 1000000;
        transition: transform;
    }

    .content-bg-top {
        position: absolute;
        width: 100%;
        height: 108rpx;
        left: 0;
        bottom: -100rpx;
    }

    .content-bg-bottom {
        position: absolute;
        width: 100%;
        height: 108rpx;
        left: 0;
        top: -100rpx;
    }

    .content-bg-left {
        position: absolute;
        width: 108rpx;
        height: 100%;
        top: 0;
        right: -100rpx;
    }

    .content-bg-right {
        position: absolute;
        width: 108rpx;
        height: 100%;
        top: 0;
        left: -100rpx;
    }
}

// 底部出来
.top-bg-color {
    width: 120vw;
    height: 120vh;
    background: #000;
    position: relative;
    left: -10vw;
    top: 121vh;
    opacity: 0;
    transition: 0.24s opacity, 0.24s top;
}

.top-bg-show {
    top: 0;
}

.top-bg-hide {
    top: 121vh;
}

.top-bg-color:after {
    content: '';
    width: 120vw;
    height: 120vw;
    border-radius: 50%;
    background-color: #000;
    position: absolute;
    left: 0;
    top: -30vw;
}

// 顶部出来
.bottom-bg-color {
    width: 120vw;
    height: 120vh;
    background: #000;
    position: relative;
    left: -10vw;
    bottom: 121vh;
    opacity: 0;
    transition: 0.24s opacity, 0.24s bottom;
}

.bottom-bg-show {
    bottom: 0;
}

.bottom-bg-hide {
    bottom: 121vh;
}

.bottom-bg-color:after {
    content: '';
    width: 120vw;
    height: 120vw;
    border-radius: 50%;
    background-color: #000;
    position: absolute;
    left: 0;
    bottom: -30vw;
}

// 右边出来
.left-bg-color {
    width: 120vw;
    height: 120vh;
    background: #000;
    position: relative;
    top: -10vh;
    left: 121vw;
    opacity: 0;
    transition: 0.24s opacity, 0.24s left;
}

.left-bg-show {
    left: 0;
}

.left-bg-hide {
    left: 121vw;
}

.left-bg-color:after {
    content: '';
    width: 120vh;
    height: 120vh;
    border-radius: 50%;
    background-color: #000;
    position: absolute;
    top: 0;
    left: -30vh;
}

// 左边边出来
.right-bg-color {
    width: 120vw;
    height: 120vh;
    background: #000;
    position: relative;
    top: -10vh;
    right: 121vw;
    opacity: 0;
    transition: 0.24s opacity, 0.24s left;
}

.right-bg-show {
    right: 0;
}

.right-bg-hide {
    right: 121vw;
}

.right-bg-color:after {
    content: '';
    width: 120vh;
    height: 120vh;
    border-radius: 50%;
    background-color: #000;
    position: absolute;
    top: 0;
    right: -30vh;
}
</style>
5月 06

在项目中,如果说什么组件用的最多。那弹窗觉得是第一个。想要一个好的弹窗,也是要花一些心思。这里从这几个方面来说。

  1. 展示效果好看。
  2. 弹窗一般分背景底色和弹窗实体。怎么做到好看呢。底色要渐变出来,底色要像子弹一样有一个圆头,刷一样出来好看。
  3. 弹窗实体怎么好看。可以先快速再慢速的展示出来。也可以像弹簧一样来回动弹几下出来。
  4. 弹出的方向可配置,背景颜色,透明度可配置。
  5. 弹窗实体中,内容太高或太宽怎么处理呢。
  6. 总之,就是要做的好看。

只有想到了才会去做到,去实现。那么就一点点实现吧。

实现过程

在实现这些效果中, css 占据了很重要的一部分。比如渐变、伸出。

实现渐变可以用 opacity、rgba。

想实现伸出弹出可以用 transform、keyframes,甚至绝对定位。

那想要一个好的弹射效果,先想到的可能会是 keyframes。它可以很精细的对目标的状态进行更改操作。可是吧,transform 可以使用 step 和 cubic-bezier(贝塞尔曲线),也可以实现弹簧的那种效果。这样实现起来就简单好多了(代码要少要少要干净),但是,觉得还是不够细腻,不够好。

先看看效果吧。

出了 css,其他就是 vue3 相关的东东了。下边直接贴出代码。

这里有个缺点,就是使用贝塞尔曲线来做弹出效果时候,底端会露白。其实,也想到了一个方法。传一个实体的背景色过来,在背景那边补上颜色也是可以解决的。

Code

popupLayer.vue

<template>
    <view v-if="controller.open" class="popup-layer" catchtouchmove="true">
        <view v-if="direction == 'top'" class="top-bg-color" :class="{ 'top-bg-show': controller.bg, 'top-bg-hide': !controller.bg }" :style="{ opacity: controller.bgOpacity }" @tap="close"></view>

        <view v-if="direction == 'bottom'" class="bottom-bg-color" :class="{ 'bottom-bg-show': controller.bg, 'bottom-bg-hide': !controller.bg }" :style="{ opacity: controller.bgOpacity }" @tap="close"></view>

        <view v-if="direction == 'left'" class="left-bg-color" :class="{ 'left-bg-show': controller.bg, 'left-bg-hide': !controller.bg }" :style="{ opacity: controller.bgOpacity }" @tap="close"></view>

        <view v-if="direction == 'right'" class="right-bg-color" :class="{ 'right-bg-show': controller.bg, 'right-bg-hide': !controller.bg }" :style="{ opacity: controller.bgOpacity }" @tap="close"></view>

        <view class="popup-content" :style="_location"><slot></slot></view>
    </view>
</template>

<script setup>
import { reactive, defineProps, defineEmits, defineExpose, computed, onMounted, watch } from 'vue';

const controller = reactive({
    open: false,
    lock: false,
    bg: false,
    bgOpacity: 0,
    translate: -100,
    time: null
});

const emits = defineEmits(['onClose']);

const timingFuns = {
    'cubic-bezier': 'transition-timing-function: cubic-bezier(.15,-0.88,.41,1.68);',
    ease: 'transition-timing-function: easy;',
    'ease-in': 'transition-timing-function: ease-in;',
    'ease-out': 'transition-timing-function: ease-out;',
    'ease-in-out': 'transition-timing-function: ease-in-out;',
    linear: 'transition-timing-function: linear;'
};

const props = defineProps({
    direction: {
        type: String,
        default: 'top' // 方向  top,bottom,left,right
    },
    bgOpacity: {
        type: Number,
        default: 0.38
    },
    timingFun: {
        type: String,
        default: 'cubic-bezier'
    }
});

watch(() => props.timingFun, (newValue, oldValue) => {}, {
    immediate: true
});

const _translate = computed(() => {
    const transformObj = {
        top: `transform:translateY(${-controller.translate}%)`,
        bottom: `transform:translateY(${controller.translate}%)`,
        left: `transform:translateX(${-controller.translate}%)`,
        right: `transform:translateX(${controller.translate}%)`
    };
    return transformObj[props.direction];
});

const _location = computed(() => {
    const positionValue = {
        top: 'bottom:0px;width:100%;',
        bottom: 'top:0px;width:100%;',
        left: 'right:0px;top:0px;height:100%;',
        right: 'left:0px;top:0px;height:100%;'
    };

    let style = '';
    if (timingFuns[props.timingFun]) {
        style = timingFuns[props.timingFun];
    }
    return style + positionValue[props.direction] + _translate.value;
});

const open = function() {
    if (controller.open) {
        return;
    }

    controller.open = true;

    let openTimer = setTimeout(() => {
        clearTimeout(openTimer);
        controller.lock = true;
        controller.bg = true;
        controller.bgOpacity = props.bgOpacity;
        controller.translate = -0.1;
        openTimer = null;

        console.log('_location', _location);
    }, 100);

    // 防止误触
    let lockTimer = setTimeout(() => {
        clearTimeout(lockTimer);
        controller.lock = false;
        lockTimer = null;
    }, 500);
};

const close = function() {
    if (controller.timer || controller.lock) {
        return;
    }

    controller.bgOpacity = 0;
    controller.bg = false;
    controller.translate = -100;

    controller.timer = setTimeout(() => {
        clearTimeout(controller.timer);
        controller.open = false;
        controller.timer = null;
        emits('onClose');
    }, 600);
};

defineExpose({
    open,
    close
});
</script>

<style lang="scss" scoped>
.popup-layer {
    width: 100vw;
    height: 100vh;
    overflow: hidden;
    top: 0px;
    left: 0px;
    position: fixed;
    z-index: 9999;

    .popup-content {
        position: absolute;
        z-index: 1000000;
        transition: transform 0.4s;
    }
}

// 底部出来
.top-bg-color {
    width: 120vw;
    height: 120vh;
    background: #000;
    position: relative;
    left: -10vw;
    top: 121vh;
    opacity: 0;
    transition: 0.24s opacity, 0.24s top;
}

.top-bg-show {
    top: 0;
}

.top-bg-hide {
    top: 121vh;
}

.top-bg-color:after {
    content: '';
    width: 120vw;
    height: 120vw;
    border-radius: 50%;
    background-color: #000;
    position: absolute;
    left: 0;
    top: -30vw;
}

// 顶部出来
.bottom-bg-color {
    width: 120vw;
    height: 120vh;
    background: #000;
    position: relative;
    left: -10vw;
    bottom: 121vh;
    opacity: 0;
    transition: 0.24s opacity, 0.24s bottom;
}

.bottom-bg-show {
    bottom: 0;
}

.bottom-bg-hide {
    bottom: 121vh;
}

.bottom-bg-color:after {
    content: '';
    width: 120vw;
    height: 120vw;
    border-radius: 50%;
    background-color: #000;
    position: absolute;
    left: 0;
    bottom: -30vw;
}

// 右边出来
.left-bg-color {
    width: 120vw;
    height: 120vh;
    background: #000;
    position: relative;
    top: -10vh;
    left: 121vw;
    opacity: 0;
    transition: 0.24s opacity, 0.24s left;
}

.left-bg-show {
    left: 0;
}

.left-bg-hide {
    left: 121vw;
}

.left-bg-color:after {
    content: '';
    width: 120vh;
    height: 120vh;
    border-radius: 50%;
    background-color: #000;
    position: absolute;
    top: 0;
    left: -30vh;
}

// 左边边出来
.right-bg-color {
    width: 120vw;
    height: 120vh;
    background: #000;
    position: relative;
    top: -10vh;
    right: 121vw;
    opacity: 0;
    transition: 0.24s opacity, 0.24s left;
}

.right-bg-show {
    right: 0;
}

.right-bg-hide {
    right: 121vw;
}

.right-bg-color:after {
    content: '';
    width: 120vh;
    height: 120vh;
    border-radius: 50%;
    background-color: #000;
    position: absolute;
    top: 0;
    right: -30vh;
}
</style>

测试调用的临时 vue

<template>
    <view class="d-flex flex-column w-100">
        <view class="flex-column mt-10 px-30">
            <view class="text-color-3 text-size-30 font-w600">设置方向</view>

            <view class="d-flex align-items-center flex-wrap mt-10">
                <view v-for="(item, index) in directions" :key="index" class="mr-10 mt-10" hover-class="hover-opacity" @tap="setDirections(item)">
                    <view class="d-flex px-16 py-6 border-radius-8 flex-shrink-0" :class="{ 'bg-color-1 text-color-2': item == popupOption.direction, 'bg-color-2 text-color-4': item != popupOption.direction}">
                        <view class="text-size-26"> {{ item }} </view> 
                    </view>
                </view>
            </view>

            <view class="text-color-3 text-size-30 font-w600 mt-30">设置弹射</view>

            <view class="d-flex align-items-center flex-wrap mt-10">
                <view v-for="(item, index) in timingFuns" :key="index" class="mr-10 mt-10" hover-class="hover-opacity" @tap="setTimingFuns(item)">
                    <view class="d-flex px-16 py-6 border-radius-8 flex-shrink-0 action-item" :class="{ 'bg-color-1 text-color-2': item == popupOption.timingFun, 'bg-color-2 text-color-4': item != popupOption.timingFun}">
                        <view class="text-size-26"> {{ item}} </view> 
                    </view>
                </view>
            </view>

            <view hover-class="hover-opacity" class="bg-color-1 text-color-2 text-size-30 border-radius-16 py-20 mt-30 d-flex justify-content-center" @tap="doPopup">开始弹出</view>
        </view>
    </view>

    <popupLayer ref="layer" :direction="popupOption.direction" :timing-fun="popupOption.timingFun" @onClose="onClose">
        <view class="box-shadow-sm" :class="boxClass"></view>
    </popupLayer>
</template>

<script setup>

import { ref, reactive } from 'vue'

const popupOption = reactive({
    'direction': 'top',
    'timingFun': 'cubic-bezier'
});

const directions = ['top', 'bottom', 'left', 'right'];

const timingFuns = ['cubic-bezier', 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear'];

const layer = ref(null);

const boxClass = ref('bottom-box');

const setDirections = function(value) {
    popupOption.direction = value;

    boxClass.value = { 'bottom': 'top-box', 'top': 'bottom-box', 'left': 'right-box', 'right': 'left-box'}[value]
}

const setTimingFuns = function(value) {
    popupOption.timingFun = value;
}

const doPopup = function() {
    layer.value.open()
}

const onClose = function() {
    console.log('close')
}
</script>

<style>
    .bottom-box {
        width: 100%;
        height: 520upx;
        border-top-left-radius: 20upx;
        border-top-right-radius: 20upx;
        background-color: #ffffff;
    }

    .top-box {
        width: 100%;
        height: 520upx;
        border-bottom-left-radius: 20upx;
        border-bottom-right-radius: 20upx;
        background-color: #ffffff;
    }

    .left-box {
        width: 520upx;
        height: 100vh;
        border-top-right-radius: 20upx;
        border-bottom-right-radius: 20upx;
        background-color: #ffffff;
    }

    .right-box {
        width: 520upx;
        height: 100vh;
        border-top-left-radius: 20upx;
        border-bottom-left-radius: 20upx;
        background-color: #ffffff;
    }
</style>

关于测试用的 vue,里边用到了很多 class,这些 class 都是全局定义好的。足够规范和足够包容就可以少写很多样式了。这样,有一个缺点就是 html 部分特别长。这就是组合起来的好处和缺点。也贴出部分代码。

base.scss

// 布局相关的样式
.d-none {
    display: none;
}

// flex 布局(全用 flex 布局)
.d-flex {
    display: flex;
}

.d-inline-flex {
    display: inline-flex;
}

.flex-row {
    flex-direction: row;
}

.flex-row-reverse {
    flex-direction: row-reverse;
}

.flex-column {
    flex-direction: column;
}
……
// margin
$list: 6 8 12 16 24;

// left
@each $i in $list {
    .ml-#{$i} {
        margin-left: #{$i}upx;
    }
}

@for $i from 1 to 5 {
    .ml-#{$i * 10} {
        margin-left: #{$i * 10}upx;
    }
}

// right
@each $i in $list {
    .mr-#{$i} {
        margin-right: #{$i}upx;
    }
}

@for $i from 1 to 5 {
    .mr-#{$i * 10} {
        margin-right: #{$i * 10}upx;
    }
}

// x
@each $i in $list {
    .mx-#{$i} {
        margin-left: #{$i}upx;
        margin-right: #{$i}upx;
    }
}

@for $i from 1 to 5 {
    .mx-#{$i * 10} {
        margin-left: #{$i * 10}upx;
        margin-right: #{$i * 10}upx;
    }
}

$num: 12;
@while $num < 54 {
    .text-size-#{$num} {
        font-size: #{$num}upx;
    }
    $num: $num + 2;
}

$num: 6;
@while $num < 30 {
    .border-radius-#{$num} {
        border-radius: #{$num}upx;
    }
    $num: $num + 2;
}

还有 css 的变量写法也要用起来。好东西就要用起来,有了变量,才算是真正的程序啊,哈哈。

root: {
--bg-color: #f00;
}

span {
background: var(--bg-color)
}

最后

其实,这里没有对超过宽度或超过高度的处理。这里仅仅是个弹出的工具,仅仅做弹出而已。实体部分都由插槽交给需要用到的地方。

如果真遇到宽度或高度超过的情况,可以套一层 scroll-view 组件。如果实体内容具有可变性,就是有时候会高一点有时候会矮一点(数据驱动ui,数据来源服务器),这个时候给 scroll-view可以加个约束,比如 max-height:calc(100vh - 100px),具体多少,试着来。

相关文章

https://developer.mozilla.org/zh-CN/docs/Web/CSS/transition-timing-function

https://zhuanlan.zhihu.com/p/397089242

https://cubic-bezier.com/#.23,-0.47,.44,1.65

https://juejin.cn/post/6844903688029339655

https://juejin.cn/post/6972720564301463565

5月 05

vue3 出来那么久了。以前的项目 vue2 也该更更更了。新项目那必须得 vue3 了。不过,有些关键的写法,会有些不同。一点点的来。

globalProperties

在 vue2,想定义一个全局的属性(方法或变量),只需要定义在 Vue.prototype 上就可以。然后在 template 中可以直接使用,在 script 中,通过 this 来访问使用。

到了 vue3 这里,就不一样了。这些方法和属性定义在 globalProperties, 就是 app.config.globalProperties 上。

比如:

import App from './App'

import {
    createSSRApp
} from 'vue'

const app = createSSRApp(App)

app.config.globalProperties.$test = (value) => {
    console.log('test ' + value)
}

vue3 是干掉 this 的。使用的时候这样。

import { onMounted, getCurrentInstance } from 'vue'

onMounted(() => {
    const { proxy,  ctx } = getCurrentInstance()
    proxy.$test('全局调用')
})

template 中,则可以直接使用。 getCurrentInstance 还有一个输出 ctx,这个和 vue2 时候的 this 一个样。