UNIAPP----IOS端原生插件开发实战(一)

1.前言

UNIAPP----Android端原生插件开发实战二进行了Android端的原生插件的开发,由于本业务同样需要IOS端的代理传输,所以下面介绍一下IOS端的原生插件开发过程。

2.工具材料清单

工具/材料 版本/版本名
HBuilder X 3.1.18
Xcode Version 12.1 (12A7403)
UNI-SDK iOSSDK@3.1.18.80433_20210609

3.原生环境配置

3.1创建插件工程

根据iOS平台uni原生插件开发教程,我们打开 Xcode,创建一个新的工程(Project),template 选择 Framework ,然后点击 Next。

根据文档描述,工程存放路径,建议直接存放在 iOSSDK目录中的 HBuilder-uniPluginDemo 插件开发主工程目录下,如下图所示,然后点击 Create

Project创建完成之后,删除掉自动创建的头文件,删除后的样子如下

然后选中工程名,在TARGETS->Build Settings中,将 Mach-O Type 设置为 Static Library如下图所示

然后将插件工程关闭,接下来需要将插件工程导入到插件开发主工程中。

3.2导入插件工程

打开 iOSSDK/HBuilder-uniPluginDemo工程目录,双击目录中的HBuilder-uniPlugin.xcodeproj文件运行插件开发主工程

在 Xcode 项目左侧目录选中主工程名,然后点击右键选择Add Files to “HBuilder-uniPlugin”

然后选择您刚刚创建的插件工程路径中,选中插件工程文件,勾选Create folder referencesAdd to targets两项,然后点击Add

这时在 Xcode 左侧目录中可以看到插件工程已经添加到了主工程中,如下图所示

3.3 工程配置

在 Xcode 项目左侧目录选中主工程名,在TARGETS->Build Phases->Dependencies中点击+

在弹窗中选中插件工程,如图所示,然后点击Add,将插件工程添加到Dependencies

然后在Link Binary With Libraries中点击+,同样在弹窗中选中插件工程,点击Add

此时可以看到 DependenciesLink Binary With Libraries都添加了插件工程,如下图所示

接下来需要在插件工程的Header Search Paths中添加开发插件所需的头文件引用,头文件存放在主工程的HBuilder-Hello/inc中,添加方法如下图所示,在 Xcode 项目左侧目录选中插件工程名,找到TARGETS->Build Settings->Header Search Paths双击右侧区域打开添加窗口,然后将inc目录拖入会自动填充相对路径,然后将模式改成recursive

4.SDK配置

SDK的库文件SecurePortal.framework是必须的,其包括了用户NBA的登录认证以及代理业务功能。
SecurePortal.framework包含两个头文件一个是SPNBAClient.hLibSecIDLite.h
SPNBAClient.h包含NBA的登录认证,代理和相关的接口。
LibSecIDLite.h是获取360ID的动态口令以及二维码授权等接口。

4.1.导入SDK文件

SDK文档中描述到:

App工程添加SecurePortal.framework的工程配置如下:
需要将动态库添加到Embeded BinariesLinked Frameworks and Libraries这两个选项里面。

再结合官网的描述

如果您的插件需要依赖第三方的SDK,开发阶段引入三方SDK的时候要引入到主工程,然后将三方SDK提供的 .h 头文件直接添加到插件工程中这样就可以正常调用三方SDK的Api了,功能开发完毕后在构建插件包的时候,需要将依赖的三方SDK库文件放到ios路径下,然后按照规范编辑 package.json;

因此,先进行第一步,将SecurePortal.framework添加到iosTunnel.xcodeproj
为方便引用,先在目录下新建一个Frameworks目录

然后将SecurePortal.framework拷贝到这个目录下面,如下图所示

拷贝成功后可以同时看到Build Phases里面也有了这个库文件的link

framework引入完毕后,接下来在主工程对SDK进行引用
选中工程,如图所示,然后在Link Binary With Libraries点击+

会发现没有看到这个SecurePortal.framework,这时候选择下面的Add Files

选择我们放到iosTunnel目录下面的SecurePortal.framework

image.png

然后在Embed Frameworks处同样添加SecurePortal.framework

4.2业务代码实现

新建头文件和.m文件,如下图所示(直接选择新建cocopods class可以同时生成.h和.m文件)

存放到iosTunnel目录下

开始业务代码的实现,头文件中先把官方文档的部分抄过来,在引入SDK的头文件

//tunnel.h
#import <Foundation/Foundation.h>
// 引入 DCUniModule.h 头文件
#import "DCUniModule.h"
#import <SecurePortal/SPNBAClient.h>
@interface tunnel : DCUniModule
@property (nonatomic, strong) UniModuleKeepAliveCallback callback;
@end

这几行代码引入过后,等待IDE编译小半分钟(此时Xcode不会提示你它正在编译),如果对着SPNBAClient等类右键能够跳转到定义的话就说明头文件引入成功了。

下面进行方法的实现

//tunnel.m
#import "tunnel.h"

@interface tunnel()<SPNBAClientDelegate,NSURLSessionDelegate>

typedef void (^CompletioBlock)(NSDictionary *dic, NSURLResponse *response, NSError *error);
typedef void (^SuccessBlock)(NSDictionary *data);
typedef void (^FailureBlock)(NSError *error);

@end
@implementation tunnel

UNI_EXPORT_METHOD(@selector(connectNBA:callback:))
// 通过宏 UNI_EXPORT_METHOD 将异步方法暴露给 js 端
//UNI_EXPORT_METHOD(@selector(testAsyncFunc:callback:))
/// 异步方法(注:异步方法会在主线程(UI线程)执行)
/// @param options js 端调用方法时传递的参数
/// @param callback 回调方法,回传参数给 js 端
- (void)connectNBA:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
    //    NSLog(@"传递过来的参数是%@",options);
    self.callback = callback;
    NSString* NBA_host = @"xxx.xxx.xxx.xx";
    NSString* NBA_port = @"xxx";
    NSString* auth_username = options[@"NBAUsername"];
    NSString* auth_password = options[@"NBAPassword"];
    NSDictionary *loginDic = @{
        @"NBA_host"      : NBA_host,
        @"NBA_port"      : NBA_port,
        //        @"auth_server"   : @"认证服务器名,默认选取第一个",
        @"auth_username" : auth_username,
        @"auth_password" : auth_password,
        @"auth_mode"     : @0, //0用户名密码登录,1,证书登录, 2 动态口令, 3 二维码
        @"auth_autologin": @1, //1自动登录,0手动登录,需要实现登录界面
        //        @"extra_xxxxxx" :  extra_ 开头的额外的参数
    };
    NSLog(@"loginDic%@",loginDic);
}

- (void)didLoginSuccess {
    NSLog(@"登录成功");
    self.callback(@"隧道登录成功", NO);
}
- (void)onLoginErrorID:(NSInteger)errid msg:(NSString*)errmsg {
    if (self.callback)
    {
        NSLog(@"失败信息---%@",errmsg);
        self.callback(errmsg, NO);
    }
}

// 通过宏 UNI_EXPORT_METHOD_SYNC 将同步方法暴露给 js 端
UNI_EXPORT_METHOD_SYNC(@selector(get:callback:))
- (void)get:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
    NSString *urlString = options[@"url"];
    NSURL *url = [NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"GET"];
    NSDictionary *proxyConfigDic = nil;
    NSInteger proxyPort = [SPNBAClient queryProxyPort];
    //NSLog(@"端口=%ld",proxyPort);
    if(proxyPort)
    {
        NSString *host = @"xxx.x.x.x";
        proxyConfigDic = @{(NSString*)kCFStreamPropertyHTTPProxyHost: host,
                           (NSString*)kCFStreamPropertyHTTPProxyPort: @(proxyPort),
                           (NSString*)kCFNetworkProxiesHTTPEnable:@YES,
                           (NSString*)kCFStreamPropertyHTTPSProxyHost: host,
                           (NSString*)kCFStreamPropertyHTTPSProxyPort:@(proxyPort)
        };
    }
    NSURLSessionDataTask* sessionTask = [self   createSessionWithRequest:request                                          withProxyConfig:proxyConfigDic
                   options:options
                    callback:callback];
    [sessionTask resume];
}

// 通过宏 UNI_EXPORT_METHOD_SYNC 将同步方法暴露给 js 端
UNI_EXPORT_METHOD_SYNC(@selector(post:callback:))
- (void)post:(NSDictionary *)options callback:(UniModuleKeepAliveCallback)callback {
    NSString *urlString = options[@"url"];
    NSURL *url = [NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = @"POST";
    NSDictionary *proxyConfigDic = nil;
    NSInteger proxyPort = [SPNBAClient queryProxyPort];
    NSLog(@"url=%@",url);
    NSLog(@"端口=%ld",proxyPort);
    if(proxyPort)
    {
        NSString *host = @"xxx.x.x.x";
        proxyConfigDic = @{(NSString*)kCFStreamPropertyHTTPProxyHost: host,
                           (NSString*)kCFStreamPropertyHTTPProxyPort: @(proxyPort),
                           (NSString*)kCFNetworkProxiesHTTPEnable:@YES,
                           (NSString*)kCFStreamPropertyHTTPSProxyHost: host,
                           (NSString*)kCFStreamPropertyHTTPSProxyPort:@(proxyPort)
        };
    }
    NSURLSessionDataTask* sessionTask = [self createSessionWithRequest:request                                                    withProxyConfig:proxyConfigDic
                                         options:options
                                         callback:callback];
    [sessionTask resume];
}

- (NSURLSessionDataTask*)createSessionWithRequest:(NSURLRequest*)aRequest
                                  withProxyConfig:(NSDictionary*)proxyConfigDic
                                          options: (NSDictionary *)options
                                         callback:(UniModuleKeepAliveCallback)callback
{
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    config.connectionProxyDictionary = proxyConfigDic;
    if([options objectForKey:@"token"])
    {
        NSLog(@"token=%@",options[@"token"]);
        config.HTTPAdditionalHeaders = @{@"token":options[@"token"]};
    }
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config
                                                          delegate:self
                                                     delegateQueue:nil];
    NSURLSessionDataTask *sessionTask = [session dataTaskWithRequest:aRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if(error)
        {
            NSLog(@"请求错误:%@",[error localizedDescription]);
            callback([error localizedDescription], NO);
        }
        else
        {
            NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            NSLog(@"请求成功:%@",dataStr);
            callback(dataStr, NO);
        }
    }];
    return sessionTask;
}
@end

5.原生应用测试

5.1 编写UNI端业务代码

<template>
    <view>
        <!--状态栏 -->
        <view class="status_bar"></view>
        <view class="login">
            <view class="content">
                <!-- 头部logo -->
                <view class="header">
                    <image src="@/static/images/logo.png"></image>
                </view>
                <text class="title"></text>
                <!-- 主体表单 -->
                <view class="main">
                    <wInput v-model="account" type="text" placeholder="账号" :focus="isFocus" :disabled="disabled">
                    </wInput>
                    <wInput v-model="password" type="password" placeholder="密码" :disabled="disabled"></wInput>
                </view>
                <wButton class="wbutton" text="登 录" :rotate="isRotate" @click="startLogin"></wButton>
            </view>
            <yomol-upgrade :type="upgradeType" :url="upgradeUrl" title="发现新版本" :content="upgradeContent"
                ref="yomolUpgrade"></yomol-upgrade>
            <!-- 隧道modal -->
            <tui-modal :show="modal" :custom="true" fadeIn >
                <view class="tui-modal-custom">
                    <view class="tui-prompt-title">NBA账号</view>
                    <input class="tui-modal-input"   v-model="NBAUsername" />
                    <view class="tui-prompt-title">NBA密码</view>
                    <input class="tui-modal-input password"   v-model="NBAPassword" />
                    <tui-button height="72rpx" :size="28" shape="circle" @click="requestNBA">提交</tui-button>
                </view>
            </tui-modal>
        </view>
    </view>
</template>

<script>
    let tunnel
    if (uni.getSystemInfoSync().platform == "android")
        tunnel = uni.requireNativePlugin('NBATunnel')
    if (uni.getSystemInfoSync().platform == "ios")
        tunnel = uni.requireNativePlugin("NBATunnel-NBATunnel")
    import wInput from "@/components/watch-login/watch-input.vue"; 
    import wButton from "@/components/watch-login/watch-button.vue"; 
    import tuiModal from '@/components/tui-modal/tui-modal.vue';
    import { DbHelper } from "@/js/db.js";
    export default {
        data() {
            return {
                account: "",
                password: "",
                isRotate: false, //是否加载旋转
                isFocus: false, // 是否聚焦
                disabled: false,
                upgradeType: "pkg", //pkg 整包 wgt 升级包
                upgradeContent: "", //更新内容
                upgradeUrl: "", //更新地址
                NBAUsername:'',
                NBAPassword:'',
                modal:false,
            };
        },
        components: {
            wInput,
            wButton,
        },
        async mounted() {
            await DbHelper.init();
            this.isLogin();
        },
        methods: {
            async isLogin() {
                if (uni.getSystemInfoSync().platform == "android")
                {
                    uni.showLoading({mask:true})
                    //无本地NBA数据
                    if(!uni.getStorageSync('NBAInfo'))
                    {
                        uni.hideLoading()
                        let [,res] = await uni.showModal({
                            content: '即将发起NBA权限请求,请点击确认,若在此过程中退出应用或者拒绝了权限,需要重装本应用才能重新发起NBA权限请求!',
                            showCancel:false,
                        });
                        this.modal = true
                    }
                    else//有本地NBA数据,说明之前已经建立了网卡
                    {
                        let NBAInfo =  uni.getStorageSync('NBAInfo')
                        this.NBAUsername = NBAInfo.NBAUsername
                        this.NBAPassword = NBAInfo.NBAPassword
                        uni.hideLoading()
                        await this.requestNBA()
                    }
                }
                if (uni.getSystemInfoSync().platform == "ios") 
                {
                    uni.showLoading({mask:true})
                    //无本地NBA数据
                    if(!uni.getStorageSync('NBAInfo'))
                    {
                        uni.hideLoading()
                        let [,res] = await uni.showModal({
                            content: '请输入正确的NBA账号密码才能后续登录!',
                            showCancel:false,
                        });
                        this.modal = true
                    }
                    else//有本地NBA数据,说明之前已经建立了网卡
                    {
                        let NBAInfo =  uni.getStorageSync('NBAInfo')
                        this.NBAUsername = NBAInfo.NBAUsername
                        this.NBAPassword = NBAInfo.NBAPassword
                        uni.hideLoading()
                        await this.requestNBA()
                    }
                    
                }
            },
            /**
             * @description 连接NBA服务器
             */
            async requestNBA(){
                return new Promise((resolve,rejcet) => {
                    uni.showLoading({
                        title: 'NBA连接中...',
                        mask: true
                    });
                    if (!this.NBAUsername)
                        return uni.showToast({
                            title: "NBA账号不能为空!",
                            icon: "none"
                        }); //  显示提示框
                    if (!this.NBAPassword)
                        return uni.showToast({
                            title: "NBA密码不能为空!",
                            icon: "none"
                        });
                    if (uni.getSystemInfoSync().platform == "android") 
                    {
                        tunnel.connectNBA({
                            NBAUsername:this.NBAUsername,
                            NBAPassword:this.NBAPassword
                        },async res=>{
                            this.modal = false
                            uni.hideLoading()
                            if(res == '隧道登录成功' || res == '请求权限')
                            {
                                let NBAInfo = {
                                    NBAUsername:this.NBAUsername,
                                    NBAPassword:this.NBAPassword
                                }
                                uni.setStorageSync('NBAInfo',NBAInfo);
                                let { account,password } = uni.getStorageSync("userInfo"); // 从本地缓存中同步获取指定 key 对应的内容。
                                if (!account) return; // 本地没有用户信息 直接返回(停留在登录页面)
                                this.isFocus = false;
                                this.isRotate = true;
                                this.disabled = true;
                                this.account = account;
                                this.password = password;
                                setTimeout(()=>{this.getUpdate()},1000)
                            }
                            else 
                            {
                                if(/02000405/.test(res))
                                {
                                    await uni.showModal({
                                        content:`NBA账号或者密码错误,请重新输入` ,
                                        showCancel:false,
                                    });
                                    this.NBAUsername = ''
                                    this.NBAPassword = ''
                                    uni.removeStorageSync('NBAInfo');
                                    this.modal = true
                                }
                                else
                                {
                                    uni.showModal({
                                        content:res,
                                        showCancel:false
                                    }); 
                                }
                                rejcet(res)
                            }
                        })
                    }
                    if (uni.getSystemInfoSync().platform == "ios") 
                    {
                        let NBAInfo = {
                            NBAUsername:this.NBAUsername,
                            NBAPassword:this.NBAPassword
                        }
                        tunnel.connectNBA(NBAInfo,async res=>{
                            console.log(res); 
                            this.modal = false
                            uni.hideLoading()
                            if(res == '隧道登录成功' || res == '请求权限')
                            {
                                uni.setStorageSync('NBAInfo',NBAInfo);
                                let { account,password } = uni.getStorageSync("userInfo"); // 从本地缓存中同步获取指定 key 对应的内容。
                                if (!account) return; // 本地没有用户信息 直接返回(停留在登录页面)
                                this.isFocus = false;
                                this.isRotate = true;
                                this.disabled = true;
                                this.account = account;
                                this.password = password;
                                setTimeout(()=>{uni.reLaunch({url: "/pages/home/home"})},1000)
                            }
                            else 
                            {
                                if(/用户名或密码错误/.test(res))
                                {
                                    await uni.showModal({
                                        content:`NBA账号或者密码错误,请重新输入` ,
                                        showCancel:false,
                                    });
                                    this.NBAUsername = ''
                                    this.NBAPassword = ''
                                    uni.removeStorageSync('NBAInfo');
                                    this.modal = true
                                }
                                else
                                {
                                    uni.showModal({
                                        title:"NBA登录失败",
                                        content:res,
                                        showCancel:false
                                    }); 
                                }
                                rejcet(res)
                            }
                        })
                    }
                })
                
                
            },
            // 检查网络状态,并进一步检查APP更新情况(有网条件)
            async getUpdate() {
                let [, netWork] = await uni.getNetworkType()
                if (netWork.networkType == "2g" || netWork.networkType == "none") 
                {
                    if (uni.getStorageSync("userInfo"))
                        uni.reLaunch({url: "/pages/home/home"}); 
                }   
                else
                {
                    plus.runtime.getProperty(plus.runtime.appid, async widgetInfo => {
                        let {data: res} = await this.$http.get('/api/basedata/GetAppUpdateMsg',{
                            params:{
                                appid: plus.runtime.appid,
                                version: plus.runtime.version,
                                imei: plus.device.imei,
                            }
                        })
                        if (res.data) 
                        {
                            this.upgradeUrl = res.data.DownLoadURL;
                            this.upgradeContent = res.data.Describe || "1.性能优化\n2.修复部分错误"
                            this.$refs.yomolUpgrade.show();
                        } else 
                            uni.reLaunch({url: "/pages/home/home"})
                    });
                }
            },
            async startLogin(e) {
                if (this.isRotate) return;
                if (!this.account)
                    return uni.showToast({
                        title: "账号不能为空!",
                        icon: "none"
                    }); //  显示提示框
                if (!this.password)
                    return uni.showToast({
                        title: "密码不能为空!",
                        icon: "none"
                    });
                this.isRotate = true; 
                this.disabled = true; 
                let res;
                if (uni.getSystemInfoSync().platform == "android")
                {
                    try {
                        let data = await this.$http.post("/api/security/token", {
                            username: this.account,
                            password: this.password,
                        });
                        res = data.data;
                    } catch (e) {
                        this.isRotate = false;
                        this.disabled = false;
                        return;
                    }
                    let {data: res2} = await this.$http.get("/api/account/GetUserInfo",{
                        custom: { auth: false },
                        header: { token: res.token }
                    });
                    let userInfo = {
                        account: this.account,
                        password: this.password,
                        token: res.token
                    };
                    for (let key in res2.data) {
                        userInfo[key] = res2.data[key];
                    }
                    uni.setStorageSync("userInfo", userInfo); 
                    await this.getUpdate()
                    this.isRotate = false;
                }
                if (uni.getSystemInfoSync().platform == "ios") 
                {
                    tunnel.post({
                        url:`${this.$http.config.baseURL}/api/security/token?username=${this.account}&password=${this.password}`,
                    },callBack=>{
                        callBack = JSON.parse(callBack)
                        console.log(callBack);
                        //存储token
                        if(callBack.status != 0)
                        {
                            uni.showToast({
                                title: callBack.msg,
                                icon: 'none'
                            });
                            this.isRotate = false;
                            this.disabled = false;
                            return
                        }
                        tunnel.get({
                            url:`${this.$http.config.baseURL}/api/account/GetUserInfo`,
                            token:callBack.token
                        },callBack2=>{
                            callBack2 = JSON.parse(callBack2)
                            console.log(callBack2);
                            let userInfo = {
                                account: this.account,
                                password: this.password,
                                token: callBack.token
                            };
                            for (let key in callBack2.data) 
                            {
                                userInfo[key] = callBack2.data[key];
                            }
                            console.log(userInfo);
                            uni.setStorageSync("userInfo", userInfo); 
                            uni.reLaunch({url: "/pages/home/home"})
                        })  
                    })
                }
            },
        },
    };
</script>

编写完成后,右键UNI项目: 发行-原生APP本地打包-生成本地打包APP资源

打开APP资源路径,然后删除掉HBuilder-uniPlugin/HBuilder-Hello/Pandora/apps/_UNI_33C5A38/www这个WWW文件夹,然后把生成的离线WWW文件拷贝过去。

资源文件拷贝完毕后,选中工程中的HBuilder-uniPlugin-Info.plist文件右键->Open As->Source Code找到dcloud_uniplugins节点,copy下面的内容添加到dcloud_uniplugins节点下,按您插件的实际信息填写对应的项

编写plist文件完毕后,选择HBuilder这个target后,插上真机,command+B后等待一段时间即可进行测试。

6.插件打包

测试完毕之后,首先生成iosTunnel.Framework文件,

再选中iosTunnel,command+B进行framework文件编译生成

生成完成后,将第三方SDK库文件与我们自己写生成的库文件放置为如下目录

//package.json
{
    "name": "原生插件",
    "id": "NBATunnel",
    "version": "1.0",
    "description": "原生插件",
    "_dp_type":"nativeplugin",
    "_dp_nativeplugin":{
        "ios": {
            "plugins": [{
                "type": "module",
                "name": "NBATunnel-NBATunnel",
                "class": "NBATunnel"
            }],
            "frameworks": ["SecurePortal.framework"],
            "integrateType": "framework",
            "deploymentTarget": "9.0"
        }
    }
}

然后再manifest.json选择本地插件,提交云端打包即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容