Compare commits
13 Commits
c8a65ee2cb
...
dev_vite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eaa77596e | ||
|
|
848e7b4692 | ||
|
|
fd140ebc56 | ||
|
|
34094c8c92 | ||
|
|
e4dc2bf729 | ||
|
|
7f226179d9 | ||
|
|
3edff6b05c | ||
|
|
6fd1212298 | ||
| e093cccb8d | |||
| b2c6c61515 | |||
| 0219b1a2f4 | |||
| 133433a260 | |||
| 90a59a3dc5 |
3
src/assets/icons/home.svg
Normal file
3
src/assets/icons/home.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.4872 5.28094L9.06266 0.853126C8.51506 0.306793 7.7733 0 7 0C6.2267 0 5.48494 0.306793 4.93734 0.853126L0.512757 5.28094C0.349671 5.44308 0.220373 5.636 0.132355 5.84851C0.0443375 6.06102 -0.000647733 6.2889 7.04636e-06 6.51894V12.249C7.04636e-06 12.7134 0.184381 13.1587 0.51257 13.4871C0.840758 13.8155 1.28588 14 1.75001 14H12.25C12.7141 14 13.1592 13.8155 13.4874 13.4871C13.8156 13.1587 14 12.7134 14 12.249V6.51894C14.0006 6.2889 13.9557 6.06102 13.8676 5.84851C13.7796 5.636 13.6503 5.44308 13.4872 5.28094ZM8.75 12.8326H5.25V10.5364C5.25 10.072 5.43438 9.62663 5.76256 9.29825C6.09075 8.96986 6.53587 8.78538 7 8.78538C7.46413 8.78538 7.90925 8.96986 8.23744 9.29825C8.56562 9.62663 8.75 10.072 8.75 10.5364V12.8326ZM12.8333 12.249C12.8333 12.4038 12.7719 12.5522 12.6625 12.6617C12.5531 12.7711 12.4047 12.8326 12.25 12.8326H9.91666V10.5364C9.91666 9.76241 9.60937 9.0201 9.06239 8.47279C8.51541 7.92549 7.77355 7.61801 7 7.61801C6.22645 7.61801 5.48459 7.92549 4.93761 8.47279C4.39063 9.0201 4.08334 9.76241 4.08334 10.5364V12.8326H1.75001C1.5953 12.8326 1.44692 12.7711 1.33753 12.6617C1.22813 12.5522 1.16667 12.4038 1.16667 12.249V6.51894C1.16721 6.36425 1.22862 6.216 1.33759 6.10627L5.76217 1.6802C6.09099 1.35272 6.53605 1.16887 7 1.16887C7.46394 1.16887 7.90901 1.35272 8.23783 1.6802L12.6624 6.10802C12.771 6.21732 12.8323 6.36486 12.8333 6.51894V12.249Z" fill="#585858"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -24,6 +24,7 @@ export default {
|
|||||||
SubscribeNow: '立即订阅',
|
SubscribeNow: '立即订阅',
|
||||||
TaskList: '任务列表',
|
TaskList: '任务列表',
|
||||||
ViewOrders: '查询订单',
|
ViewOrders: '查询订单',
|
||||||
|
PersonalCenter: '个人中心',
|
||||||
BecomeSeller: '成为卖家',
|
BecomeSeller: '成为卖家',
|
||||||
SellerDashboard: '卖家中心',
|
SellerDashboard: '卖家中心',
|
||||||
toolsToProduct: '转产品图',
|
toolsToProduct: '转产品图',
|
||||||
@@ -1744,7 +1745,7 @@ export default {
|
|||||||
cover: "封面",
|
cover: "封面",
|
||||||
productImageDesc: '从产品图中选取',
|
productImageDesc: '从产品图中选取',
|
||||||
cropDesc: '从主产品图或线稿图中裁剪',
|
cropDesc: '从主产品图或线稿图中裁剪',
|
||||||
productImageMainTitle: '产品图 ',
|
productImageMainTitle: '产品图/视频',
|
||||||
productImageSubTitle: ' (来自设计集)',
|
productImageSubTitle: ' (来自设计集)',
|
||||||
apparelSketchTitle: '服装线稿图 ',
|
apparelSketchTitle: '服装线稿图 ',
|
||||||
apparelSketchSubTitle: ' (来自设计集)',
|
apparelSketchSubTitle: ' (来自设计集)',
|
||||||
@@ -1855,6 +1856,8 @@ export default {
|
|||||||
SelectCollection: '选择商品',
|
SelectCollection: '选择商品',
|
||||||
SelectSketch: '选择线稿图',
|
SelectSketch: '选择线稿图',
|
||||||
EditListingDetails: '编辑商品详情',
|
EditListingDetails: '编辑商品详情',
|
||||||
|
VideoWarning: '首次选中的图片素材会作为产品主图,视频不可作为产品主图',
|
||||||
|
selectSketchMaxNum: '最多选择9个线稿图',
|
||||||
},
|
},
|
||||||
ApplySeller: {
|
ApplySeller: {
|
||||||
applySellerTitle: '申请成为卖家',
|
applySellerTitle: '申请成为卖家',
|
||||||
@@ -1876,7 +1879,7 @@ export default {
|
|||||||
agreementAgreement: '我已经阅读并同意了卖家协议,清楚地了解了自己在 AiDA 平台上作为卖家所应承担的责任和义务。',
|
agreementAgreement: '我已经阅读并同意了卖家协议,清楚地了解了自己在 AiDA 平台上作为卖家所应承担的责任和义务。',
|
||||||
submitApplication: '提交申请',
|
submitApplication: '提交申请',
|
||||||
applicationSubmitted: '申请已提交',
|
applicationSubmitted: '申请已提交',
|
||||||
applicationSubmittedTip: '我们的团队将审核您的申请,并在1-3个工作日回复您。您会在邮箱中收到相关通知。',
|
applicationSubmittedTip: '预计很快就会批准。收到批准后,请在发布任何商品前,确保在 <span>卖家控制面板 > 设置</span> 中关联您的收款账户。',
|
||||||
auditStatus1_title: "步骤 1:提交申请",
|
auditStatus1_title: "步骤 1:提交申请",
|
||||||
auditStatus1_tip: "请填写店铺信息并同意服务条款。",
|
auditStatus1_tip: "请填写店铺信息并同意服务条款。",
|
||||||
auditStatus2_title: "步骤 2:审核验证",
|
auditStatus2_title: "步骤 2:审核验证",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default {
|
|||||||
SubscribeNow: 'Subscribe now',
|
SubscribeNow: 'Subscribe now',
|
||||||
TaskList: 'Task List',
|
TaskList: 'Task List',
|
||||||
ViewOrders: 'View Orders',
|
ViewOrders: 'View Orders',
|
||||||
|
PersonalCenter: 'Personal Center',
|
||||||
BecomeSeller: 'Become a Seller',
|
BecomeSeller: 'Become a Seller',
|
||||||
SellerDashboard: 'Seller Dashboard',
|
SellerDashboard: 'Seller Dashboard',
|
||||||
toolsToProduct: 'To product image',
|
toolsToProduct: 'To product image',
|
||||||
@@ -1795,7 +1796,7 @@ export default {
|
|||||||
cover:'Cover',
|
cover:'Cover',
|
||||||
productImageDesc:'Choose from product image',
|
productImageDesc:'Choose from product image',
|
||||||
cropDesc:'Crop from main product image or sketch',
|
cropDesc:'Crop from main product image or sketch',
|
||||||
productImageMainTitle:'Product Image ',
|
productImageMainTitle:'Product Media ',
|
||||||
productImageSubTitle:'(from design collection)',
|
productImageSubTitle:'(from design collection)',
|
||||||
apparelSketchTitle:'Apparel Sketch ',
|
apparelSketchTitle:'Apparel Sketch ',
|
||||||
apparelSketchSubTitle:'(from design collection)',
|
apparelSketchSubTitle:'(from design collection)',
|
||||||
@@ -1909,6 +1910,8 @@ export default {
|
|||||||
SelectCollection: 'Select Collection',
|
SelectCollection: 'Select Collection',
|
||||||
SelectSketch: 'Select Sketch',
|
SelectSketch: 'Select Sketch',
|
||||||
EditListingDetails: 'Edit Listing Details',
|
EditListingDetails: 'Edit Listing Details',
|
||||||
|
VideoWarning:'The first selected item is the main product image. Videos cannot be used.',
|
||||||
|
selectSketchMaxNum: 'Select up to 9 sketches',
|
||||||
},
|
},
|
||||||
ApplySeller: {
|
ApplySeller: {
|
||||||
applySellerTitle: 'Apply to Become a Seller',
|
applySellerTitle: 'Apply to Become a Seller',
|
||||||
@@ -1930,7 +1933,7 @@ export default {
|
|||||||
agreementAgreement: "I have read and agree to the Seller Agreement, understanding my responsibilities and obligations as a seller on the AiDA platform.",
|
agreementAgreement: "I have read and agree to the Seller Agreement, understanding my responsibilities and obligations as a seller on the AiDA platform.",
|
||||||
submitApplication: "Submit Application",
|
submitApplication: "Submit Application",
|
||||||
applicationSubmitted: "Application Submitted",
|
applicationSubmitted: "Application Submitted",
|
||||||
applicationSubmittedTip: "Our team will review your application and get back to you within 1–3 business days. You'll receive a notification in your email once a decision has been made.",
|
applicationSubmittedTip: "Approval is expected shortly. Upon receipt, please ensure your payout account is linked under <span>Seller Dashboard > Settings</span> prior to listing any items.",
|
||||||
auditStatus1_title: "Step 1: Submit Application",
|
auditStatus1_title: "Step 1: Submit Application",
|
||||||
auditStatus1_tip: "Fill out the seller information form and agree to our terms",
|
auditStatus1_tip: "Fill out the seller information form and agree to our terms",
|
||||||
auditStatus2_title: "Step 2: Review & Verification",
|
auditStatus2_title: "Step 2: Review & Verification",
|
||||||
|
|||||||
@@ -235,6 +235,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
meta: { enter: "all" },
|
meta: { enter: "all" },
|
||||||
component: () => import("@/views/SellerDashboard/index.vue"),
|
component: () => import("@/views/SellerDashboard/index.vue"),
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
meta: { enter: "all" },
|
||||||
|
redirect: "/home/seller/brandProfile"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "brandProfile",
|
path: "brandProfile",
|
||||||
name: "brandProfile",
|
name: "brandProfile",
|
||||||
|
|||||||
@@ -318,7 +318,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="homeMain_user">
|
<div class="homeMain_user">
|
||||||
<div class="homeMain_user_icon" @click="openAccount">
|
<div class="homeMain_user_icon">
|
||||||
<img :src="userDetail.avatar" alt="" />
|
<img :src="userDetail.avatar" alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div class="homeMain_user_detail">
|
<div class="homeMain_user_detail">
|
||||||
@@ -373,6 +373,10 @@
|
|||||||
<i class="fi fi-rs-notebook"></i>
|
<i class="fi fi-rs-notebook"></i>
|
||||||
<span class="select_item_des">{{ $t('Header.ViewOrders') }}</span>
|
<span class="select_item_des">{{ $t('Header.ViewOrders') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="select_item" @click="openAccount">
|
||||||
|
<span class="icon"><svg-icon name="home" /></span>
|
||||||
|
<span class="select_item_des">{{ $t('Header.PersonalCenter') }}</span>
|
||||||
|
</div>
|
||||||
<div class="select_item" @click="onBecomeSeller" v-if="!isSeller">
|
<div class="select_item" @click="onBecomeSeller" v-if="!isSeller">
|
||||||
<span class="icon"><svg-icon name="seller-sellerIndex" /></span>
|
<span class="icon"><svg-icon name="seller-sellerIndex" /></span>
|
||||||
<span class="select_item_des">{{ $t('Header.BecomeSeller') }}</span>
|
<span class="select_item_des">{{ $t('Header.BecomeSeller') }}</span>
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
<div class="seller-review">
|
<div class="seller-review">
|
||||||
<img class="success" src="@/assets/images/seller/success-1.png" />
|
<img class="success" src="@/assets/images/seller/success-1.png" />
|
||||||
<div class="title">{{ $t("ApplySeller.applicationSubmitted") }}</div>
|
<div class="title">{{ $t("ApplySeller.applicationSubmitted") }}</div>
|
||||||
<div class="tip">{{ $t("ApplySeller.applicationSubmittedTip") }}</div>
|
<div
|
||||||
|
class="tip"
|
||||||
|
v-html="$t('ApplySeller.applicationSubmittedTip', { click: 'onPersonalCenter' })"
|
||||||
|
></div>
|
||||||
<div class="step-list">
|
<div class="step-list">
|
||||||
<div v-for="v in list" :key="v.title" class="step-item">
|
<div v-for="v in list" :key="v.title" class="step-item">
|
||||||
<img v-show="!v.active" src="@/assets/images/seller/success-0.png" />
|
<img v-show="!v.active" src="@/assets/images/seller/success-0.png" />
|
||||||
@@ -13,7 +16,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="home-btn" @click="onBackToHome">{{ $t("ApplySeller.backToHomepage") }}</button>
|
<button class="home-btn" @click="onBackToHome">
|
||||||
|
{{ $t("ApplySeller.backToHomepage") }}
|
||||||
|
</button>
|
||||||
<div class="tip">ID: {{ userId }}</div>
|
<div class="tip">ID: {{ userId }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -51,6 +56,9 @@
|
|||||||
const onBackToHome = () => {
|
const onBackToHome = () => {
|
||||||
router.push({ name: "home" })
|
router.push({ name: "home" })
|
||||||
}
|
}
|
||||||
|
window.onPersonalCenter = () => {
|
||||||
|
router.push("/home/account")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.seller-review {
|
.seller-review {
|
||||||
@@ -75,11 +83,15 @@
|
|||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
> .tip {
|
> .tip {
|
||||||
font-family: pingfang_medium;
|
font-family: pingfang_regular;
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
line-height: 170%;
|
line-height: 170%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #585858;
|
color: #585858;
|
||||||
|
&:deep(span) {
|
||||||
|
color: #585858;
|
||||||
|
font-family: pingfang_heavy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
> .step-list {
|
> .step-list {
|
||||||
margin: 2.6rem 0;
|
margin: 2.6rem 0;
|
||||||
|
|||||||
@@ -124,8 +124,11 @@
|
|||||||
const currentOrigin = ref("sketch")
|
const currentOrigin = ref("sketch")
|
||||||
const coverOrigin = ref([])
|
const coverOrigin = ref([])
|
||||||
const handleChangeOrigin = (type) => {
|
const handleChangeOrigin = (type) => {
|
||||||
|
const targetOrigin = coverOrigin.value.find((el) => el.type === type)
|
||||||
|
if (!targetOrigin) return
|
||||||
|
|
||||||
currentOrigin.value = type
|
currentOrigin.value = type
|
||||||
data.url = coverOrigin.value.filter((el) => el.type === type)[0].url
|
data.url = targetOrigin.url
|
||||||
}
|
}
|
||||||
|
|
||||||
const show = ref(false)
|
const show = ref(false)
|
||||||
@@ -136,7 +139,7 @@
|
|||||||
|
|
||||||
coverOrigin.value = []
|
coverOrigin.value = []
|
||||||
data.url = null
|
data.url = null
|
||||||
currentOrigin.value = 'sketch'
|
currentOrigin.value = "sketch"
|
||||||
|
|
||||||
data.url = url
|
data.url = url
|
||||||
data.callback = callback
|
data.callback = callback
|
||||||
@@ -148,9 +151,11 @@
|
|||||||
if (options.hasOwnProperty("isPreview")) data.isPreview = options.isPreview
|
if (options.hasOwnProperty("isPreview")) data.isPreview = options.isPreview
|
||||||
data.isProduct = options.isProduct
|
data.isProduct = options.isProduct
|
||||||
}
|
}
|
||||||
if (origin?.length && !data.url) {
|
if (origin?.length) {
|
||||||
coverOrigin.value = origin
|
coverOrigin.value = origin
|
||||||
data.url = origin[0].url
|
const defaultOrigin = origin.find((el) => el.type === options?.coverFrom) || origin[0]
|
||||||
|
currentOrigin.value = defaultOrigin.type
|
||||||
|
data.url = defaultOrigin.url
|
||||||
}
|
}
|
||||||
show.value = true
|
show.value = true
|
||||||
}
|
}
|
||||||
@@ -160,7 +165,7 @@
|
|||||||
const imageClipRef = ref(null)
|
const imageClipRef = ref(null)
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
imageClipRef.value.getCropBlob().then((blob) => {
|
imageClipRef.value.getCropBlob().then((blob) => {
|
||||||
if (data.callback) data.callback(blobToFile(blob, "image.png"))
|
if (data.callback) data.callback(blobToFile(blob, "image.png"), currentOrigin.value)
|
||||||
onCancel()
|
onCancel()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,25 +30,67 @@ This directory owns the seller listing edit/create detail page.
|
|||||||
Detail API images are mapped by `category`:
|
Detail API images are mapped by `category`:
|
||||||
|
|
||||||
- `cover` -> `currentListing.cover`
|
- `cover` -> `currentListing.cover`
|
||||||
|
- `cover_from` -> `currentListing.coverFrom`; `imageUrl` stores the source image URL, not the literal source key. Resolve it against `sketch` and `mainProductImage` when hydrating. Keep backward compatibility for old rows whose `imageUrl` is `sketch` or `mainProductImage`.
|
||||||
- `sketch` -> `currentListing.sketch`
|
- `sketch` -> `currentListing.sketch`
|
||||||
- `mainProductImage` -> `currentListing.mainProductImage`
|
- `mainProductImage` or `main_product` -> `currentListing.mainProductImage`
|
||||||
- `main_product` or `product` -> `currentListing.prodImageList`
|
- `product` -> non-video entries in `currentListing.prodImageList`
|
||||||
|
- `firstFrame`, `gif`, and `video` -> one video entry in `currentListing.prodImageList` when the three rows share the same `sortOrder`
|
||||||
- `apparel` -> `currentListing.sketchList`
|
- `apparel` -> `currentListing.sketchList`
|
||||||
|
|
||||||
When saving, preserve the backend's expected image categories. Confirm backend naming before changing `main_product`, `product`, or `mainProductImage`.
|
When saving, preserve the backend's expected image categories. Confirm backend naming before changing `main_product`, `product`, `firstFrame`, `gif`, `video`, or `mainProductImage`.
|
||||||
|
|
||||||
|
中文补充:
|
||||||
|
|
||||||
|
- 详情接口返回的 `images` 要按 `category` 回填到页面状态,不要只按数组顺序猜类型。
|
||||||
|
- `cover_from` 不是封面图本身,而是记录封面裁剪来源。它的 `imageUrl` 传来源图片链接,回显时用这个 URL 和 `sketch`、`mainProductImage` 比较,恢复裁剪弹窗里的来源选择。
|
||||||
|
- `main_product` 表示页面右上方的主产品图 URL;普通 `product` 只表示产品图列表里的非视频图片。
|
||||||
|
- 视频不要保存成 `product`。视频必须拆成 `firstFrame`、`gif`、`video` 三类,并在回显时按相同 `sortOrder` 合并成一个视频项。
|
||||||
|
|
||||||
## Product Image Rules
|
## Product Image Rules
|
||||||
|
|
||||||
- The `main` badge represents the first selected product image, not the most recently selected one.
|
- The `main` badge represents the first selected product image, not the most recently selected one.
|
||||||
- `firstSelectedIndex` is stored on each `ListingItem` and passed to `ProductImageList.vue`.
|
- `firstSelectedIndex` is stored on each `ListingItem` and passed to `ProductImageList.vue`.
|
||||||
|
- Hydrating detail data must only set `mainProductImage` from explicit `main_product` or `mainProductImage` rows. Never infer it from selected `product` rows.
|
||||||
- Selecting a product image should only set `mainProductImage` when no main image is currently tracked by that listing's `firstSelectedIndex`.
|
- Selecting a product image should only set `mainProductImage` when no main image is currently tracked by that listing's `firstSelectedIndex`.
|
||||||
- Unselecting the current main product image clears `mainProductImage` and resets `firstSelectedIndex`.
|
- Unselecting the current main product image clears `mainProductImage` and resets `firstSelectedIndex`.
|
||||||
|
- Videos can be selected and saved, but they cannot become `mainProductImage`, must not set `firstSelectedIndex`, and must not display the `main` badge.
|
||||||
|
|
||||||
|
中文补充:
|
||||||
|
|
||||||
|
- `main` 标识只给图片,不给视频。
|
||||||
|
- 数据回显时,只有接口明确返回 `main_product` / `mainProductImage` 才能设置主图。不要因为某个 `product` 是已选中状态,就自动把它当成 `mainProductImage` 或显示 `main`。
|
||||||
|
- 第一次选择视频时可以弹 warning,但视频本身仍然要保持选中;只是不要把它写入 `mainProductImage`,也不要更新 `firstSelectedIndex`。
|
||||||
|
- 如果先选中视频,再选中图片,图片仍然可以成为第一个主图。
|
||||||
|
- 如果取消的是当前主图图片,需要清空 `mainProductImage` 和 `firstSelectedIndex`;取消普通图片或视频不应影响主图。
|
||||||
|
|
||||||
|
## Save Image Ordering
|
||||||
|
|
||||||
|
- Every saved image row must include `sortOrder`.
|
||||||
|
- `sortOrder` is scoped per category; each category starts its own sequence.
|
||||||
|
- For `product` rows, save the image currently used as `mainProductImage` first, selected non-main images next, and unselected images last.
|
||||||
|
- Save video media as three rows with categories `firstFrame`, `gif`, and `video`. The three rows from the same video item must share the same `sortOrder`.
|
||||||
|
- When hydrating detail data, group `firstFrame`, `gif`, and `video` rows by matching `sortOrder` and restore the combined video item, including its selected state. Accept `isSelected`, old typo `isSeleted`, and `selected` from the API.
|
||||||
|
|
||||||
|
中文补充:
|
||||||
|
|
||||||
|
- `sortOrder` 是按 category 分开排的,不同 category 之间不要共用一个全局序号。
|
||||||
|
- `product` 的排序规则是:当前作为 `mainProductImage` 的图片第一,其他已选图片其次,未选图片最后。
|
||||||
|
- 同一个视频拆出的 `firstFrame`、`gif`、`video` 三条数据必须使用同一个 `sortOrder`。例如第一个视频三条都是 `sortOrder: 1`,第二个视频三条都是 `sortOrder: 2`。
|
||||||
|
- 回显视频时,用相同 `sortOrder` 找回一组 `firstFrame/gif/video`。三条里任意一条带选中标记,都应恢复为这个视频已选中。
|
||||||
|
- 选中字段要兼容 `isSelected`、历史拼写 `isSeleted`、以及 `selected`。
|
||||||
|
|
||||||
## Crop Flow
|
## Crop Flow
|
||||||
|
|
||||||
- `TopImageSection.vue` and `ApparelSketchList.vue` emit `crop`.
|
- `TopImageSection.vue` and `ApparelSketchList.vue` emit `crop`.
|
||||||
- `index.vue` handles `handleClickCrop`, opens `ImageClipDialog`, uploads with `uploadFile`, then writes the returned URL into the correct field/list item.
|
- `index.vue` handles `handleClickCrop`, opens `ImageClipDialog`, uploads with `uploadFile`, then writes the returned URL into the correct field/list item.
|
||||||
- Keep cover crop ratio at `[4, 5]`; other crop types use `[9, 16]`.
|
- Keep cover crop ratio at `[4, 5]`; other crop types use `[9, 16]`.
|
||||||
|
- Cover crop can be based on `sketch` or `mainProductImage`. Store the chosen source in `coverFrom`, save it via `cover_from.imageUrl` as the source image URL, and pass it back into `ImageClipDialog` so reopening cover crop restores the selected source.
|
||||||
|
|
||||||
|
中文补充:
|
||||||
|
|
||||||
|
- cover 裁剪弹窗会在 `sketch` 和 `mainProductImage` 之间切来源。用户保存 cover 时,父组件需要同时保存裁剪后的 cover URL 和本次使用的来源。
|
||||||
|
- 下次重新打开 cover 裁剪时,应该按 `coverFrom` 恢复来源选择,并用对应的原图作为裁剪图,不要直接拿已裁好的 cover 图再次裁。
|
||||||
|
- 如果只有一个来源图可用,就按可用来源打开;如果两个来源都可用,要显示来源切换。
|
||||||
|
|
||||||
## Form Flow
|
## Form Flow
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
import { Https } from "@/tool/https"
|
import { Https } from "@/tool/https"
|
||||||
|
import type { ListingImageCategory, SketchDetailResponse } from "./types"
|
||||||
|
|
||||||
// 编辑时根据ID获取信息
|
// 编辑时根据ID获取信息
|
||||||
export const fetchListingDetailById = (id) => {
|
export const fetchListingDetailById = (id) => {
|
||||||
return Https.axiosGet("/seller/listing/detail", { params: { id } })
|
return Https.axiosGet("/seller/listing/detail", { params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SketchIDs {
|
type SketchIDs = Array<number | string>
|
||||||
designItemIds: Array
|
|
||||||
}
|
|
||||||
interface DetailReturns {
|
|
||||||
clothes: string[]
|
|
||||||
designItemId: number
|
|
||||||
toProductImageUrls: string[]
|
|
||||||
}
|
|
||||||
// 获取designItemId对应的产品图
|
// 获取designItemId对应的产品图
|
||||||
export const fetchSketchDetail = (data: SketchIDs): Array<DetailReturns> => {
|
export const fetchSketchDetail = (data: SketchIDs): Promise<SketchDetailResponse[]> => {
|
||||||
let params = "?"
|
let params = "?"
|
||||||
data.forEach((id, index) => {
|
data.forEach((id, index) => {
|
||||||
if (index === data.length - 1) {
|
if (index === data.length - 1) {
|
||||||
@@ -27,23 +21,26 @@ export const fetchSketchDetail = (data: SketchIDs): Array<DetailReturns> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ImageObj {
|
interface ImageObj {
|
||||||
id: number // 图片id,有值会更新,没有会自动新增
|
id?: number // 图片id,有值会更新,没有会自动新增
|
||||||
category: "cover" | "main_product" | "product" | "sketch" | "apparel" // 图片类型
|
category: ListingImageCategory // 图片类型
|
||||||
|
imageUrl?: string | null
|
||||||
|
isSelected?: number
|
||||||
|
sortOrder?: number
|
||||||
}
|
}
|
||||||
interface DetailData {
|
interface DetailData {
|
||||||
id: number | string // 商品Id
|
id: number | string // 商品Id
|
||||||
title: string // 商品名
|
title: string // 商品名
|
||||||
description: string // 商品描述
|
description: string // 商品描述
|
||||||
price: number // 价格
|
price: number | string // 价格
|
||||||
stock?: number // 库存
|
stock?: number // 库存
|
||||||
viewCount?: number // 浏览量
|
viewCount?: number // 浏览量
|
||||||
status: 0 | 1 | 2 // 0草稿 1发布 2删除
|
status: 0 | 1 | 2 // 0草稿 1发布 2删除
|
||||||
images: ImageObj[]
|
images: ImageObj[]
|
||||||
designFor: "male" | "female"
|
designFor: "male" | "female"
|
||||||
productCategory: "outwear" | "trousers" | "blouse" | "dress" | "skirt" | "accessories"
|
productCategory: string[] | null
|
||||||
}
|
}
|
||||||
// 保存/更新表单
|
// 保存/更新表单
|
||||||
export const fetchUpdateListing = (data: DetailData) => {
|
export const fetchUpdateListing = (data: DetailData[]) => {
|
||||||
return Https.axiosPost("/seller/listing/batch", data)
|
return Https.axiosPost("/seller/listing/batch", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,10 @@
|
|||||||
v-for="(item, index) in imageList"
|
v-for="(item, index) in imageList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="product-image-item flex flex-center"
|
class="product-image-item flex flex-center"
|
||||||
:class="{ selected: item.selected }"
|
:class="{ selected: item.selected, video: item.isVideo }"
|
||||||
@click="emit('select', index)"
|
@click="emit('select', index)"
|
||||||
|
@mouseenter="handleMouseEnter(index, item)"
|
||||||
|
@mouseleave="handleMouseLeave(index, item)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="item.selected"
|
v-if="item.selected"
|
||||||
@@ -21,8 +23,11 @@
|
|||||||
class="checked"
|
class="checked"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
<img class="img-src" :src="item.url" alt="" />
|
<img class="img-src" :src="getDisplayUrl(item, index)" alt="" />
|
||||||
<div v-if="item.selected && index === firstSelectedIndex" class="main-pic">
|
<div v-if="item.isVideo && durationMap[index]" class="video-duration">
|
||||||
|
{{ durationMap[index] }}
|
||||||
|
</div>
|
||||||
|
<div v-if="item.selected && index === firstSelectedIndex && !item.isVideo" class="main-pic">
|
||||||
main
|
main
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,8 +35,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue"
|
import { computed, ref, watch } from "vue"
|
||||||
import type { ListingItem } from "../types"
|
import type { ListingItem, ProductMediaItem } from "../types"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
imageList: ListingItem["prodImageList"]
|
imageList: ListingItem["prodImageList"]
|
||||||
@@ -43,6 +48,70 @@
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const selectedCount = computed(() => props.imageList.filter((item) => item.selected).length)
|
const selectedCount = computed(() => props.imageList.filter((item) => item.selected).length)
|
||||||
|
const hoveredVideoIndex = ref<number | null>(null)
|
||||||
|
const durationMap = ref<Record<number, string>>({})
|
||||||
|
const videoSourceKey = computed(() =>
|
||||||
|
props.imageList
|
||||||
|
.map((item) => `${item.videoUrl || ""}::${item.url || ""}`)
|
||||||
|
.join("|")
|
||||||
|
)
|
||||||
|
|
||||||
|
const getDisplayUrl = (item: ProductMediaItem, index: number) => {
|
||||||
|
if (item.isVideo && hoveredVideoIndex.value === index && item.gifUrl) {
|
||||||
|
return item.gifUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.url
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseEnter = (index: number, item: ProductMediaItem) => {
|
||||||
|
if (!item.isVideo || !item.gifUrl) return
|
||||||
|
hoveredVideoIndex.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseLeave = (index: number, item: ProductMediaItem) => {
|
||||||
|
if (!item.isVideo) return
|
||||||
|
if (hoveredVideoIndex.value === index) hoveredVideoIndex.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatVideoDuration = (duration: number) => {
|
||||||
|
if (!Number.isFinite(duration) || duration <= 0) return ""
|
||||||
|
|
||||||
|
const totalSeconds = Math.round(duration)
|
||||||
|
const minutes = Math.floor(totalSeconds / 60)
|
||||||
|
const seconds = totalSeconds % 60
|
||||||
|
|
||||||
|
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadVideoDurations = () => {
|
||||||
|
durationMap.value = {}
|
||||||
|
props.imageList.forEach((item, index) => {
|
||||||
|
if (!item.isVideo || !item.videoUrl) return
|
||||||
|
|
||||||
|
const video = document.createElement("video")
|
||||||
|
video.preload = "metadata"
|
||||||
|
video.src = item.videoUrl
|
||||||
|
video.onloadedmetadata = () => {
|
||||||
|
durationMap.value = {
|
||||||
|
...durationMap.value,
|
||||||
|
[index]: formatVideoDuration(video.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
video.onerror = () => {
|
||||||
|
video.src = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
videoSourceKey,
|
||||||
|
() => {
|
||||||
|
hoveredVideoIndex.value = null
|
||||||
|
loadVideoDurations()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@@ -135,6 +204,20 @@
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-duration {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.8rem;
|
||||||
|
bottom: 0.8rem;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
border-radius: 1.2rem;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.main-pic {
|
.main-pic {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 2.4rem;
|
height: 2.4rem;
|
||||||
|
|||||||
@@ -88,10 +88,16 @@
|
|||||||
fetchUpdateListing
|
fetchUpdateListing
|
||||||
} from "./api"
|
} from "./api"
|
||||||
import type {
|
import type {
|
||||||
|
CoverSourceType,
|
||||||
|
CropType,
|
||||||
ListingDetailImage,
|
ListingDetailImage,
|
||||||
ListingDetailResponse,
|
ListingDetailResponse,
|
||||||
|
ListingImageCategory,
|
||||||
ListingItem,
|
ListingItem,
|
||||||
|
ProductMediaItem,
|
||||||
RadioOption,
|
RadioOption,
|
||||||
|
SketchDetailResponse,
|
||||||
|
SketchDetailVideo,
|
||||||
StatusType
|
StatusType
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
@@ -121,18 +127,14 @@
|
|||||||
desc: "",
|
desc: "",
|
||||||
gender: "FEMALE",
|
gender: "FEMALE",
|
||||||
category: null,
|
category: null,
|
||||||
|
coverFrom: "sketch",
|
||||||
firstSelectedIndex: null,
|
firstSelectedIndex: null,
|
||||||
prodImageList: [],
|
prodImageList: [],
|
||||||
sketchList: []
|
sketchList: []
|
||||||
})
|
})
|
||||||
|
|
||||||
const genderOptions = computed(() => {
|
const genderOptions = computed(() => {
|
||||||
return (
|
return STORE.state.UserHabit?.sex.value
|
||||||
STORE.state.UserHabit?.sex.value.map((el) => ({
|
|
||||||
...el,
|
|
||||||
// name: el.key.toLowerCase()
|
|
||||||
})) || []
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const fallbackCategoryOptions: Record<string, RadioOption[]> = {
|
const fallbackCategoryOptions: Record<string, RadioOption[]> = {
|
||||||
@@ -172,14 +174,50 @@
|
|||||||
return [...images].sort((prev, next) => (prev.sortOrder ?? 0) - (next.sortOrder ?? 0))
|
return [...images].sort((prev, next) => (prev.sortOrder ?? 0) - (next.sortOrder ?? 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getImageSelected = (value: ListingDetailImage["isSelected"]) =>
|
const getImageSelected = (value: ListingDetailImage["isSelected"]) => {
|
||||||
value === true || value === 1 || value === "1"
|
if (value === true || value === 1 || value === "1") return true
|
||||||
|
if (typeof value === "string") return value.toLowerCase() === "true"
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDetailImageSelected = (image: ListingDetailImage) =>
|
||||||
|
getImageSelected(image.isSelected) ||
|
||||||
|
getImageSelected(image.isSeleted) ||
|
||||||
|
getImageSelected(image.selected)
|
||||||
|
|
||||||
|
const normalizeCoverSource = (value: unknown): CoverSourceType =>
|
||||||
|
value === "mainProductImage" ? "mainProductImage" : "sketch"
|
||||||
|
|
||||||
|
const isCoverSource = (value: unknown): value is CoverSourceType =>
|
||||||
|
value === "sketch" || value === "mainProductImage"
|
||||||
|
|
||||||
|
const resolveCoverSourceFromImageUrl = (
|
||||||
|
imageUrl: string,
|
||||||
|
listing: ListingItem
|
||||||
|
): CoverSourceType => {
|
||||||
|
if (imageUrl === listing.mainProductImage) return "mainProductImage"
|
||||||
|
if (imageUrl === listing.sketch) return "sketch"
|
||||||
|
|
||||||
|
return normalizeCoverSource(imageUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoImageCategories = ["firstFrame", "gif", "video"] as const
|
||||||
|
type VideoImageCategory = (typeof videoImageCategories)[number]
|
||||||
|
|
||||||
|
const isVideoImageCategory = (
|
||||||
|
category: ListingDetailImage["category"]
|
||||||
|
): category is VideoImageCategory =>
|
||||||
|
videoImageCategories.includes(category as VideoImageCategory)
|
||||||
|
|
||||||
const normalizeDetailGender = (value: ListingDetailResponse["designFor"]) => {
|
const normalizeDetailGender = (value: ListingDetailResponse["designFor"]) => {
|
||||||
const gender = String(value || "").toUpperCase()
|
const gender = String(value || "").toUpperCase()
|
||||||
return gender === "MALE" || gender === "FEMALE" ? gender : "FEMALE"
|
return gender === "MALE" || gender === "FEMALE" ? gender : "FEMALE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getListingDesignFor = (gender: ListingItem["gender"]): "male" | "female" =>
|
||||||
|
gender === "MALE" ? "male" : "female"
|
||||||
|
|
||||||
const normalizeDetailCategory = (
|
const normalizeDetailCategory = (
|
||||||
value: ListingDetailResponse["productCategory"]
|
value: ListingDetailResponse["productCategory"]
|
||||||
): ListingItem["category"] => {
|
): ListingItem["category"] => {
|
||||||
@@ -193,6 +231,17 @@
|
|||||||
|
|
||||||
const createListingItemFromDetail = (detail: ListingDetailResponse): ListingItem => {
|
const createListingItemFromDetail = (detail: ListingDetailResponse): ListingItem => {
|
||||||
const listing = createListingItem()
|
const listing = createListingItem()
|
||||||
|
let coverFromImageUrl = ""
|
||||||
|
const videoGroupMap = new Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
sortOrder: number
|
||||||
|
firstFrameUrl?: string
|
||||||
|
gifUrl?: string
|
||||||
|
videoUrl?: string
|
||||||
|
selected: boolean
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
|
||||||
listing.productName = detail.title || ""
|
listing.productName = detail.title || ""
|
||||||
listing.price =
|
listing.price =
|
||||||
@@ -202,6 +251,11 @@
|
|||||||
listing.category = normalizeDetailCategory(detail.productCategory)
|
listing.category = normalizeDetailCategory(detail.productCategory)
|
||||||
|
|
||||||
getSortedDetailImages(detail.images || []).forEach((image) => {
|
getSortedDetailImages(detail.images || []).forEach((image) => {
|
||||||
|
if (image.category === "cover_from") {
|
||||||
|
coverFromImageUrl = image.imageUrl || ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const imageUrl = image.imageUrl || ""
|
const imageUrl = image.imageUrl || ""
|
||||||
if (!imageUrl) return
|
if (!imageUrl) return
|
||||||
|
|
||||||
@@ -215,7 +269,7 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image.category === "mainProductImage") {
|
if (image.category === "mainProductImage" || image.category === "main_product") {
|
||||||
listing.mainProductImage = imageUrl
|
listing.mainProductImage = imageUrl
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -223,23 +277,50 @@
|
|||||||
if (image.category === "product") {
|
if (image.category === "product") {
|
||||||
listing.prodImageList.push({
|
listing.prodImageList.push({
|
||||||
url: imageUrl,
|
url: imageUrl,
|
||||||
selected: getImageSelected(image.isSelected)
|
selected: getDetailImageSelected(image)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isVideoImageCategory(image.category)) {
|
||||||
|
if (image.sortOrder === null || typeof image.sortOrder === "undefined") return
|
||||||
|
|
||||||
|
const sortOrder = Number(image.sortOrder)
|
||||||
|
if (!Number.isFinite(sortOrder)) return
|
||||||
|
|
||||||
|
const group = videoGroupMap.get(sortOrder) || {
|
||||||
|
sortOrder,
|
||||||
|
selected: false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image.category === "firstFrame") group.firstFrameUrl = imageUrl
|
||||||
|
if (image.category === "gif") group.gifUrl = imageUrl
|
||||||
|
if (image.category === "video") group.videoUrl = imageUrl
|
||||||
|
group.selected = group.selected || getDetailImageSelected(image)
|
||||||
|
videoGroupMap.set(sortOrder, group)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (image.category === "apparel") {
|
if (image.category === "apparel") {
|
||||||
listing.sketchList.push({ url: imageUrl })
|
listing.sketchList.push({ url: imageUrl })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!listing.mainProductImage) {
|
Array.from(videoGroupMap.values())
|
||||||
listing.mainProductImage =
|
.sort((prev, next) => prev.sortOrder - next.sortOrder)
|
||||||
listing.prodImageList.find((item) => item.selected)?.url || ""
|
.forEach((video) => {
|
||||||
|
const videoItem = createProductVideoItem(video, video.selected)
|
||||||
|
if (videoItem) listing.prodImageList.push(videoItem)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (coverFromImageUrl) {
|
||||||
|
listing.coverFrom = resolveCoverSourceFromImageUrl(coverFromImageUrl, listing)
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedIndex = listing.prodImageList.findIndex((item) => item.selected)
|
const mainProductIndex = listing.prodImageList.findIndex(
|
||||||
listing.firstSelectedIndex = selectedIndex === -1 ? null : selectedIndex
|
(item) => !item.isVideo && item.url === listing.mainProductImage
|
||||||
|
)
|
||||||
|
listing.firstSelectedIndex = mainProductIndex === -1 ? null : mainProductIndex
|
||||||
|
|
||||||
listing.productImage = listing.prodImageList.map((item) => item.url)
|
listing.productImage = listing.prodImageList.map((item) => item.url)
|
||||||
listing.apparelSketch = listing.sketchList
|
listing.apparelSketch = listing.sketchList
|
||||||
@@ -249,6 +330,26 @@
|
|||||||
return listing
|
return listing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createProductVideoItem = (
|
||||||
|
video: SketchDetailVideo,
|
||||||
|
selected = false
|
||||||
|
): ProductMediaItem | null => {
|
||||||
|
const firstFrameUrl = video?.firstFrameUrl || ""
|
||||||
|
const gifUrl = video?.gifUrl || ""
|
||||||
|
const videoUrl = video?.videoUrl || ""
|
||||||
|
|
||||||
|
if (!firstFrameUrl || !videoUrl) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: firstFrameUrl,
|
||||||
|
firstFrameUrl,
|
||||||
|
gifUrl,
|
||||||
|
videoUrl,
|
||||||
|
isVideo: true,
|
||||||
|
selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSelectProdImg = (index: number) => {
|
const handleSelectProdImg = (index: number) => {
|
||||||
const listing = currentListing.value
|
const listing = currentListing.value
|
||||||
const target = prodImgList.value[index]
|
const target = prodImgList.value[index]
|
||||||
@@ -257,6 +358,10 @@
|
|||||||
target.selected = willSelect
|
target.selected = willSelect
|
||||||
|
|
||||||
if (willSelect && listing.firstSelectedIndex === null) {
|
if (willSelect && listing.firstSelectedIndex === null) {
|
||||||
|
if (target.isVideo) {
|
||||||
|
message.warning(t("Seller.VideoWarning"))
|
||||||
|
return
|
||||||
|
}
|
||||||
listing.mainProductImage = target.url
|
listing.mainProductImage = target.url
|
||||||
listing.firstSelectedIndex = index
|
listing.firstSelectedIndex = index
|
||||||
return
|
return
|
||||||
@@ -268,25 +373,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cropType = ref("")
|
const getCoverOriginList = (item: ListingItem) => {
|
||||||
const handleClickCrop = (data: any, type: string, paramThree: any = []) => {
|
const origin: Array<{ type: CoverSourceType; url: string }> = []
|
||||||
|
if (item.sketch) {
|
||||||
|
origin.push({ type: "sketch", url: item.sketch })
|
||||||
|
}
|
||||||
|
if (item.mainProductImage) {
|
||||||
|
origin.push({ type: "mainProductImage", url: item.mainProductImage })
|
||||||
|
}
|
||||||
|
|
||||||
|
return origin
|
||||||
|
}
|
||||||
|
|
||||||
|
const cropType = ref<CropType | "">("")
|
||||||
|
const handleClickCrop = (
|
||||||
|
data: string | null,
|
||||||
|
type: CropType,
|
||||||
|
paramThree: number | unknown[] = []
|
||||||
|
) => {
|
||||||
// 处理来自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 : []
|
|
||||||
|
|
||||||
// console.log(data, type)
|
// console.log(data, type)
|
||||||
// console.log(selectList.value[currentIndex.value])
|
// console.log(selectList.value[currentIndex.value])
|
||||||
let origin = []
|
|
||||||
const currentItem = selectList.value[currentIndex.value]
|
const currentItem = selectList.value[currentIndex.value]
|
||||||
if (currentItem.sketch) {
|
const origin = type === "cover" ? getCoverOriginList(currentItem) : []
|
||||||
origin.push({ type: "sketch", url: currentItem.sketch })
|
const titleList: Record<CropType, string> = {
|
||||||
}
|
|
||||||
if (currentItem.mainProductImage) {
|
|
||||||
origin.push({ type: "mainProductImage", url: currentItem.mainProductImage })
|
|
||||||
}
|
|
||||||
if (type !== "cover") origin = []
|
|
||||||
const titleList = {
|
|
||||||
sketch: "Crop Sketch",
|
sketch: "Crop Sketch",
|
||||||
mainProductImage: "Crop Main Product Image",
|
mainProductImage: "Crop Main Product Image",
|
||||||
cover: "Crop Cover",
|
cover: "Crop Cover",
|
||||||
@@ -296,17 +409,28 @@
|
|||||||
cropType.value = type
|
cropType.value = type
|
||||||
imageClipDialogRef.value.open(
|
imageClipDialogRef.value.open(
|
||||||
data,
|
data,
|
||||||
(file) => {
|
(file: File, coverFrom?: CoverSourceType) => {
|
||||||
// console.log(file)
|
// console.log(file)
|
||||||
uploadFile(file).then((res) => {
|
uploadFile(file).then((res) => {
|
||||||
if (type === "apparel" && typeof index !== "undefined") {
|
if (type === "apparel" && typeof index !== "undefined") {
|
||||||
selectList.value[currentIndex.value].sketchList[index].url = res
|
selectList.value[currentIndex.value].sketchList[index].url = res
|
||||||
|
} else if (type === "cover") {
|
||||||
|
selectList.value[currentIndex.value].cover = res
|
||||||
|
if (isCoverSource(coverFrom)) {
|
||||||
|
selectList.value[currentIndex.value].coverFrom = coverFrom
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
selectList.value[currentIndex.value][type] = res
|
selectList.value[currentIndex.value][type] = res
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ ratio, isPreview: true, title: titleList[type], isProduct: true },
|
{
|
||||||
|
ratio,
|
||||||
|
isPreview: true,
|
||||||
|
title: titleList[type],
|
||||||
|
isProduct: true,
|
||||||
|
coverFrom: currentItem.coverFrom
|
||||||
|
},
|
||||||
origin
|
origin
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -315,10 +439,9 @@
|
|||||||
value !== null && value !== undefined && String(value).trim() !== ""
|
value !== null && value !== undefined && String(value).trim() !== ""
|
||||||
|
|
||||||
const getMissingRequiredField = (item: ListingItem) => {
|
const getMissingRequiredField = (item: ListingItem) => {
|
||||||
const cover = item.cover || item.mainProductImage || item.sketch
|
|
||||||
const requiredFields = [
|
const requiredFields = [
|
||||||
{ value: item.sketch, label: t("SellerListEdit.sketch") },
|
{ value: item.sketch, label: t("SellerListEdit.sketch") },
|
||||||
{ value: cover, label: t("SellerListEdit.cover") },
|
{ value: item.cover, label: t("SellerListEdit.cover") },
|
||||||
{ value: item.productName, label: t("SellerListEdit.productName") },
|
{ value: item.productName, label: t("SellerListEdit.productName") },
|
||||||
{ value: item.price, label: t("SellerListEdit.price") },
|
{ value: item.price, label: t("SellerListEdit.price") },
|
||||||
{ value: item.desc, label: t("SellerListEdit.productDescription") },
|
{ value: item.desc, label: t("SellerListEdit.productDescription") },
|
||||||
@@ -358,63 +481,120 @@
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SaveListingImage = {
|
||||||
|
category: ListingImageCategory
|
||||||
|
imageUrl: string | null
|
||||||
|
isSelected: number
|
||||||
|
sortOrder: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMainProductMedia = (item: ListingItem, media: ProductMediaItem, index: number) =>
|
||||||
|
!media.isVideo &&
|
||||||
|
(item.firstSelectedIndex === index ||
|
||||||
|
(Boolean(item.mainProductImage) && item.mainProductImage === media.url))
|
||||||
|
|
||||||
|
const getSortedProductMedia = (item: ListingItem) => {
|
||||||
|
return item.prodImageList
|
||||||
|
.map((media, index) => ({ media, index }))
|
||||||
|
.filter(({ media }) => !media.isVideo)
|
||||||
|
.sort((prev, next) => {
|
||||||
|
const prevRank = isMainProductMedia(item, prev.media, prev.index)
|
||||||
|
? 0
|
||||||
|
: prev.media.selected
|
||||||
|
? 1
|
||||||
|
: 2
|
||||||
|
const nextRank = isMainProductMedia(item, next.media, next.index)
|
||||||
|
? 0
|
||||||
|
: next.media.selected
|
||||||
|
? 1
|
||||||
|
: 2
|
||||||
|
|
||||||
|
return prevRank - nextRank || prev.index - next.index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildListingImages = (item: ListingItem) => {
|
||||||
|
const images: SaveListingImage[] = []
|
||||||
|
const sortOrderMap: Partial<Record<ListingImageCategory, number>> = {}
|
||||||
|
const getNextSortOrder = (category: ListingImageCategory) => {
|
||||||
|
const sortOrder = (sortOrderMap[category] || 0) + 1
|
||||||
|
sortOrderMap[category] = sortOrder
|
||||||
|
return sortOrder
|
||||||
|
}
|
||||||
|
const pushImage = (
|
||||||
|
category: ListingImageCategory,
|
||||||
|
imageUrl: string | null,
|
||||||
|
isSelected = 1,
|
||||||
|
sortOrder = getNextSortOrder(category)
|
||||||
|
) => {
|
||||||
|
images.push({
|
||||||
|
category,
|
||||||
|
imageUrl,
|
||||||
|
isSelected,
|
||||||
|
sortOrder
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const getCoverFromImageUrl = () => {
|
||||||
|
if (item.coverFrom === "mainProductImage") return item.mainProductImage || null
|
||||||
|
return item.sketch
|
||||||
|
}
|
||||||
|
|
||||||
|
pushImage("sketch", item.sketch)
|
||||||
|
pushImage("cover", item.cover)
|
||||||
|
pushImage("cover_from", getCoverFromImageUrl())
|
||||||
|
|
||||||
|
if (item.mainProductImage) {
|
||||||
|
pushImage("main_product", item.mainProductImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSortedProductMedia(item).forEach(({ media }) => {
|
||||||
|
pushImage("product", media.url, Number(!!media.selected))
|
||||||
|
})
|
||||||
|
|
||||||
|
item.sketchList.forEach((sketch) => {
|
||||||
|
pushImage("apparel", sketch.url)
|
||||||
|
})
|
||||||
|
|
||||||
|
let videoSortOrder = 0
|
||||||
|
item.prodImageList
|
||||||
|
.filter((media) => media.isVideo)
|
||||||
|
.forEach((media) => {
|
||||||
|
videoSortOrder += 1
|
||||||
|
const isSelected = Number(!!media.selected)
|
||||||
|
|
||||||
|
pushImage(
|
||||||
|
"firstFrame",
|
||||||
|
media.firstFrameUrl || media.url,
|
||||||
|
isSelected,
|
||||||
|
videoSortOrder
|
||||||
|
)
|
||||||
|
pushImage("gif", media.gifUrl || "", isSelected, videoSortOrder)
|
||||||
|
pushImage("video", media.videoUrl || "", isSelected, videoSortOrder)
|
||||||
|
})
|
||||||
|
|
||||||
|
return images
|
||||||
|
}
|
||||||
|
|
||||||
const handleSaveForm = async (type: StatusType) => {
|
const handleSaveForm = async (type: StatusType) => {
|
||||||
const paramsList = []
|
const paramsList = selectList.value.map((item: ListingItem) => {
|
||||||
selectList.value.forEach((item: ListingItem) => {
|
return {
|
||||||
const params = {
|
|
||||||
id: itemId.value,
|
id: itemId.value,
|
||||||
title: item.productName,
|
title: item.productName,
|
||||||
description: item.desc,
|
description: item.desc,
|
||||||
price: item.price,
|
price: item.price,
|
||||||
status: type === "draft" ? 0 : 1,
|
status: type === "draft" ? 0 : 1,
|
||||||
images: [],
|
images: buildListingImages(item),
|
||||||
designFor: (item.gender || "FEMALE").toLowerCase(),
|
designFor: getListingDesignFor(item.gender),
|
||||||
productCategory: item.category
|
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)
|
await fetchUpdateListing(paramsList)
|
||||||
}
|
}
|
||||||
const handleClickMenu = async (status: StatusType) => {
|
const handleClickMenu = async (status: StatusType) => {
|
||||||
if (status === "draft" && !selectList.value[currentIndex.value].cover) {
|
|
||||||
message.error("请先完成封面制作")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!validatePublishRequired()) return
|
if (!validatePublishRequired()) return
|
||||||
|
|
||||||
await handleSaveForm(status)
|
await handleSaveForm(status)
|
||||||
if (status === "draft") {
|
if (status === "draft") {
|
||||||
// save draft logic
|
|
||||||
// console.log("Saving draft...", currentListing.value)
|
|
||||||
ROUTER.push({
|
ROUTER.push({
|
||||||
name: "Status",
|
name: "Status",
|
||||||
params: { status: "draft" },
|
params: { status: "draft" },
|
||||||
@@ -424,8 +604,6 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (status === "publish") {
|
} else if (status === "publish") {
|
||||||
// publish logic
|
|
||||||
// console.log("Publishing...", currentListing.value)
|
|
||||||
ROUTER.push({
|
ROUTER.push({
|
||||||
name: "Status",
|
name: "Status",
|
||||||
params: { status: "publish" },
|
params: { status: "publish" },
|
||||||
@@ -438,13 +616,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleFetchItemDetial = (list) => {
|
const handleFetchItemDetial = (list) => {
|
||||||
fetchSketchDetail(list).then((res) => {
|
fetchSketchDetail(list).then((res: SketchDetailResponse[]) => {
|
||||||
res.forEach((item, index) => {
|
res.forEach((item, index) => {
|
||||||
if (!selectList.value[index]) return
|
if (!selectList.value[index]) return
|
||||||
selectList.value[index].sketchList = item.clothes.map((el) => ({ url: el }))
|
selectList.value[index].sketchList = (item.clothes || []).map((el) => ({ url: el }))
|
||||||
selectList.value[index].prodImageList = item.toProductImageUrls.map((el) => ({
|
const imageItems = (item.toProductImageUrls || []).map((el) => ({
|
||||||
url: el
|
url: el,
|
||||||
|
selected: false
|
||||||
}))
|
}))
|
||||||
|
const videoItems = (item.videos || [])
|
||||||
|
.map((video) => createProductVideoItem(video))
|
||||||
|
.filter((video): video is ProductMediaItem => Boolean(video))
|
||||||
|
selectList.value[index].prodImageList = [...imageItems, ...videoItems]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,27 @@ export type RadioOption = {
|
|||||||
|
|
||||||
export type TopImageType = "sketch" | "mainProductImage" | "cover"
|
export type TopImageType = "sketch" | "mainProductImage" | "cover"
|
||||||
export type CropType = TopImageType | "apparel"
|
export type CropType = TopImageType | "apparel"
|
||||||
|
export type CoverSourceType = "sketch" | "mainProductImage"
|
||||||
|
export type ListingImageCategory =
|
||||||
|
| "cover"
|
||||||
|
| "cover_from"
|
||||||
|
| "main_product"
|
||||||
|
| "mainProductImage"
|
||||||
|
| "product"
|
||||||
|
| "sketch"
|
||||||
|
| "apparel"
|
||||||
|
| "firstFrame"
|
||||||
|
| "gif"
|
||||||
|
| "video"
|
||||||
|
|
||||||
|
export type ProductMediaItem = {
|
||||||
|
url: string
|
||||||
|
selected?: boolean
|
||||||
|
isVideo?: boolean
|
||||||
|
videoUrl?: string
|
||||||
|
gifUrl?: string
|
||||||
|
firstFrameUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ListingItem = {
|
export type ListingItem = {
|
||||||
designItemId: number | string | null
|
designItemId: number | string | null
|
||||||
@@ -20,18 +41,18 @@ export type ListingItem = {
|
|||||||
desc: string
|
desc: string
|
||||||
gender: string
|
gender: string
|
||||||
category: string[] | null
|
category: string[] | null
|
||||||
|
coverFrom: CoverSourceType
|
||||||
firstSelectedIndex: number | null
|
firstSelectedIndex: number | null
|
||||||
prodImageList: Array<{
|
prodImageList: ProductMediaItem[]
|
||||||
url: string
|
|
||||||
selected?: boolean
|
|
||||||
}>
|
|
||||||
sketchList: Array<{ url: string | null }>
|
sketchList: Array<{ url: string | null }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListingDetailImage = {
|
export type ListingDetailImage = {
|
||||||
category?: string | null
|
category?: ListingImageCategory | string | null
|
||||||
imageUrl?: string | null
|
imageUrl?: string | null
|
||||||
isSelected?: boolean | number | string | null
|
isSelected?: boolean | number | string | null
|
||||||
|
isSeleted?: boolean | number | string | null
|
||||||
|
selected?: boolean | number | string | null
|
||||||
sortOrder?: number | null
|
sortOrder?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,4 +66,17 @@ export type ListingDetailResponse = {
|
|||||||
images?: ListingDetailImage[] | null
|
images?: ListingDetailImage[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SketchDetailVideo = {
|
||||||
|
firstFrameUrl?: string | null
|
||||||
|
gifUrl?: string | null
|
||||||
|
videoUrl?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SketchDetailResponse = {
|
||||||
|
clothes?: string[] | null
|
||||||
|
designItemId?: number | string | null
|
||||||
|
toProductImageUrls?: string[] | null
|
||||||
|
videos?: SketchDetailVideo[] | null
|
||||||
|
}
|
||||||
|
|
||||||
export type StatusType = "draft" | "publish"
|
export type StatusType = "draft" | "publish"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Https } from '@/tool/https'
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
// 定义组件名称
|
// 定义组件名称
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -45,6 +46,10 @@ const chooseItem = (item:any)=>{
|
|||||||
if(chooseList.value.findIndex((i:any)=>i.designItemId == item.designItemId) != -1){
|
if(chooseList.value.findIndex((i:any)=>i.designItemId == item.designItemId) != -1){
|
||||||
chooseList.value.splice(chooseList.value.findIndex((i:any)=>i.designItemId == item.designItemId),1)
|
chooseList.value.splice(chooseList.value.findIndex((i:any)=>i.designItemId == item.designItemId),1)
|
||||||
}else{
|
}else{
|
||||||
|
if(chooseList.value.length >= 9){
|
||||||
|
message.info(t('Seller.selectSketchMaxNum'))
|
||||||
|
return
|
||||||
|
}
|
||||||
chooseList.value.push(item)
|
chooseList.value.push(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +168,7 @@ const {} = toRefs(data);
|
|||||||
<template #right>
|
<template #right>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="chooseNum">
|
<div class="chooseNum">
|
||||||
{{ chooseList.length }} {{ t('Seller.sketchesSelected') }}
|
{{ chooseList.length }} / 9 {{ t('Seller.sketchesSelected') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="button" @click="next">
|
<div class="button" @click="next">
|
||||||
<span>{{ $t('Seller.Next') }}</span>
|
<span>{{ $t('Seller.Next') }}</span>
|
||||||
@@ -177,8 +182,8 @@ const {} = toRefs(data);
|
|||||||
<div class="content" ref="listingsBoxRef">
|
<div class="content" ref="listingsBoxRef">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<i class="fi fi-rs-comments"></i>
|
<i class="fi fi-rs-comments"></i>
|
||||||
<span>{{ $t('Seller.Praka') }}</span>
|
<span>{{ $t('Seller.Praka') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<div class="generalModel_state">
|
<div class="generalModel_state">
|
||||||
|
|||||||
Reference in New Issue
Block a user