first commit

This commit is contained in:
Ls
2026-03-09 16:31:33 +08:00
commit 22f77eb290
92 changed files with 35468 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
*.local
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

20
.hbuilderx/launch.json Normal file
View File

@@ -0,0 +1,20 @@
{ // launch.json 配置了启动调试时相关设置configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtype项可配置值为local或remote, local代表前端连本地云函数remote代表前端连云端云函数
"version": "0.0",
"configurations": [{
"app-plus" :
{
"launchtype" : "local"
},
"default" :
{
"launchtype" : "local"
},
"mp-weixin" :
{
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}

20
index.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

13367
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

80
package.json Normal file
View File

@@ -0,0 +1,80 @@
{
"name": "uni-preset-vue",
"version": "0.0.0",
"scripts": {
"dev:app": "uni -p app",
"dev:app-android": "uni -p app-android",
"dev:app-ios": "uni -p app-ios",
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-jd": "uni -p mp-jd",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:mp-xhs": "uni -p mp-xhs",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:app-android": "uni build -p app-android",
"build:app-ios": "uni build -p app-ios",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-jd": "uni build -p mp-jd",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:mp-xhs": "uni build -p mp-xhs",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@climblee/uv-ui": "^1.1.20",
"@dcloudio/uni-app": "3.0.0-4010520240507001",
"@dcloudio/uni-app-plus": "3.0.0-4010520240507001",
"@dcloudio/uni-components": "3.0.0-4010520240507001",
"@dcloudio/uni-h5": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-alipay": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-baidu": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-jd": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-lark": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-qq": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-weixin": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-xhs": "3.0.0-4010520240507001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4010520240507001",
"clipboard": "^2.0.11",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"uview-plus": "^3.3.54",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"
},
"devDependencies": {
"@dcloudio/types": "^3.4.8",
"@dcloudio/uni-automator": "3.0.0-4010520240507001",
"@dcloudio/uni-cli-shared": "3.0.0-4010520240507001",
"@dcloudio/uni-stacktracey": "3.0.0-4010520240507001",
"@dcloudio/vite-plugin-uni": "3.0.0-4010520240507001",
"@vue/runtime-core": "^3.4.21",
"@vue/tsconfig": "^0.1.3",
"sass": "1.63.2",
"sass-loader": "10.4.1",
"typescript": "^4.9.4",
"vite": "5.2.8",
"vue-tsc": "^1.0.24"
}
}

10
shims-uni.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
/// <reference types='@dcloudio/types' />
import 'vue'
declare module '@vue/runtime-core' {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {
}
}

50
src/App.vue Normal file
View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
import { NvpMerchService } from '@/Service/Nvp/NvpMerchService'
onLaunch(() => {
uni.hideTabBar()
getUpData()
console.log("App Launch");
});
onShow(() => {
console.log("App Show");
});
onHide(() => {
console.log("App Hide");
});
const getUpData=()=>{
// #ifdef APP
plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => {
NvpMerchService.GetAppVersion().then(res=>{
console.log('wgtinfo.versionCode',wgtinfo.versionCode);
if (res.data.version > wgtinfo.versionCode) {
setTimeout(function() {
uni.navigateTo({
url: "/pages/upData/upData?info=" +
encodeURIComponent(
JSON.stringify(res.data))
})
}, 1000)
}
})
})
// #endif
}
</script>
<style lang="scss">
@import "uview-plus/index.scss";
@import "colorui/main.css";
@import "colorui/icon.css";
page {
--nav-mian: #F84F28; //全局颜色
--nav-vice: #F59D77; //副颜色
--nav-diluted: #F2C0A3; //淡颜色
}
</style>

View File

@@ -0,0 +1,9 @@
export class BaseConfig {
// protected static servesUrl: string = "http://192.168.0.190:8806";//线下
protected static servesUrl: string = "http://vp.xypays.cn";
protected static imgUrl: string = "http://vp.cloud.xypays.cn";
protected static mediaUrl: string = "http://byc1.xypays.cn/";
protected static uploadUrl: string = "/TencentCos/GetUpLoadInfo";
protected static payuploadUrl: string = "http://pay.xypays.cn";
}

View File

@@ -0,0 +1,36 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class NvpAddressService {
private static GetAddressListPath: string = '/Address/GetAddressList';
/*****收货地址*****/
static GetAddressList(page: number) {
var result = Service.Request(this.GetAddressListPath, "GET", { page });
return result;
}
private static DelUserAddressPath: string = '/Address/DelUserAddress';
/*****删除地址*****/
static DelUserAddress(uaId: string) {
var result = Service.Request(this.DelUserAddressPath, "POST", { uaId });
return result;
}
private static AddUserAddressPath: string = '/Address/AddUserAddress';
/*****添加地址*****/
static AddUserAddress(uaId: string, name: string, phone: string, province: string, city: string, county: string, address: string, isDefault: number) {
var result = Service.Request(this.AddUserAddressPath, "POST", { uaId, name, phone, province, city, county, address, isDefault });
return result;
}
private static UpdateUserAddressPath: string = '/Address/UpdateUserAddress';
/*****修改默认地址*****/
static UpdateUserAddress(uaId: string) {
var result = Service.Request(this.UpdateUserAddressPath, "POST", { uaId });
return result;
}
}
export {
Service,
NvpAddressService
}

View File

@@ -0,0 +1,43 @@
import { Service } from '@/Service/Service';
/*****代理端接口*****/
class NvpAgentService {
private static LoginPath: string = '/Agent/Login';
/*****登录接口*****/
static Login(name: string, pwd: string) {
var result = Service.Request(this.LoginPath, "POST", { name, pwd });
return result;
}
private static GetAgentAccInfoPath: string = '/Agent/GetAgentAccInfo';
/*****账户接口*****/
static GetAgentAccInfo() {
var result = Service.Request(this.GetAgentAccInfoPath, "GET", "");
return result;
}
private static GetAgentAccLogPath: string = '/Agent/GetAgentAccLog';
/*****账户记录*****/
static GetAgentAccLog(code: string, page: number) {
var result = Service.Request(this.GetAgentAccLogPath, "GET", { code, page });
return result;
}
private static GetAgentMerchPath: string = '/Agent/GetAgentMerch';
/*****获取代理开通商家*****/
static GetAgentMerch(type: number, page: number) {
var result = Service.Request(this.GetAgentMerchPath, "GET", { type, page });
return result;
}
private static GetAgentHomePath: string = '/Agent/GetAgentHome';
/*****代理主页*****/
static GetAgentHome() {
var result = Service.Request(this.GetAgentHomePath, "GET", { });
return result;
}
}
export {
Service,
NvpAgentService
}

View File

@@ -0,0 +1,156 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class NvpApplyService {
// private static WithDrawPath: string = '/With/WithDraw';
// /*****佣金提现*****/
// static WithDraw(money: number, name: string, account: string) {
// var result = Service.Request(this.WithDrawPath, "POST", { money, name, account });
// return result;
// }
private static GetSiteMccCodeListPath: string = '/Apply/GetSiteMccCodeList';
/*****获取mcc列表*****/
static GetSiteMccCodeList(mercType:string, mchType: string) {
var result = Service.Request(this.GetSiteMccCodeListPath, "GET", {mercType, mchType });
return result;
}
private static GetBankTypeListPath: string = '/Apply/GetBankTypeList';
/*****获取银行列表*****/
static GetBankTypeList(name:string) {
var result = Service.Request(this.GetBankTypeListPath, "GET", {name});
return result;
}
private static GetAreaListPath: string = '/Apply/GetAreaList';
/*****获取银行地区列表*****/
static GetAreaList(areaCode:string) {
var result = Service.Request(this.GetAreaListPath, "GET", { areaCode});
return result;
}
private static GetBankCodeListPath: string = '/Apply/GetBankCodeList';
/*****获取银行代码列表*****/
static GetBankCodeList(bankType:string,cityCode:string,name:string) {
var result = Service.Request(this.GetBankCodeListPath, "GET", {bankType,cityCode,name});
return result;
}
private static SendApplyMerchPath: string = '/Apply/SendApplyMerch';
/*****进价提交*****/
static SendApplyMerch(para:any) {
var result = Service.Request(this.SendApplyMerchPath, "POST", para);
return result;
}
private static GetAgentMerchLogPath: string = '/Apply/GetAgentMerchLog';
/*****获取待审核*****/
static GetAgentMerchLog() {
var result = Service.Request(this.GetAgentMerchLogPath, "GET", {});
return result;
}
private static AuditApplyPath: string = '/Apply/AuditApply';
/*****确认资料*****/
static AuditApply(outId:string) {
var result = Service.Request(this.AuditApplyPath, "POST", {outId});
return result;
}
private static SetPayFeePath: string = '/Apply/SetPayFee';
/*****确认资料*****/
static SetPayFee(outId:string) {
var result = Service.Request(this.SetPayFeePath, "POST", {outId});
return result;
}
private static GetAssortListPath: string = '/Apply/GetAssortList';
/*****获取v派分类*****/
static GetAssortList(code:string) {
var result = Service.Request(this.GetAssortListPath, "GET", {code,parent:'0'});
return result;
}
private static GetAgentMerchPath: string = '/Agent/GetAgentMerch';
/*****获取已开通商家*****/
static GetAgentMerch(type:number,page:number) {
var result = Service.Request(this.GetAgentMerchPath, "GET", {type,page});
return result;
}
private static GetAppMerchInfoPath: string = '/Apply/GetAppMerchInfo';
/*****获取银盛已填写信息*****/
static GetAppMerchInfo(outId:string) {
var result = Service.Request(this.GetAppMerchInfoPath, "GET", {outId});
return result;
}
private static UpdateMerchPath: string = '/Apply/UpdateMerch';
/*****修改银盛信息*****/
static UpdateMerch(obj:any) {
var result = Service.Request(this.UpdateMerchPath, "POST", obj);
return result;
}
private static UploadImgPath: string = '/Apply/UploadImg';
/*****修改银盛图片*****/
static UploadImg(outId:string,picType:string,img:string) {
var result = Service.Request(this.UploadImgPath, "POST", {outId,picType,img});
return result;
}
private static GetAgentApplyPath: string = '/Apply/GetAgentApply';
/*****获取添加的商家*****/
static GetAgentApply(serch:string,page:number) {
var result = Service.Request(this.GetAgentApplyPath, "GET", {serch,page});
return result;
}
private static GetCategoryPath: string = '/Apply/GetCategory';
/*****获取商家类型*****/
static GetCategory() {
var result = Service.Request(this.GetCategoryPath, "GET", {});
return result;
}
private static AddMerchInfoPath: string = '/Apply/AddMerchInfo';
/*****添加商户*****/
static AddMerchInfo(obj:any) {
var result = Service.Request(this.AddMerchInfoPath, "POST", obj);
return result;
}
private static BandAppIdPath: string = '/Agent/BandAppId';
/*****绑定appid*****/
static BandAppId(merchId:string) {
var result = Service.Request(this.BandAppIdPath, "POST", {merchId});
return result;
}
}
export {
Service,
NvpApplyService
}

View File

@@ -0,0 +1,50 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class NvpBankService {
private static GetPageBankListPath: string = '/Bank/GetPageBankList';
/*****用户银行卡列表*****/
static GetPageBankList(page: number) {
var result = Service.Request(this.GetPageBankListPath, "GET", { page });
return result;
}
private static GetBankListPath: string = '/Bank/GetBankList';
/*****用户银行卡列表*****/
static GetBankList() {
var result = Service.Request(this.GetBankListPath, "GET", "");
return result;
}
private static GetUnitBankListPath: string = '/Bank/GetUnitBankList';
/*****银行卡列表*****/
static GetUnitBankList() {
var result = Service.Request(this.GetUnitBankListPath, "GET", "");
return result;
}
private static AddUserBankPath: string = '/Bank/AddUserBank';
/*****添加银行卡*****/
static AddUserBank(account: string, bank: string, name: string) {
var result = Service.Request(this.AddUserBankPath, "POST", { account, bank, name });
return result;
}
private static UpdateBankPath: string = '/Bank/UpdateBank';
/*****修改银行卡*****/
static UpdateBank(ubId: string, bank: string, name: string, account: string) {
var result = Service.Request(this.UpdateBankPath, "POST", { ubId, bank, name, account });
return result;
}
private static DelUserBankPath: string = '/Bank/DelUserBank';
/*****删除银行卡*****/
static DelUserBank(ubId: string) {
var result = Service.Request(this.DelUserBankPath, "POST", { ubId });
return result;
}
}
export {
Service,
NvpBankService
}

View File

@@ -0,0 +1,36 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class NvpLoginService {
private static GetWxLoginOpenIdPath: string = '/Login/GetWxLoginOpenId';
/*****小程序根据code获取openId*****/
static GetWxLoginOpenId(code: string, type: number) {
var result = Service.Request(this.GetWxLoginOpenIdPath, "GET", { code, type });
return result;
}
private static AppletLoginPath: string = '/Login/AppletLogin';
/*****普通登陆接口(小程序)*****/
static AppletLogin(openId: string, channel: string) {
var result = Service.Request(this.AppletLoginPath, "POST", { openId, channel });
return result;
}
private static RegistPath: string = '/Login/Regist';
/*****注册接口*****/
static Regist(remNo: string, openId: string, uniopenId: string, channel: string, type: number) {
var result = Service.Request(this.RegistPath, "POST", { remNo, openId, uniopenId, channel, type });
return result;
}
private static GetNumberPhonePath: string = '/Login/GetNumberPhone';
/*****获取手机号*****/
static GetNumberPhone(code: string) {
var result = Service.Request(this.GetNumberPhonePath, "GET", { code });
return result;
}
}
export {
Service,
NvpLoginService
}

View File

@@ -0,0 +1,32 @@
import { Service } from '@/Service/Service';
/*****设备接口*****/
class NvpMachineService {
private static GetMerchListPath: string = '/Machine/GetMerchList';
/*****商户设备*****/
static GetMerchList(merchId: string) {
var result = Service.Request(this.GetMerchListPath, "GET", { merchId });
return result;
}
private static DelMerchMachinePath: string = '/Machine/DelMerchMachine';
/*****删除设备*****/
static DelMerchMachine(merchId: string, machineId: string) {
var result = Service.Request(this.DelMerchMachinePath, "POST", { merchId, machineId });
return result;
}
private static AddMachinePath: string = '/Machine/AddMachine';
/*****添加设备*****/
static AddMachine(merchId: string, payCode: string) {
var result = Service.Request(this.AddMachinePath, "POST", { merchId, payCode });
return result;
}
}
export {
Service,
NvpMachineService
}

View File

@@ -0,0 +1,106 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class NvpMerchService {
private static GetMerchInfoPath: string = '/Merch/GetMerchInfo';
/*****获取商家信息*****/
static GetMerchInfo(merchId: string,lon:number,lat:number) {
var result = Service.Request(this.GetMerchInfoPath, "GET", { merchId,lon,lat});
return result;
}
private static GetMerchOrderPath: string = '/Merch/GetMerchOrder';
/*****获取商家营业数据*****/
static GetMerchOrder(merchId: string) {
var result = Service.Request(this.GetMerchOrderPath, "GET", { merchId });
return result;
}
private static UpdateMerchInfoPath: string = '/Merch/UpdateMerchInfo';
/*****修改商家信息*****/
static UpdateMerchInfo(merchId: string, logo: string ,name:string, phone:string,province:string,city:string,county:string,address:string,lon:string,lat:string,sTime:string,eTime:string,showImg:string) {
var result = Service.Request(this.UpdateMerchInfoPath, "POST", { merchId, logo,name ,phone,province,city,county,address,lon,lat,sTime,eTime,showImg});
return result;
}
private static GetMerchAccPath: string = '/Merch/GetMerchAcc';
/*****获取商家佣金数据*****/
static GetMerchAcc(merchId: string) {
var result = Service.Request(this.GetMerchAccPath, "GET", { merchId });
return result;
}
private static GetMerchAccLogPath: string = '/Merch/GetMerchAccLog';
/*****获取商家佣金记录*****/
static GetMerchAccLog(merchId: string,op:string,page:number) {
var result = Service.Request(this.GetMerchAccLogPath, "GET", { merchId,op,page });
return result;
}
private static MerchWithDrawPath: string = '/Merch/MerchWithDraw';
/*****商家佣金提现*****/
static MerchWithDraw(merchId: string, money: number ,name:string, account:string) {
var result = Service.Request(this.MerchWithDrawPath, "POST", { merchId, money,name,account});
return result;
}
private static GetMerchWithLogPath: string = '/Merch/GetMerchWithLog';
/*****商家佣金提现记录*****/
static GetMerchWithLog(merchId: string) {
var result = Service.Request(this.GetMerchWithLogPath, "GET", { merchId });
return result;
}
private static GetMerchTicketListPath: string = '/Merch/GetMerchTicketList';
/*****商家优惠券列表*****/
static GetMerchTicketList(merchId: string) {
var result = Service.Request(this.GetMerchTicketListPath, "GET", { merchId });
return result;
}
private static AddMerchTicketPath: string = '/Merch/AddMerchTicket';
/*****商家优惠券修改添加*****/
static AddMerchTicket(ticketId: string, merchId: string ,code:string, atkAcc:number,redAcc:number,count:number,useTime:Number,state:Number,addTime:string,endTime:string) {
var result = Service.Request(this.AddMerchTicketPath, "POST", { ticketId, merchId,code,atkAcc,redAcc,count,useTime,state,addTime,endTime});
return result;
}
private static DelMerchTicketPath: string = '/Merch/DelMerchTicket';
/*****商家删除优惠券*****/
static DelMerchTicket(ticketId: string) {
var result = Service.Request(this.DelMerchTicketPath, "POST", { ticketId});
return result;
}
private static GetTicketInfoPath: string = '/Merch/GetTicketInfo';
/*****获取优惠券详情*****/
static GetTicketInfo(ticketId: string) {
var result = Service.Request(this.GetTicketInfoPath, "GET", { ticketId });
return result;
}
private static GetAppVersionPath: string = '/Login/GetAppVersion';
/*****更新*****/
static GetAppVersion() {
var result = Service.Request(this.GetAppVersionPath, "GET", {type:2});
return result;
}
}
export {
Service,
NvpMerchService
}

View File

@@ -0,0 +1,43 @@
import { Service } from '@/Service/Service';
/*****公共接口*****/
class NvpPubService {
private static GetIndexPath: string = '/Pub/GetIndex';
/*****主页信息*****/
static GetIndex() {
var result = Service.Request(this.GetIndexPath, "GET", "");
return result;
}
private static GetIndexDataPath: string = '/Pub/GetIndexData';
/*****获取首页数据*****/
static GetIndexData(lon: number, lat: number, city: string, county: string, sort: number, page: number) {
var result = Service.Request(this.GetIndexDataPath, "GET", { lon, lat, city, county, sort, page });
return result;
}
private static GetMenuDataPath: string = '/Pub/GetMenuData';
/*****获取分类*****/
static GetMenuData(type: string, parent: string) {
var result = Service.Request(this.GetMenuDataPath, "GET", { type, parent });
return result;
}
private static GetMerchDataPath: string = '/Pub/GetMerchData';
/*****获取店铺*****/
static GetMerchData(code: string, serch: string, assId: string, lon: number, lat: number, city: string, county: string, sort: number, page: number, limit: number) {
var result = Service.Request(this.GetMerchDataPath, "GET", { code, serch, assId, lon, lat, city, county, sort, page, limit });
return result;
}
private static GetRandomMerchPath: string = '/Pub/GetRandomMerch';
/*****随机获取商家*****/
static GetRandomMerch(count: number, lon: number, lat: number, city: string, county: string) {
var result = Service.Request(this.GetRandomMerchPath, "GET", { count, lon, lat, city, county });
return result;
}
}
export {
Service,
NvpPubService
}

View File

@@ -0,0 +1,22 @@
import { Service } from '@/Service/Service';
/*****腾讯云存储*****/
class NvpTencentCosService {
private static GetAuthorizationPath: string = '/TencentCos/GetAuthorization';
/*****获取云存储配置*****/
static GetAuthorization() {
var result = Service.Request(this.GetAuthorizationPath, "GET", "");
return result;
}
private static GetUpLoadInfoPath: string = '/TencentCos/GetUpLoadInfo';
/*****获取上传地址*****/
static GetUpLoadInfo(code: string, fileName: string, desire: string) {
var result = Service.Request(this.GetUpLoadInfoPath, "GET", { code, fileName, desire });
return result;
}
}
export {
Service,
NvpTencentCosService
}

View File

@@ -0,0 +1,43 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class NvpUserService {
private static GetUserInfoPath: string = '/User/GetUserInfo';
/*****用户基础信息*****/
static GetUserInfo() {
var result = Service.Request(this.GetUserInfoPath, "GET", "");
return result;
}
private static UpdateUserInfoPath: string = '/User/UpdateUserInfo';
/*****修改用户信息*****/
static UpdateUserInfo(par: string, smsCode: string, type: number) {
var result = Service.Request(this.UpdateUserInfoPath, "POST", { par, smsCode, type });
return result;
}
private static GetUserAccLogPath: string = '/User/GetUserAccLog';
/*****获取账户记录*****/
static GetUserAccLog(code: string, accType: string, page: number) {
var result = Service.Request(this.GetUserAccLogPath, "GET", { code, accType, page });
return result;
}
private static GetUserCommissionInfoPath: string = '/User/GetUserCommissionInfo';
/*****用户佣金账户*****/
static GetUserCommissionInfo() {
var result = Service.Request(this.GetUserCommissionInfoPath, "GET", "");
return result;
}
private static GetUserCommissionLogPath: string = '/User/GetUserCommissionLog';
/*****用户佣金记录*****/
static GetUserCommissionLog(code: string, accType: string, page: number) {
var result = Service.Request(this.GetUserCommissionLogPath, "GET", { code, accType, page });
return result;
}
}
export {
Service,
NvpUserService
}

View File

@@ -0,0 +1,22 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class NvpWithService {
private static WithDrawPath: string = '/With/WithDraw';
/*****佣金提现*****/
static WithDraw(money: number, name: string, account: string) {
var result = Service.Request(this.WithDrawPath, "POST", { money, name, account });
return result;
}
private static GetUserWithLogPath: string = '/With/GetUserWithLog';
/*****用户提现记录*****/
static GetUserWithLog(page: number) {
var result = Service.Request(this.GetUserWithLogPath, "GET", { page });
return result;
}
}
export {
Service,
NvpWithService
}

320
src/Service/Service.ts Normal file
View File

@@ -0,0 +1,320 @@
import { HttpRequest, StoreAssist, UploadAssist, ResultData } from '@/common/Common';
import { BaseConfig } from './BaseConfig';
export class Service extends BaseConfig {
//获取API地址
static ApiUrl(path : string) {
return `${this.servesUrl}${path}`;
}
//获取图片地址
static GetpayImg(path : string) {
if (path.startsWith('http') || path.startsWith('https')) {
return path;
} else {
return `${this.payuploadUrl}${path}`;
}
}
//获取图片地址
static GetMateUrlByImg(path : string) {
if (path.startsWith('http') || path.startsWith('https')) {
return path;
} else {
return `${this.imgUrl}${path}`;
}
}
//获取音视频地址
static GetMateUrlByMedia(path : string) {
if (path.startsWith('http') || path.startsWith('https')) {
return path;
} else {
return `${this.mediaUrl}${path}`;
}
}
//获取登录账号token
static GetUserToken() {
return Service.GetStorageCache('token');
}
// 获取登录状态
static GetUserIsLogin() {
var token = this.GetUserToken();
if (token == null || token == '') {
return false;
} else {
return true;
}
}
//设置登录账户Token
static SetUserToken(token : string) {
this.SetStorageCache('token', token);
}
//清理登录账户Token
static OffUserToken() {
Service.DelStorageCache('token');
uni.$emit('ImComOff', 'user');
this.ClearUserStateData();
}
//获取登录账号状态信息
static GetUserStateData() {
return Service.GetStorageCache('StateDomain');
}
//设置当前登录账号状态信息
static SetUserStateData() {
return Service.GetStorageCache('StateDomain');
}
//清理当前登录账号状态信息
static ClearUserStateData() {
Service.DelStorageCache('StateDomain');
}
//获取缓存
static GetStorageCache(key : string) {
return StoreAssist.Get(key);
}
//删除缓存
static DelStorageCache(key : string) {
StoreAssist.Delete(key);
}
//设置缓存
static SetStorageCache(key : string, data : any) {
StoreAssist.Set(key, data);
}
/*****以下是基础方法调用与拦截器*****/
static Request(url : string, method : 'GET' | 'POST' | 'PUT' | undefined, data : object | any) {
const token = Service.GetUserToken();
const _url = Service.ApiUrl(url);
var result = HttpRequest.RequestWithToken(_url, method, token, data).then((retResult : any) => {
if (retResult.statusCode == '200') {
var obj = retResult.data;
if (obj.code == 401) {
//过期
this.OffUserToken();
this.Msg('登录过期,请重新登录')
this.GoPage('/pages/login/login')
return Promise.reject();
} else if (obj.code == 40101) {
//失效
this.OffUserToken();
this.GoPageDelse('/pages/mine/login/login');
return Promise.reject();
} else if (obj.code == 1004) {
//资源不存在
this.GoPageDelse('/pages/AppSet/404/404');
return Promise.reject();
// return new ResultData(-1, '', '');
} else if (obj.code == 40188) {
//无权限
this.GoPageDelse('/pages/AppSet/40188/40188');
return Promise.reject();
// return new ResultData(-1, '', '');
} else if (obj.code == 1008) {
//业务提示
return new ResultData(obj.code, obj.msg, obj.data);
} else {
return new ResultData(obj.code, obj.msg, obj.data);
}
} else {
return new ResultData(-1, '', '');
}
});
return result;
}
/*****以下是腾讯云oss上传*****/
static UpLoadMedia(code : string, fileName : string, desire : string, path : string) {
var result = this.Request(this.uploadUrl, 'GET', { code, fileName, desire }).then((retResult) => {
if (retResult.code == 0) {
var upOk = UploadAssist.Upload(retResult.data.url, path, retResult.data.cosData).then((upRet : any) => {
if (upRet.statusCode == 200) {
const retData : any = { code: retResult.data.code, file: retResult.data.file, cache: retResult.data.cache };
return new ResultData(0, '上传成功!', retData);
} else {
this.Msg('上传失败!');
return new ResultData(-1, '', '');
}
});
return upOk;
} else {
this.Msg('上传失败!');
return new ResultData(-1, retResult.msg,retResult.data);
}
});
return result;
}
/***********消息操作**************/
static Msg(message : any, icon ?: any) : void {
if (icon != null) {
uni.showToast({
title: message,
icon: icon
});
} else {
uni.showToast({
title: message,
icon: 'none'
});
}
}
static Alert(msg : string, cb ?: any) {
uni.showModal({
title: '提示',
content: msg,
showCancel: false,
cancelText: '取消',
confirmText: '确定',
success: res => {
if (res.confirm) {
cb && cb();
}
}
})
}
static LoadIng(text : any) : void {
uni.showLoading({
title: text,
icon: 'none'
});
}
static LoadClose() : void {
uni.hideLoading();
}
/**********跳转操作*********/
static GoPageTab(path : string) : void {
uni.switchTab({
url: path
});
}
/**********跳转操作*********/
static GoPage(path : string) : void {
uni.navigateTo({
url: path, //跳转的页面
success: function (res) {
// 通过eventChannel向被打开页面传送数据
}
});
}
/**********跳转并删除当前页面操作*********/
static GoPageDelse(path : string) : void {
uni.redirectTo({
url: path //跳转的页面
});
}
/**********返回上一页*********/
static GoPageBack() : void {
uni.navigateBack({ delta: 1 });
}
/*****获取图片base64*****/
static UpLoadMediaBase64(path : string) {
return new Promise(function (resolve, reject) {
uni.uploadFile({
url: 'http://cloud.pccsh.com/DefUp/UploadFileImgBase64', //仅为示例,非真实的接口地址
filePath: path,
name: 'file',
success: (uploadFileRes) => {
resolve(uploadFileRes);
},
fail: (err) => {
reject(err);
}
});
});
}
/*****获取图片位置信息*****/
//获取时间戳
static GetTimeSpan(milliSecond : number) {
return Date.now() + milliSecond;
}
// 时间戳处理
static formatDate(time : any, type : number) : string {
const date = new Date(time);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始所以加1并用0填充
const day = String(date.getDate()).padStart(2, '0'); // 用0填充
const hours = String(date.getHours()).padStart(2, '0'); // 用0填充
const minutes = String(date.getMinutes()).padStart(2, '0'); // 用0填充
const seconds = String(date.getSeconds()).padStart(2, '0'); // 用0填充
if (type == 0) {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
else if (type == 1) {
return `${year}-${month}-${day} ${hours}:${minutes}`;
} else if (type == 2) {
return `${year}-${month}-${day}`;
} else if (type == 3) {
return `${hours}:${minutes}`;
} else if (type == 4) {
return `${year}${month}${day}`;
}
else {
return `${hours}:${minutes}`;
}
}
/*****节流*****/
static throttle(fn: () => void, time: number) {
let canRun: boolean = true;
return function () {
if (!canRun) return;
canRun = false;
setTimeout(() => {
fn(); //可以不执行
canRun = true;
}, time);
};
}
/*****防抖*****/
static debounce<T extends (...args: any[]) => void>(fn: T, time: number): (...args: Parameters<T>) => void {
let timerId: NodeJS.Timeout | null = null;
return (...args: Parameters<T>) => {
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(() => {
fn(...args); // 执行传入的函数
timerId = null; // 清除定时器ID
}, time);
};
}
// 普通图片上传
static uploadH5(path, dic, callback) {
console.log(this.payuploadUrl,'xxx')
uni.uploadFile({
url: this.payuploadUrl+'/Upload/UploadFile',
method: "POST",
header: {
'Authorization': 'Bearer ' + Service.GetUserToken(),
},
formData: {
"path": dic,
},
filePath: path,
name: 'file',
success: (data) => {
let info = data.data
callback(info)
}
})
}
}

184
src/colorui/animation.css Normal file
View File

@@ -0,0 +1,184 @@
/*
Animation 微动画
基于ColorUI组建库的动画模块 by 文晓港 2019年3月26日19:52:28
*/
/* css 滤镜 控制黑白底色gif的 */
.gif-black{
mix-blend-mode: screen;
}
.gif-white{
mix-blend-mode: multiply;
}
/* Animation css */
[class*=animation-] {
animation-duration: .5s;
animation-timing-function: ease-out;
animation-fill-mode: both
}
.animation-fade {
animation-name: fade;
animation-duration: .8s;
animation-timing-function: linear
}
.animation-scale-up {
animation-name: scale-up
}
.animation-scale-down {
animation-name: scale-down
}
.animation-slide-top {
animation-name: slide-top
}
.animation-slide-bottom {
animation-name: slide-bottom
}
.animation-slide-left {
animation-name: slide-left
}
.animation-slide-right {
animation-name: slide-right
}
.animation-shake {
animation-name: shake
}
.animation-reverse {
animation-direction: reverse
}
@keyframes fade {
0% {
opacity: 0
}
100% {
opacity: 1
}
}
@keyframes scale-up {
0% {
opacity: 0;
transform: scale(.2)
}
100% {
opacity: 1;
transform: scale(1)
}
}
@keyframes scale-down {
0% {
opacity: 0;
transform: scale(1.8)
}
100% {
opacity: 1;
transform: scale(1)
}
}
@keyframes slide-top {
0% {
opacity: 0;
transform: translateY(-100%)
}
100% {
opacity: 1;
transform: translateY(0)
}
}
@keyframes slide-bottom {
0% {
opacity: 0;
transform: translateY(100%)
}
100% {
opacity: 1;
transform: translateY(0)
}
}
@keyframes shake {
0%,
100% {
transform: translateX(0)
}
10% {
transform: translateX(-9px)
}
20% {
transform: translateX(8px)
}
30% {
transform: translateX(-7px)
}
40% {
transform: translateX(6px)
}
50% {
transform: translateX(-5px)
}
60% {
transform: translateX(4px)
}
70% {
transform: translateX(-3px)
}
80% {
transform: translateX(2px)
}
90% {
transform: translateX(-1px)
}
}
@keyframes slide-left {
0% {
opacity: 0;
transform: translateX(-100%)
}
100% {
opacity: 1;
transform: translateX(0)
}
}
@keyframes slide-right {
0% {
opacity: 0;
transform: translateX(100%)
}
100% {
opacity: 1;
transform: translateX(0)
}
}

View File

@@ -0,0 +1,65 @@
<template>
<view>
<view class="cu-custom" :style="[{height:CustomBar + 'px'}]">
<view class="cu-bar fixed" :style="style" :class="[bgImage!=''?'none-bg text-white bg-img':'',bgColor]">
<view class="action" @tap="BackPage" v-if="isBack">
<text class="cuIcon-back"></text>
<slot name="backText"></slot>
</view>
<view class="content" :style="[{top:StatusBar + 'px'}]">
<slot name="content"></slot>
</view>
<slot name="right"></slot>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
StatusBar: this.StatusBar,
CustomBar: this.CustomBar
};
},
name: 'cu-custom',
computed: {
style() {
var StatusBar= this.StatusBar;
var CustomBar= this.CustomBar;
var bgImage = this.bgImage;
var style = `height:${CustomBar}px;padding-top:${StatusBar}px;`;
if (this.bgImage) {
style = `${style}background-image:url(${bgImage});`;
}
return style
}
},
props: {
bgColor: {
type: String,
default: ''
},
isBack: {
type: [Boolean, String],
default: false
},
bgImage: {
type: String,
default: ''
},
},
methods: {
BackPage() {
uni.navigateBack({
delta: 1
});
}
}
}
</script>
<style>
</style>

1226
src/colorui/icon.css Normal file

File diff suppressed because one or more lines are too long

3912
src/colorui/main.css Normal file

File diff suppressed because it is too large Load Diff

13
src/common/Common.ts Normal file
View File

@@ -0,0 +1,13 @@
import {ResultData} from "./Domain/ResultData";
import {StoreAssist} from "./Unit/StorageAssist";
import {HttpRequest} from "./Unit/HttpRequest";
import { StringAssist } from "./Unit/StringAssist";
import { UploadAssist } from "./Unit/UploadAssist";
export {
ResultData,
HttpRequest,
StoreAssist,
StringAssist,
UploadAssist
}

View File

@@ -0,0 +1,10 @@
export class ResultData {
public code: number=-1;
public msg: string="";
public data: any;
constructor(code:number,msg:string,data:any) {
this.code=code;
this.msg = msg;
this.data = data;
}
}

View File

@@ -0,0 +1,64 @@
export class HttpRequest {
/***普通请求方法***/
static Request(url: string, method: "GET" | "POST" | "PUT" | undefined, data: object | any) {
return new Promise(function(resolve, reject) {
let header = {
'content-type': method == 'POST' || method == 'PUT' ? 'application/x-www-form-urlencoded' : 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*'
};
uni.request({
url: url,
method: method,
data: data,
header: header,
success(res: any) {
if (res.statusCode == "200") {
resolve(res.data);
}
else
{
resolve(res);
}
},
fail(err) {
//请求失败
uni.showToast({
title: '无法连接到服务器',
icon: 'none'
})
reject(err)
}
})
})
};
/***带Token的请求方法***/
static RequestWithToken(url: string, method: "GET" | "POST" | "PUT" | undefined, token: string, data: object | any) {
return new Promise(function(resolve, reject) {
let header = {
'content-type': method == 'POST' || method == 'PUT' ? 'application/x-www-form-urlencoded' : 'application/json; charset=utf-8',
'Authorization': 'Bearer ' + token,//token获取
'Access-Control-Allow-Origin': '*'
};
uni.request({
url: url,
method: method,
data: data,
header: header,
success(res: any) {
resolve(res);
},
fail(err) {
//请求失败
uni.showToast({
title: '无法连接到服务器',
icon: 'none'
})
reject(err)
}
})
})
}
}

View File

@@ -0,0 +1,14 @@
export class StoreAssist{
static Get(key:string):any
{
return uni.getStorageSync(key);
}
static Set(key:string,value:any):void
{
uni.setStorageSync(key, value);
}
static Delete(key:string):void
{
uni.removeStorageSync(key);
}
}

View File

@@ -0,0 +1,13 @@
// import { createPinia, defineStore } from "pinia";
// import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
// export class StoreAssist{
// private pinia:any=createPinia();
// constructor() {
// this.pinia.use(piniaPluginPersistedstate);
// }
// }

View File

@@ -0,0 +1,98 @@
export class StringAssist {
static NoHtml(html: string): string {
return html;
}
static PhoneToStr (e:string) {
return e.substring(0,3)+'****'+e.substring((e.length-2),(e.length))
}
// 数量过万处理
static NumToStr (sum:number) {
if(sum>=10000){
return (sum/10000).toFixed('2')+'w'
}else{
return sum
}
}
// 帖子距离现在多久
static DiffTimeTostring(dateTime: string): string {
let result = 0;
let time = Date.parse(dateTime);
let timestamp = Date.parse(new Date().toString());
if ((timestamp - time) / 1000 < 60) {
result = (timestamp - time) / 1000;
result = result < 0 ? 0 : result;
return result.toFixed(0) + '秒前';
} else if ((timestamp - time) / 1000 / 60 < 60) {
return ((timestamp - time) / 1000 / 60).toFixed(0) + '分钟前';
} else if ((timestamp - time) / 1000 / 60 / 60 < 24) {
return ((timestamp - time) / 1000 / 60 / 60).toFixed(0) + '小时前';
} else if ((timestamp - time) / 1000 / 60 / 60 / 24 < 31) {
return ((timestamp - time) / 1000 / 60 / 60 / 24).toFixed(0) + '天前';
} else {
return this.formatDate(time, 1);
}
}
private static formatDate(time:any,type:number):string
{
const date = new Date(time);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始所以加1并用0填充
const day = String(date.getDate()).padStart(2, '0'); // 用0填充
const hours = String(date.getHours()).padStart(2, '0'); // 用0填充
const minutes = String(date.getMinutes()).padStart(2, '0'); // 用0填充
const seconds = String(date.getSeconds()).padStart(2, '0'); // 用0填充
if(type==0)
{
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
else if(type==1)
{
return `${year}-${month}-${day} ${hours}:${minutes}`;
}else if(type==2)
{
return `${year}-${month}-${day}`;
}else if(type==3){
return `${month}-${day} ${hours}:${minutes}`;
}
else{
return `${hours}:${minutes}`;
}
}
// 聊天时间显示
static ChatTimeTostring(dateTime: string,upTime:string): string {
let time = Date.parse(dateTime);
let timestamp = Date.parse(upTime);
if (( time - timestamp) / 1000 / 60 < 10) {
return '0';
} else{
return this.formatDate(time, 3);
}
}
// 去除两侧的空格
static trim(str:string){
const reg = /^\s+|\s+$/g;
return str.replace(reg,'');
}
// 计算两个时分秒差值
static timesfm(dateTime: string,upTime:string){
let time = Date.parse('2000-01-01 '+dateTime);
let timestamp = Date.parse('2000-01-01 '+upTime);
return ((timestamp - time) / 1000 / 60).toFixed(0) + '分钟';
}
}

View File

@@ -0,0 +1,19 @@
export class UploadAssist {
static Upload(url: string, path: string, fromData: any) {
return new Promise(function(resolve, reject) {
uni.uploadFile({
url: url, //仅为示例,非真实的接口地址
filePath: path,
name: 'file',
formData: fromData,
success: (uploadFileRes) => {
resolve(uploadFileRes);
},
fail: (err) => {
reject(err);
},
});
})
}
}

View File

@@ -0,0 +1,54 @@
<template>
<view>
<view class="" v-if="props.show">
<qf-image-cropper :src="props.url" :width="props.width" :height="props.height" :radius="20"
@crop="handleCrop"></qf-image-cropper>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { onBeforeUnmount, onMounted, reactive } from 'vue';
import { Service, BycAdminService } from '@/Service/Byc/BycAdminService'
// const props = defineProps({
// show : string,
// // url : string,
// // width : number,
// // height:number,
// // upType:string,
// // retFun:Function
// });
let props = defineProps(["show","url",'width','height','upType',"retFun","imgName"]);
onMounted(() => {
})
const handleCrop = (e) => {
let arr = e.tempFilePath.split('.')
let name = arr[arr.length - 1]
Service.UpLoadMedia(props.upType, name, '', e.tempFilePath).then(rest => {
if (rest.code == 0) {
uni.showToast({
title: rest.msg,
icon: 'none'
})
props.retFun( {name:props.imgName,url:rest.data.file})
} else {
uni.showToast({
title: rest.msg,
icon: 'none'
})
}
})
}
</script>
<style>
</style>

View File

@@ -0,0 +1,299 @@
<template>
<view class="content">
<view class="liy-select-fixed" :class="openClass">
<view class="liy-search-warp">
<view class="liy-search-input">
<view class="lsi-warp">
<image src="../../static/liy-select/images/search.png" mode="widthFix"
class="lsi-icon"></image>
<input class="lsi-input" v-model="keyword" placeholder="请输入搜索内容" @input="getParamsList" />
</view>
</view>
<scroll-view scroll-y="true" class="scroll-Y" @scrolltolower="lower">
<view class="liy-search-list" v-if="outPutList.length > 0">
<view class="liy-search-li" v-for="(item,index) in outPutList" :key="index"
@click="mapSelectMenu(item,index)">
<view class="liy-search-left">
<view class="liy-search-title" v-if="titleKey">{{item[titleKey]}}
</view>
<view class="liy-search-title" v-else>未选择标题
</view>
<view class="liy-search-desc" v-if="subtitleKey">{{item[subtitleKey]}}</view>
</view>
<view class="liy-search-icon" v-if="mapSelectIndex == index">
<image class="lsi-icon" src="../../static/liy-select/images/check_mark.png"
mode="widthFix"></image>
</view>
</view>
<view class="liy-loading">
<image class="liy-loading-img" src="../../static/liy-select/images/complete.png"
mode="widthFix"></image>
<view class="liy-loading-text">加载完成</view>
</view>
</view>
<view v-else class="liy-void">
<image class="liy-void-img" src="../../static/liy-select/images/void.png"
mode="widthFix"></image>
<view class="liy-void-text">未查询到内容</view>
</view>
</scroll-view>
</view>
</view>
<view class="liy-overlay" @click="closeOverlay" v-if="open == true"></view>
</view>
</template>
<script>
export default {
name:'liy-select',
props:{
list:{
type:Array,
default:[]
},
titleKey:{
type:[String,null],
default: null
},
subtitleKey:{
type:[String,null],
default:null
},
open:{
type:Boolean,
default:false
}
},
data() {
return {
mapSelectIndex: null,
keyword: "",
outPutList: [],
openClass:""
}
},
watch: {
list (newList, oldList) {
this.getParamsList();
},
open(newOpen,oldOpen){
if(newOpen == true){
this.openClass = "show";
}else{
this.openClass = "hide";
}
}
},
created() {
this.getParamsList();
},
methods: {
mapSelectMenu(item,index) {
this.mapSelectIndex = index;
this.$emit("change",item,index)
this.$emit("close");
},
getParamsList() {
var selectList = this.list;
this.mapSelectIndex = null;
if (!this.keyword) {
this.outPutList = selectList;
return false;
}
var arr = [];
for (var i = 0; i < selectList.length; i++) {
let item = selectList[i];
if (item[this.titleKey].indexOf(this.keyword) > -1) {
arr.push(item);
} else {
}
}
this.outPutList = arr;
},
lower() {
},
closeOverlay() {
this.$emit("close");
}
}
}
</script>
<style scoped lang="scss">
.liy-overlay {
transition-duration: 300ms;
transition-timing-function: ease-out;
position: fixed;
inset: 0px;
z-index: 100;
background-color: rgba(0, 0, 0, 0.5);
}
.liy-select-fixed {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
z-index: 150;
transition: transform .5s ease-in-out;
transform: translateY(calc(100% + 40rpx));
&.show {
animation: showHandler 0.3s forwards;
}
&.hide {
animation: hideHandler 0.3s forwards;
}
.liy-search-warp {
background: #fff;
border-radius: 10rpx 10rpx 0 0;
.liy-search-input {
padding: 30rpx 40rpx;
font-size: 28rpx;
.lsi-warp {
display: flex;
align-items: center;
border: 2rpx solid #e3e3e3;
padding: 16rpx 20rpx;
border-radius: 50rpx;
.lsi-icon {
width: 30rpx;
height: 30rpx;
}
.lsi-input {
width: 90%;
padding-left: 16rpx;
font-size: 28rpx;
}
}
}
.liy-search-list {
.liy-search-li {
padding: 20rpx 30rpx;
display: flex;
align-items: center;
border-bottom: 1rpx solid #f7f7f7;
min-height: 50rpx;
.liy-search-left {
width: 90%;
.liy-search-title {
font-size: 28rpx;
line-height: 36rpx;
color: #333;
}
.liy-search-desc {
padding-top: 10rpx;
font-size: 24rpx;
line-height: 36rpx;
color: #999;
}
}
.liy-search-icon {
.lsi-icon {
width: 40rpx;
height: 40rpx;
}
}
}
}
}
}
.scroll-Y {
height: 500rpx;
}
.liy-loading {
color: #333;
font-size: 26rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0;
.liy-loading-img {
width: 32rpx;
height: 32rpx;
// animation: turn 3s linear infinite;
}
.liy-loading-text {
padding-left: 12rpx;
padding-right: 32rpx;
}
}
@keyframes turn {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(90deg);
}
50% {
transform: rotate(180deg);
}
75% {
transform: rotate(270deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes showHandler {
0% {
transform: translateY(calc(100% + 40rpx));
}
100% {
transform: translateY(0);
}
}
@keyframes hideHandler {
0% {
transform: translateY(0);
}
100% {
transform: translateY(calc(100% + 40rpx));
}
}
.liy-void {
text-align: center;
padding-top: 60rpx;
.liy-void-img {
width: 40%;
}
.liy-void-text {
padding-top: 30rpx;
font-size: 26rpx;
color: #909090;
}
}
</style>

8
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

13
src/main.ts Normal file
View File

@@ -0,0 +1,13 @@
import { createSSRApp } from "vue";
import App from "./App.vue";
import uviewPlus from 'uview-plus'
export function createApp() {
const app = createSSRApp(App);
app.use(uviewPlus)
return {
app,
};
}

143
src/manifest.json Normal file
View File

@@ -0,0 +1,143 @@
{
"name" : "科讯代购",
"appid" : "__UNI__06C2D6A",
"description" : "",
"versionName" : "1.0.8",
"versionCode" : 108,
"transformPx" : false,
/* 5+App */
"app-plus" : {
"compatible" : {
"ignoreVersion" : true //true表示忽略版本检查提示框HBuilderX1.9.0及以上版本支持
},
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : false,
"waiting" : false,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {
"Barcode" : {},
"Maps" : {},
"Geolocation" : {}
},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
],
"minSdkVersion" : 25,
"targetSdkVersion" : 25,
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ]
},
/* ios */
"ios" : {
"idfa" : false,
"dSYMs" : false
},
/* SDK */
"sdkConfigs" : {
"ad" : {},
"maps" : {
"amap" : {
"name" : "amapZAvZjTHj",
"appkey_ios" : "3caf9e6f01b0085be1e75e0d0e281fe7",
"appkey_android" : "3caf9e6f01b0085be1e75e0d0e281fe7"
}
},
"geolocation" : {
"amap" : {
"name" : "amapZAvZjTHj",
"__platform__" : [ "android" ],
"appkey_ios" : "",
"appkey_android" : "3caf9e6f01b0085be1e75e0d0e281fe7"
}
}
},
"icons" : {
"android" : {
"hdpi" : "unpackage/res/icons/72x72.png",
"xhdpi" : "unpackage/res/icons/96x96.png",
"xxhdpi" : "unpackage/res/icons/144x144.png",
"xxxhdpi" : "unpackage/res/icons/192x192.png"
},
"ios" : {
"appstore" : "unpackage/res/icons/1024x1024.png",
"ipad" : {
"app" : "unpackage/res/icons/76x76.png",
"app@2x" : "unpackage/res/icons/152x152.png",
"notification" : "unpackage/res/icons/20x20.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"proapp@2x" : "unpackage/res/icons/167x167.png",
"settings" : "unpackage/res/icons/29x29.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"spotlight" : "unpackage/res/icons/40x40.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png"
},
"iphone" : {
"app@2x" : "unpackage/res/icons/120x120.png",
"app@3x" : "unpackage/res/icons/180x180.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"notification@3x" : "unpackage/res/icons/60x60.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"settings@3x" : "unpackage/res/icons/87x87.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png",
"spotlight@3x" : "unpackage/res/icons/120x120.png"
}
}
}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"h5" : {
"sdkConfigs" : {
"maps" : {
"qqmap" : {
"key" : "7DIBZ-K4HCJ-ZR2FE-FOOOP-SALFT-RLFYW"
}
}
}
}
}

16
src/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"id": "liy-select",
"name": "可搜索的下拉选择框",
"displayName": "可搜索的下拉选择框",
"version": "1.0.0",
"description": "通过搜索关键字可以进行 下拉筛选",
"keywords": [
"uni-ui|select"
],
"dcloudext": {
"category": [
"前端组件",
"通用组件"
]
}
}

53
src/pages.json Normal file
View File

@@ -0,0 +1,53 @@
{
"easycom": {
// 注意一定要放在custom里否则无效https://ask.dcloud.net.cn/question/131175
"custom": {
"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue"
}
},
"pages": [ //pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "门店运营",
"navigationBarBackgroundColor": "#36394D",
"navigationStyle": "custom",
"backgroundColor": "#F8F8F8"
}
},
{
"path" : "pages/index/user",
"style" :
{
"navigationBarTitleText" : "我的"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "v派商家",
"navigationBarBackgroundColor": "#F84F28",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#000",
"selectedColor": "#000",
"backgroundColor": "#FFFFFF",
"list": [ {
"pagePath": "pages/index/index",
"iconPath": "static/tab/01.png",
"selectedIconPath": "static/tab/02.png",
"text": "主页"
}, {
"pagePath": "pages/index/user",
"iconPath": "static/tab/01.png",
"selectedIconPath": "static/tab/02.png",
"text": "我的"
}
]
}
}

29
src/pages/index/index.vue Normal file
View File

@@ -0,0 +1,29 @@
<template>
<view>
11111
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onShow, onLoad } from '@dcloudio/uni-app';
import { NvpAgentService, Service } from '@/Service/Nvp/NvpAgentService';
onShow(() => {
})
onLoad(() => {
})
</script>
<style lang="scss">
page {
background-color: #fdfdfd;
}
</style>

19
src/pages/index/user.vue Normal file
View File

@@ -0,0 +1,19 @@
<template>
<view>
</view>
</template>
<script setup lang="ts">
import { onShow,onLoad } from "@dcloudio/uni-app";
onLoad(() => {
});
onShow(() => {
});
</script>
<style lang="scss">
</style>

6
src/shime-uni.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
export {}
declare module "vue" {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
src/static/tab/01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
src/static/tab/02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
src/static/tab/03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
src/static/tab/04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
src/static/tab/05.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src/static/tab/06.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
src/static/tab/07.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
src/static/tab/08.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src/static/tab/09.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
src/static/tab/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

1
src/types/uview.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module "uview-plus"

78
src/uni.scss Normal file
View File

@@ -0,0 +1,78 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
@import 'uview-plus/theme.scss';
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color: #333; // 基本色
$uni-text-color-inverse: #fff; // 反色
$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable: #c0c0c0;
/* 背景颜色 */
$uni-bg-color: #fff;
$uni-bg-color-grey: #f8f8f8;
$uni-bg-color-hover: #f1f1f1; // 点击状态颜色
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); // 遮罩颜色
/* 边框颜色 */
$uni-border-color: #c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm: 12px;
$uni-font-size-base: 14px;
$uni-font-size-lg: 16;
/* 图片尺寸 */
$uni-img-size-sm: 20px;
$uni-img-size-base: 26px;
$uni-img-size-lg: 40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2c405a; // 文章标题颜色
$uni-font-size-title: 20px;
$uni-color-subtitle: #555; // 二级标题颜色
$uni-font-size-subtitle: 18px;
$uni-color-paragraph: #3f536e; // 文章段落颜色
$uni-font-size-paragraph: 15px;

View File

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

View File

@@ -0,0 +1,517 @@
<template>
<view class="lime-echart" :style="customStyle" v-if="canvasId" ref="limeEchart">
<!-- #ifndef APP-NVUE -->
<canvas
class="lime-echart__canvas"
v-if="use2dCanvas"
type="2d"
:id="canvasId"
:style="canvasStyle"
:disable-scroll="isDisableScroll"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
/>
<canvas
class="lime-echart__canvas"
v-else-if="isPc"
:style="canvasStyle"
:id="canvasId"
:canvas-id="canvasId"
:disable-scroll="isDisableScroll"
@mousedown="touchStart"
@mousemove="touchMove"
@mouseup="touchEnd"
/>
<canvas
class="lime-echart__canvas"
v-else
:width="nodeWidth"
:height="nodeHeight"
:style="canvasStyle"
:canvas-id="canvasId"
:id="canvasId"
:disable-scroll="isDisableScroll"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
/>
<canvas v-if="isOffscreenCanvas" :style="offscreenStyle" :canvas-id="offscreenCanvasId"></canvas>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<web-view
class="lime-echart__canvas"
:id="canvasId"
:style="canvasStyle"
:webview-styles="webviewStyles"
ref="webview"
src="/uni_modules/lime-echart/static/index.html"
@pagefinish="finished = true"
@onPostMessage="onMessage"
></web-view>
<!-- #endif -->
</view>
</template>
<script>
// #ifdef VUE3
// #ifdef APP-PLUS
global = {}
// #endif
// #endif
// #ifndef APP-NVUE
import {Canvas, setCanvasCreator, dispatch} from './canvas';
import { compareVersion, wrapTouch, devicePixelRatio ,sleep} from './utils';
// #endif
// #ifdef APP-NVUE
import { base64ToPath, sleep } from './utils';
// #endif
const charts = {}
const echartsObj = {}
export default {
name: 'lime-echart',
props: {
// #ifdef MP-WEIXIN || MP-TOUTIAO
type: {
type: String,
default: '2d'
},
// #endif
// #ifdef APP-NVUE
webviewStyles: Object,
// hybrid: Boolean,
// #endif
customStyle: String,
isDisableScroll: Boolean,
isClickable: {
type: Boolean,
default: true
},
enableHover: Boolean,
beforeDelay: {
type: Number,
default: 30
}
},
data() {
return {
// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
use2dCanvas: true,
// #endif
// #ifndef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
use2dCanvas: false,
// #endif
width: null,
height: null,
nodeWidth: null,
nodeHeight: null,
canvasNode: null,
config: {},
inited: false,
finished: false,
file: '',
platform: '',
isPc: false,
isDown: false,
isOffscreenCanvas: false,
offscreenWidth: 0,
offscreenHeight: 0
};
},
computed: {
canvasId() {
return `lime-echart${this._ && this._.uid || this._uid}`
},
offscreenCanvasId() {
return `${this.canvasId}_offscreen`
},
offscreenStyle() {
return `width:${this.offscreenWidth}px;height: ${this.offscreenHeight}px; position: fixed; left: 99999px; background: red`
},
canvasStyle() {
return this.width && this.height ? ('width:' + this.width + 'px;height:' + this.height + 'px') : ''
}
},
beforeDestroy() {
this.clear()
this.dispose()
// #ifdef H5
if(this.isPc) {
document.removeEventListener('mousewheel')
}
// #endif
},
created() {
// #ifdef H5
if(!('ontouchstart' in window)) {
this.isPc = true
document.addEventListener('mousewheel', (e) => {
if(this.chart) {
const touch = this.getTouch(e)
const handler = this.chart.getZr().handler;
dispatch.call(handler, 'mousewheel', touch)
}
})
}
// #endif
// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
const { SDKVersion, version, platform, environment } = uni.getSystemInfoSync();
// #endif
// #ifdef MP-WEIXIN
this.isPC = /windows/i.test(platform)
this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.9.2') >= 0 && !((/ios/i.test(platform) && /7.0.20/.test(version)) || /wxwork/i.test(environment)) //&& !this.isPC;
// #endif
// #ifdef MP-TOUTIAO
this.isPC = /devtools/i.test(platform)
this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '1.78.0') >= 0;
// #endif
// #ifdef MP-ALIPAY
this.use2dCanvas = this.type === '2d' && compareVersion(my.SDKVersion, '2.7.0') >= 0;
// #endif
},
mounted() {
this.$nextTick(() => {
this.$emit('finished')
})
},
methods: {
// #ifdef APP-NVUE
onMessage(e) {
const res = e?.detail?.data[0] || null;
if (res?.event) {
if(res.event === 'inited') {
this.inited = true
}
this.$emit(res.event, JSON.parse(res.data));
} else if(res?.file){
this.file = res.data
} else if(!res[0] && JSON.stringify(res[0]) != '{}'){
console.error(res);
} else {
console.log(...res)
}
},
// #endif
setChart(callback) {
if(!this.chart) {
console.warn(`组件还未初始化,请先使用 init`)
return
}
if(typeof callback === 'function' && this.chart) {
callback(this.chart);
}
// #ifdef APP-NVUE
if(typeof callback === 'function') {
this.$refs.webview.evalJs(`setChart(${JSON.stringify(callback.toString())}, ${JSON.stringify(this.roptions)})`);
}
// #endif
},
setOption() {
if (!this.chart || !this.chart.setOption) {
console.warn(`组件还未初始化,请先使用 init`)
return
}
// #ifndef APP-NVUE
this.chart.setOption(...arguments);
// #endif
// #ifdef APP-NVUE
this.$refs.webview.evalJs(`setOption(${JSON.stringify(arguments)})`);
// #endif
},
showLoading() {
if(this.chart) {
// #ifndef APP-NVUE
this.chart.showLoading(...arguments)
// #endif
// #ifdef APP-NVUE
this.$refs.webview.evalJs(`showLoading(${JSON.stringify(arguments)})`);
// #endif
}
},
hideLoading() {
if(this.chart) {
// #ifndef APP-NVUE
this.chart.hideLoading()
// #endif
// #ifdef APP-NVUE
this.$refs.webview.evalJs(`hideLoading()`);
// #endif
}
},
clear() {
if(this.chart) {
// #ifndef APP-NVUE
this.chart.clear()
// #endif
// #ifdef APP-NVUE
this.$refs.webview.evalJs(`clear()`);
// #endif
}
},
dispose() {
if(this.chart) {
// #ifndef APP-NVUE
this.chart.dispose()
// #endif
// #ifdef APP-NVUE
this.$refs.webview.evalJs(`dispose()`);
// #endif
}
},
resize(size) {
if(size && size.width && size.height) {
this.height = size.height
this.width = size.width
if(this.chart) {this.chart.resize(size)}
// #ifdef APP-NVUE
this.$refs.webview.evalJs(`resize(${size})`);
// #endif
} else {
this.$nextTick(() => {
// #ifndef APP-NVUE
uni.createSelectorQuery()
.in(this)
.select(`.lime-echart`)
.boundingClientRect()
.exec(res => {
if (res) {
let { width, height } = res[0];
this.width = width = width || 300;
this.height = height = height || 300;
this.chart.resize({width, height})
}
});
// #endif
// #ifdef APP-NVUE
this.$refs.webview.evalJs(`resize()`);
// #endif
})
}
},
canvasToTempFilePath(args = {}) {
// #ifndef APP-NVUE
const { use2dCanvas, canvasId, canvasNode } = this;
return new Promise((resolve, reject) => {
const copyArgs = Object.assign({
canvasId,
success: resolve,
fail: reject
}, args);
if (use2dCanvas) {
delete copyArgs.canvasId;
copyArgs.canvas = canvasNode;
}
uni.canvasToTempFilePath(copyArgs, this);
});
// #endif
// #ifdef APP-NVUE
this.file = ''
this.$refs.webview.evalJs(`canvasToTempFilePath()`);
return new Promise((resolve, reject) => {
this.$watch('file', async (file) => {
if(file) {
const tempFilePath = await base64ToPath(file)
resolve(args.success({tempFilePath}))
} else {
reject(args.fail({error: ``}))
}
})
})
// #endif
},
async init(echarts, ...args) {
// #ifdef APP-NVUE
if(arguments && !arguments.length) {
console.error('缺少参数init(theme?:string, opts?: object, callback: function)')
return
}
// #endif
// #ifndef APP-NVUE
if(arguments && arguments.length < 1) {
console.error('缺少参数init(echarts, theme?:string, opts?: object, callback: function)')
return
}
// #endif
let theme=null,opts={},callback;
Array.from(arguments).forEach(item => {
if(typeof item === 'function') {
callback = item
}
if(['string'].includes(typeof item)) {
theme = item
}
if(typeof item === 'object') {
opts = item
}
})
if(this.beforeDelay) {
await sleep(this.beforeDelay)
}
let config = await this.getContext();
// #ifndef APP-NVUE
setCanvasCreator(echarts, config)
this.chart = echarts.init(config.canvas, theme, Object.assign({}, config, opts))
if(typeof callback === 'function') {
callback(this.chart)
} else {
return this.chart
// console.info('callback 非 function')
}
// #endif
// #ifdef APP-NVUE
if(callback) {
this.chart = {
setOption: (options) => {
this.roptions = options
}
}
callback(this.chart)
this.$refs.webview.evalJs(`init(${JSON.stringify(callback.toString())}, ${JSON.stringify(this.roptions)}, ${JSON.stringify(opts)}, ${theme})`)
} else {
console.error('callback 非 function')
}
// #endif
},
getContext() {
// #ifdef APP-NVUE
if(this.finished) {
return Promise.resolve(this.finished)
}
return new Promise(resolve => {
this.$watch('finished', (val) => {
if(val) {
resolve(this.finished)
}
})
})
// #endif
// #ifndef APP-NVUE
const { use2dCanvas } = this;
let dpr = devicePixelRatio
if (use2dCanvas) {
return new Promise(resolve => {
uni.createSelectorQuery()
.in(this)
.select(`#${this.canvasId}`)
.fields({
node: true,
size: true
})
.exec(res => {
let { node, width, height } = res[0];
this.width = width = width || 300;
this.height = height = height || 300;
const ctx = node.getContext('2d');
const canvas = new Canvas(ctx, this, true, node);
this.canvasNode = node
resolve({ canvas, width, height, devicePixelRatio: dpr, node });
});
});
}
return new Promise(resolve => {
uni.createSelectorQuery()
.in(this)
.select(`#${this.canvasId}`)
.boundingClientRect()
.exec(res => {
if (res) {
let { width, height } = res[0];
this.width = width = width || 300;
this.height = height = height || 300;
// #ifdef MP-TOUTIAO
dpr = !this.isPC ? devicePixelRatio : 1// 1.25
// #endif
// #ifndef MP-ALIPAY || MP-TOUTIAO
dpr = this.isPC ? devicePixelRatio : 1
// #endif
// #ifdef MP-ALIPAY || MP-LARK
dpr = devicePixelRatio
// #endif
this.rect = res[0]
this.nodeWidth = width * dpr;
this.nodeHeight = height * dpr;
const ctx = uni.createCanvasContext(this.canvasId, this);
const canvas = new Canvas(ctx, this, false);
resolve({ canvas, width, height, devicePixelRatio: dpr });
}
});
});
// #endif
},
// #ifndef APP-NVUE
getRelative(e) {
return {x: e.pageX - this.rect.left, y: e.pageY - this.rect.top, wheelDelta: e.wheelDelta}
},
getTouch(e) {
return e.touches && e.touches[0] && e.touches[0].x ? e.touches[0] : this.getRelative(e);
},
touchStart(e) {
this.isDown = true
if (this.chart && ((e.touches.length > 0 || e.touches['0']) && e.type != 'mousemove' || e.type == 'mousedown')) {
const touch = this.getTouch(e)
this.startX = touch.x
this.startY = touch.y
this.startT = new Date()
const handler = this.chart.getZr().handler;
dispatch.call(handler, 'mousedown', touch)
dispatch.call(handler, 'mousemove', touch)
handler.processGesture(wrapTouch(e), 'start');
clearTimeout(this.endTimer);
}
},
touchMove(e) {
if(this.isPc && this.enableHover && !this.isDown) {this.isDown = true}
if (this.chart && ((e.touches.length > 0 || e.touches['0']) && e.type != 'mousemove' || e.type == 'mousemove' && this.isDown)) {
const handler = this.chart.getZr().handler;
dispatch.call(handler, 'mousemove', this.getTouch(e))
handler.processGesture(wrapTouch(e), 'change');
}
},
touchEnd(e) {
this.isDown = false
if (this.chart) {
const {x} = e.changedTouches && e.changedTouches[0] || {}
const touch = (x ? e.changedTouches[0] : this.getRelative(e)) || {};
const handler = this.chart.getZr().handler;
const isClick = Math.abs(touch.x - this.startX) < 10 && new Date() - this.startT < 200;
dispatch.call(handler, 'mouseup', touch)
handler.processGesture(wrapTouch(e), 'end');
if(isClick) {
dispatch.call(handler, 'click', touch)
} else {
this.endTimer = setTimeout(() => {
dispatch.call(handler, 'mousemove', {x: 999999999,y: 999999999});
dispatch.call(handler, 'mouseup', {x: 999999999,y: 999999999});
},50)
}
}
}
// #endif
}
};
</script>
<style scoped>
.lime-echart {
position: relative;
/* #ifndef APP-NVUE */
width: 100%;
height: 100%;
/* #endif */
/* #ifdef APP-NVUE */
flex: 1;
/* #endif */
}
.lime-echart__canvas {
/* #ifndef APP-NVUE */
width: 100%;
height: 100%;
/* #endif */
/* #ifdef APP-NVUE */
flex: 1;
/* #endif */
}
</style>

View File

@@ -0,0 +1,74 @@
// #ifndef APP-NVUE
// 计算版本
export function compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
export function wrapTouch(event) {
for (let i = 0; i < event.touches.length; ++i) {
const touch = event.touches[i];
touch.offsetX = touch.x;
touch.offsetY = touch.y;
}
return event;
}
export const devicePixelRatio = wx.getSystemInfoSync().pixelRatio
// #endif
// #ifdef APP-NVUE
export function base64ToPath(base64) {
return new Promise((resolve, reject) => {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
bitmap.loadBase64Data(base64, () => {
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
const time = new Date().getTime();
const filePath = `_doc/uniapp_temp/${time}.${format}`
bitmap.save(filePath, {},
() => {
bitmap.clear()
resolve(filePath)
},
(error) => {
bitmap.clear()
console.error(`${JSON.stringify(error)}`)
reject(error)
})
}, (error) => {
bitmap.clear()
console.error(`${JSON.stringify(error)}`)
reject(error)
})
})
}
// #endif
export function sleep(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
},time)
})
}

View File

@@ -0,0 +1,22 @@
## 1.1.72024-10-29
修复底部露出部分组件bug
## 1.1.62024-08-20
更新本地数据源为最新数据源
## 1.1.52024-06-12
使用说明文档优化
## 1.1.42024-06-12
增加问题反馈描述
## 1.1.32024-02-29
更新使用文档
## 1.1.22024-01-16
解决Vue3项目导入导出报错问题
## 1.1.12023-12-06
defaultValue可以传入defaultValue:['河北省','唐山市','丰南区']数组类型以及defaultValue: '420103'地区编码字符串类型
## 1.1.02023-12-05
即默认值传入地区编码,也支持传入中文省市区数组
## 1.0.92023-12-04
优化
## 1.0.82023-10-24
修复东菀市和中山市下各镇的行政编码错误问题
## 1.0.42023-09-15
改为uni_modules规范

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,281 @@
<template>
<view class="pupop">
<view class="popup-box" :animation="animationData">
<view class="pupop-btn">
<view @tap="cancel">取消</view>
<view @tap="confirm" style="color: #2979ff;">确定</view>
</view>
<picker-view :value="value" :indicator-style="indicatorStyle" @change="bindChange" class="picker-view">
<picker-view-column>
<view class="item" v-for="(item,index) in provinceList" :key="index">{{item.name}}</view>
</picker-view-column>
<picker-view-column>
<view class="item" v-for="(item,index) in cityList" :key="index">{{item.name}}</view>
</picker-view-column>
<picker-view-column v-if="column == 3">
<view class="item" v-for="(item,index) in areaList" :key="index">{{item.name}}</view>
</picker-view-column>
</picker-view>
</view>
<view @tap="close" @touchmove.stop.prevent :class="visible ? 'pupop-model' : 'pupop-models'"></view>
</view>
</template>
<script>
import {
addressList
} from './cityData.js'
export default {
data() {
return {
value: [],
addressList,
provinceList: [],
cityList: [],
areaList: [],
indicatorStyle: `height: 50px;`,
provinceIndex: 0,
cityIndex: 0,
areaIndex: 0,
animationData: {}
}
},
props: {
column: {
type: Number,
default: 3
},
defaultValue: {
default: () => ''
},
visible: {
type: Boolean,
default: () => false
},
maskCloseAble: {
type: Boolean,
default: () => true
},
},
watch: {
visible (val) {
this.changeActive()
},
defaultValue() {
this.init()
}
},
created() {
this.init()
},
methods: {
init () {
var provinceList = []
addressList.filter(item => {
provinceList.push({
code: item.code,
name: item.name
})
})
this.provinceList = [...provinceList]
if (!this.defaultValue) {
this.cityList = addressList[0].children
this.areaList = addressList[0].children[0].children
} else {
var city = {11:"北京",12:"天津",13:"河北",14:"山西",15:"内蒙古",21:"辽宁",22:"吉林",23:"黑龙江 ",31:"上海",32:"江苏",33:"浙江",34:"安徽",35:"福建",36:"江西",37:"山东",41:"河南",42:"湖北 ",43:"湖南",44:"广东",45:"广西",46:"海南",50:"重庆",51:"四川",52:"贵州",53:"云南",54:"西藏 ",61:"陕西",62:"甘肃",63:"青海",64:"宁夏",65:"新疆",71:"台湾",81:"香港",82:"澳门",91:"国外 "}
if (Array.isArray(this.defaultValue) && this.defaultValue.length >= 2) {
console.log(this.defaultValue)
var province = this.defaultValue[0]
var city = this.defaultValue[1]
this.provinceIndex = 0
this.cityIndex = 0
this.areaIndex = 0
this.provinceIndex = addressList.findIndex(obj => {
return obj.name == province
})
this.provinceIndex = this.provinceIndex >= 0 ? this.provinceIndex : 0
this.cityList = addressList[this.provinceIndex].children
this.cityIndex = this.cityList.findIndex(obj => {
return obj.name == city
})
this.cityIndex = this.cityIndex >= 0 ? this.cityIndex : 0
this.areaList = this.cityList[this.cityIndex].children
if (this.defaultValue.length > 2) {
this.areaIndex = this.areaList.findIndex(obj => {
return obj.name == this.defaultValue[2]
})
}
this.areaIndex = this.areaIndex >= 0 ? this.areaIndex : 0
this.$nextTick(() => {
if (this.column == 3) {
this.value = JSON.parse(JSON.stringify([this.provinceIndex, this.cityIndex, this.areaIndex]))
} else if (this.column == 2) {
this.value = JSON.parse(JSON.stringify([this.provinceIndex, this.cityIndex]))
}
})
} else if (/^\d{6}$/.test(this.defaultValue)) {
var province = this.defaultValue.substr(0, 2)
var city = this.defaultValue.substr(0, 4)
this.provinceIndex = 0
this.cityIndex = 0
this.areaIndex = 0
this.provinceIndex = addressList.findIndex(obj => {
return obj.code == province
})
this.provinceIndex = this.provinceIndex >= 0 ? this.provinceIndex : 0
this.cityList = addressList[this.provinceIndex].children
this.cityIndex = this.cityList.findIndex(obj => {
return obj.code == city
})
this.cityIndex = this.cityIndex >= 0 ? this.cityIndex : 0
this.areaList = this.cityList[this.cityIndex].children
this.areaIndex = this.areaList.findIndex(obj => {
return obj.code == this.defaultValue
})
this.areaIndex = this.areaIndex >= 0 ? this.areaIndex : 0
this.$nextTick(() => {
if (this.column == 3) {
this.value = JSON.parse(JSON.stringify([this.provinceIndex, this.cityIndex, this.areaIndex]))
} else if (this.column == 2) {
this.value = JSON.parse(JSON.stringify([this.provinceIndex, this.cityIndex]))
}
})
} else {
uni.showToast({
title: '地区编码格式不正确',
icon: 'none'
})
console.log('地区编码格式不正确')
}
}
this.changeActive()
},
confirm () {
if (this.column == 3) {
this.$emit('confirm', {
code: addressList[this.provinceIndex].children[this.cityIndex].children[this.areaIndex].code,
name: addressList[this.provinceIndex].name + addressList[this.provinceIndex].children[this.cityIndex].name + addressList[this.provinceIndex].children[this.cityIndex].children[this.areaIndex].name,
provinceName: addressList[this.provinceIndex].name,
cityName: addressList[this.provinceIndex].children[this.cityIndex].name,
areaName: addressList[this.provinceIndex].children[this.cityIndex].children[this.areaIndex].name
})
} else if (this.column == 2) {
this.$emit('confirm', {
code: addressList[this.provinceIndex].children[this.cityIndex].children[this.areaIndex].code.substring(0, 4) + '00',
name: addressList[this.provinceIndex].name + addressList[this.provinceIndex].children[this.cityIndex].name,
provinceName: addressList[this.provinceIndex].name,
cityName: addressList[this.provinceIndex].children[this.cityIndex].name
})
} else {
uni.showToast({
title: '目前column只能传2和3',
icon: 'none'
})
}
},
cancel () {
this.$emit('cancel')
},
// 动画
changeActive () {
var active = '-415px'
if (this.visible) {
active = 0
}
var animation = uni.createAnimation({
duration: 400,
timingFunction: 'linear'
})
animation.bottom(active).step()
this.animationData = animation.export()
},
bindChange(e) {
e.detail.value[0] = e.detail.value[0] || 0
e.detail.value[1] = e.detail.value[1] || 0
e.detail.value[2] = e.detail.value[2] || 0
if (e.detail.value[0] != this.provinceIndex) {
// console.log('监听第一列')
this.provinceChange(e.detail.value[0])
} else if (e.detail.value[1] != this.cityIndex) {
// console.log('监听第二列')
this.cityChange(e.detail.value[1])
} else if (e.detail.value[2] != this.areaIndex) {
// console.log('监听第三列')
this.areaChange(e.detail.value[2])
}
},
// 监听第一列变化
provinceChange (e) {
this.provinceIndex = e
this.cityIndex = 0
this.areaIndex = 0
this.value = [...[e, 0, 0]]
this.cityList = addressList[e].children
this.areaList = addressList[e].children[0].children
},
// 监听第二列变化
cityChange (e) {
this.cityIndex = e
this.areaIndex = 0
// console.log(this.cityIndex, this.areaIndex)
this.value = [...[this.provinceIndex, e, 0]]
this.cityList = addressList[this.provinceIndex].children
this.areaList = addressList[this.provinceIndex].children[e].children
},
// 监听第三列变化
areaChange (e) {
this.areaIndex = e
},
// 点击模态框
close () {
if (this.maskCloseAble) {
this.cancel()
}
}
}
}
</script>
<style scoped lang="scss">
.pupop {
.popup-box {
position: fixed;
left: 0;
bottom: -315px;
z-index: 99999;
background: #fff;
padding-bottom: 50px;
.pupop-btn{
height: 40px;
display: flex;
align-items: center;
padding: 0 30upx;
justify-content: space-between;
}
}
.pupop-model {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 9999;
background: rgba(0, 0, 0, .5);
}
.pupop-models{
display: none;
}
.picker-view {
width: 750rpx;
height: 225px;
margin-top: 20rpx;
}
.item {
height: 50px;
align-items: center;
justify-content: center;
text-align: center;
line-height: 50px;
}
}
</style>

View File

@@ -0,0 +1,17 @@
{
"id": "piaoyi-cityPicker",
"name": " data-cityPicker省市区地址选择器",
"displayName": " data-cityPicker省市区地址选择器(兼容vue3)",
"version": "1.1.7",
"description": "常用省市区选择器可反选自定义模态框、兼容vue3",
"keywords": [
"省市区",
"选择器",
"可反选",
"自定义模态框和弹框",
"兼容vue3"
],
"dcloudext": {
"type": "component-vue"
}
}

View File

@@ -0,0 +1,106 @@
## 1.0.72023-09-15
改为uni_modules规范
### cityPicker 省市区选择器兼容vue3
**使用方法:**
```
<template>
<view>
<button @tap="open">打开</button>
<cityPicker :column="column" :default-value="defaultValue" :mask-close-able="maskCloseAble" @confirm="confirm" @cancel="cancel" :visible="visible"/>
<view>{{str}}</view>
</view>
</template>
<script>
import cityPicker from '@/uni_modules/piaoyi-cityPicker/components/piaoyi-cityPicker/piaoyi-cityPicker'
export default {
data() {
return {
visible: false,
maskCloseAble: true,
str: '',
defaultValue: '420103',
// defaultValue: ['河北省','唐山市','丰南区'],
column: 3
}
},
components: {
cityPicker
},
methods: {
open () {
this.visible = true
},
confirm (val) {
console.log(val)
this.str = JSON.stringify(val)
this.visible = false
},
cancel () {
this.visible = false
}
},
onShareAppMessage(res) {
if (res.from === 'button') { // 来自页面内分享按钮
console.log(res.target)
}
return {
title: 'data-cityPicker省市区地址选择器',
path: '/pages/cityPicker/cityPicker'
}
},
onShareTimeline(res) {
if (res.from === 'button') { // 来自页面内分享按钮
console.log(res.target)
}
return {
title: 'data-cityPicker省市区地址选择器'
}
}
}
</script>
<style lang="scss">
</style>
```
### 注:近期收到使用用户反馈,存在以下一个问题(如有好的建议,期待私信,谢谢)
1、之前只支持默认值传入地区编码已更新可以支持传入中文省市区数组
defaultValue可以传入defaultValue:['河北省','唐山市','丰南区']数组类型以及defaultValue: '420103'地区编码字符串类型
可以使用const reg =/([\u4e00-\u9fa5]+省|自治区|[\u4e00-\u9fa5]+市|[\u4e00-\u9fa5]+区)/g;将自己的省市区数据进行处理为数组再传入
2、有些用户反馈vue3下watch监听有问题
我自己创建一个vue项目导入插件后按照示例原封不动进行测试没有发现问题; 发现有问题的朋友可以提供一下可以复现的demo给我我这边看看具体什么情况
3、有些用户返回无法关闭弹框
不要把插件放到scroll-view里面请务必放到最外层进行使用
#### 事件说明
| 事件名 | 返回值 | 描述 |
| :---------: | :----: | :------------: |
| @confirm | 对象(code,完整地区名称) | 点击确定的回调 |
| @cancel | 无 | 点击取消的回调 |
#### Prop
| 参数名称 | 描述 |
| -------- | ------------------------------ |
| visible | 控制选择器显示和隐藏 |
| column | 可选值2和32是省市两列选择3是省市区三列选择 |
| maskCloseAble | 点击模态框是否关闭弹框 |
| defaultValue | 初始地区编码(例:420102或者['河北省','唐山市','丰南区'] |
### 数据来源:[点击查看省市区数据来源](https://github.com/modood/Administrative-divisions-of-China/blob/master/dist/pca-code.json)
### 可接定制化组件开发
### 右侧有本人代表作小程序二维码,可以扫码体验
### 如使用过程中有问题或有一些好的建议欢迎加QQ群互相学习交流120594820

View File

@@ -0,0 +1,72 @@
## 2.2.52024-07-30
* 修复 当 checkRange=true 时,拖动四个伸缩角放大图片时还可能会超出或未到边界的问题
* 修复 当 checkRange=false 时,图片旋转时会放大图片适应裁剪尺寸的问题
* 修复 当 checkRange=true 时,图片旋转 90° 或 270° 进行缩放可能会无法拖动图片的问题
## 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,855 @@
/**
* 图片编辑器-手势监听
* 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
/** 容错值 */
var fault = 0.000001;
/**
* 获取a、b两数中的最小正数
* @param a
* @param b
*/
function minimum(a, b) {
if (a > 0 && b < 0) return a;
if (a < 0 && b > 0) return b;
if (a > 0 && b > 0) return Math.min(a, b);
return 0;
}
/**
* 在容错访问内获取n近似值
* @param n
*/
function num(n) {
var m = parseFloat((n).toFixed(6));
return m === fault || m === -fault ? 0 : m;
}
/**
* 比较a值在容错值范围内是否等于b值
* @param a
* @param b
*/
function equalsByFault(a, b) {
return Math.abs(a - b) <= fault;
}
/**
* 比较a值在容错值范围内是否小于b值
* @param a
* @param b
*/
function lessThanByFault(a, b) {
var c = a - b;
return c < 0 ? c < -fault : c < fault;
}
/**
* 验证并获取有效最大值
* @param v
* @param max
* @param isInclude
* @param x
* @param y
* @param rate
* @returns
*/
function validMax(v, max, isInclude, x, y, rate) {
if(typeof max === 'number') {
if(isInclude && equalsByFault(max, y)) { // 宽高不等时x轴用y轴值要做等比例转换
var n = num(max * rate);
if (n <= x) return n; // 转化后值在x轴最大值范围内
return x; // 转化后值超出x轴最大值范围则用最大值
}
return max;
}
return v;
}
/**
* 样式对象转字符串
* @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 num(reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c));
}
/**
* 旋转90°或270°时检查边界限制 x、y 拖动范围,禁止滑出边界
* @param {Object} e 点坐标
* @param {Object} xReverse x是否反向
* @param {Object} yReverse y是否反向
*/
function checkRotateRange(e, xReverse, yReverse) {
var o = num((img.height - img.width) / 2); // 宽高差值一半
return {
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, xReverse),
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, yReverse)
};
}
/**
* 检查边界:限制 x、y 拖动范围,禁止滑出边界
* @param {Object} e 点坐标
*/
function checkRange(e) {
var r = rotate / 90 % 2;
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
if (area.width === area.height) {
return checkRotateRange(e, img.height < area.height, img.width < area.width);
}
var isInclude = img.height < area.width && img.width < area.height; // 图片是否包含在裁剪区域内
if (img.width < area.height || img.height < area.width) {
if (area.width < area.height && img.width < img.height) {
return isInclude
? checkRotateRange(e, area.width < area.height, area.width < area.height)
: checkRotateRange(e, false, true);
}
if (area.height < area.width && img.height < img.width) {
return isInclude
? checkRotateRange(e, area.height < area.width, area.height < area.width)
: checkRotateRange(e, true, false);
}
}
if (img.height >= area.width && img.width >= area.height) {
return checkRotateRange(e, false, false);
}
if (isInclude) {
return area.height < area.width
? checkRotateRange(e, true, true)
: checkRotateRange(e, area.width < area.height, area.width < area.height);
}
if (img.height < area.width && !img.width < area.height) {
return checkRotateRange(e, true, false);
}
if (!img.height < area.width && img.width < area.height) {
return checkRotateRange(e, false, true);
}
return checkRotateRange(e, img.height < area.height, 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 = num(img.oldWidth * scale);
img.height = num(img.oldHeight * scale);
// 参考问题有一个长4000px、宽4000px的四方形ABCDA点的坐标固定在(-2000,-2000)
// 该四边形上有一个点E坐标为(-100,-300)将该四方形复制一份并缩小到90%后,
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
e.x = num((e.x - offset.x) * (1 - scale / last));
e.y = num((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 = num(area.width * (1 - area.minScale));
var maxY = num(area.height * (1 - area.minScale));
// console.log(x, y, maxX, maxY, offset, area)
touches[0] = point;
var r = rotate / 90 % 2;
var m = r === 1 ? num((img.height - img.width) / 2) : 0; // 宽高差值一半
var xCompare = r === 1 ? lessThanByFault(img.height, area.width) : lessThanByFault(img.width, area.width);
var yCompare = r === 1 ? lessThanByFault(img.width, area.height) : lessThanByFault(img.height, area.height)
var isInclude = xCompare && yCompare;
var isIntersect = area.checkRange && (xCompare || yCompare); // 图片是否包含在裁剪区域内
var isReverse = !isInclude || num((offset.x - area.left) / area.width) <= num((offset.y - area.top) / area.height) || (area.width > area.height && img.width < img.height && r === 1);
switch(activeAngle) {
case 1: // 左上角
x = num(x + areaOffset.left);
y = num(y + areaOffset.top);
if(x >= 0 && y >= 0) { // 有效滑动
var t = num(offset.y + m - area.top);
var l = num(offset.x - m - area.left);
// && (offset.x + img.width < area.right || offset.y + img.height < area.bottom)
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(x > y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(x > maxX) x = maxX;
y = num(x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(y > maxY) y = maxY;
x = num(y * area.width / area.height);
}
areaOffset.left = x;
areaOffset.top = y;
}
break;
case 2: // 右上角
x = num(x + areaOffset.right);
y = num(y + areaOffset.top);
if(x <= 0 && y >= 0) { // 有效滑动
var w = (r === 1 ? img.height : img.width);
var t = num(offset.y + m - area.top);
var l = num(area.right + m - offset.x - w);
var max = isIntersect && ((t >= 0) || (l >= 0))
? minimum(t, l)
: false;
if(-x > y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(-x > maxX) x = -maxX;
y = num(-x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(y > maxY) y = maxY;
x = num(-y * area.width / area.height);
}
areaOffset.right = x;
areaOffset.top = y;
}
break;
case 3: // 左下角
x += num(x + areaOffset.left);
y += num(y + areaOffset.bottom);
if(x >= 0 && y <= 0) { // 有效滑动
var w = (r === 1 ? img.width : img.height);
var t = num(area.bottom - m - offset.y - w);
var l = num(offset.x - m - area.left);
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(x > -y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(x > maxX) x = maxX;
y = num(-x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(-y > maxY) y = -maxY;
x = num(-y * area.width / area.height);
}
areaOffset.left = x;
areaOffset.bottom = y;
}
break;
case 4: // 右下角
x = num(x + areaOffset.right);
y = num(y + areaOffset.bottom);
if(x <= 0 && y <= 0) { // 有效滑动
var w = (r === 1 ? img.height : img.width);
var h = (r === 1 ? img.width : img.height);
var t = num(area.bottom - offset.y - h - m);
var l = num(area.right + m - offset.x - w);
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(-x > -y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(-x > maxX) x = -maxX;
y = num(x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(-y > maxY) y = -maxY;
x = num(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 && area.checkRange) {
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
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,743 @@
<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.toFixed(2);
this.oldHeight = +this.scaleHeight.toFixed(2);
},
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: $uni-bg-color-grey;
.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: $uni-color-primary;
padding: 0 $uni-spacing-row-lg;
line-height: 100rpx;
}
.choose-btn {
color: $uni-color-primary;
text-align: center;
line-height: 100rpx;
flex: 1;
}
.button {
margin: auto $uni-spacing-row-lg auto auto;
background-color: $uni-color-primary;
color: #fff;
}
}
.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,727 @@
/**
* 图片编辑器-手势监听
* 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 };
/** 容错值 */
var fault = 0.000001;
/**
* 获取a、b两数中的最小正数
* @param a
* @param b
*/
function minimum(a, b) {
if (a > 0 && b < 0) return a;
if (a < 0 && b > 0) return b;
if (a > 0 && b > 0) return Math.min(a, b);
return 0;
}
/**
* 在容错访问内获取n近似值
* @param n
*/
function num(n) {
var m = parseFloat((n).toFixed(6));
return m === fault || m === -fault ? 0 : m;
}
/**
* 比较a值在容错值范围内是否等于b值
* @param a
* @param b
*/
function equalsByFault(a, b) {
return Math.abs(a - b) <= fault;
}
/**
* 比较a值在容错值范围内是否小于b值
* @param a
* @param b
*/
function lessThanByFault(a, b) {
var c = a - b;
return c < 0 ? c < -fault : c < fault;
}
/**
* 验证并获取有效最大值
* @param v
* @param max
* @param isInclude
* @param x
* @param y
* @param rate
* @returns
*/
function validMax(v, max, isInclude, x, y, rate) {
if(typeof max === 'number') {
if(isInclude && equalsByFault(max, y)) { // 宽高不等时x轴用y轴值要做等比例转换
var n = num(max * rate);
if (n <= x) return n; // 转化后值在x轴最大值范围内
return x; // 转化后值超出x轴最大值范围则用最大值
}
return max;
}
return v;
}
/**
* 计算两点间距
* @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 num(reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c));
}
/**
* 旋转90°或270°时检查边界限制 x、y 拖动范围,禁止滑出边界
* @param {Object} e 点坐标
* @param {Object} xReverse x是否反向
* @param {Object} yReverse y是否反向
*/
function checkRotateRange(e, xReverse, yReverse) {
var o = num((img.height - img.width) / 2); // 宽高差值一半
return {
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, xReverse),
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, yReverse)
};
}
/**
* 检查边界:限制 x、y 拖动范围,禁止滑出边界
* @param {Object} e 点坐标
*/
function checkRange(e) {
var r = rotate / 90 % 2;
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
if (area.width === area.height) {
return checkRotateRange(e, img.height < area.height, img.width < area.width);
}
var isInclude = img.height < area.width && img.width < area.height; // 图片是否包含在裁剪区域内
if (img.width < area.height || img.height < area.width) {
if (area.width < area.height && img.width < img.height) {
return isInclude
? checkRotateRange(e, area.width < area.height, area.width < area.height)
: checkRotateRange(e, false, true);
}
if (area.height < area.width && img.height < img.width) {
return isInclude
? checkRotateRange(e, area.height < area.width, area.height < area.width)
: checkRotateRange(e, true, false);
}
}
if (img.height >= area.width && img.width >= area.height) {
return checkRotateRange(e, false, false);
}
if (isInclude) {
return area.height < area.width
? checkRotateRange(e, true, true)
: checkRotateRange(e, area.width < area.height, area.width < area.height);
}
if (img.height < area.width && !img.width < area.height) {
return checkRotateRange(e, true, false);
}
if (!img.height < area.width && img.width < area.height) {
return checkRotateRange(e, false, true);
}
return checkRotateRange(e, img.height < area.height, 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 = num(img.oldWidth * scale);
img.height = num(img.oldHeight * scale);
// 参考问题有一个长4000px、宽4000px的四方形ABCDA点的坐标固定在(-2000,-2000)
// 该四边形上有一个点E坐标为(-100,-300)将该四方形复制一份并缩小到90%后,
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
e.x = num((e.x - offset.x) * (1 - scale / last));
e.y = num((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 && area.checkRange) {
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
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 = num(area.width * (1 - area.minScale));
var maxY = num(area.height * (1 - area.minScale));
// console.log(x, y, maxX, maxY, offset, area)
touches[0] = point;
var r = rotate / 90 % 2;
var m = r === 1 ? num((img.height - img.width) / 2) : 0; // 宽高差值一半
var xCompare = r === 1 ? lessThanByFault(img.height, area.width) : lessThanByFault(img.width, area.width);
var yCompare = r === 1 ? lessThanByFault(img.width, area.height) : lessThanByFault(img.height, area.height)
var isInclude = xCompare && yCompare;
var isIntersect = area.checkRange && (xCompare || yCompare); // 图片是否包含在裁剪区域内
var isReverse = !isInclude || num((offset.x - area.left) / area.width) <= num((offset.y - area.top) / area.height) || (area.width > area.height && img.width < img.height && r === 1);
switch(activeAngle) {
case 1: // 左上角
x = num(x + areaOffset.left);
y = num(y + areaOffset.top);
if(x >= 0 && y >= 0) { // 有效滑动
var t = num(offset.y + m - area.top);
var l = num(offset.x - m - area.left);
// && (offset.x + img.width < area.right || offset.y + img.height < area.bottom)
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(x > y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(x > maxX) x = maxX;
y = num(x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(y > maxY) y = maxY;
x = num(y * area.width / area.height);
}
areaOffset.left = x;
areaOffset.top = y;
}
break;
case 2: // 右上角
x = num(x + areaOffset.right);
y = num(y + areaOffset.top);
if(x <= 0 && y >= 0) { // 有效滑动
var w = (r === 1 ? img.height : img.width);
var t = num(offset.y + m - area.top);
var l = num(area.right + m - offset.x - w);
var max = isIntersect && ((t >= 0) || (l >= 0))
? minimum(t, l)
: false;
// var max = isInclude && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y >= area.top))
// ? minimum(offset.y - area.top, area.right - offset.x - img.width)
// : false;
// console.log(offset.x, offset.y, img.width, img.height, area.top, area.right, m, max)
// console.log(offset.y + m - area.top, area.right + m - offset.x - w)
if(-x > y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(-x > maxX) x = -maxX;
y = num(-x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(y > maxY) y = maxY;
x = num(-y * area.width / area.height);
}
areaOffset.right = x;
areaOffset.top = y;
}
break;
case 3: // 左下角
x += num(x + areaOffset.left);
y += num(y + areaOffset.bottom);
if(x >= 0 && y <= 0) { // 有效滑动
var w = (r === 1 ? img.width : img.height);
var t = num(area.bottom - m - offset.y - w);
var l = num(offset.x - m - area.left);
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(x > -y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(x > maxX) x = maxX;
y = num(-x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(-y > maxY) y = -maxY;
x = num(-y * area.width / area.height);
}
areaOffset.left = x;
areaOffset.bottom = y;
}
break;
case 4: // 右下角
x = num(x + areaOffset.right);
y = num(y + areaOffset.bottom);
if(x <= 0 && y <= 0) { // 有效滑动
var w = (r === 1 ? img.height : img.width);
var h = (r === 1 ? img.width : img.height);
var t = num(area.bottom - offset.y - h - m);
var l = num(area.right + m - offset.x - w);
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(-x > -y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(-x > maxX) x = -maxX;
y = num(x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(-y > maxY) y = -maxY;
x = num(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.5",
"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,97 @@
# 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域名白名单才能生效。
5.如果组件无法正常渲染且使用了 `v-if` 时,可尝试将 `v-if` 替换为 `v-show`
6.如果App端导入组件后无法正常渲染请尝试重新运行

13
tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"compilerOptions": {
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"lib": ["esnext", "dom"],
"types": ["@dcloudio/types"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

7
vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [uni()],
});