swift 模仿 美丽说App
<h2>一 总体实现功能图</h2> <p>1 图一 : 系统自带属性完成动画翻转</p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/5b6a178680fb12ba11a260a839e6240d.gif"></p> <p>2 图二 : 自定义动画实现翻转</p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/e707882d1af00b988145eb10e32e7532.gif"></p> <p>3 应用图标;启动图片;app名字…这些我就不一 一介绍了,你们都应该知道怎么配置吧.我这里是由于录的比较短,所以这部分内容省略了.这里说下几点注意.</p> <p>—-> 3.1 修改app名字可以在Bundle name中修改,也可以在Bundle display name中修改.</p> <p>—-> 3.2 当我们向Brand Assets中设置启动图片的时候,很有可能往里面扔的图片处占满了几个格子,但是还是有少部分格子并没有图片,这时会有警告出现,我们在开发中要尽可能的减少不必要的警告,当然第三方框架是可以除外的,如果要消除警告,那还得和作者商量才行,比较麻烦.没有用的格子会报警告,我们直接删除就可以了.</p> <h2>二 demo的总体架构</h2> <p>1 通过功能图不难看出,是一个UICollectionViewController和一个NavigationController控制器构成.我这里直接采用UICollectionViewController作为NavigationController的子控制器.</p> <p>2 创建文件,删除自带控制器,拖入一个UICollectionViewController控制器.然后按下面图片展示插入一个NavigationController控制器.将NavigationController设置为箭头指向的控制器.</p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/3942d934caaba64b386e4bf3987bf399.png"></p> <p>3 其余部分都用代码实现</p> <h2>三 封装网络请求工具类</h2> <p>1 创建swift文件</p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/43b229c0c3ffad6ba42c295f81543920.png"></p> <p>2 将网络请求工具类对象设置成单例(外界创建的都是同一个对象)</p> <pre> <code class="language-swift">class NetworkTools: AFHTTPSessionManager { //1.将NetworkTools设置成单例 static let shareIntance : NetworkTools = { let tools = NetworkTools() //加上这句能增加解析的种类 tools.responseSerializer.acceptableContentTypes?.insert("text/html") return tools }() }</code></pre> <p>3 定义枚举 : 定义请求方式的枚举,用来作为参数传递,作为判断选择哪种方式进行请求</p> <pre> <code class="language-swift">// Mark: - 定义枚举,用来作为传值的参数 enum RequestType { case GET case POST }</code></pre> <p>4 封装网络请求 : 由于我们写这部分的代码可能比较多,那么我们考虑通过给类扩展一些方法,在该方法中实现网络请求的封装</p> <pre> <code class="language-swift">// MARK: - 封装网络请求 extension NetworkTools { //注意需要传入的参数:请求的方式;url;需要拼接的参数;成功后的回调(闭包) func request(requestType: RequestType, urlString: String, parameters: [String : AnyObject], finished: (result : AnyObject?, error : NSError?) ->()) { //定义成功后的闭包 let successCallBack = {(task : NSURLSessionDataTask, result : AnyObject?) -> Void in finished(result: result, error: nil) } //定义失败后的闭包 let failureCallBack = {(task : NSURLSessionDataTask?, error : NSError) -> Void in finished(result: nil, error: error) } //根据传入的参数判断选择哪种请求方式 if requestType == .GET { GET(urlString, parameters: parameters, progress: nil, success: successCallBack, failure: failureCallBack) }else{ POST(urlString, parameters: parameters, progress: nil, success: successCallBack, failure: failureCallBack) } } }</code></pre> <p>5 解析 : 成功后的闭包和失败后的闭包,都是通过回调的方式,类似于OC中的block,回调的作用.</p> <p>6 请求首页数据</p> <p>—-> 6.1 由于第4点已经封装了请求方式,那么这部分内容就是对url,参数,发送请求的处理.注意需要做出判断,才能让代码更严谨.</p> <pre> <code class="language-swift">//请求首页数据 extension NetworkTools { //需要有回调后的参数(闭包) func loadHomeData (offSet : Int, finished : (result : [[String : AnyObject]]?, error : NSError?) -> ()) { //1. 获取url let urlString = "http://mobapi.meilishuo.com/2.0/推ter/popular.json" //2. 拼接请求参数 let parameters = [ "offset" : "\(offSet)", "limit" : "30", "access_token" : "b92e0c6fd3ca919d3e7547d446d9a8c2" ] //3. 发送请求 request(.GET, urlString: urlString, parameters: parameters) { (result, error) -> () in // 3.1 判断请求是否出错 if error != nil { finished(result: nil, error: error) } //3.2 获取结果(将可选类型的结果转为具体的结果) guard let result = result as? [String : AnyObject] else { finished(result: nil, error: NSError(domain: "data error", code: -1011, userInfo: nil)) return } //3.3 将结果回调 finished(result: result["data"] as? [[String : AnyObject]], error: nil) } } }</code></pre> <p>7 外面调用方法 : 通过拿到这个方法中的单例,然后调用loadHomeData函数就能达到数据请求的目的了.</p> <h2>四 模型</h2> <p>1 创建模型 :</p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/79367fbfbc1fc8b4c765ea6e7fb2e400.png"></p> <p>2 根据我们自己的需求,将请求出来的数据,进行在线格式化,然后抽取我们自己需要的模型数据属性.我们做的demo只需要小图和大图就可以.</p> <pre> <code class="language-swift">class ShopItem: NSObject { //模型中需要用到的属性 var q_pic_url = "" var z_pic_url = "" //字典转模型 init(dict : [String : AnyObject]) { super.init() //KVC setValuesForKeysWithDictionary(dict) } //重写系统由于模型中属性不是一一对应会报错的函数(重写函数中什么都不做,就不会报错) override func setValue(value: AnyObject?, forUndefinedKey key: String) { } }</code></pre> <p>—-> 2.1 我们直接用KVC的方式直接给模型的属性赋值,不采取MJ的框架了.但是会报错,因为属性并不是一 一对应,所以需要我们将系统报错的方法重写.只要重写就不会报错了.</p> <h2>五 数据源方法</h2> <p>1 由于我们子控制器就是采用UICollectionViewController,所以就不需要设置collectionView了.</p> <p>2 这部分数据源代码可能也会很多,所以这里也采用给类扩展一个方法,在这个方法中写数据源方法</p> <pre> <code class="language-swift">//MARK: - 给类扩充方法 extension HomeViewController { //cell的个数 override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { //根据模型中的数据才能知道有多少个cell return self.shops.count } //cell的内容 override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { //注册 let cell = collectionView.dequeueReusableCellWithReuseIdentifier("HomeCell", forIndexPath: indexPath) as! HomeViewCell //取出item cell.shops = shops[indexPath.item] //判断是否是最后一个出现(如果是最后一个,那么久再次请求数据) if indexPath.item == self.shops.count - 1 { loadData(self.shops.count) } return cell } //点击cell[弹出控制器 override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { showPhotoBrowser(indexPath) } }</code></pre> <p>—-> 2.1 解析 : 里面包括cell的个数(取决于模型);cell的内容(取决于模型);判断最后一个cell出现,就调用数据请求的方法</p> <h2>六 自定义流水布局</h2> <p>1 定义一个继承于UICollectionViewFlowLayout的流水布局</p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/5493f71125dbd7f7e517a33b582d1462.png"></p> <p>2 流水布局的代码</p> <pre> <code class="language-swift">class HomeCollectionViewFlowLayout: UICollectionViewFlowLayout { override func prepareLayout() { super.prepareLayout() //列数 let cols : CGFloat = 3 //间距 let margin : CGFloat = 10 //宽度和高度 let itemWH = (UIScreen.mainScreen().bounds.width - (cols + 1) * margin) / cols //设置布局内容 itemSize = CGSize(width: itemWH, height: itemWH) minimumInteritemSpacing = margin minimumLineSpacing = margin //设置内边距 collectionView?.contentInset = UIEdgeInsets(top: margin + 64, left: margin, bottom: margin, right: margin) } }</code></pre> <p>—-> 2.1 解析 : 注意需要设置上下左右的内边距还有需要进行下面图片的配置才能出现图片</p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/35a7691908f3d640bf860af446c60ee5.png"></p> <p>—-> 2.2 需要将系统的流水布局设置为自定义的才能达到效果</p> <h2>七 自定义cell</h2> <p>1 这个相比大家都知道,对于collectionView是必须自定义cell的,不要问我为什么,这是规定.</p> <p>2 拖入个UIImageView控件到cell中,然后自动布局好约束,通过拖线的方式拿到UIImageView对象</p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/79367fbfbc1fc8b4c765ea6e7fb2e400.png"></p> <p>3 自定义cell中的代码</p> <pre> <code class="language-swift">import UIKit import SDWebImage class HomeViewCell: UICollectionViewCell { @IBOutlet weak var imageView: UIImageView! //didSet方法 var shops : ShopItem? { didSet { //1. 校验 guard let urlString = shops?.z_pic_url else { return } //2.创建url let url = NSURL(string: urlString) //3.设置图片 imageView.sd_setImageWithURL(url, placeholderImage: UIImage(named: "empty_picture")) } } }</code></pre> <h2>八 调用方法,发送网络请求</h2> <p>1 在主控制器中调用方法来发送网络请求</p> <pre> <code class="language-swift">class HomeViewController: UICollectionViewController { //懒加载装模型的数组 private lazy var shops : [ShopItem] = [ShopItem]() private lazy var photoBrowserAnimator = PhotoBrowserAnimator() override func viewDidLoad() { super.viewDidLoad() //调用网络请求函数 loadData(0) } }</code></pre> <p>2 网络数据请求</p> <pre> <code class="language-swift">//MARK: - 发送网络请求 extension HomeViewController { //加载数据 func loadData(offSet : Int) { NetworkTools.shareIntance.loadHomeData(offSet) { (result, error) -> () in //1. 错误校验 if error != nil { //打印错误信息 print(error) return } //2. 获取结果 guard let resultArray = result else { print("获取的结果不正确") return } //3. 遍历所有的结果 for resultDict in resultArray { // print(resultDict) let shop : ShopItem = ShopItem(dict : resultDict) self.shops.append(shop) } //4.刷新表格 self.collectionView?.reloadData() } } }</code></pre> <h2>九 cell的点击</h2> <p>1 实现collectionView数据源中的一个方法</p> <pre> <code class="language-swift">//点击cell弹出控制器 override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { showPhotoBrowser(indexPath) }</code></pre> <p>2 弹出控制器</p> <pre> <code class="language-swift">//Mark: - 点击图片,弹出控制器 extension HomeViewController { //定义为私有的 private func showPhotoBrowser( indexPath : NSIndexPath) { //创建控制器对象 let showPhotoBrowserVC = PhotoBrowserController() //传入点击的cell的角标 showPhotoBrowserVC.indexPath = indexPath //让model出来的控制器和源控制器中模型数据相等 showPhotoBrowserVC.shopItem = shops showPhotoBrowserVC.view.backgroundColor = UIColor.redColor() //设置model出控制器的模式 showPhotoBrowserVC.modalPresentationStyle = .Custom // showPhotoBrowserVC.modalTransitionStyle = .PartialCurl //设置动画代理 showPhotoBrowserVC.transitioningDelegate = photoBrowserAnimator //model出控制器 presentViewController(showPhotoBrowserVC, animated: true, completion: nil) } }</code></pre> <h2>十 展示点击图片的控制器</h2> <p>1 创建文件</p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/4509ac68e39f9aabe120bc770f89d095.png"></p> <p>2 由总体的功能图我们可以看出,view是支持左右滑动的,并不支持上下滑动.所以我们可以通过创建一个普通的UIViewController,在上面加上一个UICollectionView就可以了.</p> <p>3 创建一个类,用来创建按钮的时候,直接使用里面的方法</p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/b42d206a34d32f0a45e51d379d2de6c7.png"></p> <p>4 便利构造函数(我前面有介绍)</p> <pre> <code class="language-swift">//创建类 extension UIButton { //便利构造函数 //函数前面必须加上convenience关键字 convenience init(title : String, bgColor : UIColor, fontSize : CGFloat) { self.init() backgroundColor = bgColor setTitle(title, forState: .Normal) titleLabel?.font = UIFont.systemFontOfSize(fontSize) } }</code></pre> <p>—-> 4.1 解析 : 外面直接UIButton()的时候就会出现这样一个方法,在括号中填入相应的参数就可以创建按钮了.</p> <p>5 UICollectionView中的相关设置</p> <pre> <code class="language-swift">class PhotoBrowserController: UIViewController { //定义属性 let PhoptoBrowserCell = "PhoptoBrowserCell" var shopItem : [ShopItem]? var indexPath : NSIndexPath? //1.懒加载(model出来的控制器;保存按钮;取消按钮) lazy var collectionView : UICollectionView = UICollectionView(frame: CGRectMake(0, 0, UIScreen.mainScreen().bounds.width, UIScreen.mainScreen().bounds.height), collectionViewLayout: PhotoBrowerFlowLayout()) lazy var colseBtn : UIButton = UIButton(title: "关 闭", bgColor:UIColor.darkGrayColor() , fontSize: 16.0) lazy var saveBtn : UIButton = UIButton(title: "保 存", bgColor: UIColor.darkGrayColor(), fontSize: 16.0) override func viewDidLoad() { super.viewDidLoad() //添加UI控件 setUpUI() //滚到对应的位置 collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: .Left, animated: false) }</code></pre> <p>6 添加并且布局子控件的位置</p> <pre> <code class="language-swift">//Mark: - 添加UI控件 extension PhotoBrowserController { func setUpUI() { //1. 添加控件到view中 view.addSubview(collectionView) view.addSubview(colseBtn) view.addSubview(saveBtn) //2. 设置子控件的frame //2.1 设置collectionView collectionView.frame = view.bounds //2.2 设置关闭按钮 let colseBtnX : CGFloat = 20.0 let colseBtnW : CGFloat = 90.0 let colseBtnH : CGFloat = 32.0 let colseBtnY : CGFloat = view.bounds.height - colseBtnH - 20.0 colseBtn.frame = CGRectMake(colseBtnX, colseBtnY, colseBtnW, colseBtnH) //2.3 设置保存按钮 let saveBtnX = view.bounds.width - colseBtnW - 20 saveBtn.frame = CGRectMake(saveBtnX, colseBtnY, colseBtnW, colseBtnH) //监听按钮的点击 colseBtn.addTarget(self, action: "closeBtnClick", forControlEvents: .TouchUpInside) saveBtn.addTarget(self, action: "saveBtnClick", forControlEvents: .TouchUpInside) //注册 collectionView.registerClass(PhotoBrowerViewCell.self, forCellWithReuseIdentifier: PhoptoBrowserCell) //设置数据源 collectionView.dataSource = self //设置代理 collectionView.delegate = self } }</code></pre> <p>7 数据源方法</p> <pre> <code class="language-swift">extension PhotoBrowserController : UICollectionViewDataSource,UICollectionViewDelegate { //MARK: - cell的个数 func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return shopItem?.count ?? 0 } //MARK: - cell的内容 func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier(PhoptoBrowserCell, forIndexPath: indexPath) as! PhotoBrowerViewCell cell.shopItem = shopItem?[indexPath.item] return cell } func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { //调用closeBtnClick方法 closeBtnClick() } }</code></pre> <p>8 监听关闭和保存按钮的点击</p> <pre> <code class="language-swift">extension PhotoBrowserController { @objc private func closeBtnClick () { // print("closeBtnClick") //dismiss控制器 dismissViewControllerAnimated(true, completion: nil) } @objc private func saveBtnClick() { // print("saveBtnClick") //取出在屏幕中显示的cell let cell = collectionView.visibleCells().first as! PhotoBrowerViewCell //取出图片(该方法中的imageView不能直接拿来用,因为设置了为私有的) let image = cell.imageView.image //保存图片到相册 UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil) } }</code></pre> <p>9 自定义流水布局(和前面基本一样)</p> <pre> <code class="language-swift">class PhotoBrowerFlowLayout: UICollectionViewFlowLayout { //设置流水布局相关属性 override func prepareLayout() { super.prepareLayout() itemSize = (collectionView?.bounds.size)! minimumInteritemSpacing = 0 minimumLineSpacing = 0 scrollDirection = .Horizontal //设置分页 collectionView?.pagingEnabled = true collectionView?.showsHorizontalScrollIndicator = false } }</code></pre> <p>10 自定义cell</p> <p>—-> 10.1 需要考虑的因素 : 1 >返回的url是否存在? 2> 能不能直接冲缓存池中获取?</p> <p>—-> 10.2 代码</p> <pre> <code class="language-swift">class PhotoBrowerViewCell: UICollectionViewCell { //懒加载显示图片的控件 lazy var imageView : UIImageView = UIImageView() //提供加载图片的模型的set方法 var shopItem : ShopItem? { didSet { //1.校验小图片的url是否为空 guard let urlString = shopItem?.q_pic_url else { return } //2. 去Cach中获取小图 var smallImage = SDWebImageManager.sharedManager().imageCache.imageFromMemoryCacheForKey(urlString) //3.如果获取不到图片就赋值为占位图片 if smallImage == nil { smallImage = UIImage(named: "empty_picture") } //4. 计算展示的图片的frame imageView.frame = calculate(smallImage) //5. 获取大图片的url是否为空 guard let bigUrlString = shopItem?.z_pic_url else { return } //6 .创建大图的url let bigUrl = NSURL(string: bigUrlString) //7. 加载大图 imageView.sd_setImageWithURL(bigUrl, placeholderImage: smallImage, options: .RetryFailed) {(image, error, type, url) -> Void in self.imageView.frame = self.calculate(image) } } } // MARK: - 构造函数 override init(frame: CGRect) { super.init(frame: frame) setUpUI() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }</code></pre> <p>—-> 10.3 解析 : 下面这对代码是成对出现的</p> <pre> <code class="language-swift">// MARK: - 构造函数 override init(frame: CGRect) { super.init(frame: frame) setUpUI() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }</code></pre> <p>11 设置加载后图片的摆放位置</p> <pre> <code class="language-swift">//MARK: - 设置图片的frame extension PhotoBrowerViewCell { private func calculate (image : UIImage) ->CGRect { let imageViewX : CGFloat = 0.0 let imageViewW : CGFloat = UIScreen.mainScreen().bounds.width let imageViewH : CGFloat = imageViewW / image.size.width * image.size.height let imageViewY : CGFloat = (UIScreen.mainScreen().bounds.height - imageViewH) * 0.5 return CGRect(x: imageViewX, y: imageViewY, width: imageViewW, height: imageViewH) } }</code></pre> <p>12 添加imageView</p> <pre> <code class="language-swift">//MARK: - 添加子控件 extension PhotoBrowerViewCell { private func setUpUI() { contentView.addSubview(imageView) } }</code></pre> <h2>十一 第一种转场动画</h2> <p>1 直接采用苹果推荐的方法用一个属性就可以直接搞定modalTransitionStyle</p> <pre> <code class="language-swift">showPhotoBrowserVC.modalTransitionStyle = .PartialCurl</code></pre> <h2>十二 自定义转场动画</h2> <p>1 创建转场动画类</p> <p><img alt="这里写图片描述" src="https://simg.open-open.com/show/b2996b62473f5d5d0c2e8268490d26ad.png"></p> <p>2 设置该类为动画的代理</p> <pre> <code class="language-swift"> showPhotoBrowserVC.transitioningDelegate = photoBrowserAnimator</code></pre> <p>3 定义一个属性 : 作为判断弹出动画还是消失动画的条件</p> <pre> <code class="language-swift">class PhotoBrowserAnimator: NSObject { var isPresented : Bool = false } </code></pre> <p>4 实现代理方法</p> <pre> <code class="language-swift">//MARK: - 实现转场动画的代理方法 extension PhotoBrowserAnimator : UIViewControllerTransitioningDelegate { //弹出动画交给谁管理 func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { isPresented = true return self } //消失动画交给谁管理 func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { isPresented = false return self } }</code></pre> <p>5 自定义(通过改变透明度实现)</p> <pre> <code class="language-swift">extension PhotoBrowserAnimator : UIViewControllerAnimatedTransitioning { //返回动画执行的时间 func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval { return 2.0 } //获取转场的上下文:可以通过上下文获取到执行动画的view func animateTransition(transitionContext: UIViewControllerContextTransitioning) { isPresented ? animateForPresentView(transitionContext) : animateForDismissed(transitionContext) }</code></pre> <p>—-> 5.1 弹出动画</p> <pre> <code class="language-swift">//弹出 func animateForPresentView(transitionContext: UIViewControllerContextTransitioning) { //1 取出弹出的view let presentView = transitionContext.viewForKey(UITransitionContextToViewKey)! //2 将弹出的view加入到contentView中 transitionContext.containerView()?.addSubview(presentView) //3 执行动画 presentView.alpha = 0.0 UIView.animateWithDuration(transitionDuration(transitionContext), animations: { () -> Void in presentView.alpha = 1.0 }) { (_) -> Void in //完成动画 transitionContext.completeTransition(true) } }</code></pre> <p>—-> 5.2 消失动画</p> <pre> <code class="language-swift">//消失 func animateForDismissed (transitionContext: UIViewControllerContextTransitioning) { //1 取出消失的view let dismissView = transitionContext.viewForKey(UITransitionContextFromViewKey)! //2 执行动画 UIView.animateWithDuration(transitionDuration(transitionContext), animations: { () -> Void in dismissView.alpha = 0.0 }) { (_) -> Void in //移除view dismissView.removeFromSuperview() //完成动画 transitionContext.completeTransition(true) } }</code></pre> <h2>十三 细节</h2> <p>每个cell之间应该存在距离,但是直接通过设置: minimumInteritemSpacing和minimumLineSpacing两个属性是无法达到效果的.那么我这里有一种方法,直接collectionView的宽度增加一点,但是图片的宽度还是屏幕那么大,那么用户是看不到超出屏幕的view的,用下面代码就可以搞定.</p> <pre> <code class="language-swift">//设置cell之间的间隔(这种方法是通过将collectionView的宽度增加来实现的) override func loadView() { super.loadView() view.frame.size.width += 15 }</code></pre> <h2>十四 总结</h2> <p>这部分美丽说demo还不是很完善,还有一个动画翻转的方式还没有实现,那中方式是一种比较难的方式,但是达到的效果很好,后续我还是会补上的.最后,麻烦大家多多关注我的博客,谢谢!!!!</p> <p>来自: <a href="/misc/goto?guid=4959671854089457947" rel="nofollow">http://blog.csdn.net/xf931456371/article/details/51278805</a></p>
本文由用户 ooycg1609 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!