|
@@ -3,7 +3,14 @@
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useState, useEffect } from 'react';
|
|
|
import api from '@/lib/api';
|
|
import api from '@/lib/api';
|
|
|
import { useRouter } from 'next/navigation';
|
|
import { useRouter } from 'next/navigation';
|
|
|
-import { Loader2, Info } from 'lucide-react';
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ Loader2,
|
|
|
|
|
+ Info,
|
|
|
|
|
+ Check,
|
|
|
|
|
+ FileText,
|
|
|
|
|
+ CheckCircle2,
|
|
|
|
|
+ ShieldCheck
|
|
|
|
|
+} from 'lucide-react';
|
|
|
import BindEmailModal from '@/components/BindEmailModal';
|
|
import BindEmailModal from '@/components/BindEmailModal';
|
|
|
import { useLanguage } from '@/lib/i18n/LanguageContext';
|
|
import { useLanguage } from '@/lib/i18n/LanguageContext';
|
|
|
import ConfirmModal from '@/components/common/ConfirmModal';
|
|
import ConfirmModal from '@/components/common/ConfirmModal';
|
|
@@ -124,7 +131,6 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
if (prop.default !== undefined) {
|
|
if (prop.default !== undefined) {
|
|
|
initialValues[key] = prop.default;
|
|
initialValues[key] = prop.default;
|
|
|
} else if (prop.type === 'boolean') {
|
|
} else if (prop.type === 'boolean') {
|
|
|
- // boolean 类型如果没有默认值,初始化为 false
|
|
|
|
|
initialValues[key] = false;
|
|
initialValues[key] = false;
|
|
|
} else {
|
|
} else {
|
|
|
initialValues[key] = '';
|
|
initialValues[key] = '';
|
|
@@ -288,58 +294,102 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
setIsBindEmailOpen(false);
|
|
setIsBindEmailOpen(false);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // --- 核心优化:渲染描述 ---
|
|
|
|
|
+ // 解决描述文本“一大坨”和换行符丢失的问题
|
|
|
|
|
+ const renderProductDescription = (text: string) => {
|
|
|
|
|
+ if (!text) return null;
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 按换行符拆分,过滤空行
|
|
|
|
|
+ const lines = text.split('\n').filter(line => line.trim().length > 0);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 如果只有一行,直接显示(保留 pre-wrap 以防万一)
|
|
|
|
|
+ if (lines.length <= 1) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <p className="text-slate-600 text-sm leading-relaxed whitespace-pre-wrap">
|
|
|
|
|
+ {text}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 如果有多行,渲染为漂亮的权益列表
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="bg-white rounded-lg border border-slate-100 p-4 mt-3 shadow-sm">
|
|
|
|
|
+ <ul className="space-y-3">
|
|
|
|
|
+ {lines.map((line, index) => (
|
|
|
|
|
+ <li key={index} className="flex items-start gap-3 text-sm text-slate-700">
|
|
|
|
|
+ <CheckCircle2 size={18} className="text-green-500 mt-0.5 flex-shrink-0" />
|
|
|
|
|
+ <span className="leading-snug">{line.replace(/^[•\-\*]\s*/, '')}</span>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // --- Render Fields ---
|
|
|
const renderField = (key: string, fieldSchema: SchemaProperty, required: boolean = false) => {
|
|
const renderField = (key: string, fieldSchema: SchemaProperty, required: boolean = false) => {
|
|
|
const commonClasses = "w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition text-base md:text-sm bg-white min-h-[46px]";
|
|
const commonClasses = "w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition text-base md:text-sm bg-white min-h-[46px]";
|
|
|
const label = fieldSchema.title || key;
|
|
const label = fieldSchema.title || key;
|
|
|
const placeholderText = fieldSchema.description || `${t('common.enter')} ${label}`;
|
|
const placeholderText = fieldSchema.description || `${t('common.enter')} ${label}`;
|
|
|
const currentValue = formValues[key];
|
|
const currentValue = formValues[key];
|
|
|
|
|
|
|
|
- // ----------------------------------------------------------------
|
|
|
|
|
- // 1. Boolean 类型 (标准的 Checkbox)
|
|
|
|
|
- // ----------------------------------------------------------------
|
|
|
|
|
|
|
+ // Boolean 类型优化
|
|
|
if (fieldSchema.type === 'boolean') {
|
|
if (fieldSchema.type === 'boolean') {
|
|
|
const isChecked = currentValue === true;
|
|
const isChecked = currentValue === true;
|
|
|
return (
|
|
return (
|
|
|
- <div className="flex items-center h-[46px] px-1" key={key}>
|
|
|
|
|
- <input
|
|
|
|
|
- id={`field-${key}`}
|
|
|
|
|
- type="checkbox"
|
|
|
|
|
- // Boolean 不应该有 HTML required 属性,因为 unchecked (false) 也是有效值
|
|
|
|
|
- // 除非你特意需要 "Must agree" 的逻辑
|
|
|
|
|
- checked={isChecked}
|
|
|
|
|
- onChange={(e) => handleInputChange(key, e.target.checked)}
|
|
|
|
|
- className="w-5 h-5 text-blue-600 rounded border-gray-300 focus:ring-blue-500 cursor-pointer accent-blue-600"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div className="flex items-center h-[46px]" key={key}>
|
|
|
<label
|
|
<label
|
|
|
|
|
+ className="flex items-center cursor-pointer select-none group"
|
|
|
htmlFor={`field-${key}`}
|
|
htmlFor={`field-${key}`}
|
|
|
- className="ml-3 text-gray-700 text-sm cursor-pointer select-none font-medium"
|
|
|
|
|
>
|
|
>
|
|
|
- {/* 如果选中显示 Yes,否则显示 No,或者你可以只显示固定的 "Enabled/Yes" */}
|
|
|
|
|
- {isChecked ? 'Yes' : 'No'}
|
|
|
|
|
|
|
+ <div className={`
|
|
|
|
|
+ w-12 h-6 rounded-full border flex items-center transition-colors relative
|
|
|
|
|
+ ${isChecked ? 'bg-blue-600 border-blue-600' : 'bg-gray-200 border-gray-200'}
|
|
|
|
|
+ `}>
|
|
|
|
|
+ <div className={`
|
|
|
|
|
+ w-4 h-4 rounded-full bg-white shadow-sm transform transition-transform absolute top-1
|
|
|
|
|
+ ${isChecked ? 'translate-x-7' : 'translate-x-1'}
|
|
|
|
|
+ `}/>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <input
|
|
|
|
|
+ id={`field-${key}`}
|
|
|
|
|
+ type="checkbox"
|
|
|
|
|
+ className="hidden"
|
|
|
|
|
+ checked={isChecked}
|
|
|
|
|
+ onChange={(e) => handleInputChange(key, e.target.checked)}
|
|
|
|
|
+ />
|
|
|
|
|
+ <span className={`ml-3 text-sm font-medium ${isChecked ? 'text-gray-900' : 'text-gray-600'}`}>
|
|
|
|
|
+ {isChecked ? (t('common.yes') || 'Yes') : (t('common.no') || 'No')}
|
|
|
|
|
+ </span>
|
|
|
</label>
|
|
</label>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 枚举类型 (Select)
|
|
|
|
|
|
|
+ // 枚举类型
|
|
|
if (fieldSchema.enum && fieldSchema.enum.length > 0) {
|
|
if (fieldSchema.enum && fieldSchema.enum.length > 0) {
|
|
|
return (
|
|
return (
|
|
|
- <select
|
|
|
|
|
- key={key}
|
|
|
|
|
- required={required}
|
|
|
|
|
- className={`${commonClasses} appearance-none`}
|
|
|
|
|
- value={currentValue || ''}
|
|
|
|
|
- onChange={(e) => handleInputChange(key, e.target.value)}
|
|
|
|
|
- >
|
|
|
|
|
- <option value="" disabled>{t('common.select')} {label}</option>
|
|
|
|
|
- {fieldSchema.enum.map((option: string | number) => (
|
|
|
|
|
- <option key={option} value={option}>{option}</option>
|
|
|
|
|
- ))}
|
|
|
|
|
- </select>
|
|
|
|
|
|
|
+ <div className="relative">
|
|
|
|
|
+ <select
|
|
|
|
|
+ key={key}
|
|
|
|
|
+ required={required}
|
|
|
|
|
+ className={`${commonClasses} appearance-none cursor-pointer`}
|
|
|
|
|
+ value={currentValue || ''}
|
|
|
|
|
+ onChange={(e) => handleInputChange(key, e.target.value)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <option value="" disabled>{t('common.select')} {label}</option>
|
|
|
|
|
+ {fieldSchema.enum.map((option: string | number) => (
|
|
|
|
|
+ <option key={option} value={option}>{option}</option>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </select>
|
|
|
|
|
+ <div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-500">
|
|
|
|
|
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path></svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 3. 日期/时间类型
|
|
|
|
|
|
|
+ // 日期/时间
|
|
|
if (fieldSchema.format === 'date' || fieldSchema.format === 'date-time') {
|
|
if (fieldSchema.format === 'date' || fieldSchema.format === 'date-time') {
|
|
|
const realType = fieldSchema.format === 'date' ? 'date' : 'datetime-local';
|
|
const realType = fieldSchema.format === 'date' ? 'date' : 'datetime-local';
|
|
|
return (
|
|
return (
|
|
@@ -355,6 +405,8 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
style={{ lineHeight: 'normal' }}
|
|
style={{ lineHeight: 'normal' }}
|
|
|
value={currentValue || ''}
|
|
value={currentValue || ''}
|
|
|
onChange={(e) => handleInputChange(key, e.target.value)}
|
|
onChange={(e) => handleInputChange(key, e.target.value)}
|
|
|
|
|
+ onFocus={(e) => e.target.classList.remove('text-transparent')}
|
|
|
|
|
+ onBlur={(e) => !e.target.value && e.target.classList.add('text-transparent')}
|
|
|
/>
|
|
/>
|
|
|
{!currentValue && (
|
|
{!currentValue && (
|
|
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-base md:text-sm pointer-events-none truncate pr-8 w-full h-fit transition-opacity peer-focus:opacity-0">
|
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-base md:text-sm pointer-events-none truncate pr-8 w-full h-fit transition-opacity peer-focus:opacity-0">
|
|
@@ -365,7 +417,7 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 4. 其他 Input 类型
|
|
|
|
|
|
|
+ // 其他
|
|
|
let inputType = 'text';
|
|
let inputType = 'text';
|
|
|
if (fieldSchema.type === 'integer' || fieldSchema.type === 'number') inputType = 'number';
|
|
if (fieldSchema.type === 'integer' || fieldSchema.type === 'number') inputType = 'number';
|
|
|
if (fieldSchema.format === 'email') inputType = 'email';
|
|
if (fieldSchema.format === 'email') inputType = 'email';
|
|
@@ -386,11 +438,9 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
const getSortedKeys = () => {
|
|
const getSortedKeys = () => {
|
|
|
const properties = formSchema?.properties || {};
|
|
const properties = formSchema?.properties || {};
|
|
|
const keys = Object.keys(properties);
|
|
const keys = Object.keys(properties);
|
|
|
-
|
|
|
|
|
if (Array.isArray(formSchema?.['ui:order'])) {
|
|
if (Array.isArray(formSchema?.['ui:order'])) {
|
|
|
return formSchema!['ui:order'];
|
|
return formSchema!['ui:order'];
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
return keys.sort((a, b) => {
|
|
return keys.sort((a, b) => {
|
|
|
const propA = properties[a];
|
|
const propA = properties[a];
|
|
|
const propB = properties[b];
|
|
const propB = properties[b];
|
|
@@ -400,9 +450,9 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
});
|
|
});
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // --- Render ---
|
|
|
|
|
|
|
+ // --- Render Main ---
|
|
|
|
|
|
|
|
- if (loading) return <div className="p-12 flex justify-center"><Loader2 className="animate-spin text-blue-600"/></div>;
|
|
|
|
|
|
|
+ if (loading) return <div className="p-12 flex justify-center"><Loader2 className="animate-spin text-blue-600 w-8 h-8"/></div>;
|
|
|
if (!product) return <div className="p-8 text-center text-red-500">{t('order.product_not_found')}</div>;
|
|
if (!product) return <div className="p-8 text-center text-red-500">{t('order.product_not_found')}</div>;
|
|
|
|
|
|
|
|
const properties = formSchema?.properties || {};
|
|
const properties = formSchema?.properties || {};
|
|
@@ -411,91 +461,124 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
const hasFields = sortedKeys.length > 0;
|
|
const hasFields = sortedKeys.length > 0;
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
- <div className="bg-white p-5 md:p-8 rounded-xl shadow-sm border border-slate-200">
|
|
|
|
|
|
|
+ <div className="max-w-3xl mx-auto">
|
|
|
|
|
|
|
|
- {/* Header */}
|
|
|
|
|
- <div className="mb-6 md:mb-8 pb-4 md:pb-6 border-b border-gray-100">
|
|
|
|
|
- <div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2 md:gap-4">
|
|
|
|
|
- <h1 className="text-xl md:text-2xl font-bold text-gray-900 leading-tight">
|
|
|
|
|
- {product.title || productName}
|
|
|
|
|
- </h1>
|
|
|
|
|
- <span className="text-lg md:text-xl font-bold text-blue-600 flex-shrink-0">
|
|
|
|
|
- {(product.price_amount / 100).toFixed(2)} {product.price_currency}
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <p className="text-gray-500 mt-2 text-sm leading-relaxed">{product.description}</p>
|
|
|
|
|
-
|
|
|
|
|
- <div className="mt-4 flex items-start gap-2 text-xs text-amber-700 bg-amber-50 px-3 py-2.5 rounded-lg w-full md:w-fit border border-amber-100">
|
|
|
|
|
- <Info size={16} className="flex-shrink-0 mt-0.5" />
|
|
|
|
|
- <span className="leading-snug">{t('order.fill_form_hint')}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* Form Fields */}
|
|
|
|
|
- <form onSubmit={handleSubmit} className="space-y-5 md:space-y-6">
|
|
|
|
|
- {!hasFields && (
|
|
|
|
|
- <div className="text-center py-8 text-gray-400 text-sm bg-slate-50 rounded-lg border border-dashed border-slate-200">
|
|
|
|
|
- {t('order.no_extra_info_needed')}
|
|
|
|
|
|
|
+ {/*
|
|
|
|
|
+ 核心修改:头部商品信息卡片
|
|
|
|
|
+ 1. 独立背景色 (bg-slate-50)
|
|
|
|
|
+ 2. 更好的边距 (p-6)
|
|
|
|
|
+ 3. 描述文本优化 (renderProductDescription)
|
|
|
|
|
+ */}
|
|
|
|
|
+ <div className="bg-slate-50 p-6 md:p-8 rounded-2xl border border-slate-100 mb-8 relative overflow-hidden">
|
|
|
|
|
+ {/* 装饰背景 */}
|
|
|
|
|
+ <div className="absolute top-0 right-0 w-40 h-40 bg-blue-100 rounded-full blur-3xl opacity-30 -translate-y-1/2 translate-x-1/2 pointer-events-none"></div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="relative z-10">
|
|
|
|
|
+ <div className="flex flex-col md:flex-row md:items-start justify-between gap-4">
|
|
|
|
|
+ <h1 className="text-2xl font-bold text-slate-900 leading-snug flex-1">
|
|
|
|
|
+ {product.title || productName}
|
|
|
|
|
+ </h1>
|
|
|
|
|
+ <div className="bg-white px-4 py-2 rounded-lg shadow-sm border border-slate-100 flex items-baseline gap-1 shrink-0">
|
|
|
|
|
+ <span className="text-sm font-semibold text-slate-500">{product.price_currency === 'CNY' ? '¥' : product.price_currency}</span>
|
|
|
|
|
+ <span className="text-2xl font-extrabold text-slate-900">
|
|
|
|
|
+ {(product.price_amount / 100).toLocaleString()}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- )}
|
|
|
|
|
|
|
|
|
|
- {sortedKeys.map((key) => (
|
|
|
|
|
- <div key={key}>
|
|
|
|
|
- <label className="block text-sm font-bold text-gray-700 mb-1.5">
|
|
|
|
|
- {properties[key].title || key}
|
|
|
|
|
- {requiredFields.includes(key) && <span className="text-red-500 ml-1">*</span>}
|
|
|
|
|
- </label>
|
|
|
|
|
- {renderField(key, properties[key], requiredFields.includes(key))}
|
|
|
|
|
- {properties[key].description && (
|
|
|
|
|
- <p className="text-xs text-gray-400 mt-1.5 leading-snug">{properties[key].description}</p>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ <div className="mt-4">
|
|
|
|
|
+ {/* 渲染优化后的描述 */}
|
|
|
|
|
+ {renderProductDescription(product.description)}
|
|
|
</div>
|
|
</div>
|
|
|
- ))}
|
|
|
|
|
|
|
|
|
|
- <div className="pt-4 md:pt-6 border-t border-gray-50 mt-8">
|
|
|
|
|
- <button
|
|
|
|
|
- type="submit"
|
|
|
|
|
- disabled={submitting}
|
|
|
|
|
- className="w-full bg-blue-600 text-white py-3.5 rounded-xl font-bold hover:bg-blue-700 transition disabled:opacity-70 disabled:cursor-not-allowed flex justify-center items-center shadow-lg shadow-blue-200 active:scale-[0.98] text-sm md:text-base"
|
|
|
|
|
- >
|
|
|
|
|
- {submitting ? (
|
|
|
|
|
- <>
|
|
|
|
|
- <Loader2 className="animate-spin mr-2 w-5 h-5"/> {t('common.processing')}
|
|
|
|
|
- </>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <span>
|
|
|
|
|
- {t('order.submit_and_pay')}
|
|
|
|
|
- <span className="ml-1 opacity-90 text-blue-100 font-normal">
|
|
|
|
|
- ({(product.price_amount / 100).toFixed(2)} {product.price_currency})
|
|
|
|
|
- </span>
|
|
|
|
|
- </span>
|
|
|
|
|
- )}
|
|
|
|
|
- </button>
|
|
|
|
|
- <p className="text-center text-xs text-gray-400 mt-3">
|
|
|
|
|
- {t('common.agree_agreement') || "点击提交即代表同意服务条款与隐私政策"}
|
|
|
|
|
- </p>
|
|
|
|
|
|
|
+ <div className="flex items-center gap-2 mt-4 text-xs text-slate-500">
|
|
|
|
|
+ <ShieldCheck size={14} className="text-green-500"/>
|
|
|
|
|
+ <span>{t('order.official_fast_secure') || "官方保障 · 极速处理 · 隐私加密"}</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </form>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- {/* === 全局弹窗挂载 === */}
|
|
|
|
|
|
|
+ {/* 表单区域 */}
|
|
|
|
|
+ <div className="bg-white p-6 md:p-8 rounded-2xl shadow-sm border border-slate-200">
|
|
|
|
|
+ <h2 className="text-lg font-bold text-slate-800 mb-6 flex items-center gap-2">
|
|
|
|
|
+ <FileText className="text-blue-600" size={20}/>
|
|
|
|
|
+ {t('order.fill_form_title') || "填写申请信息"}
|
|
|
|
|
+ </h2>
|
|
|
|
|
+
|
|
|
|
|
+ <form onSubmit={handleSubmit} className="space-y-6">
|
|
|
|
|
+ {!hasFields ? (
|
|
|
|
|
+ <div className="text-center py-12 bg-slate-50 rounded-xl border border-dashed border-slate-200">
|
|
|
|
|
+ <div className="w-14 h-14 bg-white rounded-full flex items-center justify-center mx-auto mb-4 shadow-sm text-green-500">
|
|
|
|
|
+ <Check size={28} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p className="text-slate-900 font-medium mb-1">
|
|
|
|
|
+ {t('order.quick_checkout') || "极速下单"}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p className="text-slate-500 text-sm">
|
|
|
|
|
+ {t('order.no_extra_info_needed') || "该服务无需填写额外资料,请直接提交支付"}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="grid grid-cols-1 gap-6">
|
|
|
|
|
+ {sortedKeys.map((key) => (
|
|
|
|
|
+ <div key={key} className="group">
|
|
|
|
|
+ <label className="block text-sm font-bold text-slate-700 mb-2 flex items-center">
|
|
|
|
|
+ {properties[key].title || key}
|
|
|
|
|
+ {requiredFields.includes(key) && <span className="text-red-500 ml-1" title="Required">*</span>}
|
|
|
|
|
+ </label>
|
|
|
|
|
+
|
|
|
|
|
+ {renderField(key, properties[key], requiredFields.includes(key))}
|
|
|
|
|
+
|
|
|
|
|
+ {properties[key].description && (
|
|
|
|
|
+ <div className="flex items-start gap-1.5 mt-2 text-xs text-slate-500 bg-slate-50 p-2 rounded border border-slate-100">
|
|
|
|
|
+ <Info size={12} className="mt-0.5 shrink-0 text-slate-400"/>
|
|
|
|
|
+ <span className="leading-relaxed">{properties[key].description}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ <div className="pt-8 mt-8 border-t border-slate-100">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ disabled={submitting}
|
|
|
|
|
+ className="w-full bg-slate-900 text-white py-4 rounded-xl font-bold hover:bg-slate-800 transition-all disabled:opacity-70 disabled:cursor-not-allowed flex justify-center items-center shadow-lg shadow-slate-200 active:scale-[0.99] text-base"
|
|
|
|
|
+ >
|
|
|
|
|
+ {submitting ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Loader2 className="animate-spin mr-2 w-5 h-5"/> {t('common.processing')}
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <span className="flex items-center gap-1">
|
|
|
|
|
+ {t('order.submit_and_pay')}
|
|
|
|
|
+ <span className="ml-1 opacity-80 font-normal">
|
|
|
|
|
+ · {(product.price_amount / 100).toLocaleString()} {product.price_currency === 'CNY' ? '¥' : product.price_currency}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <p className="text-center text-xs text-slate-400 mt-4">
|
|
|
|
|
+ {t('common.agree_agreement') || "点击提交即代表同意服务条款与隐私政策"}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
|
|
+ {/* === 全局弹窗 === */}
|
|
|
<BindEmailModal
|
|
<BindEmailModal
|
|
|
isOpen={isBindEmailOpen}
|
|
isOpen={isBindEmailOpen}
|
|
|
onClose={() => setIsBindEmailOpen(false)}
|
|
onClose={() => setIsBindEmailOpen(false)}
|
|
|
onSuccess={handleBindSuccess}
|
|
onSuccess={handleBindSuccess}
|
|
|
/>
|
|
/>
|
|
|
-
|
|
|
|
|
<ConfirmModal
|
|
<ConfirmModal
|
|
|
isOpen={showConfirmModal}
|
|
isOpen={showConfirmModal}
|
|
|
- title={t('common.confirm_title') || "请确认您的操作"}
|
|
|
|
|
- message={t('order.confirm_submit_msg') || "请核对信息无误后,点击确认提交。"}
|
|
|
|
|
|
|
+ title={t('common.confirm_title')}
|
|
|
|
|
+ message={t('order.confirm_submit_msg')}
|
|
|
onConfirm={handleConfirmSubmit}
|
|
onConfirm={handleConfirmSubmit}
|
|
|
- onCancel={() => {
|
|
|
|
|
- setShowConfirmModal(false);
|
|
|
|
|
- setSubmitting(false);
|
|
|
|
|
- }}
|
|
|
|
|
|
|
+ onCancel={() => { setShowConfirmModal(false); setSubmitting(false); }}
|
|
|
/>
|
|
/>
|
|
|
-
|
|
|
|
|
<MessageModal
|
|
<MessageModal
|
|
|
isOpen={msgModal.isOpen}
|
|
isOpen={msgModal.isOpen}
|
|
|
title={msgModal.title}
|
|
title={msgModal.title}
|