优化生产展示逻辑,增加可选

增加自定义组件的说明书
This commit is contained in:
廖德云 2025-03-12 21:51:38 +08:00
commit 3030c2a261
553 changed files with 72349 additions and 1 deletions

View File

@ -13,7 +13,7 @@
<!-- 标题行 -->
<view class="header-row">
<view class="title">天然气产量</view>
<view class="more" @click="selectMore">更多 </view>
<view class="more" @click="selectMore">更多>></view>
</view>
</view>
<view class="container">

View File

@ -0,0 +1,118 @@
<template>
<view class="compact-datetime-picker">
<!-- 输入框区域 -->
<view class="input-container">
<view class="compact-input" @click="openPicker">
{{ dateRange[0] || '开始日期' }}
</view>
<text v-if="mode==='range'" class="separator"></text>
<view v-if="mode==='range'" class="compact-input" @click="openPicker">
{{ dateRange[1] || '结束日期' }}
</view>
</view>
<wu-calendar ref="calendar" :mode="mode" :date="dateRange" @confirm="calendarConfirm"
slideSwitchMode="horizontal" type="month" :fold="false" :insert="false" :rangeSameDay="true" :lunar="true"
:monthShowCurrentMonth="false" :range-end-repick="true" :item-height="45"
:rangeEndRepick="true"></wu-calendar>
</view>
</template>
<script>
import {
formatDate,
getDateAfterDays,
getDateAfterMonths
} from '@/utils/dateTime.js';
export default {
props: {
modelValue: {
type: Array,
default: () => [null, null]
},
//
mode: {
type: String,
default: 'single'
},
},
data() {
return {
dateRange: null
};
},
watch: {
modelValue: {
immediate: true,
handler(newVal) {
console.log(44, JSON.stringify(newVal))
let dateType = Object.prototype.toString.call(newVal);
if (this.mode == 'single' && dateType != '[object String]') {
return console.error(`类型错误mode=${this.mode}date=String`);
} else if (this.mode != 'single' && dateType != '[object Array]') {
return console.error(`类型错误mode=${this.mode}date=Array`);
}
if (this.mode == 'single') {
this.dateRange = ''
this.dateRange = newVal;
}
//
if (this.mode == 'multiple') {
this.dateRange = []
this.dateRange = newVal;
} else if (this.mode == 'range') {
this.dateRange = []
this.dateRange.push(formatDate(new Date(newVal[0])));
this.dateRange.push(formatDate(new Date(newVal[1])));
}
}
}
},
methods: {
openPicker() {
this.$refs.calendar.open();
},
calendarConfirm(e) {
this.dateRange = [];
try {
this.dateRange.push(formatDate(new Date(e.range.before)));
this.dateRange.push(formatDate(new Date(e.range.after)));
} catch (error) {
return;
//TODO handle the exception
}
this.$emit('update:modelValue', this.dateRange);
}
}
};
</script>
<style scoped>
.input-container {
display: flex;
align-items: center;
gap: 10px;
}
/* 紧凑型输入框 */
.compact-input {
flex: 1;
padding: 6px 8px;
font-size: 16px;
height: 20px;
line-height: 20px;
border: 1px solid #dcdfe6;
border-radius: 4px;
text-align: center;
width: 120px;
}
.compact-input.active {
border-color: #409eff;
background-color: #f5f7ff;
}
</style>

View File

@ -0,0 +1,83 @@
{
"id": "cxc-szcx-dateRangeSelect",
"displayName": "cxc-szcx-dateRangeSelect",
"version": "1.0.0",
"description": "cxc-szcx-dateRangeSelect",
"keywords": [
"cxc-szcx-dateRangeSelect"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "",
"data": "",
"permissions": ""
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "u",
"aliyun": "u",
"alipay": "u"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u",
"app-uvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,162 @@
### 紧凑型日期时间选择器组件说明
#### 一、组件功能
该组件是基于 UniApp 开发的紧凑型日期选择器,支持以下功能:
1. **三种选择模式**
- 单选single
- 范围选择range
- 多选multiple
2. **弹出式日历选择**
- 支持月份滑动切换
- 显示农历日期
- 支持同天范围选择
3. **数据双向绑定**
- 通过 `modelValue` 实现父子组件数据同步
4. **自定义样式**
- 紧凑型输入框设计
- 支持自定义主题颜色
- 响应式布局适配
#### 二、组件Props
| 参数名 | 类型 | 默认值 | 说明 |
|--------------|------------|----------|--------------------------|
| `modelValue` | Array/Date | `[null]` | 双向绑定的值(根据模式变化) |
| `mode` | String | `single` | 选择模式single/range/multiple |
#### 三、组件数据
| 名称 | 类型 | 说明 |
|------------|--------|--------------------------|
| `dateRange` | Array | 当前选中的日期范围(内部状态) |
#### 四、组件方法
| 方法名 | 参数 | 说明 |
|----------------|------------|--------------------------|
| `openPicker` | 无 | 打开日历选择器 |
| `calendarConfirm` | `e` | 日历确认回调(处理选中数据) |
#### 五、事件说明
| 事件名 | 说明 | 返回值 |
|--------------|--------------------------|----------------------|
| `update:modelValue` | 数据更新时触发 | 当前选中的日期数组 |
#### 六、样式说明
```vue
<style scoped>
.input-container {
display: flex;
align-items: center;
gap: 10px;
}
.compact-input {
flex: 1;
padding: 6px 8px;
font-size: 16px;
height: 20px;
line-height: 20px;
border: 1px solid #dcdfe6;
border-radius: 4px;
text-align: center;
width: 120px;
}
.compact-input.active {
border-color: #409eff;
background-color: #f5f7ff;
}
</style>
```
**关键样式点**
1. 输入框宽度固定为 120px
2. 紧凑型设计height: 20px
3. 活动状态样式增强
4. 水平排列布局
#### 七、使用示例
```vue
<template>
<view>
<!-- 单选模式 -->
<compact-datetime-picker
v-model="singleDate"
mode="single"
/>
<!-- 范围选择模式 -->
<compact-datetime-picker
v-model="rangeDate"
mode="range"
/>
<!-- 多选模式 -->
<compact-datetime-picker
v-model="multipleDate"
mode="multiple"
/>
</view>
</template>
<script>
export default {
data() {
return {
singleDate: null,
rangeDate: [null, null],
multipleDate: []
};
}
};
</script>
```
#### 八、注意事项
1. **日期格式要求**
- 单选模式:`YYYY-MM-DD` 字符串格式
- 范围模式:`[startDate, endDate]` 数组格式
- 多选模式:`[date1, date2, ...]` 数组格式
2. **依赖组件**
- 需安装 `wu-calendar` 组件(第三方库)
3. **异常处理**
```javascript
calendarConfirm(e) {
this.dateRange = [];
try {
this.dateRange.push(formatDate(new Date(e.range.before)));
this.dateRange.push(formatDate(new Date(e.range.after)));
} catch (error) {
return; // 异常处理
}
this.$emit('update:modelValue', this.dateRange);
}
```
4. **性能优化**
- 建议使用 `:range-end-repick="true"` 开启范围重新选择
- 通过 `:item-height="45"` 控制日历行高
#### 九、扩展功能建议
1. 添加时间选择功能
2. 支持自定义日期禁用范围
3. 增加国际化支持
4. 添加动画过渡效果
5. 支持预设常用日期范围
如果需要进一步定制化,可以修改以下部分:
1. 日历组件配置(`wu-calendar` 参数)
2. 日期格式化函数(`formatDate`
3. 输入框样式和交互逻辑
4. 异常处理机制

View File

@ -0,0 +1,71 @@
<template>
<view>
<uni-data-checkbox v-model="selectedValuesArray" :localdata="dictItems" :multiple="true" data-key="value"
data-value="label"></uni-data-checkbox>
</view>
</template>
<script setup>
import {
ref,
onMounted,
watch
} from 'vue';
import {
getDictItemsApi
} from '@/api/api.js';
// props emits
const props = defineProps({
dictCode: {
type: String,
required: true
},
modelValue: {
type: String,
default: ''
}
});
const emits = defineEmits(['update:modelValue', 'change']);
//
const dictItems = ref([]);
const selectedValuesArray = ref(props.modelValue ? props.modelValue.split(',') : []);
//
const loadDictItems = async () => {
try {
const response = await getDictItemsApi(props.dictCode);
dictItems.value = response.result; // { data: [...] }
} catch (error) {
console.error('加载字典数据失败:', error);
}
};
//
watch(selectedValuesArray, (newValue) => {
console.log('selectedValuesArray 变化:', newValue);
const selectedValuesString = newValue.join(',');
emits('update:modelValue', selectedValuesString);
emits('change', selectedValuesString);
}, {
deep: true
});
// props.modelValue
watch(() => props.modelValue, (newValue) => {
console.log('props.modelValue 变化:', newValue);
selectedValuesArray.value = newValue ? newValue.split(',') : [];
}, {
immediate: true
});
onMounted(() => {
loadDictItems();
});
</script>
<style scoped>
/* 可以添加组件的样式 */
</style>

View File

@ -0,0 +1,83 @@
{
"id": "cxc-szcx-dictSelect",
"displayName": "cxc-szcx-dictSelect",
"version": "1.0.0",
"description": "cxc-szcx-dictSelect",
"keywords": [
"cxc-szcx-dictSelect"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "",
"data": "",
"permissions": ""
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "u",
"aliyun": "u",
"alipay": "u"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u",
"app-uvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,90 @@
# cxc-szcx-dictSelect
### 组件说明:`uni-data-checkbox` 多选字典项选择组件
#### 一、组件概述
该组件基于 Vue 3 和 `uni-data-checkbox` 实现了一个多选的字典项选择器。它通过传入字典编码(`dictCode`)从后端接口获取字典项数据,并将其展示为多选框列表。组件支持双向数据绑定,可将选中的值同步给父组件,同时在选中值发生变化时触发 `change` 事件。
#### 二、组件使用的技术栈
- **框架**Vue 3使用组合式 API
- **UI 组件**`uni-data-checkbox`(用于展示多选框列表)
- **请求库**:假设使用了自定义的 `getDictItemsApi` 函数进行后端数据请求
#### 三、组件输入Props
| 属性名 | 类型 | 是否必填 | 默认值 | 说明 |
| ---- | ---- | ---- | ---- | ---- |
| `dictCode` | String | 是 | 无 | 用于从后端获取字典项数据的字典编码 |
| `modelValue` | String | 否 | '' | 双向绑定的选中值,多个值以逗号分隔 |
#### 四、组件输出Emits
| 事件名 | 说明 | 参数 |
| ---- | ---- | ---- |
| `update:modelValue` | 当选中值发生变化时,用于更新父组件的 `modelValue` | 选中值的字符串,多个值以逗号分隔 |
| `change` | 当选中值发生变化时触发 | 选中值的字符串,多个值以逗号分隔 |
#### 五、组件内部数据
| 变量名 | 类型 | 说明 |
| ---- | ---- | ---- |
| `dictItems` | Ref<Array> | 存储从后端获取的字典项数据 |
| `selectedValuesArray` | Ref<Array> | 存储当前选中的值,以数组形式存储 |
#### 六、组件方法
##### 1. `loadDictItems`
- **功能**:异步从后端接口获取字典项数据,并将其赋值给 `dictItems`
- **实现逻辑**
- 调用 `getDictItemsApi` 函数,传入 `props.dictCode` 进行数据请求。
- 若请求成功,将响应数据的 `result` 字段赋值给 `dictItems.value`
- 若请求失败,在控制台输出错误信息。
##### 2. 监听 `selectedValuesArray` 的变化
- **功能**:当 `selectedValuesArray` 发生变化时,将选中的值转换为字符串,并触发 `update:modelValue``change` 事件通知父组件。
- **实现逻辑**
- 使用 `watch` 函数监听 `selectedValuesArray` 的变化。
- 当变化发生时,将新的选中值数组使用 `join(',')` 方法转换为字符串。
- 触发 `update:modelValue``change` 事件,将转换后的字符串作为参数传递。
##### 3. 监听 `props.modelValue` 的变化
- **功能**:当父组件传递的 `modelValue` 发生变化时,更新 `selectedValuesArray`
- **实现逻辑**
- 使用 `watch` 函数监听 `props.modelValue` 的变化。
- 当变化发生时,将新的 `modelValue` 使用 `split(',')` 方法转换为数组,并赋值给 `selectedValuesArray.value`
- `immediate: true` 表示在组件初始化时就会执行一次监听回调,确保初始值能正确同步。
#### 七、组件生命周期钩子
##### `onMounted`
- **功能**:在组件挂载后调用 `loadDictItems` 函数,从后端获取字典项数据。
#### 八、使用示例
```vue
<template>
<div>
<MyDictCheckbox
:dictCode="myDictCode"
v-model="selectedValues"
@change="handleChange"
/>
<p>当前选中的值: {{ selectedValues }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import MyDictCheckbox from './MyDictCheckbox.vue';
const myDictCode = 'exampleDictCode';
const selectedValues = ref('');
const handleChange = (newValue) => {
console.log('选中值发生变化:', newValue);
};
</script>
```
#### 九、注意事项
- 确保 `getDictItemsApi` 函数能正确获取后端数据,且响应数据的格式符合 `{ result: [...] }`
- 若 `modelValue` 初始值为空字符串,`selectedValuesArray` 会初始化为空数组。
- 由于使用了 `watch``deep: true` 选项,在 `selectedValuesArray` 内部元素发生变化时也会触发监听回调,可能会影响性能,需注意。

View File

@ -0,0 +1,274 @@
<template>
<view>
<!-- ECharts图表 -->
<view class="chart-container">
<l-echart ref="chart" @finished="initChart" />
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
import { formatDate, getDateAfterDays, getDateAfterMonths } from '@/utils/dateTime.js';
const props = defineProps({
dataList: {
type: Array,
default: () => [],
required: true
},
xField: {
type: String,
default: 'rqDate'
},
yField: {
type: String,
default: 'rq'
},
legendField: {
type: String,
default: 'unit'
},
referenceValue: {
type: Number,
default: 0
}
});
const chart = ref(null);
const chartOption = ref({});
const chartTitle = ref('');
//
const processSeriesData = () => {
const legendItems = [...new Set(props.dataList.map((item) => item[props.legendField]?.trim() || '未命名'))];
return legendItems.map((legend) => {
const items = props.dataList
.filter((item) => item[props.legendField] === legend)
.sort((a, b) => new Date(a[props.xField]) - new Date(b[props.xField]))
.map((item) => ({
name: item[props.xField],
value: [
formatDate(item[props.xField]), // X
parseFloat(item[props.yField]) || 0 // Y
]
}));
return {
name: legend,
type: 'line',
showSymbol: true,
smooth: true,
data: items
};
});
};
//
watch(
() => props.dataList,
() => {
chartTitle.value = '历史趋势';
updateChart();
},
{ deep: true }
);
//
onMounted(() => {});
onUnmounted(() => {
if (chart.value) {
chart.value.dispose();
chart.value = null;
}
});
// formatDate
const updateChart = () => {
if (!chart.value) return;
chartOption.value = generateOptions();
chart.value.setOption(chartOption.value, true);
};
//
const generateOptions = () => ({
title: {
text: chartTitle.value,
padding: [0, 0, 0, 30]
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255, 255, 255, 0.96)',
borderColor: '#eee',
borderWidth: 1,
textStyle: {
color: '#333',
fontSize: 14
},
axisPointer: {
type: 'line',
lineStyle: {
color: '#FF6B6B',
width: 1,
type: 'dashed'
}
},
confine: true // tooltip
},
backgroundColor: 'rgba(255, 255, 255, 0.8)',
textStyle: {
color: '#666',
fontSize: 12
},
xAxis: {
type: 'time',
axisLine: {
lineStyle: {
color: '#ddd'
}
},
axisLabel: {
color: '#666',
formatter: '{MM}-{dd}',
rotate: 30
},
splitLine: {
show: true,
lineStyle: {
color: '#f5f5f5'
}
}
},
yAxis: {
type: 'value',
axisLine: {
show: true,
lineStyle: {
color: '#ddd'
}
},
splitLine: {
lineStyle: {
color: '#f5f5f5'
}
},
axisLabel: {
color: '#666',
formatter: (value) => `${value.toFixed(2)}`
}
},
series: [
...processSeriesData().map((series) => ({
...series,
lineStyle: {
width: 2,
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 6,
shadowOffsetY: 3
},
itemStyle: {
color: '#00007f', //
borderColor: '#fff',
borderWidth: 1
}
})),
{
type: 'line',
markLine: {
silent: true,
lineStyle: {
type: 'dashed',
color: '#FF4757',
width: 2
},
data: [{ yAxis: props.referenceValue }],
label: {
show: true,
position: 'end',
formatter: `参考值: ${props.referenceValue}`,
fontSize: 12
}
},
data: [] // 线
}
],
legend: {
type: 'scroll',
bottom: 5,
textStyle: {
color: '#666',
fontSize: 12
},
pageIconColor: '#FF6B6B',
pageTextStyle: {
color: '#999'
}
},
grid: {
top: 30,
right: 30,
bottom: 40,
left: 20,
containLabel: true
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
minValueSpan: 3600 * 24 * 1000 * 7 // 7
}
]
});
//
const initChart = () => {
setTimeout(async () => {
if (!chart.value) return;
const myChart = await chart.value.init(echarts);
myChart.setOption(chartOption.value);
}, 300);
};
</script>
<style scoped>
/* 容器样式 */
.chart-container {
width: 100%;
height: 30vh;
min-height: 200px;
padding: 20rpx;
background: linear-gradient(145deg, #f8f9fa 0%, #ffffff 100%);
border-radius: 16rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
position: relative;
overflow: hidden;
}
/* 图表加载占位 */
.chart-container::before {
/* content: '数据加载中...'; */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #666;
font-size: 28rpx;
z-index: 1;
}
/* 图表主体样式 */
.chart-wrapper {
width: 100%;
height: 100%;
transition: opacity 0.3s ease;
}
/* 移动端优化 */
@media screen and (max-width: 768px) {
.chart-container {
height: 30vh;
min-height: 200px;
padding: 10rpx;
border-radius: 12rpx;
}
}
</style>

View File

@ -0,0 +1,83 @@
{
"id": "cxc-szcx-lineChart",
"displayName": "cxc-szcx-lineChart",
"version": "1.0.0",
"description": "cxc-szcx-lineChart",
"keywords": [
"cxc-szcx-lineChart"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "",
"data": "",
"permissions": ""
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "u",
"aliyun": "u",
"alipay": "u"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u",
"app-uvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,179 @@
# cxc-szcx-lineChart
# # `line-chart.vue` 组件说明书
## 一、组件概述
`line-chart.vue` 是一个基于 ECharts 实现的折线图组件,用于在 UniApp 项目中展示数据的折线图。该组件接收一系列数据和配置参数,支持动态更新数据,并展示参考线。
## 二、组件依赖
- **Vue 3**:使用 Vue 3 的组合式 API 进行开发。
- **ECharts**:用于绘制折线图。
- **`lime-echart`**UniApp 插件,提供 ECharts 的渲染支持。
## 三、组件使用方法
### 1. 引入组件
在需要使用该组件的页面中引入 `line-chart.vue` 组件。
```vue
<template>
<view>
<line-chart
:dataList="dataList"
:xField="xField"
:yField="yField"
:legendField="legendField"
:referenceValue="referenceValue"
/>
</view>
</template>
<script setup>
import LineChart from '@/components/line-chart.vue';
const dataList = [
{ date: '2023-01-01', value: 10, category: 'A' },
// 更多数据...
];
const xField = 'date';
const yField = 'value';
const legendField = 'category';
const referenceValue = 15;
</script>
```
### 2. 组件属性
| 属性名 | 类型 | 默认值 | 描述 |
| ---- | ---- | ---- | ---- |
| `dataList` | `Object` | `[]` | 包含图表数据的数组,每个元素是一个对象,包含 `xField`、`yField` 和 `legendField` 对应的数据。 |
| `xField` | `String` | `''` | 数据中用于表示 x 轴的字段名。 |
| `yField` | `String` | `''` | 数据中用于表示 y 轴的字段名。 |
| `legendField` | `String` | `''` | 数据中用于表示图例的字段名。 |
| `referenceValue` | `Number` | `10` | 图表中参考线的数值。 |
## 四、组件内部实现
### 1. 模板部分
```vue
<template>
<view class="chart-container">
<l-echart ref="chart" @init="initChart"></l-echart>
</view>
</template>
```
- 使用 `l-echart` 组件渲染图表,通过 `ref` 绑定到 `chart`,并在初始化完成时调用 `initChart` 方法。
### 2. 脚本部分
#### 2.1 引入必要的模块
```javascript
import { ref, watch } from 'vue';
```
引入 Vue 3 的 `ref``watch` 函数。
#### 2.2 定义组件属性
```javascript
const props = defineProps({
dataList: {
type: Object,
default: () => []
},
// 其他属性...
});
```
定义组件接收的属性。
#### 2.3 初始化图表
```javascript
const chart = ref(null);
let echarts = null;
const initChart = async (canvas) => {
await initEcharts(canvas);
updateChart();
};
const initEcharts = async (canvas) => {
echarts = await import('echarts');
const { init } = await import('@/uni_modules/lime-echart/static/echarts.min');
echarts = init(canvas, echarts);
};
```
- `chart` 用于引用 `l-echart` 组件。
- `initChart` 方法在图表初始化时调用,先初始化 ECharts 实例,再更新图表。
- `initEcharts` 方法异步加载 ECharts 并初始化实例。
#### 2.4 处理数据
```javascript
const processData = () => {
const legendData = [...new Set(props.dataList.map((item) => item[props.legendField]))];
return legendData.map((legendItem) => {
const seriesData = props.dataList
.filter((item) => item[props.legendField] === legendItem)
.sort((a, b) => new Date(a[props.xField]) - new Date(b[props.xField]))
.map((item) => ({
name: item[props.xField],
value: [item[props.xField], item[props.yField]]
}));
return {
name: legendItem,
type: 'line',
showSymbol: true,
data: seriesData
};
});
};
```
处理传入的数据,根据 `legendField` 分组,对每组数据按 `xField` 排序,并转换为 ECharts 所需的格式。
#### 2.5 获取图表配置
```javascript
const getOption = () => ({
tooltip: {
trigger: 'axis',
formatter: (params) => {
return `${params[0].axisValue}<br/>` + params.map((item) => `${item.marker} ${item.seriesName}: ${item.value[1]}`).join('<br/>');
}
},
// 其他配置...
});
```
返回 ECharts 的配置对象,包括 tooltip、x 轴、y 轴、系列数据、图例和网格等配置。
#### 2.6 更新图表
```javascript
const updateChart = () => {
if (!chart.value) return;
const option = getOption();
chart.value.setOption(option);
};
```
如果 `chart` 实例存在,获取最新的图表配置并更新图表。
#### 2.7 监听数据变化
```javascript
watch(
() => props.dataList,
() => {
updateChart();
},
{ deep: true }
);
```
`props.dataList` 发生变化时,调用 `updateChart` 方法更新图表。
### 3. 样式部分
```css
.chart-container {
width: 100%;
height: 30vh;
}
```
设置图表容器的宽度为 100%,高度为 30vh。
## 五、注意事项
- 确保项目中已经正确安装并配置了 `lime-echart` 插件。
- 传入的 `dataList` 数据格式要符合要求,包含 `xField`、`yField` 和 `legendField` 对应的数据。
- 由于使用了异步加载 ECharts可能会有一定的延迟需要确保在合适的时机初始化图表。

View File

@ -0,0 +1,33 @@
## 0.1.32023-08-19
- fix: 修复使用remove导致样式错乱
## 0.1.22023-08-09
- fix: 修复nvue没有获取节点的问题
- fix: 修复因延时导致卡在中途
- fix: 修复change事件有时失效的问题
## 0.1.12023-07-03
- chore: 更新文档
## 0.1.02023-07-03
- fix: 外面的事件冒泡导致点击调动内部移动方法错乱
## 0.0.92023-05-30
- fix: 修复因手机事件为`onLongpress`导致,在手机上无法长按
- fix: 无法因css导致滚动
## 0.0.82023-04-23
- feat: 更新文档
## 0.0.72023-04-23
- feat: 由于删除是一个危险的动作,故把方法暴露出来,而不在内部处理。如果之前有使用删除的,需要注意
- feat: 原来的`add`变更为`push`,增加`unshift`
## 0.0.62023-04-12
- fix: 修复`handle`不生效问题
- feat: 增加 `to`方法
## 0.0.52023-04-11
- chore: `grid` 插槽增加 `nindex`、`oindex`
## 0.0.42023-04-04
- chore: 去掉 script-setup 语法糖
- chore: 文档增加 vue2 使用方法
## 0.0.32023-03-30
- feat: 重要说明 更新 list 只会再次初始化
- feat: 更新文档
## 0.0.22023-03-29
- 修改文档
## 0.0.12023-03-29
- 初次提交

View File

@ -0,0 +1,93 @@
$drag-handle-size: var(--l-drag-handle-size, 50rpx);
$drag-delete-size: var(--l-drag-delete-size, 32rpx);
.l-drag {
// min-height: 100rpx;
overflow: hidden;
margin: 24rpx 30rpx 0 30rpx;
// padding: 30rpx 0;
/* #ifdef APP-NVUE */
// flex: 1;
/* #endif */
/* #ifndef APP-NVUE */
// width: 100%;
/* #endif */
}
.l-drag__inner {
/* #ifdef APP-NVUE */
flex: 1;
/* #endif */
/* #ifndef APP-NVUE */
width: 100%;
/* #endif */
min-height: 100rpx;
}
.l-drag__view {
// touch-action: none;
// user-select: none;
// -webkit-user-select: auto;
z-index: 2;
transition: opacity 300ms ease;
.mask {
position: absolute;
inset: 0;
background-color: transparent;
z-index: 9;
}
/* #ifndef APP-NVUE */
> view {
&:last-child {
width: 100%;
height: 100%;
}
}
box-sizing: border-box;
/* #endif */
}
.l-drag-enter {
opacity: 0;
}
.l-drag__ghost {
/* #ifndef APP-NVUE */
> view {
&:last-child {
width: 100%;
height: 100%;
}
}
box-sizing: border-box;
/* #endif */
}
.l-is-active {
z-index: 3;
}
.l-is-hidden {
opacity: 0;
}
.l-drag__delete {
position: absolute;
z-index: 10;
width: $drag-delete-size;
height: $drag-delete-size;
}
.l-drag__handle {
position: absolute;
z-index: 10;
width: $drag-handle-size;
height: $drag-handle-size;
}
/* #ifndef APP-NVUE */
.l-drag__delete::before,.l-drag__handle::before {
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 10;
}
/* #endif */

View File

@ -0,0 +1,532 @@
<template>
<view class="l-drag l-class" :style="[areaStyles]" ref="dragRef" @touchstart="setDisabled">
<movable-area class="l-drag__inner" v-if="isReset" :style="[innerStyles]">
<slot></slot>
<movable-view class="l-drag__ghost" v-if="isDrag && props.ghost" :animation="true" :style="[viewStyles]" direction="all" :x="ghostEl.x" :y="ghostEl.y" key="l-drag-clone">
<slot name="ghost"></slot>
</movable-view>
<movable-view v-if="props.before" class="l-drag__before" disabled :animation="false" :style="[viewStyles]" :x="beforeEl.x" :y="beforeEl.y">
<slot name="before"></slot>
</movable-view>
<movable-view
v-for="(item, oindex) in cloneList" :key="item.id"
direction="all"
:data-oindex="oindex"
:style="[viewStyles]"
class="l-drag__view"
:class="[{'l-is-active': oindex == active, 'l-is-hidden': !item.show}, item.class]"
:x="item.x"
:y="item.y"
:friction="friction"
:damping="damping"
:animation="animation"
:disabled="isDisabled || props.disabled"
@touchstart="touchStart"
@change="touchMove"
@touchend="touchEnd"
@touchcancel="touchEnd"
@longpress="setDisabled"
>
<!-- <view v-if="props.remove" class="l-drag__remove" :style="removeStyle" data-remove="true">
<slot name="remove" :oindex="oindex" data-remove="true" />
</view> -->
<!-- <view v-if="props.handle" class="l-drag__handle" :style="handleStyle" data-handle="true">
<slot name="handle" :oindex="oindex" :active="!isDisabled && !isDisabled && oindex == active" />
</view> -->
<slot name="grid" :oindex="oindex" :index="item.index" :oldindex="item.oldindex" :content="item.content" :active="!isDisabled && !isDisabled && oindex == active" />
<view class="mask" v-if="!(isDisabled || props.disabled) && props.longpress"></view>
</movable-view>
<movable-view v-if="props.after" class="l-drag__after" disabled :animation="true" direction="all" :style="[viewStyles]" :x="afterEl.x" :y="afterEl.y">
<slot name="after"></slot>
</movable-view>
</movable-area>
</view>
</template>
<script lang="ts">
// @ts-nocheck
import { computed, onMounted, ref, getCurrentInstance, watch, nextTick, reactive , triggerRef, onUnmounted, defineComponent} from "./vue";
import DragProps from './props';
import type {GridRect, Grid, Position} from './type'
// #ifdef APP-NVUE
const dom = weex.requireModule('dom')
// #endif
export default defineComponent({
name: 'l-drag',
externalClasses: ['l-class'],
options: {
addGlobalClass: true,
virtualHost: true,
},
props: DragProps,
emits: ['change'],
setup(props, {emit, expose}) {
const res = wx.getSystemInfoSync();
const statusHeight = res.statusBarHeight; //
const cusnavbarheight = (statusHeight + 44) + "px";
// #ifdef APP-NVUE
const dragRef = ref(null)
// #endif
const app = getCurrentInstance()
const isDrag = ref(false)
const isInit = ref(false)
const isReset = ref(true)
const colmunId = ref(-1)
/** 选中项原始下标 */
const active = ref(-1)
const maxIndex = ref(-1)
const animation = ref(true)
const isDisabled = ref(props.handle || props.longpress ? true: false)
const dragEl = reactive({
content: null,
/** 当前视图下标*/
index: 0,
/** 旧视图下标 */
oldindex: -1,
/** 上次原始下标 */
lastindex: -1
})
const ghostEl = reactive({
content: null,
x: 0,
y: 0
})
const beforeEl = reactive({
x: 0,
y: 0
})
const afterEl = reactive({
x: 0,
y: 0
})
let gridRects = [] //ref<GridRect[]>([])
const areaWidth = ref(0)
const cloneList = ref<Grid[]>([])
//
const leaveRow = ref(0)
const extra = computed(() => (props.before ? 1 :0) + (props.after ? 1 : 0))
const rows = computed(() => Math.ceil( ((isInit.value ? cloneList.value.length : props.list.length) + extra.value) / props.column ))
const gridHeight = computed(() => props.aspectRatio ? girdWidth.value / props.aspectRatio : (/rpx$/.test(`${props.gridHeight}`) ? uni.upx2px(parseInt(`${props.gridHeight}`)) : parseInt(`${props.gridHeight}`)))
const girdWidth = computed(() => areaWidth.value / props.column)
const viewStyles = computed(() => ({width: girdWidth.value + 'px',height: gridHeight.value + 'px'}))
const areaStyles = computed(() => ({height: (rows.value + leaveRow.value ) * gridHeight.value + 'px'}))
const innerStyles = computed(() => ({
// #ifdef APP-NVUE
width: areaWidth.value + 'px',
// #endif
height: (rows.value + props.extraRow + leaveRow.value) * gridHeight.value + 'px'}))
const sleep = (cb: Function, time = 1000/60) => setTimeout(cb, time)
const createGrid = (content: any, position?:Position|null): Grid => {
colmunId.value++
maxIndex.value++
const index = maxIndex.value
const colmun = gridRects[index]
let x = 0
let y = 0
if(colmun) {
if(props.after) {
let nxet = gridRects[index + 1]
if(!nxet) {
nxet = createGridRect(gridRects.length + (props.before ? 1 : 0))
gridRects.push(nxet)
}
setReset(() => setAfter(nxet))
} else {
setReset()
}
x = colmun.x
y = colmun.y
} else {
const nxet = createGridRect(gridRects.length + (props.before ? 1 : 0))
gridRects.push(nxet)
setReset()
x = nxet.x
y = nxet.y
}
if(position) {
x = position.x
y = position.y
}
return {id: `l-drag-item-${colmunId.value}`, index, oldindex: index, content, x, y, class: '', show: true}
}
const setReset = (cb?: any) => {
// const newRow = (cloneList.value.length + extra.value) % (props.column)
if(isInit.value) {
cb&&sleep(cb)
}
}
const setAfter = ({x, y} = {x: 0, y: 0}) => {
if(props.after) {
afterEl.x = x
afterEl.y = y
}
}
const setDisabled = (e: any, flag?: boolean= false) => {
// e?.preventDefault()
const type = `${e.type}`.toLowerCase()
const {handle = props.touchHandle} = e.target.dataset
if(props.handle && !handle) {
isDisabled.value = true
} else if(props.handle && handle && !props.longpress) {
isDisabled.value = flag
} else if(props.handle && handle && props.longpress && type.includes('longpress')) {
isDisabled.value = false
} else if(props.longpress && type.includes('longpress') && !props.handle) {
isDisabled.value = false
}
if(type.includes('touchend') && props.longpress) {
isDisabled.value = true
}
}
const createGridRect = (i: number, last?: GridRect): GridRect => {
let { row } = last || gridRects[gridRects.length - 1] || { row: 0 }
const col = i % (props.column)
const grid = (row: number, x: number, y: number):GridRect => {
return {row, x, y, x1: x + girdWidth.value, y1: y + gridHeight.value}
}
if(col == 0 && i != 0) {row++}
return grid(row, col * girdWidth.value, row * gridHeight.value)
}
const createGridRects = () => {
let rects: GridRect[] = []
const length = rows.value * props.column + extra.value
gridRects = []
for (var i = 0; i < length; i++) {
const item = createGridRect(i, rects[rects.length - 1])
rects.push(item)
}
if(props.before) {
const {x, y} = rects.shift()
beforeEl.x = x
beforeEl.y = y
}
setAfter(rects[props.list.length])
gridRects = rects as GridRect[]
}
const updateList = (v: any[]) => {
cloneList.value = v.map((content) => createGrid(content))
}
const touchStart = (e: any) => {
if(e.target.dataset.remove) return
//
const {oindex} = e.currentTarget?.dataset || e.target?.dataset || {}
if(typeof oindex !== 'number') return
const target = cloneList.value[oindex]
isDrag.value = true
//
active.value = oindex
//
dragEl.index = dragEl.oldindex = target.index
ghostEl.x = target.x||0
ghostEl.y = target.y||0
dragEl.content = ghostEl.content = target.content
}
const touchEnd = (e: any) => {
setTimeout(() => {
if(e.target.dataset.remove || active.value==-1) return
setDisabled(e, true)
isDrag.value = false
const isEmit = dragEl.index !== dragEl.oldindex && dragEl.oldindex > -1 // active.value !== dragEl.index
dragEl.lastindex = active.value
dragEl.oldindex = active.value = -1
const last = cloneList.value[dragEl.lastindex]
const position = gridRects[dragEl.index]
nextTick(() => {
last.x = position.x + 0.001
last.y = position.y + 0.001
sleep(() => {
last.x = position.x
last.y = position.y
isEmit && emitting()
})
})
},80)
}
const emitting = () => {
const clone = [...cloneList.value].sort((a, b) => a.index - b.index)//.map(item => ref(item.content))
emit('change', clone)
}
const touchMove = (e: any) => {
if(!isDrag.value) return
// #ifndef APP-NVUE
let {oindex} = e.currentTarget.dataset
// #endif
// #ifdef APP-NVUE
oindex = e.currentTarget.dataset['-oindex']
// #endif
if(oindex != active.value) return
const {x, y} = e.detail
const centerX = x + girdWidth.value / 2
const centerY = y + gridHeight.value / 2
for (let i = 0; i < cloneList.value.length; i++) {
const item = gridRects[i]
if(centerX > item.x && centerX < item.x1 && centerY > item.y && centerY < item.y1) {
ghostEl.x = item.x
ghostEl.y = item.y
if(dragEl.index != i) {
_move(active.value, i)
}
break;
}
}
}
const getDragEl = (oindex: number) => {
if(isDrag.value) {return dragEl}
return cloneList.value[oindex]
}
/**
* 把原始数据中排序为index的项 移动到 toIndex
* @param oindex 原始数据的下标
* @param toIndex 视图中的下标
* @param position 指定坐标
*/
const _move = (oindex: number, toIndex: number, position?: Position|null, emit: boolean = true) => {
const length = cloneList.value.length - 1
if(toIndex > length || toIndex < 0) return
// oIdnex
const dragEl = getDragEl(oindex)
let speed = 0
let start = dragEl.index
// indexindex
if(start < toIndex) {speed = 1}
if(start > toIndex) {speed = -1}
if(!speed) return
//
let distance = start - toIndex
//
while(distance) {
distance += speed
//
const target = isDrag.value ? (dragEl.index += speed) : (start += speed)
let targetOindex = cloneList.value.findIndex(item => item.index == target && item.content != dragEl.content)
if (targetOindex == oindex) return
if (targetOindex < 0) {targetOindex = cloneList.value.length - 1}
let targetEl = cloneList.value[targetOindex]
if(!targetEl) return;
// index
const lastIndex = target - speed
const activeEl = cloneList.value[oindex]
const rect = gridRects[lastIndex]
targetEl.x = rect.x
targetEl.y = rect.y
targetEl.oldindex = targetEl.index
targetEl.index = lastIndex
activeEl.oldindex = activeEl.index //oIndex
activeEl.index = toIndex
//
if(!distance && !isDrag.value) {
const rect = gridRects[toIndex]
const {x, y} = position||rect
activeEl.x = dragEl.x = x
activeEl.y = dragEl.y = y
// triggerRef(cloneList)
if(emit) {
emitting()
}
}
}
}
/**
* 为区分是主动调用还是内部方法
*/
const move = (oindex: number, toIndex: number) => {
active.value = -1
isDrag.value = false
_move(oindex, toIndex)
}
//
const REMOVE_TIME = 400
let removeTimer = null
const remove = (oindex: number) => {
active.value = -1
isDrag.value = false
clearTimeout(removeTimer)
const item = cloneList.value[oindex]
if(props.disabled || !item) return
item.show = false
const after = cloneList.value.length - 1
_move(oindex, after, item, false)
setAfter(gridRects[after])
maxIndex.value--
const _remove = (_index = oindex) => {
//
// animation.value = false
const row = Math.ceil((cloneList.value.length - 1 + extra.value) / props.column)
if( row < rows.value) {
leaveRow.value = (rows.value - row)
}
cloneList.value.splice(_index, 1)[0]
emitting()
removeTimer = setTimeout(() => {
leaveRow.value = 0
}, REMOVE_TIME)
}
_remove()
}
const push = (...args: any) => {
if(props.disabled) return
if(Array.isArray(args)) {
Promise.all(args.map(async item => await add(item, true))).then(emitting)
}
}
const add = (content: any, after: boolean) => {
return new Promise((resolve) => {
const item = createGrid(content, after ? null : {x: -100, y:0})
item.class = 'l-drag-enter'
cloneList.value.push(item)
const length = cloneList.value.length - 1
nextTick(() => {
sleep(() => {
item.class = 'l-drag-leave'
_move(length, (after ? length : 0), null, false)
triggerRef(cloneList)
resolve(true)
})
})
})
}
const unshift = (...args: any) => {
if(props.disabled) return
if(Array.isArray(args)) {
Promise.all(args.map(async (item) => await add(item))).then(emitting)
}
}
//
const shift = () => {
if(!cloneList.value.length) return
remove(cloneList.value.findIndex(item => item.index == 0) || 0)
}
const pop = () => {
const length = cloneList.value.length-1
if(length < 0 ) return
remove(cloneList.value.findIndex(item => item.index == length) || length)
}
// const splice = (start, count, ...context) => {
// //
// }
const clear = () => {
isInit.value = isDrag.value = false
maxIndex.value = colmunId.value = active.value = -1
cloneList.value = []
gridRects = []
}
const init = () => {
clear()
createGridRects()
nextTick(() => {
updateList(props.list)
isInit.value = true
})
}
let count = 0
const getRect = () => {
count++
// #ifndef APP-NVUE
uni.createSelectorQuery().in(app.proxy).select('.l-drag').boundingClientRect((res: UniNamespace.NodeInfo) => {
if(res) {
areaWidth.value = res.width || 0
//
init()
}
}).exec()
// #endif
// #ifdef APP-NVUE
sleep(() => {
nextTick(() => {
dom.getComponentRect(dragRef.value, (res) => {
if(!res.size.width && count < 5) {
getRect()
} else {
areaWidth.value = res.size.width || 0
init()
}
})
})
})
// #endif
}
onMounted(getRect)
onUnmounted(clear)
watch(() => props.list, init)
// #ifdef VUE3
expose({
remove,
// add,
move,
push,
unshift,
shift,
pop
})
// #endif
return {
// #ifdef APP-NVUE
dragRef,
// #endif
cloneList,
areaStyles,
innerStyles,
viewStyles,
setDisabled,
isDisabled,
isReset,
isDrag,
active,
animation,
afterEl,
ghostEl,
beforeEl,
touchStart,
touchMove,
touchEnd,
remove,
// add,
move,
push,
unshift,
// shift,
// pop,
props
// isDelete: props.delete,
// ...toRefs(props)
}
}
})
</script>
<style lang="scss">
.l-drag{
margin-top: v-bind(cusnavbarheight);
}
@import './index';
</style>

View File

@ -0,0 +1,47 @@
// @ts-nocheck
export default {
list: {
type: Array,
default: []
},
column: {
type: Number,
default: 2
},
/**宽高比 填写这项, gridHeight 失效*/
aspectRatio: Number,
gridHeight: {
type: [Number, String],
default: '120rpx'
},
// removeStyle: String,
// handleStyle: String,
damping: {
type: Number,
default: 40
},
friction: {
type: Number,
default: 2
},
/**
* movable-area
*/
extraRow: {
type: Number,
default: 0
},
/**
* movable-area vif , BUG uni官方好像修复了
*/
// reset: Boolean,
// sort: Boolean,
// remove: Boolean,
ghost: Boolean,
handle: Boolean,
touchHandle: Boolean,
before: Boolean,
after: Boolean,
disabled: Boolean,
longpress: Boolean,
}

View File

@ -0,0 +1,21 @@
export interface Position {
x: number
y: number
}
export interface GridRect extends Position{
row : number
// x : number
// y : number
x1 : number
y1 : number
}
export interface Grid extends Position{
id : string
index : number
oldindex : number
content : any
// x : number
// y : number
class : string
show: boolean
}

View File

@ -0,0 +1,9 @@
// @ts-nocheck
// export * from '@/uni_modules/lime-vue'
// #ifdef VUE3
export * from 'vue';
// #endif
// #ifndef VUE3
export * from '@vue/composition-api';
// #endif

View File

@ -0,0 +1,268 @@
<template>
<demo-block type="ultra" title="拖拽">
<demo-block title="基础">
<l-drag :list="list">
<template #grid="{active, index}">
<view class="inner" :class="{active}">
<text class="text" :class="{'text-active': active}">{{index}}</text>
</view>
</template>
</l-drag>
</demo-block>
<demo-block title="多列 长按">
<!-- 列后 删除 幽灵 长按 -->
<l-drag ref="dragRef2" :list="list" @change="change2" :aspectRatio="1" :column="4" after ghost longpress>
<template #grid="{oindex, content, active}">
<view class="grid">
<view class="remove" @click="onRemove2(oindex)"></view>
<view class="inner" :class="{active}">
<text class="text" :class="{'text-active': active}">{{content}}</text>
</view>
</view>
</template>
<template #ghost>
<view class="grid">
<!-- 幽灵样式 -->
<view class="inner ghost"></view>
</view>
</template>
<!-- 示例未设置 before -->
<template #before>
<view class="grid">
<view class="inner extra" @click="onAdd2">
增加
</view>
</view>
</template>
<template #after>
<view class="grid">
<view class="inner extra" @click="onAdd2">
增加
</view>
</view>
</template>
</l-drag>
<button @click="refresh2">更新列表</button>
<button @click="onUnshift2">向前增加</button>
<button @click="onPush2">向后增加</button>
<button @click="onShift2">shift</button>
<button @click="onPop2">pop</button>
</demo-block>
<demo-block title="单列 按手柄">
<!-- 幽灵 手柄 -->
<view class="inputs">
<text>把原始下标为</text>
<input type="text" v-model="move3.index">
<text>的项移动到</text>
<input type="text" v-model="move3.nindex">
</view>
<view style="height: 600rpx; overflow: auto;">
<l-drag ref="dragRef3" :list="list3" @change="change3" grid-height="200rpx" :column="1" :touchHandle="touchHandle3" ghost handle>
<template #grid="{active, content, index, oindex}">
<view class="grid">
<view class="mover" @touchstart="touchHandle3 = true" @touchend="touchHandle3 = false" style="left: 100rpx; top: 50%; transform: translateY(-50%); position: absolute; z-index: 1;"></view>
<view class="inner" :class="{active}">
<text class="text" :class="{'text-active': active}">{{content}}</text>
<view @click.stop.prevent="to(oindex, index - 1)">上移</view>
<view @click.stop.prevent="to(oindex, index + 1)">下移</view>
</view>
</view>
</template>
<template #ghost>
<view class="grid">
<!-- 幽灵样式 -->
<view class="inner ghost"></view>
</view>
</template>
</l-drag>
</view>
</demo-block>
</demo-block>
</template>
<script>
import {ref, watch, defineComponent, reactive} from '../l-drag/vue'
export default defineComponent({
setup() {
// 12list
const list = ref(new Array(7).fill(0).map((v,i) => i));
// 2
const dragRef2 = ref(null)
const newList2 = ref([])
const change2 = v => {
console.log('示例2数据发生变化', v)
newList2.value = [...v]
}
const onRemove2 = (index) => {
if(dragRef2.value && index >= 0) {
dragRef2.value.remove(index)
}
}
const onAdd2 = () => {
dragRef2.value.push(Math.round(Math.random() * 1000))
}
const onShift2 = () => {
dragRef2.value.shift()
}
const onPop2 = () => {
dragRef2.value.pop()
}
const onUnshift2 = () => {
dragRef2.value.unshift(Math.round(Math.random() * 1000), Math.round(Math.random() * 1000))
}
const onPush2 = () => {
dragRef2.value.push(Math.round(Math.random() * 1000), Math.round(Math.random() * 1000))
}
const refresh2 = () => {
list.value = new Array(10).fill(0).map((v,i) => i);
}
// 3
const list3 = new Array(5).fill(0).map((v,i) => i);
const dragRef3 = ref(null)
const newList3 = ref([])
const touchHandle3 = ref(false)
const change3 = v => {
newList3.value = v
}
const move3 = reactive({
index: 0,
nindex: 0
})
watch(() => move3.nindex, (v, o) => {
dragRef3.value.move(move3.index*1, move3.nindex*1)
})
const to = (oindex, index) => {
dragRef3.value.move(oindex, index)
}
return {
list,
// 2
dragRef2,
change2,
onRemove2,
onAdd2,
refresh2,
onUnshift2,
onPush2,
onShift2,
onPop2,
// 3
list3,
dragRef3,
change3,
move3,
touchHandle3,
to
}
}
})
</script>
<style lang="scss">
.grid {
height: 100%;
/* #ifdef APP-NVUE */
flex: 1;
/* #endif */
/* #ifndef APP-NVUE */
width: 100%;
/* #endif */
padding: 16rpx;
box-sizing: border-box;
position: relative;
}
.remove {
width: 32rpx;
height: 32rpx;
background-color: red;
border-radius: 50%;
font-size: 16rpx;
color: white;
display: flex;
text-align: center;
justify-content: center;
z-index: 10;
position: absolute;
right: 0;
top: 0;
}
.inner {
/* #ifdef APP-NVUE */
flex: 1;
/* #endif */
/* #ifndef APP-NVUE */
height: 100%;
width: 100%;
/* #endif */
border: 1rpx solid #eee;
display: flex;
align-items: center;
justify-content: center;
background-color: white;
font-size: 50rpx;
font-weight: bold;
color: blue;
transition: all 300ms ease;
position: relative;
}
.extra {
color: #ddd
}
.mover {
position: relative;
width: 50rpx;
margin-top: 10rpx;
height: 30rpx;
border: 2px solid #ddd;
border-left: none;
border-right: none;
/* background-color: #ddd; */
}
.mover::before {
content: '';
position: absolute;
width: 50rpx;
top: 15rpx;
border-top: 2px solid #ddd;
}
.active {
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.1);
background-color: blue;
color: white;
border: 1rpx solid blue;
transform: scale(1.1);
}
/* #ifdef APP-NVUE */
.text {
font-size: 50rpx;
font-weight: bold;
}
.text-active {
font-size: 50rpx;
font-weight: bold;
color: white;
}
/* #endif */
.ghost {
background-color: rgba(0, 0, 255, 0.1);
}
.inputs {
display: flex;
align-items: center;
input {
background-color: #fff;
width: 80rpx;
border: 1rpx solid #ddd;
padding: 5rpx 10rpx;
margin: 0 8rpx;
}
}
</style>

View File

@ -0,0 +1,87 @@
{
"id": "lime-drag",
"displayName": "拖拽排序-拖动排序-LimeUI",
"version": "0.1.3",
"description": "uniapp vue3 拖拽排序插件,用于图片或列表的拖动排序,可设置列数、增加删除等功能, vue2只要配置@vue/composition-api",
"keywords": [
"拖拽",
"拖拽排序",
"排序",
"拖动",
"拖动排序"
],
"repository": "",
"engines": {
"HBuilderX": "^3.7.12"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
"lime-shared"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "n",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -0,0 +1,170 @@
# lime-drag 拖拽排序
- 当前为初版 可能会有BUG
- 基于uniapp vue3
- Q群 1169785031
### 安装
- 在市场导入插件即可在任意页面使用,无须再`import`
### 使用
- 提供简单的使用示例更多请查看下方的demo
```html
<l-drag :list="list" @change="change">
<!-- // 每一项的插槽 grid 的 content 您传入的数据 -->
<template #grid="{active, content}">
<!-- // grid.active 是否为当前拖拽项目 根据自己需要写样式 -->
<view class="inner" :class="{active: active}">
<text class="text" :class="{'text-active': active}">{{grid.content}}</text>
</view>
</template>
</l-drag>
```
```js
const list = new Array(7).fill(0).map((v,i) => i);
// 拖拽后新的数据
const newList = ref([])
const change = v => newList.value = v
```
#### 增删
- 不要给list赋值这样只会重新初始化
- 增加数据 调用暴露的`push`
- 删除某条数据调用暴露的`remove`方法,需要传入`oindex`
```html
<l-drag :list="list" @change="change" ref="dragRef" after remove>
<!-- 每一项插槽 grid 的 content 是您传入的数据 -->
<template #grid="{active, index, oldindex, oindex}">
<!-- active 是否为当前拖拽项目 根据自己需要写样式 -->
<!-- index 排序后列表下标 -->
<!-- oldindex 排序后列表旧下标 -->
<!-- oindex 列表原始下标,输入的数据排位不会因为排序而改变 -->
<view class="remove" @click="onRemove(oindex)"></view>
<view class="inner" :class="{active}">
<text class="text" :class="{'text-active': active}">{{content}}</text>
</view>
</template>
<template #after>
<view class="grid">
<view class="inner extra" @click="onAdd">
增加
</view>
</view>
</template>
</l-drag>
```
```js
const dragRef = ref(null)
const list = new Array(7).fill(0).map((v,i) => i);
const onAdd = () => {
dragRef.value.push(Math.round(Math.random() * 1000))
}
const onRemove = (oindex) => {
if(dragRef.value && oindex >= 0) {
// 记得oindex为数组的原始index
dragRef.value.remove(oindex)
}
}
```
#### 插槽
```html
<l-drag :list="list">
<!-- 每一项的插槽 -->
<template #grid="{active, index, oldindex, oindex, content}"></template>
<!-- 当前拖拽项幽灵插槽 设置`ghost`后使用 主要为实现拖拽时 有个影子跟着 -->
<template #ghots></template>
<!-- 前后方插槽为固定在列表前方和后方,不能拖动 -->
<!-- 列表前方的插槽 设置`before`后使用 -->
<template #before></template>
<!-- 列表后方的插槽 设置`after`后使用 -->
<template #after></template>
</l-drag>
```
### 查看示例
- 导入后直接使用这个标签查看演示效果
```html
<!-- // 代码位于 uni_modules/lime-drag/compoents/lime-drag -->
<lime-drag />
```
### 插件标签
- 默认 l-drag 为 component
- 默认 lime-drag 为 demo
### 关于vue2的使用方式
- 插件使用了`composition-api`, 如果你希望在vue2中使用请按官方的教程[vue-composition-api](https://uniapp.dcloud.net.cn/tutorial/vue-composition-api.html)配置
- 关键代码是: 在main.js中 在vue2部分加上这一段即可,官方是把它单独成了一个文件.
```js
// vue2
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
```
- 另外插件也用到了TSvue2可能会遇过官方的TS版本过低的问题,找到HX目录下的`compile-typescript`目录
```cmd
// \HBuilderX\plugins\compile-typescript
yarn add typescript -D
- or -
npm install typescript -D
```
- 小程序需要在`manifest.json`启用`slotMultipleInstance`
```json
"mp-weixin" : {
"slotMultipleInstance" : true
}
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| --------------------------| ------------------------------------------------------------ | ---------------- | ------------ |
| list | 列表数组,不可变化,变化后会重新初始化 | <em>array</em> | `[]` |
| column | 列数 | <em>number</em> | `2` |
| gridHeight | 行高,宫格高度 | <em>string</em> | `120rpx` |
| damping | 阻尼系数用于控制x或y改变时的动画和过界回弹的动画值越大移动越快 | <em>string</em> | `-` |
| friction | 摩擦系数用于控制惯性滑动的动画值越大摩擦力越大滑动越快停止必须大于0否则会被设置成默认值 | <em>number</em> | `2` |
| extraRow | 额外行数 | <em>number</em> | `0` |
| ghost | 开启幽灵插槽 | <em>boolean</em> | `false` |
| before | 开启列前插槽 | <em>boolean</em> | `false` |
| after | 开启列后插槽 | <em>boolean</em> | `false` |
| disabled | 是否禁用 | <em>boolean</em> | `false` |
| longpress | 是否长按 | <em>boolean</em> | `false` |
### Events
| 参数 | 说明 | 参数 |
| --------------------------| ------------------------------------------------------------ | ---------------- |
| change | 返回新数据 | list |
### Expose
| 参数 | 说明 | 参数 |
| --------------------------| ------------------------------------------------------------ | ---------------- |
| remove | 删除, 传入`oindex`,即数据列表原始的index | |
| push | 向后增加,可以是数组或单数据 | |
| unshift | 向前增加,可以是数组或单数据 | |
| move | 移动, 传入(`oindex`, `toindex`),将数据列表原始的index项移到视图中的目标位置 | |
### TODO
将来实现的功能
- splice
## 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png)
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png)

View File

@ -0,0 +1,205 @@
## 0.9.82024-12-20
- fix: 修复 APP 无法放大问题
## 0.9.72024-12-02
- feat: uniapp 增加`landscape`,当`landscape`为`true`时旋转90deg达到横屏效果。
- feat: 支持uniapp x 微信小程序
## 0.9.62024-07-23
- fix: 修复 uni is not defined
## 0.9.52024-07-19
- chore: 鸿蒙`measureText`为异步,异步字体不正常,使用模拟方式。
## 0.9.42024-07-18
- chore: 更新文档
## 0.9.32024-07-16
- feat: 鸿蒙 canvas 事件缺失,待官方修复,如何在鸿蒙使用请看文档`常见问题 vue3`
## 0.9.22024-07-12
- chore: 删除多余文件
## 0.9.12024-07-12
- fix: 修复 安卓5不显示图表问题
## 0.9.02024-06-13
- chore: 合并nvue和uvue
## 0.8.92024-05-19
- chore: 更新文档
## 0.8.82024-05-13
- chore: 更新文档和uvue示例
## 0.8.72024-04-26
- fix: uniapp x需要HBX 4.13以上
## 0.8.62024-04-10
- feat: 支持 uniapp x ios
## 0.8.52024-04-03
- fix: 修复 nvue `reset`传值不生效问题
- feat: 支持 uniapp x web
## 0.8.42024-01-27
- chore: 更新文档
## 0.8.32024-01-21
- chore: 更新文档
## 0.8.22024-01-21
- feat: 支持 `uvue`
## 0.8.12023-08-24
- fix: app 的`touch`事件为`object` 导致无法显示 `tooltip`
## 0.8.02023-08-22
- fix: 离屏 报错问题
- fix: 微信小程序PC无法使用事件
- chore: 更新文档
## 0.7.92023-07-29
- chore: 更新文档
## 0.7.82023-07-29
- fix: 离屏 报错问题
## 0.7.72023-07-27
- chore: 更新文档
- chore: lime-echart 里的示例使用自定tooltips
- feat: 对支持离屏的使用离屏创建(微信、字节、支付宝)
## 0.7.62023-06-30
- fix: vue3 报`width`的错
## 0.7.52023-05-25
- chore: 更新文档 和 demo, 使用`lime-echart`这个标签即可查看示例
## 0.7.42023-05-22
- chore: 增加关于钉钉小程序上传时提示安全问题的说明及修改建议
## 0.7.32023-05-16
- chore: 更新 vue3 非微信小程序平台可能缺少`wx`的说明
## 0.7.22023-05-16
- chore: 更新 vue3 非微信小程序平台的可以缺少`wx`的说明
## 0.7.12023-04-26
- chore: 更新demo使用`lime-echart`这个标签即可查看示例
- chore微信小程序的`tooltip`文字有阴影,怀疑是微信的锅,临时解决方法是`tooltip.shadowBlur = 0`
## 0.7.02023-04-24
- fix: 修复`setAttribute is not a function`
## 0.6.92023-04-15
- chore: 更新文档vue3请使用echarts esm的包
## 0.6.82023-03-22
- feat: mac pc无法使用canvas 2d
## 0.6.72023-03-17
- feat: 更新文档
## 0.6.62023-03-17
- feat: 微信小程序PC已经支持canvas 2d故去掉判断PC
## 0.6.52022-11-03
- fix: 某些手机touches为对象导致无法交互。
## 0.6.42022-10-28
- fix: 优化点击事件的触发条件
## 0.6.32022-10-26
- fix: 修复 dataZoom 拖动问题
## 0.6.22022-10-23
- fix: 修复 飞书小程序 尺寸问题
## 0.6.12022-10-19
- fix: 修复 PC mousewheel 事件 鼠标位置不准确的BUG不兼容火狐
- feat: showLoading 增加传参
## 0.6.02022-09-16
- feat: 增加PC的mousewheel事件
## 0.5.42022-09-16
- fix: 修复 nvue 动态数据不显示问题
## 0.5.32022-09-16
- feat: 增加enableHover属性 在PC端时当鼠标进入显示tooltip不必按下。
- chore: 更新文档
## 0.5.22022-09-16
- feat: 增加enableHover属性 在PC端时当鼠标进入显示tooltip不必按下。
## 0.5.12022-09-16
- fix: 修复nvue报错
## 0.5.02022-09-15
- feat: init(echarts, theme?:string, opts?:{}, callback: function(chart))
## 0.4.82022-09-11
- feat: 增加 @finished
## 0.4.72022-08-24
- chore: 去掉 stylus
## 0.4.62022-08-24
- feat: 增加 beforeDelay
## 0.4.52022-08-12
- chore: 更新文档
## 0.4.42022-08-12
- fix: 修复 resize 无参数时报错
## 0.4.32022-08-07
# 评论有说本插件对新手不友好,让我做不好就不要发出来。 还有的说跟官网一样,发出来做什么,给我整无语了。
# 所以在此提醒一下准备要下载的你,如果你从未使用过 echarts 请不要下载 或 谨慎下载。
# 如果你确认要下载麻烦看完文档。还有请注意插件是让echarts在uniapp能运行API 配置请自行去官网查阅!
# 如果你不会echarts 但又需要图表,市场上有个很优秀的图表插件 uchart 你可以去使用这款插件uchart的作者人很好也热情。
# 每个人都有自己的本职工作,如果你能力强可以自行兼容,如果使用了他人的插件也麻烦尊重他人的成果和劳动时间。谢谢。
# 为了心情愉悦,本人已经使用插件屏蔽差评。
- chore: 更新文档
## 0.4.22022-07-20
- feat: 增加 resize
## 0.4.12022-06-07
- fix: 修复 canvasToTempFilePath 不生效问题
## 0.4.02022-06-04
- chore 为了词云 增加一个canvas 标签
- 词云下载地址[echart-wordcloud](https://ext.dcloud.net.cn/plugin?id=8430)
## 0.3.92022-06-02
- chore: 更新文档
- tips: lines 不支持 `trailLength`
## 0.3.82022-05-31
- fix: 修复 因mouse事件冲突tooltip跳动问题
## 0.3.72022-05-26
- chore: 更新文档
- chore: 设置默认宽高300px
- fix: 修复 vue3 微信小程序 拖影BUG
- chore: 支持PC
## 0.3.52022-04-28
- chore: 更新使用方式
- 🔔 必须使用hbuilderx 3.4.8-alpha以上
## 0.3.42021-08-03
- chore: 增加 setOption的参数值
## 0.3.32021-07-22
- fix: 修复 径向渐变报错的问题
## 0.3.22021-07-09
- chore: 统一命名规范,无须主动引入组件
## [代码示例站点1](https://limeui.qcoon.cn/#/echart-example)
## [代码示例站点2](http://liangei.gitee.io/limeui/#/echart-example)
## 0.3.12021-06-21
- fix: 修复 app-nvue ios is-enable 无效的问题
## [代码示例站点1](https://limeui.qcoon.cn/#/echart-example)
## [代码示例站点2](http://liangei.gitee.io/limeui/#/echart-example)
## 0.3.02021-06-14
- fix: 修复 头条系小程序 2d 报 JSON.stringify 的问题
- 目前 头条系小程序 2d 无法在开发工具上预览划动图表页面无法滚动axisLabel 字体颜色无法更改建议使用非2d。
## 0.2.92021-06-06
- fix: 修复 头条系小程序 2d 放大的BUG
- 头条系小程序 2d 无法在开发工具上预览,也存在划动图表页面无法滚动的问题。
## [代码示例http://liangei.gitee.io/limeui/#/echart-example](http://liangei.gitee.io/limeui/#/echart-example)
## 0.2.82021-05-19
- fix: 修复 微信小程序 PC 显示过大的问题
## 0.2.72021-05-19
- fix: 修复 微信小程序 PC 不显示问题
## [代码示例http://liangei.gitee.io/limeui/#/echart-example](http://liangei.gitee.io/limeui/#/echart-example)
## 0.2.62021-05-14
- feat: 支持 `image`
- feat: props 增加 `ec.clear`,更新时是否先删除图表样式
- feat: props 增加 `isDisableScroll` ,触摸图表时是否禁止页面滚动
- feat: props 增加 `webviewStyles` webview 的样式, 仅nvue有效
## 0.2.52021-05-13
- docs: 插件用到了css 预编译器 [stylus](https://ext.dcloud.net.cn/plugin?name=compile-stylus) 请安装它
## 0.2.42021-05-12
- fix: 修复 百度平台 多个图表ctx 和 渐变色 bug
- ## [代码示例http://liangei.gitee.io/limeui/#/echart-example](http://liangei.gitee.io/limeui/#/echart-example)
## 0.2.32021-05-10
- feat: 增加 `canvasToTempFilePath` 方法,用于生成图片
```js
this.$refs.chart.canvasToTempFilePath({success: (res) => {
console.log('tempFilePath:', res.tempFilePath)
}})
```
## 0.2.22021-05-10
- feat: 增加 `dispose` 方法,用于销毁实例
- feat: 增加 `isClickable` 是否派发点击
- feat: 实验性的支持 `nvue` 使用要慎重考虑
- ## [代码示例http://liangei.gitee.io/limeui/#/echart-example](http://liangei.gitee.io/limeui/#/echart-example)
## 0.2.12021-05-06
- fix修复 微信小程序 json 报错
- chore: `reset` 更改为 `setChart`
- feat: 增加 `isEnable` 开启初始化 启用这个后 无须再使用`init`方法
```html
<l-echart ref="chart" is-enable />
```
```js
// 显示加载
this.$refs.chart.showLoading()
// 使用实例回调
this.$refs.chart.setChart(chart => ...code)
// 直接设置图表配置
this.$refs.chart.setOption(data)
```
## 0.2.02021-05-05
- fix修复 头条 百度 偏移的问题
- docs: 更新文档
## [代码示例http://liangei.gitee.io/limeui/#/echart-example](http://liangei.gitee.io/limeui/#/echart-example)
## 0.1.02021-05-02
- chore: 第一次上传,基本全端兼容,使用方法与官网一致。
- 已知BUG非2d 无法使用背景色,已反馈官方
- 已知BUG头条 百度 有许些偏移
- 后期计划兼容nvue

View File

@ -0,0 +1,395 @@
import {getDeviceInfo} from './utils';
const cacheChart = {}
const fontSizeReg = /([\d\.]+)px/;
class EventEmit {
constructor() {
this.__events = {};
}
on(type, listener) {
if (!type || !listener) {
return;
}
const events = this.__events[type] || [];
events.push(listener);
this.__events[type] = events;
}
emit(type, e) {
if (type.constructor === Object) {
e = type;
type = e && e.type;
}
if (!type) {
return;
}
const events = this.__events[type];
if (!events || !events.length) {
return;
}
events.forEach((listener) => {
listener.call(this, e);
});
}
off(type, listener) {
const __events = this.__events;
const events = __events[type];
if (!events || !events.length) {
return;
}
if (!listener) {
delete __events[type];
return;
}
for (let i = 0, len = events.length; i < len; i++) {
if (events[i] === listener) {
events.splice(i, 1);
i--;
}
}
}
}
class Image {
constructor() {
this.currentSrc = null
this.naturalHeight = 0
this.naturalWidth = 0
this.width = 0
this.height = 0
this.tagName = 'IMG'
}
set src(src) {
this.currentSrc = src
uni.getImageInfo({
src,
success: (res) => {
this.naturalWidth = this.width = res.width
this.naturalHeight = this.height = res.height
this.onload()
},
fail: () => {
this.onerror()
}
})
}
get src() {
return this.currentSrc
}
}
class OffscreenCanvas {
constructor(ctx, com, canvasId) {
this.tagName = 'canvas'
this.com = com
this.canvasId = canvasId
this.ctx = ctx
}
set width(w) {
this.com.offscreenWidth = w
}
set height(h) {
this.com.offscreenHeight = h
}
get width() {
return this.com.offscreenWidth || 0
}
get height() {
return this.com.offscreenHeight || 0
}
getContext(type) {
return this.ctx
}
getImageData() {
return new Promise((resolve, reject) => {
this.com.$nextTick(() => {
uni.canvasGetImageData({
x:0,
y:0,
width: this.com.offscreenWidth,
height: this.com.offscreenHeight,
canvasId: this.canvasId,
success: (res) => {
resolve(res)
},
fail: (err) => {
reject(err)
},
}, this.com)
})
})
}
}
export class Canvas {
constructor(ctx, com, isNew, canvasNode={}) {
cacheChart[com.canvasId] = {ctx}
this.canvasId = com.canvasId;
this.chart = null;
this.isNew = isNew
this.tagName = 'canvas'
this.canvasNode = canvasNode;
this.com = com;
if (!isNew) {
this._initStyle(ctx)
}
this._initEvent();
this._ee = new EventEmit()
}
getContext(type) {
if (type === '2d') {
return this.ctx;
}
}
setAttribute(key, value) {
if(key === 'aria-label') {
this.com['ariaLabel'] = value
}
}
setChart(chart) {
this.chart = chart;
}
createOffscreenCanvas(param){
if(!this.children) {
this.com.isOffscreenCanvas = true
this.com.offscreenWidth = param.width||300
this.com.offscreenHeight = param.height||300
const com = this.com
const canvasId = this.com.offscreenCanvasId
const context = uni.createCanvasContext(canvasId, this.com)
this._initStyle(context)
this.children = new OffscreenCanvas(context, com, canvasId)
}
return this.children
}
appendChild(child) {
console.log('child', child)
}
dispatchEvent(type, e) {
if(typeof type == 'object') {
this._ee.emit(type.type, type);
} else {
this._ee.emit(type, e);
}
return true
}
attachEvent() {
}
detachEvent() {
}
addEventListener(type, listener) {
this._ee.on(type, listener)
}
removeEventListener(type, listener) {
this._ee.off(type, listener)
}
_initCanvas(zrender, ctx) {
// zrender.util.getContext = function() {
// return ctx;
// };
// zrender.util.$override('measureText', function(text, font) {
// ctx.font = font || '12px sans-serif';
// return ctx.measureText(text, font);
// });
}
_initStyle(ctx, child) {
const styles = [
'fillStyle',
'strokeStyle',
'fontSize',
'globalAlpha',
'opacity',
'textAlign',
'textBaseline',
'shadow',
'lineWidth',
'lineCap',
'lineJoin',
'lineDash',
'miterLimit',
// #ifdef H5
'font',
// #endif
];
const colorReg = /#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])\b/g;
styles.forEach(style => {
Object.defineProperty(ctx, style, {
set: value => {
// #ifdef H5
if (style === 'font' && fontSizeReg.test(value)) {
const match = fontSizeReg.exec(value);
ctx.setFontSize(match[1]);
return;
}
// #endif
if (style === 'opacity') {
ctx.setGlobalAlpha(value)
return;
}
if (style !== 'fillStyle' && style !== 'strokeStyle' || value !== 'none' && value !== null) {
// #ifdef H5 || APP-PLUS || MP-BAIDU
if(typeof value == 'object') {
if (value.hasOwnProperty('colorStop') || value.hasOwnProperty('colors')) {
ctx['set' + style.charAt(0).toUpperCase() + style.slice(1)](value);
}
return
}
// #endif
// #ifdef MP-TOUTIAO
if(colorReg.test(value)) {
value = value.replace(colorReg, '#$1$1$2$2$3$3')
}
// #endif
ctx['set' + style.charAt(0).toUpperCase() + style.slice(1)](value);
}
}
});
});
if(!this.isNew && !child) {
ctx.uniDrawImage = ctx.drawImage
ctx.drawImage = (...a) => {
a[0] = a[0].src
ctx.uniDrawImage(...a)
}
}
if(!ctx.createRadialGradient) {
ctx.createRadialGradient = function() {
return ctx.createCircularGradient(...[...arguments].slice(-3))
};
}
// 字节不支持
if (!ctx.strokeText) {
ctx.strokeText = (...a) => {
ctx.fillText(...a)
}
}
// 钉钉不支持 , 鸿蒙是异步
if (!ctx.measureText || getDeviceInfo().osName == 'harmonyos') {
ctx._measureText = ctx.measureText
const strLen = (str) => {
let len = 0;
for (let i = 0; i < str.length; i++) {
if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
len++;
} else {
len += 2;
}
}
return len;
}
ctx.measureText = (text, font) => {
let fontSize = ctx?.state?.fontSize || 12;
if (font) {
fontSize = parseInt(font.match(/([\d\.]+)px/)[1])
}
fontSize /= 2;
let isBold = fontSize >= 16;
const widthFactor = isBold ? 1.3 : 1;
// ctx._measureText(text, (res) => {})
return {
width: strLen(text) * fontSize * widthFactor
};
}
}
}
_initEvent(e) {
this.event = {};
const eventNames = [{
wxName: 'touchStart',
ecName: 'mousedown'
}, {
wxName: 'touchMove',
ecName: 'mousemove'
}, {
wxName: 'touchEnd',
ecName: 'mouseup'
}, {
wxName: 'touchEnd',
ecName: 'click'
}];
eventNames.forEach(name => {
this.event[name.wxName] = e => {
const touch = e.touches[0];
this.chart.getZr().handler.dispatch(name.ecName, {
zrX: name.wxName === 'tap' ? touch.clientX : touch.x,
zrY: name.wxName === 'tap' ? touch.clientY : touch.y
});
};
});
}
set width(w) {
this.canvasNode.width = w
}
set height(h) {
this.canvasNode.height = h
}
get width() {
return this.canvasNode.width || 0
}
get height() {
return this.canvasNode.height || 0
}
get ctx() {
return cacheChart[this.canvasId]['ctx'] || null
}
set chart(chart) {
cacheChart[this.canvasId]['chart'] = chart
}
get chart() {
return cacheChart[this.canvasId]['chart'] || null
}
}
export function dispatch(name, {x,y, wheelDelta}) {
this.dispatch(name, {
zrX: x,
zrY: y,
zrDelta: wheelDelta,
preventDefault: () => {},
stopPropagation: () =>{}
});
}
export function setCanvasCreator(echarts, {canvas, node}) {
// echarts.setCanvasCreator(() => canvas);
if(echarts && !echarts.registerPreprocessor) {
return console.warn('echarts 版本不对或未传入echartsvue3请使用esm格式')
}
echarts.registerPreprocessor(option => {
if (option && option.series) {
if (option.series.length > 0) {
option.series.forEach(series => {
series.progressive = 0;
});
} else if (typeof option.series === 'object') {
option.series.progressive = 0;
}
}
});
function loadImage(src, onload, onerror) {
let img = null
if(node && node.createImage) {
img = node.createImage()
img.onload = onload.bind(img);
img.onerror = onerror.bind(img);
img.src = src;
return img
} else {
img = new Image()
img.onload = onload.bind(img)
img.onerror = onerror.bind(img);
img.src = src
return img
}
}
if(echarts.setPlatformAPI) {
echarts.setPlatformAPI({
loadImage: canvas.setChart ? loadImage : null,
createCanvas(){
const key = 'createOffscreenCanvas'
return uni.canIUse(key) && uni[key] ? uni[key]({type: '2d'}) : canvas
}
})
}
}

View File

@ -0,0 +1,310 @@
<template>
<!-- #ifdef APP -->
<web-view class="lime-echart" ref="chartRef" @load="loaded" :style="[customStyle]"
:webview-styles="[webviewStyles]" src="/uni_modules/lime-echart/static/uvue.html?v=10112">
</web-view>
<!-- #endif -->
<!-- #ifdef H5 -->
<div class="lime-echart" ref="chartRef"></div>
<!-- #endif -->
<!-- #ifndef H5 || APP-->
<canvas class="lime-echart" :id="canvasid" :canvas-id="canvasid"
@touchstart="touchstart"
@touchmove="touchmove"
@touchend="touchend">
</canvas>
<!-- #endif -->
</template>
<script lang="uts" setup>
// @ts-nocheck
import { getCurrentInstance, nextTick } from "vue";
import { Echarts } from './uvue';
// #ifdef WEB
import { dispatch } from './canvas';
// #endif
// #ifndef APP || WEB
import {Canvas, setCanvasCreator, dispatch} from './canvas';
import {wrapTouch, convertTouchesToArray, devicePixelRatio ,sleep, canIUseCanvas2d, getRect} from './utils';
// #endif
type EchartsResolve = (value : Echarts) => void
defineOptions({
name: 'l-echart'
})
const emits = defineEmits(['finished'])
const props = defineProps({
// #ifdef APP
webviewStyles: {
type: Object
},
customStyle: {
type: Object
},
// #endif
// #ifndef APP
webviewStyles: {
type: Object
},
customStyle: {
type: [String, Object]
},
// #endif
isDisableScroll: {
type: Boolean,
default: false
},
isClickable: {
type: Boolean,
default: true
},
enableHover: {
type: Boolean,
default: false
},
beforeDelay: {
type: Number,
default: 30
}
})
const instance = getCurrentInstance()!;
const canvasid = `lime-echart-${instance.uid}`
const finished = ref(false)
const map = [] as EchartsResolve[]
const callbackMap = [] as EchartsResolve[]
// let context = null as UniWebViewElement | null
let chart = null as Echarts | null
let chartRef = ref<UniWebViewElement | null>(null)
const trigger = () => {
// #ifdef APP
if (finished.value) {
if (chart == null) {
chart = new Echarts(chartRef.value!)
}
while (map.length > 0) {
const resolve = map.pop() as EchartsResolve
resolve(chart!)
}
}
// #endif
// #ifndef APP
while (map.length > 0) {
if(chart != null){
const resolve = map.pop() as EchartsResolve
resolve(chart!)
}
}
// #endif
if(chart != null){
while(callbackMap.length > 0){
const callback = callbackMap.pop() as EchartsResolve
callback(chart!)
}
}
}
// #ifdef APP
const loaded = (event : UniWebViewLoadEvent) => {
event.stopPropagation()
event.preventDefault()
finished.value = true
trigger()
emits('finished')
}
// #endif
const _next = () : boolean => {
if (chart == null) {
console.warn(`组件还未初始化,请先使用 init`)
return true
}
return false
}
const setOption = (option : UTSJSONObject) => {
if (_next()) return
chart!.setOption(option);
}
const showLoading = () => {
if (_next()) return
chart!.showLoading();
}
const hideLoading = () => {
if (_next()) return
chart!.hideLoading();
}
const clear = () => {
if (_next()) return
chart!.clear();
}
const dispose = () => {
if (_next()) return
chart!.dispose();
}
const resize = (size : UTSJSONObject) => {
if (_next()) return
chart!.resize(size);
}
const canvasToTempFilePath = (opt : UTSJSONObject) => {
if (_next()) return
chart!.canvasToTempFilePath(opt);
}
// #ifdef APP
function init(callback : ((chart : Echarts) => void) | null) : Promise<Echarts> {
if(callback!=null){
callbackMap.push(callback)
}
return new Promise<Echarts>((resolve) => {
map.push(resolve)
trigger()
})
}
// #endif
// #ifndef APP
// #ifndef WEB
let use2dCanvas = canIUseCanvas2d()
const getContext = async () =>{
return getRect(`#${canvasid}`, {context: instance.proxy!, type: use2dCanvas ? 'fields': 'boundingClientRect'}).then(res => {
if(res) {
let dpr = uni.getWindowInfo().pixelRatio
let {width, height, node} = res
let canvas;
if(node) {
const ctx = node.getContext('2d');
canvas = new Canvas(ctx, instance.proxy, true, node);
} else {
const ctx = uni.createCanvasContext(canvasid, instance.proxy);
canvas = new Canvas(ctx, instance.proxy, false);
}
return { canvas, width, height, devicePixelRatio: dpr, node };
} else {
return {}
}
})
}
// #endif
const getTouch = (e) => {
const touches = e.touches[0]
// #ifdef WEB
const rect = chart!.getZr().dom.getBoundingClientRect();
const touch = {
x: touches.clientX - rect.left,
y: touches.clientY - rect.top
}
// #endif
// #ifndef WEB
const touch = {
x: touches.x,
y: touches.y
}
// #endif
return touch
}
const touchstart = (e) => {
if(chart == null) return
const handler = chart.getZr().handler;
const touch = getTouch(e)
dispatch.call(handler, 'mousedown', touch)
dispatch.call(handler, 'click', touch)
}
const touchmove = (e) => {
if(chart == null) return
const handler = chart.getZr().handler;
const touch = getTouch(e)
dispatch.call(handler, 'mousemove', touch)
// const rect = chart.getZr().dom.getBoundingClientRect()
// handler.dispatch('mousemove', {
// zrX: e.touches[0].clientX - rect.left,
// zrY: e.touches[0].clientY - rect.top
// })
}
const touchend = (e) => {
if(chart == null) return
const handler = chart.getZr().handler;
const touch = {
x: 999999999,
y: 999999999
}
dispatch.call(handler, 'mousemove', touch)
dispatch.call(handler, 'touchend', touch)
}
async function init(echarts: any, ...args: any[]): Promise<Echarts>{
if(echarts == null){
console.error('请确保已经引入了 ECharts 库');
return Promise.reject('请确保已经引入了 ECharts 库');
}
let theme:string|null=null
let opts={}
let callback:Function|null=null;
args.forEach(item =>{
if(typeof item === 'function') {
callback = item
} else if(['string'].includes(typeof item)){
theme = item
} else if(typeof item === 'object'){
opts = item
}
})
// #ifdef WEB
chart = echarts.init(chartRef.value, theme, opts)
window.addEventListener('touchstart', touchstart)
window.addEventListener('touchmove', touchmove)
window.addEventListener('touchend', touchend)
// #endif
// #ifndef WEB
let config = await getContext();
setCanvasCreator(echarts, config)
chart = echarts.init(config.canvas, theme, Object.assign({}, config, opts))
// #endif
console.log('chart', chart)
if(callback!=null && typeof callback == 'function'){
callbackMap.push(callback)
}
return new Promise<Echarts>((resolve) => {
map.push(resolve)
trigger()
})
}
onMounted(()=>{
nextTick(()=>{
finished.value = true
trigger()
emits('finished')
})
})
onUnmounted(()=>{
// #ifdef WEB
window.removeEventListener('touchstart', touchstart)
window.removeEventListener('touchmove', touchmove)
window.removeEventListener('touchend', touchend)
// #endif
})
// #endif
defineExpose({
init,
setOption,
showLoading,
hideLoading,
clear,
dispose,
resize,
canvasToTempFilePath
})
</script>
<style lang="scss">
.lime-echart {
flex: 1;
width: 100%;
}
</style>

View File

@ -0,0 +1,530 @@
<template>
<view class="lime-echart" :style="[customStyle]" v-if="canvasId" ref="limeEchart" :aria-label="ariaLabel">
<!-- #ifndef APP-NVUE -->
<canvas
class="lime-echart__canvas"
v-if="use2dCanvas"
type="2d"
:id="canvasId"
:style="canvasStyle"
:disable-scroll="isDisableScroll"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
/>
<canvas
class="lime-echart__canvas"
v-else
:width="nodeWidth"
:height="nodeHeight"
:style="canvasStyle"
:canvas-id="canvasId"
:id="canvasId"
:disable-scroll="isDisableScroll"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
/>
<view class="lime-echart__mask"
v-if="isPC"
@mousedown="touchStart"
@mousemove="touchMove"
@mouseup="touchEnd"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd">
</view>
<canvas v-if="isOffscreenCanvas" :style="offscreenStyle" :canvas-id="offscreenCanvasId"></canvas>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<web-view
class="lime-echart__canvas"
:id="canvasId"
:style="canvasStyle"
:webview-styles="webviewStyles"
ref="webview"
src="/uni_modules/lime-echart/static/uvue.html?v=1"
@pagefinish="finished = true"
@onPostMessage="onMessage"
></web-view>
<!-- #endif -->
</view>
</template>
<script>
// @ts-nocheck
// #ifndef APP-NVUE
import {Canvas, setCanvasCreator, dispatch} from './canvas';
import {wrapTouch, convertTouchesToArray, devicePixelRatio ,sleep, canIUseCanvas2d, getRect, getDeviceInfo} from './utils';
// #endif
// #ifdef APP-NVUE
import { base64ToPath, sleep } from './utils';
import {Echarts} from './nvue'
// #endif
const charts = {}
const echartsObj = {}
/**
* LimeChart 图表
* @description 全端兼容的eCharts
* @tutorial https://ext.dcloud.net.cn/plugin?id=4899
* @property {String} customStyle 自定义样式
* @property {String} type 指定 canvas 类型
* @value 2d 使用canvas 2d部分小程序支持
* @value '' 使用原生canvas会有层级问题
* @value bottom right 不缩放图片只显示图片的右下边区域
* @property {Boolean} isDisableScroll
* @property {number} beforeDelay = [30] 延迟初始化 (毫秒)
* @property {Boolean} enableHover PC端使用鼠标悬浮
* @event {Function} finished 加载完成触发
*/
export default {
name: 'lime-echart',
props: {
// #ifdef MP-WEIXIN || MP-TOUTIAO
type: {
type: String,
default: '2d'
},
// #endif
// #ifdef APP-NVUE
webviewStyles: Object,
// hybrid: Boolean,
// #endif
customStyle: String,
isDisableScroll: Boolean,
isClickable: {
type: Boolean,
default: true
},
enableHover: Boolean,
beforeDelay: {
type: Number,
default: 30
},
landscape: Boolean
},
data() {
return {
// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
use2dCanvas: true,
// #endif
// #ifndef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
use2dCanvas: false,
// #endif
ariaLabel: '图表',
width: null,
height: null,
nodeWidth: null,
nodeHeight: null,
// canvasNode: null,
config: {},
inited: false,
finished: false,
file: '',
platform: '',
isPC: false,
isDown: false,
isOffscreenCanvas: false,
offscreenWidth: 0,
offscreenHeight: 0,
};
},
computed: {
rootStyle() {
if(this.landscape) {
return `transform: translate(-50%,-50%) rotate(90deg); top:50%; left:50%;`
}
},
canvasId() {
return `lime-echart${this._ && this._.uid || this._uid}`
},
offscreenCanvasId() {
return `${this.canvasId}_offscreen`
},
offscreenStyle() {
return `width:${this.offscreenWidth}px;height: ${this.offscreenHeight}px; position: fixed; left: 99999px; background: red`
},
canvasStyle() {
return this.rootStyle + (this.width && this.height ? ('width:' + this.width + 'px;height:' + this.height + 'px') : '')
}
},
// #ifndef VUE3
beforeDestroy() {
this.clear()
this.dispose()
// #ifdef H5
if(this.isPC) {
document.removeEventListener('mousewheel', this.mousewheel)
}
// #endif
},
// #endif
// #ifdef VUE3
beforeUnmount() {
this.clear()
this.dispose()
// #ifdef H5
if(this.isPC) {
document.removeEventListener('mousewheel', this.mousewheel)
}
// #endif
},
// #endif
created() {
// #ifdef H5
if(!('ontouchstart' in window)) {
this.isPC = true
document.addEventListener('mousewheel', this.mousewheel)
}
// #endif
// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
// const { platform } = uni.getSystemInfoSync();
const { platform } = getDeviceInfo();
this.isPC = /windows/i.test(platform)
// #endif
this.use2dCanvas = this.type === '2d' && canIUseCanvas2d()
},
mounted() {
this.$nextTick(() => {
this.$emit('finished')
})
},
methods: {
// #ifdef APP-NVUE
onMessage(e) {
const detail = e?.detail?.data[0] || null;
const data = detail?.data
const key = detail?.event
const options = data?.options
const event = data?.event
const file = detail?.file
if (key == 'log' && data) {
console.log(data)
}
if(event) {
this.chart.dispatchAction(event.replace(/"/g,''), options)
}
if(file) {
thie.file = file
}
},
// #endif
setChart(callback) {
if(!this.chart) {
console.warn(`组件还未初始化,请先使用 init`)
return
}
if(typeof callback === 'function' && this.chart) {
callback(this.chart);
}
// #ifdef APP-NVUE
if(typeof callback === 'function') {
this.$refs.webview.evalJs(`setChart(${JSON.stringify(callback.toString())}, ${JSON.stringify(this.chart.options)})`);
}
// #endif
},
setOption() {
if (!this.chart || !this.chart.setOption) {
console.warn(`组件还未初始化,请先使用 init`)
return
}
this.chart.setOption(...arguments);
},
showLoading() {
if(this.chart) {
this.chart.showLoading(...arguments)
}
},
hideLoading() {
if(this.chart) {
this.chart.hideLoading()
}
},
clear() {
if(this.chart) {
this.chart.clear()
}
},
dispose() {
if(this.chart) {
this.chart.dispose()
}
},
resize(size) {
if(size && size.width && size.height) {
this.height = size.height
this.width = size.width
if(this.chart) {this.chart.resize(size)}
} else {
this.$nextTick(() => {
uni.createSelectorQuery()
.in(this)
.select(`.lime-echart`)
.boundingClientRect()
.exec(res => {
if (res) {
let { width, height } = res[0];
this.width = width = width || 300;
this.height = height = height || 300;
this.chart.resize({width, height})
}
});
})
}
},
canvasToTempFilePath(args = {}) {
// #ifndef APP-NVUE
const { use2dCanvas, canvasId } = this;
return new Promise((resolve, reject) => {
const copyArgs = Object.assign({
canvasId,
success: resolve,
fail: reject
}, args);
if (use2dCanvas) {
delete copyArgs.canvasId;
copyArgs.canvas = this.canvasNode;
}
uni.canvasToTempFilePath(copyArgs, this);
});
// #endif
// #ifdef APP-NVUE
this.file = ''
this.$refs.webview.evalJs(`canvasToTempFilePath()`);
return new Promise((resolve, reject) => {
this.$watch('file', async (file) => {
if(file) {
const tempFilePath = await base64ToPath(file)
resolve(args.success({tempFilePath}))
} else {
reject(args.fail({error: ``}))
}
})
})
// #endif
},
async init(echarts, ...args) {
// #ifndef APP-NVUE
if(args && args.length == 0 && !echarts) {
console.error('缺少参数init(echarts, theme?:string, opts?: object, callback?: function)')
return
}
// #endif
let theme=null,opts={},callback;
Array.from(arguments).forEach(item => {
if(typeof item === 'function') {
callback = item
}
if(['string'].includes(typeof item)) {
theme = item
}
if(typeof item === 'object') {
opts = item
}
})
if(this.beforeDelay) {
await sleep(this.beforeDelay)
}
let config = await this.getContext();
// #ifndef APP-NVUE
setCanvasCreator(echarts, config)
try {
this.chart = echarts.init(config.canvas, theme, Object.assign({}, config, opts))
if(typeof callback === 'function') {
callback(this.chart)
} else {
return this.chart
}
} catch(e) {
console.error(e.messges)
return null
}
// #endif
// #ifdef APP-NVUE
this.chart = new Echarts(this.$refs.webview)
this.$refs.webview.evalJs(`init(null, null, ${JSON.stringify(opts)}, ${theme})`)
if(callback) {
callback(this.chart)
} else {
return this.chart
}
// #endif
},
getContext() {
// #ifdef APP-NVUE
if(this.finished) {
return Promise.resolve(this.finished)
}
return new Promise(resolve => {
this.$watch('finished', (val) => {
if(val) {
resolve(this.finished)
}
})
})
// #endif
// #ifndef APP-NVUE
return getRect(`#${this.canvasId}`, {context: this, type: this.use2dCanvas ? 'fields': 'boundingClientRect'}).then(res => {
if(res) {
let dpr = devicePixelRatio
let {width, height, node} = res
let canvas;
this.width = width = width || 300;
this.height = height = height || 300;
if(node) {
const ctx = node.getContext('2d');
canvas = new Canvas(ctx, this, true, node);
this.canvasNode = node
} else {
// #ifdef MP-TOUTIAO
dpr = !this.isPC ? devicePixelRatio : 1// 1.25
// #endif
// #ifndef MP-ALIPAY || MP-TOUTIAO
dpr = this.isPC ? devicePixelRatio : 1
// #endif
// #ifdef MP-ALIPAY || MP-LARK
dpr = devicePixelRatio
// #endif
// #ifdef WEB
dpr = 1
// #endif
this.rect = res
this.nodeWidth = width * dpr;
this.nodeHeight = height * dpr;
const ctx = uni.createCanvasContext(this.canvasId, this);
canvas = new Canvas(ctx, this, false);
}
return { canvas, width, height, devicePixelRatio: dpr, node };
} else {
return {}
}
})
// #endif
},
// #ifndef APP-NVUE
getRelative(e, touches) {
let { clientX, clientY } = e
if(!(clientX && clientY) && touches && touches[0]) {
clientX = touches[0].clientX
clientY = touches[0].clientY
}
return {x: clientX - this.rect.left, y: clientY - this.rect.top, wheelDelta: e.wheelDelta || 0}
},
getTouch(e, touches) {
const {x} = touches && touches[0] || {}
const touch = x ? touches[0] : this.getRelative(e, touches);
if(this.landscape) {
[touch.x, touch.y] = [touch.y, this.height - touch.x]
}
return touch;
},
touchStart(e) {
this.isDown = true
const next = () => {
const touches = convertTouchesToArray(e.touches)
if(this.chart) {
const touch = this.getTouch(e, touches)
this.startX = touch.x
this.startY = touch.y
this.startT = new Date()
const handler = this.chart.getZr().handler;
dispatch.call(handler, 'mousedown', touch)
dispatch.call(handler, 'mousemove', touch)
handler.processGesture(wrapTouch(e), 'start');
clearTimeout(this.endTimer);
}
}
if(this.isPC) {
getRect(`#${this.canvasId}`, {context: this}).then(res => {
this.rect = res
next()
})
return
}
next()
},
touchMove(e) {
if(this.isPC && this.enableHover && !this.isDown) {this.isDown = true}
const touches = convertTouchesToArray(e.touches)
if (this.chart && this.isDown) {
const handler = this.chart.getZr().handler;
dispatch.call(handler, 'mousemove', this.getTouch(e, touches))
handler.processGesture(wrapTouch(e), 'change');
}
},
touchEnd(e) {
this.isDown = false
if (this.chart) {
const touches = convertTouchesToArray(e.changedTouches)
const {x} = touches && touches[0] || {}
const touch = (x ? touches[0] : this.getRelative(e, touches)) || {};
if(this.landscape) {
[touch.x, touch.y] = [touch.y, this.height - touch.x]
}
const handler = this.chart.getZr().handler;
const isClick = Math.abs(touch.x - this.startX) < 10 && new Date() - this.startT < 200;
dispatch.call(handler, 'mouseup', touch)
handler.processGesture(wrapTouch(e), 'end');
if(isClick) {
dispatch.call(handler, 'click', touch)
} else {
this.endTimer = setTimeout(() => {
dispatch.call(handler, 'mousemove', {x: 999999999,y: 999999999});
dispatch.call(handler, 'mouseup', {x: 999999999,y: 999999999});
},50)
}
}
},
// #endif
// #ifdef H5
mousewheel(e){
if(this.chart) {
// dispatch.call(this.chart.getZr().handler, 'mousewheel', this.getTouch(e))
}
}
// #endif
}
};
</script>
<style>
.lime-echart {
position: relative;
/* #ifndef APP-NVUE */
width: 100%;
height: 100%;
/* #endif */
/* #ifdef APP-NVUE */
flex: 1;
/* #endif */
}
.lime-echart__canvas {
/* #ifndef APP-NVUE */
width: 100%;
height: 100%;
/* #endif */
/* #ifdef APP-NVUE */
flex: 1;
/* #endif */
}
/* #ifndef APP-NVUE */
.lime-echart__mask {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 1;
}
/* #endif */
</style>

View File

@ -0,0 +1,51 @@
export class Echarts {
eventMap = new Map()
constructor(webview) {
this.webview = webview
this.options = null
}
setOption() {
this.options = arguments
this.webview.evalJs(`setOption(${JSON.stringify(arguments)})`);
}
getOption() {
return this.options
}
showLoading() {
this.webview.evalJs(`showLoading(${JSON.stringify(arguments)})`);
}
hideLoading() {
this.webview.evalJs(`hideLoading()`);
}
clear() {
this.webview.evalJs(`clear()`);
}
dispose() {
this.webview.evalJs(`dispose()`);
}
resize(size) {
if(size) {
this.webview.evalJs(`resize(${JSON.stringify(size)})`);
} else {
this.webview.evalJs(`resize()`);
}
}
on(type, ...args) {
const query = args[0]
const useQuery = query && typeof query != 'function'
const param = useQuery ? [type, query] : [type]
const key = `${type}${useQuery ? JSON.stringify(query): '' }`
const callback = useQuery ? args[1]: args[0]
if(typeof callback == 'function'){
this.eventMap.set(key, callback)
}
this.webview.evalJs(`on(${JSON.stringify(param)})`);
console.warn('nvue 暂不支持事件')
}
dispatchAction(type, options){
const handler = this.eventMap.get(type)
if(handler){
handler(options)
}
}
}

View File

@ -0,0 +1,190 @@
// @ts-nocheck
/**
* 获取设备基础信息
*
* @see [uni.getDeviceInfo](https://uniapp.dcloud.net.cn/api/system/getDeviceInfo.html)
*/
export function getDeviceInfo() {
if (uni.getDeviceInfo && uni.canIUse('getDeviceInfo')) {
return uni.getDeviceInfo();
} else {
return uni.getSystemInfoSync();
}
}
/**
* 获取窗口信息
*
* @see [uni.getWindowInfo](https://uniapp.dcloud.net.cn/api/system/getWindowInfo.html)
*/
export function getWindowInfo() {
if (uni.getWindowInfo && uni.canIUse('getWindowInfo')) {
return uni.getWindowInfo();
} else {
return uni.getSystemInfoSync();
}
}
/**
* 获取APP基础信息
*
* @see [uni.getAppBaseInfo](https://uniapp.dcloud.net.cn/api/system/getAppBaseInfo.html)
*/
export function getAppBaseInfo() {
if (uni.getAppBaseInfo && uni.canIUse('getAppBaseInfo')) {
return uni.getAppBaseInfo();
} else {
return uni.getSystemInfoSync();
}
}
// #ifndef APP-NVUE
// 计算版本
export function compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
// const systemInfo = uni.getSystemInfoSync();
function gte(version) {
// 截止 2023-03-22 mac pc小程序不支持 canvas 2d
// let {
// SDKVersion,
// platform
// } = systemInfo;
const { platform } = getDeviceInfo();
let { SDKVersion } = getAppBaseInfo();
// #ifdef MP-ALIPAY
SDKVersion = my.SDKVersion
// #endif
// #ifdef MP-WEIXIN
return platform !== 'mac' && compareVersion(SDKVersion, version) >= 0;
// #endif
return compareVersion(SDKVersion, version) >= 0;
}
export function canIUseCanvas2d() {
// #ifdef MP-WEIXIN
return gte('2.9.0');
// #endif
// #ifdef MP-ALIPAY
return gte('2.7.0');
// #endif
// #ifdef MP-TOUTIAO
return gte('1.78.0');
// #endif
return false
}
export function convertTouchesToArray(touches) {
// 如果 touches 是一个数组,则直接返回它
if (Array.isArray(touches)) {
return touches;
}
// 如果touches是一个对象则转换为数组
if (typeof touches === 'object' && touches !== null) {
return Object.values(touches);
}
// 对于其他类型,直接返回它
return touches;
}
export function wrapTouch(event) {
event.touches = convertTouchesToArray(event.touches)
for (let i = 0; i < event.touches.length; ++i) {
const touch = event.touches[i];
touch.offsetX = touch.x;
touch.offsetY = touch.y;
}
return event;
}
// export const devicePixelRatio = uni.getSystemInfoSync().pixelRatio
export const devicePixelRatio = getWindowInfo().pixelRatio;
// #endif
// #ifdef APP-NVUE
export function base64ToPath(base64) {
return new Promise((resolve, reject) => {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
bitmap.loadBase64Data(base64, () => {
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
const time = new Date().getTime();
const filePath = `_doc/uniapp_temp/${time}.${format}`
bitmap.save(filePath, {},
() => {
bitmap.clear()
resolve(filePath)
},
(error) => {
bitmap.clear()
console.error(`${JSON.stringify(error)}`)
reject(error)
})
}, (error) => {
bitmap.clear()
console.error(`${JSON.stringify(error)}`)
reject(error)
})
})
}
// #endif
export function sleep(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
}, time)
})
}
export function getRect(selector, options = {}) {
const typeDefault = 'boundingClientRect'
const {
context,
type = typeDefault
} = options
return new Promise((resolve, reject) => {
const dom = uni.createSelectorQuery().in(context).select(selector);
const result = (rect) => {
if (rect) {
resolve(rect)
} else {
reject()
}
}
if (type == typeDefault) {
dom[type](result).exec()
} else {
dom[type]({
node: true,
size: true,
rect: true
}, result).exec()
}
});
};

View File

@ -0,0 +1,133 @@
// @ts-nocheck
// #ifdef APP
type EchartsEventHandler = (event: UTSJSONObject)=>void
// type EchartsTempResolve = (obj : UTSJSONObject) => void
// type EchartsTempOptions = UTSJSONObject
export class Echarts {
options: UTSJSONObject = {} as UTSJSONObject
context: UniWebViewElement
eventMap: Map<string, EchartsEventHandler> = new Map()
private temp: UTSJSONObject[] = []
constructor(context: UniWebViewElement){
this.context = context
this.init()
}
init(){
this.context.evalJS(`init(null, null, ${JSON.stringify({})})`)
this.context.addEventListener('message', (e : UniWebViewMessageEvent) => {
// event.stopPropagation()
// event.preventDefault()
const detail = e.detail.data[0]
const file = detail.getString('file')
const data = detail.get('data')
const key = detail.getString('event')
const options = typeof data == 'object' ? (data as UTSJSONObject).getJSON('options'): null
const event = typeof data == 'object' ? (data as UTSJSONObject).getString('event'): null
if (key == 'log' && data != null) {
console.log(data)
}
if (event != null && options != null) {
this.dispatchAction(event.replace(/"/g,''), options)
}
if(file != null){
while (this.temp.length > 0) {
const opt = this.temp.pop()
const success = opt?.get('success')
if(typeof success == 'function'){
success as (res: UTSJSONObject) => void
success({tempFilePath: file})
}
}
}
})
}
setOption(option: UTSJSONObject){
this.options = option;
this.context.evalJS(`setOption(${JSON.stringify([option])})`)
}
setOption(option: UTSJSONObject, notMerge: boolean = false, lazyUpdate: boolean = false){
this.options = option;
this.context.evalJS(`setOption(${JSON.stringify([option, notMerge, lazyUpdate])})`)
}
setOption(option: UTSJSONObject, notMerge: UTSJSONObject){
this.options = option;
this.context.evalJS(`setOption(${JSON.stringify([option, notMerge])})`)
}
getOption(): UTSJSONObject {
return this.options
}
showLoading(){
this.context.evalJS(`showLoading(${JSON.stringify([] as any[])})`);
}
showLoading(type: string, opts: UTSJSONObject){
this.context.evalJS(`showLoading(${JSON.stringify([type, opts])})`);
}
hideLoading(){
this.context.evalJS(`hideLoading()`);
}
clear(){
this.context.evalJS(`clear()`);
}
dispose(){
this.context.evalJS(`dispose()`);
}
resize(size:UTSJSONObject){
setTimeout(()=>{
this.context.evalJS(`resize(${JSON.stringify(size)})`);
},0)
}
resize(){
setTimeout(()=>{
this.context.evalJS(`resize()`);
},10)
}
on(type:string, query: any, callback: EchartsEventHandler) {
const key = `${type}${JSON.stringify(query)}`
if(typeof callback == 'function'){
this.eventMap.set(key, callback)
}
this.context.evalJS(`on(${JSON.stringify([type, query])})`);
console.warn('uvue 暂不支持事件')
}
on(type:string, callback: EchartsEventHandler) {
const key = `${type}`
if(typeof callback == 'function'){
this.eventMap.set(key, callback)
}
this.context.evalJS(`on(${JSON.stringify([type])})`);
console.warn('uvue 暂不支持事件')
}
dispatchAction(type:string, options: UTSJSONObject){
const handler = this.eventMap.get(type)
if(handler!=null){
handler(options)
}
}
canvasToTempFilePath(opt: UTSJSONObject){
// this.context.evalJS(`on(${JSON.stringify(opt)})`);
this.context.evalJS(`canvasToTempFilePath(${JSON.stringify(opt)})`);
this.temp.push(opt)
}
}
// #endif
// #ifndef APP
export class Echarts {
constructor() {}
setOption(option: UTSJSONObject): void
isDisposed(): boolean;
clear(): void;
resize(size:UTSJSONObject): void;
resize(): void;
canvasToTempFilePath(opt : UTSJSONObject): void;
dispose(): void;
showLoading(cfg?: UTSJSONObject): void;
showLoading(name?: string, cfg?: UTSJSONObject): void;
hideLoading(): void;
getZr(): any
}
// #endif

View File

@ -0,0 +1,159 @@
<template>
<view style="width: 100%; height: 408px;">
<l-echart ref="chartRef" @finished="init"></l-echart>
</view>
</template>
<script>
export default {
data() {
return {
showTip: false,
option: {
tooltip: {
trigger: 'axis',
// shadowBlur: 0,
textStyle: {
textShadowBlur: 0
},
renderMode: 'richText',
},
legend: {
data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [
{
name: '邮件营销',
type: 'line',
stack: '总量',
data: [120, 132, 101, 134, 90, 230, 210]
},
{
name: '联盟广告',
type: 'line',
stack: '总量',
data: [220, 182, 191, 234, 290, 330, 310]
},
{
name: '视频广告',
type: 'line',
stack: '总量',
data: [150, 232, 201, 154, 190, 330, 410]
},
{
name: '直接访问',
type: 'line',
stack: '总量',
data: [320, 332, 301, 334, 390, 330, 320]
},
{
name: '搜索引擎',
type: 'line',
stack: '总量',
data: [820, 932, 901, 934, 1290, 1330, 1320]
}
]
}
}
},
mounted() {
console.log('lime echarts nvue')
},
methods: {
init() {
const chartRef = this.$refs['chartRef']
chartRef.init(chart => {
chart.setOption(this.option);
setTimeout(()=>{
const option = {
tooltip: {
trigger: 'axis',
// shadowBlur: 0,
textStyle: {
textShadowBlur: 0
},
renderMode: 'richText',
},
legend: {
data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [
{
name: '邮件营销',
type: 'line',
stack: '总量',
data: [120, 132, 101, 134, 90, 230, 210]
},
{
name: '联盟广告',
type: 'line',
stack: '总量',
data: [220, 182, 191, 234, 290, 330, 310]
},
{
name: '视频广告',
type: 'line',
stack: '总量',
data: [150, 232, 201, 154, 190, 330, 410]
},
{
name: '直接访问',
type: 'line',
stack: '总量',
data: [320, 332, 301, 334, 390, 330, 320]
},
{
name: '搜索引擎',
type: 'line',
stack: '总量',
data: [820, 932, 901, 934, 1290, 1330, 1320]
}
]
}
chart.setOption(option);
},1000)
})
},
save() {
// this.$refs.chart.canvasToTempFilePath({
// success(res) {
// console.log('res::::', res)
// }
// })
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,160 @@
<template>
<view style="width: 100%; height: 408px;">
<l-echart ref="chartRef" @finished="init"></l-echart>
</view>
</template>
<script lang="uts" setup>
// @ts-nocheck
// #ifndef APP
import * as echarts from 'echarts/dist/echarts.esm.js'
// #endif
const chartRef = ref<LEchartComponentPublicInstance|null>(null)
const option = {
tooltip: {
trigger: 'axis',
// shadowBlur: 0,
textStyle: {
textShadowBlur: 0
},
renderMode: 'richText',
},
// formatter: async (params: any) => {
// console.log('params', params)
// return 1
// },
legend: {
data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [
{
name: '邮件营销',
type: 'line',
stack: '总量',
data: [120, 132, 101, 134, 90, 230, 210]
},
{
name: '联盟广告',
type: 'line',
stack: '总量',
data: [220, 182, 191, 234, 290, 330, 310]
},
{
name: '视频广告',
type: 'line',
stack: '总量',
data: [150, 232, 201, 154, 190, 330, 410]
},
{
name: '直接访问',
type: 'line',
stack: '总量',
data: [320, 332, 301, 334, 390, 330, 320]
},
{
name: '搜索引擎',
type: 'line',
stack: '总量',
data: [820, 932, 901, 934, 1290, 1330, 1320]
}
]
}
const init = async () =>{
if(chartRef.value== null) return
// #ifdef APP
const chart = await chartRef.value!.init(null)
// #endif
// #ifndef APP
const chart = await chartRef.value!.init(echarts, null)
// #endif
chart.setOption(option)
chart.on('mouseover', function (params) {
console.log('params', params);
});
// setTimeout(()=> {
// const option1 = {
// tooltip: {
// trigger: 'axis',
// // shadowBlur: 0,
// textStyle: {
// textShadowBlur: 0
// },
// renderMode: 'richText',
// },
// legend: {
// data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎']
// },
// grid: {
// left: '3%',
// right: '4%',
// bottom: '3%',
// containLabel: true
// },
// xAxis: {
// type: 'category',
// boundaryGap: false,
// data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
// },
// yAxis: {
// type: 'value'
// },
// series: [
// {
// name: '邮件营销',
// type: 'line',
// stack: '总量',
// data: [820, 132, 101, 134, 90, 230, 210]
// },
// {
// name: '联盟广告',
// type: 'line',
// stack: '总量',
// data: [220, 182, 191, 234, 290, 330, 310]
// },
// {
// name: '视频广告',
// type: 'line',
// stack: '总量',
// data: [950, 232, 201, 154, 190, 330, 410]
// },
// {
// name: '直接访问',
// type: 'line',
// stack: '总量',
// data: [320, 332, 301, 334, 390, 330, 320]
// },
// {
// name: '搜索引擎',
// type: 'line',
// stack: '总量',
// data: [820, 932, 901, 934, 1290, 1330, 1320]
// }
// ]
// }
// chart.setOption(option1)
// },1000)
}
</script>
<style>
</style>

View File

@ -0,0 +1,227 @@
<template>
<view>
<view style="height: 750rpx; position: relative">
<l-echart ref="chart" @finished="init"></l-echart>
<view
class="customTooltips"
:style="{ left: position[0] + 'px', top: position[1] + 'px' }"
v-if="params.length && position.length && showTip"
>
<view>这是个自定的tooltips</view>
<view>{{ params[0]['axisValue'] }}</view>
<view v-for="item in params">
<view>
<text>{{ item.seriesName }}</text>
<text>{{ item.value }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
// nvue
// #ifdef VUE2
import * as echarts from '@/uni_modules/lime-echart/static/echarts.min'
// #endif
// #ifdef VUE3
// #ifdef MP
// vue3 使vite umd使使require
const echarts = require('../../static/echarts.min')
// #endif
// #ifndef MP
// vue3 使vite umdnpm
import * as echarts from 'echarts/dist/echarts.esm'
// #endif
// #endif
export default {
data() {
return {
showTip: false,
position: [],
params: [],
option: {
tooltip: {
trigger: 'axis',
// shadowBlur: 0,
textStyle: {
textShadowBlur: 0,
},
renderMode: 'richText',
position: (point, params, dom, rect, size) => {
// tooltips
const box = [170, 170]
//
const offsetX = point[0] < size.viewSize[0] / 2 ? 20 : -box[0] - 20
const offsetY = point[1] < size.viewSize[1] / 2 ? 20 : -box[1] - 20
const x = point[0] + offsetX
const y = point[1] + offsetY
this.position = [x, y]
this.params = params
},
formatter: (params, ticket, callback) => {},
},
legend: {
data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎'],
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
},
yAxis: {
type: 'value',
},
series: [
{
name: '邮件营销',
type: 'line',
stack: '总量',
data: [120, 132, 101, 134, 90, 230, 210],
},
{
name: '联盟广告',
type: 'line',
stack: '总量',
data: [220, 182, 191, 234, 290, 330, 310],
},
{
name: '视频广告',
type: 'line',
stack: '总量',
data: [150, 232, 201, 154, 190, 330, 410],
},
{
name: '直接访问',
type: 'line',
stack: '总量',
data: [320, 332, 301, 334, 390, 330, 320],
},
{
name: '搜索引擎',
type: 'line',
stack: '总量',
data: [820, 932, 901, 934, 1290, 1330, 1320],
},
],
},
}
},
methods: {
init() {
// init(echarts, theme?:string, opts?:{}, chart => {})
// echarts nvuenvue
// theme 'dark'
// opts = { //
// locale?: string // `5.0.0`
// }
// chart => {} callback
// setTimeout(()=>{
// this.$refs.chart.init(echarts, chart => {
// chart.setOption(this.option);
// });
// },300)
this.$refs.chart.init(echarts, (chart) => {
chart.setOption(this.option)
// tooltip
chart.on('showTip', (params) => {
this.showTip = true
console.log('showTip::')
})
chart.on('hideTip', (params) => {
setTimeout(() => {
this.showTip = false
}, 300)
})
setTimeout(() => {
const option = {
tooltip: {
trigger: 'axis',
// shadowBlur: 0,
textStyle: {
textShadowBlur: 0,
},
renderMode: 'richText',
},
legend: {
data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎'],
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
},
yAxis: {
type: 'value',
},
series: [
{
name: '邮件营销',
type: 'line',
stack: '总量',
data: [1120, 132, 101, 134, 90, 230, 210],
},
{
name: '联盟广告',
type: 'line',
stack: '总量',
data: [220, 182, 191, 234, 290, 330, 310],
},
{
name: '视频广告',
type: 'line',
stack: '总量',
data: [150, 632, 201, 154, 190, 330, 410],
},
{
name: '直接访问',
type: 'line',
stack: '总量',
data: [820, 332, 301, 334, 390, 330, 320],
},
{
name: '搜索引擎',
type: 'line',
stack: '总量',
data: [820, 932, 901, 934, 1290, 1330, 1320],
},
],
}
chart.setOption(option)
}, 1000)
})
},
save() {
this.$refs.chart.canvasToTempFilePath({
success(res) {
console.log('res::::', res)
},
})
},
},
}
</script>
<style>
.customTooltips {
position: absolute;
background-color: rgba(255, 255, 255, 0.8);
padding: 20rpx;
}
</style>

View File

@ -0,0 +1,91 @@
{
"id": "lime-echart",
"displayName": "echarts",
"version": "0.9.8",
"description": "echarts 全端兼容一款使echarts图表能跑在uniapp各端中的插件, 支持uniapp/uniappx(web,ios,安卓)",
"keywords": [
"echarts",
"canvas",
"图表",
"可视化"
],
"repository": "https://gitee.com/liangei/lime-echart",
"engines": {
"HBuilderX": "^3.6.4"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "component-vue"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-uvue": "y",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
},
"dependencies": {
"echarts": "^5.4.1",
"zrender": "^5.4.3"
}
}

View File

@ -0,0 +1,407 @@
# echarts 图表 <span style="font-size:16px;">👑👑👑👑👑 <span style="background:#ff9d00;padding:2px 4px;color:#fff;font-size:10px;border-radius: 3px;">全端</span></span>
> 一个基于 JavaScript 的开源可视化图表库 [查看更多](https://limeui.qcoon.cn/#/echart) <br>
> 基于 echarts 做了兼容处理,更多示例请访问 [uni示例](https://limeui.qcoon.cn/#/echart-example) | [官方示例](https://echarts.apache.org/examples/zh/index.html) <br>
## 平台兼容
| H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ 小程序 | App |
| --- | ---------- | ------------ | ---------- | ---------- | --------- | ---- |
| √ | √ | √ | √ | √ | √ | √ |
## 安装
- 第一步:在市场导入 [百度图表](https://ext.dcloud.net.cn/plugin?id=4899)
- 第二步:选择插件依赖:<br>
1、可以选插件内的`echarts`包或自定义包,自定义包[下载地址](https://echarts.apache.org/zh/builder.html)<br>
2、或者使用`npm`安装`echarts`
**注意**
* 🔔 echarts 5.3.0及以上
* 🔔 如果是 `cli` 项目请下载插件到`src`目录下的`uni_modules`,没有这个目录就创建一个
## 代码演示
### Vue2
- 引入依赖,可以是插件内提供或自己下载的[自定义包](https://echarts.apache.org/zh/builder.html),也可以是`npm`包
```html
<view style="width:750rpx; height:750rpx"><l-echart ref="chartRef" @finished="init"></l-echart></view>
```
```js
// 插件内的 三选一
import * as echarts from '@/uni_modules/lime-echart/static/echarts.min'
// 自定义的 三选一 下载后放入项目的路径
import * as echarts from 'xxx/echarts.min'
// npm包 三选一 需要在控制台 输入命令npm install echarts
import * as echarts from 'echarts'
```
```js
export default {
data() {
return {
option: {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
confine: true
},
legend: {
data: ['热度', '正面', '负面']
},
grid: {
left: 20,
right: 20,
bottom: 15,
top: 40,
containLabel: true
},
xAxis: [
{
type: 'value',
axisLine: {
lineStyle: {
color: '#999999'
}
},
axisLabel: {
color: '#666666'
}
}
],
yAxis: [
{
type: 'category',
axisTick: { show: false },
data: ['汽车之家', '今日头条', '百度贴吧', '一点资讯', '微信', '微博', '知乎'],
axisLine: {
lineStyle: {
color: '#999999'
}
},
axisLabel: {
color: '#666666'
}
}
],
series: [
{
name: '热度',
type: 'bar',
label: {
normal: {
show: true,
position: 'inside'
}
},
data: [300, 270, 340, 344, 300, 320, 310],
},
{
name: '正面',
type: 'bar',
stack: '总量',
label: {
normal: {
show: true
}
},
data: [120, 102, 141, 174, 190, 250, 220]
},
{
name: '负面',
type: 'bar',
stack: '总量',
label: {
normal: {
show: true,
position: 'left'
}
},
data: [-20, -32, -21, -34, -90, -130, -110]
}
]
},
};
},
// 组件能被调用必须是组件的节点已经被渲染到页面上
methods: {
async init() {
// chart 图表实例不能存在data里
const chart = await this.$refs.chartRef.init(echarts);
chart.setOption(this.option)
}
}
}
```
### Vue3
- 小程序可以使用`require`引入插件内提供或自己下载的[自定义包](https://echarts.apache.org/zh/builder.html)
- `require`仅支持相对路径,不支持路径别名
- 非小程序使用 `npm`
```html
<view style="width:750rpx; height:750rpx"><l-echart ref="chartRef"></l-echart></view>
```
```js
// 小程序 二选一
// 插件内的 二选一
const echarts = require('../../uni_modules/lime-echart/static/echarts.min');
// 自定义的 二选一 下载后放入项目的路径
const echarts = require('xxx/xxx/echarts');
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 非小程序
// 需要在控制台 输入命令npm install echarts
import * as echarts from 'echarts'
```
```js
const chartRef = ref(null)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
confine: true
},
legend: {
data: ['热度', '正面', '负面']
},
grid: {
left: 20,
right: 20,
bottom: 15,
top: 40,
containLabel: true
},
xAxis: [
{
type: 'value',
axisLine: {
lineStyle: {
color: '#999999'
}
},
axisLabel: {
color: '#666666'
}
}
],
yAxis: [
{
type: 'category',
axisTick: { show: false },
data: ['汽车之家', '今日头条', '百度贴吧', '一点资讯', '微信', '微博', '知乎'],
axisLine: {
lineStyle: {
color: '#999999'
}
},
axisLabel: {
color: '#666666'
}
}
],
series: [
{
name: '热度',
type: 'bar',
label: {
normal: {
show: true,
position: 'inside'
}
},
data: [300, 270, 340, 344, 300, 320, 310],
},
{
name: '正面',
type: 'bar',
stack: '总量',
label: {
normal: {
show: true
}
},
data: [120, 102, 141, 174, 190, 250, 220]
},
{
name: '负面',
type: 'bar',
stack: '总量',
label: {
normal: {
show: true,
position: 'left'
}
},
data: [-20, -32, -21, -34, -90, -130, -110]
}
]
};
onMounted( ()=>{
// 组件能被调用必须是组件的节点已经被渲染到页面上
setTimeout(async()=>{
if(!chartRef.value) return
const myChart = await chartRef.value.init(echarts)
myChart.setOption(option)
},300)
})
```
### Uvue
- Uvue和Nvue不需要引入`echarts`,因为它们的实现方式是`webview`
- uniapp x需要HBX 4.13以上
```html
<view style="width: 100%; height: 408px;">
<l-echart ref="chartRef" @finished="init"></l-echart>
</view>
```
```js
// @ts-nocheck
// #ifdef H5
import * as echarts from 'echarts/dist/echarts.esm.js'
// #endif
const chartRef = ref<LEchartComponentPublicInstance|null>(null);
const init = async () => {
if(chartRef.value== null) return
// #ifdef APP
const chart = await chartRef.value!.init(null)
// #endif
// #ifdef H5
const chart = await chartRef.value!.init(echarts, null)
// #endif
chart.setOption(option)
}
```
## 数据更新
- 1、使用 `ref` 可获取`setOption`设置更新
- 2、也可以拿到图表实例`chart`设置`myChart.setOption(data)`
```js
// ref
this.$refs.chart.setOption(data)
// 图表实例
myChart.setOption(data)
```
## 图表大小
- 在有些场景下,我们希望当容器大小改变时,图表的大小也相应地改变。
```js
// 默认获取容器尺寸
this.$refs.chart.resize()
// 指定尺寸
this.$refs.chart.resize({width: 375, height: 375})
```
## 自定义Tooltips
- uvue\nvue 不支持
由于除H5之外都不存在dom但又有tooltips个性化的需求代码就不贴了看示例吧
```
代码位于/uni_modules/lime-echart/component/lime-echart
```
## 插件标签
- 默认 l-echart 为 component
- 默认 lime-echart 为 demo
```html
// 在任意地方使用可查看domo, 代码位于/uni_modules/lime-echart/component/lime-echart
<lime-echart></lime-echart>
```
## 常见问题
- 钉钉小程序 由于没有`measureText`,模拟的`measureText`又无法得到当前字体的`fontWeight`,故可能存在估计不精细的问题
- 微信小程序 `2d` 只支持 真机调试2.0
- 微信开发工具会出现 `canvas` 不跟随页面的情况,真机不影响
- 微信开发工具会出现 `canvas` 层级过高的问题,真机一般不受影响,可以先测只有两个元素的页面看是否会有层级问题。
- toolbox 不支持 `saveImage`
- echarts 5.3.0 的 lines 不支持 trailLength故需设置为 `0`
- dataZoom H5不要设置 `showDetail`
- 如果微信小程序的`tooltip`文字有阴影,可能是微信的锅,临时解决方法是`tooltip.shadowBlur = 0`
- 如果钉钉小程序上传时报安全问题`Uint8Clamped`,可以向钉钉反馈是安全代码扫描把Uint8Clamped数组错误识别了也可以在 echarts 文件修改`Uint8Clamped`
```js
// 找到这段代码把代码中`Uint8Clamped`改成`Uint8_Clamped`,再把下划线去掉,不过直接去掉`Uint8Clamped`也是可行的
// ["Int8","Uint8","Uint8Clamped","Int16","Uint16","Int32","Uint32","Float32","Float64"],(function(t,e){return t["[object "+e+"Array]"]
// 改成如下
["Int8","Uint8","Uint8_Clamped","Int16","Uint16","Int32","Uint32","Float32","Float64"],(function(t,e){return t["[object "+e.replace('_','')+"Array]"]
```
### vue3
如果您是使用 **vite + vue3** 非微信小程序可能会遇到`echarts`文件缺少`wx`判断导致无法使用或缺少`tooltip`<br>
方式一:可以在`echarts.min.js`文件开头增加以下内容参考插件内的echart.min.js的做法
```js
let global = null
let wx = uni
```
方式二:在`vite.config.js`的`define`设置环境
```js
// 或者在`vite.config.js`的`define`设置环境
import { defineConfig } from 'vite';
import uni from '@dcloudio/vite-plugin-uni';
const define = {}
if(!["mp-weixin", "h5", "web"].includes(process.env.UNI_PLATFORM)) {
define['global'] = null
define['wx'] = 'uni'
}
export default defineConfig({
plugins: [uni()],
define
});
```
## Props
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --------------- | -------- | ------- | ------------ | ----- |
| custom-style | 自定义样式 | `string` | - | - |
| type | 指定 canvas 类型 | `string` | `2d` | |
| is-disable-scroll | 触摸图表时是否禁止页面滚动 | `boolean` | `false` | |
| beforeDelay | 延迟初始化 (毫秒) | `number` | `30` | |
| enableHover | PC端使用鼠标悬浮 | `boolean` | `false` | |
| landscape | 是否旋转90deg,模拟横屏效果 | `boolean` | `false` | |
## 事件
| 参数 | 说明 |
| --------------- | --------------- |
| init(echarts, chart => {}) | 初始化调用函数,第一个参数是传入`echarts`,第二个参数是回调函数,回调函数的参数是 `chart` 实例 |
| setChart(chart => {}) | 已经初始化后,请使用这个方法,是个回调函数,参数是 `chart` 实例 |
| setOption(data) | [图表配置项](https://echarts.apache.org/zh/option.html#title),用于更新 ,传递是数据 `option` |
| clear() | 清空当前实例,会移除实例中所有的组件和图表。 |
| dispose() | 销毁实例 |
| showLoading() | 显示加载 |
| hideLoading() | 隐藏加载 |
| [canvasToTempFilePath](https://uniapp.dcloud.io/api/canvas/canvasToTempFilePath.html#canvastotempfilepath)(opt) | 用于生成图片,与官方使用方法一致,但不需要传`canvasId` |
## 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png)
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title></title>
<style type="text/css">
html,
body,
.canvas {
padding: 0;
margin: 0;
overflow-y: hidden;
background-color: transparent;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div class="canvas" id="limeChart"></div>
<script type="text/javascript" src="./uni.webview.1.5.5.js"></script>
<script type="text/javascript" src="./echarts.min.js"></script>
<script type="text/javascript" src="./ecStat.min.js"></script>
<!-- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts-liquidfill@latest/dist/echarts-liquidfill.min.js"></script> -->
<script>
let chart = null;
let cache = [];
console.log = function() {
emit('log', {
log: arguments,
})
}
function emit(event, data) {
postMessage({
event,
data
})
cache = []
}
function postMessage(data) {
uni.webView.postMessage({
data
})
// window.__uniapp_x_.postMessage(JSON.stringify(data))
};
function stringify(key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.indexOf(value) !== -1) {
return;
}
cache.push(value);
}
return value;
}
function parse(name, callback, options) {
const optionNameReg = /[\w]+\.setOption\(([\w]+\.)?([\w]+)\)/
if (optionNameReg.test(callback)) {
const optionNames = callback.match(optionNameReg)
if (optionNames[1]) {
const _this = optionNames[1].split('.')[0]
window[_this] = {}
window[_this][optionNames[2]] = options
return optionNames[2]
} else {
return null
}
}
return null
}
function init(callback, options, opts, theme) {
if (!chart) {
chart = echarts.init(document.getElementById('limeChart'), theme, opts)
if (options) {
chart.setOption(options)
}
}
}
function on(data) {
if (chart && data.length > 0) {
const [type, query] = data
const key = `${type}${JSON.stringify(query||'')}`
if (query) {
chart.on(type, query, function(options) {
var obj = {};
Object.keys(options).forEach(function(key) {
if (key != 'event') {
obj[key] = options[key];
}
});
emit(key, {
event: key,
options: obj,
});
});
} else {
chart.on(type, function(options) {
var obj = {};
Object.keys(options).forEach(function(key) {
if (key != 'event') {
obj[key] = options[key];
}
});
emit(key, {
event: key,
options: obj,
});
});
}
}
}
function setChart(callback, options) {
if (!callback) return
if (chart && callback && options) {
var r = null
const name = parse('r', callback, options)
if (name) this[name] = options
eval(`r = ${callback};`)
if (r) {
r(chart)
}
}
}
function setOption(data) {
if (chart) chart.setOption(data[0], data[1])
}
function showLoading(data) {
if (chart) chart.showLoading(data[0], data[1])
}
function hideLoading() {
if (chart) chart.hideLoading()
}
function clear() {
if (chart) chart.clear()
}
function dispose() {
if (chart) chart.dispose()
}
function resize(size) {
if (chart) chart.resize(size)
}
function canvasToTempFilePath(opt) {
if (chart) {
delete opt.success
const src = chart.getDataURL(opt)
postMessage({
// event: 'file',
file: src
})
}
}
document.addEventListener('touchmove', () => {
})
</script>
</body>
</html>

View File

@ -0,0 +1,42 @@
// @ts-nocheck
import {isNumeric} from '../isNumeric'
import {isDef} from '../isDef'
/**
* px
* @param value
* @returns null null
*/
// #ifndef UNI-APP-X && APP
export function addUnit(value?: string | number): string | null {
if (!isDef(value)) {
return null;
}
value = String(value); // 将值转换为字符串
// 如果值是数字,则在后面添加单位 "px",否则保持原始值
return isNumeric(value) ? `${value}px` : value;
}
// #endif
// #ifdef UNI-APP-X && APP
function addUnit(value: string): string
function addUnit(value: number): string
function addUnit(value: any|null): string|null {
if (!isDef(value)) {
return null;
}
value = `${value}` //value.toString(); // 将值转换为字符串
// 如果值是数字,则在后面添加单位 "px",否则保持原始值
return isNumeric(value) ? `${value}px` : value;
}
export {addUnit}
// #endif
// console.log(addUnit(100)); // 输出: "100px"
// console.log(addUnit("200")); // 输出: "200px"
// console.log(addUnit("300px")); // 输出: "300px"(已经包含单位)
// console.log(addUnit()); // 输出: undefined值为 undefined
// console.log(addUnit(null)); // 输出: undefined值为 null

View File

@ -0,0 +1,82 @@
export function cubicBezier(p1x : number, p1y : number, p2x : number, p2y : number):(x: number)=> number {
const ZERO_LIMIT = 1e-6;
// Calculate the polynomial coefficients,
// implicit first and last control points are (0,0) and (1,1).
const ax = 3 * p1x - 3 * p2x + 1;
const bx = 3 * p2x - 6 * p1x;
const cx = 3 * p1x;
const ay = 3 * p1y - 3 * p2y + 1;
const by = 3 * p2y - 6 * p1y;
const cy = 3 * p1y;
function sampleCurveDerivativeX(t : number) : number {
// `ax t^3 + bx t^2 + cx t` expanded using Horner's rule
return (3 * ax * t + 2 * bx) * t + cx;
}
function sampleCurveX(t : number) : number {
return ((ax * t + bx) * t + cx) * t;
}
function sampleCurveY(t : number) : number {
return ((ay * t + by) * t + cy) * t;
}
// Given an x value, find a parametric value it came from.
function solveCurveX(x : number) : number {
let t2 = x;
let derivative : number;
let x2 : number;
// https://trac.webkit.org/browser/trunk/Source/WebCore/platform/animation
// first try a few iterations of Newton's method -- normally very fast.
// http://en.wikipedia.org/wikiNewton's_method
for (let i = 0; i < 8; i++) {
// f(t) - x = 0
x2 = sampleCurveX(t2) - x;
if (Math.abs(x2) < ZERO_LIMIT) {
return t2;
}
derivative = sampleCurveDerivativeX(t2);
// == 0, failure
/* istanbul ignore if */
if (Math.abs(derivative) < ZERO_LIMIT) {
break;
}
t2 -= x2 / derivative;
}
// Fall back to the bisection method for reliability.
// bisection
// http://en.wikipedia.org/wiki/Bisection_method
let t1 = 1;
/* istanbul ignore next */
let t0 = 0;
/* istanbul ignore next */
t2 = x;
/* istanbul ignore next */
while (t1 > t0) {
x2 = sampleCurveX(t2) - x;
if (Math.abs(x2) < ZERO_LIMIT) {
return t2;
}
if (x2 > 0) {
t1 = t2;
} else {
t0 = t2;
}
t2 = (t1 + t0) / 2;
}
// Failure
return t2;
}
return function (x : number) : number {
return sampleCurveY(solveCurveX(x));
}
// return solve;
}

View File

@ -0,0 +1,3 @@
import {cubicBezier} from './bezier';
export let ease = cubicBezier(0.25, 0.1, 0.25, 1);
export let linear = cubicBezier(0,0,1,1);

View File

@ -0,0 +1,10 @@
// @ts-nocheck
// #ifdef UNI-APP-X && APP
export * from './uvue.uts'
// #endif
// #ifndef UNI-APP-X && APP
export * from './vue.ts'
// #endif

View File

@ -0,0 +1,100 @@
// @ts-nocheck
import type { ComponentPublicInstance } from 'vue'
import { ease, linear } from './ease';
import { Timeline, Animation } from './';
export type UseTransitionOptions = {
duration ?: number
immediate ?: boolean
context ?: ComponentPublicInstance
}
// #ifndef UNI-APP-X && APP
import { ref, watch, type Ref } from '@/uni_modules/lime-shared/vue'
export function useTransition(percent : Ref<number>|(() => number), options : UseTransitionOptions) : Ref<number> {
const current = ref(0)
const { immediate, duration = 300 } = options
let tl:Timeline|null = null;
let timer = -1
const isFunction = typeof percent === 'function'
watch(isFunction ? percent : () => percent.value, (v) => {
if(tl == null){
tl = new Timeline()
}
tl.start();
tl.add(
new Animation(
current.value,
v,
duration,
0,
ease,
nowValue => {
current.value = nowValue
clearTimeout(timer)
if(current.value == v){
timer = setTimeout(()=>{
tl?.pause();
tl = null
}, duration)
}
}
)
);
}, { immediate })
return current
}
// #endif
// #ifdef UNI-APP-X && APP
type UseTransitionReturnType = Ref<number>
export function useTransition(source : any, options : UseTransitionOptions) : UseTransitionReturnType {
const outputRef : Ref<number> = ref(0)
const immediate = options.immediate ?? false
const duration = options.duration ?? 300
const context = options.context //as ComponentPublicInstance | null
let tl:Timeline|null = null;
let timer = -1
const watchFunc = (v : number) => {
if(tl == null){
tl = new Timeline()
}
tl!.start();
tl!.add(
new Animation(
outputRef.value,
v,
duration,
0,
ease,
nowValue => {
outputRef.value = nowValue
clearTimeout(timer)
if(outputRef.value == v){
timer = setTimeout(()=>{
tl?.pause();
tl = null
}, duration)
}
}
),
null
);
}
if (context != null && typeof source == 'string') {
context.$watch(source, watchFunc, { immediate } as WatchOptions)
} else if(typeof source == 'function'){
watch(source, watchFunc, { immediate })
} else if(source instanceof Ref<number>){
watch(source as Ref<number>, watchFunc, { immediate })
}
const stop = ()=>{
}
return outputRef //as UseTransitionReturnType
}
// #endif

View File

@ -0,0 +1,119 @@
import { raf, cancelRaf} from '../raf'
// @ts-nocheck
export class Timeline {
state : string
animations : Set<Animation> = new Set<Animation>()
delAnimations : Animation[] = []
startTimes : Map<Animation, number> = new Map<Animation, number>()
pauseTime : number = 0
pauseStart : number = Date.now()
tickHandler : number = 0
tickHandlers : number[] = []
tick : (() => void) | null = null
constructor() {
this.state = 'Initiated';
}
start() {
if (!(this.state == 'Initiated')) return;
this.state = 'Started';
let startTime = Date.now();
this.pauseTime = 0;
this.tick = () => {
let now = Date.now();
this.animations.forEach((animation : Animation) => {
let t:number;
const ani = this.startTimes.get(animation)
if (ani == null) return
if (ani < startTime) {
t = now - startTime - animation.delay - this.pauseTime;
} else {
t = now - ani - animation.delay - this.pauseTime;
}
if (t > animation.duration) {
this.delAnimations.push(animation)
// 不能在 foreach 里面 对 集合进行删除操作
// this.animations.delete(animation);
t = animation.duration;
}
if (t > 0) animation.run(t);
})
// 不能在 foreach 里面 对 集合进行删除操作
while (this.delAnimations.length > 0) {
const animation = this.delAnimations.pop();
if (animation == null) return
this.animations.delete(animation);
}
// cancelAnimationFrame(this.tickHandler);
if (this.state != 'Started') return
this.tickHandler = raf(()=>{
this.tick!()
})
this.tickHandlers.push(this.tickHandler)
}
if(this.tick != null) {
this.tick!()
}
}
pause() {
if (!(this.state === 'Started')) return;
this.state = 'Paused';
this.pauseStart = Date.now();
cancelRaf(this.tickHandler);
// cancelRaf(this.tickHandler);
}
resume() {
if (!(this.state === 'Paused')) return;
this.state = 'Started';
this.pauseTime += Date.now() - this.pauseStart;
this.tick!();
}
reset() {
this.pause();
this.state = 'Initiated';
this.pauseTime = 0;
this.pauseStart = 0;
this.animations.clear()
this.delAnimations.clear()
this.startTimes.clear()
this.tickHandler = 0;
}
add(animation : Animation, startTime ?: number | null) {
if (startTime == null) startTime = Date.now();
this.animations.add(animation);
this.startTimes.set(animation, startTime);
}
}
export class Animation {
startValue : number
endValue : number
duration : number
timingFunction : (t : number) => number
delay : number
template : (t : number) => void
constructor(
startValue : number,
endValue : number,
duration : number,
delay : number,
timingFunction : (t : number) => number,
template : (v : number) => void) {
this.startValue = startValue;
this.endValue = endValue;
this.duration = duration;
this.timingFunction = timingFunction;
this.delay = delay;
this.template = template;
}
run(time : number) {
let range = this.endValue - this.startValue;
let progress = time / this.duration
if(progress != 1) progress = this.timingFunction(progress)
this.template(this.startValue + range * progress)
}
}

View File

@ -0,0 +1,123 @@
// @ts-nocheck
const TICK = Symbol('tick');
const TICK_HANDLER = Symbol('tick-handler');
const ANIMATIONS = Symbol('animations');
const START_TIMES = Symbol('start-times');
const PAUSE_START = Symbol('pause-start');
const PAUSE_TIME = Symbol('pause-time');
const _raf = typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame : function(cb: Function) {return setTimeout(cb, 1000/60)}
const _caf = typeof cancelAnimationFrame !== 'undefined' ? cancelAnimationFrame: function(id: any) {clearTimeout(id)}
// const TICK = 'tick';
// const TICK_HANDLER = 'tick-handler';
// const ANIMATIONS = 'animations';
// const START_TIMES = 'start-times';
// const PAUSE_START = 'pause-start';
// const PAUSE_TIME = 'pause-time';
// const _raf = function(callback):number|null {return setTimeout(callback, 1000/60)}
// const _caf = function(id: number):void {clearTimeout(id)}
export class Timeline {
state: string
constructor() {
this.state = 'Initiated';
this[ANIMATIONS] = new Set();
this[START_TIMES] = new Map();
}
start() {
if (!(this.state === 'Initiated')) return;
this.state = 'Started';
let startTime = Date.now();
this[PAUSE_TIME] = 0;
this[TICK] = () => {
let now = Date.now();
this[ANIMATIONS].forEach((animation) => {
let t: number;
if (this[START_TIMES].get(animation) < startTime) {
t = now - startTime - animation.delay - this[PAUSE_TIME];
} else {
t = now - this[START_TIMES].get(animation) - animation.delay - this[PAUSE_TIME];
}
if (t > animation.duration) {
this[ANIMATIONS].delete(animation);
t = animation.duration;
}
if (t > 0) animation.run(t);
})
// for (let animation of this[ANIMATIONS]) {
// let t: number;
// console.log('animation', animation)
// if (this[START_TIMES].get(animation) < startTime) {
// t = now - startTime - animation.delay - this[PAUSE_TIME];
// } else {
// t = now - this[START_TIMES].get(animation) - animation.delay - this[PAUSE_TIME];
// }
// if (t > animation.duration) {
// this[ANIMATIONS].delete(animation);
// t = animation.duration;
// }
// if (t > 0) animation.run(t);
// }
this[TICK_HANDLER] = _raf(this[TICK]);
};
this[TICK]();
}
pause() {
if (!(this.state === 'Started')) return;
this.state = 'Paused';
this[PAUSE_START] = Date.now();
_caf(this[TICK_HANDLER]);
}
resume() {
if (!(this.state === 'Paused')) return;
this.state = 'Started';
this[PAUSE_TIME] += Date.now() - this[PAUSE_START];
this[TICK]();
}
reset() {
this.pause();
this.state = 'Initiated';
this[PAUSE_TIME] = 0;
this[PAUSE_START] = 0;
this[ANIMATIONS] = new Set();
this[START_TIMES] = new Map();
this[TICK_HANDLER] = null;
}
add(animation: any, startTime?: number) {
if (arguments.length < 2) startTime = Date.now();
this[ANIMATIONS].add(animation);
this[START_TIMES].set(animation, startTime);
}
}
export class Animation {
startValue: number
endValue: number
duration: number
timingFunction: (t: number) => number
delay: number
template: (t: number) => void
constructor(startValue: number, endValue: number, duration: number, delay: number, timingFunction: (t: number) => number, template: (v: number) => void) {
timingFunction = timingFunction || (v => v);
template = template || (v => v);
this.startValue = startValue;
this.endValue = endValue;
this.duration = duration;
this.timingFunction = timingFunction;
this.delay = delay;
this.template = template;
}
run(time: number) {
let range = this.endValue - this.startValue;
let progress = time / this.duration
if(progress != 1) progress = this.timingFunction(progress)
this.template(this.startValue + range * progress)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
// @ts-nocheck
import _areaList from './city-china.json';
export const areaList = _areaList
// #ifndef UNI-APP-X
type UTSJSONObject = Record<string, string>
// #endif
// #ifdef UNI-APP-X
type Object = UTSJSONObject
// #endif
type AreaList = {
province_list : Map<string, string>;
city_list : Map<string, string>;
county_list : Map<string, string>;
}
// type CascaderOption = {
// text : string;
// value : string;
// children ?: CascaderOption[];
// };
const makeOption = (
label : string,
value : string,
children ?: UTSJSONObject[],
) : UTSJSONObject => ({
label,
value,
children,
});
export function useCascaderAreaData() : UTSJSONObject[] {
const city = areaList['city_list'] as UTSJSONObject
const county = areaList['county_list'] as UTSJSONObject
const province = areaList['province_list'] as UTSJSONObject
const provinceMap = new Map<string, UTSJSONObject>();
Object.keys(province).forEach((code) => {
provinceMap.set(code.slice(0, 2), makeOption(`${province[code]}`, code, []));
});
const cityMap = new Map<string, UTSJSONObject>();
Object.keys(city).forEach((code) => {
const option = makeOption(`${city[code]}`, code, []);
cityMap.set(code.slice(0, 4), option);
const _province = provinceMap.get(code.slice(0, 2));
if (_province != null) {
(_province['children'] as UTSJSONObject[]).push(option)
}
});
Object.keys(county).forEach((code) => {
const _city = cityMap.get(code.slice(0, 4));
if (_city != null) {
(_city['children'] as UTSJSONObject[]).push(makeOption(`${county[code]}`, code, null));
}
});
// #ifndef APP-ANDROID || APP-IOS
return Array.from(provinceMap.values());
// #endif
// #ifdef APP-ANDROID || APP-IOS
const obj : UTSJSONObject[] = []
provinceMap.forEach((value, code) => {
obj.push(value)
})
return obj
// #endif
}

View File

@ -0,0 +1,8 @@
// @ts-nocheck
// #ifndef UNI-APP-X && APP
export * from './vue.ts'
// #endif
// #ifdef UNI-APP-X && APP
export * from './uvue.uts'
// #endif

View File

@ -0,0 +1,10 @@
// @ts-nocheck
// import {platform} from '../platform'
/**
* buffer转路径
* @param {Object} buffer
*/
// @ts-nocheck
export function arrayBufferToFile(buffer: ArrayBuffer, name?: string, format?:string):Promise<(File|string)> {
console.error('[arrayBufferToFile] 当前环境不支持')
}

View File

@ -0,0 +1,63 @@
// @ts-nocheck
import {platform} from '../platform'
/**
* buffer转路径
* @param {Object} buffer
*/
// @ts-nocheck
export function arrayBufferToFile(buffer: ArrayBuffer | Blob, name?: string, format?:string):Promise<(File|string)> {
return new Promise((resolve, reject) => {
// #ifdef MP
const fs = uni.getFileSystemManager()
//自定义文件名
if (!name && !format) {
reject(new Error('ERROR_NAME_PARSE'))
}
const fileName = `${name || new Date().getTime()}.${format.replace(/(.+)?\//,'')}`;
let pre = platform()
const filePath = `${pre.env.USER_DATA_PATH}/${fileName}`
fs.writeFile({
filePath,
data: buffer,
success() {
resolve(filePath)
},
fail(err) {
console.error(err)
reject(err)
}
})
// #endif
// #ifdef H5
const file = new File([buffer], name, {
type: format,
});
resolve(file)
// #endif
// #ifdef APP-PLUS
const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
const base64 = uni.arrayBufferToBase64(buffer)
bitmap.loadBase64Data(base64, () => {
if (!name && !format) {
reject(new Error('ERROR_NAME_PARSE'))
}
const fileNmae = `${name || new Date().getTime()}.${format.replace(/(.+)?\//,'')}`;
const filePath = `_doc/uniapp_temp/${fileNmae}`
bitmap.save(filePath, {},
() => {
bitmap.clear()
resolve(filePath)
},
(error) => {
bitmap.clear()
reject(error)
})
}, (error) => {
bitmap.clear()
reject(error)
})
// #endif
})
}

View File

@ -0,0 +1,13 @@
// @ts-nocheck
// 未完成
export function base64ToArrayBuffer(base64 : string) {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
if (!format) {
new Error('ERROR_BASE64SRC_PARSE')
}
if(uni.base64ToArrayBuffer) {
return uni.base64ToArrayBuffer(bodyData)
} else {
}
}

View File

@ -0,0 +1,9 @@
// @ts-nocheck
// #ifndef UNI-APP-X && APP
export * from './vue.ts'
// #endif
// #ifdef UNI-APP-X && APP
export * from './uvue.uts'
// #endif

View File

@ -0,0 +1,22 @@
// @ts-nocheck
import { processFile, ProcessFileOptions } from '@/uni_modules/lime-file-utils'
/**
* base64转路径
* @param {Object} base64
*/
export function base64ToPath(base64: string, filename: string | null = null):Promise<string> {
return new Promise((resolve,reject) => {
processFile({
type: 'toDataURL',
path: base64,
filename,
success(res: string){
resolve(res)
},
fail(err){
reject(err)
}
} as ProcessFileOptions)
})
}

View File

@ -0,0 +1,75 @@
// @ts-nocheck
import {platform} from '../platform'
/**
* base64转路径
* @param {Object} base64
*/
export function base64ToPath(base64: string, filename?: string):Promise<string> {
const [, format] = /^data:image\/(\w+);base64,/.exec(base64) || [];
return new Promise((resolve, reject) => {
// #ifdef MP
const fs = uni.getFileSystemManager()
//自定义文件名
if (!filename && !format) {
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
// const time = new Date().getTime();
const name = filename || `${new Date().getTime()}.${format}`;
let pre = platform()
const filePath = `${pre.env.USER_DATA_PATH}/${name}`
fs.writeFile({
filePath,
data: base64.split(',')[1],
encoding: 'base64',
success() {
resolve(filePath)
},
fail(err) {
console.error(err)
reject(err)
}
})
// #endif
// #ifdef H5
// mime类型
let mimeString = base64.split(',')[0].split(':')[1].split(';')[0];
//base64 解码
let byteString = atob(base64.split(',')[1]);
//创建缓冲数组
let arrayBuffer = new ArrayBuffer(byteString.length);
//创建视图
let intArray = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
intArray[i] = byteString.charCodeAt(i);
}
resolve(URL.createObjectURL(new Blob([intArray], {
type: mimeString
})))
// #endif
// #ifdef APP-PLUS
const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
bitmap.loadBase64Data(base64, () => {
if (!filename && !format) {
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
// const time = new Date().getTime();
const name = filename || `${new Date().getTime()}.${format}`;
const filePath = `_doc/uniapp_temp/${name}`
bitmap.save(filePath, {},
() => {
bitmap.clear()
resolve(filePath)
},
(error) => {
bitmap.clear()
reject(error)
})
}, (error) => {
bitmap.clear()
reject(error)
})
// #endif
})
}

View File

@ -0,0 +1,21 @@
/**
* camelCase PascalCase
* @param str
* @param isPascalCase PascalCase false
* @returns
*/
export function camelCase(str: string, isPascalCase: boolean = false): string {
// 将字符串分割成单词数组
let words: string[] = str.split(/[\s_-]+/);
// 将数组中的每个单词首字母大写(除了第一个单词)
let camelCased: string[] = words.map((word, index):string => {
if (index == 0 && !isPascalCase) {
return word.toLowerCase(); // 第一个单词全小写
}
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
});
// 将数组中的单词拼接成一个字符串
return camelCased.join('');
};

View File

@ -0,0 +1,67 @@
// @ts-nocheck
// #ifndef UNI-APP-X && APP
// #ifdef MP-ALIPAY
interface My {
SDKVersion: string
}
declare var my: My
// #endif
function compareVersion(v1:string, v2:string) {
let a1 = v1.split('.');
let a2 = v2.split('.');
const len = Math.max(a1.length, a2.length);
while (a1.length < len) {
a1.push('0');
}
while (a2.length < len) {
a2.push('0');
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(a1[i], 10);
const num2 = parseInt(a2[i], 10);
if (num1 > num2) {
return 1;
}
if (num1 < num2) {
return -1;
}
}
return 0;
}
function gte(version: string) {
let {SDKVersion} = uni.getSystemInfoSync();
// #ifdef MP-ALIPAY
SDKVersion = my.SDKVersion
// #endif
return compareVersion(SDKVersion, version) >= 0;
}
// #endif
/** 环境是否支持canvas 2d */
export function canIUseCanvas2d(): boolean {
// #ifdef MP-WEIXIN
return gte('2.9.0');
// #endif
// #ifdef MP-ALIPAY
return gte('2.7.0');
// #endif
// #ifdef MP-TOUTIAO
return gte('1.78.0');
// #endif
// #ifdef UNI-APP-X && WEB || UNI-APP-X && APP
return true;
// #endif
// #ifndef MP-WEIXIN || MP-ALIPAY || MP-TOUTIAO
return false
// #endif
}

View File

@ -0,0 +1,111 @@
// @ts-nocheck
import { isString } from "../isString";
import { isNumber } from "../isNumber";
/**
*
* @param {number | string} amount -
* @returns {string}
*/
function capitalizedAmount(amount : number) : string
function capitalizedAmount(amount : string) : string
function capitalizedAmount(amount : any | null) : string {
try {
let _amountStr :string;
let _amountNum :number = 0;
// 如果输入是字符串,先将其转换为数字,并去除逗号
if (typeof amount == 'string') {
_amountNum = parseFloat((amount as string).replace(/,/g, ''));
}
if(isNumber(amount)) {
_amountNum = amount as number
}
// 判断输入是否为有效的金额 || isNaN(amount)
if (amount == null) throw new Error('不是有效的金额!');
let result = '';
// 处理负数情况
if (_amountNum < 0) {
result = '欠';
_amountNum = Math.abs(_amountNum);
}
// 金额不能超过千亿以上
if (_amountNum >= 10e11) throw new Error('计算金额过大!');
// 保留两位小数并转换为字符串
_amountStr = _amountNum.toFixed(2);
// 定义数字、单位和小数单位的映射
const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
const units = ['', '拾', '佰', '仟'];
const bigUnits = ['', '万', '亿'];
const decimalUnits = ['角', '分'];
// 分离整数部分和小数部分
const amountArray = _amountStr.split('.');
let integerPart = amountArray[0]; // string| number[]
const decimalPart = amountArray[1];
// 处理整数部分
if (integerPart != '0') {
let _integerPart = integerPart.split('').map((item):number => parseInt(item));
// 将整数部分按四位一级进行分组
const levels = _integerPart.reverse().reduce((prev:string[][], item, index):string[][] => {
// const level = prev?.[0]?.length < 4 ? prev[0] : [];
const level = prev.length > 0 && prev[0].length < 4 ? prev[0]: []
const value = item == 0 ? digits[item] : digits[item] + units[index % 4];
level.unshift(value);
if (level.length == 1) {
prev.unshift(level);
} else {
prev[0] = level;
}
return prev;
}, [] as string[][]);
// 将分组后的整数部分转换为中文大写形式
result += levels.reduce((prev, item, index):string => {
let _level = bigUnits[levels.length - index - 1];
let _item = item.join('').replace(/(零)\1+/g, '$1');
if (_item == '零') {
_level = '';
_item = '';
} else if (_item.endsWith('零')) {
_item = _item.slice(0, _item.length - 1);
}
return prev + _item + _level;
}, '');
} else {
result += '零';
}
// 添加元
result += '元';
// 处理小数部分
if (decimalPart != '00') {
if (result == '零元') result = '';
for (let i = 0; i < decimalPart.length; i++) {
const digit = parseInt(decimalPart.charAt(i));
if (digit != 0) {
result += digits[digit] + decimalUnits[i];
}
}
} else {
result += '整';
}
return result;
} catch (error : Error) {
return error.message;
}
};

View File

@ -0,0 +1,59 @@
## 0.2.72025-01-17
- fix: 针对canvas 平台判断优化
## 0.2.62025-01-09
- feat: 增加`areaData`中国省市区数据
## 0.2.52025-01-07
- fix: animation在app上类型问题
## 0.2.42025-01-04
- feat: getRect类型问题
## 0.2.32025-01-01
- chore: unitConvert使用uni.rpx2px
## 0.2.22024-12-11
- chore: 动画使用`requestAnimationFrame`
## 0.2.12024-11-20
- feat: 增加`characterLimit`
## 0.2.02024-11-14
- fix: vue2的类型问题
## 0.1.92024-11-14
- feat: 增加`shuffle`
## 0.1.82024-10-08
- fix: vue2 条件编译 // #ifdef APP-IOS || APP-ANDROID 会生效
## 0.1.72024-09-23
- fix: raf 类型跟随版本变更
## 0.1.62024-07-24
- fix: vue2 app ts需要明确的后缀所有补全
- chore: 减少依赖
## 0.1.52024-07-21
- feat: 删除 Hooks
- feat: 兼容uniappx
## 0.1.42023-09-05
- feat: 增加 Hooks `useIntersectionObserver`
- feat: 增加 `floatAdd`
- feat: 因为本人插件兼容 vue2 需要使用 `composition-api`故增加vue文件代码插件的条件编译
## 0.1.32023-08-13
- feat: 增加 `camelCase`
## 0.1.22023-07-17
- feat: 增加 `getClassStr`
## 0.1.12023-07-06
- feat: 增加 `isNumeric` 区别于 `isNumber`
## 0.1.02023-06-30
- fix: `clamp`忘记导出了
## 0.0.92023-06-27
- feat: 增加`arrayBufferToFile`
## 0.0.82023-06-19
- feat: 增加`createAnimation`、`clamp`
## 0.0.72023-06-08
- chore: 更新注释
## 0.0.62023-06-08
- chore: 增加`createImage`为`lime-watermark`和`lime-qrcode`提供依赖
## 0.0.52023-06-03
- chore: 更新注释
## 0.0.42023-05-22
- feat: 增加`range`,`exif`,`selectComponent`
## 0.0.32023-05-08
- feat: 增加`fillZero`,`debounce`,`throttle`,`random`
## 0.0.22023-05-05
- chore: 更新文档
## 0.0.12023-05-05
- 无

View File

@ -0,0 +1,63 @@
// @ts-nocheck
/**
*
* @param char maxcharacter条件下
* @param max
* @returns maxCharacter/maxLength maxCharacter/maxLength时返回截取之后的字符串和长度
*/
export type CharacterLengthResult = {
length : number;
characters : string;
}
// #ifdef APP-ANDROID
type ChartType = any
// #endif
// #ifndef APP-ANDROID
type ChartType = string | number
// #endif
export function characterLimit(type : string, char : ChartType, max : number) : CharacterLengthResult {
const str = `${char}`;
if (str.length == 0) {
return {
length: 0,
characters: '',
} as CharacterLengthResult
}
if (type == 'maxcharacter') {
let len = 0;
for (let i = 0; i < str.length; i += 1) {
let currentStringLength : number// = 0;
const code = str.charCodeAt(i)!
if (code > 127 || code == 94) {
currentStringLength = 2;
} else {
currentStringLength = 1;
}
if (len + currentStringLength > max) {
return {
length: len,
characters: str.slice(0, i),
} as CharacterLengthResult
}
len += currentStringLength;
}
return {
length: len,
characters: str,
} as CharacterLengthResult
} else if (type == 'maxlength') {
const length = str.length > max ? max : str.length;
return {
length: length,
characters: str.slice(0, length),
} as CharacterLengthResult
}
return {
length: str.length,
characters: str,
} as CharacterLengthResult
};

View File

@ -0,0 +1,16 @@
// @ts-nocheck
/**
*
* @param val
* @param min
* @param max
* @returns
*/
export function clamp(val: number, min: number, max: number): number {
return Math.max(min, Math.min(max, val));
}
// console.log(clamp(5 ,0, 10)); // 输出: 5在范围内不做更改
// console.log(clamp(-5 ,0, 10)); // 输出: 0小于最小值被限制为最小值
// console.log(clamp(15 ,0, 10)); // 输出: 10大于最大值被限制为最大值

View File

@ -0,0 +1,10 @@
// @ts-nocheck
// #ifdef UNI-APP-X && APP
export * from './uvue.ts'
// #endif
// #ifndef UNI-APP-X && APP
export * from './vue.ts'
// #endif

View File

@ -0,0 +1,17 @@
// @ts-nocheck
/**
*
* @param obj
* @returns
*/
export function cloneDeep<T>(obj: any): T {
// 如果传入的对象是基本数据类型(如字符串、数字等),则直接返回
// if(['number', 'string'].includes(typeof obj) || Array.isArray(obj)){
// return obj as T
// }
if(typeof obj == 'object'){
return JSON.parse(JSON.stringify(obj as T)) as T;
}
return obj as T
}

View File

@ -0,0 +1,103 @@
// @ts-nocheck
/**
*
* @param obj
* @returns
*/
export function cloneDeep<T>(obj: any): T {
// 如果传入的对象为空,返回空
if (obj === null) {
return null as unknown as T;
}
// 如果传入的对象是 Set 类型,则将其转换为数组,并通过新的 Set 构造函数创建一个新的 Set 对象
if (obj instanceof Set) {
return new Set([...obj]) as unknown as T;
}
// 如果传入的对象是 Map 类型,则将其转换为数组,并通过新的 Map 构造函数创建一个新的 Map 对象
if (obj instanceof Map) {
return new Map([...obj]) as unknown as T;
}
// 如果传入的对象是 WeakMap 类型,则直接用传入的 WeakMap 对象进行赋值
if (obj instanceof WeakMap) {
let weakMap = new WeakMap();
weakMap = obj;
return weakMap as unknown as T;
}
// 如果传入的对象是 WeakSet 类型,则直接用传入的 WeakSet 对象进行赋值
if (obj instanceof WeakSet) {
let weakSet = new WeakSet();
weakSet = obj;
return weakSet as unknown as T;
}
// 如果传入的对象是 RegExp 类型,则通过新的 RegExp 构造函数创建一个新的 RegExp 对象
if (obj instanceof RegExp) {
return new RegExp(obj) as unknown as T;
}
// 如果传入的对象是 undefined 类型,则返回 undefined
if (typeof obj === 'undefined') {
return undefined as unknown as T;
}
// 如果传入的对象是数组,则递归调用 cloneDeep 函数对数组中的每个元素进行克隆
if (Array.isArray(obj)) {
return obj.map(cloneDeep) as unknown as T;
}
// 如果传入的对象是 Date 类型,则通过新的 Date 构造函数创建一个新的 Date 对象
if (obj instanceof Date) {
return new Date(obj.getTime()) as unknown as T;
}
// 如果传入的对象是普通对象,则使用递归调用 cloneDeep 函数对对象的每个属性进行克隆
if (typeof obj === 'object') {
const newObj: any = {};
for (const [key, value] of Object.entries(obj)) {
newObj[key] = cloneDeep(value);
}
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (const key of symbolKeys) {
newObj[key] = cloneDeep(obj[key]);
}
return newObj;
}
// 如果传入的对象是基本数据类型(如字符串、数字等),则直接返回
return obj;
}
// 示例使用
// // 克隆一个对象
// const obj = { name: 'John', age: 30 };
// const clonedObj = cloneDeep(obj);
// console.log(clonedObj); // 输出: { name: 'John', age: 30 }
// console.log(clonedObj === obj); // 输出: false (副本与原对象是独立的)
// // 克隆一个数组
// const arr = [1, 2, 3];
// const clonedArr = cloneDeep(arr);
// console.log(clonedArr); // 输出: [1, 2, 3]
// console.log(clonedArr === arr); // 输出: false (副本与原数组是独立的)
// // 克隆一个包含嵌套对象的对象
// const person = {
// name: 'Alice',
// age: 25,
// address: {
// city: 'New York',
// country: 'USA',
// },
// };
// const clonedPerson = cloneDeep(person);
// console.log(clonedPerson); // 输出: { name: 'Alice', age: 25, address: { city: 'New York', country: 'USA' } }
// console.log(clonedPerson === person); // 输出: false (副本与原对象是独立的)
// console.log(clonedPerson.address === person.address); // 输出: false (嵌套对象的副本也是独立的)

View File

@ -0,0 +1,22 @@
// @ts-nocheck
/**
*
* @param arr
* @param target
* @returns
*/
export function closest(arr: number[], target: number):number {
return arr.reduce((pre: number, cur: number):number =>
Math.abs(pre - target) < Math.abs(cur - target) ? pre : cur
);
}
// 示例
// // 定义一个数字数组
// const numbers = [1, 3, 5, 7, 9];
// // 在数组中找到最接近目标数字 6 的元素
// const closestNumber = closest(numbers, 6);
// console.log(closestNumber); // 输出结果: 5

View File

@ -0,0 +1,156 @@
<template>
<view id="shared" style="height: 500px; width: 300px; background-color: aqua;">
</view>
</template>
<script lang="ts">
import { getRect, getAllRect } from '@/uni_modules/lime-shared/getRect'
import { camelCase } from '@/uni_modules/lime-shared/camelCase'
import { canIUseCanvas2d } from '@/uni_modules/lime-shared/canIUseCanvas2d'
import { clamp } from '@/uni_modules/lime-shared/clamp'
import { cloneDeep } from '@/uni_modules/lime-shared/cloneDeep'
import { closest } from '@/uni_modules/lime-shared/closest'
import { debounce } from '@/uni_modules/lime-shared/debounce'
import { fillZero } from '@/uni_modules/lime-shared/fillZero'
import { floatAdd } from '@/uni_modules/lime-shared/floatAdd'
import { floatMul } from '@/uni_modules/lime-shared/floatMul'
import { floatDiv } from '@/uni_modules/lime-shared/floatDiv'
import { floatSub } from '@/uni_modules/lime-shared/floatSub'
import { getClassStr } from '@/uni_modules/lime-shared/getClassStr'
import { getCurrentPage } from '@/uni_modules/lime-shared/getCurrentPage'
import { getStyleStr } from '@/uni_modules/lime-shared/getStyleStr'
import { hasOwn } from '@/uni_modules/lime-shared/hasOwn'
import { isBase64 } from '@/uni_modules/lime-shared/isBase64'
import { isBrowser } from '@/uni_modules/lime-shared/isBrowser'
import { isDef } from '@/uni_modules/lime-shared/isDef'
import { isEmpty } from '@/uni_modules/lime-shared/isEmpty'
import { isFunction } from '@/uni_modules/lime-shared/isFunction'
import { isNumber } from '@/uni_modules/lime-shared/isNumber'
import { isNumeric } from '@/uni_modules/lime-shared/isNumeric'
import { isObject } from '@/uni_modules/lime-shared/isObject'
import { isPromise } from '@/uni_modules/lime-shared/isPromise'
import { isString } from '@/uni_modules/lime-shared/isString'
import { kebabCase } from '@/uni_modules/lime-shared/kebabCase'
import { raf, doubleRaf } from '@/uni_modules/lime-shared/raf'
import { random } from '@/uni_modules/lime-shared/random'
import { range } from '@/uni_modules/lime-shared/range'
import { sleep } from '@/uni_modules/lime-shared/sleep'
import { throttle } from '@/uni_modules/lime-shared/throttle'
import { toArray } from '@/uni_modules/lime-shared/toArray'
import { toBoolean } from '@/uni_modules/lime-shared/toBoolean'
import { toNumber } from '@/uni_modules/lime-shared/toNumber'
import { unitConvert } from '@/uni_modules/lime-shared/unitConvert'
import { getCurrentInstance } from '@/uni_modules/lime-shared/vue'
import { capitalizedAmount } from '@/uni_modules/lime-shared/capitalizedAmount'
// #ifdef VUE2
type UTSJSONObject = any
// #endif
const context = getCurrentInstance()
// getRect('#shared', context!).then(res =>{
// console.log('res', res.bottom)
// })
// getAllRect('#shared', context).then(res =>{
// console.log('res', res)
// })
// console.log('camelCase::', camelCase("hello world"));
// console.log('camelCase::', camelCase("my_name_is_john", true));
// console.log('canIUseCanvas2d::', canIUseCanvas2d());
// console.log('clamp::', clamp(5 ,0, 10));
// console.log('cloneDeep::', cloneDeep<UTSJSONObject>({a:5}));
// console.log('closest::', closest([1, 3, 5, 7, 9], 6));
// const saveData = (data: any) => {
// //
// console.log(`Saving data: ${data}`);
// }
// const debouncedSaveData = debounce(saveData, 500);
// debouncedSaveData('Data 1');
// debouncedSaveData('Data 2');
// console.log('fillZero', fillZero(1))
// console.log('floatAdd', floatAdd(0.1, 0.2), floatAdd(1.05, 0.05), floatAdd(0.1, 0.7), floatAdd(0.0001, 0.0002), floatAdd(123.456 , 789.012))
// console.log('floatMul', floatMul(0.1, 0.02), floatMul(1.0255, 100))
// console.log('floatDiv', floatDiv(10.44, 100), floatDiv(1.0255, 100), floatDiv(5.419909340994699, 0.2))
// console.log('floatSub', floatSub(0.4, 0.1), floatSub(1.0255, 100))
const now = () : number => System.nanoTime() / 1_000_000.0
console.log('capitalizedAmount', capitalizedAmount(0.4))
console.log('capitalizedAmount', capitalizedAmount(100))
console.log('capitalizedAmount', capitalizedAmount(100000000))
console.log('capitalizedAmount', capitalizedAmount('2023.04'))
console.log('capitalizedAmount', capitalizedAmount(-1024))
console.log('now', now(), Date.now())
// console.log('getClassStr', getClassStr({hover: true}))
// console.log('getStyleStr', getStyleStr({ color: 'red', fontSize: '16px', backgroundColor: '', border: null }))
// console.log('hasOwn', hasOwn({a: true}, 'key'))
// console.log('isBase64::', isBase64("SGVsbG8sIFdvcmxkIQ=="));
// console.log('isBrowser::', isBrowser);
// console.log('isDef::', isDef('6'));
// console.log('isEmpty::', isEmpty({a: true}));
// const b = () =>{}
// console.log('isFunction::', isFunction(b));
// console.log('isNumber::', isNumber('6'));
// console.log('isNumeric::', isNumeric('6'));
// console.log('isObject::', isObject({}));
// const promise = ():Promise<boolean> => {
// return new Promise((resolve) => {
// resolve(true)
// })
// }
// const a = promise()
// console.log('isPromise::', isPromise(a));
// console.log('isString::', isString('null'));
// console.log('kebabCase::', kebabCase('my love'));
// console.log('raf::', raf(()=>{
// console.log('raf:::1')
// }));
// console.log('doubleRaf::', doubleRaf(()=>{
// console.log('doubleRaf:::1')
// }));
// console.log('random', random(0, 10))
// console.log('random', random(0, 1, 2))
// console.log('range', range(0, 10, 2))
// console.log('sleep', sleep(300).then(res => {
// console.log('log')
// }))
// const handleScroll = (a: string) => {
// console.log("Scroll event handled!", a);
// }
// // // 使 handleScroll 500
// const throttledScroll = throttle(handleScroll, 500);
// throttledScroll('5');
// const page = getCurrentPage()
// console.log('getCurrentPage::', page)
// console.log('toArray', toArray<number>(5))
// console.log('toBoolean', toBoolean(5))
// console.log('toNumber', toNumber('5'))
// console.log('unitConvert', unitConvert('5'))
// uni.getImageInfo({
// src: '/static/logo.png',
// success(res) {
// console.log('res', res)
// }
// })
export default {
}
</script>
<style>
</style>

View File

@ -0,0 +1,9 @@
// @ts-nocheck
// #ifndef UNI-APP-X
export * from './type.ts'
export * from './vue.ts'
// #endif
// #ifdef UNI-APP-X
export * from './uvue.ts'
// #endif

View File

@ -0,0 +1,25 @@
export type CreateAnimationOptions = {
/**
* ms
*/
duration ?: number;
/**
*
* - linear: 动画从头到尾的速度是相同的
* - ease: 动画以低速开始
* - ease-in:
* - ease-in-out: 动画以低速开始和结束
* - ease-out: 动画以低速结束
* - step-start: 动画第一帧就跳至结束状态直到结束
* - step-end: 动画一直保持开始状态
*/
timingFunction ?: string //'linear' | 'ease' | 'ease-in' | 'ease-in-out' | 'ease-out' | 'step-start' | 'step-end';
/**
* ms
*/
delay ?: number;
/**
* transform-origin
*/
transformOrigin ?: string;
}

View File

@ -0,0 +1,5 @@
// @ts-nocheck
// export * from '@/uni_modules/lime-animateIt'
export function createAnimation() {
console.error('当前环境不支持,请使用lime-animateIt')
}

View File

@ -0,0 +1,148 @@
// @ts-nocheck
// nvue 需要在节点上设置ref或在export里传入
// const animation = createAnimation({
// ref: this.$refs['xxx'],
// duration: 0,
// timingFunction: 'linear'
// })
// animation.opacity(1).translate(x, y).step({duration})
// animation.export(ref)
// 抹平nvue 与 uni.createAnimation的使用差距
// 但是nvue动画太慢
import { CreateAnimationOptions } from './type'
// #ifdef APP-NVUE
const nvueAnimation = uni.requireNativePlugin('animation')
type AnimationTypes = 'matrix' | 'matrix3d' | 'rotate' | 'rotate3d' | 'rotateX' | 'rotateY' | 'rotateZ' | 'scale' | 'scale3d' | 'scaleX' | 'scaleY' | 'scaleZ' | 'skew' | 'skewX' | 'skewY' | 'translate' | 'translate3d' | 'translateX' | 'translateY' | 'translateZ'
| 'opacity' | 'backgroundColor' | 'width' | 'height' | 'left' | 'right' | 'top' | 'bottom'
interface Styles {
[key : string] : any
}
interface StepConfig {
duration?: number
timingFunction?: string
delay?: number
needLayout?: boolean
transformOrigin?: string
}
interface StepAnimate {
styles?: Styles
config?: StepConfig
}
interface StepAnimates {
[key: number]: StepAnimate
}
// export interface CreateAnimationOptions extends UniApp.CreateAnimationOptions {
// ref?: string
// }
type Callback = (time: number) => void
const animateTypes1 : AnimationTypes[] = ['matrix', 'matrix3d', 'rotate', 'rotate3d', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scale3d',
'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'translate', 'translate3d', 'translateX', 'translateY',
'translateZ'
]
const animateTypes2 : AnimationTypes[] = ['opacity', 'backgroundColor']
const animateTypes3 : AnimationTypes[] = ['width', 'height', 'left', 'right', 'top', 'bottom']
class LimeAnimation {
ref : any
context : any
options : UniApp.CreateAnimationOptions
// stack : any[] = []
next : number = 0
currentStepAnimates : StepAnimates = {}
duration : number = 0
constructor(options : CreateAnimationOptions) {
const {ref} = options
this.ref = ref
this.options = options
}
addAnimate(type : AnimationTypes, args: (string | number)[]) {
let aniObj = this.currentStepAnimates[this.next]
let stepAnimate:StepAnimate = {}
if (!aniObj) {
stepAnimate = {styles: {}, config: {}}
} else {
stepAnimate = aniObj
}
if (animateTypes1.includes(type)) {
if (!stepAnimate.styles.transform) {
stepAnimate.styles.transform = ''
}
let unit = ''
if (type === 'rotate') {
unit = 'deg'
}
stepAnimate.styles.transform += `${type}(${args.map((v: number) => v + unit).join(',')}) `
} else {
stepAnimate.styles[type] = `${args.join(',')}`
}
this.currentStepAnimates[this.next] = stepAnimate
}
animateRun(styles: Styles = {}, config:StepConfig = {}, ref: any) {
const el = ref || this.ref
if (!el) return
return new Promise((resolve) => {
const time = +new Date()
nvueAnimation.transition(el, {
styles,
...config
}, () => {
resolve(+new Date() - time)
})
})
}
nextAnimate(animates: StepAnimates, step: number = 0, ref: any, cb: Callback) {
let obj = animates[step]
if (obj) {
let { styles, config } = obj
// this.duration += config.duration
this.animateRun(styles, config, ref).then((time: number) => {
step += 1
this.duration += time
this.nextAnimate(animates, step, ref, cb)
})
} else {
this.currentStepAnimates = {}
cb && cb(this.duration)
}
}
step(config:StepConfig = {}) {
this.currentStepAnimates[this.next].config = Object.assign({}, this.options, config)
this.currentStepAnimates[this.next].styles.transformOrigin = this.currentStepAnimates[this.next].config.transformOrigin
this.next++
return this
}
export(ref: any, cb?: Callback) {
ref = ref || this.ref
if(!ref) return
this.duration = 0
this.next = 0
this.nextAnimate(this.currentStepAnimates, 0, ref, cb)
return null
}
}
animateTypes1.concat(animateTypes2, animateTypes3).forEach(type => {
LimeAnimation.prototype[type] = function(...args: (string | number)[]) {
this.addAnimate(type, args)
return this
}
})
// #endif
export function createAnimation(options : CreateAnimationOptions) {
// #ifndef APP-NVUE
return uni.createAnimation({ ...options })
// #endif
// #ifdef APP-NVUE
return new LimeAnimation(options)
// #endif
}

View File

@ -0,0 +1,73 @@
// @ts-nocheck
// #ifndef UNI-APP-X && APP
import type { ComponentInternalInstance } from '@/uni_modules/lime-shared/vue'
import { getRect } from '@/uni_modules/lime-shared/getRect'
import { canIUseCanvas2d } from '@/uni_modules/lime-shared/canIUseCanvas2d'
export const isCanvas2d = canIUseCanvas2d()
// #endif
export function createCanvas(canvasId : string, component : ComponentInternalInstance) {
// #ifdef UNI-APP-X
uni.createCanvasContextAsync({
canvasId,
component,
success(context : CanvasContext) {
},
fail(error : UniError) {
}
})
// #endif
// #ifndef UNI-APP-X
const isCanvas2d = canIUseCanvas2d()
getRect('#' + canvasId, context, isCanvas2d).then(res => {
if (res.node) {
res.node.width = res.width
res.node.height = res.height
return res.node
} else {
const ctx = uni.createCanvasContext(canvasId, context)
if (!ctx._drawImage) {
ctx._drawImage = ctx.drawImage
ctx.drawImage = function (...args) {
const { path } = args.shift()
ctx._drawImage(path, ...args)
}
}
if (!ctx.getImageData) {
ctx.getImageData = function () {
return new Promise((resolve, reject) => {
uni.canvasGetImageData({
canvasId,
x: parseInt(arguments[0]),
y: parseInt(arguments[1]),
width: parseInt(arguments[2]),
height: parseInt(arguments[3]),
success(res) {
resolve(res)
},
fail(err) {
reject(err)
}
}, context)
})
}
return {
getContext(type: string) {
if(type == '2d') {
return ctx
}
},
width: res.width,
height: res.height,
createImage
}
}
}
})
// #endif
}

View File

@ -0,0 +1,71 @@
// @ts-nocheck
// #ifndef UNI-APP-X && APP
import {isBrowser} from '../isBrowser'
class Image {
currentSrc: string | null = null
naturalHeight: number = 0
naturalWidth: number = 0
width: number = 0
height: number = 0
tagName: string = 'IMG'
path: string = ''
crossOrigin: string = ''
referrerPolicy: string = ''
onload: () => void = () => {}
onerror: () => void = () => {}
complete: boolean = false
constructor() {}
set src(src: string) {
console.log('src', src)
if(!src) {
return this.onerror()
}
src = src.replace(/^@\//,'/')
this.currentSrc = src
uni.getImageInfo({
src,
success: (res) => {
const localReg = /^\.|^\/(?=[^\/])/;
// #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-TOUTIAO
res.path = localReg.test(src) ? `/${res.path}` : res.path;
// #endif
this.complete = true
this.path = res.path
this.naturalWidth = this.width = res.width
this.naturalHeight = this.height = res.height
this.onload()
},
fail: () => {
this.onerror()
}
})
}
get src() {
return this.currentSrc
}
}
interface UniImage extends WechatMiniprogram.Image {
complete?: boolean
naturalHeight?: number
naturalWidth?: number
}
/** 创建用于 canvas 的 img */
export function createImage(canvas?: any): HTMLImageElement | UniImage {
if(canvas && canvas.createImage) {
return (canvas as WechatMiniprogram.Canvas).createImage()
} else if(this && this['tagName'] == 'canvas' && !('toBlob' in this) || canvas && !('toBlob' in canvas)){
return new Image()
} else if(isBrowser) {
return new window.Image()
}
return new Image()
}
// #endif
// #ifdef UNI-APP-X && APP
export function createImage():Image{
// console.error('当前环境不支持')
return new Image()
}
// #endif

View File

@ -0,0 +1,10 @@
// @ts-nocheck
// #ifdef UNI-APP-X && APP
export * from './uvue.ts'
// #endif
// #ifndef UNI-APP-X && APP
export * from './vue.ts'
// #endif

View File

@ -0,0 +1,36 @@
// @ts-nocheck
/**
*
* @param fn
* @param wait
* @returns
*/
export function debounce<A extends any>(fn : (args: A)=> void, wait = 300): (args: A)=> void {
let timer = -1
return (args: A) => {
if (timer >-1) {clearTimeout(timer)};
timer = setTimeout(()=>{
fn(args)
}, wait)
}
};
// 示例
// 定义一个函数
// function saveData(data: string) {
// // 模拟保存数据的操作
// console.log(`Saving data: ${data}`);
// }
// // 创建一个防抖函数,延迟 500 毫秒后调用 saveData 函数
// const debouncedSaveData = debounce(saveData, 500);
// // 连续调用防抖函数
// debouncedSaveData('Data 1'); // 不会立即调用 saveData 函数
// debouncedSaveData('Data 2'); // 不会立即调用 saveData 函数
// 在 500 毫秒后,只会调用一次 saveData 函数,输出结果为 "Saving data: Data 2"

View File

@ -0,0 +1,40 @@
// @ts-nocheck
type Timeout = ReturnType<typeof setTimeout> | null;
/**
*
* @param fn
* @param wait
* @returns
*/
export function debounce<A extends any, R>(
fn : (...args : A) => R,
wait : number = 300) : (...args : A) => void {
let timer : Timeout = null;
return function (...args : A) {
if (timer) clearTimeout(timer); // 如果上一个 setTimeout 存在,则清除它
// 设置一个新的 setTimeout在指定的等待时间后调用防抖函数
timer = setTimeout(() => {
fn.apply(this, args); // 使用提供的参数调用原始函数
}, wait);
};
};
// 示例
// 定义一个函数
// function saveData(data: string) {
// // 模拟保存数据的操作
// console.log(`Saving data: ${data}`);
// }
// // 创建一个防抖函数,延迟 500 毫秒后调用 saveData 函数
// const debouncedSaveData = debounce(saveData, 500);
// // 连续调用防抖函数
// debouncedSaveData('Data 1'); // 不会立即调用 saveData 函数
// debouncedSaveData('Data 2'); // 不会立即调用 saveData 函数
// 在 500 毫秒后,只会调用一次 saveData 函数,输出结果为 "Saving data: Data 2"

View File

@ -0,0 +1,9 @@
// @ts-nocheck
// #ifndef UNI-APP-X && APP
export * from './vue.ts'
// #endif
// #ifdef UNI-APP-X && APP
export * from './uvue.ts'
// #endif

View File

@ -0,0 +1,7 @@
class EXIF {
constructor(){
console.error('当前环境不支持')
}
}
export const exif = new EXIF()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
// @ts-nocheck
/**
*
* @param number
* @param length 2
* @returns
*/
export function fillZero(number: number, length: number = 2): string {
// 将数字转换为字符串,然后使用 padStart 方法填充零到指定长度
return `${number}`.padStart(length, '0');
}

View File

@ -0,0 +1,36 @@
import { isNumber } from '../isNumber'
/**
*
* @param num1
* @param num2
* @returns
*/
export function floatAdd(num1 : number, num2 : number) : number {
// 检查 num1 和 num2 是否为数字类型
if (!(isNumber(num1) || isNumber(num2))) {
console.warn('Please pass in the number type');
return NaN;
}
let r1 : number, r2 : number, m : number;
try {
// 获取 num1 小数点后的位数
r1 = num1.toString().split('.')[1].length;
} catch (error) {
r1 = 0;
}
try {
// 获取 num2 小数点后的位数
r2 = num2.toString().split('.')[1].length;
} catch (error) {
r2 = 0;
}
// 计算需要扩大的倍数
m = Math.pow(10, Math.max(r1, r2));
// 返回相加结果
return (num1 * m + num2 * m) / m;
}

View File

@ -0,0 +1,45 @@
import { floatMul } from '../floatMul';
import { isNumber } from '../isNumber';
/**
*
* @param {number} num1 -
* @param {number} num2 -
* @returns {number}
*/
export function floatDiv(num1:number, num2:number):number {
// 如果传入的不是数字类型则打印警告并返回NaN
if (!isNumber(num1) || !isNumber(num2)) {
console.warn('请传入数字类型');
return NaN;
}
let m1 = 0, // 被除数小数点后的位数
m2 = 0, // 除数小数点后的位数
s1 = num1.toString(), // 将被除数转换为字符串
s2 = num2.toString(); // 将除数转换为字符串
// 计算被除数小数点后的位数
try {
m1 += s1.split('.')[1].length;
} catch (error) {}
// 计算除数小数点后的位数
try {
m2 += s2.split('.')[1].length;
} catch (error) {}
// 进行除法运算并处理小数点后的位数,使用之前定义的乘法函数保持精度
// #ifdef APP-ANDROID
return floatMul(
parseFloat(s1.replace('.', '')) / parseFloat(s2.replace('.', '')),
Math.pow(10, m2 - m1),
);
// #endif
// #ifndef APP-ANDROID
return floatMul(
Number(s1.replace('.', '')) / Number(s2.replace('.', '')),
Math.pow(10, m2 - m1),
);
// #endif
}

View File

@ -0,0 +1,44 @@
// @ts-nocheck
import {isNumber} from '../isNumber';
// #ifdef APP-ANDROID
import BigDecimal from 'java.math.BigDecimal'
// import BigDecimal from 'java.math.BigDecimal'
// import StringBuilder from 'java.lang.StringBuilder'
// import java.math.BigDecimal;
// #endif
/**
*
* @param {number} num1 -
* @param {number} num2 -
* @returns {number}
*/
export function floatMul(num1 : number, num2 : number) : number {
if (!(isNumber(num1) || isNumber(num2))) {
console.warn('Please pass in the number type');
return NaN;
}
let m = 0;
// #ifdef APP-ANDROID
let s1 = BigDecimal.valueOf(num1.toDouble()).toPlainString(); //new UTSNumber(num1).toString() // //`${num1.toFloat()}`// num1.toString(),
let s2 = BigDecimal.valueOf(num2.toDouble()).toPlainString(); //new UTSNumber(num2).toString() //`${num2.toFloat()}`//.toString();
// #endif
// #ifndef APP-ANDROID
let s1:string = `${num1}`// num1.toString(),
let s2:string = `${num2}`//.toString();
// #endif
try {
m += s1.split('.')[1].length;
} catch (error) { }
try {
m += s2.split('.')[1].length;
} catch (error) { }
// #ifdef APP-ANDROID
return parseFloat(s1.replace('.', '')) * parseFloat(s2.replace('.', '')) / Math.pow(10, m);
// #endif
// #ifndef APP-ANDROID
return Number(s1.replace('.', '')) * Number(s2.replace('.', '')) / Math.pow(10, m);
// #endif
}

View File

@ -0,0 +1,32 @@
import { isNumber } from '../isNumber';
/**
*
* @param {number} num1 -
* @param {number} num2 -
* @returns {number}
*/
export function floatSub(num1 : number, num2 : number) : number {
if (!(isNumber(num1) || isNumber(num2))) {
console.warn('Please pass in the number type');
return NaN;
}
let r1:number, r2:number, m:number, n:number;
try {
r1 = num1.toString().split('.')[1].length;
} catch (error) {
r1 = 0;
}
try {
r2 = num2.toString().split('.')[1].length;
} catch (error) {
r2 = 0;
}
m = Math.pow(10, Math.max(r1, r2));
n = r1 >= r2 ? r1 : r2;
// #ifndef APP-ANDROID
return Number(((num1 * m - num2 * m) / m).toFixed(n));
// #endif
// #ifdef APP-ANDROID
return parseFloat(((num1 * m - num2 * m) / m).toFixed(n));
// #endif
}

View File

@ -0,0 +1,53 @@
// @ts-nocheck
// #ifdef UNI-APP-X && APP
import { isNumber } from '../isNumber'
import { isString } from '../isString'
import { isDef } from '../isDef'
// #endif
/**
*
* @param obj -
* @returns
*/
export function getClassStr<T>(obj : T) : string {
let classNames : string[] = [];
// #ifdef UNI-APP-X && APP
if (obj instanceof UTSJSONObject) {
(obj as UTSJSONObject).toMap().forEach((value, key) => {
if (isDef(value)) {
if (isNumber(value)) {
classNames.push(key);
}
if (isString(value) && value !== '') {
classNames.push(key);
}
if (typeof value == 'boolean' && (value as boolean)) {
classNames.push(key);
}
}
})
}
// #endif
// #ifndef UNI-APP-X && APP
// 遍历对象的属性
for (let key in obj) {
// 检查属性确实属于对象自身且其值为true
if ((obj as any).hasOwnProperty(key) && obj[key]) {
// 将属性名添加到类名数组中
classNames.push(key);
}
}
// #endif
// 将类名数组用空格连接成字符串并返回
return classNames.join(' ');
}
// 示例
// const obj = { foo: true, bar: false, baz: true };
// const classNameStr = getClassStr(obj);
// console.log(classNameStr); // 输出: "foo baz"

View File

@ -0,0 +1,9 @@
// @ts-nocheck
// #ifndef UNI-APP-X && APP
export * from './vue.ts'
// #endif
// #ifdef UNI-APP-X && APP
export * from './uvue.uts'
// #endif

View File

@ -0,0 +1,5 @@
// @ts-nocheck
export const getCurrentPage = ():Page => {
const pages = getCurrentPages();
return pages[pages.length - 1]
};

View File

@ -0,0 +1,6 @@
// @ts-nocheck
/** 获取当前页 */
export const getCurrentPage = () => {
const pages = getCurrentPages();
return pages[pages.length - 1] //as T & WechatMiniprogram.Page.TrivialInstance;
};

View File

@ -0,0 +1,62 @@
// @ts-nocheck
// #ifdef APP-NVUE || APP-VUE
export const getLocalFilePath = (path : string) => {
if (typeof plus == 'undefined') return path
if (/^(_www|_doc|_documents|_downloads|file:\/\/|\/storage\/emulated\/0\/)/.test(path)) return path
if (/^\//.test(path)) {
const localFilePath = plus.io.convertAbsoluteFileSystem(path)
if (localFilePath !== path) {
return localFilePath
} else {
path = path.slice(1)
}
}
return '_www/' + path
}
// #endif
// #ifdef UNI-APP-X && APP
export { getResourcePath as getLocalFilePath } from '@/uni_modules/lime-file-utils'
// export const getLocalFilePath = (path : string) : string => {
// let uri = path
// if (uri.startsWith("http") || uri.startsWith("<svg") || uri.startsWith("data:image/svg+xml")) {
// return uri
// }
// if (uri.startsWith("file://")) {
// uri = uri.substring("file://".length)
// } else if (uri.startsWith("unifile://")) {
// uri = UTSAndroid.convert2AbsFullPath(uri)
// } else {
// uri = UTSAndroid.convert2AbsFullPath(uri)
// if (uri.startsWith("/android_asset/")) {
// uri = uri.replace("/android_asset/", "")
// }
// }
// if (new File(uri).exists()) {
// return uri
// } else {
// return null
// }
// // return UTSAndroid.convert2AbsFullPath(path)
// }
// #endif
// #ifdef APP-IOS
// export const getLocalFilePath = (path : string) : string => {
// try {
// let uri = path
// if (uri.startsWith("http") || uri.startsWith("<svg") || uri.startsWith("data:image/svg+xml")) {
// return uri
// }
// if (uri.startsWith("file://")) {
// return uri.substring("file://".length)
// } else if (path.startsWith("/var/")) {
// return path
// }
// return UTSiOS.getResourcePath(path)
// } catch (e) {
// return null
// }
// // return UTSiOS.getResourcePath(path)
// }
// #endif

View File

@ -0,0 +1,9 @@
// @ts-nocheck
// #ifdef UNI-APP-X && APP
export * from './uvue.uts'
// #endif
// #ifndef UNI-APP-X && APP
export * from './vue.ts'
// #endif

View File

@ -0,0 +1,16 @@
// @ts-nocheck
export function getRect(selector : string, context: ComponentPublicInstance):Promise<NodeInfo> {
return new Promise((resolve)=>{
uni.createSelectorQuery().in(context).select(selector).boundingClientRect(res =>{
resolve(res as NodeInfo)
}).exec();
})
}
export function getAllRect(selector : string, context: ComponentPublicInstance):Promise<NodeInfo[]> {
return new Promise((resolve)=>{
uni.createSelectorQuery().in(context).selectAll(selector).boundingClientRect(res =>{
resolve(res as NodeInfo[])
}).exec();
})
}

View File

@ -0,0 +1,117 @@
// @ts-nocheck
// #ifdef APP-NVUE
// 当编译环境是 APP-NVUE 时,引入 uni.requireNativePlugin('dom'),具体插件用途未知
const dom = uni.requireNativePlugin('dom')
// #endif
/**
*
* @param selector
* @param context ComponentInternalInstance
* @param node node
* @returns Promise
*/
export function getRect(selector : string, context : ComponentInternalInstance|ComponentPublicInstance, node: boolean = false) {
// 之前是个对象,现在改成实例,防止旧版会报错
if(context== null) {
return Promise.reject('context is null')
}
if(context.context){
context = context.context
}
// #ifdef MP || VUE2
if (context.proxy) context = context.proxy
// #endif
return new Promise<UniNamespace.NodeInfo>((resolve, reject) => {
// #ifndef APP-NVUE
const dom = uni.createSelectorQuery().in(context).select(selector);
const result = (rect: UniNamespace.NodeInfo) => {
if (rect) {
resolve(rect)
} else {
reject('no rect')
}
}
if (!node) {
dom.boundingClientRect(result).exec()
} else {
dom.fields({
node: true,
size: true,
rect: true
}, result).exec()
}
// #endif
// #ifdef APP-NVUE
let { context } = options
if (/#|\./.test(selector) && context.refs) {
selector = selector.replace(/#|\./, '')
if (context.refs[selector]) {
selector = context.refs[selector]
if(Array.isArray(selector)) {
selector = selector[0]
}
}
}
dom.getComponentRect(selector, (res) => {
if (res.size) {
resolve(res.size)
} else {
reject('no rect')
}
})
// #endif
});
};
export function getAllRect(selector : string, context: ComponentInternalInstance|ComponentPublicInstance, node:boolean = false) {
if(context== null) {
return Promise.reject('context is null')
}
// #ifdef MP || VUE2
if (context.proxy) context = context.proxy
// #endif
return new Promise<UniNamespace.NodeInfo>((resolve, reject) => {
// #ifndef APP-NVUE
const dom = uni.createSelectorQuery().in(context).selectAll(selector);
const result = (rect: UniNamespace.NodeInfo[]) => {
if (rect) {
resolve(rect)
} else {
reject('no rect')
}
}
if (!node) {
dom.boundingClientRect(result).exec()
} else {
dom.fields({
node: true,
size: true,
rect: true
}, result).exec()
}
// #endif
// #ifdef APP-NVUE
let { context } = options
if (/#|\./.test(selector) && context.refs) {
selector = selector.replace(/#|\./, '')
if (context.refs[selector]) {
selector = context.refs[selector]
if(Array.isArray(selector)) {
selector = selector[0]
}
}
}
dom.getComponentRect(selector, (res) => {
if (res.size) {
resolve([res.size])
} else {
reject('no rect')
}
})
// #endif
});
};

View File

@ -0,0 +1,54 @@
// @ts-nocheck
// #ifndef UNI-APP-X && APP
interface CSSProperties {
[key : string] : string | number | null
}
// #endif
// #ifdef VUE3
// #ifdef UNI-APP-X && APP
type CSSProperties = UTSJSONObject
// #endif
// #endif
/**
*
* @param key -
* @returns
*/
export function toLowercaseSeparator(key : string):string {
return key.replace(/([A-Z])/g, '-$1').toLowerCase();
}
/**
*
* @param style - CSS样式对象
* @returns
*/
export function getStyleStr(style : CSSProperties) : string {
// #ifdef UNI-APP-X && APP
let styleStr = '';
style.toMap().forEach((value, key) => {
if(value !== null && value != '') {
styleStr += `${toLowercaseSeparator(key as string)}: ${value};`
}
})
return styleStr
// #endif
// #ifndef UNI-APP-X && APP
return Object.keys(style)
.filter(
(key) =>
style[key] !== undefined &&
style[key] !== null &&
style[key] !== '')
.map((key : string) => `${toLowercaseSeparator(key)}: ${style[key]};`)
.join(' ');
// #endif
}
// 示例
// const style = { color: 'red', fontSize: '16px', backgroundColor: '', border: null };
// const styleStr = getStyleStr(style);
// console.log(styleStr);
// 输出: "color: red; font-size: 16px;"

View File

@ -0,0 +1,39 @@
// @ts-nocheck
// #ifndef UNI-APP-X
interface CSSProperties {
[key : string] : string | number
}
// #endif
// #ifdef UNI-APP-X
type CSSProperties = UTSJSONObject
// #endif
/**
* 将字符串转换为带有连字符分隔的小写形式
* @param key - 要转换的字符串
* @returns 转换后的字符串
*/
export function toLowercaseSeparator(key : string) : string {
return key.replace(/([A-Z])/g, '-$1').toLowerCase();
}
/**
* 获取样式对象对应的样式字符串
* @param style - CSS样式对象
* @returns 由非空有效样式属性键值对组成的字符串
*/
export function getStyleStr(style : CSSProperties) : string {
let styleStr = '';
style.toMap().forEach((value, key) => {
if(value !== null && value != '') {
styleStr += `${toLowercaseSeparator(key as string)}: ${value};`
}
})
return styleStr
}
// 示例
// const style = { color: 'red', fontSize: '16px', backgroundColor: '', border: null };
// const styleStr = getStyleStr(style);
// console.log(styleStr);
// 输出: "color: red; font-size: 16px;"

View File

@ -0,0 +1,9 @@
// @ts-nocheck
// #ifdef UNI-APP-X && APP
export * from './uvue.ts'
// #endif
// #ifndef UNI-APP-X && APP
export * from './vue.ts'
// #endif

View File

@ -0,0 +1,39 @@
// @ts-nocheck
/**
*
* @param obj
* @param key
* @returns truefalse
*/
function hasOwn(obj: UTSJSONObject, key: string): boolean
function hasOwn(obj: Map<string, unknown>, key: string): boolean
function hasOwn(obj: any, key: string): boolean {
if(obj instanceof UTSJSONObject){
return obj[key] != null
}
if(obj instanceof Map<string, unknown>){
return (obj as Map<string, unknown>).has(key)
}
return false
}
export {
hasOwn
}
// 示例
// const obj = { name: 'John', age: 30 };
// if (hasOwn(obj, 'name')) {
// console.log("对象具有 'name' 属性");
// } else {
// console.log("对象不具有 'name' 属性");
// }
// // 输出: 对象具有 'name' 属性
// const arr = [1, 2, 3];
// if (hasOwn(arr, 'length')) {
// console.log("数组具有 'length' 属性");
// } else {
// console.log("数组不具有 'length' 属性");
// }
// 输出: 数组具有 'length' 属性

View File

@ -0,0 +1,30 @@
// @ts-nocheck
const hasOwnProperty = Object.prototype.hasOwnProperty
/**
*
* @param obj
* @param key
* @returns truefalse
*/
export function hasOwn(obj: Object | Array<any>, key: string): boolean {
return hasOwnProperty.call(obj, key);
}
// 示例
// const obj = { name: 'John', age: 30 };
// if (hasOwn(obj, 'name')) {
// console.log("对象具有 'name' 属性");
// } else {
// console.log("对象不具有 'name' 属性");
// }
// // 输出: 对象具有 'name' 属性
// const arr = [1, 2, 3];
// if (hasOwn(arr, 'length')) {
// console.log("数组具有 'length' 属性");
// } else {
// console.log("数组不具有 'length' 属性");
// }
// 输出: 数组具有 'length' 属性

View File

@ -0,0 +1,43 @@
// @ts-nocheck
// validator
// export * from './isString'
// export * from './isNumber'
// export * from './isNumeric'
// export * from './isDef'
// export * from './isFunction'
// export * from './isObject'
// export * from './isPromise'
// export * from './isBase64'
// export * from './hasOwn'
// // 单位转换
// export * from './addUnit'
// export * from './unitConvert'
// export * from './toNumber'
// export * from './random'
// export * from './range'
// export * from './fillZero'
// // image
// export * from './base64ToPath'
// export * from './pathToBase64'
// export * from './exif'
// // canvas
// export * from './canIUseCanvas2d'
// // page
// export * from './getCurrentPage'
// // dom
// export * from './getRect'
// export * from './selectComponent'
// export * from './createAnimation'
// // delay
// export * from './sleep'
// export * from './debounce'
// export * from './throttle'

View File

@ -0,0 +1,23 @@
// @ts-nocheck
/**
* Base64编码
* Base64编码的字符串只包含A-Za-z0-9+/ =
* @param {string} str -
* @returns {boolean} Base64编码truefalse
*/
export function isBase64(str: string): boolean {
const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
return base64Regex.test(str);
}
/**
* Base64编码的data URI
* Base64编码的data URI通常以"data:"MIME类型和编码信息Base64编码的数据
* @param {string} str -
* @returns {boolean} Base64编码的data URItruefalse
*/
export function isBase64DataUri(str: string): boolean {
const dataUriRegex = /^data:([a-zA-Z]+\/[a-zA-Z0-9-+.]+)(;base64)?,([a-zA-Z0-9+/]+={0,2})$/;
return dataUriRegex.test(str);
}

View File

@ -0,0 +1,8 @@
// @ts-nocheck
// #ifdef WEB
export const isBrowser = typeof window !== 'undefined';
// #endif
// #ifndef WEB
export const isBrowser = false;
// #endif

View File

@ -0,0 +1,23 @@
// @ts-nocheck
/**
* undefined null
* @param value
* @returns null true false
*/
// #ifndef UNI-APP-X
export function isDef(value: unknown): boolean {
return value !== undefined && value !== null;
}
// #endif
// #ifdef UNI-APP-X
export function isDef(value : any|null) : boolean {
// #ifdef UNI-APP-X && APP
return value != null;
// #endif
// #ifndef UNI-APP-X && APP
return value != null && value != undefined;
// #endif
}
// #endif

View File

@ -0,0 +1,83 @@
// @ts-nocheck
import {isDef} from '../isDef'
import {isString} from '../isString'
import {isNumber} from '../isNumber'
/**
*
*
* 0
* 0
* 0
* null或undefinedtrue
*
*
* @param {any} value -
* @returns {boolean} truefalse
*/
// #ifdef UNI-APP-X && APP
export function isEmpty(value : any | null) : boolean {
// 为null
if(!isDef(value)){
return true
}
// 为空字符
if(isString(value)){
return value.toString().trim().length == 0
}
// 为数值
if(isNumber(value)){
return false
}
if(typeof value == 'object'){
// 数组
if(Array.isArray(value)){
return (value as Array<unknown>).length == 0
}
// Map
if(value instanceof Map<unknown, unknown>) {
return value.size == 0
}
// Set
if(value instanceof Set<unknown>) {
return value.size == 0
}
if(value instanceof UTSJSONObject) {
return value.toMap().size == 0
}
return JSON.stringify(value) == '{}'
}
return true
}
// #endif
// #ifndef UNI-APP-X && APP
export function isEmpty(value: any): boolean {
// 检查是否为null或undefined
if (value == null) {
return true;
}
// 检查字符串是否为空
if (typeof value === 'string') {
return value.trim().length === 0;
}
// 检查数组是否为空
if (Array.isArray(value)) {
return value.length === 0;
}
// 检查对象是否为空
if (typeof value === 'object') {
return Object.keys(value).length === 0;
}
// 其他类型(如数字、布尔值等)不为空
return false;
}
// #endif

Some files were not shown because too many files have changed in this diff Show More