|
|
@@ -0,0 +1,377 @@
|
|
|
+<template>
|
|
|
+ <el-card class="box-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>pH预测模型</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-form
|
|
|
+ :model="form"
|
|
|
+ ref="predictForm"
|
|
|
+ label-width="240px"
|
|
|
+ label-position="left"
|
|
|
+ >
|
|
|
+ <el-form-item label="有机质(g/kg)" prop="OM" :error="errorMessages.OM" required>
|
|
|
+ <el-input
|
|
|
+ v-model="form.OM"
|
|
|
+ size="large"
|
|
|
+ placeholder="请输入有机质0~35(g/kg)"
|
|
|
+ @input="handleInput('OM', $event, 0, 35)"
|
|
|
+ ></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="氯离子(g/kg)" prop="CL" :error="errorMessages.CL" required>
|
|
|
+ <el-input
|
|
|
+ v-model="form.CL"
|
|
|
+ size="large"
|
|
|
+ placeholder="请输入氯离子0~10(g/kg)"
|
|
|
+ @input="handleInput('CL', $event, 0, 10)"
|
|
|
+ ></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="阳离子交换量(cmol/kg)" prop="CEC" :error="errorMessages.CEC" required>
|
|
|
+ <el-input
|
|
|
+ v-model="form.CEC"
|
|
|
+ size="large"
|
|
|
+ placeholder="请输入阳离子交换量0~20(cmol/kg)"
|
|
|
+ @input="handleInput('CEC', $event, 0, 20)"
|
|
|
+ ></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="H+浓度(cmol/kg)" prop="H" :error="errorMessages.H" required>
|
|
|
+ <el-input
|
|
|
+ v-model="form.H"
|
|
|
+ size="large"
|
|
|
+ placeholder="请输入H+浓度0~1(cmol/kg)"
|
|
|
+ @input="handleInput('H', $event, 0, 1)"
|
|
|
+ ></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="铵态氮(mg/kg)" prop="HN" :error="errorMessages.HN" required>
|
|
|
+ <el-input
|
|
|
+ v-model="form.HN"
|
|
|
+ size="large"
|
|
|
+ placeholder="请输入铵态氮0~30(mg/kg)"
|
|
|
+ @input="handleInput('HN', $event, 0, 30)"
|
|
|
+ ></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="游离氧化铝(g/kg)" prop="Al3" :error="errorMessages.Al3" required>
|
|
|
+ <el-input
|
|
|
+ v-model="form.Al3"
|
|
|
+ size="large"
|
|
|
+ placeholder="请输入游离氧化铝0~2(g/kg)"
|
|
|
+ @input="handleInput('Al3', $event, 0, 2)"
|
|
|
+ ></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="氧化铝(g/kg)" prop="AlOx" :error="errorMessages.AlOx" required>
|
|
|
+ <el-input
|
|
|
+ v-model="form.AlOx"
|
|
|
+ size="large"
|
|
|
+ placeholder="请输入氧化铝0~2(g/kg)"
|
|
|
+ @input="handleInput('AlOx', $event, 0, 2)"
|
|
|
+ ></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="游离铁氧化物(g/kg)" prop="FeOx" :error="errorMessages.FeOx" required>
|
|
|
+ <el-input
|
|
|
+ v-model="form.FeOx"
|
|
|
+ size="large"
|
|
|
+ placeholder="请输入游离铁氧化物0~3(g/kg)"
|
|
|
+ @input="handleInput('FeOx', $event, 0, 3)"
|
|
|
+ ></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="无定形铁(g/Kg)" prop="AmFe" :error="errorMessages.AmFe" required>
|
|
|
+ <el-input
|
|
|
+ v-model="form.AmFe"
|
|
|
+ size="large"
|
|
|
+ placeholder="请输入无定形铁0~1(g/Kg)"
|
|
|
+ @input="handleInput('AmFe', $event, 0, 1)"
|
|
|
+ ></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="初始pH值" prop="initpH" :error="errorMessages.initpH" required>
|
|
|
+ <el-input
|
|
|
+ v-model="form.initpH"
|
|
|
+ size="large"
|
|
|
+ placeholder="请输入初始pH值0~14"
|
|
|
+ @input="handleInput('initpH', $event, 0, 14)"
|
|
|
+ ></el-input>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-button type="primary" @click="onSubmit" class="onSubmit">预测pH曲线</el-button>
|
|
|
+ <el-dialog v-model="dialogVisible" @close="onDialogClose" width="800px" align-center title="pH预测结果">
|
|
|
+ <div class="image-container">
|
|
|
+ <img v-if="imageSrc" :src="imageSrc" alt="预测图片" class="full-image"/>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="dialogVisible = false">关闭</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { reactive, ref, nextTick } from "vue";
|
|
|
+import { ElMessage } from "element-plus";
|
|
|
+import { api5000 } from "../../../utils/request";
|
|
|
+
|
|
|
+interface Form {
|
|
|
+ OM: number | null;
|
|
|
+ CL: number | null;
|
|
|
+ CEC: number | null;
|
|
|
+ H: number | null;
|
|
|
+ HN: number | null;
|
|
|
+ Al3: number | null;
|
|
|
+ AlOx: number | null;
|
|
|
+ FeOx: number | null;
|
|
|
+ AmFe: number | null;
|
|
|
+ initpH: number | null;
|
|
|
+}
|
|
|
+
|
|
|
+const form = reactive<Form>({
|
|
|
+ OM: null,
|
|
|
+ CL: null,
|
|
|
+ CEC: null,
|
|
|
+ H: null,
|
|
|
+ HN: null,
|
|
|
+ Al3: null,
|
|
|
+ AlOx: null,
|
|
|
+ FeOx: null,
|
|
|
+ AmFe: null,
|
|
|
+ initpH: null,
|
|
|
+});
|
|
|
+
|
|
|
+const imageSrc = ref<string | null>(null);
|
|
|
+const dialogVisible = ref(false);
|
|
|
+const predictForm = ref<any>(null);
|
|
|
+const errorMessages = reactive<Record<string, string>>({
|
|
|
+ OM: "",
|
|
|
+ CL: "",
|
|
|
+ CEC: "",
|
|
|
+ H: "",
|
|
|
+ HN: "",
|
|
|
+ Al3: "",
|
|
|
+ AlOx: "",
|
|
|
+ FeOx: "",
|
|
|
+ AmFe: "",
|
|
|
+ initpH: "",
|
|
|
+});
|
|
|
+
|
|
|
+const handleInput = (field: keyof Form, event: Event, min: number, max: number) => {
|
|
|
+ const target = event.target as HTMLInputElement;
|
|
|
+ let value = target.value.replace(/[^0-9.]/g, "");
|
|
|
+ (form as any)[field] = value ? parseFloat(value) : null;
|
|
|
+
|
|
|
+ if (!value) {
|
|
|
+ errorMessages[field] = "";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const numValue = parseFloat(value);
|
|
|
+ errorMessages[field] = (isNaN(numValue) || numValue < min || numValue > max)
|
|
|
+ ? `输入值应在 ${min} 到 ${max} 之间`
|
|
|
+ : "";
|
|
|
+};
|
|
|
+
|
|
|
+const validateInput = (value: string, min: number, max: number): boolean => {
|
|
|
+ const numValue = parseFloat(value);
|
|
|
+ return !isNaN(numValue) && numValue >= min && numValue <= max;
|
|
|
+};
|
|
|
+
|
|
|
+const onSubmit = async () => {
|
|
|
+ const inputConfigs = [
|
|
|
+ { field: "OM" as keyof Form, min: 0, max: 35 },
|
|
|
+ { field: "CL" as keyof Form, min: 0, max: 10 },
|
|
|
+ { field: "CEC" as keyof Form, min: 0, max: 20 },
|
|
|
+ { field: "H" as keyof Form, min: 0, max: 1 },
|
|
|
+ { field: "HN" as keyof Form, min: 0, max: 30 },
|
|
|
+ { field: "Al3" as keyof Form, min: 0, max: 2 },
|
|
|
+ { field: "AlOx" as keyof Form, min: 0, max: 2},
|
|
|
+ { field: "FeOx" as keyof Form, min: 0, max: 3 },
|
|
|
+ { field: "AmFe" as keyof Form, min: 0, max: 1 },
|
|
|
+ { field: "initpH" as keyof Form, min: 0, max: 14 },
|
|
|
+ ];
|
|
|
+
|
|
|
+ let isValid = true;
|
|
|
+ for (const config of inputConfigs) {
|
|
|
+ const { field, min, max } = config;
|
|
|
+ const value = form[field];
|
|
|
+ if (value === null || !validateInput(value.toString(), min, max)) {
|
|
|
+ isValid = false;
|
|
|
+ errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间`;
|
|
|
+ } else {
|
|
|
+ errorMessages[field] = "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isValid) {
|
|
|
+ ElMessage.error("输入值不符合要求,请检查输入");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const features = {
|
|
|
+ "OM g/kg": form.OM,
|
|
|
+ "CL g/kg": form.CL,
|
|
|
+ "CEC cmol/kg": form.CEC,
|
|
|
+ "H+ cmol/kg": form.H,
|
|
|
+ "HN mg/kg": form.HN,
|
|
|
+ "Al3+ cmol/kg": form.Al3,
|
|
|
+ "Free alumina g/kg": form.AlOx,
|
|
|
+ "Free iron oxides g/kg": form.FeOx,
|
|
|
+ "Amorphous iron g/Kg": form.AmFe,
|
|
|
+ "0 day": form.initpH
|
|
|
+ };
|
|
|
+
|
|
|
+ const curve = {
|
|
|
+ "start_day": 0,
|
|
|
+ "end_day": 200,
|
|
|
+ "num_points": 80,
|
|
|
+ "return_binary": true
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await api5000.post('/api/ph/predict', {
|
|
|
+ day: 50,
|
|
|
+ features,
|
|
|
+ curve
|
|
|
+ }, {
|
|
|
+ responseType: 'arraybuffer'
|
|
|
+ });
|
|
|
+
|
|
|
+ const blob = new Blob([response.data], { type: 'image/png' });
|
|
|
+ imageSrc.value = URL.createObjectURL(blob);
|
|
|
+ dialogVisible.value = true;
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error(`请求失败: ${error}`);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const onDialogClose = () => {
|
|
|
+ dialogVisible.value = false;
|
|
|
+ imageSrc.value = null;
|
|
|
+ Object.keys(form).forEach(key => form[key as keyof Form] = null);
|
|
|
+ Object.keys(errorMessages).forEach(key => errorMessages[key] = "");
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.box-card {
|
|
|
+ max-width: 850px;
|
|
|
+ margin: 0 auto;
|
|
|
+ padding: 20px;
|
|
|
+ background-color: #f0f5ff;
|
|
|
+ border-radius: 10px;
|
|
|
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ font-size: 25px;
|
|
|
+ text-align: center;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-form-item {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-form-item__label) {
|
|
|
+ font-size: 18px;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+.model-info {
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ font-size: 16px;
|
|
|
+ color: #555;
|
|
|
+}
|
|
|
+
|
|
|
+.el-input {
|
|
|
+ width: 80%;
|
|
|
+}
|
|
|
+
|
|
|
+.onSubmit {
|
|
|
+ display: block;
|
|
|
+ margin: 0 auto;
|
|
|
+ background-color: #007bff;
|
|
|
+ color: white;
|
|
|
+ padding: 10px 20px;
|
|
|
+ border-radius: 5px;
|
|
|
+ font-size: 16px;
|
|
|
+ transition: background-color 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.onSubmit:hover {
|
|
|
+ background-color: #0056b3;
|
|
|
+}
|
|
|
+
|
|
|
+.dialog-class {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ height: 100%; /* 确保容器占满对话框内容区域 */
|
|
|
+ font-size: 22px;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 576px) {
|
|
|
+ .el-form {
|
|
|
+ --el-form-label-width: 100px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.full-image {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 100vh; /* 最大高度不超过视口 */
|
|
|
+ width: auto;
|
|
|
+ height: auto;
|
|
|
+ object-fit: contain; /* 保持比例 */
|
|
|
+ border: 2px solid #409eff;
|
|
|
+ box-shadow: 0 0 20px rgba(0,0,0,0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.image-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100vh; /* 占满视口高度 */
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ overflow: auto; /* 添加滚动条防止极端情况 */
|
|
|
+}
|
|
|
+
|
|
|
+.el-dialog {
|
|
|
+ margin-top: 0 !important;
|
|
|
+ margin-bottom: 0 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.el-dialog__header {
|
|
|
+ padding: 15px 20px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ border-bottom: 1px solid #e4e7ed;
|
|
|
+}
|
|
|
+
|
|
|
+.el-dialog__body {
|
|
|
+ padding: 0 !important;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+/* 移动端适配 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .image-container {
|
|
|
+ height: auto;
|
|
|
+ padding: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .full-image {
|
|
|
+ max-width: calc(100vw - 20px);
|
|
|
+ max-height: calc(100vh - 40px);
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|