|
@@ -6,6 +6,9 @@ import { useRouter } from 'next/navigation';
|
|
|
import { Loader2, Info } from 'lucide-react';
|
|
import { Loader2, Info } 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 MessageModal from '@/components/common/MessageModal';
|
|
|
|
|
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
// 类型定义
|
|
// 类型定义
|
|
@@ -55,40 +58,58 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
const router = useRouter();
|
|
const router = useRouter();
|
|
|
const { t } = useLanguage();
|
|
const { t } = useLanguage();
|
|
|
|
|
|
|
|
- // 状态管理
|
|
|
|
|
|
|
+ // --- 状态管理 ---
|
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
|
const [submitting, setSubmitting] = useState<boolean>(false);
|
|
const [submitting, setSubmitting] = useState<boolean>(false);
|
|
|
|
|
+
|
|
|
|
|
+ // 数据状态
|
|
|
const [product, setProduct] = useState<ProductDetail | null>(null);
|
|
const [product, setProduct] = useState<ProductDetail | null>(null);
|
|
|
const [formSchema, setFormSchema] = useState<JsonSchema | null>(null);
|
|
const [formSchema, setFormSchema] = useState<JsonSchema | null>(null);
|
|
|
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
|
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
|
|
|
|
|
|
|
- // 绑定邮箱弹窗控制
|
|
|
|
|
|
|
+ // 弹窗控制状态
|
|
|
const [isBindEmailOpen, setIsBindEmailOpen] = useState(false);
|
|
const [isBindEmailOpen, setIsBindEmailOpen] = useState(false);
|
|
|
|
|
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
|
|
|
|
|
+
|
|
|
|
|
+ const [msgModal, setMsgModal] = useState({
|
|
|
|
|
+ isOpen: false,
|
|
|
|
|
+ title: '',
|
|
|
|
|
+ message: '',
|
|
|
|
|
+ type: 'error' as 'error' | 'info' | 'success'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // --- 辅助函数:显示消息 ---
|
|
|
|
|
+ const showMessage = (msg: string, title?: string, type: 'error' | 'info' = 'error') => {
|
|
|
|
|
+ setMsgModal({
|
|
|
|
|
+ isOpen: true,
|
|
|
|
|
+ message: msg,
|
|
|
|
|
+ title: title || t('common.notice'),
|
|
|
|
|
+ type
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- // 初始化数据
|
|
|
|
|
|
|
+ // --- 初始化数据 ---
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
async function initData() {
|
|
async function initData() {
|
|
|
try {
|
|
try {
|
|
|
setLoading(true);
|
|
setLoading(true);
|
|
|
|
|
|
|
|
// 1. 获取商品详情
|
|
// 1. 获取商品详情
|
|
|
- // API: /api/vas/product/detail?product_id={id}
|
|
|
|
|
const prodRes = await api.get('/api/vas/product/detail', {
|
|
const prodRes = await api.get('/api/vas/product/detail', {
|
|
|
params: { "product_id": productId }
|
|
params: { "product_id": productId }
|
|
|
});
|
|
});
|
|
|
const prodData = prodRes.data.data || prodRes.data;
|
|
const prodData = prodRes.data.data || prodRes.data;
|
|
|
setProduct(prodData);
|
|
setProduct(prodData);
|
|
|
|
|
|
|
|
- // 2. 获取动态表单定义 (如果有 schema_id)
|
|
|
|
|
|
|
+ // 2. 获取动态表单定义
|
|
|
if (prodData.schema_id) {
|
|
if (prodData.schema_id) {
|
|
|
try {
|
|
try {
|
|
|
- // API: /api/vas/schema/detail?schema_id={id}
|
|
|
|
|
const schemaRes = await api.get('/api/vas/schema/detail', {
|
|
const schemaRes = await api.get('/api/vas/schema/detail', {
|
|
|
params: { "schema_id": prodData.schema_id }
|
|
params: { "schema_id": prodData.schema_id }
|
|
|
});
|
|
});
|
|
|
const schemaData = schemaRes.data.data || schemaRes.data;
|
|
const schemaData = schemaRes.data.data || schemaRes.data;
|
|
|
|
|
|
|
|
- // 兼容处理:如果 schema_json 是字符串,需要 parse
|
|
|
|
|
|
|
+ // 兼容处理 JSON 字符串
|
|
|
const schemaJson = typeof schemaData.schema_json === 'string'
|
|
const schemaJson = typeof schemaData.schema_json === 'string'
|
|
|
? JSON.parse(schemaData.schema_json)
|
|
? JSON.parse(schemaData.schema_json)
|
|
|
: schemaData.schema_json;
|
|
: schemaData.schema_json;
|
|
@@ -100,7 +121,6 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
if (schemaJson.properties) {
|
|
if (schemaJson.properties) {
|
|
|
Object.keys(schemaJson.properties).forEach(key => {
|
|
Object.keys(schemaJson.properties).forEach(key => {
|
|
|
const prop = schemaJson.properties[key];
|
|
const prop = schemaJson.properties[key];
|
|
|
- // 如果有默认值就使用,否则为空字符串
|
|
|
|
|
initialValues[key] = prop.default !== undefined ? prop.default : '';
|
|
initialValues[key] = prop.default !== undefined ? prop.default : '';
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
@@ -108,13 +128,12 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
|
|
|
|
|
} catch (schemaErr) {
|
|
} catch (schemaErr) {
|
|
|
console.error("Failed to load schema", schemaErr);
|
|
console.error("Failed to load schema", schemaErr);
|
|
|
- // 依然允许渲染,只是没有动态表单
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error("Fetch product failed", error);
|
|
console.error("Fetch product failed", error);
|
|
|
- alert(t('order.load_product_failed'));
|
|
|
|
|
|
|
+ showMessage(t('order.load_product_failed'));
|
|
|
} finally {
|
|
} finally {
|
|
|
setLoading(false);
|
|
setLoading(false);
|
|
|
}
|
|
}
|
|
@@ -125,15 +144,17 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
}
|
|
}
|
|
|
}, [productId, t]);
|
|
}, [productId, t]);
|
|
|
|
|
|
|
|
- // 辅助函数:确保用户已登录(如果未登录,尝试自动注册)
|
|
|
|
|
- const ensureUserLoggedIn = async (): Promise<boolean> => {
|
|
|
|
|
- // 1. 如果已有 Token,直接通过
|
|
|
|
|
- const token = localStorage.getItem('rsid');
|
|
|
|
|
- if (token) return true;
|
|
|
|
|
|
|
+ // --- 辅助函数:用户状态检查 ---
|
|
|
|
|
+
|
|
|
|
|
+ // 确保用户已登录(如果未登录,尝试自动注册)
|
|
|
|
|
+ const ensureUserLoggedIn = async (forceNew = false): Promise<boolean> => {
|
|
|
|
|
+ if (!forceNew) {
|
|
|
|
|
+ const token = localStorage.getItem('rsid');
|
|
|
|
|
+ if (token) return true;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
console.log("Detecting no session, auto-registering...");
|
|
console.log("Detecting no session, auto-registering...");
|
|
|
- // 2. 调用自动注册
|
|
|
|
|
const res = await api.post('/api/auth/auto-register', {
|
|
const res = await api.post('/api/auth/auto-register', {
|
|
|
register_ip: 'client-lazy-init'
|
|
register_ip: 'client-lazy-init'
|
|
|
});
|
|
});
|
|
@@ -142,25 +163,23 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
const newToken = data.token || data.access_token;
|
|
const newToken = data.token || data.access_token;
|
|
|
|
|
|
|
|
if (newToken) {
|
|
if (newToken) {
|
|
|
- // 3. 保存登录状态
|
|
|
|
|
localStorage.setItem('rsid', newToken);
|
|
localStorage.setItem('rsid', newToken);
|
|
|
if (data.user) {
|
|
if (data.user) {
|
|
|
localStorage.setItem('user_info', JSON.stringify(data.user));
|
|
localStorage.setItem('user_info', JSON.stringify(data.user));
|
|
|
}
|
|
}
|
|
|
- // 通知 Navbar 变化
|
|
|
|
|
window.dispatchEvent(new Event('storage'));
|
|
window.dispatchEvent(new Event('storage'));
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
return false;
|
|
return false;
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error("Auto register failed", e);
|
|
console.error("Auto register failed", e);
|
|
|
- alert(t('auth.login_success_no_token'));
|
|
|
|
|
|
|
+ showMessage(t('auth.login_success_no_token'));
|
|
|
router.push('/login');
|
|
router.push('/login');
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 辅助函数:检查用户是否已绑定邮箱
|
|
|
|
|
|
|
+ // 检查用户是否已绑定邮箱
|
|
|
const checkUserEmail = (): boolean => {
|
|
const checkUserEmail = (): boolean => {
|
|
|
if (typeof window === 'undefined') return false;
|
|
if (typeof window === 'undefined') return false;
|
|
|
const userStr = localStorage.getItem('user_info');
|
|
const userStr = localStorage.getItem('user_info');
|
|
@@ -168,7 +187,6 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
const user = JSON.parse(userStr);
|
|
const user = JSON.parse(userStr);
|
|
|
- // 判断逻辑:必须有 email 且包含 @ 符号
|
|
|
|
|
if (user.email && user.email.includes('@')) {
|
|
if (user.email && user.email.includes('@')) {
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
@@ -178,18 +196,33 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 处理输入变更
|
|
|
|
|
const handleInputChange = (key: string, value: any) => {
|
|
const handleInputChange = (key: string, value: any) => {
|
|
|
setFormValues(prev => ({ ...prev, [key]: value }));
|
|
setFormValues(prev => ({ ...prev, [key]: value }));
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 提交表单
|
|
|
|
|
|
|
+ // --- 提交逻辑 ---
|
|
|
|
|
+
|
|
|
|
|
+ // 封装 API 请求
|
|
|
|
|
+ const performOrderCreation = async () => {
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ product_id: parseInt(productId),
|
|
|
|
|
+ user_inputs: formValues
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const res = await api.post('/api/vas/order/create', payload);
|
|
|
|
|
+ const orderId = res.data.data?.id || res.data.id;
|
|
|
|
|
+
|
|
|
|
|
+ if (!orderId) throw new Error("Missing Order ID in response");
|
|
|
|
|
+ return orderId;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 表单提交入口
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
|
e.preventDefault();
|
|
e.preventDefault();
|
|
|
setSubmitting(true);
|
|
setSubmitting(true);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- // 1. 确保已登录
|
|
|
|
|
|
|
+ // 1. 初步确保已登录
|
|
|
const isLoggedIn = await ensureUserLoggedIn();
|
|
const isLoggedIn = await ensureUserLoggedIn();
|
|
|
if (!isLoggedIn) {
|
|
if (!isLoggedIn) {
|
|
|
setSubmitting(false);
|
|
setSubmitting(false);
|
|
@@ -198,44 +231,81 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
|
|
|
|
|
// 2. 检查邮箱
|
|
// 2. 检查邮箱
|
|
|
if (!checkUserEmail()) {
|
|
if (!checkUserEmail()) {
|
|
|
- setIsBindEmailOpen(true); // 打开绑定弹窗
|
|
|
|
|
- setSubmitting(false); // 暂停提交
|
|
|
|
|
|
|
+ setIsBindEmailOpen(true);
|
|
|
|
|
+ setSubmitting(false);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 3. 提交订单
|
|
|
|
|
- const payload = {
|
|
|
|
|
- product_id: parseInt(productId),
|
|
|
|
|
- user_inputs: formValues // 动态表单数据
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ // 3. 弹出确认框 (不直接提交)
|
|
|
|
|
+ setShowConfirmModal(true);
|
|
|
|
|
+ // setSubmitting 状态会在确认框关闭或确认后处理
|
|
|
|
|
|
|
|
- const res = await api.post('/api/vas/order/create', payload);
|
|
|
|
|
- const orderId = res.data.data?.id || res.data.id;
|
|
|
|
|
-
|
|
|
|
|
- if (orderId) {
|
|
|
|
|
- router.push(`/payment/${orderId}`);
|
|
|
|
|
- } else {
|
|
|
|
|
- throw new Error("Missing Order ID in response");
|
|
|
|
|
- }
|
|
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
console.error(error);
|
|
console.error(error);
|
|
|
- const msg = error.response?.data?.message || t('order.create_failed');
|
|
|
|
|
- alert(msg);
|
|
|
|
|
setSubmitting(false);
|
|
setSubmitting(false);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 绑定成功回调
|
|
|
|
|
|
|
+ // 确认后的实际提交 (含 Token 过期重试)
|
|
|
|
|
+ const handleConfirmSubmit = async () => {
|
|
|
|
|
+ setShowConfirmModal(false);
|
|
|
|
|
+ setSubmitting(true); // 重新设置 loading,因为关闭弹窗可能导致状态丢失
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const orderId = await performOrderCreation();
|
|
|
|
|
+ router.push(`/payment/${orderId}`);
|
|
|
|
|
+ } catch (firstError: any) {
|
|
|
|
|
+ const errorMsg = firstError.response?.data?.message || "";
|
|
|
|
|
+ const status = firstError.response?.status;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是 Token 失效
|
|
|
|
|
+ if (status === 401 || errorMsg.toLowerCase().includes('token') || errorMsg.toLowerCase().includes('expired')) {
|
|
|
|
|
+ console.log("Token expired, retrying...");
|
|
|
|
|
+
|
|
|
|
|
+ // A. 强制获取新 Token
|
|
|
|
|
+ const newSessionSuccess = await ensureUserLoggedIn(true);
|
|
|
|
|
+
|
|
|
|
|
+ if (newSessionSuccess) {
|
|
|
|
|
+ // B. 新账号需绑定邮箱
|
|
|
|
|
+ if (!checkUserEmail()) {
|
|
|
|
|
+ setIsBindEmailOpen(true);
|
|
|
|
|
+ setSubmitting(false);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // C. 重试提交
|
|
|
|
|
+ try {
|
|
|
|
|
+ const retryOrderId = await performOrderCreation();
|
|
|
|
|
+ router.push(`/payment/${retryOrderId}`);
|
|
|
|
|
+ } catch (retryErr: any) {
|
|
|
|
|
+ const msg = retryErr.response?.data?.message || t('order.create_failed');
|
|
|
|
|
+ showMessage(`${t('common.error')}: ${msg}`);
|
|
|
|
|
+ setSubmitting(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showMessage("Session refresh failed");
|
|
|
|
|
+ setSubmitting(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 普通错误
|
|
|
|
|
+ const msg = firstError.response?.data?.message || t('order.create_failed');
|
|
|
|
|
+ showMessage(`${t('common.error')}: ${msg}`);
|
|
|
|
|
+ setSubmitting(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const handleBindSuccess = () => {
|
|
const handleBindSuccess = () => {
|
|
|
- // 绑定成功后,保持在当前页面,用户可再次点击提交
|
|
|
|
|
|
|
+ // 绑定成功后,仅关闭弹窗,用户需再次点击提交
|
|
|
|
|
+ setIsBindEmailOpen(false);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 渲染单个字段
|
|
|
|
|
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-sm";
|
|
|
|
|
|
|
+ // 公共样式
|
|
|
|
|
+ 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;
|
|
|
- // 使用翻译后的 Placeholder
|
|
|
|
|
const placeholderText = fieldSchema.description || `${t('common.enter')} ${label}`;
|
|
const placeholderText = fieldSchema.description || `${t('common.enter')} ${label}`;
|
|
|
|
|
+ const currentValue = formValues[key] || '';
|
|
|
|
|
|
|
|
// 1. 枚举类型 (Select)
|
|
// 1. 枚举类型 (Select)
|
|
|
if (fieldSchema.enum && fieldSchema.enum.length > 0) {
|
|
if (fieldSchema.enum && fieldSchema.enum.length > 0) {
|
|
@@ -243,8 +313,8 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
<select
|
|
<select
|
|
|
key={key}
|
|
key={key}
|
|
|
required={required}
|
|
required={required}
|
|
|
- className={commonClasses}
|
|
|
|
|
- value={formValues[key] || ''}
|
|
|
|
|
|
|
+ className={`${commonClasses} appearance-none`}
|
|
|
|
|
+ value={currentValue}
|
|
|
onChange={(e) => handleInputChange(key, e.target.value)}
|
|
onChange={(e) => handleInputChange(key, e.target.value)}
|
|
|
>
|
|
>
|
|
|
<option value="" disabled>{t('common.select')} {label}</option>
|
|
<option value="" disabled>{t('common.select')} {label}</option>
|
|
@@ -255,30 +325,35 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 日期/时间类型 (使用原生 input + 焦点切换技巧以显示 placeholder)
|
|
|
|
|
|
|
+ // 2. 日期/时间类型 (修复:聚焦时隐藏 Placeholder)
|
|
|
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 (
|
|
|
- <input
|
|
|
|
|
- key={key}
|
|
|
|
|
- type="text" // 默认为 text 以显示 placeholder
|
|
|
|
|
- required={required}
|
|
|
|
|
- className={commonClasses}
|
|
|
|
|
- placeholder={placeholderText}
|
|
|
|
|
- value={formValues[key] || ''}
|
|
|
|
|
- onChange={(e) => handleInputChange(key, e.target.value)}
|
|
|
|
|
- // 聚焦时切换为日期选择器
|
|
|
|
|
- onFocus={(e) => (e.target.type = realType)}
|
|
|
|
|
- // 失焦且无值时切回文本
|
|
|
|
|
- onBlur={(e) => {
|
|
|
|
|
- if (!e.target.value) e.target.type = 'text';
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div className="relative w-full">
|
|
|
|
|
+ <input
|
|
|
|
|
+ key={key}
|
|
|
|
|
+ type={realType}
|
|
|
|
|
+ required={required}
|
|
|
|
|
+ className={`
|
|
|
|
|
+ ${commonClasses}
|
|
|
|
|
+ peer
|
|
|
|
|
+ ${!currentValue ? 'text-transparent focus:text-gray-900' : 'text-gray-900'}
|
|
|
|
|
+ `}
|
|
|
|
|
+ style={{ lineHeight: 'normal' }}
|
|
|
|
|
+ value={currentValue}
|
|
|
|
|
+ onChange={(e) => handleInputChange(key, e.target.value)}
|
|
|
|
|
+ />
|
|
|
|
|
+ {/* 仅当无值时渲染,且聚焦时通过 CSS 隐藏 */}
|
|
|
|
|
+ {!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">
|
|
|
|
|
+ {placeholderText}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 3. 其他 Input 类型推断
|
|
|
|
|
|
|
+ // 3. 其他 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';
|
|
@@ -290,35 +365,31 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
required={required}
|
|
required={required}
|
|
|
className={commonClasses}
|
|
className={commonClasses}
|
|
|
placeholder={placeholderText}
|
|
placeholder={placeholderText}
|
|
|
- value={formValues[key] || ''}
|
|
|
|
|
|
|
+ value={currentValue}
|
|
|
onChange={(e) => handleInputChange(key, e.target.value)}
|
|
onChange={(e) => handleInputChange(key, e.target.value)}
|
|
|
/>
|
|
/>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 获取排序后的字段 Keys
|
|
|
|
|
const getSortedKeys = () => {
|
|
const getSortedKeys = () => {
|
|
|
const properties = formSchema?.properties || {};
|
|
const properties = formSchema?.properties || {};
|
|
|
const keys = Object.keys(properties);
|
|
const keys = Object.keys(properties);
|
|
|
|
|
|
|
|
- // 优先使用 ui:order 数组排序
|
|
|
|
|
if (Array.isArray(formSchema?.['ui:order'])) {
|
|
if (Array.isArray(formSchema?.['ui:order'])) {
|
|
|
return formSchema!['ui:order'];
|
|
return formSchema!['ui:order'];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 否则根据字段内的 order 或 x-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];
|
|
|
-
|
|
|
|
|
const orderA = propA.order ?? propA['x-order'] ?? 999;
|
|
const orderA = propA.order ?? propA['x-order'] ?? 999;
|
|
|
const orderB = propB.order ?? propB['x-order'] ?? 999;
|
|
const orderB = propB.order ?? propB['x-order'] ?? 999;
|
|
|
-
|
|
|
|
|
return orderA - orderB;
|
|
return orderA - orderB;
|
|
|
});
|
|
});
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // Loading 状态
|
|
|
|
|
|
|
+ // --- Render ---
|
|
|
|
|
+
|
|
|
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"/></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>;
|
|
|
|
|
|
|
@@ -328,31 +399,27 @@ 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="bg-white p-5 md:p-8 rounded-xl shadow-sm border border-slate-200">
|
|
|
|
|
|
|
|
- {/* 头部:商品信息 */}
|
|
|
|
|
|
|
+ {/* Header */}
|
|
|
<div className="mb-6 md:mb-8 pb-4 md:pb-6 border-b border-gray-100">
|
|
<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">
|
|
<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">
|
|
<h1 className="text-xl md:text-2xl font-bold text-gray-900 leading-tight">
|
|
|
{product.title || productName}
|
|
{product.title || productName}
|
|
|
</h1>
|
|
</h1>
|
|
|
<span className="text-lg md:text-xl font-bold text-blue-600 flex-shrink-0">
|
|
<span className="text-lg md:text-xl font-bold text-blue-600 flex-shrink-0">
|
|
|
- {/* 金额显示:假设单位是分 */}
|
|
|
|
|
{(product.price_amount / 100).toFixed(2)} {product.price_currency}
|
|
{(product.price_amount / 100).toFixed(2)} {product.price_currency}
|
|
|
</span>
|
|
</span>
|
|
|
</div>
|
|
</div>
|
|
|
<p className="text-gray-500 mt-2 text-sm leading-relaxed">{product.description}</p>
|
|
<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">
|
|
<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" />
|
|
<Info size={16} className="flex-shrink-0 mt-0.5" />
|
|
|
<span className="leading-snug">{t('order.fill_form_hint')}</span>
|
|
<span className="leading-snug">{t('order.fill_form_hint')}</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* 动态表单区域 */}
|
|
|
|
|
|
|
+ {/* Form Fields */}
|
|
|
<form onSubmit={handleSubmit} className="space-y-5 md:space-y-6">
|
|
<form onSubmit={handleSubmit} className="space-y-5 md:space-y-6">
|
|
|
{!hasFields && (
|
|
{!hasFields && (
|
|
|
<div className="text-center py-8 text-gray-400 text-sm bg-slate-50 rounded-lg border border-dashed border-slate-200">
|
|
<div className="text-center py-8 text-gray-400 text-sm bg-slate-50 rounded-lg border border-dashed border-slate-200">
|
|
@@ -393,17 +460,37 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
|
|
|
)}
|
|
)}
|
|
|
</button>
|
|
</button>
|
|
|
<p className="text-center text-xs text-gray-400 mt-3">
|
|
<p className="text-center text-xs text-gray-400 mt-3">
|
|
|
- 点击提交即代表同意服务条款与隐私政策
|
|
|
|
|
|
|
+ {t('common.agree_agreement') || "点击提交即代表同意服务条款与隐私政策"}
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
</form>
|
|
</form>
|
|
|
|
|
|
|
|
- {/* 绑定邮箱弹窗 */}
|
|
|
|
|
|
|
+ {/* === 全局弹窗挂载 === */}
|
|
|
|
|
+
|
|
|
<BindEmailModal
|
|
<BindEmailModal
|
|
|
isOpen={isBindEmailOpen}
|
|
isOpen={isBindEmailOpen}
|
|
|
onClose={() => setIsBindEmailOpen(false)}
|
|
onClose={() => setIsBindEmailOpen(false)}
|
|
|
onSuccess={handleBindSuccess}
|
|
onSuccess={handleBindSuccess}
|
|
|
/>
|
|
/>
|
|
|
|
|
+
|
|
|
|
|
+ <ConfirmModal
|
|
|
|
|
+ isOpen={showConfirmModal}
|
|
|
|
|
+ title={t('common.confirm_title') || "请确认您的操作"}
|
|
|
|
|
+ message={t('order.confirm_submit_msg') || "请核对信息无误后,点击确认提交。"}
|
|
|
|
|
+ onConfirm={handleConfirmSubmit}
|
|
|
|
|
+ onClose={() => {
|
|
|
|
|
+ setShowConfirmModal(false);
|
|
|
|
|
+ setSubmitting(false); // 取消后恢复按钮
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <MessageModal
|
|
|
|
|
+ isOpen={msgModal.isOpen}
|
|
|
|
|
+ title={msgModal.title}
|
|
|
|
|
+ message={msgModal.message}
|
|
|
|
|
+ type={msgModal.type}
|
|
|
|
|
+ onClose={() => setMsgModal(prev => ({ ...prev, isOpen: false }))}
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|