This commit is contained in:
李志鹏
2026-04-13 14:35:12 +08:00
parent 5e77348913
commit 74d8723ecd
10 changed files with 516 additions and 22 deletions

View File

@@ -0,0 +1,166 @@
<template>
<a-modal
class="image-clip-dialog generalModel"
v-model:visible="show"
:footer="null"
width="70%"
:maskClosable="false"
:centered="true"
:closable="false"
wrapClassName="#app"
:keyboard="false"
>
<div class="image-clip-dialog-box">
<div class="header">
<div class="title">{{ data.title }}</div>
<div class="right">
<div class="submit" v-if="!data.isPreview" @click="onSubmit">
<svg-icon name="seller-dui" size="24" />
</div>
<button @click="onCancel">Cancel</button>
</div>
</div>
<div class="content">
<image-clip
ref="imageClipRef"
:ratio="data.ratio"
:url="data.url"
@change="(v) => (data.preview_url = v)"
/>
<div class="preview" v-if="data.isPreview">
<div class="title">
<span class="icon"><svg-icon name="seller-preview" size="24" /></span>
<span class="label">Crop Preview</span>
</div>
<img :src="data.preview_url" />
<div class="submit" @click="onSubmit">
<svg-icon name="seller-dui" size="24" />
</div>
</div>
</div>
</div>
</a-modal>
</template>
<script setup>
import { ref } from "vue"
import ImageClip from "./image-clip.vue"
const data = reactive({
url: "",
title: "Crop Image",
preview_url: "",
ratio: [1, 1],
isPreview: true,
callback: null
})
const show = ref(false)
const open = (url, callback, options) => {
if (!url || !callback) return
data.url = url
data.callback = callback
data.ratio = [1, 1]
data.isPreview = true
data.preview_url = ""
data.title = "Crop Image"
if (options) {
if (options.hasOwnProperty("isPreview")) data.isPreview = options.isPreview
if (options.hasOwnProperty("ratio")) data.ratio = options.ratio
if (options.hasOwnProperty("title")) data.title = options.title
}
show.value = true
}
const onCancel = () => {
show.value = false
}
const imageClipRef = ref(null)
const onSubmit = () => {
imageClipRef.value.getCropBlob().then((blob) => {
if (data.callback) data.callback(blobToFile(blob, "image.png"))
onCancel()
})
}
// 将blob转换为file对象
const blobToFile = (blob, fileName) => {
return new File([blob], fileName, { type: blob.type })
}
defineExpose({
open
})
</script>
<style scoped lang="less">
.image-clip-dialog-box {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
.submit {
width: 4rem;
height: 4rem;
border-radius: 50%;
background: #262626;
color: #fff;
cursor: pointer;
}
> .header {
display: flex;
justify-content: space-between;
margin-bottom: 5rem;
> .title {
font-family: pingfang_heavy;
font-size: 2.4rem;
color: #595959;
}
> .right {
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
> button {
width: 10rem;
height: 4.8rem;
border-radius: 4rem;
border: none;
background: #e4e5eb;
font-family: pingfang_heavy;
font-size: 1.6rem;
color: #000;
}
}
}
> .content {
flex: 1;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
> .image-clip {
flex: 1;
}
> .preview {
margin-left: 6rem;
width: 28rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 2.4rem;
> .title {
display: flex;
align-items: center;
justify-content: center;
gap: 1.2rem;
> .label {
font-family: pingfang_heavy;
font-size: 1.6rem;
}
}
> img {
width: 100%;
height: auto;
margin-bottom: 3rem;
}
}
}
}
</style>

View File

@@ -0,0 +1,157 @@
<template>
<div class="image-clip">
<div class="image-clip-body" ref="imageClipBody">
<VueCropper
ref="cropper"
:img="url"
crossOrigin="Anonymous"
:autoCrop="true"
:fixedNumber="ratio"
fixed
movable
centerBox
@realTime="onChange"
></VueCropper>
</div>
<div class="clip_opterate">
<div class="item" @click="rotateLeft()">
<span class="icon iconfont icon-chexiao operate_icon"></span>
</div>
<div class="item" @click="rotateRight()">
<span class="icon iconfont icon-chexiao operate_icon icon_chexiao_sec"></span>
</div>
<div class="item" @click="changeScale(-0.1)">
<span class="operate_icon icon_font">-</span>
</div>
<div class="item" @click="changeScale(0.1)">
<span class="operate_icon icon_font">+</span>
</div>
<div class="item" @click="refreshCrop()">
<span class="icon iconfont icon-shuaxin operate_icon"></span>
</div>
</div>
</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()
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()
})
onMounted(() => {
observer.observe(imageClipBody.value)
})
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)
})
}
const getCropBlob = () => {
return new Promise((resolve, reject) => {
cropper.value.getCropBlob(resolve)
})
}
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(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: 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;
}
}
}
}
</style>

View File

@@ -7,7 +7,7 @@
<span class="icon"><svg-icon name="seller-picture" size="60" /></span>
<span class="tip">Your brand banner has not been set up yet.</span>
</div>
<button>Change Brand Banner</button>
<button @click="onChangeBanner">Change Brand Banner</button>
</div>
<!-- 头像 -->
<div class="avatar">
@@ -15,7 +15,7 @@
<div v-else class="null">
<svg-icon name="seller-user" size="48" />
</div>
<span class="icon">
<span class="icon" @click="onChangeAvatar">
<svg-icon name="seller-camera" size="24" />
</span>
</div>
@@ -37,15 +37,56 @@
<p class="tip">&nbsp;</p>
</template>
</div>
<image-clip-dialog ref="imageClipDialogRef" />
</template>
<script setup>
import { ref } from "vue"
import BrandInfo from "./brand-info.vue"
const banner = ref("http://118.31.39.42:3000/falls/5bd8065cbb396eb5a8ef0a142605139358734e57.png")
const avatar = ref("http://118.31.39.42:3000/falls/20251024140128_10355_1.jpg")
import ImageClipDialog from "./image-clip-dialog.vue"
const banner = ref("")
const avatar = ref("")
const isEdit = ref(false)
const brandInfoRef = ref(null)
const imageClipDialogRef = ref(null)
// 选择本机图片
const uploadImg = (onChange) => {
const input = document.createElement("input")
input.type = "file"
input.accept = "image/*"
// 监听文件输入框的变化事件
input.addEventListener("change", (event) => {
event.preventDefault()
const file = event.target.files[0]
const url = URL.createObjectURL(file)
onChange({ url, file })
})
input.click()
}
const onChangeBanner = () => {
uploadImg(({ url }) => {
imageClipDialogRef.value.open(
url,
(file) => {
banner.value = URL.createObjectURL(file)
},
{ ratio: [40, 7], isPreview: false, title: "Crop Brand Banner" }
)
})
}
const onChangeAvatar = () => {
uploadImg(({ url }) => {
imageClipDialogRef.value.open(
url,
(file) => {
avatar.value = URL.createObjectURL(file)
},
{ ratio: [1, 1], isPreview: true, title: "Crop Avatar" }
)
})
}
const onEdit = () => {
isEdit.value = true
}

View File

@@ -16,8 +16,15 @@
</div>
<div class="right">
<div class="input">
<span class="icon"><svg-icon name="seller-search" size="20" /></span>
<input type="text" placeholder="Search by item name or order ID" />
<span class="icon"
><svg-icon name="seller-search" size="20" @click="getList(true)"
/></span>
<input
type="text"
v-model="nameOrId"
placeholder="Search by item name or order ID"
@keydown.enter.prevent="getList(true)"
/>
</div>
</div>
</div>
@@ -30,34 +37,42 @@
<div class="date">Date</div>
</div>
<div class="body">
<div class="item" v-for="v in 10" :key="v">
<div class="order-id">SP897772698</div>
<div class="item" v-for="v in list" :key="v.orderId">
<div class="order-id">{{ v.orderId }}</div>
<div class="item">
<div class="images">
<img src="http://118.31.39.42:3000/falls/o-1.png" />
<img src="http://118.31.39.42:3000/falls/o-2.png" />
<span>+1 more</span>
<img
v-for="(v, i) in v.item.slice(0, maxItemNum)"
:key="i"
:src="v.url"
/>
<span v-if="v.item.length > maxItemNum"
>+{{ v.item.length - maxItemNum }} more</span
>
</div>
<div class="titles">
<div>North Outfit Set</div>
<div>Heritage Layered Set</div>
<span>...</span>
<div v-for="(v, i) in v.item.slice(0, maxItemNum)" :key="i">
{{ v.title }}
</div>
<span v-if="v.item.length > maxItemNum">...</span>
</div>
</div>
<div class="price">HK$ 100.00</div>
<div class="buyer-username">@liuyuchen</div>
<div class="price">{{ v.price }}</div>
<div class="buyer-username">{{ v.username }}</div>
<div class="date">
<div>Mar 17, 2026</div>
<div>10:15 AM</div>
<div>{{ v.date }}</div>
<div>{{ v.time }}</div>
</div>
</div>
</div>
<div class="placeholder" ref="placeholderRef" v-show="!loading"></div>
<div class="footer" v-if="!finish"><a-spin :delay="0.5" v-show="loading" /></div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue"
import { ref, onMounted, onBeforeUnmount } from "vue"
const totals = ref([
{
icon: "seller-qiandaizi",
@@ -75,6 +90,86 @@
value: "4,982"
}
])
const maxItemNum = ref(2)
const loading = ref(false)
const finish = ref(false)
const total = ref(30)
const page = ref(1)
const size = ref(10)
const nameOrId = ref("")
const list = ref([])
const getList = (isReload = false) => {
loading.value = true
if (isReload) {
list.value = []
page.value = 1
finish.value = false
}
const data = {
page: page.value,
size: size.value,
nameOrId: nameOrId.value
}
console.log(data)
setTimeout(() => {
for (let i = 0; i < size.value; i++) {
let { date, time, dateTime } = formatTimestamp(new Date(2026, 4, 20, 13, 14).getTime())
list.value.push({
orderId: "SP" + Math.random().toString().substring(2, 10),
price: "HK$ " + (Math.random() * 500).toFixed(2),
username: "@liuyuchen",
date: date,
time: time,
item: [
{
url: "http://118.31.39.42:3000/falls/o-1.png",
title: "North Outfit Set"
},
{
url: "http://118.31.39.42:3000/falls/o-2.png",
title: "Heritage Layered Set"
},
{},
{}
]
})
}
page.value++
finish.value = page.value > total.value / 10
loading.value = false
}, 1000)
}
getList(true)
const placeholderRef = ref(null)
const observer = new IntersectionObserver(
(entries) => {
if (!entries[0].intersectionRatio || loading.value || finish.value) return
getList()
},
{ root: document.body }
)
onMounted(() => {
observer.observe(placeholderRef.value)
})
onBeforeUnmount(() => {
observer.disconnect()
})
const formatTimestamp = (ts) => {
const d = new Date(ts)
const h = d.getHours()
const m = d.getMinutes()
const date = `${d.toLocaleString("en-US", {
month: "long"
})} ${d.getDate()}, ${d.getFullYear()}`
const time = `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")} ${
h >= 12 ? "PM" : "AM"
}`
return {
date,
time,
dateTime: `${date}\n${time}`
}
}
</script>
<style scoped lang="less">
.my-orders-index {
@@ -171,7 +266,7 @@
flex: 1;
}
> .order-id {
flex: 2;
flex: 1.5;
}
> .item {
flex: 3;
@@ -249,6 +344,16 @@
}
}
}
> .footer {
min-height: 10rem;
display: flex;
align-items: center;
justify-content: center;
}
> .placeholder {
width: 100%;
height: 1px;
}
}
}
</style>