Merge pull request 'main' (#2) from liubiao/APP:main into main

Reviewed-on: #2
This commit is contained in:
2025-07-11 16:48:23 +08:00
62 changed files with 7977 additions and 3277 deletions

47
App.vue
View File

@ -2,7 +2,7 @@
export default {
onLaunch: function() {
},
onShow: function() {
console.log('App Show')
@ -15,52 +15,27 @@
<style lang="scss">
@import 'vk-uview-ui/index.scss';
uni-slider .uni-slider-handle-wrapper {
uni-slider .uni-slider-handle-wrapper{
border-radius: 20rpx;
}
uni-slider .uni-slider-thumb {
uni-slider .uni-slider-thumb{
width: 66rpx !important;
height: 80rpx !important;
height:80rpx !important;
margin-top: -40rpx !important;
border-radius: 16rpx !important;
margin-left: -72rpx !important;
}
uni-slider .uni-slider-handle-wrapper {
uni-slider .uni-slider-handle-wrapper{
height: 88rpx;
position: relative;
}
uni-slider .uni-slider-value {
uni-slider .uni-slider-value{
position: absolute;
color: rgba(255, 255, 255, 0.8);
color: rgba(255, 255, 255,0.8);
font-size: 32rpx;
right: 2.5%;
top: -60rpx
}
.custom-file-picker .file-picker__box-content {
background: rgba(26, 26, 26, 1);
border: none !important;
border-radius: 45rpx;
width: 180rpx;
height: 180rpx;
}
.uni-file-picker.custom-file-picker {
overflow: inherit !important;
}
.custom-file-picker .icon-add {
height: 5rpx !important;
width: 70rpx !important;
}
.custom-file-picker .file-picker__box-content {
// display: none
right:2.5%;
top:-60rpx
}
</style>

671
api/6155/BlueHelper.js Normal file
View File

@ -0,0 +1,671 @@
export default {
featrueValueCallback: null,//蓝牙特征变化回调
BleChangeCallback:null,//蓝牙状态变化回调
//引导用户打开蓝牙
showBluetoothGuide: function(showTip) {
let platform = process.env.UNI_PLATFORM;
var openBlueSetting = function() {
// 判断平台类型
if (platform === 'mp-weixin') {
uni.openSetting();
} else if (platform === 'app-plus' || platform === 'app') {
//----------------------------------------------------------------
const osName = plus.os.name;
if (osName === 'iOS') {
// iOS 平台打开蓝牙设置
plus.runtime.openURL('App-Prefs:root=Bluetooth', function() {
console.log('成功打开蓝牙设置');
}, function(err) {
console.error('打开蓝牙设置失败:' + err.message);
uni.showModal({
title: '提示',
content: '无法自动打开蓝牙设置,请手动前往设置 > 蓝牙 进行操作。',
showCancel: false
});
});
} else if (osName === 'Android') {
// Android 平台打开蓝牙设置
try {
const Intent = plus.android.importClass('android.content.Intent');
const Settings = plus.android.importClass('android.provider.Settings');
const main = plus.android.runtimeMainActivity();
const intent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
main.startActivity(intent);
} catch (e) {
console.error('打开蓝牙设置失败:' + e.message);
// 尝试使用通用设置页面作为备选方案
plus.runtime.openURL('settings://', function() {
console.log('打开系统设置成功,请手动找到蓝牙选项');
}, function() {
uni.showModal({
title: '提示',
content: '无法自动打开蓝牙设置,请手动前往设置页面开启蓝牙。',
showCancel: false
});
});
}
} else {
uni.showModal({
title: '提示',
content: '当前系统不支持自动打开蓝牙设置,请手动操作。',
showCancel: false
});
}
//--------------------------------------------------------------------
} else if (platform === 'mp-alipay') {
uni.openSetting();
}
}
if (showTip !== undefined) {
openBlueSetting();
return;
}
if (platform === 'mp-weixin' || platform === 'app-plus' || platform === 'mp-alipay' || platform === 'app') {
uni.showModal({
title: '蓝牙未开启',
content: '请在系统设置中打开蓝牙以使用此功能',
success: (res) => {
if (res.confirm) {
openBlueSetting();
}
}
});
} else {
console.log("当前平台不支持打开系统设置" + platform);
}
},
//获取蓝牙适配器状态
CheckBlue: function(callback) {
uni.getBluetoothAdapterState({
success(res1) {
console.log("当前蓝牙适配器状态:" + JSON.stringify(res1))
if (callback) {
callback(res1);
}
},
fail(ex1) {
console.log("检查蓝牙状态异常:" + JSON.stringify(ex1));
if (callback) {
if (ex1.code == 10000) {
console.log("未初始化蓝牙适配器");
}
let res1 = {
available: false,
discovering: false
}
callback(res1);
}
},
complete() {
}
});
},
//初始化蓝牙模块
OpenBlue: function(isCheckState, callback, availCallback) {
var these = this;
var init = function() {
uni.openBluetoothAdapter({
success: (res) => {
console.log("蓝牙初始化成功:" + JSON.stringify(res));
if (callback) {
callback();
}
uni.onBluetoothAdapterStateChange(function(state) {
console.log('蓝牙状态发生变化:' + JSON.stringify(state));
if(this.BleChangeCallback){
this.BleChangeCallback()
}
})
},
fail: function(ex2) {
console.log("蓝牙初始化失败:" + JSON.stringify(ex2))
if (ex2.code == '10001') {
console.log("手机蓝牙未打开或设备不支持蓝牙");
if (availCallback) {
availCallback();
} else {
these.showBluetoothGuide();
}
}
}
});
}
if (isCheckState) {
this.CheckBlue(function(res1) {
if (res1.available) {
if (callback) {
callback();
}
return;
}
init();
})
} else {
init();
}
},
//关闭蓝牙模块,停止搜索、断开所有连接
CloseBlue: function(callback) {
this.StopSearch();
this.disconnectDevice();
uni.closeBluetoothAdapter({
success: () => {
console.log("蓝牙模块已关闭");
if (callback) {
callback();
}
}
});
},
//开始搜索新设备
StartSearch: function(callback) {
var these = this;
//发现新设备
var onDeviceFound = function() {
uni.onBluetoothDeviceFound(function(res) {
console.log("发现新设备:" + JSON.stringify(res));
if (callback) {
callback(res);
}
})
}
//开始搜索
var Search = function() {
uni.startBluetoothDevicesDiscovery({
services: ["0xFFE0"],
allowDuplicatesKey: false,
success: (res) => {
console.log('开始搜索蓝牙设备成功');
onDeviceFound();
},
fail: (err) => {
console.log(`搜索蓝牙设备失败: ${err.errMsg}`);
}
});
}
//先检查蓝牙状态是可用
this.CheckBlue(function(res1) {
if (res1.available) {
if (!res1.discovering) {
Search();
} else {
console.log("当前蓝牙正在搜索设备")
}
} else {
these.OpenBlue(false, Search, () => {
these.showBluetoothGuide();
});
}
});
},
//停止搜索
StopSearch: function() {
uni.stopBluetoothDevicesDiscovery({
success: (res) => {
console.log("停止搜索蓝牙设备成功")
},
fail() {
console.log("无法停止蓝牙搜索")
}
});
},
//获取已连接的设备
getLinkBlue: function(callback) {
uni.getConnectedBluetoothDevices({
services: ["0xFFE0"],
success: (res) => {
if (callback) {
callback(res);
}
},
fail: function(ex) {
console.log("获取已连接设备异常");
if (callback) {
callback({
devices: []
});
}
}
})
},
//连接某个设备
LinkBlue: function(deviceId, callback, error) {
this.StopSearch();
var these = this;
let key = "linkedDevices";
var store = uni.getStorageInfoSync();
var f = store.keys.find(function(v) {
return v == key;
});
var linkedDevices = [];
if (f) {
var str = uni.getStorageSync(key);
if (str) {
linkedDevices = JSON.parse(str);
}else{
linkedDevices=[];
}
}
//连接成功的回调
var lindedCallback = function () {
let c = linkedDevices.find(function (v) {
return v.deviceId == deviceId;
});
if (c) {
console.log("连接成功开始监听特征变化")
//监听设备的特征变化
uni.notifyBLECharacteristicValueChange({
deviceId: deviceId,
serviceId: c.notifyServiceid,
characteristicId: c.notifyCharactId,
state: true,
success: function (res) {
console.log("开始监听成功。。。。")
if(res.errCode=='0'){
//订阅特征值
uni.onBLECharacteristicValueChange(function(data){
// data.characteristicId
// data.deviceId
// data.serviceId
// data.value
console.log("监听到特征值:"+JSON.stringify(data));
if(these.featrueValueCallback){
these.featrueValueCallback(data);
}
});
}
}
});
}
if (callback) {
callback(deviceId);
}
}
var linkState = function(res) {
console.log("获取已连接的设备回调" + JSON.stringify(res))
let flag = res.devices.find(function(v) {
if (v.deviceId == deviceId) {
return true;
}
return false;
});
if (flag) {
console.log("设备状态已连接");
lindedCallback(deviceId);
return;
} else {
console.log("设备未连接");
linkDevice(deviceId);
}
}
var linkDevice = function(id) {
console.log("正在连接"+id);
uni.createBLEConnection({
deviceId: id,
timeout: 30000,
success: function(info) {
console.log("连接成功");
uni.setBLEMTU({
deviceId: id,
mtu: 512,
success: () => {
console.log("mtu设置成功");
if(linkedDevices){
console.log("11111"+JSON.stringify(linkedDevices));
f = linkedDevices.find(function (v) {
return v.deviceId == id;
});
}else{
console.log("22222")
f=null;
}
if (!f) {
console.log("缓存中没有找到该设备")
these.getLinkBlue(function (res) {
if (res.devices && res.devices.length) {
let f = res.devices.find(function (v) {
return v.deviceId == id;
});
linkedDevices.push(f);
uni.setStorageSync(key, JSON.stringify(linkedDevices));
getService(id);
}
});
} else {
console.log("缓存中已连接过");
if (!f.services) {
getService(id);
} else {
lindedCallback(id);
}
}
},
fail: function() {
console.log("mtu设置失败")
}
});
},
fail: function(ex) {
if (error) {
console.log("蓝牙连接失败" + JSON.stringify(error));
error(ex);
}
}
});
}
//获取服务
var getService = function(id) {
var repeatCnt = 0;
var startgetService = function() {
uni.getBLEDeviceServices({
deviceId: id,
success: function(res) {
if (res.services && res.services.length > 0) {
console.log("获取到服务:" + JSON.stringify(res));
linkedDevices.find(function(v) {
if (v.deviceId == id) {
v.services = res.services;
}
});
uni.setStorageSync(key, JSON.stringify(linkedDevices));
var promises = [];
for (var i = 0; i < res.services.length; i++) {
let service = res.services[i];
promises.push(getFeatrus(id, service.uuid));
}
Promise.all(promises)
.then(results => {
console.log('所有操作成功完成', results);
lindedCallback(id);
})
.catch(error => {
console.error('至少一个操作失败', error);
});
} else {
repeatCnt++;
if (repeatCnt > 5) {
lindedCallback(id);
return;
}
setTimeout(function() {
startgetService(id);
}, 500);
}
}
})
}
setTimeout(function() {
startgetService(id);
}, 1000);
}
//获取特性
var getFeatrus = function(id, serviceId) {
var promise = new Promise((resolve, reject) => {
uni.getBLEDeviceCharacteristics({
deviceId: id,
serviceId: serviceId,
success: (res) => {
console.log("获取到特征:" + JSON.stringify(res));
//写特征
let writeChar = res.characteristics.find(char =>
char.uuid.indexOf("FFE1") > -1
);
//通知特征
let notiChar = res.characteristics.find(char =>
char.uuid.indexOf("FFE2") > -1
);
linkedDevices.find(function(v) {
if (v.deviceId == id) {
if (!v.Characteristics) {
v.Characteristics = [];
}
v.Characteristics = v.Characteristics.concat(res
.characteristics);
if (writeChar) {
v.writeServiceId = serviceId;
v.wirteCharactId = writeChar.uuid;
}
if (notiChar) {
v.notifyServiceid = serviceId;
v.notifyCharactId = notiChar.uuid;
}
}
});
uni.setStorageSync(key, JSON.stringify(linkedDevices));
resolve(res);
},
fail: (ex) => {
console.log("获取特征出现异常:" + JSON.stringify(ex));
resolve(ex);
}
});
});
return promise;
}
//监测蓝牙状态变化
uni.onBLEConnectionStateChange(function(res) {
if (!res.connected) {
console.log("蓝牙断开连接" + res.deviceId + "");
// lindDevice(res.deviceId);
}
});
console.log("正在获取蓝牙适配器状态")
this.CheckBlue((res) => {
console.log("蓝牙状态:" + JSON.stringify(res));
if (res.available) {
this.getLinkBlue(linkState);
} else {
console.log("蓝牙适配器不可用,正在初始化");
this.OpenBlue(false, () => {
this.getLinkBlue(linkState);
}, () => {
console.log("请引导用户打开蓝牙");
these.showBluetoothGuide();
})
}
});
},
//断开连接
disconnectDevice: function(deviceId) {
var disconnect = function(id) {
uni.closeBLEConnection({
deviceId: id,
success: (res) => {
console.log("蓝牙连接已断开");
}
});
}
if (deviceId) {
disconnect(deviceId);
return;
}
//断开所有已连接的设备
this.getLinkBlue(function(res) {
if (res.devices && res.devices.length > 0) {
for (var i = 0; i < res.devices.length; i++) {
let item = res.devices[i];
disconnect(item.deviceId);
}
} else {
console.log("无连接设备");
}
});
},
//发送二进制数据
sendData: function(deviceid, buffer) {
console.log("准备向设备发送数据deviceid=" + deviceid);
return new Promise((resolve, reject) => {
if (!deviceid) {
reject(`deviceid为空请输入要发送的设备`);
return;
}
console.log("准备发送数据包");
let key = "linkedDevices";
var store = uni.getStorageInfoSync();
var f = store.keys.find(function(v) {
return v == key;
});
console.log("倒计时5");
var linkedDevices = [];
if (f) {
var str = uni.getStorageSync(key);
if (str) {
linkedDevices = JSON.parse(str);
}
}
console.log("倒计时4");
if (linkedDevices && linkedDevices.length && linkedDevices.length > 0) {
console.log("倒计时3");
f = linkedDevices.find(function(v) {
return v.deviceId == deviceid;
});
console.log("f=" + JSON.stringify(f));
// console.log("deviceid=" + deviceid);
console.log("倒计时2");
if (f) {
console.log("倒计时1");
uni.writeBLECharacteristicValue({
deviceId: f.deviceId,
serviceId: f.writeServiceId,
characteristicId: f.wirteCharactId,
value: buffer,
success: () => {
console.log("发送数据成功");
resolve();
},
fail: (err) => {
console.log("发送数据失败" + JSON.stringify(err));
reject(`发送数据失败: ${err.errMsg}`);
},
complete: function() {
console.log("发送数据complete");
}
});
} else {
reject(`已连接设备中无法找到此设备`);
// console.log("警报:已连接设备中无法找到此设备")
}
} else {
console.log("检测到未与设备建立连接");
reject(`检测到未与设备建立连接`);
}
});
},
sendDataNew: function(deviceid, serviceId, characteristicId, buffer) {
console.log("准备向设备发送数据deviceid=" + deviceid);
return new Promise((resolve, reject) => {
uni.writeBLECharacteristicValue({
deviceId: deviceid,
serviceId: serviceId,
characteristicId: characteristicId,
value: buffer,
success: () => {
console.log("发送数据成功");
resolve();
},
fail: (err) => {
console.log("发送数据失败" + JSON.stringify(err));
reject(`发送数据失败: ${err.errMsg}`);
},
complete: function() {
console.log("发送数据complete");
}
});
});
}
}

View File

@ -1,19 +0,0 @@
import request from '@/utils/request'
import qs from 'qs'
// 查询列表
export function fileInfo(params) {
return request({
url: '/app/file/list',
method: 'GET',
data: params
})
}
// 删除
export function fileDelete(ids) {
return request({
url: `/app/file/delete/${ids}`,
method: 'delete',
})
}

View File

@ -1,32 +0,0 @@
import request from '@/utils/request'
// 视频列表
export function listOperationVideos(params) {
return request({
url: '/app/operationVideo/listOperationVideos',
method: 'GET',
data: params
})
}
// 添加
export function addOperationVideo(data) {
return request({
url: '/app/operationVideo/addOperationVideo',
method: 'post',
data
})
}
// 编辑
export function editOperationVideo(data) {
return request({
url: '/app/operationVideo/editOperationVideo',
method: 'post',
data
})
}
// 删除视频列表
export function deleteOperationVideo(ids) {
return request({
url: `/app/operationVideo/deleteOperationVideo/${ids}`,
method: 'delete',
})
}

View File

@ -0,0 +1,258 @@
<template>
<view class="message-popup" :class="{ 'show': config.show }">
<view class="mask" @click="closeMenu" :class="{ 'show': config.show }"
:style="{backgroundColor:config.maskBgColor,display:(config.showMask?'':'none')}"
>
</view>
<view class="bottom-slide-menu" :style="{ backgroundColor: config.bgColor }" :class="{ 'show': config.show }"
@touchmove.stop.prevent>
<view class="menu-header" :class="config.showHeader?'':'displayNone'">
<view class="title" :style="{ color: config.textColor}">{{ config.title }}</view>
<view class="close-icon" @click="closeMenu"
:style="{display:(config.showClose?'':'none')}"
>
<image src="/static/Images/public/close.png" mode="aspectFit"></image>
</view>
</view>
<view class="menu-items" v-if="config.menuItems && config.menuItems.length>0">
<view class="menu-item" v-for="(item, index) in config.menuItems" :key="index" :style="{
color: config.textColor,
textAlign: config.textAlign,
height:config.itemHeight,
lineHeight:config.itemHeight,
paddingLeft: config.dividerMargin,
paddingRight: config.dividerMargin
}" @click="handleItemClick(item, index)">
<view class="p100" :style="{backgroundColor:config.activeIndex==index?config.itemBgColor:'',
justifyContent:config.textAlign
}">
<image v-if="item.icon" :src="item.icon" mode="aspectFit"></image>
<text>{{ item.text }}</text>
</view>
<view v-if="index<config.menuItems.length-1" class="menu-line"
:style="{paddingLeft: config.dividerMargin,paddingRight: config.dividerMargin}">
<view class="menu-divider"
:style="{borderTopWidth: config.dividerThickness,borderTopColor: config.dividerColor}">
</view>
</view>
</view>
</view>
<slot v-else></slot>
<view @click="btnClick" class="bottom-btn" :class="config.showBtn?'':'displayNone'"
:style="{ backgroundColor: config.btnBgColor ,color:config.btnTextColor}">
{{config.btnText}}
</view>
</view>
</view>
</template>
<script>
export default {
name: 'BottomSlideMenuPlus',
props: {
config: {
type: Object,
default: () => ({
show: false,//是否显示
showHeader: false,//是否显示标头
showMask:true,//是否显示mask
showDivider: false,//是否在两个项之间显示分隔线
showBtn: false,//是否显示底部按钮
showClose:false,//是否显示右上角关闭按钮
maskBgColor:'',//mask的颜色
menuItems: [],//菜单项 包含icon text
activeIndex: -1,//当前已选中的项编号
bgColor: '#2a2a2a',//主体背景
itemBgColor: '#3a3a3a',//各项被选中后的背景
textColor: '#ffffffde',//各项的文字颜色
textAlign: 'flex-start',//各项的文字居中方式
title: '',//header的文字
dividerColor: '#00000000',//分隔线颜色
dividerThickness: '0rpx',//分隔线宽度
dividerMargin: '10rpx',//分隔线距离两边的宽度
itemHeight: '80rpx',//各项的调试
btnBgColor: "#bbe600",//按钮颜色
btnText: "确定",//按钮文字
btnTextColor: "#000000",//按钮文字颜色
})
},
},
methods: {
closeMenu() {
this.$emit('close');
},
handleItemClick(item, index) {
this.$emit('itemClick', item, index);
},
btnClick() {
let item = null;
let index = null;
if (this.config.activeIndex > -1) {
item = this.config.menuItems[this.config.activeIndex];
}
index = this.config.activeIndex;
this.$emit('btnClick', item, index);
}
}
}
</script>
<style>
.message-popup {
position: relative;
display: none;
}
.message-popup.show {
display: block;
}
.p100 {
width: 100%;
height: 100%;
border-radius: 8rpx;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
justify-content: flex-start;
align-items: center;
box-sizing: border-box;
padding: 0rpx 20rpx;
}
.displayNone {
display: none !important;
}
.bottom-slide-menu {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
transition: transform 0.3s ease-out;
transform: translateY(100%);
border-radius: 16rpx 16rpx 0 0;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.mask {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9998;
width: 100%;
height: 100%;
}
.bottom-slide-menu.show {
transform: translateY(0);
}
.menu-header {
display: flex;
justify-content: center;
align-items: center;
padding-top: 30rpx;
/* border-bottom: 0.03125rem solid #eeeeee; */
flex-direction: row;
flex-wrap: nowrap;
width: 100%;
height: auto;
position: relative;
}
.title {
font-size: 32rpx;
font-weight: 600;
}
.close-icon {
width: 28rpx;
height: 28rpx;
position: absolute;
top: 30rpx;
right: 30rpx;
}
.close-icon image {
width: 100%;
height: 100%;
}
.menu-items {
padding: 20rpx 0;
}
.menu-item {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
flex-wrap: nowrap;
box-sizing: border-box;
font-size: 28rpx;
width: 100%;
box-sizing: border-box;
position: relative;
}
.menu-item image {
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
/* 分隔线样式 */
.menu-line {
width: 100%;
box-sizing: border-box;
position: absolute;
bottom: 0px;
height: auto;
}
.menu-divider {
border-top-style: solid;
width: 100%;
height: 0px;
}
.bottom-btn {
width: 100%;
height: 90rpx;
text-align: center;
color: rgba(35, 35, 35, 0.87);
font-family: PingFang SC;
font-size: 30rpx;
font-weight: 600;
line-height: 90rpx;
letter-spacing: 12rpx;
text-align: center;
}
</style>

View File

@ -0,0 +1,381 @@
<template>
<view class="message-popup" v-if="visible">
<view class="popup-mask" @click="handleMaskClick"></view>
<view
class="popup-content"
:style="{
backgroundColor: bgColor || getTypeStyle('bgColor'),
borderColor: borderColor || getTypeStyle('borderColor'),
color: textColor || getTypeStyle('textColor')
}"
>
<view v-if="showHeader&&headerTxt" class="header"
>{{headerTxt}}</view>
<view v-if="visibleClose" class="rightClose"
:style="{color:textColor || getTypeStyle('textColor')}"
@click="closeClick"
>x</view>
<view v-if="!visiblePrompt">
<image
v-if="iconUrl"
:src="iconUrl"
mode="aspectFit"
class="popup-icon"
:style="{ tintColor: textColor || getTypeStyle('textColor') }"
></image>
<view class="popup-message">{{ message }}</view>
</view>
<view v-else class="popup-prompt">
<text class="popup-prompt-title">{{ promptTitle || '请输入信息' }}</text>
<input
class="popup-prompt-input"
:placeholder="promptPlaceholder"
:value="modelValue"
@input="handleInput"
@confirm="handleButtonClick"
/>
</view>
<view class="popBtnContent">
<view
class="popup-button-cancel"
:style="{display:showCancel?'block':'none'}"
@click="handleCancelClick"
>{{ buttonCancelText?buttonCancelText:'取消' }}</view>
<view
class="popup-button"
:style="{
backgroundColor: buttonBgColor || getTypeStyle('buttonBgColor'),
color: buttonTextColor || getTypeStyle('buttonTextColor')
}"
@click="handleButtonClick"
>{{ buttonText }}</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'MessagePopup',
props: {
visible: {
type: Boolean,
default: false
},
visibleClose:{
type:Boolean,
default:true
},
visiblePrompt: {
type: Boolean,
default: false
},
promptTitle: String,
promptPlaceholder: {
type: String,
default: "请输入"
},
modelValue: {
type: String,
default: ''
},
type: {
type: String,
default: 'info',
validator: value => ['success', 'error', 'info','custom'].includes(value)
},
bgColor: String,
borderColor: String,
textColor: String,
buttonBgColor: String,
buttonTextColor: String,
iconUrl: String,
message: {
type: String,
default: ''
},
buttonText: {
type: String,
default: '确定'
},
buttonCancelText:{
type:String,
default:'取消'
},
showCancel:{
type:Boolean,
default:false
},
showHeader:{
type:Boolean,
default:false
},
headerTxt:{
type:String,
default:""
}
},
data() {
return {
inputValue: this.modelValue
}
},
watch: {
// 监听外部值变化
modelValue(newVal) {
this.inputValue = newVal
},
// 监听模式切换
visiblePrompt(newVal) {
console.log('[MessagePopup] visiblePrompt changed to:', newVal)
// 重置输入值
if (newVal) {
this.inputValue = this.modelValue
}
}
},
mounted() {
},
methods: {
getTypeStyle(styleType) {
const styles = {
success: {
bgColor: '#f0fff0',
borderColor: '#52c41a',
textColor: '#389e0d',
buttonBgColor: '#52c41a',
buttonTextColor: '#FFFFFF'
},
error: {
bgColor: '#fff0f0',
borderColor: '#ff4d4f',
textColor: '#cf1322',
buttonBgColor: '#ff4d4f',
buttonTextColor: '#FFFFFF'
},
info: {
bgColor: '#e6f7ff',
borderColor: '#1890ff',
textColor: '#0050b3',
buttonBgColor: '#1890ff',
buttonTextColor: '#FFFFFF'
}
}
return styles[this.type][styleType]
},
handleButtonClick() {
console.log('[MessagePopup] Button clicked with value:', this.inputValue)
this.$emit('buttonClick', this.inputValue)
},
handleMaskClick() {
console.log('[MessagePopup] Mask clicked')
this.$emit('maskClick')
},
closeClick(){
this.$emit('closePop')
},
handleCancelClick(){
this.$emit('cancelPop');
},
handleInput(e) {
this.inputValue = e.detail.value
this.$emit('update:modelValue', this.inputValue)
}
}
}
</script>
<style>
.message-popup {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.popup-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.popup-content {
position: relative;
width: 80%;
max-width: 400px;
border-radius: 12px;
padding: 30rpx;
background-color: white;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
border-width: 2rpx;
border-style: solid;
box-sizing: border-box;
z-index: 10;
}
.header{
width:100%;
color: rgba(255, 255, 255, 0.87);
font-family: PingFang SC;
font-size: 32rpx;
font-weight: 600;
height: 80rpx;
line-height: 80rpx;
letter-spacing: 0.07px;
text-align: center;
}
.rightClose{
position: absolute;
right: 20rpx;
top: 20rpx;
color: #FFFFFF;
font-size: 36rpx;
width: 24rpx;
height: 24rpx;
line-height: 24rpx;
}
.popup-icon {
width: 60rpx;
height: 60rpx;
margin: 20rpx auto;
display: block;
}
.popup-message {
font-size: 28rpx;
text-align: center;
padding: 20rpx 0rpx 30rpx 0rpx;
font-weight: 400;
letter-spacing: 0.07px;
}
.popup-prompt {
width: 100%;
}
.popup-prompt-title {
display: block;
font-size: 32rpx;
text-align: center;
margin-bottom: 20rpx;
font-weight: 500;
}
.popup-prompt-input {
width: 100%;
height: 80rpx;
line-height: 80rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 0 20rpx;
margin-bottom: 30rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.popBtnContent{
width:100%;
height: 60rpx;
margin-top: 20rpx;
display: flex
;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
justify-content: space-evenly;
align-items: center;
}
.popup-button {
width: 30%;
height: 60rpx;
line-height: 60rpx;
border-radius: 44rpx;
text-align: center;
font-size: 24rpx;
font-weight: 500;
letter-spacing: 4rpx;
border: none;
outline: none;
cursor: pointer;
}
.popup-button-cancel{
width: 30%;
height: 60rpx;
line-height: 60rpx;
border-radius: 44rpx;
text-align: center;
font-size: 24rpx;
font-weight: 500;
letter-spacing: 4rpx;
border: none;
outline: none;
cursor: pointer;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.5);
background-color: #00000000;
color: #FFFFFFde;
}
.popup-prompt{
width: 100%;
height: 60rpx;
line-height: 60rpx;
padding-top: 40rpx;
}
.popup-prompt-title{
color: rgba(255, 255, 255, 0.6);
text-align: right;
width: 30%;
float: left;
box-sizing: border-box;
white-space:nowrap;
padding-right: 10rpx;
}
.popup-prompt-input{
float: left;
width: 70%;
height: 60rpx;
line-height: 60rpx;
color: rgba(255, 255, 255, 0.87) ;
box-sizing: border-box;
border: 2rpx solid rgba(255, 255, 255, 0.4);
border-radius: 8rpx;
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<view class="message-popup" :class="{ 'show': config.show }">
<view class="mask" @click="closeProgress" :class="{ 'show': config.show }"
:style="{backgroundColor:config.maskBgColor,display:(config.showMask?'':'none')}">
</view>
<view class="progress-content" :style="{backgroundColor:config.contentBgColor}">
<text
:style="{color:config.txtColor,display:(config.showText?'':'none')}">{{config.curr}}/{{config.total}}</text>
<view class="jdWai"
:style="{borderRadius:config.borderRadius, backgroundColor:config.proBgColor,border:config.proBorder,height:config.height}">
<view class="jdNei"
:style="{borderRadius:config.borderRadius,height:config.height,backgroundColor:config.currBgColor,border:config.currBorder,width:(config.curr/config.total*100)+'%'}">
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: "Progress",
data() {
return {
};
},
props: {
config: {
type: Object,
default: () => ({
show: false, //是否显示
showMask: true, //是否显示mask
height:'20px',
maskBgColor: '#00000000', //mask背景颜色
contentBgColor: '#121212', //总背景颜色
showText: true, //是否显示当前进度的文字
txtColor: '#ffffffde', //文字的颜色
curr: 0, //当前进度
total: 100, //总进度
proBgColor: '#2a2a2a', //进度条底色,
proBorder: '', //进度条border
currBgColor: '#bbe600', //当前进度底色
currBorder: '', //当前进度border
borderRadius:'10px'
})
}
},methods: {
closeProgress() {
this.$emit('closeProgress');
}
}
}
</script>
<style>
.message-popup {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
display: none;
justify-content: center;
align-items: center;
}
.message-popup.show {
display: block;
}
.p100 {
width: 100%;
height: 100%;
border-radius: 8rpx;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
justify-content: flex-start;
align-items: center;
box-sizing: border-box;
padding: 0rpx 20rpx;
}
.displayNone {
display: none !important;
}
.mask {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9;
width: 100%;
height: 100%;
}
.progress-content {
position: relative;
width: 100%;
padding: 30rpx;
box-sizing: border-box;
z-index: 10;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
align-content: center;
align-items: center;
height: 100%;
justify-content: center;
}
.jdWai {
width: 100%;
}
.jdNei {
height: 100%;
}
</style>

View File

@ -22,7 +22,8 @@
"Barcode" : {},
"Camera" : {},
"Maps" : {},
"OAuth" : {}
"OAuth" : {},
"Geolocation" : {}
},
/* */
"distribute" : {
@ -58,7 +59,6 @@
},
/* ios */
"ios" : {
"appid": "uni.app.UNIA21EF43",
"privacyDescription" : {
"NSBluetoothPeripheralUsageDescription" : "需要蓝牙访问权限,用于设备通信",
"NSBluetoothAlwaysUsageDescription" : "需要蓝牙访问权限,用于设备通信"
@ -67,8 +67,21 @@
},
/* SDK */
"sdkConfigs" : {
"geolocation" : {},
"maps" : {},
"geolocation" : {
"amap" : {
"name" : "amapHG8nIFW5",
"__platform__" : [ "ios", "android" ],
"appkey_ios" : "065c43f02c7b627a74ad7dd23b16bb4f",
"appkey_android" : "d7d852dbda2b95f6f796fb9a711a9fee"
}
},
"maps" : {
"amap" : {
"name" : "amapHG8nIFW5",
"appkey_ios" : "065c43f02c7b627a74ad7dd23b16bb4f",
"appkey_android" : "d7d852dbda2b95f6f796fb9a711a9fee"
}
},
"oauth" : {},
"push" : {}
},

View File

@ -6,6 +6,17 @@
"navigationStyle": "custom"
}
},
{
"path" : "pages/6155/deviceDetail",
"style" :
{
"navigationBarTitleText" : "HBY 6155"
}
},
{
"path": "pages/common/index/index",
"style": {
@ -61,44 +72,56 @@
}
},
{
"path": "pages/common/operationVideo/index",
"path": "pages/6170/operationVideo/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/common/addvideo/index",
"path": "pages/6170/addvideo/index",
"style": {
"navigationStyle": "custom"
"navigationBarTitleText": "添加"
}
},
// 操作说明
{
"path": "pages/common/operatingInstruct/index",
"path": "pages/6170/editVideo/index",
"style": {
"navigationStyle": "custom"
"navigationBarTitleText": "编辑视频"
}
},
// 产品说明
{
"path": "pages/common/productDes/index",
"path": "pages/6170/operatingInstruct/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/6155/index",
"style": {
"navigationBarTitleText": "6155"
"path" : "pages/common/addBLE/addEquip",
"style" :
{
"navigationBarTitleText" : "添加设备"
}
},
{
"path": "pages/6155/bluetooth/bluetooth",
"style": {
"navigationStyle": "custom"
"path" : "pages/common/addBLE/LinkBle",
"style" :
{
"navigationBarTitleText" : "扫描到的设备"
}
},
{
"path" : "pages/6155/ImgCrop",
"style" :
{
"navigationBarTitleText" : "图像裁剪",
"navigationStyle": "custom",
"fullscreen": true
}
}
],
"tabBar": {
"color": "#fff",

93
pages/6155/ImgCrop.vue Normal file
View File

@ -0,0 +1,93 @@
<template>
<view>
<canvas canvas-id="splashCanvas"
style="width: 160px; height: 80px; z-index: 9999;position: fixed; visibility: visible;top: 0px;left:0px;"
</canvas>
<qf-image-cropper :src="src" :showAngle="false" :width="1600" :height="800" fileType="jpg"
@crop="handleCrop" :gpu="true">
</qf-image-cropper>
</view>
</template>
<script>
import QfImageCropper from '@/uni_modules/qf-image-cropper/components/qf-image-cropper/qf-image-cropper.vue';
export default {
components: {
QfImageCropper
},
data() {
return {
src:"",
Statu: false
}
},
onLoad:function(option){
const eventChannel = this.getOpenerEventChannel();
var these=this;
eventChannel.on('checkImg', function(data) {
console.log("我收到你的消息了,消息内容是:"+JSON.stringify(data));
these.src=data.data;
})
},
methods: {
handleCrop(e) {
var these=this;
this.Statu = true;
console.log("裁剪完成");
console.log(e.tempFilePath);
const ctx = uni.createCanvasContext('splashCanvas', these);
console.log("1111111");
// 绘制图片到canvas
ctx.drawImage(e.tempFilePath, 0, 0, 160, 80);
console.log("22222");
ctx.draw(false, () => {
// 获取canvas像素数据
console.log("444444");
setTimeout(() => {
uni.canvasGetImageData({
canvasId: 'splashCanvas',
x: 0,
y: 0,
width: 160,
height: 80,
success: (res) => {
// 处理像素数据并发送
console.log("res.data.length="+res.data.length);
// this.processAndSendImageData(res.data).then(
// resolve).catch(reject);
const eventChannel = these.getOpenerEventChannel();
eventChannel.emit('ImgCutOver',res.data);
uni.navigateBack();
},
fail: (err) => {
reject(`获取canvas数据失败: ${err.errMsg}`);
}
});
}, 500); // 等待canvas绘制完成
});
console.log("333333");
}
}
}
</script>
<style>
.canvas {
width: 1600px;
height: 800px;
border: 1px solid #ccc;
}
</style>

View File

@ -1,257 +0,0 @@
<template>
<view class="">
<custom-navbar :showBack="true"></custom-navbar>
<view class="pageContent" :style="{ paddingTop: navBarHeight + 'px' }">
<view class="scanner">
<view class="pulse"></view>
<view class="device-name">
<!-- <image src="/static/images/svgg.png" class="bluth"></image> -->
</view>
</view>
<view class="device">我的设备</view>
<scroll-view class="device-list" scroll-y>
<block v-for="(item, index) in deviceList" :key="index" :ref="'swipeItem_' + index">
<!-- 设备卡片内容保持不变 -->
<view @click.stop="handleFile(item)" class="device-card">
<view class="device-header">
<view class="deviceIMG">
<!-- <image src="/static/images/bip.png" mode="" class="IMG"></image> -->
</view>
<view class="device-name1">
<view>设备:001-00</view>
<view class="ID">
<view>ID:0222222</view>
</view>
</view>
</view>
</view>
</block>
</scroll-view>
<view class="device">其他设备</view>
<scroll-view class="device-list" scroll-y>
<block v-for="(item, index) in 3" :key="index" :ref="'swipeItem_' + index">
<!-- 设备卡片内容保持不变 -->
<view @click.stop="handleFile(item)" class="device-card">
<view class="device-header">
<view class="deviceIMG">
<!-- <image src="/static/images/bip.png" mode="" class="IMG"></image> -->
</view>
<view class="device-name1">
<view>设备:001-00</view>
<view class="ID">
<view>ID:0222222</view>
</view>
</view>
</view>
</view>
</block>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
// 这里可以添加数据绑定
navBarHeight: 70 + uni.getSystemInfoSync().statusBarHeight,
deviceList:[{}]
}
},
methods: {
// 扫描
}
}
</script>
<style scoped>
.pageContent {
min-height: 100vh;
background-color: rgb(18, 18, 18);
padding: 30rpx;
}
.scanner {
position: relative;
width: 600rpx;
height: 600rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
/* 设备名称 */
.device-name {
position: relative;
z-index: 10;
width: 104rpx;
height: 104rpx;
background: rgba(187, 230, 0, 1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.bluth {
width: 36rpx;
height: 36rpx;
}
/* 脉冲波纹效果 */
.pulse {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
border: 1px solid rgba(206, 242, 49, 0.05);
background: rgba(206, 242, 49, 0.05);
animation: pulse 4s infinite cubic-bezier(0.2, 0, 0.2, 1);
}
/* 通过伪元素创建4层波纹 */
.pulse::before,
.pulse::after,
.pulse .ripple-1,
.pulse .ripple-2 {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 50%;
background: rgba(187, 230, 0, 0.4);
border: 1px solid rgba(206, 242, 49, 0.04);
animation: inherit;
}
.pulse::before {
animation-delay: 1s;
}
.pulse::after {
animation-delay: 2s;
}
.pulse .ripple-1 {
animation-delay: 0.5s;
}
.pulse .ripple-2 {
animation-delay: 1.5s;
}
@keyframes pulse {
0% {
transform: scale(0.1);
opacity: 0.8;
}
70% {
opacity: 0.4;
}
100% {
transform: scale(1);
opacity: 0.5;
}
}
.device {
color: rgba(255, 255, 255, 0.6);
margin-bottom: 20rpx;
}
/* 设备卡片 */
.device-card {
background-color: rgb(26, 26, 26);
border-radius: 16rpx;
margin-bottom: 20rpx;
box-sizing: border-box;
position: relative;
}
.device-header {
display: flex;
align-items: center;
margin-bottom: 15rpx;
padding: 30rpx 0 10rpx 30rpx;
}
.device-name1 {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.87);
margin-left:40rpx;
line-height: 50rpx;
width: 70%;
}
.ID {
color: rgba(255, 255, 255, 0.6);
font-size: 26rpx;
display: flex;
justify-content: space-between;
}
.device-status {
width: 122rpx;
height: 52rpx;
font-size: 26rpx;
border-radius: 0px 8px 0px 8px;
background-color: rgb(42, 42, 42);
position: absolute;
top: 0rpx;
right: 0rpx;
text-align: center;
line-height: 52rpx;
}
.circle {
width:8rpx;
height: 40rpx;
position: absolute;
right:25rpx;
top:85rpx;
}
.online {
color: rgb(187, 230, 0);
}
.device-id {
font-size: 26rpx;
color: #999;
margin-bottom: 20rpx;
display: block;
}
.device-info {
display: flex;
justify-content: space-evenly;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.87);
position: relative;
padding: 0rpx 0rpx 30rpx 30rpx;
}
.deviceIMG {
/* width: 100rpx;
height: 100rpx;
border-radius: 16rpx;
position: relative;
background-color: rgba(42, 42, 42, 0.6);
display: flex;
align-items: center; */
}
.IMG {
width:38rpx;
height: 80rpx;
margin-left: 17%;
}
</style>

1334
pages/6155/deviceDetail.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
<template>
<div>
<h1>6155</h1>
</div>
</template>
<script setup>
</script>

View File

@ -1,60 +1,50 @@
<template>
<view>
<custom-navbar :title="navTitle" :showBack="true" backgroundColor="#202020" color="#FFFFFF"></custom-navbar>
<view class="device-page" :style="{ paddingTop: navBarHeight + 'px' }">
<!-- 表单内容 -->
<view class="form-content">
<!-- 名称输入框 -->
<view class="input-group">
<text class="label">名称</text>
<input class="input" type="text" placeholder="请输入名称" v-model="name" />
</view>
<!-- 视频链接输入框 -->
<view class="input-group">
<text class="label">视频链接</text>
<input class="input" type="text" placeholder="请输入视频链接" v-model="videoLink" />
</view>
<view class="device-page">
<!-- 表单内容 -->
<view class="form-content">
<!-- 名称输入框 -->
<view class="input-group">
<text class="label">名称</text>
<input class="input" type="text" placeholder="请输入名称" v-model="name" />
</view>
<!-- 保存按钮 -->
<button class="save-button" @click="saveData"> </button>
<!--===================== 保存成功提示框================== -->
<view class="agreement-mask" v-if="showModal">
<!-- 上传画面弹窗 -->
<view class="agreement-popupC">
<view class="popup-content">
<image src="/static/images/path2.png" mode="" class="path2"></image>
<view class="popup-Title">
<view>保存成功</view>
</view>
</view>
<!-- 按钮组 -->
<view class="popup-buttons">
<button class="btn agreeBtn" @click="handleBtn">确定</button>
</view>
</view>
<!-- 视频链接输入框 -->
<view class="input-group">
<text class="label">视频链接</text>
<input class="input" type="text" placeholder="请输入视频链接" v-model="videoLink" />
</view>
</view>
<!-- 保存按钮 -->
<button class="save-button" @click="saveData"> </button>
<!--===================== 保存成功提示框================== -->
<view class="agreement-mask" v-if="showModal">
<!-- 上传画面弹窗 -->
<view class="agreement-popupC">
<view class="popup-content">
<image src="/static/images/path2.png" mode="" class="path2"></image>
<view class="popup-Title">
<view>保存成功</view>
</view>
</view>
<!-- 按钮组 -->
<view class="popup-buttons">
<button class="btn agreeBtn" @click="handleBtn">确定</button>
</view>
</view>
</view>
</view>
</template>
<script>
import {
addOperationVideo,
editOperationVideo
} from '@/api/common/operationVideo/index.js'
export default {
data() {
return {
navBarHeight: 70 + uni.getSystemInfoSync().statusBarHeight,
navTitle: '添加',
name: '',
videoLink: '',
showModal: false,
deviceID: "",
id: ''
showModal: false
};
},
methods: {
@ -69,63 +59,20 @@
})
return false
}
if (this.videoLink == '') {
if (this.videoLink == '') {
uni.showToast({
icon: 'none',
title: '视频链接不为空'
})
return false
}
let data = {
videoName: this.name,
videoUrl: this.videoLink,
deviceId: this.deviceID,
id: this.id
}
//
const apiMethod = this.editData ? editOperationVideo : addOperationVideo;
console.log('apiMethod:', apiMethod);
apiMethod(data).then(res => {
if (res.code == 200) {
this.showModal = true;
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
uni.showToast({
title: res.msg,
icon: 'none'
});
}
}).catch(err => {
uni.showToast({
title: '网络请求失败',
icon: 'none'
});
});
this.showModal = true
uni.navigateBack()
},
//
handleBtn() {
this.showModal = false
}
},
onLoad(options) {
if (options.item) {
// item
this.editData = JSON.parse(decodeURIComponent(options.item));
//
this.name = this.editData.videoName || '';
this.videoLink = this.editData.videoUrl || '';
this.deviceID = this.editData.deviceId || '';
this.id = this.editData.id
//
this.navTitle = '编辑';
} else if (options.id) {
this.deviceID = options.id;
this.navTitle = '添加';
this.editData = null; // null
}
}
};
</script>

View File

@ -2,7 +2,7 @@
<view>
<!-- 使用自定义导航栏 -->
<custom-navbar :title="navTitle" :showBack="true" color="#FFFFFF" rightIcon="/static/images/path.png"
@right-click="uploadStartup"></custom-navbar>
@right-click="uploadStartup"></custom-navbar>
<view class="device-detail-container" :style="{ paddingTop: navBarHeight + 'px' }">
<!-- 设备电量信息 -->
<view class="battery-section">
@ -231,8 +231,7 @@
lightModeB: false,
lightModeC: false, //激光提示框
items: [],
isFormExpanded: true, // 默认展开
deviceID: ''
isFormExpanded: true // 默认展开
}
},
computed: {
@ -315,19 +314,17 @@
// 操纵说明
operatingInst() {
uni.navigateTo({
url: `/pages/common/operatingInstruct/index?id=${this.deviceID}`
url: '/pages/6170/operatingInstruct/index'
})
},
// 产品参数
productparams() {
uni.navigateTo({
url: `/pages/common/productDes/index?id=${this.deviceID}`
})
},
// 操作视频
operatingVideo() {
uni.navigateTo({
url: `/pages/common/operationVideo/index?id=${this.deviceID}`
url: '/pages/6170/operationVideo/index'
})
},
@ -341,10 +338,6 @@
handleDisagree() {
this.lightModeC = false
},
},
onLoad(options) {
console.log(options.id) // 输出: 123
this.deviceID = options.id
}
}
@ -556,8 +549,7 @@
transform: translate(-17%, -100%);
}
.uni-file-picker__container {
.uni-file-picker__container{
width: 200rpx !important;
border: 1px solid rgb(58, 58, 58);
}

View File

@ -0,0 +1,11 @@
<template>
<view>
<text>编辑视频</text>
</view>
</template>
<script>
</script>
<style scoped>
</style>

View File

@ -0,0 +1,350 @@
<template>
<!-- 使用自定义导航栏 -->
<view>
<custom-navbar :title="navTitle" :showBack="true" backgroundColor="#202020" color="#FFFFFF" right-text="编辑"
right-color="rgba(255, 255, 255, 0.87)" @right-click="handleEdit"></custom-navbar>
<view class="device-page" :style="{ paddingTop: navBarHeight + 'px' }">
<view class="pathContent">
<image src="/static/images/path3.png" mode="" class="path3"></image>
</view>
</view>
<!--上传文件弹框 -->
<view class="agreement-mask" v-if="opeartShow">
<!-- 上传画面弹窗 -->
<view class="agreement-popup">
<!-- 标题 -->
<view class="popup-title">编辑</view>
<view class="popup-content">
<view v-for="(item, index) in items" :key="index">
<view class="item" :class="{'selected': index === selectedItemIndex}"
@click="handleItemAction(index)"> <!-- 修改点击事件 -->
<image :src="item.image" class="setIMG"></image>
{{item.text}}
<!-- 隐藏的文件输入仅用于上传模式 -->
<!-- <input v-if="index === 0" type="file" ref="fileInput" style="display: none"
@change="onFileSelect"> -->
<uni-file-picker v-if="index === 0" limit="1" file-mediatype="all"></uni-file-picker>
</view>
</view>
</view>
<!-- 按钮组 -->
<view class="popup-buttons">
<button class="agree" @click="handleupload">确定</button>
</view>
</view>
</view>
<view class="agreement-mask" v-if="opeartSuccess">
<view class="agreement-popupC">
<view class="popup-content">
<image src="/static/images/path5.png" mode="" class="path2"></image>
<view class="popup-Title">
<view>上传成功</view>
</view>
</view>
<!-- 按钮组 -->
<view class="popup-buttons">
<button class="btn agreeBtn" @click="handleBtn">确定</button>
</view>
</view>
</view>
<!-- =============删除文件列表文件=============== -->
<view class="agreement-mask" v-if="opeartDelect">
<!-- 上传画面弹窗 -->
<view class="agreement-popup">
<view class="popup-title">删除文件</view>
<view class="popup-content">
<!-- 设备列表 -->
<scroll-view class="device-list" scroll-y>
<view class="device-card" v-for="(item, index) in delectList" :key="index"
@click="toggleSelect(index)">
<!-- 复选框 -->
<view class="checkbox" :class="{ checked: item.checked }">
<uni-icons v-if="item.checked" type="checkmarkempty" size="18"
color="rgb(0, 0, 0)"></uni-icons>
</view>
<!-- 设备信息 -->
<view class="device-content">
<view class="device-header">
<view class="device-name">
<view>设备:001-00</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 按钮组 -->
<view class="popup-buttons">
<button class="agree" @click="handleopearBtn">确定</button>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
navTitle: '操作说明',
navBarHeight: 70 + uni.getSystemInfoSync().statusBarHeight,
opeartShow: false,
items: [{
text: '点击选择上传文件',
selected: true,
image: '/static/images/upload.png'
},
{
text: '删除文件',
selected: false,
image: '/static/images/delete.png'
}
],
selectedItemIndex: 0, // 默认选中第一个
fileInput: false,
opeartSuccess: false,
opeartDelect: false,
delectList: Array(6).fill().map((_, i) => ({
id: `1321615${i}`,
checked: false,
showConfirm: false
})),
}
},
methods: {
// 编辑
handleEdit() {
this.opeartShow = true
},
handleItemAction(index) {
this.selectedItemIndex = index;
if (index === 0) {
// 调用 Uni-app 文件选择 API
// uni.chooseFile({
// count: 1, // 最多选择1个文件
// success: (res) => {
// const file = res.tempFiles[0];
// this.items[0].text = `已选择: ${file.name}`;
// this.selectedFile = file; // 存储文件对象
// }
// });
} else {
// 这里是走删除弹框列表
this.opeartDelect = true //显示批量删除弹框列表
this.opeartShow = false //隐藏编辑弹框
}
},
onFileSelect(e) {
const files = e.target.files;
if (files.length > 0) {
// 这里拿到文件对象,可以立即上传或显示文件名
console.log("选择的文件:", files[0].name);
// 示例显示文件名在item.text中
this.items[0].text = `已选择: ${files[0].name}`;
}
},
// 确认
handleupload() {
this.opeartSuccess = true
//this.opeartShow = false;
},
// 上传成功后确实弹框提示确认
handleBtn() {
this.opeartSuccess = false
},
toggleSelect(index) {
this.delectList[index].checked = !this.delectList[index].checked
this.$forceUpdate()
},
// 批量删除文件确认按钮
handleopearBtn() {
this.opeartDelect = false
}
}
}
</script>
<style scoped>
.device-page {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: rgb(18, 18, 18);
padding: 30rpx;
}
.pathContent {
background-color: rgb(52, 52, 52);
width: 100%;
min-height: 87vh;
}
.files-button uni-button {
background: red !important;
position: absolute;
left: 50rpx;
top: 50rpx
}
.path2 {
width: 64rpx;
height: 64rpx;
margin-right: 10px;
}
.popup-Title {
color: rgba(255, 255, 255, 0.87);
padding: 20rpx 0rpx;
font-size: 32rpx;
}
.path3 {
width: 172rpx;
height: 114rpx;
transform: translate(-50%, -50%);
position: absolute;
left: 50%;
top: 50%
}
/* 遮罩层 */
.agreement-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
/* 弹窗主体 */
.agreement-popup {
width: 100%;
height: 50%;
background-color: rgb(42, 42, 42);
border-radius: 60rpx 60rpx 0rpx 0rpx;
padding: 40rpx;
box-sizing: border-box;
position: absolute;
bottom: 0rpx;
}
.agreement-popupC {
width: 70%;
background-color: rgb(42, 42, 42);
border-radius: 40rpx;
padding: 30rpx;
box-sizing: border-box;
border: 1rpx solid rgba(187, 230, 0, 0.3);
}
.popup-title {
color: rgba(255, 255, 255, 0.87);
text-align: center;
}
.agree {
background-color: rgb(187, 230, 0);
color: #232323;
border: none;
font-size: 24rpx;
height: 88rpx;
line-height: 88rpx;
font-size: 32rpx;
width: 100%;
position: absolute;
bottom: 0rpx;
left: 0rpx
}
.item {
padding: 10px;
margin: 5px 0;
display: flex;
align-items: center;
cursor: pointer;
transition: background-color 0.3s;
color: #fff;
position: relative;
}
.setIMG {
width: 88rpx;
height: 88rpx;
margin-right: 10px;
}
.item.selected {
background-color: rgb(58, 58, 58);
color: rgba(255, 255, 255, 0.87);
border-radius: 8rpx;
}
.popup-content {
margin-top: 20rpx;
text-align: center;
}
/* 通用按钮样式 */
.btn {
flex: 1;
height: 70rpx;
line-height: 70rpx;
border-radius: 40rpx;
font-size: 32rpx;
margin: 10rpx auto;
width: 200rpx;
}
/* 同意按钮 */
.agreeBtn {
background-color: rgb(187, 230, 0);
color: #232323;
border: none;
font-size: 24rpx;
}
/* 删除文件,弹框列表样式文件
*/
.device-card {
position: relative;
display: flex;
align-items: center;
}
.device-content {
border-radius: 8rpx;
background: rgba(58, 58, 58, 1);
margin-bottom: 20rpx;
position: relative;
align-items: center;
padding: 20rpx;
width: 93%;
color: rgba(255, 255, 255, 0.87);
text-align: left;
}
.checkbox {
width: 40rpx;
height: 40rpx;
border: 2rpx solid rgba(255, 255, 255, 0.5);
margin-right: 20rpx;
border-radius: 4rpx;
display: flex;
align-items: center;
justify-content: center;
margin-top: -25rpx;
}
.checkbox.checked {
background-color: rgb(187, 230, 0);
border-color: rgb(187, 230, 0);
}
</style>

View File

@ -1,20 +1,21 @@
<template>
<view>
<!-- 使用自定义导航栏 -->
<custom-navbar title="操作视频" :showBack="true" backgroundColor="#202020" color="#FFFFFF"></custom-navbar>
<custom-navbar :title="navTitle" :showBack="true" backgroundColor="#202020" color="#FFFFFF" right-text="编辑"
right-color="rgba(255, 255, 255, 0.87)" @right-click="handleEdit"></custom-navbar>
<view class="device-page" :style="{ paddingTop: navBarHeight + 'px' }">
<scroll-view scroll-y>
<uni-swipe-action>
<uni-swipe-action >
<block v-for="(item, index) in videoList" :key="index">
<uni-swipe-action-item :right-options="item.showConfirm ? confirmOptions : deleteOptions"
@click="handleSwipeClick($event, index,item)" class="content">
<view class="image-box" @click="openVideoUrl(item.videoUrl)">
@click="handleSwipeClick($event, index)" class="content">
<view class="image-box" @click="openVideoUrl(item.url)">
<view class="deviceIMG">
<image src="/static/images/video.png" mode="" class="video"></image>
</view>
<view class="">
<view class="file-title">{{item.videoName}}</view>
<view class="file-baidu">{{item.videoUrl}}</view>
<view class="file-title">{{item.title}}</view>
<view class="file-baidu">{{item.url}}</view>
</view>
</view>
<image src="/static/images/cires.png" class="circle"></image>
@ -30,17 +31,42 @@
</template>
<script>
import {
listOperationVideos,
deleteOperationVideo
} from '@/api/common/operationVideo/index.js'
export default {
data() {
return {
navTitle: '操作视频',
navBarHeight: 70 + uni.getSystemInfoSync().statusBarHeight,
videoList: [],
deviceID: '',
videoList: [{
id: 1,
title: '使用教程1',
url: 'https://www.bilibili.com/variety/?spm_id_from=333.1007.0.0',
showConfirm: false
},
{
id: 2,
title: '使用教程2',
url: 'www.baidu.com',
showConfirm: false
},
{
id: 3,
title: '使用教程3',
url: 'www.baidu.com',
showConfirm: false
},
{
id: 4,
title: '使用教程4',
url: 'www.baidu.com',
showConfirm: false
},
{
id: 5,
title: '使用教程5',
url: 'www.baidu.com',
showConfirm: false
}
],
deleteOptions: [{
text: '编辑',
style: {
@ -70,49 +96,38 @@
}
},
methods: {
//
handleEdit() {
uni.showToast({
title: '点击了导航栏编辑',
icon: 'none'
});
},
//
addvideo() {
uni.navigateTo({
url: `/pages/common/addvideo/index??id=${this.deviceID}`
url: '/pages/6170/addvideo/index'
})
},
//
handleSwipeClick(e, index, item) {
console.log(item, 'eeeee');
handleSwipeClick(e, index) {
const {
content
} = e;
//
if (content.text === '确认删除') {
let ids = item.id
let data ={
ids:item.id
}
deleteOperationVideo(ids).then((res) => {
if (res.code == 200) {
uni.showToast({
title: res.msg,
icon: 'success'
});
this.getData()
} else {
uni.showToast({
title: res.msg,
icon: 'success'
});
}
})
// return;
this.videoList.splice(index, 1);
uni.showToast({
title: '删除成功',
icon: 'success'
});
return;
}
//
if (content.text === '编辑') {
console.log('编辑');
uni.navigateTo({
url: `/pages/common/addvideo/index?item=${encodeURIComponent(JSON.stringify(item))}`
});
this.editVideo(this.videoList[index].id);
}
//
else if (content.text === '删除') {
@ -127,6 +142,12 @@
}
},
//
editVideo(id) {
uni.navigateTo({
url: `/pages/6170/editVideo/index?id=${id}`
});
},
// URL
openVideoUrl(url) {
console.log(url, 'url333');
@ -145,29 +166,6 @@
});
}
},
getData() {
let data = {
deviceId: this.deviceID
}
listOperationVideos(data).then((res) => {
console.log(res, 'resss');
if (res.code == 200) {
this.videoList = res.data
} else {
uni.showToast({
title: res.msg,
icon: 'none'
})
}
})
}
},
onShow() {
this.getData()
},
onLoad(options) {
this.deviceID = options.id
}
}
</script>

View File

@ -0,0 +1,22 @@
<template>
<view>
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,598 @@
<template>
<view class="content">
<view class="topAnimate">
<view class="animate center">
<view class="animateContent">
<view class="circle"></view>
<view class="circle"></view>
<view class="circle"></view>
</view>
<view class="imgContent center">
<view class="img center">
<image src="/static/images/bluetooth.png" class="titleIco" mode="aspectFit">
</image>
</view>
</view>
</view>
</view>
<view class="mainContent">
<view class="p100">
<view class="lblTitle">配对设备</view>
<view class="list" style="margin-bottom: 30rpx;">
<view class="item " v-for="item, index in PairEquip" v-show="PairEquip.length>0">
<view class="leftImg ">
<image src="/static/images/BLEAdd/device.png" class="titleIco" mode="aspectFit">
</image>
</view>
<view class="centertxt ">
<view class="name" v-text="item.name"></view>
<view class="id" v-text="item.deviceId"></view>
</view>
<view class="rightIco center">
<image src="/static/images/BLEAdd/linked.png" class="img" mode="aspectFit">
</image>
</view>
</view>
<view v-show="PairEquip.length==0" class="item center">
<view class="noLink">无已配对设备</view>
</view>
</view>
<view class="lblTitle">搜索设备</view>
<view class="list searchList">
<view class="item" v-on:click="Link(item,index)" v-for="item, index in EquipMents"
v-show="!item['linkStatu']">
<view class="leftImg ">
<image src="/static/images/BLEAdd/device.png" class="titleIco" mode="aspectFit">
</image>
</view>
<view class="centertxt ">
<view class="name" v-text="item.name?item.name:'Unnamed'"></view>
<view class="id" v-text="item.deviceId"></view>
</view>
<view class="rightIco center">
<image :src="isItemLink(item,index)" class="img" mode="aspectFit">
</image>
</view>
</view>
</view>
</view>
</view>
<BottomSlideMenuPlus :config="Status.BottomMenu">
<view class="openBlue">
<view class="txt">
当前手机蓝牙关闭,是否打开以扫描附近的蓝牙设备?
</view>
<view class="btns">
<view class="btn cancel" @click="Status.BottomMenu.show=false">
<view>取消</view>
</view>
<view class="ok btn" @click="gotoSetting">
<view>开启</view>
</view>
</view>
</view>
</BottomSlideMenuPlus>
</view>
</template>
<script>
import ble from '../../../api/6155/BlueHelper.js';
import request from '../../../utils/request.js';
export default {
data() {
return {
Status: {
BottomMenu: {
show: false,
showHeader: false,
menuItems: [],
activeIndex: -1,
bgColor: '#2a2a2a',
itemBgColor: '#3a3a3a',
textColor: '#ffffffde',
textAlign: 'flex-start',
title: '主灯模式',
showDivider: false,
dividerColor: '#00000000',
dividerThickness: '0rpx',
dividerMargin: '10rpx',
itemHeight: '80rpx',
type: '',
showBtn: false,
btnBgColor: "#bbe600",
btnText: "确定",
btnTextColor: "#232323de",
showMask: true,
maskBgColor: '#00000066',
showClose: false
}
},
PairEquip: [], //已配对设备
EquipMents: [] //搜索出来的设备
}
},
computed: {
},
onHide: function() {
ble.StopSearch();
},
onBackPress: (e) => {
ble.StopSearch();
ble.disconnectDevice();
},
onShow: function() {
// return;
var these = this;
uni.getStorage({
key: "linkedDevices",
success: (res) => {
this.PairEquip = JSON.parse(res.data);
},
fail: (ex) => {
this.PairEquip = [];
}
});
if (process.env.UNI_PLATFORM == 'mp-weixin' ||
process.env.UNI_PLATFORM == 'mp-alipay' ||
process.env.UNI_PLATFORM == 'app-plus' ||
process.env.UNI_PLATFORM == 'app'
) {
//打开蓝牙开始搜索设备
ble.OpenBlue(true, () => {
ble.StartSearch(function(arr) {
arr = arr.devices;
for (var i = 0; i < arr.length; i++) {
arr[i].linkStatu = false;
let f = these.EquipMents.find(function(v) {
return v.deviceId == arr[i].deviceId;
});
if (!f) {
these.EquipMents.push(arr[i]);
}
}
console.log("设备列表:" + JSON.stringify(these.EquipMents));
});
},
() => {
these.showOpenSetting();
}
)
} else {
console.log('当前环境:' + process.env.UNI_PLATFORM + '不支持蓝牙');
}
},
methods: {
isItemLink: function(item, index) {
let src = '/static/images/BLEAdd/noLink.png';
if (this.PairEquip && this.PairEquip instanceof Array) {
if (this.PairEquip.length > 0) {
let f = this.PairEquip.find(function(v) {
return v.deviceId == item.deviceId;
});
if (f) {
src = '/static/images/BLEAdd/linked.png';
}
}
}
return src;
},
alert: function(title, content, callback) {
if (!title) {
title = '提示'
}
if (!content) {
content = title;
}
uni.showModal({
title: title,
content: content,
success: function(res) {
if (res.confirm) {
console.log('用户点击确定');
} else if (res.cancel) {
console.log('用户点击取消');
}
if (callback) {
callback(res);
}
}
});
},
showOpenSetting: function() {
this.Status.BottomMenu.show = true;
},
gotoSetting: function() {
this.Status.BottomMenu.show = false;
ble.showBluetoothGuide(false);
},
Link: function(item, index) {
var these = this;
if (process.env.UNI_PLATFORM == 'mp-weixin' ||
process.env.UNI_PLATFORM == 'mp-alipay' ||
process.env.UNI_PLATFORM == 'app-plus' ||
process.env.UNI_PLATFORM == 'app'
) {
uni.showLoading({
title: "正在连接",
mask: true
});
setTimeout(() => {
ble.LinkBlue(item.deviceId, function() {
let c = these.PairEquip.find(function(v) {
return v.deviceId == item.deviceId;
});
if (!c) {
these.PairEquip.push(item);
uni.setStorage({
key: 'linkedDevices',
data: JSON.stringify(these.PairEquip),
success: () => {}
});
}
// 调用绑定设备接口
let promise = request({
url: '/app/device/bind',
method: 'POST',
data: {
deviceImei: '',
deviceMac: item.deviceId,
communicationMode: '1', //0是4g,1是蓝牙
}
});
promise.then((res) => {
console.log("1111" + JSON.stringify(res));
if (res.code == 0) {
uni.hideLoading()
uni.showToast({
title: res.data,
icon: 'success'
});
} else {
uni.showToast({
title: res.msg,
});
}
}).catch((ex) => {
uni.showToast({
title: '出现了未知的异常,操作失败',
});
});
}, (ex) => {
uni.hideLoading();
});
}, 0);
} else {
these.alert("提示", "当前平台不支持蓝牙");
}
}
}
}
</script>
<style>
.noLink {
text-align: center;
width: 100%;
font-size: 28rpx;
}
.content {
background-color: #1d1d1d;
color: #ffffffde;
box-sizing: border-box;
overflow: hidden;
width: 100%;
min-height: 100vh;
height: auto;
}
.p100 {
width: 100%;
height: 100%;
}
.fleft {
float: left;
}
.fright {
float: right;
}
.clear {
clear: both;
}
.center {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
justify-content: center;
align-items: center;
}
.topAnimate {
position: absolute;
left: 0rpx;
width: 100%;
height: 400rpx;
top: 20px;
/* 距离视口顶部 20px 时固定 */
z-index: 100;
/* 确保元素显示在最上层 */
}
.animate {
width: 100%;
height: 400rpx;
position: relative;
}
.animate .animateContent {
position: relative;
z-index: 0;
}
.animate .imgContent {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
}
.animate .imgContent .img {
height: 100rpx;
width: 100rpx;
background: #bbe600;
border-radius: 50%;
}
.animate .titleIco {
width: 36rpx;
height: 36rpx;
}
.circle {
position: absolute;
border-radius: 50%;
background-color: #bbe60033;
animation: expand 3s infinite ease-out;
}
.circle:nth-child(2) {
animation-delay: 1s;
}
.circle:nth-child(3) {
animation-delay: 2s;
}
@keyframes expand {
0% {
width: 0;
height: 0;
opacity: 0.8;
transform: translate(-50%, -50%);
}
100% {
width: 18.75rem;
height: 18.75rem;
opacity: 0;
transform: translate(-50%, -50%);
}
}
.mainContent {
padding: 30rpx;
box-sizing: border-box;
width: 100%;
height: calc(100% - 400rpx);
overflow-y: scroll;
position: absolute;
top: 400rpx;
left: 0rpx;
}
.mainContent .p100 {}
.mainContent .lblTitle {
color: #ffffffde;
font-family: PingFang SC;
font-size: 28rpx;
font-weight: 700;
text-align: left;
width: 100%;
height: 28rpx;
line-height: 28rpx;
}
.list {
min-height: 120rpx;
}
.searchList {
width: 100%;
height: calc(100% - 186rpx);
overflow-y: scroll;
}
.list .item {
width: 100%;
height: 120rpx;
box-sizing: border-box;
padding: 30rpx;
border-radius: 8px;
background: #1a1a1a;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: center;
margin-top: 10rpx;
}
.list .item .leftImg {
width: 100rpx;
height: 70rpx;
}
.list .item .centertxt {
width: calc(100% - 150rpx);
height: 100%;
padding-left: 10rpx;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: space-evenly;
align-items: flex-start;
align-content: flex-start;
}
.list .item .rightIco {
width: 50rpx;
height: 100%;
}
.list .item .leftImg .titleIco {
width: 100%;
height: 100%;
}
.list .item .name {
color: #ffffffde;
font-family: PingFang SC;
font-size: 26rpx;
font-weight: 400;
line-height: 50rpx;
text-align: left;
}
.list .item .id {
color: #ffffff99;
font-family: PingFang SC;
font-size: 24rpx;
font-weight: 400;
line-height: 30rpx;
text-align: left;
}
.list .item .rightIco .img {
width: 50rpx;
height: 50rpx;
}
.openBlue {
padding: 30rpx;
width: 100%;
box-sizing: border-box;
height: auto;
}
.openBlue .txt {
color: rgba(255, 255, 255, 0.87);
font-family: PingFang SC;
font-size: 28rpx;
font-weight: 400;
letter-spacing: 0.14rpx;
text-align: center;
}
.openBlue .btns {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
align-items: center;
justify-content: space-evenly;
margin-top: 50rpx;
width: 100%;
height: auto;
}
.openBlue .btn {
border-radius: 91rpx;
box-sizing: border-box;
width: 25%;
height: 60rpx;
text-align: center;
font-family: PingFang SC;
font-size: 28rpx;
letter-spacing: 12rpx;
display: flex !important;
align-items: center;
letter-spacing: 0.375rem;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
justify-content: center;
}
.openBlue .cancel {
border: 1px solid rgba(255, 255, 255, 1);
color: rgba(255, 255, 255, 1);
}
.openBlue .ok {
background-color: #BBE600;
color: #232323;
}
</style>

View File

@ -24,8 +24,8 @@
style="height:80vh;">
<uni-swipe-action ref="swipeAction">
<block v-for="(item, index) in deviceList" :key="index" :ref="'swipeItem_' + index">
<uni-swipe-action-item :right-options="Options" @click="handleSwipeClick($event, item, index)"
class="device-card">
<uni-swipe-action-item :right-options="Options"
@click.stop="handleSwipeClick($event, item, index)" class="device-card">
<!-- 设备卡片内容保持不变 -->
<!-- <view @click.stop="handleFile(item)">
<view class="device-header">
@ -192,6 +192,7 @@
getTab() {
deviceTypeList({}).then((res) => {
if (res.code == 200) {
console.log("deviceTypeList=" + JSON.stringify(res.data));
this.tabs = [{
id: '',
name: '全部设备',
@ -264,9 +265,18 @@
});
break;
case 'bluetooth':
// uni.navigateTo({
// url: 'pages/common/addBLE/AddDevice',
// success:(res)=>{
// res.eventChannel.emit('key', { data: 'data from starter page' })
// },fail: (ex) => {
// console.log("跳转失败了",JSON.stringify(ex));
// }
// });
uni.navigateTo({
url: '/pages/6155/bluetooth/bluetooth'
});
url: "/pages/common/addBLE/addEquip"
})
break;
}
},
@ -357,31 +367,46 @@
})
},
handleFile(item) {
console.log(item, 'item');
console.log('item' + JSON.stringify(item));
// communicationMode 0是4G 1是蓝牙
if (item.communicationMode == 0) {
uni.navigateTo({
url: `/pages/6170/deviceControl/index?id=${item.id}`
})
} else {
uni.navigateTo({
url: '/pages/6155/index'
})
url: `/pages/6170/deviceControl/index?id=${item.id}`
});
return;
}
if (item.typeName == '6155') {
uni.navigateTo({
url: "/pages/6155/deviceDetail",
events: {
ack: function(data) {
}
},
success: (res) => {
res.eventChannel.emit('detailData', {
data: item
});
}
})
}
},
onIntall() {
this.page = 1;
this.finished = false;
this.getData(); // 重新加载第一页数据
}
},
onIntall() {
this.page = 1;
this.finished = false;
this.getData(); // 重新加载第一页数据
onShow() {
this.getTab()
this.onIntall()
}
},
onShow() {
this.getTab()
this.onIntall()
}
}
}
</script>
<style>

View File

@ -23,6 +23,7 @@
class="agreeT">隐私政策</a></text>
</view>
</view>
<!-- 登录按钮 -->
<button class="login-btn" @click="handleLogin">
登录
@ -61,12 +62,12 @@
return {
showView: false,
//codebtn: '获取验证码',
phone: '', //手机号码
code: "", //验证码
phone: '17671332251', //手机号码
code: "123456", //验证码
agreed: false,
isCounting: false,
countdown: 0,
isChecked: false,
isChecked: true,
showAgreement: false, // 控制弹窗显示
}
},
@ -113,6 +114,7 @@
},
// 登录
async handleLogin() {
console.log('33333');
if (this.phone == '') {
uni.showToast({
title: '手机号不能为空',
@ -144,6 +146,7 @@
smsCode: this.code,
tenantId: '894078' //租户ID
})
console.log(res, 'res44444');
if (res.code == 200) {
uni.hideLoading()
uni.setStorageSync('token', res.data.access_token) // 缓存token

View File

@ -1,404 +0,0 @@
<template>
<!-- 使用自定义导航栏 -->
<view>
<custom-navbar title="操作说明" :showBack="true" backgroundColor="#202020" color="#FFFFFF"
:right-text="isDeleteMode ? '完成' : '删除'" right-color="rgba(255, 255, 255, 0.87)"
@right-click="toggleDeleteMode">
</custom-navbar>
<view class="device-page" :style="{ paddingTop: navBarHeight + 'px' }">
<view class="example-body">
<!-- 图片列表 -->
<view class="image-list">
<view class="image-item" v-for="(image, index) in instructionImages" :key="index"
@click="handleImageClick(index,image)">
<image :src="image.fileUrl" mode="aspectFit" class="instruction-image"
:class="{ 'image-selected': isDeleteMode && selectedImages.includes(index) }"></image>
<!-- 多选模式下的选中标记 -->
<view class="checkmark" v-if="isDeleteMode && selectedImages.includes(index)">
<image src="/static/images/delete-icon.png" mode="aspectFill"></image>
</view>
</view>
<!-- 上传按钮非删除模式显示 -->
<view class="upload-btn" v-if="!isDeleteMode">
<uni-file-picker ref="filePicker" v-model="fileList" @select="selectFile" limit="9"
class="custom-file-picker"></uni-file-picker>
</view>
</view>
</view>
</view>
<!-- 底部删除按钮多选模式显示 -->
<view class="delete-footer" v-if="isDeleteMode && selectedImages.length > 0">
<button @click="deleteSelectedImages">删除({{selectedImages.length}})</button>
</view>
<!-- 删除弹框 -->
<view class="agreement-mask" v-if="deleteShow">
<view class="agreement-popupC">
<view class="popup-content">
<image src="/static/images/dell.png" mode="" class="svg"></image>
<uni-icon class="trash"></uni-icon>
<view>
<view class="popup-Title">确定删除所选设备</view>
</view>
</view>
<!-- 按钮组 -->
<view class="popup-buttons">
<button class="btn agreeBtn" @click="handleBtn">确定</button>
</view>
</view>
</view>
</view>
</template>
<script>
import {
baseURL,
getToken,
clientid
} from '@/utils/request'
import {
fileInfo,
fileDelete
} from '@/api/common/operatingInstruct/index.js'
export default {
data() {
return {
navBarHeight: 70 + uni.getSystemInfoSync().statusBarHeight,
fileList: [],
deviceID: "",
instructionImages: [],
isDeleteMode: false, // 是否删除模式
selectedImages: [], // 选中的图片索引
deleteShow: false,
ImageData: [],
filePicker: ''
}
},
methods: {
// 选择文件
selectFile(e) {
console.log('选择文件:', e)
// const file = e.tempFiles[0].file
const files = e.tempFiles.map(item => item.file)
console.log('files', files);
this.uploadFile(files)
},
// 上传图片
uploadFile(files) {
this.$refs.filePicker.clearFiles() // 通过ref操作uni-file-picker组件
uni.showLoading({
title: '上传中...'
})
files.forEach(file => {
uni.uploadFile({
url: baseURL + '/app/file/upload',
filePath: file.path || file.url,
name: 'files',
formData: {
deviceId: this.deviceID,
fileType: '1' //文件类型(1:操作说明2:产品参数)
},
header: {
'Authorization': 'Bearer ' + getToken(),
'clientid': clientid(),
},
complete: (res) => {
console.log(res, 'resss');
try {
const responseData = JSON.parse(res.data);
if (responseData.code === 200) {
uni.showToast({
title: responseData.msg,
icon: 'success'
});
this.$refs.filePicker.clearFiles(); // 清空已选文件
this.callOtherApi();
} else {
uni.showToast({
title: responseData.msg,
icon: 'none'
});
}
} catch (e) {
uni.showToast({
title: '上传失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
}
})
})
},
// 获取图片列表
callOtherApi() {
let data = {
deviceId: this.deviceID,
fileType: '1' //文件类型(1:操作说明2:产品参数)
}
fileInfo(data).then((res) => {
if (res.code == 200) {
this.instructionImages = res.data
}
})
},
// 切换删除模式
toggleDeleteMode() {
this.isDeleteMode = !this.isDeleteMode
if (!this.isDeleteMode) {
this.selectedImages = []
}
},
// 图片点击处理
handleImageClick(index, item) {
if (this.isDeleteMode) {
// 删除模式下处理多选
const selectedIndex = this.selectedImages.indexOf(index)
if (selectedIndex > -1) {
this.selectedImages.splice(selectedIndex, 1)
this.ImageData.splice(selectedIndex, 1) // 保持同步删除
} else {
this.selectedImages.push(
index
)
this.ImageData.push(item) // 保持同步添加
}
} else {
// 正常模式下预览图片
uni.previewImage({
current: this.instructionImages[index].fileUrl,
urls: this.instructionImages.map(item => item.fileUrl)
})
}
},
// 删除选中的图片
deleteSelectedImages() {
if (this.selectedImages.length === 0) return
this.deleteShow = true
},
async handleBtn() {
try {
uni.showLoading({
title: '删除中...',
mask: true
})
// 构造删除请求数据
const ids = this.ImageData.map(item => item.id).join(',')
// 调用删除接口
const res = await fileDelete(ids)
if (res.code === 200) {
uni.showToast({
title: '删除成功',
icon: 'success'
})
// 从后往前删除,避免索引变化
this.selectedImages
.sort((a, b) => b.index - a.index)
.forEach(item => {
this.instructionImages.splice(item.index, 1)
})
this.callOtherApi()
// 重置状态
this.selectedImages = []
this.isDeleteMode = false
this.deleteShow = false
} else {
uni.showToast({
title: res.msg,
icon: 'none'
})
this.deleteShow = false
}
} catch (error) {} finally {
uni.hideLoading()
}
}
},
onLoad(options) {
this.deviceID = options.id
this.callOtherApi()
}
}
</script>
<style scoped>
.device-page {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: rgb(18, 18, 18);
padding: 30rpx;
}
.example-body {
margin-top: 20rpx;
}
/* 图片列表 */
.image-list {
display: flex;
flex-wrap: wrap;
margin: -10rpx;
}
/* 图片项 */
.image-item {
width: 33.33%;
padding: 10rpx;
position: relative;
}
/* 图片样式 */
.instruction-image {
width: 180rpx;
height: 180rpx;
border-radius: 10rpx;
background-color: #2a2a2a;
}
/* 选中效果 */
.image-selected {
opacity: 0.7;
}
/* 选中标记 */
.checkmark {
position: absolute;
bottom: 20rpx;
right: 40rpx;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
.checkmark image {
width: 24rpx;
height: 24rpx;
}
/* 上传按钮 */
.upload-btn {
width: 33.33%;
padding: 10rpx;
}
/* 底部删除按钮 */
.delete-footer {
position: fixed;
bottom: 20rpx;
left: 0;
right: 0;
height: 100rpx;
display: flex;
justify-content: center;
align-items: center;
}
.delete-footer button {
width: 90%;
height: 88rpx;
line-height: 88rpx;
background: rgba(187, 230, 0, 1);
background: rgba(187, 230, 0, 1);
border-radius: 90px;
}
/* 遮罩层 */
.agreement-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
/* 弹窗主体 */
.agreement-popup {
width: 100%;
height: 50%;
background-color: rgb(42, 42, 42);
border-radius: 60rpx 60rpx 0rpx 0rpx;
padding: 40rpx;
box-sizing: border-box;
position: absolute;
bottom: 0rpx;
}
.popup-Title {
color: rgba(255, 255, 255, 0.86);
text-align: center;
padding: 30rpx 0rpx;
}
.popup-buttons {
display: flex;
text-align: center;
}
.agreement-popupC {
width: 60%;
background-color: rgb(42, 42, 42);
border-radius: 40rpx;
padding: 30rpx;
text-align: center;
border: 1px solid rgba(255, 200, 78, 0.3);
}
.popup-flex {
display: flex;
white-space: nowrap;
color: rgba(255, 255, 255, 0.87);
height: 50rpx;
padding: 30rpx;
}
.popup-input {
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 12rpx;
margin-left: 15rpx;
padding: 10rpx 0rpx;
font-size: 28rpx;
}
.svg {
width: 58rpx;
height: 62rpx;
}
/* 通用按钮样式 */
.btn {
height: 60rpx;
line-height: 60rpx;
border-radius: 40rpx;
font-size: 24rpx;
margin: 10rpx auto;
text-align: center;
}
/* 同意按钮 */
.agreeBtn {
background: #FFC84E;
color: #232323;
border: none;
width: 170rpx !important;
}
</style>

View File

@ -1,404 +0,0 @@
<template>
<!-- 使用自定义导航栏 -->
<view>
<custom-navbar title="产品说明" :showBack="true" backgroundColor="#202020" color="#FFFFFF"
:right-text="isDeleteMode ? '完成' : '删除'" right-color="rgba(255, 255, 255, 0.87)"
@right-click="toggleDeleteMode">
</custom-navbar>
<view class="device-page" :style="{ paddingTop: navBarHeight + 'px' }">
<view class="example-body">
<!-- 图片列表 -->
<view class="image-list">
<view class="image-item" v-for="(image, index) in instructionImages" :key="index"
@click="handleImageClick(index,image)">
<image :src="image.fileUrl" mode="aspectFit" class="instruction-image"
:class="{ 'image-selected': isDeleteMode && selectedImages.includes(index) }"></image>
<!-- 多选模式下的选中标记 -->
<view class="checkmark" v-if="isDeleteMode && selectedImages.includes(index)">
<image src="/static/images/delete-icon.png" mode="aspectFill"></image>
</view>
</view>
<!-- 上传按钮非删除模式显示 -->
<view class="upload-btn" v-if="!isDeleteMode">
<uni-file-picker ref="filePicker" v-model="fileList" @select="selectFile" limit="9"
class="custom-file-picker"></uni-file-picker>
</view>
</view>
</view>
</view>
<!-- 底部删除按钮多选模式显示 -->
<view class="delete-footer" v-if="isDeleteMode && selectedImages.length > 0">
<button @click="deleteSelectedImages">删除({{selectedImages.length}})</button>
</view>
<!-- 删除弹框 -->
<view class="agreement-mask" v-if="deleteShow">
<view class="agreement-popupC">
<view class="popup-content">
<image src="/static/images/dell.png" mode="" class="svg"></image>
<uni-icon class="trash"></uni-icon>
<view>
<view class="popup-Title">确定删除所选设备</view>
</view>
</view>
<!-- 按钮组 -->
<view class="popup-buttons">
<button class="btn agreeBtn" @click="handleBtn">确定</button>
</view>
</view>
</view>
</view>
</template>
<script>
import {
baseURL,
getToken,
clientid
} from '@/utils/request'
import {
fileInfo,
fileDelete
} from '@/api/common/operatingInstruct/index.js'
export default {
data() {
return {
navBarHeight: 70 + uni.getSystemInfoSync().statusBarHeight,
fileList: [],
deviceID: "",
instructionImages: [],
isDeleteMode: false, // 是否删除模式
selectedImages: [], // 选中的图片索引
deleteShow: false,
ImageData: [],
filePicker: ''
}
},
methods: {
// 选择文件
selectFile(e) {
console.log('选择文件:', e)
// const file = e.tempFiles[0].file
const files = e.tempFiles.map(item => item.file)
console.log('files', files);
this.uploadFile(files)
},
// 上传图片
uploadFile(files) {
this.$refs.filePicker.clearFiles() // 通过ref操作uni-file-picker组件
uni.showLoading({
title: '上传中...'
})
files.forEach(file => {
uni.uploadFile({
url: baseURL + '/app/file/upload',
filePath: file.path || file.url,
name: 'files',
formData: {
deviceId: this.deviceID,
fileType: '2' //文件类型(1:操作说明2:产品参数)
},
header: {
'Authorization': 'Bearer ' + getToken(),
'clientid': clientid(),
},
complete: (res) => {
console.log(res, 'resss');
try {
const responseData = JSON.parse(res.data);
if (responseData.code === 200) {
uni.showToast({
title: responseData.msg,
icon: 'success'
});
this.$refs.filePicker.clearFiles(); // 清空已选文件
this.callOtherApi();
} else {
uni.showToast({
title: responseData.msg,
icon: 'none'
});
}
} catch (e) {
uni.showToast({
title: '上传失败',
icon: 'none'
});
} finally {
uni.hideLoading();
}
}
})
})
},
// 获取图片列表
callOtherApi() {
let data = {
deviceId: this.deviceID,
fileType: '2' //文件类型(1:操作说明2:产品参数)
}
fileInfo(data).then((res) => {
if (res.code == 200) {
this.instructionImages = res.data
}
})
},
// 切换删除模式
toggleDeleteMode() {
this.isDeleteMode = !this.isDeleteMode
if (!this.isDeleteMode) {
this.selectedImages = []
}
},
// 图片点击处理
handleImageClick(index, item) {
if (this.isDeleteMode) {
// 删除模式下处理多选
const selectedIndex = this.selectedImages.indexOf(index)
if (selectedIndex > -1) {
this.selectedImages.splice(selectedIndex, 1)
this.ImageData.splice(selectedIndex, 1) // 保持同步删除
} else {
this.selectedImages.push(
index
)
this.ImageData.push(item) // 保持同步添加
}
} else {
// 正常模式下预览图片
uni.previewImage({
current: this.instructionImages[index].fileUrl,
urls: this.instructionImages.map(item => item.fileUrl)
})
}
},
// 删除选中的图片
deleteSelectedImages() {
if (this.selectedImages.length === 0) return
this.deleteShow = true
},
async handleBtn() {
try {
uni.showLoading({
title: '删除中...',
mask: true
})
// 构造删除请求数据
const ids = this.ImageData.map(item => item.id).join(',')
// 调用删除接口
const res = await fileDelete(ids)
if (res.code === 200) {
uni.showToast({
title: '删除成功',
icon: 'success'
})
// 从后往前删除,避免索引变化
this.selectedImages
.sort((a, b) => b.index - a.index)
.forEach(item => {
this.instructionImages.splice(item.index, 1)
})
this.callOtherApi()
// 重置状态
this.selectedImages = []
this.isDeleteMode = false
this.deleteShow = false
} else {
uni.showToast({
title: res.msg,
icon: 'none'
})
this.deleteShow = false
}
} catch (error) {} finally {
uni.hideLoading()
}
}
},
onLoad(options) {
this.deviceID = options.id
this.callOtherApi()
}
}
</script>
<style scoped>
.device-page {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: rgb(18, 18, 18);
padding: 30rpx;
}
.example-body {
margin-top: 20rpx;
}
/* 图片列表 */
.image-list {
display: flex;
flex-wrap: wrap;
margin: -10rpx;
}
/* 图片项 */
.image-item {
width: 33.33%;
padding: 10rpx;
position: relative;
}
/* 图片样式 */
.instruction-image {
width: 180rpx;
height: 180rpx;
border-radius: 10rpx;
background-color: #2a2a2a;
}
/* 选中效果 */
.image-selected {
opacity: 0.7;
}
/* 选中标记 */
.checkmark {
position: absolute;
bottom: 20rpx;
right: 40rpx;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
.checkmark image {
width: 24rpx;
height: 24rpx;
}
/* 上传按钮 */
.upload-btn {
width: 33.33%;
padding: 10rpx;
}
/* 底部删除按钮 */
.delete-footer {
position: fixed;
bottom: 20rpx;
left: 0;
right: 0;
height: 100rpx;
display: flex;
justify-content: center;
align-items: center;
}
.delete-footer button {
width: 90%;
height: 88rpx;
line-height: 88rpx;
background: rgba(187, 230, 0, 1);
background: rgba(187, 230, 0, 1);
border-radius: 90px;
}
/* 遮罩层 */
.agreement-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
/* 弹窗主体 */
.agreement-popup {
width: 100%;
height: 50%;
background-color: rgb(42, 42, 42);
border-radius: 60rpx 60rpx 0rpx 0rpx;
padding: 40rpx;
box-sizing: border-box;
position: absolute;
bottom: 0rpx;
}
.popup-Title {
color: rgba(255, 255, 255, 0.86);
text-align: center;
padding: 30rpx 0rpx;
}
.popup-buttons {
display: flex;
text-align: center;
}
.agreement-popupC {
width: 60%;
background-color: rgb(42, 42, 42);
border-radius: 40rpx;
padding: 30rpx;
text-align: center;
border: 1px solid rgba(255, 200, 78, 0.3);
}
.popup-flex {
display: flex;
white-space: nowrap;
color: rgba(255, 255, 255, 0.87);
height: 50rpx;
padding: 30rpx;
}
.popup-input {
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 12rpx;
margin-left: 15rpx;
padding: 10rpx 0rpx;
font-size: 28rpx;
}
.svg {
width: 58rpx;
height: 62rpx;
}
/* 通用按钮样式 */
.btn {
height: 60rpx;
line-height: 60rpx;
border-radius: 40rpx;
font-size: 24rpx;
margin: 10rpx auto;
text-align: center;
}
/* 同意按钮 */
.agreeBtn {
background: #FFC84E;
color: #232323;
border: none;
width: 170rpx !important;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 B

View File

@ -20,7 +20,7 @@ let connected = false
//测试
let service_uuid = "0000AE30-0000-1000-8000-00805F9B34FB"
let write_characteristic = "0000AE03-0000-1000-8000-00805F9B34FB"
let write_characteristic = "0000AE03-0000-1000-8000-00805F9B34FB"
let notify_characteristic = "0000AE03-0000-1000-8000-00805F9B34FB"
let notify_characteristic_2 = "0000AE02-0000-1000-8000-00805F9B34FB"//获取设备电量和设备mac地址
@ -123,15 +123,18 @@ String.prototype.transFloat = function() {
return str.indexOf('.') != -1 ? parseFloat(str).toFixed(1) : str
}
// //uni
uni.onBluetoothAdapterStateChange(function(res) {
if (!res.available) {
isOpen = false
isBleOn = false
} else {
isBleOn = true
}
})
//
if (uni.getSystemInfoSync().platform){
uni.onBluetoothAdapterStateChange(function(res) {
if (!res.available) {
isOpen = false
isBleOn = false
} else {
isBleOn = true
}
})
}

View File

@ -0,0 +1,67 @@
## 2.2.42024-06-21
* 新增 reverseRotatable 属性,是否支持逆向翻转
* 修复 `2.1.7` 版本导致旋转后图片没有自动适配裁剪框的问题
## 2.2.32024-06-21
* 新增 gpu 属性,是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题
* 修复 组件使用 `v-if` 并设置 `src` 属性时可能会出现图片渲染位置存在偏差的问题
## 2.2.22024-06-21
* 优化 组件实例 chooseImage 方法支持传参
* 修复 组件使用 `v-if` 时组件无非正常渲染的问题
## 2.2.12024-06-15
* 修复 H5平台不支持手势拖动图片的问题
## 2.2.02024-05-31
* 修复 APP平台 `vue2` 项目因 `2.1.9` 版本修复 `vue3` 项目bug而引发的问题
## 2.1.92024-05-29
* 修复 APP平台 `vue3` 项目因 uniapp `renderjs` 中未支持条件编译导致运行了H5平台代码报错的问题
## 2.1.82024-05-29
* 新增 zIndex 属性,调整组件层级
* 新增 组件内容插槽
* 优化 微信小程序平台动态修改元素style时的多余内容
## 2.1.72024-05-28
* 新增 checkRange 属性,当 checkRange=false 时允许图片位置超出裁剪边界
* 新增 minScale 属性,图片最小缩放倍数,当 minScale<0 时可使图片宽高不再受裁剪区域宽高限制
* 新增 backgroundColor 属性生成图片背景色如果裁剪区域没有完全包含在图片中时不设置该属性生成图片存在一定的透明块
* 优化 动态修改图片宽高但没有传入src时尺寸适应问题
* 修复 APP平台通过 `this.$ownerInstance` 获取组件实例时机过早其值为 `undefined` 导致报错界面没有正常渲染的问题
## 2.1.62023-04-16
* 修复 组件使用 v-show 指令会导致选择图片后初始位置严重偏位的问题
## 2.1.52023-04-15
* 新增 兼容APP平台
## 2.1.42023-03-13
* 新增 fileType 属性用于指定生成文件的类型只支持 'jpg' 'png'默认为 'png'
* 新增 delay 属性微信小程序平台使用 `Canvas 2D` 绘制时控制图片从绘制到生成所需时间
* 优化 当生成图片的尺寸宽/高超过 Canvas 2D 最大限制1365*1365则将画布尺寸缩放在限制范围内绘制完成后输出目标尺寸
* 优化 旋转图标指示方向与实际旋转方向不符
## 2.1.32023-02-06
* 优化 vue3支持
## 2.1.22023-02-03
* 新增 navigation 属性H5平台当 showAngle true 使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 必须将此值设为 false 否则四个可拉伸角的触发位置会有偏差
* 修复 H5平台部分设备已知iPhone11以下机型拍照的图片缩放时会闪动的问题
## 2.1.12022-12-06
* 修复 横屏适配问题
## 2.1.02022-12-06
* 新增 兼容H5平台使用 renderjs 响应手势事件
## 2.0.02022-12-05
* 重构 插件使用 WXS 响应手势事件
* 新增 图片翻转
* 新增 拉伸裁剪框放大图片
* 新增 监听PC鼠标滚轮触发缩放
* 新增 圆形圆角矩形的图片裁剪
* 优化 图片缩放移动端以双指触摸中心点为缩放中心点PC端以鼠标所在点为缩放中心点
* 优化 裁剪框样式
* 优化 图片位置拖动 支持边界回弹效果滑动时可滑出边界释放时回弹到边界
* 优化 生成图片使用新版 Canvas 2D 接口

View File

@ -0,0 +1,738 @@
/**
* 图片编辑器-手势监听
* 1. 支持编译到app-vueuni-app 2.5.5及以上版本、H5上
*/
/** 图片偏移量 */
var offset = { x: 0, y: 0 };
/** 图片缩放比例 */
var scale = 1;
/** 图片最小缩放比例 */
var minScale = 1;
/** 图片旋转角度 */
var rotate = 0;
/** 触摸点 */
var touches = [];
/** 图片布局信息 */
var img = {};
/** 系统信息 */
var sys = {};
/** 裁剪区域布局信息 */
var area = {};
/** 触摸行为类型 */
var touchType = '';
/** 操作角的位置 */
var activeAngle = 0;
/** 裁剪区域布局信息偏移量 */
var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
/** 元素ID */
var elIds = {
'imageStyles': 'crop-image',
'maskStylesList': 'crop-mask-block',
'borderStyles': 'crop-border',
'circleBoxStyles': 'crop-circle-box',
'circleStyles': 'crop-circle',
'gridStylesList': 'crop-grid',
'angleStylesList': 'crop-angle',
}
/** 记录上次初始化时间戳排除APP重复更新 */
var timestamp = 0;
/** vue3 renderjs 条件编译无效,以此方式区别 APP 和 H5 */
// #ifdef H5
var platform = 'H5';
// #endif
// #ifdef APP
var platform = 'APP';
// #endif
/**
* 样式对象转字符串
* @param {Object} style 样式对象
*/
function styleToString(style) {
if(typeof style === 'string') return style;
var str = '';
for (let k in style) {
str += k + ':' + style[k] + ';';
}
return str;
}
/**
*
* @param {Object} instance 页面实例对象
* @param {Object} key 要修改样式的key
* @param {Object|Array} style 样式
*/
function setStyle(instance, key, style) {
// console.log('setStyle', instance, key, JSON.stringify(style))
// #ifdef APP-PLUS
if(platform === 'APP') {
if(Object.prototype.toString.call(style) === '[object Array]') {
for (var i = 0, len = style.length; i < len; i++) {
var el = window.document.getElementById(elIds[key] + '-' + (i + 1));
el && (el.style = styleToString(style[i]));
}
} else {
var el = window.document.getElementById(elIds[key]);
el && (el.style = styleToString(style));
}
}
// #endif
// #ifdef H5
if(platform === 'H5') instance[key] = style;
// #endif
}
/**
* 触发页面实例指定方法
* @param {Object} instance 页面实例对象
* @param {Object} name 方法名称
* @param {Object} obj 传递参数
*/
function callMethod(instance, name, obj) {
// #ifdef APP-PLUS
if(platform === 'APP') instance.callMethod(name, obj);
// #endif
// #ifdef H5
if(platform === 'H5') instance[name](obj);
// #endif
}
/**
* 计算两点间距
* @param {Object} touches 触摸点信息
*/
function getDistanceByTouches(touches) {
// 根据勾股定理求两点间距离
var a = touches[1].pageX - touches[0].pageX;
var b = touches[1].pageY - touches[0].pageY;
var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
// 求两点间的中点坐标
// 1. a、b可能为负值
// 2. 在求a、b时如用touches[1]减touches[0]则求中点坐标也得用touches[1]减a/2、b/2
// 3. 同理在求a、b时也可用touches[0]减touches[1]则求中点坐标也得用touches[0]减a/2、b/2
var x = touches[1].pageX - a / 2;
var y = touches[1].pageY - b / 2;
return { c, x, y };
};
/**
* 修正取值
* @param {Object} a
* @param {Object} b
* @param {Object} c
* @param {Object} reverse 是否反向
*/
function correctValue(a, b, c, reverse) {
return reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c);
}
/**
* 检查边界:限制 x、y 拖动范围,禁止滑出边界
* @param {Object} e 点坐标
*/
function checkRange(e) {
var r = rotate / 90 % 2;
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
var o = (img.height - img.width) / 2; // 宽高差值一半
return {
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, img.height < area.height),
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, img.width < area.width)
}
}
return {
x: correctValue(e.x, -img.width + area.width + area.left, area.left, img.width < area.width),
y: correctValue(e.y, -img.height + area.height + area.top, area.top, img.height < area.height)
}
};
/**
* 变更图片布局信息
* @param {Object} e 布局信息
*/
function changeImageRect(e) {
// console.log('changeImageRect', e)
offset.x += e.x || 0;
offset.y += e.y || 0;
if(e.check && area.checkRange) { // 检查边界
var point = checkRange(offset);
if(offset.x !== point.x || offset.y !== point.y) {
offset = point;
}
}
// 因频繁修改 width/height 会造成大量的内存消耗改为scale
// e.instance.imageStyles = {
// width: img.width + 'px',
// height: img.height + 'px',
// transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + ox) + 'px) rotate(' + rotate +'deg)'
// };
var ox = (img.width - img.oldWidth) / 2;
var oy = (img.height - img.oldHeight) / 2;
// e.instance.imageStyles = {
// width: img.oldWidth + 'px',
// height: img.oldHeight + 'px',
// transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
// };
setStyle(e.instance, 'imageStyles', {
width: img.oldWidth + 'px',
height: img.oldHeight + 'px',
transform: (img.gpu ? 'translateZ(0) ' : '') + 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px' + ') rotate(' + rotate +'deg) scale(' + scale + ')'
});
callMethod(e.instance, 'dataChange', {
width: img.width,
height: img.height,
x: offset.x,
y: offset.y,
rotate: rotate
});
};
/**
* 变更裁剪区域布局信息
* @param {Object} e 布局信息
*/
function changeAreaRect(e) {
// console.log('changeAreaRect', e)
// 变更蒙版样式
setStyle(e.instance, 'maskStylesList', [
{
left: 0,
width: (area.left + areaOffset.left) + 'px',
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.right + areaOffset.right) + 'px',
right: 0,
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: 0,
height: (area.top + areaOffset.top) + 'px',
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: (area.bottom + areaOffset.bottom) + 'px',
// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
bottom: 0,
'z-index': area.zIndex + 2
}
]);
// 变更边框样式
if(area.showBorder) {
setStyle(e.instance, 'borderStyles', {
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
});
}
// 变更参考线样式
if(area.showGrid) {
setStyle(e.instance, 'gridStylesList', [
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
}
]);
}
// 变更四个伸缩角样式
if(area.showAngle) {
setStyle(e.instance, 'angleStylesList', [
{
'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
}
]);
}
// 变更圆角样式
if(area.radius > 0) {
var radius = area.radius;
if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
radius = (area.width / 2);
} else { // 圆角矩形
if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
radius = Math.min(area.width / 2, area.height / 2, radius);
}
}
setStyle(e.instance, 'circleBoxStyles', {
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 2
});
setStyle(e.instance, 'circleStyles', {
'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
'border-radius': radius + 'px'
});
}
};
/**
* 缩放图片
* @param {Object} e 布局信息
*/
function scaleImage(e) {
// console.log('scaleImage', e)
var last = scale;
scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
if(last !== scale) {
img.width = img.oldWidth * scale;
img.height = img.oldHeight * scale;
// 参考问题有一个长4000px、宽4000px的四方形ABCDA点的坐标固定在(-2000,-2000)
// 该四边形上有一个点E坐标为(-100,-300)将该四方形复制一份并缩小到90%后,
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
e.x = (e.x - offset.x) * (1 - scale / last);
e.y = (e.y - offset.y) * (1 - scale / last);
changeImageRect(e);
return true;
}
return false;
};
/**
* 获取触摸点在哪个角
* @param {number} x 触摸点x轴坐标
* @param {number} y 触摸点y轴坐标
* @return {number} 角的位置0=无1=左上2=右上3=左下4=右下;
*/
function getToucheAngle(x, y) {
// console.log('getToucheAngle', x, y, JSON.stringify(area))
var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
var oy = sys.navigation ? 0 : sys.windowTop;
if(y >= area.top - o + oy && y <= area.top + area.angleSize + o + oy) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 1; // 左上角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 2; // 右上角
}
} else if(y >= area.bottom - area.angleSize - o + oy && y <= area.bottom + o + oy) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 3; // 左下角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 4; // 右下角
}
}
return 0; // 无触摸到角
};
/**
* 重置数据
*/
function resetData() {
offset = { x: 0, y: 0 };
scale = 1;
minScale = img.minScale;
rotate = 0;
};
function getTouchs(touches) {
var result = [];
var len = touches ? touches.length : 0
for (var i = 0; i < len; i++) {
result[i] = {
pageX: touches[i].pageX,
// h5无标题栏时窗口顶部距离仍为标题栏高度且触摸点y轴坐标还是有标题栏的值即减去标题栏高度的值
pageY: touches[i].pageY + sys.windowTop
};
}
return result;
};
var mouseEvent = false;
export default {
data() {
return {
imageStyles: {},
maskStylesList: [{}, {}, {}, {}],
borderStyles: {},
gridStylesList: [{}, {}, {}, {}],
angleStylesList: [{}, {}, {}, {}],
circleBoxStyles: {},
circleStyles: {}
}
},
created() {
// 监听 PC 端鼠标滚轮
// #ifdef H5
platform === 'H5' && window.addEventListener('mousewheel', async (e) => {
var touchs = getTouchs([e])
img.src && scaleImage({
instance: await this.getInstance(),
check: true,
// 鼠标向上滚动时deltaY 固定 -100鼠标向下滚动时deltaY 固定 100
scale: e.deltaY > 0 ? -0.05 : 0.05,
x: touchs[0].pageX,
y: touchs[0].pageY
});
});
// #endif
},
// #ifdef H5
mounted() {
platform === 'H5' && this.initH5Events();
},
// #endif
setPlatform(p) {
platform = p;
},
methods: {
// #ifdef H5
getTouchEvent(e) {
e.touches = [
{ pageX: e.pageX, pageY: e.pageY }
];
return e;
},
initH5Events() {
const preview = document.getElementById('pic-preview');
preview?.addEventListener('mousedown', (e, ev) => {
mouseEvent = true;
this.touchstart(this.getTouchEvent(e));
});
preview?.addEventListener('mousemove', (e) => {
if (!mouseEvent) return;
this.touchmove(this.getTouchEvent(e));
});
preview?.addEventListener('mouseup', (e) => {
mouseEvent = false;
this.touchend(this.getTouchEvent(e))
});
preview?.addEventListener('mouseleave', (e) => {
mouseEvent = false;
this.touchend(this.getTouchEvent(e))
});
},
// #endif
async getInstance() {
// #ifdef APP-PLUS
if(platform === 'APP')
return this.$ownerInstance
? Promise.resolve(this.$ownerInstance)
: new Promise((resolve) => {
setTimeout(async () => {
resolve(await this.getInstance());
});
});
// #endif
// #ifdef H5
if(platform === 'H5')
return Promise.resolve(this);
// #endif
},
/**
* 初始化:观察数据变更
* @param {Object} newVal 新数据
* @param {Object} oldVal 旧数据
* @param {Object} o 组件实例对象
*/
initObserver: async function(newVal, oldVal, o, i) {
// console.log('initObserver', newVal, oldVal, o, i)
if(newVal && (!img.src || timestamp !== newVal.timestamp)) {
timestamp = newVal.timestamp;
img = newVal.img;
sys = newVal.sys;
area = newVal.area;
minScale = img.minScale;
resetData();
const instance = await this.getInstance()
img.src && changeImageRect({
instance,
x: (sys.windowWidth - img.width) / 2,
y: (sys.windowHeight + sys.windowTop - sys.offsetBottom - img.height) / 2
});
changeAreaRect({
instance
});
}
},
/**
* 鼠标滚轮滚动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
mousewheel: function(e, o) {
// h5平台 wheel 事件无法判断滚轮滑动方向,需使用 mousewheel
},
/**
* 触摸开始
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchstart: function(e, o) {
if(!img.src) return;
touches = getTouchs(e.touches);
activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
if(touches.length === 1 && activeAngle !== 0) {
touchType = 'stretch'; // 伸缩裁剪区域
} else {
touchType = '';
}
// console.log('touchstart', e, activeAngle)
},
/**
* 触摸移动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchmove: async function(e, o) {
if(!img.src) return;
// console.log('touchmove', e, o)
e.touches = getTouchs(e.touches);
if(touchType === 'stretch') { // 触摸四个角进行拉伸
var point = e.touches[0];
var start = touches[0];
var x = point.pageX - start.pageX;
var y = point.pageY - start.pageY;
if(x !== 0 || y !== 0) {
var maxX = area.width * (1 - area.minScale);
var maxY = area.height * (1 - area.minScale);
// console.log(x, y, maxX, maxY, offset, area)
touches[0] = point;
switch(activeAngle) {
case 1: // 左上角
x += areaOffset.left;
y += areaOffset.top;
// console.log(x, y, offset.left > area.left)
// console.log(maxX, maxY)
if(x >= 0 && y >= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x >= area.left) || (offset.y > 0 && offset.y >= area.top))
? Math.min(offset.y - area.top, offset.x - area.left)
: false;
if(x > y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(x > maxX) x = maxX;
y = x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(y > maxY) y = maxY;
x = y * area.width / area.height;
}
areaOffset.left = x;
areaOffset.top = y;
}
break;
case 2: // 右上角
x += areaOffset.right;
y += areaOffset.top;
if(x <= 0 && y >= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y >= area.top))
? Math.min(offset.y - area.top, area.right - offset.x - img.width)
: false;
if(-x > y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(-x > maxX) x = -maxX;
y = -x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(y > maxY) y = maxY;
x = -y * area.width / area.height;
}
areaOffset.right = x;
areaOffset.top = y;
}
break;
case 3: // 左下角
x += areaOffset.left;
y += areaOffset.bottom;
if(x >= 0 && y <= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x >= area.left) || (offset.y > 0 && offset.y + img.height <= area.bottom))
? Math.min(area.bottom - offset.y - img.height, offset.x - area.left)
: false;
if(x > -y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(x > maxX) x = maxX;
y = -x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(-y > maxY) y = -maxY;
x = -y * area.width / area.height;
}
areaOffset.left = x;
areaOffset.bottom = y;
}
break;
case 4: // 右下角
x += areaOffset.right;
y += areaOffset.bottom;
if(x <= 0 && y <= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y + img.height <= area.bottom))
? Math.min(area.bottom - offset.y - img.height, area.right - offset.x - img.width)
: false;
if(-x > -y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(-x > maxX) x = -maxX;
y = x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(-y > maxY) y = -maxY;
x = y * area.width / area.height;
}
areaOffset.right = x;
areaOffset.bottom = y;
}
break;
}
// console.log(x, y, JSON.stringify(areaOffset))
changeAreaRect({
instance: await this.getInstance(),
});
// this.draw();
}
} else if (e.touches.length == 2) { // 双点触摸缩放
var start = getDistanceByTouches(touches);
var end = getDistanceByTouches(e.touches);
scaleImage({
instance: await this.getInstance(),
check: !area.bounce,
scale: (end.c - start.c) / 100,
x: end.x,
y: end.y
});
touchType = 'scale';
} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
touchType = 'move';
} else {
changeImageRect({
instance: await this.getInstance(),
check: !area.bounce,
x: e.touches[0].pageX - touches[0].pageX,
y: e.touches[0].pageY - touches[0].pageY
});
touchType = 'move';
}
touches = e.touches;
},
/**
* 触摸结束
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchend: async function(e, o) {
if(!img.src) return;
if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
// 裁剪区域宽度被缩放到多少
var left = areaOffset.left;
var right = areaOffset.right;
var top = areaOffset.top;
var bottom = areaOffset.bottom;
var w = area.width + right - left;
var h = area.height + bottom - top;
// 图像放大倍数
var p = scale * (area.width / w) - scale;
// 复原裁剪区域
areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
changeAreaRect({
instance: await this.getInstance(),
});
scaleImage({
instance: await this.getInstance(),
scale: p,
x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
});
} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
changeImageRect({
instance: await this.getInstance(),
check: true
});
}
},
/**
* 顺时针翻转图片90°
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
rotateImage: async function(r) {
rotate = (rotate + (r || 90)) % 360;
if(img.minScale >= 1) {
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
minScale = 1;
if(img.width < area.height) {
minScale = area.height / img.oldWidth;
} else if(img.height < area.width) {
minScale = (area.width / img.oldHeight)
}
if(minScale !== 1) {
scaleImage({
instance: await this.getInstance(),
scale: minScale - scale,
x: sys.windowWidth / 2,
y: (sys.windowHeight - sys.offsetBottom) / 2
});
}
}
// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
changeImageRect({
instance: await this.getInstance(),
check: true,
x: -ox - oy,
y: -oy + ox
});
},
rotateImage90: function() {
this.rotateImage(90)
},
rotateImage270: function() {
this.rotateImage(270)
},
}
}

View File

@ -0,0 +1,746 @@
<template>
<view class="image-cropper" :style="{ zIndex }" @wheel="cropper.mousewheel">
<canvas v-if="use2d" type="2d" id="imgCanvas" class="img-canvas" :style="{
width: `${canvansWidth}px`,
height: `${canvansHeight}px`
}"></canvas>
<canvas v-else id="imgCanvas" canvas-id="imgCanvas" class="img-canvas" :style="{
width: `${canvansWidth}px`,
height: `${canvansHeight}px`
}"></canvas>
<view id="pic-preview" class="pic-preview" :change:init="cropper.initObserver" :init="initData" @touchstart="cropper.touchstart" @touchmove="cropper.touchmove" @touchend="cropper.touchend">
<image v-if="imgSrc" id="crop-image" class="crop-image" :style="cropper.imageStyles" :src="imgSrc" webp></image>
<view v-for="(item, index) in maskList" :key="item.id" :id="item.id" class="crop-mask-block" :style="cropper.maskStylesList[index]"></view>
<view v-if="showBorder" id="crop-border" class="crop-border" :style="cropper.borderStyles"></view>
<view v-if="radius > 0" id="crop-circle-box" class="crop-circle-box" :style="cropper.circleBoxStyles">
<view class="crop-circle" id="crop-circle" :style="cropper.circleStyles"></view>
</view>
<block v-if="showGrid">
<view v-for="(item, index) in gridList" :key="item.id" :id="item.id" class="crop-grid" :style="cropper.gridStylesList[index]"></view>
</block>
<block v-if="showAngle">
<view v-for="(item, index) in angleList" :key="item.id" :id="item.id" class="crop-angle" :style="cropper.angleStylesList[index]">
<view :style="[{
width: `${angleSize}px`,
height: `${angleSize}px`
}]"></view>
</view>
</block>
</view>
<slot />
<view class="fixed-bottom safe-area-inset-bottom" :style="{ zIndex: initData.area.zIndex + 99 }">
<view v-if="(rotatable || reverseRotatable) && !!imgSrc" class="action-bar">
<view v-if="reverseRotatable" class="rotate-icon" @click="cropper.rotateImage270"></view>
<view v-if="rotatable" class="rotate-icon is-reverse" @click="cropper.rotateImage90"></view>
</view>
<view v-if="!choosable" class="choose-btn" @click="cropClick">确定</view>
<block v-else-if="!!imgSrc">
<view class="rechoose" @click="chooseImage">重选</view>
<button class="button" size="mini" @click="cropClick">确定</button>
</block>
<view v-else class="choose-btn" @click="chooseImage">选择图片</view>
</view>
</view>
</template>
<!-- #ifdef APP-VUE -->
<script module="cropper" lang="renderjs">
import cropper from './qf-image-cropper.render.js';
// vue3 app renderjs中条件编译无效
cropper.setPlatform('APP');
export default {
mixins: [ cropper ]
}
</script>
<!-- #endif -->
<!-- #ifdef H5 -->
<script module="cropper" lang="renderjs">
import cropper from './qf-image-cropper.render.js';
export default {
mixins: [ cropper ]
}
</script>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN || MP-QQ -->
<script module="cropper" lang="wxs" src="./qf-image-cropper.wxs"></script>
<!-- #endif -->
<script>
/** 裁剪区域最大宽高所占屏幕宽度百分比 */
const AREA_SIZE = 75;
/** 图片默认宽高 */
const IMG_SIZE = 300;
export default {
name:"qf-image-cropper",
// #ifdef MP-WEIXIN
options: {
// 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响
styleIsolation: "isolated"
},
// #endif
props: {
/** 图片资源地址 */
src: {
type: String,
default: ''
},
/** 裁剪宽度有些平台或设备对于canvas的尺寸有限制过大可能会导致无法正常绘制 */
width: {
type: Number,
default: IMG_SIZE
},
/** 裁剪高度有些平台或设备对于canvas的尺寸有限制过大可能会导致无法正常绘制 */
height: {
type: Number,
default: IMG_SIZE
},
/** 是否绘制裁剪区域边框 */
showBorder: {
type: Boolean,
default: true
},
/** 是否绘制裁剪区域网格参考线 */
showGrid: {
type: Boolean,
default: true
},
/** 是否展示四个支持伸缩的角 */
showAngle: {
type: Boolean,
default: true
},
/** 裁剪区域最小缩放倍数 */
areaScale: {
type: Number,
default: 0.3
},
/** 图片最小缩放倍数 */
minScale: {
type: Number,
default: 1
},
/** 图片最大缩放倍数 */
maxScale: {
type: Number,
default: 5
},
/** 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 */
checkRange: {
type: Boolean,
default: true
},
/** 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性生成图片存在一定的透明块 */
backgroundColor: {
type: String
},
/** 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 */
bounce: {
type: Boolean,
default: true
},
/** 是否支持翻转 */
rotatable: {
type: Boolean,
default: true
},
/** 是否支持逆向翻转 */
reverseRotatable: {
type: Boolean,
default: false
},
/** 是否支持从本地选择素材 */
choosable: {
type: Boolean,
default: true
},
/** 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 */
gpu: {
type: Boolean,
default: false
},
/** 四个角尺寸单位px */
angleSize: {
type: Number,
default: 20
},
/** 四个角边框宽度单位px */
angleBorderWidth: {
type: Number,
default: 2
},
zIndex: {
type: [Number, String]
},
/** 裁剪图片圆角半径单位px */
radius: {
type: Number,
default: 0
},
/** 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' */
fileType: {
type: String,
default: 'png'
},
/**
* 图片从绘制到生成所需时间单位ms
* 微信小程序平台使用 `Canvas 2D` 绘制时有效
* 如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间
*/
delay: {
type: Number,
default: 1000
},
// #ifdef H5
/**
* 页面是否是原生标题栏
* H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。
* 注因H5平台的窗口高度是包含标题栏的而屏幕触摸点的坐标是不包含的
*/
navigation: {
type: Boolean,
default: true
}
// #endif
},
emits: ["crop"],
data() {
return {
// 用不同 id 使 v-for key 不重复
maskList: [
{ id: 'crop-mask-block-1' },
{ id: 'crop-mask-block-2' },
{ id: 'crop-mask-block-3' },
{ id: 'crop-mask-block-4' },
],
gridList: [
{ id: 'crop-grid-1' },
{ id: 'crop-grid-2' },
{ id: 'crop-grid-3' },
{ id: 'crop-grid-4' },
],
angleList: [
{ id: 'crop-angle-1' },
{ id: 'crop-angle-2' },
{ id: 'crop-angle-3' },
{ id: 'crop-angle-4' },
],
/** 本地缓存的图片路径 */
imgSrc: '',
/** 图片的裁剪宽度 */
imgWidth: IMG_SIZE,
/** 图片的裁剪高度 */
imgHeight: IMG_SIZE,
/** 裁剪区域最大宽度所占屏幕宽度百分比 */
widthPercent: AREA_SIZE,
/** 裁剪区域最大高度所占屏幕宽度百分比 */
heightPercent: AREA_SIZE,
/** 裁剪区域布局信息 */
area: {},
/** 未被缩放过的图片宽 */
oldWidth: 0,
/** 未被缩放过的图片高 */
oldHeight: 0,
/** 系统信息 */
sys: uni.getSystemInfoSync(),
scaleWidth: 0,
scaleHeight: 0,
rotate: 0,
offsetX: 0,
offsetY: 0,
use2d: false,
canvansWidth: 0,
canvansHeight: 0,
// imageStyles: {},
// maskStylesList: [{}, {}, {}, {}],
// borderStyles: {},
// gridStylesList: [{}, {}, {}, {}],
// angleStylesList: [{}, {}, {}, {}],
// circleBoxStyles: {},
// circleStyles: {},
}
},
computed: {
initData() {
// console.log('initData')
return {
timestamp: new Date().getTime(),
area: {
...this.area,
bounce: this.bounce,
showBorder: this.showBorder,
showGrid: this.showGrid,
showAngle: this.showAngle,
angleSize: this.angleSize,
angleBorderWidth: this.angleBorderWidth,
minScale: this.areaScale,
widthPercent: this.widthPercent,
heightPercent: this.heightPercent,
radius: this.radius,
checkRange: this.checkRange,
zIndex: +this.zIndex || 0,
},
sys: this.sys,
img: {
minScale: this.minScale,
maxScale: this.maxScale,
src: this.imgSrc,
width: this.oldWidth,
height: this.oldHeight,
oldWidth: this.oldWidth,
oldHeight: this.oldHeight,
gpu: this.gpu,
}
}
},
imgProps() {
return {
width: this.width,
height: this.height,
src: this.src,
}
}
},
watch: {
imgProps: {
handler(val, oldVal) {
// 自定义裁剪尺,示例如下:
this.imgWidth = Number(val.width) || IMG_SIZE;
this.imgHeight = Number(val.height) || IMG_SIZE;
let use2d = true;
// #ifndef MP-WEIXIN
use2d = false;
// #endif
// if(use2d && (this.imgWidth > 1365 || this.imgHeight > 1365)) {
// use2d = false;
// }
let canvansWidth = this.imgWidth;
let canvansHeight = this.imgHeight;
let size = Math.max(canvansWidth, canvansHeight)
let scalc = 1;
if(size > 1365) {
scalc = 1365 / size;
}
this.canvansWidth = canvansWidth * scalc;
this.canvansHeight = canvansHeight * scalc;
this.use2d = use2d;
this.initArea();
const src = val.src || this.imgSrc;
src && this.initImage(src, oldVal === undefined);
},
immediate: true
},
},
methods: {
/** 提供给wxs调用用来接收图片变更数据 */
dataChange(e) {
// console.log('dataChange', e)
this.scaleWidth = e.width;
this.scaleHeight = e.height;
this.rotate = e.rotate;
this.offsetX = e.x;
this.offsetY = e.y;
},
/** 初始化裁剪区域布局信息 */
initArea() {
// 底部操作栏高度 = 底部底部操作栏内容高度 + 设备底部安全区域高度
this.sys.offsetBottom = uni.upx2px(100) + this.sys.safeAreaInsets.bottom;
// #ifndef H5
this.sys.windowTop = 0;
this.sys.navigation = true;
// #endif
// #ifdef H5
// h5平台的窗口高度是包含标题栏的
this.sys.windowTop = this.sys.windowTop || 44;
this.sys.navigation = this.navigation;
// #endif
let wp = this.widthPercent;
let hp = this.heightPercent;
if (this.imgWidth > this.imgHeight) {
hp = hp * this.imgHeight / this.imgWidth;
} else if (this.imgWidth < this.imgHeight) {
wp = wp * this.imgWidth / this.imgHeight;
}
const size = this.sys.windowWidth > this.sys.windowHeight ? this.sys.windowHeight : this.sys.windowWidth;
const width = size * wp / 100;
const height = size * hp / 100;
const left = (this.sys.windowWidth - width) / 2;
const right = left + width;
const top = (this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - height) / 2;
const bottom = this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - top;
this.area = { width, height, left, right, top, bottom };
this.scaleWidth = width;
this.scaleHeight = height;
},
/** 从本地选取图片 */
chooseImage(options) {
// #ifdef MP-WEIXIN || MP-JD
if(uni.chooseMedia) {
uni.chooseMedia({
...options,
count: 1,
mediaType: ['image'],
success: (res) => {
this.resetData();
this.initImage(res.tempFiles[0].tempFilePath);
}
});
return;
}
// #endif
uni.chooseImage({
...options,
count: 1,
success: (res) => {
this.resetData();
this.initImage(res.tempFiles[0].path);
}
});
},
/** 重置数据 */
resetData() {
this.imgSrc = '';
this.rotate = 0;
this.offsetX = 0;
this.offsetY = 0;
this.initArea();
},
/**
* 初始化图片信息
* @param {String} url 图片链接
*/
initImage(url, isFirst) {
uni.getImageInfo({
src: url,
success: async (res) => {
if (isFirst && this.src === url) await (new Promise((resolve) => setTimeout(resolve, 50)));
this.imgSrc = res.path;
let scale = res.width / res.height;
let areaScale = this.area.width / this.area.height;
if (scale > 1) { // 横向图片
if (scale >= areaScale) { // 图片宽不小于目标宽,则高固定,宽自适应
this.scaleWidth = (this.scaleHeight / res.height) * this.scaleWidth * (res.width / this.scaleWidth);
} else { // 否则宽固定、高自适应
this.scaleHeight = res.height * this.scaleWidth / res.width;
}
} else { // 纵向图片
if (scale <= areaScale) { // 图片高不小于目标高,宽固定,高自适应
this.scaleHeight = (this.scaleWidth / res.width) * this.scaleHeight / (this.scaleHeight / res.height);
} else { // 否则高固定,宽自适应
this.scaleWidth = res.width * this.scaleHeight / res.height;
}
}
// 记录原始宽高,为缩放比列做限制
this.oldWidth = this.scaleWidth;
this.oldHeight = this.scaleHeight;
},
fail: (err) => {
console.error(err)
}
});
},
/**
* 剪切图片圆角
* @param {Object} ctx canvas 的绘图上下文对象
* @param {Number} radius 圆角半径
* @param {Number} scale 生成图片的实际尺寸与截取区域比
* @param {Function} drawImage 执行剪切时所调用的绘图方法,入参为是否执行了剪切
*/
drawClipImage(ctx, radius, scale, drawImage) {
if(radius > 0) {
ctx.save();
ctx.beginPath();
const w = this.canvansWidth;
const h = this.canvansHeight;
if(w === h && radius >= w / 2) { // 圆形
ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI);
} else { // 圆角矩形
if(w !== h) { // 限制圆角半径不能超过短边的一半
radius = Math.min(w / 2, h / 2, radius);
// radius = Math.min(Math.max(w, h) / 2, radius);
}
ctx.moveTo(radius, 0);
ctx.arcTo(w, 0, w, h, radius);
ctx.arcTo(w, h, 0, h, radius);
ctx.arcTo(0, h, 0, 0, radius);
ctx.arcTo(0, 0, w, 0, radius);
ctx.closePath();
}
ctx.clip();
drawImage && drawImage(true);
ctx.restore();
} else {
drawImage && drawImage(false);
}
},
/**
* 旋转图片
* @param {Object} ctx canvas 的绘图上下文对象
* @param {Number} rotate 旋转角度
* @param {Number} scale 生成图片的实际尺寸与截取区域比
*/
drawRotateImage(ctx, rotate, scale) {
if(rotate !== 0) {
// 1. 以图片中心点为旋转中心点
const x = this.scaleWidth * scale / 2;
const y = this.scaleHeight * scale / 2;
ctx.translate(x, y);
// 2. 旋转画布
ctx.rotate(rotate * Math.PI / 180);
// 3. 旋转完画布后恢复设置旋转中心时所做的偏移
ctx.translate(-x, -y);
}
},
drawImage(ctx, image, callback) {
// 生成图片的实际尺寸与截取区域比
const scale = this.canvansWidth / this.area.width;
if(this.backgroundColor) {
if(ctx.setFillStyle) ctx.setFillStyle(this.backgroundColor);
else ctx.fillStyle = this.backgroundColor;
ctx.fillRect(0, 0, this.canvansWidth, this.canvansHeight);
}
this.drawClipImage(ctx, this.radius, scale, () => {
this.drawRotateImage(ctx, this.rotate, scale);
const r = this.rotate / 90;
ctx.drawImage(
image,
[
(this.offsetX - this.area.left),
(this.offsetY - this.area.top),
-(this.offsetX - this.area.left),
-(this.offsetY - this.area.top)
][r] * scale,
[
(this.offsetY - this.area.top),
-(this.offsetX - this.area.left),
-(this.offsetY - this.area.top),
(this.offsetX - this.area.left)
][r] * scale,
this.scaleWidth * scale,
this.scaleHeight * scale
);
});
},
/**
* 绘图
* @param {Object} canvas
* @param {Object} ctx canvas 的绘图上下文对象
* @param {String} src 图片路径
* @param {Function} callback 开始绘制时回调
*/
draw2DImage(canvas, ctx, src, callback) {
// console.log('draw2DImage', canvas, ctx, src, callback)
if(canvas) {
const image = canvas.createImage();
image.onload = () => {
this.drawImage(ctx, image);
// 如果觉得`生成时间过长`或`出现生成图片空白`可尝试调整延迟时间
callback && setTimeout(callback, this.delay);
};
image.onerror = (err) => {
console.error(err)
uni.hideLoading();
};
image.src = src;
} else {
this.drawImage(ctx, src);
setTimeout(() => {
ctx.draw(false, callback);
}, 200);
}
},
/**
* 画布转图片到本地缓存
* @param {Object} canvas
* @param {String} canvasId
*/
canvasToTempFilePath(canvas, canvasId) {
// console.log('canvasToTempFilePath', canvas, canvasId)
uni.canvasToTempFilePath({
canvas,
canvasId,
x: 0,
y: 0,
width: this.canvansWidth,
height: this.canvansHeight,
destWidth: this.imgWidth, // 必要,保证生成图片宽度不受设备分辨率影响
destHeight: this.imgHeight, // 必要,保证生成图片高度不受设备分辨率影响
fileType: this.fileType, // 目标文件的类型默认png
success: (res) => {
// 生成的图片临时文件路径
this.handleImage(res.tempFilePath);
},
fail: (err) => {
uni.hideLoading();
uni.showToast({ title: '裁剪失败,生成图片异常!', icon: 'none' });
}
}, this);
},
/** 确认裁剪 */
cropClick() {
uni.showLoading({ title: '裁剪中...', mask: true });
if(!this.use2d) {
const ctx = uni.createCanvasContext('imgCanvas', this);
ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
this.draw2DImage(null, ctx, this.imgSrc, () => {
this.canvasToTempFilePath(null, 'imgCanvas');
});
return;
}
// #ifdef MP-WEIXIN
const query = uni.createSelectorQuery().in(this);
query.select('#imgCanvas')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node;
const dpr = uni.getSystemInfoSync().pixelRatio;
canvas.width = res[0].width * dpr;
canvas.height = res[0].height * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
this.draw2DImage(canvas, ctx, this.imgSrc, () => {
this.canvasToTempFilePath(canvas);
});
});
// #endif
},
handleImage(tempFilePath){
// 在H5平台下tempFilePath 为 base64
// console.log(tempFilePath)
uni.hideLoading();
this.$emit('crop', { tempFilePath });
}
}
}
</script>
<style lang="scss" scoped>
.image-cropper {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: #000;
.img-canvas {
position: absolute !important;
transform: translateX(-100%);
}
.pic-preview {
width: 100%;
flex: 1;
position: relative;
.crop-mask-block {
background-color: rgba(51, 51, 51, 0.8);
z-index: 2;
position: fixed;
box-sizing: border-box;
pointer-events: none;
}
.crop-circle-box {
position: fixed;
box-sizing: border-box;
z-index: 2;
pointer-events: none;
overflow: hidden;
.crop-circle {
width: 100%;
height: 100%;
}
}
.crop-image {
padding: 0 !important;
margin: 0 !important;
border-radius: 0 !important;
display: block !important;
backface-visibility: hidden;
}
.crop-border {
position: fixed;
border: 1px solid #fff;
box-sizing: border-box;
z-index: 3;
pointer-events: none;
}
.crop-grid {
position: fixed;
z-index: 3;
border-style: dashed;
border-color: #fff;
pointer-events: none;
opacity: 0.5;
}
.crop-angle {
position: fixed;
z-index: 3;
border-style: solid;
border-color: #fff;
pointer-events: none;
}
}
.fixed-bottom {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 99;
display: flex;
flex-direction: row;
background-color: #1a1a1a;
.action-bar {
position: absolute;
top: -90rpx;
left: 10rpx;
display: flex;
.rotate-icon {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABCFJREFUaEPtml3IpVMUx3//ko/ChTIyiGFSMyhllI8bc4F85yuNC2FCqLmQC1+FZORiEkUMNW7UjKjJULgxV+NzSkxDhEkZgwsyigv119J63p7zvOc8z37OmXdOb51dz82711r7/99r7bXXXucVi3xokeNnRqCvB20fDmwAlgK/5bcD+FTSr33tHXQP2H4MeHQE0A+B5yRtLiUyDQJrgVc6AAaBpyV93kXkoBMIQLbfBS5NcK8BRwDXNcD+AdwnaVMbiWkRCPBBohpxHuK7M7865sclRdgNHVMhkF6IMIpwirFEUhzo8M7lwIvASTXEqyVtH8ZgagQSbOzsDknv18HZXpHn5IL8+94IOUm7miSmSqAttjPdbgGuTrnNktYsGgLpoYuAD2qg1zRTbG8P2D4SOC6/Q7vSHPALsE/S7wWy80RsPw/ckxMfSTq/LtRJwPbxwF3ASiCUTxwHCPAnEBfVF8AWSTtL7Ng+LfWOTfmlkn6udFsJ5K15R6a4kvX6yGyUFBvTOWzHXXFzCt4g6c1OArYj9iIGh43YgR+BvztXh1PSa4cMkd0jaVmXDduPAE+k3HpJD7cSGFKvfAc8FQUX8IOk/V2L1udtB/hTgdOBW4Aba/M7Ja1qs2f7euCNlHlZUlx4/495IWQ7Jl+qGbxX0gt9AHfJ2o6zFBVoNVrDKe+F3Sm8VdK1bQQ+A85JgXckXdkFaJx527cC9TpnVdvBtl3h2iapuhsGPdBw1b9xnUvaNw7AEh3bnwDnpuwGSfeP0rN9NvAMELXRXFkxEEK2nwQeSiOtRVQJwC4Z29cAW1Nuu6TVXTrN+SaBt4ErUug2Sa/2NdhH3vZy4NvU2S/p6D768w5xI3WOrAD7LtISFpGdIhVXKfaYvjd20wP13L9M0p4DBbaFRKToSLExVkr6qs+aIwlI6iwz+izUQqC+ab29PiMwqRcmPXczD8w8MFj1zg7xXEqbpdHCw7FgWSjafZL+KcQxtpjteCeflwYulFR/J3TabSslVkj6utPChAK2f6q9uZdLitKieLQRuExSvX9ZbLRUMFs09efpUZL+KtUfVo1GW/umNHC3pOhRLtiwfSbwZS6wV9IJfRdreuBBYH0a2STp9r4G+8jbXgc8mzoDT8VSO00ClwDv1ZR7XyylC4ec7ejaLUmdsV6Aw7oSbwFXpdFdks7qA6pU1na0aR6owgeIR/1cx63UzjAC0YXYVjMQHlkn6ZtSo21ytuPZGKFagQ/xsXZ/3iGuFrYdjafXG0DiQMeBi47c9/GV3BO247UV38n5o0UAP6xmu7jFOGxjRr66On5NPBDOCBsDTapxjHY1dyOcolNXnYlx1himE53p2PmNkxosevfavhg4Izt2k7TXPwZ2S6p6QZPin/2rwcQ7OKmBohCadJGF1P8PG6aaQBKVX/8AAAAASUVORK5CYII=');
background-size: 60% 60%;
background-repeat: no-repeat;
background-position: center;
width: 80rpx;
height: 80rpx;
&.is-reverse {
transform: rotateY(180deg);
}
}
}
.rechoose {
color: #ffffffde;
padding: 0 $uni-spacing-row-lg;
line-height: 100rpx;
font-size: 24rpx;
}
.choose-btn {
color: #ffffffde;
text-align: center;
line-height: 100rpx;
flex: 1;
font-size: 24rpx;
}
.button {
margin: auto $uni-spacing-row-lg auto auto;
background-color: #BBE600;
color: #232323de;
font-size: 24rpx;
}
}
.safe-area-inset-bottom {
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom); // 兼容 IOS<11.2
padding-bottom: env(safe-area-inset-bottom); // 兼容 IOS>=11.2
}
}
</style>

View File

@ -0,0 +1,604 @@
/**
* 图片编辑器-手势监听
* 1. wxs 暂不支持 es6 语法
* 2. 支持编译到微信小程序、QQ小程序、app-vue、H5上uni-app 2.2.5及以上版本)
*/
/** 图片偏移量 */
var offset = { x: 0, y: 0 };
/** 图片缩放比例 */
var scale = 1;
/** 图片最小缩放比例 */
var minScale = 1;
/** 图片旋转角度 */
var rotate = 0;
/** 触摸点 */
var touches = [];
/** 图片布局信息 */
var img = {};
/** 系统信息 */
var sys = {};
/** 裁剪区域布局信息 */
var area = {};
/** 触摸行为类型 */
var touchType = '';
/** 操作角的位置 */
var activeAngle = 0;
/** 裁剪区域布局信息偏移量 */
var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
/**
* 计算两点间距
* @param {Object} touches 触摸点信息
*/
function getDistanceByTouches(touches) {
// 根据勾股定理求两点间距离
var a = touches[1].pageX - touches[0].pageX;
var b = touches[1].pageY - touches[0].pageY;
var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
// 求两点间的中点坐标
// 1. a、b可能为负值
// 2. 在求a、b时如用touches[1]减touches[0]则求中点坐标也得用touches[1]减a/2、b/2
// 3. 同理在求a、b时也可用touches[0]减touches[1]则求中点坐标也得用touches[0]减a/2、b/2
var x = touches[1].pageX - a / 2;
var y = touches[1].pageY - b / 2;
return { c, x, y };
};
/**
* 修正取值
* @param {Object} a
* @param {Object} b
* @param {Object} c
* @param {Object} reverse 是否反向
*/
function correctValue(a, b, c, reverse) {
return reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c);
}
/**
* 检查边界:限制 x、y 拖动范围,禁止滑出边界
* @param {Object} e 点坐标
*/
function checkRange(e) {
var r = rotate / 90 % 2;
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
var o = (img.height - img.width) / 2; // 宽高差值一半
return {
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, img.height < area.height),
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, img.width < area.width)
}
}
return {
x: correctValue(e.x, -img.width + area.width + area.left, area.left, img.width < area.width),
y: correctValue(e.y, -img.height + area.height + area.top, area.top, img.height < area.height)
}
};
/**
* 变更图片布局信息
* @param {Object} e 布局信息
*/
function changeImageRect(e) {
offset.x += e.x || 0;
offset.y += e.y || 0;
var image = e.instance.selectComponent('.crop-image');
if(e.check && area.checkRange) { // 检查边界
var point = checkRange(offset);
if(offset.x !== point.x || offset.y !== point.y) {
offset = point;
}
}
// image.setStyle({
// width: img.width + 'px',
// height: img.height + 'px',
// transform: 'translate(' + offset.x + 'px, ' + offset.y + 'px) rotate(' + rotate +'deg)'
// });
var ox = (img.width - img.oldWidth) / 2;
var oy = (img.height - img.oldHeight) / 2;
image.setStyle({
width: img.oldWidth + 'px',
height: img.oldHeight + 'px',
transform: (img.gpu ? 'translateZ(0) ' : '') + 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
});
e.instance.callMethod('dataChange', {
width: img.width,
height: img.height,
x: offset.x,
y: offset.y,
rotate: rotate
});
};
/**
* 变更裁剪区域布局信息
* @param {Object} e 布局信息
*/
function changeAreaRect(e) {
// 变更蒙版样式
var masks = e.instance.selectAllComponents('.crop-mask-block');
var maskStyles = [
{
left: 0,
width: (area.left + areaOffset.left) + 'px',
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.right + areaOffset.right) + 'px',
right: 0,
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: 0,
height: (area.top + areaOffset.top) + 'px',
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: (area.bottom + areaOffset.bottom) + 'px',
// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
bottom: 0,
'z-index': area.zIndex + 2
}
];
var len = masks.length;
for (var i = 0; i < len; i++) {
masks[i].setStyle(maskStyles[i]);
}
// 变更边框样式
if(area.showBorder) {
var border = e.instance.selectComponent('.crop-border');
border.setStyle({
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
});
}
// 变更参考线样式
if(area.showGrid) {
var grids = e.instance.selectAllComponents('.crop-grid');
var gridStyles = [
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
}
];
var len = grids.length;
for (var i = 0; i < len; i++) {
grids[i].setStyle(gridStyles[i]);
}
}
// 变更四个伸缩角样式
if(area.showAngle) {
var angles = e.instance.selectAllComponents('.crop-angle');
var angleStyles = [
{
'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
}
];
var len = angles.length;
for (var i = 0; i < len; i++) {
angles[i].setStyle(angleStyles[i]);
}
}
// 变更圆角样式
if(area.radius > 0) {
var circleBox = e.instance.selectComponent('.crop-circle-box');
var circle = e.instance.selectComponent('.crop-circle');
var radius = area.radius;
if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
radius = (area.width / 2);
} else { // 圆角矩形
if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
radius = Math.min(area.width / 2, area.height / 2, radius);
}
}
circleBox.setStyle({
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 2
});
circle.setStyle({
'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
'border-radius': radius + 'px'
});
}
};
/**
* 缩放图片
* @param {Object} e 布局信息
*/
function scaleImage(e) {
var last = scale;
scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
if(last !== scale) {
img.width = img.oldWidth * scale;
img.height = img.oldHeight * scale;
// 参考问题有一个长4000px、宽4000px的四方形ABCDA点的坐标固定在(-2000,-2000)
// 该四边形上有一个点E坐标为(-100,-300)将该四方形复制一份并缩小到90%后,
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
e.x = (e.x - offset.x) * (1 - scale / last);
e.y = (e.y - offset.y) * (1 - scale / last);
changeImageRect(e);
return true;
}
return false;
};
/**
* 获取触摸点在哪个角
* @param {number} x 触摸点x轴坐标
* @param {number} y 触摸点y轴坐标
* @return {number} 角的位置0=无1=左上2=右上3=左下4=右下;
*/
function getToucheAngle(x, y) {
// console.log('getToucheAngle', x, y, JSON.stringify(area))
var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
if(y >= area.top - o && y <= area.top + area.angleSize + o) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 1; // 左上角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 2; // 右上角
}
} else if(y >= area.bottom - area.angleSize - o && y <= area.bottom + o) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 3; // 左下角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 4; // 右下角
}
}
return 0; // 无触摸到角
};
/**
* 重置数据
*/
function resetData() {
offset = { x: 0, y: 0 };
scale = 1;
minScale = img.minScale;
rotate = 0;
};
/**
* 顺时针翻转图片90°
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
function rotateImage(e, o, r) {
rotate = (rotate + r) % 360;
if(img.minScale >= 1) {
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
minScale = 1;
if(img.width < area.height) {
minScale = area.height / img.oldWidth;
} else if(img.height < area.width) {
minScale = (area.width / img.oldHeight)
}
if(minScale !== 1) {
scaleImage({
instance: o,
scale: minScale - scale,
x: sys.windowWidth / 2,
y: (sys.windowHeight - sys.offsetBottom) / 2
});
}
}
// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
changeImageRect({
instance: o,
check: true,
x: -ox - oy,
y: -oy + ox
});
};
module.exports = {
/**
* 初始化:观察数据变更
* @param {Object} newVal 新数据
* @param {Object} oldVal 旧数据
* @param {Object} o 组件实例对象
*/
initObserver: function(newVal, oldVal, o, i) {
if(newVal) {
img = newVal.img;
sys = newVal.sys;
area = newVal.area;
minScale = img.minScale;
resetData();
img.src && changeImageRect({
instance: o,
x: (sys.windowWidth - img.width) / 2,
y: (sys.windowHeight - sys.offsetBottom - img.height) / 2
});
changeAreaRect({
instance: o
});
// console.log('initRect', JSON.stringify(newVal))
}
},
/**
* 鼠标滚轮滚动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
mousewheel: function(e, o) {
if(!img.src) return;
scaleImage({
instance: o,
check: true,
// 鼠标向上滚动时deltaY 固定 -100鼠标向下滚动时deltaY 固定 100
scale: e.detail.deltaY > 0 ? -0.05 : 0.05,
x: e.touches[0].pageX,
y: e.touches[0].pageY
});
},
/**
* 触摸开始
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchstart: function(e, o) {
if(!img.src) return;
touches = e.touches;
activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
if(touches.length === 1 && activeAngle !== 0) {
touchType = 'stretch'; // 伸缩裁剪区域
} else {
touchType = '';
}
// console.log('touchstart', JSON.stringify(e), activeAngle)
},
/**
* 触摸移动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchmove: function(e, o) {
if(!img.src) return;
// console.log('touchmove', JSON.stringify(e), JSON.stringify(o))
if(touchType === 'stretch') { // 触摸四个角进行拉伸
var point = e.touches[0];
var start = touches[0];
var x = point.pageX - start.pageX;
var y = point.pageY - start.pageY;
if(x !== 0 || y !== 0) {
var maxX = area.width * (1 - area.minScale);
var maxY = area.height * (1 - area.minScale);
// console.log(x, y, maxX, maxY, offset, area)
touches[0] = point;
switch(activeAngle) {
case 1: // 左上角
x += areaOffset.left;
y += areaOffset.top;
if(x >= 0 && y >= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x >= area.left) || (offset.y > 0 && offset.y >= area.top))
? Math.min(offset.y - area.top, offset.x - area.left)
: false;
if(x > y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(x > maxX) x = maxX;
y = x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(y > maxY) y = maxY;
x = y * area.width / area.height;
}
areaOffset.left = x;
areaOffset.top = y;
}
break;
case 2: // 右上角
x += areaOffset.right;
y += areaOffset.top;
if(x <= 0 && y >= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y >= area.top))
? Math.min(offset.y - area.top, area.right - offset.x - img.width)
: false;
if(-x > y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(-x > maxX) x = -maxX;
y = -x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(y > maxY) y = maxY;
x = -y * area.width / area.height;
}
areaOffset.right = x;
areaOffset.top = y;
}
break;
case 3: // 左下角
x += areaOffset.left;
y += areaOffset.bottom;
if(x >= 0 && y <= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x >= area.left) || (offset.y > 0 && offset.y + img.height <= area.bottom))
? Math.min(area.bottom - offset.y - img.height, offset.x - area.left)
: false;
if(x > -y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(x > maxX) x = maxX;
y = -x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(-y > maxY) y = -maxY;
x = -y * area.width / area.height;
}
areaOffset.left = x;
areaOffset.bottom = y;
}
break;
case 4: // 右下角
x += areaOffset.right;
y += areaOffset.bottom;
if(x <= 0 && y <= 0) { // 有效滑动
var max = minScale < 1 && area.checkRange && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y + img.height <= area.bottom))
? Math.min(area.bottom - offset.y - img.height, area.right - offset.x - img.width)
: false;
if(-x > -y) { // 以x轴滑动距离为缩放基准
if(typeof max === 'number') maxX = max;
if(-x > maxX) x = -maxX;
y = x * area.height / area.width;
} else { // 以y轴滑动距离为缩放基准
if(typeof max === 'number') maxY = max;
if(-y > maxY) y = -maxY;
x = y * area.width / area.height;
}
areaOffset.right = x;
areaOffset.bottom = y;
}
break;
}
// console.log(x, y, JSON.stringify(areaOffset))
changeAreaRect({
instance: o,
});
// this.draw();
}
} else if (e.touches.length == 2) { // 双点触摸缩放
var start = getDistanceByTouches(touches);
var end = getDistanceByTouches(e.touches);
scaleImage({
instance: o,
check: !area.bounce,
scale: (end.c - start.c) / 100,
x: end.x,
y: end.y
});
touchType = 'scale';
} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
touchType = 'move';
} else {
changeImageRect({
instance: o,
check: !area.bounce,
x: e.touches[0].pageX - touches[0].pageX,
y: e.touches[0].pageY - touches[0].pageY
});
touchType = 'move';
}
touches = e.touches;
},
/**
* 触摸结束
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchend: function(e, o) {
if(!img.src) return;
if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
// 裁剪区域宽度被缩放到多少
var left = areaOffset.left;
var right = areaOffset.right;
var top = areaOffset.top;
var bottom = areaOffset.bottom;
var w = area.width + right - left;
var h = area.height + bottom - top;
// 图像放大倍数
var p = scale * (area.width / w) - scale;
// 复原裁剪区域
areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
changeAreaRect({
instance: o,
});
scaleImage({
instance: o,
scale: p,
x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
});
} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
changeImageRect({
instance: o,
check: true
});
}
},
/**
* 顺时针翻转图片90°
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
rotateImage: function(e, o) {
rotateImage(e, o, 90);
},
rotateImage90: function(e, o) {
rotateImage(e, o, 90)
},
rotateImage270: function(e, o) {
rotateImage(e, o, 270)
},
// 此处只用于对齐其他平台端的样式参数,防止异常,无作用
imageStyles: '',
maskStylesList: ['', '', '', ''],
borderStyles: '',
gridStylesList: ['', '', '', ''],
angleStylesList: ['', '', '', ''],
circleBoxStyles: '',
circleStyles: '',
}

View File

@ -0,0 +1,81 @@
{
"id": "qf-image-cropper",
"displayName": "图片裁剪插件",
"version": "2.2.4",
"description": "图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。",
"keywords": [
"qf-image-cropper",
"图片裁剪",
"图片编辑",
"头像裁剪",
"小程序"
],
"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": {
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "n"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "u",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "n",
"联盟": "n"
}
}
}
}
}

View File

@ -0,0 +1,95 @@
# qf-image-cropper
## 图片裁剪插件
uniapp微信小程序图片裁剪插件支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。
### 平台支持:
1. 支持微信小程序移动端、PC端、开发者工具
2. 支持H5平台2.1.0版本起)
3. 支持APP平台2.1.5版本起Android、IOS
4. 其他平台暂未测试兼容性未知
### 支持功能:
1. 自定义裁剪尺寸
2. 定点等比例缩放移动端以双指触摸中心点为缩放中心点PC端以鼠标所在点为缩放中心点
3. 自由拖动:支持限制滑出边界,也支持回弹效果(滑动时可滑出边界,释放时回弹到边界)
4. 图片翻转:在裁剪尺寸非 1:1 的情况下,翻转时宽高无法铺满裁剪区域时,图片会自动放大到合适尺寸
5. 裁剪生成新图片
6. 本地选择图片
7. 可定制样式:可自由选择是否渲染裁剪边框、可伸缩裁剪顶角、参考线
8. 裁剪圆角图片:圆形、圆角矩形
### 属性说明
| 属性名 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| src | String | | 图片资源地址 |
| width | Number | 300 | 裁剪宽度 |
| height | Number | 300 | 裁剪高度 |
| showBorder | Boolean | true | 是否绘制裁剪区域边框 |
| showGrid | Boolean | true | 是否绘制裁剪区域网格参考线 |
| showAngle | Boolean | true | 是否展示四个支持伸缩的角 |
| areaScale | Number | 0.3 | 裁剪区域最小缩放倍数 |
| minScale | Number | 1 | 图片最小缩放倍数 |
| maxScale | Number | 5 | 图片最大缩放倍数 |
| checkRange | Boolean | true | 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 |
| backgroundColor | String | | 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性则生成图片存在一定的透明块 |
| bounce | Boolean | true | 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 |
| rotatable | Boolean | true | 是否支持翻转 |
| reverseRotatable | Boolean | false | 是否支持逆向翻转 |
| choosable | Boolean | true | 是否支持从本地选择素材 |
| gpu | Boolean | false | 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 |
| angleSize | Number | 20 | 四个角尺寸单位px |
| angleBorderWidth | Number | 2 | 四个角边框宽度单位px |
| zIndex | Number/String | | 调整组件层级 |
| radius | Number | | 裁剪图片圆角半径单位px |
| fileType | String | png | 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' |
| delay | Number | 1000 | 图片从绘制到生成所需时间单位ms<br>微信小程序平台使用 `Canvas 2D` 绘制时有效<br>如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间 |
| navigation | Boolean | true | 页面是否是原生标题栏:<br>H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 `"navigationStyle": "custom"` 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。<br>因H5平台的窗口高度是包含标题栏的而屏幕触摸点的坐标是不包含的 |
| @crop | EventHandle | | 剪裁完成后触发event = { tempFilePath }。在H5平台下tempFilePath 为 base64 |
### 基本用法
```
<template>
<div>
<qf-image-cropper :width="500" :height="500" :radius="30" @crop="handleCrop"></qf-image-cropper>
</div>
</template>
<script>
import QfImageCropper from '@/components/qf-image-cropper/qf-image-cropper.vue';
export default {
components: {
QfImageCropper
},
methods: {
handleCrop(e) {
uni.previewImage({
urls: [e.tempFilePath],
current: 0
});
}
}
}
</script>
```
通过ref组件实例可在进入页面后直接打开相册选择图片
```
mounted() {
this.$refs.qfImageCropper.chooseImage({ sourceType: ['album'] });
}
```
### 使用说明
1.建议在`pages.json`中将引用插件的页面添加一下配置禁止下拉刷新和禁止页面滑动,防止出现性能或页面抖动等问题。
```
{
"enablePullDownRefresh": false,
"disableScroll": true
}
```
2.建议使用本插件不要设置过大宽高的目标图片尺寸建议1365x1365以内否则可能会导致如下问题
```
1.界面卡顿,内存占用过高
2.生成图片失真(模糊)
3.确定裁剪后一直显示 `裁剪中...`,该问题是由 `uni.canvasToTempFilePath` 无法回调导致,不同平台不同设备限制可能有所不同。
```
3.如裁剪后的图片存在偏移的问题,请检查是否受自己项目中父组件或全局样式影响。
4.src属性设置网络图片时图片资源必须是能触发 `getImageInfo` API 的 success 回调才可用于插件裁剪。因此小程序平台获取网络图片信息需先配置download域名白名单才能生效。

11
unpackage/dist/dev/.nvue/app.css.js vendored Normal file
View File

@ -0,0 +1,11 @@
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var require_app_css = __commonJS({
"app.css.js"(exports) {
const _style_0 = {};
exports.styles = [_style_0];
}
});
export default require_app_css();

2
unpackage/dist/dev/.nvue/app.js vendored Normal file
View File

@ -0,0 +1,2 @@
Promise.resolve("./app.css.js").then(() => {
});

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,8 @@
var isReady=false;var onReadyCallbacks=[];
var isServiceReady=false;var onServiceReadyCallbacks=[];
var __uniConfig = {"pages":["pages/common/login/index","pages/common/index/index","pages/common/user/index","pages/common/scan/scan","pages/common/qrcode/qrcode","pages/common/send/index","pages/common/userAgreement/index","pages/common/privacyAgreement/index","pages/common/aboutUs/index","pages/6170/deviceControl/index","pages/common/operationVideo/index","pages/common/addvideo/index","pages/common/operatingInstruct/index","pages/common/productDes/index","pages/6155/index","pages/6155/bluetooth/bluetooth"],"window":{"navigationBarTextStyle":"white","navigationBarTitleText":"uni-app","navigationBarBackgroundColor":"#121212","backgroundColor":"#121212"},"tabBar":{"color":"#fff","selectedColor":"#BBE600","backgroundColor":"#202020","list":[{"pagePath":"pages/common/index/index","text":"我的设备","iconPath":"/static/tabs/device.png","selectedIconPath":"/static/tabs/device-HL.png"},{"pagePath":"pages/common/user/index","text":"我的","iconPath":"/static/tabs/my.png","selectedIconPath":"/static/tabs/my-HL.png"}]},"darkmode":false,"nvueCompiler":"uni-app","nvueStyleCompiler":"uni-app","renderer":"auto","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":false},"appname":"JingQuan","compilerVersion":"4.64","entryPagePath":"pages/common/login/index","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000}};
var __uniRoutes = [{"path":"/pages/common/login/index","meta":{"isQuit":true},"window":{"navigationStyle":"custom"}},{"path":"/pages/common/index/index","meta":{"isQuit":true,"isTabBar":true},"window":{"navigationStyle":"custom"}},{"path":"/pages/common/user/index","meta":{"isQuit":true,"isTabBar":true},"window":{"navigationBarTitleText":"我的"}},{"path":"/pages/common/scan/scan","meta":{},"window":{"navigationBarTitleText":"扫描"}},{"path":"/pages/common/qrcode/qrcode","meta":{},"window":{"navigationBarTitleText":"扫描到的设备"}},{"path":"/pages/common/send/index","meta":{},"window":{"navigationBarTitleText":"发送信息"}},{"path":"/pages/common/userAgreement/index","meta":{},"window":{"navigationBarTitleText":"用户协议"}},{"path":"/pages/common/privacyAgreement/index","meta":{},"window":{"navigationBarTitleText":"隐私协议"}},{"path":"/pages/common/aboutUs/index","meta":{},"window":{"navigationBarTitleText":"关于我们"}},{"path":"/pages/6170/deviceControl/index","meta":{},"window":{"navigationStyle":"custom"}},{"path":"/pages/common/operationVideo/index","meta":{},"window":{"navigationStyle":"custom"}},{"path":"/pages/common/addvideo/index","meta":{},"window":{"navigationStyle":"custom"}},{"path":"/pages/common/operatingInstruct/index","meta":{},"window":{"navigationStyle":"custom"}},{"path":"/pages/common/productDes/index","meta":{},"window":{"navigationStyle":"custom"}},{"path":"/pages/6155/index","meta":{},"window":{"navigationBarTitleText":"6155"}},{"path":"/pages/6155/bluetooth/bluetooth","meta":{},"window":{"navigationStyle":"custom"}}];
var __uniConfig = {"pages":["pages/common/login/index","pages/common/index/index","pages/common/user/index","pages/common/scan/scan","pages/common/qrcode/qrcode","pages/common/send/index","pages/common/userAgreement/index","pages/common/privacyAgreement/index","pages/common/aboutUs/index","pages/6170/deviceControl/index","pages/6170/operationVideo/index","pages/6170/addvideo/index","pages/6170/editVideo/index","pages/6170/operatingInstruct/index","pages/6155/index","pages/6155/bluetooth/bluetooth"],"window":{"navigationBarTextStyle":"white","navigationBarTitleText":"uni-app","navigationBarBackgroundColor":"#121212","backgroundColor":"#121212"},"tabBar":{"color":"#fff","selectedColor":"#BBE600","backgroundColor":"#202020","list":[{"pagePath":"pages/common/index/index","text":"我的设备","iconPath":"/static/tabs/device.png","selectedIconPath":"/static/tabs/device-HL.png"},{"pagePath":"pages/common/user/index","text":"我的","iconPath":"/static/tabs/my.png","selectedIconPath":"/static/tabs/my-HL.png"}]},"darkmode":false,"nvueCompiler":"uni-app","nvueStyleCompiler":"uni-app","renderer":"auto","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":false},"appname":"JingQuan","compilerVersion":"4.64","entryPagePath":"pages/common/login/index","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000}};
var __uniRoutes = [{"path":"/pages/common/login/index","meta":{"isQuit":true},"window":{"navigationStyle":"custom"}},{"path":"/pages/common/index/index","meta":{"isQuit":true,"isTabBar":true},"window":{"navigationStyle":"custom"}},{"path":"/pages/common/user/index","meta":{"isQuit":true,"isTabBar":true},"window":{"navigationBarTitleText":"我的"}},{"path":"/pages/common/scan/scan","meta":{},"window":{"navigationBarTitleText":"扫描"}},{"path":"/pages/common/qrcode/qrcode","meta":{},"window":{"navigationBarTitleText":"扫描到的设备"}},{"path":"/pages/common/send/index","meta":{},"window":{"navigationBarTitleText":"发送信息"}},{"path":"/pages/common/userAgreement/index","meta":{},"window":{"navigationBarTitleText":"用户协议"}},{"path":"/pages/common/privacyAgreement/index","meta":{},"window":{"navigationBarTitleText":"隐私协议"}},{"path":"/pages/common/aboutUs/index","meta":{},"window":{"navigationBarTitleText":"关于我们"}},{"path":"/pages/6170/deviceControl/index","meta":{},"window":{"navigationStyle":"custom"}},{"path":"/pages/6170/operationVideo/index","meta":{},"window":{"navigationStyle":"custom"}},{"path":"/pages/6170/addvideo/index","meta":{},"window":{"navigationBarTitleText":"添加"}},{"path":"/pages/6170/editVideo/index","meta":{},"window":{"navigationBarTitleText":"编辑视频"}},{"path":"/pages/6170/operatingInstruct/index","meta":{},"window":{"navigationStyle":"custom"}},{"path":"/pages/6155/index","meta":{},"window":{"navigationBarTitleText":"6155"}},{"path":"/pages/6155/bluetooth/bluetooth","meta":{},"window":{"navigationStyle":"custom"}}];
__uniConfig.onReady=function(callback){if(__uniConfig.ready){callback()}else{onReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"ready",{get:function(){return isReady},set:function(val){isReady=val;if(!isReady){return}const callbacks=onReadyCallbacks.slice(0);onReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
__uniConfig.onServiceReady=function(callback){if(__uniConfig.serviceReady){callback()}else{onServiceReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"serviceReady",{get:function(){return isServiceReady},set:function(val){isServiceReady=val;if(!isServiceReady){return}const callbacks=onServiceReadyCallbacks.slice(0);onServiceReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
service.register("uni-app-config",{create(a,b,c){if(!__uniConfig.viewport){var d=b.weex.config.env.scale,e=b.weex.config.env.deviceWidth,f=Math.ceil(e/d);Object.assign(__uniConfig,{viewport:f,defaultFontSize:Math.round(f/20)})}return{instance:{__uniConfig:__uniConfig,__uniRoutes:__uniRoutes,global:void 0,window:void 0,document:void 0,frames:void 0,self:void 0,location:void 0,navigator:void 0,localStorage:void 0,history:void 0,Caches:void 0,screen:void 0,alert:void 0,confirm:void 0,prompt:void 0,fetch:void 0,XMLHttpRequest:void 0,WebSocket:void 0,webkit:void 0,print:void 0}}}});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 B

File diff suppressed because one or more lines are too long

View File

@ -1,47 +1,45 @@
const BASE_URL = 'http://192.168.2.34:8000';
const BASE_URL = 'http://192.168.110.169:9001';
const request = (options) => {
return new Promise((resolve, reject) => {
// 处理GET请求参数
let url = BASE_URL + options.url;
if (options.method === 'GET' && options.data) {
// 使用qs序列化参数
const params = Object.keys(options.data)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(options.data[key])}`)
.join('&');
url += `?${params}`;
}
console.log("options"+JSON.stringify(options))
return new Promise((resolve, reject) => {
// 处理GET请求参数
let url = BASE_URL + options.url;
if (options.method === 'GET' && options.data) {
// 使用qs序列化参数
const params = Object.keys(options.data)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(options.data[key])}`)
.join('&');
url += `?${params}`;
}
const config = {
url: url,
method: options.method || 'GET',
data: options.method !== 'GET' ? options.data : {},
header: options.header || {},
timeout: 10000,
success: (res) => {
resolve(res.data);
},
fail: (err) => {
reject(err);
}
};
const config = {
url: url,
method: options.method || 'GET',
data: options.method !== 'GET' ? options.data : {},
header: options.header || {},
timeout: 10000,
success: (res) => {
resolve(res.data);
},
fail: (err) => {
reject(err);
}
};
if (!options.url.includes('/login')) {
const token = uni.getStorageSync('token');
const clientid = uni.getStorageSync('clientID');
if (token) {
config.header['Authorization'] = 'Bearer ' + token;
config.header['clientid'] = clientid;
}
}
if (!config.header['Content-Type']) {
config.header['Content-Type'] = 'application/json';
}
uni.request(config);
});
if (!options.url.includes('/login')) {
const token = uni.getStorageSync('token');
const clientid = uni.getStorageSync('clientID');
if (token) {
config.header['Authorization'] = 'Bearer ' + token;
config.header['clientid'] = clientid;
}
}
if (!config.header['Content-Type']) {
config.header['Content-Type'] = 'application/json';
}
uni.request(config);
});
};
export default request;
// 导出基础URL以便其他地方使用
export const baseURL = BASE_URL
export const getToken = () => uni.getStorageSync('token') // 获取token的方法
export const clientid =() => uni.getStorageSync('clientID');
export default request;