Compare commits

3 Commits

5 changed files with 279 additions and 171 deletions

View File

@@ -32,7 +32,7 @@
{ {
"paragraph": [ "paragraph": [
{ {
"text": "Participants have the opportunity to compete for cash prizes totaling up to US$9,000, gain global media exposure showcased by top international platforms, and connect with designers and industry leaders worldwide. Finalists will also attend an exclusive award ceremony in Hong Kong, with travel support provided, allowing them to showcase their talent, network with professionals, and celebrate their achievements on an international stage." "text": "Participants have the opportunity to compete for cash prizes totaling up to US$9,000, gain global media exposure showcased by top international platforms, and connect with designers and industry leaders worldwide. Finalists will also attend an exclusive award ceremony in Hong Kong, with travel allowance, allowing them to showcase their talent, network with professionals, and celebrate their achievements on an international stage."
} }
] ]
} }

View File

@@ -1,55 +0,0 @@
<template>
<div class="edit-detail-wrapper">
<div class="edit-detail-header flex align-center space-between">
<div class="bread-crumb">导航</div>
<div class="operate-menu flex">
<div class="menu-btn flex align-center save">
<span>Save Draft</span>
<SvgIcon name="CSave" color="#000000" size="16" />
</div>
<div class="menu-btn flex align-center publish">
<span>Publish</span>
<SvgIcon name="CPublish" color="#ffffff" size="16" />
</div>
</div>
</div>
<div class="edit-detail-content"></div>
</div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped>
.c-svg {
width: initial;
}
.edit-detail-wrapper {
.menu-btn {
height: 6rem;
border: 0.15rem solid #000000;
border-radius: 4rem;
text-align: center;
line-height: 6rem;
padding: 0 2rem;
font-weight: 400;
font-size: 1.6rem;
column-gap: 0.8rem;
cursor: pointer;
}
.edit-detail-header {
width: 100%;
height: 6rem;
margin-bottom: 2rem;
.operate-menu {
column-gap: 2rem;
.publish {
background-color: #000000;
color: #ffffff;
}
}
}
.edit-detail-content{
padding-right: 6.4rem;
}
}
</style>

View File

@@ -23,6 +23,7 @@
<div class="content"> <div class="content">
<image-clip <image-clip
ref="imageClipRef" ref="imageClipRef"
v-bind="$attrs"
:ratio="data.ratio" :ratio="data.ratio"
:url="data.url" :url="data.url"
@change="(v) => (data.preview_url = v)" @change="(v) => (data.preview_url = v)"

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="image-clip"> <div class="image-clip" :class="{ 'is-product': isProduct }">
<div class="image-clip-body" ref="imageClipBody"> <div class="image-clip-body" ref="imageClipBody">
<VueCropper <VueCropper
ref="cropper" ref="cropper"
@@ -11,6 +11,8 @@
movable movable
centerBox centerBox
@realTime="onChange" @realTime="onChange"
v-bind="$attrs"
outputType="png"
></VueCropper> ></VueCropper>
</div> </div>
<div class="clip_opterate"> <div class="clip_opterate">
@@ -44,6 +46,10 @@
ratio: { ratio: {
type: Array, type: Array,
default: () => [1, 1] default: () => [1, 1]
},
isProduct: {
type: Boolean,
default: false
} }
}) })
const attrs = useAttrs() const attrs = useAttrs()
@@ -60,6 +66,7 @@
}) })
onMounted(() => { onMounted(() => {
observer.observe(imageClipBody.value) observer.observe(imageClipBody.value)
injectCropLabel()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
observer.disconnect() observer.disconnect()
@@ -86,6 +93,49 @@
cropper.value.getCropBlob(resolve) cropper.value.getCropBlob(resolve)
}) })
} }
const injectCropLabel = () => {
const cropperBox = imageClipBody.value.querySelector(".cropper-view-box")
if (cropperBox) {
// Add crown label
const crownLabel = document.createElement("div")
crownLabel.className = "cropper-line-label label-h"
crownLabel.textContent = "crown"
crownLabel.style.top = "2.67%"
crownLabel.style.left = "0"
crownLabel.style.transform = "translateY(-50%)"
cropperBox.appendChild(crownLabel)
// Add hip line label
const hipLabel = document.createElement("div")
hipLabel.className = "cropper-line-label label-h"
hipLabel.textContent = "hip line"
hipLabel.style.top = "63.47%"
hipLabel.style.left = "0"
hipLabel.style.transform = "translateY(-50%)"
cropperBox.appendChild(hipLabel)
// Add mid-thigh label
const thighLabel = document.createElement("div")
thighLabel.className = "cropper-line-label label-h"
thighLabel.textContent = "mid-thigh"
thighLabel.style.top = "92.8%"
thighLabel.style.left = "0"
thighLabel.style.transform = "translateY(-50%)"
cropperBox.appendChild(thighLabel)
// Add center label
const centerLabel = document.createElement("div")
centerLabel.className = "cropper-line-label label-v"
centerLabel.textContent = "center"
centerLabel.style.top = "0"
centerLabel.style.left = "50%"
centerLabel.style.transform = "translate(-50%, -50%)"
cropperBox.appendChild(centerLabel)
}
}
defineExpose({ defineExpose({
getCropUrl, getCropUrl,
getCropBlob getCropBlob
@@ -108,6 +158,15 @@
height: calc(40rem * 1.2); height: calc(40rem * 1.2);
// height: 53rem; // height: 53rem;
background: yellow; background: yellow;
:deep(.cropper-box) {
.cropper-box-canvas {
background-color: #ffffff;
img {
height: 100%;
}
}
}
} }
.clip_opterate { .clip_opterate {
@@ -153,5 +212,81 @@
} }
} }
} }
&.is-product {
.image-clip-body {
width: 45.7rem;
height: 45.7rem;
:deep(.cropper-modal) {
background: transparent;
}
:deep(.vue-cropper .cropper-view-box) {
position: relative;
overflow: visible !important;
/* 原有的蓝色边框outline由组件控制这里不干涉 */
}
:deep(.vue-cropper .cropper-view-box::after) {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9; /* 位于图片之上,但在控制点之下 */
/* 绘制 4 条 #4BA5FF 的虚线 */
background-image:
linear-gradient(to right, #4ba5ff 50%, transparent 50%),
/* Crown */ linear-gradient(to right, #4ba5ff 50%, transparent 50%),
/* Hip line */ linear-gradient(to right, #4ba5ff 50%, transparent 50%),
/* Mid-thigh */ linear-gradient(to bottom, #4ba5ff 50%, transparent 50%); /* Center */
background-repeat: repeat-x, repeat-x, repeat-x, repeat-y;
background-size:
8px 1px,
8px 1px,
8px 1px,
1px 8px; /* 虚线间距 */
background-position:
0 2.67%,
0 63.47%,
0 92.8%,
50% 0;
}
}
}
}
</style>
<style lang="less">
.cropper-line-label {
position: absolute;
color: #4ba5ff; /* 统一颜色 */
font-size: 11px;
font-weight: bold;
background: rgba(255, 255, 255); /* 浅色背景确保在深色图片上可见 */
// padding: 0 4px;
border-radius: 2px;
white-space: nowrap;
pointer-events: none;
z-index: 10;
line-height: 1.2;
}
/* 水平线名称:放在线段上方 2px */
.label-h {
transform: translateY(-100%);
margin-top: -2px;
left: 4px;
}
/* 垂直线名称:放在裁剪框顶部边缘上方 */
.label-v {
transform: translateX(-50%);
top: -20px;
left: 50%;
} }
</style> </style>

View File

@@ -38,7 +38,11 @@
{{ topImageTitleMap[type] }} {{ topImageTitleMap[type] }}
</div> </div>
<div class="sketch-item flex flex-center" :class="type"> <div class="sketch-item flex flex-center" :class="type">
<div v-if="previewImageMap[type]" class="crop-tool flex flex-center"> <div
v-if="previewImageMap[type]"
class="crop-tool flex flex-center"
@click="handleClickCrop(previewImageMap[type])"
>
<SvgIcon name="CCrop" color="#fff" size="12" /> <SvgIcon name="CCrop" color="#fff" size="12" />
</div> </div>
<img <img
@@ -119,7 +123,7 @@
</div> </div>
<div class="sketch-list-container flex"> <div class="sketch-list-container flex">
<div <div
v-for="(item, index) in sketchList" v-for="(item, index) in selectList[currentIndex].sketchList"
:key="index" :key="index"
class="sketch-element flex flex-center" class="sketch-element flex flex-center"
> >
@@ -212,6 +216,13 @@
</div> </div>
</div> </div>
</div> </div>
<ImageClipDialog
ref="imageClipDialogRef"
fixedBox
isProduct
:info="false"
:autoCropWidth="90"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -220,11 +231,14 @@ import { useRouter } from "vue-router"
import SellerHeader from "../../seller-header.vue" import SellerHeader from "../../seller-header.vue"
import testImg from "@/assets/images/test.png" import testImg from "@/assets/images/test.png"
import Radio from "./components/Radio.vue" import Radio from "./components/Radio.vue"
import ImageClipDialog from "../../BrandProfile/image-clip-dialog.vue"
import { Https } from "@/tool/https" import { Https } from "@/tool/https"
import { useStore } from "vuex" import { useStore } from "vuex"
const ROUTER = useRouter() const ROUTER = useRouter()
const imageClipDialogRef = ref<InstanceType<typeof ImageClipDialog> | null>(null)
defineOptions({ defineOptions({
name: "EditDetail" name: "EditDetail"
}) })
@@ -290,7 +304,8 @@ const selectList = ref<ListingItem[]>([
{ url: testImg, selected: false }, { url: testImg, selected: false },
{ url: testImg, selected: false }, { url: testImg, selected: false },
{ url: testImg, selected: false } { url: testImg, selected: false }
] ],
sketchList: [{ url: testImg }]
}, },
{ {
sketch: testImg, sketch: testImg,
@@ -311,7 +326,8 @@ const selectList = ref<ListingItem[]>([
{ url: testImg, selected: false }, { url: testImg, selected: false },
{ url: testImg, selected: false }, { url: testImg, selected: false },
{ url: testImg, selected: false } { url: testImg, selected: false }
] ],
sketchList: [{ url: testImg }, { url: testImg }]
}, },
{ {
sketch: testImg, sketch: testImg,
@@ -332,13 +348,13 @@ const selectList = ref<ListingItem[]>([
{ url: testImg, selected: false }, { url: testImg, selected: false },
{ url: testImg, selected: false }, { url: testImg, selected: false },
{ url: testImg, selected: false } { url: testImg, selected: false }
] ],
sketchList: [{ url: testImg }, { url: testImg }, { url: testImg }]
} }
]) ])
const prodImgList = computed(() => currentListing.value.prodImageList || []) const prodImgList = computed(() => currentListing.value.prodImageList || [])
const sketchList = ref([{ url: testImg }, { url: testImg }, { url: testImg }])
const categoryOptions = computed(() => { const categoryOptions = computed(() => {
const gender = selectList.value[currentIndex.value].gender const gender = selectList.value[currentIndex.value].gender
return fallbackCategoryOptions[gender] || [] return fallbackCategoryOptions[gender] || []
@@ -378,6 +394,17 @@ const handleSelectProdImg = (index: number) => {
} }
} }
const handleClickCrop = (data) => {
console.log(data)
imageClipDialogRef.value.open(
data,
(file) => {
selectList.value[currentIndex.value].sketch = URL.createObjectURL(file)
},
{ ratio: [9, 16], isPreview: true, title: "Crop Brand Banner" }
)
}
const handleClickMenu = (status: "draft" | "publish") => { const handleClickMenu = (status: "draft" | "publish") => {
if (status === "draft") { if (status === "draft") {
// save draft logic // save draft logic