一.测试flask(暂时用不到,仅安装)
# main.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
if __name__ == '__main__':
app.run(debug=True)
测试Flask后端正常安装,可以访问
运行 python main.py 如图
二.使用json-server模拟后端api
用npm安装json-server作为数据库测试
模拟数据库如下
birds_data_test.json
{
"bird": [
{
"id": 1,
"scientific_name": "家麻雀",
"latin_name": "Passer domesticus",
"order": "雀形目",
"family": "雀科",
"genus": "麻雀属",
"feature_description": "小型鸟类,体长14厘米左右,喙短粗呈圆锥状...",
"subspecies_distribution": "共有12个亚种,广泛分布于欧洲、亚洲和非洲...",
"collection_site": "北京市海淀区颐和园",
"morphological_characteristic": "雄鸟顶冠及颈背灰色,雌鸟色淡,具深色纵纹...",
"distribution": "世界分布广泛,中国大部分地区均有分布...",
"information": "常见留鸟,喜群居,主要以谷物和昆虫为食...",
"picture": "sparrow.jpg"
},
{
"id": 2,
"scientific_name": "喜鹊",
"latin_name": "Pica pica",
"order": "雀形目",
"family": "鸦科",
"genus": "喜鹊属",
"feature_description": "中型鸟类,体长40-50厘米,羽毛黑蓝相间...",
"subspecies_distribution": "共有10个亚种,分布于欧亚大陆及非洲北部...",
"collection_site": "上海市浦东新区世纪公园",
"morphological_characteristic": "头、颈、背至尾均为黑色,并自前往后分别呈现紫色...",
"distribution": "除南美洲、大洋洲与南极洲外,几乎遍布世界各大陆...",
"information": "杂食性鸟类,适应性强,在中国文化中常象征吉祥...",
"picture": "magpie.jpg"
}
]
}
运行json-server
json-server --watch .\birds_data_test.json --port 3000
发现使用12.18.3版Node.js运行报错
更新node为22.16.0稳定版
成功运行
访问api获取json数据如下
三.设计前端页面
由于获取的数据中,多数鸟类没有对应图片,所以需要设计一些图片展示少的,文字介绍多的
需要按照科和目做成二级索引
鸟类有26目,需要考虑分类目录的长度
将以上需求告诉豆包,豆包给出设计如下
主页图片
鸟类详情页
四.开始前端开发
vue 套装
axios
原生css
创建工程化项目
npm create vue@latest
header部分css实现
.center {
width:80%;
position: relative;
left:50%;
transform:translateX(-50%);
display: flex;
justify-content: space-between;
align-items: center;
}
header {
width: 100%;
background: white;
padding:20px 0;
}
.logo {
font-size: xxx-large;
color: #353f39;
}
.rounded {
border-radius: 10px;
}
.search-box {
display: flex;
gap:10px;
}
.search-box input {
padding:10px
}
.search-box button {
padding:8px 30px;
background: #42756c;
color:white;
}
6月16日下午
主页部分涉及复杂表格,打算引入vue组件
安装 vxe table
npm install vxe-table@4.13.39
试用之后发现需要安装vxe-ui才有完整的组件,如搜索,分页等
安装vxe-ui
npm install vxe-pc-ui@4.6.23
最终完整主页代码如下
<script setup>
import {reactive, ref, onMounted} from 'vue'
import XEUtils from 'xe-utils'
import router from "@/router/index.js";
import request from '@/utils/request'
//存储选中的左侧菜单
const currentIndex = ref(0);
const currentOrder = ref("")
const loading = ref(false)
const filterName = ref('')
const list = ref([])
const tableData = ref([])
//全文搜索
const handleSearch = () => {
const filterVal = String(filterName.value).trim().toLowerCase()
if (filterVal) {
//控制分页 关
pageOn.value = false;
const filterRE = new RegExp(filterVal, 'gi')
const searchProps = ['scientific_name', 'latin_name', 'order_name', 'family_name']
const rest = tableData.value.filter(item => searchProps.some(key => String(item[key]).toLowerCase().indexOf(filterVal) > -1))
list.value = rest.map(row => {
// 搜索为克隆数据,不会污染源数据
const item = XEUtils.clone(row)
searchProps.forEach(key => {
item[key] = String(item[key]).replace(filterRE, match => `<span class="keyword-highlight">${match}</span>`)
})
return item
})
} else {
list.value = tableData.value
//控制分页 开
pageOn.value = true;
}
}
// 节流函数,间隔500毫秒触发搜索
// const searchEvent = XEUtils.throttle(function () {
// handleSearch()
// }, 500, {trailing: true, leading: true})
// handleSearch()
//左侧菜单数据
const menuData = ref();
const fetchMenu = () => {
request.get('/list')
.then(response => {
menuData.value = response.data.order_list;
currentOrder.value = response.data.order_list[0];
getList(currentOrder.value);
})
}
onMounted(() => {
fetchMenu();
});
//按下菜单事件
const selectOrder = (value, index) => {
if(document.querySelector("#mu-input").checked) {
document.querySelector("#mu-input").checked = false;
}
pageOn.value = true;
filterName.value = "";
currentIndex.value = index;
currentOrder.value = value;
getList(currentOrder.value);
if (value === "雀形目") {
setTimeout(e => {
loading.value = true
})
loading.value = false
}
}
const getList = (value) => {
request.get('/all?order=' + value)
.then(response => {
tableData.value = response.data.data;
list.value = response.data.data;
handlePageData();
})
}
//搜索关闭分页
const pageOn = ref(true)
//分页
const pageVO = reactive({
total: 0,
currentPage: 1,
pageSize: 10
})
const handlePageData = () => {
loading.value = true
setTimeout(() => {
const {pageSize, currentPage} = pageVO
pageVO.total = tableData.value.length
list.value = tableData.value.slice((currentPage - 1) * pageSize, currentPage * pageSize)
loading.value = false
}, 1)
}
const pageChange = ({pageSize, currentPage}) => {
pageVO.currentPage = currentPage
pageVO.pageSize = pageSize
handlePageData()
}
handlePageData()
const toDetail = (row) => {
// 存储当前行数据到 Pinia
// rowStore.setCurrentRow(row);
// 导航到详情页
router.push(`/detail/${row.id}`);
}
</script>
<template>
<main>
<div class="center">
<div class="main-left rounded">
<input type="checkbox" id="mu-input">
<label class="mu-button hamb" for="mu-input"> <span class="mu">目</span> <span
class="hamb-line"></span></label>
<ul class="main-left-ul">
<li :class="{ 'active': currentIndex === index }"
@mousedown="selectOrder(value,index)" v-for="(value,index) in menuData">
<div><p class="num-mu">{{ index + 1 }}</p>{{ value }}</div>
<div>›</div>
</li>
</ul>
</div>
<div class="main-right">
<div class="right-top">
</div>
<div style="height: 100%">
<div class="search-box">
<vxe-input class="home-search" v-model="filterName" type="search"
placeholder="按下回车" clearable
@keyup.enter="handleSearch"></vxe-input>
<vxe-button @click="handleSearch" status="success" content="搜索表格"></vxe-button>
</div>
<vxe-table
:loading="loading"
stripe
border
class="mylist-table"
height="80%"
:column-config="{useKey: true}"
:row-config="{useKey: true}"
:data="list">
<vxe-column field="scientific_name" title="种名" type="html"></vxe-column>
<vxe-column field="latin_name" width="auto" title="拉丁名" type="html"></vxe-column>
<vxe-column field="order_name" title="目" type="html"></vxe-column>
<vxe-column field="family_name" title="科" type="html"></vxe-column>
<vxe-column field="genus_name" title="属" type="html"></vxe-column>
<vxe-column field="id" title="详细信息" width="auto" type="html">
<template #default="{ row }">
<vxe-button
@click="toDetail(row)"
status="success"
content="详细信息"
></vxe-button>
</template>
</vxe-column>
</vxe-table>
<vxe-pager
v-show="pageOn"
v-model:currentPage="pageVO.currentPage"
v-model:pageSize="pageVO.pageSize"
:total="pageVO.total"
@page-change="pageChange">
</vxe-pager>
</div>
</div>
</div>
</main>
</template>
<style scoped>
.mu-button {
display: block;
height: 84px;
}
.hamb-line {
background: white;
display: none;
height: 4px;
position: relative;
width: 35px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
/* Style span tag */
.hamb-line::before,
.hamb-line::after {
background: white;
content: '';
display: block;
height: 100%;
position: absolute;
transition: all .2s ease-out;
width: 100%;
}
.hamb-line::before {
top: 8px;
}
.hamb-line::after {
top: -8px;
}
#mu-input {
display: none;
}
.search-box {
display: flex;
justify-content: center;
padding: 20px;
border-radius: 10px 10px 0 0;
background: #d3e1c8;
}
.home-search {
border-radius: 5px;
width: 50%;
}
main {
width: 100%;
height: 75%;
}
main > .center {
gap: 20px;
margin: 20px 0;
display: flex;
height: 100%;
}
.main-left {
width: 25%;
height: 100%;
background: #394d44;
}
.main-right {
width: 75%;
height: 100%;
}
.main-left-ul {
height: 83%;
display: flex;
gap: 10px;
flex-direction: column;
overflow: auto;
}
.mu {
font-size: xxx-large;
color: white;
display: flex;
align-items: center;
justify-content: center;
padding: 3%;
}
.main-left-ul > li {
cursor: pointer;
list-style: none;
padding: 10px 30px;
display: flex;
justify-content: space-between;
align-items: center;
color: lightgray;
}
.main-left-ul > li > div:first-child {
display: flex;
justify-content: left;
gap: 20px;
align-items: center;
}
.num-mu {
color: white;
font-size: xx-large;
}
.main-left-ul > li:hover {
background: #aecd7f;
color: #394d44;
}
.active {
background: #aecd7f;
}
.active > div {
color: #394d44;
}
#mu-input:checked ~ nav {
max-height: 100%;
}
#mu-input:checked ~ .hamb .hamb-line {
background: transparent;
}
#mu-input:checked ~ .hamb .hamb-line::before {
transform: rotate(-45deg);
top: 0;
}
#mu-input:checked ~ .hamb .hamb-line::after {
transform: rotate(45deg);
top: 0;
}
@media screen and (max-width: 980px) {
.mu {
display: none;
}
.hamb-line {
display: block;
}
.num-mu {
display: none;
}
main > .center {
flex-direction: column;
}
.main-left {
width: 100%;
}
.main-left ul {
transition: all linear 0.3s;
opacity: 0;
height: 0;
transform: translateY(-50%) scaleY(0);
}
#mu-input:checked ~ ul {
opacity: 1;
height: 83%;
transform: translateY(0) scaleY(1);
}
.main-right {
width: 100%;
}
}
</style>
<style lang="scss" scoped>
.mylist-table {
::v-deep(.keyword-highlight) {
background-color: #FFFF00;
}
}
</style>
完整详情页如下
<template>
<main>
<div v-if="loading">
<div>
<div style="height: 200px;position: relative;">
<vxe-loading v-model="loading"></vxe-loading>
</div>
</div>
</div>
<div v-else-if="rowData" class="center">
<router-link to="/" class="back_index">< 返回主页</router-link>
<div class="main-top">
<div class="name-box">
<div class="name-line">
<div v-if="rowData.picture!=='暂无图片'" class="imgbox">
<img class="bird-img" :src="rowData.picture" alt="">
</div>
<div class="name-both">
<div class="scientific_name name">{{ rowData.scientific_name }}</div>
<div class="latin_name">{{ rowData.latin_name }}</div>
</div>
</div>
</div>
<div class="back-img">
<img class="bird3" src="@/assets/images/bird3.png" alt="">
</div>
</div>
<div class="main-bottom">
<div class="three-box">
<div class="box box1">
<div class="head1">科目属分类</div>
<div class="mks info">
<div v-html="rowData.order_name" class="order_name"></div>
-
<div v-html="rowData.family_name" class="family_name"></div>
-
<div v-html="rowData.genus_name" class="genus_name"></div>
</div>
</div>
<div v-if="rowData.feature_description" class="box box1">
<div class="head1">
形态特征
</div>
<div v-html="rowData.feature_description" class="info feature_description"></div>
</div>
<div v-if="rowData.collection_site"
class="box box1">
<div class="head1">
采集地
</div>
<div v-html="rowData.collection_site" class="info feature_description"></div>
</div>
</div>
<div v-if="rowData.information" class="line-box box information">
<div class="head1">信息</div>
<div class="info" v-html="rowData.information"></div>
</div>
<div v-if="rowData.distribution" class="line-box box distribution">
<div class="head1">分布</div>
<div class="info" v-html="rowData.distribution"></div>
</div>
<div v-if="rowData.subspecies_distribution" class="line-box box distribution">
<div class="head1">亚种及分布</div>
<div class="info" v-html="rowData.subspecies_distribution"></div>
</div>
</div>
</div>
<div v-else>
<div>
<div style="height: 200px;position: relative;">
<vxe-loading v-model="loading"></vxe-loading>
</div>
</div>
</div>
</main>
</template>
<style scoped>
.latin_name {
font-weight: lighter;
}
.imgbox {
margin-right: 30px;
}
.bird-img {
height: 10rem;
border-radius: 10px;
}
.info {
overflow: auto;
color: dimgray;
}
.head1 {
font-size: large;
font-weight: bold;
}
.box1 {
display: flex;
flex-direction: column;
gap: 10px;
padding: 2%;
}
.mks {
display: flex;
}
.bird3 {
position: relative;
height: 100%;
}
.main-bottom {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 20px;
}
.three-box {
display: flex;
gap: 20px;
width: 100%;
height: 35%;
}
.line-box {
display: flex;
flex-direction: column;
gap: 20px;
padding: 3%;
min-height: 25%;
}
.box {
display: flex;
border-radius: 10px;
width: 100%;
background: whitesmoke;
}
.back-img {
display: flex;
justify-content: right;
width: 50%;
}
.name-line {
display: flex;
align-items: center;
}
.name {
font-size: xx-large;
}
.name-box {
width: 50%;
display: flex;
justify-content: center;
flex-direction: column;
}
main > .center {
height: 100%;
}
.main-top {
height: 240px;
display: flex;
color: white;
}
main {
padding-bottom: 100px;
width: 100%;
min-height: 100%;
background: #394e40;
background-size: 40%;
}
@media screen and (max-width: 980px) {
.back-img {
display: none;
}
.name-box {
width: 100%;
}
.main-top {
padding-top: 30px;
}
.imgbox {
width: 100%;
display: flex;
padding-left: 15px;
}
.scientific_name {
font-size: large;
}
.name-both {
width: 100%;
}
.name-line {
flex-direction: column;
gap: 10px;
}
.three-box {
display: flex;
flex-direction: column;
}
}
</style>
<script setup>
import {ref, onMounted, onBeforeUnmount} from 'vue';
import {useRoute, useRouter} from "vue-router";
import request from "@/utils/request.js";
// import {useRowStore} from "@/stores/rowStore.js";
const route = useRoute();
// const rowStore = useRowStore();
const rowData = ref(null);
const loading = ref(true);
const fetchData = async () => {
try {
loading.value = true;
// // 1. 尝试从 Pinia 获取数据
// const storedRow = rowStore.getCurrentRow;
//
// if (storedRow && storedRow.id.toString() === route.params.id) {
// rowData.value = storedRow;
// return;
// }
request.get("/all?id=" + route.params.id).then(response => {
rowData.value = response.data.data[0];
})
} catch (error) {
console.error('获取数据失败:', error);
} finally {
loading.value = false;
}
};
// 组件挂载时获取数据
onMounted(fetchData);
</script>
完整搜索页面如下
<template>
<main>
<div class="center">
<router-link to="/" class="back_index">< 返回主页</router-link>
<vxe-table
:loading="loading"
stripe
border
class="mylist-table"
height="80%"
:column-config="{useKey: true}"
:row-config="{useKey: true}"
:data="list">
<vxe-column field="scientific_name" title="种名" type="html"></vxe-column>
<vxe-column field="latin_name" width="auto" title="拉丁名" type="html"></vxe-column>
<vxe-column field="order_name" title="目" type="html"></vxe-column>
<vxe-column field="family_name" title="科" type="html"></vxe-column>
<vxe-column field="genus_name" title="属" type="html"></vxe-column>
<vxe-column field="id" title="详细信息" width="auto" type="html">
<template #default="{ row }">
<vxe-button
@click="toDetail(row)"
status="success"
content="详细信息"
></vxe-button>
</template>
</vxe-column>
</vxe-table>
<vxe-pager
v-model:currentPage="pageVO.currentPage"
v-model:pageSize="pageVO.pageSize"
:total="pageVO.total"
@page-change="pageChange">
</vxe-pager>
</div>
</main>
</template>
<script setup>
import {reactive, ref, onMounted, watch} from 'vue'
import router from "@/router/index.js";
import {useRoute} from "vue-router";
import request from "@/utils/request.js";
const notFount = ref(false);
const route = useRoute();
const loading = ref(true)
const list = ref([])
const tableData = ref([])
//接收搜索参数
const props = defineProps({
keywords: {
type: String,
default: ''
}
})
//分页
const pageVO = reactive({
total: 0,
currentPage: 1,
pageSize: 10
})
const handlePageData = () => {
loading.value = true
setTimeout(() => {
const {pageSize, currentPage} = pageVO
pageVO.total = tableData.value.length
list.value = tableData.value.slice((currentPage - 1) * pageSize, currentPage * pageSize)
loading.value = false
}, 1)
}
const pageChange = ({pageSize, currentPage}) => {
pageVO.currentPage = currentPage
pageVO.pageSize = pageSize
handlePageData()
}
handlePageData()
const toDetail = (row) => {
// 导航到详情页
router.push(`/detail/${row.id}`);
}
const fetchData = () => {
loading.value = true;
request.get(`/search`,{params:{"search":props.keywords}}).then(response => {
console.log(response.data)
if (response.data.status === 200) {
notFount.value = false;
tableData.value = response.data.data;
} else {
tableData.value = [];
}
loading.value = false
handlePageData()
}).catch(() => {
console.log("搜索不到数据:"+props.keywords);
tableData.value = [];
loading.value = false
handlePageData()
})
}
onMounted(() => {
if (route.query.keywords) {
loading.value = true
fetchData()
}
})
watch(
() => route.query.keywords,
(newVal, oldVal) => {
// 添加空值检查和防抖
if (newVal && newVal !== oldVal) {
loading.value = true;
fetchData();
}
},
{immediate: true} // 可选:初始立即执行(替代onMounted)
);
</script>
<style scoped>
main {
height: 80%;
margin-top: 20px;
}
.mylist-table {
height: 80%;
}
</style>
评论