,High,https://react.dev/reference/react-dom/components#all-html-components
+44,Accessibility,Manage focus properly,Handle focus for modals dialogs,Focus trap in modals return focus on close,No focus management,useEffect to focus input,Modal without focus trap,High,
+45,Accessibility,Announce dynamic content,Use ARIA live regions for updates,aria-live for dynamic updates,Silent updates to screen readers,"
{msg}
",
{msg}
,Medium,
+46,Accessibility,Label form controls,Associate labels with inputs,htmlFor matching input id,Placeholder as only label,"
","
",High,
+47,TypeScript,Type component props,Define interfaces for all props,interface Props with all prop types,any or missing types,interface Props { name: string },function Component(props: any),High,
+48,TypeScript,Type state properly,Provide types for useState,useState
() for complex state,Inferred any types,useState(null),useState(null),Medium,
+49,TypeScript,Type event handlers,Use React event types,React.ChangeEvent,Generic Event type,onChange: React.ChangeEvent,onChange: Event,Medium,
+50,TypeScript,Use generics for reusable components,Generic components for flexible typing,Generic props for list components,Union types for flexibility, items={T[]}>,,Medium,
+51,Patterns,Container/Presentational split,Separate data logic from UI,Container fetches presentational renders,Mixed data and UI in one,, with fetch and render,Low,
+52,Patterns,Render props for flexibility,Share code via render prop pattern,Render prop for customizable rendering,Duplicate logic across components, ...}/>,Copy paste fetch logic,Low,https://react.dev/reference/react/cloneElement#passing-data-with-a-render-prop
+53,Patterns,Compound components,Related components sharing state,Tab + TabPanel sharing context,Prop drilling between related,,,Low,
diff --git a/apps/frontend/.shared/ui-ux-pro-max/data/stacks/svelte.csv b/apps/frontend/.shared/ui-ux-pro-max/data/stacks/svelte.csv
new file mode 100644
index 0000000..39b6d2e
--- /dev/null
+++ b/apps/frontend/.shared/ui-ux-pro-max/data/stacks/svelte.csv
@@ -0,0 +1,54 @@
+No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
+1,Reactivity,Use $: for reactive statements,Automatic dependency tracking,$: for derived values,Manual recalculation,$: doubled = count * 2,let doubled; count && (doubled = count * 2),Medium,https://svelte.dev/docs/svelte-components#script-3-$-marks-a-statement-as-reactive
+2,Reactivity,Trigger reactivity with assignment,Svelte tracks assignments not mutations,Reassign arrays/objects to trigger update,Mutate without reassignment,"items = [...items, newItem]",items.push(newItem),High,https://svelte.dev/docs/svelte-components#script-2-assignments-are-reactive
+3,Reactivity,Use $state in Svelte 5,Runes for explicit reactivity,let count = $state(0),Implicit reactivity in Svelte 5,let count = $state(0),let count = 0 (Svelte 5),Medium,https://svelte.dev/blog/runes
+4,Reactivity,Use $derived for computed values,$derived replaces $: in Svelte 5,let doubled = $derived(count * 2),$: in Svelte 5,let doubled = $derived(count * 2),$: doubled = count * 2 (Svelte 5),Medium,
+5,Reactivity,Use $effect for side effects,$effect replaces $: side effects,Use $effect for subscriptions,$: for side effects in Svelte 5,$effect(() => console.log(count)),$: console.log(count) (Svelte 5),Medium,
+6,Props,Export let for props,Declare props with export let,export let propName,Props without export,export let count = 0,let count = 0,High,https://svelte.dev/docs/svelte-components#script-1-export-creates-a-component-prop
+7,Props,Use $props in Svelte 5,$props rune for prop access,let { name } = $props(),export let in Svelte 5,"let { name, age = 0 } = $props()",export let name; export let age = 0,Medium,
+8,Props,Provide default values,Default props with assignment,export let count = 0,Required props without defaults,export let count = 0,export let count,Low,
+9,Props,Use spread props,Pass through unknown props,{...$$restProps} on elements,Manual prop forwarding,
+
+
+
+ {{ $t('checkout.voucher') }}
+
+
+
+
+
{{ $t('checkout.loading') }}
+
+
+ {{ $t('checkout.noVouchers') }}
+
+
+
+
+
+ {{ voucher.currency === 'USD' ? '$' : voucher.currency }}{{ voucher.amount }}
+
+
{{ voucher.couponCode }}
+
+
+
{{ voucher.sourceDesc }}
+
{{ $t('checkout.minOrder') }}: {{ voucher.currency === 'USD' ? '$' : voucher.currency }}{{ voucher.minOrderAmount }}
+
{{ $t('checkout.expireAt') }}: {{ formatDate(voucher.expireAt) }}
+
+
+
+
+
+
+
@@ -74,7 +114,7 @@
{{ $t('checkout.processTitle') || '我们的流程如下' }}
- {{ $t('checkout.orderConfirmation') }}
- - {{ $t('checkout.productionTime') }}
+
- {{ $t('checkout.logistics') }}
- {{ $t('checkout.afterSales') }}
@@ -197,13 +237,15 @@ const showStripe = ref(false)
const { locale } = useI18n()
const showPayingOverlay = ref(false)
+const voucherList = ref([])
+const selectedVoucher = ref(null)
+const loadingVouchers = ref(false)
+const discount_amount= ref(0);
// 省州映射数据已移至国际化文件
const amountCents = computed(() => {
let base = price.value // 默认价格,移除了尺寸相关定价
- if (addons.value.gloss) base += 300
- if (addons.value.base) base += 400
- if (addons.value.matte) base += 200
- return base * qty.value
+ const total = (base * qty.value) - discount_amount.value
+ return total
})
// 计算支付按钮是否禁用
@@ -222,6 +264,55 @@ const isPayButtonDisabled = computed(() => {
const unt = ref('');
const price = ref(0);
const seriesId = ref('');
+
+const getVoucherList = async () => {
+ loadingVouchers.value = true
+ try {
+ const res = await requestUtils.common(clientApi.default.getAvailableCoupon)
+ if (res.code === 0) {
+ voucherList.value = res.data || []
+ }
+ } catch (error) {
+ console.error('获取优惠券列表失败:', error)
+ voucherList.value = []
+ } finally {
+ loadingVouchers.value = false
+ }
+}
+
+const onVoucherSelect = (voucher) => {
+ if (selectedVoucher.value?.id === voucher.id) {
+ selectedVoucher.value = null
+ } else {
+ selectedVoucher.value = voucher
+ }
+ updatePayInfo();
+}
+//更新支付信息
+const updatePayInfo = () => {
+ let parmas = {
+ currency:unt.value,
+ amount:price.value,
+ }
+ if(selectedVoucher.value){
+ parmas.coupon_ids = [selectedVoucher.value.id]
+ }else{
+ parmas.coupon_ids = []
+ }
+ requestUtils.common(clientApi.default.calculateUnitAmount,parmas).then(res => {
+ const data = res.data;
+ discount_amount.value = data.discount_amount || 0
+ })
+}
+const formatDate = (dateStr) => {
+ if (!dateStr) return ''
+ const date = new Date(dateStr)
+ const year = date.getFullYear()
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ return `${year}-${month}-${day}`
+}
+
//获取对应价格
const getPrice = async () => {
const res = await requestUtils.common(clientApi.default.getProductList)
@@ -271,9 +362,13 @@ const goShopify = () => {//用户点击购买
project_details:project_details,
order_info:order_info,
}
+ if(selectedVoucher.value){
+ params.coupon_ids = [selectedVoucher.value.id]
+ }else{
+ params.coupon_ids = []
+ }
// 显示支付中蒙层
showPayingOverlay.value = true
-
// 5秒后隐藏蒙层
setTimeout(() => {
showPayingOverlay.value = false
@@ -281,11 +376,9 @@ const goShopify = () => {//用户点击购买
// Save shipping and contact information if checkbox is checked
saveLocal()
// 在控制台打印整理后的信息
- console.log('Order Parameters:', params)
params.product_id = seriesId.value
payserver.createPayorOrder(params);
}
-
const saveLocal = () => {
try {
if (shipping.value.saveInfo) {
@@ -324,6 +417,7 @@ onMounted(() => {
updateCountryOptions()
updateStates()
getPrice();
+ getVoucherList();
} catch (e) {}
})
watch(() => shipping.value.country, () => { updateStates() })
@@ -625,6 +719,124 @@ const updateStates = () => {
color: #ffffff;
}
+/* Voucher Section */
+.voucher-section {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.loading-vouchers,
+.no-vouchers {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 40px 20px;
+ color: #9ca3af;
+ font-size: 14px;
+ gap: 12px;
+}
+
+.loading-spinner {
+ width: 20px;
+ height: 20px;
+ border: 2px solid rgba(139,92,246,0.3);
+ border-top-color: #8b5cf6;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.voucher-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.voucher-item {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 16px;
+ background: rgba(17,24,39,0.6);
+ border: 1px solid rgba(255,255,255,0.1);
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+}
+
+.voucher-item:hover {
+ border-color: rgba(139,92,246,0.4);
+ background: rgba(139,92,246,0.05);
+}
+
+.voucher-item.selected {
+ border-color: #8b5cf6;
+ background: rgba(139,92,246,0.1);
+}
+
+.voucher-left {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 80px;
+}
+
+.voucher-amount {
+ font-size: 24px;
+ font-weight: 700;
+ color: #10b981;
+ line-height: 1;
+}
+
+.voucher-code {
+ font-size: 12px;
+ color: #9ca3af;
+ font-family: monospace;
+}
+
+.voucher-right {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.voucher-desc {
+ font-size: 14px;
+ color: #e5e7eb;
+ font-weight: 500;
+}
+
+.voucher-min,
+.voucher-expire {
+ font-size: 12px;
+ color: #9ca3af;
+}
+
+.voucher-check {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background: rgba(139,92,246,0.2);
+ border: 2px solid #8b5cf6;
+}
+
+.check-icon {
+ color: #8b5cf6;
+ font-size: 14px;
+ font-weight: bold;
+}
+
/* Info Row Layout */
.info-row {
display: grid;
diff --git a/apps/frontend/src/locales/index.js b/apps/frontend/src/locales/index.js
index c67c1e3..c9d868b 100644
--- a/apps/frontend/src/locales/index.js
+++ b/apps/frontend/src/locales/index.js
@@ -130,7 +130,7 @@ export default {
preview: '预览',
modify: '修改',
sceneGraph: '场景图',
- edit: '编辑',
+ edit: '局部',
download: '下载'
},
modelCard: {
@@ -218,7 +218,7 @@ export default {
imageFreeCount: '生图免费',
modelFreeCount: '模型免费',
times: '次',
- remainingCredits: '剩余积分',
+ remainingCredits: '积分',
recharge: '充值',
guide: '使用指南',
back: '返回',
@@ -688,8 +688,8 @@ export default {
phone_register_button: '注册',
phone_logging: '正在登录...',
phone_registering: '正在注册...',
- phone_login_link: '使用手机号登录',
- email_login_link: '使用邮箱登录',
+ phone_login_link: '手机号',
+ email_login_link: '邮箱登录',
},
payment: {
methods: '支付方式',
@@ -745,6 +745,11 @@ export default {
productionTime: '生产时间:生产周期为 5–15 个工作日,节假日可能顺延。',
logistics: '物流:发货后将提供订单与跟踪编号,物流信息会发送到您的邮箱。',
afterSales: '售后与退款:请参考退款政策;如有问题,请联系 ',
+ voucher: '优惠券',
+ loading: '加载中...',
+ noVouchers: '暂无可用优惠券',
+ minOrder: '最低订单金额',
+ expireAt: '过期时间',
error: {
firstNameRequired: '名不能为空',
lastNameRequired: '姓不能为空',
@@ -1726,7 +1731,7 @@ export default {
imageFreeCount: 'Free Image Generation',
modelFreeCount: 'Free Model',
times: 'times',
- remainingCredits: 'Remaining Credits',
+ remainingCredits: 'Credits',
recharge: 'Recharge',
guide: 'User Guide',
back: 'Back',
@@ -2241,7 +2246,7 @@ export default {
phone_register_button: 'Register',
phone_logging: 'Logging in...',
phone_registering: 'Registering...',
- phone_login_link: 'Login with Phone',
+ phone_login_link: 'Phone',
email_login_link: 'Login with Email',
},
payment: {
@@ -2295,6 +2300,11 @@ export default {
productionTime: 'Production Time: The production cycle is 5–15 business days, which may be extended during holidays.',
logistics: 'Logistics: After shipment, we will provide the order and tracking number, and logistics information will be sent to your email.',
afterSales: 'After-sales & Refund: Please refer to the refund policy; if you have any questions, please contact ',
+ voucher: 'Voucher',
+ loading: 'Loading...',
+ noVouchers: 'No available vouchers',
+ minOrder: 'Minimum order amount',
+ expireAt: 'Expiration date',
error: {
firstNameRequired: 'First name cannot be empty',
lastNameRequired: 'Last name cannot be empty',
diff --git a/apps/frontend/src/router/index.js b/apps/frontend/src/router/index.js
index 4b35216..dafdab4 100644
--- a/apps/frontend/src/router/index.js
+++ b/apps/frontend/src/router/index.js
@@ -22,6 +22,15 @@ const UserCenter = () => import('../views/user/index.vue')
const NotFound = () => import('../views/NotFound.vue')
const Waitlist = () => import('../views/Waitlist.vue')
const KefuReduce = () => import('../views/kefuReduce.vue')
+const isPortraitMobile = () => {
+ return window.innerWidth < 768 && window.innerHeight > window.innerWidth
+}
+const CreateProject = () => {
+ if (isPortraitMobile()) {
+ return import('../views/Project/CreateProjectShu/CreateProjectShu.vue')
+ }
+ return import('../views/Project/CreateProject.vue')
+}
NProgress.configure({
showSpinner: false,
})// 开启轻量模式(顶部细线)
@@ -146,7 +155,7 @@ export const freeRoutes = [
{
path: '/project/:id/:series',
name: 'project',
- component: () => import('../views/Project/CreateProject.vue'),
+ component:CreateProject,
meta: { requiresAuth: true, fullScreen: true }
},
{
diff --git a/apps/frontend/src/views/Login/Login.vue b/apps/frontend/src/views/Login/Login.vue
index cc5f75f..70a9557 100644
--- a/apps/frontend/src/views/Login/Login.vue
+++ b/apps/frontend/src/views/Login/Login.vue
@@ -48,14 +48,14 @@
-
+
+
+
+
+
+
+
diff --git a/apps/frontend/src/views/user/index.js b/apps/frontend/src/views/user/index.js
index 7e5672a..5dd6334 100644
--- a/apps/frontend/src/views/user/index.js
+++ b/apps/frontend/src/views/user/index.js
@@ -100,7 +100,7 @@ export class UserController {
}
// 查询用户代金券列表
async getVoucherList() {
- return await requestUtils.common(clientApi.default.getCouponList);
+ return await requestUtils.common(clientApi.default.getAvailableCoupon);
/*
{
"code": 0,
diff --git a/apps/frontend/src/views/user/index.vue b/apps/frontend/src/views/user/index.vue
index 64cfaff..4f58879 100644
--- a/apps/frontend/src/views/user/index.vue
+++ b/apps/frontend/src/views/user/index.vue
@@ -125,7 +125,7 @@
{{ $t('userCenter.voucher.title') }}
-
+
@@ -181,8 +181,7 @@
{{ new Date(voucher.expireAt).toLocaleDateString() }}
-
-
-