Vue3和Electron實現(xiàn)桌面端應用

為了方便記錄一些個人隨筆啼止,我最近用LaravelVue 3.0擼了一個博客系統(tǒng),其中使用到了一個基于 markdown-itmarkdown 編輯器Vue組件v-md-editor。我感覺用它去編寫markdown還是很方便的怯疤。后面就有了一個想法,基于此組件用Electron來實現(xiàn)一個markdown桌面端應用,自己平時拿來使用也是個不錯的選擇脊髓。

題外話:VS Code就是用Electron開發(fā)出來的桌面應用,我現(xiàn)在除了移動端的開發(fā)外栅受,其他的都是使用VS Code來開發(fā)了将硝,各種插件開發(fā)起來真的很方便。

接下來我就帶大家來一步步來實現(xiàn)這個功能屏镊。

Vue CLI 搭建Vue項目

  • 在選擇的目錄下執(zhí)行vue create electron-vue3-mark-down

  • 選擇自定義的模板(可以選擇默認的Vue 3 模板)

    vue create

  • 選擇Vue3TypeScript, 其他的選項基于自身項目決定是否選擇

vue3 + TypeScript
  • 執(zhí)行npm run serve看看效果
效果

Vue項目改造為markdown編輯器

  • 執(zhí)行npm i @kangc/v-md-editor@next -S安裝v-md-editor

  • 添加TypeScript類型定義文件

由于v-md-editor這個庫沒有TypeScript類型定義文件依疼,我就直接在shims-vue.d.ts這個文件的后面添加的,當然也可以新建一個文件添加申明(tsconfig.json能找到這個文件就OK)而芥。

declare module "*.vue" {
  import type { DefineComponent } from "vue";
  const component: DefineComponent<{}, {}, any>;
  export default component;
}

<!-- 添加的內(nèi)容 -->
declare module "@kangc/v-md-editor/lib/theme/vuepress.js";
declare module "@kangc/v-md-editor/lib/plugins/copy-code/index";
declare module "@kangc/v-md-editor/lib/plugins/line-number/index";
declare module "@kangc/v-md-editor";
declare module "prismjs";
  • 改造App.vue
<template>
  <div>
    <v-md-editor v-model="content" height="100vh"></v-md-editor>
  </div>
</template>

<script lang="ts">
// 編輯器
import VMdEditor from "@kangc/v-md-editor";
import "@kangc/v-md-editor/lib/style/base-editor.css";
import vuepress from "@kangc/v-md-editor/lib/theme/vuepress.js";
import "@kangc/v-md-editor/lib/theme/style/vuepress.css";
// 高亮顯示
import Prism from "prismjs";
import "prismjs/components/prism-json";
import "prismjs/components/prism-dart";
import "prismjs/components/prism-c";
import "prismjs/components/prism-swift";
import "prismjs/components/prism-kotlin";
import "prismjs/components/prism-java";

// 快捷復制代碼
import createCopyCodePlugin from "@kangc/v-md-editor/lib/plugins/copy-code/index";
import "@kangc/v-md-editor/lib/plugins/copy-code/copy-code.css";
// 行號
import createLineNumbertPlugin from "@kangc/v-md-editor/lib/plugins/line-number/index";
VMdEditor.use(vuepress, {
  Prism,
})
  .use(createCopyCodePlugin())
  .use(createLineNumbertPlugin());

import { defineComponent, ref } from "vue";
export default defineComponent({
  name: "App",
  components: { VMdEditor },
  setup() {
    const content = ref("");

    return { content };
  },
});
</script>

<style>
/* 去掉一些按鈕 */
.v-md-icon-save,
.v-md-icon-fullscreen {
  display: none;
}
</style>

這個文件也很簡單律罢,整個頁面就是一個編輯器<v-md-editor v-model="content" height="100vh"></v-md-editor>,這個markdown編輯器有高亮顯示棍丐,代碼顯示行號误辑,復制代碼按鈕等插件,當然更方便的是可以添加其他的插件豐富這個markdown編輯器的功能.

  • 效果如下
編輯器效果

Vue CLI Plugin Electron Builder

我嘗試過用Vite 2.0去搭建Electron項目歌逢,但是沒有找到類似的ViteElectron結(jié)合好使的工具巾钉,所以放棄了Vite 2.0的誘惑。如果有小伙伴有推薦可以分享下秘案。

  • 使用vue add electron-builder安裝砰苍,我選擇的是13.0.0Electron的最新版本潦匈。

我一般是選擇最高的版本,其實這個版本有坑赚导,我后面再想想要不要介紹下這個坑历等,哈哈。

效果

我們看到新加了很多的依賴庫辟癌,還添加了一個background.ts文件寒屯。簡單介紹下,這個文件執(zhí)行在主線程黍少,其他的頁面都是在渲染線程寡夹。渲染線程有很多限制的,有些功能只能在主線程執(zhí)行厂置,這里就不具體展開了菩掏。

  • 執(zhí)行npm run electron:serve看效果
效果

至此,就可以看到桌面應用的效果了昵济,并且邊修改Vue的代碼智绸,桌面應用也能實時看到修改后的效果。

優(yōu)化功能

啟動全屏顯示
  • 引入screen
import { screen } from "electron";
  • 創(chuàng)建窗口的時候設置為screen大小
<!-- background.ts -->
async function createWindow() {
  const { width, height } = screen.getPrimaryDisplay().workAreaSize;
  const win = new BrowserWindow({
    width,
    height,
    // 省略...
  });
    // 省略...
}

這樣應用啟動的時候就是全屏顯示了访忿。

修改菜單欄
  • 定義菜單欄
<!-- background.ts -->

const template: Array<MenuItemConstructorOptions> = [
  {
    label: "MarkDown",
    submenu: [
      {
        label: "關(guān)于",
        accelerator: "CmdOrCtrl+W",
        role: "about",
      },
      {
        label: "退出程序",
        accelerator: "CmdOrCtrl+Q",
        role: "quit",
      },
    ],
  },
  {
    label: "文件",
    submenu: [
      {
        label: "打開文件",
        accelerator: "CmdOrCtrl+O",
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          _event: KeyboardEvent
        ) => {
            // TODO: 打開文件         
        },
      },
      {
        label: "存儲",
        accelerator: "CmdOrCtrl+S",
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          _event: KeyboardEvent
        ) => {
          // TODO: 存儲內(nèi)容  
        },
      },
    ],
  },
  {
    label: "編輯",
    submenu: [
      {
        label: "撤銷",
        accelerator: "CmdOrCtrl+Z",
        role: "undo",
      },
      {
        label: "重做",
        accelerator: "Shift+CmdOrCtrl+Z",
        role: "redo",
      },
      {
        type: "separator",
      },
      {
        label: "剪切",
        accelerator: "CmdOrCtrl+X",
        role: "cut",
      },
      {
        label: "復制",
        accelerator: "CmdOrCtrl+C",
        role: "copy",
      },
      {
        label: "粘貼",
        accelerator: "CmdOrCtrl+V",
        role: "paste",
      },
    ],
  },
  {
    label: "窗口",
    role: "window",
    submenu: [
      {
        label: "最小化",
        accelerator: "CmdOrCtrl+M",
        role: "minimize",
      },
      {
        label: "最大化",
        accelerator: "CmdOrCtrl+M",
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          _event: KeyboardEvent
        ) => {
          if (focusedWindow) {
            focusedWindow.maximize();
          }
        },
      },
      {
        type: "separator",
      },
      {
        label: "切換全屏",
        accelerator: (function () {
          if (process.platform === "darwin") {
            return "Ctrl+Command+F";
          } else {
            return "F11";
          }
        })(),
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          _event: KeyboardEvent
        ) => {
          if (focusedWindow) {
            focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
          }
        },
      },
    ],
  },
  {
    label: "幫助",
    role: "help",
    submenu: [
      {
        label: "學習更多",
        click: function () {
          shell.openExternal("http://electron.atom.io");
        },
      },
    ],
  },
];
  1. 具體如何定義參閱Electron Menu瞧栗。

  2. 打開文件存儲目前還沒實現(xiàn),后面實現(xiàn)海铆。

  • 設置菜單欄
import { Menu } from "electron";
app.on("ready", async () => {
  // 省略...
  // 創(chuàng)建菜單
  Menu.setApplicationMenu(Menu.buildFromTemplate(template));
});

ready鉤子函數(shù)中進行設置Menu迹恐。

  • 效果
菜單效果
編輯器打開markdonw文件的內(nèi)容
  • 主線程選擇文件,將文件路徑傳給渲染線程
<!-- background.ts -->
dialog
  .showOpenDialog({
    properties: ["openFile"],
    filters: [{ name: "Custom File Type", extensions: ["md"] }],
  })
  .then((res) => {
    if (res && res["filePaths"].length > 0) {
      const filePath = res["filePaths"][0];
      // 將文件傳給渲染線程
      if (focusedWindow) {
        focusedWindow.webContents.send("open-file-path", filePath);
      }
    }
  })
  .catch((err) => {
    console.log(err);
  });
  1. showOpenDialog是打開文件的方法卧斟,我們這里指定了只打開md文件殴边;

  2. 獲得文件路徑后,通過focusedWindow.webContents.send("open-file-path", filePath);這個方法將文件路徑傳給渲染線程珍语。

  • 渲染線程取到文件路徑锤岸,讀取文件內(nèi)容,賦值給markdown編輯器
<!-- App.vue -->
import { ipcRenderer } from "electron";
import { readFileSync } from "fs";

export default defineComponent({
  // 省略...
  setup() {
    const content = ref("");
    
    onMounted(() => {
      // 1.
      ipcRenderer.on("open-file-path", (e, filePath: string) => {
        if (filePath && filePath.length > 0) {
          // 2.
          content.value = readFileSync(filePath).toString();
        }
      });
    });

    return { content };
  },
});

  • vue添加node支持
<!-- vue.config.js -->
module.exports = {
  pluginOptions: {
    electronBuilder: {
      nodeIntegration: true,
    },
  },
};

  • 效果
效果圖
markdonw的內(nèi)容存入文件
  • 主線程發(fā)起向渲染線程獲取編輯器內(nèi)容的請求
<!-- background.js -->
if (focusedWindow) {
    focusedWindow.webContents.send("get-content", "");
}
  • 渲染線程主線程向返回編輯器的內(nèi)容
<!-- App.vue -->
onMounted(() => {
    ipcRenderer.on("get-content", () => {
        ipcRenderer.send("save-content", content.value);
    });
});
  • 主線程收到內(nèi)容然后存入文件
<!-- background.ts -->
// 存儲文件
ipcMain.on("save-content", (event: unknown, content: string) => {
  if (openedFile.length > 0) {
    // 直接存儲到文件中去
    try {
      writeFileSync(openedFile, content);
      console.log("保存成功");
    } catch (error) {
      console.log("保存失敗");
    }
  } else {
    const options = {
      title: "保存文件",
      defaultPath: "new.md",
      filters: [{ name: "Custom File Type", extensions: ["md"] }],
    };
    const focusedWindow = BrowserWindow.getFocusedWindow();
    if (focusedWindow) {
      dialog
        .showSaveDialog(focusedWindow, options)
        .then((result: Electron.SaveDialogReturnValue) => {
          if (result.filePath) {
            try {
              writeFileSync(result.filePath, content);
              console.log("保存成功");
              openedFile = result.filePath;
            } catch (error) {
              console.log("保存失敗");
            }
          }
        })
        .catch((error) => {
          console.log(error);
        });
    }
  }
});
  • 效果
效果圖

打包

  • 設置應用的名字和圖片
<!-- vue.config.js -->
module.exports = {
  pluginOptions: {
    electronBuilder: {
      nodeIntegration: true,
      // 添加的設置
      builderOptions: {
        appId: "com.johnny.markdown", 
        productName: "JJMarkDown",  // 應用的名字
        copyright: "Copyright ? 2021", //版權(quán)聲明
        mac: {
          icon: "./public/icon.icns", // icon
        },
      },
    },
  },
};

  • icon.icns生成
  1. 準備一個1024*1024的圖片板乙,同級目錄下創(chuàng)建一個為icons.iconset的文件夾是偷;
  2. 創(chuàng)建各種不同尺寸要求的圖片文件
sips -z 16 16 icon.png -o icons.iconset/icon_16x16.png
sips -z 32 32 icon.png -o icons.iconset/icon_16x16@2x.png
sips -z 32 32 icon.png -o icons.iconset/icon_32x32.png
sips -z 64 64 icon.png -o icons.iconset/icon_32x32@2x.png
sips -z 128 128 icon.png -o icons.iconset/icon_128x128.png
sips -z 256 256 icon.png -o icons.iconset/icon_128x128@2x.png
sips -z 256 256 icon.png -o icons.iconset/icon_256x256.png
sips -z 512 512 icon.png -o icons.iconset/icon_256x256@2x.png
sips -z 512 512 icon.png -o icons.iconset/icon_512x512.png
sips -z 1024 1024 icon.png -o icons.iconset/icon_512x512@2x.png
  1. 獲得名為icon.icns的圖標文件
iconutil -c icns icons.iconset -o icon.icns
  • 打包
npm run electron:build
  • 結(jié)果
dmg

獲得的dmg文件就可以直接安裝使用了。

代碼

<!-- background.ts -->
"use strict";

import {
  app,
  protocol,
  BrowserWindow,
  screen,
  Menu,
  MenuItem,
  shell,
  dialog,
  ipcMain,
} from "electron";
import { KeyboardEvent, MenuItemConstructorOptions } from "electron/main";
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
import installExtension, { VUEJS3_DEVTOOLS } from "electron-devtools-installer";
const isDevelopment = process.env.NODE_ENV !== "production";
import { writeFileSync } from "fs";

let openedFile = "";
// 存儲文件
ipcMain.on("save-content", (event: unknown, content: string) => {
  if (openedFile.length > 0) {
    // 直接存儲到文件中去
    try {
      writeFileSync(openedFile, content);
      console.log("保存成功");
    } catch (error) {
      console.log("保存失敗");
    }
  } else {
    const options = {
      title: "保存文件",
      defaultPath: "new.md",
      filters: [{ name: "Custom File Type", extensions: ["md"] }],
    };
    const focusedWindow = BrowserWindow.getFocusedWindow();
    if (focusedWindow) {
      dialog
        .showSaveDialog(focusedWindow, options)
        .then((result: Electron.SaveDialogReturnValue) => {
          if (result.filePath) {
            try {
              writeFileSync(result.filePath, content);
              console.log("保存成功");
              openedFile = result.filePath;
            } catch (error) {
              console.log("保存失敗");
            }
          }
        })
        .catch((error) => {
          console.log(error);
        });
    }
  }
});

const template: Array<MenuItemConstructorOptions> = [
  {
    label: "MarkDown",
    submenu: [
      {
        label: "關(guān)于",
        accelerator: "CmdOrCtrl+W",
        role: "about",
      },
      {
        label: "退出程序",
        accelerator: "CmdOrCtrl+Q",
        role: "quit",
      },
    ],
  },
  {
    label: "文件",
    submenu: [
      {
        label: "打開文件",
        accelerator: "CmdOrCtrl+O",
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          _event: KeyboardEvent
        ) => {
          dialog
            .showOpenDialog({
              properties: ["openFile"],
              filters: [{ name: "Custom File Type", extensions: ["md"] }],
            })
            .then((res) => {
              if (res && res["filePaths"].length > 0) {
                const filePath = res["filePaths"][0];
                // 將文件傳給渲染線程
                if (focusedWindow) {
                  focusedWindow.webContents.send("open-file-path", filePath);
                  openedFile = filePath;
                }
              }
            })
            .catch((err) => {
              console.log(err);
            });
        },
      },
      {
        label: "存儲",
        accelerator: "CmdOrCtrl+S",
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          _event: KeyboardEvent
        ) => {
          if (focusedWindow) {
            focusedWindow.webContents.send("get-content", "");
          }
        },
      },
    ],
  },
  {
    label: "編輯",
    submenu: [
      {
        label: "撤銷",
        accelerator: "CmdOrCtrl+Z",
        role: "undo",
      },
      {
        label: "重做",
        accelerator: "Shift+CmdOrCtrl+Z",
        role: "redo",
      },
      {
        type: "separator",
      },
      {
        label: "剪切",
        accelerator: "CmdOrCtrl+X",
        role: "cut",
      },
      {
        label: "復制",
        accelerator: "CmdOrCtrl+C",
        role: "copy",
      },
      {
        label: "粘貼",
        accelerator: "CmdOrCtrl+V",
        role: "paste",
      },
    ],
  },
  {
    label: "窗口",
    role: "window",
    submenu: [
      {
        label: "最小化",
        accelerator: "CmdOrCtrl+M",
        role: "minimize",
      },
      {
        label: "最大化",
        accelerator: "CmdOrCtrl+M",
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          _event: KeyboardEvent
        ) => {
          if (focusedWindow) {
            focusedWindow.maximize();
          }
        },
      },
      {
        type: "separator",
      },
      {
        label: "切換全屏",
        accelerator: (function () {
          if (process.platform === "darwin") {
            return "Ctrl+Command+F";
          } else {
            return "F11";
          }
        })(),
        click: (
          item: MenuItem,
          focusedWindow: BrowserWindow | undefined,
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          _event: KeyboardEvent
        ) => {
          if (focusedWindow) {
            focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
          }
        },
      },
    ],
  },
  {
    label: "幫助",
    role: "help",
    submenu: [
      {
        label: "學習更多",
        click: function () {
          shell.openExternal("http://electron.atom.io");
        },
      },
    ],
  },
];

protocol.registerSchemesAsPrivileged([
  { scheme: "app", privileges: { secure: true, standard: true } },
]);

async function createWindow() {
  const { width, height } = screen.getPrimaryDisplay().workAreaSize;
  const win = new BrowserWindow({
    width,
    height,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    },
  });

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    // Load the url of the dev server if in development mode
    await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string);
    if (!process.env.IS_TEST) win.webContents.openDevTools();
  } else {
    createProtocol("app");
    // Load the index.html when not in development
    win.loadURL("app://./index.html");
  }
}

// Quit when all windows are closed.
app.on("window-all-closed", () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on("ready", async () => {
  if (isDevelopment && !process.env.IS_TEST) {
    // Install Vue Devtools
    try {
      await installExtension(VUEJS3_DEVTOOLS);
    } catch (e) {
      console.error("Vue Devtools failed to install:", e.toString());
    }
  }
  createWindow();
  // 創(chuàng)建菜單
  Menu.setApplicationMenu(Menu.buildFromTemplate(template));
});

// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
  if (process.platform === "win32") {
    process.on("message", (data) => {
      if (data === "graceful-exit") {
        app.quit();
      }
    });
  } else {
    process.on("SIGTERM", () => {
      app.quit();
    });
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載亡驰,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者晓猛。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市凡辱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌栗恩,老刑警劉巖透乾,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡乳乌,警方通過查閱死者的電腦和手機捧韵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汉操,“玉大人再来,你說我怎么就攤上這事×琢觯” “怎么了芒篷?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長采缚。 經(jīng)常有香客問我针炉,道長,這世上最難降的妖魔是什么扳抽? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任篡帕,我火速辦了婚禮,結(jié)果婚禮上贸呢,老公的妹妹穿的比我還像新娘镰烧。我一直安慰自己,他們只是感情好楞陷,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布拌滋。 她就那樣靜靜地躺著,像睡著了一般猜谚。 火紅的嫁衣襯著肌膚如雪败砂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天魏铅,我揣著相機與錄音昌犹,去河邊找鬼。 笑死览芳,一個胖子當著我的面吹牛斜姥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播沧竟,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼铸敏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了悟泵?” 一聲冷哼從身側(cè)響起杈笔,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎糕非,沒想到半個月后蒙具,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體球榆,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年禁筏,在試婚紗的時候發(fā)現(xiàn)自己被綠了持钉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡篱昔,死狀恐怖每强,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情州刽,我是刑警寧澤空执,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站怀伦,受9級特大地震影響脆烟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜房待,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一邢羔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧桑孩,春花似錦拜鹤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至宣虾,卻和暖如春惯裕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背绣硝。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工蜻势, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鹉胖。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓简肴,卻偏偏與公主長得像赖淤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子曼库,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容