【react.js + hooks】useGuide 创建用户引导视图

有的时候用户可能对网站上的一些操作流程感到困惑,这时候我们需要为用户创建引导视图。为了插入指引而专门去更改组件的渲染函数,显然是不合逻辑的,创建指引视图应该是一种对源代码低侵入的行为,我们可以遵循某一套约定,使之变成一种类插件化的机制。

useGuide 设计思路

为需要引导的网页元素指定唯一id,当引导开始时,创建一个全屏的遮罩,当引导到该元素时,高亮该元素,并创建一个辅助元素挂载到具有唯一 id 的该目标元素上,当指引切换或结束时,移除辅助元素。

要实现这个思路,我们需要以下数据:

  1. 获取每一步的目标元素 id 和辅助元素
  2. 记录当前的步数
  3. 记录每次高亮的元素 id,之后取消高亮

useGuide 准备工作

createRoot 工具函数:

import ReactDOM from "react-dom";let createRoot = (targetDocument: Element) => {return {render: (element: JSX.Element) => {ReactDOM.render(element, targetDocument);},};
};if ("createRoot" in ReactDOM) {// Adapt to React 18createRoot = ReactDOM.createRoot as typeof createRoot;
}

ReactDOM 的 createRoot 方法,在 React 18 + 处于 deprecated

useGuide

代码实现

定义 useGuide 传参和返回
  • 传参:
    • steps - 数组,每个元素代表指引的每一步对应的所有目标元素id,这一步的名字,data为这一步搭载的可能需要的数据,renders为每一个目标元素各自的辅助元素的渲染函数
    • callback - 指引步变化时的回调函数
    • config - 配置项
      • containerStyle - 为辅助元素套的一层 div,指定该 div 的样式
      • containerClassName - 指定该辅助元素的套壳 div 的 css 类名
      • maskConfig - 指定当前步时,遮罩层的一些属性
  • 返回(数组):
    • [0] number - 当前步
    • [1] Guider - 对一些引导行为封装的对象
function useGuide(steps: Step[],callback?: StepCallback,config?: {containerStyle?: Partial<CSSStyleDeclaration>;containerClassName?: string;maskConfig?: MaskConfig;}
): [number, Guider]export type Render = {id: string;render: (id: string,name: string,data: any,ids: string[]) => React.ReactNode;containerStyle?: Partial<CSSStyleDeclaration>;containerClassName?: string;
};export type Step = {ids?: string[];name?: string;data?: any;renders?: Render[];
};export interface Guider {start: () => void;stop: () => void;next: () => void;last: () => void;go: (step: number) => void;
}export type MaskConfig = {backgroundColor?: string;opacity?: number;zIndex?: number;pointerEvents?:| "none !important"| "auto"| React.CSSProperties["pointerEvents"];
};export type StepCallback = (step: number, stepConfig: Step) => void;
创建遮罩层

先定义一个 ref 来保存遮罩层元素的 dom

  const maskRef = useRef<HTMLDivElement | null>(null);

定义一个创建遮罩mask的函数,传入遮罩层配置

const createMask = (config?: MaskConfig) => {const mask = document.createElement("div");mask.style.position = "fixed";mask.style.top = "0";mask.style.right = "0";mask.style.bottom = "0";mask.style.left = "0";mask.style.backgroundColor = "rgba(0, 0, 0, 0.5)";mask.style.zIndex = "999";mask.style.cursor = "default";mask.style.userSelect = "none";mask.style.webkitUserSelect = "none";mask.style.pointerEvents = "none !important";const maskConfig = config;if (maskConfig) {if (maskConfig.backgroundColor) {mask.style.backgroundColor = maskConfig.backgroundColor;}if (maskConfig.opacity) {mask.style.opacity = maskConfig.opacity.toString();}if (maskConfig.zIndex) {mask.style.zIndex = maskConfig.zIndex.toString();}}return mask;
};

其中,默认设置遮罩层的鼠标时间为绝对 none,图层高度为 999。

渲染辅助元素

记录当前步:

const [step, setStep] = useState(-1);

检测当前步,并挑选出对应的辅助元素渲染器,并渲染:

useEffect(() => {const currentStep = steps[step];const rootDom = document.body;const mask = createMask(config?.maskConfig);if (currentStep && rootDom) {rootDom.appendChild(mask);maskRef.current = mask;}currentStep?.ids?.forEach((id) => {const element = document.getElementById(id);if (element) {element.style.zIndex = "1000";}});const renders = currentStep?.renders?.map(({ id, render, containerStyle, containerClassName }) => {const target = document.getElementById(id);const container = document.createElement("div");container.style.zIndex = "1001";container.style.position = "relative";if (config?.containerStyle) {Object.keys(config.containerStyle).forEach((key) => {// @ts-ignorecontainer.style[key] = config.containerStyle[key];});}if (containerStyle) {Object.keys(containerStyle).forEach((key) => {// @ts-ignorecontainer.style[key] = containerStyle[key];});}if (config?.containerClassName) {container.className = config.containerClassName;}if (containerClassName) {container.className = containerClassName;}// 默认挂载到目标元素上target?.appendChild(container);if (container && target) {// @ts-ignorecreateRoot(container).render(// @ts-ignorerender(id, currentStep.name, currentStep.data, currentStep.ids));return container;}});callback?.(step, currentStep);return () => {if (currentStep && rootDom && maskRef.current) {rootDom.removeChild(mask);maskRef.current = null;}renders?.forEach((elem) => elem?.remove());};}, [step, steps]);

其中,每次渲染,我们需要获取到目标元素,并创建一个包装容器 div,借助 createRoot 渲染并挂载辅助元素到目标元素上。(为什么要包装?辅助render可能返回一个被 Pure 元素包裹的组件)

函数返回
return [step,{start,stop,next,last,go,},];
封装引导行为

封装常用的开始(约定step数组是顺序的,索引 -1 时不处于引导中),结束,上一步,下一步和跳转某步的操作。

const start = useCallback(() => setStep(0), []);const stop = useCallback(() => setStep(-1), []);const next = useCallback(() => setStep((prev) => Math.min(prev + 1, steps.length - 1)),[steps]);const last = useCallback(() => setStep((prev) => Math.max(prev - 1, 0)), []);const go = useCallback((step: number) => setStep(Math.max(0, Math.min(step, steps.length - 1))),[steps]);
高亮目标元素

我们需要存储元素被高亮前的 zIndex,后续恢复它们

const zIndexes = useRef<Map<string, string>>(new Map());

在渲染前拉高 zIndex,并记录原zIndex,渲染结束或组件卸载后恢复

// 监测当前步的那个副作用
useEffect(()=>{//...currentStep?.ids?.forEach((id) => {const element = document.getElementById(id);if (element) {zIndexes.current.set(id, element.style.zIndex); // 记录原值element.style.zIndex = "1000"; //拉高图层以实现高亮}});return () => {//...renders?.forEach((elem) => elem?.remove());// 当不再需要引导元素时,恢复原始的 zIndexzIndexes.current.forEach((zIndex, id) => {const element = document.getElementById(id);if (element) {element.style.zIndex = zIndex;}});zIndexes.current.clear();}
}, [step, steps])
对目标元素的包装模式

上面采取了用 createRoot 和原生 js 插入辅助元素的方案,接下来我们引入另一种方案,创建一个高阶的Pure组件(Target组件)包裹目标组件,将辅助元素通过 createPortal 挂载在下面:

拓展 Guider:
暴露 step, 传参的config,register 和 unregister,后两个 api 是让 Target 向 guider 注册自己,告诉 guider 某个 id 归它管了,你不需要渲染辅助元素了。

export interface Guider {start: () => void;stop: () => void;next: () => void;last: () => void;go: (step: number) => void;// Not for user to usestep: number;options?: {steps?: Step[];callback?: StepCallback;config?: {containerStyle?: Partial<CSSStyleDeclaration>;containerClassName?: string;maskConfig?: MaskConfig;};};register: (id: string) => void;unregister: (id: string) => void;
}

Target,用于包裹目标组件

interface TargetProps {id: string;guider: Guider;children: React.ReactNode;
}export const Target: React.FC<TargetProps> = ({ id, guider, children }) => {const [guide, setGuide] = useState<React.ReactNode>(null);const { step, options } = guider;const { steps } = options || {};const currentStep = steps?.[step];useEffect(() => {guider.register(id);const render = currentStep?.renders?.find((r) => r.id === id)?.render;if (render) {// @ts-ignoresetGuide(render(id, currentStep.name, currentStep.data, currentStep.ids));} else {setGuide(null);}return () => {guider.unregister(id);};}, [id, currentStep]);const element = document.getElementById(id);return (<>{children}{element && ReactDOM.createPortal(guide, element)}</>);
};

useGuide 内部需要存储被 Target 注册的 id:

const registered = useRef<Set<string>>(new Set());
const register = useCallback((id: string) => {registered.current.add(id);
}, []);const unregister = useCallback((id: string) => {registered.current.delete(id);
}, []);return [step,{start,stop,next,last,go,step,options: { steps, callback, config },register,unregister,},];

在 useGuide 渲染时跳过注册的 id :

const renders = currentStep?.renders?.map(({ id, render, containerStyle, containerClassName }) => {if (registered.current.has(id)) {// 如果已经注册,跳过渲染步骤return;}//...
useGuide 完整代码
import React, { useState, useEffect, useCallback, useRef } from "react";
import ReactDOM from "react-dom";let createRoot = (targetDocument: Element) => {return {render: (element: JSX.Element) => {ReactDOM.render(element, targetDocument);},};
};if ("createRoot" in ReactDOM) {// Adapt to React 18createRoot = ReactDOM.createRoot as typeof createRoot;
}export type Render = {id: string;render: (id: string,name: string,data: any,ids: string[]) => React.ReactNode;containerStyle?: Partial<CSSStyleDeclaration>;containerClassName?: string;
};export type Step = {ids?: string[];name?: string;data?: any;renders?: Render[];
};export interface Guider {start: () => void;stop: () => void;next: () => void;last: () => void;go: (step: number) => void;// Not for user to usestep: number;options?: {steps?: Step[];callback?: StepCallback;config?: {containerStyle?: Partial<CSSStyleDeclaration>;containerClassName?: string;maskConfig?: MaskConfig;};};register: (id: string) => void;unregister: (id: string) => void;
}export type MaskConfig = {backgroundColor?: string;opacity?: number;zIndex?: number;pointerEvents?:| "none !important"| "auto"| React.CSSProperties["pointerEvents"];
};export type StepCallback = (step: number, stepConfig: Step) => void;const createMask = (config?: MaskConfig) => {const mask = document.createElement("div");mask.style.position = "fixed";mask.style.top = "0";mask.style.right = "0";mask.style.bottom = "0";mask.style.left = "0";mask.style.backgroundColor = "rgba(0, 0, 0, 0.5)";mask.style.zIndex = "999";mask.style.cursor = "default";mask.style.userSelect = "none";mask.style.webkitUserSelect = "none";mask.style.pointerEvents = "none !important";const maskConfig = config;if (maskConfig) {if (maskConfig.backgroundColor) {mask.style.backgroundColor = maskConfig.backgroundColor;}if (maskConfig.opacity) {mask.style.opacity = maskConfig.opacity.toString();}if (maskConfig.zIndex) {mask.style.zIndex = maskConfig.zIndex.toString();}}return mask;
};function useGuide(steps: Step[],callback?: StepCallback,config?: {containerStyle?: Partial<CSSStyleDeclaration>;containerClassName?: string;maskConfig?: MaskConfig;}
): [number, Guider] {const [step, setStep] = useState(-1);const maskRef = useRef<HTMLDivElement | null>(null);const zIndexes = useRef<Map<string, string>>(new Map());const registered = useRef<Set<string>>(new Set());const register = useCallback((id: string) => {registered.current.add(id);}, []);const unregister = useCallback((id: string) => {registered.current.delete(id);}, []);useEffect(() => {const currentStep = steps[step];const rootDom = document.body;const mask = createMask(config?.maskConfig);if (currentStep && rootDom) {rootDom.appendChild(mask);maskRef.current = mask;}currentStep?.ids?.forEach((id) => {const element = document.getElementById(id);if (element) {zIndexes.current.set(id, element.style.zIndex);element.style.zIndex = "1000";}});const renders = currentStep?.renders?.map(({ id, render, containerStyle, containerClassName }) => {if (registered.current.has(id)) {// 如果已经注册,跳过渲染步骤return;}const target = document.getElementById(id);const container = document.createElement("div");container.style.zIndex = "1001";container.style.position = "relative";if (config?.containerStyle) {Object.keys(config.containerStyle).forEach((key) => {// @ts-ignorecontainer.style[key] = config.containerStyle[key];});}if (containerStyle) {Object.keys(containerStyle).forEach((key) => {// @ts-ignorecontainer.style[key] = containerStyle[key];});}if (config?.containerClassName) {container.className = config.containerClassName;}if (containerClassName) {container.className = containerClassName;}// 默认位于父元素的最后target?.appendChild(container);if (container && target) {// @ts-ignorecreateRoot(container).render(// @ts-ignorerender(id, currentStep.name, currentStep.data, currentStep.ids));return container;}});callback?.(step, currentStep);return () => {if (currentStep && rootDom && maskRef.current) {rootDom.removeChild(mask);maskRef.current = null;}renders?.forEach((elem) => elem?.remove());// 当不再需要引导元素时,恢复原始的 zIndexzIndexes.current.forEach((zIndex, id) => {const element = document.getElementById(id);if (element) {element.style.zIndex = zIndex;}});zIndexes.current.clear();};}, [step, steps]);const start = useCallback(() => setStep(0), []);const stop = useCallback(() => setStep(-1), []);const next = useCallback(() => setStep((prev) => Math.min(prev + 1, steps.length - 1)),[steps]);const last = useCallback(() => setStep((prev) => Math.max(prev - 1, 0)), []);const go = useCallback((step: number) => setStep(Math.max(0, Math.min(step, steps.length - 1))),[steps]);return [step,{start,stop,next,last,go,step,options: { steps, callback, config },register,unregister,},];
}export default useGuide;interface TargetProps {id: string;guider: Guider;children: React.ReactNode;
}export const Target: React.FC<TargetProps> = ({ id, guider, children }) => {const [guide, setGuide] = useState<React.ReactNode>(null);const { step, options } = guider;const { steps } = options || {};const currentStep = steps?.[step];useEffect(() => {guider.register(id);const render = currentStep?.renders?.find((r) => r.id === id)?.render;if (render) {// @ts-ignoresetGuide(render(id, currentStep.name, currentStep.data, currentStep.ids));} else {setGuide(null);}return () => {guider.unregister(id);};}, [id, currentStep]);const element = document.getElementById(id);return (<>{children}{element && ReactDOM.createPortal(guide, element)}</>);
};
useGuide 使用示例

以下代码创建了一个九宫格引导视图($css 是全局注册的 @emotion)

import useGuide from "@hooks/useGuide";const View = () => {const [currentStep, guider] = useGuide(Array.from({ length: 9 }, (_, i) => i + 1).map((i) => ({ids: [`s${i}`],name: `Step ${i}`,data: {},renders: [{id: `s${i}`,render(id, name, data, ids) {console.log(id, name, data, ids);const onClick = i === 9 ? guider.stop : guider.next;return (<divcss={$css`display: flex;align-items: center;width: fit-content; position: absolute;background: #fff;padding: 4px 20px;border-radius: 6px;transform: translate(-50%, 50%);`}><div css={$css`width: 60px;`}>{name}</div><divcss={$css`padding: 4px 12px; &:hover { cursor: pointer; background: #eee;  border-radius: 4px;}`}onClick={onClick}>{i === 9 ? "End" : "Next"}</div></div>);},},],})));return (<div css={style.containerCss}><div id="s1" css={style.boxCss("red")} onClick={guider.start}>Start</div><div id="s2" css={style.boxCss("green")}>2</div><div id="s3" css={style.boxCss("blue")}>3</div><div id="s4" css={style.boxCss("black")}>4</div><div id="s5" css={style.boxCss("purple")}>5</div><div id="s6" css={style.boxCss("pink")}>6</div><div id="s7" css={style.boxCss("cyan")}>7</div><div id="s8" css={style.boxCss("magenta")}>8</div><div id="s9" css={style.boxCss("orange")}>9</div></div>);
};module style {export const containerCss = $css`
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
gap: 10px;
width: 300px;
height: 300px;
`;export const boxCss = (color: string) => $css`
color: ${color};
display: flex;
justify-content: center;
align-items: center;
border-radius: 6px;
cursor: pointer;
`;
}

效果演示:useGuide九宫格引导视图
Bingo ! 一个实用的 useGuide 就这样实现了!需要注意的是,使用 Target 包裹目标元素 和 不使用 将导致最终渲染结果的一定差异,因为 Target 不再创建 div 包裹辅助元素,因此不建议混用 Target 和 无 Target。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/355946.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用递归算法结合数据库解析成java树形结构

使用递归算法结合数据库解析成java树形结构 1、准备表结构及对应的表数据a、表结构&#xff1a; create table TB_TREE ( CID NUMBER not null, CNAME VARCHAR2(50), PID NUMBER //父节点 ) b、表数据&#xff1a; insert into tb_tree (CID, CNAME, PID) values (1, 中国, 0);…

ug11 linux,UG11.0升级包MP02Win#Linux系统下载就上UG网

UG11.0软件又出升级包啦&#xff01;抽空可以为NX升级啦&#xff0c;从11.0发布到现在&#xff0c;近三个月了&#xff0c;每一次版本的更新&#xff0c;都会带来较多功能的改善&#xff0c;以及对错误BUG的有效处理&#xff0c;下图为升级后的UG11.0软件&#xff1b;UG11.0升级…

akka 异常处理_使用Akka处理1000万条消息

akka 异常处理Akka演员承诺并发。 有什么更好的模拟方法&#xff0c;看看使用商品硬件和软件处理1000万条消息需要花费多少时间&#xff0c;而无需进行任何低级调整。我用Java编写了整个1000万条消息的处理过程&#xff0c;整个结果令我惊讶。 当我在具有i5 – 4核心&#xff0…

20155330 2016-2017-2 《Java程序设计》第五周学习总结

20155330 2016-2017-2 《Java程序设计》第五周学习总结 教材学习内容总结 学习目标 理解异常架构掌握try...catch...finally处理异常的方法会用throw,throws理解Collection和Map架构会用常见的数据结构和算法了解Lambada和泛型第八章 章节主要内容 小结 Throwwable定义了取错误…

yum安装odbc驱动linux,在CentOS上离线配置PostgreSQL ODBC数据源

一、问题提出内网的一台CentOS服务器&#xff0c;需配置PostgreSQL ODBC。如果可以连接Internet&#xff0c;此工作很容易&#xff0c;使用yum install自动安装相应依赖包后简单配置即可。但当置于内网环境时&#xff0c;事情就有些麻烦&#xff0c;需要事先手工下载各个依赖包…

pcie组raid linux,PCIe 4.0有多强大?组RAID 0阵列之后惊呆了

技嘉在6月16日21:00正式上市了全系列B550系列主板&#xff0c;B550系列主板属于AMD中端系列芯片组&#xff0c;能原生支持PCIe 4.0技术&#xff0c;而这次技嘉B550 AORUS MASTER主板更是不得了&#xff0c;提供了3个支持PCIe 4.0技术的M.2接口&#xff0c;你还可以组RAID 0阵列…

weblogic最大线程_处理Weblogic卡住的线程

weblogic最大线程定义或什么是缠线&#xff1f; 如果线程 在设定 的时间 内连续工作&#xff08;非空闲&#xff09;&#xff0c;则WebLogic Server会将其诊断为阻塞 。 您可以通过更改在诊断出线程被阻塞之前的时间长度&#xff08; Stuck Thread Max Time &#xff09;&#…

步进电机的正反向旋转c语言编程,实现步进电机自动正反转程序怎么写

本文收集整理关于实现步进电机自动正反转程序怎么写的相关议题&#xff0c;使用内容导航快速到达。内容导航&#xff1a;Q1&#xff1a;用c语言程序实现步进电机的正反转加减速的编程内容&#xff1a;1、本程序用于测试4相步进电机常规驱动2、需要用跳帽或者杜邦线把信号输出端…

stax 和jaxb 关系_XML解组基准:JAXB,STAx,Woodstox

stax 和jaxb 关系介绍 上周末&#xff0c;我开始考虑如何以一种资源友好的方式处理大量XML数据。我要解决的主要问题是如何以块的形式处理大型XML文件&#xff0c;同时提供上游/下游系统&#xff0c;需要处理一些数据。 当然&#xff0c;我已经使用JAXB技术已有几年了。 使用J…

48道C语言上机题参考答案,二级C语言上机题库参考答案(已修改).doc

二级C语言上机题库参考答案(已修改).doc下载提示(请认真阅读)1.请仔细阅读文档&#xff0c;确保文档完整性&#xff0c;对于不预览、不比对内容而直接下载带来的问题本站不予受理。2.下载的文档&#xff0c;不会出现我们的网址水印。3、该文档所得收入(下载内容预览)归上传者、…

(原创)SpringBoot入门

本文章是SpringBoot入门的介绍在这里 我会尽量写一些细节性的东西,我用的是IDEA2016 Tomcat7 JDK1.8 Maven3.3.9 IDEA Tomcat JDK Maven的安装我就不详细了, 这里我会提到Maven的安装和如何使用阿里云的镜像,因为官网的JAR下载真的是...比乌龟还慢 先从Maven的配置开始,到官…

电脑基础c语言,C语言经验: 如何从零基础学习C语言?

C语言是面向过程的&#xff0c;而C 是面向对象的C和C 之间的区别:C是一种结构化语言&#xff0c;其重点是算法和数据结构. 在C程序设计中&#xff0c;首先要考虑的是如何通过过程来处理输入(或环境条件)以获得输出(或实现过程(事务)控制).C &#xff0c;首先要考虑的是如何构建…

java cpu_Java High CPU故障排除指南–第1部分

java cpu本文是该系列的第1部分&#xff0c;它将为您提供有关如何进行故障排除和识别Java高CPU问题根本原因的综合指南。 本指南也适用于独立的Java程序&#xff0c;但旨在帮助涉及Java EE企业日常生产支持的个人。 它还将包括最常见的高级CPU问题列表以及高级解决方案。 生产…

android:ellipsize=end 不起作用,android:ellipsize=end 失效或者 相关的Bug

其实这文章有点傻逼。相关的问题TextView android:ellipsize“end”超出一个字符时不显示…的解决http://www.pocketdigi.com/20140122/1261.html上面我到没有遇到过。但是我遇到的更神奇。就是在使用android:ellipsize“end”正常的情况下这个属性。该textView的文本就不能加入…

适用于具有Couchbase和WildFly的多容器和多主机应用程序的Docker Machine,Swarm和Compose...

该博客将说明如何使用Docker创建部署在多个主机上的多容器应用程序。 这将使用Docker Machine&#xff0c;Swarm和Compose实现。 是的&#xff0c;所有这三个工具一起使此博客更加有趣&#xff01; 该图说明了关键组件&#xff1a; Docker Machine用于配置多个Docker主机 …

一键复制android代码,兼容安卓和ios实现一键复制内容到剪切板

js兼容安卓和ios实现粘贴板一键复制color: #000;background: #fff;overflow-y: scroll;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;}html*{outline:0;-webkit-text-size-adjust: none;-webkit-tap-highlight-color: transparent}*{margin:0;padding:0}.conten…

adf时间作用域_ADF:在任务流终结器中支持bean作用域

adf时间作用域介绍 当我们需要在任务流消失之前做一些最终工作&#xff08;干净的资源&#xff0c;紧密的连接等&#xff09;时&#xff0c;这是使用任务流终结器的非常普遍的建议做法。 和往常一样&#xff0c;我们使用在任务流中声明的托管bean。 托管Bean可以具有不同的范围…

Drools:fireAllRules,fireUntilHalt和Timers内部代码清理的详细说明

在六月&#xff0c;我们在博客上发布了一个新的内部状态机&#xff0c;用于管理用户&#xff0c;计时器和引擎线程之间的交互。 现在&#xff0c;我们对该代码进行了另一次大的内部清理&#xff0c;以使其更易于阅读和理解。 如前所述&#xff0c;所有操作&#xff08;插入&am…

nodejs+vue+ElementUi房屋房产销售预约看房系统bqv00

完成房产销售系统&#xff0c;对房源的信息、用户信息及各种资料进行收集和科学的管理&#xff0c;该系统的功能基本可以满足当前市面上的小型房产企业对于房产销售的基本要求&#xff0c;收集各个地区的房源信息并进行分类管理&#xff0c;用户通过注册账号登录网站查询房源信…

ios framework 找不到.h_找不到好看的壁纸?上万张「高清壁纸」,都在iOS捷径里...

所需工具&#xff1a;iOS捷径获取方法&#xff1a;后台私信回复「363」不和大家废话&#xff0c;今天给大家分享一个超好用的ios壁纸捷径&#xff0c;用了它之后再也不怕找不到喜欢的壁纸了~将克拉壁纸的捷径链接在Safari浏览器打开&#xff0c;就会自动跳转到一个获取捷径的窗…