Compare commits
3 Commits
1fe1bd4aa2
...
68f17ba87b
| Author | SHA1 | Date | |
|---|---|---|---|
| 68f17ba87b | |||
| 92c789510f | |||
| 0a442f8132 |
@@ -32,7 +32,7 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -23,6 +23,7 @@
|
||||
<div class="content">
|
||||
<image-clip
|
||||
ref="imageClipRef"
|
||||
v-bind="$attrs"
|
||||
:ratio="data.ratio"
|
||||
:url="data.url"
|
||||
@change="(v) => (data.preview_url = v)"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="image-clip">
|
||||
<div class="image-clip" :class="{ 'is-product': isProduct }">
|
||||
<div class="image-clip-body" ref="imageClipBody">
|
||||
<VueCropper
|
||||
ref="cropper"
|
||||
@@ -11,6 +11,8 @@
|
||||
movable
|
||||
centerBox
|
||||
@realTime="onChange"
|
||||
v-bind="$attrs"
|
||||
outputType="png"
|
||||
></VueCropper>
|
||||
</div>
|
||||
<div class="clip_opterate">
|
||||
@@ -33,125 +35,258 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, useAttrs, onMounted, onBeforeUnmount } 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]
|
||||
}
|
||||
})
|
||||
const attrs = useAttrs()
|
||||
import { ref, useAttrs, onMounted, onBeforeUnmount } 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
|
||||
}
|
||||
})
|
||||
const attrs = useAttrs()
|
||||
|
||||
const onChange = (data) => {
|
||||
if (attrs.onChange) {
|
||||
getCropUrl().then((url) => attrs.onChange(url))
|
||||
}
|
||||
const onChange = (data) => {
|
||||
if (attrs.onChange) {
|
||||
getCropUrl().then((url) => attrs.onChange(url))
|
||||
}
|
||||
const cropper = ref(null)
|
||||
const imageClipBody = ref(null)
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
refreshCrop()
|
||||
}
|
||||
const cropper = ref(null)
|
||||
const imageClipBody = ref(null)
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
refreshCrop()
|
||||
})
|
||||
onMounted(() => {
|
||||
observer.observe(imageClipBody.value)
|
||||
injectCropLabel()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
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)
|
||||
})
|
||||
onMounted(() => {
|
||||
observer.observe(imageClipBody.value)
|
||||
}
|
||||
const getCropBlob = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
cropper.value.getCropBlob(resolve)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
const rotateLeft = () => {
|
||||
cropper.value.rotateLeft()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getCropUrl,
|
||||
getCropBlob
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.image-clip {
|
||||
.image-clip {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// height: 100%;
|
||||
background: #fff;
|
||||
border-radius: calc(2rem * 1.2);
|
||||
padding: calc(1.3rem * 1.2) calc(1.3rem * 1.2) calc(2rem * 1.2);
|
||||
box-sizing: border-box;
|
||||
|
||||
.image-clip-body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// height: 100%;
|
||||
background: #fff;
|
||||
border-radius: calc(2rem * 1.2);
|
||||
padding: calc(1.3rem * 1.2) calc(1.3rem * 1.2) calc(2rem * 1.2);
|
||||
box-sizing: border-box;
|
||||
height: calc(40rem * 1.2);
|
||||
// height: 53rem;
|
||||
background: yellow;
|
||||
:deep(.cropper-box) {
|
||||
.cropper-box-canvas {
|
||||
background-color: #ffffff;
|
||||
|
||||
.image-clip-body {
|
||||
width: 100%;
|
||||
height: calc(40rem * 1.2);
|
||||
// height: 53rem;
|
||||
background: yellow;
|
||||
}
|
||||
|
||||
.clip_opterate {
|
||||
margin: calc(2.7rem * 1.2) auto 0;
|
||||
border-radius: calc(1.6rem * 1.2);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e2e4;
|
||||
width: calc(24rem * 1.2);
|
||||
|
||||
.item {
|
||||
width: calc(4.7rem * 1.2);
|
||||
height: calc(4rem * 1.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 0.1rem solid #e6e8ea;
|
||||
cursor: pointer;
|
||||
|
||||
.icon_chexiao_sec {
|
||||
transform: rotateY(180deg); /* 垂直镜像翻转 */
|
||||
}
|
||||
|
||||
.operate_icon {
|
||||
font-size: calc(1.8rem * 1.2);
|
||||
color: rgba(102, 102, 102, 1);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.icon_font {
|
||||
font-size: calc(2.5rem * 1.2);
|
||||
position: relative;
|
||||
top: calc(-0.3rem * 1.2);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.icon-shuaxin {
|
||||
font-size: calc(1.4rem * 1.2);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clip_opterate {
|
||||
margin: calc(2.7rem * 1.2) auto 0;
|
||||
border-radius: calc(1.6rem * 1.2);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e2e4;
|
||||
width: calc(24rem * 1.2);
|
||||
|
||||
.item {
|
||||
width: calc(4.7rem * 1.2);
|
||||
height: calc(4rem * 1.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 0.1rem solid #e6e8ea;
|
||||
cursor: pointer;
|
||||
|
||||
.icon_chexiao_sec {
|
||||
transform: rotateY(180deg); /* 垂直镜像翻转 */
|
||||
}
|
||||
|
||||
.operate_icon {
|
||||
font-size: calc(1.8rem * 1.2);
|
||||
color: rgba(102, 102, 102, 1);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.icon_font {
|
||||
font-size: calc(2.5rem * 1.2);
|
||||
position: relative;
|
||||
top: calc(-0.3rem * 1.2);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.icon-shuaxin {
|
||||
font-size: calc(1.4rem * 1.2);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.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>
|
||||
|
||||
@@ -38,7 +38,11 @@
|
||||
{{ topImageTitleMap[type] }}
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
<img
|
||||
@@ -119,7 +123,7 @@
|
||||
</div>
|
||||
<div class="sketch-list-container flex">
|
||||
<div
|
||||
v-for="(item, index) in sketchList"
|
||||
v-for="(item, index) in selectList[currentIndex].sketchList"
|
||||
:key="index"
|
||||
class="sketch-element flex flex-center"
|
||||
>
|
||||
@@ -212,6 +216,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ImageClipDialog
|
||||
ref="imageClipDialogRef"
|
||||
fixedBox
|
||||
isProduct
|
||||
:info="false"
|
||||
:autoCropWidth="90"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -220,11 +231,14 @@ import { useRouter } from "vue-router"
|
||||
import SellerHeader from "../../seller-header.vue"
|
||||
import testImg from "@/assets/images/test.png"
|
||||
import Radio from "./components/Radio.vue"
|
||||
import ImageClipDialog from "../../BrandProfile/image-clip-dialog.vue"
|
||||
import { Https } from "@/tool/https"
|
||||
import { useStore } from "vuex"
|
||||
|
||||
const ROUTER = useRouter()
|
||||
|
||||
const imageClipDialogRef = ref<InstanceType<typeof ImageClipDialog> | null>(null)
|
||||
|
||||
defineOptions({
|
||||
name: "EditDetail"
|
||||
})
|
||||
@@ -290,7 +304,8 @@ const selectList = ref<ListingItem[]>([
|
||||
{ url: testImg, selected: false },
|
||||
{ url: testImg, selected: false },
|
||||
{ url: testImg, selected: false }
|
||||
]
|
||||
],
|
||||
sketchList: [{ url: testImg }]
|
||||
},
|
||||
{
|
||||
sketch: testImg,
|
||||
@@ -311,7 +326,8 @@ const selectList = ref<ListingItem[]>([
|
||||
{ url: testImg, selected: false },
|
||||
{ url: testImg, selected: false },
|
||||
{ url: testImg, selected: false }
|
||||
]
|
||||
],
|
||||
sketchList: [{ url: testImg }, { url: testImg }]
|
||||
},
|
||||
{
|
||||
sketch: testImg,
|
||||
@@ -332,13 +348,13 @@ const selectList = ref<ListingItem[]>([
|
||||
{ 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 sketchList = ref([{ url: testImg }, { url: testImg }, { url: testImg }])
|
||||
const categoryOptions = computed(() => {
|
||||
const gender = selectList.value[currentIndex.value].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") => {
|
||||
if (status === "draft") {
|
||||
// save draft logic
|
||||
|
||||
Reference in New Issue
Block a user