动态创建web-view加载本地html 页面通讯
我们日常使用web-view最常见的方法是新建一个页面,然后放一个web-view并配置上我们的html页面地址
这里有另外一种动态创建web-view的方法,更加的灵活
let wvPath = '/hybrid/html/webview.html'
let wv= new plus.webview.create(
wvPath,
'map-view',
{
'uni-app': 'none',
top: systemInfo.statusBarHeight,
left: 0,
width: systemInfo.screenWidth,
height: systemInfo.screenHeight - systemInfo.statusBarHeight,
background: '#ffffff',
// 启用手势返回
// popGesture: 'close',
},
{
// 这里携带web-view的额外参数
key
}
)
// 一定要记得再不需要的时候 关闭掉动态创建的we-view
wv.close()
监听动态创建的web-view发送的消息
plus.globalEvent.addEventListener('plusMessage', (e) => {
console.log("网页消息", e);
})
竟然用到了plusMessage方法,本来直接使用的message监听发现怎么都不生效
从 plus.globalEvent看出监听的是全局的plusMessage方法
很多人的使用习惯,既然我监听了消息,那么在我不需要的时候是需要把这个监听移除掉的,否则不停的监听影响APP性能,于是写了下面的移除方法:
plus.globalEvent.removeEventListener('plusMessage',messagEvent)
但是出乎意料,移除后整个APP的所有事件都失效!点击哪里都没有反应,切记这个方法是不可以移除的
第二种 消息传输方式
plus.globalEvent监听uni.postMessage推送的消息会出现重复推送等问题,建议改为Webview url拦截的方式获取html文件数据。
// html中跳转自定义url,会被拦截,不会进行跳转
window.location.href = 'push?params=loading'
// vue页面wv拦截url变更
wv.overrideUrlLoading({mode:'reject'}, e => {
var params = decodeURI(e.url.split('push?params=')[1])
})
url拦截更实时,准确率更高,不会重复接收消息,只有App支持,H5+文档参考:https://www.html5plus.org/doc/zh_cn/webview.html#plus.webview.WebviewObject.overrideUrlLoading
参考文章:
https://www.html5plus.org/doc/zh_cn/webview.html
https://ask.dcloud.net.cn/article/35083
我们日常使用web-view最常见的方法是新建一个页面,然后放一个web-view并配置上我们的html页面地址
这里有另外一种动态创建web-view的方法,更加的灵活
let wvPath = '/hybrid/html/webview.html'
let wv= new plus.webview.create(
wvPath,
'map-view',
{
'uni-app': 'none',
top: systemInfo.statusBarHeight,
left: 0,
width: systemInfo.screenWidth,
height: systemInfo.screenHeight - systemInfo.statusBarHeight,
background: '#ffffff',
// 启用手势返回
// popGesture: 'close',
},
{
// 这里携带web-view的额外参数
key
}
)
// 一定要记得再不需要的时候 关闭掉动态创建的we-view
wv.close()
监听动态创建的web-view发送的消息
plus.globalEvent.addEventListener('plusMessage', (e) => {
console.log("网页消息", e);
})
竟然用到了plusMessage方法,本来直接使用的message监听发现怎么都不生效
从 plus.globalEvent看出监听的是全局的plusMessage方法
很多人的使用习惯,既然我监听了消息,那么在我不需要的时候是需要把这个监听移除掉的,否则不停的监听影响APP性能,于是写了下面的移除方法:
plus.globalEvent.removeEventListener('plusMessage',messagEvent)
但是出乎意料,移除后整个APP的所有事件都失效!点击哪里都没有反应,切记这个方法是不可以移除的
第二种 消息传输方式
plus.globalEvent监听uni.postMessage推送的消息会出现重复推送等问题,建议改为Webview url拦截的方式获取html文件数据。
// html中跳转自定义url,会被拦截,不会进行跳转
window.location.href = 'push?params=loading'
// vue页面wv拦截url变更
wv.overrideUrlLoading({mode:'reject'}, e => {
var params = decodeURI(e.url.split('push?params=')[1])
})
url拦截更实时,准确率更高,不会重复接收消息,只有App支持,H5+文档参考:https://www.html5plus.org/doc/zh_cn/webview.html#plus.webview.WebviewObject.overrideUrlLoading
参考文章:
https://www.html5plus.org/doc/zh_cn/webview.html
https://ask.dcloud.net.cn/article/35083
uni-vue3--专为 UniApp + Vue3 + UnoCSS 打造的开箱即用模板
uni-vue3
专为 UniApp + Vue3 + UnoCSS 打造的 starter template
源码:https://github.com/vue-rookie/uni-vue3
🚀 特性
- ✅ Vue3 + Composition API
- ✅ UnoCSS 原子化CSS
- ✅ TypeScript 支持
- ✅ Vite 构建工具
📦 快速开始
产品
建议手机模式预览
uni-vue3框架--------------------------------代码:feature/main
uni-vue3模仿抖音---------------------------代码:feature/douyin
uni-vue3模仿小红书------------------------代码:feature/xiaohongshu
🚀 技术栈
- 核心框架:Vue 3.4
- 构建工具:Vite 5.0
- 开发语言:TypeScript 5.0
- 状态管理:Pinia 2.0
- 样式方案:UnoCSS
- 跨端框架:UniApp 3.0
自定义主题样式(超级自由简单的样式配置):
组件样式全部采用 tailwindcss 封装,您无需写任何 令人烦躁的 css 代码。
只需要修改对应 uno.config.ts 配置文件中的 theme 下的的色值号即可,其他无需任何更改
🪝 自定义 Hooks
项目中精心封装了丰富的自定义 Hooks,大幅提升开发效率和代码质量,所有 Hooks 支持 TypeScript 类型推导。
UI 交互类
useModal - 对话框管理
提供统一的弹窗交互解决方案,包括确认框、消息提示、加载状态等。
import { useModal } from "@/hooks"
// 在组件中使用
const {
showToast,
showSuccess,
showError,
showConfirm,
showLoading,
hideLoading,
showActionSheet,
} = useModal()
// 显示确认对话框
await showConfirm({
title: "操作确认",
content: "确定要执行此操作吗?",
})
// 显示成功提示
showSuccess("操作成功")
// 显示加载状态
const loading = showLoading()
try {
// 执行异步操作
} finally {
loading.hide() // 隐藏加载
}
// 显示底部操作菜单
const selectedIndex = await showActionSheet({
itemList: ["选项一", "选项二", "选项三"],
})
数据处理类
useStorage - 本地存储
增强版本地存储,支持过期时间、类型安全、自动序列化/反序列化、响应式存储。
import { useStorage } from "@/hooks"
const { setStorage, getStorage, removeStorage, clearStorage, createReactiveStorage } = useStorage()
// 存储数据,30天过期
await setStorage("user-info", { id: 1, name: "Admin" }, 30 * 24 * 60 * 60 * 1000)
// 读取数据
const userInfo = await getStorage("user-info")
// 创建响应式存储
const count = createReactiveStorage("visit-count", 0)
count.value++ // 自动同步到存储
useRequest - 网络请求
强大的请求管理 Hook,自动处理加载状态、错误处理、请求缓存等。
import { useRequest } from "@/hooks"
const request = useRequest({
baseURL: "https://api.example.com",
autoHandleError: true, // 自动处理错误
autoLoading: true, // 自动显示加载状态
})
// GET 请求
const { data } = await request.get("/users", { page: 1 })
// POST 请求
await request.post("/articles", { title, content })
// 文件上传
await request.upload("/upload", {
filePath: tempFilePath,
name: "file",
formData: { type: "avatar" },
})
useInputLimit - 输入限制
提供各类输入限制函数,轻松实现各种格式检查和输入控制。
import { useInputDataLimit } from "@/hooks/useInputLimit"
const {
limitNumber, // 仅限数字
limitLetter, // 仅限字母
limitNumberAndLetter, // 仅限数字和字母
limitToPositiveTwoDecimals, // 仅限两位小数的正数
limitNoChinese, // 不允许中文
} = useInputDataLimit()
// 在输入事件中使用
const handleInput = (e) => {
const rawValue = e.detail.value
const formattedValue = limitToPositiveTwoDecimals(rawValue)
// 更新表单值
}
设备能力类
useLocation - 位置服务
封装位置获取、地址解析、坐标转换等功能,支持高精度定位和后台定位。
import { useLocation } from "@/hooks"
const { getLocation, startLocationUpdate, stopLocationUpdate, chooseLocation, openLocation } =
useLocation()
// 获取当前位置
const position = await getLocation({
type: "gcj02", // 坐标系类型
isHighAccuracy: true, // 高精度定位
})
// 打开地图选择位置
const location = await chooseLocation()
// 在地图上查看位置
await openLocation({
latitude: 39.9087,
longitude: 116.3975,
name: "目的地",
address: "详细地址信息",
scale: 18,
})
useCamera - 相机功能
相机相关功能封装,包括拍照、录像、选择相册等功能。
import { useCamera } from "@/hooks"
const { takePhoto, chooseImage, chooseVideo, previewImage, compressImage } = useCamera()
// 拍照或从相册选择
const filePath = await takePhoto({
sourceType: ["camera", "album"],
})
// 选择多张图片
const images = await chooseImage({
count: 9,
sizeType: ["original", "compressed"],
})
// 压缩图片
const compressedPath = await compressImage(filePath, {
quality: 80, // 压缩质量
})
useSystem - 系统信息
获取系统信息、设备信息、网络状态等功能。
import { useSystem } from "@/hooks"
const { getSystemInfo, getNetworkType, onNetworkStatusChange, getDeviceInfo, vibrateShort } =
useSystem()
// 获取系统信息
const systemInfo = getSystemInfo()
console.log(`运行平台: ${systemInfo.platform}, 系统: ${systemInfo.system}`)
// 获取网络状态
const networkType = await getNetworkType()
// 监听网络变化
onNetworkStatusChange((res) => {
console.log(`网络变更: ${res.networkType}, 是否连接: ${res.isConnected}`)
})
其他实用 Hooks
useShare - 分享功能
统一的分享接口,支持小程序分享、系统分享、自定义分享等。
import { useShare } from "@/hooks"
const { share, shareWithSystem, configMiniProgramShare } = useShare()
// 配置页面分享参数
configMiniProgramShare({
title: "分享标题",
path: "/pages/index/index",
imageUrl: "/static/share.png",
onShareSuccess: () => {
console.log("分享成功")
},
})
// 系统分享
shareWithSystem({
title: "分享内容",
summary: "内容摘要",
href: "https://example.com",
imageUrl: "/static/share.png",
})
useValidation - 表单验证
强大的表单验证系统,内置多种常用验证规则,支持自定义验证。
import { useValidation } from "@/hooks"
const validation = useValidation()
// 创建表单数据和规则
const { formData, rules, validate, errors, resetValidation } = validation.createForm(
{
username: "",
password: "",
email: "",
phone: "",
},
{
username: [
{ required: true, message: "用户名不能为空" },
{ min: 3, max: 20, message: "长度在3到20个字符" },
],
password: [
{ required: true, message: "密码不能为空" },
{ min: 6, message: "密码长度不能小于6位" },
{ pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, message: "密码必须包含大小写字母和数字" },
],
email: [{ type: "email", message: "请输入正确的邮箱格式" }],
phone: [{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号" }],
},
)
// 提交表单
const handleSubmit = async () => {
const valid = await validate()
if (valid) {
// 表单验证通过,提交数据
console.log("表单数据:", formData)
} else {
// 表单验证失败
console.log("验证错误:", errors.value)
}
}
usePageScroll - 页面滚动
页面滚动相关功能,包括滚动到指定位置、监听滚动事件等。
import { usePageScroll } from "@/hooks"
const { scrollTo, scrollToTop, scrollToSelector, onPageScroll, getScrollPosition } = usePageScroll()
// 滚动到顶部
scrollToTop()
// 滚动到指定位置
scrollTo(0, 200, true) // 带动画效果滚动到 200px 位置
// 滚动到指定元素
scrollToSelector(".news-item-5", {
offset: -20, // 偏移量
duration: 300, // 动画时长
})
// 监听页面滚动
onPageScroll((scrollTop) => {
if (scrollTop > 100) {
// 显示返回顶部按钮
}
})
📋 环境要求
- Node.js >= 18.0.0
- pnpm >= 7.0.0
- 微信开发者工具(开发小程序时使用)
🛠️ 快速开始
# 安装依赖
pnpm install
# 开发环境运行
pnpm dev:mp-weixin
# 生产环境构建
pnpm build:mp-weixin
📱 开发指南
微信小程序开发
-
开发环境配置
- 运行
pnpm dev:mp-weixin生成开发环境代码 - 使用微信开发者工具导入
dist/dev/mp-weixin目录 - 开启"不校验合法域名"选项(开发环境)
- 运行
-
生产环境发布
- 执行
pnpm build:mp-weixin生成生产环境代码 - 使用微信开发者工具导入
dist/build/mp-weixin目录 - 点击"上传"按钮发布小程序
- 执行
项目规范
-
组件开发规范
- 全局组件统一放置在
uni-module目录下 - 遵循 UniApp 的 easycom 组件规范
- 组件命名采用 PascalCase 命名法
- 全局组件统一放置在
-
类型定义规范
- 业务类型定义统一放在对应页面的
type.ts文件中 - 公共类型定义放在
types目录下 - 类型命名采用 PascalCase 命名法
- 业务类型定义统一放在对应页面的
-
Hooks 使用规范
- 公共 hooks 统一放在
hooks目录下 - 业务相关 hooks 放在对应页面目录
- hooks 命名采用 camelCase 命名法,以
use开头
- 公共 hooks 统一放在
🚀 性能优化
构建优化
-
代码分割
- 使用
manualChunks实现代码分割 - 第三方依赖独立打包,提高缓存效率
- 路由组件按需加载
- 使用
-
资源优化
- 图片资源自动压缩
- CSS 代码压缩和优化
- 静态资源 CDN 加速
-
编译优化
- 使用
lightningcss进行 CSS 处理 - 配置合理的
assetsInlineLimit - 优化 Sass 编译配置
- 使用
运行时优化
-
渲染优化
- 图片懒加载
- 虚拟列表
- 条件渲染优化
-
性能监控
- 页面加载性能监控
- 组件渲染性能分析
- 内存使用监控
📦 项目结构
├── src
│ ├── pages # 页面文件
│ ├── pages-sub # 子页面
│ ├── hooks # 自定义hooks
│ ├── static # 静态资源
│ ├── types # 类型定义
│ ├── utils # 工具函数
│ └── uni-module # 全局组件
├── vite.config.ts # Vite 配置
└── package.json # 项目配置
源码
📄 开源协议
本项目采用 MIT 协议开源,详情请查看 LICENSE 文件。
致谢
感谢 DCloud 官方,旗下出品的 uni-app 、uni-app-x 、uniCloud、uni-app 小程序 等多平台、多元化的技术体系。
uni-vue3
专为 UniApp + Vue3 + UnoCSS 打造的 starter template
源码:https://github.com/vue-rookie/uni-vue3
🚀 特性
- ✅ Vue3 + Composition API
- ✅ UnoCSS 原子化CSS
- ✅ TypeScript 支持
- ✅ Vite 构建工具
📦 快速开始
产品
建议手机模式预览
uni-vue3框架--------------------------------代码:feature/main
uni-vue3模仿抖音---------------------------代码:feature/douyin
uni-vue3模仿小红书------------------------代码:feature/xiaohongshu
🚀 技术栈
- 核心框架:Vue 3.4
- 构建工具:Vite 5.0
- 开发语言:TypeScript 5.0
- 状态管理:Pinia 2.0
- 样式方案:UnoCSS
- 跨端框架:UniApp 3.0
自定义主题样式(超级自由简单的样式配置):
组件样式全部采用 tailwindcss 封装,您无需写任何 令人烦躁的 css 代码。
只需要修改对应 uno.config.ts 配置文件中的 theme 下的的色值号即可,其他无需任何更改
🪝 自定义 Hooks
项目中精心封装了丰富的自定义 Hooks,大幅提升开发效率和代码质量,所有 Hooks 支持 TypeScript 类型推导。
UI 交互类
useModal - 对话框管理
提供统一的弹窗交互解决方案,包括确认框、消息提示、加载状态等。
import { useModal } from "@/hooks"
// 在组件中使用
const {
showToast,
showSuccess,
showError,
showConfirm,
showLoading,
hideLoading,
showActionSheet,
} = useModal()
// 显示确认对话框
await showConfirm({
title: "操作确认",
content: "确定要执行此操作吗?",
})
// 显示成功提示
showSuccess("操作成功")
// 显示加载状态
const loading = showLoading()
try {
// 执行异步操作
} finally {
loading.hide() // 隐藏加载
}
// 显示底部操作菜单
const selectedIndex = await showActionSheet({
itemList: ["选项一", "选项二", "选项三"],
})
数据处理类
useStorage - 本地存储
增强版本地存储,支持过期时间、类型安全、自动序列化/反序列化、响应式存储。
import { useStorage } from "@/hooks"
const { setStorage, getStorage, removeStorage, clearStorage, createReactiveStorage } = useStorage()
// 存储数据,30天过期
await setStorage("user-info", { id: 1, name: "Admin" }, 30 * 24 * 60 * 60 * 1000)
// 读取数据
const userInfo = await getStorage("user-info")
// 创建响应式存储
const count = createReactiveStorage("visit-count", 0)
count.value++ // 自动同步到存储
useRequest - 网络请求
强大的请求管理 Hook,自动处理加载状态、错误处理、请求缓存等。
import { useRequest } from "@/hooks"
const request = useRequest({
baseURL: "https://api.example.com",
autoHandleError: true, // 自动处理错误
autoLoading: true, // 自动显示加载状态
})
// GET 请求
const { data } = await request.get("/users", { page: 1 })
// POST 请求
await request.post("/articles", { title, content })
// 文件上传
await request.upload("/upload", {
filePath: tempFilePath,
name: "file",
formData: { type: "avatar" },
})
useInputLimit - 输入限制
提供各类输入限制函数,轻松实现各种格式检查和输入控制。
import { useInputDataLimit } from "@/hooks/useInputLimit"
const {
limitNumber, // 仅限数字
limitLetter, // 仅限字母
limitNumberAndLetter, // 仅限数字和字母
limitToPositiveTwoDecimals, // 仅限两位小数的正数
limitNoChinese, // 不允许中文
} = useInputDataLimit()
// 在输入事件中使用
const handleInput = (e) => {
const rawValue = e.detail.value
const formattedValue = limitToPositiveTwoDecimals(rawValue)
// 更新表单值
}
设备能力类
useLocation - 位置服务
封装位置获取、地址解析、坐标转换等功能,支持高精度定位和后台定位。
import { useLocation } from "@/hooks"
const { getLocation, startLocationUpdate, stopLocationUpdate, chooseLocation, openLocation } =
useLocation()
// 获取当前位置
const position = await getLocation({
type: "gcj02", // 坐标系类型
isHighAccuracy: true, // 高精度定位
})
// 打开地图选择位置
const location = await chooseLocation()
// 在地图上查看位置
await openLocation({
latitude: 39.9087,
longitude: 116.3975,
name: "目的地",
address: "详细地址信息",
scale: 18,
})
useCamera - 相机功能
相机相关功能封装,包括拍照、录像、选择相册等功能。
import { useCamera } from "@/hooks"
const { takePhoto, chooseImage, chooseVideo, previewImage, compressImage } = useCamera()
// 拍照或从相册选择
const filePath = await takePhoto({
sourceType: ["camera", "album"],
})
// 选择多张图片
const images = await chooseImage({
count: 9,
sizeType: ["original", "compressed"],
})
// 压缩图片
const compressedPath = await compressImage(filePath, {
quality: 80, // 压缩质量
})
useSystem - 系统信息
获取系统信息、设备信息、网络状态等功能。
import { useSystem } from "@/hooks"
const { getSystemInfo, getNetworkType, onNetworkStatusChange, getDeviceInfo, vibrateShort } =
useSystem()
// 获取系统信息
const systemInfo = getSystemInfo()
console.log(`运行平台: ${systemInfo.platform}, 系统: ${systemInfo.system}`)
// 获取网络状态
const networkType = await getNetworkType()
// 监听网络变化
onNetworkStatusChange((res) => {
console.log(`网络变更: ${res.networkType}, 是否连接: ${res.isConnected}`)
})
其他实用 Hooks
useShare - 分享功能
统一的分享接口,支持小程序分享、系统分享、自定义分享等。
import { useShare } from "@/hooks"
const { share, shareWithSystem, configMiniProgramShare } = useShare()
// 配置页面分享参数
configMiniProgramShare({
title: "分享标题",
path: "/pages/index/index",
imageUrl: "/static/share.png",
onShareSuccess: () => {
console.log("分享成功")
},
})
// 系统分享
shareWithSystem({
title: "分享内容",
summary: "内容摘要",
href: "https://example.com",
imageUrl: "/static/share.png",
})
useValidation - 表单验证
强大的表单验证系统,内置多种常用验证规则,支持自定义验证。
import { useValidation } from "@/hooks"
const validation = useValidation()
// 创建表单数据和规则
const { formData, rules, validate, errors, resetValidation } = validation.createForm(
{
username: "",
password: "",
email: "",
phone: "",
},
{
username: [
{ required: true, message: "用户名不能为空" },
{ min: 3, max: 20, message: "长度在3到20个字符" },
],
password: [
{ required: true, message: "密码不能为空" },
{ min: 6, message: "密码长度不能小于6位" },
{ pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, message: "密码必须包含大小写字母和数字" },
],
email: [{ type: "email", message: "请输入正确的邮箱格式" }],
phone: [{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号" }],
},
)
// 提交表单
const handleSubmit = async () => {
const valid = await validate()
if (valid) {
// 表单验证通过,提交数据
console.log("表单数据:", formData)
} else {
// 表单验证失败
console.log("验证错误:", errors.value)
}
}
usePageScroll - 页面滚动
页面滚动相关功能,包括滚动到指定位置、监听滚动事件等。
import { usePageScroll } from "@/hooks"
const { scrollTo, scrollToTop, scrollToSelector, onPageScroll, getScrollPosition } = usePageScroll()
// 滚动到顶部
scrollToTop()
// 滚动到指定位置
scrollTo(0, 200, true) // 带动画效果滚动到 200px 位置
// 滚动到指定元素
scrollToSelector(".news-item-5", {
offset: -20, // 偏移量
duration: 300, // 动画时长
})
// 监听页面滚动
onPageScroll((scrollTop) => {
if (scrollTop > 100) {
// 显示返回顶部按钮
}
})
📋 环境要求
- Node.js >= 18.0.0
- pnpm >= 7.0.0
- 微信开发者工具(开发小程序时使用)
🛠️ 快速开始
# 安装依赖
pnpm install
# 开发环境运行
pnpm dev:mp-weixin
# 生产环境构建
pnpm build:mp-weixin
📱 开发指南
微信小程序开发
-
开发环境配置
- 运行
pnpm dev:mp-weixin生成开发环境代码 - 使用微信开发者工具导入
dist/dev/mp-weixin目录 - 开启"不校验合法域名"选项(开发环境)
- 运行
-
生产环境发布
- 执行
pnpm build:mp-weixin生成生产环境代码 - 使用微信开发者工具导入
dist/build/mp-weixin目录 - 点击"上传"按钮发布小程序
- 执行
项目规范
-
组件开发规范
- 全局组件统一放置在
uni-module目录下 - 遵循 UniApp 的 easycom 组件规范
- 组件命名采用 PascalCase 命名法
- 全局组件统一放置在
-
类型定义规范
- 业务类型定义统一放在对应页面的
type.ts文件中 - 公共类型定义放在
types目录下 - 类型命名采用 PascalCase 命名法
- 业务类型定义统一放在对应页面的
-
Hooks 使用规范
- 公共 hooks 统一放在
hooks目录下 - 业务相关 hooks 放在对应页面目录
- hooks 命名采用 camelCase 命名法,以
use开头
- 公共 hooks 统一放在
🚀 性能优化
构建优化
-
代码分割
- 使用
manualChunks实现代码分割 - 第三方依赖独立打包,提高缓存效率
- 路由组件按需加载
- 使用
-
资源优化
- 图片资源自动压缩
- CSS 代码压缩和优化
- 静态资源 CDN 加速
-
编译优化
- 使用
lightningcss进行 CSS 处理 - 配置合理的
assetsInlineLimit - 优化 Sass 编译配置
- 使用
运行时优化
-
渲染优化
- 图片懒加载
- 虚拟列表
- 条件渲染优化
-
性能监控
- 页面加载性能监控
- 组件渲染性能分析
- 内存使用监控
📦 项目结构
├── src
│ ├── pages # 页面文件
│ ├── pages-sub # 子页面
│ ├── hooks # 自定义hooks
│ ├── static # 静态资源
│ ├── types # 类型定义
│ ├── utils # 工具函数
│ └── uni-module # 全局组件
├── vite.config.ts # Vite 配置
└── package.json # 项目配置
源码
📄 开源协议
本项目采用 MIT 协议开源,详情请查看 LICENSE 文件。
致谢
感谢 DCloud 官方,旗下出品的 uni-app 、uni-app-x 、uniCloud、uni-app 小程序 等多平台、多元化的技术体系。
收起阅读 »【鸿蒙征文】星光不负,码向未来:uni-app + uniCloud 赋能社区管理系统的高效适配与生态融合实践
前言
公司前段时间接了一个社区管理的项目,看到本次 鸿蒙征文 活动,正好做一次总结。
我们基于 uni-app 跨端框架和 uniCloud Serverless服务,开发“CommunityHub 社区管理系统”。该系统旨在连接社区物业与居民。
我会在本文中重点阐述了如何利用 uni-app 实现应用在鸿蒙 OS 上的快速适配、多设备响应式布局、已经App和鸿蒙元服务共享代码。
一、技术选型与项目概述
1.1 社区数字化面临的挑战
接到需求后,我们分析,主要有两个挑战:
- 多端适配: 社区服务需要覆盖居民的手机、平板乃至管理员的PC,且在当下中国,鸿蒙的兼容是必须考虑的平台,维护多套代码成本极高。
- 后端运维: 社区系统,日常请求量并不高,但需满足特殊情况下的高并发处理,需要稳定且易运维的后端支持。
1.2 uni-app + uniCloud:理想的解决方案
我们选择了 uni-app + uniCloud 这一黄金组合作为 CommunityHub 的技术底座,侧重于开发效率和后端弹性:
-
uni-app:高效跨端开发,一次编码,多端发行,开发效率高。基于 Web 技术的快速适配和高效迭代能力,能够迅速发布应用,并确保基础体验。
-
uniCloud:Serverless 后端,免运维、弹性伸缩,提供数据安全保障。极简 API 调用,为前端提供了统一、稳定的数据接口。
二、鸿蒙 OS 适配与生态融合策略
要使 CommunityHub 在鸿蒙生态中获得成功,我们采取了“高效适配先行,生态融合跟进”的策略。
2.1 策略一:统一的响应式布局,适配多设备形态
作为社区管理系统,CommunityHub 需兼顾居民的手机端和管理员的 PC/平板大屏端体验,充分利用了 uni-app 强大的响应式能力。
设计理念: 利用 uni-app 的条件编译和 CSS 媒体查询功能,配合简洁的组件化设计,实现 UI 的自适应重构。
- 手机小屏(<768px): 采用底部 TabBar 导航,信息流以列表卡片形式展示,突出点击和触控体验。
- PC/平板大屏(>1200px): 通过leftWindow切换为侧边栏导航和分栏布局。例如“报修管理”页面,在大屏上可以同时显示报修列表、详情和派单操作区,极大提升了管理员的效率。
这种“一套代码,两种形态”的响应式开发策略,最大限度地复用了代码,为未来应用在鸿蒙多终端上的部署打下了坚实基础。
2.2 策略二:面向鸿蒙 NEXT 生态的融合与升级路径
1. 鸿蒙元服务的支持
优势: 得益于 uni-app 对鸿蒙生态的深度支持,标准 uni-app 项目可以直接发行到鸿蒙元服务。
这极大地简化了开发流程,实现了主 App 与元服务代码基础的统一。
- 解决方案: 我们采用 “uni-app 快速编译 + uniCloud 数据驱动” 的
DCloud全家桶策略。 - 元服务开发: 在 uni-app 项目中,使用条件编译或单独页面,针对性地开发轻量级的“快捷报修提交”功能模块。
- 快速部署与统一代码: 将该模块通过 uni-app 编译部署为鸿蒙元服务,确保其 UI 和逻辑与主 App 保持高度一致,并享受 uni-app 的快速迭代优势。
- 效果: 实现了“一次编码,多端复用”在 App 和元服务上的实践,确保了“即用即走”的用户体验,同时将复杂数据和业务逻辑留在了 uniCloud 统一后端,极大提升了社区服务的便捷性和开发效率。
2. 紧急公告卡片(服务卡片)设想:
对于“停水通知”等紧急公告,我们规划利用鸿蒙的服务卡片能力,通过原生卡片实时拉取 uniCloud 的最新公告数据,直接在用户桌面上显示公告摘要,确保紧急消息的触达率。
参考资料:鸿蒙元服务专题
三、关键功能实现与 uniCloud 赋能
3.1 高效报修与权限控制
核心逻辑:
- 权限与通知: 通过 uni-id 的角色体系(居民/物业/维修人员)校验身份,并将任务自动派发给对应角色的物业管理员。
- 进度跟踪: 利用 uni-push,居民端能实时获取报修单状态(已派单、处理中、已完成)的变更,保证了用户体验的实时性。
3.2 邻里交流与资源共享的数据安全
“邻里圈”和“资源共享”模块利用 uniCloud 强大的安全能力:
我们使用 uniCloud 的 云存储 存储用户上传的动态图片和视频,利用 权限规则 确保居民之间数据隔离。
对于“私信联系”功能,基于 uni-id 实现了安全的用户身份认证和通信机制,保护用户隐私。
3.3 技术特点
本系统全部基于uni-app内置组件和uni-ui扩展组件实现,尽可能使用图标代替图片,减少资源包大小,提升小程序的冷启动时间。
如下是对tabbar的一个配置,我们使用了uniicons.ttf,没有使用任何额外图片。
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#007AFF",
"borderStyle": "black",
"backgroundColor": "#FFFFFF",
"iconfontSrc": "uni_modules/uni-icons/components/uni-icons/uniicons.ttf",
"list": [{
"pagePath": "pages/home/home",
"iconfont": {
"text": "\ue662",
"selectedText": "\ue663",
"fontSize": "22px",
"color": "#7A7E83",
"selectedColor": "#007AFF"
},
"text": "首页"
}, {
"pagePath": "pages/community/post-list",
"iconfont": {
"text": "\ue682",
"selectedText": "\ue682",
"fontSize": "22px",
"color": "#7A7E83",
"selectedColor": "#007AFF"
},
"text": "邻里圈"
}, {
"pagePath": "pages/message/message-list",
"iconfont": {
"text": "\ue6a6",
"selectedText": "\ue6c1",
"fontSize": "22px",
"color": "#7A7E83",
"selectedColor": "#007AFF"
},
"text": "消息"
}, {
"pagePath": "pages/ucenter/ucenter",
"iconfont": {
"text": "\ue699",
"selectedText": "\ue69d",
"fontSize": "22px",
"color": "#7A7E83",
"selectedColor": "#007AFF"
},
"text": "我的"
}]
}
四、总结与展望
感谢DCloud提供的uni-app + uniCloud纯前端方案,我们公司2个前端工程师就把整个项目上线了,系统开发周期大幅缩短,并保证了多端体验的统一性。
展望未来: 我们正积极关注 uni-app x 对鸿蒙 NEXT 的支持进度,并计划将 CommunityHub 逐步迁移至 uni-app x,彻底实现原生渲染,以达到最终的性能目标。
同时,我们也在打磨该系统的标准化,希望发布到 DCloud插件市场,供更多社区使用,或者开源出来。
前言
公司前段时间接了一个社区管理的项目,看到本次 鸿蒙征文 活动,正好做一次总结。
我们基于 uni-app 跨端框架和 uniCloud Serverless服务,开发“CommunityHub 社区管理系统”。该系统旨在连接社区物业与居民。
我会在本文中重点阐述了如何利用 uni-app 实现应用在鸿蒙 OS 上的快速适配、多设备响应式布局、已经App和鸿蒙元服务共享代码。
一、技术选型与项目概述
1.1 社区数字化面临的挑战
接到需求后,我们分析,主要有两个挑战:
- 多端适配: 社区服务需要覆盖居民的手机、平板乃至管理员的PC,且在当下中国,鸿蒙的兼容是必须考虑的平台,维护多套代码成本极高。
- 后端运维: 社区系统,日常请求量并不高,但需满足特殊情况下的高并发处理,需要稳定且易运维的后端支持。
1.2 uni-app + uniCloud:理想的解决方案
我们选择了 uni-app + uniCloud 这一黄金组合作为 CommunityHub 的技术底座,侧重于开发效率和后端弹性:
-
uni-app:高效跨端开发,一次编码,多端发行,开发效率高。基于 Web 技术的快速适配和高效迭代能力,能够迅速发布应用,并确保基础体验。
-
uniCloud:Serverless 后端,免运维、弹性伸缩,提供数据安全保障。极简 API 调用,为前端提供了统一、稳定的数据接口。
二、鸿蒙 OS 适配与生态融合策略
要使 CommunityHub 在鸿蒙生态中获得成功,我们采取了“高效适配先行,生态融合跟进”的策略。
2.1 策略一:统一的响应式布局,适配多设备形态
作为社区管理系统,CommunityHub 需兼顾居民的手机端和管理员的 PC/平板大屏端体验,充分利用了 uni-app 强大的响应式能力。
设计理念: 利用 uni-app 的条件编译和 CSS 媒体查询功能,配合简洁的组件化设计,实现 UI 的自适应重构。
- 手机小屏(<768px): 采用底部 TabBar 导航,信息流以列表卡片形式展示,突出点击和触控体验。
- PC/平板大屏(>1200px): 通过leftWindow切换为侧边栏导航和分栏布局。例如“报修管理”页面,在大屏上可以同时显示报修列表、详情和派单操作区,极大提升了管理员的效率。
这种“一套代码,两种形态”的响应式开发策略,最大限度地复用了代码,为未来应用在鸿蒙多终端上的部署打下了坚实基础。
2.2 策略二:面向鸿蒙 NEXT 生态的融合与升级路径
1. 鸿蒙元服务的支持
优势: 得益于 uni-app 对鸿蒙生态的深度支持,标准 uni-app 项目可以直接发行到鸿蒙元服务。
这极大地简化了开发流程,实现了主 App 与元服务代码基础的统一。
- 解决方案: 我们采用 “uni-app 快速编译 + uniCloud 数据驱动” 的
DCloud全家桶策略。 - 元服务开发: 在 uni-app 项目中,使用条件编译或单独页面,针对性地开发轻量级的“快捷报修提交”功能模块。
- 快速部署与统一代码: 将该模块通过 uni-app 编译部署为鸿蒙元服务,确保其 UI 和逻辑与主 App 保持高度一致,并享受 uni-app 的快速迭代优势。
- 效果: 实现了“一次编码,多端复用”在 App 和元服务上的实践,确保了“即用即走”的用户体验,同时将复杂数据和业务逻辑留在了 uniCloud 统一后端,极大提升了社区服务的便捷性和开发效率。
2. 紧急公告卡片(服务卡片)设想:
对于“停水通知”等紧急公告,我们规划利用鸿蒙的服务卡片能力,通过原生卡片实时拉取 uniCloud 的最新公告数据,直接在用户桌面上显示公告摘要,确保紧急消息的触达率。
参考资料:鸿蒙元服务专题
三、关键功能实现与 uniCloud 赋能
3.1 高效报修与权限控制
核心逻辑:
- 权限与通知: 通过 uni-id 的角色体系(居民/物业/维修人员)校验身份,并将任务自动派发给对应角色的物业管理员。
- 进度跟踪: 利用 uni-push,居民端能实时获取报修单状态(已派单、处理中、已完成)的变更,保证了用户体验的实时性。
3.2 邻里交流与资源共享的数据安全
“邻里圈”和“资源共享”模块利用 uniCloud 强大的安全能力:
我们使用 uniCloud 的 云存储 存储用户上传的动态图片和视频,利用 权限规则 确保居民之间数据隔离。
对于“私信联系”功能,基于 uni-id 实现了安全的用户身份认证和通信机制,保护用户隐私。
3.3 技术特点
本系统全部基于uni-app内置组件和uni-ui扩展组件实现,尽可能使用图标代替图片,减少资源包大小,提升小程序的冷启动时间。
如下是对tabbar的一个配置,我们使用了uniicons.ttf,没有使用任何额外图片。
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#007AFF",
"borderStyle": "black",
"backgroundColor": "#FFFFFF",
"iconfontSrc": "uni_modules/uni-icons/components/uni-icons/uniicons.ttf",
"list": [{
"pagePath": "pages/home/home",
"iconfont": {
"text": "\ue662",
"selectedText": "\ue663",
"fontSize": "22px",
"color": "#7A7E83",
"selectedColor": "#007AFF"
},
"text": "首页"
}, {
"pagePath": "pages/community/post-list",
"iconfont": {
"text": "\ue682",
"selectedText": "\ue682",
"fontSize": "22px",
"color": "#7A7E83",
"selectedColor": "#007AFF"
},
"text": "邻里圈"
}, {
"pagePath": "pages/message/message-list",
"iconfont": {
"text": "\ue6a6",
"selectedText": "\ue6c1",
"fontSize": "22px",
"color": "#7A7E83",
"selectedColor": "#007AFF"
},
"text": "消息"
}, {
"pagePath": "pages/ucenter/ucenter",
"iconfont": {
"text": "\ue699",
"selectedText": "\ue69d",
"fontSize": "22px",
"color": "#7A7E83",
"selectedColor": "#007AFF"
},
"text": "我的"
}]
}
四、总结与展望
感谢DCloud提供的uni-app + uniCloud纯前端方案,我们公司2个前端工程师就把整个项目上线了,系统开发周期大幅缩短,并保证了多端体验的统一性。
展望未来: 我们正积极关注 uni-app x 对鸿蒙 NEXT 的支持进度,并计划将 CommunityHub 逐步迁移至 uni-app x,彻底实现原生渲染,以达到最终的性能目标。
同时,我们也在打磨该系统的标准化,希望发布到 DCloud插件市场,供更多社区使用,或者开源出来。
收起阅读 »ffmpeg 16KB问题
最近google市场要求16KB内存对齐的问题,市场上大多数的ffmpeg包都不支持;
我找到了一个,请在android studio中的build.gradle中增加
implementation 'com.moizhassan.ffmpeg:ffmpeg-kit-16kb:6.0.0'
通过google脚本测试,该包已修复16KB问题;
附件是我用ffmpeg写好的音视频处理页面;
最近google市场要求16KB内存对齐的问题,市场上大多数的ffmpeg包都不支持;
我找到了一个,请在android studio中的build.gradle中增加
implementation 'com.moizhassan.ffmpeg:ffmpeg-kit-16kb:6.0.0'
通过google脚本测试,该包已修复16KB问题;
附件是我用ffmpeg写好的音视频处理页面;
收起阅读 »HBuilder 上架 iOS 应用全流程指南:从云打包到开心上架(Appuploader)上传的跨平台发布实践
'''随着 uni-app 与 HBuilderX 的普及,越来越多的前端开发者开始进入移动应用开发领域。
借助 HBuilder 的云打包服务,开发者可以在不使用 Xcode 的情况下,快速生成 iOS 的 .ipa 包。
但问题随之而来:许多团队没有 Mac 电脑,也无法使用 Xcode 或 Transporter 完成 App Store 上传。
开心上架(Appuploader)能在 Windows / Linux / macOS 系统中直接上传 IPA,并支持证书创建、描述文件管理和多语言信息批量提交。
本文将演示:从 HBuilder 打包到 iOS 应用上架的全流程,并介绍如何通过 Appuploader 实现免 Mac 跨平台上架。
一、为什么选择 HBuilder 打包 iOS 应用?
HBuilder 是 DCloud 推出的跨平台开发工具,支持 HTML5、Vue、uni-app 等多框架项目,
通过 云打包服务 自动生成 Android APK 与 iOS IPA 包。
优势总结:
| 特点 | 说明 |
|---|---|
| 无需本地 Xcode 环境 | 由云端完成编译与签名 |
| 跨平台开发 | 前端技术栈(Vue + JS)快速上手 |
| 支持插件扩展 | 可集成本地 SDK 与原生模块 |
| App Store 上架兼容 | 云打包输出的 IPA 可直接提交审核 |
对前端开发者而言,HBuilder 是通往原生应用开发与上架的理想桥梁。
二、HBuilder 云打包生成 IPA 文件
步骤 1:配置应用信息
在 HBuilderX 中打开项目,点击顶部菜单:
发行 → 云打包 → iOS 应用
填写以下信息:
- 应用名称、Bundle ID(需与 Apple Developer 保持一致);
- 图标、启动图;
- 版本号、应用描述。
步骤 2:选择证书模式
HBuilder 支持两种方式:
使用自己的苹果证书(需上传 .p12 与描述文件);
使用 DCloud 提供的公用证书(仅用于测试,不建议用于正式上架)。
步骤 3:打包完成后下载 .ipa 文件
系统会生成一个可安装或上架的 iOS 安装包。
示例文件路径:
./unpackage/release/ios/APP_NAME.ipa
三、准备 App Store 上传所需条件
要将 IPA 上架到 App Store,需要以下三项内容:
| 项目 | 说明 |
|---|---|
| Apple 开发者账号 | 年费 99 美元(个人或企业) |
| App 专用密码 | 上传时使用,保护主账号安全 |
| 应用元数据 | 名称、简介、截图、隐私政策等 |
若没有 Mac,可完全依靠开心上架(Appuploader)进行后续操作。
四、开心上架(Appuploader)简介与核心功能
新版 开心上架(Appuploader) 是一款跨平台的 iOS 应用上架工具,
可替代 Application Loader、Transporter 等官方工具,支持 GUI 与命令行双模式。
核心特性:
| 功能 | 说明 |
|---|---|
| 跨平台支持 | 兼容 Windows、Linux、macOS |
| 上传 IPA | 直接将 IPA 文件提交 App Store Connect |
| 证书生成与管理 | 支持开发、发布、推送证书一键生成 |
| 多语言与截图上传 | 批量上传多语言描述与截图 |
| 命令行模式 | 适合自动化部署与持续集成 |
五、使用开心上架上传 HBuilder 生成的 IPA 文件
图形界面方式(推荐给新手):
打开 开心上架;
登录 Apple 开发者账号;
点击「上传 IPA」;
选择打包生成的 .ipa 文件;
等待上传完成后,即可在 App Store Connect 中看到应用信息。
命令行方式(适合开发者):
appuploader_cli -u dev@icloud.com -p xxx-xxx-xxx-xxx -c 2 -f ./unpackage/release/ios/myapp.ipa
参数说明:
| 参数 | 含义 |
|---|---|
-u |
Apple 开发者账号 |
-p |
App 专用密码 |
-c |
上传通道(1=旧通道,2=新通道) |
-f |
要上传的 IPA 文件路径 |
执行结果:
- 自动建立连接;
- 上传并验证包体信息;
- 输出上传日志与状态报告。
六、App Store Connect 审核流程简述
IPA 上传成功后,需在 App Store Connect 填写以下内容:
| 项目 | 说明 |
|---|---|
| 应用名称 | 上架显示名称 |
| 隐私政策 | 必填链接 |
| 截图 | 支持多设备尺寸上传 |
| 关键词与描述 | 提高搜索曝光 |
| 审核提交 | 点击“提交审核”按钮 |
审核时间:
- 一般应用: 1–3 天;
- 含支付、推送等功能: 3–7 天。
七、跨平台上架实践案例
某 uni-app 团队在 Windows 环境中使用以下流程完成 iOS 上架:
使用 HBuilder 云打包生成 .ipa;
在 Appuploader 中创建 iOS 发布证书;
执行上传命令:
appuploader_cli -u ios@team.com -p xxxx-xxxx-xxxx -c 2 -f ./unpackage/release/ios/teamapp.ipa
登录 App Store Connect 填写资料并提交审核。
全流程无需 Mac,整个过程耗时不足两小时。
八、常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 上传报错 “Invalid Credentials” | 密码错误 | 使用 App 专用密码 |
| IPA 无法识别 | 打包方式不正确 | 使用正式证书重新打包 |
| 上传超时 | 网络不稳定 | 切换上传通道 -c 1/2 |
| 审核被拒 | 应用隐私不合规 | 补充隐私说明与权限用途 |
| 证书过期 | 签名证书已失效 | 在 Appuploader 中重新生成 |
九、结合 Fastlane 与 Appuploader 实现自动上架
对于团队项目,可进一步将 Appuploader 集成到 Fastlane 或 Jenkins CI 流程中,实现自动化上架。
示例命令:
fastlane gym --scheme "MyApp"
appuploader_cli -u dev@icloud.com -p xxx-xxx-xxx-xxx -c 2 -f ./build/MyApp.ipa
优势:
- 自动构建 + 上传;
- 支持多版本号与自动日志记录;
- 适用于 Windows 与 Linux CI 环境。
通过 HBuilder + 开心上架(Appuploader) 的组合,前端开发者与跨平台团队无需 Mac 电脑,也能高效完成 iOS 应用上架流程。
HBuilder 负责高效云打包,Appuploader 负责证书管理与上传发布,两者协同,构建出真正的 “跨系统、全自动化上架方案”。
从写代码到上架 App Store,你只需一台电脑,不必是 Mac。'''
'''随着 uni-app 与 HBuilderX 的普及,越来越多的前端开发者开始进入移动应用开发领域。
借助 HBuilder 的云打包服务,开发者可以在不使用 Xcode 的情况下,快速生成 iOS 的 .ipa 包。
但问题随之而来:许多团队没有 Mac 电脑,也无法使用 Xcode 或 Transporter 完成 App Store 上传。
开心上架(Appuploader)能在 Windows / Linux / macOS 系统中直接上传 IPA,并支持证书创建、描述文件管理和多语言信息批量提交。
本文将演示:从 HBuilder 打包到 iOS 应用上架的全流程,并介绍如何通过 Appuploader 实现免 Mac 跨平台上架。
一、为什么选择 HBuilder 打包 iOS 应用?
HBuilder 是 DCloud 推出的跨平台开发工具,支持 HTML5、Vue、uni-app 等多框架项目,
通过 云打包服务 自动生成 Android APK 与 iOS IPA 包。
优势总结:
| 特点 | 说明 |
|---|---|
| 无需本地 Xcode 环境 | 由云端完成编译与签名 |
| 跨平台开发 | 前端技术栈(Vue + JS)快速上手 |
| 支持插件扩展 | 可集成本地 SDK 与原生模块 |
| App Store 上架兼容 | 云打包输出的 IPA 可直接提交审核 |
对前端开发者而言,HBuilder 是通往原生应用开发与上架的理想桥梁。
二、HBuilder 云打包生成 IPA 文件
步骤 1:配置应用信息
在 HBuilderX 中打开项目,点击顶部菜单:
发行 → 云打包 → iOS 应用
填写以下信息:
- 应用名称、Bundle ID(需与 Apple Developer 保持一致);
- 图标、启动图;
- 版本号、应用描述。
步骤 2:选择证书模式
HBuilder 支持两种方式:
使用自己的苹果证书(需上传 .p12 与描述文件);
使用 DCloud 提供的公用证书(仅用于测试,不建议用于正式上架)。
步骤 3:打包完成后下载 .ipa 文件
系统会生成一个可安装或上架的 iOS 安装包。
示例文件路径:
./unpackage/release/ios/APP_NAME.ipa
三、准备 App Store 上传所需条件
要将 IPA 上架到 App Store,需要以下三项内容:
| 项目 | 说明 |
|---|---|
| Apple 开发者账号 | 年费 99 美元(个人或企业) |
| App 专用密码 | 上传时使用,保护主账号安全 |
| 应用元数据 | 名称、简介、截图、隐私政策等 |
若没有 Mac,可完全依靠开心上架(Appuploader)进行后续操作。
四、开心上架(Appuploader)简介与核心功能
新版 开心上架(Appuploader) 是一款跨平台的 iOS 应用上架工具,
可替代 Application Loader、Transporter 等官方工具,支持 GUI 与命令行双模式。
核心特性:
| 功能 | 说明 |
|---|---|
| 跨平台支持 | 兼容 Windows、Linux、macOS |
| 上传 IPA | 直接将 IPA 文件提交 App Store Connect |
| 证书生成与管理 | 支持开发、发布、推送证书一键生成 |
| 多语言与截图上传 | 批量上传多语言描述与截图 |
| 命令行模式 | 适合自动化部署与持续集成 |
五、使用开心上架上传 HBuilder 生成的 IPA 文件
图形界面方式(推荐给新手):
打开 开心上架;
登录 Apple 开发者账号;
点击「上传 IPA」;
选择打包生成的 .ipa 文件;
等待上传完成后,即可在 App Store Connect 中看到应用信息。
命令行方式(适合开发者):
appuploader_cli -u dev@icloud.com -p xxx-xxx-xxx-xxx -c 2 -f ./unpackage/release/ios/myapp.ipa
参数说明:
| 参数 | 含义 |
|---|---|
-u |
Apple 开发者账号 |
-p |
App 专用密码 |
-c |
上传通道(1=旧通道,2=新通道) |
-f |
要上传的 IPA 文件路径 |
执行结果:
- 自动建立连接;
- 上传并验证包体信息;
- 输出上传日志与状态报告。
六、App Store Connect 审核流程简述
IPA 上传成功后,需在 App Store Connect 填写以下内容:
| 项目 | 说明 |
|---|---|
| 应用名称 | 上架显示名称 |
| 隐私政策 | 必填链接 |
| 截图 | 支持多设备尺寸上传 |
| 关键词与描述 | 提高搜索曝光 |
| 审核提交 | 点击“提交审核”按钮 |
审核时间:
- 一般应用: 1–3 天;
- 含支付、推送等功能: 3–7 天。
七、跨平台上架实践案例
某 uni-app 团队在 Windows 环境中使用以下流程完成 iOS 上架:
使用 HBuilder 云打包生成 .ipa;
在 Appuploader 中创建 iOS 发布证书;
执行上传命令:
appuploader_cli -u ios@team.com -p xxxx-xxxx-xxxx -c 2 -f ./unpackage/release/ios/teamapp.ipa
登录 App Store Connect 填写资料并提交审核。
全流程无需 Mac,整个过程耗时不足两小时。
八、常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 上传报错 “Invalid Credentials” | 密码错误 | 使用 App 专用密码 |
| IPA 无法识别 | 打包方式不正确 | 使用正式证书重新打包 |
| 上传超时 | 网络不稳定 | 切换上传通道 -c 1/2 |
| 审核被拒 | 应用隐私不合规 | 补充隐私说明与权限用途 |
| 证书过期 | 签名证书已失效 | 在 Appuploader 中重新生成 |
九、结合 Fastlane 与 Appuploader 实现自动上架
对于团队项目,可进一步将 Appuploader 集成到 Fastlane 或 Jenkins CI 流程中,实现自动化上架。
示例命令:
fastlane gym --scheme "MyApp"
appuploader_cli -u dev@icloud.com -p xxx-xxx-xxx-xxx -c 2 -f ./build/MyApp.ipa
优势:
- 自动构建 + 上传;
- 支持多版本号与自动日志记录;
- 适用于 Windows 与 Linux CI 环境。
通过 HBuilder + 开心上架(Appuploader) 的组合,前端开发者与跨平台团队无需 Mac 电脑,也能高效完成 iOS 应用上架流程。
HBuilder 负责高效云打包,Appuploader 负责证书管理与上传发布,两者协同,构建出真正的 “跨系统、全自动化上架方案”。
从写代码到上架 App Store,你只需一台电脑,不必是 Mac。'''
收起阅读 »关于使用Map组件截图技术方案
看网上好多技术方案都是使用集成原生插件,然后在原生插件里面生成截图,而且uniapp插件库里面的好几个这样的插件都需要收费。
如果大家对图片要求不高的话可以关注一些“天地图“的api,他们有根据坐标生成静态图片的接口,下面是链接地址
http://lbs.tianditu.gov.cn/staticapi/static.html
本人亲测可用
看网上好多技术方案都是使用集成原生插件,然后在原生插件里面生成截图,而且uniapp插件库里面的好几个这样的插件都需要收费。
如果大家对图片要求不高的话可以关注一些“天地图“的api,他们有根据坐标生成静态图片的接口,下面是链接地址
http://lbs.tianditu.gov.cn/staticapi/static.html
本人亲测可用
【报Bug】uni @touchmove 事件与 下拉刷新事件冲突
【报Bug】uni @touchmove 事件与 下拉刷新事件冲突
这么多年的bug了为啥不解决呢, 很影响体验啊。
【报Bug】uni @touchmove 事件与 下拉刷新事件冲突
这么多年的bug了为啥不解决呢, 很影响体验啊。
UniApp开发鸿蒙应用实践:从底部小白条到代理提醒的完整解决方案
前言
随着鸿蒙生态的快速发展,越来越多的开发者开始关注如何快速将现有应用适配到鸿蒙平台。UniApp作为一个成熟的跨平台开发框架,已经支持了鸿蒙应用的开发。然而在实际开发过程中,我们会遇到一些平台特有的问题和挑战。本文将基于一个服药提醒应用的开发实践,详细介绍如何解决鸿蒙平台的底部安全区适配问题,以及如何使用UTS插件实现代理提醒功能。
一、底部小白条问题的解决方案(SafeArea适配)
1.1 问题背景
在鸿蒙设备上,特别是全面屏设备,底部通常会有一个用于手势导航的"小白条"区域。这个区域被称为安全区域(Safe Area),如果应用界面没有正确处理这个区域,会导致底部内容被遮挡,影响用户体验。
1.2 SafeArea配置方案
UniApp为鸿蒙平台提供了完善的安全区域配置方案。我们需要在manifest.json文件中进行配置:
{
"app-harmony": {
"safearea": {
"background": "#ffffff",
"backgroundDark": "#2f0508",
"bottom": {
"offset": "none"
}
}
}
}
配置说明:
- background: 浅色模式下安全区域的背景色,这里设置为白色
#ffffff,与应用主题保持一致 - backgroundDark: 深色模式下安全区域的背景色,设置为深红色
#2f0508,适配暗黑主题 - bottom.offset: 底部区域占位策略,设置为
"none"表示在没有TabBar时不需要占位
我们成功解决了底部小白条的适配问题,确保应用在各种鸿蒙设备上都能正常显示和交互。
二、代理提醒功能的UTS插件实现
2.1 代理提醒简介
代理提醒(Reminder Agent)是鸿蒙系统提供的一项重要能力,允许应用在后台设置定时提醒,即使应用被关闭也能准时触发。这对于服药提醒、日程管理等场景至关重要。
鸿蒙系统的代理提醒支持三种类型:
- 闹钟提醒(ALARM): 基于时间的周期性提醒,适合每天固定时间的场景
- 日历提醒(CALENDAR): 基于日期的事件提醒,适合特定日期的事件
- 倒计时提醒(TIMER): 基于时长的一次性提醒,适合短期计时场景
2.2 为什么需要UTS插件
UniApp虽然支持鸿蒙平台,但并未内置代理提醒的API。要使用鸿蒙的原生能力,我们需要通过UTS(UniApp TypeScript)插件来调用鸿蒙的原生API。
UTS插件的优势:
- 类型安全: 基于TypeScript,提供完整的类型检查
- 性能优秀: 编译为原生代码,性能接近原生开发
- 开发便捷: 语法接近TypeScript,学习成本低
- 跨平台: 可以为不同平台编写不同的实现
2.3 UTS插件项目结构
我们创建了一个名为uni-reminder的UTS插件,项目结构如下:
uni_modules/
└── uni-reminder/
├── utssdk/
│ ├── app-harmony/ # 鸿蒙平台实现
│ │ └── index.uts # 主要实现文件
│ └── interface.uts # 接口定义文件
├── package.json
└── readme.md
关键文件说明:
- interface.uts: 定义插件的接口类型,包括所有方法的参数和回调类型
- app-harmony/index.uts: 鸿蒙平台的具体实现,调用鸿蒙原生API
2.4 接口定义(interface.uts)
首先,我们需要定义清晰的接口类型。以闹钟提醒为例:
// 闹钟提醒选项
export type PublishAlarmReminderOptions = {
hour: number // 闹钟小时数(0-23)
minute: number // 闹钟分钟数(0-59)
daysOfWeek?: Array<number> | null // 重复日期,周日=0
title?: string | null // 提醒标题
content?: string | null // 提醒内容
success?: PublishAlarmReminderSuccessCallback | null
fail?: PublishAlarmReminderFailCallback | null
complete?: PublishAlarmReminderCompleteCallback | null
}
// 成功回调返回值
export type PublishAlarmReminderSuccess = {
errMsg: string
reminderId: number // 提醒ID,用于后续取消
}
// 定义uni对象上的方法
export interface Uni {
publishAlarmReminder(options: PublishAlarmReminderOptions): void
publishCalendarReminder(options: PublishCalendarReminderOptions): void
publishTimerReminder(options: PublishTimerReminderOptions): void
cancelReminder(options: CancelReminderOptions): void
cancelAllReminders(options: CancelAllRemindersOptions): void
getValidReminders(options: GetValidRemindersOptions): void
}
这种接口定义方式遵循了UniApp的API设计规范,使用success/fail/complete回调模式,保持了与UniApp其他API的一致性。
2.5 鸿蒙平台核心实现
2.5.1 导入鸿蒙原生API
import reminderAgentManager from '@ohos.reminderAgentManager'
import { BusinessError } from '@kit.BasicServicesKit'
- reminderAgentManager: 鸿蒙的代理提醒管理器
- BusinessError: 鸿蒙的业务错误类型
2.5.2 实现闹钟提醒
export function publishAlarmReminder(options: PublishAlarmReminderOptions): void {
try {
// 构建闹钟提醒请求对象
const requestObj = {
reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_ALARM,
hour: options.hour,
minute: options.minute,
title: options.title || '闹钟提醒',
content: options.content || '该起床了',
wantAgent: {
pkgName: 'com.liudd1.anshichiyao',
abilityName: 'EntryAbility'
},
actionButton: [{
title: '关闭',
type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE
}]
} as UTSJSONObject
// 设置重复日期
if (options.daysOfWeek && options.daysOfWeek.length > 0) {
requestObj['daysOfWeek'] = options.daysOfWeek
}
// 发布提醒
reminderAgentManager.publishReminder(
requestObj as unknown as reminderAgentManager.ReminderRequest
)
.then((reminderId: number) => {
options?.success?.({
errMsg: 'publishAlarmReminder:ok',
reminderId: reminderId
})
options?.complete?.({ errMsg: 'publishAlarmReminder:ok' })
})
.catch((err: BusinessError) => {
options?.fail?.({ errMsg: `publishAlarmReminder:fail ${err.message}` })
options?.complete?.({ errMsg: `publishAlarmReminder:fail ${err.message}` })
})
} catch (err) {
const error = err as BusinessError
options?.fail?.({ errMsg: `publishAlarmReminder:fail ${error.message}` })
options?.complete?.({ errMsg: `publishAlarmReminder:fail ${error.message}` })
}
}
实现要点:
- reminderType: 指定提醒类型为闹钟
- wantAgent: 配置点击提醒后要启动的应用和页面
- actionButton: 添加操作按钮
- daysOfWeek: 可选的重复日期数组
- 错误处理: 使用try-catch和Promise.catch双重错误处理
2.5.3 实现日历提醒
export function publishCalendarReminder(options: PublishCalendarReminderOptions): void {
try {
const date = new Date(options.dateTime)
const requestObj = {
reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_CALENDAR,
dateTime: {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
hour: date.getHours(),
minute: date.getMinutes()
},
title: options.title || '日历提醒',
content: options.content || '事件提醒',
wantAgent: {
pkgName: 'com.liudd1.anshichiyao',
abilityName: 'EntryAbility'
},
actionButton: [{
title: '关闭',
type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE
}]
} as UTSJSONObject
// 设置重复月份和日期
if (options.repeatMonths && options.repeatMonths.length > 0) {
requestObj['repeatMonths'] = options.repeatMonths
}
if (options.repeatDays && options.repeatDays.length > 0) {
requestObj['repeatDays'] = options.repeatDays
}
// 发布提醒(Promise处理逻辑同闹钟提醒)
// ...
} catch (err) {
// 错误处理
}
}
2.5.4 实现倒计时提醒
export function publishTimerReminder(options: PublishTimerReminderOptions): void {
try {
const requestObj = {
reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_TIMER,
triggerTimeInSeconds: options.triggerTimeInSeconds,
title: options.title || '倒计时提醒',
content: options.content || '倒计时已到',
wantAgent: {
pkgName: 'com.liudd1.anshichiyao',
abilityName: 'EntryAbility'
},
actionButton: [{
title: '关闭',
type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE
}]
} as UTSJSONObject
// 发布提醒
// ...
} catch (err) {
// 错误处理
}
}
2.6 权限配置
使用代理提醒功能需要在manifest.json中声明权限:
{
"app-harmony": {
"permissions": [
"ohos.permission.PUBLISH_AGENT_REMINDER"
]
}
}
同时配合通知权限插件请求用户授权:
uni.requestNotification({
success: (res) => {
if (res.granted) {
console.log('通知权限已获取')
}
}
})
2.7 在应用中使用插件
插件开发完成后,在应用中的使用非常简单:
export default {
data() {
return {
reminderIds: []
}
},
methods: {
// 设置每天早上8点的服药提醒
setMedicineReminder() {
uni.publishAlarmReminder({
hour: 8,
minute: 0,
daysOfWeek: [1, 2, 3, 4, 5, 6, 0],
title: '服药提醒',
content: '该吃药了,记得按时服药哦!',
success: (res) => {
console.log('提醒设置成功,ID:', res.reminderId)
this.reminderIds.push(res.reminderId)
uni.showToast({ title: '提醒设置成功' })
},
fail: (err) => {
console.error('提醒设置失败:', err.errMsg)
}
})
},
// 设置30分钟后的倒计时提醒
setTimerReminder() {
uni.publishTimerReminder({
triggerTimeInSeconds: 30 * 60,
title: '服药倒计时',
content: '30分钟已到,该服药了',
success: (res) => {
this.reminderIds.push(res.reminderId)
}
})
},
// 取消所有提醒
cancelAllReminders() {
uni.cancelAllReminders({
success: () => {
this.reminderIds = []
uni.showToast({ title: '已清空所有提醒' })
}
})
}
}
}
希望本文的分享能够帮助到正在开发鸿蒙应用的开发者们,让我们一起为鸿蒙生态的繁荣贡献力量!
前言
随着鸿蒙生态的快速发展,越来越多的开发者开始关注如何快速将现有应用适配到鸿蒙平台。UniApp作为一个成熟的跨平台开发框架,已经支持了鸿蒙应用的开发。然而在实际开发过程中,我们会遇到一些平台特有的问题和挑战。本文将基于一个服药提醒应用的开发实践,详细介绍如何解决鸿蒙平台的底部安全区适配问题,以及如何使用UTS插件实现代理提醒功能。
一、底部小白条问题的解决方案(SafeArea适配)
1.1 问题背景
在鸿蒙设备上,特别是全面屏设备,底部通常会有一个用于手势导航的"小白条"区域。这个区域被称为安全区域(Safe Area),如果应用界面没有正确处理这个区域,会导致底部内容被遮挡,影响用户体验。
1.2 SafeArea配置方案
UniApp为鸿蒙平台提供了完善的安全区域配置方案。我们需要在manifest.json文件中进行配置:
{
"app-harmony": {
"safearea": {
"background": "#ffffff",
"backgroundDark": "#2f0508",
"bottom": {
"offset": "none"
}
}
}
}
配置说明:
- background: 浅色模式下安全区域的背景色,这里设置为白色
#ffffff,与应用主题保持一致 - backgroundDark: 深色模式下安全区域的背景色,设置为深红色
#2f0508,适配暗黑主题 - bottom.offset: 底部区域占位策略,设置为
"none"表示在没有TabBar时不需要占位
我们成功解决了底部小白条的适配问题,确保应用在各种鸿蒙设备上都能正常显示和交互。
二、代理提醒功能的UTS插件实现
2.1 代理提醒简介
代理提醒(Reminder Agent)是鸿蒙系统提供的一项重要能力,允许应用在后台设置定时提醒,即使应用被关闭也能准时触发。这对于服药提醒、日程管理等场景至关重要。
鸿蒙系统的代理提醒支持三种类型:
- 闹钟提醒(ALARM): 基于时间的周期性提醒,适合每天固定时间的场景
- 日历提醒(CALENDAR): 基于日期的事件提醒,适合特定日期的事件
- 倒计时提醒(TIMER): 基于时长的一次性提醒,适合短期计时场景
2.2 为什么需要UTS插件
UniApp虽然支持鸿蒙平台,但并未内置代理提醒的API。要使用鸿蒙的原生能力,我们需要通过UTS(UniApp TypeScript)插件来调用鸿蒙的原生API。
UTS插件的优势:
- 类型安全: 基于TypeScript,提供完整的类型检查
- 性能优秀: 编译为原生代码,性能接近原生开发
- 开发便捷: 语法接近TypeScript,学习成本低
- 跨平台: 可以为不同平台编写不同的实现
2.3 UTS插件项目结构
我们创建了一个名为uni-reminder的UTS插件,项目结构如下:
uni_modules/
└── uni-reminder/
├── utssdk/
│ ├── app-harmony/ # 鸿蒙平台实现
│ │ └── index.uts # 主要实现文件
│ └── interface.uts # 接口定义文件
├── package.json
└── readme.md
关键文件说明:
- interface.uts: 定义插件的接口类型,包括所有方法的参数和回调类型
- app-harmony/index.uts: 鸿蒙平台的具体实现,调用鸿蒙原生API
2.4 接口定义(interface.uts)
首先,我们需要定义清晰的接口类型。以闹钟提醒为例:
// 闹钟提醒选项
export type PublishAlarmReminderOptions = {
hour: number // 闹钟小时数(0-23)
minute: number // 闹钟分钟数(0-59)
daysOfWeek?: Array<number> | null // 重复日期,周日=0
title?: string | null // 提醒标题
content?: string | null // 提醒内容
success?: PublishAlarmReminderSuccessCallback | null
fail?: PublishAlarmReminderFailCallback | null
complete?: PublishAlarmReminderCompleteCallback | null
}
// 成功回调返回值
export type PublishAlarmReminderSuccess = {
errMsg: string
reminderId: number // 提醒ID,用于后续取消
}
// 定义uni对象上的方法
export interface Uni {
publishAlarmReminder(options: PublishAlarmReminderOptions): void
publishCalendarReminder(options: PublishCalendarReminderOptions): void
publishTimerReminder(options: PublishTimerReminderOptions): void
cancelReminder(options: CancelReminderOptions): void
cancelAllReminders(options: CancelAllRemindersOptions): void
getValidReminders(options: GetValidRemindersOptions): void
}
这种接口定义方式遵循了UniApp的API设计规范,使用success/fail/complete回调模式,保持了与UniApp其他API的一致性。
2.5 鸿蒙平台核心实现
2.5.1 导入鸿蒙原生API
import reminderAgentManager from '@ohos.reminderAgentManager'
import { BusinessError } from '@kit.BasicServicesKit'
- reminderAgentManager: 鸿蒙的代理提醒管理器
- BusinessError: 鸿蒙的业务错误类型
2.5.2 实现闹钟提醒
export function publishAlarmReminder(options: PublishAlarmReminderOptions): void {
try {
// 构建闹钟提醒请求对象
const requestObj = {
reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_ALARM,
hour: options.hour,
minute: options.minute,
title: options.title || '闹钟提醒',
content: options.content || '该起床了',
wantAgent: {
pkgName: 'com.liudd1.anshichiyao',
abilityName: 'EntryAbility'
},
actionButton: [{
title: '关闭',
type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE
}]
} as UTSJSONObject
// 设置重复日期
if (options.daysOfWeek && options.daysOfWeek.length > 0) {
requestObj['daysOfWeek'] = options.daysOfWeek
}
// 发布提醒
reminderAgentManager.publishReminder(
requestObj as unknown as reminderAgentManager.ReminderRequest
)
.then((reminderId: number) => {
options?.success?.({
errMsg: 'publishAlarmReminder:ok',
reminderId: reminderId
})
options?.complete?.({ errMsg: 'publishAlarmReminder:ok' })
})
.catch((err: BusinessError) => {
options?.fail?.({ errMsg: `publishAlarmReminder:fail ${err.message}` })
options?.complete?.({ errMsg: `publishAlarmReminder:fail ${err.message}` })
})
} catch (err) {
const error = err as BusinessError
options?.fail?.({ errMsg: `publishAlarmReminder:fail ${error.message}` })
options?.complete?.({ errMsg: `publishAlarmReminder:fail ${error.message}` })
}
}
实现要点:
- reminderType: 指定提醒类型为闹钟
- wantAgent: 配置点击提醒后要启动的应用和页面
- actionButton: 添加操作按钮
- daysOfWeek: 可选的重复日期数组
- 错误处理: 使用try-catch和Promise.catch双重错误处理
2.5.3 实现日历提醒
export function publishCalendarReminder(options: PublishCalendarReminderOptions): void {
try {
const date = new Date(options.dateTime)
const requestObj = {
reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_CALENDAR,
dateTime: {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
hour: date.getHours(),
minute: date.getMinutes()
},
title: options.title || '日历提醒',
content: options.content || '事件提醒',
wantAgent: {
pkgName: 'com.liudd1.anshichiyao',
abilityName: 'EntryAbility'
},
actionButton: [{
title: '关闭',
type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE
}]
} as UTSJSONObject
// 设置重复月份和日期
if (options.repeatMonths && options.repeatMonths.length > 0) {
requestObj['repeatMonths'] = options.repeatMonths
}
if (options.repeatDays && options.repeatDays.length > 0) {
requestObj['repeatDays'] = options.repeatDays
}
// 发布提醒(Promise处理逻辑同闹钟提醒)
// ...
} catch (err) {
// 错误处理
}
}
2.5.4 实现倒计时提醒
export function publishTimerReminder(options: PublishTimerReminderOptions): void {
try {
const requestObj = {
reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_TIMER,
triggerTimeInSeconds: options.triggerTimeInSeconds,
title: options.title || '倒计时提醒',
content: options.content || '倒计时已到',
wantAgent: {
pkgName: 'com.liudd1.anshichiyao',
abilityName: 'EntryAbility'
},
actionButton: [{
title: '关闭',
type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE
}]
} as UTSJSONObject
// 发布提醒
// ...
} catch (err) {
// 错误处理
}
}
2.6 权限配置
使用代理提醒功能需要在manifest.json中声明权限:
{
"app-harmony": {
"permissions": [
"ohos.permission.PUBLISH_AGENT_REMINDER"
]
}
}
同时配合通知权限插件请求用户授权:
uni.requestNotification({
success: (res) => {
if (res.granted) {
console.log('通知权限已获取')
}
}
})
2.7 在应用中使用插件
插件开发完成后,在应用中的使用非常简单:
export default {
data() {
return {
reminderIds: []
}
},
methods: {
// 设置每天早上8点的服药提醒
setMedicineReminder() {
uni.publishAlarmReminder({
hour: 8,
minute: 0,
daysOfWeek: [1, 2, 3, 4, 5, 6, 0],
title: '服药提醒',
content: '该吃药了,记得按时服药哦!',
success: (res) => {
console.log('提醒设置成功,ID:', res.reminderId)
this.reminderIds.push(res.reminderId)
uni.showToast({ title: '提醒设置成功' })
},
fail: (err) => {
console.error('提醒设置失败:', err.errMsg)
}
})
},
// 设置30分钟后的倒计时提醒
setTimerReminder() {
uni.publishTimerReminder({
triggerTimeInSeconds: 30 * 60,
title: '服药倒计时',
content: '30分钟已到,该服药了',
success: (res) => {
this.reminderIds.push(res.reminderId)
}
})
},
// 取消所有提醒
cancelAllReminders() {
uni.cancelAllReminders({
success: () => {
this.reminderIds = []
uni.showToast({ title: '已清空所有提醒' })
}
})
}
}
}
希望本文的分享能够帮助到正在开发鸿蒙应用的开发者们,让我们一起为鸿蒙生态的繁荣贡献力量!
收起阅读 »解决uniapp打包h5刷新页面无法返回上一级页面的问题
h5环境直接拦截默认的返回方法,使用router自带的返回
拦截默认的pageHead的返回方法,判断整个页面的history
import Vue from 'vue'
if (process.env.VUE_APP_PLATFORM === 'h5') {
// 替代默认的返回api
uni.navigateBack = function (params: any) {
let canBack = true
const pages = getCurrentPages()
let delta = params?.delta
if (typeof delta !== 'number') delta = 1
const router = getApp().$router
if (pages.length) {
const from = 'navigateBack'
function hasLifecycleHook(options: any, hook: string) {
return Array.isArray(options[hook]) && options[hook].length
}
const page = pages[pages.length - 1] as any
if (
hasLifecycleHook(page.$options, 'onBackPress') &&
page.__call_hook('onBackPress', {
from,
}) === true
) {
canBack = false
}
}
if (canBack) {
if (delta > 1) {
router._$delta = delta
}
router.go(-delta, {
animationType: '',
animationDuration: '',
})
}
}
// 修复 自带的pageHead 刷新点击右上角回到首页
const Page = Vue.component('Page')
const PageHead = Page.component('PageHead')
// @ts-ignore
PageHead.methods._back = function () {
if (history.length === 1) {
return uni.reLaunch({
url: '/',
})
} else {
return uni.navigateBack({
// @ts-ignore
from: 'backbutton',
})
}
}
}
h5环境直接拦截默认的返回方法,使用router自带的返回
拦截默认的pageHead的返回方法,判断整个页面的history
import Vue from 'vue'
if (process.env.VUE_APP_PLATFORM === 'h5') {
// 替代默认的返回api
uni.navigateBack = function (params: any) {
let canBack = true
const pages = getCurrentPages()
let delta = params?.delta
if (typeof delta !== 'number') delta = 1
const router = getApp().$router
if (pages.length) {
const from = 'navigateBack'
function hasLifecycleHook(options: any, hook: string) {
return Array.isArray(options[hook]) && options[hook].length
}
const page = pages[pages.length - 1] as any
if (
hasLifecycleHook(page.$options, 'onBackPress') &&
page.__call_hook('onBackPress', {
from,
}) === true
) {
canBack = false
}
}
if (canBack) {
if (delta > 1) {
router._$delta = delta
}
router.go(-delta, {
animationType: '',
animationDuration: '',
})
}
}
// 修复 自带的pageHead 刷新点击右上角回到首页
const Page = Vue.component('Page')
const PageHead = Page.component('PageHead')
// @ts-ignore
PageHead.methods._back = function () {
if (history.length === 1) {
return uni.reLaunch({
url: '/',
})
} else {
return uni.navigateBack({
// @ts-ignore
from: 'backbutton',
})
}
}
}
收起阅读 »
分片上传、文件分片,使用Html5Plus API。测试对比MD5。
兄弟们,我在看掘金的时候找到分片上传失败的原因了,就很小的一个点:
html5Plus 的文档写的不太清楚出了,很多地方让人猜。就比如File.slice(start,end) ,在js中slice截取不包含end,但在Html5Plus File.slice包含end,导致合并文件后总是比原文件大一点。
也就是使用Html5Plus的File.slice方法:每个分片end-1。
已经在安卓模拟器上测试,并对比MD5值。
因为没有 readAsArrayBuffer 方法,只能通过 Base64 转 ArrayBuffer,需要注意去除dataUrl头部
关键代码
const readFileChunk = (file, start, end) => {
return new Promise((resolve, reject) => {
try {
const reader = new plus.io.FileReader();
reader.onload = () => {
try {
// 排除base64头部
const base64 = reader.result.substring(reader.result.indexOf(',') 1);
resolve(uni.base64ToArrayBuffer(base64));
} catch (e) {
reject(e)
}
};
reader.onerror = reject;
// 使用slice方法读取指定范围, 需要注意: end 包含
const blob = file.slice(start, end);
reader.readAsDataURL(blob); // HTML5 需要转换为base64读取
} catch (e) {
reject(e)
}
});
};
完整的分片上传
async uploadMultipart(filePath, chunkSize = 1024 * 1024 * 5) {
// 通过文件路径获取File对象(Html5Plus的File)
const file = await getFile(filePath);
const uploadId = await multipartUploadApi.startMultipart(file.name)
// console.log('uploadId', uploadId)
const partList = []
const totalSize = file.size;
for (let i = 0; i < totalSize / chunkSize; i ) {
const start = i * chunkSize;
const end = Math.min((i 1) * chunkSize - 1, totalSize);
const chunkBuffer = await readFileChunk(file, start, end);
const partItem = await multipartUploadApi.uploadPart(uploadId, i 1, chunkBuffer)
partList.push(partItem)
}
const result = await multipartUploadApi.complete(uploadId, partList)
return result
},
getFile(filePath) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
if (!entry.isFile) {
reject(new Error(`不是文件:${filePath}`))
return
}
entry.file((file) => {
resolve(file)
}, (err) => {
// console.log('无法获取文件')
reject(err)
})
}, (error) => {
// console.log('文件不存在', error)
reject(error)
})
})
},
readFileChunk (file, start, end) {
return new Promise((resolve, reject) => {
try {
const reader = new plus.io.FileReader();
reader.onload = () => {
try {
// 排除base64头部
const base64 = reader.result.substring(reader.result.indexOf(',') 1);
resolve(uni.base64ToArrayBuffer(base64));
} catch (e) {
reject(e)
}
};
reader.onerror = reject;
// 使用slice方法读取指定范围, 需要注意: end 包含
const blob = file.slice(start, end);
reader.readAsDataURL(blob); // HTML5 需要转换为base64读取
} catch (e) {
reject(e)
}
});
};
参考的掘金文章:
解决Uniapp中文件切片问题
作者:Synmbrf
链接:https://juejin.cn/post/7493783786707058726
来源:稀土掘金
兄弟们,我在看掘金的时候找到分片上传失败的原因了,就很小的一个点:
html5Plus 的文档写的不太清楚出了,很多地方让人猜。就比如File.slice(start,end) ,在js中slice截取不包含end,但在Html5Plus File.slice包含end,导致合并文件后总是比原文件大一点。
也就是使用Html5Plus的File.slice方法:每个分片end-1。
已经在安卓模拟器上测试,并对比MD5值。
因为没有 readAsArrayBuffer 方法,只能通过 Base64 转 ArrayBuffer,需要注意去除dataUrl头部
关键代码
const readFileChunk = (file, start, end) => {
return new Promise((resolve, reject) => {
try {
const reader = new plus.io.FileReader();
reader.onload = () => {
try {
// 排除base64头部
const base64 = reader.result.substring(reader.result.indexOf(',') 1);
resolve(uni.base64ToArrayBuffer(base64));
} catch (e) {
reject(e)
}
};
reader.onerror = reject;
// 使用slice方法读取指定范围, 需要注意: end 包含
const blob = file.slice(start, end);
reader.readAsDataURL(blob); // HTML5 需要转换为base64读取
} catch (e) {
reject(e)
}
});
};
完整的分片上传
async uploadMultipart(filePath, chunkSize = 1024 * 1024 * 5) {
// 通过文件路径获取File对象(Html5Plus的File)
const file = await getFile(filePath);
const uploadId = await multipartUploadApi.startMultipart(file.name)
// console.log('uploadId', uploadId)
const partList = []
const totalSize = file.size;
for (let i = 0; i < totalSize / chunkSize; i ) {
const start = i * chunkSize;
const end = Math.min((i 1) * chunkSize - 1, totalSize);
const chunkBuffer = await readFileChunk(file, start, end);
const partItem = await multipartUploadApi.uploadPart(uploadId, i 1, chunkBuffer)
partList.push(partItem)
}
const result = await multipartUploadApi.complete(uploadId, partList)
return result
},
getFile(filePath) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
if (!entry.isFile) {
reject(new Error(`不是文件:${filePath}`))
return
}
entry.file((file) => {
resolve(file)
}, (err) => {
// console.log('无法获取文件')
reject(err)
})
}, (error) => {
// console.log('文件不存在', error)
reject(error)
})
})
},
readFileChunk (file, start, end) {
return new Promise((resolve, reject) => {
try {
const reader = new plus.io.FileReader();
reader.onload = () => {
try {
// 排除base64头部
const base64 = reader.result.substring(reader.result.indexOf(',') 1);
resolve(uni.base64ToArrayBuffer(base64));
} catch (e) {
reject(e)
}
};
reader.onerror = reject;
// 使用slice方法读取指定范围, 需要注意: end 包含
const blob = file.slice(start, end);
reader.readAsDataURL(blob); // HTML5 需要转换为base64读取
} catch (e) {
reject(e)
}
});
};
参考的掘金文章:
解决Uniapp中文件切片问题
作者:Synmbrf
链接:https://juejin.cn/post/7493783786707058726
来源:稀土掘金
基于vue3.5+vite7.1+tauri2.8实战客户端聊天软件
vue3-tauri2-wechat:最新研发vite7.1+tauri2.8+vue3 setup+pinia3+elementPlus跨平台仿微信/QQ风格桌面聊天系统Exe模板。包含聊天、通讯录、收藏、朋友圈、短视频、我的等板块。
运用技术
- 跨平台框架:tauri2.8
- 前端技术框架:vite^7.1.10+vue^3.5.22+vue-router^4.6.3
- 状态管理:pinia^3.0.3
- 本地存储:pinia-plugin-persistedstate^4.5.0
- 组件库:element-plus^2.11.5
- 富文本编辑器:@vueup/vue-quill^1.2.0
- 样式预处理:sass^1.93.2
- 短视频滑动插件:swiper^12.0.2
项目框架目录
基于最新版跨平台框架tauri2+vite7创建项目模板,vue3 setup语法开发。
tauri2-vue3chat聊天系统已经更新到我的原创作品集。
Tauri2.0+Vite7+ElementPlus桌面聊天Exe程序
往期推荐
Electron38-Vue3OS客户端OS系统|vite7+electron38+arco桌面os后台管理
electron38-admin桌面端后台|Electron38+Vue3+ElementPlus管理系统
Electron38-Wechat电脑端聊天|vite7+electron38仿微信桌面端聊天系统
原创uniapp+vue3+deepseek+uv-ui跨端实战仿deepseek/豆包流式ai聊天对话助手。
vue3-webseek网页版AI问答|Vite6+DeepSeek+Arco流式ai聊天打字效果
最新版uni-app+vue3+uv-ui跨三端仿微信app聊天应用【h5+小程序+app端】
Flutter3-MacOS桌面OS系统|flutter3.32+window_manager客户端OS模板
最新研发flutter3.27+bitsdojo_window+getx客户端仿微信聊天Exe应用
最新版Flutter3.32+Dart3.8跨平台仿微信app聊天界面|朋友圈
最新版uniapp+vue3+uv-ui跨三端短视频+直播+聊天【H5+小程序+App端】
uniapp-vue3-os手机oa系统|uni-app+vue3跨三端os后台管理模板
Electron35-DeepSeek桌面端AI系统|vue3.5+electron+arco客户端ai模板
uniapp+vue3酒店预订|vite5+uniapp预约订房系统模板(h5+小程序+App端)
Tauri2.0+Vite5聊天室|vue3+tauri2+element-plus仿微信|tauri聊天应用
tauri2.0-admin桌面端后台系统|Tauri2+Vite5+ElementPlus管理后台EXE程序
vue3-tauri2-wechat:最新研发vite7.1+tauri2.8+vue3 setup+pinia3+elementPlus跨平台仿微信/QQ风格桌面聊天系统Exe模板。包含聊天、通讯录、收藏、朋友圈、短视频、我的等板块。
运用技术
- 跨平台框架:tauri2.8
- 前端技术框架:vite^7.1.10+vue^3.5.22+vue-router^4.6.3
- 状态管理:pinia^3.0.3
- 本地存储:pinia-plugin-persistedstate^4.5.0
- 组件库:element-plus^2.11.5
- 富文本编辑器:@vueup/vue-quill^1.2.0
- 样式预处理:sass^1.93.2
- 短视频滑动插件:swiper^12.0.2
项目框架目录
基于最新版跨平台框架tauri2+vite7创建项目模板,vue3 setup语法开发。
tauri2-vue3chat聊天系统已经更新到我的原创作品集。
Tauri2.0+Vite7+ElementPlus桌面聊天Exe程序
往期推荐
Electron38-Vue3OS客户端OS系统|vite7+electron38+arco桌面os后台管理
electron38-admin桌面端后台|Electron38+Vue3+ElementPlus管理系统
Electron38-Wechat电脑端聊天|vite7+electron38仿微信桌面端聊天系统
原创uniapp+vue3+deepseek+uv-ui跨端实战仿deepseek/豆包流式ai聊天对话助手。
vue3-webseek网页版AI问答|Vite6+DeepSeek+Arco流式ai聊天打字效果
最新版uni-app+vue3+uv-ui跨三端仿微信app聊天应用【h5+小程序+app端】
Flutter3-MacOS桌面OS系统|flutter3.32+window_manager客户端OS模板
最新研发flutter3.27+bitsdojo_window+getx客户端仿微信聊天Exe应用
最新版Flutter3.32+Dart3.8跨平台仿微信app聊天界面|朋友圈
最新版uniapp+vue3+uv-ui跨三端短视频+直播+聊天【H5+小程序+App端】
uniapp-vue3-os手机oa系统|uni-app+vue3跨三端os后台管理模板
Electron35-DeepSeek桌面端AI系统|vue3.5+electron+arco客户端ai模板
uniapp+vue3酒店预订|vite5+uniapp预约订房系统模板(h5+小程序+App端)
Tauri2.0+Vite5聊天室|vue3+tauri2+element-plus仿微信|tauri聊天应用
tauri2.0-admin桌面端后台系统|Tauri2+Vite5+ElementPlus管理后台EXE程序
使用 uni-app x 开发2048游戏适配鸿蒙6
使用 uni-app x 开发2048游戏适配鸿蒙6
作者:坚果派小雨
发布时间:2025年10月
技术栈:uni-app x、UTS、HarmonyOS 6
📖 前言
2048是一款风靡全球的益智游戏,简单而富有策略性。本文将详细介绍如何使用 uni-app x 框架从零开始开发一款2048游戏,并实现深色模式适配、数据持久化等进阶功能。最终产品完美支持鸿蒙HarmonyOS 6、Android、iOS等多个平台。
为什么选择 uni-app x?
- 🚀 原生性能:UTS 语言编译为原生代码,性能接近原生应用
- 📱 一次开发,多端运行:支持鸿蒙、Android、iOS、Web等平台
- 💪 类型安全:基于 TypeScript,享受完整的类型检查
- 🎯 鸿蒙首选:官方支持鸿蒙6,是开发鸿蒙应用的最佳选择之一
🎯 项目目标
我们将实现以下功能:
- ✅ 完整的2048游戏逻辑
- ✅ 流畅的触摸手势控制
- ✅ 精美的动画效果
- ✅ 深色模式自动适配
- ✅ 最高分本地存储
- ✅ 多平台支持(重点支持鸿蒙6)
📐 架构设计
数据结构设计
游戏的核心是一个 4x4 的二维数组,用于存储每个格子的数值:
// 游戏网格数据
grid: number[][] = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
为了实现平滑的动画效果,我们需要一个独立的 Tile 数据结构:
type Tile = {
id: number // 唯一标识
value: number // 方块数值
row: number // 行位置
col: number // 列位置
isNew: boolean // 是否是新生成的
isMerged: boolean // 是否刚合并
}
状态管理
使用 Vue 3 的响应式系统管理游戏状态:
data() {
return {
grid: [] as number[][], // 游戏网格
tiles: [] as Tile[], // 显示的方块
score: 0, // 当前分数
bestScore: 0, // 最高分
gameOver: false, // 游戏结束
gameWon: false, // 游戏胜利
keepPlaying: false, // 继续游戏
tileIdCounter: 0, // 方块ID计数器
isDarkMode: false // 深色模式
}
}
🔧 核心功能实现
1. 游戏初始化
游戏开始时需要初始化网格并随机生成两个方块:
initGame() {
// 初始化4x4网格
this.grid = []
for (let i = 0; i < 4; i++) {
this.grid.push([0, 0, 0, 0])
}
// 重置状态
this.tiles = []
this.score = 0
this.gameOver = false
this.gameWon = false
this.tileIdCounter = 0
// 添加两个初始方块
this.addRandomTile()
this.addRandomTile()
}
2. 随机生成方块
90%概率生成2,10%概率生成4,这是经典2048的设定:
addRandomTile() {
// 找出所有空格子
const emptyCells = [] as {row: number, col: number}[]
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.grid[i][j] === 0) {
emptyCells.push({row: i, col: j})
}
}
}
if (emptyCells.length > 0) {
// 随机选择一个空格子
const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)]
// 90%概率生成2,10%概率生成4
const value = Math.random() < 0.9 ? 2 : 4
this.grid[randomCell.row][randomCell.col] = value
// 创建新的方块对象
const tile: Tile = {
id: this.tileIdCounter++,
value: value,
row: randomCell.row,
col: randomCell.col,
isNew: true,
isMerged: false
}
this.tiles.push(tile)
// 200ms后移除新方块标记,触发动画
setTimeout(() => {
tile.isNew = false
}, 200)
}
}
3. 触摸手势处理
实现流畅的滑动手势检测:
// 记录触摸起点
touchStart(e: UniTouchEvent) {
this.touchStartX = e.touches[0].pageX
this.touchStartY = e.touches[0].pageY
}
// 计算滑动方向
touchEnd(e: UniTouchEvent) {
this.touchEndX = e.changedTouches[0].pageX
this.touchEndY = e.changedTouches[0].pageY
this.handleSwipe()
}
// 判断滑动方向
handleSwipe() {
const deltaX = this.touchEndX - this.touchStartX
const deltaY = this.touchEndY - this.touchStartY
const minSwipeDistance = 30 // 最小滑动距离
// 滑动距离太短,忽略
if (Math.abs(deltaX) < minSwipeDistance &&
Math.abs(deltaY) < minSwipeDistance) {
return
}
// 比较水平和垂直位移,判断主要方向
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 水平滑动
deltaX > 0 ? this.move('right') : this.move('left')
} else {
// 垂直滑动
deltaY > 0 ? this.move('down') : this.move('up')
}
}
4. 移动和合并算法
这是游戏的核心逻辑。以向左移动为例:
moveLeft(): boolean {
let moved = false
for (let i = 0; i < 4; i++) {
// 记录每个位置是否已经合并过
let merged = [false, false, false, false]
// 从左到右遍历每一行
for (let j = 1; j < 4; j++) {
if (this.grid[i][j] !== 0) {
let col = j
// 尽可能向左移动
while (col > 0) {
// 左边是空格,移动过去
if (this.grid[i][col - 1] === 0) {
this.grid[i][col - 1] = this.grid[i][col]
this.grid[i][col] = 0
col--
moved = true
}
// 左边数字相同且未合并,合并
else if (this.grid[i][col - 1] === this.grid[i][col] &&
!merged[col - 1]) {
this.grid[i][col - 1] *= 2
this.score += this.grid[i][col - 1]
this.grid[i][col] = 0
merged[col - 1] = true
moved = true
break
}
// 无法移动
else {
break
}
}
}
}
}
return moved
}
算法关键点:
- 使用
merged数组防止一次移动中多次合并 - 从移动方向的对侧开始遍历
- 每个方块尽可能移动到最远位置
5. 游戏状态检测
胜利检测
hasWon(): boolean {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.grid[i][j] === 2048) {
return true
}
}
}
return false
}
游戏结束检测
isGameOver(): boolean {
// 1. 检查是否有空格
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.grid[i][j] === 0) {
return false
}
}
}
// 2. 检查是否可以合并
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
const current = this.grid[i][j]
// 检查右边
if (j < 3 && current === this.grid[i][j + 1]) {
return false
}
// 检查下边
if (i < 3 && current === this.grid[i + 1][j]) {
return false
}
}
}
return true
}
🎨 界面设计与动画
1. 网格布局
使用 CSS 定位实现精确的网格布局:
.game-container {
position: relative;
width: 670rpx;
height: 670rpx;
background-color: #bbada0;
border-radius: 12rpx;
padding: 20rpx;
}
.grid-cell {
width: 142.5rpx;
height: 142.5rpx;
background-color: rgba(238, 228, 218, 0.35);
border-radius: 6rpx;
}
计算公式:
单元格宽度 = (容器宽度 - 内边距*2 - 间距*3) / 4
142.5rpx = (670 - 20*2 - 20*3) / 4
2. 方块定位
使用动态类名实现方块的精确定位:
<view
:class="[
'tile',
'' + tile.value,
'tile-' + tile.row + '-' + tile.col
]"
>
<text class="tile-inner">{{tile.value}}</text>
</view>
CSS 定位规则:
.tile-position-0-0 { top: 0rpx; left: 0rpx; }
.tile-position-0-1 { top: 0rpx; left: 162.5rpx; }
/* ... 16个位置 ... */
3. 出现动画
新方块从小到大弹出:
.tile-new {
animation: appear 0.2s ease-in-out;
}
@keyframes appear {
0% {
opacity: 0;
transform: scale(0);
}
100% {
opacity: 1;
transform: scale(1);
}
}
4. 合并动画
合并时放大再缩小:
.tile-merged {
animation: pop 0.2s ease-in-out;
}
@keyframes pop {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
5. 渐变配色
不同数值的方块使用不同颜色:
.tile-2 { background-color: #eee4da; color: #776e65; }
.tile-4 { background-color: #ede0c8; color: #776e65; }
.tile-8 { background-color: #f2b179; color: #f9f6f2; }
.tile-16 { background-color: #f59563; color: #f9f6f2; }
/* ... */
.tile-2048 { background-color: #edc22e; color: #f9f6f2; }
🌓 深色模式适配
1. 检测系统主题
detectTheme() {
const systemInfo = uni.getSystemInfoSync()
// @ts-ignore
this.isDarkMode = systemInfo.theme === 'dark' ||
systemInfo.osTheme === 'dark'
}
onShow() {
// 每次显示时检测主题变化
this.detectTheme()
}
2. 深色模式样式
使用级联选择器为深色模式定制样式:
/* 深色模式容器 */
.dark-mode {
background-color: #1a1a1a;
}
/* 深色模式下的游戏网格 */
.dark-mode .game-container {
background-color: #4a4340;
}
/* 深色模式下的方块 */
.dark-mode .tile-2 {
background-color: #3a3a3a;
color: #c9c9c9;
}
.dark-mode .tile-4 {
background-color: #4a4a4a;
color: #d4d4d4;
}
设计原则:
- 降低对比度,减少眼睛疲劳
- 保持色彩层次感
- 确保文字清晰可读
💾 数据持久化
使用 uni-app 的本地存储 API 保存最高分:
// 保存最高分
saveBestScore() {
uni.setStorageSync('bestScore', this.bestScore)
}
// 加载最高分
loadBestScore() {
const saved = uni.getStorageSync('bestScore')
if (saved !== null && saved !== undefined && saved !== '') {
this.bestScore = parseInt(saved as string)
}
}
// 在分数更新时检查
if (this.score > this.bestScore) {
this.bestScore = this.score
this.saveBestScore()
}
📱 多平台适配
鸿蒙6特别优化
- 返回键处理(App.uvue):
onLastPageBackPress: function () {
if (firstBackTime == 0) {
uni.showToast({
title: '再按一次退出应用',
position: 'bottom',
})
firstBackTime = Date.now()
setTimeout(() => {
firstBackTime = 0
}, 2000)
} else if (Date.now() - firstBackTime < 2000) {
uni.exit()
}
}
-
rpx单位适配:
- rpx 是 uni-app 的响应式单位
- 750rpx = 屏幕宽度
- 自动适配不同屏幕尺寸
-
触摸事件优化:
- 使用原生触摸事件
- 最小滑动距离30px
- 防止误触
🐛 常见问题与解决方案
问题1:方块移动后位置不更新
原因:直接修改数组元素不会触发视图更新
解决方案:使用 updateTiles() 方法重建 tiles 数组
updateTiles() {
const newTiles = [] as Tile[]
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.grid[i][j] !== 0) {
let existingTile = this.tiles.find(t =>
t.row === i && t.col === j && !t.isNew
)
if (existingTile) {
existingTile.row = i
existingTile.col = j
existingTile.value = this.grid[i][j]
newTiles.push(existingTile)
}
}
}
}
this.tiles = newTiles
}
问题2:一次移动合并多次
原因:没有标记已合并的位置
解决方案:使用 merged 数组记录
let merged = [false, false, false, false]
// 合并时检查
if (!merged[col - 1]) {
// 执行合并
merged[col - 1] = true
}
问题3:动画不流畅
原因:方块 ID 重复或变化
解决方案:使用全局计数器生成唯一 ID
tileIdCounter: 0
// 创建新方块时
tile.id = this.tileIdCounter++
📊 性能优化
1. 减少不必要的渲染
// 只在有移动时才更新
if (moved) {
this.addRandomTile()
this.updateTiles()
}
2. 使用 CSS 动画而非 JS
.tile {
transition-property: transform;
transition-duration: 0.1s;
transition-timing-function: ease-in-out;
}
3. 合理使用 v-if 和 v-show
<!-- 游戏结束遮罩使用 v-if -->
<view class="game-message" v-if="gameOver || gameWon">
<!-- ... -->
</view>
🚀 打包发布
鸿蒙应用打包
- 配置
manifest.json - 在 HBuilderX 中选择"发行" -> "原生App-云打包"
- 选择鸿蒙平台
- 配置签名证书
- 打包上传
注意事项
- 准备应用图标(512x512px)
- 准备启动页图片
- 填写应用描述和权限说明
- 测试多种屏幕尺寸
📈 后续优化方向
功能扩展
- 撤销功能:保存每一步的状态
- 自定义尺寸:支持 3x3、5x5 网格
- 主题切换:多种配色方案
- 音效反馈:移动、合并音效
- 震动反馈:使用 uni.vibrateShort()
社交功能
- 排行榜:云端存储最高分
- 分享功能:分享到社交平台
- 成就系统:解锁各种成就
- 每日挑战:特殊模式挑战
体验优化
- 引导动画:首次进入时的教程
- 手势提示:显示滑动方向
- 历史记录:查看历史最高分
- 统计数据:游戏次数、时长等
💡 总结
通过这个项目,我们学到了:
- ✅ uni-app x 的基本用法:页面结构、数据绑定、事件处理
- ✅ UTS 语言特性:类型定义、类型安全
- ✅ 游戏算法实现:移动、合并、状态检测
- ✅ CSS 动画技巧:关键帧动画、过渡效果
- ✅ 响应式设计:rpx 单位、多屏适配
- ✅ 鸿蒙应用开发:平台特性、适配要点
uni-app x 是一个强大的跨平台开发框架,特别适合开发鸿蒙应用。通过本项目的实践,希望能帮助你快速上手 uni-app x 开发,创造出更多优秀的应用!
📚 参考资源
作者:坚果派小雨
项目地址: GitCode
开源协议: MIT License
如果觉得本文对你有帮助,欢迎点赞、收藏、分享!有任何问题也欢迎在评论区讨论。
💡 提示:本文涉及的完整代码已开源,你可以直接下载运行,也可以在此基础上进行二次开发。期待看到你的创意!
使用 uni-app x 开发2048游戏适配鸿蒙6
作者:坚果派小雨
发布时间:2025年10月
技术栈:uni-app x、UTS、HarmonyOS 6
📖 前言
2048是一款风靡全球的益智游戏,简单而富有策略性。本文将详细介绍如何使用 uni-app x 框架从零开始开发一款2048游戏,并实现深色模式适配、数据持久化等进阶功能。最终产品完美支持鸿蒙HarmonyOS 6、Android、iOS等多个平台。
为什么选择 uni-app x?
- 🚀 原生性能:UTS 语言编译为原生代码,性能接近原生应用
- 📱 一次开发,多端运行:支持鸿蒙、Android、iOS、Web等平台
- 💪 类型安全:基于 TypeScript,享受完整的类型检查
- 🎯 鸿蒙首选:官方支持鸿蒙6,是开发鸿蒙应用的最佳选择之一
🎯 项目目标
我们将实现以下功能:
- ✅ 完整的2048游戏逻辑
- ✅ 流畅的触摸手势控制
- ✅ 精美的动画效果
- ✅ 深色模式自动适配
- ✅ 最高分本地存储
- ✅ 多平台支持(重点支持鸿蒙6)
📐 架构设计
数据结构设计
游戏的核心是一个 4x4 的二维数组,用于存储每个格子的数值:
// 游戏网格数据
grid: number[][] = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
为了实现平滑的动画效果,我们需要一个独立的 Tile 数据结构:
type Tile = {
id: number // 唯一标识
value: number // 方块数值
row: number // 行位置
col: number // 列位置
isNew: boolean // 是否是新生成的
isMerged: boolean // 是否刚合并
}
状态管理
使用 Vue 3 的响应式系统管理游戏状态:
data() {
return {
grid: [] as number[][], // 游戏网格
tiles: [] as Tile[], // 显示的方块
score: 0, // 当前分数
bestScore: 0, // 最高分
gameOver: false, // 游戏结束
gameWon: false, // 游戏胜利
keepPlaying: false, // 继续游戏
tileIdCounter: 0, // 方块ID计数器
isDarkMode: false // 深色模式
}
}
🔧 核心功能实现
1. 游戏初始化
游戏开始时需要初始化网格并随机生成两个方块:
initGame() {
// 初始化4x4网格
this.grid = []
for (let i = 0; i < 4; i++) {
this.grid.push([0, 0, 0, 0])
}
// 重置状态
this.tiles = []
this.score = 0
this.gameOver = false
this.gameWon = false
this.tileIdCounter = 0
// 添加两个初始方块
this.addRandomTile()
this.addRandomTile()
}
2. 随机生成方块
90%概率生成2,10%概率生成4,这是经典2048的设定:
addRandomTile() {
// 找出所有空格子
const emptyCells = [] as {row: number, col: number}[]
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.grid[i][j] === 0) {
emptyCells.push({row: i, col: j})
}
}
}
if (emptyCells.length > 0) {
// 随机选择一个空格子
const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)]
// 90%概率生成2,10%概率生成4
const value = Math.random() < 0.9 ? 2 : 4
this.grid[randomCell.row][randomCell.col] = value
// 创建新的方块对象
const tile: Tile = {
id: this.tileIdCounter++,
value: value,
row: randomCell.row,
col: randomCell.col,
isNew: true,
isMerged: false
}
this.tiles.push(tile)
// 200ms后移除新方块标记,触发动画
setTimeout(() => {
tile.isNew = false
}, 200)
}
}
3. 触摸手势处理
实现流畅的滑动手势检测:
// 记录触摸起点
touchStart(e: UniTouchEvent) {
this.touchStartX = e.touches[0].pageX
this.touchStartY = e.touches[0].pageY
}
// 计算滑动方向
touchEnd(e: UniTouchEvent) {
this.touchEndX = e.changedTouches[0].pageX
this.touchEndY = e.changedTouches[0].pageY
this.handleSwipe()
}
// 判断滑动方向
handleSwipe() {
const deltaX = this.touchEndX - this.touchStartX
const deltaY = this.touchEndY - this.touchStartY
const minSwipeDistance = 30 // 最小滑动距离
// 滑动距离太短,忽略
if (Math.abs(deltaX) < minSwipeDistance &&
Math.abs(deltaY) < minSwipeDistance) {
return
}
// 比较水平和垂直位移,判断主要方向
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 水平滑动
deltaX > 0 ? this.move('right') : this.move('left')
} else {
// 垂直滑动
deltaY > 0 ? this.move('down') : this.move('up')
}
}
4. 移动和合并算法
这是游戏的核心逻辑。以向左移动为例:
moveLeft(): boolean {
let moved = false
for (let i = 0; i < 4; i++) {
// 记录每个位置是否已经合并过
let merged = [false, false, false, false]
// 从左到右遍历每一行
for (let j = 1; j < 4; j++) {
if (this.grid[i][j] !== 0) {
let col = j
// 尽可能向左移动
while (col > 0) {
// 左边是空格,移动过去
if (this.grid[i][col - 1] === 0) {
this.grid[i][col - 1] = this.grid[i][col]
this.grid[i][col] = 0
col--
moved = true
}
// 左边数字相同且未合并,合并
else if (this.grid[i][col - 1] === this.grid[i][col] &&
!merged[col - 1]) {
this.grid[i][col - 1] *= 2
this.score += this.grid[i][col - 1]
this.grid[i][col] = 0
merged[col - 1] = true
moved = true
break
}
// 无法移动
else {
break
}
}
}
}
}
return moved
}
算法关键点:
- 使用
merged数组防止一次移动中多次合并 - 从移动方向的对侧开始遍历
- 每个方块尽可能移动到最远位置
5. 游戏状态检测
胜利检测
hasWon(): boolean {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.grid[i][j] === 2048) {
return true
}
}
}
return false
}
游戏结束检测
isGameOver(): boolean {
// 1. 检查是否有空格
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.grid[i][j] === 0) {
return false
}
}
}
// 2. 检查是否可以合并
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
const current = this.grid[i][j]
// 检查右边
if (j < 3 && current === this.grid[i][j + 1]) {
return false
}
// 检查下边
if (i < 3 && current === this.grid[i + 1][j]) {
return false
}
}
}
return true
}
🎨 界面设计与动画
1. 网格布局
使用 CSS 定位实现精确的网格布局:
.game-container {
position: relative;
width: 670rpx;
height: 670rpx;
background-color: #bbada0;
border-radius: 12rpx;
padding: 20rpx;
}
.grid-cell {
width: 142.5rpx;
height: 142.5rpx;
background-color: rgba(238, 228, 218, 0.35);
border-radius: 6rpx;
}
计算公式:
单元格宽度 = (容器宽度 - 内边距*2 - 间距*3) / 4
142.5rpx = (670 - 20*2 - 20*3) / 4
2. 方块定位
使用动态类名实现方块的精确定位:
<view
:class="[
'tile',
'' + tile.value,
'tile-' + tile.row + '-' + tile.col
]"
>
<text class="tile-inner">{{tile.value}}</text>
</view>
CSS 定位规则:
.tile-position-0-0 { top: 0rpx; left: 0rpx; }
.tile-position-0-1 { top: 0rpx; left: 162.5rpx; }
/* ... 16个位置 ... */
3. 出现动画
新方块从小到大弹出:
.tile-new {
animation: appear 0.2s ease-in-out;
}
@keyframes appear {
0% {
opacity: 0;
transform: scale(0);
}
100% {
opacity: 1;
transform: scale(1);
}
}
4. 合并动画
合并时放大再缩小:
.tile-merged {
animation: pop 0.2s ease-in-out;
}
@keyframes pop {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
5. 渐变配色
不同数值的方块使用不同颜色:
.tile-2 { background-color: #eee4da; color: #776e65; }
.tile-4 { background-color: #ede0c8; color: #776e65; }
.tile-8 { background-color: #f2b179; color: #f9f6f2; }
.tile-16 { background-color: #f59563; color: #f9f6f2; }
/* ... */
.tile-2048 { background-color: #edc22e; color: #f9f6f2; }
🌓 深色模式适配
1. 检测系统主题
detectTheme() {
const systemInfo = uni.getSystemInfoSync()
// @ts-ignore
this.isDarkMode = systemInfo.theme === 'dark' ||
systemInfo.osTheme === 'dark'
}
onShow() {
// 每次显示时检测主题变化
this.detectTheme()
}
2. 深色模式样式
使用级联选择器为深色模式定制样式:
/* 深色模式容器 */
.dark-mode {
background-color: #1a1a1a;
}
/* 深色模式下的游戏网格 */
.dark-mode .game-container {
background-color: #4a4340;
}
/* 深色模式下的方块 */
.dark-mode .tile-2 {
background-color: #3a3a3a;
color: #c9c9c9;
}
.dark-mode .tile-4 {
background-color: #4a4a4a;
color: #d4d4d4;
}
设计原则:
- 降低对比度,减少眼睛疲劳
- 保持色彩层次感
- 确保文字清晰可读
💾 数据持久化
使用 uni-app 的本地存储 API 保存最高分:
// 保存最高分
saveBestScore() {
uni.setStorageSync('bestScore', this.bestScore)
}
// 加载最高分
loadBestScore() {
const saved = uni.getStorageSync('bestScore')
if (saved !== null && saved !== undefined && saved !== '') {
this.bestScore = parseInt(saved as string)
}
}
// 在分数更新时检查
if (this.score > this.bestScore) {
this.bestScore = this.score
this.saveBestScore()
}
📱 多平台适配
鸿蒙6特别优化
- 返回键处理(App.uvue):
onLastPageBackPress: function () {
if (firstBackTime == 0) {
uni.showToast({
title: '再按一次退出应用',
position: 'bottom',
})
firstBackTime = Date.now()
setTimeout(() => {
firstBackTime = 0
}, 2000)
} else if (Date.now() - firstBackTime < 2000) {
uni.exit()
}
}
-
rpx单位适配:
- rpx 是 uni-app 的响应式单位
- 750rpx = 屏幕宽度
- 自动适配不同屏幕尺寸
-
触摸事件优化:
- 使用原生触摸事件
- 最小滑动距离30px
- 防止误触
🐛 常见问题与解决方案
问题1:方块移动后位置不更新
原因:直接修改数组元素不会触发视图更新
解决方案:使用 updateTiles() 方法重建 tiles 数组
updateTiles() {
const newTiles = [] as Tile[]
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (this.grid[i][j] !== 0) {
let existingTile = this.tiles.find(t =>
t.row === i && t.col === j && !t.isNew
)
if (existingTile) {
existingTile.row = i
existingTile.col = j
existingTile.value = this.grid[i][j]
newTiles.push(existingTile)
}
}
}
}
this.tiles = newTiles
}
问题2:一次移动合并多次
原因:没有标记已合并的位置
解决方案:使用 merged 数组记录
let merged = [false, false, false, false]
// 合并时检查
if (!merged[col - 1]) {
// 执行合并
merged[col - 1] = true
}
问题3:动画不流畅
原因:方块 ID 重复或变化
解决方案:使用全局计数器生成唯一 ID
tileIdCounter: 0
// 创建新方块时
tile.id = this.tileIdCounter++
📊 性能优化
1. 减少不必要的渲染
// 只在有移动时才更新
if (moved) {
this.addRandomTile()
this.updateTiles()
}
2. 使用 CSS 动画而非 JS
.tile {
transition-property: transform;
transition-duration: 0.1s;
transition-timing-function: ease-in-out;
}
3. 合理使用 v-if 和 v-show
<!-- 游戏结束遮罩使用 v-if -->
<view class="game-message" v-if="gameOver || gameWon">
<!-- ... -->
</view>
🚀 打包发布
鸿蒙应用打包
- 配置
manifest.json - 在 HBuilderX 中选择"发行" -> "原生App-云打包"
- 选择鸿蒙平台
- 配置签名证书
- 打包上传
注意事项
- 准备应用图标(512x512px)
- 准备启动页图片
- 填写应用描述和权限说明
- 测试多种屏幕尺寸
📈 后续优化方向
功能扩展
- 撤销功能:保存每一步的状态
- 自定义尺寸:支持 3x3、5x5 网格
- 主题切换:多种配色方案
- 音效反馈:移动、合并音效
- 震动反馈:使用 uni.vibrateShort()
社交功能
- 排行榜:云端存储最高分
- 分享功能:分享到社交平台
- 成就系统:解锁各种成就
- 每日挑战:特殊模式挑战
体验优化
- 引导动画:首次进入时的教程
- 手势提示:显示滑动方向
- 历史记录:查看历史最高分
- 统计数据:游戏次数、时长等
💡 总结
通过这个项目,我们学到了:
- ✅ uni-app x 的基本用法:页面结构、数据绑定、事件处理
- ✅ UTS 语言特性:类型定义、类型安全
- ✅ 游戏算法实现:移动、合并、状态检测
- ✅ CSS 动画技巧:关键帧动画、过渡效果
- ✅ 响应式设计:rpx 单位、多屏适配
- ✅ 鸿蒙应用开发:平台特性、适配要点
uni-app x 是一个强大的跨平台开发框架,特别适合开发鸿蒙应用。通过本项目的实践,希望能帮助你快速上手 uni-app x 开发,创造出更多优秀的应用!
📚 参考资源
作者:坚果派小雨
项目地址: GitCode
开源协议: MIT License
如果觉得本文对你有帮助,欢迎点赞、收藏、分享!有任何问题也欢迎在评论区讨论。
收起阅读 »💡 提示:本文涉及的完整代码已开源,你可以直接下载运行,也可以在此基础上进行二次开发。期待看到你的创意!
































