pHPrediction.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. <template>
  2. <el-card class="box-card">
  3. <template #header>
  4. <div class="card-header">
  5. <span>{{ $t('PhPrediction.title') }}</span>
  6. </div>
  7. </template>
  8. <el-form
  9. :model="form"
  10. ref="predictForm"
  11. label-width="240px"
  12. label-position="left"
  13. >
  14. <el-form-item :label="$t('PhPrediction.organicMatter')" prop="OM" :error="errorMessages.OM" required>
  15. <el-input
  16. v-model="form.OM"
  17. size="large"
  18. :placeholder="$t('PhPrediction.organicMatterPlaceholder')"
  19. @input="handleInput('OM', $event, 0, 35)"
  20. ></el-input>
  21. </el-form-item>
  22. <el-form-item :label="$t('PhPrediction.chloride')" prop="CL" :error="errorMessages.CL" required>
  23. <el-input
  24. v-model="form.CL"
  25. size="large"
  26. :placeholder="$t('PhPrediction.chloridePlaceholder')"
  27. @input="handleInput('CL', $event, 0, 10)"
  28. ></el-input>
  29. </el-form-item>
  30. <el-form-item :label="$t('PhPrediction.cationExchangeCapacity')" prop="CEC" :error="errorMessages.CEC" required>
  31. <el-input
  32. v-model="form.CEC"
  33. size="large"
  34. :placeholder="$t('PhPrediction.cecPlaceholder')"
  35. @input="handleInput('CEC', $event, 0, 20)"
  36. ></el-input>
  37. </el-form-item>
  38. <el-form-item :label="$t('PhPrediction.hydrogenIonConcentration')" prop="H" :error="errorMessages.H" required>
  39. <el-input
  40. v-model="form.H"
  41. size="large"
  42. :placeholder="$t('PhPrediction.hPlaceholder')"
  43. @input="handleInput('H', $event, 0, 1)"
  44. ></el-input>
  45. </el-form-item>
  46. <el-form-item :label="$t('PhPrediction.ammoniumNitrogen')" prop="HN" :error="errorMessages.HN" required>
  47. <el-input
  48. v-model="form.HN"
  49. size="large"
  50. :placeholder="$t('PhPrediction.hnPlaceholder')"
  51. @input="handleInput('HN', $event, 0, 30)"
  52. ></el-input>
  53. </el-form-item>
  54. <el-form-item :label="$t('PhPrediction.freeAlumina')" prop="Al3" :error="errorMessages.Al3" required>
  55. <el-input
  56. v-model="form.Al3"
  57. size="large"
  58. :placeholder="$t('PhPrediction.al3Placeholder')"
  59. @input="handleInput('Al3', $event, 0, 2)"
  60. ></el-input>
  61. </el-form-item>
  62. <el-form-item :label="$t('PhPrediction.alumina')" prop="AlOx" :error="errorMessages.AlOx" required>
  63. <el-input
  64. v-model="form.AlOx"
  65. size="large"
  66. :placeholder="$t('PhPrediction.aloxPlaceholder')"
  67. @input="handleInput('AlOx', $event, 0, 2)"
  68. ></el-input>
  69. </el-form-item>
  70. <el-form-item :label="$t('PhPrediction.freeIronOxide')" prop="FeOx" :error="errorMessages.FeOx" required>
  71. <el-input
  72. v-model="form.FeOx"
  73. size="large"
  74. :placeholder="$t('PhPrediction.feoxPlaceholder')"
  75. @input="handleInput('FeOx', $event, 0, 3)"
  76. ></el-input>
  77. </el-form-item>
  78. <el-form-item :label="$t('PhPrediction.amorphousIron')" prop="AmFe" :error="errorMessages.AmFe" required>
  79. <el-input
  80. v-model="form.AmFe"
  81. size="large"
  82. :placeholder="$t('PhPrediction.amfePlaceholder')"
  83. @input="handleInput('AmFe', $event, 0, 1)"
  84. ></el-input>
  85. </el-form-item>
  86. <el-form-item :label="$t('PhPrediction.initialPH')" prop="initpH" :error="errorMessages.initpH" required>
  87. <el-input
  88. v-model="form.initpH"
  89. size="large"
  90. :placeholder="$t('PhPrediction.initphPlaceholder')"
  91. @input="handleInput('initpH', $event, 0, 14)"
  92. ></el-input>
  93. </el-form-item>
  94. <el-button type="primary" @click="onSubmit" class="onSubmit">{{ $t('PhPrediction.predictButton') }}</el-button>
  95. <el-dialog v-model="dialogVisible" @close="onDialogClose" width="800px" align-center :title="$t('PhPrediction.resultTitle')">
  96. <div class="image-container">
  97. <img v-if="imageSrc" :src="imageSrc" :alt="$t('PhPrediction.resultTitle')" class="full-image"/>
  98. </div>
  99. <template #footer>
  100. <el-button @click="dialogVisible = false">{{ $t('PhPrediction.closeButton') }}</el-button>
  101. </template>
  102. </el-dialog>
  103. </el-form>
  104. </el-card>
  105. </template>
  106. <script setup lang="ts">
  107. import { reactive, ref } from "vue";
  108. import { ElMessage } from "element-plus";
  109. import { api5000 } from "../../../utils/request";
  110. import { useI18n } from 'vue-i18n';
  111. const { t } = useI18n();
  112. interface Form {
  113. OM: number | null;
  114. CL: number | null;
  115. CEC: number | null;
  116. H: number | null;
  117. HN: number | null;
  118. Al3: number | null;
  119. AlOx: number | null;
  120. FeOx: number | null;
  121. AmFe: number | null;
  122. initpH: number | null;
  123. }
  124. const form = reactive<Form>({
  125. OM: null,
  126. CL: null,
  127. CEC: null,
  128. H: null,
  129. HN: null,
  130. Al3: null,
  131. AlOx: null,
  132. FeOx: null,
  133. AmFe: null,
  134. initpH: null,
  135. });
  136. const imageSrc = ref<string | null>(null);
  137. const dialogVisible = ref(false);
  138. const errorMessages = reactive<Record<string, string>>({
  139. OM: "",
  140. CL: "",
  141. CEC: "",
  142. H: "",
  143. HN: "",
  144. Al3: "",
  145. AlOx: "",
  146. FeOx: "",
  147. AmFe: "",
  148. initpH: "",
  149. });
  150. const handleInput = (field: keyof Form, event: Event, min: number, max: number) => {
  151. const target = event.target as HTMLInputElement;
  152. let value = target.value.replace(/[^0-9.]/g, "");
  153. (form as any)[field] = value ? parseFloat(value) : null;
  154. if (!value) {
  155. errorMessages[field] = "";
  156. return;
  157. }
  158. const numValue = parseFloat(value);
  159. errorMessages[field] = (isNaN(numValue) || numValue < min || numValue > max)
  160. ? t('PhPrediction.validationRange', { min, max })
  161. : "";
  162. };
  163. const validateInput = (value: string, min: number, max: number): boolean => {
  164. const numValue = parseFloat(value);
  165. return !isNaN(numValue) && numValue >= min && numValue <= max;
  166. };
  167. const onSubmit = async () => {
  168. const inputConfigs = [
  169. { field: "OM" as keyof Form, min: 0, max: 35 },
  170. { field: "CL" as keyof Form, min: 0, max: 10 },
  171. { field: "CEC" as keyof Form, min: 0, max: 20 },
  172. { field: "H" as keyof Form, min: 0, max: 1 },
  173. { field: "HN" as keyof Form, min: 0, max: 30 },
  174. { field: "Al3" as keyof Form, min: 0, max: 2 },
  175. { field: "AlOx" as keyof Form, min: 0, max: 2},
  176. { field: "FeOx" as keyof Form, min: 0, max: 3 },
  177. { field: "AmFe" as keyof Form, min: 0, max: 1 },
  178. { field: "initpH" as keyof Form, min: 0, max: 14 },
  179. ];
  180. let isValid = true;
  181. for (const config of inputConfigs) {
  182. const { field, min, max } = config;
  183. const value = form[field];
  184. if (value === null || !validateInput(value.toString(), min, max)) {
  185. isValid = false;
  186. errorMessages[field] = t('PhPrediction.validationRange', { min, max });
  187. } else {
  188. errorMessages[field] = "";
  189. }
  190. }
  191. if (!isValid) {
  192. ElMessage.error(t('PhPrediction.validationError'));
  193. return;
  194. }
  195. const features = {
  196. "OM g/kg": form.OM,
  197. "CL g/kg": form.CL,
  198. "CEC cmol/kg": form.CEC,
  199. "H+ cmol/kg": form.H,
  200. "HN mg/kg": form.HN,
  201. "Al3+ cmol/kg": form.Al3,
  202. "Free alumina g/kg": form.AlOx,
  203. "Free iron oxides g/kg": form.FeOx,
  204. "Amorphous iron g/Kg": form.AmFe,
  205. "0 day": form.initpH
  206. };
  207. const curve = {
  208. "start_day": 0,
  209. "end_day": 200,
  210. "num_points": 80,
  211. "return_binary": true
  212. };
  213. try {
  214. const response = await api5000.post('/api/ph/predict', {
  215. day: 50,
  216. features,
  217. curve
  218. }, {
  219. responseType: 'arraybuffer'
  220. });
  221. const blob = new Blob([response.data], { type: 'image/png' });
  222. imageSrc.value = URL.createObjectURL(blob);
  223. dialogVisible.value = true;
  224. } catch (error) {
  225. ElMessage.error(`${t('PhPrediction.requestFailed')}${error}`);
  226. }
  227. };
  228. const onDialogClose = () => {
  229. dialogVisible.value = false;
  230. imageSrc.value = null;
  231. Object.keys(form).forEach(key => form[key as keyof Form] = null);
  232. Object.keys(errorMessages).forEach(key => errorMessages[key] = "");
  233. };
  234. </script>
  235. <style scoped>
  236. .box-card {
  237. max-width: 850px;
  238. margin: 0 auto;
  239. padding: 20px;
  240. background-color: #f0f5ff;
  241. border-radius: 10px;
  242. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  243. }
  244. .card-header {
  245. font-size: 25px;
  246. text-align: center;
  247. color: #333;
  248. margin-bottom: 30px;
  249. }
  250. .el-form-item {
  251. margin-bottom: 20px;
  252. }
  253. :deep(.el-form-item__label) {
  254. font-size: 18px;
  255. color: #666;
  256. }
  257. .model-info {
  258. text-align: center;
  259. margin-bottom: 20px;
  260. font-size: 16px;
  261. color: #555;
  262. }
  263. .el-input {
  264. width: 80%;
  265. }
  266. .onSubmit {
  267. display: block;
  268. margin: 0 auto;
  269. background-color: #007bff;
  270. color: white;
  271. padding: 10px 20px;
  272. border-radius: 5px;
  273. font-size: 16px;
  274. transition: background-color 0.3s ease;
  275. }
  276. .onSubmit:hover {
  277. background-color: #0056b3;
  278. }
  279. .dialog-class {
  280. display: flex;
  281. justify-content: center;
  282. align-items: center;
  283. height: 100%; /* 确保容器占满对话框内容区域 */
  284. font-size: 22px;
  285. }
  286. @media (max-width: 576px) {
  287. .el-form {
  288. --el-form-label-width: 100px;
  289. }
  290. }
  291. .full-image {
  292. max-width: 100%;
  293. max-height: 100vh; /* 最大高度不超过视口 */
  294. width: auto;
  295. height: auto;
  296. object-fit: contain; /* 保持比例 */
  297. border: 2px solid #409eff;
  298. box-shadow: 0 0 20px rgba(0,0,0,0.3);
  299. }
  300. .image-container {
  301. width: 100%;
  302. height: 100vh; /* 占满视口高度 */
  303. display: flex;
  304. justify-content: center;
  305. align-items: center;
  306. overflow: auto; /* 添加滚动条防止极端情况 */
  307. }
  308. .el-dialog {
  309. margin-top: 0 !important;
  310. margin-bottom: 0 !important;
  311. }
  312. .el-dialog__header {
  313. padding: 15px 20px;
  314. background-color: #f5f7fa;
  315. border-bottom: 1px solid #e4e7ed;
  316. }
  317. .el-dialog__body {
  318. padding: 0 !important;
  319. overflow: hidden;
  320. }
  321. /* 移动端适配 */
  322. @media (max-width: 768px) {
  323. .image-container {
  324. height: auto;
  325. padding: 10px;
  326. }
  327. .full-image {
  328. max-width: calc(100vw - 20px);
  329. max-height: calc(100vh - 40px);
  330. }
  331. }
  332. </style>