bugfix: 保存草稿同样要填写必填项

This commit is contained in:
2026-04-29 14:47:23 +08:00
parent 56825fcbb1
commit 5d184bf1f9
6 changed files with 374 additions and 369 deletions

View File

@@ -20,6 +20,11 @@ This directory owns the seller listing edit/create detail page.
- Child components should receive props and emit events only. Do not import listing APIs or mutate parent state directly from children.
- If a new visual section is added to this page, prefer a new child component under `components/` plus shared types in `types.ts`.
## Vue SFC Order
- Vue single-file components must keep sections in this order: `<template>` first, then `<script setup lang="ts">`, then `<style>`.
- When creating or refactoring `.vue` files in this page, preserve that order even if external Vue guidance suggests another layout.
## Image Category Mapping
Detail API images are mapped by `category`:

View File

@@ -1,15 +1,3 @@
<script setup lang="ts">
import type { ListingItem } from "../types"
defineProps<{
sketchList: ListingItem["sketchList"]
}>()
const emit = defineEmits<{
(e: "crop", data: string | null, type: "apparel", index?: number): void
}>()
</script>
<template>
<div class="apparel-container">
<div class="title">
@@ -23,64 +11,78 @@ const emit = defineEmits<{
class="sketch-element flex flex-center"
>
<img class="img-src" :src="item.url || ''" alt="" />
<div class="crop-tool flex flex-center" @click="emit('crop', item.url, 'apparel', index)">
<div
class="crop-tool flex flex-center"
@click="emit('crop', item.url, 'apparel', index)"
>
<SvgIcon name="CCrop" color="#fff" size="12" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { ListingItem } from "../types"
defineProps<{
sketchList: ListingItem["sketchList"]
}>()
const emit = defineEmits<{
(e: "crop", data: string | null, type: "apparel", index?: number): void
}>()
</script>
<style lang="less" scoped>
.apparel-container {
margin-top: 3rem;
.apparel-container {
margin-top: 3rem;
.title {
font-size: 1.4rem;
margin-bottom: 0.8rem;
.title {
font-size: 1.4rem;
margin-bottom: 0.8rem;
.main-title {
font-weight: 400;
font-style: bold;
.main-title {
font-weight: 400;
font-style: bold;
&::after {
content: "*";
color: #df2b2c;
&::after {
content: "*";
color: #df2b2c;
}
}
.sub-title {
font-size: 1.2rem;
color: #999;
}
}
.sub-title {
font-size: 1.2rem;
color: #999;
}
}
.sketch-list-container {
column-gap: 1rem;
flex-wrap: wrap;
.sketch-list-container {
column-gap: 1rem;
flex-wrap: wrap;
.sketch-element {
width: 10rem;
height: 14.6rem;
border: 0.15rem solid #c7c7c7;
border-radius: 1.2rem;
position: relative;
overflow: hidden;
.sketch-element {
width: 10rem;
height: 14.6rem;
border: 0.15rem solid #c7c7c7;
border-radius: 1.2rem;
position: relative;
overflow: hidden;
.img-src {
width: 100%;
}
.img-src {
width: 100%;
}
.crop-tool {
position: absolute;
top: 0.4rem;
right: 0.4rem;
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: #000000;
.crop-tool {
position: absolute;
top: 0.4rem;
right: 0.4rem;
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: #000000;
}
}
}
}
}
</style>

View File

@@ -1,26 +1,3 @@
<script setup lang="ts">
import Radio from "./Radio.vue"
import type { RadioOption } from "../types"
defineProps<{
productName: string
price: string
desc: string
gender: string
category: string[] | null
genderOptions: RadioOption[]
categoryOptions: RadioOption[]
}>()
const emit = defineEmits<{
(e: "update:productName", value: string): void
(e: "update:price", value: string): void
(e: "update:desc", value: string): void
(e: "update:gender", value: string): void
(e: "update:category", value: string[] | null): void
}>()
</script>
<template>
<div class="form-container flex flex-col">
<div class="form-item">
@@ -102,121 +79,144 @@ const emit = defineEmits<{
</div>
</template>
<script setup lang="ts">
import Radio from "./Radio.vue"
import type { RadioOption } from "../types"
defineProps<{
productName: string
price: string
desc: string
gender: string
category: string[] | null
genderOptions: RadioOption[]
categoryOptions: RadioOption[]
}>()
const emit = defineEmits<{
(e: "update:productName", value: string): void
(e: "update:price", value: string): void
(e: "update:desc", value: string): void
(e: "update:gender", value: string): void
(e: "update:category", value: string[] | null): void
}>()
</script>
<style lang="less" scoped>
.required {
&::after {
content: "*";
color: #df2b2c;
margin-left: 0.4rem;
}
}
.form-container {
row-gap: 3rem;
.form-item {
.form-item-label {
font-size: 1.4rem;
font-weight: 400;
font-style: bold;
margin-bottom: 0.6rem;
line-height: 1.5;
&.with-tip {
display: flex;
align-items: center;
column-gap: 0.8rem;
}
.help-text {
font-size: 1rem;
color: #999;
}
.required {
&::after {
content: "*";
color: #df2b2c;
margin-left: 0.4rem;
}
}
.form-item-value {
border: 0.16rem solid #d1d1d1;
border-radius: 1.2rem;
position: relative;
padding: 1.6rem;
box-sizing: border-box;
font-size: 1.2rem;
color: #000;
.form-container {
row-gap: 3rem;
&.no-border {
border: none;
padding: 0;
.form-item {
.form-item-label {
font-size: 1.4rem;
font-weight: 400;
font-style: bold;
margin-bottom: 0.6rem;
line-height: 1.5;
&.with-tip {
display: flex;
align-items: center;
column-gap: 0.8rem;
}
.help-text {
font-size: 1rem;
color: #999;
}
}
&.price {
column-gap: 0.6rem;
}
:deep(.ant-input),
:deep(.ant-input-affix-wrapper),
:deep(.ant-input-textarea textarea) {
border: none;
padding: 0;
.form-item-value {
border: 0.16rem solid #d1d1d1;
border-radius: 1.2rem;
position: relative;
padding: 1.6rem;
box-sizing: border-box;
font-size: 1.2rem;
color: #000;
box-shadow: none;
}
:deep(.ant-input) {
line-height: 1.5;
}
&.no-border {
border: none;
padding: 0;
}
:deep(.ant-input-show-count-suffix) {
color: #df2c2c;
font-size: 1rem;
}
&.price {
column-gap: 0.6rem;
}
:deep(textarea.ant-input) {
resize: none;
min-height: 5.4rem;
padding-bottom: 1.8rem;
line-height: 1.5;
}
:deep(.ant-input),
:deep(.ant-input-affix-wrapper),
:deep(.ant-input-textarea textarea) {
border: none;
padding: 0;
font-size: 1.2rem;
color: #000;
box-shadow: none;
}
:deep(.ant-input-textarea-show-count) {
position: relative;
:deep(.ant-input) {
line-height: 1.5;
}
&::after {
float: none;
position: absolute;
:deep(.ant-input-show-count-suffix) {
color: #df2c2c;
bottom: 0;
right: 1.6rem;
font-size: 1rem;
}
:deep(textarea.ant-input) {
resize: none;
min-height: 5.4rem;
padding-bottom: 1.8rem;
line-height: 1.5;
}
:deep(.ant-input-textarea-show-count) {
position: relative;
&::after {
float: none;
position: absolute;
color: #df2c2c;
bottom: 0;
right: 1.6rem;
}
}
}
}
}
}
.license-note {
padding: 1.6rem;
column-gap: 1.6rem;
background: #f7f7f7;
border-radius: 0.8rem;
.license-note {
padding: 1.6rem;
column-gap: 1.6rem;
background: #f7f7f7;
border-radius: 0.8rem;
.info-icon {
width: 2.4rem;
height: 2.4rem;
flex-shrink: 0;
}
.info-icon {
width: 2.4rem;
height: 2.4rem;
flex-shrink: 0;
}
.note-copy {
font-size: 1.2rem;
line-height: 1.5;
color: #000;
font-weight: 400;
font-style: medium;
.note-copy {
font-size: 1.2rem;
line-height: 1.5;
color: #000;
font-weight: 400;
font-style: medium;
a {
color: #0080ed;
text-decoration: underline;
margin-left: 0.4rem;
a {
color: #0080ed;
text-decoration: underline;
margin-left: 0.4rem;
}
}
}
}
</style>

View File

@@ -1,19 +1,3 @@
<script setup lang="ts">
import { computed } from "vue"
import type { ListingItem } from "../types"
const props = defineProps<{
imageList: ListingItem["prodImageList"]
firstSelectedIndex: number | null
}>()
const emit = defineEmits<{
(e: "select", index: number): void
}>()
const selectedCount = computed(() => props.imageList.filter((item) => item.selected).length)
</script>
<template>
<div class="product-image-list-container">
<div class="title flex align-center space-between">
@@ -45,110 +29,125 @@ const selectedCount = computed(() => props.imageList.filter((item) => item.selec
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue"
import type { ListingItem } from "../types"
const props = defineProps<{
imageList: ListingItem["prodImageList"]
firstSelectedIndex: number | null
}>()
const emit = defineEmits<{
(e: "select", index: number): void
}>()
const selectedCount = computed(() => props.imageList.filter((item) => item.selected).length)
</script>
<style lang="less" scoped>
.product-image-list-container {
margin-top: 3rem;
.product-image-list-container {
margin-top: 3rem;
.title {
font-size: 1.4rem;
margin-bottom: 1.2rem;
.main-title {
font-weight: 400;
font-style: bold;
}
.sub-title {
font-size: 1.2rem;
color: #999;
}
.title-right {
color: #585858;
.title {
font-size: 1.4rem;
}
}
margin-bottom: 1.2rem;
.product-image-list {
overflow-x: auto;
overflow-y: hidden;
column-gap: 0.8rem;
max-width: 80.2rem;
padding-bottom: 1.2rem;
&::-webkit-scrollbar {
height: 0.8rem;
}
&::-webkit-scrollbar-track {
background: #d9d9d9;
border-radius: 0.8rem;
}
&::-webkit-scrollbar-thumb {
background: #000000;
border-radius: 0.8rem;
}
.product-image-item {
width: 11.6rem;
height: 20.6rem;
border-radius: 1rem;
border: 0.15rem solid #c7c7c7;
position: relative;
cursor: pointer;
overflow: hidden;
flex-shrink: 0;
&::after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: "";
background-color: #fcfcfc;
opacity: 0.7;
border-radius: 1rem;
.main-title {
font-weight: 400;
font-style: bold;
}
&.selected {
border-color: #000;
.sub-title {
font-size: 1.2rem;
color: #999;
}
.title-right {
color: #585858;
font-size: 1.4rem;
}
}
.product-image-list {
overflow-x: auto;
overflow-y: hidden;
column-gap: 0.8rem;
max-width: 80.2rem;
padding-bottom: 1.2rem;
&::-webkit-scrollbar {
height: 0.8rem;
}
&::-webkit-scrollbar-track {
background: #d9d9d9;
border-radius: 0.8rem;
}
&::-webkit-scrollbar-thumb {
background: #000000;
border-radius: 0.8rem;
}
.product-image-item {
width: 11.6rem;
height: 20.6rem;
border-radius: 1rem;
border: 0.15rem solid #c7c7c7;
position: relative;
cursor: pointer;
overflow: hidden;
flex-shrink: 0;
&::after {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: "";
background-color: #fcfcfc;
opacity: 0.7;
border-radius: 1rem;
}
}
.checked {
width: 2rem;
height: 2rem;
position: absolute;
top: 0.8rem;
right: 0.8rem;
z-index: 1;
}
&.selected {
border-color: #000;
.img-src {
height: 100%;
}
&::after {
display: none;
}
}
.main-pic {
position: absolute;
height: 2.4rem;
line-height: 2.4rem;
left: 0.8rem;
right: 0.8rem;
bottom: 0.8rem;
z-index: 1;
background: rgba(0, 0, 0, 0.8);
color: #fff;
font-size: 1.4rem;
border-radius: 1.2rem;
text-align: center;
.checked {
width: 2rem;
height: 2rem;
position: absolute;
top: 0.8rem;
right: 0.8rem;
z-index: 1;
}
.img-src {
height: 100%;
}
.main-pic {
position: absolute;
height: 2.4rem;
line-height: 2.4rem;
left: 0.8rem;
right: 0.8rem;
bottom: 0.8rem;
z-index: 1;
background: rgba(0, 0, 0, 0.8);
color: #fff;
font-size: 1.4rem;
border-radius: 1.2rem;
text-align: center;
}
}
}
}
}
</style>

View File

@@ -1,22 +1,3 @@
<script setup lang="ts">
import type { TopImageType } from "../types"
defineProps<{
images: Record<TopImageType, string | null>
}>()
const emit = defineEmits<{
(e: "crop", data: string | null, type: TopImageType): void
}>()
const topImageList: TopImageType[] = ["sketch", "mainProductImage", "cover"]
const topImageTitleMap: Record<TopImageType, string> = {
sketch: "SellerListEdit.sketch",
mainProductImage: "SellerListEdit.mainProductImage",
cover: "SellerListEdit.cover"
}
</script>
<template>
<div class="main-image-container flex">
<div
@@ -64,94 +45,112 @@ const topImageTitleMap: Record<TopImageType, string> = {
</div>
</div>
</template>
<script setup lang="ts">
import type { TopImageType } from "../types"
defineProps<{
images: Record<TopImageType, string | null>
}>()
const emit = defineEmits<{
(e: "crop", data: string | null, type: TopImageType): void
}>()
const topImageList: TopImageType[] = ["sketch", "mainProductImage", "cover"]
const topImageTitleMap: Record<TopImageType, string> = {
sketch: "SellerListEdit.sketch",
mainProductImage: "SellerListEdit.mainProductImage",
cover: "SellerListEdit.cover"
}
</script>
<style lang="less" scoped>
.c-svg {
width: initial;
height: initial;
}
.required {
&::after {
content: "*";
color: #df2b2c;
margin-left: 0.4rem;
.c-svg {
width: initial;
height: initial;
}
}
.main-image-container {
column-gap: 3.5rem;
.main-image-item {
flex-shrink: 0;
.title {
font-size: 1.4rem;
margin-bottom: 0.8rem;
text-align: center;
.required {
&::after {
content: "*";
color: #df2b2c;
margin-left: 0.4rem;
}
}
.sketch-item {
width: 11.6rem;
height: 20.4rem;
border: 0.15rem solid #d1d1d1;
border-radius: 1rem;
position: relative;
background-color: #f6f6f6;
overflow: hidden;
.main-image-container {
column-gap: 3.5rem;
&.cover {
width: 16.2rem;
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' rx='11' ry='11' fill='none' stroke='%23D1D1D1' stroke-width='1.5' stroke-dasharray='8%2c 5' stroke-linecap='square'/%3e%3c/svg%3e");
border: none;
.main-image-item {
flex-shrink: 0;
.title {
font-size: 1.4rem;
margin-bottom: 0.8rem;
text-align: center;
}
.crop-tool {
position: absolute;
top: 0.8rem;
right: 0.8rem;
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: #000000;
z-index: 1;
cursor: pointer;
}
.sketch-item {
width: 11.6rem;
height: 20.4rem;
border: 0.15rem solid #d1d1d1;
border-radius: 1rem;
position: relative;
background-color: #f6f6f6;
overflow: hidden;
.sketch-img {
height: 100%;
&.sketch {
height: initial;
width: 100%;
}
}
.trigger {
cursor: pointer;
height: 100%;
padding: 6rem 2rem 0;
&,
.cover-trigger {
row-gap: 1.2rem;
&.cover {
width: 16.2rem;
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' rx='11' ry='11' fill='none' stroke='%23D1D1D1' stroke-width='1.5' stroke-dasharray='8%2c 5' stroke-linecap='square'/%3e%3c/svg%3e");
border: none;
}
.placeholder {
width: 2.4rem;
height: 2.4rem;
border-radius: 0.6rem;
background: linear-gradient(135deg, #efefef 0%, #cdcdcd 100%);
.crop-tool {
position: absolute;
top: 0.8rem;
right: 0.8rem;
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: #000000;
z-index: 1;
cursor: pointer;
}
.trigger-tips {
font-size: 1.2rem;
text-align: center;
color: #585858;
line-height: 1.3;
.sketch-img {
height: 100%;
&.sketch {
height: initial;
width: 100%;
}
}
.trigger {
cursor: pointer;
height: 100%;
padding: 6rem 2rem 0;
&,
.cover-trigger {
row-gap: 1.2rem;
}
.placeholder {
width: 2.4rem;
height: 2.4rem;
border-radius: 0.6rem;
background: linear-gradient(135deg, #efefef 0%, #cdcdcd 100%);
}
.trigger-tips {
font-size: 1.2rem;
text-align: center;
color: #585858;
line-height: 1.3;
}
}
}
}
}
}
</style>

View File

@@ -267,7 +267,7 @@
const handleClickCrop = (data: any, type: string, paramThree: any = []) => {
// 处理来自TopImageSection的调用: (data, type, list)
// 处理来自ApparelSketchList的调用: (data, type, index)
const index = typeof paramThree === 'number' ? paramThree : undefined
const index = typeof paramThree === "number" ? paramThree : undefined
const list = Array.isArray(paramThree) ? paramThree : []
// console.log(data, type)
@@ -376,9 +376,9 @@
})
if (item.mainProductImage) {
params.images.push({
category: 'main_product',
category: "main_product",
imageUrl: item.mainProductImage,
isSeleted:1
isSeleted: 1
})
}
item.prodImageList.forEach((item) => {
@@ -404,7 +404,7 @@
message.error("请先完成封面制作")
return
}
if (status === "publish" && !validatePublishRequired()) return
if (!validatePublishRequired()) return
await handleSaveForm(status)
if (status === "draft") {