57 Commits

Author SHA1 Message Date
X1627315083@163.com
5eaa77596e fix 2026-05-13 16:36:23 +08:00
李志鹏
848e7b4692 更改卖家审批提示词 2026-05-12 11:23:18 +08:00
李志鹏
fd140ebc56 还原绑定 2026-05-12 11:08:18 +08:00
李志鹏
34094c8c92 Merge branch 'dev_vite' of http://18.167.251.121:10003/aidlab/aida_front into dev_vite 2026-05-12 11:01:54 +08:00
李志鹏
e4dc2bf729 还原绑定 2026-05-12 11:01:53 +08:00
李志鹏
7f226179d9 绑定修改 2026-05-11 10:50:17 +08:00
李志鹏
3edff6b05c Merge branch 'dev_vite' of http://18.167.251.121:10003/aidlab/aida_front into dev_vite 2026-05-11 10:01:19 +08:00
李志鹏
6fd1212298 修改 2026-05-11 10:01:18 +08:00
e093cccb8d bugfix: 没有产品主图时自动选中问题 2026-05-07 17:06:06 +08:00
b2c6c61515 bugfix: 封面必填校验 2026-05-07 15:53:47 +08:00
0219b1a2f4 docs: 商品编辑页面的agents.md 2026-05-07 13:53:06 +08:00
133433a260 feat: 视频保存 2026-05-07 13:15:21 +08:00
90a59a3dc5 feat: 视频显示到列表 2026-05-07 10:21:08 +08:00
c8a65ee2cb Merge branch 'dev_vite' of ssh://18.167.251.121:10002/aidlab/aida_front into dev_vite 2026-05-06 16:33:32 +08:00
226918e941 style: 裁剪弹窗标题被遮挡问题 2026-05-06 16:33:31 +08:00
李志鹏
494bfd68ca 1 2026-05-06 16:13:34 +08:00
X1627315083@163.com
95b70792ba Merge branch 'dev_vite' of ssh://18.167.251.121:10002/aidlab/aida_front into dev_vite 2026-05-06 16:06:56 +08:00
X1627315083@163.com
a0fffa5896 fix 2026-05-06 16:06:54 +08:00
李志鹏
06eaabc742 Merge branch 'dev_vite' of http://18.167.251.121:10003/aidlab/aida_front into dev_vite 2026-05-06 13:45:29 +08:00
李志鹏
27da4739a6 语言适配 2026-05-06 13:45:28 +08:00
52576aa0a1 style: 裁剪框被遮挡 2026-05-06 13:25:42 +08:00
22aa7c37cd bugfix: 裁剪组件重置状态 2026-05-06 11:04:46 +08:00
752b33f196 bugfix: 裁剪组件切换类型清除之前的缓存 2026-05-05 15:19:31 +08:00
f3b873b7ae Merge branch 'dev_vite' of ssh://18.167.251.121:10002/aidlab/aida_front into dev_vite 2026-05-05 15:10:16 +08:00
006c2e3f9c bugfix: 剪切组件顶部来源按钮样式 2026-05-05 15:10:14 +08:00
X1627315083@163.com
1f413b36ca fix 2026-05-05 14:53:49 +08:00
X1627315083@163.com
e7b052f100 fix 2026-05-05 14:43:00 +08:00
X1627315083@163.com
19bb412470 Merge branch 'dev_vite' of ssh://18.167.251.121:10002/aidlab/aida_front into dev_vite 2026-05-05 14:41:17 +08:00
X1627315083@163.com
a1e071f7bc fix 2026-05-05 14:41:14 +08:00
李志鹏
5388b2df4c Merge branch 'dev_vite' of http://18.167.251.121:10003/aidlab/aida_front into dev_vite 2026-05-05 14:39:43 +08:00
李志鹏
76e507cae3 显示id 2026-05-05 14:39:41 +08:00
X1627315083@163.com
d4e9462d39 fix 2026-05-05 14:34:12 +08:00
X1627315083@163.com
4a11d172d2 fix 2026-05-05 14:27:59 +08:00
X1627315083@163.com
e20092c77f fix 2026-05-05 13:41:18 +08:00
X1627315083@163.com
5f4656c629 fix 2026-05-05 13:38:43 +08:00
X1627315083@163.com
14eca9aff2 fix 2026-05-05 13:33:51 +08:00
X1627315083@163.com
88f0528553 Merge branch 'dev_vite' of ssh://18.167.251.121:10002/aidlab/aida_front into dev_vite 2026-05-05 13:17:24 +08:00
X1627315083@163.com
afbea289fb fix 2026-05-05 13:17:20 +08:00
eb2baa26a7 bugfix: 导航i18n 2026-05-05 10:12:19 +08:00
X1627315083@163.com
62829395ce fix 2026-05-05 09:58:01 +08:00
X1627315083@163.com
16532ce44b fix 2026-05-05 09:25:33 +08:00
李志鹏
27de720137 Merge branch 'dev_vite' of http://18.167.251.121:10003/aidlab/aida_front into dev_vite 2026-05-04 14:08:18 +08:00
李志鹏
acf2029efe 删除设计师 2026-05-04 14:08:16 +08:00
X1627315083@163.com
49398aac48 语言适配 2026-05-04 14:06:16 +08:00
李志鹏
b3d9bce440 卖家端多语言 2026-05-04 11:18:56 +08:00
李志鹏
596bc75b83 设置裁剪框原图裁剪 2026-04-30 15:43:55 +08:00
李志鹏
c673948dd3 Merge branch 'dev_vite' of http://18.167.251.121:10003/aidlab/aida_front into dev_vite 2026-04-29 17:20:25 +08:00
李志鹏
1f8ee2e48e 1 2026-04-29 17:20:23 +08:00
e5ae549a3d Merge branch 'dev_vite' of ssh://18.167.251.121:10002/aidlab/aida_front into dev_vite 2026-04-29 17:19:05 +08:00
88c2ae8583 style: 裁剪 2026-04-29 17:19:00 +08:00
X1627315083@163.com
f4897e2c92 修复点击商品草稿删除里面没有内容 2026-04-29 16:59:09 +08:00
aab96bbe70 Merge branch 'dev_vite' of ssh://18.167.251.121:10002/aidlab/aida_front into dev_vite 2026-04-29 16:33:17 +08:00
4004fa2703 style: 性别label 2026-04-29 16:33:14 +08:00
李志鹏
4a078186a9 11 2026-04-29 16:23:42 +08:00
fafccf0352 bugfix: 切换性别清空选中的商品类别 2026-04-29 16:17:32 +08:00
64844105b5 bugfix: 面包屑导航 2026-04-29 15:41:20 +08:00
d6a07e7fc7 feat: 面包屑导航 2026-04-29 15:21:50 +08:00
38 changed files with 3304 additions and 2347 deletions

View File

@@ -2,6 +2,7 @@ VITE_USER_NODE_ENV = 'development_cloud'
# VITE_APP_BASE_URL = 'https://aida.com.hk/test'
# VITE_APP_BASE_URL = 'http://18.167.251.121:10088'
# VITE_APP_BASE_URL = 'https://api.aida.com.hk'
VITE_APP_BASE_URL = 'https://develop.api.aida.com.hk'
# VITE_APP_BASE_URL = 'https://develop.api.aida.com.hk'
VITE_APP_BASE_URL = 'https://www.develop-ms.api.aida.com.hk'
# VITE_APP_BASE_URL = 'http://localhost:22170'

4
.gitignore vendored
View File

@@ -24,4 +24,6 @@ dist.rar
*.sw?
.eslintrc-auto-import.json
components.d.ts
.cursor
.cursor
*.zip
*.7z

View File

@@ -167,8 +167,7 @@ li {
padding: 0.6rem 0.5rem;
}
.ant-modal-mask {
background-color: #666666;
opacity: 0.5;
background-color: rgba(102, 102, 102, 0.5);
}
.select_block {
height: 4rem;
@@ -1251,8 +1250,8 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte
border-color: #000 !important;
}
.ant-spin .ant-spin-dot {
width: 1.5em;
height: 1.5em;
width: 4.5rem;
height: 4.5rem;
}
.ant-spin-dot-item {
background-color: #000000 !important;

View 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

View File

@@ -172,8 +172,9 @@ input:focus{
}
}
.ant-modal-mask{
background-color: #666666;
opacity: .5;
background-color: rgba(102,102,102,0.5);
// background-color: #666666;
// opacity: .5;
}
.select_block{
height: 4rem;

View File

@@ -16,7 +16,7 @@
<keep-alive :include="cachedRoutes">
<component
:is="Component"
:key="route.name"
:cachedRoutes="cachedRoutes"
/>
</keep-alive>
</router-view>

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,7 @@ export default {
SubscribeNow: 'Subscribe now',
TaskList: 'Task List',
ViewOrders: 'View Orders',
PersonalCenter: 'Personal Center',
BecomeSeller: 'Become a Seller',
SellerDashboard: 'Seller Dashboard',
toolsToProduct: 'To product image',
@@ -1795,7 +1796,7 @@ export default {
cover:'Cover',
productImageDesc:'Choose from product image',
cropDesc:'Crop from main product image or sketch',
productImageMainTitle:'Product Image ',
productImageMainTitle:'Product Media ',
productImageSubTitle:'(from design collection)',
apparelSketchTitle:'Apparel Sketch ',
apparelSketchSubTitle:'(from design collection)',
@@ -1813,5 +1814,132 @@ export default {
draftDesc: 'Your listing has been saved as a draft. \nYou can continue editing or publish it later from My Listings.',
listingLive:'Listing Live',
publishDesc:'Your listing is now live on the marketplace.\nBuyers can discover and purchase your design.'
},
Seller: {
brandProfile: "Brand Profile",
myListings: "My Listings",
myOrders: "My Orders",
settings: "Settings",
// Brand Profile
brandProfileBgNullTip: "Your brand banner has not been set up yet.",
changeBrandBanner: "Change Brand Banner",
edit: "Edit",
cancel: "Cancel",
confirm: "Confirm",
saveChange: "Save Change",
brandProfileEditTip: "Changes will be reflected on your Stylish Parade brand page.",
storeName: "Store Name",
storeNameDesc: "Enter your store name",
ownerName: "Owners Full Name",
ownerNameDesc: "Enter store owner's full name",
email: "Email",
emailDesc: "Enter email",
mobile: "Phone Number",
mobileDesc: "Enter phone number",
link: "Link {index}",
links: "Portfoilo/Social Media Links",
storeDescription: "Store Description",
storeDescriptionDesc: "Briefly describe your design style and store features...",
storeDescriptionErr: "Please enter store description",
cropBrandBanner: "Crop Brand Banner",
cropAvatar: "Crop Avatar",
cropFrom: "Crop from:",
sketch: "Sketch",
mainProductImage: "Main Product Image",
cropPreview: "Crop Preview",
imageClipCoverTip: "Align crown to top, mid-thigh to bottom for best results.",
imageClipMainProductImageTip: "Align crown to top, foot base to bottom for best results.",
imageClipSketchTip: "Align crown to top, foot base to bottom for best results.",
imageClipApparelTip: "Trim whitespace and center your apparel sketch.",
// 我的订单
totalRevenue: "Total Revenue",
totalPurchases: "Total Purchases",
totalViews: "Total Views",
allInvoice: "All Invoice",
myOrdersTip: "A summary of all completed transactions.",
myOrdersSearchPlaceholder: "Search by item name or order ID",
orderId: "Order ID",
item: "Item",
price: "Price",
buyerUsername: "Buyer Username",
date: "Date",
// 设置
notifications: "Notifications",
notificationsTitle: "New order notification",
notificationsTip: "Receive an inbox message when a new order is placed.",
payout: "Payout",
payoutTitle: "Payment Providers",
payoutTip: "Select how you want to receive payments.",
unbound: "Unbound",
manage: "Manage",
bindNow: "Bind Now",
paymentCurrency: "Payment Currency",
HKD: "HKD - Hong Kong Dollar",
fixed: "Fixed",
dataPrivacy: "Data & Privacy",
dataPrivacyTitle: "Copyright licence",
dataPrivacyTip1: "A licence certificate is automatically included with every purchase download. View the default licensing terms applied to your listings.",
dataPrivacyTip2: "BThis licence is issued by Code-Create and is legally binding upon purchase. It certifies the buyer's right to use the purchased design asset in accordance with the terms below.",
dataPrivacyTip3: "For custom licensing arrangements, <span onclick='{click}()'>contact us</span>.",
downloadToView: "Download to View",
stopSelling: "Stop Selling",
stopSellingTitle: "Deactivate seller account",
stopSellingTip: "Permanently deactivate your seller account. All listings and invoice records will be deleted. You may re-register as a seller in the future, but your previous sales data cannot be recovered.",
deactivate: "Deactivate",
newListing: "New Listing",
draftMessage: 'Product moved to drafts and stats reset.',
publishMessage: 'Item is now live on the Marketplace.',
ActiveListings: 'Active Listings',
Praka: 'Praka',
Drafts: 'Drafts',
Cancel: 'Cancel',
Delete: 'Delete',
DeleteConfirm: 'Delete this listing?',
DeleteDesc: 'Your listing and its details will be permanently removed.',
Edit: 'Edit',
Draft: 'Draft',
Publish: 'Publish',
sketchesSelected: 'Sketches selected',
Next: 'Next',
All: 'All',
SeriesDesign: 'Series Design',
SingleDesign: 'Single Design',
sketchs: 'sketches',
MyListings: 'My Listings',
MyListingsInfo: 'Active listings and unpublished inventory',
SelectCollection: 'Select Collection',
SelectSketch: 'Select Sketch',
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: {
applySellerTitle: 'Apply to Become a Seller',
applySellerDesc: 'Join the Stylish Parade and start selling your design work',
formTitle: 'Brand Information',
formTip: 'Share a few details to set up your seller profile',
termsTitle: 'Seller Terms',
termsTip: 'Please carefully read and agree to the following terms',
agreementTitle: 'AiDA Seller Agreement',
agreementTip: 'Please read and agree to the following agreement',
agreementItem1: "Provide accurate and truthful personal and store information",
agreementItem2: "Only sell original designs or content with proper licensing",
agreementItem3: "Maintain high quality standards for all products",
agreementItem4: "Respond to customer inquiries within 48 hours",
agreementItem5: "Ship orders within promised timeframes",
agreementItem6: "Comply with AiDA's terms of service and community guidelines",
agreementItem7: "Pay applicable platform fees and transaction charges",
agreementItem8: "Handle customer disputes professionally and fairly",
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",
applicationSubmitted: "Application Submitted",
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_tip: "Fill out the seller information form and agree to our terms",
auditStatus2_title: "Step 2: Review & Verification",
auditStatus2_tip: "Our team will review your application (typically 1-3 business days)",
auditStatus3_title: "Step 3: Start Selling",
auditStatus3_tip: "Once approved, access your seller dashboard and start listing products",
backToHomepage: "Back to Homepage",
}
}

View File

@@ -1,8 +1,63 @@
import { createRouter, createWebHistory, RouteRecordRaw, createWebHashHistory } from "vue-router"
import type { RouteLocationNormalizedLoaded, RouteLocationRaw } from "vue-router"
import store from "@/store"
import { Https } from "@/tool/https"
import { getCookie, setCookie } from "@/tool/cookie"
type SellerRouteMetaValue<T> = T | ((route: RouteLocationNormalizedLoaded) => T)
type SellerBreadcrumbItem = {
title?: SellerRouteMetaValue<string>
titleKey?: SellerRouteMetaValue<string>
to?: SellerRouteMetaValue<RouteLocationRaw | undefined>
}
type SellerBreadcrumbList = SellerRouteMetaValue<SellerBreadcrumbItem[]>
const myListingsBreadcrumb: SellerBreadcrumbItem = {
titleKey: "Seller.MyListings",
to: { name: "myListingsIndex" }
}
const selectCollectionBreadcrumb: SellerBreadcrumbItem = {
titleKey: "Seller.SelectCollection",
to: { name: "myListingsSelect" }
}
const selectSketchBreadcrumb: SellerBreadcrumbItem = {
titleKey: "Seller.SelectSketch",
to: (route) => {
const collectionId =
route.params.collectionId ||
(typeof history !== "undefined" ? history.state?.collectionId : "")
return collectionId
? { name: "myListingsSelectItem", params: { collectionId } }
: undefined
}
}
const editListingBreadcrumb: SellerBreadcrumbItem = {
titleKey: "Seller.EditListingDetails"
}
const statusBreadcrumb: SellerBreadcrumbItem = {
titleKey: (route) =>
route.params.status === "publish"
? "SellerListEdit.listingLive"
: "SellerListEdit.draftSaved"
}
const createListingBreadcrumbs: SellerBreadcrumbItem[] = [
myListingsBreadcrumb,
selectCollectionBreadcrumb,
selectSketchBreadcrumb,
editListingBreadcrumb
]
const isEditingListingFromList = () =>
typeof history !== "undefined" && history.state?.type === "edit"
const editListingBreadcrumbs: SellerBreadcrumbList = () =>
isEditingListingFromList()
? [myListingsBreadcrumb, editListingBreadcrumb]
: createListingBreadcrumbs
const listingStatusBreadcrumbs: SellerBreadcrumbList = () =>
isEditingListingFromList()
? [myListingsBreadcrumb, editListingBreadcrumb, statusBreadcrumb]
: [...createListingBreadcrumbs, statusBreadcrumb]
const routes: Array<RouteRecordRaw> = [
{
path: "/",
@@ -167,7 +222,11 @@ const routes: Array<RouteRecordRaw> = [
{
path: "becomeSeller",
name: "becomeSeller",
meta: { enter: "all" },
meta: {
enter: "all",
sellerHeaderTitle: "ApplySeller.applySellerTitle",
sellerHeaderTip: "ApplySeller.applySellerDesc"
},
component: () => import("@/views/SellerDashboard/BecomeSeller/index.vue")
},
{
@@ -176,6 +235,11 @@ const routes: Array<RouteRecordRaw> = [
meta: { enter: "all" },
component: () => import("@/views/SellerDashboard/index.vue"),
children: [
{
path: "",
meta: { enter: "all" },
redirect: "/home/seller/brandProfile"
},
{
path: "brandProfile",
name: "brandProfile",
@@ -185,7 +249,7 @@ const routes: Array<RouteRecordRaw> = [
{
path: "myListings",
name: "myListings",
meta: { enter: "all" },
meta: { enter: "all", sellerBreadcrumb: myListingsBreadcrumb },
children: [
{
path: "",
@@ -196,35 +260,63 @@ const routes: Array<RouteRecordRaw> = [
{
path: "index",
name: "myListingsIndex",
meta: { enter: "all" },
meta: {
enter: "all",
sellerHeaderTitleKey: "Seller.MyListings",
sellerHeaderTipKey: "Seller.MyListingsInfo",
sellerBreadcrumbs: [myListingsBreadcrumb]
},
component: () =>
import("@/views/SellerDashboard/MyListings/main/index.vue")
},
{
path: "select",
name: "myListingsSelect",
meta: { enter: "all" },
meta: {
enter: "all",
sellerHeaderTitleKey: "Seller.SelectCollection",
sellerBreadcrumbs: [
myListingsBreadcrumb,
selectCollectionBreadcrumb
]
},
component: () =>
import("@/views/SellerDashboard/MyListings/createSelect/index.vue")
},
{
path: "select/:collectionId",
name: "myListingsSelectItem",
meta: { enter: "all" },
meta: {
enter: "all",
sellerHeaderTitleKey: "Seller.SelectSketch",
sellerBreadcrumbs: [
myListingsBreadcrumb,
selectCollectionBreadcrumb,
selectSketchBreadcrumb
]
},
component: () =>
import("@/views/SellerDashboard/MyListings/createSelectItem/index.vue")
},
{
path: "edit",
name: "EditDetail",
meta: { enter: "all" },
meta: {
enter: "all",
sellerHeaderTitleKey: "Seller.EditListingDetails",
sellerBreadcrumbs: editListingBreadcrumbs
},
component: () =>
import("@/views/SellerDashboard/MyListings/EditDetail/index.vue")
},
{
path:'edit/status/:status',
name:'Status',
meta:{enter:'all'},
path: "edit/status/:status",
name: "Status",
meta: {
enter: "all",
sellerHeaderTitleKey: "Seller.EditListingDetails",
sellerBreadcrumbs: listingStatusBreadcrumbs
},
component: () =>
import("@/views/SellerDashboard/MyListings/EditDetail/Status.vue")
}

View File

@@ -12,12 +12,12 @@ interface DesignerInfo {
ownerName: string,
email: string,
mobile: string,
socialLinks: string,
socialLinks: string[] | string,
description: string,
}
interface Seller {
isSeller: boolean,
applyStatus: number,
applyStatus: number | null,
designerInfo: DesignerInfo,
}
@@ -52,8 +52,24 @@ const seller: Module<Seller, RootState> = {
...state.designerInfo,
...value,
}
if (value.socialLinks) {
if (typeof value.socialLinks === "string") {
state.designerInfo.socialLinks = JSON.parse(value.socialLinks)
} else if (Array.isArray(value.socialLinks)) {
state.designerInfo.socialLinks = value.socialLinks
}
},
clear_state(state: Seller) {
state.isSeller = false
state.applyStatus = null
state.designerInfo = {
shopName: "--",
avatar: "",
brandBanner: "",
ownerName: "--",
email: "--",
mobile: "--",
socialLinks: ["--"],
description: "--"
}
},
},

View File

@@ -470,6 +470,7 @@ export const Https = {
checkSellerDesigner: '/seller/designer/check', // 检查卖家是否为设计师
getSellerApplyStatus: '/seller/designer/apply/status', // 获取卖家申请状态
submitSellerApply: '/seller/designer/apply', // 提交卖家申请
deleteSellerDesigner: '/seller/designer/delete', // 删除设计师
getDesignerInfo: '/seller/designer/info', // 获取设计师信息
updateDesignerInfo: '/seller/designer/update', // 更新设计师信息
getSellerOrderSummary: '/seller/order/summary', // 获取卖家订单数据总览

View File

@@ -318,7 +318,7 @@
</div>
</div>
<div class="homeMain_user">
<div class="homeMain_user_icon" @click="openAccount">
<div class="homeMain_user_icon">
<img :src="userDetail.avatar" alt="" />
</div>
<div class="homeMain_user_detail">
@@ -373,6 +373,10 @@
<i class="fi fi-rs-notebook"></i>
<span class="select_item_des">{{ $t('Header.ViewOrders') }}</span>
</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">
<span class="icon"><svg-icon name="seller-sellerIndex" /></span>
<span class="select_item_des">{{ $t('Header.BecomeSeller') }}</span>

View File

@@ -1,9 +1,6 @@
<template>
<div class="become-seller">
<seller-header
title="Apply to Become a Seller"
tip="Join the Stylish Parade and start selling your design work"
/>
<seller-header />
<div class="content">
<seller-apply v-if="applyStatus === null" @submit="onSubmit" />
<seller-review v-else />

View File

@@ -2,56 +2,58 @@
<div class="seller-apply">
<div class="session">
<div class="content mini-scrollbar">
<div class="title">Brand Information</div>
<div class="tip">Share a few details to set up your seller profile</div>
<div class="title">{{ $t("ApplySeller.formTitle") }}</div>
<div class="tip">{{ $t("ApplySeller.formTip") }}</div>
<div class="form">
<a-form :model="formData" :rules="formRules" layout="vertical" ref="formRef">
<a-form-item label="Store Name" name="storeName">
<a-form-item :label="$t('Seller.storeName')" name="storeName">
<a-input
v-model:value="formData.storeName"
placeholder="Enter the store name"
:placeholder="$t('Seller.storeNameDesc')"
:maxlength="80"
/>
<span class="tip-length">{{ formData.storeName.length }}/80</span>
</a-form-item>
<a-form-item label="Owners Full Name" name="fullName">
<a-form-item :label="$t('Seller.ownerName')" name="fullName">
<a-input
v-model:value="formData.fullName"
placeholder="Enter store owner's full name"
:placeholder="$t('Seller.ownerNameDesc')"
/>
</a-form-item>
<div class="form-group">
<a-form-item label="Email" name="email">
<a-form-item :label="$t('Seller.email')" name="email">
<a-input
type="email"
v-model:value="formData.email"
placeholder="Enter email"
:placeholder="$t('Seller.emailDesc')"
/>
</a-form-item>
<a-form-item label="Phone Number" name="phoneNumber">
<a-form-item :label="$t('Seller.mobile')" name="mobile">
<a-input
type="tel"
v-model:value="formData.phoneNumber"
placeholder="Enter phone number"
v-model:value="formData.mobile"
:placeholder="$t('Seller.mobileDesc')"
/>
</a-form-item>
</div>
<a-form-item label="Store Description" name="description">
<a-form-item :label="$t('Seller.storeDescription')" name="description">
<a-textarea
v-model:value="formData.description"
placeholder="Briefly describe your design style and store features..."
:placeholder="$t('Seller.storeDescriptionDesc')"
:maxlength="500"
/>
<span class="tip-length">{{ formData.description.length }}/500</span>
</a-form-item>
<a-form-item label="Portfoilo/Social Media Links">
<a-form-item :label="$t('Seller.links')">
<a-input
placeholder="https://"
v-for="(v, i) in formData.links"
:key="i"
v-model:value="formData.links[i]"
>
<template #prefix>Link {{ i + 1 }}</template>
<template #prefix>{{
$t("Seller.link", { index: i + 1 })
}}</template>
</a-input>
<a-input
placeholder="https://"
@@ -71,33 +73,25 @@
</div>
<div class="session">
<div class="content">
<div class="title">Brand Information</div>
<div class="tip">Share a few details to set up your seller profile</div>
<div class="title">{{ $t("ApplySeller.termsTitle") }}</div>
<div class="tip">{{ $t("ApplySeller.termsTip") }}</div>
<div class="agreement">
<div class="title">AiDA Seller Agreement</div>
<div class="tip">
By checking the box below, you agree to comply with the following terms:
</div>
<div class="title">{{ $t("ApplySeller.agreementTitle") }}</div>
<div class="tip">{{ $t("ApplySeller.agreementTip") }}</div>
<ul>
<li>Provide accurate and truthful personal and store information</li>
<li>Only sell original designs or content with proper licensing</li>
<li>Maintain high quality standards for all products</li>
<li>Respond to customer inquiries within 48 hours</li>
<li>Ship orders within promised timeframes</li>
<li>Comply with AiDA's terms of service and community guidelines</li>
<li>Pay applicable platform fees and transaction charges</li>
<li>Handle customer disputes professionally and fairly</li>
<li v-for="(v, i) in 8" :key="i">
{{ $t(`ApplySeller.agreementItem${i + 1}`) }}
</li>
</ul>
</div>
<a-checkbox class="agree-agreement" v-model:checked="isAgreement">
I have read and agree to the Seller Agreement, understanding my responsibilities
and obligations as a seller on the AiDA platform.
{{ $t("ApplySeller.agreementAgreement") }}
</a-checkbox>
</div>
<div class="btns">
<button class="cancel" @click="onCancel">Cancel</button>
<button class="cancel" @click="onCancel">{{ $t("Seller.cancel") }}</button>
<button class="submit" :disabled="!isAgreement" @click="onSubmit">
Submit Application
{{ $t("ApplySeller.submitApplication") }}
</button>
</div>
</div>
@@ -106,24 +100,26 @@
<script setup>
import { ref, reactive } from "vue"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
import { useRoute, useRouter } from "vue-router"
const route = useRoute()
const router = useRouter()
import { Https } from "@/tool/https"
const emit = defineEmits(["submit"])
const formRules = {
storeName: [{ required: true, message: "Enter the store name" }],
fullName: [{ required: true, message: "Enter store owner's full name" }],
email: [{ required: true, message: "Enter email" }],
phoneNumber: [{ required: true, message: "Enter phone number" }],
description: [{ required: true, message: "Enter store description" }]
storeName: [{ required: true, message: t("Seller.storeNameDesc") }],
fullName: [{ required: true, message: t("Seller.ownerNameDesc") }],
email: [{ required: true, message: t("Seller.emailDesc") }],
mobile: [{ required: true, message: t("Seller.mobileDesc") }],
description: [{ required: true, message: t("Seller.storeDescriptionErr") }]
}
const formRef = ref(null)
const formData = reactive({
storeName: "",
fullName: "",
email: "",
phoneNumber: "",
mobile: "",
description: "",
links: ["", ""]
})
@@ -140,7 +136,6 @@
formRef.value
.validate()
.then(() => {
console.log(formData)
const data = {
// userId: 0,
shopName: formData.storeName,
@@ -148,7 +143,7 @@
// brandBanner: "",
ownerName: formData.fullName,
email: formData.email,
mobile: formData.phoneNumber,
mobile: formData.mobile,
description: formData.description,
socialLinks: JSON.stringify(formData.links.filter((v) => v))
}

View File

@@ -1,11 +1,11 @@
<template>
<div class="seller-review">
<img class="success" src="@/assets/images/seller/success-1.png" />
<div class="title">Application Submitted</div>
<div class="tip">
Our team will review your application and get back to you within 13 business days.
You'll receive a notification in your email once a decision has been made.
</div>
<div class="title">{{ $t("ApplySeller.applicationSubmitted") }}</div>
<div
class="tip"
v-html="$t('ApplySeller.applicationSubmittedTip', { click: 'onPersonalCenter' })"
></div>
<div class="step-list">
<div v-for="v in list" :key="v.title" class="step-item">
<img v-show="!v.active" src="@/assets/images/seller/success-0.png" />
@@ -16,7 +16,10 @@
</div>
</div>
</div>
<button class="home-btn" @click="onBackToHome">Back to Homepage</button>
<button class="home-btn" @click="onBackToHome">
{{ $t("ApplySeller.backToHomepage") }}
</button>
<div class="tip">ID: {{ userId }}</div>
</div>
</template>
@@ -25,30 +28,37 @@
import { useRoute, useRouter } from "vue-router"
import { useStore } from "vuex"
import { ApplyStatus } from "@/store/seller/index.d"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const store = useStore()
const userId = computed(() => store.state.UserHabit.userDetail.userId)
const applyStatus = computed(() => store.state.seller.applyStatus)
const list = computed(() => [
{
title: "Step 1: Submit Application",
tip: "Fill out the seller information form and agree to our terms",
title: t("ApplySeller.auditStatus1_title"),
tip: t("ApplySeller.auditStatus1_tip"),
active: [ApplyStatus.Pending, ApplyStatus.Approved].includes(applyStatus.value)
},
{
title: "Step 2: Review & Verification",
tip: "Our team will review your application (typically 1-3 business days)",
title: t("ApplySeller.auditStatus2_title"),
tip: t("ApplySeller.auditStatus2_tip"),
active: applyStatus.value === ApplyStatus.Approved
},
{
title: "Step 3: Start Selling",
tip: "Once approved, access your seller dashboard and start listing products ",
title: t("ApplySeller.auditStatus3_title"),
tip: t("ApplySeller.auditStatus3_tip"),
active: applyStatus.value === ApplyStatus.Approved
}
])
const onBackToHome = () => {
router.push({ name: "home" })
}
window.onPersonalCenter = () => {
router.push("/home/account")
}
</script>
<style scoped lang="less">
.seller-review {
@@ -73,11 +83,15 @@
color: #000;
}
> .tip {
font-family: pingfang_medium;
font-family: pingfang_regular;
font-size: 1.8rem;
line-height: 170%;
text-align: center;
color: #585858;
&:deep(span) {
color: #585858;
font-family: pingfang_heavy;
}
}
> .step-list {
margin: 2.6rem 0;

View File

@@ -2,10 +2,10 @@
<div class="brand-info">
<a-form :model="formData" :rules="isEdit ? formRules : {}" layout="vertical" ref="formRef">
<div class="form-group">
<a-form-item label="Store Name" name="shopName">
<a-form-item :label="$t('Seller.storeName')" name="shopName">
<a-input
v-model:value="formData.shopName"
placeholder="Enter the store name"
:placeholder="$t('Seller.storeNameDesc')"
:maxlength="80"
:readonly="!isEdit"
/>
@@ -13,34 +13,34 @@
>{{ formData.shopName.length }}/80</span
>
</a-form-item>
<a-form-item label="Owners Full Name" name="ownerName">
<a-form-item :label="$t('Seller.ownerName')" name="ownerName">
<a-input
v-model:value="formData.ownerName"
placeholder="Enter store owner's full name"
:placeholder="$t('Seller.ownerNameDesc')"
:readonly="!isEdit"
/>
</a-form-item>
</div>
<div class="form-group">
<a-form-item label="Email" name="email">
<a-form-item :label="$t('Seller.email')" name="email">
<a-input
type="email"
v-model:value="formData.email"
placeholder="Enter email"
:placeholder="$t('Seller.emailDesc')"
:readonly="!isEdit"
/>
</a-form-item>
<a-form-item label="Phone Number" name="mobile">
<a-form-item :label="$t('Seller.mobile')" name="mobile">
<a-input
type="tel"
v-model:value="formData.mobile"
placeholder="Enter phone number"
:placeholder="$t('Seller.mobileDesc')"
:readonly="!isEdit"
/>
</a-form-item>
</div>
<div class="form-group">
<a-form-item label="Portfoilo/Social Media Links">
<a-form-item :label="$t('Seller.links')">
<a-input
placeholder="https://"
v-for="(v, i) in formData.socialLinks"
@@ -48,7 +48,7 @@
v-model:value="formData.socialLinks[i]"
:readonly="!isEdit"
>
<template #prefix>Link {{ i + 1 }}</template>
<template #prefix>{{ $t("Seller.link", { index: i + 1 }) }}</template>
</a-input>
<a-input
placeholder="https://"
@@ -63,10 +63,10 @@
</template>
</a-input>
</a-form-item>
<a-form-item label="Store Description" name="description">
<a-form-item :label="$t('Seller.storeDescription')" name="description">
<a-textarea
v-model:value="formData.description"
placeholder="Briefly describe your design style and store features..."
:placeholder="$t('Seller.storeDescriptionDesc')"
:maxlength="500"
:readonly="!isEdit"
/>
@@ -83,6 +83,9 @@
import { ref, reactive, watch } from "vue"
import { useRoute, useRouter } from "vue-router"
import { useStore } from "vuex"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const store = useStore()
const designerInfo = computed(() => store.state.seller.designerInfo)
const route = useRoute()
@@ -94,11 +97,11 @@
}
})
const formRules = {
shopName: [{ required: true, message: "Enter the store name" }],
ownerName: [{ required: true, message: "Enter store owner's full name" }],
email: [{ required: true, message: "Enter email" }],
mobile: [{ required: true, message: "Enter phone number" }],
description: [{ required: true, message: "Enter store description" }]
shopName: [{ required: true, message: t("Seller.storeNameDesc") }],
ownerName: [{ required: true, message: t("Seller.ownerNameDesc") }],
email: [{ required: true, message: t("Seller.emailDesc") }],
mobile: [{ required: true, message: t("Seller.mobileDesc") }],
description: [{ required: true, message: t("Seller.storeDescriptionErr") }]
}
const formRef = ref(null)

View File

@@ -10,34 +10,35 @@
:closable="false"
wrapClassName="#app"
:keyboard="false"
:destroyOnClose="true"
>
<div class="image-clip-dialog-box">
<div class="header" :class="{ 'is-product': data.isProduct }">
<div class="title">{{ data.title }}</div>
<div class="right flex">
<div v-if="coverOrigin.length > 1" class="origin-container flex align-center">
<span>Crop from: </span>
<span>{{ $t("Seller.cropFrom") }}</span>
<div class="origin-select flex align-center">
<div
class="origin-item sketch"
:class="{ selected: currentOrigin === 'sketch' }"
@click="handleChangeOrigin('sketch')"
>
Sketch
{{ $t("Seller.sketch") }}
</div>
<div
class="origin-item product"
:class="{ selected: currentOrigin === 'mainProducImage' }"
:class="{ selected: currentOrigin === 'mainProductImage' }"
@click="handleChangeOrigin('mainProductImage')"
>
Main product image
{{ $t("Seller.mainProductImage") }}
</div>
</div>
</div>
<div class="submit" v-if="!data.isPreview" @click="onSubmit">
<svg-icon name="seller-dui" size="24" />
</div>
<button @click="onCancel">Cancel</button>
<button @click="onCancel">{{ $t("Seller.cancel") }}</button>
</div>
</div>
<div class="content" :class="{ 'is-product': data.isProduct }">
@@ -64,7 +65,7 @@
>
<div class="title">
<span class="icon"><svg-icon name="seller-preview" size="24" /></span>
<span class="label">Crop Preview</span>
<span class="label">{{ $t("Seller.cropPreview") }}</span>
</div>
<div class="preview-image">
<img :src="data.preview_url" />
@@ -79,253 +80,267 @@
</template>
<script setup>
import { ref, reactive, computed } from "vue"
import ImageClip from "./image-clip.vue"
import { ref, reactive, computed } from "vue"
import ImageClip from "./image-clip.vue"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const props = defineProps({
type: {
type: String,
default: ""
},
isProduct: {
type: Boolean,
default: false
}
})
const tips = computed(() => {
if (props.type === "cover") {
return "Align crown to top, mid-thigh to bottom for best results."
}
if (props.type === "mainProductImage") {
return "Align crown to top, foot base to bottom for best results."
}
if (props.type === "sketch") {
return "Align crown to top, foot base to bottom for best results."
}
if (props.type === "apparel") {
return "Trim whitespace and center your apparel sketch."
}
})
const data = reactive({
url: "",
title: "Crop Image",
preview_url: "",
ratio: [1, 1],
isPreview: true,
callback: null,
isProduct: false // 是否商品编辑
})
const currentOrigin = ref("sketch")
const coverOrigin = ref([])
const handleChangeOrigin = (type) => {
currentOrigin.value = type
data.url = coverOrigin.value.filter((el) => el.type === type)[0].url
}
const show = ref(false)
const open = (url, callback, options, origin) => {
if (!props.isProduct) {
if (!url || !callback) return
}
data.url = url
data.callback = callback
data.ratio = options.ratio || [1, 1]
data.isPreview = true
data.preview_url = ""
data.title = options.title || "Crop Image"
if (options) {
if (options.hasOwnProperty("isPreview")) data.isPreview = options.isPreview
data.isProduct = options.isProduct
}
if (origin?.length) {
coverOrigin.value = origin
data.url = origin[0].url
}
show.value = true
}
const onCancel = () => {
show.value = false
}
const imageClipRef = ref(null)
const onSubmit = () => {
imageClipRef.value.getCropBlob().then((blob) => {
if (data.callback) data.callback(blobToFile(blob, "image.png"))
onCancel()
const props = defineProps({
type: {
type: String,
default: ""
},
isProduct: {
type: Boolean,
default: false
}
})
const tips = computed(() => {
if (props.type === "cover") {
return t("Seller.imageClipCoverTip")
}
if (props.type === "mainProductImage") {
return t("Seller.imageClipMainProductImageTip")
}
if (props.type === "sketch") {
return t("Seller.imageClipSketchTip")
}
if (props.type === "apparel") {
return t("Seller.imageClipApparelTip")
}
})
const data = reactive({
url: "",
title: "Crop Image",
preview_url: "",
ratio: [1, 1],
isPreview: true,
callback: null,
isProduct: false // 是否商品编辑
})
const currentOrigin = ref("sketch")
const coverOrigin = ref([])
const handleChangeOrigin = (type) => {
const targetOrigin = coverOrigin.value.find((el) => el.type === type)
if (!targetOrigin) return
currentOrigin.value = type
data.url = targetOrigin.url
}
const show = ref(false)
const open = (url, callback, options, origin) => {
if (!props.isProduct) {
if (!url || !callback) return
}
coverOrigin.value = []
data.url = null
currentOrigin.value = "sketch"
data.url = url
data.callback = callback
data.ratio = options.ratio || [1, 1]
data.isPreview = true
data.preview_url = ""
data.title = options.title || "Crop Image"
if (options) {
if (options.hasOwnProperty("isPreview")) data.isPreview = options.isPreview
data.isProduct = options.isProduct
}
if (origin?.length) {
coverOrigin.value = origin
const defaultOrigin = origin.find((el) => el.type === options?.coverFrom) || origin[0]
currentOrigin.value = defaultOrigin.type
data.url = defaultOrigin.url
}
show.value = true
}
const onCancel = () => {
show.value = false
}
const imageClipRef = ref(null)
const onSubmit = () => {
imageClipRef.value.getCropBlob().then((blob) => {
if (data.callback) data.callback(blobToFile(blob, "image.png"), currentOrigin.value)
onCancel()
})
}
// 将blob转换为file对象
const blobToFile = (blob, fileName) => {
return new File([blob], fileName, { type: blob.type })
}
defineExpose({
open
})
}
// 将blob转换为file对象
const blobToFile = (blob, fileName) => {
return new File([blob], fileName, { type: blob.type })
}
defineExpose({
open
})
</script>
<style scoped lang="less">
.image-clip-dialog-box {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
.submit {
width: 4rem;
height: 4rem;
border-radius: 50%;
background: #262626;
color: #fff;
cursor: pointer;
}
> .header {
.image-clip-dialog-box {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
justify-content: space-between;
margin-bottom: 5rem;
&.is-product {
margin-bottom: 2.4rem;
flex-direction: column;
.submit {
width: 4rem;
height: 4rem;
border-radius: 50%;
background: #262626;
color: #fff;
cursor: pointer;
}
> .title {
font-family: pingfang_heavy;
font-size: 2.4rem;
color: #595959;
> .header {
display: flex;
justify-content: space-between;
margin-bottom: 5rem;
&.is-product {
margin-bottom: 2.4rem;
}
> .title {
font-family: pingfang_heavy;
font-size: 2.4rem;
color: #595959;
}
> .right {
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
> button {
width: 10rem;
height: 4.8rem;
border-radius: 4rem;
border: none;
background: #e4e5eb;
font-family: pingfang_heavy;
font-size: 1.6rem;
color: #000;
}
.origin-container {
font-weight: 400;
color: #000;
font-size: 1.4rem;
.origin-select {
margin-left: 1.2rem;
height: 4.8rem;
border: 1px solid #c7c7c7;
border-radius: 3rem;
column-gap: 1.2rem;
padding: 0.8rem;
.origin-item {
height: 3.2rem;
line-height: 3.2rem;
border-radius: 2rem;
cursor: pointer;
&.selected {
background-color: #000;
color: #fff;
}
&.sketch {
padding: 0 1.9rem;
}
&.product {
padding: 0 2.5rem;
}
}
}
}
}
}
> .right {
> .content {
flex: 1;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
> button {
width: 10rem;
height: 4.8rem;
border-radius: 4rem;
border: none;
background: #e4e5eb;
font-family: pingfang_heavy;
font-size: 1.6rem;
color: #000;
}
.origin-container {
font-weight: 400;
color: #000;
font-size: 1.4rem;
.origin-select {
margin-left: 1.2rem;
height: 4.8rem;
border: 1px solid #c7c7c7;
border-radius: 3rem;
column-gap: 1.2rem;
padding: 0.8rem;
.origin-item {
height: 3.2rem;
line-height: 3.2rem;
border-radius: 2rem;
&.selected {
background-color: #000;
color: #fff;
}
&.sketch {
padding: 0 1.9rem;
}
&.product {
padding: 0 2.5rem;
}
}
}
}
}
}
> .content {
flex: 1;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
.crop-wrapper {
width: 100%;
.tips {
text-align: center;
color: #585858;
font-size: 1.4rem;
font-weight: 400;
}
}
&.is-product {
column-gap: 18.6rem;
.crop-wrapper {
width: initial;
}
}
> .image-clip {
flex: 1;
&.is-product {
width: initial;
flex: none;
}
}
> .preview {
margin-left: 6rem;
width: 28rem;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 2.4rem;
min-height: 0;
> .title {
display: flex;
align-items: center;
justify-content: center;
gap: 1.2rem;
> .label {
font-family: pingfang_heavy;
font-size: 1.6rem;
width: 100%;
padding-top: 1.2rem;
.tips {
text-align: center;
color: #585858;
font-size: 1.4rem;
font-weight: 400;
}
}
> .preview-image {
width: 100%;
&.is-product {
column-gap: 18.6rem;
.crop-wrapper {
width: initial;
}
}
> .image-clip {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
}
> .preview-image > img {
width: 100%;
height: auto;
max-height: 100%;
}
> .submit {
margin-top: auto;
flex-shrink: 0;
}
&.is-product {
margin-left: 0;
> .preview-image > img {
width: 20.8rem;
height: 36.7rem;
box-shadow: 4px 4px 16px 0px #0000000f;
border: 1px solid #ededed;
&.is-product {
width: initial;
flex: none;
}
&.is-cover {
> .preview-image > img {
width: 29.7rem;
height: 37.5rem;
}
> .preview {
margin-left: 6rem;
width: 28rem;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 2.4rem;
min-height: 0;
> .title {
display: flex;
align-items: center;
justify-content: center;
gap: 1.2rem;
> .label {
font-family: pingfang_heavy;
font-size: 1.6rem;
}
}
&.is-apparel {
> .preview-image {
width: 100%;
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
}
> .preview-image > img {
width: 100%;
height: auto;
max-height: 100%;
}
> .submit {
margin-top: auto;
flex-shrink: 0;
}
&.is-product {
margin-left: 0;
> .preview-image > img {
width: 100%;
height: auto;
max-height: 100%;
width: 20.8rem;
height: 36.7rem;
box-shadow: 4px 4px 16px 0px #0000000f;
border: 1px solid #ededed;
}
&.is-cover {
> .preview-image > img {
width: 29.7rem;
height: 37.5rem;
}
}
&.is-apparel {
> .preview-image > img {
width: 100%;
height: auto;
max-height: 100%;
}
}
}
}
}
}
}
</style>

View File

@@ -11,6 +11,7 @@
movable
@realTime="onChange"
outputType="png"
:full="true"
></VueCropper>
</div>
<div class="clip_opterate">
@@ -95,24 +96,26 @@
return label
}
const centerLabelTop = "8px"
const cropLabelMap = {
cover: [
{ text: "crown", top: "2.67%", className: "label-h" },
{ text: "hip line", top: "63.47%", className: "label-h" },
{ text: "mid-thigh", top: "92.8%", className: "label-h" },
{ text: "center", top: "0", className: "label-v" }
{ text: "center", top: centerLabelTop, className: "label-v" }
],
mainProductImage: [
{ text: "crown", top: "2.67%", className: "label-h" },
{ text: "footbase", top: "97.6%", className: "label-h" },
{ text: "center", top: "0", className: "label-v" }
{ text: "center", top: centerLabelTop, className: "label-v" }
],
sketch: [
{ text: "crown", top: "2.67%", className: "label-h" },
{ text: "footbase", top: "97.6%", className: "label-h" },
{ text: "center", top: "0", className: "label-v" }
{ text: "center", top: centerLabelTop, className: "label-v" }
],
apparel: [{ text: "center", top: "0", className: "label-v" }]
apparel: [{ text: "center", top: centerLabelTop, className: "label-v" }]
}
const injectCropLabel = () => {
@@ -287,11 +290,16 @@
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
border: 1px solid rgba(75, 165, 255, 0.85);
pointer-events: none;
z-index: 9; /* 位于图片之上,但在控制点之下 */
background-image: none;
background-repeat: no-repeat;
}
:deep(.vue-cropper .crop-point) {
z-index: 10;
}
}
&[data-crop-type="cover"] {

View File

@@ -5,9 +5,9 @@
<img v-if="banner" :src="banner" />
<div v-else class="null">
<span class="icon"><svg-icon name="seller-picture" size="60" /></span>
<span class="tip">Your brand banner has not been set up yet.</span>
<span class="tip">{{ $t("Seller.brandProfileBgNullTip") }}</span>
</div>
<button @click="onChangeBanner">Change Brand Banner</button>
<button @click="onChangeBanner">{{ $t("Seller.changeBrandBanner") }}</button>
</div>
<!-- 头像 -->
<div class="avatar">
@@ -25,14 +25,14 @@
<div class="and-profile-footer">
<template v-if="isEdit">
<div class="btns">
<button class="cancel" @click="onCancel()">Cancel</button>
<button class="submit" @click="onSubmit()">Save Change</button>
<button class="cancel" @click="onCancel()">{{ $t("Seller.cancel") }}</button>
<button class="submit" @click="onSubmit()">{{ $t("Seller.saveChange") }}</button>
</div>
<p class="tip">Changes will be reflected on your Stylish Parade brand page.</p>
<p class="tip">{{ $t("Seller.brandProfileEditTip") }}</p>
</template>
<template v-else>
<div class="btns">
<button class="edit" @click="onEdit">Edit</button>
<button class="edit" @click="onEdit">{{ $t("Seller.edit") }}</button>
</div>
<p class="tip">&nbsp;</p>
</template>
@@ -46,6 +46,8 @@
import BrandInfo from "./brand-info.vue"
import ImageClipDialog from "./image-clip-dialog.vue"
import { useStore } from "vuex"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const store = useStore()
store.dispatch("seller/get_designerInfo")
const designerInfo = computed(() => store.state.seller.designerInfo)
@@ -90,7 +92,7 @@
onSubmit({ brandBanner: res })
store.commit("set_loading", false)
},
{ ratio: [40, 7], isPreview: false, title: "Crop Brand Banner" }
{ ratio: [40, 7], isPreview: false, title: t("Seller.cropBrandBanner") }
)
})
}
@@ -104,7 +106,7 @@
onSubmit({ avatar: res })
store.commit("set_loading", false)
},
{ ratio: [1, 1], isPreview: true, title: "Crop Avatar" }
{ ratio: [1, 1], isPreview: true, title: t("Seller.cropAvatar") }
)
})
}

View File

@@ -2,14 +2,6 @@
<div class="status-wrapper flex flex-col 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' },
{ title: $t(title), name: 'Status' }
]"
/>
<div class="status-container flex flex-col flex-1 flex-center">
<img src="@/assets/images/seller/success-0.png" class="icon" alt="" />

View File

@@ -30,25 +30,67 @@ This directory owns the seller listing edit/create detail page.
Detail API images are mapped by `category`:
- `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`
- `mainProductImage` -> `currentListing.mainProductImage`
- `main_product` or `product` -> `currentListing.prodImageList`
- `mainProductImage` or `main_product` -> `currentListing.mainProductImage`
- `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`
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
- 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`.
- 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`.
- 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
- `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.
- 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

View File

@@ -1,20 +1,14 @@
import { Https } from "@/tool/https"
import type { ListingImageCategory, SketchDetailResponse } from "./types"
// 编辑时根据ID获取信息
export const fetchListingDetailById = (id) => {
return Https.axiosGet("/seller/listing/detail", { params: { id } })
}
interface SketchIDs {
designItemIds: Array
}
interface DetailReturns {
clothes: string[]
designItemId: number
toProductImageUrls: string[]
}
type SketchIDs = Array<number | string>
// 获取designItemId对应的产品图
export const fetchSketchDetail = (data: SketchIDs): Array<DetailReturns> => {
export const fetchSketchDetail = (data: SketchIDs): Promise<SketchDetailResponse[]> => {
let params = "?"
data.forEach((id, index) => {
if (index === data.length - 1) {
@@ -27,23 +21,26 @@ export const fetchSketchDetail = (data: SketchIDs): Array<DetailReturns> => {
}
interface ImageObj {
id: number // 图片id,有值会更新,没有会自动新增
category: "cover" | "main_product" | "product" | "sketch" | "apparel" // 图片类型
id?: number // 图片id,有值会更新,没有会自动新增
category: ListingImageCategory // 图片类型
imageUrl?: string | null
isSelected?: number
sortOrder?: number
}
interface DetailData {
id: number | string // 商品Id
title: string // 商品名
description: string // 商品描述
price: number // 价格
price: number | string // 价格
stock?: number // 库存
viewCount?: number // 浏览量
status: 0 | 1 | 2 // 0草稿 1发布 2删除
images: ImageObj[]
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)
}

View File

@@ -71,6 +71,8 @@
.img-src {
width: 100%;
height: 100%;
object-fit: contain;
}
.crop-tool {

View File

@@ -12,8 +12,10 @@
v-for="(item, index) in imageList"
:key="index"
class="product-image-item flex flex-center"
:class="{ selected: item.selected }"
:class="{ selected: item.selected, video: item.isVideo }"
@click="emit('select', index)"
@mouseenter="handleMouseEnter(index, item)"
@mouseleave="handleMouseLeave(index, item)"
>
<img
v-if="item.selected"
@@ -21,8 +23,11 @@
class="checked"
alt=""
/>
<img class="img-src" :src="item.url" alt="" />
<div v-if="item.selected && index === firstSelectedIndex" class="main-pic">
<img class="img-src" :src="getDisplayUrl(item, index)" alt="" />
<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
</div>
</div>
@@ -30,8 +35,8 @@
</div>
</template>
<script setup lang="ts">
import { computed } from "vue"
import type { ListingItem } from "../types"
import { computed, ref, watch } from "vue"
import type { ListingItem, ProductMediaItem } from "../types"
const props = defineProps<{
imageList: ListingItem["prodImageList"]
@@ -43,6 +48,70 @@
}>()
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>
<style lang="less" scoped>
@@ -130,7 +199,23 @@
}
.img-src {
width: 100%;
height: 100%;
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 {

View File

@@ -119,9 +119,9 @@
.sketch-img {
height: 100%;
object-fit: contain;
height: 100%;
&.sketch {
height: initial;
width: 100%;
}
}

View File

@@ -1,15 +1,6 @@
<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' }
]"
>
<seller-header class="edit-detail-header">
<template #right>
<div class="operate-menu flex">
<div class="menu-btn flex align-center save" @click="handleClickMenu('draft')">
@@ -51,7 +42,7 @@
@update:product-name="currentListing.productName = $event"
@update:price="currentListing.price = $event"
@update:desc="currentListing.desc = $event"
@update:gender="currentListing.gender = $event"
@update:gender="handleUpdateGender"
@update:category="currentListing.category = $event"
/>
<div class="page-control flex align-center" v-if="selectList.length > 1">
@@ -97,10 +88,16 @@
fetchUpdateListing
} from "./api"
import type {
CoverSourceType,
CropType,
ListingDetailImage,
ListingDetailResponse,
ListingImageCategory,
ListingItem,
ProductMediaItem,
RadioOption,
SketchDetailResponse,
SketchDetailVideo,
StatusType
} from "./types"
@@ -130,12 +127,15 @@
desc: "",
gender: "FEMALE",
category: null,
coverFrom: "sketch",
firstSelectedIndex: null,
prodImageList: [],
sketchList: []
})
const genderOptions = STORE.state.UserHabit?.sex.value || []
const genderOptions = computed(() => {
return STORE.state.UserHabit?.sex.value
})
const fallbackCategoryOptions: Record<string, RadioOption[]> = {
MALE: STORE.state.UserHabit?.MalePosition || [],
@@ -157,6 +157,13 @@
const currentListing = computed(() => selectList.value[currentIndex.value])
const handleUpdateGender = (gender: string) => {
if (currentListing.value.gender === gender) return
currentListing.value.gender = gender
currentListing.value.category = null
}
const previewImageMap = computed(() => ({
sketch: currentListing.value.sketch,
mainProductImage: currentListing.value.mainProductImage,
@@ -167,14 +174,50 @@
return [...images].sort((prev, next) => (prev.sortOrder ?? 0) - (next.sortOrder ?? 0))
}
const getImageSelected = (value: ListingDetailImage["isSelected"]) =>
value === true || value === 1 || value === "1"
const getImageSelected = (value: ListingDetailImage["isSelected"]) => {
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 gender = String(value || "").toUpperCase()
return gender === "MALE" || gender === "FEMALE" ? gender : "FEMALE"
}
const getListingDesignFor = (gender: ListingItem["gender"]): "male" | "female" =>
gender === "MALE" ? "male" : "female"
const normalizeDetailCategory = (
value: ListingDetailResponse["productCategory"]
): ListingItem["category"] => {
@@ -188,6 +231,17 @@
const createListingItemFromDetail = (detail: ListingDetailResponse): ListingItem => {
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.price =
@@ -197,6 +251,11 @@
listing.category = normalizeDetailCategory(detail.productCategory)
getSortedDetailImages(detail.images || []).forEach((image) => {
if (image.category === "cover_from") {
coverFromImageUrl = image.imageUrl || ""
return
}
const imageUrl = image.imageUrl || ""
if (!imageUrl) return
@@ -210,7 +269,7 @@
return
}
if (image.category === "mainProductImage") {
if (image.category === "mainProductImage" || image.category === "main_product") {
listing.mainProductImage = imageUrl
return
}
@@ -218,23 +277,50 @@
if (image.category === "product") {
listing.prodImageList.push({
url: imageUrl,
selected: getImageSelected(image.isSelected)
selected: getDetailImageSelected(image)
})
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") {
listing.sketchList.push({ url: imageUrl })
}
})
if (!listing.mainProductImage) {
listing.mainProductImage =
listing.prodImageList.find((item) => item.selected)?.url || ""
Array.from(videoGroupMap.values())
.sort((prev, next) => prev.sortOrder - next.sortOrder)
.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)
listing.firstSelectedIndex = selectedIndex === -1 ? null : selectedIndex
const mainProductIndex = listing.prodImageList.findIndex(
(item) => !item.isVideo && item.url === listing.mainProductImage
)
listing.firstSelectedIndex = mainProductIndex === -1 ? null : mainProductIndex
listing.productImage = listing.prodImageList.map((item) => item.url)
listing.apparelSketch = listing.sketchList
@@ -244,6 +330,26 @@
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 listing = currentListing.value
const target = prodImgList.value[index]
@@ -252,6 +358,10 @@
target.selected = willSelect
if (willSelect && listing.firstSelectedIndex === null) {
if (target.isVideo) {
message.warning(t("Seller.VideoWarning"))
return
}
listing.mainProductImage = target.url
listing.firstSelectedIndex = index
return
@@ -263,25 +373,33 @@
}
}
const cropType = ref("")
const handleClickCrop = (data: any, type: string, paramThree: any = []) => {
const getCoverOriginList = (item: ListingItem) => {
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)
// 处理来自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 = {
const origin = type === "cover" ? getCoverOriginList(currentItem) : []
const titleList: Record<CropType, string> = {
sketch: "Crop Sketch",
mainProductImage: "Crop Main Product Image",
cover: "Crop Cover",
@@ -291,17 +409,28 @@
cropType.value = type
imageClipDialogRef.value.open(
data,
(file) => {
(file: File, coverFrom?: CoverSourceType) => {
// console.log(file)
uploadFile(file).then((res) => {
if (type === "apparel" && typeof index !== "undefined") {
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 {
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
)
}
@@ -310,10 +439,9 @@
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.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") },
@@ -353,79 +481,153 @@
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 paramsList = []
selectList.value.forEach((item: ListingItem) => {
const params = {
const paramsList = selectList.value.map((item: ListingItem) => {
return {
id: itemId.value,
title: item.productName,
description: item.desc,
price: item.price,
status: type === "draft" ? 0 : 1,
images: [],
designFor: (item.gender || "FEMALE").toLowerCase(),
images: buildListingImages(item),
designFor: getListingDesignFor(item.gender),
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" } })
ROUTER.push({
name: "Status",
params: { status: "draft" },
state: {
type: history.state?.type,
collectionId: history.state?.collectionId
}
})
} else if (status === "publish") {
// publish logic
// console.log("Publishing...", currentListing.value)
ROUTER.push({ name: "Status", params: { status: "publish" } })
ROUTER.push({
name: "Status",
params: { status: "publish" },
state: {
type: history.state?.type,
collectionId: history.state?.collectionId
}
})
}
}
const handleFetchItemDetial = (list) => {
fetchSketchDetail(list).then((res) => {
fetchSketchDetail(list).then((res: SketchDetailResponse[]) => {
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
selectList.value[index].sketchList = (item.clothes || []).map((el) => ({ url: el }))
const imageItems = (item.toProductImageUrls || []).map((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]
})
})
}
@@ -442,7 +644,7 @@
onMounted(() => {
const data = history.state
if (data?.type === "edit") {
itemId.value = history.state?.id
itemId.value = history.state?.id
handleGetDetailById()
} else {
const designItemIds = history.state?.designItemIds || []

View File

@@ -7,6 +7,27 @@ export type RadioOption = {
export type TopImageType = "sketch" | "mainProductImage" | "cover"
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 = {
designItemId: number | string | null
@@ -20,18 +41,18 @@ export type ListingItem = {
desc: string
gender: string
category: string[] | null
coverFrom: CoverSourceType
firstSelectedIndex: number | null
prodImageList: Array<{
url: string
selected?: boolean
}>
prodImageList: ProductMediaItem[]
sketchList: Array<{ url: string | null }>
}
export type ListingDetailImage = {
category?: string | null
category?: ListingImageCategory | string | null
imageUrl?: string | null
isSelected?: boolean | number | string | null
isSeleted?: boolean | number | string | null
selected?: boolean | number | string | null
sortOrder?: number | null
}
@@ -45,4 +66,17 @@ export type ListingDetailResponse = {
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"

View File

@@ -68,7 +68,7 @@ defineExpose({getCreateList})
<div class="detail">
<div class="name">{{item.name}}</div>
<div class="bottom">
<div>{{item.userLikeGroupVO?.groupDetails?.length || 0}} sketchs</div>
<div>{{item.userLikeGroupVO?.groupDetails?.length || 0}} {{ $t('Seller.sketchs') }}</div>
<div>{{setPubDate(item.updateTime,t)}}</div>
</div>
</div>

View File

@@ -40,20 +40,13 @@ defineExpose({})
</script>
<template>
<div class="create-select">
<seller-header
title="Select Collection"
:breadcrumbs="[
{title:'My Listings', name:'myListingsIndex'},
{title:'Select Collection', name: 'myListingsSelect' }
]"
>
</seller-header>
<seller-header />
<div class="content">
<div class="title">
<div class="left">
<div :class="{active:!getCollectionListData.process?.[0]}" @click="setProcess('')">All</div>
<div :class="{active:getCollectionListData.process[0] == 'SERIES_DESIGN'}" @click="setProcess('SERIES_DESIGN')">Series Design</div>
<div :class="{active:getCollectionListData.process[0] == 'SINGLE_DESIGN'}" @click="setProcess('SINGLE_DESIGN')">Single Design</div>
<div :class="{active:!getCollectionListData.process?.[0]}" @click="setProcess('')">{{$t('Seller.All')}}</div>
<div :class="{active:getCollectionListData.process[0] == 'SERIES_DESIGN'}" @click="setProcess('SERIES_DESIGN')">{{$t('Seller.SeriesDesign')}}</div>
<div :class="{active:getCollectionListData.process[0] == 'SINGLE_DESIGN'}" @click="setProcess('SINGLE_DESIGN')">{{$t('Seller.SingleDesign')}}</div>
</div>
<div class="right">
<div class="search_input flex flex-align-center">
@@ -152,4 +145,4 @@ defineExpose({})
}
}
}
</style>
</style>

View File

@@ -6,7 +6,14 @@ import selectMenu from '@/component/modules/selectMenu.vue'
import { Https } from '@/tool/https'
import { useRoute } from 'vue-router'
import { useStore } from 'vuex'
import { useI18n } from 'vue-i18n'
import { message } from 'ant-design-vue';
// 定义组件名称
defineOptions({
name: 'myListingsSelectItem'
})
const { t } = useI18n()
//const props = defineProps({
//})
//const emit = defineEmits([
@@ -18,15 +25,15 @@ const route = useRoute()
const domSize = ref('Small')
const domSizeList = ref([
{
label:'Small',
label:t('Header.Small'),
value:'Small',
},
{
label:'Medium',
label:t('Header.Medium'),
value:'Medium',
},
{
label:'Large',
label:t('Header.Large'),
value:'Large',
},
])
@@ -39,6 +46,10 @@ const chooseItem = (item:any)=>{
if(chooseList.value.findIndex((i:any)=>i.designItemId == item.designItemId) != -1){
chooseList.value.splice(chooseList.value.findIndex((i:any)=>i.designItemId == item.designItemId),1)
}else{
if(chooseList.value.length >= 9){
message.info(t('Seller.selectSketchMaxNum'))
return
}
chooseList.value.push(item)
}
}
@@ -50,7 +61,8 @@ const next = ()=>{
path:'/home/seller/myListings/edit',
state: {
designItemIds,
type:'create'
type:'create',
collectionId: route.params.collectionId
}
})
}
@@ -140,8 +152,11 @@ onMounted(()=>{
// 开始监听
if(resizeObserver)resizeObserver.observe(listingsBoxRef.value)
})
chooseList.value = []
getCollectionDetail()
})
onActivated(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
@@ -149,21 +164,14 @@ const {} = toRefs(data);
</script>
<template>
<div class="create-select-item">
<seller-header
title="Select Collection"
:breadcrumbs="[
{title:'My Listings', name:'myListingsIndex'},
{title:'Select Collection', name: 'myListingsSelect' },
{title:'Select Sketch', name: 'myListingsSelectItem' }
]"
>
<seller-header>
<template #right>
<div class="header-right">
<div class="chooseNum">
{{ chooseList.length }} sketches selected
{{ chooseList.length }} / 9 {{ t('Seller.sketchesSelected') }}
</div>
<div class="button" @click="next">
<span>Next</span>
<span>{{ $t('Seller.Next') }}</span>
<div class="icon">
<i class="fi fi-rr-arrow-small-right"></i>
</div>
@@ -174,8 +182,8 @@ const {} = toRefs(data);
<div class="content" ref="listingsBoxRef">
<div class="title">
<div class="left">
<i class="fi fi-rs-comments"></i>
<span>Active Listings</span>
<i class="fi fi-rs-comments"></i>
<span>{{ $t('Seller.Praka') }}</span>
</div>
<div class="right">
<div class="generalModel_state">
@@ -314,6 +322,7 @@ const {} = toRefs(data);
margin: 0 auto;
overflow-y: auto;
align-content: flex-start;
min-width: 90%;
&::-webkit-scrollbar {
display: none;
}
@@ -368,7 +377,8 @@ const {} = toRefs(data);
}
> img{
height: 100%;
object-fit: cover;
width: 100%;
object-fit: contain;
}
&.active{
border: 1.5px solid #000;
@@ -383,4 +393,4 @@ const {} = toRefs(data);
}
}
}
</style>
</style>

View File

@@ -7,11 +7,13 @@ import deleteDrafts from './deleteDrafts.vue'
import { Https } from '@/tool/https'
import { message } from 'ant-design-vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
//const props = defineProps({
//})
//const emit = defineEmits([
//])
const { t:$t } = useI18n()
let data = reactive({
showDrafts: false,
})
@@ -50,15 +52,15 @@ const config = ref({
const domSize = ref('Small')
const domSizeList = ref([
{
label:'Small',
label:$t('Header.Small'),
value:'Small',
},
{
label:'Medium',
label:$t('Header.Medium'),
value:'Medium',
},
{
label:'Large',
label:$t('Header.Large'),
value:'Large',
},
])
@@ -155,7 +157,7 @@ const draftListing = async (item: any)=>{
list2.value.unshift(item)
list.value = list.value.filter((v: any)=>v.id != item.id)
})
message.success('Product moved to drafts and stats reset.')
message.success($t('Seller.draftMessage'))
}
const publishListing = async (item: any)=>{
@@ -163,7 +165,7 @@ const publishListing = async (item: any)=>{
list.value.unshift(item)
list2.value = list2.value.filter((v: any)=>v.id != item.id)
})
message.success('Item is now live on the Marketplace.')
message.success($t('Seller.publishMessage'))
}
const editListing = (item: any)=>{
@@ -237,7 +239,7 @@ const { showDrafts } = toRefs(data);
<div class="title">
<div class="left">
<i class="fi fi-rs-comments"></i>
<span>Active Listings</span>
<span>{{ $t('Seller.ActiveListings') }}</span>
</div>
<div class="right">
<div class="generalModel_state">
@@ -302,7 +304,7 @@ const { showDrafts } = toRefs(data);
<div class="title">
<div class="left">
<i class="fi fi-rs-comments"></i>
<span>Drafts</span>
<span>{{ $t('Seller.Drafts') }}</span>
</div>
</div>
<VueDraggable
@@ -398,6 +400,7 @@ const { showDrafts } = toRefs(data);
margin: 0 auto;
align-items: flex-start;
overflow: auto;
min-width: 90%;
&::-webkit-scrollbar {
display: none;
}

View File

@@ -36,15 +36,15 @@ const {} = toRefs(data);
<div class="maskBtn">
<div @click="$emit('editListing',item)">
<svgIcon name="seller-edit" :size="domSize == 'Small'?32:domSize == 'Medium'?40:48" />
<div>Edit</div>
<div>{{ $t('Seller.Edit') }}</div>
</div>
<div v-if="type == 'listings'" @click="$emit('draftListing',item)">
<svgIcon name="seller-draft" :size="domSize == 'Small'?32:domSize == 'Medium'?40:48" />
<div>Draft</div>
<div>{{ $t('Seller.Draft') }}</div>
</div>
<div v-else-if="type == 'drafts'" @click="$emit('publishListing',item)">
<svgIcon name="seller-share" :size="domSize == 'Small'?32:domSize == 'Medium'?40:48" />
<div>Publish</div>
<div>{{ $t('Seller.Publish') }}</div>
</div>
</div>
</div>
@@ -132,6 +132,7 @@ const {} = toRefs(data);
width: 100%;
height: 100%;
object-fit: cover;
background-color: #fff;
}
> .maskBtn{
position: absolute;

View File

@@ -21,6 +21,7 @@ const fun = ref(null)
let deleteDraftsRef = ref(null)
const open = (data:any,deleteFun)=>{
console.log(data)
item.value = data
fun.value = deleteFun
emit('update:visible', true)
@@ -74,22 +75,22 @@ const { showAgain } = toRefs(data);
<i class="fi fi-rr-trash"></i>
</div>
<div class="titleText">
<h1>Delete this listing?</h1>
<p>Your listing and its details will be permanently removed.</p>
<h1>{{ $t('Seller.DeleteConfirm') }}</h1>
<p>{{ $t('Seller.DeleteDesc') }}</p>
</div>
</div>
<div class="deleteContent">
<div class="img">
<img :src="item?.value?.cover" alt="">
<img :src="item?.cover" alt="">
</div>
<div class="detail">
<div class="name">{{ item?.value?.title }}</div>
<div class="price">HK${{ item?.value?.price }} · Draft</div>
<div class="name">{{ item?.title }}</div>
<div class="price">HK${{ item?.price }} · {{ $t('Seller.Drafts') }}</div>
</div>
</div>
<div class="btnBox">
<div class="btn" @click.stop="cleardata()">Cancel</div>
<div class="btn" @click.stop="deleteDrafts()">Delete</div>
<div class="btn" @click.stop="cleardata()">{{ $t('Seller.Cancel') }}</div>
<div class="btn" @click.stop="deleteDrafts()">{{ $t('Seller.Delete') }}</div>
</div>
</a-modal>
</div>

View File

@@ -41,13 +41,10 @@ const {} = toRefs(data);
</script>
<template>
<div class="myListings-seller">
<seller-header
title="My Listings"
tip="Active listings and unpublished inventory."
>
<seller-header :displayBack="false">
<template #right>
<div class="button" @click="newListing">
<span>New Listing</span>
<span>{{ $t('Seller.newListing') }}</span>
<div class="icon">
<i class="fi fi-br-plus"></i>
</div>
@@ -99,4 +96,4 @@ const {} = toRefs(data);
position: relative;
}
}
</style>
</style>

View File

@@ -11,8 +11,8 @@
</div>
<div class="filter-box">
<div class="left">
<div class="title">All Invoice</div>
<div class="tip">A summary of all completed transactions.</div>
<div class="title">{{ t("Seller.allInvoice") }}</div>
<div class="tip">{{ t("Seller.myOrdersTip") }}</div>
</div>
<div class="right">
<div class="input">
@@ -22,7 +22,7 @@
<input
type="text"
v-model="nameOrId"
placeholder="Search by item name or order ID"
:placeholder="t('Seller.myOrdersSearchPlaceholder')"
@keydown.enter.prevent="getList(true)"
/>
</div>
@@ -30,11 +30,11 @@
</div>
<div class="table">
<div class="header">
<div class="order-id">Order ID</div>
<div class="item">Item</div>
<div class="price">Price</div>
<div class="buyer-username">Buyer Username</div>
<div class="date">Date</div>
<div class="order-id">{{ t("Seller.orderId") }}</div>
<div class="item">{{ t("Seller.item") }}</div>
<div class="price">{{ t("Seller.price") }}</div>
<div class="buyer-username">{{ t("Seller.buyerUsername") }}</div>
<div class="date">{{ t("Seller.date") }}</div>
</div>
<div class="body">
<div class="item" v-for="v in list" :key="v.orderId">
@@ -79,6 +79,8 @@
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from "vue"
import { Https } from "@/tool/https"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const totals_obj = ref({
totalRevenue: "--",
totalPurchases: "--",
@@ -87,17 +89,17 @@
const totals = computed(() => [
{
icon: "seller-qiandaizi",
title: "Total Revenue",
title: t("Seller.totalRevenue"),
value: `HK$ ${totals_obj.value.totalRevenue}`
},
{
icon: "seller-gouwudai",
title: "Total Purchases",
title: t("Seller.totalPurchases"),
value: totals_obj.value.totalPurchases
},
{
icon: "seller-eye",
title: "Total Views",
title: t("Seller.totalViews"),
value: totals_obj.value.totalViews
}
])

View File

@@ -2,11 +2,11 @@
<div class="settings-index">
<div>
<div class="notification">
<div class="header">Notifications</div>
<div class="header">{{ $t("Seller.notifications") }}</div>
<div class="content">
<div class="left">
<div class="title">New order notification</div>
<div class="tip">Receive an inbox message when a new order is placed.</div>
<div class="title">{{ $t("Seller.notificationsTitle") }}</div>
<div class="tip">{{ $t("Seller.notificationsTip") }}</div>
</div>
<div class="right">
<a-switch v-model:checked="checked" />
@@ -14,29 +14,29 @@
</div>
</div>
<div class="payout">
<div class="header">Payout</div>
<div class="header">{{ $t("Seller.payout") }}</div>
<div class="content">
<div class="header">
<div class="title">Payment Providers</div>
<div class="tip">Select how you want to receive payments.</div>
<div class="title">{{ $t("Seller.payoutTitle") }}</div>
<div class="tip">{{ $t("Seller.payoutTip") }}</div>
</div>
<div class="pay-item" v-for="v in payList" :key="v.type">
<div class="left">
<img :src="v.icon" />
<div class="value">{{ v.value || "Unbound" }}</div>
<div class="value">{{ v.value || $t("Seller.unbound") }}</div>
</div>
<div class="right">
<button v-if="v.value" class="manage">Manage</button>
<button v-else class="bind-now">Bind Now</button>
<button v-if="v.value" class="manage">{{ $t("Seller.manage") }}</button>
<button v-else class="bind-now">{{ $t("Seller.bindNow") }}</button>
</div>
</div>
<div class="footer">
<div class="left">
<div class="title">Payment Currency</div>
<div class="tip">HKD - Hong Kong Dollar</div>
<div class="title">{{ $t("Seller.paymentCurrency") }}</div>
<div class="tip">{{ $t("Seller.HKD") }}</div>
</div>
<div class="right">
<button>Fixed</button>
<button>{{ $t("Seller.fixed") }}</button>
</div>
</div>
</div>
@@ -44,40 +44,32 @@
</div>
<div>
<div class="data-privacy">
<div class="header">Data & Privacy</div>
<div class="header">{{ $t("Seller.dataPrivacy") }}</div>
<div class="content">
<div class="title">Copyright licence</div>
<div class="tip">
A licence certificate is automatically included with every purchase
download. View the default licensing terms applied to your listings.
</div>
<div class="tip">
This licence is issued by Code-Create and is legally binding upon purchase.
It certifies the buyer's right to use the purchased design asset in
accordance with the terms below.
</div>
<div class="tip">
For custom licensing arrangements, <span>contact us</span>.
</div>
<div class="title">{{ $t("Seller.dataPrivacyTitle") }}</div>
<div class="tip">{{ $t("Seller.dataPrivacyTip1") }}</div>
<div class="tip">{{ $t("Seller.dataPrivacyTip2") }}</div>
<div
class="tip"
v-html="
$t('Seller.dataPrivacyTip3', { click: 'onSellerSettingsContactUs' })
"
></div>
<div class="btns">
<button>
<button @click="onDownloadDataPrivacy">
<span class="icon"><svg-icon name="seller-download" size="14" /></span>
<span class="label">Download to View</span>
<span class="label">{{ $t("Seller.downloadToView") }}</span>
</button>
</div>
</div>
</div>
<div class="stop">
<div class="header">Stop Selling</div>
<div class="header">{{ $t("Seller.stopSelling") }}</div>
<div class="content">
<div class="title">Deactivate seller account</div>
<div class="tip">
Permanently deactivate your seller account. All listings and invoice records
will be deleted. You may re-register as a seller in the future, but your
previous sales data cannot be recovered.
</div>
<div class="title">{{ $t("Seller.stopSellingTitle") }}</div>
<div class="tip">{{ $t("Seller.stopSellingTip") }}</div>
<div class="btns">
<button>Deactivate</button>
<button @click="onStopSelling">{{ $t("Seller.deactivate") }}</button>
</div>
</div>
</div>
@@ -87,10 +79,19 @@
<script setup>
import { ref } from "vue"
import { Https } from "@/tool/https"
import { Modal } from "ant-design-vue"
import paypal from "@/assets/images/seller/setting/paypal.png"
import stripe from "@/assets/images/seller/setting/stripe.png"
import alipayHk from "@/assets/images/seller/setting/alipay-hk.png"
import alipayChinese from "@/assets/images/seller/setting/alipay-chinese.png"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
import { useStore } from "vuex"
const store = useStore()
import { useRouter } from "vue-router"
const router = useRouter()
const checked = ref(true)
const payList = ref([
{
@@ -114,6 +115,33 @@
value: "123123"
}
])
window.onSellerSettingsContactUs = () => {
console.log("contact us")
}
const onDownloadDataPrivacy = () => {
console.log("download data privacy")
}
const onStopSelling = () => {
Modal.confirm({
title: t("Seller.stopSellingTitle"),
content: t("Seller.stopSellingTip"),
okText: t("Seller.confirm"),
cancelText: t("Seller.cancel"),
centered: true,
onOk() {
store.commit("set_loading", true)
Https.axiosDelete(Https.httpUrls.deleteSellerDesigner)
.then((res) => {
store.commit("set_loading", false)
store.commit("seller/clear_state")
router.push({ name: "home" })
})
.catch(() => {
store.commit("set_loading", false)
})
}
})
}
</script>
<style scoped lang="less">
.settings-index {
@@ -281,7 +309,7 @@
font-size: 1.4rem;
color: #999;
margin-bottom: 3rem;
> span {
&:deep(*) {
cursor: pointer;
text-decoration: underline;
color: #0080ed;

View File

@@ -1,5 +1,5 @@
<template>
<div class="seller-dashboard-index">
<div class="seller-dashboard-index" v-if="isSeller">
<div class="nav">
<div
v-for="v in list"
@@ -12,7 +12,11 @@
</div>
</div>
<router-view></router-view>
<router-view v-slot="{ Component }">
<keep-alive :include="cachedRoutes">
<component :is="Component" />
</keep-alive>
</router-view>
<toolTipBox v-model:visible="visible" @close="() => {}" ref="toolTipBoxRef"></toolTipBox>
</div>
</template>
@@ -24,7 +28,17 @@
import toolTipBox from "./toolTipBox.vue"
import myEvent from "@/tool/myEvents.js"
import { useStore } from "vuex"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const props = defineProps({
cachedRoutes: {
type: Array,
default: () => []
}
})
const store = useStore()
const isSeller = computed(() => store.state.seller.isSeller)
// store.dispatch("seller/get_designerInfo")
const route = useRoute()
const router = useRouter()
@@ -32,22 +46,22 @@
const list = ref([
{
icon: "seller-brandProfile",
layer: "Brand Profile",
layer: t("Seller.brandProfile"),
path: "/home/seller/brandProfile"
},
{
icon: "seller-myListings",
layer: "My Listings",
layer: t("Seller.myListings"),
path: "/home/seller/myListings"
},
{
icon: "seller-myOrders",
layer: "My Orders",
layer: t("Seller.myOrders"),
path: "/home/seller/myOrders"
},
{
icon: "seller-settings",
layer: "Settings",
layer: t("Seller.settings"),
path: "/home/seller/settings"
}
])

View File

@@ -1,27 +1,23 @@
<template>
<div class="seller-header">
<div class="back" @click="() => router.back()">
<div class="back" v-if="displayBack" @click="() => router.back()">
<svg-icon name="seller-back" size="24" />
</div>
<div class="content">
<span class="title" v-show="title">{{ title }}</span>
<span class="tip" v-show="tip">{{ tip }}</span>
<div class="breadcrumbs" v-show="breadcrumbs.length > 0">
<template v-for="(v, i) in breadcrumbs" :key="i">
<span class="title" v-show="displayTitle">{{ displayTitle }}</span>
<span class="tip" v-show="displayTip">{{ displayTip }}</span>
<div class="breadcrumbs" v-show="breadcrumbList.length > 1">
<template v-for="(v, i) in breadcrumbList" :key="`${v.title}-${i}`">
<span
class="title"
:class="{
last: i === breadcrumbs.length - 1
last: i === breadcrumbList.length - 1,
clickable: i < breadcrumbList.length - 1
}"
@click="
() => {
const index = -(breadcrumbs.length - i - 1)
if (index < 0) router.go(index)
}
"
>{{ v.title }}</span
@click="onBreadcrumbClick(v, i)"
>{{ $t(v.title) }}</span
>
<span class="icon" v-show="i < breadcrumbs.length - 1">
<span class="icon" v-show="i < breadcrumbList.length - 1">
<svg-icon name="seller-arrow_right_solid" size="10" />
</span>
</template>
@@ -33,25 +29,177 @@
</div>
</template>
<script setup>
import { ref, computed } from "vue"
<script setup lang="ts">
import { computed } from "vue"
import { useI18n } from "vue-i18n"
import { useRoute, useRouter } from "vue-router"
const props = defineProps({
title: {
type: String,
default: ""
},
tip: {
type: String,
default: ""
},
breadcrumbs: {
type: Array, // { title: string, name: string }
default: () => []
import type { RouteLocationNormalizedLoaded, RouteLocationRaw } from "vue-router"
type RouteMetaValue<T> = T | ((route: RouteLocationNormalizedLoaded) => T)
type SellerBreadcrumbSource =
| string
| {
title?: RouteMetaValue<string>
titleKey?: RouteMetaValue<string>
to?: RouteMetaValue<RouteLocationRaw | undefined>
name?: string
path?: string
}
type SellerBreadcrumbItem = {
title: string
to?: RouteLocationRaw
}
type SellerBreadcrumbsSource = RouteMetaValue<SellerBreadcrumbSource[]>
const props = withDefaults(
defineProps<{
title?: string
tip?: string
displayBack?: boolean
breadcrumbs?: SellerBreadcrumbSource[]
}>(),
{
title: "",
tip: "",
breadcrumbs: () => [],
displayBack: true
}
})
)
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const resolveMetaValue = <T,>(value?: RouteMetaValue<T>) => {
if (typeof value === "function") {
return (value as (route: RouteLocationNormalizedLoaded) => T)(route)
}
return value
}
const resolveRouteLocation = (to?: RouteLocationRaw) => {
if (!to || typeof to === "string") return to
const location = to as any
if (location.name) {
return {
...location,
params: {
...route.params,
...location.params
}
}
}
return to
}
const resolveBreadcrumb = (source: SellerBreadcrumbSource) => {
if (typeof source === "string") {
return {
title: source
}
}
const titleKey = resolveMetaValue(source.titleKey)
const title = titleKey ? t(titleKey) : resolveMetaValue(source.title)
const to = resolveRouteLocation(resolveMetaValue(source.to))
const fallbackTo = source.name ? { name: source.name } : source.path
return {
title: title || "",
to: resolveRouteLocation(to || fallbackTo)
}
}
const resolveTitle = (title?: RouteMetaValue<string>, titleKey?: RouteMetaValue<string>) => {
let key = title || titleKey
// const key = resolveMetaValue(titleKey)
return key ? t(key) || "" : ""
}
const autoBreadcrumbs = computed(() => {
const currentRecord = route.matched[route.matched.length - 1]
const customBreadcrumbs = resolveMetaValue(
currentRecord?.meta?.sellerBreadcrumbs as SellerBreadcrumbsSource | undefined
)
if (Array.isArray(customBreadcrumbs)) {
return customBreadcrumbs
.map((breadcrumb) => resolveBreadcrumb(breadcrumb as SellerBreadcrumbSource))
.filter((breadcrumb) => breadcrumb.title)
}
return route.matched
.map((record) => record.meta?.sellerBreadcrumb)
.filter(
(breadcrumb): breadcrumb is SellerBreadcrumbSource =>
typeof breadcrumb === "string" ||
(typeof breadcrumb === "object" && breadcrumb !== null)
)
.map(resolveBreadcrumb)
.filter((breadcrumb) => breadcrumb.title)
})
const breadcrumbList = computed<SellerBreadcrumbItem[]>(() => {
if (props.breadcrumbs.length) {
return props.breadcrumbs
.map(resolveBreadcrumb)
.filter((breadcrumb) => breadcrumb.title)
}
return autoBreadcrumbs.value
})
const displayTitle = computed(() => {
return (
props.title ||
resolveTitle(
route.meta.sellerHeaderTitle as RouteMetaValue<string> | undefined,
route.meta.sellerHeaderTitleKey as RouteMetaValue<string> | undefined
) ||
breadcrumbList.value[breadcrumbList.value.length - 1]?.title ||
""
)
})
const displayTip = computed(() => {
return (
props.tip ||
resolveTitle(
route.meta.sellerHeaderTip as RouteMetaValue<string> | undefined,
route.meta.sellerHeaderTipKey as RouteMetaValue<string> | undefined
)
)
})
const onBreadcrumbClick = (breadcrumb: SellerBreadcrumbItem, index: number) => {
if (index >= breadcrumbList.value.length - 1) return
const historyIndex = -(breadcrumbList.value.length - index - 1)
if (canUseBreadcrumbHistory(Math.abs(historyIndex))) {
router.go(historyIndex)
return
}
if (breadcrumb.to) {
router.replace(breadcrumb.to)
}
}
const canUseBreadcrumbHistory = (steps: number) => {
if (steps <= 0 || typeof history === "undefined") return false
const state = history.state as {
back?: unknown
position?: unknown
} | null
const backPath = state?.back
return (
typeof state?.position === "number" &&
state.position >= steps &&
typeof backPath === "string" &&
backPath.startsWith("/home/seller/myListings")
)
}
</script>
<style scoped lang="less">
.seller-header {
@@ -90,8 +238,8 @@
font-family: "pingfang_regular";
color: #999;
font-size: 1.4rem;
cursor: pointer;
&:not(.last) {
&.clickable {
cursor: pointer;
text-decoration: underline;
}
}