当前位置: 首页 > news >正文

自学系列之小游戏---贪吃蛇(vue3+ts+vite+element-plus+sass)(module.scss + tsx)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、逻辑设计分析
  • 二、代码实现
    • 1.TS interface
    • 2.javascript
    • 3.页面样式(Sass)
  • 三、截图展示
  • 四、总结


前言

主要技术如下:vue3 、vite 、ts、element-plus 、 tsx、sass、xxx.module.scss


一、逻辑设计分析

数据定义定义

index  : 总下标
colNum : 列数
colIdx : 列下标
rowNum : 行数
rowIdx : 行下标
isSnake: 是否属于模型内容
isHead : 是否属于模型头部
isTail : 是否属于模型尾部

生成网格

1、生成整体网格数据单位
2、生成所有行数据
3、生成所有列数据

初始化snake

1、生成默认snake长度
2、生成默认snake位置
3、定义snake首尾

速度

1、定义初始速度
2、定义加速度模型

生成目标

1、随机生成
2、新增eat目标单元* 生成时排除snake模型部分,随机生成位置

snake移动逻辑

1、移动UI方案* 网格背景变化* 变化速度取移动速度间隔值
2、移动方向方案* 记录移动方向 默认向右* 当前方向向右`向上取当前snake头部行数加一与行下标取下一次位移目标位置为头部数据改变前进方向为向上撞自己身体判断向右操作无效向右撞墙判断撞自己身体判断(后期加入向右加速度,keydown加速度,keyup取消加速度)向下取当前snake头部行数减一与行下标取下一次位移目标位置为头部数据改变前进方向为向下撞自己身体判断向左操作无效`* 当前方向向左`向上取当前snake头部行数加一与行下标取下一次位移目标位置为头部数据改变前进方向为向上撞自己身体判断向右操作无效向下取当前snake头部行数减一与行下标取下一次位移目标位置为头部数据改变前进方向为向下撞自己身体判断向左操作无效向左撞墙判断撞自己身体判断(后期加入向左加速度,keydown加速度,keyup取消加速度)`* 当前方向向上`向上操作无效向上撞墙判断撞自己身体判断(后期加入向上加速度,keydown加速度,keyup取消加速度)向右取当前snake头部列数加一与列下标取下一次位移目标位置为头部数据改变前进方向为向右撞自己身体判断向下操作无效向左操作取当前snake头部列数减一与列下标取下一次位移目标位置为头部数据改变前进方向为向左撞自己身体判断`* 当前方向向下`向上操作无效向右取当前snake头部列数加一与列下标取下一次位移目标位置为头部数据改变前进方向为向右撞自己身体判断向下操作无效向下撞墙判断撞自己身体判断(后期加入向上加速度,keydown加速度,keyup取消加速度)向左操作取当前snake头部列数减一与列下标取下一次位移目标位置为头部数据改变前进方向为向左撞自己身体判断`

游戏结束判断

1、撞墙死亡
2、撞自己身体死亡
3、身体占满网格游戏通关

分数计算

1、身总长度减去初始身体长度

二、代码实现

1.TS interface

代码如下(示例):

declare namespace Ad{namespace Game{namespace Snake {interface BaseItem {/** 下标 */index:number,/** 列数 */colNum:number,/** 列坐标 */colIdx:number,/** 行数 */rowNum:number,/** 行坐标 */rowIdx:number,/** 是否属于目标得分点 */newSpot:boolean,/** 是否属于模型内容 */isSnake:boolean,/** 是否属于模型头部 */isHead:boolean,/** 是否属于模型尾部 */isTail:boolean}}}
}

2.javascript

代码如下:

import { Ref, computed, defineComponent, reactive, ref } from "vue";
import SnakeScss from './greedySnake.module.scss'
import '../game.scss'
import { ElMessageBox } from 'element-plus'
import { secondsToDate } from "@/utils/utils";export default defineComponent({setup(props, ctx) {/** 行-基点数 */const row    = 29/** 列-基点数 */const column = 29/** 时间 */let timer    = ref(0)/** 当前游戏状态 */let curState = ref(false)/** 贪吃蛇数据模型 */let snakeData:Ref<Array<Ad.Game.Snake.BaseItem>> = ref([])/** 时间记录器 */let timeStamp: undefined | NodeJS.Timer;/** 速度移动记录器 */let timeStampMove: undefined | NodeJS.Timer;/** 前进方向 */let direction = ref('right')/** 能用做得分点的框 */let canCreatStops:Ref<Array<number>> = ref([])/** 得分 */let score = computed(() => snakeData.value.length - 5 )/** 移动速度 */let speed = computed(() => {const baseSpeed = 500return baseSpeed - score.value * ((500 - 40) / gridData.length)})const snakeMove = () => {let nextItem:Ad.Game.Snake.BaseItem | null = nullconst curLastSpot = snakeData.value[snakeData.value.length-1]switch(direction.value){case 'top':nextItem = gridData[curLastSpot.index - row]break;case 'right':nextItem = gridData[curLastSpot.index + 1]if(nextItem.rowNum !== curLastSpot.rowNum) nextItem = nullbreak;case 'bottom':nextItem = gridData[curLastSpot.index + row]break;case 'left':nextItem = gridData[curLastSpot.index - 1]if(nextItem.rowNum !== curLastSpot.rowNum) nextItem = nullbreak;default:console.error('方向错误:', direction.value)}if(nextItem?.isSnake || !nextItem){gameOver()} else {updateSnake(nextItem)}}const windowKeyDown = (ev:KeyboardEvent) => {const { keyCode } = evev.preventDefault()switch(keyCode){case 37: //左键if(direction.value === 'right') returnelse direction.value = 'left'break;case 38://上键if(direction.value === 'bottom') returnelse direction.value = 'top'break;case 39://右键if(direction.value === 'left') returnelse direction.value = 'right'break;case 40://下键if(direction.value === 'top') returnelse direction.value = 'bottom'break;default:console.log('keyCode:', keyCode)}snakeMove()}/** 设置运动时间更新机制 */const setSpeedTime = () => {if(!curState.value) returnif(timeStampMove)clearTimeout(timeStampMove)timeStampMove = setTimeout(() => {snakeMove()setSpeedTime()}, speed.value)}/** 开始游戏 */const startGame = () => {window.addEventListener('keydown', windowKeyDown)initNewSpot()curState.value = truesetSpeedTime()timeStamp = setInterval(() => timer.value += 1, 1000)}/** 重置 */const resetGame = () => {window.removeEventListener('keydown', windowKeyDown)clearInterval(timeStamp)clearTimeout(timeStampMove)curState.value  = false}/** 游戏结束 */const gameOver = () => {resetGame()ElMessageBox.confirm(`本次存活时间${secondsToDate(timer.value, 'HH*mm*ss', true )},本次得分${score.value},再接再厉。`,'游戏结束',{confirmButtonText: 'OK',showCancelButton:false,showClose:false,closeOnClickModal:false,closeOnPressEscape:false,type: 'warning',}).then(res => {timer.value     = 0snakeData.value = []direction.value = 'right'initGrid()})}/** 得分、计时、操作面板生成 */const renderHeader = () => {return <div class='header'><div class='header-item'><div>时间:</div><div class='number-value'>{ secondsToDate(timer.value, 'HH:mm:ss', true) || '00:00:00' }</div></div><div class='header-item'><div>得分:</div><div class='number-value'>{ score.value || 0 }</div></div><div class='header-item'>{curState.value ? <div class='btn' onClick={ gameOver }>结束游戏</div> : <div class='btn' onClick={ startGame }>开始游戏</div>}</div></div>}/** 网格数据 */let gridData:Array<Ad.Game.Snake.BaseItem> = reactive([])const renderGrid = () => {return <div class={SnakeScss['snake-grid']}>{ gridData.map(gridItem => renderGridItem(gridItem)) }</div>}const renderGridItem = (gridItem:Ad.Game.Snake.BaseItem) => {const activeClass = gridItem.isSnake ? SnakeScss['grid-item_active'] : ''let directionClass:{[key:string]:string} = {top:'snake-title_top',right:'snake-title_right',bottom:'snake-title_bottom',left:'snake-title_left'}const headClass = gridItem.isHead ? SnakeScss[directionClass[direction.value]] : ''const newSpotClass = gridItem.newSpot ? SnakeScss['grid-item_new-spot'] : ''return <div class={[SnakeScss['grid-item'], activeClass, headClass, newSpotClass] }>{gridItem.isHead && [<div class={SnakeScss['eye']}></div>,<div class={SnakeScss['eye']}></div>]}</div>}/** 初始化网格 */const initGrid = () => {gridData = []const allGridNums = row * columnfor (let index = 0; index < allGridNums; index++) {let currentRow    = Math.ceil((index + 1) / row)let currentColumn = (index+1) % rowlet currentGrid:Ad.Game.Snake.BaseItem = {index:index,rowNum:currentRow,rowIdx:currentColumn,colNum:currentColumn,colIdx:currentRow,newSpot:false,isSnake:false,isHead:false,isTail:false}gridData.push(currentGrid)}initSnakePosition()}/** 初始化贪吃蛇数据模型 */const initSnakePosition = () => {const rowMidDrop    = Math.ceil(row / 2)const columnMidDrop = Math.ceil(column / 2)const mindSpotIdx = ((rowMidDrop - 1) * row) + (columnMidDrop - 1)const snakeDefaultSpotIdxs = [mindSpotIdx-2, mindSpotIdx-1, mindSpotIdx, mindSpotIdx+1, mindSpotIdx+2]for (let index = 0; index < snakeDefaultSpotIdxs.length; index++) {gridData[snakeDefaultSpotIdxs[index]].isSnake = trueif(index === 0){gridData[snakeDefaultSpotIdxs[index]].isTail = true}if(index === snakeDefaultSpotIdxs.length - 1){gridData[snakeDefaultSpotIdxs[index]].isHead = true}snakeData.value.push(gridData[snakeDefaultSpotIdxs[index]])}}/*** 更新贪吃蛇位置和长度* @param nextItem 下一个目标位置*/const updateSnake = (nextItem:Ad.Game.Snake.BaseItem) => {const preIdx = snakeData.value[snakeData.value.length - 1].indexgridData[preIdx].isHead = falsesnakeData.value.push(nextItem)if(snakeData.value.length == gridData.length){resetGame()ElMessageBox.confirm(`本次存活时间${secondsToDate(timer.value, 'HH*mm*ss', true )},本次得分${score.value},太厉害了,游戏通关。`,'通关',{confirmButtonText: 'OK',showCancelButton:false,showClose:false,closeOnClickModal:false,closeOnPressEscape:false,type: 'success',}).then(res => {timer.value     = 0snakeData.value = []direction.value = 'right'initGrid()})return}gridData[nextItem.index].isHead = truegridData[nextItem.index].isSnake = trueif(!nextItem.newSpot) {const delItme:Ad.Game.Snake.BaseItem = snakeData.value.shift() as Ad.Game.Snake.BaseItemgridData[delItme.index].isSnake = false} else {gridData[snakeData.value[snakeData.value.length - 1].index].newSpot = falseinitNewSpot()}}/** 生成新的目标得分点 */const initNewSpot = () => {const snakeDataIdxs = snakeData.value.map(item => item.index)canCreatStops.value = gridData.filter(item => !snakeDataIdxs.includes(item.index)).map(item => item.index)const idx = Number((Math.random() * canCreatStops.value.length - 1).toFixed(0))gridData[canCreatStops.value[idx]].newSpot = true}initGrid()return () => <div class='game-content'>{ renderHeader() }{ renderGrid() }</div>}
})

3.页面样式(Sass)

greedySnake.module.scss

.snake-grid{display: flex;flex-wrap: wrap;border: 1px solid #eee;border-radius: 4px;margin: 0 auto;margin-top: 20px;width: 580px;.grid-item{width: 20px;height: 20px;.eye{width: 5px;height: 5px;background-color: #fff;border-radius: 50%;margin-left: 10px;margin-top: 3px;}}.grid-item:nth-child(2n){background-color:rgb(248, 248, 248)}.grid-item:nth-child(2n-1){background-color:rgb(255, 255, 255)}.grid-item_active{background-color: rgb(0, 0, 0) !important;}.snake-title_top{border-top-right-radius: 50%;border-top-left-radius: 50%;}.snake-title_right{border-top-right-radius: 50%;border-bottom-right-radius: 50%;}.snake-title_bottom{border-bottom-right-radius: 50%;border-bottom-left-radius: 50%;}.snake-title_left{border-top-left-radius: 50%;border-bottom-left-radius: 50%;}.grid-item_new-spot{background-color: rgb(0, 0, 0) !important;}
}

game.scss

.game-content{width: 900px;margin: 0 auto;height: 700px;margin-top: 40px;box-shadow: 0px 1px 10px 4px #ccc;border-radius: 10px;user-select: none;.header{display: flex;justify-content: space-around;height: 60px;border-bottom: 1px solid #eee;margin: 0 20px;align-items: center;.header-item{display: flex;align-items: center;font-weight: bold;font-size: 16px;.number-value{font-size: 18px;}.btn{width: 120px;border-radius: 10px;height: 36px;text-align: center;line-height: 36px;box-shadow: 0px 1px 10px 0px #ccc;cursor: pointer;}}}
}

三、截图展示

在这里插入图片描述
在这里插入图片描述

四、总结

实现贪吃蛇小游戏使用的技术有为了使用而使用的嫌疑,使用还有些不太熟练,望大家多多理解,如有建议欢迎多多评论或私信指教。

相关文章:

自学系列之小游戏---贪吃蛇(vue3+ts+vite+element-plus+sass)(module.scss + tsx)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、逻辑设计分析二、代码实现1.TS interface2.javascript3.页面样式&#xff08;Sass&#xff09; 三、截图展示四、总结 前言 主要技术如下&#xff1a;vue3…...

JAVA项目中什么是DTO、DAO、PO、Controller、Common

DTO&#xff08;Data Transfer Object&#xff09;和DAO&#xff08;Data Access Object&#xff09;是Java中常用的两种设计模式&#xff0c;它们在软件开发中扮演着不同的角色。 1. **DTO (Data Transfer Object)**&#xff1a;数据传输对象&#xff0c;主要用于在远程调用等…...

Alibaba Druid整合

文章目录 方式一&#xff1a;自定义整合方式二&#xff1a;使用 Druid 官方的 Starter Druid官网&#xff1a;https://github.com/alibaba/druidDruid官网文档&#xff08;中文&#xff09;&#xff1a;https://github.com/alibaba/druid/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%…...

SpringCloud 微服务全栈体系(三)

第五章 Nacos 注册中心 国内公司一般都推崇阿里巴巴的技术&#xff0c;比如注册中心&#xff0c;SpringCloudAlibaba 也推出了一个名为 Nacos 的注册中心。 一、认识和安装 Nacos 1. 认识 Nacos Nacos是阿里巴巴的产品&#xff0c;现在是SpringCloud中的一个组件。相比Eure…...

VScode连接的服务器上使用jupyter显示请选择内核源

问题复现 我实在VScode上用ssh-remote连接的服务器&#xff0c;想用.ipynb文件上写东西&#xff0c;结果窗口上方弹出一个输入框&#xff0c;“请键入以选择内核”&#xff1b; 在扩展里找到jupyter更新一下 之前左边的图标是灰色的&#xff0c;后来我下下载了新的版本&#…...

新能源汽车展厅用哪些种类的显示屏比较好?

现在有越来越多的新能源汽车展厅开到了商场、购物中心当中。在新能源汽车展厅中&#xff0c;显示屏已经成为不可或缺的设备设施&#xff0c;可以用来展现产品介绍、优惠信息、文化宣传等。那么新能源汽车展厅的显示大屏用什么屏比较好呢&#xff1f; LED大屏幕&#xff1a;LED显…...

proxmox pve /dev/mapper/pve-root扩容

vgs3 pvs4 vgs5 lvs6 lvremove /dev/pve/data8 lvresize -l 100%FREE /dev/pve/root9 resize2fs /dev/mapper/pve-root 10 history...

【ECS游戏架构】逻辑帧驱动带来的性能和即时性问题分析

1024水一篇~ 个人拙见&#xff0c;如有错误希望大佬拔刀纠正。 根据守望先锋在GDC会议上对ECS架构的描述&#xff0c;所有的系统(system)都是由逻辑帧驱动的&#xff1a;每帧遍历所有的system&#xff0c;并调用system的update()更新游戏世界的状态。 在实际应用中这可能会存…...

数据库监控:关键指标和注意事项

【squids.cn】 全网zui低价RDS&#xff0c;免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等 听到模糊的说法“我们的数据库有问题”对于任何数据库管理员或管理员来说都是一场噩梦。有时是真的&#xff0c;有时不是&#xff0c;到底问题出在哪里呢&#xff1f;真…...

高防回源ip被源站拦截怎么办

​  在进行网站运营过程中&#xff0c;我们经常会遇到DDoS攻击等网络安全威胁。为了保护网站的正常运行&#xff0c;很多企业选择使用高防服务来应对这些攻击。有时候我们可能会遇到一个问题&#xff0c;就是高防回源IP被源站拦截的情况。 那么&#xff0c;当我们发现高防回源…...

关于集群和分布式部署

EJB的RPC是同步调用可实现分布式计算&#xff0c;是SessionBean和EntityBean用的&#xff0c;而JMS是异步调用。RMI&#xff0c;和webservice也可以实现分布式计算。 举例说明&#xff0c;假设我们的系统有三个EJB组件&#xff1a;人事、财务、销售&#xff0c;都是开放远程接口…...

XIlinx提供的DDR3 IP与 UG586

DDR系统需要关注的三样东西&#xff1a;控制器、PHY、SDRAM颗粒&#xff0c;但这是实现一个DDR3 IP所需要的&#xff0c;如果只希望调用IP的话&#xff0c;则只需要调用IP即可&#xff0c;目前时间紧急&#xff0c;我先学一学如何使用IP&#xff0c;解决卡脖子的问题&#xff0…...

C++数据结构X篇_19_排序基本概念及冒泡排序(重点是核心代码)

文章目录 1. 排序基本概念2. 冒泡排序2.1 核心代码2.2 冒泡排序代码2.3 查看冒泡排序的时间消耗2.4 冒泡排序改进版减小时间消耗 1. 排序基本概念 现实生活中排序很重要&#xff0c;例如:淘宝按条件搜索的结果展示等。 概念 排序是计算机内经常进行的一种操作&#xff0c;其目…...

LeetCode LCR 179. 查找总价格为目标值的两个商品

和为 s 的两个数字 题目链接 LCR 179. 查找总价格为目标值的两个商品 购物车内的商品价格按照升序记录于数组 price。请在购物车中找到两个商品的价格总和刚好是 target。若存在多种情况&#xff0c;返回任一结果即可。 示例 1&#xff1a; 输入&#xff1a;price [3, 9, 12, …...

上架用的SDK三方应用隐私

SDK名称&#xff1a;华为推送 使用目的&#xff1a;用于向华为手机用户推送消息 使用场景&#xff1a;用户账号相关促销活动、消息提醒更新时 信息收集类型&#xff1a;设备相关信息&#xff08;Android_ID&#xff09;使用的敏感权限&#xff1a;不涉及 使用的敏感权限&am…...

从REST到GraphQL:升级你的Apollo体验

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…...

Jupyter使用技巧-环境篇

不同于其他IDE&#xff0c;有时会出现找不到文件路径&#xff0c;通常是因为当前工作目录&#xff08;working directory&#xff09;不同所导致的。Jupyter Notebook 会在启动时选择一个初始的工作目录&#xff0c;而这个目录可能与你运行 .py 文件时所在的目录不同。 import…...

软件项目管理【UML-组件图】

目录 一、组件图概念 二、组件图包含的元素 1.组件&#xff08;Component&#xff09;->构件 2.接口&#xff08;Interface&#xff09; 3.外部接口——端口 4.连接器&#xff08;Connector&#xff09;——连接件 4.关系 5.组件图表示方法 三、例子 一、组件图概念…...

npm版本错误——npm ERR! code ERESOLVE 解决方法

起因 项目中echart版本过低&#xff0c;导致某些图表不能正确显示&#xff0c;所以大手一挥&#xff0c;将echart版本从4升级到了5&#xff0c; 再去运行项目的时候 就发现项目报错了 npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! …...

基于卷积神经网络的乳腺癌分类 深度学习 医学图像 计算机竞赛

文章目录 1 前言2 前言3 数据集3.1 良性样本3.2 病变样本 4 开发环境5 代码实现5.1 实现流程5.2 部分代码实现5.2.1 导入库5.2.2 图像加载5.2.3 标记5.2.4 分组5.2.5 构建模型训练 6 分析指标6.1 精度&#xff0c;召回率和F1度量6.2 混淆矩阵 7 结果和结论8 最后 1 前言 &…...

微信小程序之bind和catch

这两个呢&#xff0c;都是绑定事件用的&#xff0c;具体使用有些小区别。 官方文档&#xff1a; 事件冒泡处理不同 bind&#xff1a;绑定的事件会向上冒泡&#xff0c;即触发当前组件的事件后&#xff0c;还会继续触发父组件的相同事件。例如&#xff0c;有一个子视图绑定了b…...

Leetcode 3576. Transform Array to All Equal Elements

Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接&#xff1a;3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到&#xf…...

ssc377d修改flash分区大小

1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...

江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命

在华东塑料包装行业面临限塑令深度调整的背景下&#xff0c;江苏艾立泰以一场跨国资源接力的创新实践&#xff0c;重新定义了绿色供应链的边界。 跨国回收网络&#xff1a;废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点&#xff0c;将海外废弃包装箱通过标准…...

高危文件识别的常用算法:原理、应用与企业场景

高危文件识别的常用算法&#xff1a;原理、应用与企业场景 高危文件识别旨在检测可能导致安全威胁的文件&#xff0c;如包含恶意代码、敏感数据或欺诈内容的文档&#xff0c;在企业协同办公环境中&#xff08;如Teams、Google Workspace&#xff09;尤为重要。结合大模型技术&…...

Robots.txt 文件

什么是robots.txt&#xff1f; robots.txt 是一个位于网站根目录下的文本文件&#xff08;如&#xff1a;https://example.com/robots.txt&#xff09;&#xff0c;它用于指导网络爬虫&#xff08;如搜索引擎的蜘蛛程序&#xff09;如何抓取该网站的内容。这个文件遵循 Robots…...

NFT模式:数字资产确权与链游经济系统构建

NFT模式&#xff1a;数字资产确权与链游经济系统构建 ——从技术架构到可持续生态的范式革命 一、确权技术革新&#xff1a;构建可信数字资产基石 1. 区块链底层架构的进化 跨链互操作协议&#xff1a;基于LayerZero协议实现以太坊、Solana等公链资产互通&#xff0c;通过零知…...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)

文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...

华硕a豆14 Air香氛版,美学与科技的馨香融合

在快节奏的现代生活中&#xff0c;我们渴望一个能激发创想、愉悦感官的工作与生活伙伴&#xff0c;它不仅是冰冷的科技工具&#xff0c;更能触动我们内心深处的细腻情感。正是在这样的期许下&#xff0c;华硕a豆14 Air香氛版翩然而至&#xff0c;它以一种前所未有的方式&#x…...

使用Spring AI和MCP协议构建图片搜索服务

目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式&#xff08;本地调用&#xff09; SSE模式&#xff08;远程调用&#xff09; 4. 注册工具提…...