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

@@ -36,7 +36,7 @@
line-height: 150%;
}
:deep(.ant-form) .ant-form-item-label > label.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
content: "";
display: none;
}
:deep(.ant-form) .ant-form-item-label > label.ant-form-item-required:not(.ant-form-item-required-mark-optional):after {
display: inline-block;

View File

@@ -2477,6 +2477,15 @@ textarea:focus {
border-radius: 0.4rem;
background: rgba(0, 0, 0, 0.2);
}
.mosaic-bg {
--mosaic-bg-size: 1rem;
--mosaic-bg-color1: #efefef;
--mosaic-bg-color2: #fff;
background-image: repeating-conic-gradient(var(--mosaic-bg-color1) 0% 25%, var(--mosaic-bg-color2) 0% 50%);
background-repeat: repeat;
background-position: 50% 50%;
background-size: var(--mosaic-bg-size) var(--mosaic-bg-size);
}
.mark_loading {
position: fixed;
width: 100%;

View File

@@ -0,0 +1,3 @@
<svg width="24" height="17" viewBox="0 0 24 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.77 16.402C7.37319 16.4028 6.98013 16.3253 6.61339 16.1737C6.24666 16.0222 5.91348 15.7997 5.633 15.519L0 9.886L1.424 8.461L7.057 14.094C7.24602 14.2829 7.50229 14.389 7.7695 14.389C8.03671 14.389 8.29298 14.2829 8.482 14.094L22.576 0L24 1.425L9.906 15.519C9.62571 15.7997 9.2927 16.0222 8.92613 16.1737C8.55956 16.3252 8.16666 16.4028 7.77 16.402Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 479 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 7V5C0 2.243 2.243 0 5 0H7C7.552 0 8 0.448 8 1C8 1.552 7.552 2 7 2H5C3.346 2 2 3.346 2 5V7C2 7.552 1.552 8 1 8C0.448 8 0 7.552 0 7ZM12 10C10.895 10 10 10.895 10 12C10 13.105 10.895 14 12 14C13.105 14 14 13.105 14 12C14 10.895 13.105 10 12 10ZM7 22H5C3.346 22 2 20.654 2 19V17C2 16.448 1.552 16 1 16C0.448 16 0 16.448 0 17V19C0 21.757 2.243 24 5 24H7C7.552 24 8 23.552 8 23C8 22.448 7.552 22 7 22ZM19 0H17C16.448 0 16 0.448 16 1C16 1.552 16.448 2 17 2H19C20.654 2 22 3.346 22 5V7C22 7.552 22.448 8 23 8C23.552 8 24 7.552 24 7V5C24 2.243 21.757 0 19 0ZM12 18C7.423 18 4.479 13.979 3.941 12.903C3.654 12.33 3.654 11.673 3.941 11.098C4.478 10.021 7.421 6 12 6C16.579 6 19.522 10.021 20.059 11.098C20.344 11.67 20.344 12.327 20.059 12.9C19.521 13.978 16.578 17.999 11.999 17.999L12 18ZM18.27 11.992C17.942 11.335 15.534 8 12 8C8.466 8 6.058 11.335 5.73 11.992C6.058 12.666 8.467 16 12 16C15.533 16 17.942 12.648 18.27 11.992ZM23 16C22.448 16 22 16.448 22 17V19C22 20.654 20.654 22 19 22H17C16.448 22 16 22.448 16 23C16 23.552 16.448 24 17 24H19C21.757 24 24 21.757 24 19V17C24 16.448 23.552 16 23 16Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -44,7 +44,7 @@
&.ant-form-item-required:not(.ant-form-item-required-mark-optional) {
&::before {
content: "";
display: none;
}
&:after {

View File

@@ -2397,6 +2397,16 @@ textarea:focus{
}
}
.mosaic-bg{
--mosaic-bg-size: 1rem;
--mosaic-bg-color1: #efefef;
--mosaic-bg-color2: #fff;
background-image: repeating-conic-gradient(var(--mosaic-bg-color1) 0% 25%, var(--mosaic-bg-color2) 0% 50%);
background-repeat: repeat;
background-position: 50% 50%;
background-size: var(--mosaic-bg-size) var(--mosaic-bg-size);
}
//蒙层样式
.mark_loading{
position: fixed;

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>