diff --git a/frontend/src/components/CarCard/CarCard.module.css b/frontend/src/components/CarCard/CarCard.module.css new file mode 100644 index 0000000..89db054 --- /dev/null +++ b/frontend/src/components/CarCard/CarCard.module.css @@ -0,0 +1,190 @@ +.carCardWrapper { + border-radius: 14px; + background: rgba(255, 255, 255, 0.95); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06); + margin-bottom: 16px; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + overflow: hidden; +} + +.carCardWrapper:active { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); +} + +.cardHeader { + padding: 8px 12px; + color: #666; + font-size: 12px; + display: flex; + align-items: center; + justify-content: space-between; + background: #fafafa; +} + +.cardCode { + color: #666; + font-size: 12px; +} + +.editBtn { + padding: 4px 8px; + font-size: 16px; + color: #3F435B; + cursor: pointer; + background: transparent; + border: none; +} + +.carCard { + position: relative; + display: flex; + align-items: stretch; + padding: 12px; +} + +.thumb { + width: 100px; + height: 75px; + border-radius: 10px; + background: #eef2f7; + flex-shrink: 0; + overflow: hidden; +} + +.thumbImage { + width: 100%; + height: 100%; + object-fit: cover; +} + +.info { + flex: 1; + margin-left: 12px; + display: flex; + flex-direction: column; + justify-content: space-between; + min-width: 0; +} + +.row { + display: flex; + align-items: center; +} + +.row1 { + justify-content: space-between; + margin-bottom: 6px; +} + +.name { + font-size: 15px; + font-weight: 700; + color: #111827; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.price { + font-size: 16px; + font-weight: 700; + color: #ff6b35; + flex-shrink: 0; + margin-left: 8px; +} + +.row2, +.row3 { + justify-content: space-between; + gap: 8px; +} + +.kv { + flex: 1; + min-width: 0; + display: flex; + align-items: baseline; +} + +.k { + font-size: 11px; + color: #6b7280; + margin-right: 4px; + flex-shrink: 0; +} + +.v { + font-size: 12px; + color: #374151; + font-weight: 600; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.cardActions { + padding: 12px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + border-top: 1px solid #f0f0f0; + background: #fafafa; +} + +.actionsLeft { + font-size: 12px; + color: #999; +} + +.actionsRight { + display: flex; + gap: 8px; +} + +.actionBtn { + width: 36px; + height: 36px; + border-radius: 50%; + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border: none; + padding: 0; + transition: all 0.2s; + font-size: 16px; +} + +.actionBtn:active { + transform: scale(0.92); + background: #f5f5f5; +} + +.statusTag { + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; +} + +.statusNormal { + background: rgba(82, 196, 26, 0.1); + color: #52c41a; +} + +.statusArchived { + background: rgba(140, 140, 140, 0.1); + color: #8c8c8c; +} + +.statusSold { + background: rgba(250, 173, 20, 0.1); + color: #faad14; +} diff --git a/frontend/src/components/CarCard/CarCard.tsx b/frontend/src/components/CarCard/CarCard.tsx new file mode 100644 index 0000000..2b7b3c5 --- /dev/null +++ b/frontend/src/components/CarCard/CarCard.tsx @@ -0,0 +1,342 @@ +import { useEffect, useCallback } from 'react' +import { useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { Image, Toast, Dialog } from 'antd-mobile' +import { usedCarApi } from '@/services/api' +import styles from './CarCard.module.css' +import type { PriceFormData } from '@/components/PriceModal' + +export interface UsedCarItem { + id: number + code: string + product_name: string + source_place?: string + body_color?: string + plate_time?: string + mileage?: number + images?: string + images_pull?: Array<{ url: string }> + status: number + created_at: string + pdf_src?: string + price?: number +} + +interface CarCardProps { + item: UsedCarItem + isMy?: boolean + index?: number + onRefresh?: () => void + onRequestPrice?: (data: { item: UsedCarItem; index: number }) => void +} + +const CarCard: React.FC = ({ item, isMy = false, index = -1, onRefresh, onRequestPrice }) => { + const { t } = useTranslation() + const navigate = useNavigate() + + // 格式化上牌时间 + const formatPlateTime = (dateStr?: string) => { + if (!dateStr) return '-' + return String(dateStr).slice(0, 7) + } + + // 获取图片 + const getImages = (item: UsedCarItem): string[] => { + if (item.images_pull && item.images_pull.length > 0) { + return item.images_pull.map(img => img.url) + } + if (item.images) { + try { + const images = JSON.parse(item.images) + return Array.isArray(images) ? images : [] + } catch { + return [] + } + } + return [] + } + + // 点击卡片跳转详情 + const handleCardClick = () => { + if (item?.id) { + navigate(`/used-car/detail/${item.id}`) + } + } + + // 编辑 + const handleEdit = (e: React.MouseEvent) => { + e.stopPropagation() + navigate(`/used-car/edit/${item.id}`) + } + + // H5 下载:用 a 标签触发 + const downloadByAnchor = (url: string, filename?: string) => { + try { + const a = document.createElement('a') + a.href = url + if (filename) { + a.download = filename + } + a.target = '_blank' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + } catch { + // 兜底:直接打开 + window.open(url, '_blank') + } + } + + // 下载PDF - 如果存在 URL 直接下载,否则请求价格 + const handleDownloadPdf = async (e: React.MouseEvent) => { + e.stopPropagation() + + // 如果有 PDF URL,直接下载 + if (item.pdf_src && item.pdf_src.trim() !== '') { + downloadByAnchor(item.pdf_src, `${item.product_name}-${item.code}.pdf`) + return + } + + // 没有 PDF URL,触发价格输入弹框 + if (onRequestPrice) { + onRequestPrice({ item, index }) + } + } + + // 生成并下载 PDF(由价格弹框确认后调用) + const generateAndDownloadPdf = useCallback(async (priceData: PriceFormData) => { + try { + Toast.show({ content: '正在生成 PDF...', icon: 'loading' }) + + // 调用生成 PDF 接口 + const res = await usedCarApi.generatePdf({ + id: item.id, + price: Number(priceData.price), + currency: priceData.currency, + lang: priceData.lang, + trade_term: priceData.trade_term + }) as { pdf_url?: string; file_name?: string } + + // 生成成功,下载 PDF + if (res.pdf_url) { + downloadByAnchor(res.pdf_url, res.file_name || `${item.product_name}-${item.code}.pdf`) + Toast.show({ content: t('usedCar.pdfDownloadSuccess') || 'PDF 下载成功', icon: 'success' }) + // 刷新列表 + onRefresh?.() + } + } catch (error) { + const message = error instanceof Error ? error.message : (t('usedCar.pdfDownloadFailed') || 'PDF 生成失败') + Toast.show({ content: message, icon: 'fail' }) + throw error + } + }, [item.id, item.product_name, item.code, t, onRefresh]) + + // 导出方法供外部调用 + useEffect(() => { + if (window) { + (window as any).carCardDownloadPdf = generateAndDownloadPdf + return () => { delete (window as any).carCardDownloadPdf } + } + }, [generateAndDownloadPdf]) + + // 等待飞书 tt 对象准备就绪 + const waitForFeishuSDK = (): Promise => { + return new Promise((resolve, reject) => { + // 如果已经存在,直接返回 + if (window.tt && (window as any).h5sdk) { + ;(window as any).h5sdk.ready(() => { + resolve() + }) + return + } + + // 轮询等待 tt 对象 + let attempts = 0 + const maxAttempts = 50 + const checkInterval = setInterval(() => { + attempts++ + if (window.tt && (window as any).h5sdk) { + clearInterval(checkInterval) + ;(window as any).h5sdk.ready(() => { + resolve() + }) + } else if (attempts >= maxAttempts) { + clearInterval(checkInterval) + reject(new Error('TT_OBJECT_TIMEOUT')) + } + }, 100) + }) + } + + // 分享 + const handleShare = async (e: React.MouseEvent) => { + e.stopPropagation() + const title = item.product_name || '车辆详情' + const link = `${window.location.origin}/#/used-car/detail/${item.id}` + const images = getImages(item) + + // 检测是否在飞书环境 + const isFeishuEnv = /feishu|lark/i.test(navigator.userAgent) + + if (isFeishuEnv) { + try { + // 等待飞书 SDK 准备就绪 + await waitForFeishuSDK() + + // 调用飞书分享 + ;(window as any).tt.shareWebContent({ + title, + content: title, + url: link, + imageUrl: images[0] || '', + success: () => { + Toast.show({ content: '已唤起飞书分享', icon: 'success' }) + }, + fail: (err: any) => { + console.error('飞书分享失败:', err) + // 飞书分享失败,尝试复制链接 + navigator.clipboard.writeText(link).then(() => { + Toast.show({ content: '链接已复制', icon: 'success' }) + }).catch(() => { + Toast.show({ content: '复制失败', icon: 'fail' }) + }) + } + }) + } catch (error) { + console.error('飞书 SDK 加载失败:', error) + // SDK 加载失败,尝试 Web Share API 或复制链接 + if (navigator.share) { + navigator.share({ title, text: title, url: link }).catch(() => { + navigator.clipboard.writeText(link).then(() => { + Toast.show({ content: '链接已复制', icon: 'success' }) + }) + }) + } else { + navigator.clipboard.writeText(link).then(() => { + Toast.show({ content: '链接已复制', icon: 'success' }) + }).catch(() => { + Toast.show({ content: '复制失败', icon: 'fail' }) + }) + } + } + return + } + + // 非飞书环境,使用 Web Share API + if (navigator.share) { + navigator.share({ title, text: title, url: link }).catch(() => { + navigator.clipboard.writeText(link).then(() => { + Toast.show({ content: '链接已复制', icon: 'success' }) + }).catch(() => { + Toast.show({ content: '复制失败', icon: 'fail' }) + }) + }) + return + } + + // 最后的兜底:复制链接 + navigator.clipboard.writeText(link).then(() => { + Toast.show({ content: '链接已复制', icon: 'success' }) + }).catch(() => { + Toast.show({ content: '复制失败', icon: 'fail' }) + }) + } + + // 标记已售出 + const handleMarkSold = (e: React.MouseEvent) => { + e.stopPropagation() + Dialog.confirm({ + content: t('usedCar.confirmMarkSold'), + onConfirm: async () => { + try { + await usedCarApi.updateStatus(item.id, 2) + Toast.show({ content: '已标记为售出', icon: 'success' }) + onRefresh?.() + } catch (error) { + const message = error instanceof Error ? error.message : '操作失败' + Toast.show({ content: message, icon: 'fail' }) + } + } + }) + } + + const images = getImages(item) + + return ( +
+ {/* 头部 */} +
+ + {t('usedCar.code')}: {item.code || '~'} + + {isMy && ( + + )} +
+ + {/* 卡片主体 */} +
+ {/* 缩略图 */} + {images.length > 0 && ( +
+ +
+ )} + + {/* 信息 */} +
+
+ {item.product_name} + {item.price && ¥{item.price}} +
+ +
+
+ {t('usedCar.sourcePlace')} + {item.source_place || '-'} +
+
+ {t('usedCar.bodyColor')} + {item.body_color || '-'} +
+
+ +
+
+ {t('usedCar.plateTime')} + {formatPlateTime(item.plate_time)} +
+
+ {t('usedCar.mileage')} + {item.mileage ? `${item.mileage} Km` : '-'} +
+
+
+
+ + {/* 底部操作 */} +
+
+ {t('usedCar.createdAt')}: {item.created_at} +
+
+ + + {isMy && item.status === 1 && ( + + )} +
+
+
+ ) +} + +export default CarCard diff --git a/frontend/src/components/CarCard/index.ts b/frontend/src/components/CarCard/index.ts new file mode 100644 index 0000000..b8a04c1 --- /dev/null +++ b/frontend/src/components/CarCard/index.ts @@ -0,0 +1,2 @@ +export { default } from './CarCard' +export type { UsedCarItem } from './CarCard' diff --git a/frontend/src/components/PriceModal.module.css b/frontend/src/components/PriceModal.module.css new file mode 100644 index 0000000..285dbb2 --- /dev/null +++ b/frontend/src/components/PriceModal.module.css @@ -0,0 +1,25 @@ +/* 价格弹框容器 - 覆盖 antd-mobile 默认样式 */ +.modalWrapper :global(.adm-center-popup-wrap) { + width: 85%; + max-width: 420px; + min-width: unset; +} + +/* 弹框内容区域 */ +.modalWrapper :global(.adm-modal-body) { + width: 100%; + border-radius: 16px; + overflow: hidden; +} + +.modalWrapper :global(.adm-modal-content) { + padding: 0; +} + +/* 确保弹框在移动端有合适的宽度 */ +@media (max-width: 480px) { + .modalWrapper :global(.adm-center-popup-wrap) { + width: 90%; + max-width: none; + } +} diff --git a/frontend/src/components/PriceModal.tsx b/frontend/src/components/PriceModal.tsx new file mode 100644 index 0000000..f1e40e8 --- /dev/null +++ b/frontend/src/components/PriceModal.tsx @@ -0,0 +1,269 @@ +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Modal, Input, Toast } from 'antd-mobile' +import { usedCarApi } from '@/services/api' + +interface PriceModalProps { + visible: boolean + onClose: () => void + onConfirm: (data: PriceFormData) => void + item?: any +} + +export interface PriceFormData { + trade_term: string + price: string + currency: string + lang: string +} + +// 贸易条款选项 +const TRADE_TERM_OPTIONS = [ + { value: 'FOB', label: 'FOB' }, + { value: 'CIF', label: 'CIF' }, + { value: 'EXW', label: 'EXW' }, + { value: 'DDP', label: 'DDP' }, + { value: 'CIP', label: 'CIP' }, + { value: 'FCA', label: 'FCA' }, + { value: 'CFR', label: 'CFR' } +] + +// 币种选项 +const CURRENCY_OPTIONS = [ + { value: 'USD', label: '美元' }, + { value: 'CNY', label: '人民币' }, + { value: 'RUB', label: '卢布' }, + { value: 'AED', label: '迪拉姆' }, + { value: 'EUR', label: '欧元' } +] + +// 语言选项 +const LANG_OPTIONS = [ + { value: 'en', label: '英语' }, + { value: 'ru', label: '俄语' }, + { value: 'es', label: '西班牙语' }, + { value: 'ar', label: '阿拉伯语' } +] + +const PriceModal: React.FC = ({ visible, onClose, onConfirm, item }) => { + const { t } = useTranslation() + + const [formData, setFormData] = useState({ + trade_term: 'FOB', + price: '', + currency: 'USD', + lang: 'en' + }) + + const [loading, setLoading] = useState(false) + + // 注入全局样式 - 只在组件挂载时执行一次 + useEffect(() => { + const styleId = 'price-modal-override-style' + let styleEl = document.getElementById(styleId) as HTMLStyleElement + + if (!styleEl) { + styleEl = document.createElement('style') + styleEl.id = styleId + styleEl.innerHTML = ` + .adm-center-popup-wrap { + width: 85% !important; + max-width: 420px !important; + min-width: unset !important; + } + @media (max-width: 480px) { + .adm-center-popup-wrap { + width: 90% !important; + max-width: none !important; + } + } + ` + document.head.appendChild(styleEl) + } + + return () => { + styleEl?.remove() + } + }, []) + + // 重置表单 + useEffect(() => { + if (visible) { + setFormData({ + trade_term: 'FOB', + price: '', + currency: 'USD', + lang: 'en' + }) + } + }, [visible]) + + // 价格输入处理 + const handlePriceInput = (value: string) => { + // 只允许数字和小数点 + const cleanValue = value.replace(/[^\d.]/g, '') + // 只保留第一个小数点 + const parts = cleanValue.split('.') + if (parts.length > 2) { + const finalValue = parts[0] + '.' + parts.slice(1).join('') + setFormData({ ...formData, price: finalValue }) + } else { + setFormData({ ...formData, price: cleanValue }) + } + } + + // 确认生成 + const handleConfirm = async () => { + if (!formData.trade_term) { + return Toast.show({ content: t('usedCar.selectTradeTerm') || '请选择贸易条款', icon: 'fail' }) + } + if (!formData.price) { + return Toast.show({ content: t('usedCar.enterPrice') || '请输入价格', icon: 'fail' }) + } + + setLoading(true) + try { + await onConfirm(formData) + onClose() + } catch (error) { + console.error('生成 PDF 失败:', error) + } finally { + setLoading(false) + } + } + + return ( + + {/* 标题 */} +
+ {t('usedCar.setPrice') || '设置价格'} +
+ + {/* 贸易条款 */} +
+ + +
+ + {/* 价格 */} +
+ + +
+ + {/* 币种 */} +
+ + +
+ + {/* 语言 */} +
+ +
+ {LANG_OPTIONS.map(opt => ( + + ))} +
+
+ + {/* 按钮 */} +
+ + +
+ + } + closeOnMaskClick={false} + onClose={onClose} + /> + ) +} + +export default PriceModal diff --git a/frontend/src/i18n/locales/add-usedcar-create-translations.js b/frontend/src/i18n/locales/add-usedcar-create-translations.js new file mode 100644 index 0000000..5f64adb --- /dev/null +++ b/frontend/src/i18n/locales/add-usedcar-create-translations.js @@ -0,0 +1,223 @@ +const fs = require('fs'); +const path = require('path'); + +// 新增的翻译键 +const newTranslations = { + zh: { + pageTitle: '二手车录入', + bind: '绑定', + bindProduct: '绑定产品', + bindDialog: '选择要绑定的产品', + bindSearchPlaceholder: '搜索产品名称', + bindLoading: '加载中…', + bindEmpty: '未找到匹配的产品', + bindConfirm: '确认绑定', + bindClear: '清除', + bindBound: '已绑定:', + bindSuccess: '绑定成功', + bindCleared: '已清除绑定', + enterProductName: '请输入车辆的品牌 系列查找车型', + enterProductNameFirst: '请先输入产品名称', + bindFetchFailed: '拉取产品失败,请稍后重试', + bindExample: '例:比亚迪海狮07DM', + enterPrice: '请输入车价', + enterSourcePlace: '请输入车源地', + selectPlateTime: '请选择时间', + enterDisplacement: '请输入排量', + enterPower: '请输入马力', + enterMileage: '请输入里程', + enterBodyColor: '请输入车身颜色', + enterConditionDesc: '请输入车况介绍', + uploadImages: '上传产品图', + uploadAttachments: '上传附件', + maxImages: '最多上传20张图片', + maxAttachments: '最多上传20个附件', + uploading: '上传中...', + uploadFailed: '上传失败', + removeImage: '删除', + view: '查看', + requiredField: '必填项', + fillRequiredFields: '请填写所有必填项', + confirmSubmit: '确认提交?' + }, + en: { + pageTitle: 'Add Used Car', + bind: 'Bind', + bindProduct: 'Bind Product', + bindDialog: 'Select Product to Bind', + bindSearchPlaceholder: 'Search product name', + bindLoading: 'Loading…', + bindEmpty: 'No matching products found', + bindConfirm: 'Confirm Bind', + bindClear: 'Clear', + bindBound: 'Bound: ', + bindSuccess: 'Bind successful', + bindCleared: 'Bind cleared', + enterProductName: 'Enter vehicle brand and series to find model', + enterProductNameFirst: 'Please enter product name first', + bindFetchFailed: 'Failed to fetch products, please try again later', + bindExample: 'Ex: BYD Seal 07 DM', + enterPrice: 'Enter price', + enterSourcePlace: 'Enter source place', + selectPlateTime: 'Select time', + enterDisplacement: 'Enter displacement', + enterPower: 'Enter power', + enterMileage: 'Enter mileage', + enterBodyColor: 'Enter body color', + enterConditionDesc: 'Enter condition description', + uploadImages: 'Upload Images', + uploadAttachments: 'Upload Attachments', + maxImages: 'Maximum 20 images', + maxAttachments: 'Maximum 20 attachments', + uploading: 'Uploading...', + uploadFailed: 'Upload failed', + removeImage: 'Remove', + view: 'View', + requiredField: 'Required', + fillRequiredFields: 'Please fill all required fields', + confirmSubmit: 'Confirm submit?' + }, + ru: { + pageTitle: 'Добавить подержанный автомобиль', + bind: 'Привязать', + bindProduct: 'Привязать продукт', + bindDialog: 'Выберите продукт для привязки', + bindSearchPlaceholder: 'Поиск по названию', + bindLoading: 'Загрузка…', + bindEmpty: 'Продукты не найдены', + bindConfirm: 'Подтвердить', + bindClear: 'Очистить', + bindBound: 'Привязано: ', + bindSuccess: 'Привязка успешна', + bindCleared: 'Привязка очищена', + enterProductName: 'Введите бренд и серию автомобиля', + enterProductNameFirst: 'Сначала введите название продукта', + bindFetchFailed: 'Не удалось получить продукты, попробуйте позже', + bindExample: 'Пример: BYD Seal 07 DM', + enterPrice: 'Введите цену', + enterSourcePlace: 'Введите место происхождения', + selectPlateTime: 'Выберите время', + enterDisplacement: 'Введите объем двигателя', + enterPower: 'Введите мощность', + enterMileage: 'Введите пробег', + enterBodyColor: 'Введите цвет кузова', + enterConditionDesc: 'Введите описание состояния', + uploadImages: 'Загрузить изображения', + uploadAttachments: 'Загрузить вложения', + maxImages: 'Максимум 20 изображений', + maxAttachments: 'Максимум 20 вложений', + uploading: 'Загрузка...', + uploadFailed: 'Загрузка не удалась', + removeImage: 'Удалить', + view: 'Просмотр', + requiredField: 'Обязательно', + fillRequiredFields: 'Заполните все обязательные поля', + confirmSubmit: 'Подтвердить отправку?' + }, + es: { + pageTitle: 'Añadir Coche Usado', + bind: 'Vincular', + bindProduct: 'Vincular Producto', + bindDialog: 'Seleccionar Producto para Vincular', + bindSearchPlaceholder: 'Buscar nombre del producto', + bindLoading: 'Cargando…', + bindEmpty: 'No se encontraron productos coincidentes', + bindConfirm: 'Confirmar Vinculación', + bindClear: 'Limpiar', + bindBound: 'Vinculado: ', + bindSuccess: 'Vinculación exitosa', + bindCleared: 'Vinculación borrada', + enterProductName: 'Ingrese la marca y serie del vehículo', + enterProductNameFirst: 'Primero ingrese el nombre del producto', + bindFetchFailed: 'Error al obtener productos, inténtelo más tarde', + bindExample: 'Ej: BYD Seal 07 DM', + enterPrice: 'Ingrese el precio', + enterSourcePlace: 'Ingrese el lugar de origen', + selectPlateTime: 'Seleccionar tiempo', + enterDisplacement: 'Ingrese el desplazamiento', + enterPower: 'Ingrese la potencia', + enterMileage: 'Ingrese el kilometraje', + enterBodyColor: 'Ingrese el color de la carrocería', + enterConditionDesc: 'Ingrese la descripción del estado', + uploadImages: 'Subir Imágenes', + uploadAttachments: 'Subir Adjuntos', + maxImages: 'Máximo 20 imágenes', + maxAttachments: 'Máximo 20 adjuntos', + uploading: 'Subiendo...', + uploadFailed: 'Error al subir', + removeImage: 'Eliminar', + view: 'Ver', + requiredField: 'Requerido', + fillRequiredFields: 'Complete todos los campos requeridos', + confirmSubmit: '¿Confirmar envío?' + }, + fr: { + pageTitle: 'Ajouter une Voiture d\'occasion', + bind: 'Lier', + bindProduct: 'Lier le Produit', + bindDialog: 'Sélectionner un Produit à Lier', + bindSearchPlaceholder: 'Rechercher nom du produit', + bindLoading: 'Chargement…', + bindEmpty: 'Aucun produit correspondant trouvé', + bindConfirm: 'Confirmer la Liaison', + bindClear: 'Effacer', + bindBound: 'Lié : ', + bindSuccess: 'Liaison réussie', + bindCleared: 'Liaison effacée', + enterProductName: 'Entrez la marque et la série du véhicule', + enterProductNameFirst: 'Veuillez d\'abord entrer le nom du produit', + bindFetchFailed: 'Échec de la récupération des produits, réessayez plus tard', + bindExample: 'Ex: BYD Seal 07 DM', + enterPrice: 'Entrez le prix', + enterSourcePlace: 'Entrez le lieu d\'origine', + selectPlateTime: 'Sélectionner l\'heure', + enterDisplacement: 'Entrez la cylindrée', + enterPower: 'Entrez la puissance', + enterMileage: 'Entrez le kilométrage', + enterBodyColor: 'Entrez la couleur de la carrosserie', + enterConditionDesc: 'Entrez la description de l\'état', + uploadImages: 'Télécharger des Images', + uploadAttachments: 'Télécharger des Pièces Jointes', + maxImages: 'Maximum 20 images', + maxAttachments: 'Maximum 20 pièces jointes', + uploading: 'Téléchargement...', + uploadFailed: 'Échec du téléchargement', + removeImage: 'Supprimer', + view: 'Voir', + requiredField: 'Obligatoire', + fillRequiredFields: 'Veuillez remplir tous les champs obligatoires', + confirmSubmit: 'Confirmer l\'envoi ?' + } +}; + +// 语言列表 +const languages = ['zh', 'en', 'ru', 'es', 'fr']; +const localesDir = path.join(__dirname); + +// 更新每个语言文件 +languages.forEach(lang => { + const filePath = path.join(localesDir, `${lang}.json`); + const content = JSON.parse(fs.readFileSync(filePath, 'utf8')); + + // 添加 usedCar 的新键(如果不存在) + if (!content.usedCar) { + content.usedCar = {}; + } + + const newKeys = newTranslations[lang]; + Object.keys(newKeys).forEach(key => { + if (!(key in content.usedCar)) { + content.usedCar[key] = newKeys[key]; + } + }); + + // 添加 common.clear (如果不存在) + if (!content.common.clear) { + content.common.clear = newTranslations[lang].clear || '清除'; + } + + fs.writeFileSync(filePath, JSON.stringify(content, null, 2), 'utf8'); + console.log(`Updated ${lang}.json`); +}); + +console.log('All translation files updated successfully!'); diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 88cb16d..a916284 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -29,7 +29,8 @@ "status": "Status", "remark": "Remark", "createTime": "Create Time", - "updateTime": "Update Time" + "updateTime": "Update Time", + "clear": "Clear" }, "dashboard": { "welcome": "Welcome Back", @@ -186,5 +187,104 @@ "lastMonth": "Last Month", "thisYear": "This Year", "lastYear": "Last Year" + }, + "usedCar": { + "title": "Used Car", + "code": "Code", + "search": "Search", + "searchPlaceholder": "Search vehicle name", + "status": "Status", + "statusNormal": "Normal", + "statusArchived": "Archived", + "statusSold": "Sold", + "markSold": "Mark as Sold", + "downloadPdf": "Download PDF", + "share": "Share", + "empty": "No used cars", + "loadFailed": "Failed to load", + "pdfDownloadFailed": "Failed to download PDF", + "sourcePlace": "Source Place", + "bodyColor": "Body Color", + "plateTime": "Plate Time", + "mileage": "Mileage", + "createdAt": "Created At", + "productName": "Product Name", + "vin": "VIN", + "price": "Price", + "displacement": "Displacement", + "power": "Power", + "conditionDesc": "Condition Description", + "productImages": "Product Images", + "attachments": "Attachments", + "create": "Add Used Car", + "edit": "Edit", + "delete": "Delete", + "detail": "Detail", + "submit": "Submit", + "cancel": "Cancel", + "confirm": "Confirm", + "confirmDelete": "Confirm to delete this used car?", + "confirmMarkSold": "Confirm to mark as sold?", + "deleteSuccess": "Deleted successfully", + "updateSuccess": "Updated successfully", + "createSuccess": "Created successfully", + "setPrice": "Set Price", + "tradeTerm": "Trade Term", + "selectTradeTerm": "Please select trade term", + "enterPrice": "Enter price", + "currency": "Currency", + "language": "Language", + "pdfDownloadSuccess": "PDF downloaded successfully", + "linkCopied": "Link copied", + "notFound": "Vehicle not found", + "pageTitle": "Add Used Car", + "bind": "Bind", + "bindProduct": "Bind Product", + "bindDialog": "Select Product to Bind", + "bindSearchPlaceholder": "Search product name", + "bindLoading": "Loading…", + "bindEmpty": "No matching products found", + "bindConfirm": "Confirm Bind", + "bindClear": "Clear", + "bindBound": "Bound: ", + "bindSuccess": "Bind successful", + "bindCleared": "Bind cleared", + "enterProductName": "Enter vehicle brand and series to find model", + "enterProductNameFirst": "Please enter product name first", + "bindFetchFailed": "Failed to fetch products, please try again later", + "bindExample": "Ex: BYD Seal 07 DM", + "enterSourcePlace": "Enter source place", + "selectPlateTime": "Select time", + "enterDisplacement": "Enter displacement", + "enterPower": "Enter power", + "enterMileage": "Enter mileage", + "enterBodyColor": "Enter body color", + "enterConditionDesc": "Enter condition description", + "uploadImages": "Upload Images", + "uploadAttachments": "Upload Attachments", + "maxImages": "Maximum 20 images", + "maxAttachments": "Maximum 20 attachments", + "uploading": "Uploading...", + "uploadFailed": "Upload failed", + "removeImage": "Remove", + "view": "View", + "requiredField": "Required", + "fillRequiredFields": "Please fill all required fields", + "confirmSubmit": "Confirm submit?", + "myPublish": "My Publish", + "myGarage": "My Garage", + "quickEdit": "Quick Edit", + "emptyMyPublish": "No published vehicles yet", + "myPage": "Mine", + "personalInfo": "Personal Info", + "bindDialog": "Select Car Model", + "brand": "Brand", + "series": "Series", + "bindSearchPlaceholder": "Search car model", + "bindSearching": "Searching...", + "bindSearchFirst": "Please enter product name to search", + "bindNoModels": "No car models found", + "bindNoResults": "No matching results", + "bindConfirm": "Confirm Bind" } -} +} \ No newline at end of file diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index d8fcaf7..317e839 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -29,7 +29,8 @@ "status": "Estado", "remark": "Observación", "createTime": "Fecha de creación", - "updateTime": "Fecha de actualización" + "updateTime": "Fecha de actualización", + "clear": "Limpiar" }, "dashboard": { "welcome": "Bienvenido de nuevo", @@ -161,5 +162,99 @@ "lastMonth": "Mes Pasado", "thisYear": "Este Año", "lastYear": "Año Pasado" + }, + "usedCar": { + "title": "Coche de Segunda Mano", + "code": "Código", + "search": "Buscar", + "searchPlaceholder": "Buscar vehículo", + "status": "Estado", + "statusNormal": "Normal", + "statusArchived": "Archivado", + "statusSold": "Vendido", + "markSold": "Marcar como vendido", + "downloadPdf": "Descargar PDF", + "share": "Compartir", + "empty": "Sin coches", + "loadFailed": "Error al cargar", + "pdfDownloadFailed": "Error al descargar PDF", + "sourcePlace": "Lugar de origen", + "bodyColor": "Color de carrocería", + "plateTime": "Fecha de matriculación", + "mileage": "Kilometraje", + "createdAt": "Creado en", + "productName": "Nombre del producto", + "vin": "VIN", + "price": "Precio", + "displacement": "Cilindrada", + "power": "Potencia", + "conditionDesc": "Descripción del estado", + "productImages": "Imágenes del producto", + "attachments": "Adjuntos", + "create": "Añadir coche", + "edit": "Editar", + "delete": "Eliminar", + "detail": "Detalle", + "submit": "Enviar", + "cancel": "Cancelar", + "confirm": "Confirmar", + "confirmDelete": "¿Confirmar eliminar?", + "confirmMarkSold": "¿Marcar como vendido?", + "deleteSuccess": "Eliminado", + "updateSuccess": "Actualizado", + "createSuccess": "Creado", + "setPrice": "Establecer precio", + "tradeTerm": "Término comercial", + "selectTradeTerm": "Seleccione término comercial", + "enterPrice": "Ingrese el precio", + "currency": "Moneda", + "language": "Idioma", + "pdfDownloadSuccess": "PDF descargado con éxito", + "linkCopied": "Enlace copiado", + "notFound": "Vehículo no encontrado", + "pageTitle": "Añadir Coche Usado", + "bind": "Vincular", + "bindProduct": "Vincular Producto", + "bindDialog": "Seleccionar Modelo", + "brand": "Marca", + "series": "Serie", + "bindSearchPlaceholder": "Buscar modelo", + "bindSearching": "Buscando...", + "bindSearchFirst": "Primero ingrese el nombre del producto", + "bindNoModels": "No se encontraron modelos", + "bindNoResults": "Sin resultados", + "bindConfirm": "Confirmar Vinculación", + "bindClear": "Limpiar", + "bindBound": "Vinculado: ", + "bindSuccess": "Vinculación exitosa", + "bindCleared": "Vinculación borrada", + "enterProductName": "Ingrese la marca y serie del vehículo", + "enterProductNameFirst": "Primero ingrese el nombre del producto", + "bindFetchFailed": "Error al obtener productos, inténtelo más tarde", + "bindExample": "Ej: BYD Seal 07 DM", + "enterSourcePlace": "Ingrese el lugar de origen", + "selectPlateTime": "Seleccionar tiempo", + "enterDisplacement": "Ingrese el desplazamiento", + "enterPower": "Ingrese la potencia", + "enterMileage": "Ingrese el kilometraje", + "enterBodyColor": "Ingrese el color de la carrocería", + "enterConditionDesc": "Ingrese la descripción del estado", + "uploadImages": "Subir Imágenes", + "uploadAttachments": "Subir Adjuntos", + "maxImages": "Máximo 20 imágenes", + "maxAttachments": "Máximo 20 adjuntos", + "uploading": "Subiendo...", + "uploadFailed": "Error al subir", + "removeImage": "Eliminar", + "view": "Ver", + "requiredField": "Requerido", + "fillRequiredFields": "Complete todos los campos requeridos", + "confirmSubmit": "¿Confirmar envío?", + "myPublish": "Mis Publicaciones", + "myGarage": "Mi Garaje", + "quickEdit": "Edición Rápida", + "emptyMyPublish": "Aún no has publicado vehículos", + "myPage": "Mío", + "personalInfo": "Información personal" } } diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 0922bdd..4b5db41 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -29,7 +29,8 @@ "status": "Statut", "remark": "Remarque", "createTime": "Date de création", - "updateTime": "Date de mise à jour" + "updateTime": "Date de mise à jour", + "clear": "Effacer" }, "dashboard": { "welcome": "Bienvenue", @@ -161,5 +162,99 @@ "lastMonth": "Mois Dernier", "thisYear": "Cette Année", "lastYear": "Année Dernière" + }, + "usedCar": { + "title": "Voiture d'Occasion", + "code": "Code", + "search": "Rechercher", + "searchPlaceholder": "Rechercher véhicule", + "status": "Statut", + "statusNormal": "Normal", + "statusArchived": "Archivé", + "statusSold": "Vendu", + "markSold": "Marquer comme vendu", + "downloadPdf": "Télécharger PDF", + "share": "Partager", + "empty": "Aucune voiture", + "loadFailed": "Échec du chargement", + "pdfDownloadFailed": "Échec du téléchargement PDF", + "sourcePlace": "Lieu d'origine", + "bodyColor": "Couleur de la carrosserie", + "plateTime": "Date d'immatriculation", + "mileage": "Kilométrage", + "createdAt": "Créé le", + "productName": "Nom du produit", + "vin": "VIN", + "price": "Prix", + "displacement": "Cylindrée", + "power": "Puissance", + "conditionDesc": "Description de l'état", + "productImages": "Images du produit", + "attachments": "Pièces jointes", + "create": "Ajouter voiture", + "edit": "Modifier", + "delete": "Supprimer", + "detail": "Détail", + "submit": "Soumettre", + "cancel": "Annuler", + "confirm": "Confirmer", + "confirmDelete": "Confirmer la suppression?", + "confirmMarkSold": "Marquer comme vendu?", + "deleteSuccess": "Supprimé", + "updateSuccess": "Mis à jour", + "createSuccess": "Créé", + "setPrice": "Définir le prix", + "tradeTerm": "Terme commercial", + "selectTradeTerm": "Sélectionnez le terme commercial", + "enterPrice": "Entrez le prix", + "currency": "Devise", + "language": "Langue", + "pdfDownloadSuccess": "PDF téléchargé avec succès", + "linkCopied": "Lien copié", + "notFound": "Véhicule introuvable", + "pageTitle": "Ajouter une Voiture d'occasion", + "bind": "Lier", + "bindProduct": "Lier le Produit", + "bindDialog": "Sélectionner le Modèle", + "brand": "Marque", + "series": "Série", + "bindSearchPlaceholder": "Rechercher modèle", + "bindSearching": "Recherche...", + "bindSearchFirst": "Veuillez d'abord entrer le nom du produit", + "bindNoModels": "Aucun modèle trouvé", + "bindNoResults": "Aucun résultat", + "bindConfirm": "Confirmer la Liaison", + "bindClear": "Effacer", + "bindBound": "Lié : ", + "bindSuccess": "Liaison réussie", + "bindCleared": "Liaison effacée", + "enterProductName": "Entrez la marque et la série du véhicule", + "enterProductNameFirst": "Veuillez d'abord entrer le nom du produit", + "bindFetchFailed": "Échec de la récupération des produits, réessayez plus tard", + "bindExample": "Ex: BYD Seal 07 DM", + "enterSourcePlace": "Entrez le lieu d'origine", + "selectPlateTime": "Sélectionner l'heure", + "enterDisplacement": "Entrez la cylindrée", + "enterPower": "Entrez la puissance", + "enterMileage": "Entrez le kilométrage", + "enterBodyColor": "Entrez la couleur de la carrosserie", + "enterConditionDesc": "Entrez la description de l'état", + "uploadImages": "Télécharger des Images", + "uploadAttachments": "Télécharger des Pièces Jointes", + "maxImages": "Maximum 20 images", + "maxAttachments": "Maximum 20 pièces jointes", + "uploading": "Téléchargement...", + "uploadFailed": "Échec du téléchargement", + "removeImage": "Supprimer", + "view": "Voir", + "requiredField": "Obligatoire", + "fillRequiredFields": "Veuillez remplir tous les champs obligatoires", + "confirmSubmit": "Confirmer l'envoi ?", + "myPublish": "Mes Publications", + "myGarage": "Mon Garage", + "quickEdit": "Modification Rapide", + "emptyMyPublish": "Vous n'avez pas encore publié de véhicules", + "myPage": "Mon", + "personalInfo": "Informations personnelles" } } diff --git a/frontend/src/i18n/locales/ru.json b/frontend/src/i18n/locales/ru.json index 206daa5..eaec2ed 100644 --- a/frontend/src/i18n/locales/ru.json +++ b/frontend/src/i18n/locales/ru.json @@ -29,7 +29,8 @@ "status": "Статус", "remark": "Примечание", "createTime": "Время создания", - "updateTime": "Время обновления" + "updateTime": "Время обновления", + "clear": "Очистить" }, "dashboard": { "welcome": "Добро пожаловать", @@ -161,5 +162,99 @@ "lastMonth": "В прошлом месяце", "thisYear": "В этом году", "lastYear": "В прошлом году" + }, + "usedCar": { + "title": "Подержанные автомобили", + "code": "Код", + "search": "Поиск", + "searchPlaceholder": "Поиск автомобиля", + "status": "Статус", + "statusNormal": "Норма", + "statusArchived": "Архив", + "statusSold": "Продан", + "markSold": "Пометить как проданный", + "downloadPdf": "Скачать PDF", + "share": "Поделиться", + "empty": "Нет подержанных автомобилей", + "loadFailed": "Ошибка загрузки", + "pdfDownloadFailed": "Ошибка загрузки PDF", + "sourcePlace": "Место происхождения", + "bodyColor": "Цвет кузова", + "plateTime": "Дата регистрации", + "mileage": "Пробег", + "createdAt": "Дата создания", + "productName": "Название продукта", + "vin": "VIN", + "price": "Цена", + "displacement": "Объем двигателя", + "power": "Мощность", + "conditionDesc": "Описание состояния", + "productImages": "Фотографии продукта", + "attachments": "Вложения", + "create": "Добавить автомобиль", + "edit": "Редактировать", + "delete": "Удалить", + "detail": "Детали", + "submit": "Отправить", + "cancel": "Отмена", + "confirm": "Подтвердить", + "confirmDelete": "Подтвердить удаление?", + "confirmMarkSold": "Пометить как проданный?", + "deleteSuccess": "Удалено", + "updateSuccess": "Обновлено", + "createSuccess": "Создано", + "setPrice": "Установить цену", + "tradeTerm": "Условия торговли", + "selectTradeTerm": "Выберите условия торговли", + "enterPrice": "Введите цену", + "currency": "Валюта", + "language": "Язык", + "pdfDownloadSuccess": "PDF успешно загружен", + "linkCopied": "Ссылка скопирована", + "notFound": "Автомобиль не найден", + "pageTitle": "Добавить подержанный автомобиль", + "bind": "Привязать", + "bindProduct": "Привязать продукт", + "bindDialog": "Выберите модель", + "brand": "Бренд", + "series": "Серия", + "bindSearchPlaceholder": "Поиск модели", + "bindSearching": "Поиск...", + "bindSearchFirst": "Сначала введите название продукта", + "bindNoModels": "Модели не найдены", + "bindNoResults": "Нет результатов", + "bindConfirm": "Подтвердить", + "bindClear": "Очистить", + "bindBound": "Привязано: ", + "bindSuccess": "Привязка успешна", + "bindCleared": "Привязка очищена", + "enterProductName": "Введите бренд и серию автомобиля", + "enterProductNameFirst": "Сначала введите название продукта", + "bindFetchFailed": "Не удалось получить продукты, попробуйте позже", + "bindExample": "Пример: BYD Seal 07 DM", + "enterSourcePlace": "Введите место происхождения", + "selectPlateTime": "Выберите время", + "enterDisplacement": "Введите объем двигателя", + "enterPower": "Введите мощность", + "enterMileage": "Введите пробег", + "enterBodyColor": "Введите цвет кузова", + "enterConditionDesc": "Введите описание состояния", + "uploadImages": "Загрузить изображения", + "uploadAttachments": "Загрузить вложения", + "maxImages": "Максимум 20 изображений", + "maxAttachments": "Максимум 20 вложений", + "uploading": "Загрузка...", + "uploadFailed": "Загрузка не удалась", + "removeImage": "Удалить", + "view": "Просмотр", + "requiredField": "Обязательно", + "fillRequiredFields": "Заполните все обязательные поля", + "confirmSubmit": "Подтвердить отправку?", + "myPublish": "Мои объявления", + "myGarage": "Мой гараж", + "quickEdit": "Быстрое редактирование", + "emptyMyPublish": "У вас пока нет опубликованных автомобилей", + "myPage": "Мой", + "personalInfo": "Личная информация" } } diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index 9209488..4b26ac2 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -29,7 +29,8 @@ "status": "状态", "remark": "备注", "createTime": "创建时间", - "updateTime": "更新时间" + "updateTime": "更新时间", + "clear": "清除" }, "dashboard": { "welcome": "欢迎回来", @@ -186,5 +187,99 @@ "lastMonth": "上月", "thisYear": "今年", "lastYear": "去年" + }, + "usedCar": { + "title": "二手车", + "code": "编号", + "search": "搜索", + "searchPlaceholder": "搜索车辆名称", + "status": "状态", + "statusNormal": "正常", + "statusArchived": "归档", + "statusSold": "已售出", + "markSold": "标记已售出", + "downloadPdf": "下载PDF", + "share": "分享", + "empty": "暂无二手车", + "loadFailed": "加载失败", + "pdfDownloadFailed": "PDF下载失败", + "sourcePlace": "车源地", + "bodyColor": "车身颜色", + "plateTime": "上牌时间", + "mileage": "里程", + "createdAt": "创建时间", + "productName": "产品名称", + "vin": "车架号", + "price": "车价", + "displacement": "排量", + "power": "马力", + "conditionDesc": "车况介绍", + "productImages": "产品图", + "attachments": "附件", + "create": "添加二手车", + "edit": "编辑", + "delete": "删除", + "detail": "详情", + "submit": "提交", + "cancel": "取消", + "confirm": "确认", + "confirmDelete": "确认删除此二手车?", + "confirmMarkSold": "确认标记为已售出?", + "deleteSuccess": "删除成功", + "updateSuccess": "更新成功", + "createSuccess": "创建成功", + "setPrice": "设置价格", + "tradeTerm": "贸易条款", + "selectTradeTerm": "请选择贸易条款", + "enterPrice": "请输入价格", + "currency": "币种", + "language": "语言", + "pdfDownloadSuccess": "PDF下载成功", + "linkCopied": "链接已复制", + "notFound": "未找到该车辆", + "pageTitle": "二手车录入", + "bind": "绑定", + "bindProduct": "绑定产品", + "bindDialog": "选择车型", + "brand": "品牌", + "series": "车系", + "bindSearchPlaceholder": "搜索车型", + "bindSearching": "搜索中...", + "bindSearchFirst": "请先输入产品名称搜索", + "bindNoModels": "未找到相关车型", + "bindNoResults": "未找到匹配结果", + "bindConfirm": "确认绑定", + "bindClear": "清除", + "bindBound": "已绑定:", + "bindSuccess": "绑定成功", + "bindCleared": "已清除绑定", + "enterProductName": "请输入车辆的品牌 系列查找车型", + "enterProductNameFirst": "请先输入产品名称", + "bindFetchFailed": "拉取产品失败,请稍后重试", + "bindExample": "例:比亚迪海狮07DM", + "enterSourcePlace": "请输入车源地", + "selectPlateTime": "请选择时间", + "enterDisplacement": "请输入排量", + "enterPower": "请输入马力", + "enterMileage": "请输入里程", + "enterBodyColor": "请输入车身颜色", + "enterConditionDesc": "请输入车况介绍", + "uploadImages": "上传产品图", + "uploadAttachments": "上传附件", + "maxImages": "最多上传20张图片", + "maxAttachments": "最多上传20个附件", + "uploading": "上传中...", + "uploadFailed": "上传失败", + "removeImage": "删除", + "view": "查看", + "requiredField": "必填项", + "fillRequiredFields": "请填写所有必填项", + "confirmSubmit": "确认提交?", + "myPublish": "我的发布", + "myGarage": "我的车库", + "quickEdit": "快捷编辑", + "emptyMyPublish": "暂无发布车辆", + "myPage": "我的", + "personalInfo": "个人信息" } } diff --git a/frontend/src/pages/dashboard/index.tsx b/frontend/src/pages/dashboard/index.tsx index fba237f..d7184f5 100644 --- a/frontend/src/pages/dashboard/index.tsx +++ b/frontend/src/pages/dashboard/index.tsx @@ -35,12 +35,15 @@ const quickActions: QuickAction[] = [ { id: '1', icon: '✈️', label: '出差申请', path: '/business-trip/apply', color: '#1677ff', description: '提交出差申请' }, { id: '2', icon: '📝', label: '采购申请', path: '/purchase/apply', color: '#52c41a', description: '物品采购审批', disabled: true }, { id: '3', icon: '📦', label: '备货录入', path: '/stock/entry', color: '#faad14', description: '库存备货录入', disabled: true }, + { id: '4', icon: '🚗', label: '发布车辆', path: '/used-car/create', color: '#722ed1', description: '发布二手车' }, + { id: '5', icon: '📋', label: '我的发布', path: '/used-car/my-list', color: '#722ed1', description: '我发布的车辆' }, ] const allApps: QuickAction[] = [ { id: '1', icon: '✍️', label: '审批', path: '/approval', color: '#52c41a', description: '待办事项审批' }, { id: '2', icon: '📊', label: '数据统计', path: '/reports', color: '#1890ff', description: '业务数据统计', disabled: true }, { id: '3', icon: '⚙️', label: '系统设置', path: '/settings', color: '#8c8c8c', description: '系统偏好设置' }, + { id: '4', icon: '🚗', label: '二手车', path: '/used-car', color: '#722ed1', description: '二手车管理' }, ] export default function Dashboard() { diff --git a/frontend/src/pages/settings/index.tsx b/frontend/src/pages/settings/index.tsx index 4f8d894..f33a820 100644 --- a/frontend/src/pages/settings/index.tsx +++ b/frontend/src/pages/settings/index.tsx @@ -17,6 +17,18 @@ export default function Settings() { const { t } = useTranslation() const settingGroups = [ + { + title: t('profile.myInfo'), + items: [ + { + id: 'personal-info', + icon: '👤', + label: t('usedCar.personalInfo'), + description: t('usedCar.personalInfo'), + path: '/used-car/profile', + }, + ], + }, { title: t('profile.security'), items: [ diff --git a/frontend/src/pages/used-car/create.module.css b/frontend/src/pages/used-car/create.module.css new file mode 100644 index 0000000..5ec2657 --- /dev/null +++ b/frontend/src/pages/used-car/create.module.css @@ -0,0 +1,718 @@ +.createPage { + min-height: 100vh; + background: #f4f6f8; + padding-bottom: 100px; +} + +.createContent { + padding: 6px; +} + +/* 表单卡片 */ +.formCard { + background: #fff; + border-radius: 14px; + overflow: hidden; + margin-bottom: 12px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); +} + +/* 表单单元格 */ +.formCell { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-bottom: 1px solid #f0f0f0; + min-height: 52px; +} + +.formCell:last-child { + border-bottom: none; +} + +.formCellVertical { + flex-direction: column; + align-items: flex-start; + gap: 12px; +} + +.formLabel { + font-size: 15px; + font-weight: 500; + color: #333; + min-width: 80px; + flex-shrink: 0; +} + +.required { + color: #ff4d4f; + margin-left: 2px; +} + +.formCellVertical .formLabel { + width: 100%; + min-width: 0; + margin-bottom: 4px; +} + +/* 输入框样式 */ +.admInputWrapper { + flex: 1; + min-width: 0; +} + +.admInputWrapper :global(.adm-input) { + --font-size: 15px; + --color-text: #333; + background: transparent; + padding: 0; +} + +.admInputWrapper :global(.adm-input::placeholder) { + color: #bbb; +} + +/* 产品名称 + 绑定按钮 */ +.inputWithAction { + display: flex; + align-items: center; + gap: 12px; + width: 100%; +} + +.inputWithAction .admInputWrapper { + flex: 1; +} + +.bindBtn { + flex-shrink: 0; + padding: 0 16px; + height: 32px; + border-radius: 8px; + background: #2E6CF6; + color: #fff; + font-size: 13px; + font-weight: 500; + border: none; + transition: all 0.2s; +} + +.bindBtn:active { + opacity: 0.8; + transform: scale(0.98); +} + +/* 绑定提示 */ +.bindTip { + padding: 10px 16px; + background: #F3F6FF; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 13px; + margin: 0 0px 12px; + border-radius: 10px; +} + +.bindTipText { + color: #2B3A55; +} + +.bindTipClear { + color: #2E6CF6; + cursor: pointer; + font-weight: 500; +} + +/* 选择器触发器 */ +.pickerTrigger { + display: flex; + align-items: center; + justify-content: flex-end; + min-height: 24px; + cursor: pointer; + flex: 1; +} + +.pickerTrigger::after { + content: ''; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid #bbb; + margin-left: 8px; +} + +.pickerValue { + font-size: 15px; + color: #333; +} + +.pickerPlaceholder { + font-size: 15px; + color: #bbb; +} + +/* 表单区域 */ +.formSection { + background: #fff; + border-radius: 14px; + padding: 16px; + margin-bottom: 12px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); +} + +.formSectionLabel { + font-size: 15px; + font-weight: 600; + color: #111827; + margin-bottom: 12px; + display: block; +} + +.imageUploadArea { + min-height: 80px; +} + +.attachmentArea { + display: flex; + flex-direction: column; + gap: 12px; +} + +.uploadBtn { + padding: 10px 20px; + border-radius: 10px; + background: #2E6CF6; + color: #fff; + font-size: 14px; + font-weight: 500; + border: none; + transition: all 0.2s; + align-self: flex-start; +} + +.uploadBtn:active { + opacity: 0.8; + transform: scale(0.98); +} + +.uploadBtn:disabled { + opacity: 0.5; + background: #ccc; + cursor: not-allowed; +} + +.attachmentList { + display: flex; + flex-direction: column; + gap: 8px; +} + +.attachmentItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: #f9f9f9; + border-radius: 10px; + border: 1px solid #e8e8e8; +} + +.attachmentName { + font-size: 14px; + color: #333; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.attachmentDelete { + color: #ff4d4f; + font-size: 13px; + cursor: pointer; + margin-left: 12px; + flex-shrink: 0; +} + +.attachmentDelete:active { + opacity: 0.7; +} + +.attachmentHint { + padding: 24px; + text-align: center; + color: #999; + font-size: 14px; + background: #f9f9f9; + border-radius: 10px; + border: 1px dashed #ddd; +} + +/* 提交栏 */ +.submitBar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 12px 16px; + padding-bottom: max(12px, env(safe-area-inset-bottom)); + background: #fff; + border-top: 1px solid #f0f0f0; + box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.06); + z-index: 100; +} + +.submitBtn { + height: 48px; + border-radius: 24px; + background: #3F435B; + color: #fff; + font-size: 16px; + font-weight: 600; + border: none; + box-shadow: 0 4px 12px rgba(63, 67, 91, 0.3); + transition: all 0.2s; +} + +.submitBtn:active { + opacity: 0.9; + transform: scale(0.98); +} + +/* ========== 绑定弹窗样式 ========== */ + +/* 遮罩层 */ +:global(.adm-modal-mask) { + background: rgba(0, 0, 0, 0.45) !important; +} + +/* 弹窗容器 */ +:global(.adm-modal-body) { + border-radius: 20px !important; + overflow: hidden !important; + max-width: 420px !important; + /* width: 92% !important; */ + max-height: 80vh !important; +} + +:global(.adm-modal-content) { + padding: 0 !important; + border-radius: 20px !important; + overflow: hidden !important; + max-height: 80vh !important; + display: flex !important; + flex-direction: column !important; +} + +/* 弹窗内容区域 */ +.bindDialogContent { + display: flex; + flex-direction: column; + max-height: 80vh; + overflow: hidden; +} + +/* 弹窗头部 */ +.bindDialogHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18px 20px; + background: #fff; + border-bottom: 1px solid #f0f0f0; + flex-shrink: 0; +} + +.bindDialogTitle { + font-size: 17px; + font-weight: 600; + color: #111827; + letter-spacing: 0.3px; +} + +.bindDialogClose { + font-size: 28px; + color: #999; + cursor: pointer; + line-height: 1; + padding: 4px 8px; + margin: -4px -8px; + border-radius: 50%; + transition: all 0.2s; +} + +.bindDialogClose:hover { + color: #666; + background: #f5f5f5; +} + +.bindDialogClose:active { + transform: scale(0.9); +} + +/* 品牌/车系信息区 */ +.bindBrandInfo { + padding: 14px 20px; + background: linear-gradient(135deg, #F8FAFC 0%, #F3F6FF 100%); + border-bottom: 1px solid #e8ecf0; + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.bindBrandInfoLabel { + font-size: 13px; + color: #64748b; + font-weight: 500; +} + +.bindBrandInfoValue { + font-size: 15px; + color: #0f172a; + font-weight: 600; +} + +.bindBrandInfoSeparator { + color: #cbd5e1; + font-size: 14px; +} + +/* 搜索框区域 */ +.bindSearchArea { + padding: 12px 16px; + background: #fff; + flex-shrink: 0; +} + +.bindSearch { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + background: #F8FAFC; + border-radius: 14px; + border: 1px solid #e2e8f0; + transition: all 0.2s; +} + +.bindSearch:focus-within { + background: #fff; + border-color: #2E6CF6; + box-shadow: 0 0 0 3px rgba(46, 108, 246, 0.1); +} + +.bindSearch :global(.adm-input) { + flex: 1; + background: transparent; + --font-size: 15px; + --color-text: #333; +} + +.bindSearch :global(.adm-input::placeholder) { + color: #94a3b8; +} + +.bindSearchClear { + color: #2E6CF6; + font-size: 14px; + cursor: pointer; + white-space: nowrap; + font-weight: 500; + padding: 6px 10px; + border-radius: 8px; + transition: all 0.2s; +} + +.bindSearchClear:active { + background: #eff6ff; +} + +/* 车型列表区域 */ +.bindList { + flex: 1; + overflow-y: auto; + padding: 8px 16px; + background: #f8fafc; + min-height: 200px; + max-height: 40vh; +} + +.bindList::-webkit-scrollbar { + width: 4px; +} + +.bindList::-webkit-scrollbar-track { + background: transparent; +} + +.bindList::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} + +/* 加载和空状态 */ +.bindLoading, +.bindEmpty { + padding: 60px 20px; + text-align: center; + color: #94a3b8; + font-size: 14px; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.bindLoading::before { + content: ''; + width: 32px; + height: 32px; + border: 3px solid #e2e8f0; + border-top-color: #2E6CF6; + border-radius: 50%; + animation: bindSpin 0.8s linear infinite; +} + +@keyframes bindSpin { + to { + transform: rotate(360deg); + } +} + +/* 车型列表项 */ +.bindItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + margin-bottom: 8px; + border-radius: 14px; + cursor: pointer; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + background: #fff; + border: 2px solid transparent; + position: relative; + overflow: hidden; +} + +.bindItem::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(46, 108, 246, 0.05) 0%, rgba(46, 108, 246, 0.02) 100%); + opacity: 0; + transition: opacity 0.25s; +} + +.bindItem:active { + transform: scale(0.98); +} + +.bindItem.active { + background: linear-gradient(135deg, #F0F5FF 0%, #E8F0FF 100%); + border-color: #2E6CF6; + box-shadow: 0 4px 12px rgba(46, 108, 246, 0.15); +} + +.bindItem.active::before { + opacity: 1; +} + +.bindItemMain { + flex: 1; + padding-right: 16px; + min-width: 0; + position: relative; + z-index: 1; +} + +.bindItemTitle { + font-size: 15px; + font-weight: 600; + color: #1e293b; + margin-bottom: 6px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + letter-spacing: 0.2px; +} + +.bindItem.active .bindItemTitle { + color: #2E6CF6; +} + +.bindItemSub { + font-size: 13px; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: flex; + align-items: center; + gap: 6px; +} + +.bindItemSub::before { + content: '¥'; + font-size: 11px; + color: #94a3b8; +} + +.bindItem.active .bindItemSub { + color: #475569; +} + +/* 单选按钮样式 */ +.bindItemRadio { + width: 22px; + height: 22px; + border-radius: 50%; + border: 2px solid #cbd5e1; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + z-index: 1; + background: #fff; +} + +.bindItem.active .bindItemRadio { + border-color: #2E6CF6; + background: #2E6CF6; + box-shadow: 0 2px 8px rgba(46, 108, 246, 0.3); +} + +.bindItemRadioDot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #fff; + transform: scale(0); + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.bindItem.active .bindItemRadioDot { + transform: scale(1); +} + +/* 底部按钮区 - 固定在底部 */ +.bindDialogFooter { + display: flex; + gap: 12px; + padding: 16px 20px; + padding-bottom: max(16px, env(safe-area-inset-bottom)); + background: #fff; + border-top: 1px solid #f0f0f0; + flex-shrink: 0; + position: sticky; + bottom: 0; +} + +.bindCancelBtn, +.bindConfirmBtn { + flex: 1; + height: 48px; + border-radius: 14px; + font-size: 16px; + font-weight: 600; + border: none; + cursor: pointer; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + letter-spacing: 0.3px; +} + +.bindCancelBtn { + background: #f5f6f8; + color: #475569; +} + +.bindCancelBtn:active { + background: #e8eaed; + transform: scale(0.97); +} + +.bindConfirmBtn { + background: linear-gradient(135deg, #2E6CF6 0%, #2563eb 100%); + color: #fff; + box-shadow: 0 4px 12px rgba(46, 108, 246, 0.25); +} + +.bindConfirmBtn:active { + transform: scale(0.97); + box-shadow: 0 2px 6px rgba(46, 108, 246, 0.25); +} + +.bindConfirmBtn:disabled { + opacity: 0.5; + background: #e2e8f0; + box-shadow: none; + cursor: not-allowed; +} + +.bindConfirmBtn:disabled:active { + transform: none; +} + +/* 图片上传区域 */ +.admImageUploader { + --cell-size: 80px; +} + +.admImageUploader :global(.adm-image-uploader-cell-mask) { + border-radius: 10px; +} + +/* TextArea 样式 */ +.admTextAreaWrapper :global(.adm-text-area) { + --font-size: 15px; + --color-text: #333; + background: #f9f9f9; + border-radius: 10px; + padding: 12px; +} + +/* 兼容旧的 checkDot 类名 */ +.checkDot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #fff; + transform: scale(0); + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +/* 兼容旧的 bindItemCheck 类名 */ +.bindItemCheck { + width: 22px; + height: 22px; + border-radius: 50%; + border: 2px solid #cbd5e1; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + z-index: 1; + background: #fff; +} + +.bindItem.active .bindItemCheck { + border-color: #2E6CF6; + background: #2E6CF6; + box-shadow: 0 2px 8px rgba(46, 108, 246, 0.3); +} + +.bindItem.active .checkDot { + transform: scale(1); +} diff --git a/frontend/src/pages/used-car/create.tsx b/frontend/src/pages/used-car/create.tsx new file mode 100644 index 0000000..72865bc --- /dev/null +++ b/frontend/src/pages/used-car/create.tsx @@ -0,0 +1,594 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { + NavBar, + Input, + Button, + DatePicker, + Toast, + TextArea, + ImageUploader, + Modal +} from 'antd-mobile' +import { uploadApi, usedCarApi } from '@/services/api' +import styles from './create.module.css' + +// 车型接口 +interface CarModel { + id: number + name: string + url: string + msrp: number +} + +// 关键词搜索结果接口 +interface KeywordSearchResult { + brand: string + series: string + series_id: string + models: CarModel[] +} + +// 绑定状态接口 +interface BoundProduct { + brand?: string + series?: string + series_id?: string + model?: CarModel +} + +// 图片文件项接口 +interface ImageFileItem { + url: string + file?: File +} + +const UsedCarCreate: React.FC = () => { + const { t } = useTranslation() + const navigate = useNavigate() + + // 表单数据 + const [formData, setFormData] = useState({ + product_name: '', + vin: '', + price: '', + source_place: '', + plate_place: '', + plate_time: '', + displacement: '', + power: '', + body_color: '', + condition_desc: '', + mileage: '', + // 绑定车型数据字段 + crawl_brand: '', + crawl_series: '', + crawl_series_id: '', + crawl_model_name: '', + crawl_model_url: '', + crawl_msrp: '' + }) + + // 图片和附件 + const [images, setImages] = useState([]) + const [attachments, setAttachments] = useState>([]) + + // 绑定弹窗状态 + const [bindVisible, setBindVisible] = useState(false) + const [bindLoading, setBindLoading] = useState(false) + const [bindResult, setBindResult] = useState(null) + const [bindSelected, setBindSelected] = useState(null) + const [boundProduct, setBoundProduct] = useState(null) + const [bindKeyword, setBindKeyword] = useState('') + + // 提交中 + const [submitting, setSubmitting] = useState(false) + + // 上牌时间选择 + const [plateTimeVisible, setPlateTimeVisible] = useState(false) + + // 产品名称绑定(关键词搜索) + const handleBindByName = async () => { + const name = formData.product_name.trim() + if (!name) { + return Toast.show({ content: t('usedCar.enterProductNameFirst'), icon: 'fail' }) + } + + setBindVisible(true) + setBindLoading(true) + setBindResult(null) + setBindSelected(null) + setBindKeyword('') + + try { + const res = await usedCarApi.crawlKeyword(name) + setBindResult(res) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : t('usedCar.bindFetchFailed') + Toast.show({ content: message, icon: 'fail' }) + } finally { + setBindLoading(false) + } + } + + // 确认绑定 + const handleConfirmBind = () => { + if (!bindSelected || !bindResult) return + + setBoundProduct({ + brand: bindResult.brand, + series: bindResult.series, + series_id: bindResult.series_id, + model: bindSelected + }) + + setFormData({ + ...formData, + crawl_brand: bindResult.brand, + crawl_series: bindResult.series, + crawl_series_id: bindResult.series_id, + crawl_model_name: bindSelected.name, + crawl_model_url: bindSelected.url, + crawl_msrp: bindSelected.msrp ? String(bindSelected.msrp) : '' + }) + + setBindVisible(false) + Toast.show({ content: t('usedCar.bindSuccess'), icon: 'success' }) + } + + // 清除绑定 + const handleClearBind = () => { + setBoundProduct(null) + setFormData({ + ...formData, + crawl_brand: '', + crawl_series: '', + crawl_series_id: '', + crawl_model_name: '', + crawl_model_url: '', + crawl_msrp: '' + }) + Toast.show({ content: t('usedCar.bindCleared'), icon: 'success' }) + } + + // 过滤车型列表 + const filteredModels = bindResult?.models.filter((model) => { + const kw = bindKeyword.toLowerCase().trim() + if (!kw) return true + const name = String(model.name || '').toLowerCase() + return name.includes(kw) + }) || [] + + // 选择车型 + const handleSelectModel = (model: CarModel) => { + setBindSelected(model) + } + + // 跟踪上传数量 + const [uploadingCount, setUploadingCount] = useState(0) + + // 图片上传处理(在 ImageUploader 的 upload prop 中调用) + const handleImageUpload = async (file: File): Promise<{ url: string }> => { + setUploadingCount(prev => prev + 1) + try { + const result: any = await uploadApi.uploadUsedCarImage(file) + return { url: result.full_url || result.url } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : t('usedCar.uploadFailed') + Toast.show({ content: message, icon: 'fail' }) + throw error + } finally { + setUploadingCount(prev => prev - 1) + } + } + + // 图片变化处理 + const handleImageChange = (newFiles: ImageFileItem[]) => { + // 更新图片列表 + setImages(newFiles.map(f => f.url)) + } + + // 图片删除处理 + const handleImageDelete = (deletedFile: ImageFileItem) => { + const newImages = images.filter(url => url !== deletedFile.url) + setImages(newImages) + } + + // 附件上传处理 + const handleAttachmentUpload = async (file: File): Promise<{ id: number; name: string; url: string; size: number }> => { + try { + const result: any = await uploadApi.uploadFile(file, 'other', 'usedcar') + return { + id: result.id, + name: result.file_name || file.name, + url: result.url, + size: result.file_size || file.size + } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : t('usedCar.uploadFailed') + Toast.show({ content: message, icon: 'fail' }) + throw error + } + } + + // 附件选择处理 + const handleAttachmentSelect = async (e: React.ChangeEvent) => { + const files = e.target.files + if (!files || files.length === 0) return + + const newAttachments = [...attachments] + for (let i = 0; i < files.length; i++) { + const file = files[i] + if (newAttachments.length + i >= 20) { + Toast.show({ content: t('usedCar.maxAttachments'), icon: 'fail' }) + break + } + try { + const result = await handleAttachmentUpload(file) + newAttachments.push(result) + } catch (err) { + // 上传失败已在 handleAttachmentUpload 中处理 + } + } + setAttachments(newAttachments) + e.target.value = '' + } + + // 附件删除处理 + const handleAttachmentDelete = (index: number) => { + const newAttachments = attachments.filter((_, i) => i !== index) + setAttachments(newAttachments) + } + + // 表单提交 + const handleSubmit = async () => { + if (!formData.product_name) { + return Toast.show({ content: t('usedCar.enterProductNameFirst'), icon: 'fail' }) + } + + // 检查是否有正在上传的图片 + if (uploadingCount > 0) { + return Toast.show({ content: t('usedCar.uploading'), icon: 'fail' }) + } + + setSubmitting(true) + try { + const payload = { + ...formData, + price: formData.price ? Number(formData.price) : undefined, + mileage: formData.mileage ? Number(formData.mileage) : undefined, + crawl_msrp: formData.crawl_msrp ? Number(formData.crawl_msrp) : undefined, + // 图片数组转为逗号分隔字符串 + images: images.join(','), + // 附件数组转为JSON + attachment: attachments, + } + + const id = await usedCarApi.create(payload) + if (id) { + Toast.show({ content: t('usedCar.createSuccess'), icon: 'success' }) + setTimeout(() => { + navigate('/used-car') + }, 1000) + } + } catch (error: unknown) { + const message = error instanceof Error ? error.message : t('common.error') + Toast.show({ content: message, icon: 'fail' }) + } finally { + setSubmitting(false) + } + } + + return ( +
+ {/* navigate(-1)}>{t('usedCar.pageTitle')} */} + +
+ {/* 产品名称卡片 */} +
+
+ +
+
+ setFormData({ ...formData, product_name: value })} + /> +
+ +
+
+
+ + {/* 绑定状态提示 */} + {boundProduct ? ( +
+ + {t('usedCar.bindBound')}: {boundProduct.brand} · {boundProduct.series} + {boundProduct.model && · {boundProduct.model.name}} + + + {t('usedCar.bindClear')} + +
+ ) : ( +
+ {t('usedCar.bindExample')} +
+ )} + + {/* 基本信息 */} +
+
+ +
+ setFormData({ ...formData, price: value })} + /> +
+
+ +
+ +
+ setFormData({ ...formData, source_place: value })} + /> +
+
+ +
+ +
setPlateTimeVisible(true)}> + + {formData.plate_time || t('usedCar.selectPlateTime')} + +
+ setPlateTimeVisible(false)} + onConfirm={(date) => { + const dateStr = date.toISOString().split('T')[0] + setFormData({ ...formData, plate_time: dateStr }) + setPlateTimeVisible(false) + }} + /> +
+ +
+ +
+ setFormData({ ...formData, displacement: value })} + /> +
+
+ +
+ +
+ setFormData({ ...formData, power: value })} + /> +
+
+ +
+ +
+ setFormData({ ...formData, mileage: value })} + /> +
+
+ +
+ +
+ setFormData({ ...formData, body_color: value })} + /> +
+
+
+ + {/* 产品图 */} +
+ +
+ ({ url }))} + onChange={handleImageChange} + onDelete={handleImageDelete} + upload={handleImageUpload} + maxCount={20} + multiple + showUpload + /> +
+
+ + {/* 附件 */} +
+ +
+ + +
+ {attachments.map((file, index) => ( +
+ {file.name} + handleAttachmentDelete(index)} + > + {t('usedCar.removeImage')} + +
+ ))} +
+
+
+ + {/* 车况介绍 */} +
+ +
+