This commit is contained in:
2026-09-24 16:25:22 +08:00
commit 7428184f01
1198 changed files with 314515 additions and 0 deletions
+259
View File
@@ -0,0 +1,259 @@
import Context from 'android.content.Context'
import LinearLayout from 'android.widget.LinearLayout';
import ViewGroup from 'android.view.ViewGroup';
import View from 'android.view.View';
import TextView from 'android.widget.TextView';
import ImageView from 'android.widget.ImageView';
import Gravity from 'android.view.Gravity';
import Color from 'android.graphics.Color';
import Activity from 'android.app.Activity';
import { ref} from "vue"
import TextUtils from 'android.text.TextUtils';
import GradientDrawable from 'android.graphics.drawable.GradientDrawable'
import MotionEvent from 'android.view.MotionEvent';
import Typeface from 'android.graphics.Typeface';
import {hexToRgb,getDefaultColor,toFillMarginAr,colorGetHover,colorGetThint} from "../util/xCoreColorUtil.uts"
import {getUid,dp2px} from "../util/xCoreUtil.uts"
import xLinearView from "./linearView.uts";
import xCardView from "./cardView.uts";
import xIcon from "./icon.uts";
import xText from "./text.uts";
class xButton {
view:xLinearView;
textLayer:xText;
iconLayer:xIcon;
bgLayer:xCardView;
wrapLinear:xLinearView;
width:number = 180;
heigth:number = 64;
private _backgroundColor = "primary";
status = 'default';//default,success,warn,danger
type = 'primary';//normal,primary,secondary,dashed,outlin,text
constructor(context : Context){
let boxLinear = new xLinearView(context)
let wrapLinear = new xLinearView(context)
let bgView = new xCardView(context)
let iconView = new xIcon(context)
let laberView = new xText(context)
boxLinear.setLayoutParams(this.width.toInt(),this.heigth.toInt(),0)
boxLinear.setAlign(Gravity.CENTER)
wrapLinear.setLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT,0)
wrapLinear.setAlign(Gravity.CENTER_VERTICAL|Gravity.CENTER)
bgView.setRadius(6);
iconView.setFontColor("white").setFontSize(18)
laberView.setBackgroundColor('transparent').setFontColor("white").setFontSize(16)
laberView.setLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT,0)
wrapLinear.append(iconView.getView(),laberView.getView())
bgView.append(wrapLinear.getView() as View)
boxLinear.append(bgView.getView() as View)
this.textLayer = laberView;
this.wrapLinear = wrapLinear;
this.bgLayer = bgView;
this.iconLayer = iconView;
this.view = boxLinear;
this._setBackgroundColor('','up')
bgView.setTouchStart((event : MotionEvent):void=>{
this._setBackgroundColor('','down')
}).setTouchEnd((event : MotionEvent):void=>{
this._setBackgroundColor('','up')
})
}
/**
* type:
* auto:按钮宽度自动为内容宽,w宽度失效。
* block:按钮宽度自动为父级宽,w宽度失效。
* '':空值,取w,h为按钮宽和高。
* 'mini':44,24,10
* 'small':64,32,12
* 'medium':180,64,15
* 'large':220,76,16
*/
setSize(w:number,h:number,type= ''):xButton{
if(type =='auto'){
this.width = w;
this.heigth = h;
this.view.setLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,this.heigth.toInt(),0)
}else if(type =='mini'){
this.heigth = 28;
this.view.setLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,this.heigth.toInt(),0)
// this.bgLayer.setLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,this.heigth.toInt(),0)
this.setFontSize(10)
}else if(type =='small'){
this.width = 64;
this.heigth = 32;
this.view.setLayoutParams(this.width.toInt(),this.heigth.toInt(),0)
}else if(type =='medium'||type==''){
this.width = 180;
this.heigth = 64;
this.view.setLayoutParams(this.width.toInt(),this.heigth.toInt(),0)
}else if(type =='large'){
this.width = 220;
this.heigth = 76;
this.view.setLayoutParams(this.width.toInt(),this.heigth.toInt(),0)
}
return this;
}
//primary,secondary,dashed,outlin,text
setType(str:string):xButton{
this.type = str;
this._setBackgroundColor('','up')
return this;
}
//primary,secondary,dashed,outlin,text
setStatus(str:string):xButton{
this.status = str;
this._setBackgroundColor('','up')
return this;
}
setDisabled(dis:boolean) : xButton{
this.bgLayer.setDisabled(dis);
this._setBackgroundColor("#A5A5A5",'down')
return this;
}
setLabel(str:string):xButton{
let label = this.textLayer as xText;
label.setText(str);
return this;
}
setRadius(radius:number):xButton{
let bgView = this.bgLayer as xCardView;
bgView.setRadius(radius);
return this;
}
setClick(fun?:(event: MotionEvent)=>void) : xButton{
if(typeof fun !== 'undefined'){
this.bgLayer.setClick(fun)
}
return this;
}
setIcon(name:string):xButton{
let iconLayer = this.iconLayer as xIcon;
let label = this.textLayer as xText;
iconLayer.setIcon(name);
label.setPadding([10,0,0,0])
return this;
}
setFontSize(size:number):xButton{
let label = this.textLayer as xText;
let iconLayer = this.iconLayer as xIcon;
label.setFontSize(size.toInt())
iconLayer.setFontSize(size.toInt())
return this;
}
setFontColor(str:string):xButton{
let label = this.textLayer as xText;
let iconLayer = this.iconLayer as xIcon;
label.setFontColor(str)
iconLayer.setFontColor(str)
return this;
}
setBorder(w?:number,colorStr?:string,dashed?:boolean):xButton{
this.bgLayer.setBorder(w,colorStr,dashed)
return this;
}
private _setBackgroundColor(str:string,clickStatus:string):xButton{
let cr = this._backgroundColor;
let fontColor = ""
if(str !=''){
cr = str;
}else{
if(this.status==""||this.status=="default"){
cr = 'primary'
}else if(this.status == 'success'){
cr = 'green'
}else if(this.status == 'warn'){
cr = 'orange'
}else if(this.status == 'danger'){
cr = 'red'
}else if(this.status == 'normal'){
cr = '#D8D8D8'
fontColor = '#333333'
}
}
let lightColor = colorGetThint(cr);
let defaultColor = colorGetHover(cr);
if(clickStatus=='down'){
if(this.type == 'primary' || this.type==""){
this.bgLayer.setBackgroundColor(defaultColor.getString("hover")!);
}else if(this.type == 'secondary'){
this.setFontColor(cr)
this.bgLayer.setBackgroundColor(lightColor.getString("hover")!);
}else if(this.type == 'outline'){
this.setFontColor(cr)
this.bgLayer.setBackgroundColor('transparent');
this.setBorder(1,cr,false)
}else if(this.type == 'dashed'){
this.setFontColor(cr)
this.bgLayer.setBackgroundColor(lightColor.getString("hover")!);
this.setBorder(1,cr,true)
}else if(this.type == 'text'){
this.setFontColor(defaultColor.getString("hover")!)
this.bgLayer.setBackgroundColor('transparent');
}
}else if(clickStatus=='up'){
if(this.type == 'primary' || this.type==""){
this.bgLayer.setBackgroundColor(defaultColor.getString("default")!);
}else if(this.type == 'secondary'){
this.setFontColor(cr)
this.bgLayer.setBackgroundColor(lightColor.getString("default")!);
}else if(this.type == 'outline'){
this.setFontColor(cr)
this.bgLayer.setBackgroundColor('transparent');
this.setBorder(1,cr,false)
}else if(this.type == 'dashed'){
this.setFontColor(cr)
this.bgLayer.setBackgroundColor(lightColor.getString("default")!);
this.setBorder(1,cr,true)
}else if(this.type == 'text'){
this.setFontColor(cr)
this.bgLayer.setBackgroundColor('transparent');
}
}
if(fontColor!=''){
this.setFontColor(fontColor)
}
return this;
}
setBackgroundColor(str:string):xButton{
this.bgLayer.setBackgroundColor(str);
this._backgroundColor = str;
return this;
}
getView():View{
return this.view.getView() as View;
}
}
export default xButton;
@@ -0,0 +1,242 @@
import Context from 'android.content.Context'
import LinearLayout from 'android.widget.LinearLayout';
import RelativeLayout from 'android.widget.RelativeLayout';
import ViewGroup from 'android.view.ViewGroup';
import View from 'android.view.View';
import TextView from 'android.widget.TextView';
import ImageView from 'android.widget.ImageView';
import Gravity from 'android.view.Gravity';
import Color from 'android.graphics.Color';
import Activity from 'android.app.Activity';
// import CardView from 'androidx.cardview.widget.CardView';
import { ref, } from "vue"
import TextUtils from 'android.text.TextUtils';
import GradientDrawable from 'android.graphics.drawable.GradientDrawable'
import MotionEvent from 'android.view.MotionEvent';
import {hexToRgb,getDefaultColor} from "../util/xCoreColorUtil.uts"
import {getUid, dp2px} from "../util/xCoreUtil.uts"
import xView from "./view.uts";
class xCardView extends xView {
override view:any;
bgLayer:xView;
contentView:any
wrap:RelativeLayout
contentParams:LinearLayout.LayoutParams
constructor(context : Context){
super(context);
let box = new LinearLayout(context);
box.setOrientation(LinearLayout.VERTICAL)
box.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
(0).toInt(),
(0).toFloat()
))
let wrap = new RelativeLayout(context);
let content = new LinearLayout(context);
let RLay = new RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
wrap.setLayoutParams(RLay)
this.bgLayer = new xView(context);
content.setBackground(this.bgLayer.bgView);
content.setClipToOutline(true);
this.contentParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
content.setLayoutParams(this.contentParams)
content.setOrientation(LinearLayout.VERTICAL)
this.contentView = content;
this.wrap =wrap;
wrap.addView(this.bgLayer.getView())
wrap.addView(content as View)
box.addView(wrap as View)
this.view = box;
}
override getView():LinearLayout{
return this.view as LinearLayout;
}
override setBackgroundColor(colorStr : string) : xCardView {
this.bgLayer.setBackgroundColor(colorStr)
return this;
}
override setRadius(radius:any) : xCardView{
this.bgLayer.setRadius(radius)
return this;
}
override setBorder(w?:number,colorStr?:string,dashed?:boolean):xCardView{
this.bgLayer.setBorder(w,colorStr,dashed)
return this;
}
override setHeight(height : number) : xCardView {
console.error("不支持,请通过linerView设置,并将本CardView加入其中来设置高度。")
return this;
}
override setPadding(n:any):xCardView{
let view = this.contentView as ViewGroup;
if(Array.isArray(n)){
let rd = n as number[];
if(rd.length==0){
let rds = 0;
let rdf = dp2px(rds).toInt();
view.setPadding(rdf,rdf,rdf,rdf)
}else if(rd.length==1){
let rdf = dp2px(rd[0]).toInt();
view.setPadding(rdf,rdf,rdf,rdf)
}else if(rd.length==2){
view.setPadding(dp2px(rd[0]).toInt(),dp2px(rd[1]).toInt(),dp2px(rd[0]).toInt(),dp2px(rd[1]).toInt())
}else if(rd.length==3){
let rds = 0;
let rdf = rds.toInt();
view.setPadding(dp2px(rd[0]).toInt(),dp2px(rd[1]).toInt(),dp2px(rd[2]).toInt(),rdf)
}else if(rd.length==4){
view.setPadding(dp2px(rd[0]).toInt(),dp2px(rd[1]).toInt(),dp2px(rd[2]).toInt(),dp2px(rd[3]).toInt())
}
}else{
let rd = n as number;
let rdf = dp2px(rd).toInt();
view.setPadding(rdf,rdf,rdf,rdf)
}
return this;
}
override setDisabled(dis:boolean) : xCardView{
this.bgLayer.setDisabled(dis)
return this;
}
override setSwiper(fun?:(event: MotionEvent,detail?:UTSJSONObject)=>void) : xCardView{
if(typeof fun !== 'undefined'){
this.bgLayer.setSwiper(fun)
}
return this;
}
override setDoubleClick(fun?:(event: MotionEvent)=>void) : xCardView{
if(typeof fun !== 'undefined'){
this.bgLayer.setDoubleClick(fun)
}
return this;
}
override setClick(fun?:(event: MotionEvent)=>void) : xCardView{
if(typeof fun !== 'undefined'){
this.bgLayer.setClick(fun)
}
return this;
}
override setTouchStart(fun?:(event: MotionEvent)=>void) : xCardView{
if(typeof fun !== 'undefined'){
this.bgLayer.setTouchStart(fun)
}
return this;
}
override setTouchMove(fun?:(event: MotionEvent)=>void) : xCardView{
if(typeof fun !== 'undefined'){
this.bgLayer.setTouchMove(fun)
}
return this;
}
override setTouchEnd(fun?:(event: MotionEvent)=>void) : xCardView{
if(typeof fun !== 'undefined'){
this.bgLayer.setTouchEnd(fun)
}
return this;
}
override setTouchCancel(fun?:(event: MotionEvent)=>void) : xCardView{
if(typeof fun !== 'undefined'){
this.bgLayer.setTouchCancel(fun)
}
return this;
}
override setTouchLongPress(fun?:(event: MotionEvent)=>void) : xCardView{
if(typeof fun !== 'undefined'){
this.bgLayer.setTouchCancel(fun)
}
return this;
}
setAlign(cr:Int) : xCardView{
let view = this.contentView as LinearLayout;
view.setGravity(cr)
return this;
}
setLayoutParams(width?:number,height?:number,layouWidth?:number):xCardView{
let view = this.contentView as LinearLayout;
let w = (0).toInt();
let h = ViewGroup.LayoutParams.WRAP_CONTENT;
let flex = (1).toFloat();
if(typeof width =='number'){
let tw = width!;
w = tw.toInt();
}
if(typeof height =='number'){
let th = height!;
h = th.toInt();
}
if(typeof layouWidth =='number'&&layouWidth!=-1){
let tf = layouWidth!;
flex = tf.toFloat();
console.log('null',layouWidth)
let layoutParams_crd = new LinearLayout.LayoutParams(
w,
h,
flex
)
this.contentParams = layoutParams_crd
}else{
console.log('null')
let layoutParams_crd = new LinearLayout.LayoutParams(
w,
h
)
this.contentParams = layoutParams_crd
}
// 未知原因设置这个会闪退。
// view.setLayoutParams(layoutParams_crd);
return this;
}
append(children : View,...args: View[]):xCardView{
let view = this.contentView as ViewGroup;
view.addView(children)
args.forEach((el)=>view.addView(el))
return this;
}
appendChild(children : View):xCardView {
let view = this.contentView as ViewGroup;
view.addView(children)
return this;
}
}
export default xCardView;
+68
View File
@@ -0,0 +1,68 @@
import Context from 'android.content.Context'
import LinearLayout from 'android.widget.LinearLayout';
import ViewGroup from 'android.view.ViewGroup';
import View from 'android.view.View';
import TextView from 'android.widget.TextView';
import ImageView from 'android.widget.ImageView';
import Gravity from 'android.view.Gravity';
import Color from 'android.graphics.Color';
import Activity from 'android.app.Activity';
import { ref} from "vue"
import TextUtils from 'android.text.TextUtils';
import GradientDrawable from 'android.graphics.drawable.GradientDrawable'
import MotionEvent from 'android.view.MotionEvent';
import Typeface from 'android.graphics.Typeface';
import {hexToRgb,getDefaultColor,toFillMarginAr} from "../util/xCoreColorUtil.uts"
import {getUid,dp2px} from "../util/xCoreUtil.uts"
class xIcon {
view:any
constructor(context : Context,name:string=""){
let view = new TextView(context);
let layoutParams_crd = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
view.setGravity(Gravity.CENTER)
view.setLayoutParams(layoutParams_crd);
this.view = view;
this._setCodeStr(name)
}
getView():View{
return this.view as View;
}
setIcon(name:string=""):xIcon{
this._setCodeStr(name)
return this;
}
setFontSize(n:number):xIcon{
let view = this.view as TextView;
view.setTextSize(dp2px(n).toFloat())
return this;
}
setFontColor(str:string):xIcon{
let view = this.view as TextView;
view.setTextColor(Color.parseColor(getDefaultColor(str)))
return this;
}
private _setCodeStr(code:string){
let view = this.view as TextView;
let assetManager = view.getContext()!.getAssets();
let typeface = Typeface.createFromAsset(assetManager, "remixicon.ttf")
view.setTypeface(typeface)
if(!TextUtils.isEmpty(code)){
let codePoint = Integer.parseInt(code, 16);
let charArray = Character.toChars(codePoint);
let text = new String(charArray);
view.setText(text);
}
}
}
export default xIcon;
@@ -0,0 +1,130 @@
import Context from 'android.content.Context'
import LinearLayout from 'android.widget.LinearLayout';
import RelativeLayout from 'android.widget.RelativeLayout';
import ViewGroup from 'android.view.ViewGroup';
import View from 'android.view.View';
import TextView from 'android.widget.TextView';
import ImageView from 'android.widget.ImageView';
import Gravity from 'android.view.Gravity';
import Color from 'android.graphics.Color';
import Activity from 'android.app.Activity';
// import CardView from 'androidx.cardview.widget.CardView';
import { ref, } from "vue"
import TextUtils from 'android.text.TextUtils';
import GradientDrawable from 'android.graphics.drawable.GradientDrawable'
import MotionEvent from 'android.view.MotionEvent';
import {hexToRgb,getDefaultColor} from "../util/xCoreColorUtil.uts"
import {getUid, dp2px} from "../util/xCoreUtil.uts"
import xView from "./view.uts";
class xLinearView extends xView {
override view:any;
contentParams:LinearLayout.LayoutParams
constructor(context : Context){
super(context);
let box = new LinearLayout(context);
this.contentParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
box.setLayoutParams(this.contentParams)
this.view = box;
}
override getView():LinearLayout{
return this.view as LinearLayout;
}
override setBackgroundColor(colorStr : string) : xLinearView {
console.error("不支持,请通过cardView设置")
return this;
}
override setRadius(radius:any) : xLinearView{
console.error("不支持,请通过cardView设置")
return this;
}
override setBorder(w?:number,colorStr?:string,dashed?:boolean):xLinearView{
console.error("不支持,请通过cardView设置")
return this;
}
override setHeight(height : number) : xLinearView {
console.error("不支持,请通过linerView设置,并将本CardView加入其中来设置高度。")
return this;
}
//让内容的对齐方向,
//https://developer.android.google.cn/reference/kotlin/android/view/Gravity
//只有tmxTextView,有这个属性。
setAlign(cr:Int) : xLinearView{
let view = this.view as LinearLayout;
view.setGravity(cr)
return this;
}
setLayoutParams(width?:number,height?:number,layouWidth?:number) : xLinearView{
let view = this.view as LinearLayout;
let w = (0).toInt();
let h = ViewGroup.LayoutParams.WRAP_CONTENT;
let flex = (1).toFloat();
if(typeof width =='number'){
w = width!.toInt();
}
if(typeof height =='number'){
h = height!.toInt();
}
if(typeof layouWidth =='number'&&layouWidth!=-1){
let tf = layouWidth!;
flex = tf.toFloat();
console.log('null',layouWidth)
let layoutParams_crd = new LinearLayout.LayoutParams(
w,
h,
flex
)
this.contentParams = layoutParams_crd
}else{
console.log('null')
let layoutParams_crd = new LinearLayout.LayoutParams(
w,
h
)
this.contentParams = layoutParams_crd
}
return this;
}
//内容的排版方向:verticalhorizontal
setLayoutDirection(dir:string) : xLinearView{
let view = this.view as LinearLayout;
if(dir=='VERTICAL'){
view.setOrientation(LinearLayout.VERTICAL)
}else if(dir=='HORIZONTAL'){
view.setOrientation(LinearLayout.HORIZONTAL)
}
return this
}
append(children : View,...args: View[]) : xLinearView {
let view = this.view as ViewGroup;
view.addView(children)
args.forEach((el)=>view.addView(el))
return this;
}
appendChild(children : View) : xLinearView {
let view = this.view as ViewGroup;
view.addView(children)
return this;
}
}
export default xLinearView;
+301
View File
@@ -0,0 +1,301 @@
import Context from 'android.content.Context'
import LinearLayout from 'android.widget.LinearLayout';
import ViewGroup from 'android.view.ViewGroup';
import View from 'android.view.View';
import TextView from 'android.widget.TextView';
import ImageView from 'android.widget.ImageView';
import Gravity from 'android.view.Gravity';
import Color from 'android.graphics.Color';
import Activity from 'android.app.Activity';
// import CardView from 'androidx.cardview.widget.CardView';
import { ref, } from "vue"
import TextUtils from 'android.text.TextUtils';
import GradientDrawable from 'android.graphics.drawable.GradientDrawable'
import MotionEvent from 'android.view.MotionEvent';
import Typeface from 'android.graphics.Typeface';
import {hexToRgb,getDefaultColor,toFillMarginAr} from "../util/xCoreColorUtil.uts"
import {getUid,dp2px} from "../util/xCoreUtil.uts"
import xView from "./view.uts";
class xText extends xView {
override view:any
constructor(context : Context,str:string=""){
super(context);
let view = new TextView(context);
let layoutParams_crd = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
(1).toFloat()
)
view.setText(str);
view.setLayoutParams(layoutParams_crd);
this.bgView = new GradientDrawable();
this.bgView.setShape(0x00000000)
this.bgView.setColor(Color.WHITE)
this.bgView.mutate()
view.setBackground(this.bgView)
this.view = view;
}
override getView():View{
return this.view as View;
}
override setBackgroundColor(colorStr : string) : xText {
this.bgView.mutate()
if(colorStr == 'transparent'){
this.bgView.setColor(Color.TRANSPARENT)
}else{
this.bgView.setColor(Color.parseColor(getDefaultColor(colorStr)))
}
return this;
}
override setBorder(w?:number,colorStr?:string,dashed?:boolean) : xText {
this.bgView.mutate()
let tw:number = 1;
let tcolor = 'transparent';
let tdashed = false;
if(typeof tw !== 'undefined'){
tw = w as number;
}
if(typeof tw !== 'undefined'){
tcolor = colorStr as string;
}
if(typeof dashed !== 'undefined'){
tdashed = dashed as boolean;
}
let colorNum = Color.TRANSPARENT;
if(tcolor != 'transparent'){
colorNum = Color.parseColor(getDefaultColor(tcolor));
}
if(dashed == true){
let dashedWidth = dp2px(6)
let dashedGap = dp2px(4)
this.bgView.setStroke(dp2px(tw).toInt(),colorNum,dashedWidth.toFloat(),dashedGap.toFloat())
}else{
this.bgView.setStroke(dp2px(tw).toInt(),colorNum)
}
return this;
}
override setRadius(radius:any) : xText{
let x =0
let y =0
this.bgView.mutate()
if(Array.isArray(radius)){
let rd = radius as number[];
if(rd.length==0){
let rdfs = 0;
this.bgView.setCornerRadius(dp2px(rdfs).toFloat());
// view.setBackground(this.bgView)
}else if(rd.length==1){
let rdfs = rd[0];
this.bgView.setCornerRadius(dp2px(rdfs).toFloat());
// view.setBackground(this.bgView)
}else if(rd.length==2){
let tl = [
dp2px(rd[0]).toFloat(),dp2px(rd[0]).toFloat(),
dp2px(rd[1]).toFloat(),dp2px(rd[1]).toFloat(),
x.toFloat(),x.toFloat(),
x.toFloat(),x.toFloat()
]
let tlint = tl.toKotlinList().toFloatArray()
this.bgView.setCornerRadii(tlint);
}else if(rd.length==3){
let tl = [
dp2px(rd[0]).toFloat(),dp2px(rd[0]).toFloat(),
dp2px(rd[1]).toFloat(),dp2px(rd[1]).toFloat(),
dp2px(rd[2]).toFloat(),dp2px(rd[2]).toFloat(),
x.toFloat(),x.toFloat()
]
let tlint = tl.toKotlinList().toFloatArray()
this.bgView.setCornerRadii(tlint);
}else if(rd.length==4){
let tl = [
dp2px(rd[0]).toFloat(),dp2px(rd[0]).toFloat(),
dp2px(rd[1]).toFloat(),dp2px(rd[1]).toFloat(),
dp2px(rd[2]).toFloat(),dp2px(rd[2]).toFloat(),
dp2px(rd[3]).toFloat(),dp2px(rd[3]).toFloat(),
]
let tlint = tl.toKotlinList().toFloatArray()
this.bgView.setCornerRadii(tlint);
}
}else{
let rd = radius as number;
this.bgView.setCornerRadius(dp2px(rd).toFloat());
}
return this;
}
setFontSize(n:number):xText{
let view = this.view as TextView;
view.setTextSize(dp2px(n).toFloat())
return this;
}
setFontColor(str:string):xText{
let view = this.view as TextView;
view.setTextColor(Color.parseColor(getDefaultColor(str)))
return this;
}
//是否允许复制。
setTextIsSelectable(isSelectet:boolean):xText{
let view = this.view as TextView;
view.setTextIsSelectable(isSelectet)
return this;
}
//bold,normal,ligth
setFontWeight(str:string):xText{
let view = this.view as TextView;
if(str=='bold'){
view.setTypeface(null,Typeface.BOLD)
}else{
view.setTypeface(null,Typeface.NORMAL)
}
return this;
}
setFontStyle(str:string):xText{
let view = this.view as TextView;
if(str=='italtc'){
view.setTypeface(null,Typeface.ITALIC)
}else if(str=='italtc-bold'){
view.setTypeface(null,Typeface.BOLD_ITALIC)
}
return this;
}
setText(str:string):xText{
let view = this.view as TextView;
view.setText(str)
return this;
}
override setPadding(n:any):xText{
let view = this.view as TextView;
if(Array.isArray(n)){
let rd = n as number[];
if(rd.length==0){
let rds = 0;
let rdf = dp2px(rds).toInt();
view.setPadding(rdf,rdf,rdf,rdf)
}else if(rd.length==1){
let rdf = dp2px(rd[0]).toInt();
view.setPadding(rdf,rdf,rdf,rdf)
}else if(rd.length==2){
view.setPadding(dp2px(rd[0]).toInt(),dp2px(rd[1]).toInt(),dp2px(rd[0]).toInt(),dp2px(rd[1]).toInt())
}else if(rd.length==3){
let rds = 0;
let rdf = rds.toInt();
view.setPadding(dp2px(rd[0]).toInt(),dp2px(rd[1]).toInt(),dp2px(rd[2]).toInt(),rdf)
}else if(rd.length==4){
view.setPadding(dp2px(rd[0]).toInt(),dp2px(rd[1]).toInt(),dp2px(rd[2]).toInt(),dp2px(rd[3]).toInt())
}
}else{
let rd = n as number;
let rdf = dp2px(rd).toInt();
view.setPadding(rdf,rdf,rdf,rdf)
}
return this;
}
//让内容的对齐方向,
//https://developer.android.google.cn/reference/kotlin/android/view/Gravity
//只有xTextView,有这个属性。
setAlign(cr:Int) : xText{
let view = this.view as TextView;
view.setGravity(cr)
return this;
}
//追加文本
//start,end不填写就追加到末尾。
setIcon(code:string,start:number=0,end:number=0) : xText{
let view = this.view as TextView;
let nowText = view.getText().toString();
let assetManager = view.getContext()!.getAssets();
let typeface = Typeface.createFromAsset(assetManager, "remixicon.ttf")
view.setTypeface(typeface)
if(!TextUtils.isEmpty(code)){
let codePoint = Integer.parseInt(code, 16);
let charArray = Character.toChars(codePoint);
let text = new String(charArray);
view.setText(text+" "+nowText);
}
return this;
}
setLayoutParams(width?:number,height?:number,layouWidth?:number) : xText{
let view = this.view as TextView;
let w = (0).toInt();
let h = ViewGroup.LayoutParams.WRAP_CONTENT;
let flex = (1).toFloat();
if(typeof width =='number'){
w = width!.toInt();
}
if(typeof height =='number'){
h = height!.toInt();
}
if(typeof layouWidth =='number'){
flex = layouWidth!.toFloat();
}
let layoutParams_crd = new LinearLayout.LayoutParams(
w,
h,
flex
)
view.setLayoutParams(layoutParams_crd);
return this;
}
setLineHeight(line:number) : xText{
let view = this.view as TextView;
view.setLineHeight(line.toInt())
return this;
}
setLetterSpacing(space:number) : xText{
let view = this.view as TextView;
view.setLineHeight(space.toInt())
return this;
}
setHighlightColor(color:string) : xText{
let view = this.view as TextView;
if(!TextUtils.isEmpty(color)){
view.setHighlightColor(Color.parseColor(getDefaultColor(color)))
}
return this;
}
// 设置文字的省略情况,0表示不显示省略号,数字表示几行显示省略号。
setEllipsis(lines:number=0) : xText{
let view = this.view as TextView;
if(lines>0){
view.setMaxLines(lines.toInt())
view.setEllipsize(TextUtils.TruncateAt.END)
}else{
view.setMaxLines((1).toInt())
view.setEllipsize(null)
}
return this;
}
}
export default xText;
+486
View File
@@ -0,0 +1,486 @@
import Context from 'android.content.Context'
import LinearLayout from 'android.widget.LinearLayout';
import ViewGroup from 'android.view.ViewGroup';
import View from 'android.view.View';
import TextView from 'android.widget.TextView';
import ImageView from 'android.widget.ImageView';
import Gravity from 'android.view.Gravity';
import Color from 'android.graphics.Color';
import Activity from 'android.app.Activity';
// import CardView from 'androidx.cardview.widget.CardView';
import { ref, } from "vue"
import TextUtils from 'android.text.TextUtils';
import GradientDrawable from 'android.graphics.drawable.GradientDrawable'
import MotionEvent from 'android.view.MotionEvent';
import ColorStateList from 'android.content.res.ColorStateList';
import attr from 'android.R.attr'
import {hexToRgb,getDefaultColor} from "../util/xCoreColorUtil.uts"
/**
* 数字转16进制
*/
function toHex(numbers : number) : string {
let i = 0 as number
if (numbers === i) {
return '00';
}
let hex = '';
const hexChars = '0123456789abcdef';
while (numbers > 0) {
const remainder = numbers % 16;
hex = hexChars[remainder] + hex;
numbers = Math.floor(numbers / 16);
}
return hex;
}
/**
* 随机一个uid
* @param rdix 随机因子
* @param length 取的长度
* @param isAddStr 是否限制随机结果中的长度,不允许输出长度
* @returns String
*/
function getUid(rdix = 1, length = 12) : string {
let ix = "";
// #ifndef APP
ix = Math.floor(Math.random() * rdix * Math.floor(Math.random() * Date.now())).toString().substring(0, length);
// #endif
// #ifdef APP
ix = Math.floor(Math.random() * rdix * Math.floor(Math.random() * Date.now())).toString().substring(0, length as Int);
// #endif
return ix;
}
class MyClickListener implements View.OnClickListener {
callbackFun:()=>void;
constructor(cal?:()=>void){
this.callbackFun = cal as ()=>void;
}
override onClick(v?:View):void {
if(typeof this.callbackFun !='underfinde'){
this.callbackFun();
}
}
}
class MyTouchListener implements View.OnTouchListener {
//down,up,move,cancel,longClick
callbackFun:(types:string,event : MotionEvent,detail:UTSJSONObject)=>void;
dubleTime = 0;
tid = 56;
_x = 0;
_y = 0;
// 判断方向的差值
swipe_mindiff = 40
// 滑动时,一定时间内只能触发一次事件。不能连续触发。
tid_siper = 100;
swipeDirection = ""
constructor(call:(types:string,event : MotionEvent,detail?:UTSJSONObject)=>void) {
this.callbackFun = call;
}
override onTouch(view : View, event : MotionEvent) : Boolean {
if(event.action == MotionEvent.ACTION_DOWN ){
this._x = event.getX();
this._y = event.getY();
this.swipeDirection = ""
this.callbackFun('down',event,{});
let difftime = new Date().getTime() - this.dubleTime;
if(difftime >0 && difftime<=300){
this.callbackFun('doubleClick',event ,{});
}
this.dubleTime = new Date().getTime();
clearTimeout(this.tid)
tid = setTimeout(function() {
this.callbackFun('longPress',event,{});
}, 800);
}
if(event.action == MotionEvent.ACTION_UP ){
clearTimeout(this.tid)
this.callbackFun('up',event,{});
if(new Date().getTime() - this.dubleTime > 50){
this.callbackFun('click',event,{});
}
}
if(event.action == MotionEvent.ACTION_CANCEL || event.action== MotionEvent.ACTION_OUTSIDE ){
this.callbackFun('cancel',event,{});
clearTimeout(this.tid)
}
if(event.action == MotionEvent.ACTION_MOVE ){
let x = event.getX();
let y = event.getY();
let deltaX = Math.abs(x - this._x);
let deltaY = Math.abs(y - this._y);
if(deltaX > deltaY && deltaX > this.swipe_mindiff){
this.swipeDirection = (this._x > x) ? "left" : "right";
}else if(deltaY > deltaX && deltaY > this.swipe_mindiff){
this.swipeDirection = (this._y < y) ? "down" : "up";
}
if(this.swipeDirection !=""){
this.callbackFun('swiper',event,{x,y,diffX:deltaX,diffY:deltaY,direction:this.swipeDirection});
}
// this._x = x;
// this._y = y;
this.callbackFun('move',event,{});
clearTimeout(this.tid)
}
return true;
}
}
class xView {
public view : any;
public tagId:string = 'x_view_id_'+getUid();
public bgView:GradientDrawable
private _onclickFun= (_event : MotionEvent):void=>{};
private _onTouchStart= (_event : MotionEvent):void=>{};
private _onTouchEnd= (_event : MotionEvent):void=>{};
private _onTouchCancel= (_event : MotionEvent):void=>{};
private _onTouchMove= (_event : MotionEvent):void=>{};
private _onTouchLongPress= (_event : MotionEvent):void=>{};
private _onTouchDubleClick= (_event : MotionEvent):void=>{};
private _onTouchSwiper= (_event : MotionEvent,detail?:UTSJSONObject):void=>{};
// 设置禁用,不会触发上面的事件。
private _disabled = false;
constructor(context : Context) {
let view = new View(context);
view.setTag(this.tagId)
let layoutParams_crd = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
view.setLayoutParams(layoutParams_crd);
this.bgView = new GradientDrawable();
// let statuesInts= [[attr.state_enabled].toKotlinList().toIntArray(),[attr.state_pressed].toKotlinList().toIntArray(),[-attr.state_enabled].toKotlinList().toIntArray()]
// let statuesColors= [Color.WHITE,Color.RED,Color.RED]
// let states = new ColorStateList(statuesInts.toKotlinList().toTypedArray(),statuesColors.toKotlinList().toIntArray())
this.bgView.setShape(GradientDrawable.RECTANGLE)
this.bgView.setColor(Color.WHITE)
this.bgView.mutate()
view.setBackground(this.bgView)
this.view = view;
// view.setOnClickListener(new MyClickListener(():void=>{
// this._onclickFun()
// }));
view.setOnTouchListener(new MyTouchListener((types:string,event : MotionEvent,detail?:UTSJSONObject):void=>{
if(this._disabled == false){
if(types=='down'){
this._onTouchStart(event)
}
if(types=='up'){
this._onTouchEnd(event);
}
if(types=='click'){
this._onclickFun(event);
}
if(types=='move'){
this._onTouchMove(event)
}
if(types=='cancel'){
this._onTouchCancel(event)
}
if(types=='longPress'){
this._onTouchLongPress(event)
}
if(types=='doubleClick'){
this._onTouchDubleClick(event)
}
if(types=='swiper'){
this._onTouchSwiper(event,detail)
}
}
}));
}
getView():View{
return this.view as View;
}
setDisabled(dis:boolean) : xView{
this._disabled = dis;
return this;
}
/**
* 触发距离是大于50时触发并判断方向
* x,y当前坐标,diffX滑动时距离开始时按下的横向x距离,diffY表示竖向。Direction为方向:left,right,up,dowon
*detail:
*{x,y,diffX,diffY,direction}
*/
setSwiper(fun?:(event: MotionEvent,detail?:UTSJSONObject)=>void) : xView{
if(typeof fun !== 'undefined'){
this._onTouchSwiper = fun;
}
return this;
}
setDoubleClick(fun?:(event: MotionEvent)=>void) : xView{
if(typeof fun !== 'undefined'){
this._onTouchDubleClick = fun;
}
return this;
}
setClick(fun?:(event: MotionEvent)=>void) : xView{
if(typeof fun !== 'undefined'){
this._onclickFun = fun;
}
return this;
}
setTouchStart(fun?:(event: MotionEvent)=>void) : xView{
if(typeof fun !== 'undefined'){
this._onTouchStart = fun;
}
return this;
}
setTouchMove(fun?:(event: MotionEvent)=>void) : xView{
if(typeof fun !== 'undefined'){
this._onTouchMove = fun;
}
return this;
}
setTouchEnd(fun?:(event: MotionEvent)=>void) : xView{
if(typeof fun !== 'undefined'){
this._onTouchEnd = fun;
}
return this;
}
setTouchCancel(fun?:(event: MotionEvent)=>void) : xView{
if(typeof fun !== 'undefined'){
this._onTouchCancel = fun;
}
return this;
}
setTouchLongPress(fun?:(event: MotionEvent)=>void) : xView{
if(typeof fun !== 'undefined'){
this._onTouchLongPress = fun;
}
return this;
}
setBackgroundColor(colorStr : string) : xView {
this.bgView.mutate()
if(colorStr == 'transparent'){
this.bgView.setColor(Color.TRANSPARENT)
}else{
this.bgView.setColor(Color.parseColor(getDefaultColor(colorStr)))
}
return this;
}
setBorder(w?:number,colorStr?:string,dashed?:boolean) : xView {
this.bgView.mutate()
let tw:number = 1;
let tcolor = 'transparent';
let tdashed = false;
if(typeof tw !== 'undefined'){
tw = w as number;
}
if(typeof tw !== 'undefined'){
tcolor = colorStr as string;
}
if(typeof dashed !== 'undefined'){
tdashed = dashed as boolean;
}
let colorNum = Color.TRANSPARENT;
if(tcolor != 'transparent'){
colorNum = Color.parseColor(getDefaultColor(tcolor));
}
if(dashed == true){
let dashedWidth = 8
let dashedGap = 5
this.bgView.setStroke(tw.toInt(),colorNum,dashedWidth.toFloat(),dashedGap.toFloat())
}else{
this.bgView.setStroke(tw.toInt(),colorNum)
}
return this;
}
setMargin(n:any):xView{
let view = this.view as View;
let selfPrams = view.getLayoutParams()
let params = new LinearLayout.LayoutParams(
selfPrams.width,
selfPrams.height
)
if(Array.isArray(n)){
let rd = n as number[];
if(rd.length==0){
let rds = 0;
let rdf = (rds).toInt();
params.setMargins(rdf,rdf,rdf,rdf)
}else if(rd.length==1){
let rdf = (rd[0]).toInt();
params.setMargins(rdf,rdf,rdf,rdf)
}else if(rd.length==2){
let rds = 0;
params.setMargins((rd[0]).toInt(),(rd[1]).toInt(),(rd[0]).toInt(),(rd[1]).toInt())
}else if(rd.length==3){
let rds = 0;
let rdf = rds.toInt();
params.setMargins((rd[0]).toInt(),(rd[1]).toInt(),(rd[2]).toInt(),rdf)
}else if(rd.length==4){
params.setMargins((rd[0]).toInt(),(rd[1]).toInt(),(rd[2]).toInt(),(rd[3]).toInt())
}
}else{
let rd = n as number;
let rdf = (rd).toInt();
params.setMargins(rdf,rdf,rdf,rdf)
}
view.setLayoutParams(params);
return this;
}
setPadding(n:any):xView{
let view = this.view as View;
if(Array.isArray(n)){
let rd = n as number[];
if(rd.length==0){
let rds = 0;
let rdf = (rds).toInt();
view.setPadding(rdf,rdf,rdf,rdf)
}else if(rd.length==1){
let rdf = (rd[0]).toInt();
view.setPadding(rdf,rdf,rdf,rdf)
}else if(rd.length==2){
view.setPadding((rd[0]).toInt(),(rd[1]).toInt(),(rd[0]).toInt(),(rd[1]).toInt())
}else if(rd.length==3){
let rds = 0;
let rdf = rds.toInt();
view.setPadding((rd[0]).toInt(),(rd[1]).toInt(),(rd[2]).toInt(),rdf)
}else if(rd.length==4){
view.setPadding((rd[0]).toInt(),(rd[1]).toInt(),(rd[2]).toInt(),(rd[3]).toInt())
}
}else{
let rd = n as number;
let rdf = (rd).toInt();
view.setPadding(rdf,rdf,rdf,rdf)
}
return this;
}
setRadius(radius:any) : xView{
let x =0
let y =0
this.bgView.mutate()
if(Array.isArray(radius)){
let rd = radius as number[];
if(rd.length==0){
let rdfs = 0;
this.bgView.setCornerRadius((rdfs).toFloat());
// view.setBackground(this.bgView)
}else if(rd.length==1){
let rdfs = rd[0];
this.bgView.setCornerRadius((rdfs).toFloat());
// view.setBackground(this.bgView)
}else if(rd.length==2){
let tl = [
(rd[0]).toFloat(),(rd[0]).toFloat(),
(rd[1]).toFloat(),(rd[1]).toFloat(),
x.toFloat(),x.toFloat(),
x.toFloat(),x.toFloat()
]
let tlint = tl.toKotlinList().toFloatArray()
this.bgView.setCornerRadii(tlint);
}else if(rd.length==3){
let tl = [
(rd[0]).toFloat(),(rd[0]).toFloat(),
(rd[1]).toFloat(),(rd[1]).toFloat(),
(rd[2]).toFloat(),(rd[2]).toFloat(),
x.toFloat(),x.toFloat()
]
let tlint = tl.toKotlinList().toFloatArray()
this.bgView.setCornerRadii(tlint);
}else if(rd.length==4){
let tl = [
(rd[0]).toFloat(),(rd[0]).toFloat(),
(rd[1]).toFloat(),(rd[1]).toFloat(),
(rd[2]).toFloat(),(rd[2]).toFloat(),
(rd[3]).toFloat(),(rd[3]).toFloat(),
]
let tlint = tl.toKotlinList().toFloatArray()
this.bgView.setCornerRadii(tlint);
}
}else{
let rd = radius as number;
this.bgView.setCornerRadius((rd).toFloat());
}
return this;
}
/** 设置当前宽和高为父级的相对宽和高
*w,h有效值为:auto|100%
*/
setSizeBy(w:string="100%",h:string = "auto"){
let view = this.view as View;
let ww = ViewGroup.LayoutParams.MATCH_PARENT;
let hh = ViewGroup.LayoutParams.MATCH_PARENT;
if(w=="auto"){
ww = ViewGroup.LayoutParams.WRAP_CONTENT
}
if(h=="auto"){
hh = ViewGroup.LayoutParams.WRAP_CONTENT
}
let layoutParams_crd = new LinearLayout.LayoutParams(ww,hh)
view.setLayoutParams(layoutParams_crd);
}
setSize(width : number, height : number) : xView {
let view = this.view as View;
let layoutParams_crd = new LinearLayout.LayoutParams(
(width).toInt(),
(height).toInt()
)
view.setLayoutParams(layoutParams_crd);
return this;
}
setWidth(width : number) : xView {
let view = this.view as View;
let lpar = view.getLayoutParams();
let layoutParams_crd = new LinearLayout.LayoutParams(
(width).toInt(),
(lpar.height).toInt(),
)
view.setLayoutParams(layoutParams_crd);
return this;
}
setHeight(height : number) : xView {
let view = this.view as View;
let lpar = view.getLayoutParams();
let layoutParams_crd = new LinearLayout.LayoutParams(
(lpar.width).toInt(),
(height).toInt(),
)
view.setLayoutParams(layoutParams_crd);
return this;
}
}
export default xView;
+17
View File
@@ -0,0 +1,17 @@
import Dialog from "android.app.Dialog"
import Context from 'android.content.Context'
import { UTSAndroid } from "io.dcloud.uts";
import FrameLayout from "android.widget.FrameLayout";
class xOverflay {
static show(){
let d = new Dialog(UTSAndroid.getUniActivity() as Context);
let decorView = UTSAndroid.getUniActivity()!.window.decorView;
let frameContent = decorView.findViewById<FrameLayout>(android.R.id.content) as FrameLayout
// test
d.show();
}
}
export default xOverflay;
+839
View File
@@ -0,0 +1,839 @@
import { ShapeType, ICanvasEvent, ICanvasDomEvent, IEventListener, CanvasEventType, ICanvasOptional, IcanvasDomEventDetail } from './interface.uts';
// import { Shape } from './lib/shape.uts';
import { Shape } from '@/uni_modules/tmx-ui/core/canvas/lib/shape.uts';
export class ICanvas {
canvas : CanvasContext | null = null;
ctx : CanvasRenderingContext2D | null = null;
canvasNode : NodeInfo | null = null
width : number = 0;
height : number = 0;
boxWidth : number = 0;
boxHeight : number = 0;
dpr : number = 1;
/**
* 命中检测事件范围,默认只对clik点击进行命中测试,如果全放开会有性能风险
* 相当于是事件过滤。比如['click']那么元素只会触发click事件,其它事件不会响应
* 空数组响应所有事件
*/
checkPointInHitEvents = ['click'] as CanvasEventType[]
//点击时的位置
private touch_x = 0
private touch_y = 0
//移动时的位置
private touch_move_x = 0
private touch_move_y = 0
//结束时的位置
private touch_end_x = 0
private touch_end_y = 0
//结束时相对起始时之间的位置距离
private touch_end_len_x = 0
private touch_end_len_y = 0
//移动时相对上一次移动的距离
private touch_move_len_x = 0
private touch_move_len_y = 0
//点击时的时间
private touchStartTime = 0
//点击抬起时的结束时间.
private touchEndTime = 0
private touchTimeTid = 0
id = Math.random().toString(16).substring(4)
gaptimeId = 23
private shapes : ShapeType[] = [];
private eventListeners : Map<CanvasEventType, IEventListener[]> = new Map();
component : any | null = null;
canvasId : string;
constructor(config : ICanvasOptional) {
this.canvasId = config.canvasId;
this.component = config.component
}
init() : Promise<any | null> {
const _this = this;
return new Promise((res) => {
uni.createSelectorQuery()
.in(_this.component)
.select("#" + _this.canvasId)
.boundingClientRect()
.exec(result => {
if (result.length == 0) return;
let node = result[0]! as NodeInfo
uni.createCanvasContextAsync({
id: _this.canvasId,
component: _this.component as ComponentPublicInstance | null,
success: (context : CanvasContext) => {
_this.canvasNode = node
_this.canvas = context;
const dpr = uni.getWindowInfo().pixelRatio ?? 1;
const canvasContext = context.getContext('2d')!;
// #ifdef APP
canvasContext.resetTransform()
// #endif
const canvas = canvasContext.canvas;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
canvasContext.scale(dpr, dpr);
_this.ctx = canvasContext as CanvasRenderingContext2D;
_this.dpr = dpr;
_this.ctx!.globalAlpha = 1;
_this.width = node.width!;
_this.height = node.height!;
_this.boxWidth = canvas.offsetWidth;
_this.boxHeight = canvas.offsetHeight;
res(_this)
}
})
})
})
}
private _resizePos() {
const _this = this;
uni.createSelectorQuery()
.in(_this.component)
.select("#" + _this.canvasId)
.boundingClientRect()
.exec(result => {
if (result.length == 0) return;
let node = result[0]! as NodeInfo
_this.canvasNode = node
})
}
clear() : ICanvas {
this.ctx!.clearRect(0, 0, this.width, this.height)
return this;
}
addShape(shape : ShapeType|ShapeType[]) : ICanvas {
if(Array.isArray(shape)){
let _this = this;
shape.forEach((el)=>{
_this.shapes.push(el);
})
}else{
this.shapes.push(shape);
}
this.shapes.sort((a, b ) : number => {
let ela = a as Shape
let elb = b as Shape
return ela.zIndex - elb.zIndex
})
return this;
}
removeShape(shape : ShapeType) : ICanvas {
for (let i = 0; i < this.shapes.length; i++) {
let itemshape = this.shapes[i]
if (itemshape instanceof Shape && shape instanceof Shape) {
if (itemshape.id == shape.id) {
this.shapes.splice(i, 1);
break;
}
}
}
return this;
}
render() : ICanvas {
this.clear();
for (let i = 0; i < this.shapes.length; i++) {
const shape = this.shapes[i];
if (shape instanceof Shape) {
shape.draw(this.ctx!);
}
}
return this;
}
update() : ICanvas {
let needsRender = false;
for (let i = 0; i < this.shapes.length; i++) {
const shape = this.shapes[i];
if (shape instanceof Shape) {
if (shape.needsUpdate) {
needsRender = true;
shape.update()
}
}
}
if (needsRender) {
this.render();
}
return this;
}
getImage():Promise<string>{
return new Promise((res,_rej)=>{
// #ifdef WEB||H5
try {
this.ctx.canvas.toBlob((blob)=>{
res(URL.createObjectURL(blob))
},'images/png',1)
} catch (error) {
console.error(error,'注意画布中的图片要同域')
res('')
}
// #endif
// #ifdef APP
let ele = uni.getElementById(this.canvasId) as UniElement
ele!.takeSnapshot({
success(result){
res(result.tempFilePath)
},
fail(){
res('')
}
})
// #endif
// #ifdef MP
uni.canvasToTempFilePath({
canvasId:this.canvasId,
success(result){
res(result.tempFilePath)
},
fail(){
res('')
}
},this.component)
// #endif
})
}
/** 将外部事件绑定到对象上,实现事件分发. **/
bindEvent(eventType : any | null) {
let _this = this;
clearTimeout(this.touchTimeTid)
if (eventType == null || this.canvasNode == null) return;
const winTop = uni.getWindowInfo().windowTop
const offsetTop = this.canvasNode!.top! + 0
const offsetLeft = this.canvasNode!.left!
//防止触摸和鼠标事件同时触发。
let isTouch = false;
// 移动端
// #ifdef APP-ANDROID||H5
if (eventType instanceof TouchEvent) {
// #endif
// #ifdef APP-IOS||APP-HARMONY
if (eventType instanceof UniTouchEvent) {
// #endif
// #ifdef MP
if (true) {
// #endif
eventType.stopPropagation()
eventType.preventDefault()
isTouch = true;
let changedTouches = eventType.changedTouches
// #ifndef APP
changedTouches = Array.from(changedTouches)
// #endif
const clientX = changedTouches[0].clientX - offsetLeft
const clientY = changedTouches[0].clientY - offsetTop
if (eventType.type == 'touchstart') {
this.touch_x = clientX
this.touch_y = clientY
this.touch_move_x = clientX
this.touch_move_y = clientY
this.touch_end_x = clientX
this.touch_end_y = clientY
this.touch_end_len_x = 0
this.touch_end_len_y = 0
this.touch_move_len_x = 0
this.touch_move_len_y = 0
this.touchStartTime = Date.now()
this.buildEvents('down', {
type: 'down',
x: changedTouches[0].clientX - offsetLeft,
y: changedTouches[0].clientY - offsetTop,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
touches: changedTouches.map((el) : ICanvasDomEvent => {
return {
type: 'down',
x: el.clientX - offsetLeft,
y: el.clientY - offsetTop,
touches: [],
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
} as ICanvasDomEvent
})
} as ICanvasDomEvent)
this.touchTimeTid = setTimeout(() => {
_this.buildEvents('longpress', {
type: 'longpress',
x: changedTouches[0].clientX - offsetLeft,
y: changedTouches[0].clientY - offsetTop,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
touches: changedTouches.map((el) : ICanvasDomEvent => {
return {
type: 'longpress',
x: el.clientX - offsetLeft,
y: el.clientY - offsetTop,
touches: [],
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
} as ICanvasDomEvent
})
} as ICanvasDomEvent)
}, 500)
} else if (eventType.type == 'touchmove') {
this.touch_move_len_x = this.touch_move_x - clientX
this.touch_move_len_y = this.touch_move_y - clientY
this.touch_end_len_x = clientX - this.touch_x
this.touch_end_len_y = clientY - this.touch_y
this.touch_move_x = clientX
this.touch_move_y = clientY
this.buildEvents('move', {
type: 'move',
x: changedTouches[0].clientX - offsetLeft,
y: changedTouches[0].clientY - offsetTop,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
touches: changedTouches.map((el) : ICanvasDomEvent => {
return {
type: 'move',
x: el.clientX - offsetLeft,
y: el.clientY - offsetTop,
touches: [],
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
} as ICanvasDomEvent
})
} as ICanvasDomEvent)
} else if (eventType.type == 'touchend') {
this.touch_end_x = clientX
this.touch_end_y = clientY
this.touch_end_len_x = this.touch_end_x - this.touch_x
this.touch_end_len_y = this.touch_end_y - this.touch_y
let nowTIme = Date.now()
this.buildEvents('up', {
type: 'up',
x: changedTouches[0].clientX - offsetLeft,
y: changedTouches[0].clientY - offsetTop,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
touches: changedTouches.map((el) : ICanvasDomEvent => {
return {
type: 'up',
x: el.clientX - offsetLeft,
y: el.clientY - offsetTop,
touches: [],
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
} as ICanvasDomEvent
})
} as ICanvasDomEvent)
// 判断是不是单击
const difftime = nowTIme - this.touchStartTime
if (difftime > 50 && difftime < 250) {
this.buildEvents('click', {
type: 'click',
x: changedTouches[0].clientX - offsetLeft,
y: changedTouches[0].clientY - offsetTop,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
touches: changedTouches.map((el) : ICanvasDomEvent => {
return {
type: 'click',
x: el.clientX - offsetLeft,
y: el.clientY - offsetTop,
touches: [],
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
} as ICanvasDomEvent
})
} as ICanvasDomEvent)
}
// 判断是不是双击
const diffEndtime = nowTIme - this.touchEndTime
if (diffEndtime > 100 && diffEndtime < 280) {
this.buildEvents('dbclick', {
type: 'dbclick',
x: changedTouches[0].clientX - offsetLeft,
y: changedTouches[0].clientY - offsetTop,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
touches: [] as ICanvasDomEvent[]
})
}
this.touchEndTime = nowTIme
} else if (eventType.type == 'touchcancel') {
this.touch_end_x = clientX
this.touch_end_y = clientY
this.touch_end_len_x = this.touch_end_x - this.touch_x
this.touch_end_len_y = this.touch_end_y - this.touch_y
const touchesList = changedTouches.map((el) : ICanvasDomEvent => {
return {
type: 'cancel',
x: el.clientX - offsetLeft,
y: el.clientY - offsetTop,
touches: [],
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
} as IcanvasDomEventDetail ] as IcanvasDomEventDetail[],
} as ICanvasDomEvent
})
this.buildEvents('cancel', {
type: 'cancel',
x: changedTouches[0].clientX - offsetLeft,
y: changedTouches[0].clientY - offsetTop,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
} as IcanvasDomEventDetail ] as IcanvasDomEventDetail[],
touches: touchesList as ICanvasDomEvent[]
} as ICanvasDomEvent)
}
}
// 兼容PC
// #ifdef WEB
if (eventType instanceof MouseEvent && !(eventType instanceof PointerEvent) && !isTouch) {
eventType.stopPropagation()
const clientX = eventType.layerX
const clientY = eventType.layerY
if (eventType.type == 'mousedown') {
this.touch_x = clientX
this.touch_y = clientY
this.touch_move_x = clientX
this.touch_move_y = clientY
this.touch_end_x = clientX
this.touch_end_y = clientY
this.touch_end_len_x = 0
this.touch_end_len_y = 0
this.touch_move_len_x = 0
this.touch_move_len_y = 0
this.touchStartTime = Date.now()
window.ICnavansId = this.id
this.buildEvents('down', {
type: 'down',
x: clientX,
y: clientY,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
touches: [] as ICanvasDomEvent[]
})
this.touchTimeTid = setTimeout(() => {
_this.buildEvents('longpress', {
type: 'longpress',
x: clientX,
y: clientY,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
touches: [] as ICanvasDomEvent[]
} as ICanvasDomEvent)
}, 500)
}
if (eventType.type == 'mousemove' && window.ICnavansId === this.id) {
this.touch_move_len_x = this.touch_move_x - clientX
this.touch_move_len_y = this.touch_move_y - clientY
this.touch_end_len_x = clientX - this.touch_x
this.touch_end_len_y = clientY - this.touch_y
this.touch_move_x = clientX
this.touch_move_y = clientY
this.buildEvents('move', {
type: 'move',
x: clientX,
y: clientY,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
touches: [] as ICanvasDomEvent[]
})
}
if ((eventType.type == 'mouseup') && window.ICnavansId === this.id) {
this.touch_end_x = clientX
this.touch_end_y = clientY
this.touch_end_len_x = this.touch_end_x - this.touch_x
this.touch_end_len_y = this.touch_end_y - this.touch_y
let nowTIme = Date.now()
window.ICnavansId = ""
this.buildEvents('up', {
type: 'up',
x: clientX,
y: clientY,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
touches: [] as ICanvasDomEvent[]
})
// 判断是不是单击
const difftime = nowTIme - this.touchStartTime
if (difftime > 50 && difftime < 250) {
this.buildEvents('click', {
type: 'click',
x: clientX,
y: clientY,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
touches: [] as ICanvasDomEvent[]
} as ICanvasDomEvent)
}
// 判断是不是双击
const diffEndtime = nowTIme - this.touchEndTime
if (diffEndtime > 100 && diffEndtime < 280) {
this.buildEvents('dbclick', {
type: 'dbclick',
x: clientX,
y: clientY,
detail: [],
touches: [] as ICanvasDomEvent[]
})
}
this.touchEndTime = nowTIme
}
if ((eventType.type == 'mouseleave') && window.ICnavansId === this.id) {
this.touch_end_x = clientX
this.touch_end_y = clientY
this.touch_end_len_x = this.touch_end_x - this.touch_x
this.touch_end_len_y = this.touch_end_y - this.touch_y
window.ICnavansId = ""
this.buildEvents('cancel', {
type: 'cancel',
x: clientX,
y: clientY,
detail: [{
startX: this.touch_x,
startY: this.touch_y,
endX: this.touch_end_x,
endY: this.touch_end_y,
moveX: this.touch_move_x,
moveY: this.touch_move_y,
moveLenX: this.touch_move_len_x,
moveLenY: this.touch_move_len_y,
endLenX: this.touch_end_len_x,
endLenY: this.touch_end_len_y
}],
touches: [] as ICanvasDomEvent[]
})
}
}
// #endif
}
addEventListener(eventName : CanvasEventType, listener : IEventListener) : ICanvas {
if (!this.eventListeners.has(eventName)) {
this.eventListeners.set(eventName, []);
}
this.eventListeners.get(eventName)!.push(listener);
return this;
}
removeEventListener(eventName : CanvasEventType, listener : IEventListener) : ICanvas {
const listeners = this.eventListeners.get(eventName);
if (listeners == null) return this;
const index = listeners.indexOf(listener);
if (index != -1) {
listeners.splice(index, 1);
}
return this;
}
private getTargetShape(eventName : CanvasEventType, x : number, y : number) : ShapeType[] {
let shapes = [] as ShapeType[]
// 只针对开命中事件进行检测
if (!this.checkPointInHitEvents.includes(eventName) && this.checkPointInHitEvents.length > 0) return shapes;
for (let i = this.shapes.length - 1; i >= 0; i--) {
const shape = this.shapes[i];
if (shape instanceof Shape) {
if (shape.isPointInPath(x, y, shape.id)) {
shapes.push(shape)
if (!shape.bubbleEvent) {
return shapes;
}
}
}
}
return shapes;
}
private buildEvents(eventName : CanvasEventType, eventDetail : ICanvasDomEvent) {
this._resizePos()
const listeners = this.eventListeners.get(eventName);
const target = this.getTargetShape(eventName, eventDetail.x, eventDetail.y);
const event : ICanvasEvent = {
type: eventName,
x: eventDetail.x,
y: eventDetail.y,
target: target,
detail: eventDetail.detail,
touches: eventDetail.touches.map((el : ICanvasDomEvent) : ICanvasEvent => {
return {
type: eventName,
x: el.x,
y: el.y,
target: target,
detail: el.detail,
touches: [] as ICanvasEvent[]
} as ICanvasEvent
})
};
if (listeners != null) {
for (let i = 0; i < listeners.length; i++) {
let children = listeners[i]
//执行画布的监听事件
children(event);
}
}
for (let i = 0; i < this.shapes.length; i++) {
const shape = this.shapes[i];
if (shape instanceof Shape) {
if (event.detail.length == 0) continue;
let startX = event.detail[0].startX
let startY = event.detail[0].startY
if (shape.isPointInPath(startX, startY, shape.id) && shape.draggable && eventName == 'down') {
shape.draggableing = true;
}
if (shape.draggable && (eventName == 'up' || eventName == 'cancel') && shape.draggableing) {
shape.draggableing = false;
}
if (shape.draggableing && eventName == 'move') {
shape.drag(eventName, event)
}
}
}
if (target.length > 0) {
let isUpdate = false;
for (let i = 0; i < target.length; i++) {
const shape = target[i];
if (shape instanceof Shape) {
shape.buildEvents(eventName, event)
if(shape.draggable&&!isUpdate){
isUpdate = true;
}
}
}
if(isUpdate){
this.render()
}
}
}
}
@@ -0,0 +1,255 @@
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
export type ShapeType = any;
/** 旋转元素时的中心点,默认是topLeft左顶,center表示元素的中间 */
export type ITextAlignType = 'left' | 'center' | 'right';
export type ITextBaselineType = 'top' | 'middle' | 'bottom';
export type ILineJoinType = "round" | "bevel" | "miter";
export type ILineCapType = "butt" | "round" | "square";
export type CanvasRotateCenter = "topLeft"|"center"|"topRight"|"bottomLeft"|"bottomRight"
export type DirectionEventType = 'down' | 'move' | 'up';
export type InteractionEventType = 'click' | 'longpress' | 'dbclick' | 'cancel';
export type CanvasEventType = DirectionEventType | InteractionEventType;
export type ShapeSetAttrType = string | number | boolean | Array<string>
export type IShapeVector2d = {
x:number,
y:number
}
export type ICanvasEvent = {
type: CanvasEventType;
/** 在画布上的坐标X */
x: number;
/** 在画布上的坐标Y */
y: number;
detail:IcanvasDomEventDetail[],
touches:ICanvasEvent[];
target: ShapeType[];
}
export type IcanvasDomEventDetail = {
startX:number,
startY:number,
endX:number,
endY:number,
moveX:number,
moveY:number,
/** 相对上一次移动的距离*/
moveLenX:number,
/** 相对上一次移动的距离*/
moveLenY:number,
/** 相对起始时到当前位置移动的距离*/
endLenX:number,
/** 相对起始时到当前位置移动的距离*/
endLenY:number
// /** 多点触摸时,两点之间的距离 */
// touchesLenX:number,
// /** 多点触摸时,两点之间的距离 */
// touchesLenY:number
}
export type ICanvasDomEvent = {
type: CanvasEventType;
x: number;
y: number;
detail:IcanvasDomEventDetail[],
touches:ICanvasDomEvent[];
}
export type IEventListener = (event: ICanvasEvent) => void;
export type IEventShape = {
type: CanvasEventType;
x: number;
y: number;
/** 元素本身内的坐标X */
layerX:number;
/** 元素本身内的坐标Y */
layerY:number;
touches:IEventShape[];
target:ShapeType
}
export type IEventShapeListener = (event: IEventShape) => void;
export type IEventMap = Map<string, IEventListener[]>;
export type ICanvasOptional = {
/** 选式时传递当前组件this,组合式传递 getCurrentInstance()?.proxy ?? null */
component:any|null,
/** 画布id */
canvasId:string,
}
export type IShapeOptional = {
x?: number;
y?: number;
width?: number;
height?: number;
fill?: string;
stroke?: string;
strokeWidth?: number;
opacity?: number;
visible?: boolean;
rotation?: number;
scaleX?: number;
scaleY?: number;
/** 元素相对舞台x坐标系的坐标,就是x,y为起始点,如果x,y为0就相当于是舞台的顶左 **/
offsetX?: number;
/** 元素相对舞台y坐标系的坐标,就是x,y为起始点,如果x,y为0就相当于是舞台的顶左 **/
offsetY?: number;
draggable?: boolean;
// 元素之间上下重叠时,是否允许穿透冒泡逐层触发,默认不允许
bubbleEvent?: boolean;
rotateCenter?:CanvasRotateCenter;
// 线条样式
lineJoin?: ILineJoinType;
lineDashOffset?: number;
lineDash ?:number[] ;
lineCap?: ILineCapType;
src?:string;
// 二维码的前景块的颜色。
foreground?:string;
// 二维码内容。
qrcodeText?:string;
text?: string;
fontSize?: number;
fontFamily?: string;
textAlign?: ITextAlignType;
textBaseline?: ITextBaselineType;
padding?: number;
lineHeight?: number;
innerRadius ?: number;
outerRadius ?: number;
startAngle ?: number;
endAngle ?: number;
radius ?:number;
sides ?:number;
//多边形线的点
points ?:number[];
//多边形线是否自动闭合
closed ?:boolean;
// 多边线将连接线曲线化
tension ?:number
//将曲线平滑化
bezier ?:boolean;
// 椭圆的小径
radiusX ?:number
//椭圆的大径
radiusY ?:number
//星的角数量,默认5角星
numPoints ?: number;
pointStart?:IShapeVector2d;
pointEnd?:IShapeVector2d;
strokeGradient?:string[];
fillGradient?:string[];
clip?:boolean;
zIndex?:number;
textBgColor?:string;
}
export type IShapeBoundRect = {
x : number,
y : number,
height : number,
width : number
}
export type QrGenerateFrameResult = {
frameBuffer: number[],
width: number
}
export interface IShape {
canvas : ICanvas,
x: number;
y: number;
width: number;
height: number;
fill: string;
stroke: string;
strokeWidth: number;
opacity: number;
visible: boolean;
rotation: number;
scaleX: number;
scaleY: number;
/** 元素相对舞台x坐标系的坐标,就是x,y为起始点,如果x,y为0就相当于是舞台的顶左 **/
offsetX: number;
/** 元素相对舞台y坐标系的坐标,就是x,y为起始点,如果x,y为0就相当于是舞台的顶左 **/
offsetY: number;
draggable: boolean;
// 元素之间上下重叠时,是否允许穿透冒泡逐层触发,默认不允许
bubbleEvent: boolean;
rotateCenter:CanvasRotateCenter;
/**toggle方法中的选中状态 */
toggleStatus:boolean;
// 线条样式
lineJoin: ILineJoinType;
lineDashOffset: number;
lineDash:number[]
lineCap: ILineCapType;
// text数据
text : string;
fontSize : number;
fontFamily : string ;
textAlign : ITextAlignType;
textBaseline : ITextBaselineType;
padding : number;
lineHeight : number;
//ring数据
innerRadius : number;
outerRadius : number;
startAngle : number;
endAngle : number;
radius :number;
sides: number;
src:string;
// 二维码的前景块的颜色。
foreground:string;
// 二维码内容。
qrcodeText:string;
points :number[];
//多边形线是否自动闭合
closed :boolean;
// 多边线将连接线曲线化
tension:number
bezier :boolean;
// 椭圆的小径
radiusX :number;
//椭圆的大径
radiusY :number;
//星的角数量,默认5角星
numPoints: number;
pointStart:IShapeVector2d;
pointEnd:IShapeVector2d;
strokeGradient:string[];
fillGradient:string[];
clip:boolean;
zIndex:number;
textBgColor:string;
drag : (eventName : CanvasEventType, parentEventDetail : ICanvasEvent) => void
draw(ctx: CanvasRenderingContext2D): void;
isPointInPath(x: number, y: number,shapeId:string): boolean;
getBoundRect() : IShapeBoundRect;
setAttr(key:string,value:any):void;
getAttr(key: string): any | null;
toggle(call:(status:boolean,target:any)=>void):void;
}
@@ -0,0 +1,348 @@
import { Shape } from '../shape.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
export type EasingFunction = (t: number) => number;
type UpdateCall = (t: number) => void;
type AnimationConfig = {
props: UTSJSONObject;
duration: number;
easing?: EasingFunction;
startValues: Map<string, number>;
}
export class Tween {
private target: Shape;
private startValues: Map<string, number> = new Map();
private endValues: Map<string, number> = new Map();
private duration: number;
private startTime: number = 0;
private isPlaying: boolean = false;
private easing: EasingFunction;
private onUpdateFun: ((progress:number) => void) | null = null;
private onCompleteFun: (() => void) | null = null;
private canvas: CanvasContext;
private parentICanvas: ICanvas;
private requestAnimationFrameId = 0;
private loop: number = 0; // 循环次数,0表示不循环,-1表示无限循环
private currentLoop: number = 0; // 当前已完成的循环次数
private yoyo: boolean = false; // 是否开启往返播放
private isReverse: boolean = false; // 当前是否为反向播放
private animationQueue: AnimationConfig[] = []; // 动画队列
private currentAnimationIndex: number = -1; // 当前执行的动画索引
private delayDur = 0
private delayDurTid = 12
constructor(target: Shape, canvas: ICanvas) {
this.target = target;
this.canvas = canvas.canvas!;
this.parentICanvas = canvas;
this.duration = 1000;
this.easing = Easing.linear as EasingFunction;
}
to(props: UTSJSONObject, duration: number = 1000): Tween {
return this.addTo(props, duration);
}
addTo(props: UTSJSONObject, duration: number = 1000, easing: EasingFunction = this.easing): Tween {
const startValues = new Map<string, number>();
for (const key in props) {
let keyValue = props[key];
if (typeof keyValue == 'number') {
let startValue = this.target.getAttr(key);
if(typeof startValue == 'number'){
startValues.set(key, startValue);
}
}
}
this.animationQueue.push({
props: props,
duration: duration,
easing: easing,
startValues: startValues
});
return this;
}
// 在队列中插入一个仅用于延时的动画段
delay(ms: number): Tween {
this.delayDur = ms;
return this;
}
clearAnimations(): Tween {
this.stop();
this.canvas.cancelAnimationFrame(this.requestAnimationFrameId);
this.requestAnimationFrameId = 0;
this.animationQueue = [];
this.currentAnimationIndex = -1;
this.startValues.clear();
this.endValues.clear();
return this;
}
start(): Tween {
let _this = this;
if (this.isPlaying) {
return this;
}
if(this.delayDur>0){
_this.delayDurTid = setTimeout(function() {
if (_this.requestAnimationFrameId != 0) {
_this.canvas.cancelAnimationFrame(_this.requestAnimationFrameId);
_this.requestAnimationFrameId = 0;
}
_this.update();
}, _this.delayDur);
}else{
if (this.requestAnimationFrameId != 0) {
this.canvas.cancelAnimationFrame(this.requestAnimationFrameId);
this.requestAnimationFrameId = 0;
}
this.update();
}
this.isPlaying = true;
return this;
}
stop(): Tween {
clearTimeout(this.delayDurTid)
this.isPlaying = false;
if (this.requestAnimationFrameId != 0) {
this.canvas.cancelAnimationFrame(this.requestAnimationFrameId);
this.requestAnimationFrameId = 0;
}
return this;
}
private update(): void {
const selftThis = this;
let updateSelf = null as null|UpdateCall;
selftThis.currentLoop = 1;
selftThis.isReverse = false;
// 开始执行队列中的第一个动画
if (selftThis.currentAnimationIndex == -1 && selftThis.animationQueue.length > 0) {
selftThis.currentAnimationIndex = 0;
const currentAnimation = selftThis.animationQueue[selftThis.currentAnimationIndex];
selftThis.duration = currentAnimation.duration;
selftThis.easing = currentAnimation.easing ?? selftThis.easing;
selftThis.startValues.clear();
selftThis.endValues.clear();
for (const key in currentAnimation.props) {
let keyValue = currentAnimation.props[key];
if (typeof keyValue == 'number') {
let startValue = currentAnimation.startValues?.get(key);
if(typeof startValue == 'number'){
selftThis.startValues.set(key, startValue!);
selftThis.endValues.set(key, keyValue);
}
}
}
}
updateSelf = (time:number)=>{
if (!selftThis.isPlaying) return;
const currentTime = time;
let elapsed = currentTime - selftThis.startTime;
if (elapsed > selftThis.duration) {
if (selftThis.yoyo && !selftThis.isReverse && (selftThis.currentLoop < selftThis.loop||selftThis.loop==-1)) {
// 开启yoyo且当前为正向播放,切换为反向播放
selftThis.isReverse = true;
if (selftThis.isPlaying) {
selftThis.requestAnimationFrameId = selftThis.canvas.requestAnimationFrame((t) => {
selftThis.startTime = t;
updateSelf!(t)
});
return;
}
} else {
if(this.yoyo){
selftThis.isReverse = false;
}
// 检查是否还有下一个动画
if (selftThis.currentAnimationIndex < selftThis.animationQueue.length - 1) {
selftThis.currentAnimationIndex++;
const nextAnimation = selftThis.animationQueue[selftThis.currentAnimationIndex];
selftThis.duration = nextAnimation.duration;
selftThis.easing = nextAnimation.easing ?? selftThis.easing;
selftThis.startValues.clear();
selftThis.endValues.clear();
for (const key in nextAnimation.props) {
let keyValue = nextAnimation.props[key];
if (typeof keyValue == 'number') {
let startValue = selftThis.target.getAttr(key);
if(typeof startValue == 'number'){
selftThis.startValues.set(key, nextAnimation.startValues.get(key)!);
selftThis.endValues.set(key, keyValue);
}
}
}
selftThis.requestAnimationFrameId = selftThis.canvas.requestAnimationFrame((t) => {
selftThis.startTime = t;
updateSelf!(t);
});
return;
} else if (selftThis.loop == -1 || selftThis.currentLoop < selftThis.loop) {
// 重置动画索引,开始新一轮循环
selftThis.currentAnimationIndex = 0;
selftThis.currentLoop++;
if(selftThis.animationQueue.length>0){
const nextAnimation = selftThis.animationQueue[selftThis.currentAnimationIndex];
selftThis.duration = nextAnimation.duration;
selftThis.easing = nextAnimation.easing ?? selftThis.easing;
selftThis.startValues.clear();
selftThis.endValues.clear();
for (const key in nextAnimation.props) {
let keyValue = nextAnimation.props[key];
if (typeof keyValue == 'number') {
let startValue = selftThis.target.getAttr(key);
if(typeof startValue == 'number'){
selftThis.startValues.set(key, nextAnimation.startValues.get(key)!);
selftThis.endValues.set(key, keyValue);
}
}
}
selftThis.requestAnimationFrameId = selftThis.canvas.requestAnimationFrame((t) => {
selftThis.startTime = t;
updateSelf!(t);
});
}else{
selftThis.duration = currentTime;
selftThis.easing = selftThis.easing;
selftThis.startValues.clear();
selftThis.endValues.clear();
selftThis.requestAnimationFrameId = selftThis.canvas.requestAnimationFrame((t) => {
selftThis.startTime = t;
updateSelf!(t);
});
}
return;
}
selftThis.isPlaying = false;
selftThis.currentAnimationIndex = -1; // 重置动画索引
selftThis.canvas.cancelAnimationFrame(selftThis.requestAnimationFrameId);
selftThis.requestAnimationFrameId = 0;
selftThis.animationQueue = [];
selftThis.startValues.clear();
selftThis.endValues.clear();
}
}
let progress = selftThis.easing(elapsed / selftThis.duration);
if (selftThis.isReverse) {
progress = 1 - progress;
}
selftThis.startValues.forEach((startValue, key) => {
const endValue = selftThis.endValues.get(key)!;
const value = startValue + (endValue - startValue) * progress;
selftThis.target.setAttr(key,value);
});
selftThis.target.needsUpdate = true;
if (selftThis.onUpdateFun!=null) {
selftThis.onUpdateFun!(progress);
}
selftThis.parentICanvas.update();
if (!selftThis.isPlaying && selftThis.onCompleteFun!=null) {
selftThis.onCompleteFun!();
return;
}
if (selftThis.isPlaying) {
selftThis.requestAnimationFrameId = selftThis.canvas.requestAnimationFrame((t) => updateSelf!(t));
}
}
selftThis.requestAnimationFrameId = selftThis.canvas.requestAnimationFrame((t) => {
selftThis.startTime = t;
updateSelf!(t);
});
}
destroy(): void {
this.stop();
this.animationQueue = [];
this.currentAnimationIndex = -1;
this.startValues.clear();
this.endValues.clear();
this.onUpdateFun = null;
this.onCompleteFun = null;
this.loop = 0;
this.yoyo = false;
}
setEasing(easingFunction: EasingFunction): Tween {
this.easing = easingFunction;
return this;
}
onUpdate(callback: (progress:number) => void): Tween {
this.onUpdateFun = callback;
return this;
}
onComplete(callback: () => void): Tween {
this.onCompleteFun = callback;
return this;
}
setLoop(count: number): Tween {
this.loop = count;
return this;
}
setYoyo(value: boolean): Tween {
this.yoyo = value;
return this;
}
}
export const Easing = {
linear: (t: number): number => t,
easeInQuad: (t: number): number => t * t,
easeOutQuad: (t: number): number => t * (2 - t),
easeInOutQuad: (t: number): number => {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
},
easeInCubic: (t: number): number => t * t * t,
easeOutCubic: (t: number): number => (t-1) * t * t + 1,
easeInOutCubic: (t: number): number => {
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
},
easeInElastic: (t: number): number => {
const c4 = (2 * Math.PI) / 3;
return t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
},
easeOutElastic: (t: number): number => {
const c4 = (2 * Math.PI) / 3;
return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
},
easeInOutElastic: (t: number): number => {
const c5 = (2 * Math.PI) / 4.5;
return t === 0 ? 0 : t === 1 ? 1 : t < 0.5
? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2
: (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1;
}
};
+200
View File
@@ -0,0 +1,200 @@
import { Shape } from './shape.uts';
import { CanvasRotateCenter, ShapeSetAttrType, IShapeBoundRect, IShapeOptional } from '../interface.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
export class IArc extends Shape {
constructor(config : IShapeOptional,canvas:ICanvas) {
super(config,canvas);
this.radius = config?.radius ?? 30;
this.startAngle = config?.startAngle ?? 0;
this.endAngle = config?.endAngle ?? 90;
this.type = "IArc"
}
setRadius(value : number) : IArc {
this.radius = value;
this.needsUpdate = true;
return this;
}
override getBoundRect() : IShapeBoundRect {
// 计算精确的弧段/扇形包围盒
const centerX = this.x;
const centerY = this.y;
const r = Math.max(0, this.radius);
const d2r = Math.PI / 180;
// 角度标准化到 [0, 360)
const normalize = (deg:number):number => {
let a = deg % 360;
if (a < 0) a += 360;
return a;
};
let start = normalize(this.startAngle);
let end = normalize(this.endAngle);
if (end < start) end += 360; // 保证 end >= start(顺时针绘制)
const withinSweep = (deg:number):boolean => {
let a = normalize(deg);
if (a < start) a += 360;
return a >= start && a <= end;
};
const candidatesX:number[] = [];
const candidatesY:number[] = [];
// 起止点
const sx = centerX + r * Math.cos(start * d2r);
const sy = centerY + r * Math.sin(start * d2r);
const ex = centerX + r * Math.cos(end * d2r);
const ey = centerY + r * Math.sin(end * d2r);
candidatesX.push(sx, ex);
candidatesY.push(sy, ey);
// 若为扇形(有填充),中心点也参与包围盒
const isFilled = (this.fill != "" || this.fillGradient.length > 0);
if (isFilled) {
candidatesX.push(centerX);
candidatesY.push(centerY);
}
// 检查跨越极值角(0, 90, 180, 270
const extrema = [0, 90, 180, 270];
for (const deg of extrema) {
if (withinSweep(deg)) {
const rad = deg * d2r;
candidatesX.push(centerX + r * Math.cos(rad));
candidatesY.push(centerY + r * Math.sin(rad));
}
}
let minX = candidatesX.length>0?candidatesX[0]:centerX;
let maxX = candidatesX.length>0?candidatesX[0]:centerX;
let minY = candidatesY.length>0?candidatesY[0]:centerY;
let maxY = candidatesY.length>0?candidatesY[0]:centerY;
for (let i=1;i<candidatesX.length;i++) {
const vx = candidatesX[i];
if (!isNaN(vx)) {
if (vx < minX) minX = vx;
if (vx > maxX) maxX = vx;
}
}
for (let i=1;i<candidatesY.length;i++) {
const vy = candidatesY[i];
if (!isNaN(vy)) {
if (vy < minY) minY = vy;
if (vy > maxY) maxY = vy;
}
}
// 描边扩张
const hasStroke = (this.stroke != "" || this.strokeGradient.length > 0);
const pad = hasStroke ? this.strokeWidth : 0;
minX -= pad;
minY -= pad;
maxX += pad;
maxY += pad;
return {
x: minX,
y: minY,
width: Math.max(0, maxX - minX),
height: Math.max(0, maxY - minY)
} as IShapeBoundRect;
}
override setWidth(value : number) : IArc {
this.height = value;
this.width = value;
this.radius = value / 2
this.needsUpdate = true;
return this;
}
override setHeight(value : number) : IArc {
this.height = value;
this.width = value;
this.radius = value / 2
this.needsUpdate = true;
return this;
}
setStartAngle(angle:number):IArc{
this.startAngle = angle;
this.needsUpdate = true;
return this;
}
setEndAngle(angle:number):IArc{
this.endAngle = angle;
this.needsUpdate = true;
return this;
}
override draw(ctx : CanvasRenderingContext2D) {
if (this.visible == false) return;
super.draw(ctx);
ctx.beginPath();
// 将角度转换为弧度
const startRad = this.startAngle * Math.PI / 180;
const endRad = this.endAngle * Math.PI / 180;
// 如果有填充色,先移动到圆心,绘制扇形
if (this.fill != ""||this.fillGradient.length>0) {
ctx.moveTo(this.x, this.y);
}
// 绘制圆弧
ctx.arc(this.x, this.y, this.radius, startRad, endRad, false);
if (this.fill != ""||this.fillGradient.length>0) {
ctx.closePath();
ctx.fill();
}
if (this.stroke != ""||this.strokeGradient.length>0) {
ctx.stroke();
}
ctx.restore();
}
override isPointInPath(x: number, y: number, shapeId: string): boolean {
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
const realX = x - (this.offsetX) - this.x;
const realY = y - (this.offsetY) - this.y;
// 计算点到圆心的距离
const dx = realX;
const dy = realY;
const distance = Math.sqrt(dx * dx + dy * dy);
// 计算点相对于圆心的角度(弧度)
const angle = Math.atan2(dy, dx);
// 将角度转换为0-360度范围
let degrees = angle * 180 / Math.PI;
if (degrees < 0) degrees += 360;
// 将起始角度和结束角度标准化到0-360度范围
let start = this.startAngle % 360;
if (start < 0) start += 360;
let end = this.endAngle % 360;
if (end < 0) end += 360;
// 确保end大于start
if (end < start) end += 360;
// 检查点的角度是否在弧的范围内
const inAngle = degrees >= start && degrees <= end;
if (this.fill !== "") {
// 填充模式:检查是否在扇形内
return inAngle && distance <= this.radius;
} else if (this.stroke !== "") {
// 非填充模式:检查是否在圆弧线附近
// 考虑整个线宽作为检测范围
const tolerance = this.strokeWidth;
const distanceFromArc = Math.abs(distance - this.radius);
return inAngle && distanceFromArc <= tolerance;
}
return false;
}
}
@@ -0,0 +1,133 @@
import { Shape } from './shape.uts';
import { CanvasRotateCenter, IShapeBoundRect, IShapeOptional } from '../interface.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
export class ICircle extends Shape {
override type = 'ICircle'
constructor(config : IShapeOptional,canvas:ICanvas) {
super(config,canvas);
this.radius = config?.radius ?? 30;
}
setRadius(value : number) : ICircle {
this.radius = value;
this.needsUpdate = true;
return this;
}
override getBoundRect() : IShapeBoundRect {
const r = Math.max(0, this.radius);
const pad = (this.stroke != "" ? this.strokeWidth / 2 : 0);
const d = r * 2 + pad * 2;
return {
x: this.x - r - pad,
y: this.y - r - pad,
width: d,
height: d
} as IShapeBoundRect;
}
override setWidth(value : number) : ICircle {
this.height = value;
this.width = value;
this.radius = value / 2
this.needsUpdate = true;
return this;
}
override setHeight(value : number) : ICircle {
this.height = value;
this.width = value;
this.radius = value / 2
this.needsUpdate = true;
return this;
}
override draw(ctx : CanvasRenderingContext2D) {
if (this.visible == false) return;
super.draw(ctx);
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
ctx.closePath();
if (this.fill != ""||this.fillGradient.length>0) {
ctx.fill();
}
if (this.stroke != "") {
ctx.stroke();
}
ctx.restore();
}
override isPointInPath(x: number, y: number, shapeId: string): boolean {
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
// 计算点击位置相对于圆心的实际坐标
let realX = x - this.offsetX - this.x;
let realY = y - this.offsetY - this.y;
// 如果有旋转,需要将坐标转换回未旋转状态
if (this.rotation != 0) {
const angle = -this.rotation * Math.PI / 180;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let centerX = 0;
let centerY = 0;
// 根据不同的旋转中心点设置centerX和centerY
switch(this.rotateCenter) {
case 'topLeft':
centerX = 0;
centerY = 0;
break;
case 'topRight':
centerX = this.width;
centerY = 0;
break;
case 'bottomLeft':
centerX = 0;
centerY = this.height;
break;
case 'bottomRight':
centerX = this.width;
centerY = this.height;
break;
case 'center':
default:
centerX = this.width/2;
centerY = this.height/2;
break;
}
const dx = realX - centerX;
const dy = realY - centerY;
realX = centerX + dx * cos - dy * sin;
realY = centerY + dx * sin + dy * cos;
}
// 计算点到圆心的距离
const distance = Math.sqrt(realX * realX + realY * realY);
// 考虑缩放因素
const scaledRadius = this.radius * Math.min(this.scaleX, this.scaleY);
// 如果只有描边模式,检查点是否在圆形线条附近
if (this.stroke != "" && this.fill == ""&&this.fillGradient.length==0) {
const strokeWidth = this.strokeWidth / 2;
return Math.abs(distance - scaledRadius) <= strokeWidth;
}
// 如果同时有描边和填充,或者只有填充
if (this.fill != ""||this.fillGradient.length>0) {
// 如果有描边,扩大检测范围到描边外缘
if (this.stroke != "") {
return distance <= (scaledRadius + this.strokeWidth / 2);
}
// 只有填充时,检查点是否在圆内
return distance <= scaledRadius;
}
return false;
}
}
@@ -0,0 +1,141 @@
import { Shape } from './shape.uts';
import { CanvasRotateCenter, IShapeBoundRect, IShapeOptional } from '../interface.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
export class IEllipse extends Shape {
override type = 'IEllipse'
constructor(config : IShapeOptional,canvas:ICanvas) {
super(config,canvas);
this.radiusX = config?.radiusX ?? 80;
this.radiusY = config?.radiusY ?? 20;
}
setRadiusX(value : number) : IEllipse {
this.radiusX = value;
this.needsUpdate = true;
return this;
}
setRadiusY(value : number) : IEllipse {
this.radiusY = value;
this.needsUpdate = true;
return this;
}
override getBoundRect() : IShapeBoundRect {
const rx = Math.max(0, this.radiusX);
const ry = Math.max(0, this.radiusY);
const pad = (this.stroke != "" ? this.strokeWidth / 2 : 0);
return {
x: this.x - rx - pad,
y: this.y - ry - pad,
width: rx * 2 + pad * 2,
height: ry * 2 + pad * 2
} as IShapeBoundRect;
}
override setWidth(value : number) : IEllipse {
this.width = value;
this.radiusX = value / 2;
this.needsUpdate = true;
return this;
}
override setHeight(value : number) : IEllipse {
this.height = value;
this.radiusY = value / 2;
this.needsUpdate = true;
return this;
}
override draw(ctx : CanvasRenderingContext2D) {
if (this.visible == false) return;
super.draw(ctx);
ctx.beginPath();
ctx.ellipse(this.x, this.y, this.radiusX, this.radiusY, 0, 0, Math.PI * 2, false);
ctx.closePath();
if (this.fill != "") {
ctx.fill();
}
if (this.stroke != "") {
ctx.stroke();
}
ctx.restore();
}
override isPointInPath(x: number, y: number, shapeId: string): boolean {
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
// 计算点击位置相对于椭圆中心的实际坐标
let realX = x - this.offsetX - this.x;
let realY = y - this.offsetY - this.y;
// 如果有旋转,需要将坐标转换回未旋转状态
if (this.rotation != 0) {
const angle = -this.rotation * Math.PI / 180;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let centerX = 0;
let centerY = 0;
// 根据不同的旋转中心点设置centerX和centerY
switch(this.rotateCenter) {
case 'topLeft':
centerX = 0;
centerY = 0;
break;
case 'topRight':
centerX = this.width;
centerY = 0;
break;
case 'bottomLeft':
centerX = 0;
centerY = this.height;
break;
case 'bottomRight':
centerX = this.width;
centerY = this.height;
break;
case 'center':
default:
centerX = this.width/2;
centerY = this.height/2;
break;
}
const dx = realX - centerX;
const dy = realY - centerY;
realX = centerX + dx * cos - dy * sin;
realY = centerY + dx * sin + dy * cos;
}
// 考虑缩放因素
const scaledRadiusX = this.radiusX * Math.abs(this.scaleX);
const scaledRadiusY = this.radiusY * Math.abs(this.scaleY);
// 计算点是否在椭圆内(标准椭圆方程)
const normalizedX = realX / scaledRadiusX;
const normalizedY = realY / scaledRadiusY;
const distance = normalizedX * normalizedX + normalizedY * normalizedY;
// 如果只有描边模式,检查点是否在椭圆线条附近
if (this.stroke != "" && this.fill == "") {
const strokeWidth = this.strokeWidth / 2;
const outerDistance = Math.sqrt(distance);
return Math.abs(outerDistance - 1) * Math.min(scaledRadiusX, scaledRadiusY) <= strokeWidth;
}
// 如果同时有描边和填充,或者只有填充
if (this.fill != "") {
// 如果有描边,扩大检测范围到描边外缘
if (this.stroke != "") {
const strokeOffset = this.strokeWidth / (2 * Math.min(scaledRadiusX, scaledRadiusY));
return distance <= (1 + strokeOffset) * (1 + strokeOffset);
}
// 只有填充时,检查点是否在椭圆内
return distance <= 1;
}
return false;
}
}
@@ -0,0 +1,205 @@
import { Shape } from './shape.uts';
import { IShapeBoundRect, IShapeOptional, ShapeSetAttrType } from '../interface.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
/**
* 图片
* @description 注意创建添加完图片后。要通过方法setSrc(xx)来设置加载图片
* @version 1.0.0
* @date 2025/2/6
* @copright 不允许外发给别人,只可在tmui4x是使用
*/
export class ImageShape extends Shape {
override type = 'ImageShape'
private image : any | null = null;
loaded : boolean = false;
private srcWidth : number = 0;
private srcHeight : number = 0;
private cropX : number = 0;
private cropY : number = 0;
private cropWidth : number = 0;
private cropHeight : number = 0;
private useCrop : boolean = false;
private canvasContext : CanvasContext;
constructor(config : IShapeOptional,canvas:ICanvas) {
super(config,canvas);
this.canvasContext = canvas.canvas!
this.type = 'ImageShape';
if(this.src!=''){
this.setSrc(this.src)
}
}
setSrc(src : string) : ImageShape {
if (!this.visible) return this;
this.src = src;
this.loaded = false;
if (this.src != '') {
this.loadImage();
}
return this;
}
private loadImage() : void {
if (this.loaded) return;
const image = this.canvasContext.createImage();
image.src = this.src;
let _this = this;
image.onload = () => {
_this.image = image;
_this.loaded = true;
if (_this.width == 0) _this.width = image.width;
if (_this.height == 0) _this.height = image.height;
_this.srcWidth = image.width;
_this.srcHeight = image.height;
_this.needsUpdate = true;
_this.canvas.update()
};
}
crop(x : number, y : number, width : number, height : number) : ImageShape {
this.cropX = x;
this.cropY = y;
this.cropWidth = width;
this.cropHeight = height;
this.useCrop = true;
this.needsUpdate = true;
return this;
}
resetCrop() : ImageShape {
this.useCrop = false;
this.needsUpdate = true;
return this;
}
override draw(ctx : CanvasRenderingContext2D) : void {
if (!this.loaded || this.image == null) return;
super.draw(ctx);
// #ifdef APP||WEB
if (this.useCrop) {
ctx.drawImage(
this.image! as Image,
this.cropX,
this.cropY,
this.cropWidth,
this.cropHeight,
this.x,
this.y,
this.width,
this.height
);
} else {
ctx.drawImage(
this.image! as Image,
this.x,
this.y,
this.width,
this.height
);
}
// #endif
// #ifdef MP
if (this.useCrop) {
ctx.drawImage(
this.image!,
this.cropX,
this.cropY,
this.cropWidth,
this.cropHeight,
this.x,
this.y,
this.width,
this.height
);
} else {
ctx.drawImage(
this.image!,
this.x,
this.y,
this.width,
this.height
);
}
// #endif
ctx.restore()
}
override isPointInPath(x : number, y : number, shapeId : string) : boolean {
if (!this.visible || !this.loaded || (shapeId != "" && shapeId != this.id)) return false;
const realX = x - this.x - this.offsetX;
const realY = y - this.y - this.offsetY;
if (this.rotation != 0) {
const angle = -this.rotation * Math.PI / 180;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let centerX = 0;
let centerY = 0;
// 根据不同的旋转中心点设置centerX和centerY
switch(this.rotateCenter) {
case 'topLeft':
centerX = 0;
centerY = 0;
break;
case 'topRight':
centerX = this.width;
centerY = 0;
break;
case 'bottomLeft':
centerX = 0;
centerY = this.height;
break;
case 'bottomRight':
centerX = this.width;
centerY = this.height;
break;
case 'center':
default:
centerX = this.width/2;
centerY = this.height/2;
break;
}
const dx = realX - centerX;
const dy = realY - centerY;
const rotatedX = centerX + dx * cos - dy * sin;
const rotatedY = centerY + dx * sin + dy * cos;
return rotatedX >= 0 && rotatedX <= this.width * this.scaleX && rotatedY >= 0 && rotatedY <= this.height * this.scaleY;
}
return realX >= 0 && realX <= this.width * this.scaleX && realY >= 0 && realY <= this.height * this.scaleY;
}
override getBoundRect() : IShapeBoundRect {
return {
x: this.x,
y: this.y,
width: this.width,
height: this.height
} as IShapeBoundRect;
}
override getAttr(key: string): any | null {
const superValue = super.getAttr(key);
if(superValue!=null) return superValue;
switch (key) {
case 'src': return this.src;
case 'cropX': return this.cropX;
case 'cropY': return this.cropY;
case 'cropWidth': return this.cropWidth;
case 'cropHeight': return this.cropHeight;
case 'useCrop': return this.useCrop;
default: return null;
}
}
}
@@ -0,0 +1,355 @@
import { Shape } from '@/uni_modules/tmx-ui/core/canvas/lib/shape.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
type LayoutDirection = 'horizontal'|'vertical'
type LayoutAnchor = 'top'|'bottom'
type LayoutMode = 'left'|'right'|'between'|'center';//左对齐,右对齐,两边对齐均分,中间对齐
type CurSizeType = {width:number,height:number}
type RoysType = {items:Shape[], sizes:CurSizeType[], rowWidth:number, rowHeight:number}
type CoysType = {items:Shape[], sizes:CurSizeType[], colWidth:number, colHeight:number}
export class ILayout {
x:number = 0
y:number = 0
width:number = 0;
height:number = 0;
shapeList:Shape[] = [];
colSpace:number = 10
rowSpace:number = 10
direction:LayoutDirection = 'horizontal'
mode:LayoutMode = 'left'
anchor:LayoutAnchor = 'top';//是以顶开始往排,还是以容器底往上排
// 自动断行
wrap:boolean = false
icanvas:ICanvas;
/**
* 创建布局容器
* @param ic 画布实例(默认用于初始化容器宽高)
*/
constructor(ic:ICanvas){
this.icanvas = ic;
this.width = ic.width
this.height = ic.height
}
/**
* 添加需要参与布局的元素(按 id 去重)
* @param shapes 形状列表
*/
addShape(shapes:Shape[]){
let ids = this.shapeList.map((el:Shape):string => el.id)
let realShapes = shapes.filter((el:Shape):boolean => !ids.includes(el.id))
this.shapeList.push(...realShapes)
}
/**
* 设置容器 X 偏移
* @param val X 坐标
*/
setsX(val:number){
this.x = val
}
/**
* 设置容器 Y 偏移
* @param val Y 坐标
*/
setsY(val:number){
this.y = val
}
/**
* 设置容器位置
* @param x X 坐标
* @param y Y 坐标
*/
setsPosition(x:number,y:number){
this.x = x
this.y = y
}
/**
* 设置容器尺寸
* @param w 宽度
* @param h 高度
*/
setsSize(w:number,h:number){
this.width = w;
this.height = h;
}
/**
* 设置容器宽度
* @param w 宽度
*/
setsWidth(w:number){
this.width = w;
}
/**
* 设置容器高度
* @param h 高度
*/
setsHeight(h:number){
this.height = h;
}
/**
* 设置列间距(横向相邻元素间距)
* @param val 间距像素
*/
setsSpaceCol(val:number){
this.colSpace = val;
}
/**
* 设置行间距(纵向相邻元素间距)
* @param val 间距像素
*/
setsSpaceRow(val:number){
this.rowSpace = val
}
/**
* 同时设置列/行间距
* @param col 列间距
* @param row 行间距
*/
setsSpace(col:number,row:number){
this.colSpace = col;
this.rowSpace = row
}
/**
* 设置主轴方向
* @param val 'horizontal' | 'vertical'
*/
setsLayoutDirection(val:LayoutDirection){
this.direction = val;
}
/**
* 设置主轴对齐模式
* @param val 'left' | 'right' | 'between' | 'center'
*/
setsMode(val:LayoutMode){
this.mode = val;
}
/**
* 设置交叉轴锚点(顶/底对齐)
* @param val 'top' | 'bottom'
*/
setsAnchor(val:LayoutAnchor){
this.anchor = val;
}
/**
* 设置是否自动换行/换列
* @param val true 开启
*/
setsWrap(val:boolean){
this.wrap = val;
}
/**
* 执行布局:根据 direction / mode / anchor / wrap / spacing 将 shapeList 定位
*/
render(){
const items = this.shapeList.filter((s:Shape)=> s.visible);
if(items.length==0) return;
if(this.direction=='horizontal'){
this.layoutHorizontal(items);
}else{
this.layoutVertical(items);
}
items.forEach((el) => {
el.needsUpdate = true
})
}
/**
* 获取元素参与布局的尺寸(来自元素 getBoundRect
* @param shape 形状
* @returns width/height
*/
private getItemSize(shape:Shape):CurSizeType{
const rect = shape.getBoundRect();
const w = Math.max(0, rect.width);
const h = Math.max(0, rect.height);
return {width:w,height:h};
}
/**
* 横向布局:按主轴水平排列,支持均分与换行
* @param items 待布局元素
*/
private layoutHorizontal(items:Shape[]){
if(!this.wrap){
// single row
const sizes = items.map(s=> this.getItemSize(s));
const totalWidth = sizes.reduce((acc,it)=> acc + it.width, 0);
const gaps = Math.max(0, items.length-1);
let spacing = this.colSpace;
let startX = this.x;
if(this.mode=='between' && gaps>0){
spacing = Math.max(0, (this.width - totalWidth) / gaps);
startX = this.x;
}else if(this.mode=='center'){
startX = this.x + Math.max(0, (this.width - (totalWidth + spacing*gaps)) / 2);
}else if(this.mode=='right'){
startX = this.x + Math.max(0, this.width - (totalWidth + spacing*gaps));
}else{
startX = this.x;
}
let x = startX;
for(let i=0;i<items.length;i++){
const s = items[i];
const sz = sizes[i];
const y = this.anchor=='top' ? this.y : this.y + Math.max(0, this.height - sz.height);
s.setX(x).setY(y);
x += sz.width + spacing;
}
return;
}
// wrapping: multiple rows
let rows: RoysType[] = [];
let curItems:Shape[] = [];
let curSizes:CurSizeType[] = [];
let curWidth = 0;
let curHeight = 0;
for(let i=0;i<items.length;i++){
const s = items[i];
const sz = this.getItemSize(s);
const addWidth = (curItems.length>0? this.colSpace:0) + sz.width;
if(curItems.length>0 && curWidth + addWidth > this.width){
rows.push({items:curItems, sizes:curSizes, rowWidth:curWidth, rowHeight:curHeight});
curItems = [];
curSizes = [];
curWidth = 0;
curHeight = 0;
}
if(curItems.length>0){
curWidth += this.colSpace;
}
curItems.push(s);
curSizes.push(sz);
curWidth += sz.width;
curHeight = Math.max(curHeight, sz.height);
}
if(curItems.length>0){
rows.push({items:curItems, sizes:curSizes, rowWidth:curWidth, rowHeight:curHeight});
}
// total height with row spaces
const totalRowsHeight = rows.reduce((acc,r)=> acc + r.rowHeight, 0);
const totalRowSpaces = this.rowSpace * Math.max(0, rows.length-1);
let startY = this.anchor=='top' ? this.y : this.y + Math.max(0, this.height - (totalRowsHeight + totalRowSpaces));
let y = startY;
for(let r=0;r<rows.length;r++){
const row = rows[r];
// horizontal positioning per row by mode
const gaps = Math.max(0, row.items.length-1);
let spacing = this.colSpace;
let startX = this.x;
if(this.mode=='between' && gaps>0){
spacing = Math.max(0, (this.width - row.rowWidth + this.colSpace*gaps) / gaps);
startX = this.x;
}else if(this.mode=='center'){
startX = this.x + Math.max(0, (this.width - (row.rowWidth + this.colSpace*gaps)) / 2);
}else if(this.mode=='right'){
startX = this.x + Math.max(0, this.width - (row.rowWidth + this.colSpace*gaps));
}else{
startX = this.x;
}
let x = startX;
for(let i=0;i<row.items.length;i++){
const s = row.items[i];
const sz = row.sizes[i];
const offsetY = this.anchor=='top' ? 0 : (row.rowHeight - sz.height);
s.setX(x).setY(y + offsetY);
x += sz.width + spacing;
}
y += row.rowHeight + this.rowSpace;
}
}
/**
* 纵向布局:按主轴垂直排列,支持均分与换列
* @param items 待布局元素
*/
private layoutVertical(items:Shape[]){
if(!this.wrap){
// single column
const sizes = items.map(s=> this.getItemSize(s));
const totalHeight = sizes.reduce((acc,it)=> acc + it.height, 0);
const gaps = Math.max(0, items.length-1);
let spacing = this.rowSpace;
let startY = this.y;
if(this.mode=='between' && gaps>0){
spacing = Math.max(0, (this.height - totalHeight) / gaps);
startY = this.y;
}else if(this.anchor=='top'){
startY = this.y;
}else{
startY = this.y + Math.max(0, this.height - (totalHeight + spacing*gaps));
}
let y = startY;
for(let i=0;i<items.length;i++){
const s = items[i];
const sz = sizes[i];
let x = this.x;
if(this.mode=='center') x = this.x + Math.max(0, (this.width - sz.width)/2);
else if(this.mode=='right') x = this.x + Math.max(0, this.width - sz.width);
else x = this.x; // left or between on single column
s.setX(x).setY(y);
y += sz.height + spacing;
}
return;
}
// wrapping into columns
let cols: CoysType[] = [];
let curItems:Shape[] = [];
let curSizes:CurSizeType[] = [];
let curHeight = 0;
let curWidth = 0;
for(let i=0;i<items.length;i++){
const s = items[i];
const sz = this.getItemSize(s);
const addHeight = (curItems.length>0? this.rowSpace:0) + sz.height;
if(curItems.length>0 && curHeight + addHeight > this.height){
cols.push({items:curItems, sizes:curSizes, colWidth:curWidth, colHeight:curHeight});
curItems = [];
curSizes = [];
curHeight = 0;
curWidth = 0;
}
if(curItems.length>0){
curHeight += this.rowSpace;
}
curItems.push(s);
curSizes.push(sz);
curHeight += sz.height;
curWidth = Math.max(curWidth, sz.width);
}
if(curItems.length>0){
cols.push({items:curItems, sizes:curSizes, colWidth:curWidth, colHeight:curHeight});
}
const totalColsWidth = cols.reduce((acc,c)=> acc + c.colWidth, 0);
const totalColSpaces = this.colSpace * Math.max(0, cols.length-1);
let startX = this.x;
if(this.mode=='between' && cols.length>1){
// distribute columns
startX = this.x;
}else if(this.mode=='center'){
startX = this.x + Math.max(0, (this.width - (totalColsWidth + totalColSpaces))/2);
}else if(this.mode=='right'){
startX = this.x + Math.max(0, this.width - (totalColsWidth + totalColSpaces));
}else{
startX = this.x;
}
let x = startX;
let betweenSpacing = this.colSpace;
if(this.mode=='between' && cols.length>1){
betweenSpacing = Math.max(0, (this.width - totalColsWidth) / (cols.length-1));
}
for(let c=0;c<cols.length;c++){
const col = cols[c];
let y = this.anchor=='top' ? this.y : this.y + Math.max(0, this.height - col.colHeight - this.rowSpace*(col.items.length-1));
for(let i=0;i<col.items.length;i++){
const s = col.items[i];
const sz = col.sizes[i];
let offsetX = 0;
if(this.mode=='center') offsetX = Math.max(0, (col.colWidth - sz.width)/2);
else if(this.mode=='right') offsetX = Math.max(0, col.colWidth - sz.width);
else offsetX = 0;
s.setX(x + offsetX).setY(y);
y += sz.height + this.rowSpace;
}
x += col.colWidth + betweenSpacing;
}
}
}
+228
View File
@@ -0,0 +1,228 @@
import { Shape } from './shape.uts';
import { CanvasRotateCenter, IShapeBoundRect, IShapeOptional, IShapeVector2d } from '../interface.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
export class ILine extends Shape {
override type = 'ILine'
// 线条的起点和终点
constructor(config: IShapeOptional, canvas: ICanvas) {
super(config, canvas);
// 更新宽高
this.updateDimensions();
}
// 设置起点
setStart(x: number, y: number): ILine {
this.pointStart.x = x;
this.pointStart.y = y;
this.updateDimensions();
this.needsUpdate = true;
return this;
}
// 设置终点
setEnd(x: number, y: number): ILine {
this.pointEnd.x = x;
this.pointEnd.y = y;
this.updateDimensions();
this.needsUpdate = true;
return this;
}
// 根据参考点旋转线条
rotateByPoint(isStartPoint: boolean, angle: number): ILine {
// 计算线条当前长度
const length = Math.sqrt(
Math.pow(this.pointEnd.x - this.pointStart.x, 2) +
Math.pow(this.pointEnd.y - this.pointStart.y, 2)
);
// 将角度转换为弧度,顺时针为正,逆时针为负
const radians = angle * Math.PI / 180;
if (isStartPoint) {
// 以起点为基准旋转
this.pointEnd.x = this.pointStart.x + length * Math.cos(radians);
this.pointEnd.y = this.pointStart.y + length * Math.sin(radians);
} else {
// 以终点为基准旋转
this.pointStart.x = this.pointEnd.x - length * Math.cos(radians);
this.pointStart.y = this.pointEnd.y - length * Math.sin(radians);
}
// 更新线条的包围盒尺寸
this.updateDimensions();
this.needsUpdate = true;
return this;
}
// 更新线条的包围盒尺寸
private updateDimensions(): void {
this.x = Math.min(this.pointStart.x, this.pointEnd.x);
this.y = Math.min(this.pointStart.y, this.pointEnd.y);
this.width = Math.abs(this.pointEnd.x - this.pointStart.x);
this.height = Math.abs(this.pointEnd.y - this.pointStart.y);
}
override getBoundRect(): IShapeBoundRect {
// include stroke width padding around the line segment
const pad = this.stroke != "" || this.strokeGradient.length>0 ? this.strokeWidth / 2 : 0;
return {
x: this.x - pad,
y: this.y - pad,
width: this.width + pad * 2,
height: this.height + pad * 2
} as IShapeBoundRect;
}
override draw(ctx: CanvasRenderingContext2D) {
if (this.visible == false) return;
super.draw(ctx);
ctx.beginPath();
ctx.moveTo(this.pointStart.x, this.pointStart.y);
ctx.lineTo(this.pointEnd.x, this.pointEnd.y);
if (this.stroke != ""||this.strokeGradient.length>0) {
ctx.stroke();
}
ctx.restore()
}
override setHeight(value : number) : Shape {
// 计算线条的中心点
const centerX = (this.pointStart.x + this.pointEnd.x) / 2;
const centerY = (this.pointStart.y + this.pointEnd.y) / 2;
// 计算当前线条的宽度(保持不变)
const currentWidth = Math.abs(this.pointEnd.x - this.pointStart.x);
// 根据新的高度重新计算起点和终点的Y坐标
// 保持线条的水平方向不变,只改变垂直方向
if (this.pointStart.x <= this.pointEnd.x) {
// 从左到右的线条
this.pointStart.x = centerX - currentWidth / 2;
this.pointEnd.x = centerX + currentWidth / 2;
} else {
// 从右到左的线条
this.pointStart.x = centerX + currentWidth / 2;
this.pointEnd.x = centerX - currentWidth / 2;
}
// 根据新高度设置Y坐标
if (this.pointStart.y <= this.pointEnd.y) {
// 从上到下的线条
this.pointStart.y = centerY - value / 2;
this.pointEnd.y = centerY + value / 2;
} else {
// 从下到上的线条
this.pointStart.y = centerY + value / 2;
this.pointEnd.y = centerY - value / 2;
}
this.updateDimensions();
this.needsUpdate = true;
return this;
}
override setWidth(value : number) : Shape {
// 计算线条的中心点
const centerX = (this.pointStart.x + this.pointEnd.x) / 2;
const centerY = (this.pointStart.y + this.pointEnd.y) / 2;
// 计算当前线条的高度(保持不变)
const currentHeight = Math.abs(this.pointEnd.y - this.pointStart.y);
// 根据新的宽度重新计算起点和终点的X坐标
// 保持线条的垂直方向不变,只改变水平方向
if (this.pointStart.y <= this.pointEnd.y) {
// 从上到下的线条
this.pointStart.y = centerY - currentHeight / 2;
this.pointEnd.y = centerY + currentHeight / 2;
} else {
// 从下到上的线条
this.pointStart.y = centerY + currentHeight / 2;
this.pointEnd.y = centerY - currentHeight / 2;
}
// 根据新宽度设置X坐标
if (this.pointStart.x <= this.pointEnd.x) {
// 从左到右的线条
this.pointStart.x = centerX - value / 2;
this.pointEnd.x = centerX + value / 2;
} else {
// 从右到左的线条
this.pointStart.x = centerX + value / 2;
this.pointEnd.x = centerX - value / 2;
}
this.updateDimensions();
this.needsUpdate = true;
return this;
}
// 获取两点之间的角度,以起点为基准,水平向右为0度,顺时针为正,逆时针为负
getAngle(): number {
// 直接使用起点和终点计算角度
const angle = Math.atan2(this.pointEnd.y - this.pointStart.y, this.pointEnd.x - this.pointStart.x);
// 将弧度转换为角度,顺时针为正
return angle * (180 / Math.PI);
}
getAngleByPoints(start:IShapeVector2d,end:IShapeVector2d): number {
// 直接使用起点和终点计算角度
const angle = Math.atan2(start.y - end.y, end.x - start.x);
// 将弧度转换为角度,顺时针为正,逆时针为负
return angle * (180 / Math.PI);
}
// 计算线条绕指定锚点的旋转角度
getAngleByAnchor(anchorX: number, anchorY: number, startX: number, startY: number, endX: number, endY: number): number {
// 计算向量1:从锚点到起点的向量
const vector1X = startX - anchorX;
const vector1Y = startY - anchorY;
// 计算向量2:从锚点到终点的向量
const vector2X = endX - anchorX;
const vector2Y = endY - anchorY;
// 使用向量的点积和叉积计算角度
const dotProduct = vector1X * vector2X + vector1Y * vector2Y;
const crossProduct = vector1X * vector2Y - vector1Y * vector2X;
// 计算角度(弧度)
const angle = Math.atan2(crossProduct, dotProduct);
// 将弧度转换为角度,并确保角度范围在-180到180度之间
return angle * (180 / Math.PI);
}
override isPointInPath(x: number, y: number, shapeId: string): boolean {
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
// 计算点到线段的距离
const lineLength = Math.sqrt(
Math.pow(this.pointEnd.x - this.pointStart.x, 2) +
Math.pow(this.pointEnd.y - this.pointStart.y, 2)
);
if (lineLength === 0) return false;
const distance = Math.abs(
(this.pointEnd.y - this.pointStart.y) * x -
(this.pointEnd.x - this.pointStart.x) * y +
this.pointEnd.x * this.pointStart.y -
this.pointEnd.y * this.pointStart.x
) / lineLength;
// 判断点是否在线段的范围内
const dotProduct =
((x - this.pointStart.x) * (this.pointEnd.x - this.pointStart.x) +
(y - this.pointStart.y) * (this.pointEnd.y - this.pointStart.y)) / lineLength;
return distance <= this.strokeWidth && dotProduct >= 0 && dotProduct <= lineLength;
}
}
@@ -0,0 +1,314 @@
import { Shape } from './shape.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
import { IShapeBoundRect, IShapeOptional } from '../interface.uts';
function getControlPoints(x0 : number, y0 : number, x1 : number, y1 : number, x2 : number, y2 : number, t : number) : number[] {
const d01 = Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2)),
d12 = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)),
fa = (t * d01) / (d01 + d12),
fb = (t * d12) / (d01 + d12),
p1x = x1 - fa * (x2 - x0),
p1y = y1 - fa * (y2 - y0),
p2x = x1 + fb * (x2 - x0),
p2y = y1 + fb * (y2 - y0);
return [p1x, p1y, p2x, p2y];
}
function expandPoints(p : number[], tension : number) : number[] {
const len = p.length
const allPoints : Array<number> = [];
for (let n = 2; n < len - 2; n += 2) {
const cp = getControlPoints(
p[n - 2],
p[n - 1],
p[n],
p[n + 1],
p[n + 2],
p[n + 3],
tension
);
if (isNaN(cp[0])) {
continue;
}
allPoints.push(cp[0]);
allPoints.push(cp[1]);
allPoints.push(p[n]);
allPoints.push(p[n + 1]);
allPoints.push(cp[2]);
allPoints.push(cp[3]);
}
return allPoints;
}
export class ILinePolygon extends Shape {
override type = 'ILinePolygon'
constructor(config : IShapeOptional, canvas : ICanvas) {
super(config, canvas);
}
override getBoundRect() : IShapeBoundRect {
let points = this.points;
if (points.length < 4) {
let cp = [] as number[]
for(let i=0;i<(4-points.length);i++){
cp.push(0)
}
points = points.concat(cp)
return {
x: points[0],
y: points[1],
width: 0,
height: 0,
};
}
if (this.tension !== 0) {
points = [
points[0],
points[1],
...this._getTensionPoints(false),
points[points.length - 2],
points[points.length - 1],
];
} else {
points = this.points;
}
let minX = this.points[0] + this.x;
let maxX = this.points[0] + this.x;
let minY = this.points[1] + this.y;
let maxY = this.points[1] + this.y;
let x=0
let y =0;
for (let i = 0; i < points.length / 2; i++) {
x = points[i * 2] + this.x;
y = points[i * 2 + 1] + this.y;
minX = Math.min(minX, x);
maxX = Math.max(maxX, x);
minY = Math.min(minY, y);
maxY = Math.max(maxY, y);
}
let borderWidth = (this.stroke!='' ? this.strokeWidth / 2 : 0);
return {
x: minX - borderWidth,
y: minY - borderWidth,
width: maxX - minX + borderWidth*2,
height: maxY - minY + borderWidth*2,
} as IShapeBoundRect;
}
private getTensionPoints() {
return this._getTensionPoints();
}
private _getTensionPoints(isXYOffset:boolean = true) {
if (this.closed) {
return this._getTensionPointsClosed(isXYOffset);
} else {
return expandPoints(isXYOffset?this._getPoints():this.points, this.tension);
}
}
private _getTensionPointsClosed(isXYOffset:boolean = true) {
const p = isXYOffset?this._getPoints():this.points
const len = p.length;
const tension = this.tension;
const firstControlPoints = getControlPoints(
p[len - 2],
p[len - 1],
p[0],
p[1],
p[2],
p[3],
tension
);
const lastControlPoints = getControlPoints(
p[len - 4],
p[len - 3],
p[len - 2],
p[len - 1],
p[0],
p[1],
tension
);
const middle = expandPoints(p, tension);
const tp = [firstControlPoints[2], firstControlPoints[3]]
.concat(middle)
.concat([
lastControlPoints[0],
lastControlPoints[1],
p[len - 2],
p[len - 1],
lastControlPoints[2],
lastControlPoints[3],
firstControlPoints[0],
firstControlPoints[1],
p[0],
p[1],
]);
return tp;
}
private _getPoints():number[]{
return this.points.map((el:number,index:number)=>{
return (index+1)%2 == 0 ?el+this.y:el+this.x;
})
}
override draw(ctx : CanvasRenderingContext2D) {
if (this.visible == false || this.points.length < 4) return;
super.draw(ctx);
ctx.beginPath();
let points = this._getPoints()
let length = points.length
let tension = this.tension
let closed = this.closed
let bezier = this.bezier
let tp = [] as number[]
let len = 0
let n = 0;
if (length==0) {
return;
}
ctx.beginPath();
ctx.moveTo(points[0], points[1]);
if (tension !== 0 && length > 4) {
tp = this.getTensionPoints();
len = tp.length;
n = closed ? 0 : 4;
if (!closed) {
ctx.quadraticCurveTo(tp[0], tp[1], tp[2], tp[3]);
}
while (n < len - 2) {
// #ifdef APP-ANDROID
ctx.bezierCurveTo(
tp[(n++).toInt()],
tp[(n++).toInt()],
tp[(n++).toInt()],
tp[(n++).toInt()],
tp[(n++).toInt()],
tp[(n++).toInt()]
);
// #endif
// #ifndef APP-ANDROID
ctx.bezierCurveTo(
tp[n++],
tp[n++],
tp[n++],
tp[n++],
tp[n++],
tp[n++]
);
// #endif
}
if (!closed) {
ctx.quadraticCurveTo(
tp[len - 2],
tp[len - 1],
points[length - 2],
points[length - 1]
);
}
} else if (bezier) {
n = 2;
while (n < length) {
// #ifdef APP-ANDROID
ctx.bezierCurveTo(
points[(n++).toInt()],
points[(n++).toInt()],
points[(n++).toInt()],
points[(n++).toInt()],
points[(n++).toInt()],
points[(n++).toInt()]
);
// #endif
// #ifndef APP-ANDROID
ctx.bezierCurveTo(
points[n++],
points[n++],
points[n++],
points[n++],
points[n++],
points[n++]
);
// #endif
}
} else {
for (n = 2; n < length; n += 2) {
ctx.lineTo(points[n], points[n + 1]);
}
}
if (closed&&this.fill!='') {
ctx.closePath();
ctx.fill();
} else if(this.stroke!='') {
ctx.stroke();
}
ctx.restore();
}
override isPointInPath(x : number, y : number, shapeId : string) : boolean {
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
const rect = this.getBoundRect()
const realX = x - (this.offsetX) - this.x - this.points[0];
const realY = y - (this.offsetY) - this.y - this.points[1];
if (this.rotation != 0) {
const angle = -this.rotation * Math.PI / 180;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let centerX = 0;
let centerY = 0;
// 根据不同的旋转中心点设置centerX和centerY
switch(this.rotateCenter) {
case 'topLeft':
centerX = 0;
centerY = 0;
break;
case 'topRight':
centerX = this.width;
centerY = 0;
break;
case 'bottomLeft':
centerX = 0;
centerY = this.height;
break;
case 'bottomRight':
centerX = this.width;
centerY = this.height;
break;
case 'center':
default:
centerX = this.width/2;
centerY = this.height/2;
break;
}
const dx = realX - centerX;
const dy = realY - centerY;
const rotatedX = centerX + dx * cos - dy * sin;
const rotatedY = centerY + dx * sin + dy * cos;
return rotatedX >= 0 && rotatedX <= rect.width* this.scaleX && rotatedY >= 0 && rotatedY <= rect.height* this.scaleY;
}
let isInRect = realX >= 0 && realX <= rect.width* this.scaleX && realY >= 0 && realY <= rect.height* this.scaleY;
return isInRect
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,105 @@
import { Shape } from './shape.uts';
import { IShapeBoundRect, IShapeOptional } from '../interface.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
import { generateFrame } from "./qrcode/qrcode.uts"
export class IQrcode extends Shape {
override type = 'IQrcode'
constructor(config : IShapeOptional,canvas:ICanvas) {
super(config,canvas);
}
override getBoundRect() : IShapeBoundRect {
const pad = (this.stroke!='' ? this.strokeWidth/2 : 0);
return {
x: this.x - pad,
y: this.y - pad,
width: this.width + pad*2,
height: this.height + pad*2
} as IShapeBoundRect;
}
setRadius(value : number) : IQrcode {
this.radius = value;
this.needsUpdate = true;
return this;
}
// 根据新高度调整矩形,以左上角 (x,y) 为起点
override setHeight(value : number) : Shape {
const originalY = this.y;
if (value >= 0) {
this.y = originalY;
this.height = value;
} else {
this.y = originalY + value; // value 为负,向上扩展
this.height = Math.abs(value);
}
this.needsUpdate = true;
return this;
}
// 根据新宽度调整矩形,以左上角 (x,y) 为起点
override setWidth(value : number) : Shape {
const originalX = this.x;
if (value >= 0) {
this.x = originalX;
this.width = value;
} else {
this.x = originalX + value; // value 为负,向左扩展
this.width = Math.abs(value);
}
this.needsUpdate = true;
return this;
}
// 设置二维码图片图片。
setQrcodeLabel(text:string){
this.qrcodeText = text;
this.needsUpdate = true;
}
override draw(ctx : CanvasRenderingContext2D) {
if (this.visible == false) return;
super.draw(ctx);
ctx.beginPath();
if (this.radius > 0) {
let radiuss = Math.min(this.radius, this.width / 2, this.height / 2);
const radius = Math.max(radiuss, 0)
ctx.moveTo(this.x + radius, this.y);
ctx.lineTo(this.x + this.width - radius, this.y);
ctx.arcTo(this.x + this.width, this.y, this.x + this.width, this.y + radius, radius);
ctx.lineTo(this.x + this.width, this.y + this.height - radius);
ctx.arcTo(this.x + this.width, this.y + this.height, this.x + this.width - radius, this.y + this.height, radius);
ctx.lineTo(this.x + radius, this.y + this.height);
ctx.arcTo(this.x, this.y + this.height, this.x, this.y + this.height - radius, radius);
ctx.lineTo(this.x, this.y + radius);
ctx.arcTo(this.x, this.y, this.x + radius, this.y, radius);
} else {
ctx.beginPath();
ctx.rect(this.x, this.y, this.width, this.height);
}
ctx.closePath();
if (this.fill != "") {
ctx.fill();
}
if (this.stroke != "") {
ctx.stroke();
}
let fo = generateFrame(this.qrcodeText, "H")
let points = fo.frameBuffer
let width = fo.width
let px = this.width / width
let borderWidth = 0
for (let i = 0; i < width; i++) {
for (let j = 0; j < width; j++) {
if (points[j * width + i] > 0) {
ctx.fillStyle = this.foreground;
ctx.fillRect( borderWidth + px * i + this.x, borderWidth + px * j + this.y, px, px)
}
}
}
ctx.restore()
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,86 @@
import { Shape } from './shape.uts';
import { CanvasRotateCenter, IShapeBoundRect, IShapeOptional, ShapeSetAttrType } from '../interface.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
export class IRect extends Shape {
override type = 'IRect'
constructor(config : IShapeOptional,canvas:ICanvas) {
super(config,canvas);
}
override getBoundRect() : IShapeBoundRect {
const pad = (this.stroke!='' ? this.strokeWidth/2 : 0);
return {
x: this.x - pad,
y: this.y - pad,
width: this.width + pad*2,
height: this.height + pad*2
} as IShapeBoundRect;
}
setRadius(value : number) : IRect {
this.radius = value;
this.needsUpdate = true;
return this;
}
// 根据新高度调整矩形,以左上角 (x,y) 为起点
override setHeight(value : number) : Shape {
const originalY = this.y;
if (value >= 0) {
this.y = originalY;
this.height = value;
} else {
this.y = originalY + value; // value 为负,向上扩展
this.height = Math.abs(value);
}
this.needsUpdate = true;
return this;
}
// 根据新宽度调整矩形,以左上角 (x,y) 为起点
override setWidth(value : number) : Shape {
const originalX = this.x;
if (value >= 0) {
this.x = originalX;
this.width = value;
} else {
this.x = originalX + value; // value 为负,向左扩展
this.width = Math.abs(value);
}
this.needsUpdate = true;
return this;
}
override draw(ctx : CanvasRenderingContext2D) {
if (this.visible == false) return;
super.draw(ctx);
ctx.beginPath();
if (this.radius > 0) {
let radiuss = Math.min(this.radius, this.width / 2, this.height / 2);
const radius = Math.max(radiuss, 0)
ctx.moveTo(this.x + radius, this.y);
ctx.lineTo(this.x + this.width - radius, this.y);
ctx.arcTo(this.x + this.width, this.y, this.x + this.width, this.y + radius, radius);
ctx.lineTo(this.x + this.width, this.y + this.height - radius);
ctx.arcTo(this.x + this.width, this.y + this.height, this.x + this.width - radius, this.y + this.height, radius);
ctx.lineTo(this.x + radius, this.y + this.height);
ctx.arcTo(this.x, this.y + this.height, this.x, this.y + this.height - radius, radius);
ctx.lineTo(this.x, this.y + radius);
ctx.arcTo(this.x, this.y, this.x + radius, this.y, radius);
} else {
ctx.beginPath();
ctx.rect(this.x, this.y, this.width, this.height);
}
ctx.closePath();
if (this.fill != "") {
ctx.fill();
}
if (this.stroke != "") {
ctx.stroke();
}
ctx.restore()
}
}
@@ -0,0 +1,189 @@
import { Shape } from './shape.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
import { IShapeBoundRect, IShapeOptional, IShapeVector2d } from '../interface.uts';
export class IRegularPolygon extends Shape {
override type = 'IRegularPolygon'
constructor(config : IShapeOptional, canvas : ICanvas) {
super(config, canvas);
this.radius = config?.radius ?? 30;
this.sides = config?.sides ?? 3;
}
setRadius(value : number) : IRegularPolygon {
this.radius = value;
this.needsUpdate = true;
return this;
}
setSides(value : number) : IRegularPolygon {
this.sides = value;
this.needsUpdate = true;
return this;
}
override getBoundRect() : IShapeBoundRect {
const points = this.getPointsVec();
let minX = points[0].x;
let maxX = points[0].x;
let minY = points[0].y;
let maxY = points[0].y;
points.forEach((point) => {
minX = Math.min(minX, point.x);
maxX = Math.max(maxX, point.x);
minY = Math.min(minY, point.y);
maxY = Math.max(maxY, point.y);
});
let borderWidth = (this.stroke!='' ? this.strokeWidth/2 : 0)
return {
x: minX-borderWidth,
y: minY-borderWidth,
width: maxX - minX+borderWidth*2,
height: maxY - minY+borderWidth*2,
} as IShapeBoundRect;
}
override setWidth(value : number) : IRegularPolygon {
this.height = value;
this.width = value;
this.radius = value / 2
this.needsUpdate = true;
return this;
}
override setHeight(value : number) : IRegularPolygon {
this.height = value;
this.width = value;
this.radius = value / 2
this.needsUpdate = true;
return this;
}
private getPointsVec() : IShapeVector2d[] {
const sides = this.sides as number;
const radius = this.radius;
const points : IShapeVector2d[] = [];
for (let n = 0; n < sides; n++) {
points.push({
x: this.x + radius * Math.sin((n * 2 * Math.PI) / sides),
y: this.y + (-1 * radius * Math.cos((n * 2 * Math.PI) / sides)),
});
}
return points;
}
override draw(ctx : CanvasRenderingContext2D) {
if (this.visible == false) return;
super.draw(ctx);
ctx.beginPath();
const points = this.getPointsVec();
ctx.moveTo(points[0].x, points[0].y);
for (let n = 1; n < points.length; n++) {
ctx.lineTo(points[n].x, points[n].y);
}
ctx.closePath();
if (this.fill != "") {
ctx.fill();
}
if (this.stroke != "") {
ctx.stroke();
}
ctx.restore();
}
override isPointInPath(x : number, y : number, shapeId : string) : boolean {
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
// 计算相对于图形原点的坐标
let realX = x - this.offsetX - this.x;
let realY = y - this.offsetY - this.y;
// 处理旋转
if (this.rotation != 0) {
const angle = -this.rotation * Math.PI / 180;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let centerX = 0;
let centerY = 0;
// 根据不同的旋转中心点设置centerX和centerY
switch(this.rotateCenter) {
case 'topLeft':
centerX = 0;
centerY = 0;
break;
case 'topRight':
centerX = this.width;
centerY = 0;
break;
case 'bottomLeft':
centerX = 0;
centerY = this.height;
break;
case 'bottomRight':
centerX = this.width;
centerY = this.height;
break;
case 'center':
default:
centerX = this.width/2;
centerY = this.height/2;
break;
}
const dx = realX - centerX;
const dy = realY - centerY;
realX = centerX + dx * cos - dy * sin;
realY = centerY + dx * sin + dy * cos;
}
// 获取多边形的顶点
const points = this.getPointsVec();
const n = points.length;
let inside = false;
// 使用射线法判断点是否在多边形内部
for (let i = 0, j = n - 1; i < n; j = i++) {
const xi = points[i].x - this.x;
const yi = points[i].y - this.y;
const xj = points[j].x - this.x;
const yj = points[j].y - this.y;
if (((yi > realY) != (yj > realY)) &&
(realX < (xj - xi) * (realY - yi) / (yj - yi) + xi)) {
inside = !inside;
}
}
// 如果有描边,检查点是否在描边范围内
if (!inside && this.stroke != "") {
for (let i = 0, j = n - 1; i < n; j = i++) {
const xi = points[i].x - this.x;
const yi = points[i].y - this.y;
const xj = points[j].x - this.x;
const yj = points[j].y - this.y;
// 计算点到线段的距离
const dx = xj - xi;
const dy = yj - yi;
const len = Math.sqrt(dx * dx + dy * dy);
if (len > 0) {
const t = ((realX - xi) * dx + (realY - yi) * dy) / (len * len);
if (t >= 0 && t <= 1) {
const px = xi + t * dx;
const py = yi + t * dy;
const distance = Math.sqrt((realX - px) * (realX - px) + (realY - py) * (realY - py));
if (distance <= this.strokeWidth / 2) {
return true;
}
}
}
}
}
return inside;
}
}
+153
View File
@@ -0,0 +1,153 @@
import { Shape } from './shape.uts';
import { CanvasRotateCenter, ShapeSetAttrType, IShapeBoundRect, IShapeOptional } from '../interface.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
export class IRing extends Shape {
override type = 'IRing'
constructor(config : IShapeOptional,canvas:ICanvas) {
super(config,canvas);
this.width = this.outerRadius * 2
this.height = this.outerRadius * 2
}
setInnerRadius(value : number) : IRing {
this.innerRadius = value;
this.needsUpdate = true;
return this;
}
setOuterRadius(value : number) : IRing {
this.outerRadius = value;
this.needsUpdate = true;
return this;
}
override getBoundRect() : IShapeBoundRect {
const r = Math.max(0, this.outerRadius);
const pad = (this.stroke != "" ? this.strokeWidth / 2 : 0);
const d = r * 2 + pad * 2;
return {
x: this.x - r - pad,
y: this.y - r - pad,
width: d,
height: d
} as IShapeBoundRect;
}
override setWidth(value : number) : IRing {
this.height = value;
this.width = value;
this.outerRadius = value / 2
this.needsUpdate = true;
return this;
}
override setHeight(value : number) : IRing {
this.height = value;
this.width = value;
this.outerRadius = value / 2
this.needsUpdate = true;
return this;
}
override draw(ctx : CanvasRenderingContext2D) {
if (this.visible == false) return;
super.draw(ctx);
ctx.beginPath();
// 将角度转换为弧度
const startRad = this.startAngle * Math.PI / 180;
const endRad = this.endAngle * Math.PI / 180;
// 绘制外圆弧
ctx.arc(this.x, this.y, this.outerRadius, startRad, endRad, false);
// 绘制内圆弧(反方向)
ctx.arc(this.x, this.y, this.innerRadius, endRad, startRad, true);
ctx.closePath();
if (this.fill != "") {
ctx.fill();
}
if (this.stroke != "") {
ctx.stroke();
}
ctx.restore();
}
override isPointInPath(x : number, y : number, shapeId : string) : boolean {
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
// 计算点击位置相对于圆心的实际坐标
let realX = x - this.offsetX - this.x;
let realY = y - this.offsetY - this.y;
// 如果有旋转,需要将坐标转换回未旋转状态
if (this.rotation != 0) {
const angle = -this.rotation * Math.PI / 180;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let centerX = 0;
let centerY = 0;
// 根据不同的旋转中心点设置centerX和centerY
switch(this.rotateCenter) {
case 'topLeft':
centerX = 0;
centerY = 0;
break;
case 'topRight':
centerX = this.width;
centerY = 0;
break;
case 'bottomLeft':
centerX = 0;
centerY = this.height;
break;
case 'bottomRight':
centerX = this.width;
centerY = this.height;
break;
case 'center':
default:
centerX = this.width/2;
centerY = this.height/2;
break;
}
const dx = realX - centerX;
const dy = realY - centerY;
realX = centerX + dx * cos - dy * sin;
realY = centerY + dx * sin + dy * cos;
}
// 计算点到圆心的距离
const distance = Math.sqrt(realX * realX + realY * realY);
// 考虑缩放因素
const scaledInnerRadius = this.innerRadius * Math.min(this.scaleX, this.scaleY);
const scaledOuterRadius = this.outerRadius * Math.min(this.scaleX, this.scaleY);
// 检查距离是否在内径和外径之间
if (distance < scaledInnerRadius || distance > scaledOuterRadius) {
return false;
}
// 计算点击位置的角度(弧度)
let angle = Math.atan2(realY, realX);
// 将角度转换为0-2π范围
if (angle < 0) angle += 2 * Math.PI;
// 将角度转换为度数
angle = angle * 180 / Math.PI;
// 将起始角度和结束角度标准化到0-360度范围
let start = this.startAngle % 360;
if (start < 0) start += 360;
let end = this.endAngle % 360;
if (end < 0) end += 360;
// 处理跨越0度线的情况
if (start <= end) {
return angle >= start && angle <= end;
} else {
return angle >= start || angle <= end;
}
}
}
@@ -0,0 +1,184 @@
import { Shape } from './shape.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
import { IShapeBoundRect, IShapeOptional } from '../interface.uts';
export class ISector extends Shape {
constructor(config : IShapeOptional,canvas:ICanvas) {
super(config,canvas);
this.radius = config?.radius ?? 30;
this.startAngle = config?.startAngle ?? 0;
this.endAngle = config?.endAngle ?? 90;
this.type = "ISector"
}
setRadius(value : number) : ISector {
this.radius = value;
this.needsUpdate = true;
return this;
}
override getBoundRect() : IShapeBoundRect {
// 与 IArc 相同的严谨包围盒逻辑:扇形需包含圆心
const centerX = this.x;
const centerY = this.y;
const r = Math.max(0, this.radius);
const d2r = Math.PI / 180;
const normalize = (deg:number):number => { let a = deg % 360; if (a < 0) a += 360; return a; };
let start = normalize(this.startAngle);
let end = normalize(this.endAngle);
if (end < start) end += 360;
const withinSweep = (deg:number):boolean => { let a = normalize(deg); if (a < start) a += 360; return a >= start && a <= end; };
const candidatesX:number[] = [centerX];
const candidatesY:number[] = [centerY];
const sx = centerX + r * Math.cos(start * d2r);
const sy = centerY + r * Math.sin(start * d2r);
const ex = centerX + r * Math.cos(end * d2r);
const ey = centerY + r * Math.sin(end * d2r);
candidatesX.push(sx, ex);
candidatesY.push(sy, ey);
const extrema = [0, 90, 180, 270];
for (const deg of extrema) {
if (withinSweep(deg)) {
const rad = deg * d2r;
candidatesX.push(centerX + r * Math.cos(rad));
candidatesY.push(centerY + r * Math.sin(rad));
}
}
let minX = candidatesX.length>0?candidatesX[0]:centerX;
let maxX = candidatesX.length>0?candidatesX[0]:centerX;
let minY = candidatesY.length>0?candidatesY[0]:centerY;
let maxY = candidatesY.length>0?candidatesY[0]:centerY;
for (let i=1;i<candidatesX.length;i++) {
const vx = candidatesX[i];
if (!isNaN(vx)) {
if (vx < minX) minX = vx;
if (vx > maxX) maxX = vx;
}
}
for (let i=1;i<candidatesY.length;i++) {
const vy = candidatesY[i];
if (!isNaN(vy)) {
if (vy < minY) minY = vy;
if (vy > maxY) maxY = vy;
}
}
const pad = (this.stroke != "" ? this.strokeWidth / 2 : 0);
minX -= pad; minY -= pad; maxX += pad; maxY += pad;
return { x: minX, y: minY, width: Math.max(0, maxX - minX), height: Math.max(0, maxY - minY) } as IShapeBoundRect;
}
override setWidth(value : number) : ISector {
this.height = value;
this.width = value;
this.radius = value / 2
this.needsUpdate = true;
return this;
}
override setHeight(value : number) : ISector {
this.height = value;
this.width = value;
this.radius = value / 2
this.needsUpdate = true;
return this;
}
override draw(ctx : CanvasRenderingContext2D) {
if (this.visible == false) return;
super.draw(ctx);
ctx.beginPath();
// 将角度转换为弧度
const startRad = this.startAngle * Math.PI / 180;
const endRad = this.endAngle * Math.PI / 180;
// 如果有填充色,先移动到圆心,绘制扇形
if (this.fill !== "") {
ctx.moveTo(this.x, this.y);
}
// 绘制圆弧
ctx.arc(this.x, this.y, this.radius, startRad, endRad, false);
// 如果有填充色,连接回圆心形成扇形
if (this.fill != "") {
ctx.lineTo(this.x, this.y);
}
ctx.closePath();
if (this.fill != "") {
ctx.fill();
}
if (this.stroke != "") {
ctx.stroke();
}
ctx.restore();
}
override isPointInPath(x: number, y: number, shapeId: string): boolean {
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
// 计算实际点击位置(考虑偏移量)
const realX = x - (this.offsetX) - this.x;
const realY = y - (this.offsetY) - this.y;
// 如果有旋转,先进行反向旋转变换
let finalX = realX;
let finalY = realY;
if (this.rotation != 0) {
const angle = -this.rotation * Math.PI / 180;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let centerX = 0;
let centerY = 0;
// 根据不同的旋转中心点设置centerX和centerY
switch(this.rotateCenter) {
case 'topLeft':
centerX = 0;
centerY = 0;
break;
case 'topRight':
centerX = this.width;
centerY = 0;
break;
case 'bottomLeft':
centerX = 0;
centerY = this.height;
break;
case 'bottomRight':
centerX = this.width;
centerY = this.height;
break;
case 'center':
default:
centerX = this.width/2;
centerY = this.height/2;
break;
}
const dx = realX - centerX;
const dy = realY - centerY;
finalX = centerX + dx * cos - dy * sin;
finalY = centerY + dx * sin + dy * cos;
}
// 计算点到圆心的距离
const distance = Math.sqrt(finalX * finalX + finalY * finalY);
// 如果点击位置超出半径范围,则不在扇形内
if (distance > this.radius * this.scaleX) return false;
// 计算点击位置的角度(弧度)
let angle = Math.atan2(finalY, finalX) * 180 / Math.PI;
// 将角度转换为0-360度范围
angle = (angle + 360) % 360;
// 将起始角度和结束角度标准化到0-360度范围
let start = (this.startAngle + 360) % 360;
let end = (this.endAngle + 360) % 360;
// 处理跨越0度线的情况
if (end < start) {
return angle >= start || angle <= end;
}
// 判断点击角度是否在扇形角度范围内
return angle >= start && angle <= end;
}
}
@@ -0,0 +1,857 @@
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
// import { Path2DShape } from '@/uni_modules/tmx-ui/core/canvas/lib/path2d.uts';
import {
IShape, IShapeVector2d,
ILineJoinType, ILineCapType,
IShapeOptional, IShapeBoundRect, ICanvasEvent, CanvasEventType, IEventShapeListener, IEventShape, CanvasRotateCenter, ShapeSetAttrType, ITextAlignType, ITextBaselineType
} from '../interface.uts';
// #ifdef APP-ANDROID
import { IArc } from './arc.uts';
import { ICircle } from './circle.uts';
import { IEllipse } from './ellipse.uts';
import { ImageShape } from './image.uts';
import { ILine } from './line.uts';
import { ILinePolygon } from './linePolygon.uts';
import { Path2DShape } from './path2d.uts';
import { IRect } from './rect.uts';
import { IRegularPolygon } from './regularPolygon.uts';
import { IRing } from './ring.uts';
import { ISector } from './sector.uts';
import { IStar } from './star.uts';
import { IText } from './text.uts';
// #endif
type CloneCallActions = (el:Shape,index:number) => void
export class Shape implements IShape {
canvas : ICanvas
x : number = 0;
y : number = 0;
width : number = 0;
height : number = 0;
fill : string = "";
stroke : string = "";
strokeWidth : number = 0;
opacity : number = 1;
visible : boolean = true;
rotation : number = 0;
scaleX : number = 1;
scaleY : number = 1;
offsetX : number = 0;
offsetY : number = 0;
draggable : boolean = false;
draggableing : boolean = false;
// 元素之间上下重叠时,是否允许穿透冒泡逐层触发,默认不允许
bubbleEvent = false;
toggleStatus = false;
// 线条样式
lineJoin : ILineJoinType = "round";
lineDashOffset : number = 0;
lineDash : number[] = []
lineCap : ILineCapType = "butt";
text : string = "";
fontSize : number = 12;
fontFamily : string = "Arial";
textAlign : 'left' | 'center' | 'right' = 'left';
textBaseline : 'top' | 'middle' | 'bottom' = 'top';
padding : number = 0;
lineHeight : number = 1.2;
innerRadius : number = 10;
outerRadius : number = 20;
startAngle : number = 0;
endAngle : number = 360;
src:string = "";
foreground:string = 'rgba(0,0,0,1)'
qrcodeText:string = 'TMUI4x Great'
radius : number = 0;
sides : number = 3;
points : number[] = [];
//多边形线是否自动闭合
closed : boolean = true;
tension : number = 0
bezier : boolean = false;
radiusX : number = 20;
radiusY : number = 80;
//星的角数量,默认5角星
numPoints : number = 5
pointStart : IShapeVector2d = { x: 0, y: 0 };
pointEnd : IShapeVector2d = { x: 0, y: 0 };
clip : boolean = false;
zIndex : number = 0;
textBgColor : string = '';
id : string = "shape-" + (Math.random()).toString(8).substring(4, 20)
type : string = ''
/** 旋转元素时的中心点,默认是topLeft左顶,center表示元素的中间 */
rotateCenter : CanvasRotateCenter = "topLeft"
strokeGradient = [] as string[]
fillGradient = [] as string[]
needsUpdate = false
private toggleListener = (status : boolean, target : any) => { }
private eventListeners : Map<CanvasEventType, IEventShapeListener[]> = new Map();
drag : (eventName : CanvasEventType, parentEventDetail : ICanvasEvent) => void = (eventName : CanvasEventType, parentEventDetail : ICanvasEvent) => {
if (!this.draggable) return
this.x -= parentEventDetail.detail[0].moveLenX
this.y -= parentEventDetail.detail[0].moveLenY
}
constructor(config : IShapeOptional, canvas : ICanvas) {
this.canvas = canvas
this.x = config?.x ?? this.x;
this.y = config?.y ?? this.y;
this.width = config?.width ?? this.width;
this.height = config?.height ?? this.height;
this.fill = config?.fill ?? this.fill;
this.stroke = config?.stroke ?? this.stroke;
this.strokeWidth = config?.strokeWidth ?? this.strokeWidth;
this.opacity = config?.opacity ?? this.opacity;
this.visible = config?.visible ?? this.visible;
this.rotation = config?.rotation ?? this.rotation;
this.scaleX = config?.scaleX ?? this.scaleX;
this.scaleY = config?.scaleY ?? this.scaleY;
this.offsetX = config?.offsetX ?? this.offsetX;
this.offsetY = config?.offsetY ?? this.offsetY;
this.draggable = config?.draggable ?? this.draggable;
this.bubbleEvent = config?.bubbleEvent ?? this.bubbleEvent;
this.rotateCenter = config?.rotateCenter ?? this.rotateCenter;
this.lineJoin = config?.lineJoin ?? this.lineJoin;
this.lineDashOffset = config?.lineDashOffset ?? this.lineDashOffset;
this.lineDash = config?.lineDash ?? this.lineDash;
this.lineCap = config?.lineCap ?? this.lineCap;
this.text = config?.text ?? this.text;
this.fontSize = config?.fontSize ?? this.fontSize;
this.fontFamily = config?.fontFamily ?? this.fontFamily;
this.textAlign = config?.textAlign ?? this.textAlign;
this.textBaseline = config?.textBaseline ?? this.textBaseline;
this.padding = config?.padding ?? this.padding;
this.lineHeight = config?.lineHeight ?? this.lineHeight;
this.innerRadius = config?.innerRadius ?? this.innerRadius;
this.outerRadius = config?.outerRadius ?? this.outerRadius;
this.startAngle = config?.startAngle ?? this.startAngle;
this.endAngle = config?.endAngle ?? this.endAngle;
this.radius = config?.radius ?? this.radius;
this.sides = config?.sides ?? this.sides;
this.points = config?.points ?? this.points;
this.closed = config?.closed ?? this.closed;
this.tension = config?.tension ?? this.tension;
this.bezier = config?.bezier ?? this.bezier;
this.src = config?.src ?? this.src;
this.foreground = config?.foreground ?? this.foreground;
this.qrcodeText = config?.qrcodeText ?? this.qrcodeText;
this.radiusX = config?.radiusX ?? this.radiusX;
this.radiusY = config?.radiusY ?? this.radiusY;
this.numPoints = config?.numPoints ?? this.numPoints;
this.pointStart = config?.pointStart ?? this.pointStart;
this.pointEnd = config?.pointEnd ?? this.pointEnd;
this.strokeGradient = config?.strokeGradient ?? this.strokeGradient;
this.fillGradient = config?.fillGradient ?? this.fillGradient;
this.clip = config?.clip ?? this.clip;
this.zIndex = config?.zIndex ?? this.zIndex;
this.textBgColor = config?.textBgColor ?? this.textBgColor;
}
draw(ctx : CanvasRenderingContext2D) : void {
if (this.visible == false) return;
ctx.save();
ctx.globalAlpha = this.opacity;
ctx.translate(this.x + this.offsetX, this.y + this.offsetY);
if (this.rotation != 0) {
if (this.rotateCenter == 'topLeft') {
ctx.rotate(this.rotation * Math.PI / 180);
} else if (this.rotateCenter == 'center') {
ctx.translate(this.width / 2, this.height / 2);
ctx.rotate(this.rotation * Math.PI / 180);
ctx.translate(-this.width / 2, -this.height / 2);
} else if (this.rotateCenter == 'topRight') {
ctx.translate(this.width, 0);
ctx.rotate(this.rotation * Math.PI / 180);
ctx.translate(-this.width, 0);
} else if (this.rotateCenter == 'bottomLeft') {
ctx.translate(0, this.height);
ctx.rotate(this.rotation * Math.PI / 180);
ctx.translate(0, -this.height);
} else if (this.rotateCenter == 'bottomRight') {
ctx.translate(this.width, this.height);
ctx.rotate(this.rotation * Math.PI / 180);
ctx.translate(-this.width, -this.height);
}
}
if (this.scaleX != 1 || this.scaleY != 1) {
if (this.rotateCenter == 'topLeft') {
ctx.scale(this.scaleX, this.scaleY);
} else if (this.rotateCenter == 'center') {
ctx.translate(this.width / 2, this.height / 2);
ctx.scale(this.scaleX, this.scaleY);
ctx.translate(-this.width / 2, -this.height / 2);
} else if (this.rotateCenter == 'topRight') {
ctx.translate(this.width, 0);
ctx.scale(this.scaleX, this.scaleY);
ctx.translate(-this.width, 0);
} else if (this.rotateCenter == 'bottomLeft') {
ctx.translate(0, this.height);
ctx.scale(this.scaleX, this.scaleY);
ctx.translate(0, -this.height);
} else if (this.rotateCenter == 'bottomRight') {
ctx.translate(this.width, this.height);
ctx.scale(this.scaleX, this.scaleY);
ctx.translate(-this.width, -this.height);
}
}
ctx.translate(-this.x, -this.y);
if (this.clip) {
ctx.clip()
}
if (this.fill != "" || this.fillGradient.length > 0) {
if (this.fillGradient.length > 0) {
const gradient = this.createLinearGradient(ctx, this.fillGradient);
ctx.fillStyle = gradient;
} else if (this.fill != "") {
ctx.fillStyle = this.fill;
}
}
if (this.stroke != "" || this.strokeGradient.length > 0) {
if (this.strokeGradient.length > 0) {
const gradient = this.createLinearGradient(ctx, this.strokeGradient);
ctx.strokeStyle = gradient;
} else {
ctx.strokeStyle = this.stroke
}
ctx.lineWidth = this.strokeWidth;
ctx.lineJoin = this.lineJoin;
ctx.lineCap = this.lineCap;
ctx.lineDashOffset = this.lineDashOffset;
if (this.lineDash.length > 0) {
ctx.setLineDash(this.lineDash)
}
}
}
isPointInPath(x : number, y : number, shapeId : string) : boolean {
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
const realX = x - (this.offsetX) - this.x;
const realY = y - (this.offsetY) - this.y;
if (this.rotation != 0) {
const angle = -this.rotation * Math.PI / 180;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let centerX = 0;
let centerY = 0;
if (this.rotateCenter == 'topLeft') {
centerX = 0;
centerY = 0;
} else if (this.rotateCenter == 'center') {
centerX = this.width / 2;
centerY = this.height / 2;
} else if (this.rotateCenter == 'topRight') {
centerX = this.width;
centerY = 0;
} else if (this.rotateCenter == 'bottomLeft') {
centerX = 0;
centerY = this.height;
} else if (this.rotateCenter == 'bottomRight') {
centerX = this.width;
centerY = this.height;
}
const dx = realX - centerX;
const dy = realY - centerY;
const rotatedX = centerX + dx * cos - dy * sin;
const rotatedY = centerY + dx * sin + dy * cos;
return rotatedX >= 0 && rotatedX <= this.width * this.scaleX && rotatedY >= 0 && rotatedY <= this.height * this.scaleY;
}
return realX >= 0 && realX <= this.width * this.scaleX &&
realY >= 0 && realY <= this.height * this.scaleY;
}
addEventListener(eventName : CanvasEventType, listener : IEventShapeListener) : Shape {
if (!this.eventListeners.has(eventName)) {
this.eventListeners.set(eventName, []);
}
this.eventListeners.get(eventName)!.push(listener);
return this;
}
removeEventListener(eventName : CanvasEventType, listener : IEventShapeListener) : Shape {
const listeners = this.eventListeners.get(eventName);
if (listeners == null) return this;
const index = listeners.indexOf(listener);
if (index != -1) {
listeners.splice(index, 1);
}
return this;
}
buildEvents(eventName : CanvasEventType, parentEventDetail : ICanvasEvent) {
const listeners = this.eventListeners.get(eventName);
if (eventName == 'click') {
this.toggleStatus = !this.toggleStatus
this.toggleListener(this.toggleStatus, this)
}
if (listeners == null) return;
let layerX = parentEventDetail.x - ((this.offsetX * this.scaleX) + this.x);
let layerY = parentEventDetail.y - ((this.offsetY * this.scaleY) + this.y);
const events = {
type: eventName,
x: parentEventDetail.x,
y: parentEventDetail.y,
/** 元素本身内的坐标X */
layerX: layerX,
/** 元素本身内的坐标Y */
layerY: layerY,
target: this,
touches: parentEventDetail.touches.map((el : ICanvasEvent) : IEventShape => {
let layerX_self = el.x - ((this.offsetX * this.scaleX) + this.x);
let layerY_self = el.y - ((this.offsetY * this.scaleY) + this.y);
return {
touches: [] as IEventShape[],
type: eventName,
x: el.x,
y: el.y,
layerX: layerX_self,
layerY: layerY_self,
target: this
} as IEventShape
})
} as IEventShape
for (let i = 0; i < listeners.length; i++) {
let children = listeners[i]
//执行画布的监听事件
children(events);
}
}
setRotateCenter(center : CanvasRotateCenter) : Shape {
this.rotateCenter = center;
return this;
}
setX(value : number) : Shape {
this.x = value;
this.needsUpdate = true;
return this;
}
setY(value : number) : Shape {
this.y = value;
this.needsUpdate = true;
return this;
}
setWidth(value : number) : Shape {
if(
this.type=='ILinePolygon'||
this.type=='ILayout'||
this.type=='Path2DShape'||
this.type=='IRegularPolygon'
){
console.warn(`所在图形:${this.type}不支持本方法`)
return this;
}
this.width = value;
this.needsUpdate = true;
return this;
}
setHeight(value : number) : Shape {
if(
this.type=='ILinePolygon'||
this.type=='ILayout'||
this.type=='Path2DShape'||
this.type=='IRegularPolygon'
){
console.warn(`所在图形:${this.type}不支持本方法`)
return this;
}
this.height = value;
this.needsUpdate = true;
return this;
}
setFill(value : string) : Shape {
this.fill = value;
this.needsUpdate = true;
return this;
}
setStroke(value : string) : Shape {
this.stroke = value;
this.needsUpdate = true;
return this;
}
setStrokeWidth(value : number) : Shape {
this.strokeWidth = value;
this.needsUpdate = true;
return this;
}
setOpacity(value : number) : Shape {
this.opacity = value;
this.needsUpdate = true;
return this;
}
setVisible(value : boolean) : Shape {
this.visible = value;
this.needsUpdate = true;
return this;
}
setRotation(value : number) : Shape {
this.rotation = value;
this.needsUpdate = true;
return this;
}
setScaleX(value : number) : Shape {
this.scaleX = value;
this.needsUpdate = true;
return this;
}
setScaleY(value : number) : Shape {
this.scaleY = value;
this.needsUpdate = true;
return this;
}
setOffsetX(value : number) : Shape {
this.offsetX = value;
this.needsUpdate = true;
return this;
}
setOffsetY(value : number) : Shape {
this.offsetY = value;
this.needsUpdate = true;
return this;
}
setLineDash(value : number[]) : Shape {
this.lineDash = value;
this.needsUpdate = true;
return this;
}
setDraggable(value : boolean) : Shape {
this.draggable = value;
this.needsUpdate = true;
return this;
}
setBubbleEvent(value : boolean) : Shape {
this.bubbleEvent = value;
this.needsUpdate = true;
return this;
}
setZindex(value : number) : Shape {
this.zIndex = value;
this.needsUpdate = true;
return this;
}
setClip(value : boolean) : Shape {
this.clip = value;
this.needsUpdate = true;
return this;
}
setTextBgColor(value : string) : Shape {
this.textBgColor = value;
this.needsUpdate = true;
return this;
}
setAttr(key : string, value : any) {
this.needsUpdate = true;
switch (key) {
case 'x': { this.x = value as number; break; }
case 'y': { this.y = value as number; break; }
case 'width': { this.width = value as number; break; }
case 'height': { this.height = value as number; break; }
case 'fill': { this.fill = value as string; break; }
case 'stroke': { this.stroke = value as string; break; }
case 'strokeWidth': { this.strokeWidth = value as number; break; }
case 'opacity': { this.opacity = value as number; break; }
case 'visible': { this.visible = value as boolean; break; }
case 'rotation': { this.rotation = value as number; break; }
case 'scaleX': { this.scaleX = value as number; break; }
case 'scaleY': { this.scaleY = value as number; break; }
case 'offsetX': { this.offsetX = value as number; break; }
case 'offsetY': { this.offsetY = value as number; break; }
case 'draggable': { this.draggable = value as boolean; break; }
case 'bubbleEvent': { this.bubbleEvent = value as boolean; break; }
case 'lineJoin': { this.lineJoin = value as ILineJoinType; break; }
case 'lineDashOffset': { this.lineDashOffset = value as number; break; }
case 'lineDash': { this.lineDash = value as number[]; break; }
case 'lineCap': { this.lineCap = value as ILineCapType; break; }
case 'text': this.text = value as string;
case 'src': this.src = value as string;
case 'fontSize': this.fontSize = value as number;
case 'fontFamily': this.fontFamily = value as string;
case 'textAlign': this.textAlign = value as ITextAlignType;
case 'textBaseline': this.textBaseline = value as ITextBaselineType;
case 'padding': this.padding = value as number;
case 'lineHeight': this.lineHeight = value as number;
case 'innerRadius': { this.innerRadius = value as number; break; }
case 'outerRadius': { this.outerRadius = value as number; break; }
case 'startAngle': { this.startAngle = value as number; break; }
case 'endAngle': { this.endAngle = value as number; break; }
case 'radius': { this.radius = value as number; break; }
case 'sides': { this.sides = value as number; break; }
case 'points': { this.points = value as number[]; break; }
case 'closed': { this.closed = value as boolean; break; }
case 'tension': { this.tension = value as number; break; }
case 'bezier': { this.bezier = value as boolean; break; }
case 'radiusX': { this.radiusX = value as number; break; }
case 'radiusY': { this.radiusY = value as number; break; }
case 'numPoints': { this.numPoints = value as number; break; }
case 'pointStart': { this.pointStart = value as IShapeVector2d; break; }
case 'pointEnd': { this.pointEnd = value as IShapeVector2d; break; }
case 'clip': { this.clip = value as boolean; break; }
case 'zIndex': { this.zIndex = value as number; break; }
case 'textBgColor': { this.textBgColor = value as string; break; }
case 'foreground': { this.foreground = value as string; break; }
case 'qrcodeText': { this.qrcodeText = value as string; break; }
default: return;
}
}
getAttr(key : string) : any | null {
switch (key) {
case 'x': return this.x;
case 'y': return this.y;
case 'src': return this.src;
case 'width': return this.width;
case 'height': return this.height;
case 'fill': return this.fill;
case 'stroke': return this.stroke;
case 'strokeWidth': return this.strokeWidth;
case 'opacity': return this.opacity;
case 'visible': return this.visible;
case 'rotation': return this.rotation;
case 'scaleX': return this.scaleX;
case 'scaleY': return this.scaleY;
case 'offsetX': return this.offsetX;
case 'offsetY': return this.offsetY;
case 'draggable': return this.draggable;
case 'bubbleEvent': return this.bubbleEvent;
case 'lineJoin': return this.lineJoin;
case 'lineDashOffset': return this.lineDashOffset;
case 'lineDash': return this.lineDash;
case 'lineCap': return this.lineCap;
case 'text': return this.text;
case 'fontSize': return this.fontSize;
case 'fontFamily': return this.fontFamily;
case 'textAlign': return this.textAlign;
case 'textBaseline': return this.textBaseline;
case 'padding': return this.padding;
case 'lineHeight': return this.lineHeight;
case 'innerRadius': return this.innerRadius;
case 'outerRadius': return this.outerRadius;
case 'startAngle': return this.startAngle;
case 'endAngle': return this.endAngle;
case 'radius': return this.radius;
case 'sides': return this.sides;
case 'closed': return this.closed;
case 'points': return this.points;
case 'tension': return this.tension;
case 'bezier': return this.bezier;
case 'radiusX': return this.radiusX;
case 'radiusY': return this.radiusY;
case 'numPoints': return this.numPoints;
case 'pointStart': return this.pointStart;
case 'pointEnd': return this.pointEnd;
case 'clip': return this.pointEnd;
case 'zIndex': return this.zIndex;
case 'textBgColor': return this.textBgColor;
case 'foreground': return this.foreground;
case 'qrcodeText': return this.qrcodeText;
default: return null;
}
}
getBoundRect() : IShapeBoundRect {
return {
x: this.x,
y: this.y,
width: this.width,
height: this.height
} as IShapeBoundRect;
}
toggle(call : (status : boolean, target : any) => void) : void {
this.toggleListener = call;
}
update() : Shape {
if (!this.needsUpdate) return this;
this.needsUpdate = false;
return this;
}
/**
* 克隆当前图形
* @param count 要克隆的数量,默认 1
* @returns 克隆出来的新图形数组(长度为 count)
*/
clone(count : number = 1,callActons:CloneCallActions|null = null) : Shape[] {
const clones : Shape[] = [];
const baseConfig = {
x: this.x,
y: this.y,
width: this.width,
height: this.height,
fill: this.fill,
stroke: this.stroke,
strokeWidth: this.strokeWidth,
opacity: this.opacity,
visible: this.visible,
rotation: this.rotation,
scaleX: this.scaleX,
scaleY: this.scaleY,
offsetX: this.offsetX,
offsetY: this.offsetY,
draggable: this.draggable,
bubbleEvent: this.bubbleEvent,
rotateCenter: this.rotateCenter,
lineJoin: this.lineJoin,
lineDashOffset: this.lineDashOffset,
lineDash: this.lineDash.slice(),
lineCap: this.lineCap,
text: this.text,
fontSize: this.fontSize,
fontFamily: this.fontFamily,
textAlign: this.textAlign,
textBaseline: this.textBaseline,
padding: this.padding,
lineHeight: this.lineHeight,
innerRadius: this.innerRadius,
outerRadius: this.outerRadius,
startAngle: this.startAngle,
endAngle: this.endAngle,
radius: this.radius,
sides: this.sides,
points: this.points.slice(),
closed: this.closed,
tension: this.tension,
bezier: this.bezier,
radiusX: this.radiusX,
radiusY: this.radiusY,
numPoints: this.numPoints,
pointStart: { x: this.pointStart.x, y: this.pointStart.y },
pointEnd: { x: this.pointEnd.x, y: this.pointEnd.y },
strokeGradient: this.strokeGradient.slice(),
fillGradient: this.fillGradient.slice(),
clip: this.clip,
zIndex: this.zIndex,
textBgColor: this.textBgColor,
src: this.src,
} as IShapeOptional;
for (let i = 0; i < Math.max(1, count); i++) {
let shape:any|null = null;
// #ifdef APP-ANDROID
switch(this.type){
case 'IArc':{
shape = new IArc(baseConfig,this.canvas)
break;
}
case 'ICircle':{
shape = new ICircle(baseConfig,this.canvas)
break;
}
case 'IEllipse':{
shape = new IEllipse(baseConfig,this.canvas)
break;
}
case 'ImageShape':{
shape = new ImageShape(baseConfig,this.canvas)
break;
}
case 'ILine':{
shape = new ILine(baseConfig,this.canvas)
break;
}
case 'ILinePolygon':{
shape = new ILinePolygon(baseConfig,this.canvas)
break;
}
case 'Path2DShape':{
shape = new Path2DShape(baseConfig,this.canvas)
break;
}
case 'IRect':{
shape = new IRect(baseConfig,this.canvas)
break;
}
case 'IRegularPolygon':{
shape = new IRegularPolygon(baseConfig,this.canvas)
break;
}
case 'IRing':{
shape = new IRing(baseConfig,this.canvas)
break;
}
case 'ISector':{
shape = new ISector(baseConfig,this.canvas)
break;
}
case 'IStar':{
shape = new IStar(baseConfig,this.canvas)
break;
}
case 'IText':{
shape = new IText(baseConfig,this.canvas)
break;
}
default:{
break;
}
}
// #endif
// #ifndef APP-ANDROID
const Ctor = (this as any).constructor;
shape = new Ctor(baseConfig,this.canvas) as any
// #endif
if(shape!=null){
const cloned : Shape = shape! as Shape;
cloned.needsUpdate = true;
if(callActons!=null){
callActons!(cloned!,i)
}
clones.push(cloned);
}
}
return clones;
}
private createLinearGradient(ctx : CanvasRenderingContext2D, gradientArray : string[]) : CanvasGradient {
if (gradientArray.length < 2) return ctx.createLinearGradient(0, 0, 0, 0);
// 解析角度
let angle = 0;
const firstItem = gradientArray[0];
if (firstItem.includes('deg')) {
angle = parseFloat(firstItem);
gradientArray = gradientArray.slice(1);
}
// 计算渐变的起点和终点
const radian = angle * Math.PI / 180;
const length = Math.max(this.width, this.height);
let startX = this.x;
let startY = this.y;
let endX = this.x;
let endY = this.y;
if (this.type == 'ICircle') {
// 对于圆形,使用半径计算
const dx = Math.cos(radian) * this.radius;
const dy = Math.sin(radian) * this.radius;
startX = this.x - dx;
startY = this.y - dy;
endX = this.x + dx;
endY = this.y + dy;
} else if (this.type == 'IEllipse') {
// 对于椭圆,使用radiusX和radiusY计算
const dx = Math.cos(radian) * this.radiusX;
const dy = Math.sin(radian) * this.radiusY;
startX = this.x - dx;
startY = this.y - dy;
endX = this.x + dx;
endY = this.y + dy;
} else if (this.type == 'IStar') {
// 对于星形,使用外接圆半径计算
const radius = this.outerRadius;
const dx = Math.cos(radian) * radius;
const dy = Math.sin(radian) * radius;
startX = this.x - dx;
startY = this.y - dy;
endX = this.x + dx;
endY = this.y + dy;
} else if (this.type == 'IText') {
// 对于文本,使用文本框的宽高计算
const dx = Math.cos(radian) * this.width;
const dy = Math.sin(radian) * this.height;
endX = startX + dx;
endY = startY + dy;
} else if (this.type == 'IArc' || this.type == 'ISector') {
// 对于圆弧和扇形,使用半径计算
const dx = Math.cos(radian) * this.radius;
const dy = Math.sin(radian) * this.radius;
startX = this.x - dx;
startY = this.y - dy;
endX = this.x + dx;
endY = this.y + dy;
} else {
// 默认使用矩形的宽高计算
const dx = Math.cos(radian) * length;
const dy = Math.sin(radian) * length;
endX = startX + dx;
endY = startY + dy;
}
const gradient = ctx.createLinearGradient(startX, startY, endX, endY);
// 添加渐变色停止点
gradientArray.forEach(item => {
const ar = item.split(' ');
const color = ar[0];
const stop = ar[1];
const offset = parseFloat(stop) / 100;
gradient.addColorStop(offset, color);
});
return gradient;
}
}
+170
View File
@@ -0,0 +1,170 @@
import { Shape } from './shape.uts';
import { CanvasRotateCenter, IShapeBoundRect, IShapeOptional } from '../interface.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
export class IStar extends Shape {
override type = 'IStar'
constructor(config: IShapeOptional, canvas: ICanvas) {
super(config, canvas);
this.numPoints = config?.numPoints ?? 5;
this.innerRadius = config?.innerRadius ?? 16;
this.outerRadius = config?.outerRadius ?? 30;
this.width = this.outerRadius * 2;
this.height = this.outerRadius * 2;
}
setInnerRadius(value: number): IStar {
this.innerRadius = value;
this.needsUpdate = true;
return this;
}
setOuterRadius(value: number): IStar {
this.outerRadius = value;
this.width = value * 2;
this.height = value * 2;
this.needsUpdate = true;
return this;
}
setNumPoints(value: number): IStar {
this.numPoints = value;
this.needsUpdate = true;
return this;
}
override getBoundRect(): IShapeBoundRect {
// 估算星形的外接矩形(以中心为圆心,外半径为界)
const r = Math.max(0, this.outerRadius);
const pad = (this.stroke != "" ? this.strokeWidth/2 : 0);
const d = r*2 + pad*2;
return {
x: this.x - r - pad,
y: this.y - r - pad,
width: d,
height: d
} as IShapeBoundRect;
}
override setWidth(value: number): IStar {
this.height = value;
this.width = value;
this.outerRadius = value / 2;
this.needsUpdate = true;
return this;
}
override setHeight(value: number): IStar {
this.height = value;
this.width = value;
this.outerRadius = value / 2;
this.needsUpdate = true;
return this;
}
override draw(ctx: CanvasRenderingContext2D) {
if (this.visible == false) return;
super.draw(ctx);
ctx.beginPath();
// 计算每个角的角度增量
const angleStep = Math.PI * 2 / this.numPoints;
// 起始角度(使星形垂直向上)
const startAngle = -Math.PI / 2;
// 绘制第一个点(外部点)
let x = this.x + Math.cos(startAngle) * this.outerRadius;
let y = this.y + Math.sin(startAngle) * this.outerRadius;
ctx.moveTo(x, y);
// 绘制其余的点
for (let i = 1; i <= this.numPoints * 2; i++) {
const angle = startAngle + angleStep * i / 2;
const radius = i % 2 === 0 ? this.outerRadius : this.innerRadius;
x = this.x + Math.cos(angle) * radius;
y = this.y + Math.sin(angle) * radius;
ctx.lineTo(x, y);
}
ctx.closePath();
if (this.fill != "") {
ctx.fill();
}
if (this.stroke != "") {
ctx.stroke();
}
ctx.restore();
}
override isPointInPath(x: number, y: number, shapeId: string): boolean {
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
// 计算点击位置相对于星形中心的实际坐标
let realX = x - this.offsetX - this.x;
let realY = y - this.offsetY - this.y;
// 如果有旋转,需要将坐标转换回未旋转状态
if (this.rotation != 0) {
const angle = -this.rotation * Math.PI / 180;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let centerX = 0;
let centerY = 0;
// 根据不同的旋转中心点设置centerX和centerY
switch(this.rotateCenter) {
case 'topLeft':
centerX = 0;
centerY = 0;
break;
case 'topRight':
centerX = this.width;
centerY = 0;
break;
case 'bottomLeft':
centerX = 0;
centerY = this.height;
break;
case 'bottomRight':
centerX = this.width;
centerY = this.height;
break;
case 'center':
default:
centerX = this.width/2;
centerY = this.height/2;
break;
}
const dx = realX - centerX;
const dy = realY - centerY;
realX = centerX + dx * cos - dy * sin;
realY = centerY + dx * sin + dy * cos;
}
// 计算点到中心的距离
const distance = Math.sqrt(realX * realX + realY * realY);
// 如果点击位置超出外半径,则不在星形内
if (distance > this.outerRadius * Math.max(this.scaleX, this.scaleY)) return false;
// 计算点击位置的角度
let angle = Math.atan2(realY, realX);
if (angle < 0) angle += Math.PI * 2;
// 调整角度使其从垂直向上开始计算
angle = (angle + Math.PI / 2) % (Math.PI * 2);
// 计算点所在的扇区
const angleStep = Math.PI * 2 / this.numPoints;
const sector = Math.floor(angle / angleStep);
const sectorAngle = angle - sector * angleStep;
// 计算该角度对应的半径
const ratio = sectorAngle / angleStep;
const radius = this.innerRadius + (this.outerRadius - this.innerRadius) * Math.abs(0.5 - ratio) * 2;
// 考虑缩放因素
const scaledRadius = radius * Math.max(this.scaleX, this.scaleY);
// 如果点击位置在计算出的半径内,则在星形内
return distance <= scaledRadius;
}
}
+226
View File
@@ -0,0 +1,226 @@
import { Shape } from './shape.uts';
import { ShapeSetAttrType, ITextBaselineType, ITextAlignType, CanvasRotateCenter, IShapeBoundRect, IShapeOptional } from '../interface.uts';
import { ICanvas } from '@/uni_modules/tmx-ui/core/canvas/ICanvas.uts';
export class IText extends Shape {
constructor(config : IShapeOptional,canvas:ICanvas) {
super(config,canvas);
this.type = 'IText'
}
private wrapText(ctx : CanvasRenderingContext2D, text : string, maxWidth : number) : string[] {
// 首先处理所有类型的换行符,统一转换为\n
text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
// 按换行符分割文本
const paragraphs = text.split('\n');
const lines : string[] = [];
// 处理每个段落
for (let i = 0; i < paragraphs.length; i++) {
let paragraph = paragraphs[i]
if (paragraph == '') {
// 保留空行
lines.push('');
continue;
}
const words = paragraph.split('');
let currentLine = '';
for (let i = 0; i < words.length; i++) {
const testLine = currentLine + words[i];
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && i > 0) {
lines.push(currentLine);
currentLine = words[i];
} else {
currentLine = testLine;
}
}
lines.push(currentLine);
}
return lines;
}
override draw(ctx : CanvasRenderingContext2D) {
if (this.visible == false) return;
ctx.font = `${this.fontSize}px ${this.fontFamily}`;
ctx.textAlign = this.textAlign;
ctx.textBaseline = this.textBaseline;
const maxWidth = this.width > 0 ? this.width - this.padding * 2 : ctx.canvas.offsetWidth - this.x - this.padding * 2;
const lines = this.wrapText(ctx, this.text, maxWidth);
const lineHeightPixels = this.fontSize * this.lineHeight;
this.width = maxWidth + this.padding * 2;
this.height = lineHeightPixels * lines.length + this.padding * 2;
if(this.textBgColor!=''){
ctx.fillStyle = this.textBgColor
// ctx.fillRect(this.x,this.y,this.width,this.height)
ctx.beginPath();
if (this.radius > 0) {
let radiuss = Math.min(this.radius, this.width / 2, this.height / 2);
const radius = Math.max(radiuss, 0)
ctx.moveTo(this.x + radius, this.y);
ctx.lineTo(this.x + this.width - radius, this.y);
ctx.arcTo(this.x + this.width, this.y, this.x + this.width, this.y + radius, radius);
ctx.lineTo(this.x + this.width, this.y + this.height - radius);
ctx.arcTo(this.x + this.width, this.y + this.height, this.x + this.width - radius, this.y + this.height, radius);
ctx.lineTo(this.x + radius, this.y + this.height);
ctx.arcTo(this.x, this.y + this.height, this.x, this.y + this.height - radius, radius);
ctx.lineTo(this.x, this.y + radius);
ctx.arcTo(this.x, this.y, this.x + radius, this.y, radius);
} else {
ctx.beginPath();
ctx.rect(this.x, this.y, this.width, this.height);
}
ctx.closePath();
ctx.fill()
ctx.fillStyle = this.fill
}
for (let i = 0; i < lines.length; i++) {
const y = this.y + this.padding + i * lineHeightPixels;
let adjustedY = y;
// 根据textBaseline调整y坐标
if (this.textBaseline === 'middle') {
adjustedY = y + this.fontSize / 2;
} else if (this.textBaseline === 'bottom') {
adjustedY = y + this.fontSize;
}
let x = this.x + this.padding;
if (this.textAlign === 'center') {
x = this.x + this.width / 2;
} else if (this.textAlign === 'right') {
x = this.x + this.width - this.padding;
}
if (this.fill != "") {
ctx.fillStyle = this.fill;
ctx.fillText(lines[i], x, adjustedY);
}
if (this.stroke != "") {
ctx.strokeStyle = this.stroke;
ctx.lineWidth = this.strokeWidth;
ctx.strokeText(lines[i], x, adjustedY);
}
}
ctx.restore()
}
setText(value : string) : IText {
this.text = value;
this.needsUpdate = true;
return this;
}
setFontSize(value : number) : IText {
this.fontSize = value;
this.needsUpdate = true;
return this;
}
setFontFamily(value : string) : IText {
this.fontFamily = value;
this.needsUpdate = true;
return this;
}
setTextAlign(value : 'left' | 'center' | 'right') : IText {
this.textAlign = value;
this.needsUpdate = true;
return this;
}
setTextBaseline(value : 'top' | 'middle' | 'bottom') : IText {
this.textBaseline = value;
this.needsUpdate = true;
return this;
}
setPadding(value : number) : IText {
this.padding = value;
this.needsUpdate = true;
return this;
}
setLineHeight(value : number) : IText {
this.lineHeight = value;
this.needsUpdate = true;
return this;
}
override getBoundRect() : IShapeBoundRect {
const ctx = this.canvas.ctx!
ctx.font = `${this.fontSize}px ${this.fontFamily}`;
const maxWidth = this.width > 0 ? this.width - this.padding * 2 : ctx.canvas.offsetWidth - this.x - this.padding * 2;
const lines = this.wrapText(ctx, this.text, maxWidth);
const lineHeightPixels = this.fontSize * this.lineHeight;
const measuredWidth = Math.max(...lines.map(line => ctx.measureText(line).width), 0);
const effectiveWidth = (this.width > 0 ? Math.min(measuredWidth, maxWidth) : measuredWidth) + this.padding * 2;
const width = effectiveWidth;
const height = lineHeightPixels * lines.length + this.padding * 2;
return {
x: this.x,
y: this.y,
width: width,
height: height
} as IShapeBoundRect;
}
override isPointInPath(x : number, y : number, shapeId : string) : boolean {
if (!this.visible || (shapeId != "" && shapeId != this.id)) return false;
const textBounds = this.getBoundRect();
const realX = x - textBounds.x - this.offsetX;
const realY = y - textBounds.y - this.offsetY;
if (this.rotation != 0) {
const angle = -this.rotation * Math.PI / 180;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let centerX = 0;
let centerY = 0;
// 根据不同的旋转中心点设置centerX和centerY
switch(this.rotateCenter) {
case 'topLeft':
centerX = 0;
centerY = 0;
break;
case 'topRight':
centerX = textBounds.width;
centerY = 0;
break;
case 'bottomLeft':
centerX = 0;
centerY = textBounds.height;
break;
case 'bottomRight':
centerX = textBounds.width;
centerY = textBounds.height;
break;
case 'center':
default:
centerX = textBounds.width/2;
centerY = textBounds.height/2;
break;
}
const dx = realX - centerX;
const dy = realY - centerY;
const rotatedX = centerX + dx * cos - dy * sin;
const rotatedY = centerY + dx * sin + dy * cos;
return rotatedX >= 0 && rotatedX <= textBounds.width * this.scaleX && rotatedY >= 0 && rotatedY <= textBounds.height * this.scaleY;
}
return realX >= 0 && realX <= textBounds.width * this.scaleX && realY >= 0 && realY <= textBounds.height * this.scaleY;
}
}
+78
View File
@@ -0,0 +1,78 @@
type callbackFunType = (x:number)=>number
let kSplineTableSize = 11;
let kSampleStepSize = 1.0 / (kSplineTableSize - 1.0);
function A(aA1:number, aA2:number):number { return 1.0 - 3.0 * aA2 + 3.0 * aA1 }
function B(aA1:number, aA2:number):number { return 3.0 * aA2 - 6.0 * aA1 }
function C(aA1:number):number { return 3.0 * aA1 }
function calcBezier(aT:number, aA1:number, aA2:number):number { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT }
function getSlope(aT:number, aA1:number, aA2:number):number { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1) }
function binarySubdivide(aX:number, aA:number, aB:number, mX1:number, mX2:number):number {
let currentX = 0;
let currentT = 0;
let i = 0;
do {
currentT = aA + (aB - aA) / 2.0;
currentX = calcBezier(currentT, mX1, mX2) - aX;
if (currentX > 0.0) { aB = currentT; } else { aA = currentT; }
} while (Math.abs(currentX) > 0.0000001 && ++i < 10);
return currentT;
}
function newtonRaphsonIterate(aX:number, aGuessT:number, mX1:number, mX2:number):number {
let pat = aGuessT;
for (let i = 0; i < 4; ++i) {
let currentSlope = getSlope(aGuessT, mX1, mX2);
if (currentSlope == 0.0) { return aGuessT; }
let currentX = calcBezier(aGuessT, mX1, mX2) - aX;
pat -= currentX / currentSlope;
}
return pat;
}
function bezier(mX1:number, mY1:number, mX2:number, mY2:number):callbackFunType|null {
if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { return null; }
let sampleValues:number[] = [];
if (mX1 != mY1 || mX2 != mY2) {
for (let i = 0; i < kSplineTableSize; ++i) {
sampleValues.push(calcBezier(i * kSampleStepSize, mX1, mX2))
}
}
function getTForX(aX:number):number {
let intervalStart = 0;
let currentSample = 1;
let lastSample = kSplineTableSize - 1;
for (; currentSample != lastSample && sampleValues[currentSample] <= aX; ++currentSample) {
intervalStart += kSampleStepSize;
}
--currentSample;
let dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]);
let guessForT:number = intervalStart + dist * kSampleStepSize;
let initialSlope = getSlope(guessForT, mX1, mX2);
if (initialSlope >= 0.001) {
return newtonRaphsonIterate(aX, guessForT, mX1, mX2);
} else if (initialSlope == 0.0) {
return guessForT;
}
return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2);
}
return function (x:number):number {
if (mX1 == mY1 && mX2 == mY2) { return x; }
if (x == 0 || x == 1) { return x; }
return calcBezier(getTForX(x), mY1, mY2);
}
}
export default bezier
+20
View File
@@ -0,0 +1,20 @@
import {xRequestHistoryType,X_REQUEST_PRIVATE_CALL_FUN_TYPE} from "../../interface.uts"
/**
* 内部组件私有
*/
export const xRequestCall = reactive({
history:[] as xRequestHistoryType[],
authPass:true,
hostUrl:"",
dev:false,
header:null,
/** 请求前是否显示loading遮罩 */
showLoadToast:true,
/** 请求成功后是否提示,否则不提示 */
showSuccessToast:true,
/** 出错时,是否显示提示,否则不提示 */
showErrorToast:true
} as X_REQUEST_PRIVATE_CALL_FUN_TYPE)
+850
View File
@@ -0,0 +1,850 @@
/**
* xAnimate
* @author tmzdy
* @copyright tmui4.0附带核心基类
* @version 1.0.0
* @description 通过nodeElement元素来控制属性动画变得更为简单方便
*
*/
import { XANIMATE_OPIONS } from "../../interface.uts"
import { getUid } from "./xCoreUtil.uts";
import { hexToRgb,getDefaultColor,rgbToHex } from "./xCoreColorUtil.uts";
import bezier from "./bezier.uts"
// #ifdef APP-ANDROID
import Choreographer from "android.view.Choreographer";
import FrameCallback from "android.view.Choreographer.FrameCallback";
// #endif
// #ifdef APP-IOS
// import * as UIKit from 'UIKit';
// #endif
type ATTRGERTS = {
from : string,
to : string,
unit : string,
now : string,
progress : number,
name : string
}
type callbackFunType = (x:number)=>number
type easingFunType = {
x:number,
y:number,
m:number,
n:number,
}
export class xAnimate {
easingList = new Map<string,number[]>([
["linear",[0.250, 0.250, 0.750, 0.750]],
["ease",[0.250, 0.100, 0.250, 1.000]],
["easeIn",[0.420, 0.000, 1.000, 1.000]],
["easeOut",[0.000, 0.000, 0.580, 1.000]],
["easeInOut",[0.420, 0.000, 0.580, 1.000]],
["easeInQuad", [0.550, 0.085, 0.680, 0.530]],
["easeOutQuad", [0.250, 0.460, 0.450, 0.940]],
["easeInOutQuad", [0.455, 0.030, 0.515, 0.955]],
["easeInCubic", [0.550, 0.055, 0.675, 0.190]],
["easeOutCubic", [0.215, 0.610, 0.355, 1.000]],
["easeInOutCubic", [0.645, 0.045, 0.355, 1.000]],
["easeInQuart", [0.895, 0.030, 0.685, 0.220]],
["easeOutQuart", [0.165, 0.840, 0.440, 1.000]],
["easeInOutQuart", [0.770, 0.000, 0.175, 1.000]],
["easeInQuint", [0.755, 0.050, 0.855, 0.060]],
["easeOutQuint", [0.230, 1.000, 0.320, 1.000]],
["easeInOutQuint", [0.860, 0.000, 0.070, 1.000]],
["easeInSine", [0.470, 0.000, 0.745, 0.715]],
["easeOutSine", [0.390, 0.575, 0.565, 1.000]],
["easeInOutSine", [0.445, 0.050, 0.550, 0.950]],
["easeInExpo", [0.950, 0.050, 0.795, 0.035]],
["easeOutExpo", [0.190, 1.000, 0.220, 1.000]],
["easeInOutExpo", [1.000, 0.000, 0.000, 1.000]],
["easeInCirc", [0.600, 0.040, 0.980, 0.335]],
["easeOutCirc", [0.075, 0.820, 0.165, 1.000]],
["easeInOutBack", [0.680, -0.550, 0.265, 1.550]],
]);
private tid = null as null | number;
element = null as null|UniElement;
// 当前播放的动画函数
timingFunction = 'linear';
//当前设置的播放时间
duration = 500;
//元素id
ele = "";
// 当前是否播放中
running = false
//当前是否暂停中
pauseing = false
//当前的播放进度
progress = 0
reverse = false;
//设置true时,loop>1播放时会来回播放动画,而不是从起始开始而是
// 始-终-始这样的模式播放。
tyty = false;
_tyty = false;
// 循环播放次数-1表示无限循环.
loop = 1
// 播放计数
private _loop = 0
// 是否停止动画。
private _isStopping = true
private attrIndex = 0;
private completeCallBack = () => { }
private startCallBack = () => { }
private doCallBack = (propress:number) => { }
private tagetsAttr = [] as ATTRGERTS[]
private startTime = 0
private easing = null as null | callbackFunType;
/** 是否让动画顺序挨过按attr添加的属性顺序执行动画,而不是统一一起执行。 */
private isDescPlay = false
private enterCallFun = ()=>{}
// #ifdef APP-ANDROID
private ChoreographerDemo = null as Choreographer|null
private FrameCallbackCallFun = null as FrameCallback|null
// #endif
// #ifdef APP-IOS
// private CADisplayLink = null as UIKit.CADisplayLink|null
private dhid = 0
// #endif
// #ifdef WEB || APP-IOS
private canFrameIds:number|null = null
// #endif
/**
* 创建xAnimate动画实例
* @param ele 元素UniElement
* @param options 动画参数 XANIMATE_OPIONS
*/
constructor(ele:UniElement|null,options : XANIMATE_OPIONS) {
// this.element = uni.getElementById(options.ele);
this.element = ele;
this.duration = options.duration == null?this.duration:options.duration!
this.loop = options.loop == null?this.loop:options.loop!
this.tyty = options.tyty == null?this.tyty:options.tyty!
this.isDescPlay = options.isDescPlay == null?this.isDescPlay:options.isDescPlay!
let easingName = options.timingFunction == null?'linear':options.timingFunction!
let ecall = this.easingList.get(easingName);
if(ecall!=null){
let ecallps = ecall!
this.easing = bezier(ecallps[0],ecallps[1],ecallps[2],ecallps[3]);
}
if(options.bezier!=null){
let ecallps = options.bezier!
this.easing = bezier(ecallps[0],ecallps[1],ecallps[2],ecallps[3]);
}
if(options.complete!=null){
this.completeCallBack = options.complete!
}
if(options.start!=null){
this.startCallBack = options.start!
}
if(options.frame!=null){
this.doCallBack = options.frame!
}
}
private getUnit(n ?: string) : string {
if (n == null) return 'px';
let unit = n.replace(/[\d|\-|\+]/g, '');
if(unit=="."){
unit = ""
}
return unit;
}
/**
* 添加自定义动画
*/
addTimingFunction(name:string,nubs:number[]){
this.easingList.set(name,nubs)
}
/**
* 设置动画反转状态
*/
setAniReverse(n:boolean|null=null){
if(n!=null){
this.reverse = n!
}else{
this.reverse = !this.reverse
}
}
/**
* 设置播放动画的次数
*/
setLoops(n:number|null = null){
if(n!=null){
this.loop = n!;
}
}
/**
* 设置单次动画播放的持续时间
*/
setDurations(n:number|null = null){
if(n!=null){
this.duration = n!;
}
}
/**
* 连续播放的模式切换
*/
setTytys(n:boolean|null = null){
if(n!=null){
this.tyty = n!;
}
}
/**
* 添加动画属性
* @param {string} name 属性名,目前支持官方的css属性,相同的name会被覆盖
* @param {string} from 起始值,允许数字字符,带单位2px,20%这样的,但必须和to相同单位
* @param {string} to 结束值,允许数字字符,带单位2px,20%这样的,但必须和from相同单位
* @param {boolean} only 是否让其是唯一性,重复添加相同的name会被最后一次替换。
*/
attr(name:string,from : string, to : string ,only:boolean = true):xAnimate {
let unit = this.isColorStyle(name)?'':this.getUnit(from);
let from_n = this.isColorStyle(name)?getDefaultColor(from):parseFloat(from).toString()
let to_n = this.isColorStyle(name)?getDefaultColor(to):parseFloat(to).toString()
let index = this.tagetsAttr.findIndex((item:ATTRGERTS):boolean => item.name == name);
if(!only){
index=-1;
}
if(index==-1){
this.tagetsAttr.push({
from: from_n.toString(),
to: to_n.toString(),
unit,
progress: 0,
now: from_n,
name: name
} as ATTRGERTS)
}else{
this.tagetsAttr[index] = {
from: from_n.toString(),
to: to_n.toString(),
unit,
progress: 0,
now: from_n,
name: name
} as ATTRGERTS
}
return this;
}
private interpolate(startValue:number, endValue:number, progress:number):number {
return startValue + (endValue - startValue) * progress;
}
private isColorStyle(val:string):boolean{
return val.indexOf('background')>-1||val.indexOf('color')>-1;
}
private _setAttr(name:string,current:number,unit:string,progress:number,item:ATTRGERTS){
if(this.element==null) return
if(name=='scaleX'){
this.element!.style!.setProperty("transform",`scaleX(${current})`)
}else if(name=='scaleY'){
this.element!.style!.setProperty("transform",`scaleY(${current})`)
}else if(name=='scale'){
this.element!.style!.setProperty("transform",`scale(${current})`)
}else if(name=='rotateX'){
this.element!.style!.setProperty("transform",`rotateX(${(current).toString()+unit})`)
}else if(name=='rotateY'){
this.element!.style!.setProperty("transform",`rotateY(${(current).toString()+unit})`)
}else if(name=='rotate'){
this.element!.style!.setProperty("transform",`rotate(${(current).toString()+unit})`)
}else if(name=='translateX'){
this.element!.style!.setProperty("transform",`translateX(${(current).toString()+unit})`)
}else if(name=='translateY'){
this.element!.style!.setProperty("transform",`translateY(${(current).toString()+unit})`)
}else if(name=='translate'){
this.element!.style!.setProperty("transform",`translate(${(current).toString()+unit},${(current).toString()+unit})`)
}else if(this.isColorStyle(name)){
let startRgba = hexToRgb(item.from)
let dndRgba = hexToRgb(item.to)
let r = this.interpolate(startRgba.getNumber('r')!,dndRgba.getNumber('r')!,progress);
let g = this.interpolate(startRgba.getNumber('g')!,dndRgba.getNumber('g')!,progress);
let b = this.interpolate(startRgba.getNumber('b')!,dndRgba.getNumber('b')!,progress);
let a = this.interpolate(startRgba.getNumber('a')!,dndRgba.getNumber('a')!,progress);
this.element!.style!
.setProperty(name,`rgba(${r.toFixed(0)},${g.toFixed(0)},${b.toFixed(0)},${a.toFixed(1)})`)
}else{
this.element!.style!.setProperty(name,current.toFixed(2)+unit)
}
}
private _run_web() {
// #ifdef H5 || APP-IOS || APP-HARMONY
let _this = this;
_this.startTime = 0
if(_this.canFrameIds!=null){
cancelAnimationFrame(_this.canFrameIds)
}
function run(){
if (_this.startTime<=0) {
_this.startTime = Date.now(); // 记录动画开始时间
}
const progress = Math.min((Date.now() - _this.startTime) / _this.duration + _this.progress, 1); // 计算当前进度
if(_this.element!=null){
if(!_this.isDescPlay){
for(let i=0;i<_this.tagetsAttr.length;i++){
let item = _this.tagetsAttr[i]
item.progress = progress
if(!_this.isColorStyle(item.name)){
let fromN = parseFloat(item.from)
let toN = parseFloat(item.to)
let easeInt = 1
if(_this.easing!=null){
let eas = _this.easing!
easeInt = eas(progress)
}
let current = fromN + (toN - fromN) * (easeInt==1?progress:easeInt); // 根据进度计算当前值
if(_this.reverse||_this._tyty ){
current = toN + (fromN - toN) * (easeInt==1?progress:easeInt);
}
if(_this.element!=null){
_this._setAttr(item.name,current,item.unit,progress,item)
}
}else{
if(_this.element!=null){
_this._setAttr(item.name,0,item.unit,progress,item)
}
}
}
}else{
if(_this.attrIndex<_this.tagetsAttr.length){
let item = _this.tagetsAttr[_this.attrIndex]
item.progress = progress
if(!_this.isColorStyle(item.name)){
let fromN = parseFloat(item.from)
let toN = parseFloat(item.to)
let easeInt = 1
if(_this.easing!=null){
let eas = _this.easing!
easeInt = eas(progress)
}
let current = fromN + (toN - fromN) * (easeInt==1?progress:easeInt); // 根据进度计算当前值
if(_this.reverse||_this._tyty ){
current = toN + (fromN - toN) * (easeInt==1?progress:easeInt);
}
if(_this.element!=null){
_this._setAttr(item.name,current,item.unit,progress,item)
}
}else{
if(_this.element!=null){
_this._setAttr(item.name,0,item.unit,progress,item)
}
}
}
}
}
if(_this.pauseing){
if(_this.canFrameIds!=null){
cancelAnimationFrame(_this.canFrameIds)
}
_this.running = false
console.log(_this.pauseing,"动画暂停")
_this.progress = progress
return;
}
// console.log(_this.running,"动画结束")
if(progress>=1||_this._isStopping){
if(_this.isDescPlay&&_this.attrIndex<_this.tagetsAttr.length){
_this.attrIndex +=1;
_this.progress = 0
_this._run_web();
return;
}
if(_this.canFrameIds!=null) {
cancelAnimationFrame(_this.canFrameIds)
}
_this.attrIndex = 0;
_this.progress = 0
if(_this.tyty){
_this._tyty = !_this._tyty
}
if(_this.loop==-1){
_this._run_web();
return;
}else{
_this._loop+=1;
if(_this._loop<_this.loop){
_this._run_web();
return;
}
}
_this.running = false
_this.completeCallBack()
return;
}
if (progress < 1 && _this.running) {
_this.doCallBack(progress)
_this.canFrameIds = requestAnimationFrame(run)
}
}
_this.startCallBack()
run()
// #endif
}
private _run_weapp() {
// #ifdef MP
let _this = this;
_this.startTime = 0
clearTimeout(_this.dhid)
function run(){
if (_this.startTime<=0) {
_this.startTime = Date.now(); // 记录动画开始时间
}
const progress = Math.min((Date.now() - _this.startTime) / _this.duration + _this.progress, 1); // 计算当前进度
if(_this.element!=null){
if(!_this.isDescPlay){
for(let i=0;i<_this.tagetsAttr.length;i++){
let item = _this.tagetsAttr[i]
item.progress = progress
if(!_this.isColorStyle(item.name)){
let fromN = parseFloat(item.from)
let toN = parseFloat(item.to)
let easeInt = 1
if(_this.easing!=null){
let eas = _this.easing!
easeInt = eas(progress)
}
let current = fromN + (toN - fromN) * (easeInt==1?progress:easeInt); // 根据进度计算当前值
if(_this.reverse||_this._tyty ){
current = toN + (fromN - toN) * (easeInt==1?progress:easeInt);
}
if(_this.element!=null){
_this._setAttr(item.name,current,item.unit,progress,item)
}
}else{
if(_this.element!=null){
_this._setAttr(item.name,0,item.unit,progress,item)
}
}
}
}else{
if(_this.attrIndex<_this.tagetsAttr.length){
let item = _this.tagetsAttr[_this.attrIndex]
item.progress = progress
if(!_this.isColorStyle(item.name)){
let fromN = parseFloat(item.from)
let toN = parseFloat(item.to)
let easeInt = 1
if(_this.easing!=null){
let eas = _this.easing!
easeInt = eas(progress)
}
let current = fromN + (toN - fromN) * (easeInt==1?progress:easeInt); // 根据进度计算当前值
if(_this.reverse||_this._tyty ){
current = toN + (fromN - toN) * (easeInt==1?progress:easeInt);
}
if(_this.element!=null){
_this._setAttr(item.name,current,item.unit,progress,item)
}
}else{
if(_this.element!=null){
_this._setAttr(item.name,0,item.unit,progress,item)
}
}
}
}
}
if(_this.pauseing){
clearTimeout(_this.dhid)
_this.running = false
console.log(_this.pauseing,"动画暂停")
_this.progress = progress
return;
}
// console.log(_this.running,"动画结束")
if(progress>=1||_this._isStopping){
if(_this.isDescPlay&&_this.attrIndex<_this.tagetsAttr.length){
_this.attrIndex +=1;
_this.progress = 0
_this._run_weapp();
return;
}
clearTimeout(_this.dhid)
_this.attrIndex = 0;
_this.progress = 0
if(_this.tyty){
_this._tyty = !_this._tyty
}
if(_this.loop==-1){
_this._run_weapp();
return;
}else{
_this._loop+=1;
if(_this._loop<_this.loop){
_this._run_weapp();
return;
}
}
_this.running = false
_this.completeCallBack()
return;
}
if (progress < 1 && _this.running) {
_this.dhid = setTimeout(()=>{
_this.doCallBack(progress)
run()
}, 6); // 递归调用自身,实现动画效果
}
}
_this.startCallBack()
run()
// #endif
}
private _run_andriod() {
// #ifdef APP-ANDROID
let _this = this;
_this.startTime = 0
let dhid = 0
if(this.ChoreographerDemo==null){
this.ChoreographerDemo = Choreographer.getInstance();
}else{
if(this.FrameCallbackCallFun!=null){
_this.ChoreographerDemo!.removeFrameCallback(this.FrameCallbackCallFun!)
}
}
class frameCallback extends Choreographer.FrameCallback {
override doFrame(frameTimeNanos:Long) {
if (_this.startTime<=0) {
_this.startTime = Date.now(); // 记录动画开始时间
}
const progress = Math.min((Date.now() - _this.startTime) / _this.duration + _this.progress, 1); // 计算当前进度
if(_this.element!=null){
if(!_this.isDescPlay){
for(let i=0;i<_this.tagetsAttr.length;i++){
let item = _this.tagetsAttr[i]
if(!_this.isColorStyle(item.name)){
let fromN = parseFloat(item.from)
let toN = parseFloat(item.to)
let easeInt = 1
if(_this.easing!=null){
let eas = _this.easing!
easeInt = eas(progress)
}
let current = fromN + (toN - fromN) * (easeInt==1?progress:easeInt); // 根据进度计算当前值
if(_this.reverse||_this._tyty ){
current = toN + (fromN - toN) * (easeInt==1?progress:easeInt);
}
if(_this.element!=null){
_this._setAttr(item.name,current,item.unit,progress,item)
}
}else{
if(_this.element!=null){
_this._setAttr(item.name,0,item.unit,progress,item)
}
}
}
}else{
if(_this.attrIndex<_this.tagetsAttr.length){
let item = _this.tagetsAttr[_this.attrIndex]
item.progress = progress
if(!_this.isColorStyle(item.name)){
let fromN = parseFloat(item.from)
let toN = parseFloat(item.to)
let easeInt = 1
if(_this.easing!=null){
let eas = _this.easing!
easeInt = eas(progress)
}
let current = fromN + (toN - fromN) * (easeInt==1?progress:easeInt); // 根据进度计算当前值
if(_this.reverse||_this._tyty ){
current = toN + (fromN - toN) * (easeInt==1?progress:easeInt);
}
if(_this.element!=null){
_this._setAttr(item.name,current,item.unit,progress,item)
}
}else{
if(_this.element!=null){
_this._setAttr(item.name,0,item.unit,progress,item)
}
}
}
}
}
if(progress>=1||_this._isStopping){
if(_this.isDescPlay&&_this.attrIndex<_this.tagetsAttr.length){
_this.attrIndex +=1;
_this.progress = 0
_this._run_andriod();
return;
}
_this.progress = 0
if(_this.tyty){
_this._tyty = !_this._tyty
}
if(_this.loop==-1){
_this._run_andriod();
return;
}else{
_this._loop+=1;
if(_this._loop<_this.loop){
_this._run_andriod();
return;
}
}
_this.running = false
_this.completeCallBack()
return;
}
if(_this.pauseing){
_this.running = false
console.log(_this.pauseing,"动画暂停")
_this.progress = progress
return;
}
if (progress < 1 && _this.running) {
_this.doCallBack(progress)
_this.ChoreographerDemo!.postFrameCallback(this);
}
}
};
_this.startCallBack()
this.FrameCallbackCallFun = new frameCallback()
_this.ChoreographerDemo!.postFrameCallback(this.FrameCallbackCallFun!);
// #endif
}
private __run_web() {
// #ifdef H5
let _this = this;
if(_this.canFrameIds!=null) {
cancelAnimationFrame(_this.canFrameIds)
}
function run(){
_this.enterCallFun()
if(_this._isStopping){
_this.running = false
if(_this.canFrameIds!=null) {
cancelAnimationFrame(_this.canFrameIds)
}
return;
}
if (_this.running) {
_this.canFrameIds = requestAnimationFrame(run)
}
}
run()
// #endif
}
private __run_andriod() {
// #ifdef APP-ANDROID
let _this = this;
if(this.ChoreographerDemo==null){
this.ChoreographerDemo = Choreographer.getInstance();
}else{
if(this.FrameCallbackCallFun!=null){
_this.ChoreographerDemo!.removeFrameCallback(this.FrameCallbackCallFun!)
}
}
class frameCallback extends Choreographer.FrameCallback {
override doFrame(frameTimeNanos:Long) {
_this.enterCallFun()
if(_this._isStopping){
_this.running = false
return;
}
if (_this.running) {
_this.ChoreographerDemo!.postFrameCallback(this);
}
}
};
this.FrameCallbackCallFun = new frameCallback()
_this.ChoreographerDemo!.postFrameCallback(this.FrameCallbackCallFun!);
// #endif
}
private __run_weapp(){
// #ifdef MP
let _this = this;
clearTimeout(_this.dhid)
function run(){
_this.enterCallFun()
if(_this._isStopping){
_this.running = false
clearTimeout(_this.dhid)
return;
}
if (_this.running) {
_this.dhid = setTimeout(run, 6);
}
}
run()
// #endif
}
/**
* 启动动画
*/
play():xAnimate {
if(this.running) return this;
this.running = true;
// 开始动画
this._isStopping = false;
this.pauseing = false;
this._loop = 0
this.attrIndex =0;
// #ifdef H5 || APP-IOS || APP-HARMONY
this._run_web();
// #endif
// #ifdef APP-ANDROID
this._run_andriod();
// #endif
// #ifdef MP
this._run_weapp();
// #endif
return this;
}
/**
* 停止,会让动画直接结束,把目标值直接前进到结束的地方。
*/
stop():xAnimate {
let _this = this;
this._isStopping = true;
this.progress = 0
this.attrIndex = _this.tagetsAttr.length
// #ifdef WEB || APP-IOS
if(_this.canFrameIds!=null){
cancelAnimationFrame(_this.canFrameIds)
}
// #endif
// #ifdef APP-ANDROID
if(this.ChoreographerDemo==null){
this.ChoreographerDemo = Choreographer.getInstance();
}else{
if(this.FrameCallbackCallFun!=null){
this.ChoreographerDemo!.removeFrameCallback(this.FrameCallbackCallFun!)
}
}
// #endif
// #ifdef MP
clearTimeout(_this.dhid)
// setTimeout(function(){
// for(let i=0;i<_this.tagetsAttr.length;i++){
// let item = _this.tagetsAttr[i]
// item.now = item.to;
// if(_this.element!=null){
// _this.element!.style!.setProperty(item.name,(_this.reverse?item.from:item.to)+item.unit)
// }
// }
// _this.reverse = false;
// },10)
// _this.running = false;
// #endif
return this;
}
/**
* 暂停,会保留当前的动画效果,不会直接结束。
*/
pause():xAnimate {
this.pauseing = true;
return this;
}
/**
* 刷新回调函数
*/
enterFrame(evt:()=>void){
this.stop()
this.enterCallFun=evt;
this.running = true;
this._isStopping = false
// #ifdef WEB || APP-IOS
this.__run_web()
// #endif
// #ifdef APP-ANDROID
this.__run_andriod();
// #endif
// #ifdef MP
this.__run_weapp();
// #endif
}
}
@@ -0,0 +1,726 @@
import { toHex } from "./xCoreUtil.uts";
import { xConfig } from "../../config/xConfig.uts";
/**
* 颜色值列表
*/
export const colors = new Map<string, string>([
['primary', '#0088ff'],
['success', '#34c759'],
['danger', '#ff8d28'],
['warn', '#F3BF12'],
['error', '#ff383c'],
['info', '#f2f2f7'],
['kleinblue', '#002FA7'], // 克莱因蓝
['chinesered', '#FF0000'], // 中国红
['internationalorange', '#FF4F00'], // 国际橙
['egyptianvlue', '#1034A6'], // 埃及艳蓝
['parisviolet', '#6C3082'], // 巴黎紫
['moroccanblue', '#1256A7'], // 摩洛哥蓝
['brazilgreen', '#009B3A'], // 巴西绿
['britishracinggreen', '#004225'], // 英国赛车绿
['indianyellow', '#E3A857'], // 印度黄
['australiangold', '#FFDF00'], // 澳大利亚金
['venetianred', '#C80815'], // 威尼斯红
['majorelleblue', '#6050DC'], // 马若雷蓝
['tuscanred', '#7C3030'], // 托斯卡红
['naplesyellow', '#FADA5E'], // 那不勒斯黄
['capumortuum', '#592720'], // 死者之首
['mayablue', '#73C2FB'], // 玛雅蓝
['persianrose', '#FE28A2'], // 波斯玫瑰
['tyrianpurple', '#66023C'], // 泰尔紫
['saharasand', '#F1E788'], // 撒哈拉沙
['burmeseruby', '#B00A0A'], // 缅甸红宝石
['transparent', 'rgba(0,0,0,0)'],
['aliceblue', '#F0F8FF'],
['antiquewhite', '#FAEBD7'],
['aqua', '#00FFFF'],
['aquamarine', '#7FFFD4'],
['azure', '#F0FFFF'],
['beige', '#F5F5DC'],
['bisque', '#FFE4C4'],
['black', '#000000'],
['blanchedalmond', '#FFEBCD'],
['blue', '#0000FF'],
['blueviolet', '#8A2BE2'],
['brown', '#A52A2A'],
['burlywood', '#DEB887'],
['cadetblue', '#5F9EA0'],
['chartreuse', '#7FFF00'],
['chocolate', '#D2691E'],
['coral', '#FF7F50'],
['cornflowerblue', '#6495ED'],
['cornsilk', '#FFF8DC'],
['crimson', '#DC143C'],
['cyan', '#00FFFF'],
['darkblue', '#00008B'],
['darkcyan', '#008B8B'],
['darkgoldenrod', '#B8860B'],
['darkgray', '#A9A9A9'],
['darkgreen', '#006400'],
['darkkhaki', '#BDB76B'],
['darkmagenta', '#8B008B'],
['darkolivegreen', '#556B2F'],
['darkorange', '#FF8C00'],
['darkorchid', '#9932CC'],
['darkred', '#8B0000'],
['darksalmon', '#E9967A'],
['darkseagreen', '#8FBC8F'],
['darkslateblue', '#483D8B'],
['darkslategray', '#2F4F4F'],
['darkturquoise', '#00CED1'],
['darkviolet', '#9400D3'],
['deeppink', '#FF1493'],
['deepskyblue', '#00BFFF'],
['dimgray', '#696969'],
['dodgerblue', '#1E90FF'],
['firebrick', '#B22222'],
['floralwhite', '#FFFAF0'],
['forestgreen', '#228B22'],
['fuchsia', '#FF00FF'],
['gainsboro', '#DCDCDC'],
['ghostwhite', '#F8F8FF'],
['gold', '#FFD700'],
['goldenrod', '#DAA520'],
['gray', '#808080'],
['green', '#008000'],
['greenyellow', '#ADFF2F'],
['honeydew', '#F0FFF0'],
['hotpink', '#FF69B4'],
['indianred', '#CD5C5C'],
['indigo', '#4B0082'],
['ivory', '#FFFFF0'],
['khaki', '#F0E68C'],
['lavender', '#E6E6FA'],
['lavenderblush', '#FFF0F5'],
['lawngreen', '#7CFC00'],
['lemonchiffon', '#FFFACD'],
['lightblue', '#ADD8E6'],
['lightcoral', '#F08080'],
['lightcyan', '#E0FFFF'],
['lightgoldenrodyellow', '#FAFAD2'],
['lightgray', '#D3D3D3'],
['lightgreen', '#90EE90'],
['lightpink', '#FFB6C1'],
['lightsalmon', '#FFA07A'],
['lightseagreen', '#20B2AA'],
['lightskyblue', '#87CEFA'],
['lightslategray', '#778899'],
['lightsteelblue', '#B0C4DE'],
['lightyellow', '#FFFFE0'],
['lime', '#00FF00'],
['limegreen', '#32CD32'],
['linen', '#FAF0E6'],
['magenta', '#FF00FF'],
['maroon', '#800000'],
['mediumaquamarine', '#66CDAA'],
['mediumblue', '#0000CD'],
['mediumorchid', '#BA55D3'],
['mediumpurple', '#9370DB'],
['mediumseagreen', '#3CB371'],
['mediumslateblue', '#7B68EE'],
['mediumspringgreen', '#00FA9A'],
['mediumturquoise', '#48D1CC'],
['mediumvioletred', '#C71585'],
['midnightblue', '#191970'],
['mintcream', '#F5FFFA'],
['mistyrose', '#FFE4E1'],
['moccasin', '#FFE4B5'],
['navajowhite', '#FFDEAD'],
['navy', '#000080'],
['oldlace', '#FDF5E6'],
['olive', '#808000'],
['olivedrab', '#6B8E23'],
['orange', '#FFA500'],
['orangered', '#FF4500'],
['orchid', '#DA70D6'],
['palegoldenrod', '#EEE8AA'],
['palegreen', '#98FB98'],
['paleturquoise', '#AFEEEE'],
['palevioletred', '#DB7093'],
['papayawhip', '#FFEFD5'],
['peachpuff', '#FFDAB9'],
['peru', '#CD853F'],
['pink', '#FFC0CB'],
['plum', '#DDA0DD'],
['powderblue', '#B0E0E6'],
['purple', '#800080'],
['rebeccapurple', '#663399'],
['red', '#FF0000'],
['rosybrown', '#BC8F8F'],
['royalblue', '#4169E1'],
['saddlebrown', '#8B4513'],
['salmon', '#FA8072'],
['sandybrown', '#F4A460'],
['seagreen', '#2E8B57'],
['seashell', '#FFF5EE'],
['sienna', '#A0522D'],
['silver', '#C0C0C0'],
['skyblue', '#87CEEB'],
['slateblue', '#6A5ACD'],
['slategray', '#708090'],
['snow', '#FFFAFA'],
['springgreen', '#00FF7F'],
['steelblue', '#4682B4'],
['tan', '#D2B48C'],
['teal', '#008080'],
['thistle', '#D8BFD8'],
['tomato', '#FF6347'],
['turquoise', '#40E0D0'],
['violet', '#EE82EE'],
['wheat', '#F5DEB3'],
['white', '#FFFFFF'],
['whitesmoke', '#F5F5F5'],
['yellow', '#FFFF00'],
['yellowgreen', '#9ACD32']
])
export function isValidColor(color : string) : boolean {
// return chroma.valid(color);
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8})$/;
const rgbRegex = /^rgb\((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s*,\s*(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s*,\s*(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\)$/;
const rgbaRegex = /^rgba\((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s*,\s*(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s*,\s*(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s*,\s*((1(\.0{1,2})?)|(0(\.\d{1,2})?))\)$/;
const hslRegex = /^hsl\((\d{1,3}), (\d{1,3})%, (\d{1,3})%\)$/;
const hslaRegex = /^hsla\(\s*((\d{1,2}|[1-2]\d{2}|3[0-5]\d)(\.\d+)?|\d+(\.\d+)?)\s*,\s*((\d{1,2}|\d{0,1}\d{1}\d{1}|[1-2]\d{2}|3[0-5]\d)(\.\d+)?|\d+(\.\d+)?)%\s*,\s*((\d{1,2}|\d{0,1}\d{1}\d{1}|[1-2]\d{2}|3[0-5]\d)(\.\d+)?|\d+(\.\d+)?)%\s*,\s*((1|0(\.\d{1,2})?|(\.\d{1,2})))\)$/i;
if (color == '') {
return false; // 颜色值为空
} else if (color === 'inherit' || color === 'transparent') {
return false; // 特殊颜色值
} else if (color === 'currentColor') {
return false; // currentColor 不是有效的颜色值
} else if (hexRegex.test(color) || rgbRegex.test(color) || rgbaRegex.test(color) || hslRegex.test(color) || hslaRegex.test(color)) {
return true; // 符合颜色值的格式
}
return false;
}
/**
* 获取默认的颜色值
* @param [string] 颜色名称或者16进制颜色值
*/
export function getDefaultColor(sColor : string) : string {
if (sColor == "") return ""
let sc = sColor.toLocaleLowerCase().trim().replace(" ", "");
if (isValidColor(sc)) {
// 如果符合所有颜色值,进行转换为16进制。css方便统一解析。
return sc;
}
let colorhtme = xConfig.theme.get(sc)
// 检测是否是颜色名称。
let sco = colorhtme==null?colors.get(sc):colorhtme;
if (typeof sco == 'string') return sco as string;
return colors.get("primary")!;
}
/**
* 16进制转rgb
*/
export function hexToRgb(sColors : string) : UTSJSONObject {
if (sColors == "") {
return { r: 0, g: 0, b: 0, a: 0 }
}
let reg = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8})$/;
let sColor : string = sColors.toLowerCase();
if (sColor != '' && reg.test(sColor)) {
if (sColor.length == 4) {
let sColorNew = "#";
for (let i = 1; i < 4; i += 1) {
sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));
}
sColor = sColorNew;
}
//处理六位的颜色值
let sColorChange : number[] = [];
sColorChange.push(parseInt(sColor.substring(1, 3), 16));
sColorChange.push(parseInt(sColor.substring(3, 5), 16));
sColorChange.push(parseInt(sColor.substring(5, 7), 16));
if (sColor.length == 9) {
sColorChange.push(parseInt(sColor.substring(7, 9), 16) / 255);
}
return {
r: sColorChange[0],
g: sColorChange[1],
b: sColorChange[2],
a: sColorChange.length == 4 ? sColorChange[3] : 1
}
} else if (/^(rgb|RGB|rgba|RGBA)/.test(sColor)) {
let arr : string[] = sColor.replace(/(?:\(|\)|rgba|rgb|RGB|RGBA)*/g, "").split(",")
let p : number[] = arr.map((val : string) : number => parseInt(val));
if (p.length < 3) {
return {
r: 0,
g: 0,
b: 0,
a: 1
}
}
if (p.length == 3) {
arr.push('1')
}
return {
r: p[0],
g: p[1],
b: p[2],
a: parseFloat(arr[3])
}
} else {
return {
r: 0,
g: 0,
b: 0,
a: 1
}
}
}
/**
* rgb转hsl
*/
export function rgbToHsl(rgb : UTSJSONObject) : UTSJSONObject {
let r = rgb.getNumber("r")
r = r as number / 255
let g = rgb.getNumber("g")
g = g as number / 255
let b = rgb.getNumber("b")
b = b as number / 255
let a = rgb.getNumber("a")
var max = Math.max(r, g, b);
var min = Math.min(r, g, b);
let maxmindiff = max - min;
let maxmindiffAdd = max + min;
// #ifdef APP-ANDROID && uniVersion >=4.31
maxmindiff = maxmindiff.toDouble()
maxmindiffAdd = maxmindiffAdd.toDouble()
// #endif
let h = 60 * (4 + (r - g) / maxmindiff);
let sdy = 2 - max - min
// #ifdef APP-ANDROID && uniVersion >=4.31
sdy = sdy.toDouble()
// #endif
let s = (max - min) / sdy;
let l = (max + min) / 2;
if (max === r) {
h = (60 * (g - b)) / maxmindiff;
} else if (max === g) {
h = 60 * (2 + (b - r) / maxmindiff);
}
if (h < 0) {
h += 360;
}
if (max === min) {
s = 0;
} else if (l < 0.5) {
s = (max - min) / maxmindiffAdd;
}
return { h: h, s: s * 100, l: l * 100, a: a };
}
/**
* hsl转rgb
*/
export function hslToRgb(hsl : UTSJSONObject) : UTSJSONObject {
let h = hsl.getNumber("h")!
let s = hsl.getNumber("s")!
let l = hsl.getNumber("l")!
let a = hsl.getNumber("a")!
h = h / 360;
s = s / 100;
l = l / 100;
let r = l;
let g = l;
let b = l;
function hue2rgb(pxx : number, q : number, txs : number) : number {
let t = txs;
let p = pxx;
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
if (s > 0) {
let q = l * (1 + s);
if (l >= 0.5) {
q = l + s - l * s;
}
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
r = Math.round(r * 255);
g = Math.round(g * 255);
b = Math.round(b * 255);
return { r, g, b, a };
}
/**
* rgb转16进制
*/
export function rgbToHex(rgb : UTSJSONObject) : string {
let r = rgb.getNumber("r")!
let g = rgb.getNumber("g")!
let b = rgb.getNumber("b")!
let a = rgb.getNumber("a")!
return "#" + toHex(r) + toHex(g) + toHex(b) + toHex(a * 255)
}
/**
* rgb转16进制
*/
export function rgbToHexNoAlpha(rgb : UTSJSONObject) : string {
let r = rgb.getNumber("r")!
let g = rgb.getNumber("g")!
let b = rgb.getNumber("b")!
return "#" + toHex(r) + toHex(g) + toHex(b)
}
export function hslaToCss(hsl : UTSJSONObject) : string {
let rgb = hslToRgb(hsl)
return rgbToHex(rgb)
}
export function hslaToRgbCss(hsl : UTSJSONObject) : string {
let rgb = hslToRgb(hsl)
let r = rgb.getNumber("r")!
let g = rgb.getNumber("g")!
let b = rgb.getNumber("b")!
let a = rgb.getNumber("a")!
return `rgba(${r},${g},${b},${a})`
}
// 根据 HSLA 颜色计算亮度
export function getLuminance(color : UTSJSONObject, type = "rgba") : number {
let colordefault : UTSJSONObject = type == 'rgba' ? color : hslToRgb(color);
const r = colordefault.getNumber("r")!
const g = colordefault.getNumber("g")!
const b = colordefault.getNumber("b")!
const rs = r / 255
const gs = g / 255
const bs = b / 255
const rl = Math.pow(rs <= 0.03928 ? rs / 12.92 : ((rs + 0.055) / 1.055), 2.4);
const gl = Math.pow(gs <= 0.03928 ? gs / 12.92 : ((gs + 0.055) / 1.055), 2.4);
const bl = Math.pow(bs <= 0.03928 ? bs / 12.92 : ((bs + 0.055) / 1.055), 2.4);
return 0.2126 * rl + 0.7152 * gl + 0.0722 * bl;
}
/**
* 对颜色加深
*/
export function colorAddDeepen(sColor : string) : string {
let rgb = hexToRgb(getDefaultColor(sColor));
let hsl = rgbToHsl(rgb);
let l = hsl.getNumber("l")! - 5;
l = Math.max(0, Math.min(l, 100))
return hslaToRgbCss({ h: hsl.getNumber("h"), s: hsl.getNumber("s"), l, a: hsl.getNumber("a")! })
}
/**
* 对颜色提亮
*/
export function colorAddBrighten(sColor : string) : string {
let rgb = hexToRgb(getDefaultColor(sColor));
let hsl = rgbToHsl(rgb);
let l = hsl.getNumber("l")! + 10;
l = Math.max(0, Math.min(l, 100))
return rgbToHex(hslToRgb({ h: hsl.getNumber("h"), s: hsl.getNumber("s"), l, a: hsl.getNumber("a")! }))
}
/**
* 获取浅色系主题,根据颜色值计算。返回正常值和hover值。
*/
export function colorGetThint(sColor : string) {
let rgb = hexToRgb(getDefaultColor(sColor));
let hsl = rgbToHsl(rgb);
let s = hsl.getNumber("s")!;
// let l = hsl.getNumber("l");
if (Math.ceil(s) > 0) {
s = 45
}
return {
default: rgbToHex(hslToRgb({ h: hsl.getNumber("h"), s, l: 93, a: hsl.getNumber("a")! })),
hover: rgbToHex(hslToRgb({ h: hsl.getNumber("h"), s, l: 85, a: hsl.getNumber("a")! }))
}
}
/**
* 获取正常主题,根据颜色值计算。返回正常值和hover值。
*/
export function colorGetHover(sColor : string) {
return {
default: sColor,
hover: colorAddDeepen(sColor)
}
}
/**
* 处理相主题色。
* @param color 需要处理的颜色值
* @param hoverColor 对应按下去的颜色默认与color相等。
* @returns UTSJSONObject
*/
export function getDefaultColorObj(color : string, hoverColor : string) : UTSJSONObject {
let hsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(color)));
let hoverHsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(hoverColor)));
let shadow = hslaToRgbCss({ h: hsla.getNumber("h")!, s: (hsla.getNumber("s")!) == 0 ? 0 : 60, l: 40, a: 0.4 });
let shadowHOver = hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: (hoverHsla.getNumber("s")!) == 0 ? 0 : 60, l: 40, a: 0.4 });
if ((hsla.getNumber("h")!) == 0 && (hsla.getNumber("s")!) == 0) {
shadow = "transparent"
}
if ((hoverHsla.getNumber("h")!) == 0 && (hoverHsla.getNumber("s")!) == 0) {
shadowHOver = "transparent"
}
let lightnum = getLuminance(hoverHsla, 'hsla');
let h = hsla.getNumber('h')!;
let maxLightNum = 0.55
if(h>=31&&h<=90){
maxLightNum = 0.60
}
let o = {
default: {
background: hslaToRgbCss(hsla),
borderColor: hslaToRgbCss({ h: hsla.getNumber("h")!, s: hsla.getNumber("s")!, a: hsla.getNumber("a")!, l: Math.min(Math.max(0, hsla.getNumber("l")! - 3), hsla.getNumber("l")!) }),
fontColor: lightnum < maxLightNum && hsla.getNumber("a")! > 0.1 ? '#ffffff' : '#000000',
shadow: shadow
},
active: {
background: hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: Math.min(Math.max(0, hoverHsla.getNumber("l")! - 5), hoverHsla.getNumber("l")!) }),
borderColor: hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: Math.max(Math.max(0, hoverHsla.getNumber("l")! - 10), hoverHsla.getNumber("l")!) }),
fontColor: lightnum < maxLightNum && hoverHsla.getNumber("a")! > 0.1 ? '#ffffff' : '#000000',
shadow: shadowHOver
}
} as UTSJSONObject
return o;
}
const hslColorMap = new Map<string,number[]>([
['红色', [331, 360]], // 0-30度
['红色', [0, 30]], // 0-30度
['橙色', [31, 60]], // 30-60度
['黄色', [61, 90]], // 60-90度
['绿色', [91, 150]], // 90-150度
['青色', [151, 210]], // 150-210度
['蓝色', [211, 270]], // 210-270度
['紫色', [271, 330]] // 270-330度
]);
/**
* 处理相主题色。
* @param color 需要处理的颜色值
* @param hoverColor 对应按下去的颜色默认与color相等。
* @returns UTSJSONObject
*/
export function getTextColorObj(color : string, hoverColor : string, isCoverDark ?: boolean) : UTSJSONObject {
let dark = isCoverDark == null ? false : isCoverDark
// #ifdef WEB
dark = isCoverDark == undefined ? false : dark
// #endif
let hsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(color)));
let hsla2 : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(color)));
let hoverHsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(hoverColor)));
// let fontcolor = getLuminance(hsla, 'hsla') < 0.3 && hsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hsla) : hslaToRgbCss({ h: hsla.getNumber("h")!, s: hsla.getNumber("s")!, a: hsla.getNumber("a")!, l:30 })
// let bordercolor = hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: dark==true?21:92 })
let bgcolor = hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: dark == true ? 20 : 95 })
if (dark) {
let p = hsla2;
// p.set('l',90)
// fontcolor = hslaToRgbCss(p)
p.set('l', 20)
p.set('s', (p.getNumber('s')) != 0 ? 0 : 20)
bgcolor = hslaToRgbCss(p)
// p.set('l',22)
// bordercolor = hslaToRgbCss(p)
}
let lightnum = getLuminance(hoverHsla, 'hsla');
let h = hsla.getNumber('h')!;
let maxLightNum = 0.3
// if(h>=31&&h<=90){
// maxLightNum = 1
// }
// tmxColor.hslaToCss({ ...hsla}) : tmxColor.hslaToCss({ ...hsla,l:30})
let o = {
default: {
background: "transparent",
borderColor: "transparent",
fontColor: lightnum < maxLightNum && hsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hsla) : hslaToRgbCss({ h: hsla.getNumber("h")!, s: hsla.getNumber("s")!, a: hsla.getNumber("a")!, l: 30 }),
shadow: "transparent",
},
active: {
background: bgcolor,
borderColor: "transparent",
fontColor: lightnum < maxLightNum && hoverHsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hoverHsla) : hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: 30 }),
shadow: "transparent",
}
} as UTSJSONObject
return o;
}
/**
* 处理相主题色。
* @param color 需要处理的颜色值
* @param hoverColor 对应按下去的颜色默认与color相等。
* @returns UTSJSONObject
*/
export function getThinColorObj(color : string, hoverColor : string, isCoverDark ?: boolean) : UTSJSONObject {
let dark = isCoverDark == null ? false : isCoverDark
// #ifdef WEB
dark = isCoverDark == undefined ? false : dark
// #endif
let hsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(color)));
let hoverHsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(hoverColor)));
// tmxColor.hslaToCss({ ...hsla}) : tmxColor.hslaToCss({ ...hsla,l:30})
let fontcolor = getLuminance(hsla, 'hsla') < 0.3 && hsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hsla) : hslaToRgbCss({ h: hsla.getNumber("h")!, s: hsla.getNumber("s")!, a: hsla.getNumber("a")!, l: 30 })
let bordercolor = hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: dark == true ? 21 : 92 })
let bgcolor = hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: dark == true ? 20 : 95 })
let lightnum = getLuminance(hoverHsla, 'hsla');
if (dark) {
let p = hsla;
p.set('l', 98)
fontcolor = hslaToRgbCss(p)
p.set('l', 20)
p.set('s', (p.getNumber('s')) != 0 ? 5 : 20)
bgcolor = hslaToRgbCss(p)
p.set('l', 22)
bordercolor = hslaToRgbCss(p)
}
let o = {
default: {
background: bgcolor,
borderColor: bordercolor,
fontColor: fontcolor,
shadow: "transparent",
},
active: {
background: hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: dark ? 10 : 50, a: hoverHsla.getNumber("a")!, l: dark == true ? 15 : 88 }),
borderColor: hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: 0, l: dark == true ? 18 : 80 }),
fontColor: lightnum < 0.3 && hoverHsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hoverHsla) : hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: 64 }),
shadow: "transparent",
}
} as UTSJSONObject
return o;
}
/**
* 处理相主题色。
* @param color 需要处理的颜色值
* @param hoverColor 对应按下去的颜色默认与color相等。
* @returns UTSJSONObject
*/
export function getOutlineColorObj(color : string, hoverColor : string, isCoverDark ?: boolean) : UTSJSONObject {
let dark = isCoverDark == null ? false : isCoverDark
// #ifdef WEB
dark = isCoverDark == undefined ? false : dark
// #endif
let hsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(color)));
let hoverHsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(hoverColor)));
// tmxColor.hslaToCss({ ...hsla}) : tmxColor.hslaToCss({ ...hsla,l:30})
let lightnum = getLuminance(hoverHsla, 'hsla');
let h = hsla.getNumber('h')!;
let borderLight = 70;
// let maxLightNum = 0.55
if(dark){
borderLight = 50
}
let o = {
default: {
background: 'transparent',
borderColor: hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: 1, l: borderLight+5 }),
fontColor: lightnum < 0.6 && hsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hsla) : hslaToRgbCss({ h: hsla.getNumber("h")!, s: hsla.getNumber("s")!, a: hsla.getNumber("a")!, l: 30 }),
shadow: "transparent",
},
active: {
background: 'transparent',
borderColor: hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: 1, l: borderLight }),
fontColor: lightnum < 0.6 && hoverHsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hoverHsla) : hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: 30 }),
shadow: "transparent",
}
} as UTSJSONObject
return o;
}
/**
* 设置颜色的亮度值。29
* isCoverDark:是根据亮度值,是否要设置n的值,如果n超过了
* 指定的30就不操作。
*/
export function setTextColorLightByDark(color : string) : string {
let realColor = getDefaultColor(color);
let hsla = rgbToHsl(hexToRgb(realColor))
let l = hsla.getNumber('l')!
let s = hsla.getNumber('s')!
if (l < 30) {
if (s > 0) {
hsla.set('l', 50)
} else {
hsla.set('l', 100)
}
}
return hslaToRgbCss(hsla)
}
/**
* 变动的颜色是否需要变深。如果不需要返回原始颜色
* 如果果需要自动加深。
*/
export function setBgColorLightByDark(color : string) : string {
let realColor = getDefaultColor(color);
let hsla = rgbToHsl(hexToRgb(realColor))
let l = hsla.getNumber('l')!
let s = hsla.getNumber('s')!
if (s == 0 && l > 50) {
hsla.set('l', 100 - l)
}
if (s > 0) {
if (l > 50) {
hsla.set('l', 100 - l)
}
}
return hslaToRgbCss(hsla)
}
export function isBlackAndWhite(color : string) : boolean {
let realColor = getDefaultColor(color);
let hsla = rgbToHsl(hexToRgb(realColor))
let s = hsla.getNumber('s')!
return s == 0
}
+333
View File
@@ -0,0 +1,333 @@
import { getDefaultColor } from "./xCoreColorUtil.uts";
import { xConfig } from "../../config/xConfig.uts"
/**
* 数字转16进制
*/
function toHex(numbers : number) : string {
// let i = 0 as number
// if (numbers === i) {
// return '00';
// }
// let hex = '';
// const hexChars = '0123456789abcdef';
// while (numbers > 0) {
// const remainder = numbers % 16;
// hex = hexChars[remainder] + hex;
// numbers = Math.floor(numbers / 16);
// }
let n = numbers.toString(16) as string;
if(n.length==1){
n = '0'+n
}
return n;
}
/**
* 随机一个uid
* @param rdix 随机因子
* @param length 取的长度
* @param isAddStr 是否限制随机结果中的长度,不允许输出长度
* @returns String
*/
function getUid(rdix = 1, length = 12) : string {
let ix = "";
// #ifndef APP
ix = Math.floor(Math.random() * rdix * Math.floor(Math.random() * Date.now())).toString().substring(0, length);
// #endif
// #ifdef APP
ix = Math.floor(Math.random() * rdix * Math.floor(Math.random() * Date.now())).toString().substring(0, length as Int);
// #endif
return ix;
}
/**
* 给定一个值,来填充边距所需要的数组值
* @param val any
* @returns [左,上,右,下]
*/
function toFillMarginAr(val : number[]) : number[] {
let ar : number[] = [];
if (val.length == 1) {
let firstEl = val[0];
ar = [firstEl, firstEl, firstEl, firstEl]
} else if (val.length == 2) {
ar = [val[0], val[1], val[0], val[1]]
} else if (val.length == 3) {
ar = [val[0], val[1], val[2], 0]
}
return ar;
}
function rpx2px(n : number, _w = 750) : number {
let r = n
// #ifdef APP
r = uni.rpx2px(n);
// #endif
// #ifdef H5
function getLayoutRatio() : number {
const devicePixelRatio = window.devicePixelRatio || 1;
const screenWidth = window.innerWidth
if (screenWidth <= 950) {
// 小屏幕设备
return screenWidth / 750;
} else {
return 0.5
}
}
if (uni?.rpx2px) {
r = uni.rpx2px(n)
} else {
r = getLayoutRatio() * n
}
// #endif
return r
}
function px2dp(n : number) : number {
let w = n;
// #ifdef APP
const mets = UTSAndroid.getAppContext()!.resources!.getDisplayMetrics()
// 屏幕逻辑像素的宽度
let width = mets.widthPixels;
// 屏幕宽度
let screenWidth = uni.getWindowInfo().screenWidth
w = n / (width / screenWidth);
// #endif
return w
}
function checkIsCssUnit(str : string|number, unit : string) : string {
if(typeof str != 'string'){
return (str as number).toString() + unit;
}
let s = str as string;
if (s.indexOf("px") > -1 || s.indexOf("%") > -1 || s.indexOf("auto") > -1 || s.indexOf("vw") > -1 || s.indexOf("vh") > -1) {
return s;
}
return s + (unit==''?'px':unit)
}
// function checkIsCssUnit(str : string|number, unit : string) : string {
// let screenWidth = uni.getWindowInfo().windowWidth;
// let base = 0;
// let baseUnit = unit == ''?xConfig.unit:unit;
// if(typeof str != 'string'){
// base = str as number;
// }else if(typeof str == 'string'){
// let s = str as string;
// if (
// s.indexOf("px") > -1 ||
// s.indexOf("rpx") > -1 ||
// s.indexOf("%") > -1 ||
// s.indexOf("auto") > -1 ||
// s.indexOf("vw") > -1 ||
// s.indexOf("rem") > -1 ||
// s.indexOf("em") > -1 ||
// s.indexOf("in") > -1 ||
// s.indexOf("vh") > -1)
// {
// return s;
// }
// base = parseFloat(s);
// base = isNaN(base)?0:base
// }
// if(baseUnit=='rpx'){
// let baseDesize = Math.max(xConfig.designSize,375)
// let baseMaxWidth = Math.max(xConfig.maximumCalculatedSize,375)
// let origSize = 375;
// let ratio = 1 - baseDesize / origSize;
// let maxRatio = screenWidth / baseMaxWidth;
// let baseSize = base - (ratio*base)
// // 说明超过了设定缩放的最大屏幕尺寸。
// if(maxRatio>=1){
// base = baseSize;
// }else{
// const calcRatio = Math.max(screenWidth/baseDesize,1)
// base = calcRatio*baseSize ;
// }
// let baseunitReal = 'px'
// // #ifdef H5
// if(base<12){
// base = base/16
// baseunitReal = "rem"
// }
// // #endif
// return base + baseunitReal
// }
// return base + baseUnit
// }
function fillArrayCssValue(val : Array<string>) : string[] {
let ar : string[] = val.map((el : string) : string => {
return checkIsCssUnit(el, xConfig.unit)
})
if (ar.length == 0) return [];
if (ar.length == 1) return [ar[0], ar[0], ar[0], ar[0]]
if (ar.length == 2) {
return [ar[1], ar[0], ar[1], ar[0]]
}
if (ar.length == 3) return [ar[1], ar[2], '0px', ar[0]]
return [ar[1], ar[2], ar[3], ar[0]];
}
function fillArrayCssValueByround(val : Array<string>) : string[] {
let ar : string[] = val.map((el : string) : string => {
return checkIsCssUnit(el, xConfig.unit)
})
if (ar.length == 0) return [];
if (ar.length == 1) return [ar[0], ar[0], ar[0], ar[0]]
if (ar.length == 2) {
return [ar[1], ar[0], ar[1], ar[0]]
}
if (ar.length == 3) return [ar[0], ar[1], ar[0], '0px']
return [ar[0], ar[1], ar[2], ar[3]];
}
function fillArrayCssValueBycolor(val : Array<string>) : string[] {
let ar : string[] = val.map((el : string) : string => {
return getDefaultColor(el)
})
if (ar.length == 0) return [];
if (ar.length == 1) return [ar[0], ar[0], ar[0], ar[0]]
if (ar.length == 2) {
return [ar[1], ar[0], ar[1], ar[0]]
}
if (ar.length == 3) return [ar[1], ar[2], '0px', ar[0]]
return [ar[1], ar[2], ar[3], ar[0]];
}
/**
* 对数组进行分组。按数量分组
*/
function splitArray<T>(target : Array<T>, value : number) : Array<Array<T>> {
var result = [] as Array<Array<T>>;
for (var i = 0; i < target.length; i += value) {
let ml = target.slice(i, i + value);
result.push(ml);
}
return result;
}
/**
* 对数组进行分组。按指定组数量
*/
function splitArrayByGroup<T>(target : Array<T>, group : number) : Array<Array<T>> {
var groupSize = Math.ceil(target.length / group);
var result = [] as Array<Array<T>>;
for (var i = 0; i < target.length; i += groupSize) {
result.push(target.slice(i, i + groupSize));
}
return result;
}
/**
* 获取字符的css单位。
*/
function getUnit(n ?: string) : string {
if (n == null || n == '') return xConfig.unit;
let unit = n.replace(/[\d|\-|\+|\.]/g, '');
if (unit == "") {
unit = xConfig.unit;
}
return unit;
}
/**
* 设置当前页面是否刷新。
*/
function setPagePullRefresh(enbledpull : boolean) {
// #ifdef APP
let pages = getCurrentPages()
let page = pages[pages.length - 1]
// let pageJson = page.$getPageStyle()
// pageJson.set("enablePullDownRefresh",enbledpull)
page.$setPageStyle({ "enablePullDownRefresh": enbledpull } as UTSJSONObject)
// #endif
}
/**
* 获取当前的下拉状态。
*/
function getPagePullRefresh() : boolean {
// #ifdef APP
let pages = getCurrentPages()
let page = pages[pages.length - 1]
let pageJson = page.$getPageStyle()
let enb = pageJson.get("enablePullDownRefresh") as boolean|null;
if(enb==null) return false;
return enb as boolean;
// #endif
return false;
}
/**
* 防抖
*/
function debounce(
func : (args : any) => void,
wait : number,
immediate ?: boolean
) : (args : any) => number | null {
let timeout : number | null = null;
let callNow = immediate == null ? true : (immediate! as boolean);
// 返回的函数是实际被调用的防抖函数
return (args : any) : number | null => {
if (timeout != null && callNow == true) {
return timeout;
}
if (timeout != null) {
clearTimeout(timeout! as number);
timeout = null;
}
// 决定是否立即执行函数
if (callNow == true) {
func(args)
timeout = setTimeout(() => {
timeout = null;
}, wait);
return timeout;
}
// 否则,设置定时器以稍后执行函数
timeout = setTimeout(() => {
func(args)
}, wait);
return timeout
}
}
export {
toHex,
toFillMarginAr,
getUid,
checkIsCssUnit,
px2dp,
rpx2px,
splitArray,
splitArrayByGroup,
fillArrayCssValue,
fillArrayCssValueByround,
fillArrayCssValueBycolor,
getUnit,
setPagePullRefresh,
getPagePullRefresh,
debounce
}
+545
View File
@@ -0,0 +1,545 @@
# xDate
### 开发文档
[TMUI4.0文档](https://xui.tmui.design/)
[TMUI4.0组件库](https://ext.dcloud.net.cn/plugin?id=16369)
日期处理库
### 说明
这是tmui4.0|XUI的日期处理库,提供了丰富的日期操作功能。简单好上手,单库只有14kb,能满足大部分日期处理需求。库采用类Jq式的链式调用,使用灵活方便。
### 兼容性
| IOS | Android | WEB | 小程序 |
| --- | --- | --- | --- |
| 支持 | 支持 | 支持 | 支持 |
### 导入使用
在页面中直接使用快捷导入即可使用,它是class类,需要new
```ts
import {xDate, createDate} from "@/uni_modules/tmx-ui/index.uts"
// 打印当前
console.log(new xDate().date)
// 辅助函数用来创建系统的Date对象时间
console.log(createDate('2023-3-1'))
```
### 创建实例
支持string, number, Date, null四种类型
```ts
import {xDate, createDate} from "@/uni_modules/tmx-ui/index.uts"
// 为空,默认是现在
const now = new xDate()
// 传递字符串的毫秒数
const date1 = new xDate('15888888')
// 传递字符串的时间
const date2 = new xDate('2023-2-3')
const date3 = new xDate('2023/2/3')
// 传递Date对象
const date4 = new xDate(new Date())
```
### 实例方法
| 名称 | 参数 | 介绍 |
| --- | --- | --- |
| format | s: string \| null = null | 格式化日期,默认格式为YYYY/MM/DD hh:mm:ss |
| getYear | 无 | 获取当前年份 |
| getMonth | 无 | 获取当前月份(0-11,0代表1月) |
| getMonthName | useShort: boolean = false | 获取当前月份的名称(支持国际化) |
| getDate | 无 | 获取当前日(1-31 |
| getHours | 无 | 获取当前小时(0-23 |
| getMinutes | 无 | 获取当前分钟(0-59 |
| getSeconds | 无 | 获取当前秒(0-59 |
| getDateOf | d: xDateTypeTime = 'd' | 获取指定类型的数据,d可为'y'年,'m'月,'d'日,'h'时,'M'分,'s'秒,'ms'毫秒,'w'周 |
| setDateOf | n: number, d: xDateTypeTime = 'd' | 设定日期,支持链式调用 |
| getTime | d: xDateTypeTime | 获取时间戳,可指定单位 |
| getClone | 无 | 获取当前时间的副本 |
| getWeek | 无 | 获取本年的第几周 |
| getDateStartOf | d: 'm' \| 'w' \| 'y' = 'm' | 获取时间的第一天(月/周/年) |
| getDateEndOf | d: 'm' \| 'w' \| 'y' = 'm' | 获取时间的最后一天(月/周/年) |
| getWeekDay | 无 | 获取星期几(0-6,0表示星期天) |
| getWeekDayCn | model: string[] \| null = null, useShort: boolean = true | 获取星期名称(支持国际化) |
| getMonthCountDay | 无 | 获取当前月份的最大天数 |
| getDateInfo | str: string \| null = null | 获取日期详细信息 |
| getDaysOf | d: 'm' \| 'w' = 'm' | 获取日期数组(月/周) |
| add | count: number, d: xDateTypeTime = 'd' | 日期加法操作,支持链式调用 |
| subtraction | count: number, d: xDateTypeTime = 'd' | 日期减法操作,支持链式调用 |
| isBetween | start: xDate, end: xDate, type: xDateTypeTime = 'ms', d: '()' \| '[]' \| '(]' \| '[)' = '[]' | 判断日期是否在区间内 |
| isBetweenOf | targetDate: xDate, d: '>' \| '>=' \| '<' \| '<=' \| '=' = '>', type: xDateTypeTime = 'ms' | 与目标日期比较 |
| fromBetweenLongTime | target: any, model: Map<number, string> \| null, format: string \| null | 计算与目标时间过了多久(支持国际化) |
| diffTime | target: any, type: xDateTypeTime = 's' | 计算与目标时间相差多少个单位 |
| getQuarter | type: string = '' | 获取季度信息 |
| setDateLocale | locale: xDateLanguage | 设置xDate的语言 |
| getDateLocale | 无 | 获取当前xDate使用的语言 |
### 类型定义
```ts
// 日期时间单位类型
export type xDateTypeTime = 'y' | 'm' | 'd' | 'h' | 'M' | 's' | 'ms' | 'w'
// 季度信息类型
export type xDateTypeQuarter = {
quarter : number,
start : string,
end : string
}
// 日期格式类型
export type DateFormat = 'RFC2822' | 'ISO8601' | 'CUSTOM';
// 日期信息类型
export type xDateDayInfoType = {
year: number,
month: number,
day: number,
hours: number,
minutes: number,
seconds: number,
week: number,
weeks: number,
weekCn: string,
date: string
}
// 支持的语言类型
export type xDateLanguage = 'zh-Hans' | 'en' | 'ja' | 'ko' | 'zh-Hant' | 'fr' | 'ru';
// 国际化相对时间配置类型
export type xDateI18nTypeRelativeTime = {
future: string,
past: string,
s: string,
m: string,
mm: string,
h: string,
hh: string,
d: string,
dd: string,
M: string,
MM: string,
y: string,
yy: string
}
// 国际化配置类型
export type xDateI18nType = {
weekdays: string[],
weekdaysShort: string[],
months: string[],
monthsShort: string[],
meridiem: (hour: number, minute: number, isLowercase: boolean) => string,
relativeTime: xDateI18nTypeRelativeTime
}
```
### 国际化相关API
```ts
// 设置xDate的语言
setDateLocale(locale: xDateLanguage): void
// 获取当前xDate使用的语言
getDateLocale(): xDateLanguage
// 获取当前月份的名称
getMonthName(useShort: boolean = false): string
// 获取星期名称(支持国际化)
getWeekDayCn(model: string[] | null = null, useShort: boolean = true): string
```
```
### API说明
#### 辅助函数
##### createDate
```ts
function createDate(dateStrs: string): Date
```
- 描述:用来解析非标准时间以及各种奇怪的时间格式
- 参数:
- dateStrs: 日期字符串,支持多种格式如YYYY、YYYY-MM、YYYY-MM-DD等
- 返回值:Date对象
##### dateCovertXdate
```ts
function dateCovertXdate(date: Date): xDate
```
- 描述:将一个日期转换为xDate对象
- 参数:
- date: Date对象
- 返回值:xDate对象
#### xDate类方法
##### 构造函数
```ts
constructor(dateStr: string | number | Date | null = null)
```
- 描述:创建xDate实例
- 参数:
- dateStr: 可以是字符串、数字、Date对象或null(默认为当前时间)
##### detectDateFormat
```ts
detectDateFormat(dateStr: string): DateFormat
```
- 描述:检测字符串的日期格式类型
- 参数:
- dateStr: 日期字符串
- 返回值:日期格式类型('RFC2822'、'ISO8601'或'CUSTOM'
##### format
```ts
format(s: string | null = null): string
```
- 描述:格式化日期
- 参数:
- s: 模板,比如YYYY/MM/DD hh:mm:ss,默认为YYYY/MM/DD hh:mm:ss
- 返回值:格式化后的日期字符串
##### getYear
```ts
getYear(): number
```
- 描述:获取当前年
- 返回值:根据当地时间,返回一个对应于给定日期的年份数字
##### getMonth
```ts
getMonth(): number
```
- 描述:获取当前月(0-11,0代表1月)
- 返回值:一个0到11的整数值
##### getMonthName
```ts
getMonthName(useShort: boolean = false): string
```
- 描述:获取当前月份的名称(支持国际化)
- 参数:
- useShort: 是否使用短名称,默认为false
- 返回值:返回当前语言环境下的月份名称
##### getDate
```ts
getDate(): number
```
- 描述:获取当前天
- 返回值:返回一个1到31的整数值
##### getHours
```ts
getHours(): number
```
- 描述:获取当前小时
- 返回值:返回一个0到23之间的整数值
##### getMinutes
```ts
getMinutes(): number
```
- 描述:获取当前分钟
- 返回值:返回一个0到59的整数值
##### getSeconds
```ts
getSeconds(): number
```
- 描述:获取当前秒
- 返回值:返回一个0到59的整数值
##### getDateOf
```ts
getDateOf(d: xDateTypeTime = 'd'): number
```
- 描述:获取指定类型的数据
- 参数:
- d: 'y'年 'm'月 'd'日 'h'时 'M'分 's'秒 'ms'毫秒 'w'周
- 返回值:指定类型的数值
##### setDateOf
```ts
setDateOf(n: number, d: xDateTypeTime = 'd'): xDate
```
- 描述:设定日期
- 参数:
- n: 设定的数据
- d: 比如d=y,那么就是设置本日期的年份数据
- 返回值:xDate对象实例(支持链式调用)
##### getTime
```ts
getTime(d: xDateTypeTime): number
```
- 描述:获取时间戳
- 参数:
- d: 'y'年 'm'月 'd'日 'h'时 'M'分 's'秒 'ms'毫秒
- 返回值:返回从UTC时间1970年1月1日午夜开始以毫秒为单位存储的时间值
- 注意:如果不提供d就默认返回毫秒,比如设置d=m那么后面的h,M,s,ms会被设置为0
##### getClone
```ts
getClone(): xDate
```
- 描述:当前时间的副本
- 返回值:新的xDate对象
##### getWeek
```ts
getWeek(): number
```
- 描述:本年的第几周
- 返回值:周次
##### getDateStartOf
```ts
getDateStartOf(d: 'm' | 'w' | 'y' = 'm'): xDate
```
- 描述:取时间的第一天
- 参数:
- d: 'm'表示取本月的第一天,'w'取本周的第一天,'y'表示本年的第一天
- 返回值:返回xDate对象
##### getDateEndOf
```ts
getDateEndOf(d: 'm' | 'w' | 'y' = 'm'): xDate
```
- 描述:取时间的最后一天
- 参数:
- d: 'm'表示取本月的最后一天,'w'取本周的最后一天,'y'表示本年的最后一天
- 返回值:返回xDate对象
##### getWeekDay
```ts
getWeekDay(): number
```
- 描述:根据本地时间,返回一个具体日期中一周的第几天,0表示星期天
- 返回值:根据本地时间,返回一个0到6之间的整数值
##### getWeekDayCn
```ts
getWeekDayCn(model: string[] | null = null, useShort: boolean = true): string
```
- 描述:获取星期名称(支持国际化)
- 参数:
- model: 具有星期的中文模板,请按照顺序放置如["周日","周一","周二","周三","周四","周五","周六"]
- useShort: 是否使用短名称,默认为true
- 返回值:返回当前语言环境下的星期名称
##### getMonthCountDay
```ts
getMonthCountDay(): number
```
- 描述:返回当前日期本月的最大天数
- 返回值:当前月份的最大天数
##### getDateInfo
```ts
getDateInfo(str: string | null = null): xDateDayInfoType
```
- 描述:返回当日的信息
- 参数:
- str: 日期字符串,为null时使用当前xDate实例
- 返回值:返回一个包含年月日周次,星期,农历等的对象
##### getDaysOf
```ts
getDaysOf(d: 'm' | 'w' = 'm'): xDateDayInfoType[]
```
- 描述:返回日期数组
- 参数:
- d: 'm'表示返回本月的日期数据,'w'表示返回本周的日期数据
- 返回值:返回一个日期数组
##### add
```ts
add(count: number, d: xDateTypeTime = 'd'): xDate
```
- 描述:为当前日期进行加操作
- 参数:
- count: 要增加的数量
- d: 'y'年 'm'月 'd'日 'h'时 'M'分 's'秒 'w'周
- 返回值:xDate对象(支持链式调用)
##### subtraction
```ts
subtraction(count: number, d: xDateTypeTime = 'd'): xDate
```
- 描述:为当前日期进行减操作
- 参数:
- count: 要减少的数量
- d: 'y'年 'm'月 'd'日 'h'时 'M'分 's'秒 'w'周
- 返回值:xDate对象(支持链式调用)
##### isBetween
```ts
isBetween(start: xDate, end: xDate, type: xDateTypeTime = 'ms', d: '()' | '[]' | '(]' | '[)' = '[]'): boolean
```
- 描述:日期是否在一个区间内
- 参数:
- start: 开始日期
- end: 结束日期
- type: 要比较的单位,默认为ms
- d: 区间类型,'()'不含起始'[]'包含起始'(]'不包含开始但包含结束'[)'包含开始但不包含结束
- 返回值:是否在区间内
##### isBetweenOf
```ts
isBetweenOf(targetDate: xDate, d: '>' | '>=' | '<' | '<=' | '=' = '>', type: xDateTypeTime = 'ms'): boolean
```
- 描述:与目标日期比较
- 参数:
- targetDate: 要比较的日期
- d: 比较类型,'>'大于目标日期,'>='大于等于目标'<'小于目标,'<='小于等于目标,'='全等
- type: 要比较的单位
- 返回值:比较结果
##### fromBetweenLongTime
```ts
fromBetweenLongTime(target: any, model: Map<number, string> | null, format: string | null): string
```
- 描述:与目标时间过了多久(支持国际化)
- 参数:
- target: 要对比的相对时间,如果填写null就表示当前时间以来的的多久
- model: 如果为null或者为空的map采用默认值,map格式key表示以秒为单位的时间,value为对应的文本
- format: 超过最大值最使用格式化日期,如果为null默认为YYYY-MM-DD
- 返回值:返回对应的时间文本
##### diffTime
```ts
diffTime(target: any, type: xDateTypeTime = 's'): number
```
- 描述:与目标时间相差多少个单位
- 参数:
- target: 目标时间
- type: 相差的单位,默认:s秒
- 返回值:返回以xDateTypeTime为单位相关的时间数
##### getQuarter
```ts
getQuarter(type: string = ''): xDateTypeQuarter[]
```
- 描述:获取季度
- 参数:
- type: 'y'表示获取本年的4个季度,空值表示获取当前时间所在的季度
- 返回值:返回一个季度数组{quarter:number,start:string,end:string}
##### setDateLocale
```ts
setDateLocale(locale: xDateLanguage): void
```
- 描述:设置xDate的语言
- 参数:
- locale: 语言代码,支持'zh-Hans'(中文简体)、'en'(英文)、'ja'(日文)、'ko'(韩文)、'zh-Hant'(繁体中文)、'fr'(法语)、'ru'(俄语)
- 返回值:无
##### getDateLocale
```ts
getDateLocale(): xDateLanguage
```
- 描述:获取当前xDate使用的语言
- 返回值:当前语言代码
### 使用示例
#### 基本使用
```ts
import {xDate, createDate} from "@/uni_modules/tmx-ui/index.uts"
// 创建日期实例
const date = new xDate('2023-05-15')
// 格式化日期
console.log(date.format('YYYY年MM月DD日')) // 输出: 2023年05月15日
// 获取年月日
console.log(date.getYear()) // 输出: 2023
console.log(date.getMonth()) // 输出: 4 (5月)
console.log(date.getDate()) // 输出: 15
// 链式调用
const nextWeek = date.getClone().add(1, 'w')
console.log(nextWeek.format()) // 输出下周同一天的日期
// 获取月份天数
console.log(date.getMonthCountDay()) // 输出: 31 (5月有31天)
// 获取日期信息
const info = date.getDateInfo()
console.log(info.weekCn) // 输出: 周一 (假设5月15日是周一)
// 日期比较
const date1 = new xDate('2023-01-01')
const date2 = new xDate('2023-12-31')
console.log(date.isBetween(date1, date2)) // 输出: true
```
#### 高级用法
```ts
import {xDate} from "@/uni_modules/tmx-ui/index.uts"
// 获取本月所有日期
const currentDate = new xDate()
const daysInMonth = currentDate.getDaysOf('m')
console.log(`本月共有 ${daysInMonth.length}`)
// 获取本周日期
const daysInWeek = currentDate.getDaysOf('w')
console.log(`本周的第一天是 ${daysInWeek[0].date}`)
// 获取季度信息
const quarters = currentDate.getQuarter('y')
console.log(`今年第一季度: ${quarters[0].start}${quarters[0].end}`)
// 计算两个日期之间的差距
const birthday = new xDate('1990-01-01')
const daysDiff = currentDate.diffTime(birthday, 'd')
console.log(`距离生日已经过去了 ${daysDiff}`)
// 友好的时间显示
const pastTime = new xDate().subtraction(3, 'h')
const timeText = currentDate.fromBetweenLongTime(pastTime, null, null)
console.log(timeText) // 输出: 3小时前
```
#### 国际化使用示例
```ts
import {xDate} from "@/uni_modules/tmx-ui/index.uts"
// 创建日期实例
const date = new xDate('2023-05-15')
// 默认使用中文简体
console.log(date.getWeekDayCn()) // 输出: 周一 (假设5月15日是周一)
console.log(date.getMonthName()) // 输出: 五月
// 切换到英文
date.setDateLocale('en')
console.log(date.getWeekDayCn()) // 输出: Mon
console.log(date.getMonthName()) // 输出: May
console.log(date.getMonthName(false)) // 输出: May (完整名称)
// 切换到日文
date.setDateLocale('ja')
console.log(date.getWeekDayCn()) // 输出: 月
console.log(date.getMonthName()) // 输出: 5月
// 相对时间显示也会根据语言环境变化
const currentDate = new xDate()
const pastTime = new xDate().subtraction(3, 'h')
// 中文环境
currentDate.setDateLocale('zh-Hans')
console.log(currentDate.fromBetweenLongTime(pastTime, null, null)) // 输出: 3小时前
// 英文环境
currentDate.setDateLocale('en')
console.log(currentDate.fromBetweenLongTime(pastTime, null, null)) // 输出: 3 hours ago
// 获取当前语言设置
console.log(currentDate.getDateLocale()) // 输出: en
```
+873
View File
@@ -0,0 +1,873 @@
import { xDateDayInfoType } from "../../interface.uts"
import { getCurrentLocale, getLocaleData, setLocale, xDateLanguage } from "./xDateI18n.uts"
export type xDateTypeTime = 'y' | 'm' | 'd' | 'h' | 'M' | 's' | 'ms' | 'w'
export type xDateTypeQuarter = {
quarter : number,
start : string,
end : string
}
export type DateFormat = 'RFC2822' | 'ISO8601' | 'CUSTOM';
/**
* 用来解析非标准时间以及各种奇怪的时间格式。
*/
export function createDate(dateStrs : string) : Date {
const dateStr = dateStrs.replace(/\//g, '-')
const result = new Date();
// YYYY,YYYY-MM,YYYY-MM-DD,YYYY-MM-DD HH,YYYY-MM-DD HH:mm,YYYY-MM-DD HH:mm:ss
let regxyy = /^(\d{4})$/
let regxyymm = /^(\d{4})[-/](\d{1,2})$/
let regxyymmdd = /^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$/
let regxyymmddhh = /^(\d{4})[-/](\d{1,2})[-/](\d{1,2}) (\d{1,2})$/
let regxyymmddhhmm = /^(\d{4})[-/](\d{1,2})[-/](\d{1,2}) (\d{1,2}):(\d{1,2})$/
let regxyymmddhhmmss = /^(\d{4})[-/](\d{1,2})[-/](\d{1,2}) (\d{1,2}):(\d{1,2}):(\d{1,2})$/
let year = result.getFullYear();
let month = result.getMonth() - 1
let day = result.getDate()
let hour = result.getHours()
let minute = result.getMinutes()
let second = result.getSeconds()
result.setSeconds(59);
result.setMinutes(59);
result.setHours(59);
result.setDate(1);
if (regxyymmddhhmmss.test(dateStr)) {
const match = dateStr.match(regxyymmddhhmmss)!;
year = (parseInt(match[1] as string));
month = (parseInt(match[2] as string) - 1);
day = (parseInt(match[3] as string));
hour = (parseInt(match[4] as string));
minute = (parseInt(match[5] as string));
second = (parseInt(match[6] as string));
} else if (regxyymmddhhmm.test(dateStr)) {
const match = dateStr.match(regxyymmddhhmm)!;
year = (parseInt(match[1] as string));
month = (parseInt(match[2] as string) - 1);
day = (parseInt(match[3] as string));
hour = (parseInt(match[4] as string));
minute = (parseInt(match[5] as string));
} else if (regxyymmddhh.test(dateStr)) {
const match = dateStr.match(regxyymmddhh)!;
year = (parseInt(match[1] as string));
month = (parseInt(match[2] as string) - 1);
day = (parseInt(match[3] as string));
hour = (parseInt(match[4] as string));
} else if (regxyymmdd.test(dateStr)) {
const match = dateStr.match(regxyymmdd)!;
year = (parseInt(match[1] as string));
month = (parseInt(match[2] as string) - 1);
day = (parseInt(match[3] as string));
} else if (regxyymm.test(dateStr)) {
const match = dateStr.match(regxyymm)!;
year = (parseInt(match[1] as string));
month = (parseInt(match[2] as string) - 1);
} else if (regxyy.test(dateStr)) {
const match = dateStr.match(regxyy)!;
year = (parseInt(match[1] as string));
}
result.setSeconds(second);
result.setMinutes(minute);
result.setHours(hour);
result.setFullYear(year);
result.setMonth(month);
result.setDate(day);
return result;
}
/**
* 日期库 xDate
*/
export class xDate {
date : Date;
/**
* dateStr,可能是数字,字符串,Date对象
*/
constructor(dateStr : string | number | Date | null = null) {
this.date = this.checkDate(dateStr)
}
private checkDate(dateStr : string | number | Date | null = null) : Date {
let tempDate = new Date();
if (dateStr == null) {
return tempDate;
}
if (typeof dateStr == 'number') {
return new Date(dateStr! as number);
} else if (typeof dateStr == 'string') {
if (dateStr == '') {
return tempDate;
}
const dateformatStr = this.detectDateFormat(dateStr!)
if (dateformatStr == 'CUSTOM') {
let str = dateStr! as string;
str = str.replace(/-/g, '/')
let isNumberStr = /^\d+$/.test(str)
if (!isNumberStr) {
return createDate(dateStr!)
} else {
return new Date(parseInt(str!));
}
} else {
return new Date(dateStr! as string);
}
} else if (dateStr instanceof Date) {
return dateStr! as Date;
}
return tempDate;
}
/**
* 检测字符串的日期格式类型
* @param dateStr - 日期字符串
* @returns 日期格式类型
*/
detectDateFormat(dateStr : string) : DateFormat {
// RFC2822 格式检测
if (/^(?:\w{3},\s)?(?:\d{1,2}\s(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}(?::\d{2})?(?:\sGMT)?)|(?:\w{3}\s(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{1,2}\s\d{2}:\d{2}(?::\d{2})?\s\d{4})/.test(dateStr)) {
return 'RFC2822';
}
// ISO8601 格式检测
if (/^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/.test(dateStr)) {
return 'ISO8601';
}
return 'CUSTOM';
}
/**
* 给数字添加前缀0
*/
translateFullDate(d : number, w : number) : string {
let dstr = d.toString();
if (dstr.length < w) {
let dar : number[] = new Array(w - dstr.length).fill(0);
dstr = dar.join('') + dstr
}
return dstr;
}
/**
* 格式化日期
* @param {string} s 模板,比如YYYY/MM/DD hh:mm:ss
* @returns {string} 格式化后的日期字符串
*/
format(s : string | null = null) : string {
let str = "YYYY/MM/DD hh:mm:ss"
if (s != null) {
str = s;
}
str = str.replace(/YYYY/g, this.translateFullDate(this.date.getFullYear(), 2))
str = str.replace(/MM/g, this.translateFullDate(this.date.getMonth() + 1, 2))
str = str.replace(/DD/g, this.translateFullDate(this.date.getDate(), 2))
str = str.replace(/hh/g, this.translateFullDate(this.date.getHours(), 2))
str = str.replace(/mm/g, this.translateFullDate(this.date.getMinutes(), 2))
str = str.replace(/ss/g, this.translateFullDate(this.date.getSeconds(), 2))
return str;
}
/**
* 获取当前年
* @return — 根据当地时间,返回一个对应于给定日期的年份数字。
*/
getYear() : number {
return this.date.getFullYear()
}
/**
* 获取当前月
* @description 是从0开始,1-11月
* @return — 一个 0 到 11 的整数值:0 代表一月份,1 代表二月份,2 代表三月份,依次类推
*/
getMonth() : number {
return this.date.getMonth()
}
/**
* 获取当前月份的名称
* @param {boolean} useShort - 是否使用短名称
* @return — 返回当前语言环境下的月份名称
*/
getMonthName(useShort : boolean = false) : string {
const monthIndex = this.date.getMonth();
return useShort ?
getLocaleData().monthsShort[monthIndex] :
getLocaleData().months[monthIndex];
}
/**
* 获取当前天
* @return — 返回一个 1 到 31 的整数值。
*/
getDate() : number {
return this.date.getDate()
}
/**
* 获取当前小时
* @return — 返回一个 0 到 23 之间的整数值。
*/
getHours() : number {
return this.date.getHours()
}
/**
* 获取当前分钟
* @return — 返回一个 0 到 59 的整数值。
*/
getMinutes() : number {
return this.date.getMinutes()
}
/**
* 获取当前秒
* @return — 返回一个 0 到 59 的整数值。
*/
getSeconds() : number {
return this.date.getSeconds()
}
/**
* 获取指定是数据
* @param {xDateTypeTime} d - 'y' 年 'm' 月 'd' 日 'h' 时 'M' 分 's' 秒 'ms' 毫秒
* @returns - 指定类型的数值
*/
getDateOf(d : xDateTypeTime = 'd') : number {
if (d == 'y') {
return this.date.getFullYear()
}
else if (d == 'm') {
return this.date.getMonth()
}
else if (d == 'd') {
return this.date.getDate()
}
else if (d == 'h') {
return this.date.getHours()
}
else if (d == 'M') {
return this.date.getMinutes()
}
else if (d == 's') {
return this.date.getSeconds()
}
return this.date.getMilliseconds()
}
/**
* 设定日期
* @param {number} n - 设定的数据,
* @param {string} d - 比如d=y,那么就是设置本日期的年份数据,'y' 年 'm' 月 'd' 日 'h' 时 'M' 分 's' 秒 'ms' 毫秒
* @returns - xDate对象实例
*/
setDateOf(n : number, d : xDateTypeTime = 'd') : xDate {
if (d == 'y') {
this.date.setFullYear(n)
}
else if (d == 'm') {
this.date.setMonth(n)
}
else if (d == 'd') {
this.date.setDate(n)
}
else if (d == 'h') {
this.date.setHours(n)
}
else if (d == 'M') {
this.date.setMinutes(n)
}
else if (d == 's') {
this.date.setSeconds(n)
}
else if (d == 'ms') {
this.date.setMilliseconds(n)
}
return this;
}
/**
* @param {string} - d:'y' 年 'm' 月 'd' 日 'h' 时 'M' 分 's' 秒 'ms' 毫秒
* @returns - 返回从UTC时间1970年1月1日午夜开始以毫秒为单位存储的时间值。
* @description 注意,如果不提供d就默认返回毫秒,比如设置d=m那么后面的h,M,s,ms会被设置为0
*/
getTime(d : xDateTypeTime) : number {
let date = new Date(this.date.getTime())
if (d == 'y') {
date.setMonth(0)
date.setDate(1)
date.setHours(0)
date.setMinutes(0)
date.setSeconds(0)
date.setMilliseconds(0)
return date.getTime()
}
else if (d == 'm') {
date.setDate(1)
date.setHours(0)
date.setMinutes(0)
date.setSeconds(0)
date.setMilliseconds(0)
return date.getTime()
}
else if (d == 'd') {
date.setHours(0)
date.setMinutes(0)
date.setSeconds(0)
date.setMilliseconds(0)
return date.getTime()
}
else if (d == 'h') {
date.setMinutes(0)
date.setSeconds(0)
date.setMilliseconds(0)
return date.getTime()
}
else if (d == 'M') {
date.setSeconds(0)
date.setMilliseconds(0)
return date.getTime()
}
else if (d == 's') {
date.setMilliseconds(0)
return date.getTime()
}
return date.getTime()
}
/**
* 当前时间的副本
*/
getClone() : xDate {
return new xDate(this.format());
}
/**
* 本年的第几周
* @return 周次
*/
getWeek() : number {
let target = new Date(this.format())
target.setDate(target.getDate() - (target.getDay() == 0 ? 7 : target.getDay()));
let firstDayOfYear = new Date(target.getFullYear(), 0, 1);
firstDayOfYear.setDate(firstDayOfYear.getDate() - (firstDayOfYear.getDay() == 0 ? 7 : firstDayOfYear.getDay()));
return Math.ceil((((target.getTime() - firstDayOfYear.getTime()) / 86400000) + 1) / 7);
}
/**
* 取时间的第一天
* @param {m,w,y} - m表示取本月的第一天,w取本周的第一天,y表示本年的第一天
* @returns {xDate} - 返回xDate对象
*/
getDateStartOf(d : 'm' | 'w' | 'y' = 'm') : xDate {
// 获取当前日期对象
let now = new Date(this.format())
if (d == 'w') {
// 将日期设置为所在周的周一(遵循ISO 8601标准,周一开始于周一)
let dayOfWeek = now.getDay();
if (dayOfWeek === 0) { // 如果是星期日,则向前推一天至上周的周一
now.setDate(now.getDate() - 7);
}
now.setDate(now.getDate() - dayOfWeek + 1);
}
if (d == 'm' || d == 'y') {
now.setDate(1);
}
if (d == 'y') {
now.setMonth(0);
}
now.setHours(0);
now.setMinutes(0);
now.setSeconds(0);
return dateCovertXdate(now);
}
/**
* 取时间的最后一天
* @param {string} - m,w,y m表示取本月的最后一天,w取本周的最后一天,y表示本年的最后一天
* @returns {xDate} - 返回xDate对象
*/
getDateEndOf(d : 'm' | 'w' | 'y' = 'm') : xDate {
// 获取当前日期对象
let now = new Date(this.format())
if (d == 'w') {
// 将日期设置为所在周的周一(遵循ISO 8601标准,周一开始于周一)
let dayOfWeek = now.getDay();
if (dayOfWeek === 0) { // 如果是星期日,则向前推一天至上周的周一
return dateCovertXdate(now);
}
now.setDate(now.getDate() + (7 - dayOfWeek));
}
if (d == 'm') {
now.setDate(this.getMonthCountDay());
}
if (d == 'y') {
// 先设置月份为12月,再设置为该月的最后一天
now.setMonth(11);
// 设置为12月的最后一天
now.setDate(31);
}
now.setHours(23);
now.setMinutes(59);
now.setSeconds(59);
return dateCovertXdate(now);
}
/**
* 根据本地时间,返回一个具体日期中一周的第几天,0 表示星期天。
* @return — 根据本地时间,返回一个 0 到 6 之间的整数值,代表星期几:0 代表星期日,1 代表星期一,2 代表星期二,依次类推。
*/
getWeekDay() : number {
return this.date.getDay()
}
/**
* 根据本地时间,返回一个具体日期中一周的第几天,0 表示星期天。
* @param {string[]} model 具有星期的中文模板,请按照顺序放置如["周日","周一","周二","周三","周四","周五","周六"]
* @param {boolean} useShort 是否使用短名称
* @return — 返回一个当前语言环境下的星期名称
*/
getWeekDayCn(model : string[] | null = null, useShort : boolean = true) : string {
let ml = useShort ? getLocaleData().weekdaysShort : getLocaleData().weekdays;
if (model != null && model?.length == 7) {
ml = model!;
}
return ml[this.getWeekDay()]!
}
/**
* 返回当前日期本月的最大天数
* @returns - 当前月份的最大天数
*/
getMonthCountDay() : number {
let nextDate = new Date(this.format())
// 先设置为1号,避免后面设置月份时超过月的天数。
nextDate.setDate(1)
nextDate.setMonth(this.getMonth() + 1)
// 设置为上月的最后一天。
nextDate.setDate(0)
return nextDate.getDate()
}
/**
* 返回当日的信息
* @returns - 返回一个包含年月日周次,星期,农历等的对象
*/
getDateInfo(str : string | null = null) : xDateDayInfoType {
let date = this as xDate
if (str != null) {
date = new xDate(str)
}
let info = {
year: date.getYear(),
month: date.getMonth(),
day: date.getDate(),
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds(),
week: date.getWeekDay(),
weeks: date.getWeek(),
weekCn: date.getWeekDayCn(),
date: ""
} as xDateDayInfoType
info.date = info.year + '/' + (info.month + 1) + '/' + info.day
return info
}
/**
* 返回日期数组
* @param {string} - m,w,m表示返回本月的日期数据,w表示返回本周的日期数据
* @returns - 返回一个日期数组
*/
getDaysOf(d : 'm' | 'w' = 'm') : xDateDayInfoType[] {
let dates = [] as xDateDayInfoType[]
if (d == 'w') {
let first = this.getDateStartOf('w')
for (let i = 0; i < 7; i++) {
let date = first.getClone()
date.date.setDate(date.date.getDate() + i)
dates.push(date.getDateInfo())
}
}
if (d == 'm') {
let first = this.getDateStartOf('m')
let maxDay = this.getMonthCountDay()
for (let i = 1; i <= maxDay; i++) {
let date = first.getClone()
date.date.setDate(i)
dates.push(date.getDateInfo())
}
}
return dates;
}
/**
* 按指定天数返回日期数组
* @param {string} d - 返回多少天的日期数组
* @param {string} type - after表示返回当前日期之后的日期,before表示返回当前日期之前的日期
*/
getDaysOfNum(d : number = 0, type : 'after' | 'before') : xDateDayInfoType[] {
let ar = [] as xDateDayInfoType[];
let nowdate = this.getClone()
if (type == 'after') {
for (let i = 0; i < d; i++) {
nowdate.add(1, 'd')
ar.push(nowdate.getDateInfo())
}
} else if (type == 'before') {
for (let i = 0; i < d; i++) {
nowdate.subtraction(1, 'd')
ar.push(nowdate.getDateInfo())
}
ar.reverse()
}
return ar;
}
/**
* 为当前日期进行加操作
* @param {string} - y,m,d,h,M,s,w,代表的是年份,月份,日期,小时,分钟,秒数,周
*/
add(count : number, d : xDateTypeTime = 'd') : xDate {
if (d == 'y') {
this.date.setFullYear(this.getYear() + count)
}
else if (d == 'm') {
this.date.setMonth(this.getMonth() + count)
}
else if (d == 'd') {
this.date.setDate(this.getDate() + count)
}
else if (d == 'h') {
this.date.setHours(this.getHours() + count)
}
else if (d == 'M') {
this.date.setMinutes(this.getMinutes() + count)
}
else if (d == 's') {
this.date.setSeconds(this.getSeconds() + count)
}
// 添加一周
else if (d == 'w') {
this.date.setDate(this.getDate() + count * 7)
}
return this;
}
/**
* 为当前日期进行减操作
* @param {string} - y,m,d,h,M,s,w,代表的是年份,月份,日期,小时,分钟,秒数,周
*/
subtraction(count : number, d : xDateTypeTime = 'd') : xDate {
if (d == 'y') {
this.date.setFullYear(this.getYear() - count)
}
else if (d == 'm') {
this.date.setMonth(this.getMonth() - count)
}
else if (d == 'd') {
this.date.setDate(this.getDate() - count)
}
else if (d == 'h') {
this.date.setHours(this.getHours() - count)
}
else if (d == 'M') {
this.date.setMinutes(this.getMinutes() - count)
}
else if (d == 's') {
this.date.setSeconds(this.getSeconds() - count)
}
// 添加一周
else if (d == 'w') {
this.date.setDate(this.getDate() - count * 7)
}
return this;
}
/**
* 日期是否在一个区间内
* @param {xDate} start 开始日期
* @param {xDate} end 结束日期
* @param {string} - type要比较的单位,默认为ms,'y' 年 'm' 月 'd' 日 'h' 时 'M' 分 's' 秒 'ms' 毫秒
* @param {string} - d,默认为[] 区间类型,'()'不含起始'[]'包含起始'(]'不包含开始,但包含结束'[)'包含开始,但不包含结束
* @returns {boolean} 是否在区间内
*/
isBetween(start : xDate, end : xDate, type : xDateTypeTime = 'ms', d : '()' | '[]' | '(]' | '[)' = '[]') : boolean {
let startTime = start.getTime(type)
// new Date('1970-1-1 8:0:0').getTime()
let endTime = end.getTime(type)
let nowTime = this.getTime(type)
if (d == '()') {
return nowTime > startTime && nowTime < endTime
}
else if (d == '[]') {
return nowTime >= startTime && nowTime <= endTime
}
else if (d == '(]') {
return nowTime > startTime && nowTime <= endTime
}
else if (d == '[)') {
return nowTime >= startTime && nowTime < endTime
}
return nowTime >= startTime && nowTime <= endTime
}
/**
* 与目标日期比较
* @param {xDate} targetDate 要比较的日期
* @param {string} - d,默认为> 比较类型,'>'大于目标日期,'>='大于等于目标'<'小于目标,'<='小于等于目标,'='全等
* @param {string} - type要比较的单位,默认为ms,'y' 年 'm' 月 'd' 日 'h' 时 'M' 分 's' 秒 'ms' 毫秒
* @returns {boolean} 比较结果
*/
isBetweenOf(targetDate : xDate, d : '>' | '>=' | '<' | '<=' | '=' = '>', type : xDateTypeTime = 'ms') : boolean {
let startTime = targetDate.getTime(type)
let nowTime = this.getTime(type)
if (d == '>') {
return nowTime > startTime
}
else if (d == '>=') {
return nowTime >= startTime
}
else if (d == '<') {
return nowTime < startTime
}
else if (d == '<=') {
return nowTime <= startTime
}
else if (d == '=') {
return nowTime == startTime
}
return nowTime > startTime
}
/**
* 与目标时间过了多久
* @param {string|xDate|null} target 要对比的相对时间,如果填写null就表示当前时间以来的的多久
* @param {Map<number,string>} model 如果为null或者为空的map采用默认值,map格式key表示以秒为单位的时间,value为对应的文本
* 比如60,1分钟前
* @param {string} format 超过最大值最使用格式化日期,如果为null默认为YYYY-MM-DD
* @returns {string} 返回对应的时间文本
*/
fromBetweenLongTime(target : any, model : Map<number, string> | null, format : string | null) : string {
let bijiaodate : xDate = new xDate()
if (target instanceof xDate) {
bijiaodate = (target as xDate)
} else if (typeof target == 'string') {
bijiaodate = new xDate(target as string)
} else if (target == null) {
bijiaodate = new xDate()
}
// 使用国际化相对时间配置
let relativeTime = getLocaleData().relativeTime;
let mapmodel = new Map<number, string>([
[30, relativeTime.s],
[60, relativeTime.m],
[60 * 5, '5' + relativeTime.mm.replace('%d', '')],
[60 * 10, '10' + relativeTime.mm.replace('%d', '')],
[60 * 30, '30' + relativeTime.mm.replace('%d', '')],
[60 * 60, relativeTime.h],
[60 * 60 * 2, '2' + relativeTime.hh.replace('%d', '')],
[60 * 60 * 3, '3' + relativeTime.hh.replace('%d', '')],
[60 * 60 * 5, '5' + relativeTime.hh.replace('%d', '')],
[60 * 60 * 23, '23' + relativeTime.hh.replace('%d', '')],
[60 * 60 * 24 * 1, relativeTime.d],
[60 * 60 * 24 * 2, '2' + relativeTime.dd.replace('%d', '')],
[60 * 60 * 24 * 7, '7' + relativeTime.dd.replace('%d', '')],
[60 * 60 * 24 * 30, relativeTime.M],
[60 * 60 * 24 * 30 * 2, '2' + relativeTime.MM.replace('%d', '')],
[60 * 60 * 24 * 30 * 3, '3' + relativeTime.MM.replace('%d', '')],
[60 * 60 * 24 * 30 * 4, '4' + relativeTime.MM.replace('%d', '')],
[60 * 60 * 24 * 30 * 5, '5' + relativeTime.MM.replace('%d', '')],
[60 * 60 * 24 * 30 * 6, '6' + relativeTime.MM.replace('%d', '')],
[60 * 60 * 24 * 30 * 12, relativeTime.y],
[60 * 60 * 24 * 30 * 12 + 60 * 60 * 24 * 30, '']
])
let fmt = format == null ? 'YYYY-MM-DD' : (format!)
if (model != null) {
if (model!.size > 0) {
mapmodel = model!
}
}
let str = ""
let fanzhumap = [] as number[];
mapmodel.forEach((value : string, key : number) => {
fanzhumap.push(key)
})
fanzhumap.reverse()
try {
let startTime = bijiaodate.getTime('s')
let nowTime = this.getTime('s')
let diff = (nowTime - startTime) / 1000
if (diff > 0) {
for (let i = 0; i < fanzhumap.length; i++) {
let key = fanzhumap[i];
if (diff >= key) {
console.log(diff, key, 5 * 60 * 60, '---', fanzhumap)
str = mapmodel.get(key)!
break;
}
}
} else {
str = mapmodel.get(fanzhumap[fanzhumap.length - 1])!
}
if (str == '') {
str = bijiaodate.format(fmt)
}
} catch (e) {
//TODO handle the exception
}
return str
}
/**
* 与目标时间相差多少个单位
* @param {string|xDate|null} target 目标时间
* @param {xDateTypeTime} type 相差的单位,默认:s秒
* @returns {string} 反返回以xDateTypeTime为单位相关的时间数
*/
diffTime(target : any, type : xDateTypeTime = 's') : number {
let startTime = 0
let typed : xDateTypeTime = (type == 'w' ? 'd' : type)
if (target instanceof xDate) {
startTime = (target as xDate).getTime(typed)
} else if (typeof target == 'string') {
startTime = new xDate(target as string).getTime(typed)
} else if (target == null) {
startTime = new xDate().getTime(typed)
}
let nowTime = this.getTime(typed)
let diff = Math.abs(nowTime - startTime);
let d = 0
if (type == 's') {
d = diff
} else if (type == 'M') {
d = diff / 60 / 1000
} else if (type == 'h') {
d = diff / 60 / 60 / 1000
} else if (type == 'd') {
d = diff / 60 / 60 / 24 / 1000
} else if (type == 'm') {
d = diff / 60 / 60 / 24 / 30 / 1000
} else if (type == 'y') {
d = diff / 60 / 60 / 24 / 30 / 12 / 1000
} else if (type == 'w') {
let ondate = (1000 * 60 * 60 * 24 * 7);
d = diff / ondate
}
return Math.floor(d)
}
/**
* 获取季度
* @param {string} type y表示获取本年的4个季度,空值表示获取当前时间所在的季度
* @returns {xDateTypeQuarter[]} 返回一个季度数组{quarter:number,start:string,end:string}
*/
getQuarter(type : string = '') : xDateTypeQuarter[] {
let q1 = [1, 2, 3]
let q2 = [4, 5, 6]
let q3 = [7, 8, 9]
let q4 = [10, 11, 12]
let nowMonth = this.getMonth() + 1
let qall = [q1, q2, q3, q4] as number[][]
let qmap = [] as xDateTypeQuarter[]
if (type == 'y') {
for (let i = 0; i < qall.length; i++) {
let item = qall[i]
let sdate = new xDate(this.getYear() + '/' + item[0] + '/1')
let edate = new xDate(this.getYear() + '/' + item[item.length - 1] + '/1')
let start = sdate.format('YYYY/MM/DD')
let end = edate.getDateEndOf('m').format('YYYY/MM/DD')
qmap.push({
quarter: i,
start,
end
} as xDateTypeQuarter)
}
} else {
for (let i = 0; i < qall.length; i++) {
let item = qall[i]
if (item.includes(nowMonth)) {
let sdate = new xDate(this.getYear() + '/' + item[0] + '/1')
let edate = new xDate(this.getYear() + '/' + item[item.length - 1] + '/1')
let start = sdate.format('YYYY/MM/DD')
let end = edate.getDateEndOf('m').format('YYYY/MM/DD')
qmap.push({
quarter: i,
start,
end
} as xDateTypeQuarter)
break;
}
}
}
return qmap;
}
/**
* 设置xDate的语言
* @param {xDateLanguage} locale - 语言代码
*/
setDateLocale(locale : xDateLanguage) : void {
setLocale(locale);
}
/**
* 获取当前xDate使用的语言
* @returns {xDateLanguage} - 当前语言代码
*/
getDateLocale() : xDateLanguage {
return getCurrentLocale();
}
/**
* 根据开始和结束时间取之间的时间(含起始)
* @param {string|xDate|null} start 目标时间
* @param {string|xDate|null} end 目标时间
*/
getBetweenDate(start : string | number | Date, end : string | number | Date, minx : 'min' | 'max' | 'auto' = 'auto') : Date {
let _start = this.checkDate(start);
let _end = this.checkDate(end);
let startDiff = _start.getTime()
let endDiff = _end.getTime()
let nowDiff = this.getTime('ms')
if(nowDiff>=startDiff&&nowDiff<=endDiff){
return this.date;
}
if (minx == 'min') {
return _start;
}
if (minx == 'max') {
return _end;
}
if (startDiff < endDiff) {
return _end;
}
return _start;
}
}
/**
* 将一个日期转换为xDate对象
*/
export const dateCovertXdate = function (date : Date) : xDate {
return new xDate(date)
}
+258
View File
@@ -0,0 +1,258 @@
/**
* xDate国际化支持
* 支持中文、英文、日文、韩文、繁体中文、法语和俄语
*/
export type xDateLanguage = 'zh-Hans' | 'en' | 'ja' | 'ko' | 'zh-Hant' | 'fr' | 'ru';
export type xDateI18nTypeRelativeTime = {
future: string,
past: string,
s: string,
m: string,
mm: string,
h: string,
hh: string,
d: string,
dd: string,
M: string,
MM: string,
y: string,
yy: string
}
export type xDateI18nType = {
weekdays: string[],
weekdaysShort: string[],
months: string[],
monthsShort: string[],
meridiem: (hour: number, minute: number, isLowercase: boolean) => string,
relativeTime: xDateI18nTypeRelativeTime
}
// 中文简体
const zhHans: xDateI18nType = {
weekdays: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
weekdaysShort: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
monthsShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
meridiem: (hour: number, minute: number, isLowercase: boolean): string => {
return hour < 12 ? '上午' : '下午';
},
relativeTime: {
future: '%s内',
past: '%s前',
s: '几秒',
m: '1分钟',
mm: '%d分钟',
h: '1小时',
hh: '%d小时',
d: '1天',
dd: '%d天',
M: '1个月',
MM: '%d个月',
y: '1年',
yy: '%d年'
}
};
// 英文
const en: xDateI18nType = {
weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
weekdaysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
meridiem: (hour: number, minute: number, isLowercase: boolean): string => {
return hour < 12 ? (isLowercase ? 'am' : 'AM') : (isLowercase ? 'pm' : 'PM');
},
relativeTime: {
future: 'in %s',
past: '%s ago',
s: 'a few seconds',
m: 'a minute',
mm: '%d minutes',
h: 'an hour',
hh: '%d hours',
d: 'a day',
dd: '%d days',
M: 'a month',
MM: '%d months',
y: 'a year',
yy: '%d years'
}
};
// 日文
const ja: xDateI18nType = {
weekdays: ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'],
weekdaysShort: ['日', '月', '火', '水', '木', '金', '土'],
months: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
monthsShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
meridiem: (hour: number, minute: number, isLowercase: boolean): string => {
return hour < 12 ? '午前' : '午後';
},
relativeTime: {
future: '%s後',
past: '%s前',
s: '数秒',
m: '1分',
mm: '%d分',
h: '1時間',
hh: '%d時間',
d: '1日',
dd: '%d日',
M: '1ヶ月',
MM: '%dヶ月',
y: '1年',
yy: '%d年'
}
};
// 韩文
const ko: xDateI18nType = {
weekdays: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'],
weekdaysShort: ['일', '월', '화', '수', '목', '금', '토'],
months: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
monthsShort: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
meridiem: (hour: number, minute: number, isLowercase: boolean): string => {
return hour < 12 ? '오전' : '오후';
},
relativeTime: {
future: '%s 후',
past: '%s 전',
s: '몇 초',
m: '1분',
mm: '%d분',
h: '1시간',
hh: '%d시간',
d: '1일',
dd: '%d일',
M: '1개월',
MM: '%d개월',
y: '1년',
yy: '%d년'
}
};
// 繁体中文
const zhHant: xDateI18nType = {
weekdays: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
weekdaysShort: ['週日', '週一', '週二', '週三', '週四', '週五', '週六'],
months: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
monthsShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
meridiem: (hour: number, minute: number, isLowercase: boolean): string => {
return hour < 12 ? '上午' : '下午';
},
relativeTime: {
future: '%s內',
past: '%s前',
s: '幾秒',
m: '1分鐘',
mm: '%d分鐘',
h: '1小時',
hh: '%d小時',
d: '1天',
dd: '%d天',
M: '1個月',
MM: '%d個月',
y: '1年',
yy: '%d年'
}
};
// 法语
const fr: xDateI18nType = {
weekdays: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
weekdaysShort: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'],
months: ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
monthsShort: ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.'],
meridiem: (hour: number, minute: number, isLowercase: boolean): string => {
return '';
},
relativeTime: {
future: 'dans %s',
past: 'il y a %s',
s: 'quelques secondes',
m: 'une minute',
mm: '%d minutes',
h: 'une heure',
hh: '%d heures',
d: 'un jour',
dd: '%d jours',
M: 'un mois',
MM: '%d mois',
y: 'un an',
yy: '%d ans'
}
};
// 俄语
const ru: xDateI18nType = {
weekdays: ['воскресенье', 'понедельник', 'вторник', 'среда', 'четверг', 'пятница', 'суббота'],
weekdaysShort: ['вс', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб'],
months: ['январь', 'февраль', 'март', 'апрель', 'май', 'июнь', 'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь'],
monthsShort: ['янв.', 'февр.', 'март', 'апр.', 'май', 'июнь', 'июль', 'авг.', 'сент.', 'окт.', 'нояб.', 'дек.'],
meridiem: (hour: number, minute: number, isLowercase: boolean): string => {
return '';
},
relativeTime: {
future: 'через %s',
past: '%s назад',
s: 'несколько секунд',
m: 'минута',
mm: '%d минут',
h: 'час',
hh: '%d часов',
d: 'день',
dd: '%d дней',
M: 'месяц',
MM: '%d месяцев',
y: 'год',
yy: '%d лет'
}
};
// 语言映射表
export const locales: Map<xDateLanguage, xDateI18nType> = new Map([
['zh-Hans', zhHans],
['en', en],
['ja', ja],
['ko', ko],
['zh-Hant', zhHant],
['fr', fr],
['ru', ru]
]);
// 默认语言
let currentLocale: xDateLanguage = 'zh-Hans';
/**
* 获取当前语言设置
*/
export function getCurrentLocale(): xDateLanguage {
return currentLocale;
}
/**
* 设置当前语言
* @param locale 语言代码
*/
export function setLocale(locale: xDateLanguage): void {
if (locales.has(locale)) {
currentLocale = locale;
}
}
/**
* 获取当前语言的本地化数据
*/
export function getLocaleData(): xDateI18nType {
return locales.get(currentLocale)!;
}
/**
* 获取指定语言的本地化数据
* @param locale 语言代码
*/
export function getLocaleDataByCode(locale: xDateLanguage): xDateI18nType | null {
return locales.has(locale) ? locales.get(locale)! : null;
}
+676
View File
@@ -0,0 +1,676 @@
import { xRequestCall } from "./config.uts"
import { xRequestOptions, xRequestOptionsCallBack, xRequestResult, xRequestHistoryType, xRequestMethond } from "../../interface.uts"
import { xConfig } from "../../config/xConfig.uts"
const i18n = xConfig.i18n;
/**
* 请求库xRequest
* @version 1.0.0
* @author tmzdy
* @host https://xui.tmui.design
* @description 异常简便,没有繁杂的配置选项,只有快速的开发部署。
*/
/**
* auth 表示全局设置了全限auth为false,后面的所有请求都被中止请求,从而触发此事件。
* 如果此时返回true,将继续请求。
* before 如果返回true,将继续请求。返回false被中断请求。
* after 可以返回请求的后台数据格式化并返回到最终的请求结果中。
*
* auth,before监听函数请务必返回Promise.resolve(boolean),其中函数内得到的arg为xRequestOptionsCallBack类型(请求参数配置数据)
* 除了authbefore 函数体内arg 得到的参数全是xRequestResult(请求结果数据), 返回随意,因为内部不采用。
* after,需要返回 Promise.resolve(xRequestResult),会覆盖success请求后的数据,相当于你在此hook事件中修改和格式化服务器,并返回到最终结果中。
*/
type xRequestEventType = "before" | "after" | "abort" | "timeout" | "error" | "auth" | "success" | "complete"
type funType = (arg : any) => Promise<any>;
type funCall = {
fun : funType,
type : xRequestEventType
}
function getUid(rdix = 1, length = 12) : string {
let ix = "";
// #ifndef APP
ix = Math.floor(Math.random() * rdix * Math.floor(Math.random() * Date.now())).toString().substring(0, length);
// #endif
// #ifdef APP
ix = Math.floor(Math.random() * rdix * Math.floor(Math.random() * Date.now())).toString().substring(0, length as Int);
// #endif
return ix;
}
// 缓存管理器
class xCacheManager {
private cache : UTSJSONObject = {};
constructor() {
let cs = uni.getStorageSync('xCacheManager');
if (cs instanceof UTSJSONObject) {
this.cache = cs! as UTSJSONObject
}
}
// 生成缓存键
generateCacheKey(config : xRequestOptionsCallBack) : string {
const url = config.url;
const method = config.method;
let keyQ = (url?.split('?')?.[0] ?? url) as string;
// 简化缓存键生成,避免复杂对象序列化可能导致的不一致
let key = `${method}_${keyQ}`;
// 对data进行类似处理
if (config.data != null) {
// 如果是简单类型,直接使用
if (!(config.data instanceof UTSJSONObject)) {
key += `_d:${config.data}`;
} else if (config.data instanceof UTSJSONObject) {
let _data = config.data as UTSJSONObject;
let _keys = [] as string[]
for (const key in _data) {
_keys.push(key)
}
// 对于对象,只使用键名作为缓存键的一部分
const dataKeys = _keys.sort()
key += `_d:${dataKeys.join(',')}`;
}
}
return key;
}
// 设置缓存
set(config : xRequestOptionsCallBack, response : any) : void {
const key = this.generateCacheKey(config);
this.cache.set(key, {
data: response,
timestamp: Date.now()
} as UTSJSONObject)
try {
uni.setStorageSync('xCacheManager', this.cache);
} catch (error) {
// 如果存储失败,可能是缓存太大,清理一部分
this.pruneCache();
try {
uni.setStorageSync('xCacheManager', this.cache);
} catch (e) {
console.error('二次缓存设置失败');
}
}
}
// 获取缓存
get(config : xRequestOptionsCallBack) : any | null {
const key = this.generateCacheKey(config);
const cacheItem = this.cache.getJSON(key)
if (cacheItem == null) return null;
const cacheTime = config.cacheTime == 0 ? 60000 : config.cacheTime
const now = Date.now();
const timestamp = cacheItem.getNumber("timestamp")!
// 检查缓存是否过期
if (now - timestamp > cacheTime) {
let allkeys = {} as UTSJSONObject;
for (const keySelf in this.cache) {
if (keySelf != key) {
allkeys.set(keySelf, this.cache[keySelf])
}
}
this.cache = allkeys
uni.setStorageSync('xCacheManager', allkeys);
return null;
}
// console.log('命中缓存:', key);
return cacheItem.getAny('data');
}
// 清除缓存
clear() : void {
this.cache = {};
uni.removeStorageSync('xCacheManager');
}
// 清理部分缓存
private pruneCache() : void {
const _keys = [] as string[]
for (const key in this.cache) {
_keys.push(key)
}
if (_keys.length > 20) { // 如果缓存项超过20个
// 按时间戳排序,删除最旧的一半
const sortedKeys = _keys.sort((a : string, b : string) : number => {
let ad = this.cache[a]! as UTSJSONObject
let bd = this.cache[b]! as UTSJSONObject
return ad.getNumber('timestamp')! - bd.getNumber('timestamp')!
});
const toRemove = sortedKeys.slice(0, Math.floor(_keys.length / 2));
let allkeys = {} as UTSJSONObject;
for (let i = 0; i < sortedKeys.length; i++) {
let key = sortedKeys[i]
allkeys.set(key, this.cache[key])
}
this.cache = allkeys
uni.setStorageSync('xCacheManager', allkeys);
console.log(`已清理${toRemove.length}个旧缓存项`);
}
}
}
export class xRequest {
private auth = true;
private cacheManager : xCacheManager;
private selfOpts = {
useCache: false,
cacheTime: 60 * 1000,
hostUrl: xRequestCall.hostUrl,
successStatusCode: 200,
url: "",
data: {} as UTSJSONObject,
header: {
'content-type': 'application/json'
} as UTSJSONObject,
method: "GET",
timeout: 6000,
firstIpv4: false,
/** 请求前是否显示loading遮罩 */
showLoadToast: xRequestCall.showLoadToast,
/** 请求成功后是否提示,否则不提示 */
showSuccessToast: xRequestCall.showSuccessToast,
/** 出错时,是否显示提示,否则不提示 */
showErrorToast: xRequestCall.showErrorToast,
loadToastText: i18n.t("tmui4x.xRequest.loading"),
successToastText: i18n.t("tmui4x.xRequest.success"),
errorToastText: ""
} as xRequestOptionsCallBack
private _lisentEventList = [] as funCall[]
private status = null as xRequestEventType | null
private result = {
data: null,
statusCode: 0,
header: {} as any,
cookies: [] as string[],
} as xRequestResult
private reqtask = null as null | RequestTask
constructor(opts : xRequestOptions | null = null) {
this.setOptions(opts)
this.cacheManager = new xCacheManager()
}
/**
* 设置当前请求的参数
* 只影响本次的实例请求,不影响其它请求。
* 但配置中的header,data会与全局的合并。
*/
setOptions(opts : xRequestOptions | null = null) : xRequestOptionsCallBack {
if (opts == null) return this.selfOpts;
let headerTmep = opts.header
headerTmep = opts.header == null ? this.selfOpts.header : opts.header!
// #ifdef WEB
headerTmep = opts.header == undefined ? this.selfOpts.header : opts.header
// #endif
let dataTmep = opts?.data ?? ({} as UTSJSONObject)
if (xRequestCall.header != null) {
let obj = xRequestCall.header as UTSJSONObject;
let newObj = headerTmep! as UTSJSONObject;
for (let key in newObj) {
obj.set(key, newObj.get(key))
}
headerTmep = obj;
}
let firstIpv4 = (opts?.firstIpv4 ?? false) as boolean;
let method = (opts?.method ?? this.selfOpts.method) as xRequestMethond
let timeout = (opts?.timeout ?? this.selfOpts.timeout) as number
let successStatusCode = (opts?.successStatusCode ?? this.selfOpts.successStatusCode) as number
let url = (opts?.url ?? this.selfOpts.url) as string
let showLoadToast = (opts?.showLoadToast ?? this.selfOpts.showSuccessToast) as boolean
let showSuccessToast = (opts?.showSuccessToast ?? this.selfOpts.showSuccessToast) as boolean
let showErrorToast = (opts?.showErrorToast ?? this.selfOpts.showErrorToast) as boolean
let hostUrl = (opts?.hostUrl ?? xRequestCall.hostUrl) as string
let cacheTime = (opts?.cacheTime ?? this.selfOpts.cacheTime) as number
let useCache = (opts?.useCache ?? this.selfOpts.useCache) as boolean
let loadToastText = (opts?.loadToastText ?? this.selfOpts!.loadToastText) as string
let successToastText = (opts?.successToastText ?? this.selfOpts!.successToastText) as string
let errorToastText = (opts?.errorToastText ?? this.selfOpts!.errorToastText) as string
this.selfOpts.firstIpv4 = firstIpv4!
this.selfOpts.method = method! as xRequestMethond;
this.selfOpts.timeout = timeout!
this.selfOpts.cacheTime = cacheTime!
this.selfOpts.useCache = useCache!
this.selfOpts.successStatusCode = successStatusCode!
this.selfOpts.data = dataTmep!
this.selfOpts.header = headerTmep!
this.selfOpts.url = url! as string
this.selfOpts.showLoadToast = showLoadToast! as boolean;
this.selfOpts.showSuccessToast = showSuccessToast! as boolean
this.selfOpts.showErrorToast = showErrorToast! as boolean
this.selfOpts.responseType = opts.responseType
this.selfOpts.dataType = opts.dataType
this.selfOpts.hostUrl = hostUrl
this.selfOpts!.loadToastText = loadToastText
this.selfOpts!.successToastText = successToastText
this.selfOpts!.errorToastText = errorToastText
return this.selfOpts;
}
/**
* 添加事件监听
* 可以重复添加,被添加的函数会被按顺序执行。
* 相当于Hooks
*/
addEventListener(event : xRequestEventType = "success", fun : (arg : any) => Promise<any>) : xRequest {
this._lisentEventList.push({
type: event,
fun
} as funCall)
return this;
}
/**
* 中断当前的事件
* @returns {boolean} true表示中断成功,false表示失败
*/
abort() : boolean {
if (this.reqtask != null) {
this.reqtask!.abort()
return true;
}
return false
}
/**
* 设置请求授权失败还是通过
* 如果设置了false
* 接下来的所有请求全部会被中断掉。哪怕你异步或者跨页面发起请求。
*/
static setAuth(isPass : boolean) {
xRequestCall.authPass = isPass
}
/**
* 设置的头数据是会被所有请求共享
* 包括跨页面。
* 同时新增的数据会与旧的数据合并,如果之前不存在则新增。
*/
static setHeader(header : UTSJSONObject | null = null) : UTSJSONObject | null {
if (xRequestCall.header == null) {
xRequestCall.header = header;
} else {
let obj = xRequestCall.header as UTSJSONObject;
let newObj = header! as UTSJSONObject;
for (let key in newObj) {
obj.set(key, newObj.get(key))
}
xRequestCall.header = obj;
}
return xRequestCall.header
}
/**
* 设置请求的主域名地址
* 所有的请求共享,包括跨页面。
*/
static setHostUrl(url : string | null) {
xRequestCall.hostUrl = url == null ? "" : url!;
}
static setDev(dev : boolean = false) {
xRequestCall.dev = dev;
if (dev) {
console.warn('tmui4.0提醒:' + `你开启了请求调试模式`)
} else {
console.warn('tmui4.0提醒:' + `你关闭了请求调试模式`)
}
}
/**
* 获取整个应用运行期间的所有请求日志列表
*/
static getHistory() : xRequestHistoryType[] {
return xRequestCall.history.slice(0)
}
static setShowToast(showLoadToast : boolean = true, showSuccessToast : boolean = true, showErrorToast : boolean = true) {
xRequestCall.showLoadToast = showLoadToast
xRequestCall.showSuccessToast = showSuccessToast
xRequestCall.showErrorToast = showErrorToast
}
/**
* 执行Hooks函数。
*/
private _callFun_build() : Promise<any[]> {
if (this.status == null) return Promise.resolve([true] as any[]);
// "before"|"after"|"abort"|"timeout"|"error"|"auth"|"success"|"complete"
let fun = this._lisentEventList.filter((el) : boolean => el.type == this.status!)
let funCalls = fun.map((el) : funType => el.fun)
if (funCalls.length == 0) {
if (this.status == 'auth' || this.status == 'before') {
return Promise.resolve([true] as any[]);
}
return Promise.resolve([this.result] as any[]);
}
return this._buildPromise(funCalls)
}
/**
* 递归执行异步函数
* 由于官方的Promise.all,不知道是bug还是什么,始终无法执行成功
* 因此自己写了个。
*/
private _buildPromise(lst : funType[]) : Promise<any[]> {
let _this = this;
let len = lst.length;
let i = 0;
let p = [] as any[]
async function customPromiseAll(evt : funType) : Promise<any> {
if (i >= len) return Promise.resolve(p);
let arg : any = _this.selfOpts
// 非这两事件,全部传递结果集给监听事件中。
if (_this.status != 'auth' && _this.status != 'before') {
arg = _this.result as xRequestResult
}
let v = await evt(arg);
// 如果这两事件中返回的不是boolean,则强制转换为true通过。
if ((_this.status == 'auth') && typeof v != 'boolean') {
v = true;
}
if ((_this.status == 'before') && typeof v != 'boolean') {
let beforeOpts = JSON.stringify(v)
_this.setOptions(JSON.parse<xRequestOptions>(beforeOpts!)!)
v = true;
}
// before中按顺序中断了前面的,后面的不要再执行,直接结束。
if ((_this.status == 'before') && typeof v == 'boolean') {
let iabort = v as boolean;
if (!iabort) {
p.push(v);
return Promise.resolve(p);
}
}
// 请后,与事件中返回的修改后的数据覆盖原本的。
if ((_this.status == 'after') && typeof v == 'object') {
_this.result = v as xRequestResult
}
// 其它事件则全部结果集。
if (_this.status == 'success' || _this.status == 'timeout' || _this.status == 'error' || _this.status == 'abort' || _this.status == 'complete') {
v = _this.result
}
i += 1;
p.push(v);
if (i >= len) return Promise.resolve(p);
return await customPromiseAll(lst[i]);
}
return new Promise((res, rej) => {
customPromiseAll(lst[i]).then(() => {
res(p)
})
})
}
/**
* 请置指定请求的状态值。
*/
private _setDevReqStatus(id : string) {
if (!xRequestCall.dev) return;
let eventType = this.status;
let index = -1;
for (let i = 0; i < xRequestCall.history.length; i++) {
let item = xRequestCall.history[i];
if (id == item.id) {
index = i;
break;
}
}
if (index > -1 && eventType != null) {
xRequestCall.history[index].status = eventType;
if (eventType == 'complete' || eventType == 'abort' || eventType == 'auth') {
xRequestCall.history[index].loaded = true;
xRequestCall.history[index].loading = false;
}
if (eventType == 'before') {
xRequestCall.history[index].loading = true;
}
if (eventType == 'success') {
xRequestCall.history[index].time = Date.now() - xRequestCall.history[index].time;
}
if (eventType == 'complete' || eventType == 'success') {
xRequestCall.history[index].result = this.result;
}
}
}
async request(opts : xRequestOptions | null = null) : Promise<xRequestResult> {
let _this = this;
let _thisOpts = this.setOptions(opts)
let requestId = getUid()
if (xRequestCall.dev) {
xRequestCall.history.push({
id: requestId,
loading: true,
loaded: false,
status: "",
time: Date.now(),
api: _thisOpts.url,
result: {} as any
} as xRequestHistoryType)
}
if (_thisOpts.showLoadToast) {
uni.showLoading({ title: _thisOpts!.loadToastText!, mask: true })
}
const url = _thisOpts.hostUrl + _thisOpts.url;
if (url == "" || _thisOpts.hostUrl == "") {
this.status = 'error';
this._setDevReqStatus(requestId);
await this._callFun_build()
console.warn("未填写请求接口")
if (_thisOpts.showLoadToast) {
uni.hideLoading()
}
if (_thisOpts.showErrorToast) {
return new Promise<xRequestResult>((tres, trej) => {
uni.showToast({ title: i18n.t("tmui4x.xRequest.hostFailEmpty"), icon: 'error', mask: true, complete() { trej(_this.result) } })
})
}
return Promise.reject(_this.result)
}
if (xRequestCall.authPass == false) {
this.status = 'auth'
console.warn("授权失败,中断请求")
this._setDevReqStatus(requestId);
await this._callFun_build()
if (_thisOpts.showLoadToast) {
uni.hideLoading()
}
return Promise.reject(_thisOpts)
}
// 检查是否使用缓存,只针对post及get方式进行缓存.
if (_thisOpts.useCache && (_thisOpts.method == 'GET' || _thisOpts.method == 'POST')) {
// 尝试从缓存获取响应
const cachedResponse = this.cacheManager.get(_thisOpts);
if (cachedResponse != null) {
if (process.env.NODE_ENV == 'development') {
console.log('缓存接口:', _thisOpts.url)
}
const cachedata = cachedResponse as UTSJSONObject
const _cookies = cachedata.getArray<string>('cookies');
const cacheResult = {
data: cachedata.getAny('data'),
statusCode: cachedata.getNumber('statusCode')!,
header: cachedata.getAny('header')!,
cookies: _cookies == null ? ([] as string[]) : _cookies!,
} as xRequestResult
if (_thisOpts.showLoadToast) {
uni.hideLoading()
}
return Promise.resolve(cacheResult);
}
}
this.status = 'before';
this._setDevReqStatus(requestId);
let jg2 = await this._callFun_build()
let isAbor = (jg2 as boolean[]).some((el) : boolean => !el)
// 是否中断请求
if (isAbor) {
console.warn("事件before中断了请求")
this.status = 'abort';
this._setDevReqStatus(requestId);
await this._callFun_build()
if (_thisOpts.showLoadToast) {
uni.hideLoading()
}
return Promise.reject(this.result)
}
// "before" | "after" | "abort" | "timeout" | "error" | "auth" | "success" | "complete"
return new Promise((res, rej) => {
_this.reqtask = uni.request({
url: url,
data: _thisOpts.data,
header: _thisOpts.header,
method: _thisOpts.method,
timeout: _thisOpts.timeout,
firstIpv4: _thisOpts.firstIpv4,
sslVerify: false,
withCredentials: false,
dataType: _thisOpts.dataType,
responseType: _thisOpts.responseType,
success(rst) {
if (process.env.NODE_ENV == 'development') {
console.log('请求接口成功:', _thisOpts.url, rst)
}
_this.result = {
data: rst.data,
statusCode: rst.statusCode,
header: rst.header,
cookies: rst.cookies as string[],
} as xRequestResult
_this.status = 'after';
_this._setDevReqStatus(requestId);
_this._callFun_build()
.then((v) => {
if (_thisOpts.successStatusCode != rst.statusCode) {
_this.status = 'error';
_this.result.statusCode = rst.statusCode
_this._setDevReqStatus(requestId);
_this._callFun_build()
.then(() => {
if (_thisOpts.showLoadToast) {
uni.hideLoading()
}
if (_thisOpts.showErrorToast) {
let msg = _thisOpts!.errorToastText
msg = msg==""?(i18n.t("tmui4x.xRequest.error")+rst.statusCode.toString()):msg
uni.showToast({ title: msg!, mask: true, icon: 'error', complete() { rej(_this.result) } })
} else {
rej(_this.result)
}
})
return;
}
let jgtss = v as xRequestResult[]
let formartv = _this.result as xRequestResult
if (jgtss.length > 0) {
formartv = jgtss[jgtss.length - 1]
}
_this.result = formartv;
_this.status = 'success';
_this._setDevReqStatus(requestId);
_this._callFun_build()
.then(() => {
if (_thisOpts.showLoadToast) {
uni.hideLoading()
}
_this.cacheManager.set(_thisOpts, _this.result);
if (_thisOpts.showSuccessToast) {
//默认读取data下的msg字段。
let d = formartv.data;
let msg = _thisOpts!.successToastText
if (typeof d == 'object' && d != null && !Array.isArray(d)&&msg=="") {
try {
let s = d as UTSJSONObject;
msg = s.getString('msg') != null ? s.getString('msg')! : msg
} catch (e) {
//TODO handle the exception
console.error("服务没有返回msg字段")
}
}
msg = msg==""?i18n.t("tmui4x.xRequest.success"):msg
uni.showToast({ title: msg!, icon: 'none', mask: true, complete() { res(_this.result) } })
} else {
res(_this.result)
}
})
})
},
fail(er) {
if (process.env.NODE_ENV == 'development') {
console.error(er)
}
if (er.errCode == 5) {
_this.status = 'timeout';
} else {
_this.status = 'error';
}
_this.result.statusCode = er.errCode
_this._setDevReqStatus(requestId);
_this._callFun_build()
.then(() => {
if (_thisOpts.showLoadToast) {
uni.hideLoading()
}
if (_thisOpts.showErrorToast) {
uni.showToast({ title: i18n.t("tmui4x.xRequest.error") + er.errCode.toString(), mask: true, icon: 'error', complete() { rej(_this.result) } })
} else {
rej(_this.result)
}
})
},
complete() {
_this.status = 'complete';
_this._setDevReqStatus(requestId);
_this._callFun_build()
}
})
})
}
}
+435
View File
@@ -0,0 +1,435 @@
/**
* 动画过渡函数
* @version 1.0.0
* @author tmzdy
* @host https://xui.tmui.design
* @description 异常简便,没有繁杂的配置选项,只有快速的开发部署。
*/
import bezier from "./bezier.uts"
import { xTweenStatus, xTweenCallbackFunType, xTweenAnimate, xTweenEventCallFunType, xTweenEventCall } from "../../interface.uts"
import { getUid } from "./xCoreUtil.uts";
type FrameRequestCallback = (time:number)=>void;
export class xTween {
private frameId : null | number = null;
//当前全局动画是否在执行中,只要有一个动画在执行就会是true
isRuning : boolean = false;
//所有动画是否结束
isStoping : boolean = true;
//刷新率,不会立即得出结题,需要有至少执行16ms才可能计算出帖率
frmae = 0
// 舞台是否在执行中
isRendering = false;
private _callListFun : xTweenEventCallFunType[] = []
private _frema_starttimes = 0
private enters : (listAni : xTweenEventCallFunType[], tims : number) => void = (listAni : xTweenEventCallFunType[], tims : number) => { }
private complete : () => void = () => { }
private lastTime = 0
private nextHandle = 0
// 存储回调函数的映射
private callbacks : Map<number, FrameRequestCallback> = new Map();
private easingList = new Map<string, number[]>([
["linear", [0.250, 0.250, 0.750, 0.750]],
["ease", [0.250, 0.100, 0.250, 1.000]],
["easeIn", [0.420, 0.000, 1.000, 1.000]],
["easeOut", [0.000, 0.000, 0.580, 1.000]],
["easeInOut", [0.420, 0.000, 0.580, 1.000]],
["easeInQuad", [0.550, 0.085, 0.680, 0.530]],
["easeOutQuad", [0.250, 0.460, 0.450, 0.940]],
["easeInOutQuad", [0.455, 0.030, 0.515, 0.955]],
["easeInCubic", [0.550, 0.055, 0.675, 0.190]],
["easeOutCubic", [0.215, 0.610, 0.355, 1.000]],
["easeInOutCubic", [0.645, 0.045, 0.355, 1.000]],
["easeInQuart", [0.895, 0.030, 0.685, 0.220]],
["easeOutQuart", [0.165, 0.840, 0.440, 1.000]],
["easeInOutQuart", [0.770, 0.000, 0.175, 1.000]],
["easeInQuint", [0.755, 0.050, 0.855, 0.060]],
["easeOutQuint", [0.230, 1.000, 0.320, 1.000]],
["easeInOutQuint", [0.860, 0.000, 0.070, 1.000]],
["easeInSine", [0.470, 0.000, 0.745, 0.715]],
["easeOutSine", [0.390, 0.575, 0.565, 1.000]],
["easeInOutSine", [0.445, 0.050, 0.550, 0.950]],
["easeInExpo", [0.950, 0.050, 0.795, 0.035]],
["easeOutExpo", [0.190, 1.000, 0.220, 1.000]],
["easeInOutExpo", [1.000, 0.000, 0.000, 1.000]],
["easeInCirc", [0.600, 0.040, 0.980, 0.335]],
["easeOutCirc", [0.075, 0.820, 0.165, 1.000]],
["easeInOutBack", [0.680, -0.550, 0.265, 1.550]],
["tmxEase", [0.42, 0.38, 0.15, 0.93]]
]);
constructor() {
}
private customRequestAnimationFrame(callback : FrameRequestCallback) : number {
const currTime : number = Date.now();
const timeToCall : number = Math.max(0, 16 - (currTime - this.lastTime));
const handle : number = this.nextHandle++;
this.callbacks.set(handle, callback);
const id = setTimeout(() => {
callback(currTime + timeToCall);
}, timeToCall);
this.lastTime = currTime + timeToCall;
return handle;
}
private customCancelAnimationFrame(handle : number) {
if (this.callbacks.has(handle)) {
this.callbacks.delete(handle);
clearTimeout(handle);
}
}
/**
* 启动渲染,并非执行动画,执行动画需要使用play
* 但如果不启动渲染,play中的动画都无法执行.
*/
startRender() : xTween {
this.isRendering = true;
if (this.frameId == null) {
this._run(this)
}
return this;
}
/**
* 销毁渲染.
*/
destroy() {
if (this.frameId != null) {
// #ifdef APP||WEB
cancelAnimationFrame(this.frameId!)
// #endif
// #ifdef MP
this.customCancelAnimationFrame(this.frameId!)
// #endif
}
this.frameId = null;
this.isRendering = false;
}
getFrame() : number {
return this.frmae
}
/**
* 所有事件动画执行完毕
*/
setComplete(call : () => void) : xTween {
this.complete = call
return this;
}
/**
* 设置帖动画,舞台渲染执行帖函数
* 它不是animate动画,没有进度,只会不停的执行.直接舞台渲染被注销.
*/
setEnter(call : (listAni : xTweenEventCallFunType[], tims : number) => void) : xTween {
this.enters = call
return this;
}
/**
* 添加一个动画,可以重复加达到多个动画联合执行的效果
* 并且 每一个动画都是独立存在,这样可以细微的控制每个动画
* 来达到联合处理的效果.
*/
addAnimate(opts : xTweenAnimate) : string {
let uid = getUid();
let call : xTweenEventCall = (item : xTweenEventCallFunType) => { };
this._callListFun.push({
id: uid,
ease: this._getEasing(opts?.ease ?? 'linear'),
duration: opts.duration,
status: 1,
progress: 0,
oldProgeress: 0,
startTime: 0,
autoRemove: opts?.autoRemove != null ? (opts!.autoRemove as boolean) : true,
complete: opts?.complete != null ? (opts!.complete!) : call,
enter: opts?.enter != null ? (opts!.enter!) : call,
start: opts?.start != null ? (opts!.start!) : call,
pause: opts?.pause != null ? (opts!.pause!) : call,
loop: opts?.loop != null ? (opts!.loop!) : 1,
tyty: opts?.tyty != null ? (opts!.tyty!) : false,
step: opts?.step != null ? (opts!.step!) : 1,
_finishLoop: 0,
reverse: false
} as xTweenEventCallFunType)
return uid;
}
/**
* 删除一个事件.
* @param {string} uid 如果为null表示删除所有.
*/
removeAnimate(uid : string | null = null) : xTween {
if (uid == null) {
this._callListFun = [] as xTweenEventCallFunType[]
} else {
let index = this._getCallIndex(uid as string);
if (index > -1) {
this._callListFun.splice(index, 1)
}
}
return this;
}
/**
* 播放动画。
* @param {string|null} uid 动画id,如果不填写或者为null表示播放所有动画
*/
play(uid : string | null = null) {
if (uid != null && uid != '') {
let index = this._getCallIndex(uid! as string)
if (index > -1) {
let item = this._callListFun[index]
if (item.status == 1) {
this._by_run_set_status(item, 1)
this._by_run_item_callFun(item, 0)
}
// 马上要执行运行
this._by_run_set_status(item, 4)
}
} else {
for (let i = 0; i < this._callListFun.length; i++) {
let item = this._callListFun[i]
if (item.status == 1) {
this._by_run_set_status(item, 1)
this._by_run_item_callFun(item, 0)
}
this._by_run_set_status(item, 4)
}
}
this._setGlobaleStatus(1)
}
getAnimationListLen() : number {
return this._callListFun.length;
}
/**
* 暂停动画。
* @param {string|null} uid 动画id,如果不填写或者为null表示暂停所有动画
*/
pause(uid : string | null = null) : xTween {
if (uid != null && uid != '') {
let index = this._getCallIndex(uid! as string)
if (index > -1) {
let item = this._callListFun[index]
this._by_run_set_status(item, this._isLoopPauseing(item) ? 6 : 3)
this._by_run_item_callFun(item, item.progress)
}
} else {
for (let i = 0; i < this._callListFun.length; i++) {
let item = this._callListFun[i]
this._by_run_set_status(item, this._isLoopPauseing(item) ? 6 : 3)
this._by_run_item_callFun(item, item.progress)
}
this._setGlobaleStatus(2)
}
return this;
}
/**
* 结束动画。
* @param {string|null} uid 动画id,如果不填写或者为null表示结束所有动画
*/
stop(uid : string | null = null) : xTween {
if (uid != null && uid != '') {
let index = this._getCallIndex(uid! as string)
if (index > -1) {
let item = this._callListFun[index]
this._by_run_set_status(item, 2)
this._by_run_item_callFun(item, 1)
}
} else {
for (let i = 0; i < this._callListFun.length; i++) {
let item = this._callListFun[i]
this._by_run_set_status(item, 2)
this._by_run_item_callFun(item, 1)
}
this._callListFun = this._callListFun.filter((el : xTweenEventCallFunType) : boolean => !el.autoRemove)
this.enters(this._callListFun, 0)
}
return this;
}
private _getEasing(args : any) : xTweenCallbackFunType | null {
let fun : xTweenCallbackFunType | null = (x : number) : number => x;
if (typeof args == 'string') {
let animateNumber = this.easingList.get(args as string)
if (animateNumber != null) {
let ease = animateNumber! as number[]
fun = bezier(ease[0], ease[1], ease[2], ease[3])
}
} else if (Array.isArray(args)) {
let argsar = args as number[]
if (argsar.length == 4) {
fun = bezier(argsar[0], argsar[1], argsar[2], argsar[3])
}
} else {
let animateNumber = this.easingList.get('linear')
if (animateNumber != null) {
let ease = animateNumber! as number[]
fun = bezier(ease[0], ease[1], ease[2], ease[3])
}
}
return fun;
}
private _getCallIndex(uid : string) : number {
let index = -1;
for (let i = 0; i < this._callListFun.length; i++) {
let item = this._callListFun[i]
if (item.id == uid) {
index = i;
break;
}
}
return index;
}
private _isLoopPauseing(item : xTweenEventCallFunType) : boolean {
if (item.loop == -1) return true;
if (item._finishLoop < item.loop) return true;
return false;
}
private _by_run_set_status(item : xTweenEventCallFunType, status : xTweenStatus) {
if (status == 1) {
item.progress = 0
item.startTime = 0
item._finishLoop = 0
} else if (status == 2) {
item.progress = 1;
item.oldProgeress = 0;
item.startTime = 0
item._finishLoop = item.loop
} else if (status == 3) {
item.startTime = 0
item.oldProgeress = item.progress
} else if (status == 5) {
item.progress = 0
item.oldProgeress = 0
item.startTime = 0
} else if (status == 6) {
item.oldProgeress = item.progress
item.startTime = 0
}
item.status = status;
}
private _by_run_item_callFun(item : xTweenEventCallFunType, progress : number) {
if (item.status == 1) {
item.start(item)
}
if (item.status == 2) {
item.enter(item)
item.complete(item)
if (item.autoRemove) {
let index = this._getCallIndex(item.id)
if (index > -1) {
this._callListFun.splice(index, 1)
}
}
}
if (item.status == 3) {
item.pause(item)
}
if (item.status == 4 || item.status == 5) {
item.enter(item)
}
}
/**
* 1:执行中
* 2:未在执行动画。
*/
private _setGlobaleStatus(type : number) {
if (type == 1) {
this.isRuning = true;
this.isStoping = false
} else if (type == 2) {
this.isRuning = false;
this.isStoping = true
}
}
private _run(_this : xTween) {
if (!_this.isRendering) {
return;
}
function actions(times:number){
_this.enters(_this._callListFun, times)
if (_this._frema_starttimes == 0) {
_this._frema_starttimes = times
}
let isAllCompelted = true;
for (let i = 0; i < _this._callListFun.length; i++) {
let item = _this._callListFun[i]
let isFinishStatus = 1
if (item.status == 4 || item.status == 5) {
if (item.startTime == 0) {
item.startTime = times
}
// 计算当前进度
if (_this.frmae > 0) {
let progress = Math.min((times - (item.startTime)) / item.duration + item.oldProgeress, 1);
let eas = item.ease!
item.progress = eas(progress)
if (progress == isFinishStatus) {
item._finishLoop += 1
if (item.loop > 0) {
if (item.loop == item._finishLoop) {
_this._by_run_set_status(item, 2)
_this._by_run_item_callFun(item, isFinishStatus)
} else {
_this._by_run_set_status(item, 5)
}
} else if (item.loop == -1) {
_this._by_run_set_status(item, 5)
}
if (item.tyty) {
item.reverse = !item.reverse
}
}
_this._by_run_item_callFun(item, item.progress)
}
}
if (item.progress != 2) {
isAllCompelted = false;
}
}
if (isAllCompelted && _this.isRuning) {
_this.complete()
_this._setGlobaleStatus(2)
}
_this.frmae = Math.ceil(1000 / (times - _this._frema_starttimes))
_this._frema_starttimes = times
_this._run(_this)
}
// #ifdef APP||WEB
_this.frameId = requestAnimationFrame((times : number) => {
actions(times)
})
// #endif
// #ifdef MP
_this.frameId = this.customRequestAnimationFrame((times : number) => {
actions(times)
})
// #endif
}
}
@@ -0,0 +1,528 @@
import { XUPLOADFILE_INFO, XUPLOADFILE_FILE_VALUE, XUPLOADFILE_FILE_INFO, XUPLOADFILE_EVENT_NAME } from "../../interface.uts"
import { getUid } from "./xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type CONFIG = {
count : number,
sourceType : Array<string>,
sizeType : Array<string>,
hostUrl : string,
name : string,
header : UTSJSONObject,
formData : UTSJSONObject,
multipart : boolean,
autoUpload : boolean,
statusCode : number,
compress:boolean,
quality:number|null,
compressedHeight:number|null,
compressedWidth:number|null
}
/**
* 媒体文件上传
*/
export class xUploadMedia {
model = 'photo'
videoOps={
pageOrientation:'auto',
albumMode:'system',
sourceType:['album', 'camera'] as string[],
compressed:true,
maxDuration:60,
camera:'back'
} as UTSJSONObject
config = {
count: 9,
sourceType: ['album', 'camera'],
sizeType: ['original', 'compressed'],
hostUrl: "",
name: "file",
header: {} as UTSJSONObject,
multipart: false,
formData:{} as UTSJSONObject,
autoUpload: true,
statusCode: 200,
compress:true,
quality:80,
compressedHeight:null,
compressedWidth:null
} as CONFIG
fileList : Array<XUPLOADFILE_FILE_INFO> = []
currentIndex = 0;
uploading = false;
uploadObj = null as null | UploadTask
systemError = xConfig.i18n.t("tmui4x.uploadMedia.systemError")
limitMaxCount = xConfig.i18n.t("tmui4x.uploadMedia.limitMaxCount")
constructor(opts : XUPLOADFILE_INFO = {} as XUPLOADFILE_INFO) {
this.chuliConfigArgs(opts)
}
chooseBefore = (res : string[]) : Promise<string[]> => Promise.resolve(res)
complete = (res : XUPLOADFILE_FILE_INFO[]) : Promise<XUPLOADFILE_FILE_INFO[]> => Promise.resolve(res)
change = (res : XUPLOADFILE_FILE_INFO[]) : Promise<XUPLOADFILE_FILE_INFO[]> => Promise.resolve(res)
beforeComplete = (res : XUPLOADFILE_FILE_INFO) :XUPLOADFILE_FILE_INFO => res
myChangeSync = function (res : XUPLOADFILE_FILE_INFO[]) { }
beforeUpload = (res : XUPLOADFILE_FILE_INFO) : Promise<XUPLOADFILE_FILE_INFO> => Promise.resolve(res)
events:Map<XUPLOADFILE_EVENT_NAME,(res : any) => Promise<any>> = new Map();
/**
* 处理配置参数
* @param opts 配置参数
*/
private chuliConfigArgs(opts : XUPLOADFILE_INFO) : CONFIG {
this.config = {
count: opts.count == null ? this.config.count : opts.count as number,
statusCode: opts.statusCode == null ? this.config.statusCode : opts.statusCode as number,
sourceType: opts.sourceType == null ? this.config.sourceType : opts.sourceType as string[],
sizeType: opts.sizeType == null ? this.config.sizeType : opts.sizeType as string[],
hostUrl: opts.hostUrl == null ? this.config.hostUrl : opts.hostUrl as string,
name: opts.name == null ? this.config.name : opts.name as string,
header: opts.header == null ? this.config.header : opts.header as UTSJSONObject,
formData: opts.formData == null ? this.config.formData : opts.formData as UTSJSONObject,
multipart: opts.multipart == null ? this.config.multipart : opts.multipart as boolean,
autoUpload: opts.autoUpload == null ? this.config.autoUpload : opts.autoUpload as boolean,
compress: opts.compress == null ? this.config.compress : opts.compress as boolean,
quality: opts.quality == null ? this.config.quality : opts.quality as number,
compressedHeight: opts.compressedHeight,
compressedWidth: opts.compressedWidth,
} as CONFIG
return this.config
}
stop() {
if (this.uploadObj == null) return
this.currentIndex = 0;
this.uploading = false;
this.uploadObj!.abort()
this.uploadObj = null;
console.info("xUploadMedia:中断上传")
}
chooseMedia() {
if (this.fileList.length == this.config.count) {
// "已超最大上传数量"
uni.showToast({ title: this.limitMaxCount, mask: true, icon: 'none' })
console.warn("xUploadMedia:已经超过最大上传数量")
return;
}
if(this.model == 'photo'){
uni.chooseImage({
count: Math.max(this.config.count - this.fileList.length, 0),
sourceType: this.config.sourceType,
sizeType: this.config.sizeType,
success: (res) => {
let temps = [] as UTSJSONObject[]
// #ifdef uniVersion >= 4.31
let items = res.tempFiles;
for(let i=0;i<res.tempFiles.length;i++){
temps.push({path:items[i].path,size:items[i].size} as UTSJSONObject)
}
// #endif
// #ifdef uniVersion < 4.31
temps = res.tempFiles as UTSJSONObject[]
// #endif
let chooseBefore = this.events.get('chooseBefore')
if(chooseBefore!=null){
// 处理新文件。
if (Array.isArray(temps)) {
chooseBefore(res.tempFilePaths).then(() => {
this.addNewFile(temps)
if (this.config.autoUpload && !this.uploading){
this.start()
}
}).catch(er => {
console.error(er)
uni.showModal({
title:this.systemError,
content:er as string,
showCancel:false
})
})
} else {
// @ts-ignore
let tps = res.tempFilePaths as UTSJSONObject
chooseBefore([tps] as UTSJSONObject[]).then(() => {
this.addNewFile(temps)
if (this.config.autoUpload && !this.uploading){
this.start()
}
}).catch(er => {
console.error(er)
uni.showModal({
title:this.systemError,
content:er as string,
showCancel:false
})
})
}
}else{
if (Array.isArray(temps)) {
this.addNewFile(temps)
if (this.config.autoUpload && !this.uploading){
this.start()
}
}else{
// @ts-ignore
let tps = res.tempFilePaths as UTSJSONObject
this.addNewFile([tps] as UTSJSONObject[])
if (this.config.autoUpload && !this.uploading){
this.start()
}
}
}
},
fail: (err) => {
this.chooseBefore([] as string[])
console.warn("xUploadMedia:", err)
}
})
}else if(this.model == 'video'){
let pageOrientation = this.videoOps.getString('pageOrientation')
pageOrientation = pageOrientation==null?'auto':pageOrientation
let albumMode = this.videoOps.getString('albumMode')
albumMode = albumMode==null?'system':albumMode
let sourceType = this.videoOps.getArray<string>('sourceType')
sourceType = sourceType==null?(['album', 'camera'] as string[]):sourceType
let compressed = this.videoOps.getBoolean('compressed')
compressed = compressed==null?true:compressed
let maxDuration = this.videoOps.getNumber('maxDuration')
maxDuration = maxDuration==null?60:maxDuration
let camera = this.videoOps.getString('camera')
camera = camera==null?'back':camera
uni.chooseVideo({
pageOrientation,
albumMode,
sourceType,
compressed,
maxDuration,
camera,
success: (res) => {
let temps = [
{path:res.tempFilePath,size:res.size} as UTSJSONObject
] as UTSJSONObject[]
let chooseBefore = this.events.get('chooseBefore')
if(chooseBefore!=null){
chooseBefore([res.tempFilePath] as string[]).then(() => {
this.addNewFile(temps)
if (this.config.autoUpload && !this.uploading){
this.start()
}
}).catch(er => {
console.error(er)
uni.showModal({
title:this.systemError,
content:er as string,
showCancel:false
})
})
}else{
this.addNewFile(temps)
if (this.config.autoUpload && !this.uploading){
this.start()
}
}
},
fail: (err) => {
this.chooseBefore([] as string[])
console.warn("xUploadMedia:", err)
}
} as ChooseVideoOptions)
}
}
/**
* 增加事件监听
* @param eventName 事件名称
* @param callback(res : T) => Promise<T> 回调,注意如果是chooseBefore,外部回调时res是string[],其它为 XUPLOADFILE_FILE_INFO[]
*/
addListenEvent(eventName : XUPLOADFILE_EVENT_NAME, callback : (res : any) => Promise<any>) {
this.events.set(eventName,callback)
// if (eventName == 'chooseBefore') {
// this.chooseBefore = (res2 : string[]) : Promise<string[]> => callback(res2 as unknown[] as T) as Promise<string[]>
// } else if (eventName == 'complete') {
// this.complete = (res2 : XUPLOADFILE_FILE_INFO[]) : Promise<XUPLOADFILE_FILE_INFO[]> => callback(res2 as unknown[] as T) as Promise<XUPLOADFILE_FILE_INFO[]>
// } else if (eventName == 'change') {
// this.change = (res2 : XUPLOADFILE_FILE_INFO[]) : Promise<XUPLOADFILE_FILE_INFO[]> => callback(res2 as unknown[] as T) as Promise<XUPLOADFILE_FILE_INFO[]>
// }
}
/**
* 增加事件监听
* @param eventName 事件名称
* @param callback(res : T) => Promise<T> 回调,注意如果是chooseBefore,外部回调时res是string[],其它为 XUPLOADFILE_FILE_INFO[]
*/
addListenEventsss<T extends unknown[]>(eventName : XUPLOADFILE_EVENT_NAME, callback : (res : T) => Promise<T>) {
if (eventName == 'chooseBefore') {
this.chooseBefore = (res2 : string[]) : Promise<string[]> => callback(res2 as unknown[] as T) as Promise<string[]>
} else if (eventName == 'complete') {
this.complete = (res2 : XUPLOADFILE_FILE_INFO[]) : Promise<XUPLOADFILE_FILE_INFO[]> => callback(res2 as unknown[] as T) as Promise<XUPLOADFILE_FILE_INFO[]>
} else if (eventName == 'change') {
this.change = (res2 : XUPLOADFILE_FILE_INFO[]) : Promise<XUPLOADFILE_FILE_INFO[]> => callback(res2 as unknown[] as T) as Promise<XUPLOADFILE_FILE_INFO[]>
}
}
/**
* 设置上传的配置参数
* @param opts 配置参数-XUPLOADFILE_INFO
*/
setConfig(opts : XUPLOADFILE_INFO) {
this.chuliConfigArgs(opts)
}
setVideoOps(config:UTSJSONObject,model:string){
this.videoOps = config
this.model = model;
}
setChangeSync(callBack : (res : XUPLOADFILE_FILE_INFO[]) => void) {
this.myChangeSync = function (res2 : XUPLOADFILE_FILE_INFO[]) {
callBack(res2)
}
}
delFile(id : string) : boolean {
let index = this.fileList.findIndex((item : XUPLOADFILE_FILE_INFO) : boolean => item.id == id)
if (index >= 0) {
let item = this.fileList[index];
if (item.status == 1) {
item.status = 4
item.statusText = "取消上传"
this.stop()
}
this.fileList.splice(index, 1)
this.myChangeSync(this.fileList)
return true;
}
return false
}
clear(){
this.fileList = [] as XUPLOADFILE_FILE_INFO[]
this.myChangeSync(this.fileList)
}
/**
* 添加新选择的未上传文件
*/
private addNewFile(files : UTSJSONObject[]) {
if (files.length == 0) return;
let t = this;
// #ifdef APP-ANDROID||APP-IOS
if(this.model == 'photo'){
let i=0;
function compress(){
if(i>=files.length) return;
let item = files[i]! as UTSJSONObject;
uni.compressImage({
src: item.getString('path')!,
quality: t.config.quality,
compressedHeight: t.config.compressedHeight,
compressedWidth: t.config.compressedWidth,
success: (res:CompressImageSuccess) => {
item.set("path",res.tempFilePath)
i+=1;
compress()
},
fail: (err) => {
i+=1;
compress()
}
})
}
if(this.config.compress){
compress()
}
}
// #endif
files.forEach((item : UTSJSONObject) => {
let id = getUid();
let name = "";
let size = 0
// #ifdef APP
let items = item as UTSJSONObject
name = items.getString('path')!
size = items.getNumber('size')!
// #endif
// #ifndef APP
name = item.path!
size = item.size!
// #endif
this.fileList.push({
id: id,
type: "",
size: size,
extension: name.substring(name.lastIndexOf(".") + 1),
statusText: '待上传',
status: 0,
path: name,
progress: 0,
response: "",
name: name.substring(name.lastIndexOf("/") + 1),
model:this.model
} as XUPLOADFILE_FILE_INFO)
})
this.myChangeSync(this.fileList)
}
addFile(files:XUPLOADFILE_FILE_VALUE[]){
let ids = this.fileList.map((el:XUPLOADFILE_FILE_INFO):string=>el.id)
let i=0;
files.forEach((el:XUPLOADFILE_FILE_VALUE)=>{
let oldId = el.id == null?"":el.id! as string;
if(!ids.includes(oldId)){
let id = getUid();
let name = el.url
let status = el?.status??2;
this.fileList.push({
id: id,
type: "",
size: 0,
extension: name.substring(name.lastIndexOf(".") + 1),
statusText: status==2?'上传成功':'待上传',
status: el?.status??2,
path: name,
progress: status==2?100:0,
response: el.response == null ?"":el.response! as string,
name: name.substring(name.lastIndexOf("/") + 1)
} as XUPLOADFILE_FILE_INFO)
++i;
}
})
if(i>0){
this.myChangeSync(this.fileList)
}
}
_addFilesByself(files:XUPLOADFILE_FILE_INFO[]){
this.fileList = files.slice(0)
}
/**
* 获取待上传的文件数量。
*/
private getWaitUploadFilesNumber() : number {
let num = 0;
this.fileList.forEach((item : XUPLOADFILE_FILE_INFO) => {
if (item.status == 0 || item.status == 3 || item.status == 4) {
num += 1;
}
})
return num;
}
start() {
if (this.uploading) return;
this.uploading = true;
this.currentIndex = 0
this.uploadFile();
}
private uploadFile() {
// if (!this.config.autoUpload) return;
// if (this.fileList.length == 0 || this.getWaitUploadFilesNumber() == 0) {
// this.uploadObj = null
// this.uploading = false;
// console.warn("xUploadMedia:上传结束了")
// return;
// }
if (this.currentIndex >= this.fileList.length) {
this.uploadObj = null;
this.uploading = false;
this.complete(this.fileList)
this.myChangeSync(this.fileList)
let complete = this.events.get('complete')
if(complete!=null){
complete(this.fileList.slice(0))
}
return;
}
this.beforeUpload(this.fileList[this.currentIndex]).then((beforeUploadFileRes:XUPLOADFILE_FILE_INFO)=>{
this.fileList[this.currentIndex] = beforeUploadFileRes;
let nowitemStatus = this.fileList[this.currentIndex].status
if (nowitemStatus == 1 || nowitemStatus == 2 || nowitemStatus == 5) {
this.uploadObj = null
this.uploading = false;
this.currentIndex += 1;
this.uploadFile();
return;
}
this.fileList[this.currentIndex].status = 1;
this.fileList[this.currentIndex].statusText = "上传中";
this.myChangeSync(this.fileList)
this.uploadObj = uni.uploadFile({
url: this.config.hostUrl,
filePath: this.fileList[this.currentIndex].path,
name: this.config.name,
formData: this.config.formData,
header:this.config.header,
success: (uploadFileRes) => {
if (uploadFileRes.statusCode != 200) {
this.fileList[this.currentIndex].status = 3;
this.fileList[this.currentIndex].statusText = "上传失败";
this.currentIndex += 1;
this.uploadFile();
this.myChangeSync(this.fileList)
return;
}
this.fileList[this.currentIndex].status = 2;
this.fileList[this.currentIndex].response = uploadFileRes.data;
this.fileList[this.currentIndex].statusText = "上传成功";
let itemtemp = this.fileList.slice(0)[this.currentIndex]
let calllItem = this.beforeComplete(itemtemp)
this.fileList[this.currentIndex] = calllItem
this.currentIndex += 1;
this.uploadFile();
this.myChangeSync(this.fileList)
},
fail: (err) => {
console.error("上传失败了,请检查配置:",err)
this.fileList[this.currentIndex].status = 3;
this.fileList[this.currentIndex].statusText = "上传失败";
this.currentIndex += 1;
this.uploadFile();
},
complete: () => {
this.myChangeSync(this.fileList)
}
})
this.uploadObj?.onProgressUpdate((res) => {
this.fileList[this.currentIndex].progress = res.progress;
// if(res.progress == 100){
// this.fileList[this.currentIndex].status = 1;
// this.fileList[this.currentIndex].statusText = "上传中";
// }else{
// this.fileList[this.currentIndex].status = 1;
// this.fileList[this.currentIndex].statusText = "上传中";
// }
this.myChangeSync(this.fileList)
})
})
}
}