@@ -7,7 +7,7 @@
crossOrigin = "Anonymous"
:autoCrop = "true"
:fixedNumber = "ratio"
: fixed = "type !== 'apparel' && isProduct "
: fixed = "type !== 'apparel'"
movable
centerBox
:fixedBox = "fixedBox"
@@ -36,361 +36,345 @@
< / div >
< / template >
< script setup >
import { ref , useAttrs , onMounted , onBeforeUnmount , computed , nextTick , watch } from "vue"
import "vue-cropper/dist/index.css"
import { VueCropper } from "vue-cropper"
const props = defineProps ( {
url : {
type : String ,
default : ""
} ,
ratio : {
type : Array ,
default : ( ) => [ 1 , 1 ]
} ,
isProduct : {
type : Boolean ,
default : false
} ,
fixedBox : {
type : Boolean ,
default : true
} ,
type : {
type : String ,
default : ( ) => ""
}
} )
const attrs = useAttrs ( )
const autoCropHeight = computed ( ( ) => {
let height = 426
if ( props . type === "cover" ) height = 375
else if ( props . type === "apparel" ) height = 320
return height
} )
const bindProps = computed ( ( ) => {
// :autoCropWidth="isProduct ? undefined : type === 'cover' ? 297 : 242"
// :autoCropHeight="isProduct ? undefined : autoCropHeight"
if ( props . isProduct ) {
return {
autoCropHeight : autoCropHeight . value ,
autoCropWidth : props . type === "cover" ? 297 : 242
}
}
} )
const onChange = ( data ) => {
if ( attrs . onChange ) {
getCropUrl ( ) . then ( ( url ) => attrs . onChange ( url ) )
}
}
const cropper = ref ( null )
const imageClipBody = ref ( null )
let injectLabelFrame = 0
const observer = new ResizeObserver ( ( entries ) => {
refreshCrop ( )
} )
const clearCropLabels = ( cropperBox ) => {
if ( ! cropperBox ) return
cropperBox . querySelectorAll ( ".cropper-line-label" ) . forEach ( ( node ) => node . remove ( ) )
}
const createCropLabel = ( { text , top , className } ) => {
const label = document . createElement ( "div" )
label . className = ` cropper-line-label ${ className } `
label . textContent = text
label . style . top = top
label . style . left = className === "label-v" ? "50%" : "0"
label . style . transform = className === "label-v" ? "translate(-50%, -50%)" : "translateY(-50%)"
return label
}
const cropLabelMap = {
cover : [
{ text : "crown" , top : "2.67%" , className : "label-h" } ,
{ text : "hip line" , top : "63.47%" , className : "label-h" } ,
{ text : "mid-thigh" , top : "92.8%" , className : "label-h" } ,
{ text : "center" , top : "0" , className : "label-v" }
] ,
mainProductImage : [
{ text : "crown" , top : "2.67%" , className : "label-h" } ,
{ text : "footbase" , top : "97.6%" , className : "label-h" } ,
{ text : "center" , top : "0" , className : "label-v" }
] ,
sketch : [
{ text : "crown" , top : "2.67%" , className : "label-h" } ,
{ text : "footbase" , top : "97.6%" , className : "label-h" } ,
{ text : "center" , top : "0" , className : "label-v" }
] ,
apparel : [ { text : "center" , top : "0" , className : "label-v" } ]
}
const injectCropLabel = ( ) => {
const cropperBox = imageClipBody . value ? . querySelector ( ".cropper-view-box" )
if ( ! cropperBox ) return false
clearCropLabels ( cropperBox )
; ( cropLabelMap [ props . type ] || [ ] ) . forEach ( ( config ) => {
cropperBox . appendChild ( createCropLabel ( config ) )
} )
return true
}
const scheduleInjectCropLabel = ( retry = 0 ) => {
cancelAnimationFrame ( injectLabelFrame )
injectLabelFrame = requestAnimationFrame ( ( ) => {
if ( ! injectCropLabel ( ) && retry < 10 ) {
scheduleInjectCropLabel ( retry + 1 )
import { ref , useAttrs , onMounted , onBeforeUnmount , computed , nextTick , watch } from "vue"
import "vue-cropper/dist/index.css"
import { VueCropper } from "vue-cropper"
const props = defineProps ( {
url : {
type : String ,
default : ""
} ,
ratio : {
type : Array ,
default : ( ) => [ 1 , 1 ]
} ,
isProduct : {
type : Boolean ,
default : false
} ,
fixedBox : {
type : Boolean ,
default : true
} ,
type : {
type : String ,
default : ( ) => ""
}
} )
}
const attrs = useAttrs ( )
onMoun ted( ( ) => {
observer . observe ( imageClipBody . value )
scheduleInjectCropLabel ( )
} )
onBeforeUnmount ( ( ) => {
observer . disconnect ( )
cancelAnimationFrame ( injectLabelFrame )
} )
const rotateLeft = ( ) => {
cropper . value . rotateLeft ( )
}
const rotateRight = ( ) => {
cropper . value . rotateRight ( )
}
const refreshCrop = ( ) => {
cropper . value . refresh ( )
}
const changeScale = ( num = 1 ) => {
cropper . value . changeScale ( num )
}
const getCropUrl = ( ) => {
return new Promise ( ( resolve , reject ) => {
cropper . value . getCropData ( resolve )
const autoCropHeight = compu ted ( ( ) => {
let height = 426
if ( props . type === "cover" ) height = 375
else if ( props . type === "apparel" ) height = 320
return height
} )
}
const getCropBlob = ( ) => {
return new Promise ( ( resolve , reject ) => {
cropper . value . getCropBlob ( resolve )
} )
}
watch (
[ ( ) => props . type , ( ) => props . url ] ,
async ( ) => {
await nextTick ( )
const bindProps = computed ( ( ) => {
// :autoCropWidth="isProduct ? undefined : type === 'cover' ? 297 : 242"
// :autoCropHeight="isProduct ? undefined : autoCropHeight"
if ( props . isProduct ) {
return {
autoCropHeight : autoCropHeight . value ,
autoCropWidth : props . type === "cover" ? 297 : 242
}
}
} )
const onChange = ( data ) => {
if ( attrs . onChange ) {
getCropUrl ( ) . then ( ( url ) => attrs . onChange ( url ) )
}
}
const cropper = ref ( null )
const imageClipBody = ref ( null )
let injectLabelFrame = 0
const observer = new ResizeObserver ( ( entries ) => {
refreshCrop ( )
} )
const clearCropLabels = ( cropperBox ) => {
if ( ! cropperBox ) return
cropperBox . querySelectorAll ( ".cropper-line-label" ) . forEach ( ( node ) => node . remove ( ) )
}
const createCropLabel = ( { text , top , className } ) => {
const label = document . createElement ( "div" )
label . className = ` cropper-line-label ${ className } `
label . textContent = text
label . style . top = top
label . style . left = className === "label-v" ? "50%" : "0"
label . style . transform = className === "label-v" ? "translate(-50%, -50%)" : "translateY(-50%)"
return label
}
const cropLabelMap = {
cover : [
{ text : "crown" , top : "2.67%" , className : "label-h" } ,
{ text : "hip line" , top : "63.47%" , className : "label-h" } ,
{ text : "mid-thigh" , top : "92.8%" , className : "label-h" } ,
{ text : "center" , top : "0" , className : "label-v" }
] ,
mainProductImage : [
{ text : "crown" , top : "2.67%" , className : "label-h" } ,
{ text : "footbase" , top : "97.6%" , className : "label-h" } ,
{ text : "center" , top : "0" , className : "label-v" }
] ,
sketch : [
{ text : "crown" , top : "2.67%" , className : "label-h" } ,
{ text : "footbase" , top : "97.6%" , className : "label-h" } ,
{ text : "center" , top : "0" , className : "label-v" }
] ,
apparel : [ { text : "center" , top : "0" , className : "label-v" } ]
}
const injectCropLabel = ( ) => {
const cropperBox = imageClipBody . value ? . querySelector ( ".cropper-view-box" )
if ( ! cropperBox ) return false
clearCropLabels ( cropperBox )
; ( cropLabelMap [ props . type ] || [ ] ) . forEach ( ( config ) => {
cropperBox . appendChild ( createCropLabel ( config ) )
} )
return true
}
const scheduleInjectCropLabel = ( retry = 0 ) => {
cancelAnimationFrame ( injectLabelFrame )
injectLabelFrame = requestAnimationFrame ( ( ) => {
if ( ! injectCropLabel ( ) && retry < 10 ) {
scheduleInjectCropLabel ( retry + 1 )
}
} )
}
onMounted ( ( ) => {
observer . observe ( imageClipBody . value )
scheduleInjectCropLabel ( )
} ,
{ flush : "post" }
)
} )
onBeforeUnmount ( ( ) => {
observer . disconnect ( )
cancelAnimationFrame ( injectLabelFrame )
} )
const rotateLeft = ( ) => {
cropper . value . rotateLeft ( )
}
const rotateRight = ( ) => {
cropper . value . rotateRight ( )
}
const refreshCrop = ( ) => {
cropper . value . refresh ( )
}
const changeScale = ( num = 1 ) => {
cropper . value . changeScale ( num )
}
const getCropUrl = ( ) => {
return new Promise ( ( resolve , reject ) => {
cropper . value . getCropData ( resolve )
} )
}
const getCropBlob = ( ) => {
return new Promise ( ( resolve , reject ) => {
cropper . value . getCropBlob ( resolve )
} )
}
defineExpose ( {
getCropUrl ,
getCropBlob
} )
watch (
[ ( ) => props . type , ( ) => props . url ] ,
async ( ) => {
await nextTick ( )
scheduleInjectCropLabel ( )
} ,
{ flush : "post" }
)
defineExpose ( {
getCropUrl ,
getCropBlob
} )
< / script >
< style lang = "less" scoped >
. image - clip {
width : 100 % ;
height : 100 % ;
display : flex ;
flex - direction : column ;
// height: 100%;
background : # fff ;
border - radius : calc ( 2 rem * 1.2 ) ;
padding : calc ( 1.3 rem * 1.2 ) calc ( 1.3 rem * 1.2 ) calc ( 2 rem * 1.2 ) ;
box - sizing : border - box ;
. image - clip - body {
. image - clip {
width : 100 % ;
height : calc ( 40 rem * 1.2 ) ;
// height: 53rem;
background : yellow ;
: deep ( . cropper - box ) {
. cropper - box - canvas {
background - color : # ffffff ;
img {
height : 100 % ;
}
}
}
& . is - cover {
: deep ( . vue - cropper ) {
overflow : hidden ;
}
: deep ( . cropper - box - canvas ) {
width : 31.1 rem ! important ;
left : 50 % ! important ;
transform : translateX ( - 50 % ) ! important ;
img {
display : none ;
}
}
}
}
. clip _opterate {
margin : calc ( 2.7 rem * 1.2 ) auto 0 ;
border - radius : calc ( 1.6 rem * 1.2 ) ;
height : 100 % ;
display : flex ;
overflow : hidde n;
border : 1 px solid # e2e2e4 ;
width : calc ( 24 rem * 1.2 ) ;
flex - direction : colum n;
// height: 100%;
background : # fff ;
border - radius : calc ( 2 rem * 1.2 ) ;
padding : calc ( 1.3 rem * 1.2 ) calc ( 1.3 rem * 1.2 ) calc ( 2 rem * 1.2 ) ;
box - sizing : border - box ;
. item {
width : calc ( 4.7 rem * 1.2 ) ;
height : calc ( 4 rem * 1.2 ) ;
display : flex ;
align - items : center ;
justify - content : center ;
border - right : 0.1 rem solid # e6e8ea ;
cursor : pointer ;
. icon _chexiao _sec {
transform : rotateY ( 180 deg ) ; /* 垂直镜像翻转 */
}
. operate _icon {
font - size : calc ( 1.8 rem * 1.2 ) ;
color : rgba ( 102 , 102 , 102 , 1 ) ;
font - weight : bold ;
}
. icon _font {
font - size : calc ( 2.5 rem * 1.2 ) ;
position : relative ;
top : calc ( - 0.3 rem * 1.2 ) ;
user - select : none ;
}
. icon - shuaxin {
font - size : calc ( 1.4 rem * 1.2 ) ;
}
& : last - child {
border : none ;
}
}
}
& . is - product {
. image - clip - body {
width : 45.7 rem ;
height : 45.7 rem ;
: deep ( . cropper - modal ) {
background : transparent ;
}
: deep ( . vue - cropper . cropper - view - box ) {
position : relative ;
overflow : visible ! important ;
/* 原有的蓝色边框( outline) 由组件控制, 这里不干涉 */
}
width : 100 % ;
height : calc ( 40 rem * 1.2 ) ;
// height: 53rem;
background : yellow ;
: deep ( . cropper - box ) {
. cropper - box - canvas {
background - color : # ffffff ;
: deep ( . vue - cropper . cropper - view - box : : after ) {
content : "" ;
position : absolute ;
top : 0 ;
left : 0 ;
width : 100 % ;
height : 100 % ;
pointer - events : none ;
z - index : 9 ; /* 位于图片之上,但在控制点之下 */
background - image : none ;
background - repeat : no - repeat ;
img {
height : 100 % ;
}
}
}
}
& [ data - crop - type = "cover" ] {
. image - clip - body {
: deep ( . vue - cropper . cropper - view - box : : after ) {
background - image :
linear - gradient ( to right , # 4 ba5ff 50 % , transparent 50 % ) ,
linear - gradient ( to right , # 4 ba5ff 50 % , transparent 50 % ) ,
linear - gradient ( to right , # 4 ba5ff 50 % , transparent 50 % ) ,
linear - gradient ( to bottom , # 4 ba5ff 50 % , transparent 50 % ) ;
background - repeat : repeat - x , repeat - x , repeat - x , repeat - y ;
background - size :
8 px 1 px ,
8 px 1 px ,
8 px 1 px ,
1 px 8 px ;
background - position :
0 2.67 % ,
0 63.47 % ,
0 92.8 % ,
50 % 0 ;
& . is - cover {
: deep ( . vue - cropper ) {
overflow : hidden ;
}
: deep ( . cropper - box - canvas ) {
width : 31.1 rem ! important ;
left : 50 % ! important ;
transform : translateX ( - 50 % ) ! important ;
img {
display : none ;
}
}
}
}
& [ data - crop - type = "mainProductImage" ] ,
& [ data - crop - type = "sketch" ] {
. image - clip - body {
: deep ( . vue - cropper . cropper - view - box : : after ) {
background - image :
linear - gradient ( to right , # 4 ba5ff 50 % , transparent 50 % ) ,
linear - gradient ( to right , # 4 ba5ff 50 % , transparent 50 % ) ,
linear - gradient ( to bottom , # 4 ba5ff 50 % , transparent 50 % ) ;
background - repeat : repeat - x , repeat - x , repeat - y ;
background - size :
8 px 1 px ,
8 px 1 p x ,
1 px 8 px ;
background - position :
0 2.67 % ,
0 97.6 % ,
50 % 0 ;
. clip _opterate {
margin : calc ( 2.7 rem * 1.2 ) auto 0 ;
border - radius : calc ( 1.6 rem * 1.2 ) ;
display : flex ;
overflow : hidden ;
border : 1 px solid # e2e2e4 ;
width : calc ( 2 4rem * 1.2 ) ;
. item {
width : calc ( 4.7 rem * 1.2 ) ;
height : calc ( 4 rem * 1.2 ) ;
display : fle x;
align - items : center ;
justify - content : center ;
border - right : 0.1 rem solid # e6e8ea ;
cursor : pointer ;
. icon _chexiao _sec {
transform : rotateY ( 180 deg ) ; /* 垂直镜像翻转 */
}
. operate _icon {
font - size : calc ( 1.8 rem * 1.2 ) ;
color : rgba ( 102 , 102 , 102 , 1 ) ;
font - weight : bold ;
}
. icon _font {
font - size : calc ( 2.5 rem * 1.2 ) ;
position : relative ;
top : calc ( - 0.3 rem * 1.2 ) ;
user - select : none ;
}
. icon - shuaxin {
font - size : calc ( 1.4 rem * 1.2 ) ;
}
& : last - child {
border : none ;
}
}
}
& [ data - c rop - type = "apparel" ] {
& . is - p roduct {
. image - clip - body {
width : 45.7 rem ;
height : 45.7 rem ;
: deep ( . cropper - modal ) {
background : transparent ;
}
: deep ( . vue - cropper . cropper - view - box ) {
position : relative ;
overflow : visible ! important ;
/* 原有的蓝色边框( outline) 由组件控制, 这里不干涉 */
}
: deep ( . vue - cropper . cropper - view - box : : after ) {
background - image : linear - gradient ( to bottom , # 4 ba5ff 50 % , transparent 50 % ) ;
background - repeat : repeat - y ;
background - size : 1 px 8 px ;
background - position : 50 % 0 ;
content : "" ;
position : absolute ;
top : 0 ;
left : 0 ;
width : 100 % ;
height : 100 % ;
pointer - events : none ;
z - index : 9 ; /* 位于图片之上,但在控制点之下 */
background - image : none ;
background - repeat : no - repeat ;
}
}
& [ data - crop - type = "cover" ] {
. image - clip - body {
: deep ( . vue - cropper . cropper - view - box : : after ) {
background - image : linear - gradient ( to right , # 4 ba5ff 50 % , transparent 50 % ) ,
linear - gradient ( to right , # 4 ba5ff 50 % , transparent 50 % ) ,
linear - gradient ( to right , # 4 ba5ff 50 % , transparent 50 % ) ,
linear - gradient ( to bottom , # 4 ba5ff 50 % , transparent 50 % ) ;
background - repeat : repeat - x , repeat - x , repeat - x , repeat - y ;
background - size : 8 px 1 px , 8 px 1 px , 8 px 1 px , 1 px 8 px ;
background - position : 0 2.67 % , 0 63.47 % , 0 92.8 % , 50 % 0 ;
}
}
}
& [ data - crop - type = "mainProductImage" ] ,
& [ data - crop - type = "sketch" ] {
. image - clip - body {
: deep ( . vue - cropper . cropper - view - box : : after ) {
background - image : linear - gradient ( to right , # 4 ba5ff 50 % , transparent 50 % ) ,
linear - gradient ( to right , # 4 ba5ff 50 % , transparent 50 % ) ,
linear - gradient ( to bottom , # 4 ba5ff 50 % , transparent 50 % ) ;
background - repeat : repeat - x , repeat - x , repeat - y ;
background - size : 8 px 1 px , 8 px 1 px , 1 px 8 px ;
background - position : 0 2.67 % , 0 97.6 % , 50 % 0 ;
}
}
}
& [ data - crop - type = "apparel" ] {
. image - clip - body {
: deep ( . vue - cropper . cropper - view - box : : after ) {
background - image : linear - gradient ( to bottom , # 4 ba5ff 50 % , transparent 50 % ) ;
background - repeat : repeat - y ;
background - size : 1 px 8 px ;
background - position : 50 % 0 ;
}
}
}
}
}
}
< / style >
< style lang = "less" >
. cropper - line - label {
position : absolute ;
color : # 4 ba5ff ; /* 统一颜色 */
font - size : 11 px ;
font - weight : bold ;
background : rgba ( 255 , 255 , 255 ) ; /* 浅色背景确保在深色图片上可见 */
// padding: 0 4px;
border - radius : 2 px ;
white - space : nowrap ;
pointer - events : none ;
z - index : 10 ;
line - height : 1.2 ;
}
. cropper - line - label {
position : absolute ;
color : # 4 ba5ff ; /* 统一颜色 */
font - size : 11 px ;
font - weight : bold ;
background : rgba ( 255 , 255 , 255 ) ; /* 浅色背景确保在深色图片上可见 */
// padding: 0 4px;
border - radius : 2 px ;
white - space : nowrap ;
pointer - events : none ;
z - index : 10 ;
line - height : 1.2 ;
}
/* 水平线名称:放在线段上方 2px */
. label - h {
transform : translateY ( - 100 % ) ;
margin - top : - 2 px ;
left : 4 px ;
}
/* 水平线名称:放在线段上方 2px */
. label - h {
transform : translateY ( - 100 % ) ;
margin - top : - 2 px ;
left : 4 px ;
}
/* 垂直线名称:放在裁剪框顶部边缘上方 */
. label - v {
transform : translateX ( - 50 % ) ;
top : - 20 px ;
left : 50 % ;
}
/* 垂直线名称:放在裁剪框顶部边缘上方 */
. label - v {
transform : translateX ( - 50 % ) ;
top : - 20 px ;
left : 50 % ;
}
< / style >