将页面按比例划分,并将底部固定
.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');
}
});
反思: 为什么beforeEach和router.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数据展示在前端
思路:
点击病虫害名称触发函数向flask发送对应的post请求 -->
flask接收数据并作对应查询返回具体信息 -->
nodejs处理请求,将数据写入links和node两个json文件中 -->
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>
运行效果
步骤二:加上请求,将json数据换成从neo4j返回的的病虫害种类对应的数据
在 JavaScript 中,diseaseUrls[action] 和 diseaseUrls.action 是两种不同的访问对象属性的方式,它们的使用场景也不同。
区别
-
diseaseUrls[action](方括号表示法) :- 动态访问:适用于在运行时动态确定属性名称的情况。方括号内可以是一个变量或表达式。
- 灵活性:可以使用变量、字符串、甚至数字或其他表达式来访问对象的属性。
示例:
let action = "番茄早疫病"; let nodeJsonUrl = diseaseUrls[action].nodeJsonUrl;这里,
action是一个变量,它的值决定了要访问的属性名称。diseaseUrls[action]通过变量action动态访问对象中的属性。 -
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
- 修改路由为hash
const router = createRouter({
history: createWebHashHistory(),
routes,
})
- 修改vite配置
export default defineConfig({
assetsDir: 'assets',
base: './',
})
- 解决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 请求头,兼容表如下