Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/Aida_Purchaser_Front

This commit is contained in:
X1627315083@163.com
2026-06-01 14:12:43 +08:00
15 changed files with 160 additions and 257 deletions

View File

@@ -18,7 +18,7 @@ export default {
pleaseInputName: 'Please input the name',
nameLengthError: 'Name length must be between {min} and {max} characters',
passwordSpecial: 'Must contain special characters',
passwordCase: 'Mix of uppercase, lowercase and numbers',
passwordCase: 'A combination of numbers and letters',
pleaseInputEmail: 'Please input the email',
emailFormatError: 'Please input the email again',
pleaseInputPassword: 'Please input the password',
@@ -355,7 +355,7 @@ export default {
Offices: "Offices",
JoinWithUs: "Join with Us",
},
addShoppingCart:{
addShoppingCart: {
title: 'Added to your Shopping Cart',
statement: 'Digital Assets Only. No physical product included.',
button: 'See Shopping Cart'
@@ -375,5 +375,23 @@ export default {
germany: 'Germany',
australia: 'Australia',
canada: 'Canada'
},
Pay: {
OrderSummary: 'Order Summary',
PaymentDetails: 'Payment Details',
CreditDebitCard: 'Credit / Debit Card',
AgreementText: 'I agree to the <span onclick="{onTermsClick}">Terms & Conditions</span> and <span onclick="{onPrivacyClick}">Privacy Policy</span>. All digital item sales are final and non-refundable.',
PayWithStripe: 'Pay with Stripe',
PayWith: 'Pay with',
Cancel: 'Cancel',
IHaveCompletedPayment: 'I Have Completed payment',
Back: 'Back',
PayTip1: "You'll be redirected to a Stripe popup to log in and confirm. No card details are shared with Stylish Parade — Stripe handles all payment security.",
PayTip2: "Please keep the window open until the payment is completed. If you are to open the payment window, please check your browser settings to see if pop-ups are being blocked. Points may be delayed after successful payment. Please wait 1-3 minutes and click the credits refresh button.",
PurchaseSuccessful: "Purchase Successful",
PurchaseSuccessfulTip: "Your digital items are now available and have been saved in Personal Center → My Wardrobe.",
DownloadAllAssets: "download all Assets",
ExportInvoice: "Export Invoice",
ContinueShopping: "Continue Shopping"
}
}

View File

@@ -18,7 +18,7 @@ export default {
pleaseInputName: '请输入姓名',
nameLengthError: '姓名长度必须在 {min} 到 {max} 个字符之间',
passwordSpecial: '必须包含特殊符号',
passwordCase: '大小写字母数字混合组合',
passwordCase: '字母数字组合',
pleaseInputEmail: '请输入邮箱',
emailFormatError: '请输入正确的邮箱',
pleaseInputPassword: '请输入密码',
@@ -376,5 +376,23 @@ export default {
AboutUs: '关于我们',
Offices: '办公室',
JoinWithUs: '加入我们'
},
Pay: {
OrderSummary: '订单信息',
PaymentDetails: '支付详情',
CreditDebitCard: '信用卡/借记卡',
AgreementText: '我同意 <span onclick="{onTermsClick}">使用条款与条件</span> 和 <span onclick="{onPrivacyClick}">隐私政策</span>。所有数字资产销售均为最终销售,不可退款。',
PayWithStripe: '使用 Stripe 支付',
PayWith: '支付',
Cancel: '取消',
IHaveCompletedPayment: '我已完成支付',
Back: '返回',
PayTip1: "您将被重定向到 Stripe 弹窗以登录并确认支付。您的信用卡信息不会被 Stylish Parade 收集。Stripe 处理所有支付安全。",
PayTip2: "请保持窗口打开,直到支付完成。如果您打开支付窗口,请检查浏览器设置以查看是否已阻止弹窗。支付完成后,积分可能会有延迟。请等待 1-3 分钟并点击积分刷新按钮。",
PurchaseSuccessful: "购买成功",
PurchaseSuccessfulTip: "您的数字资产已保存在个人中心的我的衣橱中。",
DownloadAllAssets: "下载所有资产",
ExportInvoice: "导出发票",
ContinueShopping: "继续购物"
}
}

View File

@@ -4,6 +4,7 @@ import { useGlobalStore } from '@/stores/global'
import { getUserLanguage } from '@/api/user'
import { fetchAllUnreadMessage } from '@/api/notification'
import i18n from '@/lang/index'
import myEvent from '@/utils/myEvent'
// 语言映射:后端格式 -> i18n 格式
const backendToI18nLanguage: Record<string, string> = {
@@ -19,6 +20,8 @@ let languageSynced = false
* 1. 设置路由的meta属性为{ cache: true },表示需要缓存
* 2. App.vue中使用RouteCache组件通过路由的name来进行匹配
* 3. 路由的name默认是文件名,如果文件名与name不一致,通过defineOptions({ name: 'componentName' })来设置
*
* 需要登录路由: meta={ login:true }
*/
const router = createRouter({
routes: [
@@ -62,7 +65,8 @@ const router = createRouter({
{
path: '/shoppingCart', // 购物车
name: 'shoppingCart',
component: () => import('@/views/shoppingCart/index.vue')
component: () => import('@/views/shoppingCart/index.vue'),
meta: { login: true }
},
{
path: '/notifications',
@@ -94,6 +98,9 @@ const router = createRouter({
})
router.beforeEach((to, from, next) => {
if(to.meta?.login && !useUserInfoStore().state.token) {
myEvent.emit('openLoginDialog')
}
next()
})

View File

@@ -28,6 +28,7 @@
> section {
width: 100%;
height: auto;
min-height: 50vw;
}
> section.bgw {
position: relative;
@@ -40,5 +41,8 @@
position: absolute;
}
}
> .section-footer {
min-height: 0;
}
}
</style>

View File

@@ -78,6 +78,7 @@
> div {
padding: 1rem;
border: 0.1rem solid #979797;
background-color: #fff;
> img {
width: 27.4rem;
height: 34.6rem;

View File

@@ -1,6 +1,6 @@
<template>
<section class="digital-items2 bgw">
<img src="@/assets/images/home/digital-items-1.jpg" class="bg" />
<img src="@/assets/images/home/digital-items-2.jpg" class="bg" />
<div class="content">
<div class="tip" v-html="$t('Home.DigitalItemsTip2')"></div>
<div class="list">
@@ -65,6 +65,7 @@
> div {
padding: 1rem;
border: 0.1rem solid #979797;
background-color: #fff;
> img {
width: 27.4rem;
height: 34.6rem;

View File

@@ -7,13 +7,13 @@
</el-icon>
<span>{{ $t('Login.passwordLengthError', { min: 6, max: 20 }) }}</span>
</div>
<div>
<!-- <div>
<el-icon>
<CloseBold v-if="validateSpecial(value)" />
<Select v-else />
</el-icon>
<span>{{ $t('Login.passwordSpecial') }}</span>
</div>
</div> -->
<div>
<el-icon>
<CloseBold v-if="validateCase(value)" />
@@ -42,7 +42,7 @@
color: #fff;
font-size: 1.1rem;
padding: 1.5rem;
border-radius: 1.5rem;
// border-radius: 1.5rem;
line-height: normal;
> div {

View File

@@ -23,15 +23,15 @@ export const validateEmail = (rule, value, callback) => {
export const validateLength = (v, min = 6, max = 20) => (v.length < 6 || v.length > 20);
//检查特殊字符
export const validateSpecial = (v) => (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(v));
//检查大小写字母和数字
export const validateCase = (v) => (!/[a-z]/.test(v) || !/[A-Z]/.test(v) || !/\d/.test(v));
//检查字母和数字
export const validateCase = (v) => (!/[A-z]/.test(v) || !/\d/.test(v));
// 检查密码
export const validatePass = (rule, value, callback) => {
var str = ''
if (validateLength(value)) {
str = t('Login.passwordLengthError', { min: 6, max: 20 })
} else if (validateSpecial(value)) {
str = t('Login.passwordSpecial')
// } else if (validateSpecial(value)) {
// str = t('Login.passwordSpecial')
} else if (validateCase(value)) {
str = t('Login.passwordCase')
}

View File

@@ -1,7 +1,11 @@
<template>
<div class="main-header" id="main-header">
<div class="left">
<img class="logo" src="@/assets/images/logo.png" @click="onNavItemClick('/')" />
<img
class="logo"
src="@/assets/images/logo.png"
@click="onNavItemClick({ path: '/' })"
/>
</div>
<div class="center">
<div
@@ -14,7 +18,7 @@
? activePath === v.path
: new RegExp(`^${v.path}`).test(activePath)
}"
@click="onNavItemClick(v.path)"
@click="onNavItemClick(v)"
>
<span>{{ $t(v.name) }}</span>
</div>
@@ -25,7 +29,7 @@
v-for="v in navList2"
:key="v.path"
:class="{ active: new RegExp(`^${v.path}`).test(activePath) }"
@click="onNavItemClick(v.path)"
@click="onNavItemClick(v)"
>
<svg-icon :name="activePath === v.path ? v.active_icon : v.icon" size="22" />
</div>
@@ -129,10 +133,15 @@
{
icon: 'cart_0',
active_icon: 'cart_1',
path: '/shoppingCart'
path: '/shoppingCart',
login: true
}
])
const onNavItemClick = (path: string) => {
const onNavItemClick = (v: any) => {
const path = v.path
if (v.login && !userInfoStore.state.token) {
return onLogin()
}
if (path === activePath.value) return
router.push(path)
}

View File

@@ -2,7 +2,7 @@
<div class="pay">
<div class="content">
<payment :ids="ids" />
<sc-list title="Order Summary" is-view is-mini :list="list" />
<sc-list :title="$t('Pay.OrderSummary')" is-view is-mini :list="list" />
</div>
<my-footer />
</div>

View File

@@ -2,57 +2,47 @@
<div class="payment">
<div class="header" @click="onBack">
<span class="icon"><svg-icon name="back" size="30" /></span>
<span class="text">Payment Details</span>
<span class="text">{{ $t('Pay.PaymentDetails') }}</span>
</div>
<!-- 未支付 -->
<template v-if="paymentStatus !== ORDER_STATUS.SUCCESS">
<div class="paylist">
<div class="item">
<img src="@/assets/images/pay/stripe.png" alt="" />
<span>Credit / Debit Card</span>
<span>{{ $t('Pay.CreditDebitCard') }}</span>
</div>
</div>
<div class="agreement">
<el-checkbox v-model="agreement">
<div class="text">
I agree to the <span>Terms & Conditions</span> and
<span>Privacy Policy</span>. All digital item sales are final and
non-refundable.
</div></el-checkbox
>
<div class="text" v-html="$t('Pay.AgreementText')"></div
></el-checkbox>
</div>
<div class="title">
<span class="icon"><svg-icon name="card" size="20" /></span>
<span class="text">Pay with Paypal</span>
<span class="text">{{ $t('Pay.PayWithStripe') }}</span>
</div>
<template v-if="!query.paymentId">
<div class="tip">
You'll be redirected to a Paypal popup to log in and confirm. No card details
are shared with Stylish Parade — PayPal handles all payment security.
</div>
<div class="tip">{{ $t('Pay.PayTip1') }}</div>
<div class="buttons">
<button custom="black" @click="onPayWith">
<span class="text">Pay with</span>
<span class="text">{{ $t('Pay.PayWith') }}</span>
<span class="icon pay"><svg-icon name="pay-stripe" /></span>
</button>
</div>
<div class="buttons">
<span class="text" @click="onBack">Cancel</span>
<span class="text" @click="onBack">{{ $t('Pay.Cancel') }}</span>
</div>
</template>
<!-- 已支付等待确认 -->
<template v-if="query.paymentId">
<div class="tip">
Please keep the window open until the payment is completed. If you are to open
the payment window, please check your browser settings to see if pop-ups are
being blocked. Points may be delayed after successful payment. Please wait 1-3
minutes and click the credits refresh button.
<div class="tip">{{ $t('Pay.PayTip2') }}</div>
<div class="buttons">
<button custom="black" @click="getOrderStatus">
{{ $t('Pay.IHaveCompletedPayment') }}
</button>
</div>
<div class="buttons">
<button custom="black" @click="getOrderStatus">I Have Completed payment</button>
</div>
<div class="buttons">
<span class="text" @click="onBack">Back</span>
<span class="text" @click="onBack">{{ $t('Pay.Back') }}</span>
</div>
</template>
</template>
@@ -60,11 +50,8 @@
<template v-if="paymentStatus === ORDER_STATUS.SUCCESS">
<div class="success">
<img src="@/assets/images/pay/success.png" alt="" />
<div class="title">Purchase Successful!</div>
<div class="tip">
Your digital items are now available and have been saved in Personal Center → My
Wardrobe.
</div>
<div class="title">{{ $t('Pay.PurchaseSuccessful') }}</div>
<div class="tip">{{ $t('Pay.PurchaseSuccessfulTip') }}</div>
</div>
<div class="progres" v-if="downloadInfo.status !== DOWNLOAD_STATUS.null">
@@ -81,14 +68,16 @@
</el-progress>
</div>
<div class="buttons">
<button custom="black" @click="handleDownloadAllAssets">download all Assets</button>
<button custom="black" @click="handleDownloadAllAssets">
{{ $t('Pay.DownloadAllAssets') }}
</button>
<button custom="black-box">
<span class="icon"><svg-icon name="order-file" size="18" /></span>
<span class="text">Export Invoice</span>
<span class="text">{{ $t('Pay.ExportInvoice') }}</span>
</button>
</div>
<div class="buttons">
<span class="text" @click="onBack">Continue Shopping</span>
<span class="text" @click="onBack">{{ $t('Pay.ContinueShopping') }}</span>
</div>
</template>
</div>

View File

@@ -11,7 +11,7 @@
<span class="text">{{ $t('ShoppingCart.brands') }}</span>
</div>
<div class="brands-item" v-for="v in brandsList" :key="v.brand">
<span class="label">{{ v.brand }}</span>
<span class="label" @click="handleBrandClick(v.id)">{{ v.brand }}</span>
<span class="value"
><span>{{ v.children.length }}</span
>{{ $t('ShoppingCart.item') }}</span
@@ -51,6 +51,7 @@
if (index === -1) {
arr.push({
brand: v.brand,
id: v.sellerId,
children: [v]
})
} else {
@@ -68,6 +69,12 @@
query: { list }
})
}
const handleBrandClick = (id) => {
router.push({
name: 'brandDetail',
params: { id }
})
}
</script>
<style lang="less" scoped>
@@ -124,6 +131,8 @@
> .label {
text-decoration: underline;
color: #585858;
cursor: pointer;
user-select: none;
}
> .value {
color: #808080;

View File

@@ -319,6 +319,7 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
> .text {
font-family: KaiseiOpti-Bold;
font-size: var(--sc-list-title-font-size, 4rem);
@@ -420,7 +421,7 @@
}
}
.sc-list:deep(.el-select) {
width: 15rem;
width: 18rem;
--el-border-radius-base: 0;
--el-select-input-color: rgba(0, 0, 0, 0.5);
--el-select-input-font-size: 1rem;

View File

@@ -241,63 +241,6 @@
const selectedCount = computed(() => {
return dataList.value.filter((el) => el.checked === true).length
})
const allCategoriesSelected = computed(() => {
return (
filters.categories.length === categoryValues.value.length &&
categoryValues.value.every((value) => filters.categories.includes(value))
)
})
const allGendersSelected = computed(() => {
return (
filters.genders.length === genderValues.value.length &&
genderValues.value.every((value) => filters.genders.includes(value))
)
})
const isCategoryActive = (value: string) => {
if (value === 'all') {
return allCategoriesSelected.value
}
return filters.categories.includes(value)
}
const toggleCategory = (value: string) => {
if (value === 'all') {
filters.categories = allCategoriesSelected.value ? [] : [...categoryValues.value]
return
}
if (filters.categories.includes(value)) {
filters.categories = filters.categories.filter((item) => item !== value)
return
}
filters.categories = [...filters.categories, value]
}
const isGenderActive = (value: string) => {
if (value === 'all') {
return allGendersSelected.value
}
return filters.genders.includes(value)
}
const toggleGender = (value: string) => {
if (value === 'all') {
filters.genders = allGendersSelected.value ? [] : [...genderValues.value]
return
}
if (filters.genders.includes(value)) {
filters.genders = filters.genders.filter((item) => item !== value)
return
}
filters.genders = [...filters.genders, value]
}
const updateFilters = (value: { categories: string[]; genders: string[] }) => {
filters.categories = value.categories

View File

@@ -11,41 +11,21 @@
<section class="filter-group">
<h3 class="filter-group__title">{{ t('Wardrobe.assets.categories') }}</h3>
<div class="filter-group__line"></div>
<div class="filter-group__options">
<button
v-for="option in categories"
:key="option.value"
class="filter-option"
type="button"
:class="{ 'is-active': isCategoryActive(option.value) }"
@click="toggleCategory(option.value)"
>
<span class="filter-option__box">
<span class="filter-option__tick"></span>
</span>
<span class="filter-option__label">{{ option.label }}</span>
</button>
</div>
<Checked
:list="categoryOptions"
:selected="selectedCategories"
@change="handleCategoryChange"
/>
</section>
<section class="filter-group">
<h3 class="filter-group__title">{{ t('Wardrobe.assets.gender') }}</h3>
<div class="filter-group__line"></div>
<div class="filter-group__options">
<button
v-for="option in genders"
:key="option.value"
class="filter-option"
type="button"
:class="{ 'is-active': isGenderActive(option.value) }"
@click="toggleGender(option.value)"
>
<span class="filter-option__box">
<span class="filter-option__tick"></span>
</span>
<span class="filter-option__label">{{ option.label }}</span>
</button>
</div>
<CheckedGender
:list="genderOptions"
:selected="selectedGenders"
@change="handleGenderChange"
/>
</section>
</div>
</aside>
@@ -55,6 +35,8 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { PropType } from 'vue'
import Checked from '@/components/checked.vue'
import CheckedGender from '@/components/checked-gender.vue'
interface FilterOption {
label: string
@@ -95,90 +77,64 @@
props.genders.filter((option) => option.value !== 'all').map((option) => option.value)
)
const allCategoriesSelected = computed(() => {
return (
// 为 Checked 组件准备的选项列表(不包含 'all'
const categoryOptions = computed(() =>
props.categories.filter((option) => option.value !== 'all')
)
const genderOptions = computed(() => props.genders.filter((option) => option.value !== 'all'))
// 转换当前选中状态为组件需要的格式
const selectedCategories = computed(() => {
const allSelected =
props.filters.categories.length === categoryValues.value.length &&
categoryValues.value.every((value) => props.filters.categories.includes(value))
)
return allSelected ? ['all'] : props.filters.categories
})
const allGendersSelected = computed(() => {
return (
const selectedGenders = computed(() => {
const allSelected =
props.filters.genders.length === genderValues.value.length &&
genderValues.value.every((value) => props.filters.genders.includes(value))
)
return allSelected ? ['all'] : props.filters.genders
})
const currentFilters = computed<FilterState>(() => ({
categories: [...props.filters.categories],
genders: [...props.filters.genders]
}))
const handleCategoryChange = (selected: string[]) => {
let categories: string[]
if (selected.includes('all') || selected.length === 0) {
// 如果选择了 'all' 或者没有选择任何项,则选择所有分类
categories = [...categoryValues.value]
} else {
categories = selected
}
const updateFilters = (updated: Partial<FilterState>) => {
emit('update:filters', {
categories: updated.categories ?? currentFilters.value.categories,
genders: updated.genders ?? currentFilters.value.genders
categories,
genders: props.filters.genders
})
}
const isCategoryActive = (value: string) => {
if (value === 'all') {
return allCategoriesSelected.value
const handleGenderChange = (selected: string[]) => {
let genders: string[]
if (selected.includes('all') || selected.length === 0) {
// 如果选择了 'all' 或者没有选择任何项,则选择所有性别
genders = [...genderValues.value]
} else {
genders = selected
}
return props.filters.categories.includes(value)
}
const toggleCategory = (value: string) => {
if (value === 'all') {
updateFilters({
categories: allCategoriesSelected.value ? [] : [...categoryValues.value]
})
return
}
if (props.filters.categories.includes(value)) {
updateFilters({
categories: props.filters.categories.filter((item) => item !== value)
})
return
}
updateFilters({
categories: [...props.filters.categories, value]
})
}
const isGenderActive = (value: string) => {
if (value === 'all') {
return allGendersSelected.value
}
return props.filters.genders.includes(value)
}
const toggleGender = (value: string) => {
if (value === 'all') {
updateFilters({
genders: allGendersSelected.value ? [] : [...genderValues.value]
})
return
}
if (props.filters.genders.includes(value)) {
updateFilters({
genders: props.filters.genders.filter((item) => item !== value)
})
return
}
updateFilters({
genders: [...props.filters.genders, value]
emit('update:filters', {
categories: props.filters.categories,
genders
})
}
const clearFilters = () => {
updateFilters({
emit('update:filters', {
categories: [...categoryValues.value],
genders: [...genderValues.value]
})
@@ -241,60 +197,7 @@
margin-bottom: 2rem;
}
.filter-group__options {
display: flex;
flex-direction: column;
gap: 1.2rem;
.filter-option {
display: inline-flex;
align-items: center;
gap: 1.2rem;
width: fit-content;
padding: 0;
border: 0;
background: transparent;
font-family: 'KaiseiOpti-Regular';
font-size: 1.5rem;
line-height: 1.4;
color: #6e665d;
cursor: pointer;
text-align: left;
> .filter-option__box {
width: 1.6rem;
height: 1.6rem;
border: 0.1rem solid var(--wardrobe-border-dark);
display: inline-flex;
align-items: center;
justify-content: center;
background: #ffffff;
flex-shrink: 0;
.filter-option__tick {
width: 0.9rem;
height: 0.5rem;
border-left: 0.18rem solid #ffffff;
border-bottom: 0.18rem solid #ffffff;
transform: rotate(-45deg) translateY(-0.05rem);
opacity: 0;
}
}
&.is-active {
color: var(--wardrobe-text-main);
.filter-option__box {
border-color: var(--wardrobe-text-main);
background: var(--wardrobe-text-main);
.filter-option__tick {
opacity: 1;
}
}
}
}
}
}
}
}