番茄病虫害识别系统【移动端】(项目)

128 阅读10分钟

将页面按比例划分,并将底部固定

.layout {
    display: flex;
    flex-direction: column;
    height: 100vh; // 使布局占满整个视口高度 

    .navbar {
        flex: 2;
        background: @layout-white-bg;
    }

    .container {
        flex: 7;
        background: @layout-grey-bg;
    }

    .footbar {
        // 占据总高度的 10% 
        flex: 1;
        background: @layout-white-bg;
        // 将footbar固定在底部
        position: fixed;
        bottom: 0;
        width: 100%;
    }
}

父子组件通信:porps

<template>
    <el-col :span="6">
        <svg class="icon" aria-hidden="true" :style="{ width: iconSize, height: iconSize }">
            <use :xlink:href="'#icon-' + iconName"></use>
        </svg>
        <div class="text" v-html="formattedText"></div>
    </el-col>
</template>

<script>
import { computed } from 'vue';
export default {
    props: {
        iconName: String,
        text: String,
        iconSize: {
            type: String,
            default: '25px' // 设置默认大小
        }
    },
    setup(props) {
        const formattedText = computed(() => {
            return props.text.replace(/\n/g, '<br>');
            // 替换文本中的换行符(\n)为 HTML 的换行标签(<br>)
        });
        return { formattedText };
    }

}
</script>
<template>
    <el-row :gutter="20">
        <Icons1 iconName="bingchonghai" text="病虫害<br>识别" :iconSize="iconSize"/>
    </el-row>
</template>

<script>
    setup(){
        const iconSize = ref("50px")
        return { 
            iconSize
        }
    }
</script>

实时监听路由,切换时立即更新某数据:watchEffect

setup() {
    const serviceIcon = ref("fuwu2");
    const courseIcon = ref("ketang1");
    const userIcon = ref("wode1");
    const router = useRouter();

    // 监听路由变化
    watchEffect(() => {
        if (router.currentRoute.value.path === '/course') {
            serviceIcon.value = "fuwu1";
            courseIcon.value = "ketang2";
            userIcon.value = "wode1";
        }
    })

    return {
        serviceIcon,
        courseIcon,
        userIcon
    }
}

*【路由守卫:某数据为空不跳转,不为空时路由跳转】

错误写法: 只写了路由守卫

setup() {
    ...reader.readAsDataURL(file);
    // 拍照上传路由守卫
    onBeforeRouteLeave((to, from, next) => {
        if (to.name === 'ResultShow' && imageUrl.value === '') {
            next(from); // 如果 imageUrl 为空,则阻止路由离开
            console.log('路由没跳转!')
        } else {
            next(); // 否则允许路由离开
            console.log('路由跳转了!')
        }
    });
}

错误原因: reader.readAsDataUR是异步操作,而在异步操作完成之前,路由守卫已经被执行。这可能会导致在路由守卫中获取的 imageUrl.value 不是最新的值。需要在异步操作完成后再调用 next()

解决方法: 路由守卫 + watch监测数据变化

vue-router@4的setup中使用:onBeforeRouteLeave + watch

import { onBeforeRouteLeave } from 'vue-router'

onBeforeRouteLeave((to, from, next) => {
    if (to.name === 'ResultShow' && imageUrl.value === '') {
        console.log('imageUrl 为空,阻止路由离开');
        next(false); // 阻止路由离开,参数也可写成from
    } else {
        console.log('允许路由离开');
        next(); // 允许路由离开
    }
});
watch(imageUrl, (newValue, oldValue) => {
    if (newValue !== '') {
        router.push('/resultShow');
    }
});

或者直接用:beforeEach + watch

import { useRouter } from 'vue-router'

const router = useRouter();
router.beforeEach((to, from, next) => {
    if (to.name === 'ResultShow' && imageUrl.value === '') {
        console.log('imageUrl 为空,阻止路由离开');
        next(false); // 阻止路由离开
    } else {
        console.log('允许路由离开');
        next(); // 允许路由离开
    }
});

// 监听 imageUrl 的变化
watch(imageUrl, (newValue, oldValue) => {
    if (newValue !== '') {
        console.log('自动跳转到下一个路由');
        // 执行跳转
        router.push('/resultShow');
    }
});

反思: 为什么beforeEachrouter.push缺一不可?

因为路由守卫只做了拦截跳转和允许跳转的逻辑,而没有设置跳转的路径

vue中使用h5调取摄像头上传并预览图片

调取多媒体

<input type="file" accept="image/*"> 相机或相册
<input type="file" accept="image/*" capture="camera"> 相机  
<input type="file" accept="video/*" capture="camcorder"> 录像
<input type="file" accept="audio/*" capture="microphone"> 系统相册
<input type="file" accept="image/*" multiple> 多选

accept属性:指定文件上传的类型,可以是MIME类型,也可以是文件扩展名。在调用摄像头拍照时,通常使用`accept="image/*"`来表示只接受图片类型。
capture属性: 指定调用摄像头或录像功能。
onChange事件: 当用户选择文件后触发的事件。在该事件中,可以通过`event.target.files`获取用户选择的文件对象,进而进行处理,例如读取文件内容、上传文件等。

按钮点击获取摄像头案例

// 与按钮结合【button触发input事件改变】
<template>
    <button><label for="fileInput">点击上传</label></button>
    <input id="fileInput" type="file" accept="image/*" style="display: none;" 
        @change="handleFileInputChange">
    <img :src="imageUrl" v-if="imageUrl" style="width: 100%; max-height: 300px;">
</template>
<script>
setup(){
    const imageUrl = ref('');       // 用于存储图片地址
    const uploadResult = ref(null)      // 用于获取返回结果
    handleFileInputChange= ()=> {
        const file = event.target.files[0];
            const reader = new FileReader();
            reader.onload = () => {
                imageUrl.value = reader.result;
                console.log('图片地址:', imageUrl.value);
                // 将 imageUrl 发送到服务器或进行其他操作
            };
            reader.readAsDataURL(file);

            // 上传操作:创建 FormData 对象, 并将图片文件添加到以后端预期的字段名 “file” 中
            const formData = new FormData();
            formData.append('url', file);
            // 请求操作
            axios.post('地址', formData).then((res) => uploadResult.value=result)
    }
}
</script>

icon图点击获取摄像头案例

// 与icon结合【点击icon图触发】
<template>
<div for="fileInput" @click="openFile">
    <svg class="icon-bingchonghai" aria-hidden="true" >
        <use xlink:href="#icon-xxx"></use>
    </svg>
</div>
<input id="fileInput" type="file" accept="image/*" style="display: none;" 
    @change="handleFileInputChange">
</template>
<script>
const openFile = () => {
    const fileInput = document.getElementById('fileInput');
    // 点击图标时触发文件选择器
    fileInput.click();
}
const handleFileInputChange = (event) => {}
</script>

Bug:element-plus的布局组件el-row中的内容不满5个导致css变化的问题:做一个假的内容用来占位

<div class="icons1">
    <el-row :gutter="20">
        <Icons1 module="course" iconName="ketang2" text="技术课堂" :iconSize="iconSize" />
        <Icons1 iconName="wenda" text="专家问答" :iconSize="iconSize" />
        <Icons1 iconName="chengjiu" text="识别成就" :iconSize="iconSize" />
        <Icons1 iconName="shoucangjia" text="收藏夹" :iconSize="iconSize" />
        <!-- 占位用,解决el-row组件不满5个icons影响css的问题 -->
        <el-col class="false" :span="6">
            <svg class="icon" aria-hidden="true" :style="{ width: iconSize, height: iconSize }">
                <use xlink:href=""></use>
            </svg>
        </el-col>
    </el-row>
</div>

<style>

</style>

使用Pinia

下载:npm install pinia

配置:

import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)

创建store:数据仓库

import { defineStore } from 'pinia'  

// 你可以任意命名 `defineStore()` 的返回值,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。
export const useUsersStore = defineStore('users', {  
    // 第一个参数是应用程序中 store 的唯一 id  
    // 其它配置项
    state: {
        return {
            a: '',
            b: null
        }
    }
})

创建store很简单,调用pinia中的defineStore函数即可,该函数接收两个参数:

  • name:一个字符串,必传项,该store的唯一id。
  • options:一个对象,store的配置项,比如配置store内的数据,修改数据的方法等等。

我们可以定义任意数量的store,因为我们其实一个store就是一个函数,这也是pinia的好处之一,让我们的代码扁平化了,这和Vue3的实现思想是一样的。

使用store:使用toRefs解构来保持响应式

import { useUserStore } from '/store/user';
import { toRefs, ref, watch } from 'vue';
const store = useUserStore();
// 响应式解构
const { a, b } = toRefs(store);
// 强制重新渲染组件
const forceRerender = ref(0);
watch([a, b], () => {
   forceRerender.value++;
});
// 改变store的操作
const changeStore = ()=> {
    a.value = xxx,
    b.value = { yyy: zzz }
}

*【强制渲染组件更新UI】

方法一:创建一个新ref数据,watch检测到被监测数据的变化后更新创建的新ref,从而更新UI

在 Vue 中,当数据发生改变时,Vue 会自动地重新渲染组件,以确保 UI 能够与数据保持同步。在 Vue 3 中,使用 watch 函数监视数据的变化,并在回调函数中执行重新渲染的操作是一种常见的做法。

在这段代码中,forceRerender 是一个响应式的变量,它的值被绑定到组件的渲染上下文中。当 forceRerender.value 的值发生改变时,Vue 会检测到这个变化,并且会重新执行组件的渲染逻辑,从而更新 UI。

所以,当执行 forceRerender.value++ 时,forceRerender.value 的值会增加,这会触发 Vue 检测到变化,然后重新执行组件的渲染逻辑,从而导致页面重新渲染。这就是为什么通过增加 forceRerender.value 来触发重新渲染的原理。

方法二:watchEffect

通过echarts将neo4j数据展示在前端

思路:

  1. 点击病虫害名称触发函数向flask发送对应的post请求 -->

  2. flask接收数据并作对应查询返回具体信息 -->

  3. nodejs处理请求,将数据写入links和node两个json文件中 -->

  4. echarts读取文件并渲染到页面

还是有些复杂,下面我们分步进行

步骤一:手动创建假数据,通过echarts进行对应关系的展示

节点数据(路径:public/data/tupu/node.json)

[
    {
        "source": 0,
        "target": 2,
        "category": 0,
        "value": "导演",
        "symbolSize": 5
    },
    {
        "source": 1,
        "target": 2,
        "category": 0,
        "value": "导演",
        "symbolSize": 5
    },
    {
        "source": 0,
        "target": 3,
        "category": 0,
        "value": "类型",
        "symbolSize": 5
    },
    {
        "source": 1,
        "target": 3,
        "category": 0,
        "value": "类型",
        "symbolSize": 5
    },
    {
        "source": 0,
        "target": 4,
        "category": 0,
        "value": "类型",
        "symbolSize": 5
    },
    {
        "source": 1,
        "target": 4,
        "category": 0,
        "value": "类型",
        "symbolSize": 5
    },
    {
        "source": 0,
        "target": 5,
        "category": 0,
        "value": "主演",
        "symbolSize": 5
    },
    {
        "source": 1,
        "target": 5,
        "category": 0,
        "value": "主演",
        "symbolSize": 5
    },
    {
        "source": 2,
        "target": 5,
        "category": 0,
        "value": "朋友",
        "symbolSize": 5
    },
    {
        "source": 5,
        "target": 2,
        "category": 0,
        "value": "",
        "symbolSize": 5
    },
    {
        "source": 0,
        "target": 6,
        "category": 0,
        "value": "主演",
        "symbolSize": 5
    },
    {
        "source": 1,
        "target": 6,
        "category": 0,
        "value": "主演",
        "symbolSize": 5
    },
    {
        "source": 2,
        "target": 6,
        "category": 0,
        "value": "朋友",
        "symbolSize": 5
    },
    {
        "source": 6,
        "target": 2,
        "category": 0,
        "value": "",
        "symbolSize": 5
    },
    {
        "source": 5,
        "target": 6,
        "category": 0,
        "value": "朋友",
        "symbolSize": 5
    },
    {
        "source": 6,
        "target": 5,
        "category": 0,
        "value": "",
        "symbolSize": 5
    }
]

关系数据(路径:public/data/tupu/links.json)

[
    {
        "source": 0,
        "target": 2,
        "category": 0,
        "value": "导演",
        "symbolSize": 5
    },
    {
        "source": 1,
        "target": 2,
        "category": 0,
        "value": "导演",
        "symbolSize": 5
    },
    {
        "source": 0,
        "target": 3,
        "category": 0,
        "value": "类型",
        "symbolSize": 5
    },
    {
        "source": 1,
        "target": 3,
        "category": 0,
        "value": "类型",
        "symbolSize": 5
    },
    {
        "source": 0,
        "target": 4,
        "category": 0,
        "value": "类型",
        "symbolSize": 5
    },
    {
        "source": 1,
        "target": 4,
        "category": 0,
        "value": "类型",
        "symbolSize": 5
    },
    {
        "source": 0,
        "target": 5,
        "category": 0,
        "value": "主演",
        "symbolSize": 5
    },
    {
        "source": 1,
        "target": 5,
        "category": 0,
        "value": "主演",
        "symbolSize": 5
    },
    {
        "source": 2,
        "target": 5,
        "category": 0,
        "value": "朋友",
        "symbolSize": 5
    },
    {
        "source": 5,
        "target": 2,
        "category": 0,
        "value": "",
        "symbolSize": 5
    },
    {
        "source": 0,
        "target": 6,
        "category": 0,
        "value": "主演",
        "symbolSize": 5
    },
    {
        "source": 1,
        "target": 6,
        "category": 0,
        "value": "主演",
        "symbolSize": 5
    },
    {
        "source": 2,
        "target": 6,
        "category": 0,
        "value": "朋友",
        "symbolSize": 5
    },
    {
        "source": 6,
        "target": 2,
        "category": 0,
        "value": "",
        "symbolSize": 5
    },
    {
        "source": 5,
        "target": 6,
        "category": 0,
        "value": "朋友",
        "symbolSize": 5
    },
    {
        "source": 6,
        "target": 5,
        "category": 0,
        "value": "",
        "symbolSize": 5
    }
]

创建echarts实例,并获取数据填入配置项中

<template>
    <div id="graph" style="width: 100%; height: 600px;"></div>
</template>

<script>
import * as echarts from 'echarts';

export default {
    setup() {
        const nodeJsonUrl = '/data/tupu/node.json'; // node.json 文件的绝对路径
        const linksJsonUrl = '/data/tupu/links.json'; // links.json 文件的绝对路径

        const fetchData = async () => {
            try {
                console.log('Fetching data from URLs:', nodeJsonUrl, linksJsonUrl);
                const responses = await Promise.all([
                    fetch(nodeJsonUrl),
                    fetch(linksJsonUrl)
                ]);

                /*// Check if all responses are ok
                for (const response of responses) {
                    if (!response.ok) {
                        throw new Error(`Failed to fetch ${response.url}: ${response.statusText}`);
                    }

                    const contentType = response.headers.get('content-type');
                    if (!contentType || !contentType.includes('application/json')) {
                        const text = await response.text();
                        throw new SyntaxError(`Expected JSON, but got: ${text}`);
                    }
                }*/

                // Parse JSON
                const results = await Promise.all(responses.map(response => response.json()));

                const mydata = results[0];
                const links = results[1];

                const myChart = echarts.init(document.getElementById('graph'));

                const option = {
                    // 提示框组件,鼠标悬浮时的提示信息
                    tooltip: {
                        formatter: a => {    // a为形参
                            // 提示框的格式化函数,返回节点名称
                            return `${a.data.name}<br> ${a.data.detail}`;
                        }
                    },
                    // 动画更新的时长
                    //animationDurationUpdate: 5000,
                    // 动画更新的缓动效果
                    //animationEasingUpdate: 'quarticInOut',

                    // 标签配置,显示标签文字
                    label: {
                        // 是否显示标签
                        show: true,
                        // 标签的文本样式
                        textStyle: {
                            // 字体大小
                            fontSize: 12
                        },
                    },

                    // 图例组件,展示类目
                    legend: {
                        // 图例组件的位置
                        x: "center",
                        // 是否显示图例
                        show: true
                    },

                    // 系列列表,每个系列通过 type 决定图表类型
                    series: [
                        {
                            // 系列类型为图形(关系图)
                            type: 'graph',
                            // 图的布局类型,'force' 表示力引导布局
                            layout: 'force',
                            // 每个节点的大小
                            symbolSize: 65,
                            // 聚焦节点的邻居节点时高亮显示
                            focusNodeAdjacency: true,
                            // 节点是否可拖拽
                            draggable: true,
                            // 是否开启鼠标缩放和平移漫游
                            roam: true,

                            // 节点分类,用于给不同类别的节点设置不同的样式
                            categories: [
                                {
                                    // 分类名称为 '电影'
                                    name: '电影',
                                    // 节点样式
                                    itemStyle: {
                                        // 颜色为浅绿色
                                        color: "lightgreen"
                                    }
                                },
                                {
                                    // 分类名称为 '主演'
                                    name: '主演',
                                    itemStyle: {
                                        // 颜色为橙色
                                        color: "orange",
                                    }
                                },
                                {
                                    // 分类名称为 '类型'
                                    name: '类型',
                                    itemStyle: {
                                        // 颜色为粉色
                                        color: "pink",
                                    }
                                },
                                {
                                    // 分类名称为 '导演'
                                    name: '导演',
                                    // 节点颜色
                                    color: "lightblue",
                                }
                            ],

                            // 标签配置
                            label: {
                                // 是否显示标签
                                show: true,
                                // 标签的文本样式
                                textStyle: {
                                    // 字体大小
                                    fontSize: 12,
                                    // 字体颜色
                                    color: "black",
                                }
                            },

                            // 力引导布局的配置项
                            force: {
                                // 节点之间的斥力因子,数值越大斥力越大
                                repulsion: 4000,
                                // 边的长度
                                edgeLength: 80,
                                // 重力系数,控制节点聚拢的速度
                                gravity: 0.1,
                            },

                            // 边的符号大小
                            edgeSymbolSize: [4, 50],
                            // 边的符号, ['none', 'arrow'] 表示无符号和箭头符号
                            edgeSymbol: ['none', 'arrow'],
                            // 边的标签配置
                            edgeLabel: {
                                // 是否显示标签
                                show: true,
                                // 标签的文本样式
                                textStyle: {
                                    // 字体大小
                                    fontSize: 10
                                },
                                /* 标签内容的格式器 通用占位符:
                                {a}:系列名(series name)
                                {b}:数据名(data name)
                                {c}:数据值(data value)
                                {d}:百分比(仅在饼图中可用)*/
                                formatter: "{c}"
                            },

                            // 节点数据
                            data: mydata,
                            // 边数据
                            links: links,

                            // 边的样式
                            lineStyle: {
                                // 透明度
                                opacity: 0.9,
                                // 边的宽度
                                width: 1.1,
                                // 边的曲率,0 表示直线
                                curveness: 0,
                                // 边的颜色
                                color: "#262626",
                            }
                        }
                    ]
                };


                myChart.setOption(option);
                myChart.on('click', function (params) {
                    if (params.dataType === 'node') {
                        const content = `${params.data.name}\n${params.data.detail}`;
                        alert(content); // 使用 alert 弹窗显示完整内容
                    }
                })
                /* 拖拽不回弹
                myChart.on('mouseup', function (params) {
                    var op = myChart.getOption();
                    op.series[0].data[params.dataIndex].x = params.event.offsetX;
                    op.series[0].data[params.dataIndex].y = params.event.offsetY;
                    op.series[0].data[params.dataIndex].fixed = true;
                    myChart.setOption(op);
                });*/
            } catch (error) {
                console.error('Error fetching data:', error);
            }
        };

        fetchData();

        return {};
    }
};
</script>

<style scoped>
#graph {
    width: 100%;
    height: 600px;
}
</style>

运行效果

uTools_1717402022067.png

步骤二:加上请求,将json数据换成从neo4j返回的的病虫害种类对应的数据

在 JavaScript 中,diseaseUrls[action]diseaseUrls.action 是两种不同的访问对象属性的方式,它们的使用场景也不同。

区别

  1. diseaseUrls[action](方括号表示法)

    • 动态访问:适用于在运行时动态确定属性名称的情况。方括号内可以是一个变量或表达式。
    • 灵活性:可以使用变量、字符串、甚至数字或其他表达式来访问对象的属性。

    示例

    let action = "番茄早疫病";
    let nodeJsonUrl = diseaseUrls[action].nodeJsonUrl;
    

    这里,action 是一个变量,它的值决定了要访问的属性名称。diseaseUrls[action] 通过变量 action 动态访问对象中的属性。

  2. diseaseUrls.action(点表示法)

    • 静态访问:适用于属性名称在编写代码时已知且是有效的标识符(如简单的字符串)的情况。
    • 限制:属性名称必须是合法的 JavaScript 标识符,不能包含空格、特殊字符,也不能是数字开头。

    示例

    let nodeJsonUrl = diseaseUrls.action; // 只适用于 `diseaseUrls` 对象中有名为 "action" 的属性
    

    这种写法只能用于访问名为 action 的属性,如果 diseaseUrls 中没有 action 这个属性,或者需要动态访问属性,这种方法就无法使用。

为什么在你的代码中使用 diseaseUrls[action]

在你的代码中,action 是一个变量,它的值是用户选择的某个病害的名称。因此,action 的值可能是 "番茄早疫病""番茄晚疫病" 等等。因为你需要根据 action 的值动态地访问 diseaseUrls 对象中的属性,所以必须使用方括号表示法 diseaseUrls[action]

如果使用 diseaseUrls.action,JavaScript 只会试图访问名为 "action" 的属性,而不是将 action 变量的值作为属性名称来访问,因此无法满足动态访问的需求。

总结

  • diseaseUrls[action] 允许根据变量的值动态地访问对象的属性,这是你代码中所需的功能。
  • diseaseUrls.action 只能访问名为 "action" 的属性,不适用于需要动态确定属性名称的场景。

上传到Github Pages

  1. 修改路由为hash
const router = createRouter({
    history: createWebHashHistory(),
    routes,
})
  1. 修改vite配置
export default defineConfig({
  assetsDir: 'assets',
  base: './',
})
  1. 解决MIxed混合内容内容问题

前端地址:atlfsj.github.io/tomato-iden…

后端地址:http://...

报错:

Mixed Content: The page at 'atlfsj.github.io/tomato-iden…' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '1a96d5e.r20.cpolar.top/'. This request has been blocked; the content must be served over HTTPS.

解决方法:

在我们的网站标签里面加入如下内容即可,它会自动将HTTP请求升级成安全的HTTPS请求

<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">  

不过这个有前提:

1、升级后的链接在服务器端必需有对应的资源。
2、只会升级网站内部的链接,对于不属于网站同部的链接,则保持默认状态。
3、并不是所有的浏览器兼容此 HTTP 请求头,兼容表如下