pHPrediction.vue 9.7 KB

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