Files
aida_front/src/views/SellerDashboard/MyListings/EditDetail/index.vue
2026-04-29 15:07:43 +08:00

540 lines
14 KiB
Vue

<template>
<div class="edit-detail-wrapper flex-1">
<seller-header
class="edit-detail-header"
title="Edit Listing Details"
:breadcrumbs="[
{ title: 'My Listings', name: 'myListingsIndex' },
{ title: 'Select Collection', name: 'myListingsSelect' },
{ title: 'Select Sketch', name: 'myListingsSelectItem' },
{ title: 'Edit Listing Details', name: 'EditDetail' }
]"
>
<template #right>
<div class="operate-menu flex">
<div class="menu-btn flex align-center save" @click="handleClickMenu('draft')">
<span>{{ $t("SellerListEdit.saveDraft") }}</span>
<SvgIcon name="CSave" size="16" />
</div>
<div
class="menu-btn flex align-center publish"
@click="handleClickMenu('publish')"
>
<span>{{ $t("SellerListEdit.publish") }}</span>
<SvgIcon name="CPublish" size="16" />
</div>
</div>
</template>
</seller-header>
<div class="edit-detail-content flex">
<div class="left">
<TopImageSection :images="previewImageMap" @crop="handleClickCrop" />
<ProductImageList
:image-list="prodImgList"
:first-selected-index="currentListing.firstSelectedIndex"
@select="handleSelectProdImg"
/>
<ApparelSketchList
:sketch-list="currentListing.sketchList"
@crop="handleClickCrop"
/>
</div>
<div class="right">
<ListingForm
:product-name="currentListing.productName"
:price="currentListing.price"
:desc="currentListing.desc"
:gender="currentListing.gender"
:category="currentListing.category"
:gender-options="genderOptions"
:category-options="categoryOptions"
@update:product-name="currentListing.productName = $event"
@update:price="currentListing.price = $event"
@update:desc="currentListing.desc = $event"
@update:gender="currentListing.gender = $event"
@update:category="currentListing.category = $event"
/>
<div class="page-control flex align-center" v-if="selectList.length > 1">
<a-pagination
v-model:current="currentPage"
:total="selectList.length"
:page-size="1"
showQuickJumper
showLessItems
responsive
:showSizeChanger="false"
/>
</div>
</div>
</div>
</div>
<ImageClipDialog
ref="imageClipDialogRef"
fixedBox
isProduct
:info="false"
v-bind="$attrs"
:type="cropType"
/>
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from "vue"
import { useRouter } from "vue-router"
import { useI18n } from "vue-i18n"
import { message } from "ant-design-vue"
import SellerHeader from "../../seller-header.vue"
import ImageClipDialog from "../../BrandProfile/image-clip-dialog.vue"
import ApparelSketchList from "./components/ApparelSketchList.vue"
import ListingForm from "./components/ListingForm.vue"
import ProductImageList from "./components/ProductImageList.vue"
import TopImageSection from "./components/TopImageSection.vue"
import { useStore } from "vuex"
import {
fetchSketchDetail,
uploadFile,
fetchListingDetailById,
fetchUpdateListing
} from "./api"
import type {
ListingDetailImage,
ListingDetailResponse,
ListingItem,
RadioOption,
StatusType
} from "./types"
const ROUTER = useRouter()
const { t } = useI18n()
const imageClipDialogRef = ref<InstanceType<typeof ImageClipDialog> | null>(null)
defineOptions({
name: "EditDetail"
})
const STORE = useStore()
const createListingItem = (
sketch: string | null = null,
designItemId: number | string | null = null
): ListingItem => ({
designItemId,
sketch,
mainProductImage: "",
cover: "",
productImage: [],
apparelSketch: [],
productName: "",
price: "",
desc: "",
gender: "FEMALE",
category: null,
firstSelectedIndex: null,
prodImageList: [],
sketchList: []
})
const genderOptions = STORE.state.UserHabit?.sex.value || []
const fallbackCategoryOptions: Record<string, RadioOption[]> = {
MALE: STORE.state.UserHabit?.MalePosition || [],
FEMALE: STORE.state.UserHabit?.FemalePosition || []
}
const currentPage = ref(1)
const currentIndex = computed(() => currentPage.value - 1)
const itemId = ref("")
const selectList = ref<ListingItem[]>([createListingItem()])
const prodImgList = computed(() => currentListing.value.prodImageList || [])
const categoryOptions = computed(() => {
const gender = selectList.value[currentIndex.value].gender
return fallbackCategoryOptions[gender] || []
})
const currentListing = computed(() => selectList.value[currentIndex.value])
const previewImageMap = computed(() => ({
sketch: currentListing.value.sketch,
mainProductImage: currentListing.value.mainProductImage,
cover: currentListing.value.cover
}))
const getSortedDetailImages = (images: ListingDetailImage[] = []) => {
return [...images].sort((prev, next) => (prev.sortOrder ?? 0) - (next.sortOrder ?? 0))
}
const getImageSelected = (value: ListingDetailImage["isSelected"]) =>
value === true || value === 1 || value === "1"
const normalizeDetailGender = (value: ListingDetailResponse["designFor"]) => {
const gender = String(value || "").toUpperCase()
return gender === "MALE" || gender === "FEMALE" ? gender : "FEMALE"
}
const normalizeDetailCategory = (
value: ListingDetailResponse["productCategory"]
): ListingItem["category"] => {
const categories = Array.isArray(value) ? value : value ? [value] : []
const normalized = categories
.filter((category) => category !== null && typeof category !== "undefined")
.map((category) => String(category).toLowerCase())
return normalized.length ? normalized : null
}
const createListingItemFromDetail = (detail: ListingDetailResponse): ListingItem => {
const listing = createListingItem()
listing.productName = detail.title || ""
listing.price =
detail.price === null || typeof detail.price === "undefined" ? "" : String(detail.price)
listing.desc = detail.description || ""
listing.gender = normalizeDetailGender(detail.designFor)
listing.category = normalizeDetailCategory(detail.productCategory)
getSortedDetailImages(detail.images || []).forEach((image) => {
const imageUrl = image.imageUrl || ""
if (!imageUrl) return
if (image.category === "cover") {
listing.cover = imageUrl
return
}
if (image.category === "sketch") {
listing.sketch = imageUrl
return
}
if (image.category === "mainProductImage") {
listing.mainProductImage = imageUrl
return
}
if (image.category === "product") {
listing.prodImageList.push({
url: imageUrl,
selected: getImageSelected(image.isSelected)
})
return
}
if (image.category === "apparel") {
listing.sketchList.push({ url: imageUrl })
}
})
if (!listing.mainProductImage) {
listing.mainProductImage =
listing.prodImageList.find((item) => item.selected)?.url || ""
}
const selectedIndex = listing.prodImageList.findIndex((item) => item.selected)
listing.firstSelectedIndex = selectedIndex === -1 ? null : selectedIndex
listing.productImage = listing.prodImageList.map((item) => item.url)
listing.apparelSketch = listing.sketchList
.map((item) => item.url)
.filter((url): url is string => Boolean(url))
return listing
}
const handleSelectProdImg = (index: number) => {
const listing = currentListing.value
const target = prodImgList.value[index]
const willSelect = !target.selected
target.selected = willSelect
if (willSelect && listing.firstSelectedIndex === null) {
listing.mainProductImage = target.url
listing.firstSelectedIndex = index
return
}
if (!willSelect && listing.mainProductImage === target.url) {
listing.firstSelectedIndex = null
listing.mainProductImage = ""
}
}
const cropType = ref("")
const handleClickCrop = (data: any, type: string, paramThree: any = []) => {
// 处理来自TopImageSection的调用: (data, type, list)
// 处理来自ApparelSketchList的调用: (data, type, index)
const index = typeof paramThree === "number" ? paramThree : undefined
const list = Array.isArray(paramThree) ? paramThree : []
// console.log(data, type)
// console.log(selectList.value[currentIndex.value])
let origin = []
const currentItem = selectList.value[currentIndex.value]
if (currentItem.sketch) {
origin.push({ type: "sketch", url: currentItem.sketch })
}
if (currentItem.mainProductImage) {
origin.push({ type: "mainProductImage", url: currentItem.mainProductImage })
}
if (type !== "cover") origin = []
const titleList = {
sketch: "Crop Sketch",
mainProductImage: "Crop Main Product Image",
cover: "Crop Cover",
apparel: "Crop Apparel Sketch"
}
const ratio = type === "cover" ? [4, 5] : [9, 16]
cropType.value = type
imageClipDialogRef.value.open(
data,
(file) => {
// console.log(file)
uploadFile(file).then((res) => {
if (type === "apparel" && typeof index !== "undefined") {
selectList.value[currentIndex.value].sketchList[index].url = res
} else {
selectList.value[currentIndex.value][type] = res
}
})
},
{ ratio, isPreview: true, title: titleList[type], isProduct: true },
origin
)
}
const hasValue = (value: unknown) =>
value !== null && value !== undefined && String(value).trim() !== ""
const getMissingRequiredField = (item: ListingItem) => {
const cover = item.cover || item.mainProductImage || item.sketch
const requiredFields = [
{ value: item.sketch, label: t("SellerListEdit.sketch") },
{ value: cover, label: t("SellerListEdit.cover") },
{ value: item.productName, label: t("SellerListEdit.productName") },
{ value: item.price, label: t("SellerListEdit.price") },
{ value: item.desc, label: t("SellerListEdit.productDescription") },
{ value: item.gender, label: t("SellerListEdit.designFor") },
{ value: item.category, label: t("SellerListEdit.productCategory") }
]
const missingField = requiredFields.find((field) => !hasValue(field.value))
if (missingField) return missingField.label
const missingSketchIndex = item.sketchList.findIndex((sketch) => !hasValue(sketch.url))
if (item.sketchList.length === 0 || missingSketchIndex !== -1) {
return `${t("SellerListEdit.apparelSketchTitle").trim()} ${
missingSketchIndex === -1 ? 1 : missingSketchIndex + 1
}`
}
return ""
}
const validatePublishRequired = () => {
for (let index = 0; index < selectList.value.length; index += 1) {
const field = getMissingRequiredField(selectList.value[index])
if (!field) continue
currentPage.value = index + 1
message.warning(
t(
selectList.value.length > 1
? "SellerListEdit.requiredFieldTipsWithPage"
: "SellerListEdit.requiredFieldTips",
{ index: index + 1, field }
)
)
return false
}
return true
}
const handleSaveForm = async (type: StatusType) => {
const paramsList = []
selectList.value.forEach((item: ListingItem) => {
const params = {
id: itemId.value,
title: item.productName,
description: item.desc,
price: item.price,
status: type === "draft" ? 0 : 1,
images: [],
designFor: (item.gender || "FEMALE").toLowerCase(),
productCategory: item.category
}
;["sketch", "cover"].forEach((el) => {
params.images.push({
category: el,
imageUrl: item[el],
isSelected: 1
})
})
if (item.mainProductImage) {
params.images.push({
category: "main_product",
imageUrl: item.mainProductImage,
isSeleted: 1
})
}
item.prodImageList.forEach((item) => {
params.images.push({
category: "product",
imageUrl: item.url,
isSelected: Number(!!item.selected)
})
})
item.sketchList.forEach((item) => {
params.images.push({
category: "apparel",
imageUrl: item.url,
isSelected: 1
})
})
paramsList.push(params)
})
await fetchUpdateListing(paramsList)
}
const handleClickMenu = async (status: StatusType) => {
if (status === "draft" && !selectList.value[currentIndex.value].cover) {
message.error("请先完成封面制作")
return
}
if (!validatePublishRequired()) return
await handleSaveForm(status)
if (status === "draft") {
// save draft logic
// console.log("Saving draft...", currentListing.value)
ROUTER.push({ name: "Status", params: { status: "draft" } })
} else if (status === "publish") {
// publish logic
// console.log("Publishing...", currentListing.value)
ROUTER.push({ name: "Status", params: { status: "publish" } })
}
}
const handleFetchItemDetial = (list) => {
fetchSketchDetail(list).then((res) => {
res.forEach((item, index) => {
if (!selectList.value[index]) return
selectList.value[index].sketchList = item.clothes.map((el) => ({ url: el }))
selectList.value[index].prodImageList = item.toProductImageUrls.map((el) => ({
url: el
}))
})
})
}
const handleGetDetailById = () => {
fetchListingDetailById(itemId.value).then((res: ListingDetailResponse) => {
const listing = createListingItemFromDetail(res)
currentPage.value = 1
selectList.value = [listing]
})
}
onMounted(() => {
const data = history.state
if (data?.type === "edit") {
itemId.value = history.state?.id
handleGetDetailById()
} else {
const designItemIds = history.state?.designItemIds || []
if (!designItemIds.length) return
currentPage.value = 1
selectList.value = designItemIds.map((item) =>
createListingItem(item.designOutfitUrl, item.designItemId)
)
const list = designItemIds.map((el) => el.designItemId)
// console.log("list", list.length, list)
handleFetchItemDetial(list)
}
})
</script>
<style lang="less" scoped>
.c-svg {
width: initial;
height: initial;
}
.edit-detail-wrapper {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
margin-top: -1rem;
overflow: hidden;
&::-webkit-scrollbar {
display: none;
}
.menu-btn {
height: 6rem;
border: 0.15rem solid #000000;
border-radius: 4rem;
text-align: center;
line-height: 6rem;
padding: 0 2rem;
font-size: 1.6rem;
column-gap: 0.8rem;
cursor: pointer;
transition: all 0.25s ease;
&:hover {
background: #000;
color: #fff;
}
// &.publish:hover {
// background: #fff;
// color: #000;
// }
}
.edit-detail-header {
margin-bottom: 2rem;
}
.operate-menu {
column-gap: 2rem;
// .publish {
// background-color: #000000;
// color: #ffffff;
// }
}
}
.edit-detail-content {
flex: 1;
min-height: 0;
overflow-y: auto;
justify-content: space-between;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
.right {
width: 55.2rem;
flex-shrink: 0;
.page-control {
justify-content: flex-end;
margin-top: 4rem;
}
}
}
</style>