Merge branch 'dev_vite' of ssh://18.167.251.121:10002/aidlab/aida_front into dev_vite

This commit is contained in:
X1627315083@163.com
2026-04-29 14:50:00 +08:00
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. - 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`. - 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 ## Image Category Mapping
Detail API images are mapped by `category`: 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> <template>
<div class="apparel-container"> <div class="apparel-container">
<div class="title"> <div class="title">
@@ -23,64 +11,78 @@ const emit = defineEmits<{
class="sketch-element flex flex-center" class="sketch-element flex flex-center"
> >
<img class="img-src" :src="item.url || ''" alt="" /> <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" /> <SvgIcon name="CCrop" color="#fff" size="12" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </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> <style lang="less" scoped>
.apparel-container { .apparel-container {
margin-top: 3rem; margin-top: 3rem;
.title { .title {
font-size: 1.4rem; font-size: 1.4rem;
margin-bottom: 0.8rem; margin-bottom: 0.8rem;
.main-title { .main-title {
font-weight: 400; font-weight: 400;
font-style: bold; font-style: bold;
&::after { &::after {
content: "*"; content: "*";
color: #df2b2c; color: #df2b2c;
}
}
.sub-title {
font-size: 1.2rem;
color: #999;
} }
} }
.sub-title { .sketch-list-container {
font-size: 1.2rem; column-gap: 1rem;
color: #999; flex-wrap: wrap;
}
}
.sketch-list-container { .sketch-element {
column-gap: 1rem; width: 10rem;
flex-wrap: wrap; height: 14.6rem;
border: 0.15rem solid #c7c7c7;
border-radius: 1.2rem;
position: relative;
overflow: hidden;
.sketch-element { .img-src {
width: 10rem; width: 100%;
height: 14.6rem; }
border: 0.15rem solid #c7c7c7;
border-radius: 1.2rem;
position: relative;
overflow: hidden;
.img-src { .crop-tool {
width: 100%; position: absolute;
} top: 0.4rem;
right: 0.4rem;
.crop-tool { width: 2rem;
position: absolute; height: 2rem;
top: 0.4rem; border-radius: 50%;
right: 0.4rem; background-color: #000000;
width: 2rem; }
height: 2rem;
border-radius: 50%;
background-color: #000000;
} }
} }
} }
}
</style> </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> <template>
<div class="form-container flex flex-col"> <div class="form-container flex flex-col">
<div class="form-item"> <div class="form-item">
@@ -102,121 +79,144 @@ const emit = defineEmits<{
</div> </div>
</template> </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> <style lang="less" scoped>
.required { .required {
&::after { &::after {
content: "*"; content: "*";
color: #df2b2c; color: #df2b2c;
margin-left: 0.4rem; 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;
}
} }
}
.form-item-value { .form-container {
border: 0.16rem solid #d1d1d1; row-gap: 3rem;
border-radius: 1.2rem;
position: relative;
padding: 1.6rem;
box-sizing: border-box;
font-size: 1.2rem;
color: #000;
&.no-border { .form-item {
border: none; .form-item-label {
padding: 0; 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 { .form-item-value {
column-gap: 0.6rem; border: 0.16rem solid #d1d1d1;
} border-radius: 1.2rem;
position: relative;
:deep(.ant-input), padding: 1.6rem;
:deep(.ant-input-affix-wrapper), box-sizing: border-box;
:deep(.ant-input-textarea textarea) {
border: none;
padding: 0;
font-size: 1.2rem; font-size: 1.2rem;
color: #000; color: #000;
box-shadow: none;
}
:deep(.ant-input) { &.no-border {
line-height: 1.5; border: none;
} padding: 0;
}
:deep(.ant-input-show-count-suffix) { &.price {
color: #df2c2c; column-gap: 0.6rem;
font-size: 1rem; }
}
:deep(textarea.ant-input) { :deep(.ant-input),
resize: none; :deep(.ant-input-affix-wrapper),
min-height: 5.4rem; :deep(.ant-input-textarea textarea) {
padding-bottom: 1.8rem; border: none;
line-height: 1.5; padding: 0;
} font-size: 1.2rem;
color: #000;
box-shadow: none;
}
:deep(.ant-input-textarea-show-count) { :deep(.ant-input) {
position: relative; line-height: 1.5;
}
&::after { :deep(.ant-input-show-count-suffix) {
float: none;
position: absolute;
color: #df2c2c; color: #df2c2c;
bottom: 0; font-size: 1rem;
right: 1.6rem; }
: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 { .license-note {
padding: 1.6rem; padding: 1.6rem;
column-gap: 1.6rem; column-gap: 1.6rem;
background: #f7f7f7; background: #f7f7f7;
border-radius: 0.8rem; border-radius: 0.8rem;
.info-icon { .info-icon {
width: 2.4rem; width: 2.4rem;
height: 2.4rem; height: 2.4rem;
flex-shrink: 0; flex-shrink: 0;
} }
.note-copy { .note-copy {
font-size: 1.2rem; font-size: 1.2rem;
line-height: 1.5; line-height: 1.5;
color: #000; color: #000;
font-weight: 400; font-weight: 400;
font-style: medium; font-style: medium;
a { a {
color: #0080ed; color: #0080ed;
text-decoration: underline; text-decoration: underline;
margin-left: 0.4rem; margin-left: 0.4rem;
}
} }
} }
}
</style> </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> <template>
<div class="product-image-list-container"> <div class="product-image-list-container">
<div class="title flex align-center space-between"> <div class="title flex align-center space-between">
@@ -45,110 +29,125 @@ const selectedCount = computed(() => props.imageList.filter((item) => item.selec
</div> </div>
</div> </div>
</template> </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> <style lang="less" scoped>
.product-image-list-container { .product-image-list-container {
margin-top: 3rem; margin-top: 3rem;
.title { .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;
font-size: 1.4rem; font-size: 1.4rem;
} margin-bottom: 1.2rem;
}
.product-image-list { .main-title {
overflow-x: auto; font-weight: 400;
overflow-y: hidden; font-style: bold;
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;
} }
&.selected { .sub-title {
border-color: #000; 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 { &::after {
display: none; position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: "";
background-color: #fcfcfc;
opacity: 0.7;
border-radius: 1rem;
} }
}
.checked { &.selected {
width: 2rem; border-color: #000;
height: 2rem;
position: absolute;
top: 0.8rem;
right: 0.8rem;
z-index: 1;
}
.img-src { &::after {
height: 100%; display: none;
} }
}
.main-pic { .checked {
position: absolute; width: 2rem;
height: 2.4rem; height: 2rem;
line-height: 2.4rem; position: absolute;
left: 0.8rem; top: 0.8rem;
right: 0.8rem; right: 0.8rem;
bottom: 0.8rem; z-index: 1;
z-index: 1; }
background: rgba(0, 0, 0, 0.8);
color: #fff; .img-src {
font-size: 1.4rem; height: 100%;
border-radius: 1.2rem; }
text-align: center;
.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> </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> <template>
<div class="main-image-container flex"> <div class="main-image-container flex">
<div <div
@@ -64,94 +45,112 @@ const topImageTitleMap: Record<TopImageType, string> = {
</div> </div>
</div> </div>
</template> </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> <style lang="less" scoped>
.c-svg { .c-svg {
width: initial; width: initial;
height: initial; height: initial;
}
.required {
&::after {
content: "*";
color: #df2b2c;
margin-left: 0.4rem;
} }
}
.main-image-container { .required {
column-gap: 3.5rem; &::after {
content: "*";
.main-image-item { color: #df2b2c;
flex-shrink: 0; margin-left: 0.4rem;
.title {
font-size: 1.4rem;
margin-bottom: 0.8rem;
text-align: center;
} }
}
.sketch-item { .main-image-container {
width: 11.6rem; column-gap: 3.5rem;
height: 20.4rem;
border: 0.15rem solid #d1d1d1;
border-radius: 1rem;
position: relative;
background-color: #f6f6f6;
overflow: hidden;
&.cover { .main-image-item {
width: 16.2rem; flex-shrink: 0;
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; .title {
font-size: 1.4rem;
margin-bottom: 0.8rem;
text-align: center;
} }
.crop-tool { .sketch-item {
position: absolute; width: 11.6rem;
top: 0.8rem; height: 20.4rem;
right: 0.8rem; border: 0.15rem solid #d1d1d1;
width: 2rem; border-radius: 1rem;
height: 2rem; position: relative;
border-radius: 50%; background-color: #f6f6f6;
background-color: #000000; overflow: hidden;
z-index: 1;
cursor: pointer;
}
.sketch-img { &.cover {
height: 100%; 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");
&.sketch { border: none;
height: initial;
width: 100%;
}
}
.trigger {
cursor: pointer;
height: 100%;
padding: 6rem 2rem 0;
&,
.cover-trigger {
row-gap: 1.2rem;
} }
.placeholder { .crop-tool {
width: 2.4rem; position: absolute;
height: 2.4rem; top: 0.8rem;
border-radius: 0.6rem; right: 0.8rem;
background: linear-gradient(135deg, #efefef 0%, #cdcdcd 100%); width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: #000000;
z-index: 1;
cursor: pointer;
} }
.trigger-tips { .sketch-img {
font-size: 1.2rem; height: 100%;
text-align: center;
color: #585858; &.sketch {
line-height: 1.3; 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> </style>

View File

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