|
6 | 6 | <div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl w-full max-w-2xl relative z-10 overflow-hidden flex flex-col max-h-[90vh]"> |
7 | 7 | <div class="px-6 py-4 border-b border-slate-200 dark:border-slate-700 flex justify-between items-center bg-slate-50 dark:bg-slate-800"> |
8 | 8 | <h2 class="text-lg font-bold text-slate-800 dark:text-white">{{ isEditing ? '编辑车票' : '新增车票' }}</h2> |
9 | | - <button @click="handleCancel" class="text-slate-400 hover:text-red-500 transition-colors"> |
10 | | - <X class="w-6 h-6" /> |
11 | | - </button> |
| 9 | + <div class="flex items-center gap-2"> |
| 10 | + <button |
| 11 | + @click="triggerFileInput" |
| 12 | + :disabled="recognizing" |
| 13 | + class="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-indigo-50 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-400 rounded-md hover:bg-indigo-100 dark:hover:bg-indigo-900/50 transition-colors" |
| 14 | + title="上传车票图片自动识别" |
| 15 | + > |
| 16 | + <Sparkles v-if="!recognizing" class="w-4 h-4" /> |
| 17 | + <span v-else class="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"></span> |
| 18 | + {{ recognizing ? '识别中...' : '智能填充' }} |
| 19 | + </button> |
| 20 | + <input |
| 21 | + ref="fileInput" |
| 22 | + type="file" |
| 23 | + accept="image/*" |
| 24 | + class="hidden" |
| 25 | + @change="handleFileChange" |
| 26 | + /> |
| 27 | + <button @click="handleCancel" class="text-slate-400 hover:text-red-500 transition-colors ml-2"> |
| 28 | + <X class="w-6 h-6" /> |
| 29 | + </button> |
| 30 | + </div> |
12 | 31 | </div> |
13 | 32 | <div class="p-6 overflow-y-auto dark:text-slate-200"> |
14 | 33 | <form @submit.prevent="handleSubmit" class="grid grid-cols-1 md:grid-cols-2 gap-2"> |
@@ -241,6 +260,67 @@ const axiosInstance = axios.create({ |
241 | 260 | }, |
242 | 261 | }); |
243 | 262 |
|
| 263 | +// 识别相关 |
| 264 | +const fileInput = ref(null); |
| 265 | +const recognizing = ref(false); |
| 266 | +
|
| 267 | +const triggerFileInput = () => { |
| 268 | + fileInput.value.click(); |
| 269 | +}; |
| 270 | +
|
| 271 | +const handleFileChange = async (event) => { |
| 272 | + const file = event.target.files[0]; |
| 273 | + if (!file) return; |
| 274 | +
|
| 275 | + // Reset input |
| 276 | + event.target.value = ''; |
| 277 | +
|
| 278 | + recognizing.value = true; |
| 279 | + try { |
| 280 | + const formData = new FormData(); |
| 281 | + formData.append('file', file); |
| 282 | + |
| 283 | + // 直接使用 axiosInstance 调用后端接口 |
| 284 | + // 注意:这里需要根据实际 API 路径调整,假设 apiBaseUrl 是后端根地址 |
| 285 | + // 如果 ticketService 已经封装好了,最好通过 props 传入 ticketService 或者 emit 事件让父组件处理 |
| 286 | + // 但为了保持组件独立性,这里我们直接调用 /api/train-ticket/recognize |
| 287 | + |
| 288 | + // 由于 props.apiBaseUrl 可能包含 /api 也可能不包含,这里假设它指向后端根目录 |
| 289 | + // 检查 apiBaseUrl 是否以 /api 结尾 |
| 290 | + let url = '/api/train-ticket/recognize'; |
| 291 | + if (!props.apiBaseUrl.includes('/api')) { |
| 292 | + // 如果 base url 没有 /api,保持原样 |
| 293 | + } |
| 294 | + |
| 295 | + const response = await axiosInstance.post('/train-ticket/recognize', formData, { |
| 296 | + headers: { |
| 297 | + 'Content-Type': 'multipart/form-data' |
| 298 | + } |
| 299 | + }); |
| 300 | + |
| 301 | + const data = response.data; |
| 302 | + |
| 303 | + // 填充表单 |
| 304 | + if (data.train_code) form.value.train_code = data.train_code; |
| 305 | + if (data.departure_station) form.value.from = data.departure_station; |
| 306 | + if (data.arrival_station) form.value.to = data.arrival_station; |
| 307 | + if (data.datetime) form.value.dateTime = data.datetime; |
| 308 | + if (data.carriage) form.value.carriage = data.carriage; |
| 309 | + if (data.seat_num) form.value.seatNumber = data.seat_num; |
| 310 | + if (data.price) form.value.price = data.price; |
| 311 | + if (data.seat_type) form.value.seatType = data.seat_type; |
| 312 | + if (data.name) form.value.name = data.name; |
| 313 | + |
| 314 | + ElMessage.success('车票识别成功,已自动填充'); |
| 315 | + |
| 316 | + } catch (error) { |
| 317 | + console.error('Recognition failed:', error); |
| 318 | + ElMessage.error(error.response?.data?.detail || '识别失败,请重试'); |
| 319 | + } finally { |
| 320 | + recognizing.value = false; |
| 321 | + } |
| 322 | +}; |
| 323 | +
|
244 | 324 | // 表单模型 |
245 | 325 | const form = ref({ |
246 | 326 | id: null, |
@@ -319,7 +399,7 @@ async function fetchSchedules() { |
319 | 399 | scheduleError.value = ''; |
320 | 400 | try { |
321 | 401 | // 只传递train_code,其他参数留空 |
322 | | - const response = await axiosInstance.get('/api/railway/train-schedules', { |
| 402 | + const response = await axiosInstance.get('/railway/train-schedules', { |
323 | 403 | params: { |
324 | 404 | train_code: trainCode, |
325 | 405 | page: 1, |
|
0 commit comments