卖家ui
This commit is contained in:
166
src/views/SellerDashboard/BrandProfile/image-clip-dialog.vue
Normal file
166
src/views/SellerDashboard/BrandProfile/image-clip-dialog.vue
Normal 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>
|
||||
157
src/views/SellerDashboard/BrandProfile/image-clip.vue
Normal file
157
src/views/SellerDashboard/BrandProfile/image-clip.vue
Normal 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>
|
||||
@@ -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"> </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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user