昆明网站建设公司哪家好做网站cnfg
- 作者: 五速梦信息网
- 时间: 2026年03月21日 10:33
当前位置: 首页 > news >正文
昆明网站建设公司哪家好,做网站cnfg,二维码怎么做网站,信阳网站建设公司汉狮排名目录
一、selectedKeys与onSelect
官方文档
代码演示
onSelect
注意事项
二、expandedKeys与onExpand
官方文档
代码演示
onExpand
注意事项
三、loadedKeys与onLoad和onExpand
官方文档
代码演示 onExpand与onLoad#xff1a;
注意事项
四、loadData
…目录
一、selectedKeys与onSelect
官方文档
代码演示
onSelect
注意事项
二、expandedKeys与onExpand
官方文档
代码演示
onExpand
注意事项
三、loadedKeys与onLoad和onExpand
官方文档
代码演示 onExpand与onLoad
注意事项
四、loadData
官方文档
代码演示 loadData
注意事项
五、树节点局部节点刷新
实现思路
代码演示
六、递归获取与修改数据
获取数据
修改数据
七、总结 最近一周都在忙关于文件管理的东西从提出这个需求到目前实现为止已经快一周的时间了。从最开始的找插件然后发现没有插件可以用再到打算自己手撸一个发现手写树状图过于困难且因为技术力的原因估计会留下很多坑。所以在经过多方考虑以后觉得还是通过 antd-tree手动控制的方式去实现一个文件管理页面。 下面我将着重讲解我在使用antd-tree组件时遇到的各种苦难已经官方文档中方法属性的应用。
一、selectedKeys与onSelect
官方文档
参数说明类型版本selectedKeys受控设置选中的树节点string[]onSelect点击树节点触发function(selectedKeys, e:{selected: bool, selectedNodes, node, event})
代码演示 onSelect 形参
selectedKeys 代表当前选中的树节点的key值。获取的值的格式为[ key ]。可以通过selectedKeys[0]取值。
info: 当前选择的树节点的信息。可以通过info.selectedNodes.props.dataRef.children来获取当前节点的子节点。
注意事项 这里需要注意的是selectedKeys是一个数组类型。有且只有一个当前选中的节点key。一旦点击其他节点数组内的值就会被替换。
如果树组件设置了selectedKeys这个属性那么需要在onSelect函数执行时将值赋给该属性。
二、expandedKeys与onExpand
官方文档
参数说明类型默认值版本expandedKeys受控展开指定的树节点string[][]onExpand展开/收起节点时触发function(expandedKeys, {expanded: bool, node})-
代码演示 onExpand 形参
expandedKeys 代表当前打开的树节点的key值。
info: 当前打开的树节点的信息。
注意事项 这里需要注意的是expandedKeys也是一个数组的格式但它与selectedKeys的区别是selectedKeys始终是一个长度为0或1的数组而expandedKeys则是包含所有被打开的树节点的key值。
三、loadedKeys与onLoad和onExpand
官方文档
参数说明类型默认值版本loadedKeys受控已经加载的节点需要配合 loadData 使用string[][]3.7.0onExpand展开/收起节点时触发function(expandedKeys, {expanded: bool, node})-onLoad节点加载完毕时触发function(loadedKeys, {event, node})-3.7.0
代码演示 onExpand与onLoad
形参
loadedKeys已经完成加载的树节点的key是一个数组的数据类型。
注意事项 这里需要注意的是loadedKeys是一个数组数据类型且可以存放多个key。一旦被加载过以后无论怎么点击都不会再触发重新刷新重新加载了。如果想让其刷新请移步至节点刷新。
四、loadData
官方文档
参数说明类型默认值版本loadData异步加载数据function(node)-loadedKeys受控已经加载的节点需要配合 loadData 使用string[][]3.7.0treeDatatreeNodes 数据如果设置则不需要手动构造 TreeNode 节点key 在整个树范围内唯一array{key, title, children, [disabled, selectable]}-
代码演示 loadData 形参
treeNode 要加载的树节点的信息。
注意事项 这里需要注意的是如果你的树节点是通过点击以后再加载子节点那么对于后端的数据格式返回可能就有一些要求了。比如 title 与 isLeaf 等。当然也可以在loadData中自行设置。 loadData中代码的大概流程就是先判断 treeNode 是否有 children这个属性注意是是否有这个属性如果有这个属性但这个属性为空数组在执行中也会判定为true从而不会执行更新操作而是直接return出去。
五、树节点局部节点刷新
实现思路 因为tree的机制问题当key节点加载过以后该节点将不会再被重新加载因此如果我上传了一个文件实际上服务器上已经有文件了但因为节点刷新问题该节点没有重新刷新我就看不到对应的节点文件。因此需要进行局部节点刷新。 满足节点刷新的条件有这几个。
- loadedKeys中移除该节点的key值和其子孙节点的key值2. treeData中将该节点的children属性删除3. expandedKeys中移除A节点下的所有子孙节点的key值完成这三点以后再将selectedKeys选取到该节点 并将以上数据重新赋值给对应的属性即可完成节点刷新操作。 代码演示 updateTree () {const { selectedKeys , expandedKeys, loadedKeys, treeData } this.state// 获取新的expandedKeys数组不包含该节点及子节点const newExpandedKeys expandedKeys.filter(item {return item.indexOf(selectedKeys[0]) -1})// 获取新的loadedKeys数组不包含该节点及子节点const newLoadedKeys loadedKeys.filter(item {return item.indexOf(selectedKeys[0]) -1})const newTreedata treeDatathis.setState({expandedKeys: […newExpandedKeys,…[\({selectedKeys[0]}]],loadedKeys: [...newLoadedKeys],treeData: this.removeShowData(newTreedata),selectedKeys: [\){selectedKeys[0]}],})}// 获取新的treeData数据removeShowData (datas) {const { selectedKeys } this.stateconst newData datas;function setGrayNode(data){ //遍历树 获取id数组for(var i0;idata.length;i){if(data[i].key selectedKeys[0]){// 如果某一个节点是禁用的它的所有子节点都应该禁用delete data[i].childrencontinue;} else {if(data[i].children){// 如果当前节点有子节点就递归调用一次setGrayNode(data[i].children);}}}}setGrayNode(newData)return newData;} 这里需要注意的是 expandedKeys 虽然删除了当前节点但要想操作通顺需要再次手动赋值将该节点打开并获取新的数据。这样就省去了用户需要再次点击节点的尴尬情况。 六、递归获取与修改数据 因为这是一个树状图数据结构也稍微复杂一些所以获取数据时难免需要通过递归拿取数据。所以需要一个递归函数取实现数据的拿取。 获取数据 //递归获取Showdata数据 getShowData (datas) {const { selectedKeys } this.statedatas.map(item {const { key, children } itemif (key selectedKeys[0]) {//符合条件this.setState({showData: datas})return}//如果有孩子再次调用自己将孩子传进去。if (children children.length 0) {this.getShowData(children)}})} 修改数据 // 获取新的treeData数据removeShowData (datas) {const { selectedKeys } this.stateconst newData datas;function setGrayNode(data){ //遍历树 获取id数组for(var i0;idata.length;i){if(data[i].key selectedKeys[0]){// 如果某一个节点是禁用的它的所有子节点都应该禁用delete data[i].childrencontinue;} else {if(data[i].children){// 如果当前节点有子节点就递归调用一次setGrayNode(data[i].children);}}}}setGrayNode(newData)return newData;} 七、总结 一个星期下来还是比较累的原因是因为组件使用不熟练且自己的技术力较弱导致的但好在也顺利完成任务倒也没有什么大碍。记录一下这一个星期以来遇到的一些问题和实践吧。前端小白一枚如有错误欢迎指正。 源码 import React, { Component } from react; import { connect } from dva; import {Modal,Button,Tree,Row,Col,Empty,Tooltip,Icon,Upload,Popconfirm,Select,Spin,notification } from antd; import { formatMessage, FormattedMessage } from umi-plugin-locale; import globalUtil from ../../utils/global import download from /utils/download; import apiconfig from ../../../config/api.config; import SVG from ./svg; import styles from ./index.less;const { TreeNode, DirectoryTree } Tree; connect(({ appControl }) ({appDetail: appControl.appDetail,}) )class Index extends Component {constructor(props) {super(props);this.state {treeData: [],selectedKeys: [],expandedKeys: [],pathArr: [],keyArr: [],dowloadArr: [],path: ,podsList: [],selectDefaultValue: ,hostPath: this.props this.props.hostPath,selectLoading: false,treeDataLoading: false,loadedKeys:[]}}componentDidMount() {this.fetchInstanceInfo()}// 获取podnamefetchInstanceInfo () {const { dispatch } this.props;dispatch({type: appControl/fetchPods,payload: {team_name: globalUtil.getCurrTeamName(),app_alias: this.props.appAlias,},callback: res {if (res res.list) {this.setState({podsList: res.list.new_pods,selectDefaultValue: res res.list res.list.new_pods[0] res.list.new_pods[0].pod_name,selectLoading: true}, () {if (this.props.isType) {this.determineStorageType()}else{this.getListFiles()}})}}});};// 获取文件类型determineStorageType () {this.props.dispatch({type: appControl/determineStorageType,payload: {team_name: globalUtil.getCurrTeamName(),group_id: this.props.appAlias,region_name: globalUtil.getCurrRegionName(),pod_name: this.state.selectDefaultValue,namespace: this.props.appDetail this.props.appDetail.service this.props.appDetail.service.namespace,volume_path: this.props his.props.volumePath,},callback: res {if(res){this.setState({hostPath: res.bean,},(){this.getListFiles()})}}});};// 获取文件列表getListFiles () {this.props.dispatch({type: appControl/getListFiles,payload: {team_name: globalUtil.getCurrTeamName(),group_id: this.props.appAlias,region_name: globalUtil.getCurrRegionName(),pod_name: this.state.selectDefaultValue,host_path: this.props.appDetail this.props.appDetail.service this.props.appDetail.service.extend_method state_multiple ? \({this.state.hostPath}/\){this.state.selectDefaultValue} : this.state.hostPath,extend_method: this.props.appDetail this.props.appDetail.service this.props.appDetail.service.extend_method},callback: res {if (res res.list) {res.list.map((item, index) {item.key index,item.isLeaf item.is_leaf})this.setState({treeData: res.list,showData: res.list,treeDataLoading: true})}},handleError: res {if(res){notification.error({ message: formatMessage({id:componentOverview.body.DirectoryPersistence.error}) });this.setState({showData: [],treeData: []})}}});}// 获取文件列表updataListFiles (path) {this.setState({treeDataLoading: false},(){this.props.dispatch({type: appControl/getListFiles,payload: {team_name: globalUtil.getCurrTeamName(),group_id: this.props.appAlias,region_name: globalUtil.getCurrRegionName(),pod_name: this.state.selectDefaultValue,host_path: this.props.appDetail this.props.appDetail.service this.props.appDetail.service.extend_method state_multiple ? \({this.state.hostPath}/\){this.state.selectDefaultValue}\({path} : \){this.state.hostPath}\({path},extend_method: this.props.appDetail this.props.appDetail.service this.props.appDetail.service.extend_method},callback: res {if (res res.list) {res.list.map((item, index) {item.key index,item.isLeaf item.is_leaf})this.setState({treeData: res.list,showData: res.list,treeDataLoading: true})}},handleError: res {if(res){notification.error({ message: formatMessage({id:componentOverview.body.DirectoryPersistence.error}) });this.setState({showData: [],treeData: []})}}});})}// 加载树图onLoadData treeNode new Promise(resolve {if (treeNode.props.children) {resolve();return;}setTimeout(() {this.props.dispatch({type: appControl/getListFiles,payload: {team_name: globalUtil.getCurrTeamName(),group_id: this.props.appAlias,region_name: globalUtil.getCurrRegionName(),pod_name: this.state.selectDefaultValue,host_path: this.props.appDetail this.props.appDetail.service this.props.appDetail.service.extend_method state_multiple ? \){this.state.hostPath}/\({this.state.selectDefaultValue}/\){this.state.path} : \({this.state.hostPath}/\){this.state.path},extend_method: this.props.appDetail this.props.appDetail.service this.props.appDetail.service.extend_method},callback: res {if (res) {if (res.list res.list.length 0) {this.setState({treeData: […this.state.treeData],showData: res.list});treeNode.props.dataRef.children []resolve();} else {const arr res.listarr.map((item, index) {item.key \({treeNode.props.eventKey}-\){index}item.isLeaf item.is_leaf})treeNode.props.dataRef.children arrthis.setState({treeData: […this.state.treeData],showData: res.list});resolve();}}}});}, 100)});// 渲染函数renderTreeNodes data data data.map((item, index) {if (item.isLeaf) {return (TreeNode title{item.title} key{item.key} dataRef{item} {this.renderTreeNodes(item.children)}/TreeNode);}return null;});//选择树节点 onSelect (selectedKeys, info) {// 选择为空时直接returnif (selectedKeys selectedKeys.length 0) {return null}if (info) {const { selectedNodes } infoconst { props } selectedNodes[0]const { dataRef } propsthis.setState({selectedKeys: selectedKeys,expandedKeys: this.state.expandedKeys.includes(selectedKeys[0]) ? […this.state.expandedKeys] : […this.state.expandedKeys, …selectedKeys],showData: dataRef.children || this.state.showData,dowloadArr: [],pathArr: [],path: }, () {this.getPath()})} else {this.setState({selectedKeys: selectedKeys,expandedKeys: this.state.expandedKeys.includes(selectedKeys[0]) ? […this.state.expandedKeys] : […this.state.expandedKeys, …selectedKeys],dowloadArr: [],pathArr: [],path: }, () {this.getPath()})}}onLoad (loadedKeys) {this.setState({loadedKeys: loadedKeys})}// 展开树图onExpand (expandedKeys, info) {let newLoadKeys this.state.loadedKeysif (this.state.expandedKeys.length expandedKeys.length) {// 当是收起的时候把这个收起的节点从loadedKeys中移除newLoadKeys this.state.loadedKeys.filter((i) expandedKeys.includes(i))}this.setState({expandedKeys: expandedKeys,selectedKeys: [\({info.node.props.dataRef.key}],showData: info.node.props.dataRef.children,loadedKeys: newLoadKeys}, () {this.getPath()})};// 获取后缀名getSvgIcon (name) {if (name) {const str name.substr(name.lastIndexOf(.) 1)return \){str}}}// 鼠标点击folderClick (data) {// 判断data数据是否有孩子如果没有就加载如果有就if (data data.children data.children.length 0) {this.setState({expandedKeys: […this.state.expandedKeys, …[\({data.key}]],selectedKeys: [\){data.key}],showData: data.children,dowloadArr: []}, () {this.getPath()})} else {this.setState({expandedKeys: […this.state.expandedKeys, …[\({data.key}]],selectedKeys: [\){data.key}],dowloadArr: []}, () {this.getPath()})}}//递归获取Showdata数据 getShowData (datas) {const { selectedKeys } this.statedatas.map(item {const { key, children } itemif (key selectedKeys[0]) {this.setState({showData: datas})}if (children children.length 0) {this.getShowData(children)}})}// 获取key值的path数据getPathData (data) {const { treeData, keyArr } this.statedata.map(item {const { title, children } itemif (keyArr.indexOf(\({item.key}) ! -1) {const arr this.state.pathArrarr.push(title)this.setState({pathArr: arr})}if (children children.length 0) {this.getPathData(children)}})}//递归获取path数据 getPath () {const { selectedKeys, treeData, pathArr } this.stateif (selectedKeys []) {return}if (selectedKeys selectedKeys[0]) {const length selectedKeys[0].lengthconst str selectedKeys[0]const arr str.split(-)const keyArr []for (let index 0; index arr.length 1; index) {const newarr arr.slice(0, index)const newstr newarr.join(-)keyArr.push(newstr)}keyArr.shift();this.setState({keyArr: keyArr,pathArr: []}, () {this.getPathData(treeData)})setTimeout(() {const path this.state.pathArr.join(/)this.setState({path: path})}, 100)}}// 返回上一级goBack () {const { selectedKeys } this.state// 如果选择为空则展示所有数据if (selectedKeys[0] undefined) {return}// 如果选择有值且值不大于1if ((selectedKeys[0]).indexOf(-) -1) {this.setState({selectedKeys: [],showData: this.state.treeData,dowloadArr: []}, () {this.getPath()})// 如果选择有值且值大于1} else {this.getShowData(this.state.treeData)this.setState({selectedKeys: [\){selectedKeys[0].substring(0, (selectedKeys[0]).lastIndexOf(-))}],dowloadArr: []}, () {this.getPath()})}}// 下载dowloadTitle (val) {const { dowloadArr } this.statesetTimeout(() {if (dowloadArr.includes(val)) {const arr []dowloadArr.map(item {if (item ! val) {arr.push(item)}})this.setState({dowloadArr: […arr]})} else {const arr []arr.push(val)this.setState({dowloadArr: […this.state.dowloadArr, …arr]})}}, 10)}// 下拉框选择selectChange (val) {this.setState({selectDefaultValue: val},(){this.getListFiles()})}fileDownload () {const { dowloadArr } this.stateif(dowloadArr.length 0 ){notification.info({ message: formatMessage({id:componentOverview.body.DirectoryPersistence.download}) });}else{dowloadArr.map(item {this.fileDownloadApi(item)})}setTimeout((){this.setState({dowloadArr:[]})},100)}// 下载接口fileDownloadApi ( title ) {const dowloadPath this.state.path ? this.props.appDetail this.props.appDetail.service this.props.appDetail.service.extend_method state_multiple ? \({this.state.hostPath}/\){this.state.selectDefaultValue}/\({this.state.path} : \){this.state.hostPath}/\({this.state.path} : this.props.appDetail this.props.appDetail.service this.props.appDetail.service.extend_method state_multiple ? \){this.state.hostPath}/\({this.state.selectDefaultValue} : \){this.state.hostPath};const host apiconfig.baseUrl;const url host.slice(0,host.lastIndexOf(:))// const path \({url}:6060/v2/ws/download/\){title}?path\({dowloadPath}const path http://47.104.161.96:6060/v2/ws/download/\){title}?path\({dowloadPath}this.download(\){path},title)}download (downloadPath, title) {console.log(title.indexOf(txt) -1,title.indexOf() -1);if(title.indexOf(txt) -1){let aEle document.querySelector(#down-a-element);if (!aEle) {aEle document.createElement(a);aEle.setAttribute(target, _blank)aEle.setAttribute(download, title);document.body.appendChild(aEle);}aEle.href downloadPath;if (document.all) {aEle.click();} else {const e document.createEvent(MouseEvents);e.initEvent(click, true, true);aEle.dispatchEvent(e);}}else{var element document.createElement(a);element.setAttribute(href, data:text/plain;charsetutf-8, encodeURIComponent(title));element.setAttribute(download, title);element.style.display none;document.body.appendChild(element);element.click();document.body.removeChild(element);}};uploadChange info {const { path, selectedKeys } this.stateif (info info.file info.file.status done) {notification.success({ message: formatMessage({id:notification.success.upload})});if(selectedKeys[0] undefined){this.getListFiles()}else{this.updateTree()}} else if (info info.file info.file.status error) {notification.error({ message: formatMessage({id:notification.error.update}) });}};updateTree () {const { selectedKeys , expandedKeys, loadedKeys, treeData } this.state// 获取新的expandedKeys数组不包含该节点及子节点const newExpandedKeys expandedKeys.filter(item {return item.indexOf(selectedKeys[0]) -1})// 获取新的loadedKeys数组不包含该节点及子节点const newLoadedKeys loadedKeys.filter(item {return item.indexOf(selectedKeys[0]) -1})const newTreedata treeDatathis.setState({expandedKeys: […newExpandedKeys,…[\({selectedKeys[0]}]],loadedKeys: [...newLoadedKeys],treeData: this.removeShowData(newTreedata),selectedKeys: [\){selectedKeys[0]}],})}// 获取新的treeData数据removeShowData (datas) {const { selectedKeys } this.stateconst newData datas;function setGrayNode(data){ //遍历树 获取id数组for(var i0;idata.length;i){if(data[i].key selectedKeys[0]){// 如果某一个节点是禁用的它的所有子节点都应该禁用delete data[i].childrencontinue;} else {if(data[i].children){// 如果当前节点有子节点就递归调用一次setGrayNode(data[i].children);}}}}setGrayNode(newData)return newData;}render() {const {selectedKeys,expandedKeys,showData,path,dowloadArr,podsList,selectDefaultValue,selectLoading,treeDataLoading,hostPath,loadedKeys} this.stateconst upLoadPath this.state.path ? this.props.appDetail this.props.appDetail.service this.props.appDetail.service.extend_method state_multiple ? \({this.state.hostPath}/\){this.state.selectDefaultValue}/\({this.state.path} : \){this.state.hostPath}/\({this.state.path} : this.props.appDetail this.props.appDetail.service this.props.appDetail.service.extend_method state_multiple ? \){this.state.hostPath}/\({this.state.selectDefaultValue} : \){this.state.hostPath};const host apiconfig.baseUrl;const url host.slice(0,host.lastIndexOf(:))// const upload ${url}:6060/v2/ws/uploadconst upload http://47.104.161.96:6060/v2/ws/uploadconst props {action: upload,data:{path: upLoadPath},method:post,name:packageTarFile,};const isFile showData.filter(item { return item.title.indexOf(.) -1 })const notFile showData.filter(item { return item.title.indexOf(.) ! -1 })const folder []isFile.map((item,index) {if(item.isLeaf true){folder.unshift(item)}else{folder.push(item)}})const showDataArr […folder,…notFile]return (divModalclassName{styles.ModalStyle}title{{formatMessage({id:componentOverview.body.DirectoryPersistence.example})}Selectvalue{selectDefaultValue}style{{ maxWidth: 184, marginLeft: 5 }}onChange{this.selectChange}loading{!selectLoading}{podsList podsList.length 0 podsList.map(item {return Select.Option value{item.pod_name}{item.pod_name}/Select.Option})}/Select/}visible{true}width{1000}closable{false}footer{Upload{…props}showUploadList{false}multipleonChange{this.uploadChange}// directory{true}Button typeprimary style{{ marginRight: 10 }}Icon typeupload / {formatMessage({id:applicationMarket.Offline.upload})}/Button/UploadButton typeprimary onClick{this.fileDownload}Icon typedownload /{formatMessage({id:button.download})}/ButtonButton onClick{this.props.isShow}{formatMessage({id:popover.cancel})}/Button/}{treeDataLoading ? (RowCol span{6}TreeloadData{this.onLoadData}onSelect{this.onSelect}selectedKeys{selectedKeys}onExpand{this.onExpand}expandedKeys{expandedKeys}switcherIcon{Icon typedown /}onLoad{this.onLoad} loadedKeys{loadedKeys} {this.renderTreeNodes(this.state.treeData)}/Tree/ColCol span{18} style{{ position: relative }}div className{styles.goBack}button onClick{this.goBack}{SVG.getSvg(goBack, 12)}{formatMessage({id:componentOverview.body.DirectoryPersistence.return})}/button/divdiv className{styles.iconShow}{showDataArr showDataArr.length 0 ? (showDataArr.map((item, index) {const { title, isLeaf } itemif (isLeaf) {return div className{styles.outerLayer} style{{ cursor: pointer }} onDoubleClick{() this.folderClick(item)}div{SVG.getSvg(file, 70)}/divdivTooltip placementtop title{item.title}{item.title}/Tooltip/div/div} else {return div className{styles.outerLayer} onClick{() this.dowloadTitle(item.title)} style{{ background: dowloadArr.includes(item.title) ? #e6f7ff : #fff }}div{SVG.getSvg(this.getSvgIcon(title), 70)}/divdivTooltip placementtop title{item.title}{item.title}/Tooltip/div/div}})) : (Empty className{styles.emptyStyle} /)}/div/Col/Row) : (Spin sizelarge style{{width: 100%,height: 400,display: flex,alignItems: center,justifyContent: center,}} /)}/Modal/div);} }export default Index;
- 上一篇: 昆明网站建设报价亿网中国网站管理系统
- 下一篇: 昆明网站建设那家好wordpress变英文
相关文章
-
昆明网站建设报价亿网中国网站管理系统
昆明网站建设报价亿网中国网站管理系统
- 技术栈
- 2026年03月21日
-
昆明网站建设案例一个公司可以注册几个网站
昆明网站建设案例一个公司可以注册几个网站
- 技术栈
- 2026年03月21日
-
昆明网站建设ynmdwlwordpress去除版本号
昆明网站建设ynmdwlwordpress去除版本号
- 技术栈
- 2026年03月21日
-
昆明网站建设那家好wordpress变英文
昆明网站建设那家好wordpress变英文
- 技术栈
- 2026年03月21日
-
昆明网站建设培训求个免费网站好人有好报
昆明网站建设培训求个免费网站好人有好报
- 技术栈
- 2026年03月21日
-
昆明网站建设天软科技连锁餐饮网站建设
昆明网站建设天软科技连锁餐饮网站建设
- 技术栈
- 2026年03月21日
