You've already forked template-MP-ts
Initial commit
This commit is contained in:
4
.env
Normal file
4
.env
Normal file
@@ -0,0 +1,4 @@
|
||||
VITE_BASE_URL= #接口地址
|
||||
VITE_ASSETSURL=https://cdn.vrupup.com/s/1598/assets/ #资源地址
|
||||
VITE_APPID=wx9cb717d8151d8486 #小程序APPID
|
||||
VITE_UNI_APPID=_UNI_8842336 #UNI-APPID
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
unpackage
|
||||
/dist
|
||||
node_modules
|
||||
80
.iflow/settings.json
Normal file
80
.iflow/settings.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"Framelink Figma MCP": {
|
||||
"description": "为AI编程工具提供Figma设计文件访问能力,支持获取设计数据和下载图像资源,帮助实现设计到代码的一键转换。",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@iflow-mcp/figma-developer-mcp@0.5.0", "--stdio"],
|
||||
"env": {
|
||||
"FIGMA_API_KEY": ""
|
||||
}
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"]
|
||||
},
|
||||
"context7": {
|
||||
"description": "为开发者提供最新技术文档和代码示例的智能检索服务,支持库搜索和文档获取功能",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@iflow-mcp/context7-mcp@1.0.0"]
|
||||
},
|
||||
"fetch": {
|
||||
"description": "Fetch服务提供了获取各种格式的Web内容的功能,包括HTML、JSON、纯文本、Markdown。",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@iflow-mcp/fetch@1.0.2"]
|
||||
},
|
||||
"server-puppeteer": {
|
||||
"description": "基于Puppeteer的浏览器自动化工具,支持页面导航、截图、元素操作和JavaScript执行等功能",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@iflow-mcp/server-puppeteer@0.6.2"],
|
||||
"env": {
|
||||
"PUPPETEER_LAUNCH_OPTIONS": "{\"executablePath\":\"C:\\Program Files\\Google\\Chrome\\Application\"}",
|
||||
"ALLOW_DANGEROUS": "{\"executablePath\":\"C:\\Program Files\\Google\\Chrome\\Application\"}"
|
||||
}
|
||||
},
|
||||
"math-tools": {
|
||||
"description": "提供34个数学运算工具,包含算术运算、统计计算、数据科学和表达式求值功能,支持高精度计算。",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@iflow-mcp/math-tools"]
|
||||
},
|
||||
"desktop-commander": {
|
||||
"description": "强大的桌面文件系统和进程管理工具,提供文件操作、搜索、进程管理等全面功能",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@iflow-mcp/desktop-commander"]
|
||||
},
|
||||
"desktop-commander-puls": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@wonderwhy-er/desktop-commander@latest"]
|
||||
},
|
||||
"fast-filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "fast-filesystem-mcp"]
|
||||
},
|
||||
"filesystem": {
|
||||
"description": "提供完整的文件系统操作功能,包括文件读写、目录管理、文件搜索等,支持安全的路径验证和权限控制",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@iflow-mcp/server-filesystem@0.6.2", "E:\\"]
|
||||
},
|
||||
"mcp-server-code-runner": {
|
||||
"description": "多语言代码执行器,支持35种编程语言的代码片段执行,包括JavaScript、Python、Go、Java等主流语言",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@iflow-mcp/mcp-server-code-runner"]
|
||||
},
|
||||
"gitlab": {
|
||||
"description": "提供GitLab项目管理、文件操作、Issue管理、合并请求等功能的MCP服务器工具集",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@iflow-mcp/server-gitlab@latest"],
|
||||
"env": {
|
||||
"GITLAB_PERSONAL_ACCESS_TOKEN": "",
|
||||
"GITLAB_API_URL": "https://git.flyh5.cn/api/v4"
|
||||
}
|
||||
},
|
||||
"API 文档": {
|
||||
"description": "将Apifox项目的接口文档作为数据源提供给AI工具,支持根据API文档生成代码、搜索接口内容等功能",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@iflow-mcp/apifox-mcp-server", "--project-id="],
|
||||
"env": {
|
||||
"APIFOX_ACCESS_TOKEN": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
App.vue
Normal file
21
App.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
onLaunch() {
|
||||
console.log('App Launch')
|
||||
},
|
||||
onShow() {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide() {
|
||||
console.log('App Hide')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "./uview-plus/index.scss";
|
||||
@import "./common/styles/common.css";
|
||||
</style>
|
||||
134
IFLOW.md
Normal file
134
IFLOW.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# 项目概述
|
||||
|
||||
这是一个基于 UniApp + Vue3 + TypeScript 的微信小程序项目模板。它提供了一个基础的项目结构和一些常用的工具函数,方便快速开发微信小程序。
|
||||
|
||||
## 技术栈
|
||||
|
||||
* **UniApp**: 跨平台开发框架,用于构建微信小程序。
|
||||
* **Vue3**: 渐进式 JavaScript 框架,用于构建用户界面。
|
||||
* **TypeScript**: JavaScript 的超集,提供类型检查和更好的开发体验。
|
||||
* **uView-Plus**: 基于 UniApp 的 UI 组件库。
|
||||
* **z-paging**: 一个用于处理分页加载的组件库。
|
||||
* **Vuex**: 状态管理库。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
.
|
||||
├── api/ # 接口相关
|
||||
│ ├── modules/ # 业务接口
|
||||
│ └── request.ts # 请求封装
|
||||
├── common/ # 公共资源
|
||||
│ ├── styles/ # 全局样式
|
||||
│ │ ├── common.css # codefun原子类样式
|
||||
│ │ └── base.scss # 全局样式变量
|
||||
│ └── utils/ # 工具函数
|
||||
│ └── tool.ts # 常用工具函数
|
||||
├── components/ # 公共组件
|
||||
├── lib/ # 第三方库
|
||||
│ └── luch-request/ # luch-request 网络请求库
|
||||
├── mixins/ # Vue 混入
|
||||
│ └── global.ts # 全局混入
|
||||
├── pages/ # 主包页面
|
||||
│ └── index/ # 首页
|
||||
│ └── index.vue # 首页页面
|
||||
├── subPages/ # 分包页面
|
||||
├── store/ # 状态管理
|
||||
│ └── index.ts # Vuex store
|
||||
├── uni_modules/ # uni-app 组件
|
||||
│ └── z-paging/ # 分页组件库
|
||||
├── uview-plus/ # uView-Plus 组件库
|
||||
├── App.vue # 应用入口
|
||||
├── main.ts # 主入口文件
|
||||
├── pages.json # 页面配置
|
||||
├── manifest.json # 应用配置
|
||||
├── uni.scss # 全局样式变量
|
||||
├── vite.config.js # Vite 编译配置
|
||||
├── .env # 环境变量
|
||||
├── .nvmdrc # Node.js 版本要求
|
||||
└── tsconfig.json # TypeScript 配置
|
||||
```
|
||||
|
||||
# 开发环境与运行
|
||||
|
||||
## 环境要求
|
||||
|
||||
* Node.js (版本信息在 `.nvmdrc` 文件中指定,当前为 20.0.0)
|
||||
* npm 或 yarn
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## 运行项目
|
||||
|
||||
由于这是一个 UniApp 项目,通常需要使用 HBuilderX 或其他支持 UniApp 的 IDE 来运行和调试。具体的运行命令取决于你的开发环境。
|
||||
|
||||
## 构建项目
|
||||
|
||||
同样,构建项目也需要使用 HBuilderX 或相应的 CLI 工具。
|
||||
|
||||
# 代码规范与开发约定
|
||||
|
||||
## 样式
|
||||
|
||||
* 全局样式文件位于 `common/styles/` 目录下,包括 `common.css` 和 `base.scss`。
|
||||
* `common.css` 提供了codefun原子类样式,用于快速布局。
|
||||
* `base.scss` 提供了SCSS变量和mixins,以及常用的样式类生成器。
|
||||
* 样式规范应遵循项目中已有的风格。
|
||||
|
||||
## TypeScript
|
||||
|
||||
* 严格遵循TypeScript规范,使用类型注解。
|
||||
* 遵循函数式编程范式。
|
||||
* 方法类函数应该使用 `function` 进行定义。
|
||||
* 避免出现超过4个以上的 `ref`,超过4个则使用 `reactive`。
|
||||
* 页面的生命周期按需进行导入,如(`import { onLoad } from '@dcloudio/uni-app'`)。
|
||||
* 全局变量都集中放置于代码顶部。
|
||||
* 变量名使用小驼峰命名法。
|
||||
* 常量名使用全大写。
|
||||
* 状态类变量命名参考 `isLogin`、`isOpen`。
|
||||
* 事件类方法命名参考 `handleClick`、`onSelect`。
|
||||
* 变量都应该写有注释说明、类型说明。
|
||||
* `Promise` 方法使用 `async` `await` 写法,并进行容错处理。
|
||||
* 字符串拼接使用ES6的模板语法。
|
||||
* TypeScript规范应遵循项目中已有的风格。
|
||||
|
||||
## 静态资源
|
||||
|
||||
* 静态资源变量 `ASSETSURL` 已进行全局混入,可以在 `<template></template>` 中直接使用。
|
||||
* 所有静态资源URL应该使用 `ASSETSURL` 进行拼接,如:`${ASSETSURL}simple.png`。
|
||||
|
||||
## 工具函数 (tool.ts)
|
||||
|
||||
`common/utils/tool.ts` 文件提供了一系列常用的工具函数:
|
||||
|
||||
* **提示与加载**: `alert`, `loading`, `hideLoading`
|
||||
* **页面跳转**: `navigateTo`, `redirectTo`, `reLaunch`, `switchTab`, `navigateBack`
|
||||
* **本地存储**: `storage`, `removeStorage`, `getStorageInfo`
|
||||
* **其他功能**: `copy` (复制文本), `saveImageToPhotos` (保存图片), `requestPayment` (微信支付), `upload` (文件上传), `loadFont` (加载字体)
|
||||
|
||||
## 网络请求
|
||||
|
||||
* 网络请求使用 `lib/luch-request` 库进行封装。
|
||||
* 全局配置在 `api/request.ts` 中定义,包括基础URL、请求头、SSL验证等。
|
||||
* 包含请求和响应拦截器,用于处理通用逻辑(如错误提示、鉴权等)。
|
||||
* 各业务板块的接口都应存放在 `api/modules` 下,并将单个接口进行导出以便页面按需导入。
|
||||
|
||||
## 组件
|
||||
|
||||
* 项目集成了 `uView-Plus` 和 `z-paging` 两个组件库。
|
||||
* `uView-Plus` 组件已通过 `easycom` 自动导入,可以直接使用,如:`<u-button>`、`<u-icon>`。
|
||||
* 全局组件放在 `components/` 目录下。
|
||||
* 页面独立组件放在页面根目录下的 `components/`。
|
||||
* 每个组件应该附带 `README.MD` 文档。
|
||||
* 组件编写应遵循项目中已有的风格。
|
||||
|
||||
## 页面
|
||||
|
||||
* 页面配置在 `pages.json` 中管理。
|
||||
* 主包页面放在 `pages/` 目录下,分包页面放在 `subPages/` 目录下。
|
||||
* 页面使用 Composition API (setup语法糖) 编写。
|
||||
* 注释、结构规范应遵循项目中已有的风格。
|
||||
84
README.md
Normal file
84
README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# 项目模板使用说明
|
||||
|
||||
本模板是基于 UniApp + Vue3 + TypeScript 的小程序项目模板,提供了一些常用的工具和规范。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
├── api/ # 接口相关
|
||||
│ ├── modules/ # 模块化 API 请求
|
||||
│ └── request.ts # 请求封装配置
|
||||
├── common/ # 公共资源
|
||||
│ ├── styles/ # 全局样式
|
||||
│ └── utils/ # 工具函数
|
||||
├── components/ # 公共组件
|
||||
├── lib/ # 第三方库
|
||||
├── mixins/ # Vue 混入
|
||||
├── pages/ # 主包页面
|
||||
├── store/ # 状态管理
|
||||
├── uni_modules/ # uni-app 组件库
|
||||
├── uview-plus/ # uView-Plus 组件库
|
||||
├── App.vue # 应用入口
|
||||
├── main.ts # 主入口文件
|
||||
├── pages.json # 页面路由和配置
|
||||
├── manifest.json # 应用配置
|
||||
├── uni.scss # 全局样式变量
|
||||
└── .env # 环境变量配置
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 将模板目录复制到您的项目目录下
|
||||
2. 根据需要修改 package.json 中的项目名称和描述
|
||||
3. 使用 npm install 安装依赖
|
||||
4. 根据需要修改 pages.json 中的页面配置
|
||||
5. 开始开发新功能
|
||||
|
||||
## 项目重要配置说明
|
||||
|
||||
### 核心依赖包
|
||||
- **UniApp**: 跨平台开发框架
|
||||
- **Vue3**: 渐进式 JavaScript 框架
|
||||
- **TypeScript**: JavaScript 超集,提供类型检查
|
||||
- **uView-Plus**: UI 组件库
|
||||
- **z-paging**: 分页组件
|
||||
- **Vuex**: 状态管理
|
||||
- **luch-request**: 网络请求库
|
||||
- **dotenv**: 环境变量注入
|
||||
|
||||
### 样式
|
||||
|
||||
- common.css: 全局基础样式,包含codefun原子类
|
||||
- base.scss: SCSS 变量和 mixins,提供常用的样式类生成器
|
||||
|
||||
### 工具函数 (tool.ts)
|
||||
|
||||
- alert: 文字轻提示
|
||||
- loading/hideLoading: 显示/隐藏加载提示
|
||||
- 页面跳转方法: navigateTo, redirectTo, reLaunch, switchTab, navigateBack
|
||||
- 本地缓存操作: storage, removeStorage, getStorageInfo
|
||||
- copy: 复制文本到剪贴板
|
||||
- saveImageToPhotos: 保存图片到相册
|
||||
- requestPayment: 微信支付
|
||||
- upload: 文件上传
|
||||
- loadFont: 加载外部字体
|
||||
|
||||
### 配置文件
|
||||
|
||||
- App.vue: 全局样式引入和基础设置
|
||||
- main.ts: Vue 应用初始化和全局组件注册
|
||||
- pages.json: 页面路由和窗口表现配置
|
||||
- manifest.json: 应用配置(appid等)
|
||||
- uni.scss: 全局 SCSS 变量
|
||||
- .env: 环境变量配置
|
||||
- vite.config.js: Vite 构建配置
|
||||
|
||||
## 开发注意事项
|
||||
|
||||
1. 实际项目开发中请根据需要修改或扩展功能函数
|
||||
2. 项目中的配置可根据具体需求进行修改和替换
|
||||
3. 请遵循项目中已有的代码规范和风格
|
||||
4. 样式文件可参考项目中的规范进行编写
|
||||
5. 网络请求统一使用 api/request.ts 中封装的 luch-request
|
||||
6. 工具函数统一在 common/utils/tool.ts 中维护
|
||||
7. 环境变量通过 .env 文件进行配置,构建时会自动替换 manifest.json 中的 appid
|
||||
45
api/request.ts
Normal file
45
api/request.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import Request, { HttpRequestConfig, HttpResponse, HttpError } from '@/lib/luch-request/index'
|
||||
import tool from '@/common/utils/tool'
|
||||
|
||||
// @ts-ignore
|
||||
const baseUrl = import.meta.env.VITE_BASE_URL
|
||||
const http = new Request()
|
||||
|
||||
/* 设置全局配置 */
|
||||
http.setConfig((config: HttpRequestConfig): HttpRequestConfig => {
|
||||
config.header = { ...config.header }
|
||||
config.sslVerify = false
|
||||
config.baseURL = baseUrl
|
||||
return config
|
||||
})
|
||||
|
||||
http.interceptors.request.use(
|
||||
async (config: HttpRequestConfig): Promise<HttpRequestConfig> => {
|
||||
config.header = { ...config.header }
|
||||
return config
|
||||
},
|
||||
(config: HttpRequestConfig): Promise<HttpRequestConfig> => {
|
||||
return Promise.reject(config)
|
||||
}
|
||||
)
|
||||
|
||||
http.interceptors.response.use((response: HttpResponse): Promise<HttpResponse> => {
|
||||
if (response.statusCode == 500 || response.statusCode == 404 || response.statusCode == 403) {
|
||||
console.error(response)
|
||||
// @ts-ignore
|
||||
return tool.alert('网络错误,请稍后重试')
|
||||
}
|
||||
|
||||
if (response.statusCode == 401 || response.data.code == 401) {
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return Promise.resolve(response)
|
||||
} else {
|
||||
return Promise.reject(response)
|
||||
}
|
||||
}, (error: HttpError): Promise<HttpError> => {
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
export default http
|
||||
127
common/styles/base.scss
Normal file
127
common/styles/base.scss
Normal file
@@ -0,0 +1,127 @@
|
||||
// 定义内外边距,历遍1-40
|
||||
@for $i from 0 through 40 {
|
||||
// 只要双数和能被5除尽的数
|
||||
@if $i % 2==0 or $i % 5==0 {
|
||||
// 得出:u-margin-30或者u-m-30
|
||||
.w-#{$i} {
|
||||
width: calc($i * 1%) !important;
|
||||
}
|
||||
.p-x-#{$i} {
|
||||
padding-left: $i + rpx !important;
|
||||
padding-right: $i + rpx !important;
|
||||
}
|
||||
|
||||
.p-y-#{$i} {
|
||||
padding-top: $i + rpx !important;
|
||||
padding-bottom: $i + rpx !important;
|
||||
}
|
||||
|
||||
.m-x-#{$i} {
|
||||
margin-left: $i + rpx !important;
|
||||
margin-right: $i + rpx !important;
|
||||
}
|
||||
|
||||
.m-y-#{$i} {
|
||||
margin-top: $i + rpx !important;
|
||||
margin-bottom: $i + rpx !important;
|
||||
}
|
||||
|
||||
.m-#{$i} {
|
||||
margin-left: $i + rpx !important;
|
||||
margin-right: $i + rpx !important;
|
||||
margin-top: $i + rpx !important;
|
||||
margin-bottom: $i + rpx !important;
|
||||
}
|
||||
|
||||
.p-#{$i} {
|
||||
padding-left: $i + rpx !important;
|
||||
padding-right: $i + rpx !important;
|
||||
padding-top: $i + rpx !important;
|
||||
padding-bottom: $i + rpx !important;
|
||||
}
|
||||
|
||||
.m-l-#{$i} {
|
||||
margin-left: $i + rpx !important;
|
||||
}
|
||||
|
||||
.m-t-#{$i} {
|
||||
margin-top: $i + rpx !important;
|
||||
}
|
||||
|
||||
.m-r-#{$i} {
|
||||
margin-right: $i + rpx !important;
|
||||
}
|
||||
|
||||
.m-b-#{$i} {
|
||||
margin-bottom: $i + rpx !important;
|
||||
}
|
||||
|
||||
.p-l-#{$i} {
|
||||
padding-left: $i + rpx !important;
|
||||
}
|
||||
|
||||
.p-t-#{$i} {
|
||||
padding-top: $i + rpx !important;
|
||||
}
|
||||
|
||||
.p-r-#{$i} {
|
||||
padding-right: $i + rpx !important;
|
||||
}
|
||||
|
||||
.p-b-#{$i} {
|
||||
padding-bottom: $i + rpx !important;
|
||||
}
|
||||
|
||||
.l-p-#{$i} {
|
||||
letter-spacing: $i + rpx !important;
|
||||
}
|
||||
|
||||
.z-i-#{$i} {
|
||||
z-index: $i;
|
||||
}
|
||||
|
||||
.l-h-#{$i} {
|
||||
line-height: $i + rpx !important;
|
||||
}
|
||||
.r-#{$i} {
|
||||
right: $i + rpx !important;
|
||||
}
|
||||
.l-#{$i} {
|
||||
left: $i + rpx !important;
|
||||
}
|
||||
|
||||
.t-#{$i} {
|
||||
top: $i + rpx !important;
|
||||
}
|
||||
.b-#{$i} {
|
||||
bottom: $i + rpx !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义字体(rpx)单位,大于或等于20的都为rpx单位字体
|
||||
@for $i from 9 through 60 {
|
||||
.font-#{$i} {
|
||||
font-size: $i + rpx !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 圆角
|
||||
@for $i from 4 through 60 {
|
||||
.rounded-#{$i} {
|
||||
border-radius: $i + rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 多行文本溢出
|
||||
@for $i from 1 through 5 {
|
||||
.over-line-#{$i} {
|
||||
width: 100%;
|
||||
height: calc($i * 1em + 0.5em);
|
||||
display: -webkit-box;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: $i;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
521
common/styles/common.css
Normal file
521
common/styles/common.css
Normal file
@@ -0,0 +1,521 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Microsoft Yahei', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
view,
|
||||
image,
|
||||
text {
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.code-fun-flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.code-fun-flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.code-fun-justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.code-fun-justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.code-fun-justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.code-fun-justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.code-fun-justify-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.code-fun-justify-evenly {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.code-fun-items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.code-fun-items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.code-fun-items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.code-fun-items-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.code-fun-items-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.code-fun-self-start {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.code-fun-self-end {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.code-fun-self-center {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.code-fun-self-baseline {
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
.code-fun-self-stretch {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.code-fun-flex-1 {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
.code-fun-flex-auto {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.code-fun-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.code-fun-grow-0 {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.code-fun-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.code-fun-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.code-fun-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.code-fun-ml-2 {
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-2 {
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-4 {
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-4 {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-6 {
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-6 {
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-8 {
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-8 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-10 {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-10 {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-12 {
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-12 {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-14 {
|
||||
margin-left: 28rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-14 {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-16 {
|
||||
margin-left: 32rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-16 {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-18 {
|
||||
margin-left: 36rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-18 {
|
||||
margin-top: 36rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-20 {
|
||||
margin-left: 40rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-20 {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-22 {
|
||||
margin-left: 44rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-22 {
|
||||
margin-top: 44rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-24 {
|
||||
margin-left: 48rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-24 {
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-26 {
|
||||
margin-left: 52rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-26 {
|
||||
margin-top: 52rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-28 {
|
||||
margin-left: 56rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-28 {
|
||||
margin-top: 56rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-30 {
|
||||
margin-left: 60rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-30 {
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-32 {
|
||||
margin-left: 64rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-32 {
|
||||
margin-top: 64rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-34 {
|
||||
margin-left: 68rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-34 {
|
||||
margin-top: 68rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-36 {
|
||||
margin-left: 72rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-36 {
|
||||
margin-top: 72rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-38 {
|
||||
margin-left: 76rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-38 {
|
||||
margin-top: 76rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-40 {
|
||||
margin-left: 80rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-40 {
|
||||
margin-top: 80rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-42 {
|
||||
margin-left: 84rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-42 {
|
||||
margin-top: 84rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-44 {
|
||||
margin-left: 88rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-44 {
|
||||
margin-top: 88rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-46 {
|
||||
margin-left: 92rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-46 {
|
||||
margin-top: 92rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-48 {
|
||||
margin-left: 96rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-48 {
|
||||
margin-top: 96rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-50 {
|
||||
margin-left: 100rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-50 {
|
||||
margin-top: 100rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-52 {
|
||||
margin-left: 104rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-52 {
|
||||
margin-top: 104rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-54 {
|
||||
margin-left: 108rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-54 {
|
||||
margin-top: 108rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-56 {
|
||||
margin-left: 112rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-56 {
|
||||
margin-top: 112rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-58 {
|
||||
margin-left: 116rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-58 {
|
||||
margin-top: 116rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-60 {
|
||||
margin-left: 120rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-60 {
|
||||
margin-top: 120rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-62 {
|
||||
margin-left: 124rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-62 {
|
||||
margin-top: 124rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-64 {
|
||||
margin-left: 128rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-64 {
|
||||
margin-top: 128rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-66 {
|
||||
margin-left: 132rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-66 {
|
||||
margin-top: 132rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-68 {
|
||||
margin-left: 136rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-68 {
|
||||
margin-top: 136rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-70 {
|
||||
margin-left: 140rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-70 {
|
||||
margin-top: 140rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-72 {
|
||||
margin-left: 144rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-72 {
|
||||
margin-top: 144rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-74 {
|
||||
margin-left: 148rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-74 {
|
||||
margin-top: 148rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-76 {
|
||||
margin-left: 152rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-76 {
|
||||
margin-top: 152rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-78 {
|
||||
margin-left: 156rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-78 {
|
||||
margin-top: 156rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-80 {
|
||||
margin-left: 160rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-80 {
|
||||
margin-top: 160rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-82 {
|
||||
margin-left: 164rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-82 {
|
||||
margin-top: 164rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-84 {
|
||||
margin-left: 168rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-84 {
|
||||
margin-top: 168rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-86 {
|
||||
margin-left: 172rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-86 {
|
||||
margin-top: 172rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-88 {
|
||||
margin-left: 176rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-88 {
|
||||
margin-top: 176rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-90 {
|
||||
margin-left: 180rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-90 {
|
||||
margin-top: 180rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-92 {
|
||||
margin-left: 184rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-92 {
|
||||
margin-top: 184rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-94 {
|
||||
margin-left: 188rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-94 {
|
||||
margin-top: 188rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-96 {
|
||||
margin-left: 192rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-96 {
|
||||
margin-top: 192rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-98 {
|
||||
margin-left: 196rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-98 {
|
||||
margin-top: 196rpx;
|
||||
}
|
||||
|
||||
.code-fun-ml-100 {
|
||||
margin-left: 200rpx;
|
||||
}
|
||||
|
||||
.code-fun-mt-100 {
|
||||
margin-top: 200rpx;
|
||||
}
|
||||
402
common/utils/tool.ts
Normal file
402
common/utils/tool.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
// @ts-ignore
|
||||
const baseUrl = import.meta.env.VITE_BASE_URL
|
||||
// @ts-ignore
|
||||
const assetsUrl = import.meta.env.VITE_ASSETSURL
|
||||
|
||||
interface StorageInfo {
|
||||
keys: string[]
|
||||
currentSize: number
|
||||
limitSize: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具类 - 提供常用的工具方法
|
||||
* @class Tool
|
||||
*/
|
||||
class Tool {
|
||||
ICON_TYPES: { [key: string]: number }
|
||||
loadedFonts: Set<string>
|
||||
|
||||
constructor() {
|
||||
// 图标类型映射
|
||||
this.ICON_TYPES = {
|
||||
NONE: 0,
|
||||
SUCCESS: 1,
|
||||
LOADING: 2,
|
||||
}
|
||||
|
||||
// 字体加载状态缓存
|
||||
this.loadedFonts = new Set()
|
||||
}
|
||||
|
||||
/**
|
||||
* 文字轻提示
|
||||
* @param {string} str 提示文字
|
||||
* @param {number} [icon=0] 提示icon (0: none, 1: success, 2: loading)
|
||||
* @param {number} [duration=1500] 提示时间(毫秒)
|
||||
*/
|
||||
alert(str: string, icon: number = this.ICON_TYPES.NONE, duration: number = 1500): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!str && str !== '0') {
|
||||
console.warn('alert方法需要提供提示文字')
|
||||
return
|
||||
}
|
||||
|
||||
const iconMap: { [key: number]: string } = {
|
||||
[this.ICON_TYPES.NONE]: 'none',
|
||||
[this.ICON_TYPES.SUCCESS]: 'success',
|
||||
[this.ICON_TYPES.LOADING]: 'loading',
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
uni.showToast({
|
||||
title: String(str),
|
||||
// @ts-ignore
|
||||
icon: iconMap[icon] || 'none',
|
||||
mask: true,
|
||||
duration,
|
||||
success: () => {
|
||||
setTimeout(resolve, duration)
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示loading加载
|
||||
* @param {string} [title=' '] 加载文案
|
||||
* @param {boolean} [mask=true] 是否显示遮罩
|
||||
*/
|
||||
loading(title: string = ' ', mask: boolean = true): void {
|
||||
// @ts-ignore
|
||||
uni.showLoading({ title, mask })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭loading提示框
|
||||
*/
|
||||
hideLoading(): void {
|
||||
// @ts-ignore
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一处理URL格式,确保以/开头
|
||||
* @param {string} url 页面地址
|
||||
* @returns {string} 格式化后的URL
|
||||
* @private
|
||||
*/
|
||||
_formatUrl(url: string): string {
|
||||
if (!url || typeof url !== 'string') {
|
||||
throw new Error('URL必须是字符串')
|
||||
}
|
||||
|
||||
return url.startsWith('/') ? url : `/${url}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 可返回跳转(导航到新页面)
|
||||
* @param {string} url 页面地址
|
||||
*/
|
||||
navigateTo(url: string): void {
|
||||
const formattedUrl = this._formatUrl(url)
|
||||
|
||||
// @ts-ignore
|
||||
uni.navigateTo({
|
||||
url: formattedUrl,
|
||||
fail: (err: any) => {
|
||||
console.warn('navigateTo失败,尝试switchTab:', err)
|
||||
// @ts-ignore
|
||||
uni.switchTab({ url: formattedUrl })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 不可返回跳转(重定向到新页面)
|
||||
* @param {string} url 页面地址
|
||||
*/
|
||||
redirectTo(url: string): void {
|
||||
// @ts-ignore
|
||||
uni.redirectTo({ url: this._formatUrl(url) })
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除页面栈跳转(重新启动到新页面)
|
||||
* @param {string} url 页面地址
|
||||
*/
|
||||
reLaunch(url: string): void {
|
||||
// @ts-ignore
|
||||
uni.reLaunch({ url: this._formatUrl(url) })
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转tabBar页
|
||||
* @param {string} url 页面地址
|
||||
*/
|
||||
switchTab(url: string): void {
|
||||
// @ts-ignore
|
||||
uni.switchTab({ url: this._formatUrl(url) })
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页面或指定页面
|
||||
* @param {number} [delta=1] 返回的页面数
|
||||
* @param {string} [fallbackUrl='/pages/index/index'] 无上一页时的回退地址
|
||||
*/
|
||||
navigateBack(delta: number = 1, fallbackUrl: string = '/pages/index/index'): void {
|
||||
// @ts-ignore
|
||||
const pages = getCurrentPages()
|
||||
|
||||
if (pages.length <= 1) {
|
||||
console.warn('无上一页,使用回退地址')
|
||||
// @ts-ignore
|
||||
uni.reLaunch({ url: fallbackUrl })
|
||||
} else {
|
||||
// @ts-ignore
|
||||
uni.navigateBack({ delta })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作本地缓存
|
||||
* @param {string} key 缓存键值
|
||||
* @param {any} [value] 缓存数据,不传则为读取
|
||||
* @returns {any|undefined} 读取操作时返回数据
|
||||
*/
|
||||
storage(key: string, value?: any): any {
|
||||
if (typeof key !== 'string') {
|
||||
throw new Error('key必须是字符串')
|
||||
}
|
||||
|
||||
// 设置操作
|
||||
if (value !== undefined && value !== null) {
|
||||
// @ts-ignore
|
||||
uni.setStorageSync(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
// 读取操作
|
||||
if (key !== '#') {
|
||||
// @ts-ignore
|
||||
return uni.getStorageSync(key)
|
||||
}
|
||||
|
||||
// 特殊操作
|
||||
if (key === '#') {
|
||||
// @ts-ignore
|
||||
uni.clearStorageSync()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定缓存
|
||||
* @param {string} key 要删除的缓存键
|
||||
*/
|
||||
removeStorage(key: string): void {
|
||||
if (typeof key !== 'string') {
|
||||
throw new Error('key必须是字符串')
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
uni.removeStorageSync(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存信息
|
||||
* @returns {Object} 缓存信息
|
||||
*/
|
||||
getStorageInfo(): StorageInfo {
|
||||
// @ts-ignore
|
||||
return uni.getStorageInfoSync()
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板
|
||||
* @param {string} data 要复制的文本
|
||||
* @returns {Promise<boolean>} 复制是否成功
|
||||
*/
|
||||
async copy(data: string): Promise<boolean> {
|
||||
if (!data && data !== '0') {
|
||||
this.alert('暂无内容')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
uni.setClipboardData({
|
||||
data: String(data),
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
|
||||
this.alert('复制成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
this.alert('复制失败,请重试')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入外部字体
|
||||
* @param {string} fontName 字体文件名(不含路径)
|
||||
* @returns {Promise<boolean>} 字体加载是否成功
|
||||
*/
|
||||
async loadFont(fontName: string): Promise<boolean> {
|
||||
if (!fontName || typeof fontName !== 'string') {
|
||||
throw new Error('字体名称必须是字符串')
|
||||
}
|
||||
|
||||
// 检查是否已加载过
|
||||
if (this.loadedFonts.has(fontName)) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
const fontFamily = fontName.replace(/\.[^/.]+$/, '') // 移除文件扩展名
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
uni.loadFontFace({
|
||||
family: fontFamily,
|
||||
source: `url(${assetsUrl}${fontName})`,
|
||||
global: true,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
|
||||
this.loadedFonts.add(fontName)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`字体加载失败: ${fontName}`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图片到相册
|
||||
* @param {string} url 图片URL
|
||||
* @returns {Promise<boolean>} 保存是否成功
|
||||
*/
|
||||
async saveImageToPhotos(url: string): Promise<boolean> {
|
||||
if (!url) {
|
||||
this.alert('图片地址不能为空')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查权限
|
||||
const { authSetting } = await new Promise((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
uni.getSetting({
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
})
|
||||
}) as any
|
||||
|
||||
if (!authSetting['scope.writePhotosAlbum']) {
|
||||
// 请求权限
|
||||
await new Promise((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
uni.authorize({
|
||||
scope: 'scope.writePhotosAlbum',
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 获取图片信息
|
||||
const { path } = await new Promise((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
uni.getImageInfo({
|
||||
src: url,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
})
|
||||
}) as any
|
||||
|
||||
// 保存到相册
|
||||
await new Promise((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: path,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
|
||||
this.alert('已保存到相册')
|
||||
return true
|
||||
} catch (error: any) {
|
||||
console.error('保存图片失败:', error)
|
||||
|
||||
if (error.errMsg && error.errMsg.includes('auth')) {
|
||||
// 权限相关错误
|
||||
await new Promise(resolve => {
|
||||
// @ts-ignore
|
||||
uni.showModal({
|
||||
title: '保存失败',
|
||||
content: '请开启访问手机相册权限',
|
||||
showCancel: false,
|
||||
success: resolve,
|
||||
})
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
uni.openSetting()
|
||||
} else {
|
||||
this.alert('保存失败,请重试')
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信支付
|
||||
* @param {Object} paymentData 支付参数
|
||||
* @returns {Promise<Object>} 支付结果
|
||||
*/
|
||||
requestPayment(paymentData: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
uni.requestPayment({
|
||||
provider: 'wxpay',
|
||||
...paymentData,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
upload(filePath: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
uni.uploadFile({
|
||||
url: `${baseUrl}file/upload`,
|
||||
fileType: 'image',
|
||||
header: {
|
||||
Authorization: `Bearer ${this.storage('token')}`,
|
||||
},
|
||||
filePath,
|
||||
name: 'file',
|
||||
success: ({ data }: any) => {
|
||||
resolve(JSON.parse(data))
|
||||
},
|
||||
fail: (error: any) => {
|
||||
reject(error)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例并导出
|
||||
export default new Tool()
|
||||
99
lib/luch-request/adapters/index.js
Normal file
99
lib/luch-request/adapters/index.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import buildURL from '../helpers/buildURL'
|
||||
import buildFullPath from '../core/buildFullPath'
|
||||
import settle from '../core/settle'
|
||||
import { isUndefined } from "../utils"
|
||||
|
||||
/**
|
||||
* 返回可选值存在的配置
|
||||
* @param {Array} keys - 可选值数组
|
||||
* @param {Object} config2 - 配置
|
||||
* @return {{}} - 存在的配置项
|
||||
*/
|
||||
const mergeKeys = (keys, config2) => {
|
||||
let config = {}
|
||||
keys.forEach(prop => {
|
||||
if (!isUndefined(config2[prop])) {
|
||||
config[prop] = config2[prop]
|
||||
}
|
||||
})
|
||||
return config
|
||||
}
|
||||
export default (config) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let fullPath = buildURL(buildFullPath(config.baseURL, config.url), config.params)
|
||||
const _config = {
|
||||
url: fullPath,
|
||||
header: config.header,
|
||||
complete: (response) => {
|
||||
config.fullPath = fullPath
|
||||
response.config = config
|
||||
try {
|
||||
// 对可能字符串不是json 的情况容错
|
||||
if (typeof response.data === 'string') {
|
||||
response.data = JSON.parse(response.data)
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {
|
||||
}
|
||||
settle(resolve, reject, response)
|
||||
}
|
||||
}
|
||||
let requestTask
|
||||
if (config.method === 'UPLOAD') {
|
||||
delete _config.header['content-type']
|
||||
delete _config.header['Content-Type']
|
||||
let otherConfig = {
|
||||
// #ifdef MP-ALIPAY
|
||||
fileType: config.fileType,
|
||||
// #endif
|
||||
filePath: config.filePath,
|
||||
name: config.name
|
||||
}
|
||||
const optionalKeys = [
|
||||
// #ifdef APP-PLUS || H5
|
||||
'files',
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
'file',
|
||||
// #endif
|
||||
// #ifdef H5 || APP-PLUS
|
||||
'timeout',
|
||||
// #endif
|
||||
'formData'
|
||||
]
|
||||
requestTask = uni.uploadFile({..._config, ...otherConfig, ...mergeKeys(optionalKeys, config)})
|
||||
} else if (config.method === 'DOWNLOAD') {
|
||||
// #ifdef H5 || APP-PLUS
|
||||
if (!isUndefined(config['timeout'])) {
|
||||
_config['timeout'] = config['timeout']
|
||||
}
|
||||
// #endif
|
||||
requestTask = uni.downloadFile(_config)
|
||||
} else {
|
||||
const optionalKeys = [
|
||||
'data',
|
||||
'method',
|
||||
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
|
||||
'timeout',
|
||||
// #endif
|
||||
'dataType',
|
||||
// #ifndef MP-ALIPAY || APP-PLUS
|
||||
'responseType',
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
'sslVerify',
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
'withCredentials',
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
'firstIpv4',
|
||||
// #endif
|
||||
]
|
||||
requestTask = uni.request({..._config,...mergeKeys(optionalKeys, config)})
|
||||
}
|
||||
if (config.getTask) {
|
||||
config.getTask(requestTask, config)
|
||||
}
|
||||
})
|
||||
}
|
||||
51
lib/luch-request/core/InterceptorManager.js
Normal file
51
lib/luch-request/core/InterceptorManager.js
Normal file
@@ -0,0 +1,51 @@
|
||||
'use strict'
|
||||
|
||||
|
||||
function InterceptorManager() {
|
||||
this.handlers = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new interceptor to the stack
|
||||
*
|
||||
* @param {Function} fulfilled The function to handle `then` for a `Promise`
|
||||
* @param {Function} rejected The function to handle `reject` for a `Promise`
|
||||
*
|
||||
* @return {Number} An ID used to remove interceptor later
|
||||
*/
|
||||
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
|
||||
this.handlers.push({
|
||||
fulfilled: fulfilled,
|
||||
rejected: rejected
|
||||
})
|
||||
return this.handlers.length - 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an interceptor from the stack
|
||||
*
|
||||
* @param {Number} id The ID that was returned by `use`
|
||||
*/
|
||||
InterceptorManager.prototype.eject = function eject(id) {
|
||||
if (this.handlers[id]) {
|
||||
this.handlers[id] = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over all the registered interceptors
|
||||
*
|
||||
* This method is particularly useful for skipping over any
|
||||
* interceptors that may have become `null` calling `eject`.
|
||||
*
|
||||
* @param {Function} fn The function to call for each interceptor
|
||||
*/
|
||||
InterceptorManager.prototype.forEach = function forEach(fn) {
|
||||
this.handlers.forEach(h => {
|
||||
if (h !== null) {
|
||||
fn(h)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default InterceptorManager
|
||||
199
lib/luch-request/core/Request.js
Normal file
199
lib/luch-request/core/Request.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* @Class Request
|
||||
* @description luch-request http请求插件
|
||||
* @version 3.0.5
|
||||
* @Author lu-ch
|
||||
* @Date 2021-01-06
|
||||
* @Email webwork.s@qq.com
|
||||
* 文档: https://www.quanzhan.co/luch-request/
|
||||
* github: https://github.com/lei-mu/luch-request
|
||||
* DCloud: http://ext.dcloud.net.cn/plugin?id=392
|
||||
* HBuilderX: beat-3.0.4 alpha-3.0.4
|
||||
*/
|
||||
|
||||
|
||||
import dispatchRequest from './dispatchRequest'
|
||||
import InterceptorManager from './InterceptorManager'
|
||||
import mergeConfig from './mergeConfig'
|
||||
import defaults from './defaults'
|
||||
import { isPlainObject } from '../utils'
|
||||
|
||||
export default class Request {
|
||||
/**
|
||||
* @param {Object} arg - 全局配置
|
||||
* @param {String} arg.baseURL - 全局根路径
|
||||
* @param {Object} arg.header - 全局header
|
||||
* @param {String} arg.method = [GET|POST|PUT|DELETE|CONNECT|HEAD|OPTIONS|TRACE] - 全局默认请求方式
|
||||
* @param {String} arg.dataType = [json] - 全局默认的dataType
|
||||
* @param {String} arg.responseType = [text|arraybuffer] - 全局默认的responseType。App和支付宝小程序不支持
|
||||
* @param {Object} arg.custom - 全局默认的自定义参数
|
||||
* @param {Number} arg.timeout - 全局默认的超时时间,单位 ms。默认60000。H5(HBuilderX 2.9.9+)、APP(HBuilderX 2.9.9+)、微信小程序(2.10.0)、支付宝小程序
|
||||
* @param {Boolean} arg.sslVerify - 全局默认的是否验证 ssl 证书。默认true.仅App安卓端支持(HBuilderX 2.3.3+)
|
||||
* @param {Boolean} arg.withCredentials - 全局默认的跨域请求时是否携带凭证(cookies)。默认false。仅H5支持(HBuilderX 2.6.15+)
|
||||
* @param {Boolean} arg.firstIpv4 - 全DNS解析时优先使用ipv4。默认false。仅 App-Android 支持 (HBuilderX 2.8.0+)
|
||||
* @param {Function(statusCode):Boolean} arg.validateStatus - 全局默认的自定义验证器。默认statusCode >= 200 && statusCode < 300
|
||||
*/
|
||||
constructor(arg = {}) {
|
||||
if (!isPlainObject(arg)) {
|
||||
arg = {}
|
||||
console.warn('设置全局参数必须接收一个Object')
|
||||
}
|
||||
this.config = {...defaults, ...arg}
|
||||
this.interceptors = {
|
||||
request: new InterceptorManager(),
|
||||
response: new InterceptorManager()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Function
|
||||
* @param {Request~setConfigCallback} f - 设置全局默认配置
|
||||
*/
|
||||
setConfig(f) {
|
||||
this.config = f(this.config)
|
||||
}
|
||||
|
||||
middleware(config) {
|
||||
config = mergeConfig(this.config, config)
|
||||
let chain = [dispatchRequest, undefined]
|
||||
let promise = Promise.resolve(config)
|
||||
|
||||
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
|
||||
chain.unshift(interceptor.fulfilled, interceptor.rejected)
|
||||
})
|
||||
|
||||
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
|
||||
chain.push(interceptor.fulfilled, interceptor.rejected)
|
||||
})
|
||||
|
||||
while (chain.length) {
|
||||
promise = promise.then(chain.shift(), chain.shift())
|
||||
}
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
* @Function
|
||||
* @param {Object} config - 请求配置项
|
||||
* @prop {String} options.url - 请求路径
|
||||
* @prop {Object} options.data - 请求参数
|
||||
* @prop {Object} [options.responseType = config.responseType] [text|arraybuffer] - 响应的数据类型
|
||||
* @prop {Object} [options.dataType = config.dataType] - 如果设为 json,会尝试对返回的数据做一次 JSON.parse
|
||||
* @prop {Object} [options.header = config.header] - 请求header
|
||||
* @prop {Object} [options.method = config.method] - 请求方法
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
request(config = {}) {
|
||||
return this.middleware(config)
|
||||
}
|
||||
|
||||
get(url, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
method: 'GET',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
post(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'POST',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #ifndef MP-ALIPAY
|
||||
put(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'PUT',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
|
||||
delete(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'DELETE',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifdef H5 || MP-WEIXIN
|
||||
connect(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'CONNECT',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifdef H5 || MP-WEIXIN || MP-BAIDU
|
||||
head(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'HEAD',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
|
||||
options(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'OPTIONS',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifdef H5 || MP-WEIXIN
|
||||
trace(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'TRACE',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
upload(url, config = {}) {
|
||||
config.url = url
|
||||
config.method = 'UPLOAD'
|
||||
return this.middleware(config)
|
||||
}
|
||||
|
||||
download(url, config = {}) {
|
||||
config.url = url
|
||||
config.method = 'DOWNLOAD'
|
||||
return this.middleware(config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* setConfig回调
|
||||
* @return {Object} - 返回操作后的config
|
||||
* @callback Request~setConfigCallback
|
||||
* @param {Object} config - 全局默认config
|
||||
*/
|
||||
20
lib/luch-request/core/buildFullPath.js
Normal file
20
lib/luch-request/core/buildFullPath.js
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict'
|
||||
|
||||
import isAbsoluteURL from '../helpers/isAbsoluteURL'
|
||||
import combineURLs from '../helpers/combineURLs'
|
||||
|
||||
/**
|
||||
* Creates a new URL by combining the baseURL with the requestedURL,
|
||||
* only when the requestedURL is not already an absolute URL.
|
||||
* If the requestURL is absolute, this function returns the requestedURL untouched.
|
||||
*
|
||||
* @param {string} baseURL The base URL
|
||||
* @param {string} requestedURL Absolute or relative URL to combine
|
||||
* @returns {string} The combined full path
|
||||
*/
|
||||
export default function buildFullPath(baseURL, requestedURL) {
|
||||
if (baseURL && !isAbsoluteURL(requestedURL)) {
|
||||
return combineURLs(baseURL, requestedURL)
|
||||
}
|
||||
return requestedURL
|
||||
}
|
||||
30
lib/luch-request/core/defaults.js
Normal file
30
lib/luch-request/core/defaults.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 默认的全局配置
|
||||
*/
|
||||
|
||||
|
||||
export default {
|
||||
baseURL: '',
|
||||
header: {},
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
// #ifndef MP-ALIPAY || APP-PLUS
|
||||
responseType: 'text',
|
||||
// #endif
|
||||
custom: {},
|
||||
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
|
||||
timeout: 60000,
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
sslVerify: true,
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
withCredentials: false,
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
firstIpv4: false,
|
||||
// #endif
|
||||
validateStatus: function validateStatus(status) {
|
||||
return status >= 200 && status < 300
|
||||
}
|
||||
}
|
||||
6
lib/luch-request/core/dispatchRequest.js
Normal file
6
lib/luch-request/core/dispatchRequest.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import adapter from '../adapters/index'
|
||||
|
||||
|
||||
export default (config) => {
|
||||
return adapter(config)
|
||||
}
|
||||
103
lib/luch-request/core/mergeConfig.js
Normal file
103
lib/luch-request/core/mergeConfig.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import {deepMerge, isUndefined} from '../utils'
|
||||
|
||||
/**
|
||||
* 合并局部配置优先的配置,如果局部有该配置项则用局部,如果全局有该配置项则用全局
|
||||
* @param {Array} keys - 配置项
|
||||
* @param {Object} globalsConfig - 当前的全局配置
|
||||
* @param {Object} config2 - 局部配置
|
||||
* @return {{}}
|
||||
*/
|
||||
const mergeKeys = (keys, globalsConfig, config2) => {
|
||||
let config = {}
|
||||
keys.forEach(prop => {
|
||||
if (!isUndefined(config2[prop])) {
|
||||
config[prop] = config2[prop]
|
||||
} else if (!isUndefined(globalsConfig[prop])) {
|
||||
config[prop] = globalsConfig[prop]
|
||||
}
|
||||
})
|
||||
return config
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param globalsConfig - 当前实例的全局配置
|
||||
* @param config2 - 当前的局部配置
|
||||
* @return - 合并后的配置
|
||||
*/
|
||||
export default (globalsConfig, config2 = {}) => {
|
||||
const method = config2.method || globalsConfig.method || 'GET'
|
||||
let config = {
|
||||
baseURL: globalsConfig.baseURL || '',
|
||||
method: method,
|
||||
url: config2.url || '',
|
||||
params: config2.params || {},
|
||||
custom: {...(globalsConfig.custom || {}), ...(config2.custom || {})},
|
||||
header: deepMerge(globalsConfig.header || {}, config2.header || {})
|
||||
}
|
||||
const defaultToConfig2Keys = ['getTask', 'validateStatus']
|
||||
config = {...config, ...mergeKeys(defaultToConfig2Keys, globalsConfig, config2)}
|
||||
|
||||
// eslint-disable-next-line no-empty
|
||||
if (method === 'DOWNLOAD') {
|
||||
// #ifdef H5 || APP-PLUS
|
||||
if (!isUndefined(config2.timeout)) {
|
||||
config['timeout'] = config2['timeout']
|
||||
} else if (!isUndefined(globalsConfig.timeout)) {
|
||||
config['timeout'] = globalsConfig['timeout']
|
||||
}
|
||||
// #endif
|
||||
} else if (method === 'UPLOAD') {
|
||||
delete config.header['content-type']
|
||||
delete config.header['Content-Type']
|
||||
const uploadKeys = [
|
||||
// #ifdef APP-PLUS || H5
|
||||
'files',
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
'fileType',
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
'file',
|
||||
// #endif
|
||||
'filePath',
|
||||
'name',
|
||||
// #ifdef H5 || APP-PLUS
|
||||
'timeout',
|
||||
// #endif
|
||||
'formData',
|
||||
]
|
||||
uploadKeys.forEach(prop => {
|
||||
if (!isUndefined(config2[prop])) {
|
||||
config[prop] = config2[prop]
|
||||
}
|
||||
})
|
||||
// #ifdef H5 || APP-PLUS
|
||||
if (isUndefined(config.timeout) && !isUndefined(globalsConfig.timeout)) {
|
||||
config['timeout'] = globalsConfig['timeout']
|
||||
}
|
||||
// #endif
|
||||
} else {
|
||||
const defaultsKeys = [
|
||||
'data',
|
||||
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
|
||||
'timeout',
|
||||
// #endif
|
||||
'dataType',
|
||||
// #ifndef MP-ALIPAY || APP-PLUS
|
||||
'responseType',
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
'sslVerify',
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
'withCredentials',
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
'firstIpv4',
|
||||
// #endif
|
||||
]
|
||||
config = {...config, ...mergeKeys(defaultsKeys, globalsConfig, config2)}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
16
lib/luch-request/core/settle.js
Normal file
16
lib/luch-request/core/settle.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Resolve or reject a Promise based on response status.
|
||||
*
|
||||
* @param {Function} resolve A function that resolves the promise.
|
||||
* @param {Function} reject A function that rejects the promise.
|
||||
* @param {object} response The response.
|
||||
*/
|
||||
export default function settle(resolve, reject, response) {
|
||||
const validateStatus = response.config.validateStatus
|
||||
const status = response.statusCode
|
||||
if (status && (!validateStatus || validateStatus(status))) {
|
||||
resolve(response)
|
||||
} else {
|
||||
reject(response)
|
||||
}
|
||||
}
|
||||
69
lib/luch-request/helpers/buildURL.js
Normal file
69
lib/luch-request/helpers/buildURL.js
Normal file
@@ -0,0 +1,69 @@
|
||||
'use strict'
|
||||
|
||||
import * as utils from './../utils'
|
||||
|
||||
function encode(val) {
|
||||
return encodeURIComponent(val).
|
||||
replace(/%40/gi, '@').
|
||||
replace(/%3A/gi, ':').
|
||||
replace(/%24/g, '$').
|
||||
replace(/%2C/gi, ',').
|
||||
replace(/%20/g, '+').
|
||||
replace(/%5B/gi, '[').
|
||||
replace(/%5D/gi, ']')
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a URL by appending params to the end
|
||||
*
|
||||
* @param {string} url The base of the url (e.g., http://www.google.com)
|
||||
* @param {object} [params] The params to be appended
|
||||
* @returns {string} The formatted url
|
||||
*/
|
||||
export default function buildURL(url, params) {
|
||||
/*eslint no-param-reassign:0*/
|
||||
if (!params) {
|
||||
return url
|
||||
}
|
||||
|
||||
var serializedParams
|
||||
if (utils.isURLSearchParams(params)) {
|
||||
serializedParams = params.toString()
|
||||
} else {
|
||||
var parts = []
|
||||
|
||||
utils.forEach(params, function serialize(val, key) {
|
||||
if (val === null || typeof val === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
if (utils.isArray(val)) {
|
||||
key = key + '[]'
|
||||
} else {
|
||||
val = [val]
|
||||
}
|
||||
|
||||
utils.forEach(val, function parseValue(v) {
|
||||
if (utils.isDate(v)) {
|
||||
v = v.toISOString()
|
||||
} else if (utils.isObject(v)) {
|
||||
v = JSON.stringify(v)
|
||||
}
|
||||
parts.push(encode(key) + '=' + encode(v))
|
||||
})
|
||||
})
|
||||
|
||||
serializedParams = parts.join('&')
|
||||
}
|
||||
|
||||
if (serializedParams) {
|
||||
var hashmarkIndex = url.indexOf('#')
|
||||
if (hashmarkIndex !== -1) {
|
||||
url = url.slice(0, hashmarkIndex)
|
||||
}
|
||||
|
||||
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
14
lib/luch-request/helpers/combineURLs.js
Normal file
14
lib/luch-request/helpers/combineURLs.js
Normal file
@@ -0,0 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Creates a new URL by combining the specified URLs
|
||||
*
|
||||
* @param {string} baseURL The base URL
|
||||
* @param {string} relativeURL The relative URL
|
||||
* @returns {string} The combined URL
|
||||
*/
|
||||
export default function combineURLs(baseURL, relativeURL) {
|
||||
return relativeURL
|
||||
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
|
||||
: baseURL
|
||||
}
|
||||
14
lib/luch-request/helpers/isAbsoluteURL.js
Normal file
14
lib/luch-request/helpers/isAbsoluteURL.js
Normal file
@@ -0,0 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Determines whether the specified URL is absolute
|
||||
*
|
||||
* @param {string} url The URL to test
|
||||
* @returns {boolean} True if the specified URL is absolute, otherwise false
|
||||
*/
|
||||
export default function isAbsoluteURL(url) {
|
||||
// A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
|
||||
// RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
|
||||
// by any combination of letters, digits, plus, period, or hyphen.
|
||||
return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url)
|
||||
}
|
||||
116
lib/luch-request/index.d.ts
vendored
Normal file
116
lib/luch-request/index.d.ts
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
type AnyObject = Record<string | number | symbol, any>
|
||||
type HttpPromise<T> = Promise<HttpResponse<T>>;
|
||||
type Tasks = UniApp.RequestTask | UniApp.UploadTask | UniApp.DownloadTask
|
||||
export interface RequestTask {
|
||||
abort: () => void;
|
||||
offHeadersReceived: () => void;
|
||||
onHeadersReceived: () => void;
|
||||
}
|
||||
export interface HttpRequestConfig<T = Tasks> {
|
||||
/** 请求基地址 */
|
||||
baseURL?: string;
|
||||
/** 请求服务器接口地址 */
|
||||
url?: string;
|
||||
|
||||
/** 请求查询参数,自动拼接为查询字符串 */
|
||||
params?: AnyObject;
|
||||
/** 请求体参数 */
|
||||
data?: AnyObject;
|
||||
|
||||
/** 文件对应的 key */
|
||||
name?: string;
|
||||
/** HTTP 请求中其他额外的 form data */
|
||||
formData?: AnyObject;
|
||||
/** 要上传文件资源的路径。 */
|
||||
filePath?: string;
|
||||
/** 需要上传的文件列表。使用 files 时,filePath 和 name 不生效,App、H5( 2.6.15+) */
|
||||
files?: Array<{
|
||||
name?: string;
|
||||
file?: File;
|
||||
uri: string;
|
||||
}>;
|
||||
/** 要上传的文件对象,仅H5(2.6.15+)支持 */
|
||||
file?: File;
|
||||
|
||||
/** 请求头信息 */
|
||||
header?: AnyObject;
|
||||
/** 请求方式 */
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE" | "CONNECT" | "HEAD" | "OPTIONS" | "TRACE" | "UPLOAD" | "DOWNLOAD";
|
||||
/** 如果设为 json,会尝试对返回的数据做一次 JSON.parse */
|
||||
dataType?: string;
|
||||
/** 设置响应的数据类型,App和支付宝小程序不支持 */
|
||||
responseType?: "text" | "arraybuffer";
|
||||
/** 自定义参数 */
|
||||
custom?: AnyObject;
|
||||
/** 超时时间,仅微信小程序(2.10.0)、支付宝小程序支持 */
|
||||
timeout?: number;
|
||||
/** DNS解析时优先使用ipv4,仅 App-Android 支持 (HBuilderX 2.8.0+) */
|
||||
firstIpv4?: boolean;
|
||||
/** 验证 ssl 证书 仅5+App安卓端支持(HBuilderX 2.3.3+) */
|
||||
sslVerify?: boolean;
|
||||
/** 跨域请求时是否携带凭证(cookies)仅H5支持(HBuilderX 2.6.15+) */
|
||||
withCredentials?: boolean;
|
||||
|
||||
/** 返回当前请求的task, options。请勿在此处修改options。 */
|
||||
getTask?: (task: T, options: HttpRequestConfig<T>) => void;
|
||||
/** 全局自定义验证器 */
|
||||
validateStatus?: (statusCode: number) => boolean | void;
|
||||
}
|
||||
export interface HttpResponse<T = any> {
|
||||
config: HttpRequestConfig;
|
||||
statusCode: number;
|
||||
cookies: Array<string>;
|
||||
data: T;
|
||||
errMsg: string;
|
||||
header: AnyObject;
|
||||
}
|
||||
export interface HttpUploadResponse<T = any> {
|
||||
config: HttpRequestConfig;
|
||||
statusCode: number;
|
||||
data: T;
|
||||
errMsg: string;
|
||||
}
|
||||
export interface HttpDownloadResponse extends HttpResponse {
|
||||
tempFilePath: string;
|
||||
}
|
||||
export interface HttpError {
|
||||
config: HttpRequestConfig;
|
||||
statusCode?: number;
|
||||
cookies?: Array<string>;
|
||||
data?: any;
|
||||
errMsg: string;
|
||||
header?: AnyObject;
|
||||
}
|
||||
export interface HttpInterceptorManager<V, E = V> {
|
||||
use(
|
||||
onFulfilled?: (config: V) => Promise<V> | V,
|
||||
onRejected?: (config: E) => Promise<E> | E
|
||||
): void;
|
||||
eject(id: number): void;
|
||||
}
|
||||
export abstract class HttpRequestAbstract {
|
||||
constructor(config?: HttpRequestConfig);
|
||||
config: HttpRequestConfig;
|
||||
interceptors: {
|
||||
request: HttpInterceptorManager<HttpRequestConfig, HttpRequestConfig>;
|
||||
response: HttpInterceptorManager<HttpResponse, HttpError>;
|
||||
}
|
||||
middleware<T = any>(config: HttpRequestConfig): HttpPromise<T>;
|
||||
request<T = any>(config: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
get<T = any>(url: string, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
upload<T = any>(url: string, config?: HttpRequestConfig<UniApp.UploadTask>): HttpPromise<T>;
|
||||
delete<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
head<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
post<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
put<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
connect<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
options<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
trace<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
|
||||
download(url: string, config?: HttpRequestConfig<UniApp.DownloadTask>): Promise<HttpDownloadResponse>;
|
||||
|
||||
setConfig(onSend: (config: HttpRequestConfig) => HttpRequestConfig): void;
|
||||
}
|
||||
|
||||
declare class HttpRequest extends HttpRequestAbstract { }
|
||||
export default HttpRequest;
|
||||
2
lib/luch-request/index.js
Normal file
2
lib/luch-request/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Request from './core/Request'
|
||||
export default Request
|
||||
135
lib/luch-request/utils.js
Normal file
135
lib/luch-request/utils.js
Normal file
@@ -0,0 +1,135 @@
|
||||
'use strict'
|
||||
|
||||
// utils is a library of generic helper functions non-specific to axios
|
||||
|
||||
var toString = Object.prototype.toString
|
||||
|
||||
/**
|
||||
* Determine if a value is an Array
|
||||
*
|
||||
* @param {Object} val The value to test
|
||||
* @returns {boolean} True if value is an Array, otherwise false
|
||||
*/
|
||||
export function isArray (val) {
|
||||
return toString.call(val) === '[object Array]'
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if a value is an Object
|
||||
*
|
||||
* @param {Object} val The value to test
|
||||
* @returns {boolean} True if value is an Object, otherwise false
|
||||
*/
|
||||
export function isObject (val) {
|
||||
return val !== null && typeof val === 'object'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a value is a Date
|
||||
*
|
||||
* @param {Object} val The value to test
|
||||
* @returns {boolean} True if value is a Date, otherwise false
|
||||
*/
|
||||
export function isDate (val) {
|
||||
return toString.call(val) === '[object Date]'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a value is a URLSearchParams object
|
||||
*
|
||||
* @param {Object} val The value to test
|
||||
* @returns {boolean} True if value is a URLSearchParams object, otherwise false
|
||||
*/
|
||||
export function isURLSearchParams (val) {
|
||||
return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Iterate over an Array or an Object invoking a function for each item.
|
||||
*
|
||||
* If `obj` is an Array callback will be called passing
|
||||
* the value, index, and complete array for each item.
|
||||
*
|
||||
* If 'obj' is an Object callback will be called passing
|
||||
* the value, key, and complete object for each property.
|
||||
*
|
||||
* @param {Object|Array} obj The object to iterate
|
||||
* @param {Function} fn The callback to invoke for each item
|
||||
*/
|
||||
export function forEach (obj, fn) {
|
||||
// Don't bother if no value provided
|
||||
if (obj === null || typeof obj === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
// Force an array if not already something iterable
|
||||
if (typeof obj !== 'object') {
|
||||
/*eslint no-param-reassign:0*/
|
||||
obj = [obj]
|
||||
}
|
||||
|
||||
if (isArray(obj)) {
|
||||
// Iterate over array values
|
||||
for (var i = 0, l = obj.length; i < l; i++) {
|
||||
fn.call(null, obj[i], i, obj)
|
||||
}
|
||||
} else {
|
||||
// Iterate over object keys
|
||||
for (var key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
fn.call(null, obj[key], key, obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为boolean 值
|
||||
* @param val
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isBoolean(val) {
|
||||
return typeof val === 'boolean'
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为真正的对象{} new Object
|
||||
* @param {any} obj - 检测的对象
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPlainObject(obj) {
|
||||
return Object.prototype.toString.call(obj) === '[object Object]'
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Function equal to merge with the difference being that no reference
|
||||
* to original objects is kept.
|
||||
*
|
||||
* @see merge
|
||||
* @param {Object} obj1 Object to merge
|
||||
* @returns {Object} Result of all merge properties
|
||||
*/
|
||||
export function deepMerge(/* obj1, obj2, obj3, ... */) {
|
||||
let result = {}
|
||||
function assignValue(val, key) {
|
||||
if (typeof result[key] === 'object' && typeof val === 'object') {
|
||||
result[key] = deepMerge(result[key], val)
|
||||
} else if (typeof val === 'object') {
|
||||
result[key] = deepMerge({}, val)
|
||||
} else {
|
||||
result[key] = val
|
||||
}
|
||||
}
|
||||
for (let i = 0, l = arguments.length; i < l; i++) {
|
||||
forEach(arguments[i], assignValue)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function isUndefined (val) {
|
||||
return typeof val === 'undefined'
|
||||
}
|
||||
21
main.ts
Normal file
21
main.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createApp } from 'vue'
|
||||
// @ts-ignore
|
||||
import App from './App.vue'
|
||||
|
||||
// 创建应用实例
|
||||
const app = createApp(App)
|
||||
|
||||
// 全局混入
|
||||
// @ts-ignore
|
||||
import mixin from './mixins/global'
|
||||
app.use(mixin)
|
||||
|
||||
// 全局组件
|
||||
// @ts-ignore
|
||||
import uView from './uview-plus/index'
|
||||
app.use(uView)
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
|
||||
export default app
|
||||
25
manifest.json
Normal file
25
manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "template",
|
||||
"appid": "_UNI_8842336",
|
||||
"description": "",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"vueVersion": "3",
|
||||
"uniStatistics": {
|
||||
"enable": false
|
||||
},
|
||||
"mp-weixin": {
|
||||
"appid": "wx9cb717d8151d8486",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true
|
||||
},
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"usingComponents": true,
|
||||
"optimization": {
|
||||
"subPackages": true
|
||||
}
|
||||
}
|
||||
}
|
||||
18
mixins/global.ts
Normal file
18
mixins/global.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// @ts-ignore
|
||||
const ASSETSURL = import.meta.env.VITE_ASSETSURL
|
||||
|
||||
export default {
|
||||
install(app: any) {
|
||||
app.mixin({
|
||||
data() {
|
||||
return {
|
||||
// 资源地址
|
||||
ASSETSURL,
|
||||
}
|
||||
},
|
||||
onLoad() {},
|
||||
onShow() {},
|
||||
methods: {},
|
||||
})
|
||||
},
|
||||
}
|
||||
30146
package-lock.json
generated
Normal file
30146
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
package.json
Normal file
44
package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "template-mp-ts",
|
||||
"version": "1.0.0",
|
||||
"description": "A TypeScript template for UniApp Mini Program",
|
||||
"scripts": {
|
||||
"dev:mp-weixin": "npm run build:mp-weixin -- --watch",
|
||||
"build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build",
|
||||
"dev:mp-alipay": "npm run build:mp-alipay -- --watch",
|
||||
"build:mp-alipay": "cross-env NODE_ENV=production UNI_PLATFORM=mp-alipay vue-cli-service uni-build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "^3.0.0-3000020210720001",
|
||||
"@dcloudio/uni-app-plus": "^3.0.0-3000020210720001",
|
||||
"@dcloudio/uni-h5": "^3.0.0-3000020210720001",
|
||||
"@dcloudio/uni-mp-alipay": "^3.0.0-3000020210720001",
|
||||
"@dcloudio/uni-mp-weixin": "^3.0.0-3000020210720001",
|
||||
"vue": "^3.2.0",
|
||||
"vuex": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "^3.4.19",
|
||||
"@dcloudio/uni-automator": "^3.0.0-3000020210720001",
|
||||
"@dcloudio/uni-cli-shared": "^3.0.0-3000020210720001",
|
||||
"@dcloudio/uni-migration": "^3.0.0-3000020210720001",
|
||||
"@dcloudio/uni-template-compiler": "^3.0.0-3000020210720001",
|
||||
"@dcloudio/vue-cli-plugin-hbuilderx": "^3.0.0-3000020210720001",
|
||||
"@dcloudio/vue-cli-plugin-uni": "^3.0.0-3000020210720001",
|
||||
"@dcloudio/vue-cli-plugin-uni-optimize": "^3.0.0-3000020210720001",
|
||||
"@dcloudio/webpack-uni-mp-loader": "^3.0.0-3000020210720001",
|
||||
"@dcloudio/webpack-uni-pages-loader": "^3.0.0-3000020210720001",
|
||||
"@types/wechat-miniprogram": "^3.4.8",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-typescript": "^4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^17.2.2",
|
||||
"miniprogram-api-typings": "^3.4.1",
|
||||
"sass": "^1.43.4",
|
||||
"sass-loader": "^10.1.0",
|
||||
"typescript": "^4.4.0",
|
||||
"vue-tsc": "^0.3.0"
|
||||
}
|
||||
}
|
||||
22
pages.json
Normal file
22
pages.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "首页"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "uni-app",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^u-(.*)": "@/uview-plus/components/u-$1/u-$1.vue"
|
||||
}
|
||||
}
|
||||
}
|
||||
49
pages/index/index.vue
Normal file
49
pages/index/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<!-- pages/index/index.vue -->
|
||||
<template>
|
||||
<view class="content">
|
||||
<image class="logo" src="/static/logo.png" mode="aspectFit"></image>
|
||||
<view class="text-area">
|
||||
<text class="title">{{ title }}</text>
|
||||
</view>
|
||||
<u-button type="primary" @click="handleClick">点击按钮</u-button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const title = 'Hello'
|
||||
|
||||
function handleClick() {
|
||||
uni.showToast({
|
||||
title: '点击了按钮',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 200rpx;
|
||||
width: 200rpx;
|
||||
margin-top: 200rpx;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 50rpx;
|
||||
}
|
||||
|
||||
.text-area {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
color: #8f8f94;
|
||||
}
|
||||
</style>
|
||||
12
store/index.ts
Normal file
12
store/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createStore } from 'vuex'
|
||||
|
||||
const store = createStore({
|
||||
state() {
|
||||
return {}
|
||||
},
|
||||
mutations: {},
|
||||
actions: {},
|
||||
getters: {},
|
||||
})
|
||||
|
||||
export default store
|
||||
52
tsconfig.json
Normal file
52
tsconfig.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env",
|
||||
"@dcloudio/types",
|
||||
"miniprogram-api-typings",
|
||||
"@types/wechat-miniprogram"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"*"
|
||||
],
|
||||
"@/lib/*": [
|
||||
"lib/*"
|
||||
],
|
||||
"@/common/*": [
|
||||
"common/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"api/**/*.ts",
|
||||
"common/**/*.ts",
|
||||
"store/**/*.ts",
|
||||
"mixins/**/*.ts",
|
||||
"lib/**/*.js",
|
||||
"pages/**/*.ts",
|
||||
"pages/**/*.vue",
|
||||
"*.ts",
|
||||
"*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
13
uni.promisify.adaptor.js
Normal file
13
uni.promisify.adaptor.js
Normal file
@@ -0,0 +1,13 @@
|
||||
uni.addInterceptor({
|
||||
returnValue (res) {
|
||||
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
|
||||
return res;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
res.then((res) => {
|
||||
if (!res) return resolve(res)
|
||||
return res[0] ? reject(res[0]) : resolve(res[1])
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
76
uni.scss
Normal file
76
uni.scss
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
@import '@/uview-plus/theme.scss';
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||
41
uni_modules/z-paging/changelog.md
Normal file
41
uni_modules/z-paging/changelog.md
Normal file
@@ -0,0 +1,41 @@
|
||||
## 2.8.6(2025-03-17)
|
||||
1.`新增` 聊天记录模式流式输出(类似chatGPT回答)演示demo。
|
||||
2.`新增` z-paging及其公共子组件支持`HBuilderX`代码文档提示。
|
||||
3.`新增` props:`virtual-in-swiper-slot`,用以解决vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在`swiper-item`中存在的无法获取slot插入的cell高度进而导致虚拟列表失败的问题。
|
||||
4.`新增` `@scrolltolower`和@`scrolltoupper`支持nvue。
|
||||
5.`修复` 由`v2.8.1`引出的方法`scrollIntoViewById`在微信小程序+vue3中无效的问题。
|
||||
6.`修复` 由`v2.8.1`引出的在子组件内使用z-paging虚拟列表无效的问题。
|
||||
7.`修复` 在微信小程序中基础库版本较高时`wx.getSystemInfoSync is deprecated`警告。
|
||||
8.`优化` 提升下拉刷新在鸿蒙Next中的性能。
|
||||
9.`优化` `@scrolltolower`和`@scrolltoupper`在倒置的聊天记录模式下的触发逻辑。
|
||||
10.`优化` 其他细节调整。
|
||||
## 2.8.5(2025-02-09)
|
||||
1.`新增` 方法`scrollToX`,支持控制x轴滚动到指定位置。
|
||||
2.`修复` 快手小程序中报错`await isn't allowed in non-async function`的问题。
|
||||
3.`修复` 在iOS+nvue中,设置了`:loading-more-enabled="false"`后,调用`scrollToBottom`无法滚动到底部的问题。
|
||||
4.`修复` 在支付宝小程序+页面滚动中,数据为空时空数据图未居中的问题。
|
||||
5.`优化` fetch types修改。
|
||||
## 2.8.4(2024-12-02)
|
||||
1.`修复` 在虚拟列表+vue2中,顶部占位采用transformY方案;在虚拟列表+vue3中,顶部占位采用view占位方案。以解决在vue2+微信小程序+安卓+兼容模式中,可能出现的虚拟列表闪动的问题。
|
||||
2.`修复` 在列表渲染时(尤其是在虚拟列表中)偶现的【点击加载更多】闪现的问题。
|
||||
3.`优化` 统一在RefresherStatus枚举中Loading取值。
|
||||
4.`优化` `defaultPageNo`&`defaultPageSize`修改为只允许number类型。
|
||||
5.`优化` 提升兼容性&细节优化。
|
||||
## 2.8.3(2024-11-27)
|
||||
1.`修复` `doInsertVirtualListItem`插入数据无效的问题。
|
||||
2.`优化` 提升兼容性&细节优化。
|
||||
## 2.8.2(2024-11-25)
|
||||
1.`优化` types中`ZPagingRef`和`ZPagingInstance`支持泛型。
|
||||
## 2.8.1(2024-11-24)
|
||||
1.`新增` 完整的`props`、`slots`、`methods`、`events`的typescript types声明,可在ts中获得绝佳的代码提示体验。
|
||||
2.`新增` `virtual-cell-id-prefix`:虚拟列表cell id的前缀,适用于一个页面有多个虚拟列表的情况,用以区分不同虚拟列表cell的id。
|
||||
3.`修复` 在vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若`z-paging`在`swiper-item`标签内的情况下存在的无法获取slot插入的cell高度的问题。
|
||||
4.`修复` 在虚拟列表中分页数据小于1页时插入新数据,虚拟列表未生效的问题。
|
||||
5.`修复` 在虚拟列表中调用`refresh`时,cell的index计算不正确的问题。
|
||||
6.`修复` 在快手小程序中内容较少或空数据时`z-paging`未能铺满全屏的问题。
|
||||
7.`优化` `events`中的参数涉及枚举的部分,统一由之前的number类型修改为string类型,展示更直观!涉及的events:`@query`中的`from`参数;`@refresherStatusChange`中的`status`参数;`@loadingStatusChange`中的`status`参数;`slot=refresher`中的`refresherStatus`参数;`slot=chatLoading`中的`loadingMoreStatus`参数。更新版本请特别留意!
|
||||
## 2.8.0(2024-10-21)
|
||||
1.`新增` 全面支持鸿蒙Next。
|
||||
2.`修复` 设置了`refresher-complete-delay`后,在下拉刷新期间调用reload导致的无法再次下拉刷新的问题。
|
||||
3.`优化` 废弃虚拟列表transformY顶部占位方案,修改为空view占位。解决因使用旧方案导致的vue3中可能出现的虚拟列表闪动问题。提升虚拟列表的兼容性。
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<!-- z-paging -->
|
||||
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
|
||||
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
|
||||
<!-- 反馈QQ群:790460711 -->
|
||||
|
||||
<!-- z-paging-cell,用于在nvue中使用cell包裹,vue中使用view包裹 -->
|
||||
<template>
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<cell :style="[cellStyle]" @touchstart="onTouchstart">
|
||||
<slot />
|
||||
</cell>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<view :style="[cellStyle]" @touchstart="onTouchstart">
|
||||
<slot />
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* z-paging-cell 组件
|
||||
* @description 用于兼容 nvue 和 vue 中的 cell 渲染。因为在 nvue 中 z-paging 内置的是 list,因此列表 item 必须使用 cell 包住;在 vue 中不能使用 cell,否则会报组件找不到的错误。此子组件为了兼容这两种情况,内部作了条件编译处理。
|
||||
* @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-cell配置
|
||||
* @notice 以下为 z-paging-cell 的配置项
|
||||
* @property {Object} cellStyle cell 样式,默认为 {}
|
||||
* @example <z-paging-cell :cellStyle="{ backgroundColor: '#f0f0f0' }"></z-paging-cell>
|
||||
*/
|
||||
export default {
|
||||
name: "z-paging-cell",
|
||||
props: {
|
||||
//cellStyle
|
||||
cellStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTouchstart(e) {
|
||||
this.$emit('touchstart', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
<!-- z-paging -->
|
||||
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
|
||||
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
|
||||
<!-- 反馈QQ群:790460711 -->
|
||||
|
||||
<!-- 空数据占位view,此组件支持easycom规范,可以在项目中直接引用 -->
|
||||
<template>
|
||||
<view :class="{'zp-container':true,'zp-container-fixed':emptyViewFixed}" :style="[finalEmptyViewStyle]" @click="emptyViewClick">
|
||||
<view class="zp-main">
|
||||
<image v-if="!emptyViewImg.length" :class="{'zp-main-image-rpx':unit==='rpx','zp-main-image-px':unit==='px'}" :style="[emptyViewImgStyle]" :src="emptyImg" />
|
||||
<image v-else :class="{'zp-main-image-rpx':unit==='rpx','zp-main-image-px':unit==='px'}" mode="aspectFit" :style="[emptyViewImgStyle]" :src="emptyViewImg" />
|
||||
<text class="zp-main-title" :class="{'zp-main-title-rpx':unit==='rpx','zp-main-title-px':unit==='px'}" :style="[emptyViewTitleStyle]">{{emptyViewText}}</text>
|
||||
<text v-if="showEmptyViewReload" :class="{'zp-main-error-btn':true,'zp-main-error-btn-rpx':unit==='rpx','zp-main-error-btn-px':unit==='px'}" :style="[emptyViewReloadStyle]" @click.stop="reloadClick">{{emptyViewReloadText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import zStatic from '../z-paging/js/z-paging-static'
|
||||
|
||||
/**
|
||||
* z-paging-empty-view 空数据组件
|
||||
* @description 通用的 z-paging 空数据组件
|
||||
* @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-empty-view配置
|
||||
* @property {Boolean} emptyViewFixed 空数据图片是否铺满 z-paging,默认为 false。若设置为 true,则为填充满 z-paging 的剩余部分
|
||||
* @property {String} emptyViewText 空数据图描述文字,默认为 '没有数据哦~'
|
||||
* @property {String} emptyViewImg 空数据图图片,默认使用 z-paging 内置的图片 (建议使用绝对路径,开头不要添加 "@",请以 "/" 开头)
|
||||
* @property {String} emptyViewReloadText 空数据图点击重新加载文字,默认为 '重新加载'
|
||||
* @property {Object} emptyViewStyle 空数据图样式,可设置空数据 view 的 top 等,如: empty-view-style="{'top':'100rpx'}" (如果空数据图不是 fixed 布局,则此处是 margin-top),默认为 {}
|
||||
* @property {Object} emptyViewImgStyle 空数据图 img 样式,默认为 {}
|
||||
* @property {Object} emptyViewTitleStyle 空数据图描述文字样式,默认为 {}
|
||||
* @property {Object} emptyViewReloadStyle 空数据图重新加载按钮样式,默认为 {}
|
||||
* @property {Boolean} showEmptyViewReload 是否显示空数据图重新加载按钮(无数据时),默认为 false
|
||||
* @property {Boolean} isLoadFailed 是否是加载失败,默认为 false
|
||||
* @property {String} unit 空数据图中布局的单位,默认为 'rpx'
|
||||
* @event {Function} reload 点击了重新加载按钮
|
||||
* @event {Function} viewClick 点击了空数据图 view
|
||||
* @example <z-paging-empty-view empty-view-text="暂无数据" />
|
||||
*/
|
||||
export default {
|
||||
name: "z-paging-empty-view",
|
||||
data() {
|
||||
return {
|
||||
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 空数据描述文字
|
||||
emptyViewText: {
|
||||
type: String,
|
||||
default: '没有数据哦~'
|
||||
},
|
||||
// 空数据图片
|
||||
emptyViewImg: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示空数据图重新加载按钮
|
||||
showEmptyViewReload: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 空数据点击重新加载文字
|
||||
emptyViewReloadText: {
|
||||
type: String,
|
||||
default: '重新加载'
|
||||
},
|
||||
// 是否是加载失败
|
||||
isLoadFailed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 空数据图样式
|
||||
emptyViewStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 空数据图img样式
|
||||
emptyViewImgStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 空数据图描述文字样式
|
||||
emptyViewTitleStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 空数据图重新加载按钮样式
|
||||
emptyViewReloadStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 空数据图z-index
|
||||
emptyViewZIndex: {
|
||||
type: Number,
|
||||
default: 9
|
||||
},
|
||||
// 空数据图片是否使用fixed布局并铺满z-paging
|
||||
emptyViewFixed: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 空数据图中布局的单位,默认为rpx
|
||||
unit: {
|
||||
type: String,
|
||||
default: 'rpx'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
emptyImg() {
|
||||
return this.isLoadFailed ? zStatic.base64Error : zStatic.base64Empty;
|
||||
},
|
||||
finalEmptyViewStyle(){
|
||||
this.emptyViewStyle['z-index'] = this.emptyViewZIndex;
|
||||
return this.emptyViewStyle;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击了reload按钮
|
||||
reloadClick() {
|
||||
this.$emit('reload');
|
||||
},
|
||||
// 点击了空数据view
|
||||
emptyViewClick() {
|
||||
this.$emit('viewClick');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zp-container{
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.zp-container-fixed {
|
||||
/* #ifndef APP-NVUE */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
flex: 1;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-main{
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 50rpx 0rpx;
|
||||
}
|
||||
|
||||
.zp-main-image-rpx {
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
}
|
||||
.zp-main-image-px {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.zp-main-title {
|
||||
color: #aaaaaa;
|
||||
text-align: center;
|
||||
}
|
||||
.zp-main-title-rpx {
|
||||
font-size: 28rpx;
|
||||
margin-top: 10rpx;
|
||||
padding: 0rpx 20rpx;
|
||||
}
|
||||
.zp-main-title-px {
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
padding: 0px 10px;
|
||||
}
|
||||
|
||||
.zp-main-error-btn {
|
||||
border: solid 1px #dddddd;
|
||||
color: #aaaaaa;
|
||||
}
|
||||
.zp-main-error-btn-rpx {
|
||||
font-size: 28rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-top: 50rpx;
|
||||
}
|
||||
.zp-main-error-btn-px {
|
||||
font-size: 14px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 3px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<!-- z-paging -->
|
||||
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
|
||||
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
|
||||
<!-- 反馈QQ群:790460711 -->
|
||||
|
||||
<!-- 滑动切换选项卡swiper-item,此组件支持easycom规范,可以在项目中直接引用 -->
|
||||
<template>
|
||||
<view class="zp-swiper-item-container">
|
||||
<z-paging ref="paging" :fixed="false"
|
||||
:auto="false" :useVirtualList="useVirtualList" :useInnerList="useInnerList" :cellKeyName="cellKeyName" :innerListStyle="innerListStyle"
|
||||
:preloadPage="preloadPage" :cellHeightMode="cellHeightMode" :virtualScrollFps="virtualScrollFps" :virtualListCol="virtualListCol"
|
||||
@query="_queryList" @listChange="_updateList">
|
||||
<slot />
|
||||
<template #header>
|
||||
<slot name="header"/>
|
||||
</template>
|
||||
<template #cell="{item,index}">
|
||||
<slot name="cell" :item="item" :index="index"/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<slot name="footer"/>
|
||||
</template>
|
||||
</z-paging>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import zPaging from '../z-paging/z-paging'
|
||||
/**
|
||||
* z-paging-swiper-item 组件
|
||||
* @description swiper+list极简写法中使用到,实际上就是对普通的swiper+list中swiper-item的包装封装,用以简化写法,但个性化配置局限较多
|
||||
* @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-swiper-item配置
|
||||
* @notice 以下为 z-paging-swiper-item 的配置项
|
||||
* @property {Number} tabIndex 当前组件的 index,也就是当前组件是 swiper 中的第几个,默认为 0
|
||||
* @property {Number} currentIndex 当前 swiper 切换到第几个 index,默认为 0
|
||||
* @property {Boolean} useVirtualList 是否使用虚拟列表,默认为 false
|
||||
* @property {Boolean} useInnerList 是否在 z-paging 内部循环渲染列表(内置列表),默认为 false。若 useVirtualList 为 true,则此项恒为 true
|
||||
* @property {String} cellKeyName 内置列表 cell 的 key 名称,仅 nvue 有效,在 nvue 中开启 useInnerList 时必须填此项,默认为 ''
|
||||
* @property {Object} innerListStyle innerList 样式,默认为 {}
|
||||
* @property {Number|String} preloadPage 预加载的列表可视范围(列表高度)页数,默认为 12。此数值越大,则虚拟列表中加载的 dom 越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
|
||||
* @property {String} cellHeightMode 虚拟列表 cell 高度模式,默认为 'fixed',也就是每个 cell 高度完全相同,将以第一个 cell 高度为准进行计算。可选值【dynamic】,即代表高度是动态非固定的,【dynamic】性能低于【fixed】
|
||||
* @property {Number|String} virtualListCol 虚拟列表列数,默认为 1。常用于每行有多列的情况,例如每行有 2 列数据,需要将此值设置为 2
|
||||
* @property {Number|String} virtualScrollFps 虚拟列表 scroll 取样帧率,默认为 60,过高可能出现卡顿等问题
|
||||
* @example <z-paging-swiper-item ref="swiperItem" :tabIndex="index" :currentIndex="current" @query="queryList" @updateList="updateList"></z-paging-swiper-item>
|
||||
*/
|
||||
export default {
|
||||
name: "z-paging-swiper-item",
|
||||
components: { zPaging },
|
||||
data() {
|
||||
return {
|
||||
firstLoaded: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
// 当前组件的index,也就是当前组件是swiper中的第几个
|
||||
tabIndex: {
|
||||
type: Number,
|
||||
default: function() {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
// 当前swiper切换到第几个index
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
default: function() {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
// 是否使用虚拟列表,默认为否
|
||||
useVirtualList: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否在z-paging内部循环渲染列表(内置列表),默认为否。若use-virtual-list为true,则此项恒为true
|
||||
useInnerList: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 内置列表cell的key名称,仅nvue有效,在nvue中开启use-inner-list时必须填此项
|
||||
cellKeyName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// innerList样式
|
||||
innerListStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 预加载的列表可视范围(列表高度)页数,默认为12,即预加载当前页及上下各12页的cell。此数值越大,则虚拟列表中加载的dom越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
|
||||
preloadPage: {
|
||||
type: [Number, String],
|
||||
default: 12
|
||||
},
|
||||
// 虚拟列表cell高度模式,默认为fixed,也就是每个cell高度完全相同,将以第一个cell高度为准进行计算。可选值【dynamic】,即代表高度是动态非固定的,【dynamic】性能低于【fixed】。
|
||||
cellHeightMode: {
|
||||
type: String,
|
||||
default: 'fixed'
|
||||
},
|
||||
// 虚拟列表列数,默认为1。常用于每行有多列的情况,例如每行有2列数据,需要将此值设置为2
|
||||
virtualListCol: {
|
||||
type: [Number, String],
|
||||
default: 1
|
||||
},
|
||||
// 虚拟列表scroll取样帧率,默认为60,过高可能出现卡顿等问题
|
||||
virtualScrollFps: {
|
||||
type: [Number, String],
|
||||
default: 60
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentIndex: {
|
||||
handler(newVal, oldVal) {
|
||||
if (newVal === this.tabIndex) {
|
||||
// 懒加载,当滑动到当前的item时,才去加载
|
||||
if (!this.firstLoaded) {
|
||||
this.$nextTick(()=>{
|
||||
let delay = 5;
|
||||
// #ifdef MP-TOUTIAO
|
||||
delay = 100;
|
||||
// #endif
|
||||
setTimeout(() => {
|
||||
this.$refs.paging.reload().catch(() => {});
|
||||
}, delay);
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reload(data) {
|
||||
return this.$refs.paging.reload(data);
|
||||
},
|
||||
complete(data) {
|
||||
this.firstLoaded = true;
|
||||
return this.$refs.paging.complete(data);
|
||||
},
|
||||
_queryList(pageNo, pageSize, from) {
|
||||
this.$emit('query', pageNo, pageSize, from);
|
||||
},
|
||||
_updateList(list) {
|
||||
this.$emit('updateList', list);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zp-swiper-item-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
flex: 1;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<!-- z-paging -->
|
||||
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
|
||||
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
|
||||
<!-- 反馈QQ群:790460711 -->
|
||||
|
||||
<!-- 滑动切换选项卡swiper容器,此组件支持easycom规范,可以在项目中直接引用 -->
|
||||
<template>
|
||||
<view :class="fixed?'zp-swiper-container zp-swiper-container-fixed':'zp-swiper-container'" :style="[finalSwiperStyle]">
|
||||
<!-- #ifndef APP-PLUS -->
|
||||
<view v-if="cssSafeAreaInsetBottom===-1" class="zp-safe-area-inset-bottom"></view>
|
||||
<!-- #endif -->
|
||||
<slot v-if="zSlots.top" name="top" />
|
||||
<view class="zp-swiper-super">
|
||||
<view v-if="zSlots.left" :class="{'zp-swiper-left':true,'zp-absoulte':isOldWebView}">
|
||||
<slot name="left" />
|
||||
</view>
|
||||
<view :class="{'zp-swiper':true,'zp-absoulte':isOldWebView}" :style="[swiperContentStyle]">
|
||||
<slot />
|
||||
</view>
|
||||
<view v-if="zSlots.right" :class="{'zp-swiper-right':true,'zp-absoulte zp-right':isOldWebView}">
|
||||
<slot name="right" />
|
||||
</view>
|
||||
</view>
|
||||
<slot v-if="zSlots.bottom" name="bottom" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import commonLayoutModule from '../z-paging/js/modules/common-layout'
|
||||
|
||||
/**
|
||||
* z-paging-swiper 组件
|
||||
* @description 在 swiper 中使用 z-paging 时(左右滑动切换列表),在根节点使用 z-paging-swiper,其相当于一个 view 容器,默认铺满全屏,可免计算高度直接插入 swiper 的视图容器。
|
||||
* @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-swiper配置
|
||||
* @property {Boolean} fixed 是否使用 fixed 布局,默认为 true
|
||||
* @property {Boolean} safeAreaInsetBottom 是否开启底部安全区域适配,默认为 false
|
||||
* @property {Object} swiperStyle z-paging-swiper 样式,默认为 {}
|
||||
* @example <z-paging-swiper :safeAreaInsetBottom="true"></z-paging-swiper>
|
||||
*/
|
||||
export default {
|
||||
name: "z-paging-swiper",
|
||||
mixins: [commonLayoutModule],
|
||||
data() {
|
||||
return {
|
||||
swiperContentStyle: {}
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 是否使用fixed布局,默认为是
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否开启底部安全区域适配
|
||||
safeAreaInsetBottom: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// z-paging-swiper样式
|
||||
swiperStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {};
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.systemInfo = this._getSystemInfoSync();
|
||||
setTimeout(this.updateFixedLayout, 100)
|
||||
})
|
||||
// #ifndef APP-PLUS
|
||||
this._getCssSafeAreaInsetBottom();
|
||||
// #endif
|
||||
this.updateLeftAndRightWidth();
|
||||
|
||||
this.swiperContentStyle = { 'flex': '1' };
|
||||
// #ifndef APP-NVUE
|
||||
this.swiperContentStyle = { width: '100%',height: '100%' };
|
||||
// #endif
|
||||
},
|
||||
computed: {
|
||||
finalSwiperStyle() {
|
||||
const swiperStyle = { ...this.swiperStyle };
|
||||
if (!this.systemInfo) return swiperStyle;
|
||||
const windowTop = this.windowTop;
|
||||
const windowBottom = this.systemInfo.windowBottom;
|
||||
if (this.fixed) {
|
||||
if (windowTop && !swiperStyle.top) {
|
||||
swiperStyle.top = windowTop + 'px';
|
||||
}
|
||||
if (!swiperStyle.bottom) {
|
||||
let bottom = windowBottom || 0;
|
||||
bottom += this.safeAreaInsetBottom ? this.safeAreaBottom : 0;
|
||||
if (bottom > 0) {
|
||||
swiperStyle.bottom = bottom + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
return swiperStyle;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 更新slot="left"和slot="right"宽度,当slot="left"或slot="right"宽度动态改变时调用
|
||||
updateLeftAndRightWidth() {
|
||||
if (!this.isOldWebView) return;
|
||||
this.$nextTick(() => this._updateLeftAndRightWidth(this.swiperContentStyle, 'zp-swiper'));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zp-swiper-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.zp-swiper-container-fixed {
|
||||
position: fixed;
|
||||
/* #ifndef APP-NVUE */
|
||||
height: auto;
|
||||
width: auto;
|
||||
/* #endif */
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.zp-safe-area-inset-bottom {
|
||||
position: absolute;
|
||||
/* #ifndef APP-PLUS */
|
||||
height: env(safe-area-inset-bottom);
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-swiper-super {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.zp-swiper-left,.zp-swiper-right{
|
||||
/* #ifndef APP-NVUE */
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-swiper {
|
||||
flex: 1;
|
||||
/* #ifndef APP-NVUE */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-absoulte {
|
||||
/* #ifndef APP-NVUE */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: auto;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-swiper-item {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,182 @@
|
||||
<!-- [z-paging]上拉加载更多view -->
|
||||
<template>
|
||||
<view class="zp-l-container" :class="{'zp-l-container-rpx':c.unit==='rpx','zp-l-container-px':c.unit==='px'}" :style="[c.customStyle]" @click="doClick">
|
||||
<template v-if="!c.hideContent">
|
||||
<!-- 底部加载更多没有更多数据分割线 -->
|
||||
<text v-if="c.showNoMoreLine&&finalStatus===M.NoMore" :class="{'zp-l-line-rpx':c.unit==='rpx','zp-l-line-px':c.unit==='px'}" :style="[{backgroundColor:zTheme.line[ts]},c.noMoreLineCustomStyle]" />
|
||||
<!-- 底部加载更多loading -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<image v-if="finalStatus===M.Loading&&!!c.loadingIconCustomImage"
|
||||
:src="c.loadingIconCustomImage" :style="[c.iconCustomStyle]" :class="{'zp-l-line-loading-custom-image':true,'zp-l-line-loading-custom-image-animated':c.loadingAnimated,'zp-l-line-loading-custom-image-rpx':c.unit==='rpx','zp-l-line-loading-custom-image-px':c.unit==='px'}" />
|
||||
<image v-if="finalStatus===M.Loading&&finalLoadingIconType==='flower'&&!c.loadingIconCustomImage.length"
|
||||
:class="{'zp-line-loading-image':true,'zp-line-loading-image-rpx':c.unit==='rpx','zp-line-loading-image-px':c.unit==='px'}" :style="[c.iconCustomStyle]" :src="zTheme.flower[ts]" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<!-- 在nvue中底部加载更多loading使用系统自带的 -->
|
||||
<view>
|
||||
<loading-indicator v-if="finalStatus===M.Loading&&finalLoadingIconType!=='circle'" :class="{'zp-line-loading-image-rpx':c.unit==='rpx','zp-line-loading-image-px':c.unit==='px'}" :style="[{color:zTheme.indicator[ts]}]" :animating="true" />
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- 底部加载更多文字 -->
|
||||
<text v-if="finalStatus===M.Loading&&finalLoadingIconType==='circle'&&!c.loadingIconCustomImage.length"
|
||||
class="zp-l-circle-loading-view" :class="{'zp-l-circle-loading-view-rpx':c.unit==='rpx','zp-l-circle-loading-view-px':c.unit==='px'}" :style="[{borderColor:zTheme.circleBorder[ts],borderTopColor:zTheme.circleBorderTop[ts]},c.iconCustomStyle]" />
|
||||
<text v-if="!c.isChat||(!c.chatDefaultAsLoading&&finalStatus===M.Default)||finalStatus===M.Fail" :class="{'zp-l-text-rpx':c.unit==='rpx','zp-l-text-px':c.unit==='px'}" :style="[{color:zTheme.title[ts]},c.titleCustomStyle]">{{ownLoadingMoreText}}</text>
|
||||
<!-- 底部加载更多没有更多数据分割线 -->
|
||||
<text v-if="c.showNoMoreLine&&finalStatus===M.NoMore" :class="{'zp-l-line-rpx':c.unit==='rpx','zp-l-line-px':c.unit==='px'}" :style="[{backgroundColor:zTheme.line[ts]},c.noMoreLineCustomStyle]" />
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import zStatic from '../js/z-paging-static'
|
||||
import Enum from '../js/z-paging-enum'
|
||||
export default {
|
||||
name: 'z-paging-load-more',
|
||||
data() {
|
||||
return {
|
||||
M: Enum.More,
|
||||
zTheme: {
|
||||
title: { white: '#efefef', black: '#a4a4a4' },
|
||||
line: { white: '#efefef', black: '#eeeeee' },
|
||||
circleBorder: { white: '#aaaaaa', black: '#c8c8c8' },
|
||||
circleBorderTop: { white: '#ffffff', black: '#444444' },
|
||||
flower: { white: zStatic.base64FlowerWhite, black: zStatic.base64Flower },
|
||||
indicator: { white: '#eeeeee', black: '#777777' }
|
||||
}
|
||||
};
|
||||
},
|
||||
props: ['zConfig'],
|
||||
computed: {
|
||||
ts() {
|
||||
return this.c.defaultThemeStyle;
|
||||
},
|
||||
// 底部加载更多配置
|
||||
c() {
|
||||
return this.zConfig || {};
|
||||
},
|
||||
// 底部加载更多文字
|
||||
ownLoadingMoreText() {
|
||||
return {
|
||||
[this.M.Default]: this.c.defaultText,
|
||||
[this.M.Loading]: this.c.loadingText,
|
||||
[this.M.NoMore]: this.c.noMoreText,
|
||||
[this.M.Fail]: this.c.failText,
|
||||
}[this.finalStatus];
|
||||
},
|
||||
// 底部加载更多状态
|
||||
finalStatus() {
|
||||
if (this.c.defaultAsLoading && this.c.status === this.M.Default) return this.M.Loading;
|
||||
return this.c.status;
|
||||
},
|
||||
// 加载更多icon类型
|
||||
finalLoadingIconType() {
|
||||
// #ifdef APP-NVUE
|
||||
return 'flower';
|
||||
// #endif
|
||||
return this.c.loadingIconType;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击了加载更多
|
||||
doClick() {
|
||||
this.$emit('doClick');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import "../css/z-paging-static.css";
|
||||
|
||||
.zp-l-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
clear: both;
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.zp-l-container-rpx {
|
||||
height: 80rpx;
|
||||
font-size: 27rpx;
|
||||
}
|
||||
.zp-l-container-px {
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.zp-l-line-loading-custom-image {
|
||||
color: #a4a4a4;
|
||||
}
|
||||
.zp-l-line-loading-custom-image-rpx {
|
||||
margin-right: 8rpx;
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
}
|
||||
.zp-l-line-loading-custom-image-px {
|
||||
margin-right: 4px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.zp-l-line-loading-custom-image-animated{
|
||||
/* #ifndef APP-NVUE */
|
||||
animation: loading-circle 1s linear infinite;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-l-circle-loading-view {
|
||||
border: 3rpx solid #dddddd;
|
||||
border-radius: 50%;
|
||||
/* #ifndef APP-NVUE */
|
||||
animation: loading-circle 1s linear infinite;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
/* #endif */
|
||||
}
|
||||
.zp-l-circle-loading-view-rpx {
|
||||
margin-right: 8rpx;
|
||||
width: 23rpx;
|
||||
height: 23rpx;
|
||||
}
|
||||
.zp-l-circle-loading-view-px {
|
||||
margin-right: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.zp-l-text-rpx {
|
||||
font-size: 30rpx;
|
||||
margin: 0rpx 6rpx;
|
||||
}
|
||||
.zp-l-text-px {
|
||||
font-size: 15px;
|
||||
margin: 0px 3px;
|
||||
}
|
||||
|
||||
.zp-l-line-rpx {
|
||||
height: 1px;
|
||||
width: 100rpx;
|
||||
margin: 0rpx 10rpx;
|
||||
}
|
||||
.zp-l-line-px {
|
||||
height: 1px;
|
||||
width: 50px;
|
||||
margin: 0rpx 5px;
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
@keyframes loading-circle {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
</style>
|
||||
@@ -0,0 +1,214 @@
|
||||
<!-- [z-paging]下拉刷新view -->
|
||||
<template>
|
||||
<view style="height: 100%;">
|
||||
<view :class="showUpdateTime?'zp-r-container zp-r-container-padding':'zp-r-container'">
|
||||
<view class="zp-r-left">
|
||||
<!-- 非加载中(继续下拉刷新、松手立即刷新状态图片) -->
|
||||
<image v-if="status!==R.Loading" :class="leftImageClass" :style="[leftImageStyle,imgStyle]" :src="leftImageSrc" />
|
||||
<!-- 加载状态图片 -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<image v-else :class="{'zp-line-loading-image':refreshingAnimated,'zp-r-left-image':true,'zp-r-left-image-pre-size-rpx':unit==='rpx','zp-r-left-image-pre-size-px':unit==='px'}" :style="[leftImageStyle,imgStyle]" :src="leftImageSrc" />
|
||||
<!-- #endif -->
|
||||
<!-- 在nvue中,加载状态loading使用系统loading -->
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<view v-else :style="[{'margin-right':showUpdateTime?addUnit(18,unit):addUnit(12, unit)}]">
|
||||
<loading-indicator :class="isIos?{'zp-loading-image-ios-rpx':unit==='rpx','zp-loading-image-ios-px':unit==='px'}:{'zp-loading-image-android-rpx':unit==='rpx','zp-loading-image-android-px':unit==='px'}"
|
||||
:style="[{color:zTheme.indicator[ts]},imgStyle]" :animating="true" />
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<!-- 右侧文字内容 -->
|
||||
<view class="zp-r-right">
|
||||
<!-- 右侧下拉刷新状态文字 -->
|
||||
<text class="zp-r-right-text" :style="[rightTextStyle,titleStyle]">{{currentTitle}}</text>
|
||||
<!-- 右侧下拉刷新时间文字 -->
|
||||
<text v-if="showUpdateTime&&refresherTimeText.length" class="zp-r-right-text" :class="{'zp-r-right-time-text-rpx':unit==='rpx','zp-r-right-time-text-px':unit==='px'}" :style="[{color:zTheme.title[ts]},updateTimeStyle]">
|
||||
{{refresherTimeText}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import zStatic from '../js/z-paging-static'
|
||||
import u from '../js/z-paging-utils'
|
||||
import Enum from '../js/z-paging-enum'
|
||||
|
||||
export default {
|
||||
name: 'z-paging-refresh',
|
||||
data() {
|
||||
return {
|
||||
R: Enum.Refresher,
|
||||
refresherTimeText: '',
|
||||
zTheme: {
|
||||
title: { white: '#efefef', black: '#555555' },
|
||||
arrow: { white: zStatic.base64ArrowWhite, black: zStatic.base64Arrow },
|
||||
flower: { white: zStatic.base64FlowerWhite, black: zStatic.base64Flower },
|
||||
success: { white: zStatic.base64SuccessWhite, black: zStatic.base64Success },
|
||||
indicator: { white: '#eeeeee', black: '#777777' }
|
||||
}
|
||||
};
|
||||
},
|
||||
props: ['status', 'defaultThemeStyle', 'defaultText', 'pullingText', 'refreshingText', 'completeText', 'goF2Text', 'defaultImg', 'pullingImg',
|
||||
'refreshingImg', 'completeImg', 'refreshingAnimated', 'showUpdateTime', 'updateTimeKey', 'imgStyle', 'titleStyle', 'updateTimeStyle', 'updateTimeTextMap', 'unit', 'isIos'
|
||||
],
|
||||
computed: {
|
||||
ts() {
|
||||
return this.defaultThemeStyle;
|
||||
},
|
||||
// 当前状态Map
|
||||
statusTextMap() {
|
||||
this.updateTime();
|
||||
const { R, defaultText, pullingText, refreshingText, completeText, goF2Text } = this;
|
||||
return {
|
||||
[R.Default]: defaultText,
|
||||
[R.ReleaseToRefresh]: pullingText,
|
||||
[R.Loading]: refreshingText,
|
||||
[R.Complete]: completeText,
|
||||
[R.GoF2]: goF2Text,
|
||||
};
|
||||
},
|
||||
// 当前状态文字
|
||||
currentTitle() {
|
||||
return this.statusTextMap[this.status] || this.defaultText;
|
||||
},
|
||||
// 左侧图片class
|
||||
leftImageClass() {
|
||||
const preSizeClass = `zp-r-left-image-pre-size-${this.unit}`;
|
||||
if (this.status === this.R.Complete) return preSizeClass;
|
||||
return `zp-r-left-image ${preSizeClass} ${this.status === this.R.Default ? 'zp-r-arrow-down' : 'zp-r-arrow-top'}`;
|
||||
},
|
||||
// 左侧图片style
|
||||
leftImageStyle() {
|
||||
const showUpdateTime = this.showUpdateTime;
|
||||
const size = showUpdateTime ? u.addUnit(36, this.unit) : u.addUnit(34, this.unit);
|
||||
return {width: size,height: size,'margin-right': showUpdateTime ? u.addUnit(20, this.unit) : u.addUnit(9, this.unit)};
|
||||
},
|
||||
// 左侧图片src
|
||||
leftImageSrc() {
|
||||
const R = this.R;
|
||||
const status = this.status;
|
||||
if (status === R.Default) {
|
||||
if (!!this.defaultImg) return this.defaultImg;
|
||||
return this.zTheme.arrow[this.ts];
|
||||
} else if (status === R.ReleaseToRefresh) {
|
||||
if (!!this.pullingImg) return this.pullingImg;
|
||||
if (!!this.defaultImg) return this.defaultImg;
|
||||
return this.zTheme.arrow[this.ts];
|
||||
} else if (status === R.Loading) {
|
||||
if (!!this.refreshingImg) return this.refreshingImg;
|
||||
return this.zTheme.flower[this.ts];;
|
||||
} else if (status === R.Complete) {
|
||||
if (!!this.completeImg) return this.completeImg;
|
||||
return this.zTheme.success[this.ts];;
|
||||
} else if (status === R.GoF2) {
|
||||
return this.zTheme.arrow[this.ts];
|
||||
}
|
||||
return '';
|
||||
},
|
||||
// 右侧文字style
|
||||
rightTextStyle() {
|
||||
let stl = {};
|
||||
// #ifdef APP-NVUE
|
||||
const textHeight = this.showUpdateTime ? u.addUnit(40, this.unit) : u.addUnit(80, this.unit);
|
||||
stl = {'height': textHeight, 'line-height': textHeight}
|
||||
// #endif
|
||||
stl['color'] = this.zTheme.title[this.ts];
|
||||
stl['font-size'] = u.addUnit(30, this.unit);
|
||||
return stl;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 添加单位
|
||||
addUnit(value, unit) {
|
||||
return u.addUnit(value, unit);
|
||||
},
|
||||
// 更新下拉刷新时间
|
||||
updateTime() {
|
||||
if (this.showUpdateTime) {
|
||||
this.refresherTimeText = u.getRefesrherFormatTimeByKey(this.updateTimeKey, this.updateTimeTextMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import "../css/z-paging-static.css";
|
||||
|
||||
.zp-r-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.zp-r-container-padding {
|
||||
/* #ifdef APP-NVUE */
|
||||
padding: 7px 0rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-r-left {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
/* #ifdef MP-ALIPAY */
|
||||
margin-top: -4rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-r-left-image {
|
||||
transition-duration: .2s;
|
||||
transition-property: transform;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.zp-r-left-image-pre-size-rpx {
|
||||
/* #ifndef APP-NVUE */
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
overflow: hidden;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-r-left-image-pre-size-px {
|
||||
/* #ifndef APP-NVUE */
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
overflow: hidden;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-r-arrow-top {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.zp-r-arrow-down {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.zp-r-right {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.zp-r-right-time-text-rpx {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.zp-r-right-time-text-px {
|
||||
margin-top: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
3
uni_modules/z-paging/components/z-paging/config/index.js
Normal file
3
uni_modules/z-paging/components/z-paging/config/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// z-paging全局配置文件,注意避免更新时此文件被覆盖,若被覆盖,可在此文件中右键->点击本地历史记录,找回覆盖前的配置
|
||||
|
||||
export default {}
|
||||
241
uni_modules/z-paging/components/z-paging/css/z-paging-main.css
Normal file
241
uni_modules/z-paging/components/z-paging/css/z-paging-main.css
Normal file
@@ -0,0 +1,241 @@
|
||||
/* [z-paging]公共css*/
|
||||
|
||||
.z-paging-content {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
/* #ifndef APP-NVUE */
|
||||
overflow: hidden;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.z-paging-content-full {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.z-paging-content-fixed, .zp-loading-fixed {
|
||||
position: fixed;
|
||||
/* #ifndef APP-NVUE */
|
||||
height: auto;
|
||||
width: auto;
|
||||
/* #endif */
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.zp-f2-content {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.zp-page-top, .zp-page-bottom {
|
||||
/* #ifndef APP-NVUE */
|
||||
width: auto;
|
||||
/* #endif */
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.zp-page-left, .zp-page-right {
|
||||
/* #ifndef APP-NVUE */
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-scroll-view-super {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.zp-view-super {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.zp-scroll-view-container, .zp-scroll-view {
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-absoulte {
|
||||
/* #ifndef APP-NVUE */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: auto;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-scroll-view-absolute {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
.zp-scroll-view-hide-scrollbar ::-webkit-scrollbar {
|
||||
display: none;
|
||||
-webkit-appearance: none;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
background: transparent;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
.zp-paging-touch-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.zp-fixed-bac-view {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.zp-paging-main {
|
||||
height: 100%;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.zp-paging-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.zp-chat-record-loading-custom-image {
|
||||
width: 35rpx;
|
||||
height: 35rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
animation: loading-flower 1s linear infinite;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-page-bottom-keyboard-placeholder-animate {
|
||||
transition-property: height;
|
||||
transition-duration: 0.15s;
|
||||
/* #ifndef APP-NVUE */
|
||||
will-change: height;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-custom-refresher-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.zp-custom-refresher-refresh {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: block;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-back-to-top {
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
bottom: 0rpx;
|
||||
transition-duration: .3s;
|
||||
transition-property: opacity;
|
||||
}
|
||||
.zp-back-to-top-rpx {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
bottom: 0rpx;
|
||||
right: 25rpx;
|
||||
}
|
||||
.zp-back-to-top-px {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
bottom: 0px;
|
||||
right: 13px;
|
||||
}
|
||||
|
||||
.zp-back-to-top-show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.zp-back-to-top-hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.zp-back-to-top-img {
|
||||
/* #ifndef APP-NVUE */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
flex: 1;
|
||||
/* #endif */
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.zp-back-to-top-img-inversion {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.zp-empty-view {
|
||||
/* #ifdef APP-NVUE */
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.zp-empty-view-center {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.zp-loading-fixed {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.zp-safe-area-inset-bottom {
|
||||
position: absolute;
|
||||
/* #ifndef APP-PLUS */
|
||||
height: env(safe-area-inset-bottom);
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-n-refresh-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
justify-content: center;
|
||||
width: 750rpx;
|
||||
}
|
||||
|
||||
.zp-n-list-container{
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/* [z-paging]公用的静态css资源 */
|
||||
|
||||
.zp-line-loading-image {
|
||||
/* #ifndef APP-NVUE */
|
||||
animation: loading-flower 1s steps(12) infinite;
|
||||
/* #endif */
|
||||
color: #666666;
|
||||
}
|
||||
.zp-line-loading-image-rpx {
|
||||
margin-right: 8rpx;
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
}
|
||||
.zp-line-loading-image-px {
|
||||
margin-right: 4px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
.zp-loading-image-ios-rpx {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
.zp-loading-image-ios-px {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.zp-loading-image-android-rpx {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
}
|
||||
.zp-loading-image-android-px {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
@keyframes loading-flower {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(1turn);
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
23
uni_modules/z-paging/components/z-paging/i18n/en.json
Normal file
23
uni_modules/z-paging/components/z-paging/i18n/en.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"zp.refresher.default": "Pull down to refresh",
|
||||
"zp.refresher.pulling": "Release to refresh",
|
||||
"zp.refresher.refreshing": "Refreshing...",
|
||||
"zp.refresher.complete": "Refresh succeeded",
|
||||
"zp.refresher.f2": "Refresh to enter 2f",
|
||||
|
||||
"zp.loadingMore.default": "Click to load more",
|
||||
"zp.loadingMore.loading": "Loading...",
|
||||
"zp.loadingMore.noMore": "No more data",
|
||||
"zp.loadingMore.fail": "Load failed,click to reload",
|
||||
|
||||
"zp.emptyView.title": "No data",
|
||||
"zp.emptyView.reload": "Reload",
|
||||
"zp.emptyView.error": "Sorry,load failed",
|
||||
|
||||
"zp.refresherUpdateTime.title": "Last update: ",
|
||||
"zp.refresherUpdateTime.none": "None",
|
||||
"zp.refresherUpdateTime.today": "Today",
|
||||
"zp.refresherUpdateTime.yesterday": "Yesterday",
|
||||
|
||||
"zp.systemLoading.title": "Loading..."
|
||||
}
|
||||
8
uni_modules/z-paging/components/z-paging/i18n/index.js
Normal file
8
uni_modules/z-paging/components/z-paging/i18n/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import en from './en.json'
|
||||
import zhHans from './zh-Hans.json'
|
||||
import zhHant from './zh-Hant.json'
|
||||
export default {
|
||||
en,
|
||||
'zh-Hans': zhHans,
|
||||
'zh-Hant': zhHant
|
||||
}
|
||||
23
uni_modules/z-paging/components/z-paging/i18n/zh-Hans.json
Normal file
23
uni_modules/z-paging/components/z-paging/i18n/zh-Hans.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"zp.refresher.default": "继续下拉刷新",
|
||||
"zp.refresher.pulling": "松开立即刷新",
|
||||
"zp.refresher.refreshing": "正在刷新...",
|
||||
"zp.refresher.complete": "刷新成功",
|
||||
"zp.refresher.f2": "松手进入二楼",
|
||||
|
||||
"zp.loadingMore.default": "点击加载更多",
|
||||
"zp.loadingMore.loading": "正在加载...",
|
||||
"zp.loadingMore.noMore": "没有更多了",
|
||||
"zp.loadingMore.fail": "加载失败,点击重新加载",
|
||||
|
||||
"zp.emptyView.title": "没有数据哦~",
|
||||
"zp.emptyView.reload": "重新加载",
|
||||
"zp.emptyView.error": "很抱歉,加载失败",
|
||||
|
||||
"zp.refresherUpdateTime.title": "最后更新:",
|
||||
"zp.refresherUpdateTime.none": "无",
|
||||
"zp.refresherUpdateTime.today": "今天",
|
||||
"zp.refresherUpdateTime.yesterday": "昨天",
|
||||
|
||||
"zp.systemLoading.title": "加载中..."
|
||||
}
|
||||
23
uni_modules/z-paging/components/z-paging/i18n/zh-Hant.json
Normal file
23
uni_modules/z-paging/components/z-paging/i18n/zh-Hant.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"zp.refresher.default": "繼續下拉重繪",
|
||||
"zp.refresher.pulling": "鬆開立即重繪",
|
||||
"zp.refresher.refreshing": "正在重繪...",
|
||||
"zp.refresher.complete": "重繪成功",
|
||||
"zp.refresher.f2": "鬆手進入二樓",
|
||||
|
||||
"zp.loadingMore.default": "點擊加載更多",
|
||||
"zp.loadingMore.loading": "正在加載...",
|
||||
"zp.loadingMore.noMore": "沒有更多了",
|
||||
"zp.loadingMore.fail": "加載失敗,點擊重新加載",
|
||||
|
||||
"zp.emptyView.title": "沒有數據哦~",
|
||||
"zp.emptyView.reload": "重新加載",
|
||||
"zp.emptyView.error": "很抱歉,加載失敗",
|
||||
|
||||
"zp.refresherUpdateTime.title": "最後更新:",
|
||||
"zp.refresherUpdateTime.none": "無",
|
||||
"zp.refresherUpdateTime.today": "今天",
|
||||
"zp.refresherUpdateTime.yesterday": "昨天",
|
||||
|
||||
"zp.systemLoading.title": "加載中..."
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// [z-paging]useZPaging hooks
|
||||
|
||||
import { onPageScroll, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app';
|
||||
|
||||
function useZPaging(paging) {
|
||||
const cPaging = !!paging ? paging.value || paging : null;
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.reload().catch(() => {});
|
||||
})
|
||||
|
||||
onPageScroll(e => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.updatePageScrollTop(e.scrollTop);
|
||||
e.scrollTop < 10 && cPaging.value.doChatRecordLoadMore();
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.pageReachBottom();
|
||||
})
|
||||
}
|
||||
|
||||
export default useZPaging
|
||||
@@ -0,0 +1,25 @@
|
||||
// [z-paging]useZPagingComp hooks
|
||||
|
||||
function useZPagingComp(paging) {
|
||||
const cPaging = !!paging ? paging.value || paging : null;
|
||||
|
||||
const reload = () => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.reload().catch(() => {});
|
||||
}
|
||||
const updatePageScrollTop = scrollTop => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.updatePageScrollTop(scrollTop);
|
||||
}
|
||||
const doChatRecordLoadMore = () => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.doChatRecordLoadMore();
|
||||
}
|
||||
const pageReachBottom = () => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.pageReachBottom();
|
||||
}
|
||||
return { reload, updatePageScrollTop, doChatRecordLoadMore, pageReachBottom };
|
||||
}
|
||||
|
||||
export default useZPagingComp
|
||||
@@ -0,0 +1,125 @@
|
||||
// [z-paging]点击返回顶部view模块
|
||||
import u from '.././z-paging-utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 自动显示点击返回顶部按钮,默认为否
|
||||
autoShowBackToTop: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoShowBackToTop', false)
|
||||
},
|
||||
// 点击返回顶部按钮显示/隐藏的阈值(滚动距离),单位为px,默认为400rpx
|
||||
backToTopThreshold: {
|
||||
type: [Number, String],
|
||||
default: u.gc('backToTopThreshold', '400rpx')
|
||||
},
|
||||
// 点击返回顶部按钮的自定义图片地址,默认使用z-paging内置的图片
|
||||
backToTopImg: {
|
||||
type: String,
|
||||
default: u.gc('backToTopImg', '')
|
||||
},
|
||||
// 点击返回顶部按钮返回到顶部时是否展示过渡动画,默认为是
|
||||
backToTopWithAnimate: {
|
||||
type: Boolean,
|
||||
default: u.gc('backToTopWithAnimate', true)
|
||||
},
|
||||
// 点击返回顶部按钮与底部的距离,注意添加单位px或rpx,默认为160rpx
|
||||
backToTopBottom: {
|
||||
type: [Number, String],
|
||||
default: u.gc('backToTopBottom', '160rpx')
|
||||
},
|
||||
// 点击返回顶部按钮的自定义样式
|
||||
backToTopStyle: {
|
||||
type: Object,
|
||||
default: u.gc('backToTopStyle', {}),
|
||||
},
|
||||
// iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部,只支持竖向,默认为是
|
||||
enableBackToTop: {
|
||||
type: Boolean,
|
||||
default: u.gc('enableBackToTop', true)
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 点击返回顶部的class
|
||||
backToTopClass: 'zp-back-to-top zp-back-to-top-hide',
|
||||
// 上次点击返回顶部的时间
|
||||
lastBackToTopShowTime: 0,
|
||||
// 点击返回顶部显示的class是否在展示中,使得按钮展示/隐藏过度效果更自然
|
||||
showBackToTopClass: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
backToTopThresholdUnitConverted() {
|
||||
return u.addUnit(this.backToTopThreshold, this.unit);
|
||||
},
|
||||
backToTopBottomUnitConverted() {
|
||||
return u.addUnit(this.backToTopBottom, this.unit);
|
||||
},
|
||||
finalEnableBackToTop() {
|
||||
return this.usePageScroll ? false : this.enableBackToTop;
|
||||
},
|
||||
finalBackToTopThreshold() {
|
||||
return u.convertToPx(this.backToTopThresholdUnitConverted);
|
||||
},
|
||||
finalBackToTopStyle() {
|
||||
const backToTopStyle = this.backToTopStyle;
|
||||
if (!backToTopStyle.bottom) {
|
||||
backToTopStyle.bottom = this.windowBottom + u.convertToPx(this.backToTopBottomUnitConverted) + 'px';
|
||||
}
|
||||
if(!backToTopStyle.position){
|
||||
backToTopStyle.position = this.usePageScroll ? 'fixed': 'absolute';
|
||||
}
|
||||
return backToTopStyle;
|
||||
},
|
||||
finalBackToTopClass() {
|
||||
return `${this.backToTopClass} zp-back-to-top-${this.unit}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击了返回顶部
|
||||
_backToTopClick() {
|
||||
let callbacked = false;
|
||||
this.$emit('backToTopClick', toTop => {
|
||||
(toTop === undefined || toTop === true) && this._handleToTop();
|
||||
callbacked = true;
|
||||
});
|
||||
// 如果用户没有禁止默认的返回顶部事件,则触发滚动到顶部
|
||||
this.$nextTick(() => {
|
||||
!callbacked && this._handleToTop();
|
||||
})
|
||||
},
|
||||
// 处理滚动到顶部(聊天记录模式中为滚动到底部)
|
||||
_handleToTop() {
|
||||
!this.backToTopWithAnimate && this._checkShouldShowBackToTop(0);
|
||||
!this.useChatRecordMode ? this.scrollToTop(this.backToTopWithAnimate) : this.scrollToBottom(this.backToTopWithAnimate);
|
||||
},
|
||||
// 判断是否要显示返回顶部按钮
|
||||
_checkShouldShowBackToTop(scrollTop) {
|
||||
if (!this.autoShowBackToTop) {
|
||||
this.showBackToTopClass = false;
|
||||
return;
|
||||
}
|
||||
if (scrollTop > this.finalBackToTopThreshold) {
|
||||
if (!this.showBackToTopClass) {
|
||||
// 记录当前点击返回顶部按钮显示的class生效了
|
||||
this.showBackToTopClass = true;
|
||||
this.lastBackToTopShowTime = new Date().getTime();
|
||||
// 当滚动到需要展示返回顶部的阈值内,则延迟300毫秒展示返回到顶部按钮
|
||||
u.delay(() => {
|
||||
this.backToTopClass = 'zp-back-to-top zp-back-to-top-show';
|
||||
}, 300)
|
||||
}
|
||||
} else {
|
||||
// 如果当前点击返回顶部按钮显示的class是生效状态并且滚动小于触发阈值,则隐藏返回顶部按钮
|
||||
if (this.showBackToTopClass) {
|
||||
this.backToTopClass = 'zp-back-to-top zp-back-to-top-hide';
|
||||
u.delay(() => {
|
||||
this.showBackToTopClass = false;
|
||||
}, new Date().getTime() - this.lastBackToTopShowTime < 500 ? 0 : 300)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
// [z-paging]聊天记录模式模块
|
||||
import u from '.././z-paging-utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 使用聊天记录模式,默认为否
|
||||
useChatRecordMode: {
|
||||
type: Boolean,
|
||||
default: u.gc('useChatRecordMode', false)
|
||||
},
|
||||
// 使用聊天记录模式时滚动到顶部后,列表垂直移动偏移距离。默认0rpx。单位px(暂时无效)
|
||||
chatRecordMoreOffset: {
|
||||
type: [Number, String],
|
||||
default: u.gc('chatRecordMoreOffset', '0rpx')
|
||||
},
|
||||
// 使用聊天记录模式时是否自动隐藏键盘:在用户触摸列表时候自动隐藏键盘,默认为是
|
||||
autoHideKeyboardWhenChat: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoHideKeyboardWhenChat', true)
|
||||
},
|
||||
// 使用聊天记录模式中键盘弹出时是否自动调整slot="bottom"高度,默认为是
|
||||
autoAdjustPositionWhenChat: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoAdjustPositionWhenChat', true)
|
||||
},
|
||||
// 使用聊天记录模式中键盘弹出时占位高度偏移距离。默认0rpx。单位px
|
||||
chatAdjustPositionOffset: {
|
||||
type: [Number, String],
|
||||
default: u.gc('chatAdjustPositionOffset', '0rpx')
|
||||
},
|
||||
// 使用聊天记录模式中键盘弹出时是否自动滚动到底部,默认为否
|
||||
autoToBottomWhenChat: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoToBottomWhenChat', false)
|
||||
},
|
||||
// 使用聊天记录模式中reload时是否显示chatLoading,默认为否
|
||||
showChatLoadingWhenReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('showChatLoadingWhenReload', false)
|
||||
},
|
||||
// 在聊天记录模式中滑动到顶部状态为默认状态时,以加载中的状态展示,默认为是。若设置为否,则默认会显示【点击加载更多】,然后才会显示loading
|
||||
chatLoadingMoreDefaultAsLoading: {
|
||||
type: Boolean,
|
||||
default: u.gc('chatLoadingMoreDefaultAsLoading', true)
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 键盘高度
|
||||
keyboardHeight: 0,
|
||||
// 键盘高度是否未改变,此时占位高度变化不需要动画效果
|
||||
isKeyboardHeightChanged: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
finalChatRecordMoreOffset() {
|
||||
return u.convertToPx(this.chatRecordMoreOffset);
|
||||
},
|
||||
finalChatAdjustPositionOffset() {
|
||||
return u.convertToPx(this.chatAdjustPositionOffset);
|
||||
},
|
||||
// 聊天记录模式旋转180度style
|
||||
chatRecordRotateStyle() {
|
||||
let cellStyle;
|
||||
// 在vue中,直接将列表倒置,因此在vue的cell中,也直接写style="transform: scaleY(-1)"转回来即可。
|
||||
// #ifndef APP-NVUE
|
||||
cellStyle = this.useChatRecordMode ? { transform: 'scaleY(-1)' } : {};
|
||||
// #endif
|
||||
|
||||
// 在nvue中,需要考虑数据量不满一页的情况,因为nvue中的list无法通过flex-end修改不满一页的起始位置,会导致不满一页时列表数据从底部开始,因此需要特别判断
|
||||
// 当数据不满一屏的时候,不进行列表倒置
|
||||
// #ifdef APP-NVUE
|
||||
cellStyle = this.useChatRecordMode ? { transform: this.isFirstPageAndNoMore ? 'scaleY(1)' : 'scaleY(-1)' } : {};
|
||||
// #endif
|
||||
|
||||
this.$emit('update:cellStyle', cellStyle);
|
||||
this.$emit('cellStyleChange', cellStyle);
|
||||
|
||||
// 在聊天记录模式中,如果列表没有倒置并且当前是第一页,则需要自动滚动到最底部
|
||||
this.$nextTick(() => {
|
||||
if (this.isFirstPage && this.isChatRecordModeAndNotInversion) {
|
||||
this.$nextTick(() => {
|
||||
// 这里多次触发滚动到底部是为了避免在某些情况下,即使是在nextTick但是cell未渲染完毕导致滚动到底部位置不正确的问题
|
||||
this._scrollToBottom(false);
|
||||
u.delay(() => {
|
||||
this._scrollToBottom(false);
|
||||
u.delay(() => {
|
||||
this._scrollToBottom(false);
|
||||
}, 50)
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
})
|
||||
return cellStyle;
|
||||
},
|
||||
// 是否是聊天记录列表并且有配置transform
|
||||
isChatRecordModeHasTransform() {
|
||||
return this.useChatRecordMode && this.chatRecordRotateStyle && this.chatRecordRotateStyle.transform;
|
||||
},
|
||||
// 是否是聊天记录列表并且列表未倒置
|
||||
isChatRecordModeAndNotInversion() {
|
||||
return this.isChatRecordModeHasTransform && this.chatRecordRotateStyle.transform === 'scaleY(1)';
|
||||
},
|
||||
// 是否是聊天记录列表并且列表倒置
|
||||
isChatRecordModeAndInversion() {
|
||||
return this.isChatRecordModeHasTransform && this.chatRecordRotateStyle.transform === 'scaleY(-1)';
|
||||
},
|
||||
// 最终的聊天记录模式中底部安全区域的高度,如果开启了底部安全区域并且键盘未弹出,则添加底部区域高度
|
||||
chatRecordModeSafeAreaBottom() {
|
||||
return this.safeAreaInsetBottom && !this.keyboardHeight ? this.safeAreaBottom : 0;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 监听键盘高度变化(H5、百度小程序、抖音小程序、飞书小程序不支持)
|
||||
// #ifndef H5 || MP-BAIDU || MP-TOUTIAO
|
||||
if (this.useChatRecordMode) {
|
||||
uni.onKeyboardHeightChange(this._handleKeyboardHeightChange);
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
// 添加聊天记录
|
||||
addChatRecordData(data, toBottom = true, toBottomWithAnimate = true) {
|
||||
if (!this.useChatRecordMode) return;
|
||||
this.isTotalChangeFromAddData = true;
|
||||
this.addDataFromTop(data, toBottom, toBottomWithAnimate);
|
||||
},
|
||||
// 手动触发滚动到顶部加载更多,聊天记录模式时有效
|
||||
doChatRecordLoadMore() {
|
||||
this.useChatRecordMode && this._onLoadingMore('click');
|
||||
},
|
||||
// 处理键盘高度变化
|
||||
_handleKeyboardHeightChange(res) {
|
||||
this.$emit('keyboardHeightChange', res);
|
||||
if (this.autoAdjustPositionWhenChat) {
|
||||
this.isKeyboardHeightChanged = true;
|
||||
this.keyboardHeight = res.height > 0 ? res.height + this.finalChatAdjustPositionOffset : res.height;
|
||||
}
|
||||
if (this.autoToBottomWhenChat && this.keyboardHeight > 0) {
|
||||
u.delay(() => {
|
||||
this.scrollToBottom(false);
|
||||
u.delay(() => {
|
||||
this.scrollToBottom(false);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// [z-paging]通用布局相关模块
|
||||
import u from '.././z-paging-utils'
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
const weexDom = weex.requireModule('dom');
|
||||
// #endif
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
systemInfo: null,
|
||||
cssSafeAreaInsetBottom: -1,
|
||||
isReadyDestroy: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 顶部可用距离
|
||||
windowTop() {
|
||||
if (!this.systemInfo) return 0;
|
||||
// 暂时修复vue3中隐藏系统导航栏后windowTop获取不正确的问题,具体bug详见https://ask.dcloud.net.cn/question/141634
|
||||
// 感谢litangyu!!https://github.com/SmileZXLee/uni-z-paging/issues/25
|
||||
// #ifdef VUE3 && H5
|
||||
const pageHeadNode = document.getElementsByTagName("uni-page-head");
|
||||
if (!pageHeadNode.length) return 0;
|
||||
// #endif
|
||||
return this.systemInfo.windowTop || 0;
|
||||
},
|
||||
// 底部安全区域高度
|
||||
safeAreaBottom() {
|
||||
if (!this.systemInfo) return 0;
|
||||
let safeAreaBottom = 0;
|
||||
// #ifdef APP-PLUS
|
||||
safeAreaBottom = this.systemInfo.safeAreaInsets.bottom || 0 ;
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
safeAreaBottom = Math.max(this.cssSafeAreaInsetBottom, 0);
|
||||
// #endif
|
||||
return safeAreaBottom;
|
||||
},
|
||||
// 是否是比较老的webview,在一些老的webview中,需要进行一些特殊处理
|
||||
isOldWebView() {
|
||||
// #ifndef APP-NVUE || MP-KUAISHOU
|
||||
try {
|
||||
const systemInfos = u.getSystemInfoSync(true).system.split(' ');
|
||||
const deviceType = systemInfos[0];
|
||||
const version = parseInt(systemInfos[1]);
|
||||
if ((deviceType === 'iOS' && version <= 10) || (deviceType === 'Android' && version <= 6)) {
|
||||
return true;
|
||||
}
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
// #endif
|
||||
return false;
|
||||
},
|
||||
// 当前组件的$slots,兼容不同平台
|
||||
zSlots() {
|
||||
// #ifdef VUE2
|
||||
|
||||
// #ifdef MP-ALIPAY
|
||||
return this.$slots;
|
||||
// #endif
|
||||
|
||||
return this.$scopedSlots || this.$slots;
|
||||
// #endif
|
||||
|
||||
return this.$slots;
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.isReadyDestroy = true;
|
||||
},
|
||||
// #ifdef VUE3
|
||||
unmounted() {
|
||||
this.isReadyDestroy = true;
|
||||
},
|
||||
// #endif
|
||||
methods: {
|
||||
// 更新fixed模式下z-paging的布局
|
||||
updateFixedLayout() {
|
||||
this.fixed && this.$nextTick(() => {
|
||||
this.systemInfo = u.getSystemInfoSync();
|
||||
})
|
||||
},
|
||||
// 获取节点尺寸
|
||||
_getNodeClientRect(select, inDom = true, scrollOffset = false) {
|
||||
if (this.isReadyDestroy) {
|
||||
return Promise.resolve(false);
|
||||
};
|
||||
// nvue中获取节点信息
|
||||
// #ifdef APP-NVUE
|
||||
select = select.replace(/[.|#]/g, '');
|
||||
const ref = this.$refs[select];
|
||||
return new Promise((resolve, reject) => {
|
||||
if (ref) {
|
||||
weexDom.getComponentRect(ref, option => {
|
||||
resolve(option && option.result ? [option.size] : false);
|
||||
})
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
// #endif
|
||||
|
||||
// vue中获取节点信息
|
||||
//#ifdef MP-ALIPAY
|
||||
inDom = false;
|
||||
//#endif
|
||||
|
||||
/*
|
||||
inDom可能是true、false,也可能是具体的dom节点
|
||||
如果inDom不为false,则使用uni.createSelectorQuery().in()进行查询,如果inDom为true,则in中的是this,否则in中的为具体的dom
|
||||
如果inDom为false,则使用uni.createSelectorQuery()进行查询
|
||||
*/
|
||||
let res = !!inDom ? uni.createSelectorQuery().in(inDom === true ? this : inDom) : uni.createSelectorQuery();
|
||||
scrollOffset ? res.select(select).scrollOffset() : res.select(select).boundingClientRect();
|
||||
return new Promise((resolve, reject) => {
|
||||
res.exec(data => {
|
||||
resolve((data && data != '' && data != undefined && data.length) ? data : false);
|
||||
});
|
||||
});
|
||||
},
|
||||
// 获取slot="left"和slot="right"宽度并且更新布局
|
||||
_updateLeftAndRightWidth(targetStyle, parentNodePrefix) {
|
||||
this.$nextTick(() => {
|
||||
let delayTime = 0;
|
||||
// #ifdef MP-BAIDU
|
||||
delayTime = 10;
|
||||
// #endif
|
||||
setTimeout(() => {
|
||||
['left','right'].map(position => {
|
||||
this._getNodeClientRect(`.${parentNodePrefix}-${position}`).then(res => {
|
||||
this.$set(targetStyle, position, res ? res[0].width + 'px' : '0px');
|
||||
});
|
||||
})
|
||||
}, delayTime)
|
||||
})
|
||||
},
|
||||
// 通过获取css设置的底部安全区域占位view高度设置bottom距离(直接通过systemInfo在部分平台上无法获取到底部安全区域)
|
||||
_getCssSafeAreaInsetBottom(success) {
|
||||
this._getNodeClientRect('.zp-safe-area-inset-bottom').then(res => {
|
||||
this.cssSafeAreaInsetBottom = res ? res[0].height : -1;
|
||||
res && success && success();
|
||||
});
|
||||
},
|
||||
// 同步获取系统信息,兼容不同平台(供z-paging-swiper使用)
|
||||
_getSystemInfoSync(useCache = false) {
|
||||
return u.getSystemInfoSync(useCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
// [z-paging]数据处理模块
|
||||
import u from '.././z-paging-utils'
|
||||
import c from '.././z-paging-constant'
|
||||
import Enum from '.././z-paging-enum'
|
||||
import interceptor from '../z-paging-interceptor'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 自定义初始的pageNo,默认为1
|
||||
defaultPageNo: {
|
||||
type: Number,
|
||||
default: u.gc('defaultPageNo', 1),
|
||||
observer: function(newVal) {
|
||||
this.pageNo = newVal;
|
||||
},
|
||||
},
|
||||
// 自定义pageSize,默认为10
|
||||
defaultPageSize: {
|
||||
type: Number,
|
||||
default: u.gc('defaultPageSize', 10),
|
||||
validator: (value) => {
|
||||
if (value <= 0) u.consoleErr('default-page-size必须大于0!');
|
||||
return value > 0;
|
||||
}
|
||||
},
|
||||
// 为保证数据一致,设置当前tab切换时的标识key,并在complete中传递相同key,若二者不一致,则complete将不会生效
|
||||
dataKey: {
|
||||
type: [Number, String, Object],
|
||||
default: u.gc('dataKey', null),
|
||||
},
|
||||
// 使用缓存,若开启将自动缓存第一页的数据,默认为否。请注意,因考虑到切换tab时不同tab数据不同的情况,默认仅会缓存组件首次加载时第一次请求到的数据,后续的下拉刷新操作不会更新缓存。
|
||||
useCache: {
|
||||
type: Boolean,
|
||||
default: u.gc('useCache', false)
|
||||
},
|
||||
// 使用缓存时缓存的key,用于区分不同列表的缓存数据,useCache为true时必须设置,否则缓存无效
|
||||
cacheKey: {
|
||||
type: String,
|
||||
default: u.gc('cacheKey', null)
|
||||
},
|
||||
// 缓存模式,默认仅会缓存组件首次加载时第一次请求到的数据,可设置为always,即代表总是缓存,每次列表刷新(下拉刷新、调用reload等)都会更新缓存
|
||||
cacheMode: {
|
||||
type: String,
|
||||
default: u.gc('cacheMode', Enum.CacheMode.Default)
|
||||
},
|
||||
// 自动注入的list名,可自动修改父view(包含ref="paging")中对应name的list值
|
||||
autowireListName: {
|
||||
type: String,
|
||||
default: u.gc('autowireListName', '')
|
||||
},
|
||||
// 自动注入的query名,可自动调用父view(包含ref="paging")中的query方法
|
||||
autowireQueryName: {
|
||||
type: String,
|
||||
default: u.gc('autowireQueryName', '')
|
||||
},
|
||||
// 获取分页数据Function,功能与@query类似。若设置了fetch则@query将不再触发
|
||||
fetch: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
// fetch的附加参数,fetch配置后有效
|
||||
fetchParams: {
|
||||
type: Object,
|
||||
default: u.gc('fetchParams', null)
|
||||
},
|
||||
// z-paging mounted后自动调用reload方法(mounted后自动调用接口),默认为是
|
||||
auto: {
|
||||
type: Boolean,
|
||||
default: u.gc('auto', true)
|
||||
},
|
||||
// 用户下拉刷新时是否触发reload方法,默认为是
|
||||
reloadWhenRefresh: {
|
||||
type: Boolean,
|
||||
default: u.gc('reloadWhenRefresh', true)
|
||||
},
|
||||
// reload时自动滚动到顶部,默认为是
|
||||
autoScrollToTopWhenReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoScrollToTopWhenReload', true)
|
||||
},
|
||||
// reload时立即自动清空原list,默认为是,若立即自动清空,则在reload之后、请求回调之前页面是空白的
|
||||
autoCleanListWhenReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoCleanListWhenReload', true)
|
||||
},
|
||||
// 列表刷新时自动显示下拉刷新view,默认为否
|
||||
showRefresherWhenReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('showRefresherWhenReload', false)
|
||||
},
|
||||
// 列表刷新时自动显示加载更多view,且为加载中状态,默认为否
|
||||
showLoadingMoreWhenReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('showLoadingMoreWhenReload', false)
|
||||
},
|
||||
// 组件created时立即触发reload(可解决一些情况下先看到页面再看到loading的问题),auto为true时有效。为否时将在mounted+nextTick后触发reload,默认为否
|
||||
createdReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('createdReload', false)
|
||||
},
|
||||
// 本地分页时上拉加载更多延迟时间,单位为毫秒,默认200毫秒
|
||||
localPagingLoadingTime: {
|
||||
type: [Number, String],
|
||||
default: u.gc('localPagingLoadingTime', 200)
|
||||
},
|
||||
// 自动拼接complete中传过来的数组(使用聊天记录模式时无效)
|
||||
concat: {
|
||||
type: Boolean,
|
||||
default: u.gc('concat', true)
|
||||
},
|
||||
// 请求失败是否触发reject,默认为是
|
||||
callNetworkReject: {
|
||||
type: Boolean,
|
||||
default: u.gc('callNetworkReject', true)
|
||||
},
|
||||
// 父组件v-model所绑定的list的值
|
||||
value: {
|
||||
type: Array,
|
||||
default: function() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
// #ifdef VUE3
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: function() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
data (){
|
||||
return {
|
||||
currentData: [],
|
||||
totalData: [],
|
||||
realTotalData: [],
|
||||
totalLocalPagingList: [],
|
||||
dataPromiseResultMap: {
|
||||
reload: null,
|
||||
complete: null,
|
||||
localPaging: null
|
||||
},
|
||||
isSettingCacheList: false,
|
||||
pageNo: 1,
|
||||
currentRefreshPageSize: 0,
|
||||
isLocalPaging: false,
|
||||
isAddedData: false,
|
||||
isTotalChangeFromAddData: false,
|
||||
privateConcat: true,
|
||||
myParentQuery: -1,
|
||||
firstPageLoaded: false,
|
||||
pagingLoaded: false,
|
||||
loaded: false,
|
||||
isUserReload: true,
|
||||
fromEmptyViewReload: false,
|
||||
queryFrom: '',
|
||||
listRendering: false,
|
||||
isHandlingRefreshToPage: false,
|
||||
isFirstPageAndNoMore: false,
|
||||
totalDataChangeThrow: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageSize() {
|
||||
return this.defaultPageSize;
|
||||
},
|
||||
finalConcat() {
|
||||
return this.concat && this.privateConcat;
|
||||
},
|
||||
finalUseCache() {
|
||||
if (this.useCache && !this.cacheKey) {
|
||||
u.consoleErr('use-cache为true时,必须设置cache-key,否则缓存无效!');
|
||||
}
|
||||
return this.useCache && !!this.cacheKey;
|
||||
},
|
||||
finalCacheKey() {
|
||||
return this.cacheKey ? `${c.cachePrefixKey}-${this.cacheKey}` : null;
|
||||
},
|
||||
isFirstPage() {
|
||||
return this.pageNo === this.defaultPageNo;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
totalData(newVal, oldVal) {
|
||||
this._totalDataChange(newVal, oldVal, this.totalDataChangeThrow);
|
||||
this.totalDataChangeThrow = true;
|
||||
},
|
||||
currentData(newVal, oldVal) {
|
||||
this._currentDataChange(newVal, oldVal);
|
||||
},
|
||||
useChatRecordMode(newVal, oldVal) {
|
||||
if (newVal) {
|
||||
this.nLoadingMoreFixedHeight = false;
|
||||
}
|
||||
},
|
||||
value: {
|
||||
handler(newVal) {
|
||||
// 当v-model绑定的数据源被更改时,此时数据源改变不emit input事件,避免循环调用
|
||||
if (newVal !== this.totalData) {
|
||||
this.totalDataChangeThrow = false;
|
||||
this.totalData = newVal;
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
// #ifdef VUE3
|
||||
modelValue: {
|
||||
handler(newVal) {
|
||||
// 当v-model绑定的数据源被更改时,此时数据源改变不emit input事件,避免循环调用
|
||||
if (newVal !== this.totalData) {
|
||||
this.totalDataChangeThrow = false;
|
||||
this.totalData = newVal;
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
// 请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为是否成功(默认为是)
|
||||
complete(data, success = true) {
|
||||
this.customNoMore = -1;
|
||||
return this.addData(data, success);
|
||||
},
|
||||
//【保证数据一致】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为dataKey,需与:data-key绑定的一致,第三个参数为是否成功(默认为是)
|
||||
completeByKey(data, dataKey = null, success = true) {
|
||||
if (dataKey !== null && this.dataKey !== null && dataKey !== this.dataKey) {
|
||||
this.isFirstPage && this.endRefresh();
|
||||
return new Promise(resolve => resolve());
|
||||
}
|
||||
this.customNoMore = -1;
|
||||
return this.addData(data, success);
|
||||
},
|
||||
//【通过total判断是否有更多数据】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为total(列表总数),第三个参数为是否成功(默认为是)
|
||||
completeByTotal(data, total, success = true) {
|
||||
if (total == 'undefined') {
|
||||
this.customNoMore = -1;
|
||||
} else {
|
||||
const dataTypeRes = this._checkDataType(data, success, false);
|
||||
data = dataTypeRes.data;
|
||||
success = dataTypeRes.success;
|
||||
if (total >= 0 && success) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.$nextTick(() => {
|
||||
let nomore = false;
|
||||
const realTotalDataCount = this.pageNo == this.defaultPageNo ? 0 : this.realTotalData.length;
|
||||
const dataLength = this.privateConcat ? data.length : 0;
|
||||
let exceedCount = realTotalDataCount + dataLength - total;
|
||||
// 没有更多数据了
|
||||
if (exceedCount >= 0) {
|
||||
nomore = true;
|
||||
// 仅截取total内部分的数据
|
||||
exceedCount = this.defaultPageSize - exceedCount;
|
||||
if (this.privateConcat && exceedCount > 0 && exceedCount < data.length) {
|
||||
data = data.splice(0, exceedCount);
|
||||
}
|
||||
}
|
||||
this.completeByNoMore(data, nomore, success).then(res => resolve(res)).catch(() => reject());
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
return this.addData(data, success);
|
||||
},
|
||||
//【自行判断是否有更多数据】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为是否没有更多数据,第三个参数为是否成功(默认是是)
|
||||
completeByNoMore(data, nomore, success = true) {
|
||||
if (nomore != 'undefined') {
|
||||
this.customNoMore = nomore == true ? 1 : 0;
|
||||
}
|
||||
return this.addData(data, success);
|
||||
},
|
||||
// 请求结束且请求失败时调用,支持传入请求失败原因
|
||||
completeByError(errorMsg) {
|
||||
this.customerEmptyViewErrorText = errorMsg;
|
||||
return this.complete(false);
|
||||
},
|
||||
// 与上方complete方法功能一致,新版本中设置服务端回调数组请使用complete方法
|
||||
addData(data, success = true) {
|
||||
if (!this.fromCompleteEmit) {
|
||||
this.disabledCompleteEmit = true;
|
||||
this.fromCompleteEmit = false;
|
||||
}
|
||||
const currentTimeStamp = u.getTime();
|
||||
const disTime = currentTimeStamp - this.requestTimeStamp;
|
||||
let minDelay = this.minDelay;
|
||||
if (this.isFirstPage && this.finalShowRefresherWhenReload) {
|
||||
minDelay = Math.max(400, minDelay);
|
||||
}
|
||||
const addDataDalay = (this.requestTimeStamp > 0 && disTime < minDelay) ? minDelay - disTime : 0;
|
||||
this.$nextTick(() => {
|
||||
u.delay(() => {
|
||||
this._addData(data, success, false);
|
||||
}, this.delay > 0 ? this.delay : addDataDalay)
|
||||
})
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dataPromiseResultMap.complete = { resolve, reject };
|
||||
});
|
||||
},
|
||||
// 从顶部添加数据,不会影响分页的pageNo和pageSize
|
||||
addDataFromTop(data, toTop = true, toTopWithAnimate = true) {
|
||||
// 数据是否拼接到顶部,如果是聊天记录模式并且列表没有倒置,则应该拼接在底部
|
||||
let addFromTop = !this.isChatRecordModeAndNotInversion;
|
||||
data = Object.prototype.toString.call(data) !== '[object Array]' ? [data] : (addFromTop ? data.reverse() : data);
|
||||
// #ifndef APP-NVUE
|
||||
this.finalUseVirtualList && this._setCellIndex(data, 'top')
|
||||
// #endif
|
||||
|
||||
this.totalData = addFromTop ? [...data, ...this.totalData] : [...this.totalData, ...data];
|
||||
if (toTop) {
|
||||
u.delay(() => this.useChatRecordMode ? this.scrollToBottom(toTopWithAnimate) : this.scrollToTop(toTopWithAnimate));
|
||||
}
|
||||
},
|
||||
// 重新设置列表数据,调用此方法不会影响pageNo和pageSize,也不会触发请求。适用场景:当需要删除列表中某一项时,将删除对应项后的数组通过此方法传递给z-paging。(当出现类似的需要修改列表数组的场景时,请使用此方法,请勿直接修改page中:list.sync绑定的数组)
|
||||
resetTotalData(data) {
|
||||
this.isTotalChangeFromAddData = true;
|
||||
data = Object.prototype.toString.call(data) !== '[object Array]' ? [data] : data;
|
||||
this.totalData = data;
|
||||
},
|
||||
// 设置本地分页数据,请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging作分页处理(若调用了此方法,则上拉加载更多时内部会自动分页,不会触发@query所绑定的事件)
|
||||
setLocalPaging(data, success = true) {
|
||||
this.isLocalPaging = true;
|
||||
this.$nextTick(() => {
|
||||
this._addData(data, success, true);
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dataPromiseResultMap.localPaging = { resolve, reject };
|
||||
});
|
||||
},
|
||||
// 重新加载分页数据,pageNo会恢复为默认值,相当于下拉刷新的效果(animate为true时会展示下拉刷新动画,默认为false)
|
||||
reload(animate = this.showRefresherWhenReload) {
|
||||
if (animate) {
|
||||
this.privateShowRefresherWhenReload = animate;
|
||||
this.isUserPullDown = true;
|
||||
}
|
||||
if (!this.showLoadingMoreWhenReload) {
|
||||
this.listRendering = true;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this._preReload(animate, false);
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dataPromiseResultMap.reload = { resolve, reject };
|
||||
});
|
||||
},
|
||||
// 刷新列表数据,pageNo和pageSize不会重置,列表数据会重新从服务端获取。必须保证@query绑定的方法中的pageNo和pageSize和传给服务端的一致
|
||||
refresh() {
|
||||
return this._handleRefreshWithDisPageNo(this.pageNo - this.defaultPageNo + 1);
|
||||
},
|
||||
// 刷新列表数据至指定页,例如pageNo=5时则代表刷新列表至第5页,此时pageNo会变为5,列表会展示前5页的数据。必须保证@query绑定的方法中的pageNo和pageSize和传给服务端的一致
|
||||
refreshToPage(pageNo) {
|
||||
this.isHandlingRefreshToPage = true;
|
||||
return this._handleRefreshWithDisPageNo(pageNo + this.defaultPageNo - 1);
|
||||
},
|
||||
// 手动更新列表缓存数据,将自动截取v-model绑定的list中的前pageSize条覆盖缓存,请确保在list数据更新到预期结果后再调用此方法
|
||||
updateCache() {
|
||||
if (this.finalUseCache && this.totalData.length) {
|
||||
this._saveLocalCache(this.totalData.slice(0, Math.min(this.totalData.length, this.pageSize)));
|
||||
}
|
||||
},
|
||||
// 清空分页数据
|
||||
clean() {
|
||||
this._reload(true);
|
||||
this._addData([], true, false);
|
||||
},
|
||||
// 清空分页数据
|
||||
clear() {
|
||||
this.clean();
|
||||
},
|
||||
// reload之前的一些处理
|
||||
_preReload(animate = this.showRefresherWhenReload, isFromMounted = true, retryCount = 0) {
|
||||
const showRefresher = this.finalRefresherEnabled && this.useCustomRefresher;
|
||||
// #ifndef APP-NVUE
|
||||
// 如果获取slot="refresher"高度失败,则不触发reload,直到获取slot="refresher"高度成功
|
||||
if (this.customRefresherHeight === -1 && showRefresher) {
|
||||
u.delay(() => {
|
||||
retryCount ++;
|
||||
// 如果重试次数是10的倍数(也就是每500毫秒),尝试重新获取一下slot="refresher"高度
|
||||
// 此举是为了解决在某些特殊情况下,z-paging组件mounted了,但是未展示在用户面前,(比如在tabbar页面中,未切换到对应tabbar但是通过代码让z-paging展示了,此时控制台会报Error: Not Found:Page,因为这时候去获取dom节点信息获取不到)
|
||||
// 当用户在某个时刻让此z-paging展示在面前时,即可顺利获取到slot="refresher"高度,递归停止
|
||||
if (retryCount % 10 === 0) {
|
||||
this._updateCustomRefresherHeight();
|
||||
}
|
||||
this._preReload(animate, isFromMounted, retryCount);
|
||||
}, c.delayTime / 2);
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
this.isUserReload = true;
|
||||
this.loadingType = Enum.LoadingType.Refresher;
|
||||
if (animate) {
|
||||
this.privateShowRefresherWhenReload = animate;
|
||||
// #ifndef APP-NVUE
|
||||
if (this.useCustomRefresher) {
|
||||
this._doRefresherRefreshAnimate();
|
||||
} else {
|
||||
this.refresherTriggered = true;
|
||||
}
|
||||
// #endif
|
||||
// #ifdef APP-NVUE
|
||||
this.refresherStatus = Enum.Refresher.Loading;
|
||||
this.refresherRevealStackCount ++;
|
||||
u.delay(() => {
|
||||
this._getNodeClientRect('zp-n-refresh-container', false).then((node) => {
|
||||
if (node) {
|
||||
let nodeHeight = node[0].height;
|
||||
this.nShowRefresherReveal = true;
|
||||
this.nShowRefresherRevealHeight = nodeHeight;
|
||||
u.delay(() => {
|
||||
this._nDoRefresherEndAnimation(0, -nodeHeight, false, false);
|
||||
u.delay(() => {
|
||||
this._nDoRefresherEndAnimation(nodeHeight, 0);
|
||||
}, 10)
|
||||
}, 10)
|
||||
}
|
||||
this._reload(false, isFromMounted);
|
||||
this._doRefresherLoad(false);
|
||||
});
|
||||
}, this.pagingLoaded ? 10 : 100)
|
||||
return;
|
||||
// #endif
|
||||
} else {
|
||||
this._refresherEnd(false, false, false, false);
|
||||
}
|
||||
this._reload(false, isFromMounted);
|
||||
},
|
||||
// 重新加载分页数据
|
||||
_reload(isClean = false, isFromMounted = false, isUserPullDown = false) {
|
||||
this.isAddedData = false;
|
||||
this.insideOfPaging = -1;
|
||||
this.cacheScrollNodeHeight = -1;
|
||||
this.pageNo = this.defaultPageNo;
|
||||
this._cleanRefresherEndTimeout();
|
||||
!this.privateShowRefresherWhenReload && !isClean && this._startLoading(true);
|
||||
this.firstPageLoaded = true;
|
||||
this.isTotalChangeFromAddData = false;
|
||||
if (!this.isSettingCacheList) {
|
||||
this.totalData = [];
|
||||
}
|
||||
if (!isClean) {
|
||||
this._emitQuery(this.pageNo, this.defaultPageSize, isUserPullDown ? Enum.QueryFrom.UserPullDown : Enum.QueryFrom.Reload);
|
||||
let delay = 0;
|
||||
// #ifdef MP-TOUTIAO
|
||||
delay = 5;
|
||||
// #endif
|
||||
u.delay(this._callMyParentQuery, delay);
|
||||
if (!isFromMounted && this.autoScrollToTopWhenReload) {
|
||||
let checkedNRefresherLoading = true;
|
||||
// #ifdef APP-NVUE
|
||||
checkedNRefresherLoading = !this.nRefresherLoading;
|
||||
// #endif
|
||||
checkedNRefresherLoading && this._scrollToTop(false);
|
||||
}
|
||||
}
|
||||
// #ifdef APP-NVUE
|
||||
this.$nextTick(() => {
|
||||
this.nShowBottom = this.realTotalData.length > 0;
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
// 处理服务端返回的数组
|
||||
_addData(data, success, isLocal) {
|
||||
this.isAddedData = true;
|
||||
this.fromEmptyViewReload = false;
|
||||
this.isTotalChangeFromAddData = true;
|
||||
this.refresherTriggered = false;
|
||||
this._endSystemLoadingAndRefresh();
|
||||
const tempIsUserPullDown = this.isUserPullDown;
|
||||
if (this.showRefresherUpdateTime && this.isFirstPage) {
|
||||
u.setRefesrherTime(u.getTime(), this.refresherUpdateTimeKey);
|
||||
this.$refs.refresh && this.$refs.refresh.updateTime();
|
||||
}
|
||||
if (!isLocal && tempIsUserPullDown && this.isFirstPage) {
|
||||
this.isUserPullDown = false;
|
||||
}
|
||||
this.listRendering = true;
|
||||
this.$nextTick(() => {
|
||||
u.delay(() => this.listRendering = false);
|
||||
})
|
||||
let dataTypeRes = this._checkDataType(data, success, isLocal);
|
||||
data = dataTypeRes.data;
|
||||
success = dataTypeRes.success;
|
||||
let delayTime = c.delayTime;
|
||||
if (this.useChatRecordMode) delayTime = 0;
|
||||
this.loadingForNow = false;
|
||||
u.delay(() => {
|
||||
this.pagingLoaded = true;
|
||||
this.$nextTick(()=>{
|
||||
!isLocal && this._refresherEnd(delayTime > 0, true, tempIsUserPullDown);
|
||||
})
|
||||
})
|
||||
if (this.isFirstPage) {
|
||||
this.isLoadFailed = !success;
|
||||
this.$emit('isLoadFailedChange', this.isLoadFailed);
|
||||
if (this.finalUseCache && success && (this.cacheMode === Enum.CacheMode.Always ? true : this.isSettingCacheList)) {
|
||||
this._saveLocalCache(data);
|
||||
}
|
||||
}
|
||||
this.isSettingCacheList = false;
|
||||
if (success) {
|
||||
if (!(this.privateConcat === false && !this.isHandlingRefreshToPage && this.loadingStatus === Enum.More.NoMore)) {
|
||||
this.loadingStatus = Enum.More.Default;
|
||||
}
|
||||
if (isLocal) {
|
||||
// 如果当前是本地分页,则必然是由setLocalPaging方法触发,此时直接本地加载第一页数据即可。后续本地分页加载更多方法由滚动到底部加载更多事件处理
|
||||
this.totalLocalPagingList = data;
|
||||
const localPageNo = this.defaultPageNo;
|
||||
const localPageSize = this.queryFrom !== Enum.QueryFrom.Refresh ? this.defaultPageSize : this.currentRefreshPageSize;
|
||||
this._localPagingQueryList(localPageNo, localPageSize, 0, res => {
|
||||
u.delay(() => {
|
||||
this.completeByTotal(res, this.totalLocalPagingList.length);;
|
||||
}, 0)
|
||||
})
|
||||
} else {
|
||||
// 如果当前不是本地分页,则按照正常分页逻辑进行数据处理&emit数据
|
||||
let dataChangeDelayTime = 0;
|
||||
// #ifdef APP-NVUE
|
||||
if (this.privateShowRefresherWhenReload && this.finalNvueListIs === 'waterfall') {
|
||||
dataChangeDelayTime = 150;
|
||||
}
|
||||
// #endif
|
||||
u.delay(() => {
|
||||
this._currentDataChange(data, this.currentData);
|
||||
this._callDataPromise(true, this.totalData);
|
||||
}, dataChangeDelayTime)
|
||||
}
|
||||
if (this.isHandlingRefreshToPage) {
|
||||
this.isHandlingRefreshToPage = false;
|
||||
this.pageNo = this.defaultPageNo + Math.ceil(data.length / this.pageSize) - 1;
|
||||
if (data.length % this.pageSize !== 0) {
|
||||
this.customNoMore = 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._currentDataChange(data, this.currentData);
|
||||
this._callDataPromise(false);
|
||||
this.loadingStatus = Enum.More.Fail;
|
||||
this.isHandlingRefreshToPage = false;
|
||||
if (this.loadingType === Enum.LoadingType.LoadMore) {
|
||||
this.pageNo --;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 所有数据改变时调用
|
||||
_totalDataChange(newVal, oldVal, eventThrow=true) {
|
||||
if ((!this.isUserReload || !this.autoCleanListWhenReload) && this.firstPageLoaded && !newVal.length && oldVal.length) {
|
||||
return;
|
||||
}
|
||||
this._doCheckScrollViewShouldFullHeight(newVal);
|
||||
if(!this.realTotalData.length && !newVal.length){
|
||||
eventThrow = false;
|
||||
}
|
||||
this.realTotalData = newVal;
|
||||
// emit列表更新事件
|
||||
if (eventThrow) {
|
||||
this.$emit('input', newVal);
|
||||
// #ifdef VUE3
|
||||
this.$emit('update:modelValue', newVal);
|
||||
// #endif
|
||||
this.$emit('update:list', newVal);
|
||||
this.$emit('listChange', newVal);
|
||||
this._callMyParentList(newVal);
|
||||
}
|
||||
this.firstPageLoaded = false;
|
||||
this.isTotalChangeFromAddData = false;
|
||||
this.$nextTick(() => {
|
||||
u.delay(()=>{
|
||||
// emit z-paging内容区域高度改变事件
|
||||
this._getNodeClientRect('.zp-paging-container-content').then(res => {
|
||||
res && this.$emit('contentHeightChanged', res[0].height);
|
||||
});
|
||||
}, c.delayTime * (this.isIos ? 1 : 3))
|
||||
// #ifdef APP-NVUE
|
||||
// 在nvue中延时600毫秒展示底部加载更多,避免底部加载更多太早加载闪一下的问题
|
||||
u.delay(() => {
|
||||
this.nShowBottom = true;
|
||||
}, c.delayTime * 6, 'nShowBottomDelay');
|
||||
// #endif
|
||||
})
|
||||
},
|
||||
// 当前数据改变时调用
|
||||
_currentDataChange(newVal, oldVal) {
|
||||
newVal = [...newVal];
|
||||
// #ifndef APP-NVUE
|
||||
this.finalUseVirtualList && this._setCellIndex(newVal, 'bottom');
|
||||
// #endif
|
||||
if (this.isFirstPage && this.finalConcat) {
|
||||
this.totalData = [];
|
||||
}
|
||||
// customNoMore:-1代表交由z-paging自行判断;1代表没有更多了;0代表还有更多数据
|
||||
if (this.customNoMore !== -1) {
|
||||
// 如果customNoMore等于1 或者 customNoMore不是0并且新增数组长度为0(也就是不是明确的还有更多数据并且新增的数组长度为0),则没有更多数据了
|
||||
if (this.customNoMore === 1 || (this.customNoMore !== 0 && !newVal.length)) {
|
||||
this.loadingStatus = Enum.More.NoMore;
|
||||
}
|
||||
} else {
|
||||
// 如果新增的数据数组长度为0 或者 新增的数组长度小于默认的pageSize,则没有更多数据了
|
||||
if (!newVal.length || (newVal.length && newVal.length < this.defaultPageSize)) {
|
||||
this.loadingStatus = Enum.More.NoMore;
|
||||
}
|
||||
}
|
||||
if (!this.totalData.length) {
|
||||
// #ifdef APP-NVUE
|
||||
// 如果在聊天记录模式+nvue中,并且数据不满一页时需要将列表倒序,因为此时没有将列表旋转180度,数组中第0条数据应当在最底下显示
|
||||
if (this.useChatRecordMode && this.finalConcat && this.isFirstPage && this.loadingStatus === Enum.More.NoMore) {
|
||||
newVal.reverse();
|
||||
}
|
||||
// #endif
|
||||
this.totalData = newVal;
|
||||
} else {
|
||||
if (this.finalConcat) {
|
||||
const currentScrollTop = this.oldScrollTop;
|
||||
this.totalData = [...this.totalData, ...newVal];
|
||||
// 此处是为了解决在微信小程序中,在某些情况下滚动到底部加载更多后滚动位置直接变为最底部的问题,因此需要通过代码强制滚动回加载更多前的位置
|
||||
// #ifdef MP-WEIXIN
|
||||
if (!this.isIos && !this.refresherOnly && !this.usePageScroll && newVal.length) {
|
||||
this.loadingMoreTimeStamp = u.getTime();
|
||||
this.$nextTick(() => {
|
||||
this.scrollToY(currentScrollTop);
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
} else {
|
||||
this.totalData = newVal;
|
||||
}
|
||||
}
|
||||
this.privateConcat = true;
|
||||
},
|
||||
// 根据pageNo处理refresh操作
|
||||
_handleRefreshWithDisPageNo(pageNo) {
|
||||
if (!this.isHandlingRefreshToPage && !this.realTotalData.length) return this.reload();
|
||||
if (pageNo >= 1) {
|
||||
this.loading = true;
|
||||
this.privateConcat = false;
|
||||
const totalPageSize = pageNo * this.pageSize;
|
||||
this.currentRefreshPageSize = totalPageSize;
|
||||
// 如果调用refresh时是本地分页,则在组件内部自己处理分页逻辑,不emit query相关事件
|
||||
if (this.isLocalPaging && this.isHandlingRefreshToPage) {
|
||||
this._localPagingQueryList(this.defaultPageNo, totalPageSize, 0, res => {
|
||||
this.complete(res);
|
||||
})
|
||||
} else {
|
||||
// emit query相关事件
|
||||
this._emitQuery(this.defaultPageNo, totalPageSize, Enum.QueryFrom.Refresh);
|
||||
this._callMyParentQuery(this.defaultPageNo, totalPageSize);
|
||||
}
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dataPromiseResultMap.reload = { resolve, reject };
|
||||
});
|
||||
},
|
||||
// 本地分页请求
|
||||
_localPagingQueryList(pageNo, pageSize, localPagingLoadingTime, callback) {
|
||||
pageNo = Math.max(1, pageNo);
|
||||
pageSize = Math.max(1, pageSize);
|
||||
const totalPagingList = [...this.totalLocalPagingList];
|
||||
const pageNoIndex = (pageNo - 1) * pageSize;
|
||||
const finalPageNoIndex = Math.min(totalPagingList.length, pageNoIndex + pageSize);
|
||||
const resultPagingList = totalPagingList.splice(pageNoIndex, finalPageNoIndex - pageNoIndex);
|
||||
u.delay(() => callback(resultPagingList), localPagingLoadingTime)
|
||||
},
|
||||
// 存储列表缓存数据
|
||||
_saveLocalCache(data) {
|
||||
uni.setStorageSync(this.finalCacheKey, data);
|
||||
},
|
||||
// 通过缓存数据填充列表数据
|
||||
_setListByLocalCache() {
|
||||
this.totalData = uni.getStorageSync(this.finalCacheKey) || [];
|
||||
this.isSettingCacheList = true;
|
||||
},
|
||||
// 修改父view的list
|
||||
_callMyParentList(newVal) {
|
||||
if (this.autowireListName.length) {
|
||||
const myParent = u.getParent(this.$parent);
|
||||
if (myParent && myParent[this.autowireListName]) {
|
||||
myParent[this.autowireListName] = newVal;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 调用父view的query
|
||||
_callMyParentQuery(customPageNo = 0, customPageSize = 0) {
|
||||
if (this.autowireQueryName) {
|
||||
if (this.myParentQuery === -1) {
|
||||
const myParent = u.getParent(this.$parent);
|
||||
if (myParent && myParent[this.autowireQueryName]) {
|
||||
this.myParentQuery = myParent[this.autowireQueryName];
|
||||
}
|
||||
}
|
||||
if (this.myParentQuery !== -1) {
|
||||
customPageSize > 0 ? this.myParentQuery(customPageNo, customPageSize) : this.myParentQuery(this.pageNo, this.defaultPageSize);
|
||||
}
|
||||
}
|
||||
},
|
||||
// emit query事件
|
||||
_emitQuery(pageNo, pageSize, from){
|
||||
this.queryFrom = from;
|
||||
this.requestTimeStamp = u.getTime();
|
||||
const [lastItem] = this.realTotalData.slice(-1);
|
||||
if (this.fetch) {
|
||||
const fetchParams = interceptor._handleFetchParams({pageNo, pageSize, from, lastItem: lastItem || null}, this.fetchParams);
|
||||
const fetchResult = this.fetch(fetchParams);
|
||||
if (!interceptor._handleFetchResult(fetchResult, this, fetchParams)) {
|
||||
u.isPromise(fetchResult) ? fetchResult.then(res => {
|
||||
this.complete(res);
|
||||
}).catch(err => {
|
||||
this.complete(false);
|
||||
}) : this.complete(fetchResult)
|
||||
}
|
||||
} else {
|
||||
this.$emit('query', ...interceptor._handleQuery(pageNo, pageSize, from, lastItem || null));
|
||||
}
|
||||
},
|
||||
// 触发数据改变promise
|
||||
_callDataPromise(success, totalList) {
|
||||
for (const key in this.dataPromiseResultMap) {
|
||||
const obj = this.dataPromiseResultMap[key];
|
||||
if (!obj) continue;
|
||||
success ? obj.resolve({ totalList, noMore: this.loadingStatus === Enum.More.NoMore }) : this.callNetworkReject && obj.reject(`z-paging-${key}-error`);
|
||||
}
|
||||
},
|
||||
// 检查complete data的类型
|
||||
_checkDataType(data, success, isLocal) {
|
||||
const dataType = Object.prototype.toString.call(data);
|
||||
if (dataType === '[object Boolean]') {
|
||||
success = data;
|
||||
data = [];
|
||||
} else if (dataType !== '[object Array]') {
|
||||
data = [];
|
||||
if (dataType !== '[object Undefined]' && dataType !== '[object Null]') {
|
||||
u.consoleErr(`${isLocal ? 'setLocalPaging' : 'complete'}参数类型不正确,第一个参数类型必须为Array!`);
|
||||
}
|
||||
}
|
||||
return { data, success };
|
||||
},
|
||||
}
|
||||
}
|
||||
144
uni_modules/z-paging/components/z-paging/js/modules/empty.js
Normal file
144
uni_modules/z-paging/components/z-paging/js/modules/empty.js
Normal file
@@ -0,0 +1,144 @@
|
||||
// [z-paging]空数据图view模块
|
||||
import u from '.././z-paging-utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 是否强制隐藏空数据图,默认为否
|
||||
hideEmptyView: {
|
||||
type: Boolean,
|
||||
default: u.gc('hideEmptyView', false)
|
||||
},
|
||||
// 空数据图描述文字,默认为“没有数据哦~”
|
||||
emptyViewText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('emptyViewText', null)
|
||||
},
|
||||
// 是否显示空数据图重新加载按钮(无数据时),默认为否
|
||||
showEmptyViewReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('showEmptyViewReload', false)
|
||||
},
|
||||
// 加载失败时是否显示空数据图重新加载按钮,默认为是
|
||||
showEmptyViewReloadWhenError: {
|
||||
type: Boolean,
|
||||
default: u.gc('showEmptyViewReloadWhenError', true)
|
||||
},
|
||||
// 空数据图点击重新加载文字,默认为“重新加载”
|
||||
emptyViewReloadText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('emptyViewReloadText', null)
|
||||
},
|
||||
// 空数据图图片,默认使用z-paging内置的图片
|
||||
emptyViewImg: {
|
||||
type: String,
|
||||
default: u.gc('emptyViewImg', '')
|
||||
},
|
||||
// 空数据图“加载失败”描述文字,默认为“很抱歉,加载失败”
|
||||
emptyViewErrorText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('emptyViewErrorText', null)
|
||||
},
|
||||
// 空数据图“加载失败”图片,默认使用z-paging内置的图片
|
||||
emptyViewErrorImg: {
|
||||
type: String,
|
||||
default: u.gc('emptyViewErrorImg', '')
|
||||
},
|
||||
// 空数据图样式
|
||||
emptyViewStyle: {
|
||||
type: Object,
|
||||
default: u.gc('emptyViewStyle', {})
|
||||
},
|
||||
// 空数据图容器样式
|
||||
emptyViewSuperStyle: {
|
||||
type: Object,
|
||||
default: u.gc('emptyViewSuperStyle', {})
|
||||
},
|
||||
// 空数据图img样式
|
||||
emptyViewImgStyle: {
|
||||
type: Object,
|
||||
default: u.gc('emptyViewImgStyle', {})
|
||||
},
|
||||
// 空数据图描述文字样式
|
||||
emptyViewTitleStyle: {
|
||||
type: Object,
|
||||
default: u.gc('emptyViewTitleStyle', {})
|
||||
},
|
||||
// 空数据图重新加载按钮样式
|
||||
emptyViewReloadStyle: {
|
||||
type: Object,
|
||||
default: u.gc('emptyViewReloadStyle', {})
|
||||
},
|
||||
// 空数据图片是否铺满z-paging,默认为否,即填充满z-paging内列表(滚动区域)部分。若设置为否,则为填铺满整个z-paging
|
||||
emptyViewFixed: {
|
||||
type: Boolean,
|
||||
default: u.gc('emptyViewFixed', false)
|
||||
},
|
||||
// 空数据图片是否垂直居中,默认为是,若设置为否即为从空数据容器顶部开始显示。emptyViewFixed为false时有效
|
||||
emptyViewCenter: {
|
||||
type: Boolean,
|
||||
default: u.gc('emptyViewCenter', true)
|
||||
},
|
||||
// 加载中时是否自动隐藏空数据图,默认为是
|
||||
autoHideEmptyViewWhenLoading: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoHideEmptyViewWhenLoading', true)
|
||||
},
|
||||
// 用户下拉列表触发下拉刷新加载中时是否自动隐藏空数据图,默认为是
|
||||
autoHideEmptyViewWhenPull: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoHideEmptyViewWhenPull', true)
|
||||
},
|
||||
// 空数据view的z-index,默认为9
|
||||
emptyViewZIndex: {
|
||||
type: Number,
|
||||
default: u.gc('emptyViewZIndex', 9)
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
customerEmptyViewErrorText: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
finalEmptyViewImg() {
|
||||
return this.isLoadFailed ? this.emptyViewErrorImg : this.emptyViewImg;
|
||||
},
|
||||
finalShowEmptyViewReload() {
|
||||
return this.isLoadFailed ? this.showEmptyViewReloadWhenError : this.showEmptyViewReload;
|
||||
},
|
||||
// 是否展示空数据图
|
||||
showEmpty() {
|
||||
if (this.refresherOnly || this.hideEmptyView || this.realTotalData.length) return false;
|
||||
if (this.autoHideEmptyViewWhenLoading) {
|
||||
if (this.isAddedData && !this.firstPageLoaded && !this.loading) return true;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return !this.autoHideEmptyViewWhenPull && !this.isUserReload;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 点击了空数据view重新加载按钮
|
||||
_emptyViewReload() {
|
||||
let callbacked = false;
|
||||
this.$emit('emptyViewReload', reload => {
|
||||
if (reload === undefined || reload === true) {
|
||||
this.fromEmptyViewReload = true;
|
||||
this.reload().catch(() => {});
|
||||
}
|
||||
callbacked = true;
|
||||
});
|
||||
// 如果用户没有禁止默认的点击重新加载刷新列表事件,则触发列表重新刷新
|
||||
this.$nextTick(() => {
|
||||
if (!callbacked) {
|
||||
this.fromEmptyViewReload = true;
|
||||
this.reload().catch(() => {});
|
||||
}
|
||||
})
|
||||
},
|
||||
// 点击了空数据view
|
||||
_emptyViewClick() {
|
||||
this.$emit('emptyViewClick');
|
||||
},
|
||||
}
|
||||
}
|
||||
113
uni_modules/z-paging/components/z-paging/js/modules/i18n.js
Normal file
113
uni_modules/z-paging/components/z-paging/js/modules/i18n.js
Normal file
@@ -0,0 +1,113 @@
|
||||
// [z-paging]i18n模块
|
||||
import { initVueI18n } from '@dcloudio/uni-i18n'
|
||||
import messages from '../../i18n/index.js'
|
||||
const { t } = initVueI18n(messages)
|
||||
|
||||
import u from '.././z-paging-utils'
|
||||
import c from '.././z-paging-constant'
|
||||
import interceptor from '../z-paging-interceptor'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
finalLanguage() {
|
||||
try {
|
||||
const local = uni.getLocale();
|
||||
const language = this.systemInfo.appLanguage;
|
||||
return local === 'auto' ? interceptor._handleLanguage2Local(language, this._language2Local(language)) : local;
|
||||
} catch (e) {
|
||||
// 如果获取系统本地语言异常,则默认返回中文,uni.getLocale在部分低版本HX或者cli中可能报找不到的问题
|
||||
return 'zh-Hans';
|
||||
}
|
||||
},
|
||||
// 最终的下拉刷新默认状态的文字
|
||||
finalRefresherDefaultText() {
|
||||
return this._getI18nText('zp.refresher.default', this.refresherDefaultText);
|
||||
},
|
||||
// 最终的下拉刷新下拉中的文字
|
||||
finalRefresherPullingText() {
|
||||
return this._getI18nText('zp.refresher.pulling', this.refresherPullingText);
|
||||
},
|
||||
// 最终的下拉刷新中文字
|
||||
finalRefresherRefreshingText() {
|
||||
return this._getI18nText('zp.refresher.refreshing', this.refresherRefreshingText);
|
||||
},
|
||||
// 最终的下拉刷新完成文字
|
||||
finalRefresherCompleteText() {
|
||||
return this._getI18nText('zp.refresher.complete', this.refresherCompleteText);
|
||||
},
|
||||
// 最终的下拉刷新上次更新时间文字
|
||||
finalRefresherUpdateTimeTextMap() {
|
||||
return {
|
||||
title: t('zp.refresherUpdateTime.title'),
|
||||
none: t('zp.refresherUpdateTime.none'),
|
||||
today: t('zp.refresherUpdateTime.today'),
|
||||
yesterday: t('zp.refresherUpdateTime.yesterday')
|
||||
};
|
||||
},
|
||||
// 最终的继续下拉进入二楼文字
|
||||
finalRefresherGoF2Text() {
|
||||
return this._getI18nText('zp.refresher.f2', this.refresherGoF2Text);
|
||||
},
|
||||
// 最终的底部加载更多默认状态文字
|
||||
finalLoadingMoreDefaultText() {
|
||||
return this._getI18nText('zp.loadingMore.default', this.loadingMoreDefaultText);
|
||||
},
|
||||
// 最终的底部加载更多加载中文字
|
||||
finalLoadingMoreLoadingText() {
|
||||
return this._getI18nText('zp.loadingMore.loading', this.loadingMoreLoadingText);
|
||||
},
|
||||
// 最终的底部加载更多没有更多数据文字
|
||||
finalLoadingMoreNoMoreText() {
|
||||
return this._getI18nText('zp.loadingMore.noMore', this.loadingMoreNoMoreText);
|
||||
},
|
||||
// 最终的底部加载更多加载失败文字
|
||||
finalLoadingMoreFailText() {
|
||||
return this._getI18nText('zp.loadingMore.fail', this.loadingMoreFailText);
|
||||
},
|
||||
// 最终的空数据图title
|
||||
finalEmptyViewText() {
|
||||
return this.isLoadFailed ? this.finalEmptyViewErrorText : this._getI18nText('zp.emptyView.title', this.emptyViewText);
|
||||
},
|
||||
// 最终的空数据图reload title
|
||||
finalEmptyViewReloadText() {
|
||||
return this._getI18nText('zp.emptyView.reload', this.emptyViewReloadText);
|
||||
},
|
||||
// 最终的空数据图加载失败文字
|
||||
finalEmptyViewErrorText() {
|
||||
return this.customerEmptyViewErrorText || this._getI18nText('zp.emptyView.error', this.emptyViewErrorText);
|
||||
},
|
||||
// 最终的系统loading title
|
||||
finalSystemLoadingText() {
|
||||
return this._getI18nText('zp.systemLoading.title', this.systemLoadingText);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 获取当前z-paging的语言
|
||||
getLanguage() {
|
||||
return this.finalLanguage;
|
||||
},
|
||||
// 获取国际化转换后的文本
|
||||
_getI18nText(key, value) {
|
||||
const dataType = Object.prototype.toString.call(value);
|
||||
if (dataType === '[object Object]') {
|
||||
const nextValue = value[this.finalLanguage];
|
||||
if (nextValue) return nextValue;
|
||||
} else if (dataType === '[object String]') {
|
||||
return value;
|
||||
}
|
||||
return t(key);
|
||||
},
|
||||
// 系统language转i18n local
|
||||
_language2Local(language) {
|
||||
const formatedLanguage = language.toLowerCase().replace(new RegExp('_', ''), '-');
|
||||
if (formatedLanguage.indexOf('zh') !== -1) {
|
||||
if (formatedLanguage === 'zh' || formatedLanguage === 'zh-cn' || formatedLanguage.indexOf('zh-hans') !== -1) {
|
||||
return 'zh-Hans';
|
||||
}
|
||||
return 'zh-Hant';
|
||||
}
|
||||
if (formatedLanguage.indexOf('en') !== -1) return 'en';
|
||||
return language;
|
||||
}
|
||||
}
|
||||
}
|
||||
374
uni_modules/z-paging/components/z-paging/js/modules/load-more.js
Normal file
374
uni_modules/z-paging/components/z-paging/js/modules/load-more.js
Normal file
@@ -0,0 +1,374 @@
|
||||
// [z-paging]滚动到底部加载更多模块
|
||||
import u from '.././z-paging-utils'
|
||||
import Enum from '.././z-paging-enum'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 自定义底部加载更多样式
|
||||
loadingMoreCustomStyle: {
|
||||
type: Object,
|
||||
default: u.gc('loadingMoreCustomStyle', {})
|
||||
},
|
||||
// 自定义底部加载更多文字样式
|
||||
loadingMoreTitleCustomStyle: {
|
||||
type: Object,
|
||||
default: u.gc('loadingMoreTitleCustomStyle', {})
|
||||
},
|
||||
// 自定义底部加载更多加载中动画样式
|
||||
loadingMoreLoadingIconCustomStyle: {
|
||||
type: Object,
|
||||
default: u.gc('loadingMoreLoadingIconCustomStyle', {})
|
||||
},
|
||||
// 自定义底部加载更多加载中动画图标类型,可选flower或circle,默认为flower
|
||||
loadingMoreLoadingIconType: {
|
||||
type: String,
|
||||
default: u.gc('loadingMoreLoadingIconType', 'flower')
|
||||
},
|
||||
// 自定义底部加载更多加载中动画图标图片
|
||||
loadingMoreLoadingIconCustomImage: {
|
||||
type: String,
|
||||
default: u.gc('loadingMoreLoadingIconCustomImage', '')
|
||||
},
|
||||
// 底部加载更多加载中view是否展示旋转动画,默认为是
|
||||
loadingMoreLoadingAnimated: {
|
||||
type: Boolean,
|
||||
default: u.gc('loadingMoreLoadingAnimated', true)
|
||||
},
|
||||
// 是否启用加载更多数据(含滑动到底部加载更多数据和点击加载更多数据),默认为是
|
||||
loadingMoreEnabled: {
|
||||
type: Boolean,
|
||||
default: u.gc('loadingMoreEnabled', true)
|
||||
},
|
||||
// 是否启用滑动到底部加载更多数据,默认为是
|
||||
toBottomLoadingMoreEnabled: {
|
||||
type: Boolean,
|
||||
default: u.gc('toBottomLoadingMoreEnabled', true)
|
||||
},
|
||||
// 滑动到底部状态为默认状态时,以加载中的状态展示,默认为否。若设置为是,可避免滚动到底部看到默认状态然后立刻变为加载中状态的问题,但分页数量未超过一屏时,不会显示【点击加载更多】
|
||||
loadingMoreDefaultAsLoading: {
|
||||
type: Boolean,
|
||||
default: u.gc('loadingMoreDefaultAsLoading', false)
|
||||
},
|
||||
// 滑动到底部"默认"文字,默认为【点击加载更多】
|
||||
loadingMoreDefaultText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('loadingMoreDefaultText', null)
|
||||
},
|
||||
// 滑动到底部"加载中"文字,默认为【正在加载...】
|
||||
loadingMoreLoadingText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('loadingMoreLoadingText', null)
|
||||
},
|
||||
// 滑动到底部"没有更多"文字,默认为【没有更多了】
|
||||
loadingMoreNoMoreText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('loadingMoreNoMoreText', null)
|
||||
},
|
||||
// 滑动到底部"加载失败"文字,默认为【加载失败,点击重新加载】
|
||||
loadingMoreFailText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('loadingMoreFailText', null)
|
||||
},
|
||||
// 当没有更多数据且分页内容未超出z-paging时是否隐藏没有更多数据的view,默认为否
|
||||
hideNoMoreInside: {
|
||||
type: Boolean,
|
||||
default: u.gc('hideNoMoreInside', false)
|
||||
},
|
||||
// 当没有更多数据且分页数组长度少于这个值时,隐藏没有更多数据的view,默认为0,代表不限制。
|
||||
hideNoMoreByLimit: {
|
||||
type: Number,
|
||||
default: u.gc('hideNoMoreByLimit', 0)
|
||||
},
|
||||
// 是否显示默认的加载更多text,默认为是
|
||||
showDefaultLoadingMoreText: {
|
||||
type: Boolean,
|
||||
default: u.gc('showDefaultLoadingMoreText', true)
|
||||
},
|
||||
// 是否显示没有更多数据的view
|
||||
showLoadingMoreNoMoreView: {
|
||||
type: Boolean,
|
||||
default: u.gc('showLoadingMoreNoMoreView', true)
|
||||
},
|
||||
// 是否显示没有更多数据的分割线,默认为是
|
||||
showLoadingMoreNoMoreLine: {
|
||||
type: Boolean,
|
||||
default: u.gc('showLoadingMoreNoMoreLine', true)
|
||||
},
|
||||
// 自定义底部没有更多数据的分割线样式
|
||||
loadingMoreNoMoreLineCustomStyle: {
|
||||
type: Object,
|
||||
default: u.gc('loadingMoreNoMoreLineCustomStyle', {})
|
||||
},
|
||||
// 当分页未满一屏时,是否自动加载更多,默认为否(nvue无效)
|
||||
insideMore: {
|
||||
type: Boolean,
|
||||
default: u.gc('insideMore', false)
|
||||
},
|
||||
// 距底部/右边多远时(单位px),触发 scrolltolower 事件,默认为100rpx
|
||||
lowerThreshold: {
|
||||
type: [Number, String],
|
||||
default: u.gc('lowerThreshold', '100rpx')
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
M: Enum.More,
|
||||
// 底部加载更多状态
|
||||
loadingStatus: Enum.More.Default,
|
||||
// 在渲染之后的底部加载更多状态
|
||||
loadingStatusAfterRender: Enum.More.Default,
|
||||
// 底部加载更多时间戳
|
||||
loadingMoreTimeStamp: 0,
|
||||
// 底部加载更多slot
|
||||
loadingMoreDefaultSlot: null,
|
||||
// 是否展示底部加载更多
|
||||
showLoadingMore: false,
|
||||
// 是否是开发者自定义的加载更多,-1代表交由z-paging自行判断;1代表没有更多了;0代表还有更多数据
|
||||
customNoMore: -1,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 底部加载更多配置
|
||||
zLoadMoreConfig() {
|
||||
return {
|
||||
status: this.loadingStatusAfterRender,
|
||||
defaultAsLoading: this.loadingMoreDefaultAsLoading || (this.useChatRecordMode && this.chatLoadingMoreDefaultAsLoading),
|
||||
defaultThemeStyle: this.finalLoadingMoreThemeStyle,
|
||||
customStyle: this.loadingMoreCustomStyle,
|
||||
titleCustomStyle: this.loadingMoreTitleCustomStyle,
|
||||
iconCustomStyle: this.loadingMoreLoadingIconCustomStyle,
|
||||
loadingIconType: this.loadingMoreLoadingIconType,
|
||||
loadingIconCustomImage: this.loadingMoreLoadingIconCustomImage,
|
||||
loadingAnimated: this.loadingMoreLoadingAnimated,
|
||||
showNoMoreLine: this.showLoadingMoreNoMoreLine,
|
||||
noMoreLineCustomStyle: this.loadingMoreNoMoreLineCustomStyle,
|
||||
defaultText: this.finalLoadingMoreDefaultText,
|
||||
loadingText: this.finalLoadingMoreLoadingText,
|
||||
noMoreText: this.finalLoadingMoreNoMoreText,
|
||||
failText: this.finalLoadingMoreFailText,
|
||||
hideContent: !this.loadingMoreDefaultAsLoading && this.listRendering,
|
||||
unit: this.unit,
|
||||
isChat: this.useChatRecordMode,
|
||||
chatDefaultAsLoading: this.chatLoadingMoreDefaultAsLoading
|
||||
};
|
||||
},
|
||||
// 最终的底部加载更多主题
|
||||
finalLoadingMoreThemeStyle() {
|
||||
return this.loadingMoreThemeStyle.length ? this.loadingMoreThemeStyle : this.defaultThemeStyle;
|
||||
},
|
||||
// 最终的底部加载更多触发阈值
|
||||
finalLowerThreshold() {
|
||||
return u.convertToPx(this.lowerThreshold);
|
||||
},
|
||||
// 是否显示默认状态下的底部加载更多
|
||||
showLoadingMoreDefault() {
|
||||
return this._showLoadingMore('Default');
|
||||
},
|
||||
// 是否显示加载中状态下的底部加载更多
|
||||
showLoadingMoreLoading() {
|
||||
return this._showLoadingMore('Loading');
|
||||
},
|
||||
// 是否显示没有更多了状态下的底部加载更多
|
||||
showLoadingMoreNoMore() {
|
||||
return this._showLoadingMore('NoMore');
|
||||
},
|
||||
// 是否显示加载失败状态下的底部加载更多
|
||||
showLoadingMoreFail() {
|
||||
return this._showLoadingMore('Fail');
|
||||
},
|
||||
// 是否显示自定义状态下的底部加载更多
|
||||
showLoadingMoreCustom() {
|
||||
return this._showLoadingMore('Custom');
|
||||
},
|
||||
// 底部加载更多固定高度
|
||||
loadingMoreFixedHeight() {
|
||||
return u.addUnit('80rpx', this.unit);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 页面滚动到底部时通知z-paging进行进一步处理
|
||||
pageReachBottom() {
|
||||
!this.useChatRecordMode && this.toBottomLoadingMoreEnabled && this._onLoadingMore('toBottom');
|
||||
},
|
||||
// 手动触发上拉加载更多(非必须,可依据具体需求使用)
|
||||
doLoadMore(type) {
|
||||
this._onLoadingMore(type);
|
||||
},
|
||||
// 通过@scroll事件检测是否滚动到了底部(顺带检测下是否滚动到了顶部)
|
||||
_checkScrolledToBottom(scrollDiff, checked = false) {
|
||||
// 如果当前scroll-view高度未获取,则获取其高度
|
||||
if (this.cacheScrollNodeHeight === -1) {
|
||||
// 获取当前scroll-view高度
|
||||
this._getNodeClientRect('.zp-scroll-view').then((res) => {
|
||||
if (res) {
|
||||
const scrollNodeHeight = res[0].height;
|
||||
// 缓存当前scroll-view高度,如果获取过了不再获取
|
||||
this.cacheScrollNodeHeight = scrollNodeHeight;
|
||||
// // scrollDiff - this.cacheScrollNodeHeight = 当前滚动区域的顶部与内容底部的距离 - scroll-view高度 = 当前滚动区域的底部与内容底部的距离(也就是最终的与底部的距离)
|
||||
if (scrollDiff - scrollNodeHeight <= this.finalLowerThreshold) {
|
||||
// 如果与底部的距离小于阈值,则判断为滚动到了底部,触发滚动到底部事件
|
||||
this._onLoadingMore('toBottom');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// scrollDiff - this.cacheScrollNodeHeight = 当前滚动区域的顶部与内容底部的距离 - scroll-view高度 = 当前滚动区域的底部与内容底部的距离(也就是最终的与底部的距离)
|
||||
if (scrollDiff - this.cacheScrollNodeHeight <= this.finalLowerThreshold) {
|
||||
// 如果与底部的距离小于阈值,则判断为滚动到了底部,触发滚动到底部事件
|
||||
this._onLoadingMore('toBottom');
|
||||
} else if (scrollDiff - this.cacheScrollNodeHeight <= 500 && !checked) {
|
||||
// 如果与底部的距离小于500px,则获取当前滚动的位置,延迟150毫秒重复上述步骤再次检测(避免@scroll触发时获取的scrollTop不正确导致的其他问题,此时获取的scrollTop不一定可信)。防止因为部分性能较差安卓设备@scroll采样率过低导致的滚动到底部但是依然没有触发的问题
|
||||
u.delay(() => {
|
||||
this._getNodeClientRect('.zp-scroll-view', true, true).then((res) => {
|
||||
if (res) {
|
||||
this.oldScrollTop = res[0].scrollTop;
|
||||
const newScrollDiff = res[0].scrollHeight - this.oldScrollTop;
|
||||
this._checkScrolledToBottom(newScrollDiff, true);
|
||||
}
|
||||
})
|
||||
}, 150, 'checkScrolledToBottomDelay')
|
||||
}
|
||||
// 检测一下是否已经滚动到了顶部了,因为在安卓中滚动到顶部时scrollTop不一定为0(和滚动到底部一样的原因),所以需要在scrollTop小于150px时,通过获取.zp-scroll-view的scrollTop再判断一下
|
||||
if (this.oldScrollTop <= 150 && this.oldScrollTop !== 0) {
|
||||
u.delay(() => {
|
||||
// 这里再判断一下是否确实已经滚动到顶部了,如果已经滚动到顶部了,则不用再判断了,再次判断的原因是可能150毫秒之后oldScrollTop才是0
|
||||
if (this.oldScrollTop !== 0) {
|
||||
this._getNodeClientRect('.zp-scroll-view', true, true).then((res) => {
|
||||
// 如果150毫秒后.zp-scroll-view的scrollTop为0,则认为已经滚动到了顶部了
|
||||
if (res && res[0].scrollTop === 0 && this.oldScrollTop !== 0) {
|
||||
this._onScrollToUpper();
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 150, 'checkScrolledToTopDelay')
|
||||
}
|
||||
}
|
||||
},
|
||||
// 触发加载更多时调用,from:toBottom-滑动到底部触发;click-点击加载更多触发
|
||||
_onLoadingMore(from = 'click') {
|
||||
// 如果是ios并且是滚动到底部的,则在滚动到底部时候尝试将列表设置为禁止滚动然后设置为允许滚动,以禁止底部bounce的效果
|
||||
if (this.isIos && from === 'toBottom' && !this.scrollToBottomBounceEnabled && this.scrollEnable) {
|
||||
this.scrollEnable = false;
|
||||
this.$nextTick(() => {
|
||||
this.scrollEnable = true;
|
||||
})
|
||||
}
|
||||
// emit scrolltolower
|
||||
this._emitScrollEvent('scrolltolower');
|
||||
// 如果是只使用下拉刷新 或者 禁用底部加载更多 或者 底部加载更多不是默认状态或加载失败状态 或者 是加载中状态 或者 空数据图已经展示了,则return,不触发内部加载更多逻辑
|
||||
if (this.refresherOnly || !this.loadingMoreEnabled || !(this.loadingStatus === Enum.More.Default || this.loadingStatus === Enum.More.Fail) || this.loading || this.showEmpty) return;
|
||||
// #ifdef MP-WEIXIN
|
||||
if (!this.isIos && !this.refresherOnly && !this.usePageScroll) {
|
||||
const currentTimestamp = u.getTime();
|
||||
// 在非ios平台+scroll-view中节流处理
|
||||
if (this.loadingMoreTimeStamp > 0 && currentTimestamp - this.loadingMoreTimeStamp < 100) {
|
||||
this.loadingMoreTimeStamp = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
// 处理加载更多数据
|
||||
this._doLoadingMore();
|
||||
},
|
||||
// 处理开始加载更多
|
||||
_doLoadingMore() {
|
||||
if (this.pageNo >= this.defaultPageNo && this.loadingStatus !== Enum.More.NoMore) {
|
||||
this.pageNo ++;
|
||||
this._startLoading(false);
|
||||
if (this.isLocalPaging) {
|
||||
// 如果是本地分页,则在组件内部对数据进行分页处理,不触发@query事件
|
||||
this._localPagingQueryList(this.pageNo, this.defaultPageSize, this.localPagingLoadingTime, res => {
|
||||
this.completeByTotal(res, this.totalLocalPagingList.length);
|
||||
this.queryFrom = Enum.QueryFrom.LoadMore;
|
||||
})
|
||||
} else {
|
||||
// emit @query相关加载更多事件
|
||||
this._emitQuery(this.pageNo, this.defaultPageSize, Enum.QueryFrom.LoadMore);
|
||||
this._callMyParentQuery();
|
||||
}
|
||||
// 设置当前加载状态为底部加载更多状态
|
||||
this.loadingType = Enum.LoadingType.LoadMore;
|
||||
}
|
||||
},
|
||||
// (预处理)判断当没有更多数据且分页内容未超出z-paging时是否显示没有更多数据的view
|
||||
_preCheckShowNoMoreInside(newVal, scrollViewNode, pagingContainerNode) {
|
||||
if (this.loadingStatus === Enum.More.NoMore && this.hideNoMoreByLimit > 0 && newVal.length) {
|
||||
this.showLoadingMore = newVal.length > this.hideNoMoreByLimit;
|
||||
} else if ((this.loadingStatus === Enum.More.NoMore && this.hideNoMoreInside && newVal.length) || (this.insideMore && this.insideOfPaging !== false && newVal.length)) {
|
||||
this.$nextTick(() => {
|
||||
this._checkShowNoMoreInside(newVal, scrollViewNode, pagingContainerNode);
|
||||
})
|
||||
if (this.insideMore && this.insideOfPaging !== false && newVal.length) {
|
||||
this.showLoadingMore = newVal.length;
|
||||
}
|
||||
} else {
|
||||
this.showLoadingMore = newVal.length;
|
||||
}
|
||||
},
|
||||
// 判断当没有更多数据且分页内容未超出z-paging时是否显示没有更多数据的view
|
||||
async _checkShowNoMoreInside(totalData, oldScrollViewNode, oldPagingContainerNode) {
|
||||
try {
|
||||
const scrollViewNode = oldScrollViewNode || await this._getNodeClientRect('.zp-scroll-view');
|
||||
// 在页面滚动模式下
|
||||
if (this.usePageScroll) {
|
||||
if (scrollViewNode) {
|
||||
// 获取滚动内容总高度
|
||||
const scrollViewTotalH = scrollViewNode[0].top + scrollViewNode[0].height;
|
||||
// 如果滚动内容总高度小于窗口高度,则认为内容未超出z-paging
|
||||
this.insideOfPaging = scrollViewTotalH < this.windowHeight;
|
||||
// 如果需要没有更多数据时,隐藏底部加载更多view,并且内容未超过z-paging,则隐藏底部加载更多
|
||||
if (this.hideNoMoreInside) {
|
||||
this.showLoadingMore = !this.insideOfPaging;
|
||||
}
|
||||
// 如果需要内容未超过z-paging时自动加载更多,则触发加载更多
|
||||
this._updateInsideOfPaging();
|
||||
}
|
||||
} else {
|
||||
// 在scroll-view滚动模式下
|
||||
const pagingContainerNode = oldPagingContainerNode || await this._getNodeClientRect('.zp-paging-container-content');
|
||||
// 获取滚动内容总高度
|
||||
const pagingContainerH = pagingContainerNode ? pagingContainerNode[0].height : 0;
|
||||
// 获取z-paging内置scroll-view高度
|
||||
const scrollViewH = scrollViewNode ? scrollViewNode[0].height : 0;
|
||||
// 如果滚动内容总高度小于z-paging内置scroll-view高度,则认为内容未超出z-paging
|
||||
this.insideOfPaging = pagingContainerH < scrollViewH;
|
||||
if (this.hideNoMoreInside) {
|
||||
this.showLoadingMore = !this.insideOfPaging;
|
||||
}
|
||||
// 如果需要内容未超过z-paging时自动加载更多,则触发加载更多
|
||||
this._updateInsideOfPaging();
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果发生了异常,判断totalData数组长度为0,则认为内容未超出z-paging
|
||||
this.insideOfPaging = !totalData.length;
|
||||
if (this.hideNoMoreInside) {
|
||||
this.showLoadingMore = !this.insideOfPaging;
|
||||
}
|
||||
// 如果需要内容未超过z-paging时自动加载更多,则触发加载更多
|
||||
this._updateInsideOfPaging();
|
||||
}
|
||||
},
|
||||
// 是否要展示上拉加载更多view
|
||||
_showLoadingMore(type) {
|
||||
if (!this.showLoadingMoreWhenReload && (!(this.loadingStatus === Enum.More.Default ? this.nShowBottom : true) || !this.realTotalData.length)) return false;
|
||||
if (((!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading) && !this.showLoadingMore) ||
|
||||
(!this.loadingMoreEnabled && (!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading)) || this.refresherOnly) {
|
||||
return false;
|
||||
}
|
||||
if (this.useChatRecordMode && type !== 'Loading') return false;
|
||||
if (!this.zSlots) return false;
|
||||
if (type === 'Custom') {
|
||||
return this.showDefaultLoadingMoreText && !(this.loadingStatus === Enum.More.NoMore && !this.showLoadingMoreNoMoreView);
|
||||
}
|
||||
const res = this.loadingStatus === Enum.More[type] && this.zSlots[`loadingMore${type}`] && (type === 'NoMore' ? this.showLoadingMoreNoMoreView : true);
|
||||
if (res) {
|
||||
// #ifdef APP-NVUE
|
||||
if (!this.isIos) {
|
||||
this.nLoadingMoreFixedHeight = false;
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
return res;
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// [z-paging]loading相关模块
|
||||
import u from '.././z-paging-utils'
|
||||
import Enum from '.././z-paging-enum'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 第一次加载后自动隐藏loading slot,默认为是
|
||||
autoHideLoadingAfterFirstLoaded: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoHideLoadingAfterFirstLoaded', true)
|
||||
},
|
||||
// loading slot是否铺满屏幕并固定,默认为否
|
||||
loadingFullFixed: {
|
||||
type: Boolean,
|
||||
default: u.gc('loadingFullFixed', false)
|
||||
},
|
||||
// 是否自动显示系统Loading:即uni.showLoading,若开启则将在刷新列表时(调用reload、refresh时)显示,下拉刷新和滚动到底部加载更多不会显示,默认为false。
|
||||
autoShowSystemLoading: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoShowSystemLoading', false)
|
||||
},
|
||||
// 显示系统Loading时是否显示透明蒙层,防止触摸穿透,默认为是(H5、App、微信小程序、百度小程序有效)
|
||||
systemLoadingMask: {
|
||||
type: Boolean,
|
||||
default: u.gc('systemLoadingMask', true)
|
||||
},
|
||||
// 显示系统Loading时显示的文字,默认为"加载中"
|
||||
systemLoadingText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('systemLoadingText', null)
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
loadingForNow: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// loading状态
|
||||
loadingStatus(newVal) {
|
||||
this.$emit('loadingStatusChange', newVal);
|
||||
this.$nextTick(() => {
|
||||
this.loadingStatusAfterRender = newVal;
|
||||
})
|
||||
if (this.useChatRecordMode) {
|
||||
if (this.isFirstPage && (newVal === Enum.More.NoMore || newVal === Enum.More.Fail)) {
|
||||
this.isFirstPageAndNoMore = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.isFirstPageAndNoMore = false;
|
||||
},
|
||||
loading(newVal){
|
||||
if (newVal) {
|
||||
this.loadingForNow = newVal;
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// 是否显示loading
|
||||
showLoading() {
|
||||
if (this.firstPageLoaded || !this.loading || !this.loadingForNow) return false;
|
||||
if (this.finalShowSystemLoading) {
|
||||
// 显示系统loading
|
||||
uni.showLoading({
|
||||
title: this.finalSystemLoadingText,
|
||||
mask: this.systemLoadingMask
|
||||
})
|
||||
}
|
||||
return this.autoHideLoadingAfterFirstLoaded ? (this.fromEmptyViewReload ? true : !this.pagingLoaded) : this.loadingType === Enum.LoadingType.Refresher;
|
||||
},
|
||||
// 最终的是否显示系统loading
|
||||
finalShowSystemLoading() {
|
||||
return this.autoShowSystemLoading && this.loadingType === Enum.LoadingType.Refresher;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 处理开始加载更多状态
|
||||
_startLoading(isReload = false) {
|
||||
if ((this.showLoadingMoreWhenReload && !this.isUserPullDown) || !isReload) {
|
||||
this.loadingStatus = Enum.More.Loading;
|
||||
}
|
||||
this.loading = true;
|
||||
},
|
||||
// 停止系统loading和refresh
|
||||
_endSystemLoadingAndRefresh(){
|
||||
this.finalShowSystemLoading && uni.hideLoading();
|
||||
!this.useCustomRefresher && uni.stopPullDownRefresh();
|
||||
// #ifdef APP-NVUE
|
||||
this.usePageScroll && uni.stopPullDownRefresh();
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}
|
||||
268
uni_modules/z-paging/components/z-paging/js/modules/nvue.js
Normal file
268
uni_modules/z-paging/components/z-paging/js/modules/nvue.js
Normal file
@@ -0,0 +1,268 @@
|
||||
// [z-paging]nvue独有部分模块
|
||||
import u from '.././z-paging-utils'
|
||||
import c from '.././z-paging-constant'
|
||||
import Enum from '.././z-paging-enum'
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
const weexAnimation = weex.requireModule('animation');
|
||||
// #endif
|
||||
export default {
|
||||
props: {
|
||||
// #ifdef APP-NVUE
|
||||
// nvue中修改列表类型,可选值有list、waterfall和scroller,默认为list
|
||||
nvueListIs: {
|
||||
type: String,
|
||||
default: u.gc('nvueListIs', 'list')
|
||||
},
|
||||
// nvue waterfall配置,仅在nvue中且nvueListIs=waterfall时有效,配置参数详情参见:https://uniapp.dcloud.io/component/waterfall
|
||||
nvueWaterfallConfig: {
|
||||
type: Object,
|
||||
default: u.gc('nvueWaterfallConfig', {})
|
||||
},
|
||||
// nvue 控制是否回弹效果,iOS不支持动态修改
|
||||
nvueBounce: {
|
||||
type: Boolean,
|
||||
default: u.gc('nvueBounce', true)
|
||||
},
|
||||
// nvue中通过代码滚动到顶部/底部时,是否加快动画效果(无滚动动画时无效),默认为否
|
||||
nvueFastScroll: {
|
||||
type: Boolean,
|
||||
default: u.gc('nvueFastScroll', false)
|
||||
},
|
||||
// nvue中list的id
|
||||
nvueListId: {
|
||||
type: String,
|
||||
default: u.gc('nvueListId', '')
|
||||
},
|
||||
// nvue中refresh组件的样式
|
||||
nvueRefresherStyle: {
|
||||
type: Object,
|
||||
default: u.gc('nvueRefresherStyle', {})
|
||||
},
|
||||
// nvue中是否按分页模式(类似竖向swiper)显示List,默认为false
|
||||
nvuePagingEnabled: {
|
||||
type: Boolean,
|
||||
default: u.gc('nvuePagingEnabled', false)
|
||||
},
|
||||
// 是否隐藏nvue列表底部的tagView,此view用于标识滚动到底部位置,若隐藏则滚动到底部功能将失效,在nvue中实现吸顶+swiper功能时需将最外层z-paging的此属性设置为true。默认为否
|
||||
hideNvueBottomTag: {
|
||||
type: Boolean,
|
||||
default: u.gc('hideNvueBottomTag', false)
|
||||
},
|
||||
// nvue中控制onscroll事件触发的频率:表示两次onscroll事件之间列表至少滚动了10px。注意,将该值设置为较小的数值会提高滚动事件采样的精度,但同时也会降低页面的性能
|
||||
offsetAccuracy: {
|
||||
type: Number,
|
||||
default: u.gc('offsetAccuracy', 10)
|
||||
},
|
||||
// #endif
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
nRefresherLoading: false,
|
||||
nListIsDragging: false,
|
||||
nShowBottom: true,
|
||||
nFixFreezing: false,
|
||||
nShowRefresherReveal: false,
|
||||
nLoadingMoreFixedHeight: false,
|
||||
nShowRefresherRevealHeight: 0,
|
||||
nOldShowRefresherRevealHeight: -1,
|
||||
nRefresherWidth: u.rpx2px(750),
|
||||
nF2Opacity: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// #ifdef APP-NVUE
|
||||
nScopedSlots() {
|
||||
// #ifdef VUE2
|
||||
return this.$scopedSlots;
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
return null;
|
||||
// #endif
|
||||
},
|
||||
nWaterfallColumnCount() {
|
||||
if (this.finalNvueListIs !== 'waterfall') return 0;
|
||||
return this._nGetWaterfallConfig('column-count', 2);
|
||||
},
|
||||
nWaterfallColumnWidth() {
|
||||
return this._nGetWaterfallConfig('column-width', 'auto');
|
||||
},
|
||||
nWaterfallColumnGap() {
|
||||
return this._nGetWaterfallConfig('column-gap', 'normal');
|
||||
},
|
||||
nWaterfallLeftGap() {
|
||||
return this._nGetWaterfallConfig('left-gap', 0);
|
||||
},
|
||||
nWaterfallRightGap() {
|
||||
return this._nGetWaterfallConfig('right-gap', 0);
|
||||
},
|
||||
nViewIs() {
|
||||
const is = this.finalNvueListIs;
|
||||
return is === 'scroller' || is === 'view' ? 'view' : is === 'waterfall' ? 'header' : 'cell';
|
||||
},
|
||||
nSafeAreaBottomHeight() {
|
||||
return this.safeAreaInsetBottom ? this.safeAreaBottom : 0;
|
||||
},
|
||||
finalNvueListIs() {
|
||||
if (this.usePageScroll) return 'view';
|
||||
const nvueListIsLowerCase = this.nvueListIs.toLowerCase();
|
||||
if (['list','waterfall','scroller'].indexOf(nvueListIsLowerCase) !== -1) return nvueListIsLowerCase;
|
||||
return 'list';
|
||||
},
|
||||
finalNvueSuperListIs() {
|
||||
return this.usePageScroll ? 'view' : 'scroller';
|
||||
},
|
||||
finalNvueRefresherEnabled() {
|
||||
return this.finalNvueListIs !== 'view' && this.finalRefresherEnabled && !this.nShowRefresherReveal && !this.useChatRecordMode;
|
||||
},
|
||||
// #endif
|
||||
},
|
||||
mounted(){
|
||||
// #ifdef APP-NVUE
|
||||
//旋转屏幕时更新宽度
|
||||
uni.onWindowResize((res) => {
|
||||
// this._nUpdateRefresherWidth();
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
// #ifdef APP-NVUE
|
||||
// 列表滚动时触发
|
||||
_nOnScroll(e) {
|
||||
this.$emit('scroll', e);
|
||||
const contentOffsetY = -e.contentOffset.y;
|
||||
this.oldScrollTop = contentOffsetY;
|
||||
this.nListIsDragging = e.isDragging;
|
||||
this._checkShouldShowBackToTop(contentOffsetY, contentOffsetY - 1);
|
||||
},
|
||||
// 列表滚动结束
|
||||
_nOnScrollend(e) {
|
||||
this.$emit('scrollend', e);
|
||||
|
||||
// 判断是否滚动到顶部了
|
||||
if (e?.contentOffset?.y >= 0) {
|
||||
this._emitScrollEvent('scrolltoupper');
|
||||
}
|
||||
// 判断是否滚动到底部了
|
||||
this._getNodeClientRect('.zp-n-list').then(node => {
|
||||
if (node) {
|
||||
if (e?.contentSize?.height + e?.contentOffset?.y <= node[0].height) {
|
||||
this._emitScrollEvent('scrolltolower');
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
// 下拉刷新刷新中
|
||||
_nOnRrefresh() {
|
||||
if (this.nShowRefresherReveal) return;
|
||||
// 进入刷新状态
|
||||
this.nRefresherLoading = true;
|
||||
if (this.refresherStatus === Enum.Refresher.GoF2) {
|
||||
this._handleGoF2();
|
||||
this.$nextTick(() => {
|
||||
this._nRefresherEnd();
|
||||
})
|
||||
} else {
|
||||
this.refresherStatus = Enum.Refresher.Loading;
|
||||
this._doRefresherLoad();
|
||||
}
|
||||
|
||||
},
|
||||
// 下拉刷新下拉中
|
||||
_nOnPullingdown(e) {
|
||||
if (this.refresherStatus === Enum.Refresher.Loading || (this.isIos && !this.nListIsDragging)) return;
|
||||
this._emitTouchmove(e);
|
||||
let { viewHeight, pullingDistance } = e;
|
||||
// 更新下拉刷新状态
|
||||
// 下拉刷新距离超过阈值
|
||||
if (pullingDistance >= viewHeight) {
|
||||
// 如果开启了下拉进入二楼并且下拉刷新距离超过进入二楼阈值,则当前下拉刷新状态为松手进入二楼,否则为松手立即刷新
|
||||
// (pullingDistance - viewHeight) + this.finalRefresherThreshold 不等同于pullingDistance,此处是为了兼容不同平台下拉相同距离pullingDistance不一致的问题,pullingDistance仅与viewHeight互相关联
|
||||
this.refresherStatus = this.refresherF2Enabled && (pullingDistance - viewHeight) + this.finalRefresherThreshold >= this.finalRefresherF2Threshold ? Enum.Refresher.GoF2 : Enum.Refresher.ReleaseToRefresh;
|
||||
} else {
|
||||
// 下拉刷新距离未超过阈值,显示默认状态
|
||||
this.refresherStatus = Enum.Refresher.Default;
|
||||
}
|
||||
},
|
||||
// 下拉刷新结束
|
||||
_nRefresherEnd(doEnd = true) {
|
||||
if (doEnd) {
|
||||
this._nDoRefresherEndAnimation(0, -this.nShowRefresherRevealHeight);
|
||||
!this.usePageScroll && this.$refs['zp-n-list'].resetLoadmore();
|
||||
this.nRefresherLoading = false;
|
||||
}
|
||||
},
|
||||
// 执行主动触发下拉刷新动画
|
||||
_nDoRefresherEndAnimation(height, translateY, animate = true, checkStack = true) {
|
||||
// 清除下拉刷新相关timeout
|
||||
this._cleanRefresherCompleteTimeout();
|
||||
this._cleanRefresherEndTimeout();
|
||||
|
||||
if (!this.finalShowRefresherWhenReload) {
|
||||
// 如果reload不需要自动展示下拉刷新view,则在complete duration结束后再把下拉刷新状态设置回默认
|
||||
this.refresherEndTimeout = u.delay(() => {
|
||||
this.refresherStatus = Enum.Refresher.Default;
|
||||
}, this.refresherCompleteDuration);
|
||||
return;
|
||||
}
|
||||
// 用户处理用户在短时间内多次调用reload的情况,此时下拉刷新view不需要重复显示,只需要保证最后一次reload对应的请求结束后收回下拉刷新view即可
|
||||
const stackCount = this.refresherRevealStackCount;
|
||||
if (height === 0 && checkStack) {
|
||||
this.refresherRevealStackCount --;
|
||||
if (stackCount > 1) return;
|
||||
this.refresherEndTimeout = u.delay(() => {
|
||||
this.refresherStatus = Enum.Refresher.Default;
|
||||
}, this.refresherCompleteDuration);
|
||||
}
|
||||
if (stackCount > 1) {
|
||||
this.refresherStatus = Enum.Refresher.Loading;
|
||||
}
|
||||
|
||||
const duration = animate ? 200 : 0;
|
||||
if (this.nOldShowRefresherRevealHeight !== height) {
|
||||
if (height > 0) {
|
||||
this.nShowRefresherReveal = true;
|
||||
}
|
||||
// 展示下拉刷新view
|
||||
weexAnimation.transition(this.$refs['zp-n-list-refresher-reveal'], {
|
||||
styles: {
|
||||
height: `${height}px`,
|
||||
transform: `translateY(${translateY}px)`,
|
||||
},
|
||||
duration,
|
||||
timingFunction: 'linear',
|
||||
needLayout: true,
|
||||
delay: 0
|
||||
})
|
||||
}
|
||||
u.delay(() => {
|
||||
if (animate) {
|
||||
this.nShowRefresherReveal = height > 0;
|
||||
}
|
||||
}, duration > 0 ? duration - 60 : 0);
|
||||
this.nOldShowRefresherRevealHeight = height;
|
||||
},
|
||||
// 滚动到底部加载更多
|
||||
_nOnLoadmore() {
|
||||
if (this.nShowRefresherReveal || !this.totalData.length) return;
|
||||
this.useChatRecordMode ? this.doChatRecordLoadMore() : this._onLoadingMore('toBottom');
|
||||
},
|
||||
// 获取nvue waterfall单项配置
|
||||
_nGetWaterfallConfig(key, defaultValue) {
|
||||
return this.nvueWaterfallConfig[key] || defaultValue;
|
||||
},
|
||||
// 更新nvue 下拉刷新view容器的宽度
|
||||
_nUpdateRefresherWidth() {
|
||||
u.delay(() => {
|
||||
this.$nextTick(()=>{
|
||||
this._getNodeClientRect('.zp-n-list').then(node => {
|
||||
if (node) {
|
||||
this.nRefresherWidth = node[0].width || this.nRefresherWidth;
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
831
uni_modules/z-paging/components/z-paging/js/modules/refresher.js
Normal file
831
uni_modules/z-paging/components/z-paging/js/modules/refresher.js
Normal file
@@ -0,0 +1,831 @@
|
||||
// [z-paging]下拉刷新view模块
|
||||
import u from '.././z-paging-utils'
|
||||
import c from '.././z-paging-constant'
|
||||
import Enum from '.././z-paging-enum'
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
const weexAnimation = weex.requireModule('animation');
|
||||
// #endif
|
||||
export default {
|
||||
props: {
|
||||
// 下拉刷新的主题样式,支持black,white,默认black
|
||||
refresherThemeStyle: {
|
||||
type: String,
|
||||
default: u.gc('refresherThemeStyle', '')
|
||||
},
|
||||
// 自定义下拉刷新中左侧图标的样式
|
||||
refresherImgStyle: {
|
||||
type: Object,
|
||||
default: u.gc('refresherImgStyle', {})
|
||||
},
|
||||
// 自定义下拉刷新中右侧状态描述文字的样式
|
||||
refresherTitleStyle: {
|
||||
type: Object,
|
||||
default: u.gc('refresherTitleStyle', {})
|
||||
},
|
||||
// 自定义下拉刷新中右侧最后更新时间文字的样式(show-refresher-update-time为true时有效)
|
||||
refresherUpdateTimeStyle: {
|
||||
type: Object,
|
||||
default: u.gc('refresherUpdateTimeStyle', {})
|
||||
},
|
||||
// 在微信小程序和QQ小程序中,是否实时监听下拉刷新中进度,默认为否
|
||||
watchRefresherTouchmove: {
|
||||
type: Boolean,
|
||||
default: u.gc('watchRefresherTouchmove', false)
|
||||
},
|
||||
// 底部加载更多的主题样式,支持black,white,默认black
|
||||
loadingMoreThemeStyle: {
|
||||
type: String,
|
||||
default: u.gc('loadingMoreThemeStyle', '')
|
||||
},
|
||||
// 是否只使用下拉刷新,设置为true后将关闭mounted自动请求数据、关闭滚动到底部加载更多,强制隐藏空数据图。默认为否
|
||||
refresherOnly: {
|
||||
type: Boolean,
|
||||
default: u.gc('refresherOnly', false)
|
||||
},
|
||||
// 自定义下拉刷新默认状态下回弹动画时间,单位为毫秒,默认为100毫秒,nvue无效
|
||||
refresherDefaultDuration: {
|
||||
type: [Number, String],
|
||||
default: u.gc('refresherDefaultDuration', 100)
|
||||
},
|
||||
// 自定义下拉刷新结束以后延迟回弹的时间,单位为毫秒,默认为0
|
||||
refresherCompleteDelay: {
|
||||
type: [Number, String],
|
||||
default: u.gc('refresherCompleteDelay', 0)
|
||||
},
|
||||
// 自定义下拉刷新结束回弹动画时间,单位为毫秒,默认为300毫秒(refresherEndBounceEnabled为false时,refresherCompleteDuration为设定值的1/3),nvue无效
|
||||
refresherCompleteDuration: {
|
||||
type: [Number, String],
|
||||
default: u.gc('refresherCompleteDuration', 300)
|
||||
},
|
||||
// 自定义下拉刷新中是否允许列表滚动,默认为是
|
||||
refresherRefreshingScrollable: {
|
||||
type: Boolean,
|
||||
default: u.gc('refresherRefreshingScrollable', true)
|
||||
},
|
||||
// 自定义下拉刷新结束状态下是否允许列表滚动,默认为否
|
||||
refresherCompleteScrollable: {
|
||||
type: Boolean,
|
||||
default: u.gc('refresherCompleteScrollable', false)
|
||||
},
|
||||
// 是否使用自定义的下拉刷新,默认为是,即使用z-paging的下拉刷新。设置为false即代表使用uni scroll-view自带的下拉刷新,h5、App、微信小程序以外的平台不支持uni scroll-view自带的下拉刷新
|
||||
useCustomRefresher: {
|
||||
type: Boolean,
|
||||
default: u.gc('useCustomRefresher', true)
|
||||
},
|
||||
// 自定义下拉刷新下拉帧率,默认为40,过高可能会出现抖动问题
|
||||
refresherFps: {
|
||||
type: [Number, String],
|
||||
default: u.gc('refresherFps', 40)
|
||||
},
|
||||
// 自定义下拉刷新允许触发的最大下拉角度,默认为40度,当下拉角度小于设定值时,自定义下拉刷新动画不会被触发
|
||||
refresherMaxAngle: {
|
||||
type: [Number, String],
|
||||
default: u.gc('refresherMaxAngle', 40)
|
||||
},
|
||||
// 自定义下拉刷新的角度由未达到最大角度变到达到最大角度时,是否继续下拉刷新手势,默认为否
|
||||
refresherAngleEnableChangeContinued: {
|
||||
type: Boolean,
|
||||
default: u.gc('refresherAngleEnableChangeContinued', false)
|
||||
},
|
||||
// 自定义下拉刷新默认状态下的文字
|
||||
refresherDefaultText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('refresherDefaultText', null)
|
||||
},
|
||||
// 自定义下拉刷新松手立即刷新状态下的文字
|
||||
refresherPullingText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('refresherPullingText', null)
|
||||
},
|
||||
// 自定义下拉刷新刷新中状态下的文字
|
||||
refresherRefreshingText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('refresherRefreshingText', null)
|
||||
},
|
||||
// 自定义下拉刷新刷新结束状态下的文字
|
||||
refresherCompleteText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('refresherCompleteText', null)
|
||||
},
|
||||
// 自定义继续下拉进入二楼文字
|
||||
refresherGoF2Text: {
|
||||
type: [String, Object],
|
||||
default: u.gc('refresherGoF2Text', null)
|
||||
},
|
||||
// 自定义下拉刷新默认状态下的图片
|
||||
refresherDefaultImg: {
|
||||
type: String,
|
||||
default: u.gc('refresherDefaultImg', null)
|
||||
},
|
||||
// 自定义下拉刷新松手立即刷新状态下的图片,默认与refresherDefaultImg一致
|
||||
refresherPullingImg: {
|
||||
type: String,
|
||||
default: u.gc('refresherPullingImg', null)
|
||||
},
|
||||
// 自定义下拉刷新刷新中状态下的图片
|
||||
refresherRefreshingImg: {
|
||||
type: String,
|
||||
default: u.gc('refresherRefreshingImg', null)
|
||||
},
|
||||
// 自定义下拉刷新刷新结束状态下的图片
|
||||
refresherCompleteImg: {
|
||||
type: String,
|
||||
default: u.gc('refresherCompleteImg', null)
|
||||
},
|
||||
// 自定义下拉刷新刷新中状态下是否展示旋转动画
|
||||
refresherRefreshingAnimated: {
|
||||
type: Boolean,
|
||||
default: u.gc('refresherRefreshingAnimated', true)
|
||||
},
|
||||
// 是否开启自定义下拉刷新刷新结束回弹效果,默认为是
|
||||
refresherEndBounceEnabled: {
|
||||
type: Boolean,
|
||||
default: u.gc('refresherEndBounceEnabled', true)
|
||||
},
|
||||
// 是否开启自定义下拉刷新,默认为是
|
||||
refresherEnabled: {
|
||||
type: Boolean,
|
||||
default: u.gc('refresherEnabled', true)
|
||||
},
|
||||
// 设置自定义下拉刷新阈值,默认为80rpx
|
||||
refresherThreshold: {
|
||||
type: [Number, String],
|
||||
default: u.gc('refresherThreshold', '80rpx')
|
||||
},
|
||||
// 设置系统下拉刷新默认样式,支持设置 black,white,none,none 表示不使用默认样式,默认为black
|
||||
refresherDefaultStyle: {
|
||||
type: String,
|
||||
default: u.gc('refresherDefaultStyle', 'black')
|
||||
},
|
||||
// 设置自定义下拉刷新区域背景
|
||||
refresherBackground: {
|
||||
type: String,
|
||||
default: u.gc('refresherBackground', 'transparent')
|
||||
},
|
||||
// 设置固定的自定义下拉刷新区域背景
|
||||
refresherFixedBackground: {
|
||||
type: String,
|
||||
default: u.gc('refresherFixedBackground', 'transparent')
|
||||
},
|
||||
// 设置固定的自定义下拉刷新区域高度,默认为0
|
||||
refresherFixedBacHeight: {
|
||||
type: [Number, String],
|
||||
default: u.gc('refresherFixedBacHeight', 0)
|
||||
},
|
||||
// 设置自定义下拉刷新下拉超出阈值后继续下拉位移衰减的比例,范围0-1,值越大代表衰减越多。默认为0.65(nvue无效)
|
||||
refresherOutRate: {
|
||||
type: Number,
|
||||
default: u.gc('refresherOutRate', 0.65)
|
||||
},
|
||||
// 是否开启下拉进入二楼功能,默认为否
|
||||
refresherF2Enabled: {
|
||||
type: Boolean,
|
||||
default: u.gc('refresherF2Enabled', false)
|
||||
},
|
||||
// 下拉进入二楼阈值,默认为200rpx
|
||||
refresherF2Threshold: {
|
||||
type: [Number, String],
|
||||
default: u.gc('refresherF2Threshold', '200rpx')
|
||||
},
|
||||
// 下拉进入二楼动画时间,单位为毫秒,默认为200毫秒
|
||||
refresherF2Duration: {
|
||||
type: [Number, String],
|
||||
default: u.gc('refresherF2Duration', 200)
|
||||
},
|
||||
// 下拉进入二楼状态松手后是否弹出二楼,默认为是
|
||||
showRefresherF2: {
|
||||
type: Boolean,
|
||||
default: u.gc('showRefresherF2', true)
|
||||
},
|
||||
// 设置自定义下拉刷新下拉时实际下拉位移与用户下拉距离的比值,默认为0.75,即代表若用户下拉10px,则实际位移为7.5px(nvue无效)
|
||||
refresherPullRate: {
|
||||
type: Number,
|
||||
default: u.gc('refresherPullRate', 0.75)
|
||||
},
|
||||
// 是否显示最后更新时间,默认为否
|
||||
showRefresherUpdateTime: {
|
||||
type: Boolean,
|
||||
default: u.gc('showRefresherUpdateTime', false)
|
||||
},
|
||||
// 如果需要区别不同页面的最后更新时间,请为不同页面的z-paging的`refresher-update-time-key`设置不同的字符串
|
||||
refresherUpdateTimeKey: {
|
||||
type: String,
|
||||
default: u.gc('refresherUpdateTimeKey', 'default')
|
||||
},
|
||||
// 下拉刷新时下拉到“松手立即刷新”或“松手进入二楼”状态时是否使手机短振动,默认为否(h5无效)
|
||||
refresherVibrate: {
|
||||
type: Boolean,
|
||||
default: u.gc('refresherVibrate', false)
|
||||
},
|
||||
// 下拉刷新时是否禁止下拉刷新view跟随用户触摸竖直移动,默认为否。注意此属性只是禁止下拉刷新view移动,其他下拉刷新逻辑依然会正常触发
|
||||
refresherNoTransform: {
|
||||
type: Boolean,
|
||||
default: u.gc('refresherNoTransform', false)
|
||||
},
|
||||
// 是否开启下拉刷新状态栏占位,适用于隐藏导航栏时,下拉刷新需要避开状态栏高度的情况,默认为否
|
||||
useRefresherStatusBarPlaceholder: {
|
||||
type: Boolean,
|
||||
default: u.gc('useRefresherStatusBarPlaceholder', false)
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
R: Enum.Refresher,
|
||||
//下拉刷新状态
|
||||
refresherStatus: Enum.Refresher.Default,
|
||||
refresherTouchstartY: 0,
|
||||
lastRefresherTouchmove: null,
|
||||
refresherReachMaxAngle: true,
|
||||
refresherTransform: 'translateY(0px)',
|
||||
refresherTransition: '',
|
||||
finalRefresherDefaultStyle: 'black',
|
||||
refresherRevealStackCount: 0,
|
||||
refresherCompleteTimeout: null,
|
||||
refresherCompleteSubTimeout: null,
|
||||
refresherEndTimeout: null,
|
||||
isTouchmovingTimeout: null,
|
||||
refresherTriggered: false,
|
||||
isTouchmoving: false,
|
||||
isTouchEnded: false,
|
||||
isUserPullDown: false,
|
||||
privateRefresherEnabled: -1,
|
||||
privateShowRefresherWhenReload: false,
|
||||
customRefresherHeight: -1,
|
||||
showCustomRefresher: false,
|
||||
doRefreshAnimateAfter: false,
|
||||
isRefresherInComplete: false,
|
||||
showF2: false,
|
||||
f2Transform: '',
|
||||
pullDownTimeStamp: 0,
|
||||
moveDis: 0,
|
||||
oldMoveDis: 0,
|
||||
currentDis: 0,
|
||||
oldCurrentMoveDis: 0,
|
||||
oldRefresherTouchmoveY: 0,
|
||||
oldTouchDirection: '',
|
||||
oldEmitedTouchDirection: '',
|
||||
oldPullingDistance: -1,
|
||||
refresherThresholdUpdateTag: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
refresherDefaultStyle: {
|
||||
handler(newVal) {
|
||||
if (newVal.length) {
|
||||
this.finalRefresherDefaultStyle = newVal;
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
refresherStatus(newVal) {
|
||||
newVal === Enum.Refresher.Loading && this._cleanRefresherEndTimeout();
|
||||
this.refresherVibrate && (newVal === Enum.Refresher.ReleaseToRefresh || newVal === Enum.Refresher.GoF2) && this._doVibrateShort();
|
||||
this.$emit('refresherStatusChange', newVal);
|
||||
this.$emit('update:refresherStatus', newVal);
|
||||
},
|
||||
// 监听当前下拉刷新启用/禁用状态
|
||||
refresherEnabled(newVal) {
|
||||
// 当禁用下拉刷新时,强制收回正在展示的下拉刷新view
|
||||
!newVal && this.endRefresh();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pullDownDisTimeStamp() {
|
||||
return 1000 / this.refresherFps;
|
||||
},
|
||||
refresherThresholdUnitConverted() {
|
||||
return u.addUnit(this.refresherThreshold, this.unit);
|
||||
},
|
||||
finalRefresherEnabled() {
|
||||
if (this.useChatRecordMode) return false;
|
||||
if (this.privateRefresherEnabled === -1) return this.refresherEnabled;
|
||||
return this.privateRefresherEnabled === 1;
|
||||
},
|
||||
finalRefresherThreshold() {
|
||||
let refresherThreshold = this.refresherThresholdUnitConverted;
|
||||
let idDefault = false;
|
||||
if (refresherThreshold === u.addUnit(80, this.unit)) {
|
||||
idDefault = true;
|
||||
if (this.showRefresherUpdateTime) {
|
||||
refresherThreshold = u.addUnit(120, this.unit);
|
||||
}
|
||||
}
|
||||
if (idDefault && this.customRefresherHeight > 0) return this.customRefresherHeight + this.finalRefresherThresholdPlaceholder;
|
||||
return u.convertToPx(refresherThreshold) + this.finalRefresherThresholdPlaceholder;
|
||||
},
|
||||
finalRefresherF2Threshold() {
|
||||
return u.convertToPx(u.addUnit(this.refresherF2Threshold, this.unit));
|
||||
},
|
||||
finalRefresherThresholdPlaceholder() {
|
||||
return this.useRefresherStatusBarPlaceholder ? this.statusBarHeight : 0;
|
||||
},
|
||||
finalRefresherFixedBacHeight() {
|
||||
return u.convertToPx(this.refresherFixedBacHeight);
|
||||
},
|
||||
finalRefresherThemeStyle() {
|
||||
return this.refresherThemeStyle.length ? this.refresherThemeStyle : this.defaultThemeStyle;
|
||||
},
|
||||
finalRefresherOutRate() {
|
||||
let rate = this.refresherOutRate;
|
||||
rate = Math.max(0,rate);
|
||||
rate = Math.min(1,rate);
|
||||
return rate;
|
||||
},
|
||||
finalRefresherPullRate() {
|
||||
let rate = this.refresherPullRate;
|
||||
rate = Math.max(0,rate);
|
||||
return rate;
|
||||
},
|
||||
finalRefresherTransform() {
|
||||
if (this.refresherNoTransform || this.refresherTransform === 'translateY(0px)') return 'none';
|
||||
return this.refresherTransform;
|
||||
},
|
||||
finalShowRefresherWhenReload() {
|
||||
return this.showRefresherWhenReload || this.privateShowRefresherWhenReload;
|
||||
},
|
||||
finalRefresherTriggered() {
|
||||
if (!(this.finalRefresherEnabled && !this.useCustomRefresher)) return false;
|
||||
return this.refresherTriggered;
|
||||
},
|
||||
showRefresher() {
|
||||
const showRefresher = this.finalRefresherEnabled || this.useCustomRefresher && !this.useChatRecordMode;
|
||||
// #ifndef APP-NVUE
|
||||
this.active && this.customRefresherHeight === -1 && showRefresher && this.updateCustomRefresherHeight();
|
||||
// #endif
|
||||
return showRefresher;
|
||||
},
|
||||
hasTouchmove() {
|
||||
// #ifdef VUE2
|
||||
// #ifdef APP-VUE || H5
|
||||
if (this.$listeners && !this.$listeners.refresherTouchmove) return false;
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN || MP-QQ
|
||||
return this.watchRefresherTouchmove;
|
||||
// #endif
|
||||
return true;
|
||||
// #endif
|
||||
return this.watchRefresherTouchmove;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 终止下拉刷新状态
|
||||
endRefresh() {
|
||||
this.totalData = this.realTotalData;
|
||||
this._refresherEnd();
|
||||
this._endSystemLoadingAndRefresh();
|
||||
this._handleScrollViewBounce({ bounce: true });
|
||||
this.$nextTick(() => {
|
||||
this.refresherTriggered = false;
|
||||
})
|
||||
},
|
||||
// 手动更新自定义下拉刷新view高度
|
||||
updateCustomRefresherHeight() {
|
||||
u.delay(() => this.$nextTick(this._updateCustomRefresherHeight));
|
||||
},
|
||||
// 关闭二楼
|
||||
closeF2() {
|
||||
this._handleCloseF2();
|
||||
},
|
||||
// 自定义下拉刷新被触发
|
||||
_onRefresh(fromScrollView = false, isUserPullDown = true) {
|
||||
if (fromScrollView && !(this.finalRefresherEnabled && !this.useCustomRefresher)) return;
|
||||
this.$emit('onRefresh');
|
||||
this.$emit('Refresh');
|
||||
// #ifdef APP-NVUE
|
||||
if (this.loading) {
|
||||
u.delay(this._nRefresherEnd, 500)
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
if (this.loading || this.isRefresherInComplete) return;
|
||||
this.loadingType = Enum.LoadingType.Refresher;
|
||||
if (this.nShowRefresherReveal) return;
|
||||
this.isUserPullDown = isUserPullDown;
|
||||
this.isUserReload = !isUserPullDown;
|
||||
this._startLoading(true);
|
||||
this.refresherTriggered = true;
|
||||
if (this.reloadWhenRefresh && isUserPullDown) {
|
||||
this.useChatRecordMode ? this._onLoadingMore('click') : this._reload(false, false, isUserPullDown);
|
||||
}
|
||||
},
|
||||
// 自定义下拉刷新被复位
|
||||
_onRestore() {
|
||||
this.refresherTriggered = 'restore';
|
||||
this.$emit('onRestore');
|
||||
this.$emit('Restore');
|
||||
},
|
||||
// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
|
||||
// touch开始
|
||||
_refresherTouchstart(e) {
|
||||
this._handleListTouchstart();
|
||||
if (this._touchDisabled()) return;
|
||||
this._handleRefresherTouchstart(u.getTouch(e));
|
||||
},
|
||||
// #endif
|
||||
// 进一步处理touch开始结果
|
||||
_handleRefresherTouchstart(touch) {
|
||||
if (!this.loading && this.isTouchEnded) {
|
||||
this.isTouchmoving = false;
|
||||
}
|
||||
this.loadingType = Enum.LoadingType.Refresher;
|
||||
this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
|
||||
this.isTouchEnded = false;
|
||||
this.refresherTransition = '';
|
||||
this.refresherTouchstartY = touch.touchY;
|
||||
this.$emit('refresherTouchstart', this.refresherTouchstartY);
|
||||
this.lastRefresherTouchmove = touch;
|
||||
this._cleanRefresherCompleteTimeout();
|
||||
this._cleanRefresherEndTimeout();
|
||||
},
|
||||
|
||||
// 非app-vue或微信小程序或QQ小程序或h5平台,使用js控制下拉刷新
|
||||
// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
|
||||
// touch中
|
||||
_refresherTouchmove(e) {
|
||||
const currentTimeStamp = u.getTime();
|
||||
let touch = null;
|
||||
let refresherTouchmoveY = 0;
|
||||
if (this.watchTouchDirectionChange) {
|
||||
// 检测下拉刷新方向改变
|
||||
touch = u.getTouch(e);
|
||||
refresherTouchmoveY = touch.touchY;
|
||||
const direction = refresherTouchmoveY > this.oldRefresherTouchmoveY ? 'top' : 'bottom';
|
||||
// 只有在方向改变的时候才emit相关事件
|
||||
if (direction === this.oldTouchDirection && direction !== this.oldEmitedTouchDirection) {
|
||||
this._handleTouchDirectionChange({ direction });
|
||||
this.oldEmitedTouchDirection = direction;
|
||||
}
|
||||
this.oldTouchDirection = direction;
|
||||
this.oldRefresherTouchmoveY = refresherTouchmoveY;
|
||||
}
|
||||
// 节流处理,在pullDownDisTimeStamp时间内的下拉刷新中事件不进行处理
|
||||
if (this.pullDownTimeStamp && currentTimeStamp - this.pullDownTimeStamp <= this.pullDownDisTimeStamp) return;
|
||||
// 如果不允许下拉,则return
|
||||
if (this._touchDisabled()) return;
|
||||
this.pullDownTimeStamp = Number(currentTimeStamp);
|
||||
touch = u.getTouch(e);
|
||||
refresherTouchmoveY = touch.touchY;
|
||||
// 获取当前touch的y - 初始touch的y,计算它们的差
|
||||
let moveDis = refresherTouchmoveY - this.refresherTouchstartY;
|
||||
if (moveDis < 0) return;
|
||||
// 对下拉刷新的角度进行限制
|
||||
if (this.refresherMaxAngle >= 0 && this.refresherMaxAngle <= 90 && this.lastRefresherTouchmove && this.lastRefresherTouchmove.touchY <= refresherTouchmoveY) {
|
||||
if (!moveDis && !this.refresherAngleEnableChangeContinued && this.moveDis < 1 && !this.refresherReachMaxAngle) return;
|
||||
const x = Math.abs(touch.touchX - this.lastRefresherTouchmove.touchX);
|
||||
const y = Math.abs(refresherTouchmoveY - this.lastRefresherTouchmove.touchY);
|
||||
const z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
||||
if ((x || y) && x > 1) {
|
||||
// 获取下拉刷新前后两次位移的角度
|
||||
const angle = Math.asin(y / z) / Math.PI * 180;
|
||||
// 如果角度小于配置要求,则return
|
||||
if (angle < this.refresherMaxAngle) {
|
||||
this.lastRefresherTouchmove = touch;
|
||||
this.refresherReachMaxAngle = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取最终的moveDis
|
||||
moveDis = this._getFinalRefresherMoveDis(moveDis);
|
||||
// 处理下拉刷新位移
|
||||
this._handleRefresherTouchmove(moveDis, touch);
|
||||
// 下拉刷新时,禁止页面滚动以防止页面向下滚动和下拉刷新同时作用导致下拉刷新位置偏移超过预期
|
||||
if (!this.disabledBounce) {
|
||||
// #ifndef MP-LARK
|
||||
this._handleScrollViewBounce({ bounce: false });
|
||||
// #endif
|
||||
this.disabledBounce = true;
|
||||
}
|
||||
this._emitTouchmove({ pullingDistance: moveDis, dy: this.moveDis - this.oldMoveDis });
|
||||
},
|
||||
// #endif
|
||||
// 进一步处理touch中结果
|
||||
_handleRefresherTouchmove(moveDis, touch) {
|
||||
this.refresherReachMaxAngle = true;
|
||||
this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
|
||||
this.isTouchmoving = true;
|
||||
this.isTouchEnded = false;
|
||||
// 更新下拉刷新状态
|
||||
// 下拉刷新距离超过阈值
|
||||
if (moveDis >= this.finalRefresherThreshold) {
|
||||
// 如果开启了下拉进入二楼并且下拉刷新距离超过进入二楼阈值,则当前下拉刷新状态为松手进入二楼,否则为松手立即刷新
|
||||
this.refresherStatus = this.refresherF2Enabled && moveDis >= this.finalRefresherF2Threshold ? Enum.Refresher.GoF2 : Enum.Refresher.ReleaseToRefresh;
|
||||
} else {
|
||||
// 下拉刷新距离未超过阈值,显示默认状态
|
||||
this.refresherStatus = Enum.Refresher.Default;
|
||||
}
|
||||
// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
|
||||
// this.scrollEnable = false;
|
||||
// 通过transform控制下拉刷新view垂直偏移
|
||||
this.refresherTransform = `translateY(${moveDis}px)`;
|
||||
this.lastRefresherTouchmove = touch;
|
||||
// #endif
|
||||
this.moveDis = moveDis;
|
||||
},
|
||||
// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
|
||||
// touch结束
|
||||
_refresherTouchend(e) {
|
||||
// 下拉刷新用户手离开屏幕,允许列表滚动
|
||||
this._handleScrollViewBounce({bounce: true});
|
||||
if (this._touchDisabled() || !this.isTouchmoving) return;
|
||||
const touch = u.getTouch(e);
|
||||
let refresherTouchendY = touch.touchY;
|
||||
let moveDis = refresherTouchendY - this.refresherTouchstartY;
|
||||
moveDis = this._getFinalRefresherMoveDis(moveDis);
|
||||
this._handleRefresherTouchend(moveDis);
|
||||
this.disabledBounce = false;
|
||||
},
|
||||
// #endif
|
||||
// 进一步处理touch结束结果
|
||||
_handleRefresherTouchend(moveDis) {
|
||||
// #ifndef APP-PLUS || H5 || MP-WEIXIN
|
||||
if (!this.isTouchmoving) return;
|
||||
// #endif
|
||||
this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
|
||||
this.refresherReachMaxAngle = true;
|
||||
this.isTouchEnded = true;
|
||||
const refresherThreshold = this.finalRefresherThreshold;
|
||||
if (moveDis >= refresherThreshold && (this.refresherStatus === Enum.Refresher.ReleaseToRefresh || this.refresherStatus === Enum.Refresher.GoF2)) {
|
||||
// 如果是松手进入二楼状态,则触发进入二楼
|
||||
if (this.refresherStatus === Enum.Refresher.GoF2) {
|
||||
this._handleGoF2();
|
||||
this._refresherEnd();
|
||||
} else {
|
||||
// 如果是松手立即刷新状态,则触发下拉刷新
|
||||
// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
|
||||
this.refresherTransform = `translateY(${refresherThreshold}px)`;
|
||||
this.refresherTransition = 'transform .1s linear';
|
||||
// #endif
|
||||
u.delay(() => {
|
||||
this._emitTouchmove({ pullingDistance: refresherThreshold, dy: this.moveDis - refresherThreshold });
|
||||
}, 0.1);
|
||||
this.moveDis = refresherThreshold;
|
||||
this.refresherStatus = Enum.Refresher.Loading;
|
||||
this._doRefresherLoad();
|
||||
}
|
||||
} else {
|
||||
this._refresherEnd();
|
||||
this.isTouchmovingTimeout = u.delay(() => {
|
||||
this.isTouchmoving = false;
|
||||
}, this.refresherDefaultDuration);
|
||||
}
|
||||
this.scrollEnable = true;
|
||||
this.$emit('refresherTouchend', moveDis);
|
||||
},
|
||||
// 处理列表触摸开始事件
|
||||
_handleListTouchstart() {
|
||||
if (this.useChatRecordMode && this.autoHideKeyboardWhenChat) {
|
||||
uni.hideKeyboard();
|
||||
this.$emit('hidedKeyboard');
|
||||
}
|
||||
},
|
||||
// 处理scroll-view bounce是否生效
|
||||
_handleScrollViewBounce({ bounce }) {
|
||||
if (!this.usePageScroll && !this.scrollToTopBounceEnabled) {
|
||||
if (this.wxsScrollTop <= 5) {
|
||||
// #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5
|
||||
this.refresherTransition = '';
|
||||
// #endif
|
||||
this.scrollEnable = bounce;
|
||||
} else if (bounce) {
|
||||
this.scrollEnable = bounce;
|
||||
}
|
||||
}
|
||||
},
|
||||
// wxs正在下拉状态改变处理
|
||||
_handleWxsPullingDownStatusChange(onPullingDown) {
|
||||
this.wxsOnPullingDown = onPullingDown;
|
||||
if (onPullingDown && !this.useChatRecordMode) {
|
||||
this.renderPropScrollTop = 0;
|
||||
}
|
||||
},
|
||||
// wxs正在下拉处理
|
||||
_handleWxsPullingDown({ moveDis, diffDis }){
|
||||
this._emitTouchmove({ pullingDistance: moveDis,dy: diffDis });
|
||||
},
|
||||
// wxs触摸方向改变
|
||||
_handleTouchDirectionChange({ direction }) {
|
||||
this.$emit('touchDirectionChange',direction);
|
||||
},
|
||||
// wxs通知更新其props
|
||||
_handlePropUpdate(){
|
||||
this.wxsPropType = u.getTime().toString();
|
||||
},
|
||||
// 下拉刷新结束
|
||||
_refresherEnd(shouldEndLoadingDelay = true, fromAddData = false, isUserPullDown = false, setLoading = true) {
|
||||
if (this.loadingType === Enum.LoadingType.Refresher) {
|
||||
// 计算当前下拉刷新结束需要延迟的时间
|
||||
const refresherCompleteDelay = (fromAddData && (isUserPullDown || this.showRefresherWhenReload)) ? this.refresherCompleteDelay : 0;
|
||||
// 如果延迟时间大于0,则展示刷新结束状态,否则直接展示默认状态
|
||||
const refresherStatus = refresherCompleteDelay > 0 ? Enum.Refresher.Complete : Enum.Refresher.Default;
|
||||
if (this.finalShowRefresherWhenReload) {
|
||||
const stackCount = this.refresherRevealStackCount;
|
||||
this.refresherRevealStackCount --;
|
||||
if (stackCount > 1) return;
|
||||
}
|
||||
this._cleanRefresherEndTimeout();
|
||||
this.refresherEndTimeout = u.delay(() => {
|
||||
// 更新下拉刷新状态
|
||||
this.refresherStatus = refresherStatus;
|
||||
// 如果当前下拉刷新状态不是刷新结束,则认为其不在刷新结束状态
|
||||
if (refresherStatus !== Enum.Refresher.Complete) {
|
||||
this.isRefresherInComplete = false;
|
||||
}
|
||||
}, this.refresherStatus !== Enum.Refresher.Default && refresherStatus === Enum.Refresher.Default ? this.refresherCompleteDuration : 0);
|
||||
|
||||
// #ifndef APP-NVUE
|
||||
if (refresherCompleteDelay > 0) {
|
||||
this.isRefresherInComplete = true;
|
||||
}
|
||||
// #endif
|
||||
this._cleanRefresherCompleteTimeout();
|
||||
this.refresherCompleteTimeout = u.delay(() => {
|
||||
let animateDuration = 1;
|
||||
const animateType = this.refresherEndBounceEnabled && fromAddData ? 'cubic-bezier(0.19,1.64,0.42,0.72)' : 'linear';
|
||||
if (fromAddData) {
|
||||
animateDuration = this.refresherEndBounceEnabled ? this.refresherCompleteDuration / 1000 : this.refresherCompleteDuration / 3000;
|
||||
}
|
||||
this.refresherTransition = `transform ${fromAddData ? animateDuration : this.refresherDefaultDuration / 1000}s ${animateType}`;
|
||||
// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
|
||||
this.refresherTransform = 'translateY(0px)';
|
||||
this.currentDis = 0;
|
||||
// #endif
|
||||
// #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5
|
||||
this.wxsPropType = this.refresherTransition + 'end' + u.getTime();
|
||||
// #endif
|
||||
// #ifdef APP-NVUE
|
||||
this._nRefresherEnd();
|
||||
// #endif
|
||||
this.moveDis = 0;
|
||||
// #ifndef APP-NVUE
|
||||
if (refresherStatus === Enum.Refresher.Complete) {
|
||||
if (this.refresherCompleteSubTimeout) {
|
||||
clearTimeout(this.refresherCompleteSubTimeout);
|
||||
this.refresherCompleteSubTimeout = null;
|
||||
}
|
||||
this.refresherCompleteSubTimeout = u.delay(() => {
|
||||
this.$nextTick(() => {
|
||||
this.refresherStatus = Enum.Refresher.Default;
|
||||
this.isRefresherInComplete = false;
|
||||
})
|
||||
}, animateDuration * 800);
|
||||
}
|
||||
// #endif
|
||||
this._emitTouchmove({ pullingDistance: 0, dy: this.moveDis });
|
||||
}, refresherCompleteDelay);
|
||||
}
|
||||
if (setLoading) {
|
||||
u.delay(() => this.loading = false, shouldEndLoadingDelay ? 10 : 0);
|
||||
isUserPullDown && this._onRestore();
|
||||
}
|
||||
},
|
||||
// 处理进入二楼
|
||||
_handleGoF2() {
|
||||
if (this.showF2 || !this.refresherF2Enabled) return;
|
||||
this.$emit('refresherF2Change', 'go');
|
||||
|
||||
if (!this.showRefresherF2) return;
|
||||
// #ifndef APP-NVUE
|
||||
this.f2Transform = `translateY(${-this.superContentHeight}px)`;
|
||||
this.showF2 = true;
|
||||
u.delay(() => {
|
||||
this.f2Transform = 'translateY(0px)';
|
||||
}, 100, 'f2ShowDelay')
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
this.showF2 = true;
|
||||
this.$nextTick(() => {
|
||||
weexAnimation.transition(this.$refs['zp-n-f2'], {
|
||||
styles: { transform: `translateY(${-this.superContentHeight}px)` },
|
||||
duration: 0,
|
||||
timingFunction: 'linear',
|
||||
needLayout: true,
|
||||
delay: 0
|
||||
})
|
||||
this.nF2Opacity = 1;
|
||||
})
|
||||
u.delay(() => {
|
||||
weexAnimation.transition(this.$refs['zp-n-f2'], {
|
||||
styles: { transform: 'translateY(0px)' },
|
||||
duration: this.refresherF2Duration,
|
||||
timingFunction: 'linear',
|
||||
needLayout: true,
|
||||
delay: 0
|
||||
})
|
||||
}, 10, 'f2GoDelay')
|
||||
// #endif
|
||||
},
|
||||
// 处理退出二楼
|
||||
_handleCloseF2() {
|
||||
if (!this.showF2 || !this.refresherF2Enabled) return;
|
||||
this.$emit('refresherF2Change', 'close');
|
||||
|
||||
if (!this.showRefresherF2) return;
|
||||
// #ifndef APP-NVUE
|
||||
this.f2Transform = `translateY(${-this.superContentHeight}px)`;
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
weexAnimation.transition(this.$refs['zp-n-f2'], {
|
||||
styles: { transform: `translateY(${-this.superContentHeight}px)` },
|
||||
duration: this.refresherF2Duration,
|
||||
timingFunction: 'linear',
|
||||
needLayout: true,
|
||||
delay: 0
|
||||
})
|
||||
// #endif
|
||||
|
||||
u.delay(() => {
|
||||
this.showF2 = false;
|
||||
this.nF2Opacity = 0;
|
||||
}, this.refresherF2Duration, 'f2CloseDelay')
|
||||
},
|
||||
// 模拟用户手动触发下拉刷新
|
||||
_doRefresherRefreshAnimate() {
|
||||
this._cleanRefresherCompleteTimeout();
|
||||
// 用户处理用户在短时间内多次调用reload的情况,此时下拉刷新view不需要重复显示,只需要保证最后一次reload对应的请求结束后收回下拉刷新view即可
|
||||
// #ifndef APP-NVUE
|
||||
const doRefreshAnimateAfter = !this.doRefreshAnimateAfter && (this.finalShowRefresherWhenReload) && this
|
||||
.customRefresherHeight === -1 && this.refresherThreshold === u.addUnit(80, this.unit);
|
||||
if (doRefreshAnimateAfter) {
|
||||
this.doRefreshAnimateAfter = true;
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
this.refresherRevealStackCount ++;
|
||||
// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
|
||||
this.refresherTransform = `translateY(${this.finalRefresherThreshold}px)`;
|
||||
// #endif
|
||||
// #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5
|
||||
this.wxsPropType = 'begin' + u.getTime();
|
||||
// #endif
|
||||
this.moveDis = this.finalRefresherThreshold;
|
||||
this.refresherStatus = Enum.Refresher.Loading;
|
||||
this.isTouchmoving = true;
|
||||
this.isTouchmovingTimeout && clearTimeout(this.isTouchmovingTimeout);
|
||||
this._doRefresherLoad(false);
|
||||
},
|
||||
// 触发下拉刷新
|
||||
_doRefresherLoad(isUserPullDown = true) {
|
||||
this._onRefresh(false, isUserPullDown);
|
||||
this.loading = true;
|
||||
},
|
||||
// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5
|
||||
// 获取处理后的moveDis
|
||||
_getFinalRefresherMoveDis(moveDis) {
|
||||
let diffDis = moveDis - this.oldCurrentMoveDis;
|
||||
this.oldCurrentMoveDis = moveDis;
|
||||
if (diffDis > 0) {
|
||||
// 根据配置的下拉刷新用户手势位移与实际需要的位移比率计算最终的diffDis
|
||||
diffDis = diffDis * this.finalRefresherPullRate;
|
||||
if (this.currentDis > this.finalRefresherThreshold) {
|
||||
diffDis = diffDis * (1 - this.finalRefresherOutRate);
|
||||
}
|
||||
}
|
||||
// 控制diffDis过大的情况,比如进入页面突然猛然下拉,此时diffDis不应进行太大的偏移
|
||||
diffDis = diffDis > 100 ? diffDis / 100 : diffDis;
|
||||
this.currentDis += diffDis;
|
||||
this.currentDis = Math.max(0, this.currentDis);
|
||||
return this.currentDis;
|
||||
},
|
||||
// 判断touch手势是否要触发
|
||||
_touchDisabled() {
|
||||
const checkOldScrollTop = this.oldScrollTop > 5;
|
||||
return this.loading || this.isRefresherInComplete || this.useChatRecordMode || !this.refresherEnabled || !this.useCustomRefresher ||(this.usePageScroll && this.useCustomRefresher && this.pageScrollTop > 10) || (!(this.usePageScroll && this.useCustomRefresher) && checkOldScrollTop);
|
||||
},
|
||||
// #endif
|
||||
// 更新自定义下拉刷新view高度
|
||||
_updateCustomRefresherHeight() {
|
||||
this._getNodeClientRect('.zp-custom-refresher-slot-view').then((res) => {
|
||||
this.customRefresherHeight = res ? res[0].height : 0;
|
||||
this.showCustomRefresher = this.customRefresherHeight > 0;
|
||||
if (this.doRefreshAnimateAfter) {
|
||||
this.doRefreshAnimateAfter = false;
|
||||
this._doRefresherRefreshAnimate();
|
||||
}
|
||||
});
|
||||
},
|
||||
// emit pullingDown事件
|
||||
_emitTouchmove(e) {
|
||||
// #ifndef APP-NVUE
|
||||
e.viewHeight = this.finalRefresherThreshold;
|
||||
// #endif
|
||||
e.rate = e.viewHeight > 0 ? e.pullingDistance / e.viewHeight : 0;
|
||||
this.hasTouchmove && this.oldPullingDistance !== e.pullingDistance && this.$emit('refresherTouchmove', e);
|
||||
this.oldPullingDistance = e.pullingDistance;
|
||||
},
|
||||
// 清除refresherCompleteTimeout
|
||||
_cleanRefresherCompleteTimeout() {
|
||||
this.refresherCompleteTimeout = this._cleanTimeout(this.refresherCompleteTimeout);
|
||||
// #ifdef APP-NVUE
|
||||
this._nRefresherEnd(false);
|
||||
// #endif
|
||||
},
|
||||
// 清除refresherEndTimeout
|
||||
_cleanRefresherEndTimeout() {
|
||||
this.refresherEndTimeout = this._cleanTimeout(this.refresherEndTimeout);
|
||||
},
|
||||
}
|
||||
}
|
||||
550
uni_modules/z-paging/components/z-paging/js/modules/scroller.js
Normal file
550
uni_modules/z-paging/components/z-paging/js/modules/scroller.js
Normal file
@@ -0,0 +1,550 @@
|
||||
// [z-paging]scroll相关模块
|
||||
import u from '.././z-paging-utils'
|
||||
import Enum from '.././z-paging-enum'
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
const weexDom = weex.requireModule('dom');
|
||||
// #endif
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 使用页面滚动,默认为否,当设置为是时则使用页面的滚动而非此组件内部的scroll-view的滚动,使用页面滚动时z-paging无需设置确定的高度且对于长列表展示性能更高,但配置会略微繁琐
|
||||
usePageScroll: {
|
||||
type: Boolean,
|
||||
default: u.gc('usePageScroll', false)
|
||||
},
|
||||
// 是否可以滚动,使用内置scroll-view和nvue时有效,默认为是
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: u.gc('scrollable', true)
|
||||
},
|
||||
// 控制是否出现滚动条,默认为是
|
||||
showScrollbar: {
|
||||
type: Boolean,
|
||||
default: u.gc('showScrollbar', true)
|
||||
},
|
||||
// 是否允许横向滚动,默认为否
|
||||
scrollX: {
|
||||
type: Boolean,
|
||||
default: u.gc('scrollX', false)
|
||||
},
|
||||
// iOS设备上滚动到顶部时是否允许回弹效果,默认为否。关闭回弹效果后可使滚动到顶部与下拉刷新更连贯,但是有吸顶view时滚动到顶部时可能出现抖动。
|
||||
scrollToTopBounceEnabled: {
|
||||
type: Boolean,
|
||||
default: u.gc('scrollToTopBounceEnabled', false)
|
||||
},
|
||||
// iOS设备上滚动到底部时是否允许回弹效果,默认为是。
|
||||
scrollToBottomBounceEnabled: {
|
||||
type: Boolean,
|
||||
default: u.gc('scrollToBottomBounceEnabled', true)
|
||||
},
|
||||
// 在设置滚动条位置时使用动画过渡,默认为否
|
||||
scrollWithAnimation: {
|
||||
type: Boolean,
|
||||
default: u.gc('scrollWithAnimation', false)
|
||||
},
|
||||
// 值应为某子元素id(id不能以数字开头)。设置哪个方向可滚动,则在哪个方向滚动到该元素
|
||||
scrollIntoView: {
|
||||
type: String,
|
||||
default: u.gc('scrollIntoView', '')
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scrollTop: 0,
|
||||
oldScrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
oldScrollLeft: 0,
|
||||
scrollViewStyle: {},
|
||||
scrollViewContainerStyle: {},
|
||||
scrollViewInStyle: {},
|
||||
pageScrollTop: -1,
|
||||
scrollEnable: true,
|
||||
privateScrollWithAnimation: -1,
|
||||
cacheScrollNodeHeight: -1,
|
||||
superContentHeight: 0,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
oldScrollTop(newVal) {
|
||||
!this.usePageScroll && this._scrollTopChange(newVal,false);
|
||||
},
|
||||
pageScrollTop(newVal) {
|
||||
this.usePageScroll && this._scrollTopChange(newVal,true);
|
||||
},
|
||||
usePageScroll: {
|
||||
handler(newVal) {
|
||||
this.loaded && this.autoHeight && this._setAutoHeight(!newVal);
|
||||
// #ifdef H5
|
||||
if (newVal) {
|
||||
this.$nextTick(() => {
|
||||
const mainScrollRef = this.$refs['zp-scroll-view'].$refs.main;
|
||||
if (mainScrollRef) {
|
||||
mainScrollRef.style = {};
|
||||
}
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
finalScrollTop(newVal) {
|
||||
this.renderPropScrollTop = newVal < 6 ? 0 : 10;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
finalScrollWithAnimation() {
|
||||
if (this.privateScrollWithAnimation !== -1) {
|
||||
return this.privateScrollWithAnimation === 1;
|
||||
}
|
||||
return this.scrollWithAnimation;
|
||||
},
|
||||
finalScrollViewStyle() {
|
||||
if (this.superContentZIndex != 1) {
|
||||
this.scrollViewStyle['z-index'] = this.superContentZIndex;
|
||||
this.scrollViewStyle['position'] = 'relative';
|
||||
}
|
||||
return this.scrollViewStyle;
|
||||
},
|
||||
finalScrollTop() {
|
||||
return this.usePageScroll ? this.pageScrollTop : this.oldScrollTop;
|
||||
},
|
||||
// 当前是否是旧版webview
|
||||
finalIsOldWebView() {
|
||||
return this.isOldWebView && !this.usePageScroll;
|
||||
},
|
||||
// 当前scroll-view/list-view是否允许滚动
|
||||
finalScrollable() {
|
||||
return this.scrollable && !this.usePageScroll && this.scrollEnable
|
||||
&& (this.refresherCompleteScrollable ? true : this.refresherStatus !== Enum.Refresher.Complete)
|
||||
&& (this.refresherRefreshingScrollable ? true : this.refresherStatus !== Enum.Refresher.Loading);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 滚动到顶部,animate为是否展示滚动动画,默认为是
|
||||
scrollToTop(animate, checkReverse = true) {
|
||||
// 如果是聊天记录模式并且列表倒置了,则滚动到顶部实际上是滚动到底部
|
||||
if (this.useChatRecordMode && checkReverse && !this.isChatRecordModeAndNotInversion) {
|
||||
this.scrollToBottom(animate, false);
|
||||
return;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this._scrollToTop(animate, false);
|
||||
// #ifdef APP-NVUE
|
||||
if (this.nvueFastScroll && animate) {
|
||||
u.delay(() => {
|
||||
this._scrollToTop(false, false);
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
},
|
||||
// 滚动到底部,animate为是否展示滚动动画,默认为是
|
||||
scrollToBottom(animate, checkReverse = true) {
|
||||
// 如果是聊天记录模式并且列表倒置了,则滚动到底部实际上是滚动到顶部
|
||||
if (this.useChatRecordMode && checkReverse && !this.isChatRecordModeAndNotInversion) {
|
||||
this.scrollToTop(animate, false);
|
||||
return;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this._scrollToBottom(animate);
|
||||
// #ifdef APP-NVUE
|
||||
if (this.nvueFastScroll && animate) {
|
||||
u.delay(() => {
|
||||
this._scrollToBottom(false);
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
},
|
||||
// 滚动到指定view(vue中有效)。sel为需要滚动的view的id值,不包含"#";offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
|
||||
scrollIntoViewById(sel, offset, animate) {
|
||||
this._scrollIntoView(sel, offset, animate);
|
||||
},
|
||||
// 滚动到指定view(vue中有效)。nodeTop为需要滚动的view的top值(通过uni.createSelectorQuery()获取);offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
|
||||
scrollIntoViewByNodeTop(nodeTop, offset, animate) {
|
||||
this.scrollTop = this.oldScrollTop;
|
||||
this.$nextTick(() => {
|
||||
this._scrollIntoViewByNodeTop(nodeTop, offset, animate);
|
||||
})
|
||||
},
|
||||
// y轴滚动到指定位置(vue中有效)。y为与顶部的距离,单位为px;offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
|
||||
scrollToY(y, offset, animate) {
|
||||
this.scrollTop = this.oldScrollTop;
|
||||
this.$nextTick(() => {
|
||||
this._scrollToY(y, offset, animate);
|
||||
})
|
||||
},
|
||||
// x轴滚动到指定位置(非页面滚动且在vue中有效)。x为与左侧的距离,单位为px;offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
|
||||
scrollToX(x, offset, animate) {
|
||||
this.scrollLeft = this.oldScrollLeft;
|
||||
this.$nextTick(() => {
|
||||
this._scrollToX(x, offset, animate);
|
||||
})
|
||||
},
|
||||
// 滚动到指定view(nvue中和虚拟列表中有效)。index为需要滚动的view的index(第几个,从0开始);offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
|
||||
scrollIntoViewByIndex(index, offset, animate) {
|
||||
if (index >= this.realTotalData.length) {
|
||||
u.consoleErr('当前滚动的index超出已渲染列表长度,请先通过refreshToPage加载到对应index页并等待渲染成功后再调用此方法!');
|
||||
return;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
// #ifdef APP-NVUE
|
||||
// 在nvue中,根据index获取对应节点信息并滚动到此节点位置
|
||||
this._scrollIntoView(index, offset, animate);
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
if (this.finalUseVirtualList) {
|
||||
const isCellFixed = this.cellHeightMode === Enum.CellHeightMode.Fixed;
|
||||
u.delay(() => {
|
||||
if (this.finalUseVirtualList) {
|
||||
// 虚拟列表 + 每个cell高度完全相同模式下,此时滚动到对应index的cell就是滚动到scrollTop = cellHeight * index的位置
|
||||
// 虚拟列表 + 高度是动态非固定的模式下,此时滚动到对应index的cell就是滚动到scrollTop = 缓存的cell高度数组中第index个的lastTotalHeight的位置
|
||||
const scrollTop = isCellFixed ? this.virtualCellHeight * index : this.virtualHeightCacheList[index].lastTotalHeight;
|
||||
this.scrollToY(scrollTop, offset, animate);
|
||||
}
|
||||
}, isCellFixed ? 0 : 100)
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
},
|
||||
// 滚动到指定view(nvue中有效)。view为需要滚动的view(通过`this.$refs.xxx`获取),不包含"#";offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
|
||||
scrollIntoViewByView(view, offset, animate) {
|
||||
this._scrollIntoView(view, offset, animate);
|
||||
},
|
||||
// 当使用页面滚动并且自定义下拉刷新时,请在页面的onPageScroll中调用此方法,告知z-paging当前的pageScrollTop,否则会导致在任意位置都可以下拉刷新
|
||||
updatePageScrollTop(value) {
|
||||
this.pageScrollTop = value;
|
||||
},
|
||||
// 当使用页面滚动并且设置了slot="top"时,默认初次加载会自动获取其高度,并使内部容器下移,当slot="top"的view高度动态改变时,在其高度需要更新时调用此方法
|
||||
updatePageScrollTopHeight() {
|
||||
this._updatePageScrollTopOrBottomHeight('top');
|
||||
},
|
||||
// 当使用页面滚动并且设置了slot="bottom"时,默认初次加载会自动获取其高度,并使内部容器下移,当slot="bottom"的view高度动态改变时,在其高度需要更新时调用此方法
|
||||
updatePageScrollBottomHeight() {
|
||||
this._updatePageScrollTopOrBottomHeight('bottom');
|
||||
},
|
||||
// 更新slot="left"和slot="right"宽度,当slot="left"或slot="right"宽度动态改变时调用
|
||||
updateLeftAndRightWidth() {
|
||||
if (!this.finalIsOldWebView) return;
|
||||
this.$nextTick(() => this._updateLeftAndRightWidth(this.scrollViewContainerStyle, 'zp-page'));
|
||||
},
|
||||
// 更新z-paging内置scroll-view的scrollTop
|
||||
updateScrollViewScrollTop(scrollTop, animate = true) {
|
||||
this._updatePrivateScrollWithAnimation(animate);
|
||||
this.scrollTop = this.oldScrollTop;
|
||||
this.$nextTick(() => {
|
||||
this.scrollTop = scrollTop;
|
||||
this.oldScrollTop = this.scrollTop;
|
||||
});
|
||||
},
|
||||
|
||||
// 当滚动到顶部时
|
||||
_onScrollToUpper() {
|
||||
this._emitScrollEvent('scrolltoupper');
|
||||
this.$emit('scrollTopChange', 0);
|
||||
this.$nextTick(() => {
|
||||
this.oldScrollTop = 0;
|
||||
})
|
||||
},
|
||||
// 当滚动到底部时
|
||||
_onScrollToLower(e) {
|
||||
(!e.detail || !e.detail.direction || e.detail.direction === 'bottom')
|
||||
&& this.toBottomLoadingMoreEnabled
|
||||
&& this._onLoadingMore(this.useChatRecordMode ? 'click' : 'toBottom')
|
||||
},
|
||||
// 滚动到顶部
|
||||
_scrollToTop(animate = true, isPrivate = true) {
|
||||
// #ifdef APP-NVUE
|
||||
// 在nvue中需要通过weex.scrollToElement滚动到顶部,此时在顶部插入了一个view,使得滚动到这个view位置
|
||||
const el = this.$refs['zp-n-list-top-tag'];
|
||||
if (this.usePageScroll) {
|
||||
this._getNodeClientRect('zp-page-scroll-top', false).then(node => {
|
||||
const nodeHeight = node ? node[0].height : 0;
|
||||
weexDom.scrollToElement(el, {
|
||||
offset: -nodeHeight,
|
||||
animated: animate
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (!this.isIos && this.nvueListIs === 'scroller') {
|
||||
this._getNodeClientRect('zp-n-refresh-container', false).then(node => {
|
||||
const nodeHeight = node ? node[0].height : 0;
|
||||
weexDom.scrollToElement(el, {
|
||||
offset: -nodeHeight,
|
||||
animated: animate
|
||||
});
|
||||
});
|
||||
} else {
|
||||
weexDom.scrollToElement(el, {
|
||||
offset: 0,
|
||||
animated: animate
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
// #endif
|
||||
if (this.usePageScroll) {
|
||||
this.$nextTick(() => {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: 0,
|
||||
duration: animate ? 100 : 0,
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._updatePrivateScrollWithAnimation(animate);
|
||||
this.scrollTop = this.oldScrollTop;
|
||||
this.$nextTick(() => {
|
||||
this.scrollTop = 0;
|
||||
this.oldScrollTop = this.scrollTop;
|
||||
});
|
||||
},
|
||||
// 滚动到底部
|
||||
async _scrollToBottom(animate = true) {
|
||||
// #ifdef APP-NVUE
|
||||
// 在nvue中需要通过weex.scrollToElement滚动到顶部,此时在底部插入了一个view,使得滚动到这个view位置
|
||||
const el = this.$refs['zp-n-list-bottom-tag'];
|
||||
if (el) {
|
||||
weexDom.scrollToElement(el, {
|
||||
offset: 0,
|
||||
animated: animate
|
||||
});
|
||||
} else {
|
||||
u.consoleErr('滚动到底部失败,因为您设置了hideNvueBottomTag为true');
|
||||
}
|
||||
return;
|
||||
// #endif
|
||||
if (this.usePageScroll) {
|
||||
this.$nextTick(() => {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: Number.MAX_VALUE,
|
||||
duration: animate ? 100 : 0,
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._updatePrivateScrollWithAnimation(animate);
|
||||
const pagingContainerNode = await this._getNodeClientRect('.zp-paging-container');
|
||||
const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
|
||||
const pagingContainerH = pagingContainerNode ? pagingContainerNode[0].height : 0;
|
||||
const scrollViewH = scrollViewNode ? scrollViewNode[0].height : 0;
|
||||
if (pagingContainerH > scrollViewH) {
|
||||
this.scrollTop = this.oldScrollTop;
|
||||
this.$nextTick(() => {
|
||||
this.scrollTop = pagingContainerH - scrollViewH + this.virtualPlaceholderTopHeight;
|
||||
this.oldScrollTop = this.scrollTop;
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
// 滚动到指定view
|
||||
_scrollIntoView(sel, offset = 0, animate = false, finishCallback) {
|
||||
try {
|
||||
this.scrollTop = this.oldScrollTop;
|
||||
this.$nextTick(() => {
|
||||
// #ifdef APP-NVUE
|
||||
const refs = this.$parent.$refs;
|
||||
if (!refs) return;
|
||||
const dataType = Object.prototype.toString.call(sel);
|
||||
let el = null;
|
||||
if (dataType === '[object Number]') {
|
||||
const els = refs[`z-paging-${sel}`];
|
||||
el = els ? els[0] : null;
|
||||
} else if (dataType === '[object Array]') {
|
||||
el = sel[0];
|
||||
} else {
|
||||
el = sel;
|
||||
}
|
||||
if (el) {
|
||||
weexDom.scrollToElement(el, {
|
||||
offset: -offset,
|
||||
animated: animate
|
||||
});
|
||||
} else {
|
||||
u.consoleErr('在nvue中滚动到指定位置,cell必须设置 :ref="`z-paging-${index}`"');
|
||||
}
|
||||
return;
|
||||
// #endif
|
||||
this._getNodeClientRect('#' + sel.replace('#', ''), this.$parent).then((node) => {
|
||||
if (node) {
|
||||
let nodeTop = node[0].top;
|
||||
this._scrollIntoViewByNodeTop(nodeTop, offset, animate);
|
||||
finishCallback && finishCallback();
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {}
|
||||
},
|
||||
// 通过nodeTop滚动到指定view
|
||||
_scrollIntoViewByNodeTop(nodeTop, offset = 0, animate = false) {
|
||||
// 如果是聊天记录模式并且列表倒置了,此时nodeTop需要等于scroll-view高度 - nodeTop
|
||||
if (this.isChatRecordModeAndInversion) {
|
||||
this._getNodeClientRect('.zp-scroll-view').then(sNode => {
|
||||
if (sNode) {
|
||||
this._scrollToY(sNode[0].height - nodeTop, offset, animate, true);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this._scrollToY(nodeTop, offset, animate, true);
|
||||
}
|
||||
},
|
||||
// y轴滚动到指定位置
|
||||
_scrollToY(y, offset = 0, animate = false, addScrollTop = false) {
|
||||
this._updatePrivateScrollWithAnimation(animate);
|
||||
u.delay(() => {
|
||||
if (this.usePageScroll) {
|
||||
if (addScrollTop && this.pageScrollTop !== -1) {
|
||||
y += this.pageScrollTop;
|
||||
}
|
||||
const scrollTop = y - offset;
|
||||
uni.pageScrollTo({
|
||||
scrollTop,
|
||||
duration: animate ? 100 : 0
|
||||
});
|
||||
} else {
|
||||
if (addScrollTop) {
|
||||
y += this.oldScrollTop;
|
||||
}
|
||||
this.scrollTop = y - offset;
|
||||
}
|
||||
}, 10)
|
||||
},
|
||||
// x轴滚动到指定位置
|
||||
_scrollToX(x, offset = 0, animate = false) {
|
||||
this._updatePrivateScrollWithAnimation(animate);
|
||||
u.delay(() => {
|
||||
if (!this.usePageScroll) {
|
||||
this.scrollLeft = x - offset;
|
||||
} else {
|
||||
u.consoleErr('使用页面滚动时不支持scrollToX');
|
||||
}
|
||||
}, 10)
|
||||
},
|
||||
// scroll-view滚动中
|
||||
_scroll(e) {
|
||||
this.$emit('scroll', e);
|
||||
const { scrollTop, scrollLeft } = e.detail;
|
||||
// #ifndef APP-NVUE
|
||||
this.finalUseVirtualList && this._updateVirtualScroll(scrollTop, this.oldScrollTop - scrollTop);
|
||||
// #endif
|
||||
this.oldScrollTop = scrollTop;
|
||||
this.oldScrollLeft = scrollLeft;
|
||||
// 滚动区域内容的总高度 - 当前滚动的scrollTop = 当前滚动区域的顶部与内容底部的距离
|
||||
const scrollDiff = e.detail.scrollHeight - this.oldScrollTop;
|
||||
// 在非ios平台滚动中,再次验证一下是否滚动到了底部。因为在一些安卓设备中,有概率滚动到底部不触发@scrolltolower事件,因此添加双重检测逻辑
|
||||
!this.isIos && this._checkScrolledToBottom(scrollDiff);
|
||||
},
|
||||
// emit scrolltolower/scrolltoupper事件
|
||||
_emitScrollEvent(type) {
|
||||
const reversedType = type === 'scrolltolower' ? 'scrolltoupper' : 'scrolltolower';
|
||||
const eventType = this.useChatRecordMode && !this.isChatRecordModeAndNotInversion
|
||||
? reversedType
|
||||
: type;
|
||||
|
||||
this.$emit(eventType);
|
||||
},
|
||||
// 更新内置的scroll-view是否启用滚动动画
|
||||
_updatePrivateScrollWithAnimation(animate) {
|
||||
this.privateScrollWithAnimation = animate ? 1 : 0;
|
||||
u.delay(() => this.$nextTick(() => {
|
||||
// 在滚动结束后将滚动动画状态设置回初始状态
|
||||
this.privateScrollWithAnimation = -1;
|
||||
}), 100, 'updateScrollWithAnimationDelay')
|
||||
},
|
||||
// 检测scrollView是否要铺满屏幕
|
||||
_doCheckScrollViewShouldFullHeight(totalData) {
|
||||
if (this.autoFullHeight && this.usePageScroll && this.isTotalChangeFromAddData) {
|
||||
// #ifndef APP-NVUE
|
||||
this.$nextTick(() => {
|
||||
this._checkScrollViewShouldFullHeight((scrollViewNode, pagingContainerNode) => {
|
||||
this._preCheckShowNoMoreInside(totalData, scrollViewNode, pagingContainerNode)
|
||||
});
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-NVUE
|
||||
this._preCheckShowNoMoreInside(totalData)
|
||||
// #endif
|
||||
} else {
|
||||
this._preCheckShowNoMoreInside(totalData)
|
||||
}
|
||||
},
|
||||
// 检测z-paging是否要全屏覆盖(当使用页面滚动并且不满全屏时,默认z-paging需要铺满全屏,避免数据过少时内部的empty-view无法正确展示)
|
||||
async _checkScrollViewShouldFullHeight(callback) {
|
||||
try {
|
||||
const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
|
||||
const pagingContainerNode = await this._getNodeClientRect('.zp-paging-container-content');
|
||||
if (!scrollViewNode || !pagingContainerNode) return;
|
||||
const scrollViewHeight = pagingContainerNode[0].height;
|
||||
const scrollViewTop = scrollViewNode[0].top;
|
||||
if (this.isAddedData && scrollViewHeight + scrollViewTop <= this.windowHeight) {
|
||||
this._setAutoHeight(true, scrollViewNode);
|
||||
callback(scrollViewNode, pagingContainerNode);
|
||||
} else {
|
||||
this._setAutoHeight(false);
|
||||
callback(null, null);
|
||||
}
|
||||
} catch (e) {
|
||||
callback(null, null);
|
||||
}
|
||||
},
|
||||
// 更新缓存中z-paging整个内容容器高度
|
||||
async _updateCachedSuperContentHeight() {
|
||||
const superContentNode = await this._getNodeClientRect('.z-paging-content');
|
||||
if (superContentNode) {
|
||||
this.superContentHeight = superContentNode[0].height;
|
||||
}
|
||||
},
|
||||
// scrollTop改变时触发
|
||||
_scrollTopChange(newVal, isPageScrollTop){
|
||||
this.$emit('scrollTopChange', newVal);
|
||||
this.$emit('update:scrollTop', newVal);
|
||||
this._checkShouldShowBackToTop(newVal);
|
||||
// 之前在安卓中scroll-view有概率滚动到顶部时scrollTop不为0导致下拉刷新判断异常,因此判断scrollTop在105之内都允许下拉刷新,但此方案会导致某些情况(例如滚动到距离顶部10px处)下拉抖动,因此改为通过获取zp-scroll-view的节点信息中的scrollTop进行验证的方案
|
||||
// const scrollTop = this.isIos ? (newVal > 5 ? 6 : 0) : (newVal > 105 ? 106 : (newVal > 5 ? 6 : 0));
|
||||
const scrollTop = newVal > 5 ? 6 : 0;
|
||||
if (isPageScrollTop && this.wxsPageScrollTop !== scrollTop) {
|
||||
this.wxsPageScrollTop = scrollTop;
|
||||
} else if (!isPageScrollTop && this.wxsScrollTop !== scrollTop) {
|
||||
this.wxsScrollTop = scrollTop;
|
||||
if (scrollTop > 6) {
|
||||
this.scrollEnable = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 更新使用页面滚动时slot="top"或"bottom"插入view的高度
|
||||
_updatePageScrollTopOrBottomHeight(type) {
|
||||
// #ifndef APP-NVUE
|
||||
if (!this.usePageScroll) return;
|
||||
// #endif
|
||||
this._doCheckScrollViewShouldFullHeight(this.realTotalData);
|
||||
const node = `.zp-page-${type}`;
|
||||
const marginText = `margin${type.slice(0,1).toUpperCase() + type.slice(1)}`;
|
||||
let safeAreaInsetBottomAdd = this.safeAreaInsetBottom;
|
||||
this.$nextTick(() => {
|
||||
let delayTime = 0;
|
||||
// #ifdef MP-BAIDU || APP-NVUE
|
||||
delayTime = 50;
|
||||
// #endif
|
||||
u.delay(() => {
|
||||
this._getNodeClientRect(node).then((res) => {
|
||||
if (res) {
|
||||
let pageScrollNodeHeight = res[0].height;
|
||||
if (type === 'bottom') {
|
||||
if (safeAreaInsetBottomAdd) {
|
||||
pageScrollNodeHeight += this.safeAreaBottom;
|
||||
}
|
||||
} else {
|
||||
this.cacheTopHeight = pageScrollNodeHeight;
|
||||
}
|
||||
this.$set(this.scrollViewStyle, marginText, `${pageScrollNodeHeight}px`);
|
||||
} else if (safeAreaInsetBottomAdd) {
|
||||
this.$set(this.scrollViewStyle, marginText, `${this.safeAreaBottom}px`);
|
||||
}
|
||||
});
|
||||
}, delayTime)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
// [z-paging]虚拟列表模块
|
||||
import u from '.././z-paging-utils'
|
||||
import c from '.././z-paging-constant'
|
||||
import Enum from '.././z-paging-enum'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 是否使用虚拟列表,默认为否
|
||||
useVirtualList: {
|
||||
type: Boolean,
|
||||
default: u.gc('useVirtualList', false)
|
||||
},
|
||||
// 在使用虚拟列表时,是否使用兼容模式,默认为否
|
||||
useCompatibilityMode: {
|
||||
type: Boolean,
|
||||
default: u.gc('useCompatibilityMode', false)
|
||||
},
|
||||
// 使用兼容模式时传递的附加数据
|
||||
extraData: {
|
||||
type: Object,
|
||||
default: u.gc('extraData', {})
|
||||
},
|
||||
// 是否在z-paging内部循环渲染列表(内置列表),默认为否。若use-virtual-list为true,则此项恒为true
|
||||
useInnerList: {
|
||||
type: Boolean,
|
||||
default: u.gc('useInnerList', false)
|
||||
},
|
||||
// 强制关闭inner-list,默认为false,如果为true将强制关闭innerList,适用于开启了虚拟列表后需要强制关闭inner-list的情况
|
||||
forceCloseInnerList: {
|
||||
type: Boolean,
|
||||
default: u.gc('forceCloseInnerList', false)
|
||||
},
|
||||
// 内置列表cell的key名称,仅nvue有效,在nvue中开启use-inner-list时必须填此项
|
||||
cellKeyName: {
|
||||
type: String,
|
||||
default: u.gc('cellKeyName', '')
|
||||
},
|
||||
// innerList样式
|
||||
innerListStyle: {
|
||||
type: Object,
|
||||
default: u.gc('innerListStyle', {})
|
||||
},
|
||||
// innerCell样式
|
||||
innerCellStyle: {
|
||||
type: Object,
|
||||
default: u.gc('innerCellStyle', {})
|
||||
},
|
||||
// 预加载的列表可视范围(列表高度)页数,默认为12,即预加载当前页及上下各12页的cell。此数值越大,则虚拟列表中加载的dom越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
|
||||
preloadPage: {
|
||||
type: [Number, String],
|
||||
default: u.gc('preloadPage', 12),
|
||||
validator: (value) => {
|
||||
if (value <= 0) u.consoleErr('preload-page必须大于0!');
|
||||
return value > 0;
|
||||
}
|
||||
},
|
||||
// 虚拟列表cell高度模式,默认为fixed,也就是每个cell高度完全相同,将以第一个cell高度为准进行计算。可选值【dynamic】,即代表高度是动态非固定的,【dynamic】性能低于【fixed】。
|
||||
cellHeightMode: {
|
||||
type: String,
|
||||
default: u.gc('cellHeightMode', Enum.CellHeightMode.Fixed)
|
||||
},
|
||||
// 固定的cell高度,cellHeightMode=fixed才有效,若设置了值,则不计算第一个cell高度而使用设置的cell高度
|
||||
fixedCellHeight: {
|
||||
type: [Number, String],
|
||||
default: u.gc('fixedCellHeight', 0)
|
||||
},
|
||||
// 虚拟列表列数,默认为1。常用于每行有多列的情况,例如每行有2列数据,需要将此值设置为2
|
||||
virtualListCol: {
|
||||
type: [Number, String],
|
||||
default: u.gc('virtualListCol', 1)
|
||||
},
|
||||
// 虚拟列表scroll取样帧率,默认为80,过低容易出现白屏问题,过高容易出现卡顿问题
|
||||
virtualScrollFps: {
|
||||
type: [Number, String],
|
||||
default: u.gc('virtualScrollFps', 80)
|
||||
},
|
||||
// 虚拟列表cell id的前缀,适用于一个页面有多个虚拟列表的情况,用以区分不同虚拟列表cell的id,注意:请勿传数字或以数字开头的字符串。如设置为list1,则cell的id应为:list1-zp-id-${item.zp_index}
|
||||
virtualCellIdPrefix: {
|
||||
type: String,
|
||||
default: u.gc('virtualCellIdPrefix', '')
|
||||
},
|
||||
// 虚拟列表是否使用swiper-item包裹,默认为否,此属性为了解决vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在swiper-item内存在无法获取slot插入的cell高度进而导致虚拟列表失败的问题
|
||||
// 仅vue3+(微信小程序或QQ小程序)+非内置列表写法虚拟列表有效,其他情况此属性设置任何值都无效,所以如果您在swiper-item内使用z-paging的非内置虚拟列表写法,将此属性设置为true即可
|
||||
virtualInSwiperSlot: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
virtualListKey: u.getInstanceId(),
|
||||
virtualPageHeight: 0,
|
||||
virtualCellHeight: 0,
|
||||
virtualScrollTimeStamp: 0,
|
||||
|
||||
virtualList: [],
|
||||
virtualPlaceholderTopHeight: 0,
|
||||
virtualPlaceholderBottomHeight: 0,
|
||||
virtualTopRangeIndex: 0,
|
||||
virtualBottomRangeIndex: 0,
|
||||
lastVirtualTopRangeIndex: 0,
|
||||
lastVirtualBottomRangeIndex: 0,
|
||||
virtualItemInsertedCount: 0,
|
||||
|
||||
virtualHeightCacheList: [],
|
||||
|
||||
getCellHeightRetryCount: {
|
||||
fixed: 0,
|
||||
dynamic: 0
|
||||
},
|
||||
pagingOrgTop: -1,
|
||||
updateVirtualListFromDataChange: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听总数据的改变,刷新虚拟列表布局
|
||||
realTotalData() {
|
||||
this.updateVirtualListRender();
|
||||
},
|
||||
// 监听虚拟列表渲染数组的改变并emit
|
||||
virtualList(newVal){
|
||||
this.$emit('update:virtualList', newVal);
|
||||
this.$emit('virtualListChange', newVal);
|
||||
},
|
||||
// 监听虚拟列表顶部占位高度改变并emit
|
||||
virtualPlaceholderTopHeight(newVal) {
|
||||
this.$emit('virtualTopHeightChange', newVal);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
virtualCellIndexKey() {
|
||||
return c.listCellIndexKey;
|
||||
},
|
||||
finalUseVirtualList() {
|
||||
if (this.useVirtualList && this.usePageScroll){
|
||||
u.consoleErr('使用页面滚动时,开启虚拟列表无效!');
|
||||
}
|
||||
return this.useVirtualList && !this.usePageScroll;
|
||||
},
|
||||
finalUseInnerList() {
|
||||
return this.useInnerList || (this.finalUseVirtualList && !this.forceCloseInnerList);
|
||||
},
|
||||
finalCellKeyName() {
|
||||
// #ifdef APP-NVUE
|
||||
if (this.finalUseVirtualList && !this.cellKeyName.length){
|
||||
u.consoleErr('在nvue中开启use-virtual-list必须设置cell-key-name,否则将可能导致列表渲染错误!');
|
||||
}
|
||||
// #endif
|
||||
return this.cellKeyName;
|
||||
},
|
||||
finalVirtualPageHeight(){
|
||||
return this.virtualPageHeight > 0 ? this.virtualPageHeight : this.windowHeight;
|
||||
},
|
||||
finalFixedCellHeight() {
|
||||
return u.convertToPx(this.fixedCellHeight);
|
||||
},
|
||||
fianlVirtualCellIdPrefix() {
|
||||
const prefix = this.virtualCellIdPrefix ? this.virtualCellIdPrefix + '-' : '';
|
||||
return prefix + 'zp-id';
|
||||
},
|
||||
finalPlaceholderTopHeightStyle() {
|
||||
// #ifdef VUE2
|
||||
return { transform: this.virtualPlaceholderTopHeight > 0 ? `translateY(${this.virtualPlaceholderTopHeight}px)` : 'none' };
|
||||
// #endif
|
||||
return {};
|
||||
},
|
||||
virtualRangePageHeight(){
|
||||
return this.finalVirtualPageHeight * this.preloadPage;
|
||||
},
|
||||
virtualScrollDisTimeStamp() {
|
||||
return 1000 / this.virtualScrollFps;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 在使用动态高度虚拟列表时,若在列表数组中需要插入某个item,需要调用此方法;item:需要插入的item,index:插入的cell位置,若index为2,则插入的item在原list的index=1之后,index从0开始
|
||||
doInsertVirtualListItem(item, index) {
|
||||
if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
|
||||
this.realTotalData.splice(index, 0, item);
|
||||
// #ifdef VUE3
|
||||
this.realTotalData = [...this.realTotalData];
|
||||
// #endif
|
||||
this.virtualItemInsertedCount ++;
|
||||
if (!item || Object.prototype.toString.call(item) !== '[object Object]') {
|
||||
item = { item };
|
||||
}
|
||||
const cellIndexKey = this.virtualCellIndexKey;
|
||||
item[cellIndexKey] = `custom-${this.virtualItemInsertedCount}`;
|
||||
item[c.listCellIndexUniqueKey] = `${this.virtualListKey}-${item[cellIndexKey]}`;
|
||||
this.$nextTick(async () => {
|
||||
let retryCount = 0;
|
||||
while (retryCount <= 10) {
|
||||
await u.wait(c.delayTime);
|
||||
|
||||
const cellNode = await this._getVirtualCellNodeByIndex(item[cellIndexKey]);
|
||||
// 如果获取当前cell的节点信息失败,则重试(不超过10次)
|
||||
if (!cellNode) {
|
||||
retryCount ++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentHeight = cellNode ? cellNode[0].height : 0;
|
||||
const lastHeightCache = this.virtualHeightCacheList[index - 1];
|
||||
const lastTotalHeight = lastHeightCache ? lastHeightCache.totalHeight : 0;
|
||||
// 在缓存的cell高度数组中,插入此cell高度信息
|
||||
this.virtualHeightCacheList.splice(index, 0, {
|
||||
height: currentHeight,
|
||||
lastTotalHeight,
|
||||
totalHeight: lastTotalHeight + currentHeight
|
||||
});
|
||||
|
||||
// 从当前index起后续的cell缓存高度的lastTotalHeight和totalHeight需要加上当前cell的高度
|
||||
for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
|
||||
const thisNode = this.virtualHeightCacheList[i];
|
||||
thisNode.lastTotalHeight += currentHeight;
|
||||
thisNode.totalHeight += currentHeight;
|
||||
}
|
||||
|
||||
this._updateVirtualScroll(this.oldScrollTop);
|
||||
break;
|
||||
}
|
||||
})
|
||||
},
|
||||
// 在使用动态高度虚拟列表时,手动更新指定cell的缓存高度(当cell高度在初始化之后再次改变后调用);index:需要更新的cell在列表中的位置,从0开始
|
||||
didUpdateVirtualListCell(index) {
|
||||
if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
|
||||
const currentNode = this.virtualHeightCacheList[index];
|
||||
this.$nextTick(() => {
|
||||
this._getVirtualCellNodeByIndex(index).then(cellNode => {
|
||||
// 更新当前cell的高度
|
||||
const cellNodeHeight = cellNode ? cellNode[0].height : 0;
|
||||
const heightDis = cellNodeHeight - currentNode.height;
|
||||
currentNode.height = cellNodeHeight;
|
||||
currentNode.totalHeight = currentNode.lastTotalHeight + cellNodeHeight;
|
||||
|
||||
// 从当前index起后续的cell缓存高度的lastTotalHeight和totalHeight需要加上当前cell变化的高度
|
||||
for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
|
||||
const thisNode = this.virtualHeightCacheList[i];
|
||||
thisNode.totalHeight += heightDis;
|
||||
thisNode.lastTotalHeight += heightDis;
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
// 在使用动态高度虚拟列表时,若删除了列表数组中的某个item,需要调用此方法以更新高度缓存数组;index:删除的cell在列表中的位置,从0开始
|
||||
didDeleteVirtualListCell(index) {
|
||||
if (this.cellHeightMode !== Enum.CellHeightMode.Dynamic) return;
|
||||
const currentNode = this.virtualHeightCacheList[index];
|
||||
// 从当前index起后续的cell缓存高度的lastTotalHeight和totalHeight需要减去当前cell的高度
|
||||
for (let i = index + 1; i < this.virtualHeightCacheList.length; i++) {
|
||||
const thisNode = this.virtualHeightCacheList[i];
|
||||
thisNode.totalHeight -= currentNode.height;
|
||||
thisNode.lastTotalHeight -= currentNode.height;
|
||||
}
|
||||
// 将当前cell的高度信息从高度缓存数组中删除
|
||||
this.virtualHeightCacheList.splice(index, 1);
|
||||
},
|
||||
// 手动触发虚拟列表渲染更新,可用于解决例如修改了虚拟列表数组中元素,但展示未更新的情况
|
||||
updateVirtualListRender() {
|
||||
// #ifndef APP-NVUE
|
||||
if (this.finalUseVirtualList) {
|
||||
this.updateVirtualListFromDataChange = true;
|
||||
this.$nextTick(() => {
|
||||
this.getCellHeightRetryCount.fixed = 0;
|
||||
if (this.realTotalData.length) {
|
||||
this.cellHeightMode === Enum.CellHeightMode.Fixed && this.isFirstPage && this._updateFixedCellHeight()
|
||||
} else {
|
||||
this._resetDynamicListState(!this.isUserPullDown);
|
||||
}
|
||||
this._updateVirtualScroll(this.oldScrollTop);
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
// 初始化虚拟列表
|
||||
_virtualListInit() {
|
||||
this.$nextTick(() => {
|
||||
u.delay(() => {
|
||||
// 获取虚拟列表滚动区域的高度
|
||||
this._getNodeClientRect('.zp-scroll-view').then(node => {
|
||||
if (node) {
|
||||
this.pagingOrgTop = node[0].top;
|
||||
this.virtualPageHeight = node[0].height;
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
},
|
||||
// cellHeightMode为fixed时获取第一个cell高度
|
||||
_updateFixedCellHeight() {
|
||||
if (!this.finalFixedCellHeight) {
|
||||
this.$nextTick(() => {
|
||||
u.delay(() => {
|
||||
this._getVirtualCellNodeByIndex(0).then(cellNode => {
|
||||
if (!cellNode) {
|
||||
if (this.getCellHeightRetryCount.fixed > 10) return;
|
||||
this.getCellHeightRetryCount.fixed ++;
|
||||
// 如果获取第一个cell的节点信息失败,则重试(不超过10次)
|
||||
this._updateFixedCellHeight();
|
||||
} else {
|
||||
this.virtualCellHeight = cellNode[0].height;
|
||||
this._updateVirtualScroll(this.oldScrollTop);
|
||||
}
|
||||
});
|
||||
}, c.delayTime, 'updateFixedCellHeightDelay');
|
||||
})
|
||||
} else {
|
||||
this.virtualCellHeight = this.finalFixedCellHeight;
|
||||
}
|
||||
},
|
||||
// cellHeightMode为dynamic时获取每个cell高度
|
||||
_updateDynamicCellHeight(list, dataFrom = 'bottom') {
|
||||
const dataFromTop = dataFrom === 'top';
|
||||
const heightCacheList = this.virtualHeightCacheList;
|
||||
const currentCacheList = dataFromTop ? [] : heightCacheList;
|
||||
let listTotalHeight = 0;
|
||||
this.$nextTick(() => {
|
||||
u.delay(async () => {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const cellNode = await this._getVirtualCellNodeByIndex(list[i][this.virtualCellIndexKey]);
|
||||
const currentHeight = cellNode ? cellNode[0].height : 0;
|
||||
if (!cellNode) {
|
||||
if (this.getCellHeightRetryCount.dynamic <= 10) {
|
||||
heightCacheList.splice(heightCacheList.length - i, i);
|
||||
this.getCellHeightRetryCount.dynamic ++;
|
||||
// 如果获取当前cell的节点信息失败,则重试(不超过10次)
|
||||
this._updateDynamicCellHeight(list, dataFrom);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const lastHeightCache = currentCacheList.length ? currentCacheList.slice(-1)[0] : null;
|
||||
const lastTotalHeight = lastHeightCache ? lastHeightCache.totalHeight : 0;
|
||||
// 缓存当前cell的高度信息:height-当前cell高度;lastTotalHeight-前面所有cell的高度总和;totalHeight-包含当前cell的所有高度总和
|
||||
currentCacheList.push({
|
||||
height: currentHeight,
|
||||
lastTotalHeight,
|
||||
totalHeight: lastTotalHeight + currentHeight
|
||||
});
|
||||
if (dataFromTop) {
|
||||
listTotalHeight += currentHeight;
|
||||
}
|
||||
}
|
||||
// 如果数据是从顶部拼接的
|
||||
if (dataFromTop && list.length) {
|
||||
for (let i = 0; i < heightCacheList.length; i++) {
|
||||
// 更新之前所有项的缓存高度,需要加上此次插入的所有cell高度之和(因为是从顶部插入的cell)
|
||||
const heightCacheItem = heightCacheList[i];
|
||||
heightCacheItem.lastTotalHeight += listTotalHeight;
|
||||
heightCacheItem.totalHeight += listTotalHeight;
|
||||
}
|
||||
this.virtualHeightCacheList = currentCacheList.concat(heightCacheList);
|
||||
}
|
||||
this._updateVirtualScroll(this.oldScrollTop);
|
||||
}, c.delayTime, 'updateDynamicCellHeightDelay')
|
||||
})
|
||||
},
|
||||
// 设置cellItem的index
|
||||
_setCellIndex(list, dataFrom = 'bottom') {
|
||||
let currentItemIndex = 0;
|
||||
const cellIndexKey = this.virtualCellIndexKey;
|
||||
dataFrom === 'bottom' && ([Enum.QueryFrom.Refresh, Enum.QueryFrom.Reload].indexOf(this.queryFrom) >= 0) && this._resetDynamicListState();
|
||||
if (this.totalData.length && this.queryFrom !== Enum.QueryFrom.Refresh) {
|
||||
if (dataFrom === 'bottom') {
|
||||
currentItemIndex = this.realTotalData.length;
|
||||
const lastItem = this.realTotalData.length ? this.realTotalData.slice(-1)[0] : null;
|
||||
if (lastItem && lastItem[cellIndexKey] !== undefined) {
|
||||
currentItemIndex = lastItem[cellIndexKey] + 1;
|
||||
}
|
||||
} else if (dataFrom === 'top') {
|
||||
const firstItem = this.realTotalData.length ? this.realTotalData[0] : null;
|
||||
if (firstItem && firstItem[cellIndexKey] !== undefined) {
|
||||
currentItemIndex = firstItem[cellIndexKey] - list.length;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._resetDynamicListState();
|
||||
}
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let item = list[i];
|
||||
if (!item || Object.prototype.toString.call(item) !== '[object Object]') {
|
||||
item = { item };
|
||||
}
|
||||
if (item[c.listCellIndexUniqueKey]) {
|
||||
item = u.deepCopy(item);
|
||||
}
|
||||
item[cellIndexKey] = currentItemIndex + i;
|
||||
item[c.listCellIndexUniqueKey] = `${this.virtualListKey}-${item[cellIndexKey]}`;
|
||||
list[i] = item;
|
||||
}
|
||||
this.getCellHeightRetryCount.dynamic = 0;
|
||||
this.cellHeightMode === Enum.CellHeightMode.Dynamic && this._updateDynamicCellHeight(list, dataFrom);
|
||||
},
|
||||
// 更新scroll滚动(虚拟列表滚动时触发)
|
||||
_updateVirtualScroll(scrollTop, scrollDiff = 0) {
|
||||
const currentTimeStamp = u.getTime();
|
||||
scrollTop === 0 && this._resetTopRange();
|
||||
if (scrollTop !== 0 && this.virtualScrollTimeStamp && currentTimeStamp - this.virtualScrollTimeStamp <= this.virtualScrollDisTimeStamp) {
|
||||
return;
|
||||
}
|
||||
this.virtualScrollTimeStamp = currentTimeStamp;
|
||||
|
||||
let scrollIndex = 0;
|
||||
const cellHeightMode = this.cellHeightMode;
|
||||
if (cellHeightMode === Enum.CellHeightMode.Fixed) {
|
||||
// 如果是固定高度的虚拟列表
|
||||
// 计算当前滚动到的cell的index = scrollTop / 虚拟列表cell的固定高度
|
||||
scrollIndex = parseInt(scrollTop / this.virtualCellHeight) || 0;
|
||||
// 更新顶部和底部占位view的高度(为兼容考虑,顶部采用transformY的方式占位)
|
||||
this._updateFixedTopRangeIndex(scrollIndex);
|
||||
this._updateFixedBottomRangeIndex(scrollIndex);
|
||||
} else if(cellHeightMode === Enum.CellHeightMode.Dynamic) {
|
||||
// 如果是不固定高度的虚拟列表
|
||||
// 当前滚动的方向
|
||||
const scrollDirection = scrollDiff > 0 ? 'top' : 'bottom';
|
||||
// 视图区域的高度
|
||||
const rangePageHeight = this.virtualRangePageHeight;
|
||||
// 顶部视图区域外的高度(顶部不需要渲染而是需要占位部分的高度)
|
||||
const topRangePageOffset = scrollTop - rangePageHeight;
|
||||
// 底部视图区域外的高度(底部不需要渲染而是需要占位部分的高度)
|
||||
const bottomRangePageOffset = scrollTop + this.finalVirtualPageHeight + rangePageHeight;
|
||||
|
||||
let virtualBottomRangeIndex = 0;
|
||||
let virtualPlaceholderBottomHeight = 0;
|
||||
let reachedLimitBottom = false;
|
||||
const heightCacheList = this.virtualHeightCacheList;
|
||||
const lastHeightCache = !!heightCacheList ? heightCacheList.slice(-1)[0] : null;
|
||||
|
||||
let startTopRangeIndex = this.virtualTopRangeIndex;
|
||||
// 如果是向底部滚动(顶部占位的高度不断增大,顶部的实际渲染cell数量不断减少)
|
||||
if (scrollDirection === 'bottom') {
|
||||
// 从顶部视图边缘的cell的位置开始向后查找
|
||||
for (let i = startTopRangeIndex; i < heightCacheList.length; i++){
|
||||
const heightCacheItem = heightCacheList[i];
|
||||
// 如果查找到某个cell对应的totalHeight大于顶部视图区域外的高度,则此cell为顶部视图边缘的cell
|
||||
if (heightCacheItem && heightCacheItem.totalHeight > topRangePageOffset) {
|
||||
// 记录顶部视图边缘cell的index并更新顶部占位区域的高度并停止继续查找
|
||||
this.virtualTopRangeIndex = i;
|
||||
this.virtualPlaceholderTopHeight = heightCacheItem.lastTotalHeight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果是向顶部滚动(顶部占位的高度不断减少,顶部的实际渲染cell数量不断增加)
|
||||
let topRangeMatched = false;
|
||||
// 从顶部视图边缘的cell的位置开始向前查找
|
||||
for (let i = startTopRangeIndex; i >= 0; i--){
|
||||
const heightCacheItem = heightCacheList[i];
|
||||
// 如果查找到某个cell对应的totalHeight小于顶部视图区域外的高度,则此cell为顶部视图边缘的cell
|
||||
if (heightCacheItem && heightCacheItem.totalHeight < topRangePageOffset) {
|
||||
// 记录顶部视图边缘cell的index并更新顶部占位区域的高度并停止继续查找
|
||||
this.virtualTopRangeIndex = i;
|
||||
this.virtualPlaceholderTopHeight = heightCacheItem.lastTotalHeight;
|
||||
topRangeMatched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 如果查找不到,则认为顶部占位高度为0了,顶部cell不需要继续复用,重置topRangeIndex和placeholderTopHeight
|
||||
!topRangeMatched && this._resetTopRange();
|
||||
}
|
||||
// 从顶部视图边缘的cell的位置开始向后查找
|
||||
for (let i = this.virtualTopRangeIndex; i < heightCacheList.length; i++){
|
||||
const heightCacheItem = heightCacheList[i];
|
||||
// 如果查找到某个cell对应的totalHeight大于底部视图区域外的高度,则此cell为底部视图边缘的cell
|
||||
if (heightCacheItem && heightCacheItem.totalHeight > bottomRangePageOffset) {
|
||||
// 记录底部视图边缘cell的index并更新底部占位区域的高度并停止继续查找
|
||||
virtualBottomRangeIndex = i;
|
||||
virtualPlaceholderBottomHeight = lastHeightCache.totalHeight - heightCacheItem.totalHeight;
|
||||
reachedLimitBottom = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!reachedLimitBottom || this.virtualBottomRangeIndex === 0) {
|
||||
this.virtualBottomRangeIndex = this.realTotalData.length ? this.realTotalData.length - 1 : this.pageSize;
|
||||
this.virtualPlaceholderBottomHeight = 0;
|
||||
} else {
|
||||
this.virtualBottomRangeIndex = virtualBottomRangeIndex;
|
||||
this.virtualPlaceholderBottomHeight = virtualPlaceholderBottomHeight;
|
||||
}
|
||||
this._updateVirtualList();
|
||||
}
|
||||
},
|
||||
// 更新fixedCell模式下topRangeIndex&placeholderTopHeight
|
||||
_updateFixedTopRangeIndex(scrollIndex) {
|
||||
let virtualTopRangeIndex = this.virtualCellHeight === 0 ? 0 : scrollIndex - (parseInt(this.finalVirtualPageHeight / this.virtualCellHeight) || 1) * this.preloadPage;
|
||||
virtualTopRangeIndex *= this.virtualListCol;
|
||||
virtualTopRangeIndex = Math.max(0, virtualTopRangeIndex);
|
||||
this.virtualTopRangeIndex = virtualTopRangeIndex;
|
||||
this.virtualPlaceholderTopHeight = (virtualTopRangeIndex / this.virtualListCol) * this.virtualCellHeight;
|
||||
},
|
||||
// 更新fixedCell模式下bottomRangeIndex&placeholderBottomHeight
|
||||
_updateFixedBottomRangeIndex(scrollIndex) {
|
||||
let virtualBottomRangeIndex = this.virtualCellHeight === 0 ? this.pageSize : scrollIndex + (parseInt(this.finalVirtualPageHeight / this.virtualCellHeight) || 1) * (this.preloadPage + 1);
|
||||
virtualBottomRangeIndex *= this.virtualListCol;
|
||||
virtualBottomRangeIndex = Math.min(this.realTotalData.length, virtualBottomRangeIndex);
|
||||
this.virtualBottomRangeIndex = virtualBottomRangeIndex;
|
||||
this.virtualPlaceholderBottomHeight = (this.realTotalData.length - virtualBottomRangeIndex) * this.virtualCellHeight / this.virtualListCol;
|
||||
this._updateVirtualList();
|
||||
},
|
||||
// 更新virtualList
|
||||
_updateVirtualList() {
|
||||
const shouldUpdateList = this.updateVirtualListFromDataChange || (this.lastVirtualTopRangeIndex !== this.virtualTopRangeIndex || this.lastVirtualBottomRangeIndex !== this.virtualBottomRangeIndex);
|
||||
if (shouldUpdateList) {
|
||||
this.updateVirtualListFromDataChange = false;
|
||||
this.lastVirtualTopRangeIndex = this.virtualTopRangeIndex;
|
||||
this.lastVirtualBottomRangeIndex = this.virtualBottomRangeIndex;
|
||||
this.virtualList = this.realTotalData.slice(this.virtualTopRangeIndex, this.virtualBottomRangeIndex + 1);
|
||||
}
|
||||
},
|
||||
// 重置动态cell模式下的高度缓存数据、虚拟列表和滚动状态
|
||||
_resetDynamicListState(resetVirtualList = false) {
|
||||
this.virtualHeightCacheList = [];
|
||||
if (resetVirtualList) {
|
||||
this.virtualList = [];
|
||||
}
|
||||
this.virtualTopRangeIndex = 0;
|
||||
this.virtualPlaceholderTopHeight = 0;
|
||||
},
|
||||
// 重置topRangeIndex和placeholderTopHeight
|
||||
_resetTopRange() {
|
||||
this.virtualTopRangeIndex = 0;
|
||||
this.virtualPlaceholderTopHeight = 0;
|
||||
this._updateVirtualList();
|
||||
},
|
||||
// 检测虚拟列表当前滚动位置,如发现滚动位置不正确则重新计算虚拟列表相关参数(为解决在App中可能出现的长时间进入后台后打开App白屏的问题)
|
||||
_checkVirtualListScroll() {
|
||||
if (this.finalUseVirtualList) {
|
||||
this.$nextTick(() => {
|
||||
this._getNodeClientRect('.zp-paging-touch-view').then(node => {
|
||||
const currentTop = node ? node[0].top : 0;
|
||||
if (!node || (currentTop === this.pagingOrgTop && this.virtualPlaceholderTopHeight !== 0)) {
|
||||
this._updateVirtualScroll(0);
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
},
|
||||
// 获取对应index的虚拟列表cell节点信息
|
||||
_getVirtualCellNodeByIndex(index) {
|
||||
let inDom = this.finalUseInnerList;
|
||||
// 在vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在swiper-item内存在无法获取slot插入的cell高度的问题
|
||||
// 通过uni.createSelectorQuery().in(this.$parent)来解决此问题
|
||||
// #ifdef VUE3
|
||||
// #ifdef MP-WEIXIN || MP-QQ
|
||||
if (this.forceCloseInnerList && this.virtualInSwiperSlot) {
|
||||
inDom = this.$parent;
|
||||
}
|
||||
// #endif
|
||||
// #endif
|
||||
return this._getNodeClientRect(`#${this.fianlVirtualCellIdPrefix}-${index}`, inDom);
|
||||
},
|
||||
// 处理使用内置列表时点击了cell事件
|
||||
_innerCellClick(item, index) {
|
||||
this.$emit('innerCellClick', item, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// [z-paging]常量
|
||||
|
||||
export default {
|
||||
// 当前版本号
|
||||
version: '2.8.6',
|
||||
// 延迟操作的通用时间
|
||||
delayTime: 100,
|
||||
// 请求失败时候全局emit使用的key
|
||||
errorUpdateKey: 'z-paging-error-emit',
|
||||
// 全局emit complete的key
|
||||
completeUpdateKey: 'z-paging-complete-emit',
|
||||
// z-paging缓存的前缀key
|
||||
cachePrefixKey: 'z-paging-cache',
|
||||
|
||||
// 虚拟列表中列表index的key
|
||||
listCellIndexKey: 'zp_index',
|
||||
// 虚拟列表中列表的唯一key
|
||||
listCellIndexUniqueKey: 'zp_unique_index'
|
||||
}
|
||||
45
uni_modules/z-paging/components/z-paging/js/z-paging-enum.js
Normal file
45
uni_modules/z-paging/components/z-paging/js/z-paging-enum.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// [z-paging]枚举
|
||||
|
||||
export default {
|
||||
// 当前加载类型 refresher:下拉刷新 load-more:上拉加载更多
|
||||
LoadingType: {
|
||||
Refresher: 'refresher',
|
||||
LoadMore: 'load-more'
|
||||
},
|
||||
// 下拉刷新状态 default:默认状态 release-to-refresh:松手立即刷新 loading:刷新中 complete:刷新结束 go-f2:松手进入二楼
|
||||
Refresher: {
|
||||
Default: 'default',
|
||||
ReleaseToRefresh: 'release-to-refresh',
|
||||
Loading: 'loading',
|
||||
Complete: 'complete',
|
||||
GoF2: 'go-f2'
|
||||
},
|
||||
// 底部加载更多状态 default:默认状态 loading:加载中 no-more:没有更多数据 fail:加载失败
|
||||
More: {
|
||||
Default: 'default',
|
||||
Loading: 'loading',
|
||||
NoMore: 'no-more',
|
||||
Fail: 'fail'
|
||||
},
|
||||
// @query触发来源 user-pull-down:用户主动下拉刷新 reload:通过reload触发 refresh:通过refresh触发 load-more:通过滚动到底部加载更多或点击底部加载更多触发
|
||||
QueryFrom: {
|
||||
UserPullDown: 'user-pull-down',
|
||||
Reload: 'reload',
|
||||
Refresh: 'refresh',
|
||||
LoadMore: 'load-more'
|
||||
},
|
||||
// 虚拟列表cell高度模式
|
||||
CellHeightMode: {
|
||||
// 固定高度
|
||||
Fixed: 'fixed',
|
||||
// 动态高度
|
||||
Dynamic: 'dynamic'
|
||||
},
|
||||
// 列表缓存模式
|
||||
CacheMode: {
|
||||
// 默认模式,只会缓存一次
|
||||
Default: 'default',
|
||||
// 总是缓存,每次列表刷新(下拉刷新、调用reload等)都会更新缓存
|
||||
Always: 'always'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// [z-paging]拦截器
|
||||
|
||||
const queryKey = 'Query';
|
||||
const fetchParamsKey = 'FetchParams';
|
||||
const fetchResultKey = 'FetchResult';
|
||||
const language2LocalKey = 'Language2Local';
|
||||
|
||||
// 拦截&处理@query事件
|
||||
function handleQuery(callback) {
|
||||
_addHandleByKey(queryKey, callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
// 拦截&处理@query事件(私有,请勿调用)
|
||||
function _handleQuery(pageNo, pageSize, from, lastItem) {
|
||||
const callback = _getHandleByKey(queryKey);
|
||||
return callback ? callback(pageNo, pageSize, from, lastItem) : [pageNo, pageSize, from];
|
||||
}
|
||||
|
||||
// 拦截&处理:fetch参数
|
||||
function handleFetchParams(callback) {
|
||||
_addHandleByKey(fetchParamsKey, callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
// 拦截&处理:fetch参数(私有,请勿调用)
|
||||
function _handleFetchParams(parmas, extraParams) {
|
||||
const callback = _getHandleByKey(fetchParamsKey);
|
||||
return callback ? callback(parmas, extraParams || {}) : { pageNo: parmas.pageNo, pageSize: parmas.pageSize, ...(extraParams || {}) };
|
||||
}
|
||||
|
||||
// 拦截&处理:fetch结果
|
||||
function handleFetchResult(callback) {
|
||||
_addHandleByKey(fetchResultKey, callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
// 拦截&处理:fetch结果(私有,请勿调用)
|
||||
function _handleFetchResult(result, paging, params) {
|
||||
const callback = _getHandleByKey(fetchResultKey);
|
||||
callback && callback(result, paging, params);
|
||||
return callback ? true : false;
|
||||
}
|
||||
|
||||
// 拦截&处理系统language转i18n local
|
||||
function handleLanguage2Local(callback) {
|
||||
_addHandleByKey(language2LocalKey, callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
// 拦截&处理系统language转i18n local(私有,请勿调用)
|
||||
function _handleLanguage2Local(language, local) {
|
||||
const callback = _getHandleByKey(language2LocalKey);
|
||||
return callback ? callback(language, local) : local;
|
||||
}
|
||||
|
||||
// 获取当前app对象
|
||||
function _getApp(){
|
||||
// #ifndef APP-NVUE
|
||||
return getApp();
|
||||
// #endif
|
||||
// #ifdef APP-NVUE
|
||||
return getApp({ allowDefault: true });
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 是否可以访问globalData
|
||||
function _hasGlobalData() {
|
||||
return _getApp() && _getApp().globalData;
|
||||
}
|
||||
|
||||
// 添加处理函数
|
||||
function _addHandleByKey(key, callback) {
|
||||
try {
|
||||
setTimeout(function() {
|
||||
if (_hasGlobalData()) {
|
||||
_getApp().globalData[`zp_handle${key}Callback`] = callback;
|
||||
}
|
||||
}, 1);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 获取处理回调函数
|
||||
function _getHandleByKey(key) {
|
||||
return _hasGlobalData() ? _getApp().globalData[`zp_handle${key}Callback`] : null;
|
||||
}
|
||||
|
||||
export default {
|
||||
handleQuery,
|
||||
_handleQuery,
|
||||
handleFetchParams,
|
||||
_handleFetchParams,
|
||||
handleFetchResult,
|
||||
_handleFetchResult,
|
||||
handleLanguage2Local,
|
||||
_handleLanguage2Local
|
||||
};
|
||||
515
uni_modules/z-paging/components/z-paging/js/z-paging-main.js
Normal file
515
uni_modules/z-paging/components/z-paging/js/z-paging-main.js
Normal file
@@ -0,0 +1,515 @@
|
||||
// [z-paging]核心js
|
||||
|
||||
import zStatic from './z-paging-static'
|
||||
import c from './z-paging-constant'
|
||||
import u from './z-paging-utils'
|
||||
|
||||
import zPagingRefresh from '../components/z-paging-refresh'
|
||||
import zPagingLoadMore from '../components/z-paging-load-more'
|
||||
import zPagingEmptyView from '../../z-paging-empty-view/z-paging-empty-view'
|
||||
|
||||
// modules
|
||||
import commonLayoutModule from './modules/common-layout'
|
||||
import dataHandleModule from './modules/data-handle'
|
||||
import i18nModule from './modules/i18n'
|
||||
import nvueModule from './modules/nvue'
|
||||
import emptyModule from './modules/empty'
|
||||
import refresherModule from './modules/refresher'
|
||||
import loadMoreModule from './modules/load-more'
|
||||
import loadingModule from './modules/loading'
|
||||
import chatRecordModerModule from './modules/chat-record-mode'
|
||||
import scrollerModule from './modules/scroller'
|
||||
import backToTopModule from './modules/back-to-top'
|
||||
import virtualListModule from './modules/virtual-list'
|
||||
|
||||
import Enum from './z-paging-enum'
|
||||
|
||||
const systemInfo = u.getSystemInfoSync();
|
||||
export default {
|
||||
name: "z-paging",
|
||||
components: {
|
||||
zPagingRefresh,
|
||||
zPagingLoadMore,
|
||||
zPagingEmptyView
|
||||
},
|
||||
mixins: [
|
||||
commonLayoutModule,
|
||||
dataHandleModule,
|
||||
i18nModule,
|
||||
nvueModule,
|
||||
emptyModule,
|
||||
refresherModule,
|
||||
loadMoreModule,
|
||||
loadingModule,
|
||||
chatRecordModerModule,
|
||||
scrollerModule,
|
||||
backToTopModule,
|
||||
virtualListModule
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
// --------------静态资源---------------
|
||||
base64BackToTop: zStatic.base64BackToTop,
|
||||
|
||||
// -------------全局数据相关--------------
|
||||
// 当前加载类型
|
||||
loadingType: Enum.LoadingType.Refresher,
|
||||
requestTimeStamp: 0,
|
||||
wxsPropType: '',
|
||||
renderPropScrollTop: -1,
|
||||
checkScrolledToBottomTimeOut: null,
|
||||
cacheTopHeight: -1,
|
||||
statusBarHeight: systemInfo.statusBarHeight,
|
||||
|
||||
// --------------状态&判断---------------
|
||||
insideOfPaging: -1,
|
||||
isLoadFailed: false,
|
||||
isIos: systemInfo.platform === 'ios',
|
||||
disabledBounce: false,
|
||||
fromCompleteEmit: false,
|
||||
disabledCompleteEmit: false,
|
||||
pageLaunched: false,
|
||||
active: false,
|
||||
|
||||
// ---------------wxs相关---------------
|
||||
wxsIsScrollTopInTopRange: true,
|
||||
wxsScrollTop: 0,
|
||||
wxsPageScrollTop: 0,
|
||||
wxsOnPullingDown: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 调用complete后延迟处理的时间,单位为毫秒,默认0毫秒,优先级高于minDelay
|
||||
delay: {
|
||||
type: [Number, String],
|
||||
default: u.gc('delay', 0),
|
||||
},
|
||||
// 触发@query后最小延迟处理的时间,单位为毫秒,默认0毫秒,优先级低于delay(假设设置为300毫秒,若分页请求时间小于300毫秒,则在调用complete后延迟[300毫秒-请求时长];若请求时长大于300毫秒,则不延迟),当show-refresher-when-reload为true或reload(true)时,其最小值为400
|
||||
minDelay: {
|
||||
type: [Number, String],
|
||||
default: u.gc('minDelay', 0),
|
||||
},
|
||||
// 设置z-paging的style,部分平台(如微信小程序)无法直接修改组件的style,可使用此属性代替
|
||||
pagingStyle: {
|
||||
type: Object,
|
||||
default: u.gc('pagingStyle', {}),
|
||||
},
|
||||
// z-paging的高度,优先级低于pagingStyle中设置的height;传字符串,如100px、100rpx、100%
|
||||
height: {
|
||||
type: String,
|
||||
default: u.gc('height', '')
|
||||
},
|
||||
// z-paging的宽度,优先级低于pagingStyle中设置的width;传字符串,如100px、100rpx、100%
|
||||
width: {
|
||||
type: String,
|
||||
default: u.gc('width', '')
|
||||
},
|
||||
// z-paging的最大宽度,优先级低于pagingStyle中设置的max-width;传字符串,如100px、100rpx、100%。默认为空,也就是铺满窗口宽度,若设置了特定值则会自动添加margin: 0 auto
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: u.gc('maxWidth', '')
|
||||
},
|
||||
// z-paging的背景色,优先级低于pagingStyle中设置的background。传字符串,如"#ffffff"
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: u.gc('bgColor', '')
|
||||
},
|
||||
// 设置z-paging的容器(插槽的父view)的style
|
||||
pagingContentStyle: {
|
||||
type: Object,
|
||||
default: u.gc('pagingContentStyle', {}),
|
||||
},
|
||||
// z-paging是否自动高度,若自动高度则会自动铺满屏幕
|
||||
autoHeight: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoHeight', false)
|
||||
},
|
||||
// z-paging是否自动高度时,附加的高度,注意添加单位px或rpx,若需要减少高度,则传负数
|
||||
autoHeightAddition: {
|
||||
type: [Number, String],
|
||||
default: u.gc('autoHeightAddition', '0px')
|
||||
},
|
||||
// loading(下拉刷新、上拉加载更多)的主题样式,支持black,white,默认black
|
||||
defaultThemeStyle: {
|
||||
type: String,
|
||||
default: u.gc('defaultThemeStyle', 'black')
|
||||
},
|
||||
// z-paging是否使用fixed布局,若使用fixed布局,则z-paging的父view无需固定高度,z-paging高度默认为100%,默认为是(当使用内置scroll-view滚动时有效)
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: u.gc('fixed', true)
|
||||
},
|
||||
// 是否开启底部安全区域适配
|
||||
safeAreaInsetBottom: {
|
||||
type: Boolean,
|
||||
default: u.gc('safeAreaInsetBottom', false)
|
||||
},
|
||||
// 开启底部安全区域适配后,是否使用placeholder形式实现,默认为否。为否时滚动区域会自动避开底部安全区域,也就是所有滚动内容都不会挡住底部安全区域,若设置为是,则滚动时滚动内容会挡住底部安全区域,但是当滚动到底部时才会避开底部安全区域
|
||||
useSafeAreaPlaceholder: {
|
||||
type: Boolean,
|
||||
default: u.gc('useSafeAreaPlaceholder', false)
|
||||
},
|
||||
// z-paging bottom的背景色,默认透明,传字符串,如"#ffffff"
|
||||
bottomBgColor: {
|
||||
type: String,
|
||||
default: u.gc('bottomBgColor', '')
|
||||
},
|
||||
// slot="top"的view的z-index,默认为99,仅使用页面滚动时有效
|
||||
topZIndex: {
|
||||
type: Number,
|
||||
default: u.gc('topZIndex', 99)
|
||||
},
|
||||
// z-paging内容容器父view的z-index,默认为1
|
||||
superContentZIndex: {
|
||||
type: Number,
|
||||
default: u.gc('superContentZIndex', 1)
|
||||
},
|
||||
// z-paging内容容器部分的z-index,默认为1
|
||||
contentZIndex: {
|
||||
type: Number,
|
||||
default: u.gc('contentZIndex', 1)
|
||||
},
|
||||
// z-paging二楼的z-index,默认为100
|
||||
f2ZIndex: {
|
||||
type: Number,
|
||||
default: u.gc('f2ZIndex', 100)
|
||||
},
|
||||
// 使用页面滚动时,是否在不满屏时自动填充满屏幕,默认为是
|
||||
autoFullHeight: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoFullHeight', true)
|
||||
},
|
||||
// 是否监听列表触摸方向改变,默认为否
|
||||
watchTouchDirectionChange: {
|
||||
type: Boolean,
|
||||
default: u.gc('watchTouchDirectionChange', false)
|
||||
},
|
||||
// z-paging中布局的单位,默认为rpx
|
||||
unit: {
|
||||
type: String,
|
||||
default: u.gc('unit', 'rpx')
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 组件创建时,检测是否开始加载状态
|
||||
if (this.createdReload && !this.refresherOnly && this.auto) {
|
||||
this._startLoading();
|
||||
this.$nextTick(this._preReload);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.active = true;
|
||||
this.wxsPropType = u.getTime().toString();
|
||||
this.renderJsIgnore;
|
||||
if (!this.createdReload && !this.refresherOnly && this.auto) {
|
||||
// 开始预加载
|
||||
u.delay(() => this.$nextTick(this._preReload), 0);
|
||||
}
|
||||
// 如果开启了列表缓存,在初始化的时候通过缓存数据填充列表数据
|
||||
this.finalUseCache && this._setListByLocalCache();
|
||||
let delay = 0;
|
||||
// #ifdef H5 || MP
|
||||
delay = c.delayTime;
|
||||
// #endif
|
||||
this.$nextTick(() => {
|
||||
// 初始化systemInfo
|
||||
this.systemInfo = u.getSystemInfoSync();
|
||||
// 初始化z-paging高度
|
||||
!this.usePageScroll && this.autoHeight && this._setAutoHeight();
|
||||
// #ifdef MP-KUAISHOU
|
||||
this._setFullScrollViewInHeight();
|
||||
// #endif
|
||||
this.loaded = true;
|
||||
u.delay(() => {
|
||||
// 更新fixed模式下z-paging的布局,主要是更新windowTop、windowBottom
|
||||
this.updateFixedLayout();
|
||||
// 更新缓存中z-paging整个内容容器高度
|
||||
this._updateCachedSuperContentHeight();
|
||||
});
|
||||
})
|
||||
// 初始化页面滚动模式下slot="top"、slot="bottom"高度
|
||||
this.updatePageScrollTopHeight();
|
||||
this.updatePageScrollBottomHeight();
|
||||
// 初始化slot="left"、slot="right"宽度
|
||||
this.updateLeftAndRightWidth();
|
||||
if (this.finalRefresherEnabled && this.useCustomRefresher) {
|
||||
this.$nextTick(() => {
|
||||
this.isTouchmoving = true;
|
||||
})
|
||||
}
|
||||
// 监听uni.$emit中全局emit的complete error等事件
|
||||
this._onEmit();
|
||||
// #ifdef APP-NVUE
|
||||
if (!this.isIos && !this.useChatRecordMode) {
|
||||
this.nLoadingMoreFixedHeight = true;
|
||||
}
|
||||
// 在nvue中更新nvue下拉刷新view容器的宽度,而不是写死默认的750rpx,需要考虑列表宽度不是铺满屏幕的情况
|
||||
this._nUpdateRefresherWidth();
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
// 虚拟列表模式时,初始化数据
|
||||
this.finalUseVirtualList && this._virtualListInit();
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
this.$nextTick(() => {
|
||||
// 非app平台中,在通过获取css设置的底部安全区域占位view高度设置bottom距离后,更新页面滚动底部高度
|
||||
setTimeout(() => {
|
||||
this._getCssSafeAreaInsetBottom(() => this.safeAreaInsetBottom && this.updatePageScrollBottomHeight());
|
||||
}, delay)
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
destroyed() {
|
||||
this._handleUnmounted();
|
||||
},
|
||||
// #ifdef VUE3
|
||||
unmounted() {
|
||||
this._handleUnmounted();
|
||||
},
|
||||
// #endif
|
||||
watch: {
|
||||
defaultThemeStyle: {
|
||||
handler(newVal) {
|
||||
if (newVal.length) {
|
||||
this.finalRefresherDefaultStyle = newVal;
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
autoHeight(newVal) {
|
||||
this.loaded && !this.usePageScroll && this._setAutoHeight(newVal);
|
||||
},
|
||||
autoHeightAddition(newVal) {
|
||||
this.loaded && !this.usePageScroll && this.autoHeight && this._setAutoHeight(newVal);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// 当前z-paging的内置样式
|
||||
finalPagingStyle() {
|
||||
const pagingStyle = { ...this.pagingStyle };
|
||||
if (!this.systemInfo) return pagingStyle;
|
||||
const { windowTop, windowBottom } = this;
|
||||
if (!this.usePageScroll && this.fixed) {
|
||||
if (windowTop && !pagingStyle.top) {
|
||||
pagingStyle.top = windowTop + 'px';
|
||||
}
|
||||
if (windowBottom && !pagingStyle.bottom) {
|
||||
pagingStyle.bottom = windowBottom + 'px';
|
||||
}
|
||||
}
|
||||
if (this.bgColor.length && !pagingStyle['background']) {
|
||||
pagingStyle['background'] = this.bgColor;
|
||||
}
|
||||
if (this.height.length && !pagingStyle['height']) {
|
||||
pagingStyle['height'] = this.height;
|
||||
}
|
||||
if (this.width.length && !pagingStyle['width']) {
|
||||
pagingStyle['width'] = this.width;
|
||||
}
|
||||
if (this.maxWidth.length && !pagingStyle['max-width']) {
|
||||
pagingStyle['max-width'] = this.maxWidth;
|
||||
pagingStyle['margin'] = '0 auto';
|
||||
}
|
||||
return pagingStyle;
|
||||
},
|
||||
// 当前z-paging内容的样式
|
||||
finalPagingContentStyle() {
|
||||
if (this.contentZIndex != 1) {
|
||||
this.pagingContentStyle['z-index'] = this.contentZIndex;
|
||||
this.pagingContentStyle['position'] = 'relative';
|
||||
}
|
||||
return this.pagingContentStyle;
|
||||
},
|
||||
|
||||
renderJsIgnore() {
|
||||
if ((this.usePageScroll && this.useChatRecordMode) || (!this.refresherEnabled && this.scrollable) || !this.useCustomRefresher) {
|
||||
this.$nextTick(() => {
|
||||
this.renderPropScrollTop = 10;
|
||||
})
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
windowHeight() {
|
||||
if (!this.systemInfo) return 0;
|
||||
return this.systemInfo.windowHeight || 0;
|
||||
},
|
||||
windowBottom() {
|
||||
if (!this.systemInfo) return 0;
|
||||
let windowBottom = this.systemInfo.windowBottom || 0;
|
||||
// 如果开启底部安全区域适配并且不使用placeholder的形式体现并且不是聊天记录模式(因为聊天记录模式在keyboardHeight计算初已添加了底部安全区域),在windowBottom添加底部安全区域高度
|
||||
if (this.safeAreaInsetBottom && !this.useSafeAreaPlaceholder && !this.useChatRecordMode) {
|
||||
windowBottom += this.safeAreaBottom;
|
||||
}
|
||||
return windowBottom;
|
||||
},
|
||||
isIosAndH5() {
|
||||
// #ifndef H5
|
||||
return false;
|
||||
// #endif
|
||||
return this.isIos;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 当前版本号
|
||||
getVersion() {
|
||||
return `z-paging v${c.version}`;
|
||||
},
|
||||
// 设置nvue List的specialEffects
|
||||
setSpecialEffects(args) {
|
||||
this.setListSpecialEffects(args);
|
||||
},
|
||||
// 与setSpecialEffects等效,兼容旧版本
|
||||
setListSpecialEffects(args) {
|
||||
this.nFixFreezing = args && Object.keys(args).length;
|
||||
if (this.isIos) {
|
||||
this.privateRefresherEnabled = 0;
|
||||
}
|
||||
!this.usePageScroll && this.$refs['zp-n-list'].setSpecialEffects(args);
|
||||
},
|
||||
// #ifdef APP-VUE
|
||||
// 当app长时间进入后台后进入前台,因系统内存管理导致app重新加载时,进行一些适配处理
|
||||
_handlePageLaunch() {
|
||||
// 首次触发不进行处理,只有进入后台后打开app重新加载时才处理
|
||||
if (this.pageLaunched) {
|
||||
// 解决在vue3+ios中,app ReLaunch时顶部下拉刷新展示位置向下偏移的问题
|
||||
// #ifdef VUE3
|
||||
this.refresherThresholdUpdateTag = 1;
|
||||
this.$nextTick(() => {
|
||||
this.refresherThresholdUpdateTag = 0;
|
||||
})
|
||||
// #endif
|
||||
// 解决使用虚拟列表时,app ReLaunch时白屏问题
|
||||
this._checkVirtualListScroll();
|
||||
}
|
||||
this.pageLaunched = true;
|
||||
},
|
||||
// #endif
|
||||
// 使手机发生较短时间的振动(15ms)
|
||||
_doVibrateShort() {
|
||||
// #ifndef H5
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
if (this.isIos) {
|
||||
const UISelectionFeedbackGenerator = plus.ios.importClass('UISelectionFeedbackGenerator');
|
||||
const feedbackGenerator = new UISelectionFeedbackGenerator();
|
||||
feedbackGenerator.init();
|
||||
setTimeout(() => {
|
||||
feedbackGenerator.selectionChanged();
|
||||
}, 0)
|
||||
} else {
|
||||
plus.device.vibrate(15);
|
||||
}
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
uni.vibrateShort();
|
||||
// #endif
|
||||
|
||||
// #endif
|
||||
},
|
||||
// 设置z-paging高度
|
||||
async _setAutoHeight(shouldFullHeight = true, scrollViewNode = null) {
|
||||
const heightKey = 'min-height';
|
||||
try {
|
||||
if (shouldFullHeight) {
|
||||
// 如果需要铺满全屏,则计算当前全屏可是区域的高度
|
||||
let finalScrollViewNode = scrollViewNode || await this._getNodeClientRect('.zp-scroll-view');
|
||||
let finalScrollBottomNode = await this._getNodeClientRect('.zp-page-bottom');
|
||||
if (finalScrollViewNode) {
|
||||
const scrollViewTop = finalScrollViewNode[0].top;
|
||||
let scrollViewHeight = this.windowHeight - scrollViewTop;
|
||||
scrollViewHeight -= finalScrollBottomNode ? finalScrollBottomNode[0].height : 0;
|
||||
const additionHeight = u.convertToPx(this.autoHeightAddition);
|
||||
// 在支付宝小程序中,添加!important会导致min-height失效,因此在支付宝小程序中需要去掉
|
||||
let importantSuffix = ' !important';
|
||||
// #ifdef MP-ALIPAY
|
||||
importantSuffix = '';
|
||||
// #endif
|
||||
const finalHeight = scrollViewHeight + additionHeight - (this.insideMore ? 1 : 0) + 'px' + importantSuffix;
|
||||
this.$set(this.scrollViewStyle, heightKey, finalHeight);
|
||||
this.$set(this.scrollViewInStyle, heightKey, finalHeight);
|
||||
}
|
||||
} else {
|
||||
this.$delete(this.scrollViewStyle, heightKey);
|
||||
this.$delete(this.scrollViewInStyle, heightKey);
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
// #ifdef MP-KUAISHOU
|
||||
// 设置scroll-view内容器的最小高度等于scroll-view的高度(为了解决在快手小程序中内容较少时scroll-view内容器高度无法铺满scroll-view的问题)
|
||||
async _setFullScrollViewInHeight() {
|
||||
try {
|
||||
// 如果需要铺满全屏,则计算当前全屏可是区域的高度
|
||||
const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
|
||||
scrollViewNode && this.$set(this.scrollViewInStyle, 'min-height', scrollViewNode[0].height + 'px');
|
||||
} catch (e) {}
|
||||
},
|
||||
// #endif
|
||||
// 组件销毁后续处理
|
||||
_handleUnmounted() {
|
||||
this.active = false;
|
||||
this._offEmit();
|
||||
// 取消监听键盘高度变化事件(H5、百度小程序、抖音小程序、飞书小程序、QQ小程序、快手小程序不支持)
|
||||
// #ifndef H5 || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-KUAISHOU
|
||||
this.useChatRecordMode && uni.offKeyboardHeightChange(this._handleKeyboardHeightChange);
|
||||
// #endif
|
||||
},
|
||||
// 触发更新是否超出页面状态
|
||||
_updateInsideOfPaging() {
|
||||
this.insideMore && this.insideOfPaging === true && setTimeout(this.doLoadMore, 200)
|
||||
},
|
||||
// 清除timeout
|
||||
_cleanTimeout(timeout) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
return timeout;
|
||||
},
|
||||
// 添加全局emit监听
|
||||
_onEmit() {
|
||||
uni.$on(c.errorUpdateKey, (errorMsg) => {
|
||||
if (this.loading) {
|
||||
if (!!errorMsg) {
|
||||
this.customerEmptyViewErrorText = errorMsg;
|
||||
}
|
||||
this.complete(false).catch(() => {});
|
||||
}
|
||||
})
|
||||
uni.$on(c.completeUpdateKey, (data) => {
|
||||
setTimeout(() => {
|
||||
if (this.loading) {
|
||||
if (!this.disabledCompleteEmit) {
|
||||
const type = data.type || 'normal';
|
||||
const list = data.list || data;
|
||||
const rule = data.rule;
|
||||
this.fromCompleteEmit = true;
|
||||
switch (type){
|
||||
case 'normal':
|
||||
this.complete(list);
|
||||
break;
|
||||
case 'total':
|
||||
this.completeByTotal(list, rule);
|
||||
break;
|
||||
case 'nomore':
|
||||
this.completeByNoMore(list, rule);
|
||||
break;
|
||||
case 'key':
|
||||
this.completeByKey(list, rule);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this.disabledCompleteEmit = false;
|
||||
}
|
||||
}
|
||||
}, 1);
|
||||
})
|
||||
},
|
||||
// 销毁全局emit和listener监听
|
||||
_offEmit(){
|
||||
uni.$off(c.errorUpdateKey);
|
||||
uni.$off(c.completeUpdateKey);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
// [z-paging]使用页面滚动时引入此mixin,用于监听和处理onPullDownRefresh等页面生命周期方法
|
||||
|
||||
export default {
|
||||
onPullDownRefresh() {
|
||||
if (this.isPagingRefNotFound()) return;
|
||||
this.$refs.paging.reload().catch(() => {});
|
||||
},
|
||||
onPageScroll(e) {
|
||||
if (this.isPagingRefNotFound()) return;
|
||||
this.$refs.paging.updatePageScrollTop(e.scrollTop);
|
||||
e.scrollTop < 10 && this.$refs.paging.doChatRecordLoadMore();
|
||||
},
|
||||
onReachBottom() {
|
||||
if (this.isPagingRefNotFound()) return;
|
||||
this.$refs.paging.pageReachBottom();
|
||||
},
|
||||
methods: {
|
||||
isPagingRefNotFound() {
|
||||
return !this.$refs.paging;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
302
uni_modules/z-paging/components/z-paging/js/z-paging-utils.js
Normal file
302
uni_modules/z-paging/components/z-paging/js/z-paging-utils.js
Normal file
@@ -0,0 +1,302 @@
|
||||
// [z-paging]工具类
|
||||
|
||||
import zLocalConfig from '../config/index'
|
||||
import c from './z-paging-constant'
|
||||
|
||||
const storageKey = 'Z-PAGING-REFRESHER-TIME-STORAGE-KEY';
|
||||
let config = null;
|
||||
let configLoaded = false;
|
||||
let cachedSystemInfo = null;
|
||||
const timeoutMap = {};
|
||||
|
||||
// 获取默认配置信息
|
||||
function gc(key, defaultValue) {
|
||||
// 这里return一个函数以解决在vue3+appvue中,props默认配置读取在main.js之前执行导致uni.$zp全局配置无效的问题。相当于props的default中传入一个带有返回值的函数
|
||||
return () => {
|
||||
// 处理z-paging全局配置
|
||||
_handleDefaultConfig();
|
||||
// 如果全局配置不存在,则返回默认值
|
||||
if (!config) return defaultValue;
|
||||
const value = config[key];
|
||||
// 如果全局配置存在但对应的配置项不存在,则返回默认值;反之返回配置项
|
||||
return value === undefined ? defaultValue : value;
|
||||
};
|
||||
}
|
||||
|
||||
// 获取最终的touch位置
|
||||
function getTouch(e) {
|
||||
let touch = null;
|
||||
if (e.touches && e.touches.length) {
|
||||
touch = e.touches[0];
|
||||
} else if (e.changedTouches && e.changedTouches.length) {
|
||||
touch = e.changedTouches[0];
|
||||
} else if (e.datail && e.datail != {}) {
|
||||
touch = e.datail;
|
||||
} else {
|
||||
return { touchX: 0, touchY: 0 }
|
||||
}
|
||||
return {
|
||||
touchX: touch.clientX,
|
||||
touchY: touch.clientY
|
||||
};
|
||||
}
|
||||
|
||||
// 判断当前手势是否在z-paging内触发
|
||||
function getTouchFromZPaging(target) {
|
||||
if (target && target.tagName && target.tagName !== 'BODY' && target.tagName !== 'UNI-PAGE-BODY') {
|
||||
const classList = target.classList;
|
||||
if (classList && classList.contains('z-paging-content')) {
|
||||
// 此处额外记录当前z-paging是否是页面滚动、是否滚动到了顶部、是否是聊天记录模式以传给renderjs。避免不同z-paging组件renderjs内部判断数据互相影响导致的各种问题
|
||||
return {
|
||||
isFromZp: true,
|
||||
isPageScroll: classList.contains('z-paging-content-page'),
|
||||
isReachedTop: classList.contains('z-paging-reached-top'),
|
||||
isUseChatRecordMode: classList.contains('z-paging-use-chat-record-mode')
|
||||
};
|
||||
} else {
|
||||
return getTouchFromZPaging(target.parentNode);
|
||||
}
|
||||
} else {
|
||||
return { isFromZp: false };
|
||||
}
|
||||
}
|
||||
|
||||
// 递归获取z-paging所在的parent,如果查找不到则返回null
|
||||
function getParent(parent) {
|
||||
if (!parent) return null;
|
||||
if (parent.$refs.paging) return parent;
|
||||
return getParent(parent.$parent);
|
||||
}
|
||||
|
||||
// 打印错误信息
|
||||
function consoleErr(err) {
|
||||
console.error(`[z-paging]${err}`);
|
||||
}
|
||||
|
||||
// 延时操作,如果key存在,调用时清除对应key之前的延时操作
|
||||
function delay(callback, ms = c.delayTime, key) {
|
||||
const timeout = setTimeout(callback, ms);;
|
||||
if (!!key) {
|
||||
timeoutMap[key] && clearTimeout(timeoutMap[key]);
|
||||
timeoutMap[key] = timeout;
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
// 设置下拉刷新时间
|
||||
function setRefesrherTime(time, key) {
|
||||
const datas = getRefesrherTime() || {};
|
||||
datas[key] = time;
|
||||
uni.setStorageSync(storageKey, datas);
|
||||
}
|
||||
|
||||
// 获取下拉刷新时间
|
||||
function getRefesrherTime() {
|
||||
return uni.getStorageSync(storageKey);
|
||||
}
|
||||
|
||||
// 通过下拉刷新标识key获取下拉刷新时间
|
||||
function getRefesrherTimeByKey(key) {
|
||||
const datas = getRefesrherTime();
|
||||
return datas && datas[key] ? datas[key] : null;
|
||||
}
|
||||
|
||||
// 通过下拉刷新标识key获取下拉刷新时间(格式化之后)
|
||||
function getRefesrherFormatTimeByKey(key, textMap) {
|
||||
const time = getRefesrherTimeByKey(key);
|
||||
const timeText = time ? _timeFormat(time, textMap) : textMap.none;
|
||||
return `${textMap.title}${timeText}`;
|
||||
}
|
||||
|
||||
// 将文本的px或者rpx转为px的值
|
||||
function convertToPx(text) {
|
||||
const dataType = Object.prototype.toString.call(text);
|
||||
if (dataType === '[object Number]') return text;
|
||||
let isRpx = false;
|
||||
if (text.indexOf('rpx') !== -1 || text.indexOf('upx') !== -1) {
|
||||
text = text.replace('rpx', '').replace('upx', '');
|
||||
isRpx = true;
|
||||
} else if (text.indexOf('px') !== -1) {
|
||||
text = text.replace('px', '');
|
||||
}
|
||||
if (!isNaN(text)) {
|
||||
if (isRpx) return Number(rpx2px(text));
|
||||
return Number(text);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// rpx => px,预留的兼容处理
|
||||
function rpx2px(rpx) {
|
||||
return uni.upx2px(rpx);
|
||||
}
|
||||
|
||||
// 同步获取系统信息,兼容不同平台
|
||||
function getSystemInfoSync(useCache = false) {
|
||||
if (useCache && cachedSystemInfo) {
|
||||
return cachedSystemInfo;
|
||||
}
|
||||
// 目前只用到了deviceInfo、appBaseInfo和windowInfo中的信息,因此仅整合这两个信息数据
|
||||
const infoTypes = ['DeviceInfo', 'AppBaseInfo', 'WindowInfo'];
|
||||
const { deviceInfo, appBaseInfo, windowInfo } = infoTypes.reduce((acc, key) => {
|
||||
const method = `get${key}`;
|
||||
if (uni[method] && uni.canIUse(method)) {
|
||||
acc[key.charAt(0).toLowerCase() + key.slice(1)] = uni[method]();
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
// 如果deviceInfo、appBaseInfo和windowInfo都可以从各自专属的api中获取,则整合它们的数据
|
||||
if (deviceInfo && appBaseInfo && windowInfo) {
|
||||
cachedSystemInfo = { ...deviceInfo, ...appBaseInfo, ...windowInfo };
|
||||
} else {
|
||||
// 使用uni.getSystemInfoSync兜底,确保能获取到最终的系统信息
|
||||
cachedSystemInfo = uni.getSystemInfoSync();
|
||||
}
|
||||
return cachedSystemInfo;
|
||||
}
|
||||
|
||||
// 获取当前时间
|
||||
function getTime() {
|
||||
return (new Date()).getTime();
|
||||
}
|
||||
|
||||
// 获取z-paging实例id,随机生成10位数字+字母
|
||||
function getInstanceId() {
|
||||
const s = [];
|
||||
const hexDigits = "0123456789abcdef";
|
||||
for (let i = 0; i < 10; i++) {
|
||||
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
|
||||
}
|
||||
return s.join('') + getTime();
|
||||
}
|
||||
|
||||
// 等待一段时间
|
||||
function wait(ms) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
// 是否是promise
|
||||
function isPromise(func) {
|
||||
return Object.prototype.toString.call(func) === '[object Promise]';
|
||||
}
|
||||
|
||||
// 添加单位
|
||||
function addUnit(value, unit) {
|
||||
if (Object.prototype.toString.call(value) === '[object String]') {
|
||||
let tempValue = value;
|
||||
tempValue = tempValue.replace('rpx', '').replace('upx', '').replace('px', '');
|
||||
if (value.indexOf('rpx') === -1 && value.indexOf('upx') === -1 && value.indexOf('px') !== -1) {
|
||||
tempValue = parseFloat(tempValue) * 2;
|
||||
}
|
||||
value = tempValue;
|
||||
}
|
||||
return unit === 'rpx' ? value + 'rpx' : (value / 2) + 'px';
|
||||
}
|
||||
|
||||
// 深拷贝
|
||||
function deepCopy(obj) {
|
||||
if (typeof obj !== 'object' || obj === null) return obj;
|
||||
let newObj = Array.isArray(obj) ? [] : {};
|
||||
for (let key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
newObj[key] = deepCopy(obj[key]);
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
// ------------------ 私有方法 ------------------------
|
||||
// 处理全局配置
|
||||
function _handleDefaultConfig() {
|
||||
// 确保只加载一次全局配置
|
||||
if (configLoaded) return;
|
||||
// 优先从config.js中读取
|
||||
if (zLocalConfig && Object.keys(zLocalConfig).length) {
|
||||
config = zLocalConfig;
|
||||
}
|
||||
// 如果在config.js中读取不到,则尝试到uni.$zp读取
|
||||
if (!config && uni.$zp) {
|
||||
config = uni.$zp.config;
|
||||
}
|
||||
// 将config中的短横线写法全部转为驼峰写法,使得读取配置时可以直接通过key去匹配,而非读取每个配置时候再去转,减少不必要的性能开支
|
||||
config = config ? Object.keys(config).reduce((result, key) => {
|
||||
result[_toCamelCase(key)] = config[key];
|
||||
return result;
|
||||
}, {}) : null;
|
||||
configLoaded = true;
|
||||
}
|
||||
|
||||
// 时间格式化
|
||||
function _timeFormat(time, textMap) {
|
||||
const date = new Date(time);
|
||||
const currentDate = new Date();
|
||||
// 设置time对应的天,去除时分秒,使得可以直接比较日期
|
||||
const dateDay = new Date(time).setHours(0, 0, 0, 0);
|
||||
// 设置当前的天,去除时分秒,使得可以直接比较日期
|
||||
const currentDateDay = new Date().setHours(0, 0, 0, 0);
|
||||
const disTime = dateDay - currentDateDay;
|
||||
let dayStr = '';
|
||||
const timeStr = _dateTimeFormat(date);
|
||||
if (disTime === 0) {
|
||||
dayStr = textMap.today;
|
||||
} else if (disTime === -86400000) {
|
||||
dayStr = textMap.yesterday;
|
||||
} else {
|
||||
dayStr = _dateDayFormat(date, date.getFullYear() !== currentDate.getFullYear());
|
||||
}
|
||||
return `${dayStr} ${timeStr}`;
|
||||
}
|
||||
|
||||
// date格式化为年月日
|
||||
function _dateDayFormat(date, showYear = true) {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
return showYear ? `${year}-${_fullZeroToTwo(month)}-${_fullZeroToTwo(day)}` : `${_fullZeroToTwo(month)}-${_fullZeroToTwo(day)}`;
|
||||
}
|
||||
|
||||
// data格式化为时分
|
||||
function _dateTimeFormat(date) {
|
||||
const hour = date.getHours();
|
||||
const minute = date.getMinutes();
|
||||
return `${_fullZeroToTwo(hour)}:${_fullZeroToTwo(minute)}`;
|
||||
}
|
||||
|
||||
// 不满2位在前面填充0
|
||||
function _fullZeroToTwo(str) {
|
||||
str = str.toString();
|
||||
return str.length === 1 ? '0' + str : str;
|
||||
}
|
||||
|
||||
// 驼峰转短横线
|
||||
function _toKebab(value) {
|
||||
return value.replace(/([A-Z])/g, "-$1").toLowerCase();
|
||||
}
|
||||
|
||||
// 短横线转驼峰
|
||||
function _toCamelCase(value) {
|
||||
return value.replace(/-([a-z])/g, (_, group1) => group1.toUpperCase());
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
gc,
|
||||
setRefesrherTime,
|
||||
getRefesrherFormatTimeByKey,
|
||||
getTouch,
|
||||
getTouchFromZPaging,
|
||||
getParent,
|
||||
convertToPx,
|
||||
getTime,
|
||||
getInstanceId,
|
||||
consoleErr,
|
||||
delay,
|
||||
wait,
|
||||
isPromise,
|
||||
addUnit,
|
||||
deepCopy,
|
||||
rpx2px,
|
||||
getSystemInfoSync
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
// [z-paging]使用renderjs在app-vue和h5中对touchmove事件冒泡进行处理
|
||||
|
||||
import u from '../js/z-paging-utils'
|
||||
const data = {
|
||||
startY: 0,
|
||||
isTouchFromZPaging: false,
|
||||
isUsePageScroll: false,
|
||||
isReachedTop: true,
|
||||
isIosAndH5: false,
|
||||
useChatRecordMode: false,
|
||||
appLaunched: false
|
||||
}
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
if (window) {
|
||||
this._handleTouch();
|
||||
// #ifdef APP-VUE
|
||||
this.$ownerInstance.callMethod('_handlePageLaunch');
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 接收逻辑层发送的数据(是否是ios+h5)
|
||||
renderPropIsIosAndH5Change(newVal) {
|
||||
if (newVal === -1) return;
|
||||
data.isIosAndH5 = newVal;
|
||||
},
|
||||
|
||||
// 拦截处理touch事件
|
||||
_handleTouch() {
|
||||
if (!window.$zPagingRenderJsInited) {
|
||||
window.$zPagingRenderJsInited = true;
|
||||
window.addEventListener('touchstart', this._handleTouchstart, { passive: true })
|
||||
window.addEventListener('touchmove', this._handleTouchmove, { passive: false })
|
||||
}
|
||||
},
|
||||
// 处理touch开始
|
||||
_handleTouchstart(e) {
|
||||
const touch = u.getTouch(e);
|
||||
data.startY = touch.touchY;
|
||||
const touchResult = u.getTouchFromZPaging(e.target);
|
||||
data.isTouchFromZPaging = touchResult.isFromZp;
|
||||
data.isUsePageScroll = touchResult.isPageScroll;
|
||||
data.isReachedTop = touchResult.isReachedTop;
|
||||
data.useChatRecordMode = touchResult.isUseChatRecordMode;
|
||||
},
|
||||
// 处理touch中
|
||||
_handleTouchmove(e) {
|
||||
const touch = u.getTouch(e);
|
||||
const moveY = touch.touchY - data.startY;
|
||||
// 如果是在z-paging内触摸并且(是在顶部位置且是下拉的情况下(或不是聊天记录滚动模式并且在iOS+h5+scroll-view并且是往上拉的情况:避免在此平台中滚动到底部后上拉有个系统灰色遮罩导致列表被短暂锁定的问题))
|
||||
// (data.useChatRecordMode ? moveY < 0 : moveY > 0)是为了判断是否是上拉的情况,聊天记录模式列表倒置,因此moveY < 0为上拉
|
||||
if (data.isTouchFromZPaging && ((data.isReachedTop && (data.useChatRecordMode ? moveY < 0 : moveY > 0)) || (!data.useChatRecordMode && data.isIosAndH5 && !data.isUsePageScroll && moveY < 0))) {
|
||||
if (e.cancelable && !e.defaultPrevented) {
|
||||
// 阻止事件冒泡,以避免在一些平台中下拉刷新时整个page跟着一起下拉&在iOS+h5+scroll-view中在底部上拉有个系统灰色遮罩导致列表被短暂锁定的问题
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
// 移除touch相关事件监听
|
||||
_removeAllEventListener(){
|
||||
window.removeEventListener('touchstart');
|
||||
window.removeEventListener('touchmove');
|
||||
}
|
||||
}
|
||||
};
|
||||
382
uni_modules/z-paging/components/z-paging/wxs/z-paging-wxs.wxs
Normal file
382
uni_modules/z-paging/components/z-paging/wxs/z-paging-wxs.wxs
Normal file
@@ -0,0 +1,382 @@
|
||||
// [z-paging]微信小程序、QQ小程序、app-vue、h5上使用wxs实现自定义下拉刷新,降低逻辑层与视图层的通信折损,提升性能
|
||||
|
||||
var currentDis = 0;
|
||||
var isPCFlag = -1;
|
||||
var startY = -1;
|
||||
|
||||
// 监听js层传过来的数据
|
||||
function propObserver(newVal, oldVal, ownerIns, ins) {
|
||||
var state = ownerIns.getState() || {};
|
||||
state.currentIns = ins;
|
||||
var dataset = ins.getDataset();
|
||||
var loading = dataset.loading == true;
|
||||
// 如果是下拉刷新结束,更新transform
|
||||
if (newVal && newVal.indexOf('end') != -1) {
|
||||
var transition = newVal.split('end')[0];
|
||||
_setTransform('translateY(0px)', ins, false, transition);
|
||||
state.moveDis = 0;
|
||||
state.oldMoveDis = 0;
|
||||
currentDis = 0;
|
||||
} else if (newVal && newVal.indexOf('begin') != -1) {
|
||||
// 如果是下拉刷新开始,更新transform
|
||||
var refresherThreshold = ins.getDataset().refresherthreshold;
|
||||
_setTransformValue(refresherThreshold, ins, state, false);
|
||||
}
|
||||
}
|
||||
|
||||
// touch开始
|
||||
function touchstart(e, ownerIns) {
|
||||
var ins = _getIns(ownerIns);
|
||||
var state = {};
|
||||
var dataset = {};
|
||||
ownerIns.callMethod('_handleListTouchstart');
|
||||
if (ins) {
|
||||
state = ins.getState();
|
||||
dataset = ins.getDataset();
|
||||
if (_touchDisabled(e, ins, 0)) return;
|
||||
}
|
||||
var isTouchEnded = state.isTouchEnded;
|
||||
state.oldMoveDis = 0;
|
||||
var touch = _getTouch(e);
|
||||
var loading = _isTrue(dataset.loading);
|
||||
state.startY = touch.touchY;
|
||||
startY = state.startY;
|
||||
state.lastTouch = touch;
|
||||
if (!loading && isTouchEnded) {
|
||||
state.isTouchmoving = false;
|
||||
}
|
||||
state.isTouchEnded = false;
|
||||
// 通知js层touch开始
|
||||
ownerIns.callMethod('_handleRefresherTouchstart', touch);
|
||||
}
|
||||
|
||||
// touch中
|
||||
function touchmove(e, ownerIns) {
|
||||
var touch = _getTouch(e);
|
||||
var ins = _getIns(ownerIns);
|
||||
var dataset = ins.getDataset();
|
||||
var refresherThreshold = dataset.refresherthreshold;
|
||||
var refresherF2Threshold = dataset.refresherf2threshold;
|
||||
var refresherF2Enabled = _isTrue(dataset.refresherf2enabled);
|
||||
var isIos = _isTrue(dataset.isios);
|
||||
var state = ins.getState();
|
||||
var watchTouchDirectionChange = _isTrue(dataset.watchtouchdirectionchange);
|
||||
var moveDisObj = {};
|
||||
var moveDis = 0;
|
||||
var prevent = false;
|
||||
// 如果需要监听touch方向的改变
|
||||
if (watchTouchDirectionChange) {
|
||||
moveDisObj = _getMoveDis(e, ins);
|
||||
moveDis = moveDisObj.currentDis;
|
||||
prevent = moveDisObj.isDown;
|
||||
var direction = prevent ? 'top' : 'bottom';
|
||||
// 确保只在touch方向改变时通知一次js层,而不是touchmove中持续通知
|
||||
if (prevent == state.oldTouchDirection && prevent != state.oldEmitedTouchDirection) {
|
||||
ownerIns.callMethod('_handleTouchDirectionChange', { direction: direction });
|
||||
state.oldEmitedTouchDirection = prevent;
|
||||
}
|
||||
state.oldTouchDirection = prevent;
|
||||
}
|
||||
// 判断是否允许下拉刷新
|
||||
if (_touchDisabled(e, ins, 1)) {
|
||||
_handlePullingDown(state, ownerIns, false);
|
||||
return true;
|
||||
}
|
||||
// 判断下拉刷新的角度是否在要求范围内
|
||||
if (!_getAngleIsInRange(e, touch, state, dataset)) {
|
||||
_handlePullingDown(state, ownerIns, false);
|
||||
return true;
|
||||
}
|
||||
moveDisObj = _getMoveDis(e, ins);
|
||||
moveDis = moveDisObj.currentDis;
|
||||
prevent = moveDisObj.isDown;
|
||||
if (moveDis < 0) {
|
||||
// moveDis小于0,将transform重置为0
|
||||
_setTransformValue(0, ins, state, false);
|
||||
_handlePullingDown(state, ownerIns, false);
|
||||
return true;
|
||||
}
|
||||
if (prevent && !state.disabledBounce) {
|
||||
// 如果是用户下拉并且需要触发下拉刷新,需要通知js层将列表禁止滚动,防止在下拉刷新过程中列表也可以滚动导致的下拉刷新偏移过大的问题(在下拉刷新过程中仅通知一次)
|
||||
ownerIns.callMethod('_handleScrollViewBounce', { bounce: false });
|
||||
state.disabledBounce = true;
|
||||
_handlePullingDown(state, ownerIns, prevent);
|
||||
return !prevent;
|
||||
}
|
||||
// 更新transform
|
||||
_setTransformValue(moveDis, ins, state, false);
|
||||
var oldRefresherStatus = state.refresherStatus;
|
||||
var oldIsTouchmoving = _isTrue(dataset.oldistouchmoving);
|
||||
var hasTouchmove = _isTrue(dataset.hastouchmove);
|
||||
var isTouchmoving = state.isTouchmoving;
|
||||
state.refresherStatus = moveDis >= refresherThreshold ? (refresherF2Enabled && moveDis > refresherF2Threshold ? 'goF2' : 'releaseToRefresh') : 'default';
|
||||
if (!isTouchmoving) {
|
||||
state.isTouchmoving = true;
|
||||
isTouchmoving = true;
|
||||
}
|
||||
if (state.isTouchEnded) {
|
||||
state.isTouchEnded = false;
|
||||
}
|
||||
// 如果需要实时监听下拉位置偏移,则需要实时通知js层,此操作会使wxs层与js层频繁通信从而导致在一些性能较差设备中下拉刷新卡顿
|
||||
if (hasTouchmove) {
|
||||
ownerIns.callMethod('_handleWxsPullingDown', { moveDis: moveDis, diffDis: moveDisObj.diffDis });
|
||||
}
|
||||
// 在下拉刷新状态改变时通知js层
|
||||
if (oldRefresherStatus == undefined || oldRefresherStatus != state.refresherStatus || oldIsTouchmoving != isTouchmoving) {
|
||||
ownerIns.callMethod('_handleRefresherTouchmove', moveDis, touch);
|
||||
}
|
||||
_handlePullingDown(state, ownerIns, prevent);
|
||||
return !prevent;
|
||||
}
|
||||
|
||||
// touch结束
|
||||
function touchend(e, ownerIns) {
|
||||
var touch = _getTouch(e);
|
||||
var ins = _getIns(ownerIns);
|
||||
var dataset = ins.getDataset();
|
||||
var state = ins.getState();
|
||||
if (state.disabledBounce) {
|
||||
// 通知js允许列表滚动
|
||||
ownerIns.callMethod('_handleScrollViewBounce', { bounce: true });
|
||||
state.disabledBounce = false;
|
||||
}
|
||||
if (_touchDisabled(e, ins, 2)) return;
|
||||
state.reachMaxAngle = true;
|
||||
state.hitReachMaxAngleCount = 0;
|
||||
state.fixedIsTopHitCount = 0;
|
||||
if (!state.isTouchmoving) return;
|
||||
var oldRefresherStatus = state.refresherStatus;
|
||||
var oldMoveDis = state.moveDis;
|
||||
var refresherThreshold = ins.getDataset().refresherthreshold;
|
||||
var moveDis = _getMoveDis(e, ins).currentDis;
|
||||
if (!(moveDis >= refresherThreshold && oldRefresherStatus === 'releaseToRefresh')) {
|
||||
state.isTouchmoving = false;
|
||||
}
|
||||
// 通知js层touch结束
|
||||
ownerIns.callMethod('_handleRefresherTouchend', moveDis);
|
||||
state.isTouchEnded = true;
|
||||
if (oldMoveDis < refresherThreshold) return;
|
||||
var animate = false;
|
||||
if (moveDis >= refresherThreshold) {
|
||||
moveDis = refresherThreshold;
|
||||
animate = true;
|
||||
}
|
||||
_setTransformValue(moveDis, ins, state, animate);
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
// 判断是否是pc平台
|
||||
function isPC() {
|
||||
if (!navigator) return false;
|
||||
if (isPCFlag != -1) return isPCFlag;
|
||||
var agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
|
||||
isPCFlag = agents.every(function(item) { return navigator.userAgent.indexOf(item) < 0 });
|
||||
return isPCFlag;
|
||||
}
|
||||
|
||||
var movable = false;
|
||||
|
||||
// 在pc平台监听mousedown、mousemove、mouseup等相关事件并转为对应touch事件处理,使得在pc平台也支持通过鼠标进行下拉刷新
|
||||
|
||||
function mousedown(e, ins) {
|
||||
if (!isPC()) return;
|
||||
touchstart(e, ins);
|
||||
movable = true;
|
||||
}
|
||||
|
||||
function mousemove(e, ins) {
|
||||
if (!isPC() || !movable) return;
|
||||
touchmove(e, ins);
|
||||
}
|
||||
|
||||
function mouseup(e, ins) {
|
||||
if (!isPC()) return;
|
||||
touchend(e, ins);
|
||||
movable = false;
|
||||
}
|
||||
|
||||
function mouseleave(e, ins) {
|
||||
if (!isPC()) return;
|
||||
movable = false;
|
||||
}
|
||||
// #endif
|
||||
|
||||
|
||||
// 修改视图层transform
|
||||
function _setTransformValue(value, ins, state, animate) {
|
||||
value = value || 0;
|
||||
if (state.moveDis == value) return;
|
||||
state.moveDis = value;
|
||||
_setTransform('translateY(' + value + 'px)', ins, animate, '');
|
||||
}
|
||||
|
||||
// 设置视图层transform,直接在视图层操作下拉刷新,使得js层不需要频繁和视图层通信,从而大大提升下拉刷新性能
|
||||
function _setTransform(transform, ins, animate, transition) {
|
||||
var dataset = ins.getDataset();
|
||||
if (_isTrue(dataset.refreshernotransform)) return;
|
||||
transform = transform == 'translateY(0px)' ? 'none' : transform;
|
||||
ins.requestAnimationFrame(function() {
|
||||
var stl = { 'transform': transform };
|
||||
if (animate) {
|
||||
stl['transition'] = 'transform .1s linear';
|
||||
}
|
||||
if (transition.length) {
|
||||
stl['transition'] = transition;
|
||||
}
|
||||
ins.setStyle(stl);
|
||||
})
|
||||
}
|
||||
|
||||
// 进一步处理下拉刷新的偏移数据
|
||||
function _getMoveDis(e, ins) {
|
||||
var state = ins.getState();
|
||||
var refresherThreshold = parseFloat(ins.getDataset().refresherthreshold);
|
||||
var refresherOutRate = parseFloat(ins.getDataset().refresheroutrate);
|
||||
var refresherPullRate = parseFloat(ins.getDataset().refresherpullrate);
|
||||
var touch = _getTouch(e);
|
||||
var currentStartY = !state.startY || state.startY == 'NaN' ? startY : state.startY;
|
||||
var moveDis = touch.touchY - currentStartY;
|
||||
var oldMoveDis = state.oldMoveDis || 0;
|
||||
state.oldMoveDis = moveDis;
|
||||
// 获取当前下拉刷新位置与上次的偏移量
|
||||
var diffDis = moveDis - oldMoveDis;
|
||||
if (diffDis > 0) {
|
||||
// 对偏移量进行进一步处理,通过refresherPullRate等配置进行约束
|
||||
diffDis = diffDis * refresherPullRate;
|
||||
if (currentDis > refresherThreshold) {
|
||||
diffDis = diffDis * (1 - refresherOutRate);
|
||||
}
|
||||
}
|
||||
// 控制diffDis过大的情况,比如进入页面突然猛然下拉,此时diffDis不应进行太大的偏移
|
||||
diffDis = diffDis > 100 ? diffDis / 100 : (diffDis > 20 ? diffDis / 2.2 : diffDis);
|
||||
currentDis += diffDis;
|
||||
currentDis = Math.max(0, currentDis);
|
||||
return {
|
||||
currentDis: currentDis,
|
||||
diffDis: diffDis,
|
||||
isDown: diffDis > 0
|
||||
};
|
||||
}
|
||||
|
||||
// 获取经过统一格式包装的当前touch对象
|
||||
function _getTouch(e) {
|
||||
var touch = e;
|
||||
if (e.touches && e.touches.length) {
|
||||
touch = e.touches[0];
|
||||
} else if (e.changedTouches && e.changedTouches.length) {
|
||||
touch = e.changedTouches[0];
|
||||
} else if (e.datail && e.datail != {}) {
|
||||
touch = e.datail;
|
||||
}
|
||||
return {
|
||||
touchX: touch.clientX,
|
||||
touchY: touch.clientY
|
||||
};
|
||||
}
|
||||
|
||||
// 获取当前currentIns
|
||||
function _getIns(ownerIns) {
|
||||
var ins = ownerIns.getState().currentIns;
|
||||
if (!ins) {
|
||||
ownerIns.callMethod('_handlePropUpdate');
|
||||
}
|
||||
return ins;
|
||||
}
|
||||
|
||||
// 判断当前状态是否允许下拉刷新
|
||||
function _touchDisabled(e, ins, processTag) {
|
||||
var dataset = ins.getDataset();
|
||||
var state = ins.getState();
|
||||
var loading = _isTrue(dataset.loading);
|
||||
var useChatRecordMode = _isTrue(dataset.usechatrecordmode);
|
||||
var refresherEnabled = _isTrue(dataset.refresherenabled);
|
||||
var useCustomRefresher = _isTrue(dataset.usecustomrefresher);
|
||||
var usePageScroll = _isTrue(dataset.usepagescroll);
|
||||
var pageScrollTop = parseFloat(dataset.pagescrolltop);
|
||||
var scrollTop = parseFloat(dataset.scrolltop);
|
||||
var finalScrollTop = usePageScroll ? pageScrollTop : scrollTop;
|
||||
var fixedIsTop = false;
|
||||
// 是否要处理滚动到顶部scrollTop不为0时候的容错,为解决在安卓中scroll-view有概率滚动到顶部时scrollTop不为0导致下拉刷新判断异常,但此方案会导致某些情况(例如滚动到距离顶部10px处)下拉抖动,因此改为通过获取zp-scroll-view的节点信息中的scrollTop进行验证的方案
|
||||
var handleFaultTolerantMove = false;
|
||||
if (handleFaultTolerantMove && finalScrollTop == (state.startScrollTop || 0) && finalScrollTop <= 105) {
|
||||
fixedIsTop = true;
|
||||
}
|
||||
var fixedIsTopHitCount = state.fixedIsTopHitCount || 0;
|
||||
if (fixedIsTop) {
|
||||
fixedIsTopHitCount ++;
|
||||
if (fixedIsTopHitCount <= 2) {
|
||||
fixedIsTop = false;
|
||||
}
|
||||
state.fixedIsTopHitCount = fixedIsTopHitCount;
|
||||
} else {
|
||||
state.fixedIsTopHitCount = 0;
|
||||
}
|
||||
if (handleFaultTolerantMove && processTag === 0) {
|
||||
state.startScrollTop = finalScrollTop || 0;
|
||||
}
|
||||
if (handleFaultTolerantMove && processTag === 2) {
|
||||
fixedIsTop = true;
|
||||
}
|
||||
return loading || useChatRecordMode || !refresherEnabled || !useCustomRefresher ||
|
||||
((usePageScroll && useCustomRefresher && pageScrollTop > 5) && !fixedIsTop) ||
|
||||
((!usePageScroll && useCustomRefresher && scrollTop > 5) && !fixedIsTop);
|
||||
}
|
||||
|
||||
// 判断下拉刷新的角度是否在要求范围内
|
||||
function _getAngleIsInRange(e, touch, state, dataset) {
|
||||
var maxAngle = dataset.refreshermaxangle;
|
||||
var refresherAecc = _isTrue(dataset.refresheraecc);
|
||||
var lastTouch = state.lastTouch;
|
||||
var reachMaxAngle = state.reachMaxAngle;
|
||||
var moveDis = state.oldMoveDis;
|
||||
if (!lastTouch) return true;
|
||||
if (maxAngle >= 0 && maxAngle <= 90 && lastTouch) {
|
||||
// 考虑下拉刷新手势由水平移动转为垂直方向移动的情况,此时不应当只判断垂直方向角度是否符合要求,应当直接禁止以避免在swiper中使用下拉刷新时,横向切换swiper途中手未离开屏幕还可以下拉刷新的问题
|
||||
if ((!moveDis || moveDis < 1) && !refresherAecc && reachMaxAngle != null && !reachMaxAngle) return false;
|
||||
var x = Math.abs(touch.touchX - lastTouch.touchX);
|
||||
var y = Math.abs(touch.touchY - lastTouch.touchY);
|
||||
var z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
||||
if ((x || y) && x > 1) {
|
||||
// 获取下拉刷新前后两次位移的角度
|
||||
var angle = Math.asin(y / z) / Math.PI * 180;
|
||||
if (angle < maxAngle) {
|
||||
// 如果角度小于配置要求,则return,同时通过hitReachMaxAngleCount控制角度判断的灵敏程度以最大程度兼容各种使用场景
|
||||
var hitReachMaxAngleCount = state.hitReachMaxAngleCount || 0;
|
||||
state.hitReachMaxAngleCount = ++hitReachMaxAngleCount;
|
||||
if (state.hitReachMaxAngleCount > 2) {
|
||||
state.lastTouch = touch;
|
||||
state.reachMaxAngle = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
state.lastTouch = touch;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 进一步处理是否在下拉刷新并通知js层
|
||||
function _handlePullingDown(state, ins, onPullingDown) {
|
||||
var oldOnPullingDown = state.onPullingDown || false;
|
||||
if (oldOnPullingDown != onPullingDown) {
|
||||
ins.callMethod('_handleWxsPullingDownStatusChange', onPullingDown);
|
||||
}
|
||||
state.onPullingDown = onPullingDown;
|
||||
}
|
||||
|
||||
// 判断js层传过来的值是否为true
|
||||
function _isTrue(value) {
|
||||
value = (typeof(value) === 'string' ? JSON.parse(value) : value) || false;
|
||||
return value == true || value == 'true';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
touchstart: touchstart,
|
||||
touchmove: touchmove,
|
||||
touchend: touchend,
|
||||
mousedown: mousedown,
|
||||
mousemove: mousemove,
|
||||
mouseup: mouseup,
|
||||
mouseleave: mouseleave,
|
||||
propObserver: propObserver
|
||||
}
|
||||
538
uni_modules/z-paging/components/z-paging/z-paging.vue
Normal file
538
uni_modules/z-paging/components/z-paging/z-paging.vue
Normal file
@@ -0,0 +1,538 @@
|
||||
<!-- _
|
||||
____ _ __ __ _ __ _(_)_ __ __ _
|
||||
|_ /____| '_ \ / _` |/ _` | | '_ \ / _` |
|
||||
/ /_____| |_) | (_| | (_| | | | | | (_| |
|
||||
/___| | .__/ \__,_|\__, |_|_| |_|\__, |
|
||||
|_| |___/ |___/
|
||||
v2.8.6 (2025-03-17)
|
||||
@author ZXLee <admin@zxlee.cn>
|
||||
-->
|
||||
<!-- 文档地址:https://z-paging.zxlee.cn -->
|
||||
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
|
||||
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
|
||||
<!-- 反馈QQ群:343409055 -->
|
||||
|
||||
<template name="z-paging">
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<view :class="{'z-paging-content':true,'z-paging-content-full':!usePageScroll,'z-paging-content-fixed':!usePageScroll&&fixed,'z-paging-content-page':usePageScroll,'z-paging-reached-top':renderPropScrollTop<1,'z-paging-use-chat-record-mode':useChatRecordMode}" :style="[finalPagingStyle]">
|
||||
<!-- #ifndef APP-PLUS -->
|
||||
<view v-if="cssSafeAreaInsetBottom===-1" class="zp-safe-area-inset-bottom"></view>
|
||||
<!-- #endif -->
|
||||
<!-- 二楼view -->
|
||||
<view v-if="showF2 && showRefresherF2" @touchmove.stop.prevent class="zp-f2-content" :style="[{'transform': f2Transform, 'transition': `transform .2s linear`, 'height': superContentHeight + 'px', 'z-index': f2ZIndex}]">
|
||||
<slot name="f2"/>
|
||||
</view>
|
||||
<!-- 顶部固定的slot -->
|
||||
<slot v-if="!usePageScroll&&zSlots.top" name="top" />
|
||||
<view class="zp-page-top" @touchmove.stop.prevent v-else-if="usePageScroll&&zSlots.top" :style="[{'top':`${windowTop}px`,'z-index':topZIndex}]">
|
||||
<slot name="top" />
|
||||
</view>
|
||||
<view :class="{'zp-view-super':true,'zp-scroll-view-super':!usePageScroll}" :style="[finalScrollViewStyle]">
|
||||
<view v-if="zSlots.left" :class="{'zp-page-left':true,'zp-absoulte':finalIsOldWebView}">
|
||||
<slot name="left" />
|
||||
</view>
|
||||
<view :class="{'zp-scroll-view-container':true,'zp-absoulte':finalIsOldWebView}" :style="[scrollViewContainerStyle]">
|
||||
<scroll-view
|
||||
ref="zp-scroll-view" :class="{'zp-scroll-view':true,'zp-scroll-view-absolute':!usePageScroll,'zp-scroll-view-hide-scrollbar':!showScrollbar}" :style="[chatRecordRotateStyle]"
|
||||
:scroll-top="scrollTop" :scroll-left="scrollLeft" :scroll-x="scrollX"
|
||||
:scroll-y="finalScrollable" :enable-back-to-top="finalEnableBackToTop"
|
||||
:show-scrollbar="showScrollbar" :scroll-with-animation="finalScrollWithAnimation"
|
||||
:scroll-into-view="scrollIntoView" :lower-threshold="finalLowerThreshold" :upper-threshold="5"
|
||||
:refresher-enabled="finalRefresherEnabled&&!useCustomRefresher" :refresher-threshold="finalRefresherThreshold"
|
||||
:refresher-default-style="finalRefresherDefaultStyle" :refresher-background="refresherBackground"
|
||||
:refresher-triggered="finalRefresherTriggered" @scroll="_scroll" @scrolltolower="_onScrollToLower"
|
||||
@scrolltoupper="_onScrollToUpper" @refresherrestore="_onRestore" @refresherrefresh="_onRefresh(true)"
|
||||
>
|
||||
<view class="zp-paging-touch-view"
|
||||
<!-- #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
|
||||
@touchstart="_refresherTouchstart" @touchmove="_refresherTouchmove" @touchend="_refresherTouchend" @touchcancel="_refresherTouchend"
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
|
||||
@touchstart="pagingWxs.touchstart" @touchmove="pagingWxs.touchmove" @touchend="pagingWxs.touchend" @touchcancel="pagingWxs.touchend"
|
||||
@mousedown="pagingWxs.mousedown" @mousemove="pagingWxs.mousemove" @mouseup="pagingWxs.mouseup" @mouseleave="pagingWxs.mouseleave"
|
||||
<!-- #endif -->
|
||||
>
|
||||
<view v-if="finalRefresherFixedBacHeight>0" class="zp-fixed-bac-view" :style="[{'background': refresherFixedBackground,'height': `${finalRefresherFixedBacHeight}px`}]"></view>
|
||||
<view class="zp-paging-main" :style="[scrollViewInStyle,{'transform': finalRefresherTransform,'transition': refresherTransition}]"
|
||||
<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
|
||||
:change:prop="pagingWxs.propObserver" :prop="wxsPropType"
|
||||
:data-refresherThreshold="finalRefresherThreshold" :data-refresherF2Enabled="refresherF2Enabled" :data-refresherF2Threshold="finalRefresherF2Threshold" :data-isIos="isIos"
|
||||
:data-loading="loading||isRefresherInComplete" :data-useChatRecordMode="useChatRecordMode"
|
||||
:data-refresherEnabled="refresherEnabled" :data-useCustomRefresher="useCustomRefresher" :data-pageScrollTop="wxsPageScrollTop"
|
||||
:data-scrollTop="wxsScrollTop" :data-refresherMaxAngle="refresherMaxAngle" :data-refresherNoTransform="refresherNoTransform"
|
||||
:data-refresherAecc="refresherAngleEnableChangeContinued" :data-usePageScroll="usePageScroll" :data-watchTouchDirectionChange="watchTouchDirectionChange"
|
||||
:data-oldIsTouchmoving="isTouchmoving" :data-refresherOutRate="finalRefresherOutRate" :data-refresherPullRate="finalRefresherPullRate" :data-hasTouchmove="hasTouchmove"
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-VUE || H5 -->
|
||||
:change:renderPropIsIosAndH5="pagingRenderjs.renderPropIsIosAndH5Change" :renderPropIsIosAndH5="isIosAndH5"
|
||||
<!-- #endif -->
|
||||
>
|
||||
<view v-if="showRefresher" class="zp-custom-refresher-view" :style="[{'margin-top': `-${finalRefresherThreshold+refresherThresholdUpdateTag}px`,'background': refresherBackground,'opacity': isTouchmoving ? 1 : 0}]">
|
||||
<view class="zp-custom-refresher-container" :style="[{'height': `${finalRefresherThreshold}px`,'background': refresherBackground}]">
|
||||
<view v-if="useRefresherStatusBarPlaceholder" class="zp-custom-refresher-status-bar-placeholder" :style="[{'height': `${statusBarHeight}px`}]" />
|
||||
<!-- 下拉刷新view -->
|
||||
<view class="zp-custom-refresher-slot-view">
|
||||
<slot v-if="!(zSlots.refresherComplete&&refresherStatus===R.Complete)&&!(zSlots.refresherF2&&refresherStatus===R.GoF2)" :refresherStatus="refresherStatus" name="refresher" />
|
||||
</view>
|
||||
<slot v-if="zSlots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
|
||||
<slot v-else-if="zSlots.refresherF2&&refresherStatus===R.GoF2" name="refresherF2" />
|
||||
<z-paging-refresh ref="refresh" v-else-if="!showCustomRefresher" class="zp-custom-refresher-refresh" :style="[{'height': `${finalRefresherThreshold - finalRefresherThresholdPlaceholder}px`}]" :status="refresherStatus"
|
||||
:defaultThemeStyle="finalRefresherThemeStyle" :defaultText="finalRefresherDefaultText" :isIos="isIos"
|
||||
:pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText" :goF2Text="finalRefresherGoF2Text"
|
||||
:defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg" :refreshingAnimated="refresherRefreshingAnimated"
|
||||
:showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
|
||||
:imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" :unit="unit" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="zp-paging-container" :style="[{justifyContent:useChatRecordMode?'flex-end':'flex-start'}]">
|
||||
<!-- 全屏Loading -->
|
||||
<slot v-if="showLoading&&zSlots.loading&&!loadingFullFixed" name="loading" />
|
||||
<!-- 主体内容 -->
|
||||
<view class="zp-paging-container-content" :style="[finalPlaceholderTopHeightStyle,finalPagingContentStyle]">
|
||||
<!-- #ifdef VUE3 -->
|
||||
<!-- 虚拟列表顶部占位view -->
|
||||
<view v-if="useVirtualList" class="zp-virtual-placeholder" :style="[{height:virtualPlaceholderTopHeight+'px'}]"/>
|
||||
<!-- #endif -->
|
||||
<slot />
|
||||
<!-- 内置列表&虚拟列表 -->
|
||||
<template v-if="finalUseInnerList">
|
||||
<slot name="header"/>
|
||||
<view class="zp-list-container" :style="[innerListStyle]">
|
||||
<template v-if="finalUseVirtualList">
|
||||
<view class="zp-list-cell" :style="[innerCellStyle]" :id="`${fianlVirtualCellIdPrefix}-${item[virtualCellIndexKey]}`" v-for="(item,index) in virtualList" :key="item['zp_unique_index']" @click="_innerCellClick(item,virtualTopRangeIndex+index)">
|
||||
<view v-if="useCompatibilityMode">使用兼容模式请在组件源码z-paging.vue第103行中注释这一行,并打开下面一行注释</view>
|
||||
<!-- <zp-public-virtual-cell v-if="useCompatibilityMode" :extraData="extraData" :item="item" :index="virtualTopRangeIndex+index" /> -->
|
||||
<slot v-else name="cell" :item="item" :index="virtualTopRangeIndex+index"/>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view class="zp-list-cell" v-for="(item,index) in realTotalData" :key="index" @click="_innerCellClick(item,index)">
|
||||
<slot name="cell" :item="item" :index="index"/>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
<slot name="footer"/>
|
||||
</template>
|
||||
<!-- 聊天记录模式加载更多loading -->
|
||||
<template v-if="useChatRecordMode&&realTotalData.length>=defaultPageSize&&(loadingStatus!==M.NoMore||zSlots.chatNoMore)&&(realTotalData.length||(showChatLoadingWhenReload&&showLoading))&&!isFirstPageAndNoMore">
|
||||
<view :style="[chatRecordRotateStyle]">
|
||||
<slot v-if="loadingStatus===M.NoMore&&zSlots.chatNoMore" name="chatNoMore" />
|
||||
<template v-else>
|
||||
<slot v-if="zSlots.chatLoading" :loadingMoreStatus="loadingStatus" name="chatLoading" />
|
||||
<z-paging-load-more v-else @doClick="_onLoadingMore('click')" :zConfig="zLoadMoreConfig" />
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 虚拟列表底部占位view -->
|
||||
<view v-if="useVirtualList" class="zp-virtual-placeholder" :style="[{height:virtualPlaceholderBottomHeight+'px'}]"/>
|
||||
<!-- 上拉加载更多view -->
|
||||
<!-- #ifndef MP-ALIPAY -->
|
||||
<slot v-if="showLoadingMoreDefault" name="loadingMoreDefault" />
|
||||
<slot v-else-if="showLoadingMoreLoading" name="loadingMoreLoading" />
|
||||
<slot v-else-if="showLoadingMoreNoMore" name="loadingMoreNoMore" />
|
||||
<slot v-else-if="showLoadingMoreFail" name="loadingMoreFail" />
|
||||
<z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMoreCustom" :zConfig="zLoadMoreConfig" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-ALIPAY -->
|
||||
<slot v-if="loadingStatus===M.Default&&zSlots.loadingMoreDefault&&showLoadingMore&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreDefault" />
|
||||
<slot v-else-if="loadingStatus===M.Loading&&zSlots.loadingMoreLoading&&showLoadingMore&&loadingMoreEnabled" name="loadingMoreLoading" />
|
||||
<slot v-else-if="loadingStatus===M.NoMore&&zSlots.loadingMoreNoMore&&showLoadingMore&&showLoadingMoreNoMoreView&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreNoMore" />
|
||||
<slot v-else-if="loadingStatus===M.Fail&&zSlots.loadingMoreFail&&showLoadingMore&&loadingMoreEnabled&&!useChatRecordMode" name="loadingMoreFail" />
|
||||
<z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMore&&showDefaultLoadingMoreText&&!(loadingStatus===M.NoMore&&!showLoadingMoreNoMoreView)&&loadingMoreEnabled&&!useChatRecordMode" :zConfig="zLoadMoreConfig" />
|
||||
<!-- #endif -->
|
||||
<view v-if="safeAreaInsetBottom&&useSafeAreaPlaceholder&&!useChatRecordMode" class="zp-safe-area-placeholder" :style="[{height:safeAreaBottom+'px'}]" />
|
||||
</view>
|
||||
<!-- 空数据图 -->
|
||||
<view v-if="showEmpty" :class="{'zp-empty-view':true,'zp-empty-view-center':emptyViewCenter}" :style="[emptyViewSuperStyle,chatRecordRotateStyle]">
|
||||
<slot v-if="zSlots.empty" name="empty" :isLoadFailed="isLoadFailed"/>
|
||||
<z-paging-empty-view v-else :emptyViewImg="finalEmptyViewImg" :emptyViewText="finalEmptyViewText" :showEmptyViewReload="finalShowEmptyViewReload"
|
||||
:emptyViewReloadText="finalEmptyViewReloadText" :isLoadFailed="isLoadFailed" :emptyViewStyle="emptyViewStyle" :emptyViewTitleStyle="emptyViewTitleStyle"
|
||||
:emptyViewImgStyle="emptyViewImgStyle" :emptyViewReloadStyle="emptyViewReloadStyle" :emptyViewZIndex="emptyViewZIndex" :emptyViewFixed="emptyViewFixed" :unit="unit"
|
||||
@reload="_emptyViewReload" @viewClick="_emptyViewClick" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view v-if="zSlots.right" :class="{'zp-page-right':true,'zp-absoulte zp-right':finalIsOldWebView}">
|
||||
<slot name="right" />
|
||||
</view>
|
||||
</view>
|
||||
<!-- 底部固定的slot -->
|
||||
<view class="zp-page-bottom-container" :style="{'background': bottomBgColor}">
|
||||
<slot v-if="!usePageScroll&&zSlots.bottom" name="bottom" />
|
||||
<view class="zp-page-bottom" @touchmove.stop.prevent v-else-if="usePageScroll&&zSlots.bottom" :style="[{'bottom': `${windowBottom}px`}]">
|
||||
<slot name="bottom" />
|
||||
</view>
|
||||
<!-- 聊天记录模式底部占位 -->
|
||||
<template v-if="useChatRecordMode&&autoAdjustPositionWhenChat">
|
||||
<view :style="[{height:chatRecordModeSafeAreaBottom+'px'}]" />
|
||||
<view class="zp-page-bottom-keyboard-placeholder-animate" :style="[{height:keyboardHeight+'px'}]" />
|
||||
</template>
|
||||
</view>
|
||||
<!-- 点击返回顶部view -->
|
||||
<view v-if="showBackToTopClass" :class="finalBackToTopClass" :style="[finalBackToTopStyle]" @click.stop="_backToTopClick">
|
||||
<slot v-if="zSlots.backToTop" name="backToTop" />
|
||||
<image v-else class="zp-back-to-top-img" :class="{'zp-back-to-top-img-inversion': useChatRecordMode&&!backToTopImg.length}" :src="backToTopImg.length?backToTopImg:base64BackToTop" />
|
||||
</view>
|
||||
<!-- 全屏Loading(铺满z-paging并固定) -->
|
||||
<view v-if="showLoading&&zSlots.loading&&loadingFullFixed" class="zp-loading-fixed">
|
||||
<slot name="loading" />
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<component ref="z-paging-content" :is="finalNvueSuperListIs" :style="[finalPagingStyle]" :class="{'z-paging-content-fixed':fixed&&!usePageScroll}" :scrollable="false">
|
||||
<!-- 二楼view -->
|
||||
<view v-if="showF2 && showRefresherF2" ref="zp-n-f2" class="zp-f2-content" @touchmove.stop.prevent :style="[{'height': superContentHeight + 'px', 'width': nRefresherWidth + 'px', 'opacity': nF2Opacity}]">
|
||||
<slot name="f2"/>
|
||||
</view>
|
||||
<!-- 顶部固定的slot -->
|
||||
<view ref="zp-page-top" v-if="zSlots.top" :class="{'zp-page-top':usePageScroll}" :style="[usePageScroll?{'top':`${windowTop}px`,'z-index':topZIndex}:{}]">
|
||||
<slot name="top" />
|
||||
</view>
|
||||
<!-- 聊天记录模式加载更多loading(loading时候显示) -->
|
||||
<view v-if="useChatRecordMode&&loadingStatus!==M.NoMore&&showChatLoadingWhenReload&&showLoading">
|
||||
<slot v-if="zSlots.chatLoading" :loadingMoreStatus="loadingStatus" name="chatLoading" />
|
||||
<z-paging-load-more v-else @doClick="_onLoadingMore('click')" :zConfig="zLoadMoreConfig" />
|
||||
</view>
|
||||
<component :is="finalNvueSuperListIs" class="zp-n-list-container" :scrollable="false">
|
||||
<view v-if="zSlots.left" class="zp-page-left">
|
||||
<slot name="left" />
|
||||
</view>
|
||||
<component :is="finalNvueListIs" ref="zp-n-list" :id="nvueListId" :style="[{'flex': 1,'top':isIos?'0px':'-1px'},usePageScroll?scrollViewStyle:{},chatRecordRotateStyle]" :alwaysScrollableVertical="true"
|
||||
:fixFreezing="nFixFreezing" :show-scrollbar="showScrollbar" :loadmoreoffset="finalLowerThreshold" :enable-back-to-top="enableBackToTop"
|
||||
:scrollable="finalScrollable" :bounce="nvueBounce" :column-count="nWaterfallColumnCount" :column-width="nWaterfallColumnWidth"
|
||||
:column-gap="nWaterfallColumnGap" :left-gap="nWaterfallLeftGap" :right-gap="nWaterfallRightGap" :pagingEnabled="nvuePagingEnabled" :offset-accuracy="offsetAccuracy"
|
||||
@loadmore="_nOnLoadmore" @scroll="_nOnScroll" @scrollend="_nOnScrollend">
|
||||
<refresh v-if="(zSlots.top?cacheTopHeight!==-1:true)&&finalNvueRefresherEnabled" class="zp-n-refresh" :style="[nvueRefresherStyle]" :display="nRefresherLoading?'show':'hide'" @refresh="_nOnRrefresh" @pullingdown="_nOnPullingdown">
|
||||
<view ref="zp-n-refresh-container" class="zp-n-refresh-container" :style="[{background:refresherBackground,width:nRefresherWidth}]" id="zp-n-refresh-container">
|
||||
<view v-if="useRefresherStatusBarPlaceholder" class="zp-custom-refresher-status-bar-placeholder" :style="[{'height': `${statusBarHeight}px`}]" />
|
||||
<!-- 下拉刷新view -->
|
||||
<slot v-if="zSlots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
|
||||
<slot v-else-if="zSlots.refresherF2&&refresherStatus===R.GoF2" name="refresherF2" />
|
||||
<slot v-else-if="(nScopedSlots?nScopedSlots:zSlots).refresher" :refresherStatus="refresherStatus" name="refresher" />
|
||||
<z-paging-refresh ref="refresh" v-else :status="refresherStatus" :defaultThemeStyle="finalRefresherThemeStyle" :isIos="isIos"
|
||||
:defaultText="finalRefresherDefaultText" :pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText" :goF2Text="finalRefresherGoF2Text"
|
||||
:defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg" :refreshingAnimated="refresherRefreshingAnimated"
|
||||
:showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
|
||||
:imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" :unit="unit" />
|
||||
</view>
|
||||
</refresh>
|
||||
<component :is="nViewIs" v-if="isIos&&!useChatRecordMode?oldScrollTop>10:true" ref="zp-n-list-top-tag" class="zp-n-list-top-tag" style="margin-top: -1rpx;" :style="[{height:finalNvueRefresherEnabled?'0px':'1px'}]"></component>
|
||||
<component :is="nViewIs" v-if="nShowRefresherReveal" ref="zp-n-list-refresher-reveal" :style="[{transform:`translateY(-${nShowRefresherRevealHeight}px)`},{background:refresherBackground}]">
|
||||
<view v-if="useRefresherStatusBarPlaceholder" class="zp-custom-refresher-status-bar-placeholder" :style="[{'height': `${statusBarHeight}px`}]" />
|
||||
<!-- 下拉刷新view -->
|
||||
<slot v-if="zSlots.refresherComplete&&refresherStatus===R.Complete" name="refresherComplete" />
|
||||
<slot v-else-if="zSlots.refresherF2&&refresherStatus===R.GoF2" name="refresherF2" />
|
||||
<slot v-else-if="(nScopedSlots?nScopedSlots:$slots).refresher" :refresherStatus="R.Loading" name="refresher" />
|
||||
<z-paging-refresh ref="refresh" v-else :status="R.Loading" :defaultThemeStyle="finalRefresherThemeStyle" :isIos="isIos"
|
||||
:defaultText="finalRefresherDefaultText" :pullingText="finalRefresherPullingText" :refreshingText="finalRefresherRefreshingText" :completeText="finalRefresherCompleteText" :goF2Text="finalRefresherGoF2Text"
|
||||
:defaultImg="refresherDefaultImg" :pullingImg="refresherPullingImg" :refreshingImg="refresherRefreshingImg" :completeImg="refresherCompleteImg" :refreshingAnimated="refresherRefreshingAnimated"
|
||||
:showUpdateTime="showRefresherUpdateTime" :updateTimeKey="refresherUpdateTimeKey" :updateTimeTextMap="finalRefresherUpdateTimeTextMap"
|
||||
:imgStyle="refresherImgStyle" :titleStyle="refresherTitleStyle" :updateTimeStyle="refresherUpdateTimeStyle" :unit="unit" />
|
||||
</component>
|
||||
<!-- 内置列表 -->
|
||||
<template v-if="finalUseInnerList">
|
||||
<component :is="nViewIs">
|
||||
<slot name="header"/>
|
||||
</component>
|
||||
<component :is="nViewIs" class="zp-list-cell" v-for="(item,index) in realTotalData" :key="finalCellKeyName.length?item[finalCellKeyName]:index">
|
||||
<slot name="cell" :item="item" :index="index"/>
|
||||
</component>
|
||||
<component :is="nViewIs">
|
||||
<slot name="footer"/>
|
||||
</component>
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot />
|
||||
</template>
|
||||
<!-- 全屏Loading -->
|
||||
<component :is="nViewIs" v-if="showLoading&&zSlots.loading&&!loadingFullFixed" :class="{'z-paging-content-fixed':usePageScroll}" style="flex:1" :style="[chatRecordRotateStyle]">
|
||||
<slot name="loading" />
|
||||
</component>
|
||||
<!-- 上拉加载更多view -->
|
||||
<component :is="nViewIs" v-if="!refresherOnly&&loadingMoreEnabled&&!showEmpty">
|
||||
<!-- 聊天记录模式加载更多loading(滚动到顶部加载更多或无更多数据时显示) -->
|
||||
<template v-if="useChatRecordMode&&realTotalData.length>=defaultPageSize&&(loadingStatus!==M.NoMore||zSlots.chatNoMore)&&realTotalData.length&&isChatRecordModeAndInversion">
|
||||
<view :style="[chatRecordRotateStyle]">
|
||||
<slot v-if="loadingStatus===M.NoMore&&zSlots.chatNoMore" name="chatNoMore" />
|
||||
<template v-else>
|
||||
<slot v-if="zSlots.chatLoading" :loadingMoreStatus="loadingStatus" name="chatLoading" />
|
||||
<z-paging-load-more v-else @doClick="_onLoadingMore('click')" :zConfig="zLoadMoreConfig" />
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view :style="nLoadingMoreFixedHeight?{height:loadingMoreCustomStyle&&loadingMoreCustomStyle.height?loadingMoreCustomStyle.height:loadingMoreFixedHeight}:{}">
|
||||
<slot v-if="showLoadingMoreDefault" name="loadingMoreDefault" />
|
||||
<slot v-else-if="showLoadingMoreLoading" name="loadingMoreLoading" />
|
||||
<slot v-else-if="showLoadingMoreNoMore" name="loadingMoreNoMore" />
|
||||
<slot v-else-if="showLoadingMoreFail" name="loadingMoreFail" />
|
||||
<z-paging-load-more @doClick="_onLoadingMore('click')" v-else-if="showLoadingMoreCustom" :zConfig="zLoadMoreConfig" />
|
||||
<view v-if="safeAreaInsetBottom&&useSafeAreaPlaceholder&&!useChatRecordMode" class="zp-safe-area-placeholder" :style="[{height:safeAreaBottom+'px'}]" />
|
||||
</view>
|
||||
</component>
|
||||
<!-- 空数据图 -->
|
||||
<component :is="nViewIs" v-if="showEmpty" :class="{'z-paging-content-fixed':usePageScroll}" :style="[{flex:emptyViewCenter?1:0},emptyViewSuperStyle,chatRecordRotateStyle]">
|
||||
<view :class="{'zp-empty-view':true,'zp-empty-view-center':emptyViewCenter}">
|
||||
<slot v-if="zSlots.empty" name="empty" :isLoadFailed="isLoadFailed" />
|
||||
<z-paging-empty-view v-else :emptyViewImg="finalEmptyViewImg" :emptyViewText="finalEmptyViewText" :showEmptyViewReload="finalShowEmptyViewReload"
|
||||
:emptyViewReloadText="finalEmptyViewReloadText" :isLoadFailed="isLoadFailed" :emptyViewStyle="emptyViewStyle" :emptyViewTitleStyle="emptyViewTitleStyle"
|
||||
:emptyViewImgStyle="emptyViewImgStyle" :emptyViewReloadStyle="emptyViewReloadStyle" :emptyViewZIndex="emptyViewZIndex" :emptyViewFixed="emptyViewFixed" :unit="unit"
|
||||
@reload="_emptyViewReload" @viewClick="_emptyViewClick" />
|
||||
</view>
|
||||
</component>
|
||||
<component :is="nViewIs" v-if="!hideNvueBottomTag" ref="zp-n-list-bottom-tag" class="zp-n-list-bottom-tag"></component>
|
||||
</component>
|
||||
<view v-if="zSlots.right" class="zp-page-right">
|
||||
<slot name="right" />
|
||||
</view>
|
||||
</component>
|
||||
<!-- 底部固定的slot -->
|
||||
<view class="zp-page-bottom-container" :style="{'background': bottomBgColor}">
|
||||
<slot name="bottom" />
|
||||
<!-- 聊天记录模式底部占位 -->
|
||||
<template v-if="useChatRecordMode&&autoAdjustPositionWhenChat">
|
||||
<view :style="[{height:chatRecordModeSafeAreaBottom+'px'}]" />
|
||||
<view class="zp-page-bottom-keyboard-placeholder-animate" :style="[{height:keyboardHeight+'px'}]" />
|
||||
</template>
|
||||
</view>
|
||||
<!-- 点击返回顶部view -->
|
||||
<view v-if="showBackToTopClass" :class="finalBackToTopClass" :style="[finalBackToTopStyle]" @click.stop="_backToTopClick">
|
||||
<slot v-if="zSlots.backToTop" name="backToTop" />
|
||||
<image v-else class="zp-back-to-top-img" :class="{'zp-back-to-top-img-inversion': useChatRecordMode&&!backToTopImg.length}" :src="backToTopImg.length?backToTopImg:base64BackToTop" />
|
||||
</view>
|
||||
<!-- 全屏Loading(铺满z-paging并固定) -->
|
||||
<view v-if="showLoading&&zSlots.loading&&loadingFullFixed" class="zp-loading-fixed">
|
||||
<slot name="loading" />
|
||||
</view>
|
||||
</component>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
<script module="pagingRenderjs" lang="renderjs">
|
||||
import pagingRenderjs from './wxs/z-paging-renderjs.js';
|
||||
/**
|
||||
* z-paging 分页组件
|
||||
* @description z-paging 分页组件,高性能,全平台兼容。支持自定义下拉刷新、上拉加载更多、虚拟列表、下拉进入二楼、自动管理空数据图、全自动分页、无闪动聊天分页、本地分页等,也支持作为基本布局容器使用
|
||||
* @tutorial https://z-paging.zxlee.cn
|
||||
* @property {Array} value 父组件v-model所绑定的list的值,默认为[]
|
||||
* @property {Number|String} defaultPageNo 自定义初始的pageNo,默认为1
|
||||
* @property {Number|String} defaultPageSize 自定义pageSize(每页显示多少条),默认为10
|
||||
* @property {Boolean} fixed z-paging是否使用fixed布局,默认为true
|
||||
* @property {Boolean} safeAreaInsetBottom 是否开启底部安全区域适配,默认为false
|
||||
* @property {Boolean} useSafeAreaPlaceholder 开启底部安全区域适配后,是否使用placeholder形式实现,默认为false
|
||||
* @property {Boolean} usePageScroll 使用页面滚动,默认为false
|
||||
* @property {Boolean} autoFullHeight 使用页面滚动时,是否在不满屏时自动填充满屏幕,默认为true
|
||||
* @property {String} defaultThemeStyle loading(下拉刷新、上拉加载更多)的主题样式,支持black,white,默认为black
|
||||
* @property {Object} pagingStyle 设置z-paging的style,部分平台(如微信小程序)无法直接修改组件的style,可使用此属性代替
|
||||
* @property {String} height z-paging的高度,优先级低于pagingStyle中设置的height,传字符串,如100px、100rpx、100%
|
||||
* @property {String} width z-paging的宽度,优先级低于pagingStyle中设置的width,传字符串,如100px、100rpx、100%
|
||||
* @property {String} maxWidth z-paging的最大宽度,优先级低于pagingStyle中设置的max-width,默认为空
|
||||
* @property {String} bgColor z-paging的背景色(为css中的background,因此也可以设置渐变,背景图片等),优先级低于pagingStyle中设置的background-color
|
||||
* @property {Boolean} watchTouchDirectionChange 是否监听列表触摸方向改变,默认为false
|
||||
* @property {Number|String} delay 调用complete后延迟处理的时间,单位为毫秒,优先级高于min-delay,默认为0
|
||||
* @property {Number|String} minDelay 触发@query后最小延迟处理的时间,单位为毫秒,优先级低于delay,默认为0
|
||||
* @property {Boolean} callNetworkReject 请求失败是否触发reject,默认为true
|
||||
* @property {String} unit z-paging中默认布局的单位,默认为rpx
|
||||
* @property {Boolean} concat 自动拼接complete中传过来的数组,默认为true
|
||||
* @property {Number|String|Object} dataKey 为保证数据一致,设置当前tab切换时的标识key,并在complete中传递相同key,若二者不一致,则complete将不会生效
|
||||
* @property {String} autowireListName 【极简写法】自动注入的list名,可自动修改父view(包含ref="paging")中对应name的list值
|
||||
* @property {String} autowireQueryName 【极简写法】自动注入的query名,可自动调用父view(包含ref="paging")中的query方法
|
||||
* @property {Function} fetch 【极简写法】获取分页数据Function,功能与@query类似。若设置了fetch则@query将不再触发
|
||||
* @property {Object} fetchParams fetch的附加参数,fetch配置后有效
|
||||
* @property {Boolean} auto [z-paging]mounted后是否自动调用reload方法(mounted后自动调用接口),默认为true
|
||||
* @property {Boolean} autoScrollToTopWhenReload reload时自动滚动到顶部,默认为true
|
||||
* @property {Boolean} autoCleanListWhenReload reload时立即自动清空原list,默认为true
|
||||
* @property {Boolean} showRefresherWhenReload 列表刷新时自动显示下拉刷新view,默认为false
|
||||
* @property {Boolean} showLoadingMoreWhenReload 列表刷新时自动显示加载更多view,且为加载中状态,默认为false
|
||||
* @property {Boolean} createdReload 组件created时立即触发reload,默认为false
|
||||
* @property {Boolean} refresherEnabled 是否开启下拉刷新,默认为true
|
||||
* @property {Number|String} refresherThreshold 设置自定义下拉刷新阈值,默认单位为px,默认为80rpx
|
||||
* @property {Boolean} useRefresherStatusBarPlaceholder 是否开启下拉刷新状态栏占位,默认为false
|
||||
* @property {Boolean} refresherOnly 是否只使用下拉刷新,默认为false
|
||||
* @property {Boolean} useCustomRefresher 是否使用自定义的下拉刷新,默认为true
|
||||
* @property {Boolean} reloadWhenRefresh 用户下拉刷新时是否触发reload方法,默认为true
|
||||
* @property {String} refresherThemeStyle 下拉刷新的主题样式,支持black,white,默认为black
|
||||
* @property {Object} refresherImgStyle 自定义下拉刷新中左侧图标的样式
|
||||
* @property {Object} refresherTitleStyle 自定义下拉刷新中右侧状态描述文字的样式
|
||||
* @property {Object} refresherUpdateTimeStyle 自定义下拉刷新中右侧最后更新时间文字的样式
|
||||
* @property {Boolean} watchRefresherTouchmove 是否实时监听下拉刷新中进度,并通过@refresherTouchmove传递给父组件,默认为false
|
||||
* @property {Boolean} showRefresherUpdateTime 是否显示最后更新时间,默认为false
|
||||
* @property {String|Object} refresherDefaultText 自定义下拉刷新默认状态下的文字
|
||||
* @property {String|Object} refresherPullingText 自定义下拉刷新松手立即刷新状态下的文字
|
||||
* @property {String|Object} refresherRefreshingText 自定义下拉刷新刷新中状态下的文字
|
||||
* @property {String|Object} refresherCompleteText 自定义下拉刷新刷新结束状态下的文字
|
||||
* @property {String} refresherDefaultImg 自定义下拉刷新默认状态下的图片
|
||||
* @property {String} refresherPullingImg 自定义下拉刷新松手立即刷新状态下的图片
|
||||
* @property {String} refresherRefreshingImg 自定义下拉刷新刷新中状态下的图片
|
||||
* @property {String} refresherCompleteImg 自定义下拉刷新刷新结束状态下的图片
|
||||
* @property {Boolean} refresherRefreshingAnimated 自定义下拉刷新刷新中状态下是否展示旋转动画,默认为true
|
||||
* @property {Boolean} refresherEndBounceEnabled 是否开启自定义下拉刷新刷新结束回弹动画效果,默认为true
|
||||
* @property {String} refresherDefaultStyle 设置系统下拉刷新默认样式,支持设置black,white,none,默认为black
|
||||
* @property {String} refresherBackground 设置自定义下拉刷新区域背景颜色,默认为#FFFFFF00
|
||||
* @property {String} refresherFixedBackground 设置固定的自定义下拉刷新区域背景颜色,默认为#FFFFFF00
|
||||
* @property {Number|String} refresherFixedBacHeight 设置固定的自定义下拉刷新区域高度,默认为0
|
||||
* @property {Number|String} refresherDefaultDuration 设置自定义下拉刷新默认状态下回弹动画时间,单位为毫秒,默认为100
|
||||
* @property {Number|String} refresherCompleteDelay 自定义下拉刷新结束以后延迟收回的时间,单位为毫秒,默认为0
|
||||
* @property {Number|String} refresherCompleteDuration 自定义下拉刷新结束收回动画时间,单位为毫秒,默认为300
|
||||
* @property {Boolean} refresherVibrate 下拉刷新时下拉到“松手立即刷新”状态时是否使手机短振动,默认为false
|
||||
* @property {Boolean} refresherRefreshingScrollable 自定义下拉刷新刷新中状态是否允许列表滚动,默认为true
|
||||
* @property {Boolean} refresherCompleteScrollable 自定义下拉刷新结束状态下是否允许列表滚动,默认为false
|
||||
* @property {Number} refresherOutRate 设置自定义下拉刷新下拉超出阈值后继续下拉位移衰减的比例,默认为0.65
|
||||
* @property {Boolean} refresherF2Enabled 是否开启下拉进入二楼功能,默认为false
|
||||
* @property {Number|String} refresherF2Threshold 下拉进入二楼阈值,默认为200rpx
|
||||
* @property {Number|String} refresherF2Duration 下拉进入二楼动画时间,单位为毫秒,默认为200
|
||||
* @property {Boolean} showRefresherF2 下拉进入二楼状态松手后是否弹出二楼,默认为true
|
||||
* @property {Number} refresherPullRate 设置自定义下拉刷新下拉时实际下拉位移与用户下拉距离的比值,默认为0.75
|
||||
* @property {Number|String} refresherFps 自定义下拉刷新下拉帧率,默认为40
|
||||
* @property {Number|String} refresherMaxAngle 自定义下拉刷新允许触发的最大下拉角度,默认为40度
|
||||
* @property {Boolean} refresherAngleEnableChangeContinued 自定义下拉刷新的角度由未达到最大角度变到达到最大角度时,是否继续下拉刷新手势,默认为false
|
||||
* @property {Boolean} refresherNoTransform 下拉刷新时是否禁止下拉刷新view跟随用户触摸竖直移动,默认为false
|
||||
* @property {Boolean} loadingMoreEnabled 是否启用加载更多数据(含滑动到底部加载更多数据和点击加载更多数据),默认为true
|
||||
* @property {Number|String} lowerThreshold 距底部/右边多远时,触发scrolltolower事件,默认单位为px,默认为100rpx
|
||||
* @property {Boolean} toBottomLoadingMoreEnabled 是否启用滑动到底部加载更多数据,默认为true
|
||||
* @property {String} loadingMoreThemeStyle 底部加载更多的主题样式,支持black,white,默认为black
|
||||
* @property {Object} loadingMoreCustomStyle 自定义底部加载更多样式
|
||||
* @property {Object} loadingMoreTitleCustomStyle 自定义底部加载更多文字样式
|
||||
* @property {Object} loadingMoreLoadingIconCustomStyle 自定义底部加载更多加载中动画样式
|
||||
* @property {String} loadingMoreLoadingIconType 自定义底部加载更多加载中动画图标类型,可选flower或circle,默认为flower
|
||||
* @property {String} loadingMoreLoadingIconCustomImage 自定义底部加载更多加载中动画图标图片
|
||||
* @property {Boolean} loadingMoreLoadingAnimated 底部加载更多加载中view是否展示旋转动画,默认为true
|
||||
* @property {String|Object} loadingMoreDefaultText 滑动到底部"默认"文字
|
||||
* @property {String|Object} loadingMoreLoadingText 滑动到底部"加载中"文字
|
||||
* @property {String|Object} loadingMoreNoMoreText 滑动到底部"没有更多"文字
|
||||
* @property {String|Object} loadingMoreFailText 滑动到底部"加载失败"文字
|
||||
* @property {Boolean} hideNoMoreInside 当没有更多数据且分页内容未超出z-paging时是否隐藏没有更多数据的view,默认为false
|
||||
* @property {Number} hideNoMoreByLimit 当没有更多数据且分页数组长度少于这个值时,隐藏没有更多数据的view,默认为0
|
||||
* @property {Boolean} insideMore 当分页未满一屏时,是否自动加载更多,默认为false
|
||||
* @property {Boolean} loadingMoreDefaultAsLoading 滑动到底部状态为默认状态时,以加载中的状态展示,默认为false
|
||||
* @property {Boolean} showLoadingMoreNoMoreView 是否显示没有更多数据的view,默认为true
|
||||
* @property {Boolean} showDefaultLoadingMoreText 是否显示默认的加载更多text,默认为true
|
||||
* @property {Boolean} showLoadingMoreNoMoreLine 是否显示没有更多数据的分割线,默认为true
|
||||
* @property {Object} loadingMoreNoMoreLineCustomStyle 自定义底部没有更多数据的分割线样式
|
||||
* @property {Boolean} hideEmptyView 是否强制隐藏空数据图,默认为false
|
||||
* @property {Boolean} emptyViewFixed 空数据图片是否铺满z-paging,默认为false
|
||||
* @property {Boolean} emptyViewCenter 空数据图片是否垂直居中,默认为true
|
||||
* @property {String|Object} emptyViewText 空数据图描述文字
|
||||
* @property {String} emptyViewImg 空数据图图片
|
||||
* @property {String} emptyViewErrorImg 空数据图“加载失败”图片
|
||||
* @property {String|Object} emptyViewReloadText 空数据图点击重新加载文字
|
||||
* @property {String|Object} emptyViewErrorText 空数据图“加载失败”描述文字
|
||||
* @property {Object} emptyViewSuperStyle 空数据图父view样式
|
||||
* @property {Object} emptyViewStyle 空数据图样式
|
||||
* @property {Object} emptyViewImgStyle 空数据图img样式
|
||||
* @property {Object} emptyViewTitleStyle 空数据图描述文字样式
|
||||
* @property {Object} emptyViewReloadStyle 空数据图重新加载按钮样式
|
||||
* @property {Boolean} showEmptyViewReload 是否显示空数据图重新加载按钮(无数据时),默认为false
|
||||
* @property {Boolean} showEmptyViewReloadWhenError 加载失败时是否显示空数据图重新加载按钮,默认为true
|
||||
* @property {Boolean} autoHideEmptyViewWhenLoading 加载中时是否自动隐藏空数据图,默认为true
|
||||
* @property {Boolean} autoHideEmptyViewWhenPull 用户下拉列表触发下拉刷新加载中时是否自动隐藏空数据图,默认为true
|
||||
* @property {Boolean} autoHideLoadingAfterFirstLoaded 第一次加载后自动隐藏loading slot,默认为true
|
||||
* @property {Boolean} loadingFullFixed loading slot的父view是否铺满屏幕并固定,默认为false
|
||||
* @property {Boolean} autoShowSystemLoading 是否自动显示系统Loading:即uni.showLoading,默认为false
|
||||
* @property {String|Object} systemLoadingText 显示系统Loading时显示的文字
|
||||
* @property {Boolean} systemLoadingMask 显示系统Loading时是否显示透明蒙层,防止触摸穿透,默认为true
|
||||
* @property {Boolean} autoShowBackToTop 自动显示点击返回顶部按钮,默认为false
|
||||
* @property {Number|String} backToTopThreshold 点击返回顶部按钮显示/隐藏的阈值(滚动距离),默认单位为px,默认为400rpx
|
||||
* @property {String} backToTopImg 点击返回顶部按钮的自定义图片地址
|
||||
* @property {Boolean} backToTopWithAnimate 点击返回顶部按钮返回到顶部时是否展示过渡动画,默认为true
|
||||
* @property {Number|String} backToTopBottom 点击返回顶部按钮与底部的距离,默认单位为px,默认为160rpx
|
||||
* @property {Object} backToTopStyle 点击返回顶部按钮的自定义样式
|
||||
* @property {Boolean} useVirtualList 是否使用虚拟列表,默认为false
|
||||
* @property {Boolean} useCompatibilityMode 在使用虚拟列表时,是否使用兼容模式,默认为false
|
||||
* @property {Object} extraData 使用兼容模式时传递的附加数据
|
||||
* @property {String} cellHeightMode 虚拟列表cell高度模式,默认为fixed
|
||||
* @property {Number|String} preloadPage 预加载的列表可视范围(列表高度)页数,默认为12
|
||||
* @property {Number|String} fixedCellHeight 固定的cell高度,`cell-height-mode=fixed`才有效,默认为空
|
||||
* @property {Number|String} virtualListCol 虚拟列表列数,默认为1
|
||||
* @property {Number|String} virtualScrollFps 虚拟列表scroll取样帧率,默认为80
|
||||
* @property {String} virtualCellIdPrefix 虚拟列表cell id的前缀
|
||||
* @property {Boolean} useInnerList 是否在z-paging内部循环渲染列表(使用内置列表),默认为false
|
||||
* @property {Boolean} forceCloseInnerList 强制关闭inner-list,默认为false
|
||||
* @property {Boolean} virtualInSwiperSlot 虚拟列表是否使用swiper-item包裹,默认为false
|
||||
* @property {String} cellKeyName 内置列表cell的key名称(仅nvue有效)
|
||||
* @property {Object} innerListStyle innerList样式
|
||||
* @property {Object} innerCellStyle innerCell样式
|
||||
* @property {Number|String} localPagingLoadingTime 本地分页时上拉加载更多延迟时间,单位为毫秒,默认为200
|
||||
* @property {Boolean} useChatRecordMode 使用聊天记录模式,默认为false
|
||||
* @property {Boolean} autoHideKeyboardWhenChat 使用聊天记录模式时是否自动隐藏键盘,默认为true
|
||||
* @property {Boolean} autoAdjustPositionWhenChat 使用聊天记录模式中键盘弹出时是否自动调整slot="bottom"高度,默认为true
|
||||
* @property {Boolean} autoToBottomWhenChat 使用聊天记录模式中键盘弹出时是否自动滚动到底部,默认为false
|
||||
* @property {String} chatAdjustPositionOffset 使用聊天记录模式中键盘弹出时占位高度偏移距离,默认为0px
|
||||
* @property {Boolean} showChatLoadingWhenReload 使用聊天记录模式中`reload`时是否显示`chatLoading`,默认为false
|
||||
* @property {String} bottomBgColor `bottom`的背景色,默认透明
|
||||
* @property {Boolean} chatLoadingMoreDefaultAsLoading 在聊天记录模式中滑动到顶部状态为默认状态时,是否以加载中的状态展示,默认为true
|
||||
* @property {Boolean} showScrollbar 控制是否出现滚动条,默认为true
|
||||
* @property {Boolean} scrollable 是否可以滚动,使用内置scroll-view和nvue时有效,默认为true
|
||||
* @property {Boolean} scrollX 是否允许横向滚动,默认为false
|
||||
* @property {Boolean} scrollToTopBounceEnabled iOS设备上滚动到顶部时是否允许回弹效果,默认为false
|
||||
* @property {Boolean} scrollToBottomBounceEnabled iOS设备上滚动到底部时是否允许回弹效果,默认为true
|
||||
* @property {Boolean} scrollWithAnimation 在设置滚动条位置时使用动画过渡,默认为false
|
||||
* @property {String} scrollIntoView 值应为某子元素id(id不能以数字开头)。设置哪个方向可滚动,则在哪个方向滚动到该元素
|
||||
* @property {Boolean} enableBackToTop iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部,默认为true
|
||||
* @property {String} nvueListIs nvue中修改列表类型,默认为list
|
||||
* @property {Object} nvueWaterfallConfig waterfall配置,仅在nvue中且nvueListIs=waterfall时有效
|
||||
* @property {Boolean} nvueBounce nvue控制是否回弹效果,iOS不支持动态修改,默认为true
|
||||
* @property {Boolean} nvueFastScroll nvue中通过代码滚动到顶部/底部时,是否加快动画效果,默认为false
|
||||
* @property {String} nvueListId nvue中list的id
|
||||
* @property {Boolean} hideNvueBottomTag 是否隐藏nvue列表底部的tagView,默认为false
|
||||
* @property {Boolean} nvuePagingEnabled 设置nvue中是否按分页模式(类似竖向swiper)显示List,默认为false
|
||||
* @property {Number} offsetAccuracy nvue中控制onscroll事件触发的频率,默认为空
|
||||
* @property {Boolean} useCache 是否使用缓存,默认为false
|
||||
* @property {String} cacheKey 使用缓存时缓存的key
|
||||
* @property {String} cacheMode 缓存模式,默认为default
|
||||
* @property {Number} topZIndex slot="top"的view的z-index,默认为99
|
||||
* @property {Number} superContentZIndex z-paging内容容器父view的z-index,默认为1
|
||||
* @property {Number} contentZIndex z-paging内容容器部分的z-index,默认为1
|
||||
* @property {Number} emptyViewZIndex 空数据view的z-index,默认为9
|
||||
* @property {Boolean} autoHeight z-paging是否自动高度,默认为false
|
||||
* @property {Number|String} autoHeightAddition z-paging自动高度时的附加高度,默认为0px
|
||||
* @event {Function} input 父组件v-model所绑定的list的值改变时触发此事件
|
||||
* @event {Function} query 下拉刷新或滚动到底部时会自动触发此方法。z-paging加载时也会触发(若要禁止,请设置:auto="false")。pageNo和pageSize会自动计算好,直接传给服务器即可。
|
||||
* @event {Function} listChange 分页渲染的数组改变时触发
|
||||
* @event {Function} refresherStatusChange 自定义下拉刷新状态改变
|
||||
* @event {Function} refresherTouchstart 自定义下拉刷新下拉开始
|
||||
* @event {Function} refresherTouchmove 自定义下拉刷新下拉拖动中
|
||||
* @event {Function} refresherTouchend 自定义下拉刷新下拉结束
|
||||
* @event {Function} refresherF2Change 下拉进入二楼状态改变
|
||||
* @event {Function} refresh 自定义下拉刷新被触发
|
||||
* @event {Function} restore 自定义下拉刷新被复位
|
||||
* @event {Function} loadingStatusChange 自定义下拉刷新状态改变
|
||||
* @event {Function} emptyViewReload 点击了空数据图中的重新加载按钮
|
||||
* @event {Function} emptyViewClick 点击了空数据图view
|
||||
* @event {Function} isLoadFailedChange z-paging请求失败状态改变
|
||||
* @event {Function} backToTopClick 点击了返回顶部按钮
|
||||
* @event {Function} virtualListChange 虚拟列表当前渲染的数组改变时触发
|
||||
* @event {Function} innerCellClick 使用虚拟列表或内置列表时点击了cell
|
||||
* @event {Function} virtualPlaceholderTopHeight 虚拟列表顶部占位高度改变
|
||||
* @event {Function} hidedKeyboard 在聊天记录模式下,触摸列表隐藏了键盘
|
||||
* @event {Function} keyboardHeightChange 键盘高度改变
|
||||
* @event {Function} scroll z-paging列表滚动时触发
|
||||
* @event {Function} scrollTopChange scrollTop改变时触发
|
||||
* @event {Function} scrolltolower z-paging内置的scroll-view/list-view/waterfall滚动底部时触发
|
||||
* @event {Function} scrolltoupper z-paging内置的scroll-view/list-view/waterfall滚动顶部时触发
|
||||
* @event {Function} scrollend z-paging内置的list滚动结束时触发
|
||||
* @event {Function} contentHeightChanged z-paging中内容高度改变时触发
|
||||
* @event {Function} touchDirectionChange 监听列表触摸方向改变
|
||||
* @example <z-paging ref="paging" v-model="dataList" @query="queryList"></z-paging>
|
||||
*/
|
||||
export default {
|
||||
name:"z-paging",
|
||||
// #ifdef APP-VUE || H5
|
||||
mixins: [pagingRenderjs],
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="./js/z-paging-main.js" />
|
||||
<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->
|
||||
<script src="./wxs/z-paging-wxs.wxs" module="pagingWxs" lang="wxs"></script>
|
||||
<!-- #endif -->
|
||||
|
||||
|
||||
<style scoped>
|
||||
@import "./css/z-paging-main.css";
|
||||
@import "./css/z-paging-static.css";
|
||||
</style>
|
||||
89
uni_modules/z-paging/package.json
Normal file
89
uni_modules/z-paging/package.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"id": "z-paging",
|
||||
"name": "z-paging",
|
||||
"displayName": "【z-paging下拉刷新、上拉加载】高性能,全平台兼容。支持虚拟列表,分页全自动处理",
|
||||
"version": "2.8.6",
|
||||
"description": "超简单、低耦合!使用wxs+renderjs实现。支持自定义下拉刷新、上拉加载更多、虚拟列表、下拉进入二楼、自动管理空数据图、无闪动聊天分页、本地分页、国际化等数百项配置",
|
||||
"keywords": [
|
||||
"下拉刷新",
|
||||
"上拉加载",
|
||||
"分页器",
|
||||
"nvue",
|
||||
"虚拟列表"
|
||||
],
|
||||
"repository": "https://github.com/SmileZXLee/uni-z-paging",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.0.7"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": "393727164"
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "https://www.npmjs.com/package/z-paging",
|
||||
"type": "component-vue"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y",
|
||||
"alipay": "n"
|
||||
},
|
||||
"client": {
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "y",
|
||||
"app-harmony": "u",
|
||||
"app-uvue": "u"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "y"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "y",
|
||||
"IE": "y",
|
||||
"Edge": "y",
|
||||
"Firefox": "y",
|
||||
"Safari": "y"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "y",
|
||||
"百度": "y",
|
||||
"字节跳动": "y",
|
||||
"QQ": "y",
|
||||
"钉钉": "y",
|
||||
"快手": "y",
|
||||
"飞书": "y",
|
||||
"京东": "y"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "y",
|
||||
"联盟": "y"
|
||||
},
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
uni_modules/z-paging/readme.md
Normal file
57
uni_modules/z-paging/readme.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# z-paging
|
||||
|
||||
<p align="center">
|
||||
<img alt="logo" src="https://z-paging.zxlee.cn/img/title-logo.png" height="100" style="margin-bottom: 50px;" />
|
||||
</p>
|
||||
|
||||
[](https://github.com/SmileZXLee/uni-z-paging) [](https://en.wikipedia.org/wiki/MIT_License)
|
||||
<img height="0" width="0" src="https://api.z-notify.zxlee.cn/v1/public/statistics/8293556910106066944/addOnly?from=uni" />
|
||||
|
||||
`z-paging-x`现已支持uniapp x,持续完善中,插件地址👉🏻 [https://ext.dcloud.net.cn/plugin?name=z-paging-x](https://ext.dcloud.net.cn/plugin?name=z-paging-x)
|
||||
|
||||
### 文档地址:[https://z-paging.zxlee.cn](https://z-paging.zxlee.cn)
|
||||
|
||||
### 更新组件前,请注意[版本差异](https://z-paging.zxlee.cn/start/upgrade-guide.html)
|
||||
|
||||
***
|
||||
### 功能&特点
|
||||
* 【配置简单】仅需两步(绑定网络请求方法、绑定分页结果数组)轻松完成完整下拉刷新,上拉加载更多功能。
|
||||
* 【低耦合,低侵入】分页自动管理。在page中无需处理任何分页相关逻辑,无需在data中定义任何分页相关变量,全由z-paging内部处理。
|
||||
* 【超灵活,支持各种类型自定义】支持自定义下拉刷新,自定义上拉加载更多等各种自定义效果;支持使用内置自动分页,同时也支持通过监听下拉刷新和滚动到底部事件自行处理;支持使用自带全屏布局规范,同时也支持将z-paging自由放在任意容器中。
|
||||
* 【功能丰富】支持国际化,支持自定义且自动管理空数据图,支持主题模式切换,支持本地分页,支持无闪动聊天分页模式,支持展示最后更新时间,支持吸顶效果,支持内部scroll-view滚动与页面滚动,支持一键滚动到顶部,支持下拉进入二楼等诸多功能。
|
||||
* 【【全平台兼容】支持vue&nvue,vue2&vue3,js&ts,支持h5、app、鸿蒙Next及各家小程序。
|
||||
* 【高性能】在app-vue、h5、微信小程序、QQ小程序上使用wxs+renderjs在视图层实现下拉刷新;支持虚拟列表,轻松渲染百万级列表数据!
|
||||
|
||||
***
|
||||
### 反馈qq群
|
||||
* 官方1群`已满`:[790460711](https://jq.qq.com/?_wv=1027&k=vU2fKZZH)
|
||||
|
||||
* 官方2群`已满`:[371624008](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=avPmibADf2TNi4LxkIwjCE5vbfXpa-r1&authKey=dQ%2FVDAR87ONxI4b32Py%2BvmXbhnopjHN7%2FJPtdsqJdsCPFZB6zDQ17L06Uh0kITUZ&noverify=0&group_code=371624008)
|
||||
* 官方3群:[343409055](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=sIaNqiCMIjxGQVksjytCw6R8DSiibHR7&authKey=pp995q8ZzFtl5F2xUwvvceP24QTcguWW%2FRVoDnMa8JZF4L2DmS%2B%2FV%2F5sYrcgPsmW&noverify=0&group_code=343409055)
|
||||
|
||||
***
|
||||
|
||||
### 预览
|
||||
|
||||
***
|
||||
|
||||
| 自定义下拉刷新效果演示 | 滑动切换选项卡+吸顶演示 | 聊天记录模式演示 |
|
||||
| :----------------------------------------------------------: | :----------------------------------------------------------: | ------------------------------------------------------------ |
|
||||
|  |  |  |
|
||||
|
||||
| 虚拟列表(流畅渲染1万+条)演示 | 下拉进入二楼演示 | 在弹窗内使用演示 |
|
||||
| :----------------------------------------------------------: | :----------------------------------------------------------: | ------------------------------------------------------------ |
|
||||
|  |  |  |
|
||||
|
||||
|
||||
### 在线demo体验地址:
|
||||
|
||||
* [https://demo.z-paging.zxlee.cn](https://demo.z-paging.zxlee.cn)
|
||||
|
||||
| 扫码体验 |
|
||||
| ------------------------------------------------------------ |
|
||||
|  |
|
||||
|
||||
### demo下载
|
||||
* 支持vue2&vue3的`选项式api`写法demo下载,请点击页面右上角的【使用HBuilderX导入示例项目】或【下载示例项目ZIP】。
|
||||
* 支持vue3的`组合式api`写法demo下载,请访问[github](https://github.com/SmileZXLee/uni-z-paging)。
|
||||
11
uni_modules/z-paging/types/comps.d.ts
vendored
Normal file
11
uni_modules/z-paging/types/comps.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
['z-paging']: typeof import('./comps/z-paging')['ZPaging']
|
||||
['z-paging-swiper']: typeof import('./comps/z-paging-swiper')['ZPagingSwiper']
|
||||
['z-paging-swiper-item']: typeof import('./comps/z-paging-swiper-item')['ZPagingSwiperItem']
|
||||
['z-paging-empty-view']: typeof import('./comps/z-paging-empty-view')['ZPagingEmptyView']
|
||||
['z-paging-cell']: typeof import('./comps/z-paging-cell')['ZPagingCell']
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
9
uni_modules/z-paging/types/comps/_common.d.ts
vendored
Normal file
9
uni_modules/z-paging/types/comps/_common.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface AllowedComponentProps {
|
||||
class?: unknown;
|
||||
style?: unknown;
|
||||
}
|
||||
|
||||
export interface VNodeProps {
|
||||
key?: string | number | symbol;
|
||||
ref?: unknown;
|
||||
}
|
||||
29
uni_modules/z-paging/types/comps/z-paging-cell.d.ts
vendored
Normal file
29
uni_modules/z-paging/types/comps/z-paging-cell.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
import { AllowedComponentProps, VNodeProps } from './_common'
|
||||
|
||||
// ****************************** Props ******************************
|
||||
declare interface ZPagingCellProps {
|
||||
/**
|
||||
* z-paging-cell样式
|
||||
*/
|
||||
cellStyle?: Record<string, any>
|
||||
}
|
||||
|
||||
// ****************************** Slots ******************************
|
||||
declare interface ZPagingCellSlots {
|
||||
// ******************** 主体布局Slot ********************
|
||||
/**
|
||||
* 默认插入的view
|
||||
*/
|
||||
['default']?: () => any
|
||||
}
|
||||
|
||||
declare interface _ZPagingCell {
|
||||
new (): {
|
||||
$props: AllowedComponentProps &
|
||||
VNodeProps &
|
||||
ZPagingCellProps
|
||||
$slots: ZPagingCellSlots
|
||||
}
|
||||
}
|
||||
|
||||
export declare const ZPagingCell: _ZPagingCell
|
||||
95
uni_modules/z-paging/types/comps/z-paging-empty-view.d.ts
vendored
Normal file
95
uni_modules/z-paging/types/comps/z-paging-empty-view.d.ts
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
import { AllowedComponentProps, VNodeProps } from './_common'
|
||||
|
||||
// ****************************** Props ******************************
|
||||
declare interface ZPagingEmptyViewProps {
|
||||
/**
|
||||
* 空数据图片是否铺满z-paging,默认为是。若设置为否,则为填充满z-paging的剩余部分
|
||||
* @default false
|
||||
* @since 2.0.3
|
||||
*/
|
||||
emptyViewFixed?: boolean;
|
||||
|
||||
/**
|
||||
* 空数据图描述文字
|
||||
* @default "没有数据哦~"
|
||||
*/
|
||||
emptyViewText?: string;
|
||||
|
||||
/**
|
||||
* 空数据图图片,默认使用z-paging内置的图片
|
||||
* - 建议使用绝对路径,开头不要添加"@",请以"/"开头
|
||||
*/
|
||||
emptyViewImg?: string;
|
||||
|
||||
/**
|
||||
* 空数据图点击重新加载文字
|
||||
* @default "重新加载"
|
||||
* @since 1.6.7
|
||||
*/
|
||||
emptyViewReloadText?: string;
|
||||
|
||||
/**
|
||||
* 空数据图样式,可设置空数据view的top等
|
||||
* - 如果空数据图不是fixed布局,则此处是`margin-top`
|
||||
*/
|
||||
emptyViewStyle?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 空数据图img样式
|
||||
*/
|
||||
emptyViewImgStyle?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 空数据图描述文字样式
|
||||
*/
|
||||
emptyViewTitleStyle?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 空数据图重新加载按钮样式
|
||||
* @since 1.6.7
|
||||
*/
|
||||
emptyViewReloadStyle?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* 是否显示空数据图重新加载按钮(无数据时)
|
||||
* @default false
|
||||
* @since 1.6.7
|
||||
*/
|
||||
showEmptyViewReload?: boolean;
|
||||
|
||||
/**
|
||||
* 是否是加载失败
|
||||
* @default false
|
||||
*/
|
||||
isLoadFailed?: boolean;
|
||||
|
||||
/**
|
||||
* 空数据图中布局的单位
|
||||
* @default 'rpx'
|
||||
* @since 2.6.7
|
||||
*/
|
||||
unit?: 'rpx' | 'px';
|
||||
|
||||
// ****************************** Events ******************************
|
||||
/**
|
||||
* 点击了重新加载按钮
|
||||
*/
|
||||
onReload?: () => void
|
||||
|
||||
/**
|
||||
* 点击了空数据view
|
||||
* @since 2.3.3
|
||||
*/
|
||||
onViewClick?: () => void
|
||||
}
|
||||
|
||||
declare interface _ZPagingEmptyView {
|
||||
new (): {
|
||||
$props: AllowedComponentProps &
|
||||
VNodeProps &
|
||||
ZPagingEmptyViewProps
|
||||
}
|
||||
}
|
||||
|
||||
export declare const ZPagingEmptyView: _ZPagingEmptyView
|
||||
|
||||
95
uni_modules/z-paging/types/comps/z-paging-swiper-item.d.ts
vendored
Normal file
95
uni_modules/z-paging/types/comps/z-paging-swiper-item.d.ts
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
import { AllowedComponentProps, VNodeProps } from './_common'
|
||||
|
||||
// ****************************** Props ******************************
|
||||
declare interface ZPagingSwiperItemProps {
|
||||
/**
|
||||
* 当前组件的index,也就是当前组件是swiper中的第几个
|
||||
* @default 0
|
||||
*/
|
||||
tabIndex?: number
|
||||
|
||||
/**
|
||||
* 当前swiper切换到第几个index
|
||||
* @default 0
|
||||
*/
|
||||
currentIndex?: number
|
||||
|
||||
/**
|
||||
* 是否使用虚拟列表。使用页面滚动或nvue时,不支持虚拟列表。在nvue中z-paging内置了list组件,效果与虚拟列表类似,并且可以提供更好的性能。
|
||||
* @default false
|
||||
*/
|
||||
useVirtualList?: boolean
|
||||
|
||||
/**
|
||||
* 虚拟列表cell高度模式,默认为`fixed`,也就是每个cell高度完全相同,将以第一个cell高度为准进行计算。
|
||||
* @default 'fixed'
|
||||
*/
|
||||
cellHeightMode?: 'fixed' | 'dynamic'
|
||||
|
||||
/**
|
||||
* 预加载的列表可视范围(列表高度)页数。此数值越大,则虚拟列表中加载的dom越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题。
|
||||
* @default 12
|
||||
*/
|
||||
preloadPage?: number | string
|
||||
|
||||
/**
|
||||
* 虚拟列表列数,默认为1。常用于每行有多列的情况,例如每行有2列数据,需要将此值设置为2。
|
||||
* @default 1
|
||||
* @since 2.2.8
|
||||
*/
|
||||
virtualListCol?: number | string
|
||||
|
||||
/**
|
||||
* 虚拟列表scroll取样帧率,默认为80,过低容易出现白屏问题,过高容易出现卡顿问题
|
||||
* @default 80
|
||||
*/
|
||||
virtualScrollFps?: number | string
|
||||
|
||||
/**
|
||||
* 是否在z-paging内部循环渲染列表(使用内置列表)。
|
||||
* @default false
|
||||
*/
|
||||
useInnerList?: boolean
|
||||
|
||||
/**
|
||||
* 内置列表cell的key名称(仅nvue有效,在nvue中开启use-inner-list时必须填此项)
|
||||
* @since 2.2.7
|
||||
*/
|
||||
cellKeyName?: string
|
||||
|
||||
/**
|
||||
* innerList样式
|
||||
*/
|
||||
innerListStyle?: Record<string, any>
|
||||
}
|
||||
|
||||
// ****************************** Methods ******************************
|
||||
declare interface _ZPagingSwiperItemRef {
|
||||
/**
|
||||
* 重新加载分页数据,pageNo恢复为默认值,相当于下拉刷新的效果
|
||||
*
|
||||
* @param [animate=false] 是否展示下拉刷新动画
|
||||
*/
|
||||
reload: (animate?: boolean) => void;
|
||||
|
||||
/**
|
||||
* 请求结束
|
||||
* - 当通过complete传进去的数组长度小于pageSize时,则判定为没有更多了
|
||||
*
|
||||
* @param [data] 请求结果数组
|
||||
* @param [success=true] 是否请求成功
|
||||
*/
|
||||
complete: (data?: any[] | false, success?: boolean) => void;
|
||||
}
|
||||
|
||||
declare interface _ZPagingSwiperItem {
|
||||
new (): {
|
||||
$props: AllowedComponentProps &
|
||||
VNodeProps &
|
||||
ZPagingSwiperItemProps
|
||||
}
|
||||
}
|
||||
|
||||
export declare const ZPagingSwiperItem: _ZPagingSwiperItem
|
||||
|
||||
export declare const ZPagingSwiperItemRef: _ZPagingSwiperItemRef
|
||||
89
uni_modules/z-paging/types/comps/z-paging-swiper.d.ts
vendored
Normal file
89
uni_modules/z-paging/types/comps/z-paging-swiper.d.ts
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
import { AllowedComponentProps, VNodeProps } from './_common'
|
||||
|
||||
// ****************************** Props ******************************
|
||||
declare interface ZPagingSwiperProps {
|
||||
/**
|
||||
* 是否使用fixed布局,若使用fixed布局,则z-paging-swiper的父view无需固定高度,z-paging高度默认铺满屏幕,页面中的view请放z-paging-swiper标签内,需要固定在顶部的view使用slot="top"包住,需要固定在底部的view使用slot="bottom"包住。
|
||||
* @default true
|
||||
*/
|
||||
fixed?: boolean
|
||||
|
||||
/**
|
||||
* 是否开启底部安全区域适配
|
||||
* @default false
|
||||
*/
|
||||
safeAreaInsetBottom?: boolean
|
||||
|
||||
/**
|
||||
* z-paging-swiper样式
|
||||
*/
|
||||
swiperStyle?: Record<string, any>
|
||||
}
|
||||
|
||||
|
||||
// ****************************** Slots ******************************
|
||||
declare interface ZPagingSwiperSlots {
|
||||
// ******************** 主体布局Slot ********************
|
||||
/**
|
||||
* 默认插入的view
|
||||
*/
|
||||
['default']?: () => any
|
||||
|
||||
/**
|
||||
* 可以将自定义导航栏、tab-view等需要固定的(不需要跟着滚动的)元素放入slot="top"的view中。
|
||||
* 注意,当有多个需要固定的view时,请用一个view包住它们,并且在这个view上设置slot="top"。需要固定在顶部的view请勿设置position: fixed;
|
||||
* @since 1.5.5
|
||||
*/
|
||||
['top']?: () => any
|
||||
|
||||
/**
|
||||
* 可以将需要固定在底部的(不需要跟着滚动的)元素放入slot="bottom"的view中。
|
||||
* 注意,当有多个需要固定的view时,请用一个view包住它们,并且在这个view上设置slot="bottom"。需要固定在底部的view请勿设置position: fixed;
|
||||
* @since 1.6.2
|
||||
*/
|
||||
['bottom']?: () => any
|
||||
|
||||
/**
|
||||
* 可以将需要固定在左侧的(不需要跟着滚动的)元素放入slot="left"的view中。
|
||||
* 注意,当有多个需要固定的view时,请用一个view包住它们,并且在这个view上设置slot="left"。需要固定在左侧的view请勿设置position: fixed;
|
||||
* @since 2.2.3
|
||||
*/
|
||||
['left']?: () => any
|
||||
|
||||
/**
|
||||
* 可以将需要固定在左侧的(不需要跟着滚动的)元素放入slot="right"的view中。
|
||||
* 注意,当有多个需要固定的view时,请用一个view包住它们,并且在这个view上设置slot="right"。需要固定在右侧的view请勿设置position: fixed;
|
||||
* @since 2.2.3
|
||||
*/
|
||||
['right']?: () => any
|
||||
}
|
||||
|
||||
// ****************************** Methods ******************************
|
||||
declare interface _ZPagingSwiperRef {
|
||||
/**
|
||||
* 更新slot="left"和slot="right"宽度,当slot="left"或slot="right"宽度动态改变后调用
|
||||
*
|
||||
* @since 2.3.5
|
||||
*/
|
||||
updateLeftAndRightWidth: () => void;
|
||||
|
||||
/**
|
||||
* 更新fixed模式下z-paging-swiper的布局,在onShow时候调用,以修复在iOS+h5+tabbar+fixed+底部有安全区域的设备中从tabbar页面跳转到无tabbar页面后返回,底部有一段空白区域的问题
|
||||
*
|
||||
* @since 2.6.5
|
||||
*/
|
||||
updateFixedLayout: () => void;
|
||||
}
|
||||
|
||||
declare interface _ZPagingSwiper {
|
||||
new (): {
|
||||
$props: AllowedComponentProps &
|
||||
VNodeProps &
|
||||
ZPagingSwiperProps
|
||||
$slots: ZPagingSwiperSlots
|
||||
}
|
||||
}
|
||||
|
||||
export declare const ZPagingSwiper: _ZPagingSwiper
|
||||
|
||||
export declare const ZPagingSwiperRef: _ZPagingSwiperRef
|
||||
2083
uni_modules/z-paging/types/comps/z-paging.d.ts
vendored
Normal file
2083
uni_modules/z-paging/types/comps/z-paging.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
24
uni_modules/z-paging/types/index.d.ts
vendored
Normal file
24
uni_modules/z-paging/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/// <reference path="./comps.d.ts" />
|
||||
declare module 'z-paging' {
|
||||
export function install() : void
|
||||
/**
|
||||
* z-paging全局配置
|
||||
* - uni.$zp
|
||||
*
|
||||
* @since 2.6.5
|
||||
*/
|
||||
interface $zp {
|
||||
/**
|
||||
* 全局配置
|
||||
*/
|
||||
config : Record<string, any>;
|
||||
}
|
||||
global {
|
||||
interface Uni {
|
||||
$zp : $zp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare type ZPagingSwiperRef = typeof import('./comps/z-paging-swiper')['ZPagingSwiperRef']
|
||||
declare type ZPagingSwiperItemRef = typeof import('./comps/z-paging-swiper-item')['ZPagingSwiperItemRef']
|
||||
21
uview-plus/LICENSE
Normal file
21
uview-plus/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 https://uiadmin.net/uview-plus
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
74
uview-plus/README.md
Normal file
74
uview-plus/README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
<p align="center">
|
||||
<img alt="logo" src="https://uiadmin.net/uview-plus/common/logo.png" width="120" height="120" style="margin-bottom: 10px;">
|
||||
</p>
|
||||
<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uview-plus 3.0</h3>
|
||||
<h3 align="center">多平台快速开发的UI框架</h3>
|
||||
|
||||
[](https://github.com/ijry/uview-plus)
|
||||
[](https://github.com/ijry/uview-plus)
|
||||
[](https://github.com/ijry/uview-plus/issues)
|
||||
[](https://gitee.com/jry/uview-plus/releases)
|
||||
[](https://en.wikipedia.org/wiki/MIT_License)
|
||||
|
||||
## 说明
|
||||
|
||||
uview-plus,是uni-app全面兼容vue3/nvue/鸿蒙/uni-app-x的uni-app生态框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水。uview-plus是基于uView2.x移植的支持vue3的版本,感谢uView。
|
||||
|
||||
## 可视化设计
|
||||
|
||||
uview-plus现已推出免费可视化设计,可以方便的进行页面可视化设计,导出源码即可使用。极大提高前端页面开发效率;如产品经理设计师直接使用更可作为高保真高可用原型制作工具,让设计稿即代码,无需传统的设计稿开发还原步骤。
|
||||
|
||||
<img src="https://s3.bmp.ovh/imgs/2024/11/24/fd58d00071e6e5df.png" width="900" height="auto" >
|
||||
<img src="https://s3.bmp.ovh/imgs/2024/11/24/8e85a519fe627fb1.png" width="900" height="auto" >
|
||||
|
||||
|
||||
## 文档
|
||||
[官方文档:https://uview-plus.jiangruyi.com](https://uview-plus.jiangruyi.com)
|
||||
[备用文档:https://uiadmin.net/uview-plus](https://uiadmin.net/uview-plus)
|
||||
|
||||
|
||||
## 预览
|
||||
|
||||
您可以通过**微信**扫码,查看最佳的演示效果。
|
||||
<br>
|
||||
<br>
|
||||
<img src="https://uview-plus.jiangruyi.com/common/h5_qrcode.png" width="220" height="220" >
|
||||
|
||||
## 链接
|
||||
|
||||
- [官方文档](https://uview-plus.jiangruyi.com)
|
||||
- [更新日志](https://uview-plus.jiangruyi.com/components/changelog.html)
|
||||
- [升级指南](https://uview-plus.jiangruyi.com/components/changeGuide.html)
|
||||
- [关于我们](https://uview-plus.jiangruyi.com/cooperation/about.html)
|
||||
|
||||
## 交流反馈
|
||||
|
||||
欢迎加入我们的QQ群交流反馈:[点此跳转](https://uview-plus.jiangruyi.com/components/addQQGroup.html)
|
||||
|
||||
## 关于PR
|
||||
|
||||
> 我们非常乐意接受各位的优质PR,但在此之前我希望您了解uview-plus是一个需要兼容多个平台的(小程序、h5、ios app、android app)包括nvue页面、vue页面。
|
||||
> 所以希望在您修复bug并提交之前尽可能的去这些平台测试一下兼容性。最好能携带测试截图以方便审核。非常感谢!
|
||||
|
||||
## 安装
|
||||
|
||||
#### **uni-app插件市场链接** —— [https://ext.dcloud.net.cn/plugin?name=uview-plus](https://ext.dcloud.net.cn/plugin?name=uview-plus)
|
||||
|
||||
请通过[官网安装文档](https://uview-plus.jiangruyi.com/components/install.html)了解更详细的内容
|
||||
|
||||
## 快速上手
|
||||
|
||||
请通过[快速上手](https://uview-plus.jiangruyi.com/components/quickstart.html)了解更详细的内容
|
||||
|
||||
## 使用方法
|
||||
配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。
|
||||
|
||||
```html
|
||||
<template>
|
||||
<u-button text="按钮"></u-button>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 版权信息
|
||||
uview-plus遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uview-plus应用到您的产品中。
|
||||
|
||||
610
uview-plus/changelog.md
Normal file
610
uview-plus/changelog.md
Normal file
@@ -0,0 +1,610 @@
|
||||
## 3.3.69(2025-02-19)
|
||||
picker允许传递禁用颜色props
|
||||
|
||||
slider组件isRange状态下增加min max插槽分开显示内容
|
||||
|
||||
feat: 新增经典下拉框组件up-select
|
||||
|
||||
## 3.3.68(2025-02-12)
|
||||
fix: 修复weekText类型
|
||||
|
||||
feat: 日历增加单选与多选指定禁止选中的日期功能
|
||||
|
||||
fix: NumberBox删除数字时取值有误 #613
|
||||
|
||||
## 3.3.67(2025-02-11)
|
||||
feat: navbar支持返回全局拦截器配置
|
||||
|
||||
feat: 表单-校验-支持无提示-得到校验结果
|
||||
|
||||
feat: picker传递hasInput属性时候,可以禁用输入框点击
|
||||
|
||||
## 3.3.66(2025-02-09)
|
||||
feat: steps-item增加content插槽
|
||||
|
||||
## 3.3.65(2025-02-05)
|
||||
feat: number-box组件新增按钮圆角/按钮宽度/数据框背景色/迷你模式
|
||||
## 3.3.64(2025-01-18)
|
||||
feat: 日历组件支持自定义星期文案
|
||||
|
||||
## 3.3.63(2025-01-13)
|
||||
fix: cate-tab支持支付宝小程序
|
||||
|
||||
fix: textarea 修复 placeholder-style
|
||||
|
||||
fix: 修复在图片加载及加载失败时容器宽度
|
||||
|
||||
fix: waterfall组件报错Maximum recursive updates
|
||||
|
||||
## 3.3.62(2025-01-10)
|
||||
feat: sleder滑动选择器双滑块增加外层触发值的变动功能
|
||||
|
||||
fix: picker支持hasInput优化
|
||||
|
||||
## 3.3.61(2024-12-31)
|
||||
fix: 修复微信getSystemInfoSync接口废弃警告
|
||||
|
||||
fix: 'u-status-bar' symbol missing
|
||||
|
||||
## 3.3.60(2024-12-30)
|
||||
feat: 日期组件支持禁用
|
||||
|
||||
fix: ts定义修复 #600
|
||||
|
||||
feat: Tabs组件选中时增加一个active的class #595
|
||||
|
||||
## 3.3.59(2024-12-30)
|
||||
fix: Property "isH5" was accessed during render
|
||||
|
||||
## 3.3.58(2024-12-26)
|
||||
fix: slider组件change事件传参
|
||||
|
||||
## 3.3.57(2024-12-23)
|
||||
fix: slider组件change事件传参
|
||||
|
||||
feat: 更新u-picker组件增加当前选中class类名
|
||||
|
||||
## 3.3.56(2024-12-18)
|
||||
feat: 在u-alert组件中添加关闭事件
|
||||
|
||||
## 3.3.55(2024-12-17)
|
||||
add: swiper增加双向绑定
|
||||
|
||||
## 3.3.54(2024-12-11)
|
||||
add: qrcode支持props控制是否开启点击预览
|
||||
|
||||
add: 新增cate-tab垂直分类组件
|
||||
|
||||
## 3.3.53(2024-12-10)
|
||||
fix: 修复popup居中模式点击内容区域触发关闭
|
||||
|
||||
## 3.3.52(2024-12-09)
|
||||
add: notice-bar支持justifyContent属性
|
||||
|
||||
## 3.3.51(2024-12-09)
|
||||
add: radio增加label插槽
|
||||
|
||||
## 3.3.50(2024-12-05)
|
||||
fix: 优化popup等对禁止背景滚动机制
|
||||
|
||||
add: slider在弹窗使用示例
|
||||
|
||||
fix: card组件类名问题
|
||||
|
||||
## 3.3.49(2024-12-02)
|
||||
fix: 去除album多余的$u引用
|
||||
|
||||
fix: 优化图片组件兼容性
|
||||
|
||||
add: picker组件增加zIndex属性
|
||||
|
||||
add: text增加是否占满剩余空间属性
|
||||
|
||||
add: input颜色示例
|
||||
|
||||
## 3.3.48(2024-11-29)
|
||||
add: 文本行数限制样式提高到10行
|
||||
|
||||
del: 去除不跨端的inputmode
|
||||
## 3.3.47(2024-11-28)
|
||||
fix: 时间选择器在hasInput模式下部分机型键盘弹出
|
||||
|
||||
## 3.3.46(2024-11-26)
|
||||
fix: 修复text传递事件参数
|
||||
|
||||
## 3.3.45(2024-11-24)
|
||||
add: navbar组件支持配置标题颜色
|
||||
|
||||
fix: 边框按钮警告类型下颜色变量使用错误
|
||||
|
||||
## 3.3.43(2024-11-18)
|
||||
fix: 支持瀑布流组件v-model置为[]
|
||||
|
||||
add: 新增字符串路径访问工具方法getValueByPath
|
||||
|
||||
add: 新增float-button悬浮按钮组件
|
||||
|
||||
## 3.3.42(2024-11-15)
|
||||
add: button组件支持stop参数阻止冒泡
|
||||
|
||||
## 3.3.41(2024-11-13)
|
||||
fix: u-radio-group invalid import
|
||||
|
||||
improvement: 优化图片组件宽高及修复事件event传递
|
||||
|
||||
## 3.3.40(2024-11-11)
|
||||
add: 组件radioGroup增加gap属性用于设置item间隔
|
||||
|
||||
fix: 修复H5全局导入
|
||||
|
||||
## 3.3.39(2024-11-04)
|
||||
fix: 修复相册组件
|
||||
|
||||
## 3.3.38(2024-11-04)
|
||||
fix: 修复视频预览报错 #510
|
||||
|
||||
add: album组件增加stop参数支持阻止事件冒泡
|
||||
|
||||
## 3.3.37(2024-10-21)
|
||||
fix: 修复因为修改组件名称前缀,导致h5打包后$parent方法内找不到父组件的问题
|
||||
|
||||
fix: 修复datetime-picker选择2000年以前日期出错
|
||||
|
||||
## 3.3.36(2024-10-09)
|
||||
fix: toast 自动关闭
|
||||
|
||||
feat: 增加微信小程序用户昵称审核完毕回调及修改 ts 定义文件
|
||||
|
||||
## 3.3.35(2024-10-08)
|
||||
feat: modal和picker支持v-model:show双向绑定
|
||||
|
||||
feat: 支持checkbox使用slot自定义label后自带点击事件 #522
|
||||
|
||||
feat: swipe-action支持自动关闭特性及初始化打开状态
|
||||
|
||||
## 3.3.34(2024-09-23)
|
||||
feat: 支持toast设置duration值为-1时不自动关闭
|
||||
|
||||
## 3.3.33(2024-09-18)
|
||||
fix: 修复test.date('008')等验证结果不准确
|
||||
|
||||
## 3.3.32(2024-09-09)
|
||||
fix: u-keyboard名称冲突warning
|
||||
|
||||
## 3.3.31(2024-08-31)
|
||||
feat: qrcode初步支持nvue
|
||||
|
||||
## 3.3.30(2024-08-30)
|
||||
fix: slider兼容step为字符串类型
|
||||
|
||||
## 3.3.29(2024-08-30)
|
||||
fix: 修复tabs组件current参数为字符串处理逻辑
|
||||
|
||||
## 3.3.28(2024-08-26)
|
||||
fix: list组件滑动偏移量不一样取绝对值导致iOS下拉偏移量计算错误
|
||||
|
||||
## 3.3.27(2024-08-22)
|
||||
fix: 修复up-datetime-picker组件toolbarRightSlot定义缺失
|
||||
|
||||
fix: 修复FormItem的rules更新错误的问题
|
||||
|
||||
## 3.3.26(2024-08-22)
|
||||
fix: 批量注册全局组件优化
|
||||
|
||||
## 3.3.25(2024-08-21)
|
||||
fix: 修复slider在app-vue下样式问题
|
||||
|
||||
## 3.3.24(2024-08-19)
|
||||
fix: 修复时间选择器hasInput模式小程序不生效
|
||||
|
||||
feat: 支持H5导入所有组件
|
||||
|
||||
## 3.3.23(2024-08-17)
|
||||
feat: swipe-action增加closeAll方法
|
||||
|
||||
fix: 兼容tabs在某些场景下index小于0时自动设置为0
|
||||
|
||||
add: 通用mixin新增navTo页面跳转方法
|
||||
|
||||
## 3.3.21(2024-08-15)
|
||||
improvement: 优化二维码组件loading及支持预览与长按事件 #351
|
||||
|
||||
fix: 修复swipe-action自动关闭其它功能及组件卸载自动关闭
|
||||
|
||||
## 3.3.20(2024-08-15)
|
||||
refactor: props默认值文件移至组件文件夹内便于查找
|
||||
## 3.3.19(2024-08-14)
|
||||
fix: 修复2被rpx兼容处理只在数字值生效
|
||||
|
||||
add: 增加swiper自定义插槽示例
|
||||
|
||||
## 3.3.18(2024-08-13)
|
||||
feat: 新增支持datetime-picker工具栏插槽及picker插槽支持修复
|
||||
## 3.3.17(2024-08-12)
|
||||
feat: swiper组件增加默认slot便于自定义
|
||||
|
||||
feat: grid新增间隔参数
|
||||
|
||||
feat: picker新增toolbar-right和toolbar-bottom插槽
|
||||
|
||||
## 3.3.16(2024-08-12)
|
||||
fix: 解决swiper中title换行后多余的内容未被遮挡问题
|
||||
|
||||
fix: 修复迷你导航适配异形屏
|
||||
|
||||
## 3.3.15(2024-08-09)
|
||||
fix: 修复默认单位设置为rpx时一些组件高度间距异常
|
||||
|
||||
fix: 修复日历在rpx单位下布局异常
|
||||
|
||||
feat: code-input支持App端展示输入光标
|
||||
|
||||
## 3.3.14(2024-08-09)
|
||||
add: 增加box组件
|
||||
|
||||
add: 增加card卡片组件
|
||||
|
||||
|
||||
## 3.3.13(2024-08-08)
|
||||
feat: input支持调用原生组件的focus和blur方法
|
||||
|
||||
improvement: grid-item条件编译优化
|
||||
|
||||
add: 新增迷你导航组件
|
||||
|
||||
## 3.3.12(2024-08-06)
|
||||
improvement: $u挂载时机调整便于打包分离chunk
|
||||
|
||||
fix: steps新增itemStyle属性名称冲突
|
||||
|
||||
## 3.3.11(2024-08-05)
|
||||
feat: 新增支持upload组件的deletable/maxCount/accept变更监听 #333
|
||||
|
||||
feat: 新增支持tabs在swiper中使用
|
||||
|
||||
feat: 新增FormItem支持独立设置验证规则rules
|
||||
|
||||
fix: 修复index-list未设置$slots.header时索引高亮失效
|
||||
|
||||
## 3.3.10(2024-08-02)
|
||||
fix: 修复index-list偶发的滑动最后一个索引报错top不存在
|
||||
|
||||
fix: 修复gird在QQ、抖音小程序下布局
|
||||
|
||||
feat: 优化step支持自定义样式prop
|
||||
|
||||
feat: action-sheet组件支持v-model:show双向绑定
|
||||
|
||||
fix: 小程序下steps和grid都统一采用grid布局
|
||||
|
||||
fix: 修复支付宝小程序下input类型为数字时双向绑定失效
|
||||
|
||||
feat : form 表单 validate 校验不通过后 error增加字段prop信息 #304
|
||||
|
||||
fix: form组件异步校异常验问题 #393
|
||||
|
||||
## 3.3.9(2024-08-01)
|
||||
fix: 优化获取nvue元素
|
||||
|
||||
feat: modal新增contentTextAlign设置文案对齐方式
|
||||
|
||||
fix: 修复NVUE下tabbar文字不显示 #458
|
||||
|
||||
feat: loading-page增加zIndex属性
|
||||
|
||||
fix: 相册在宽度较小时换行问题
|
||||
|
||||
feat: album相册增加自适应自动换行模式
|
||||
|
||||
feat: album相册增加图片尺寸单位prop
|
||||
|
||||
fix: 修复calendar日历月份居中
|
||||
|
||||
## 3.3.8(2024-07-31)
|
||||
feat: slider支持进度条任意位置触发按钮拖动
|
||||
|
||||
fix: 修复app-vue下modal标题不居中
|
||||
|
||||
fix: #459 TS setConfig 声明异常
|
||||
|
||||
feat: tabs组件增加longPress长按事件
|
||||
|
||||
feat: 新增showRight属性控制collapse右侧图标显隐
|
||||
|
||||
fix: 优化nvue下css警告
|
||||
|
||||
## 3.3.7(2024-07-29)
|
||||
feat: 支持IndexList组件支持在弹窗等场景下使用及联动优化
|
||||
|
||||
feat: popup组件支持v-model:show双向绑定
|
||||
|
||||
feat: 优化tabs的current双向绑定
|
||||
|
||||
fix: checkbox独立使用时checked赋初始值可以,但是手动切换时值没有做双向绑定! #455
|
||||
|
||||
feat: slider组件支持区间双滑块
|
||||
|
||||
fix: toast 支持自定义图标?可传入了决对路径的 icon也没有用 #409
|
||||
|
||||
feat: form-item校验失败时 增加class方便自定义显示错误的展示方式 #394
|
||||
|
||||
fix: up-cell的required配置不生效 #395
|
||||
|
||||
fix: 横向滚动组件,微信小程序编译后会有警告 #415
|
||||
|
||||
fix: u-picker内部对默认值defaultIndex的监听 #425
|
||||
|
||||
feat: toast 组件支持遮掩层穿透 #417
|
||||
|
||||
fix: 兼容vue的slot编译bug #423
|
||||
|
||||
fix: upload 微信小程序 点击预览视频报错 #424
|
||||
|
||||
fix: u-number-box 组件修改【integer, decimalLength, min, max 】props时没有触发绑定值更新 #429
|
||||
|
||||
feat: Tabs组件能否支持自定义插槽 #439
|
||||
|
||||
feat: ActionSheet 可以配置最大高度吗, 我当做select使用了。 #445
|
||||
|
||||
fix: cursor-pointer优化
|
||||
|
||||
feat: 新版slider组件兼容NVUE改造
|
||||
|
||||
feat: 新增slider组件手动实现以支持样式自定义
|
||||
|
||||
perf:补充TS声明提示信息
|
||||
|
||||
修复:ActionSheet 操作菜单cancelText属性为空DOM节点还存在并且可以点击问题
|
||||
|
||||
fix: 去除预留的beforeDestroy兼容容易在某些sdk下不识别条件编译
|
||||
|
||||
## 3.3.6(2024-07-23)
|
||||
feat: u-album组件添加radius,shape参数,定义参考当前u-image参数
|
||||
|
||||
fix: 修复了calendar组件title和日期title未垂直居中的问题
|
||||
|
||||
fix: update:modelValue缺失emit定义
|
||||
|
||||
## 3.3.5(2024-07-10)
|
||||
picker组件支持hasInput模式
|
||||
|
||||
## 3.3.4(2024-07-07)
|
||||
fix: input组件双向绑定问题 #419
|
||||
|
||||
lazy-load完善emit
|
||||
|
||||
优化通用小程序分享
|
||||
|
||||
## 3.3.2(2024-06-27)
|
||||
fix: 在Nvue环境中编译,出现大量警告 #406
|
||||
## 3.3.1(2024-06-27)
|
||||
u-button组件报错,找不到button mixins #407
|
||||
## 3.3.0(2024-06-27)
|
||||
feat: checkbox支持label设置slot
|
||||
|
||||
feat: modal增加customClass
|
||||
|
||||
feat: navbar、popup、tabs、text支持customClass
|
||||
|
||||
fix: cell组建缺少flex布局
|
||||
|
||||
fix: 修复微信小程序真机调试时快速输入出现文本回退问题
|
||||
|
||||
feat: tag增加默认slot
|
||||
|
||||
公共mixin改造为按需导入语法
|
||||
|
||||
refactor: 组件props混入mixin改造为按需导入语法
|
||||
|
||||
fix: u-tabbar 安卓手机点击按钮变蓝问题 #396
|
||||
|
||||
feat: upload组建增加extension属性
|
||||
|
||||
fix: upload组件参数mode添加left
|
||||
|
||||
fix: 修复阴影在非nvue时白色背景色不显示
|
||||
|
||||
## 3.2.24(2024-06-11)
|
||||
fix: 修复时间选择器confirm事件触发时机导致2次才会触发v-model更新
|
||||
## 3.2.23(2024-05-30)
|
||||
fix: #378 H5 u-input 在表单中初始值为空也会触发一次 formValidate(this,"change")事件导致进入页面直接校验了一次
|
||||
|
||||
fix: #373 搜索组件up-search的@clear事件无效
|
||||
|
||||
fix: #372 ActionSheet 组件的取消按钮触发区域太小
|
||||
|
||||
## 3.2.22(2024-05-13)
|
||||
上传组件支持微信小程序预览视频
|
||||
|
||||
修复折叠面板右侧箭头不显示
|
||||
|
||||
修复uxp2px
|
||||
|
||||
## 3.2.21(2024-05-10)
|
||||
fix: loading-icon修复flex布局
|
||||
## 3.2.20(2024-05-10)
|
||||
修复瀑布流大小写#355
|
||||
## 3.2.19(2024-05-10)
|
||||
去除意外的文件引入
|
||||
## 3.2.18(2024-05-09)
|
||||
fix: 349 popup 组件设置 zIndex 属性后,组件渲染异常#
|
||||
feat: 搜索框增加adjustPosition属性
|
||||
fix: #331增加u-action-sheet__cancel
|
||||
优化mixin兼容性
|
||||
feat: #326 up-list增加下拉刷新功能
|
||||
fix: #319 优化up-tabs参数与定义匹配
|
||||
fix: index-list组件微信小程序端使用自定义导航栏异常
|
||||
fix: #285 pickerimmediateChange 写死为true
|
||||
fix: #111 u-scroll-list组件,隐藏指示器后报错, 提示找不到ref
|
||||
list增加微信小程序防抖配置
|
||||
|
||||
## 3.2.17(2024-05-08)
|
||||
fix: 支付宝小程序二维码渲染
|
||||
## 3.2.16(2024-05-06)
|
||||
修复tabs中,当前激活样式的undefined bug
|
||||
|
||||
fix: #341u-code 倒计时没结束前退出,再次进入结束后退出界面,再次进入重新开始倒计时bug
|
||||
|
||||
受到uni-app内置text样式影响修复
|
||||
|
||||
## 3.2.15(2024-04-28)
|
||||
优化时间选择器hasInput模式初始化值
|
||||
## 3.2.14(2024-04-24)
|
||||
去除pleaseSetTranspileDependencies
|
||||
|
||||
http采用useStore
|
||||
|
||||
## 3.2.13(2024-04-22)
|
||||
修复modal标题样式
|
||||
|
||||
优化日期选择器hasInput模式宽度
|
||||
|
||||
## 3.2.12(2024-04-22)
|
||||
修复color应用
|
||||
## 3.2.11(2024-04-18)
|
||||
修复import化带来的问题
|
||||
## 3.2.10(2024-04-17)
|
||||
完善input清空事件App端失效的兼容性
|
||||
|
||||
修复日历组件二次打开后当前月份显示不正确
|
||||
|
||||
## 3.2.9(2024-04-16)
|
||||
组件内uni.$u用法改为import引入
|
||||
|
||||
规范化及兼容性增强
|
||||
|
||||
## 3.2.8(2024-04-15)
|
||||
修复up-tag语法错
|
||||
## 3.2.7(2024-04-15)
|
||||
修复下拉菜单背景色在支付宝小程序无效
|
||||
|
||||
setConfig改为浅拷贝解决无法用import导入代替uni.$u.props设置
|
||||
|
||||
## 3.2.6(2024-04-14)
|
||||
修复某些情况下滑动单元格默认右侧按钮是展开的问题
|
||||
## 3.2.5(2024-04-13)
|
||||
调整分段器尺寸及修复窗口大小改变时重新计算尺寸
|
||||
|
||||
多个组件支持cursor-pointer增强PC端体验
|
||||
|
||||
## 3.2.4(2024-04-12)
|
||||
初步支持typescript
|
||||
## 3.2.3(2024-04-12)
|
||||
fix: 修复square属性在小程序下无效问题
|
||||
|
||||
fix:修复lastIndex异常导致的column异常问题
|
||||
|
||||
fix: alipayapp picker style
|
||||
|
||||
feat(button): 添加用户同意隐私协议事件回调
|
||||
|
||||
fix: input switch password
|
||||
|
||||
fix: 修复u-code组件keepRuning失效问题
|
||||
|
||||
feat: form-item添加labelPosition属性
|
||||
|
||||
新增dropdown组件
|
||||
|
||||
分段器支持内部current值
|
||||
|
||||
优化cell和action-sheet视觉大小
|
||||
|
||||
修复tabs文字换行
|
||||
|
||||
## 3.2.2(2024-04-11)
|
||||
修复换行符问题
|
||||
## 3.2.1(2024-04-11)
|
||||
修复演示H5二维码
|
||||
|
||||
fix: #270 ReadMore 展开阅读更多内容变化兼容
|
||||
|
||||
fix: #238Calendar组件maxDate修改为不能小于minDate
|
||||
|
||||
checkbox支持独立使用
|
||||
|
||||
修复popup中在微信小程序中真机调试滚动失效
|
||||
|
||||
## 3.2.0(2024-04-10)
|
||||
修复轮播图在nvue显示
|
||||
修复疑似u-slider名称被占用导致slider在App下不显示
|
||||
解决微信小程序提示 Some selectors are not allowed in component wxss
|
||||
示例中u-前缀统一为up-
|
||||
增加瀑布流与图片懒加载组件
|
||||
fix: #308修复tag组件缺失iconColor参数
|
||||
fix: #297使用grid布局解决目前编译为抖音小程序无法开启virtualHost
|
||||
## 3.1.52(2024-04-07)
|
||||
工具类方法调用import化改造
|
||||
新增up-copy复制组件
|
||||
## 3.1.51(2024-04-07)
|
||||
优化时间选择器自带输入框格式化显示
|
||||
防止按钮文字换行
|
||||
修复订单列表模板滑动
|
||||
增加u-qrcode二维码组件
|
||||
## 3.1.49(2024-03-27)
|
||||
日期时间组件支持自带输入框
|
||||
fix: popup弹窗滚动穿透问题
|
||||
fix: 修复小程序numberbox bug
|
||||
## 3.1.48(2024-03-18)
|
||||
fix:[plugin:uni:pre-css] Unbalanced delimiter found in string
|
||||
## 3.1.47(2024-03-18)
|
||||
fix: setConfig设置组件默认参数无效问题
|
||||
fix: 修复自定义图标无效问题
|
||||
feat: 增加u-form-item单独设置规则变量
|
||||
fix:#293小程序是自定义导航栏的时候即传了customNavHeight的时候会出现跳转偏移的情况
|
||||
|
||||
## 3.1.46(2024-01-29)
|
||||
beforeUnmount
|
||||
## 3.1.45(2024-01-24)
|
||||
fix: #262ext组件为超链接的情况下size属性不生效
|
||||
fix: #263最新版本3.1.42中微信小程序u-swipe-action-item报错
|
||||
fix: #224最新版本3.1.42中微信小程序u-swipe-action-item报错
|
||||
fix: #263支持支付宝小程序
|
||||
fix: #261u-input在直接修改v-model的绑定值时,每隔一次会无法出发change事件
|
||||
优化折叠面板兼容微信小程序
|
||||
## 3.1.42(2024-01-15)
|
||||
修复u-number-box默认值0时在小程序不显示值
|
||||
优化u-code的timer判断
|
||||
优化支付宝小程序下textarea字数统计兼容
|
||||
优化u-calendar
|
||||
## 3.1.41(2023-11-18)
|
||||
#215优化u-cell图标容器间距问题
|
||||
## 3.1.40(2023-11-16)
|
||||
修复u-slider双向绑定
|
||||
## 3.1.39(2023-11-10)
|
||||
修复头条小程序不支持env(safe-area-inset-bottom)
|
||||
优化#201u-grid 指定列数导致闪烁
|
||||
#193IndexList 索引列表 高度错误
|
||||
其他优化
|
||||
## 3.1.38(2023-10-08)
|
||||
修复u-slider
|
||||
## 3.1.37(2023-09-13)
|
||||
完善emits定义及修复code-input双向数据绑定
|
||||
## 3.1.36(2023-08-08)
|
||||
修复富文本事件名称大小写
|
||||
## 3.1.35(2023-08-02)
|
||||
修复编译到支付宝小程序u-form报错
|
||||
## 3.1.34(2023-07-27)
|
||||
修复App打包uni.$u.mpMixin方式sdk暂时不支持导致报错
|
||||
## 3.1.33(2023-07-13)
|
||||
修复弹窗进入动画、模板页面样式等
|
||||
## 3.1.31(2023-07-11)
|
||||
修复dayjs引用
|
||||
## 3.0.8(2022-07-12)
|
||||
修复u-tag默认宽度撑满容器
|
||||
## 3.0.7(2022-07-12)
|
||||
修复u-navbar自定义插槽演示示例
|
||||
## 3.0.6(2022-07-11)
|
||||
修复u-image缺少emits申明
|
||||
## 3.0.5(2022-07-11)
|
||||
修复u-upload缺少emits申明
|
||||
## 3.0.4(2022-07-10)
|
||||
修复u-textarea/u-input/u-datetime-picker/u-number-box/u-radio-group/u-switch/u-rate在vue3下数据绑定
|
||||
## 3.0.3(2022-07-09)
|
||||
启用自建演示二维码
|
||||
## 3.0.2(2022-07-09)
|
||||
修复dayjs/clipboard等导致打包报错
|
||||
## 3.0.1(2022-07-09)
|
||||
增加插件市场地址
|
||||
## 3.0.0(2022-07-09)
|
||||
# uview-plus(vue3)初步发布
|
||||
85
uview-plus/components/u--form/u--form.vue
Normal file
85
uview-plus/components/u--form/u--form.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<uvForm
|
||||
ref="uForm"
|
||||
:model="model"
|
||||
:rules="rules"
|
||||
:errorType="errorType"
|
||||
:borderBottom="borderBottom"
|
||||
:labelPosition="labelPosition"
|
||||
:labelWidth="labelWidth"
|
||||
:labelAlign="labelAlign"
|
||||
:labelStyle="labelStyle"
|
||||
:customStyle="customStyle"
|
||||
>
|
||||
<slot />
|
||||
</uvForm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 此组件存在的理由是,在nvue下,u-form被uni-app官方占用了,u-form在nvue中相当于form组件
|
||||
* 所以在nvue下,取名为u--form,内部其实还是u-form.vue,只不过做一层中转
|
||||
*/
|
||||
import uvForm from '../u-form/u-form.vue';
|
||||
import { props } from '../u-form/props.js';
|
||||
import { mpMixin } from '../../libs/mixin/mpMixin';
|
||||
import { mixin } from '../../libs/mixin/mixin';
|
||||
export default {
|
||||
// #ifdef MP-WEIXIN
|
||||
name: 'u-form',
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
name: 'u--form',
|
||||
// #endif
|
||||
mixins: [mpMixin, props, mixin],
|
||||
components: {
|
||||
uvForm
|
||||
},
|
||||
created() {
|
||||
this.children = []
|
||||
},
|
||||
methods: {
|
||||
// 手动设置校验的规则,如果规则中有函数的话,微信小程序中会过滤掉,所以只能手动调用设置规则
|
||||
setRules(rules) {
|
||||
this.$refs.uForm.setRules(rules)
|
||||
},
|
||||
/**
|
||||
* 校验全部数据
|
||||
* @param {Object} options
|
||||
* @param {Boolean} options.showErrorMsg -是否显示校验信息,
|
||||
*/
|
||||
validate(options) {
|
||||
/**
|
||||
* 在微信小程序中,通过this.$parent拿到的父组件是u--form,而不是其内嵌的u-form
|
||||
* 导致在u-form组件中,拿不到对应的children数组,从而校验无效,所以这里每次调用u-form组件中的
|
||||
* 对应方法的时候,在小程序中都先将u--form的children赋值给u-form中的children
|
||||
*/
|
||||
// #ifdef MP-WEIXIN
|
||||
this.setMpData()
|
||||
// #endif
|
||||
return this.$refs.uForm.validate(options)
|
||||
},
|
||||
validateField(value, callback) {
|
||||
// #ifdef MP-WEIXIN
|
||||
this.setMpData()
|
||||
// #endif
|
||||
return this.$refs.uForm.validateField(value, callback)
|
||||
},
|
||||
resetFields() {
|
||||
// #ifdef MP-WEIXIN
|
||||
this.setMpData()
|
||||
// #endif
|
||||
return this.$refs.uForm.resetFields()
|
||||
},
|
||||
clearValidate(props) {
|
||||
// #ifdef MP-WEIXIN
|
||||
this.setMpData()
|
||||
// #endif
|
||||
return this.$refs.uForm.clearValidate(props)
|
||||
},
|
||||
setMpData() {
|
||||
this.$refs.uForm.children = this.children
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
50
uview-plus/components/u--image/u--image.vue
Normal file
50
uview-plus/components/u--image/u--image.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<uvImage
|
||||
:src="src"
|
||||
:mode="mode"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:shape="shape"
|
||||
:radius="radius"
|
||||
:lazyLoad="lazyLoad"
|
||||
:showMenuByLongpress="showMenuByLongpress"
|
||||
:loadingIcon="loadingIcon"
|
||||
:errorIcon="errorIcon"
|
||||
:showLoading="showLoading"
|
||||
:showError="showError"
|
||||
:fade="fade"
|
||||
:webp="webp"
|
||||
:duration="duration"
|
||||
:bgColor="bgColor"
|
||||
:customStyle="customStyle"
|
||||
@click="$emit('click')"
|
||||
@error="$emit('error')"
|
||||
@load="$emit('load')"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<slot name="loading"></slot>
|
||||
</template>
|
||||
<template v-slot:error>
|
||||
<slot name="error"></slot>
|
||||
</template>
|
||||
</uvImage>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 此组件存在的理由是,在nvue下,u-image被uni-app官方占用了,u-image在nvue中相当于image组件
|
||||
* 所以在nvue下,取名为u--image,内部其实还是u-iamge.vue,只不过做一层中转
|
||||
*/
|
||||
import uvImage from '../u-image/u-image.vue';
|
||||
import { props } from '../u-image/props.js';
|
||||
import { mpMixin } from '../../libs/mixin/mpMixin';
|
||||
import { mixin } from '../../libs/mixin/mixin';
|
||||
export default {
|
||||
name: 'u--image',
|
||||
mixins: [mpMixin, props, mixin],
|
||||
components: {
|
||||
uvImage
|
||||
},
|
||||
emits: ['click', 'error', 'load']
|
||||
}
|
||||
</script>
|
||||
74
uview-plus/components/u--input/u--input.vue
Normal file
74
uview-plus/components/u--input/u--input.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<uvInput
|
||||
<!-- #ifdef VUE2 -->
|
||||
:value="value"
|
||||
@input="e => $emit('input', e)"
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef VUE3 -->
|
||||
:modelValue="modelValue"
|
||||
@update:modelValue="e => $emit('update:modelValue', e)"
|
||||
<!-- #endif -->
|
||||
:type="type"
|
||||
:fixed="fixed"
|
||||
:disabled="disabled"
|
||||
:disabledColor="disabledColor"
|
||||
:clearable="clearable"
|
||||
:password="password"
|
||||
:maxlength="maxlength"
|
||||
:placeholder="placeholder"
|
||||
:placeholderClass="placeholderClass"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
:showWordLimit="showWordLimit"
|
||||
:confirmType="confirmType"
|
||||
:confirmHold="confirmHold"
|
||||
:holdKeyboard="holdKeyboard"
|
||||
:focus="focus"
|
||||
:autoBlur="autoBlur"
|
||||
:disableDefaultPadding="disableDefaultPadding"
|
||||
:cursor="cursor"
|
||||
:cursorSpacing="cursorSpacing"
|
||||
:selectionStart="selectionStart"
|
||||
:selectionEnd="selectionEnd"
|
||||
:adjustPosition="adjustPosition"
|
||||
:inputAlign="inputAlign"
|
||||
:fontSize="fontSize"
|
||||
:color="color"
|
||||
:prefixIcon="prefixIcon"
|
||||
:suffixIcon="suffixIcon"
|
||||
:suffixIconStyle="suffixIconStyle"
|
||||
:prefixIconStyle="prefixIconStyle"
|
||||
:border="border"
|
||||
:readonly="readonly"
|
||||
:shape="shape"
|
||||
:customStyle="customStyle"
|
||||
:formatter="formatter"
|
||||
:ignoreCompositionEvent="ignoreCompositionEvent"
|
||||
>
|
||||
<!-- #ifdef MP -->
|
||||
<slot name="prefix"></slot>
|
||||
<slot name="suffix"></slot>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP -->
|
||||
<slot name="prefix" slot="prefix"></slot>
|
||||
<slot name="suffix" slot="suffix"></slot>
|
||||
<!-- #endif -->
|
||||
</uvInput>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 此组件存在的理由是,在nvue下,u-input被uni-app官方占用了,u-input在nvue中相当于input组件
|
||||
* 所以在nvue下,取名为u--input,内部其实还是u-input.vue,只不过做一层中转
|
||||
*/
|
||||
import uvInput from '../u-input/u-input.vue';
|
||||
import { props } from '../u-input/props.js';
|
||||
import { mpMixin } from '../../libs/mixin/mpMixin';
|
||||
import { mixin } from '../../libs/mixin/mixin';
|
||||
export default {
|
||||
name: 'u--input',
|
||||
mixins: [mpMixin, props, mixin],
|
||||
components: {
|
||||
uvInput
|
||||
},
|
||||
}
|
||||
</script>
|
||||
45
uview-plus/components/u--text/u--text.vue
Normal file
45
uview-plus/components/u--text/u--text.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<uvText
|
||||
:type="type"
|
||||
:show="show"
|
||||
:text="text"
|
||||
:prefixIcon="prefixIcon"
|
||||
:suffixIcon="suffixIcon"
|
||||
:mode="mode"
|
||||
:href="href"
|
||||
:format="format"
|
||||
:call="call"
|
||||
:openType="openType"
|
||||
:bold="bold"
|
||||
:block="block"
|
||||
:lines="lines"
|
||||
:color="color"
|
||||
:decoration="decoration"
|
||||
:size="size"
|
||||
:iconStyle="iconStyle"
|
||||
:margin="margin"
|
||||
:lineHeight="lineHeight"
|
||||
:align="align"
|
||||
:wordWrap="wordWrap"
|
||||
:customStyle="customStyle"
|
||||
></uvText>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 此组件存在的理由是,在nvue下,u-text被uni-app官方占用了,u-text在nvue中相当于input组件
|
||||
* 所以在nvue下,取名为u--input,内部其实还是u-text.vue,只不过做一层中转
|
||||
* 不使用v-bind="$attrs",而是分开独立写传参,是因为微信小程序不支持此写法
|
||||
*/
|
||||
import uvText from "../u-text/u-text.vue";
|
||||
import { props } from "../u-text/props.js";
|
||||
import { mpMixin } from '../../libs/mixin/mpMixin.js'
|
||||
import { mixin } from '../../libs/mixin/mixin.js'
|
||||
export default {
|
||||
name: "u--text",
|
||||
mixins: [mpMixin, mixin, props,],
|
||||
components: {
|
||||
uvText,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
47
uview-plus/components/u--textarea/u--textarea.vue
Normal file
47
uview-plus/components/u--textarea/u--textarea.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<uvTextarea
|
||||
:value="value"
|
||||
:modelValue="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:height="height"
|
||||
:confirmType="confirmType"
|
||||
:disabled="disabled"
|
||||
:count="count"
|
||||
:focus="focus"
|
||||
:autoHeight="autoHeight"
|
||||
:fixed="fixed"
|
||||
:cursorSpacing="cursorSpacing"
|
||||
:cursor="cursor"
|
||||
:showConfirmBar="showConfirmBar"
|
||||
:selectionStart="selectionStart"
|
||||
:selectionEnd="selectionEnd"
|
||||
:adjustPosition="adjustPosition"
|
||||
:disableDefaultPadding="disableDefaultPadding"
|
||||
:holdKeyboard="holdKeyboard"
|
||||
:maxlength="maxlength"
|
||||
:border="border"
|
||||
:customStyle="customStyle"
|
||||
:formatter="formatter"
|
||||
:ignoreCompositionEvent="ignoreCompositionEvent"
|
||||
@input="e => $emit('input', e)"
|
||||
@update:modelValue="e => $emit('update:modelValue', e)"
|
||||
></uvTextarea>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 此组件存在的理由是,在nvue下,u--textarea被uni-app官方占用了,u-textarea在nvue中相当于textarea组件
|
||||
* 所以在nvue下,取名为u--textarea,内部其实还是u-textarea.vue,只不过做一层中转
|
||||
*/
|
||||
import uvTextarea from '../u-textarea/u-textarea.vue';
|
||||
import { props } from '../u-textarea/props.js';
|
||||
import { mpMixin } from '../../libs/mixin/mpMixin';
|
||||
import { mixin } from '../../libs/mixin/mixin';
|
||||
export default {
|
||||
name: 'u--textarea',
|
||||
mixins: [mpMixin, props, mixin],
|
||||
components: {
|
||||
uvTextarea
|
||||
},
|
||||
}
|
||||
</script>
|
||||
26
uview-plus/components/u-action-sheet/actionSheet.js
Normal file
26
uview-plus/components/u-action-sheet/actionSheet.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* @Author : LQ
|
||||
* @Description :
|
||||
* @version : 1.0
|
||||
* @Date : 2021-08-20 16:44:21
|
||||
* @LastAuthor : LQ
|
||||
* @lastTime : 2021-08-20 16:44:35
|
||||
* @FilePath : /u-view2.0/uview-ui/libs/config/props/actionSheet.js
|
||||
*/
|
||||
export default {
|
||||
// action-sheet组件
|
||||
actionSheet: {
|
||||
show: false,
|
||||
title: '',
|
||||
description: '',
|
||||
actions: [],
|
||||
index: '',
|
||||
cancelText: '',
|
||||
closeOnClickAction: true,
|
||||
safeAreaInsetBottom: true,
|
||||
openType: '',
|
||||
closeOnClickOverlay: true,
|
||||
round: 0,
|
||||
wrapMaxHeight: '600px'
|
||||
}
|
||||
}
|
||||
62
uview-plus/components/u-action-sheet/props.js
Normal file
62
uview-plus/components/u-action-sheet/props.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { defineMixin } from '../../libs/vue'
|
||||
import defProps from '../../libs/config/props.js'
|
||||
|
||||
export const props = defineMixin({
|
||||
props: {
|
||||
// 操作菜单是否展示 (默认false)
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: () => defProps.actionSheet.show
|
||||
},
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: () => defProps.actionSheet.title
|
||||
},
|
||||
// 选项上方的描述信息
|
||||
description: {
|
||||
type: String,
|
||||
default: () => defProps.actionSheet.description
|
||||
},
|
||||
// 数据
|
||||
actions: {
|
||||
type: Array,
|
||||
default: () => defProps.actionSheet.actions
|
||||
},
|
||||
// 取消按钮的文字,不为空时显示按钮
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: () => defProps.actionSheet.cancelText
|
||||
},
|
||||
// 点击某个菜单项时是否关闭弹窗
|
||||
closeOnClickAction: {
|
||||
type: Boolean,
|
||||
default: () => defProps.actionSheet.closeOnClickAction
|
||||
},
|
||||
// 处理底部安全区(默认true)
|
||||
safeAreaInsetBottom: {
|
||||
type: Boolean,
|
||||
default: () => defProps.actionSheet.safeAreaInsetBottom
|
||||
},
|
||||
// 小程序的打开方式
|
||||
openType: {
|
||||
type: String,
|
||||
default: () => defProps.actionSheet.openType
|
||||
},
|
||||
// 点击遮罩是否允许关闭 (默认true)
|
||||
closeOnClickOverlay: {
|
||||
type: Boolean,
|
||||
default: () => defProps.actionSheet.closeOnClickOverlay
|
||||
},
|
||||
// 圆角值
|
||||
round: {
|
||||
type: [Boolean, String, Number],
|
||||
default: () => defProps.actionSheet.round
|
||||
},
|
||||
// 选项区域最大高度
|
||||
wrapMaxHeight: {
|
||||
type: [String],
|
||||
default: () => defProps.actionSheet.wrapMaxHeight
|
||||
},
|
||||
}
|
||||
})
|
||||
283
uview-plus/components/u-action-sheet/u-action-sheet.vue
Normal file
283
uview-plus/components/u-action-sheet/u-action-sheet.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
|
||||
<template>
|
||||
<u-popup
|
||||
:show="show"
|
||||
mode="bottom"
|
||||
@close="closeHandler"
|
||||
:safeAreaInsetBottom="safeAreaInsetBottom"
|
||||
:round="round"
|
||||
>
|
||||
<view class="u-action-sheet">
|
||||
<view
|
||||
class="u-action-sheet__header"
|
||||
v-if="title"
|
||||
>
|
||||
<text class="u-action-sheet__header__title u-line-1">{{title}}</text>
|
||||
<view
|
||||
class="u-action-sheet__header__icon-wrap"
|
||||
@tap.stop="cancel"
|
||||
>
|
||||
<u-icon
|
||||
name="close"
|
||||
size="17"
|
||||
color="#c8c9cc"
|
||||
bold
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<text
|
||||
class="u-action-sheet__description"
|
||||
:style="[{
|
||||
marginTop: `${title && description ? 0 : '18px'}`
|
||||
}]"
|
||||
v-if="description"
|
||||
>{{description}}</text>
|
||||
<slot>
|
||||
<u-line v-if="description"></u-line>
|
||||
<scroll-view scroll-y class="u-action-sheet__item-wrap" :style="{maxHeight: wrapMaxHeight}">
|
||||
<view :key="index" v-for="(item, index) in actions">
|
||||
<!-- #ifdef MP -->
|
||||
<button
|
||||
class="u-reset-button"
|
||||
:openType="item.openType"
|
||||
@getuserinfo="onGetUserInfo"
|
||||
@contact="onContact"
|
||||
@getphonenumber="onGetPhoneNumber"
|
||||
@error="onError"
|
||||
@launchapp="onLaunchApp"
|
||||
@opensetting="onOpenSetting"
|
||||
:lang="lang"
|
||||
:session-from="sessionFrom"
|
||||
:send-message-title="sendMessageTitle"
|
||||
:send-message-path="sendMessagePath"
|
||||
:send-message-img="sendMessageImg"
|
||||
:show-message-card="showMessageCard"
|
||||
:app-parameter="appParameter"
|
||||
@tap="selectHandler(index)"
|
||||
:hover-class="!item.disabled && !item.loading ? 'u-action-sheet--hover' : ''"
|
||||
>
|
||||
<!-- #endif -->
|
||||
<view
|
||||
class="u-action-sheet__item-wrap__item"
|
||||
@tap.stop="selectHandler(index)"
|
||||
:hover-class="!item.disabled && !item.loading ? 'u-action-sheet--hover' : ''"
|
||||
:hover-stay-time="150"
|
||||
>
|
||||
<template v-if="!item.loading">
|
||||
<text
|
||||
class="u-action-sheet__item-wrap__item__name"
|
||||
:style="[itemStyle(index)]"
|
||||
>{{ item.name }}</text>
|
||||
<text
|
||||
v-if="item.subname"
|
||||
class="u-action-sheet__item-wrap__item__subname"
|
||||
>{{ item.subname }}</text>
|
||||
</template>
|
||||
<u-loading-icon
|
||||
v-else
|
||||
custom-class="van-action-sheet__loading"
|
||||
size="18"
|
||||
mode="circle"
|
||||
/>
|
||||
</view>
|
||||
<!-- #ifdef MP -->
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
<u-line v-if="index !== actions.length - 1"></u-line>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</slot>
|
||||
<u-gap
|
||||
bgColor="#eaeaec"
|
||||
height="6"
|
||||
v-if="cancelText"
|
||||
></u-gap>
|
||||
<view class="u-action-sheet__item-wrap__item u-action-sheet__cancel"
|
||||
hover-class="u-action-sheet--hover" @tap="cancel" v-if="cancelText">
|
||||
<text
|
||||
@touchmove.stop.prevent
|
||||
:hover-stay-time="150"
|
||||
class="u-action-sheet__cancel-text"
|
||||
>{{cancelText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { openType } from '../../libs/mixin/openType'
|
||||
import { buttonMixin } from '../../libs/mixin/button'
|
||||
import { props } from './props';
|
||||
import { mpMixin } from '../../libs/mixin/mpMixin';
|
||||
import { mixin } from '../../libs/mixin/mixin';
|
||||
import { addUnit } from '../../libs/function/index';
|
||||
/**
|
||||
* ActionSheet 操作菜单
|
||||
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
|
||||
* @tutorial https://ijry.github.io/uview-plus/components/actionSheet.html
|
||||
*
|
||||
* @property {Boolean} show 操作菜单是否展示 (默认 false )
|
||||
* @property {String} title 操作菜单标题
|
||||
* @property {String} description 选项上方的描述信息
|
||||
* @property {Array<Object>} actions 按钮的文字数组,见官方文档示例
|
||||
* @property {String} cancelText 取消按钮的提示文字,不为空时显示按钮
|
||||
* @property {Boolean} closeOnClickAction 点击某个菜单项时是否关闭弹窗 (默认 true )
|
||||
* @property {Boolean} safeAreaInsetBottom 处理底部安全区 (默认 true )
|
||||
* @property {String} openType 小程序的打开方式 (contact | launchApp | getUserInfo | openSetting |getPhoneNumber |error )
|
||||
* @property {Boolean} closeOnClickOverlay 点击遮罩是否允许关闭 (默认 true )
|
||||
* @property {Number|String} round 圆角值,默认无圆角 (默认 0 )
|
||||
* @property {String} lang 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文
|
||||
* @property {String} sessionFrom 会话来源,openType="contact"时有效
|
||||
* @property {String} sendMessageTitle 会话内消息卡片标题,openType="contact"时有效
|
||||
* @property {String} sendMessagePath 会话内消息卡片点击跳转小程序路径,openType="contact"时有效
|
||||
* @property {String} sendMessageImg 会话内消息卡片图片,openType="contact"时有效
|
||||
* @property {Boolean} showMessageCard 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,用户点击后可以快速发送小程序消息,openType="contact"时有效 (默认 false )
|
||||
* @property {String} appParameter 打开 APP 时,向 APP 传递的参数,openType=launchApp 时有效
|
||||
*
|
||||
* @event {Function} select 点击ActionSheet列表项时触发
|
||||
* @event {Function} close 点击取消按钮时触发
|
||||
* @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,回调的 detail 数据与 wx.getUserInfo 返回的一致,openType="getUserInfo"时有效
|
||||
* @event {Function} contact 客服消息回调,openType="contact"时有效
|
||||
* @event {Function} getphonenumber 获取用户手机号回调,openType="getPhoneNumber"时有效
|
||||
* @event {Function} error 当使用开放能力时,发生错误的回调,openType="error"时有效
|
||||
* @event {Function} launchapp 打开 APP 成功的回调,openType="launchApp"时有效
|
||||
* @event {Function} opensetting 在打开授权设置页后回调,openType="openSetting"时有效
|
||||
* @example <u-action-sheet :actions="list" :title="title" :show="show"></u-action-sheet>
|
||||
*/
|
||||
export default {
|
||||
name: "u-action-sheet",
|
||||
// 一些props参数和methods方法,通过mixin混入,因为其他文件也会用到
|
||||
mixins: [openType, buttonMixin, mixin, props],
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 操作项目的样式
|
||||
itemStyle() {
|
||||
return (index) => {
|
||||
let style = {};
|
||||
if (this.actions[index].color) style.color = this.actions[index].color
|
||||
if (this.actions[index].fontSize) style.fontSize = addUnit(this.actions[index].fontSize)
|
||||
// 选项被禁用的样式
|
||||
if (this.actions[index].disabled) style.color = '#c0c4cc'
|
||||
return style;
|
||||
}
|
||||
},
|
||||
},
|
||||
emits: ["close", "select", "update:show"],
|
||||
methods: {
|
||||
closeHandler() {
|
||||
// 允许点击遮罩关闭时,才发出close事件
|
||||
if(this.closeOnClickOverlay) {
|
||||
this.$emit('update:show', false)
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
// 点击取消按钮
|
||||
cancel() {
|
||||
this.$emit('update:show', false)
|
||||
this.$emit('close')
|
||||
},
|
||||
selectHandler(index) {
|
||||
const item = this.actions[index]
|
||||
if (item && !item.disabled && !item.loading) {
|
||||
this.$emit('select', item)
|
||||
if (this.closeOnClickAction) {
|
||||
this.$emit('update:show', false)
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/components.scss";
|
||||
$u-action-sheet-reset-button-width:100% !default;
|
||||
$u-action-sheet-title-font-size: 16px !default;
|
||||
$u-action-sheet-title-padding: 12px 30px !default;
|
||||
$u-action-sheet-title-color: $u-main-color !default;
|
||||
$u-action-sheet-header-icon-wrap-right:15px !default;
|
||||
$u-action-sheet-header-icon-wrap-top:15px !default;
|
||||
$u-action-sheet-description-font-size:13px !default;
|
||||
$u-action-sheet-description-color:14px !default;
|
||||
$u-action-sheet-description-margin: 18px 15px !default;
|
||||
$u-action-sheet-item-wrap-item-padding:17px !default;
|
||||
$u-action-sheet-item-wrap-name-font-size:16px !default;
|
||||
$u-action-sheet-item-wrap-subname-font-size:13px !default;
|
||||
$u-action-sheet-item-wrap-subname-color: #c0c4cc !default;
|
||||
$u-action-sheet-item-wrap-subname-margin-top:10px !default;
|
||||
$u-action-sheet-cancel-text-font-size:16px !default;
|
||||
$u-action-sheet-cancel-text-color:$u-content-color !default;
|
||||
$u-action-sheet-cancel-text-font-size:15px !default;
|
||||
$u-action-sheet-cancel-text-hover-background-color:rgb(242, 243, 245) !default;
|
||||
|
||||
.u-reset-button {
|
||||
width: $u-action-sheet-reset-button-width;
|
||||
}
|
||||
|
||||
.u-action-sheet {
|
||||
text-align: center;
|
||||
&__header {
|
||||
position: relative;
|
||||
padding: $u-action-sheet-title-padding;
|
||||
&__title {
|
||||
font-size: $u-action-sheet-title-font-size;
|
||||
color: $u-action-sheet-title-color;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__icon-wrap {
|
||||
position: absolute;
|
||||
right: $u-action-sheet-header-icon-wrap-right;
|
||||
top: $u-action-sheet-header-icon-wrap-top;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: $u-action-sheet-description-font-size;
|
||||
color: $u-tips-color;
|
||||
margin: $u-action-sheet-description-margin;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__item-wrap {
|
||||
|
||||
&__item {
|
||||
padding: $u-action-sheet-item-wrap-item-padding;
|
||||
@include flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
&__name {
|
||||
font-size: $u-action-sheet-item-wrap-name-font-size;
|
||||
color: $u-main-color;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__subname {
|
||||
font-size: $u-action-sheet-item-wrap-subname-font-size;
|
||||
color: $u-action-sheet-item-wrap-subname-color;
|
||||
margin-top: $u-action-sheet-item-wrap-subname-margin-top;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__cancel-text {
|
||||
font-size: $u-action-sheet-cancel-text-font-size;
|
||||
color: $u-action-sheet-cancel-text-color;
|
||||
text-align: center;
|
||||
// padding: $u-action-sheet-cancel-text-font-size;
|
||||
}
|
||||
|
||||
&--hover {
|
||||
background-color: $u-action-sheet-cancel-text-hover-background-color;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
28
uview-plus/components/u-album/album.js
Normal file
28
uview-plus/components/u-album/album.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* @Author : LQ
|
||||
* @Description :
|
||||
* @version : 1.0
|
||||
* @Date : 2021-08-20 16:44:21
|
||||
* @LastAuthor : LQ
|
||||
* @lastTime : 2021-08-20 16:47:24
|
||||
* @FilePath : /u-view2.0/uview-ui/libs/config/props/album.js
|
||||
*/
|
||||
export default {
|
||||
// album 组件
|
||||
album: {
|
||||
urls: [],
|
||||
keyName: '',
|
||||
singleSize: 180,
|
||||
multipleSize: 70,
|
||||
space: 6,
|
||||
singleMode: 'scaleToFill',
|
||||
multipleMode: 'aspectFill',
|
||||
maxCount: 9,
|
||||
previewFullImage: true,
|
||||
rowCount: 3,
|
||||
showMore: true,
|
||||
autoWrap: false,
|
||||
unit: 'px',
|
||||
stop: true,
|
||||
}
|
||||
}
|
||||
86
uview-plus/components/u-album/props.js
Normal file
86
uview-plus/components/u-album/props.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { defineMixin } from '../../libs/vue'
|
||||
import defProps from '../../libs/config/props.js'
|
||||
export const props = defineMixin({
|
||||
props: {
|
||||
// 图片地址,Array<String>|Array<Object>形式
|
||||
urls: {
|
||||
type: Array,
|
||||
default: () => defProps.album.urls
|
||||
},
|
||||
// 指定从数组的对象元素中读取哪个属性作为图片地址
|
||||
keyName: {
|
||||
type: String,
|
||||
default: () => defProps.album.keyName
|
||||
},
|
||||
// 单图时,图片长边的长度
|
||||
singleSize: {
|
||||
type: [String, Number],
|
||||
default: () => defProps.album.singleSize
|
||||
},
|
||||
// 多图时,图片边长
|
||||
multipleSize: {
|
||||
type: [String, Number],
|
||||
default: () => defProps.album.multipleSize
|
||||
},
|
||||
// 多图时,图片水平和垂直之间的间隔
|
||||
space: {
|
||||
type: [String, Number],
|
||||
default: () => defProps.album.space
|
||||
},
|
||||
// 单图时,图片缩放裁剪的模式
|
||||
singleMode: {
|
||||
type: String,
|
||||
default: () => defProps.album.singleMode
|
||||
},
|
||||
// 多图时,图片缩放裁剪的模式
|
||||
multipleMode: {
|
||||
type: String,
|
||||
default: () => defProps.album.multipleMode
|
||||
},
|
||||
// 最多展示的图片数量,超出时最后一个位置将会显示剩余图片数量
|
||||
maxCount: {
|
||||
type: [String, Number],
|
||||
default: () => defProps.album.maxCount
|
||||
},
|
||||
// 是否可以预览图片
|
||||
previewFullImage: {
|
||||
type: Boolean,
|
||||
default: () => defProps.album.previewFullImage
|
||||
},
|
||||
// 每行展示图片数量,如设置,singleSize和multipleSize将会无效
|
||||
rowCount: {
|
||||
type: [String, Number],
|
||||
default: () => defProps.album.rowCount
|
||||
},
|
||||
// 超出maxCount时是否显示查看更多的提示
|
||||
showMore: {
|
||||
type: Boolean,
|
||||
default: () => defProps.album.showMore
|
||||
},
|
||||
// 图片形状,circle-圆形,square-方形
|
||||
shape: {
|
||||
type: String,
|
||||
default: () => defProps.image.shape
|
||||
},
|
||||
// 圆角,单位任意
|
||||
radius: {
|
||||
type: [String, Number],
|
||||
default: () => defProps.image.radius
|
||||
},
|
||||
// 自适应换行
|
||||
autoWrap: {
|
||||
type: Boolean,
|
||||
default: () => defProps.album.autoWrap
|
||||
},
|
||||
// 单位
|
||||
unit: {
|
||||
type: [String],
|
||||
default: () => defProps.album.unit
|
||||
},
|
||||
// 阻止点击冒泡
|
||||
stop: {
|
||||
type: Boolean,
|
||||
default: () => defProps.album.stop
|
||||
}
|
||||
}
|
||||
})
|
||||
279
uview-plus/components/u-album/u-album.vue
Normal file
279
uview-plus/components/u-album/u-album.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<view class="u-album">
|
||||
<view
|
||||
class="u-album__row"
|
||||
ref="u-album__row"
|
||||
v-for="(arr, index) in showUrls"
|
||||
:forComputedUse="albumWidth"
|
||||
:key="index"
|
||||
:style="{flexWrap: autoWrap ? 'wrap' : 'nowrap'}"
|
||||
>
|
||||
<view
|
||||
class="u-album__row__wrapper"
|
||||
v-for="(item, index1) in arr"
|
||||
:key="index1"
|
||||
:style="[imageStyle(index + 1, index1 + 1)]"
|
||||
@tap="previewFullImage ? onPreviewTap($event, getSrc(item)) : ''"
|
||||
>
|
||||
<image
|
||||
:src="getSrc(item)"
|
||||
:mode="
|
||||
urls.length === 1
|
||||
? imageHeight > 0
|
||||
? singleMode
|
||||
: 'widthFix'
|
||||
: multipleMode
|
||||
"
|
||||
:style="[
|
||||
{
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
borderRadius: shape == 'circle' ? '10000px' : addUnit(radius)
|
||||
}
|
||||
]"
|
||||
></image>
|
||||
<view
|
||||
v-if="
|
||||
showMore &&
|
||||
urls.length > rowCount * showUrls.length &&
|
||||
index === showUrls.length - 1 &&
|
||||
index1 === showUrls[showUrls.length - 1].length - 1
|
||||
"
|
||||
class="u-album__row__wrapper__text"
|
||||
:style="{
|
||||
borderRadius: shape == 'circle' ? '50%' : addUnit(radius),
|
||||
}"
|
||||
>
|
||||
<up-text
|
||||
:text="`+${urls.length - maxCount}`"
|
||||
color="#fff"
|
||||
:size="multipleSize * 0.3"
|
||||
align="center"
|
||||
customStyle="justify-content: center"
|
||||
></up-text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { props } from './props';
|
||||
import { mpMixin } from '../../libs/mixin/mpMixin';
|
||||
import { mixin } from '../../libs/mixin/mixin';
|
||||
import { addUnit, sleep } from '../../libs/function/index';
|
||||
import test from '../../libs/function/test';
|
||||
// #ifdef APP-NVUE
|
||||
// 由于weex为阿里的KPI业绩考核的产物,所以不支持百分比单位,这里需要通过dom查询组件的宽度
|
||||
const dom = uni.requireNativePlugin('dom')
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* Album 相册
|
||||
* @description 本组件提供一个类似相册的功能,让开发者开发起来更加得心应手。减少重复的模板代码
|
||||
* @tutorial https://ijry.github.io/uview-plus/components/album.html
|
||||
*
|
||||
* @property {Array} urls 图片地址列表 Array<String>|Array<Object>形式
|
||||
* @property {String} keyName 指定从数组的对象元素中读取哪个属性作为图片地址
|
||||
* @property {String | Number} singleSize 单图时,图片长边的长度 (默认 180 )
|
||||
* @property {String | Number} multipleSize 多图时,图片边长 (默认 70 )
|
||||
* @property {String | Number} space 多图时,图片水平和垂直之间的间隔 (默认 6 )
|
||||
* @property {String} singleMode 单图时,图片缩放裁剪的模式 (默认 'scaleToFill' )
|
||||
* @property {String} multipleMode 多图时,图片缩放裁剪的模式 (默认 'aspectFill' )
|
||||
* @property {String | Number} maxCount 取消按钮的提示文字 (默认 9 )
|
||||
* @property {Boolean} previewFullImage 是否可以预览图片 (默认 true )
|
||||
* @property {String | Number} rowCount 每行展示图片数量,如设置,singleSize和multipleSize将会无效 (默认 3 )
|
||||
* @property {Boolean} showMore 超出maxCount时是否显示查看更多的提示 (默认 true )
|
||||
* @property {String} shape 图片形状,circle-圆形,square-方形 (默认 'square' )
|
||||
* @property {String | Number} radius 圆角值,单位任意,如果为数值,则为px单位 (默认 0 )
|
||||
* @property {Boolean} autoWrap 自适应换行模式,不受rowCount限制,图片会自动换行 (默认 false )
|
||||
* @property {String} unit 图片单位 (默认 px )
|
||||
* @event {Function} albumWidth 某些特殊的情况下,需要让文字与相册的宽度相等,这里事件的形式对外发送 (回调参数 width )
|
||||
* @example <u-album :urls="urls2" @albumWidth="width => albumWidth = width" multipleSize="68" ></u-album>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-album',
|
||||
mixins: [mpMixin, mixin, props],
|
||||
data() {
|
||||
return {
|
||||
// 单图的宽度
|
||||
singleWidth: 0,
|
||||
// 单图的高度
|
||||
singleHeight: 0,
|
||||
// 单图时,如果无法获取图片的尺寸信息,让图片宽度默认为容器的一定百分比
|
||||
singlePercent: 0.6
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
urls: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal.length === 1) {
|
||||
this.getImageRect()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: ["albumWidth"],
|
||||
computed: {
|
||||
imageStyle() {
|
||||
return (index1, index2) => {
|
||||
const { space, rowCount, multipleSize, urls } = this,
|
||||
rowLen = this.showUrls.length,
|
||||
allLen = this.urls.length
|
||||
const style = {
|
||||
marginRight: addUnit(space),
|
||||
marginBottom: addUnit(space)
|
||||
}
|
||||
// 如果为最后一行,则每个图片都无需下边框
|
||||
if (index1 === rowLen && !this.autoWrap) style.marginBottom = 0
|
||||
// 每行的最右边一张和总长度的最后一张无需右边框
|
||||
if (!this.autoWrap) {
|
||||
if (
|
||||
index2 === rowCount ||
|
||||
(index1 === rowLen &&
|
||||
index2 === this.showUrls[index1 - 1].length)
|
||||
)
|
||||
style.marginRight = 0
|
||||
}
|
||||
return style
|
||||
}
|
||||
},
|
||||
// 将数组划分为二维数组
|
||||
showUrls() {
|
||||
if (this.autoWrap) {
|
||||
return [ this.urls.slice(0, this.maxCount) ];
|
||||
} else {
|
||||
const arr = []
|
||||
this.urls.map((item, index) => {
|
||||
// 限制最大展示数量
|
||||
if (index + 1 <= this.maxCount) {
|
||||
// 计算该元素为第几个素组内
|
||||
const itemIndex = Math.floor(index / this.rowCount)
|
||||
// 判断对应的索引是否存在
|
||||
if (!arr[itemIndex]) {
|
||||
arr[itemIndex] = []
|
||||
}
|
||||
arr[itemIndex].push(item)
|
||||
}
|
||||
})
|
||||
return arr
|
||||
}
|
||||
},
|
||||
imageWidth() {
|
||||
return addUnit(
|
||||
this.urls.length === 1 ? this.singleWidth : this.multipleSize, this.unit
|
||||
)
|
||||
},
|
||||
imageHeight() {
|
||||
return addUnit(
|
||||
this.urls.length === 1 ? this.singleHeight : this.multipleSize, this.unit
|
||||
)
|
||||
},
|
||||
// 此变量无实际用途,仅仅是为了利用computed特性,让其在urls长度等变化时,重新计算图片的宽度
|
||||
// 因为用户在某些特殊的情况下,需要让文字与相册的宽度相等,所以这里事件的形式对外发送
|
||||
albumWidth() {
|
||||
let width = 0
|
||||
if (this.urls.length === 1) {
|
||||
width = this.singleWidth
|
||||
} else {
|
||||
width =
|
||||
this.showUrls[0].length * this.multipleSize +
|
||||
this.space * (this.showUrls[0].length - 1)
|
||||
}
|
||||
this.$emit('albumWidth', width)
|
||||
return width
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addUnit,
|
||||
// 预览图片
|
||||
onPreviewTap(e, url) {
|
||||
const urls = this.urls.map((item) => {
|
||||
return this.getSrc(item)
|
||||
})
|
||||
uni.previewImage({
|
||||
current: url,
|
||||
urls
|
||||
})
|
||||
// 是否阻止事件传播
|
||||
this.stop && this.preventEvent(e)
|
||||
},
|
||||
// 获取图片的路径
|
||||
getSrc(item) {
|
||||
return test.object(item)
|
||||
? (this.keyName && item[this.keyName]) || item.src
|
||||
: item
|
||||
},
|
||||
// 单图时,获取图片的尺寸
|
||||
// 在小程序中,需要将网络图片的的域名添加到小程序的download域名才可能获取尺寸
|
||||
// 在没有添加的情况下,让单图宽度默认为盒子的一定宽度(singlePercent)
|
||||
getImageRect() {
|
||||
const src = this.getSrc(this.urls[0])
|
||||
uni.getImageInfo({
|
||||
src,
|
||||
success: (res) => {
|
||||
// 判断图片横向还是竖向展示方式
|
||||
const isHorizotal = res.width >= res.height
|
||||
this.singleWidth = isHorizotal
|
||||
? this.singleSize
|
||||
: (res.width / res.height) * this.singleSize
|
||||
this.singleHeight = !isHorizotal
|
||||
? this.singleSize
|
||||
: (res.height / res.width) * this.singleWidth
|
||||
},
|
||||
fail: () => {
|
||||
this.getComponentWidth()
|
||||
}
|
||||
})
|
||||
},
|
||||
// 获取组件的宽度
|
||||
async getComponentWidth() {
|
||||
// 延时一定时间,以获取dom尺寸
|
||||
await sleep(30)
|
||||
// #ifndef APP-NVUE
|
||||
this.$uGetRect('.u-album__row').then((size) => {
|
||||
this.singleWidth = size.width * this.singlePercent
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
// 这里ref="u-album__row"所在的标签为通过for循环出来,导致this.$refs['u-album__row']是一个数组
|
||||
const ref = this.$refs['u-album__row'][0]
|
||||
ref &&
|
||||
dom.getComponentRect(ref, (res) => {
|
||||
this.singleWidth = res.size.width * this.singlePercent
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/components.scss';
|
||||
|
||||
.u-album {
|
||||
@include flex(column);
|
||||
|
||||
&__row {
|
||||
@include flex(row);
|
||||
|
||||
&__wrapper {
|
||||
position: relative;
|
||||
|
||||
&__text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
@include flex(row);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
uview-plus/components/u-alert/alert.js
Normal file
22
uview-plus/components/u-alert/alert.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* @Author : LQ
|
||||
* @Description :
|
||||
* @version : 1.0
|
||||
* @Date : 2021-08-20 16:44:21
|
||||
* @LastAuthor : LQ
|
||||
* @lastTime : 2021-08-20 16:48:53
|
||||
* @FilePath : /u-view2.0/uview-ui/libs/config/props/alert.js
|
||||
*/
|
||||
export default {
|
||||
// alert警告组件
|
||||
alert: {
|
||||
title: '',
|
||||
type: 'warning',
|
||||
description: '',
|
||||
closable: false,
|
||||
showIcon: false,
|
||||
effect: 'light',
|
||||
center: false,
|
||||
fontSize: 14
|
||||
}
|
||||
}
|
||||
46
uview-plus/components/u-alert/props.js
Normal file
46
uview-plus/components/u-alert/props.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineMixin } from '../../libs/vue'
|
||||
import defProps from '../../libs/config/props.js'
|
||||
export const props = defineMixin({
|
||||
props: {
|
||||
// 显示文字
|
||||
title: {
|
||||
type: String,
|
||||
default: () => defProps.alert.title
|
||||
},
|
||||
// 主题,success/warning/info/error
|
||||
type: {
|
||||
type: String,
|
||||
default: () => defProps.alert.type
|
||||
},
|
||||
// 辅助性文字
|
||||
description: {
|
||||
type: String,
|
||||
default: () => defProps.alert.description
|
||||
},
|
||||
// 是否可关闭
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: () => defProps.alert.closable
|
||||
},
|
||||
// 是否显示图标
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: () => defProps.alert.showIcon
|
||||
},
|
||||
// 浅或深色调,light-浅色,dark-深色
|
||||
effect: {
|
||||
type: String,
|
||||
default: () => defProps.alert.effect
|
||||
},
|
||||
// 文字是否居中
|
||||
center: {
|
||||
type: Boolean,
|
||||
default: () => defProps.alert.center
|
||||
},
|
||||
// 字体大小
|
||||
fontSize: {
|
||||
type: [String, Number],
|
||||
default: () => defProps.alert.fontSize
|
||||
}
|
||||
}
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user