React Hooks + TypeScript 做個仿 MacOS 桌面(三):點(diǎn)擊效果與彈窗

這是我的項(xiàng)目記錄系列文章第三篇嫌佑,目前項(xiàng)目進(jìn)度有些停滯豆茫,主要是最近其他事情比較多加懶,于是我強(qiáng)行讓自己在這幾天對點(diǎn)擊圖標(biāo)跳出彈窗這一過程進(jìn)行優(yōu)化屋摇,及時總結(jié)和記錄揩魂,同時讓大家知道我還活著。

本篇將介紹目前項(xiàng)目當(dāng)中炮温,點(diǎn)擊 Dock 圖標(biāo)所產(chǎn)生的系列效果火脉,如生成可拖住的彈窗等,目前只有計(jì)算器和畫板等四個圖標(biāo)可用柒啤。

本文所有代碼均在 項(xiàng)目代碼倦挂,項(xiàng)目會一直優(yōu)化,歡迎 watch 和 star担巩。

過程分析

上篇我們已經(jīng)實(shí)現(xiàn) Dock 的動態(tài)效果方援,接下來我們肯定會不由自主想點(diǎn)圖標(biāo)。當(dāng)我們點(diǎn)擊圖標(biāo)涛癌,首先會出現(xiàn)圖標(biāo)彈跳的動效犯戏,然后出現(xiàn)圖標(biāo)對應(yīng)應(yīng)用彈框窥浪,并同時在圖標(biāo)下方出現(xiàn)高亮小圓點(diǎn)。接下來我會用畫板 drawing 作為例子展示代碼笛丙,關(guān)于畫板的詳細(xì)內(nèi)容本篇暫不作介紹漾脂,預(yù)計(jì)會成為第四篇主角。

本文出現(xiàn)代碼內(nèi)容對應(yīng)目錄:

圖標(biāo)點(diǎn)擊交互

動效實(shí)現(xiàn)

當(dāng)我們初次點(diǎn)擊圖標(biāo)使其變成激活狀態(tài)時胚鸯,應(yīng)該有交互動畫:

這里我參考了 animate-css 的 bounce.css

// footer/index.scss
@keyframes bounce {
  from,
  20%,
  53%,
  to {
    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
    transform: translate3d(0, 0, 0);
  }

  40%,
  43% {
    animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
    transform: translate3d(0, -35px, 0) scaleY(1.1);
  }

  70% {
    animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
    transform: translate3d(0, -35px, 0) scaleY(1.05);
  }

  80% {
    transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
    transform: translate3d(0, 0, 0) scaleY(0.95);
  }

  90% {
    transform: translate3d(0, -6px, 0) scaleY(1.02);
  }
}
.bounce {
  animation-duration: 2s;
  animation-name: top; 
}

isDrawingOpen(應(yīng)用開啟骨稿、關(guān)閉)和 isDrawingShow(應(yīng)用展示、最小化)

給圖標(biāo)加上點(diǎn)擊事件姜钳,通過其名字判斷是哪個圖標(biāo)坦冠。每一個圖標(biāo)我們給到一個布爾值對象,如這里的 isDrawingOpen哥桥,它是個對象辙浑,里面記錄一個布爾值 type,作為彈框開關(guān)(只有在打開和關(guān)閉應(yīng)用時使用)拟糕;一個 index 記錄圖標(biāo)對應(yīng)順序判呕。

點(diǎn)擊后給對應(yīng)圖標(biāo)增加 .bounce,此時圖標(biāo)開始 bounce 動畫送滞,同時我們在 2.5s 后改變 type (畫板出現(xiàn))和記錄 index侠草,并且將類選擇器移除,便于下次重新點(diǎn)擊使用犁嗅。

// Footer.tsx
interface OpenTypes {
  type: boolean;
  index?: number;
}

const [isDrawingOpen, setDrawingOpen] = useState<OpenTypes>({
  type: false
});

const [isDrawingShow, setDrawingShow] = useState(true);

const dockItemClick = useCallback(
  (item: string, index: number) => {
    if (!dockRef.current) {
      return;
    }
    const imgList = dockRef.current.childNodes;
    const img = imgList[index] as HTMLDivElement;
    switch (item) {
      case "PrefApp.png":
        if (!isDrawingOpen.type) {
          img.classList.add("bounce");
          setTimeout(() => {
            setDrawingOpen({ type: !isDrawingOpen.type, index });
            img.classList.remove("bounce");
          }, 2500);
          return;
        }
        setDrawingShow(!isDrawingShow);
        return;
    }
  },
  [isDrawingOpen, isDrawingShow]
);

與此同時可以看到有一個單獨(dú)的布爾值:isDrawingShow边涕,它的作用是在應(yīng)用激活時點(diǎn)擊圖標(biāo)或最小化按鈕時切換展示狀態(tài)。

useEffect(() => {
  if (!dockRef.current) {
    return;
  }
  const imgList = dockRef.current.childNodes;
  [isDrawingOpen].forEach((item) => {
    if (item.index) {
      const img = imgList[item.index] as HTMLDivElement;
      !item.type
        ? setTimeout(() => {
            img?.classList.remove("active");
          }, 1000)
        : img.classList.add("active");
    }
  });
}, [isDrawingOpen]);

上面就是我們記錄 index 的作用褂微,由于關(guān)閉應(yīng)用不受 Dock 控制功蜓,我們需要監(jiān)聽 isDrawingOpen 來判斷是否加類選擇器 active,它的作用主要是圖標(biāo)高亮小圓點(diǎn)的開關(guān)

小圓點(diǎn)的實(shí)現(xiàn)

// footer/index.scss

#DockItem {
  position: relative;
  display: flex;
  &.active {
    &::after {
      content: "●";
      font-size: 0.1em;
      position: absolute;
      bottom: -7px;
    }
  }
}

createContext 實(shí)現(xiàn)組件通信:

這里我們的畫板組件肯定是單獨(dú)成文件的宠蚂,因此開啟和關(guān)閉彈窗操作就要用到組件通信式撼。

export const FooterContext = createContext<any>([]);
...
return (
   <React.Fragment>
    <FooterContext.Provider
      value={[isDrawingOpen, setDrawingOpen, isDrawingShow, setDrawingShow]}
      >
      <Drawing />
    </FooterContext.Provider>
    <div ref={dockRef} style={{ height: defaultWidth }}>
      {dockList.map((item, index) => {
        return (
          <div
            id="DockItem"
            style={
              {
                backgroundImage: "url(" + require("./image/" + item) + ")",
                backgroundPosition: "center",
                backgroundSize: "cover",
                backgroundRepeat: "no-repeat",
              } as CSSProperties
            }
            key={index + item}
            onClick={() => dockItemClick(item, index)}
          />
        );
      })}
    </div>
  </React.Fragment>
);

看過該系列 第二篇 的朋友或許還記得,之前我們的圖標(biāo)均為 img 肥矢,而現(xiàn)在改為了 div端衰,其主要目的是為了配合 active 下的偽元素使用(img 使用 ::after 無效)叠洗。

我們通過 createContext 生成一個 FooterContext甘改,像我們的 Drawing 子組件傳遞 [isDrawingOpen, setDrawingOpen, isDrawingShow, setDrawingShow] ,同時子組件可以調(diào)用 FooterContext灭抑,改變應(yīng)用狀態(tài)十艾。

下面是子組件 Drawing 使用 FooterContext 的完整代碼:

// drawing/index.tsx
import React, { useContext, useEffect, useState, useCallback } from "react";
import { useModal } from "../modal/UseModal";
import { FooterContext } from "../footer/Footer";
import { TitleBar } from "react-desktop/macOs";
import Canvas from "./Canvas";
import "./index.scss";
/// <reference path="react-desktop.d.ts" />

export const Drawing = React.memo(() => {
  const { open, close, RenderModal } = useModal();
  const [
    isDrawingOpen,
    setDrawingOpen,
    isDrawingShow,
    setDrawingShow,
  ] = useContext(FooterContext);
  const [style, setStyle] = useState({ width: 1200, height: 800 });
  const [isFullscreen, setFullscreen] = useState(false);

  useEffect(isDrawingOpen.type ? open : close, [isDrawingOpen]);
  const maximizeClick = useCallback(() => {
    if (isFullscreen) {
      setStyle({ width: 1200, height: 800 });
    } else {
      setStyle({ width: -1, height: -1 });
    }
    setFullscreen(!isFullscreen);
  }, [isFullscreen]);

  return (
    <RenderModal
      data={{
        width: style.width,
        height: style.height,
        id: "DrawingView",
        moveId: "DrawingMove",
        isShow: isDrawingShow,
      }}
    >
      <div className="drawing-wrapper">
        <TitleBar
          controls
          id="DrawingMove"
          isFullscreen={isFullscreen}
          onCloseClick={() => {
            close();
            setDrawingOpen({ ...isDrawingOpen, type: false });
          }}
          onMinimizeClick={() => {
            setDrawingShow(false);
          }}
          onMaximizeClick={maximizeClick}
          onResizeClick={maximizeClick}
        ></TitleBar>
        <Canvas
          height={isFullscreen ? document.body.clientHeight - 32 : style.height}
          width={isFullscreen ? document.body.clientWidth : style.width}
        />
      </div>
    </RenderModal>
  );
});

這里的 useModal 是一個彈框組件,下文詳解腾节。Canvas 是 drawing 的主體忘嫉,這里我們不過多介紹荤牍。

react-desktop/macOs 的使用及自定義聲明文件

可以看到我使用了 react-desktop/macOs 組件,一個 react 的桌面 UI 庆冕,但是這個庫沒有 @types 康吵,需要自己寫 .d.ts:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./",
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react"
  },
  "include": ["src", "typings"] // 主要是這里加了 typings
}
// typings/react-desktop.d.ts
declare module "react-desktop/macOs" {
  export const View: JSX;
  export const Radio: JSX;
  export const TitleBar: JSX;
  export const Toolbar: JSX;
  export const Text: JSX;
  export const Box: JSX;
  export const ListView: JSX;
  export const ListViewRow: JSX;
  export const Window: JSX;
  export const Dialog: JSX;
  export const Button: JSX;
}

然后通過下面方式引入,就可以在 TypeScript 內(nèi)使用了

/// <reference path="react-desktop.d.ts" />

TitleBar

我們繼續(xù)看我們的 drawing/index.tsx访递,這里主要用到了 TitleBar

可以看到 useModal 里釋出了 open, close, RenderModal晦嵌,其中 RenderModal 就是一會講到的 彈窗,前兩個就是控制彈窗的開關(guān)拷姿。

我們點(diǎn)擊紅色的關(guān)閉時惭载,會調(diào)用父組件傳過來的 isDrawingOpen, setDrawingOpen;而黃色的最小化按鈕則調(diào)用 setDrawingShow(false)响巢,這里我們直接設(shè)置為 false 因?yàn)樵俅握故臼峭ㄟ^點(diǎn)擊圖標(biāo)描滔,最小化時高亮點(diǎn)不應(yīng)該去除;maximizeClick 函數(shù)用于綠色最大化按鈕踪古,其中我用 width 和 height 是 -1 告訴 Modal 全屏含长,彈窗及其拖拽需要包括他倆再內(nèi)的 data 所傳遞過去的值。

用 Portal 實(shí)現(xiàn)彈窗組件

項(xiàng)目的每個小應(yīng)用本質(zhì)上是個彈窗伏穆,因此實(shí)現(xiàn)一個可復(fù)用的組件十分必要茎芋,得益于 Portal ,我們能快速實(shí)現(xiàn)蜈出。
我直接復(fù)用了 這篇文章 里的 React Hooks 版本 Portal 實(shí)現(xiàn)方式田弥。

可拖拽彈窗:

// 代碼篇幅較長,可以先看上面參考博客內(nèi)版本
// Modal.tsx
import ReactDOM from "react-dom";
import React, {
  useState,
  useCallback,
  useMemo,
  useEffect,
  CSSProperties,
} from "react";

type Props = {
  children: React.ReactChild;
  closeModal: () => void;
  onDrag: (T: any) => void;
  onDragEnd: () => void;
  data: {
    width: number;
    height: number;
    id: string;
    moveId: string;
    isShow: boolean;
  };
};

const Modal = React.memo(
  ({ children, closeModal, onDrag, onDragEnd, data }: Props) => {
    const domEl = document.getElementById("main-view") as HTMLDivElement;
    if (!domEl) return null;
    const dragEl = document.getElementById(data.id) as HTMLDivElement;
    const moveEl = document.getElementById(data.moveId) as HTMLDivElement;
    const localPosition = localStorage.getItem(data.id) || null;
    const initPosition = localPosition
      ? JSON.parse(localPosition)
      : {
          x: data.width === -1 ? 0 : (window.innerWidth - data.width) / 2,
          y: data.height === -1 ? 0 : (window.innerHeight - data.height) / 2,
        };
    const [state, setState] = useState({
      isDragging: false,
      origin: { x: 0, y: 0 },
      position: initPosition,
    });

    const handleMouseDown = useCallback(({ clientX, clientY }) => {
      setState((state) => ({
        ...state,
        isDragging: true,
        origin: {
          x: clientX - state.position.x,
          y: clientY - state.position.y,
        },
      }));
    }, []);

    const handleMouseMove = useCallback(
      ({ clientX, clientY, target }) => {
        if (!state.isDragging || (moveEl && target !== moveEl)) return;
        let x = clientX - state.origin.x;
        let y = clientY - state.origin.y;
        if (x <= 0) {
          x = 0;
        } else if (x > window.innerWidth - dragEl.offsetWidth) {
          x = window.innerWidth - dragEl.offsetWidth;
        }
        if (y <= 0) {
          y = 0;
        } else if (y > window.innerHeight - dragEl.offsetHeight) {
          y = window.innerHeight - dragEl.offsetHeight;
        }
        const newPosition = { x, y };
        setState((state) => ({
          ...state,
          position: newPosition,
        }));
        onDrag({ newPosition, domEl });
      },
      [state.isDragging, state.origin, moveEl, dragEl, onDrag, domEl]
    );

    const handleMouseUp = useCallback(() => {
      if (state.isDragging) {
        setState((state) => ({
          ...state,
          isDragging: false,
        }));

        onDragEnd();
      }
    }, [state.isDragging, onDragEnd]);

    useEffect(() => {
      if (data.width === -1) {
        setState({
          isDragging: false,
          origin: { x: 0, y: 0 },
          position: { x: 0, y: 0 },
        });
      }
    }, [data.width]);

    useEffect(() => {
      if (!domEl) return;
      domEl.addEventListener("mousemove", handleMouseMove);
      domEl.addEventListener("mouseup", handleMouseUp);
      return () => {
        domEl.removeEventListener("mousemove", handleMouseMove);
        domEl.removeEventListener("mouseup", handleMouseUp);
        if (data.width !== -1) {
          localStorage.setItem(data.id, JSON.stringify(state.position));
        }
      };
    }, [
      domEl,
      handleMouseMove,
      handleMouseUp,
      data.id,
      data.width,
      state.position,
    ]);

    const styles = useMemo(
      () => ({
        left: `${state.position.x}px`,
        top: `${state.position.y}px`,
        zIndex: state.isDragging ? 2 : 1,
        display: data.isShow ? "block" : "none",
        position: "absolute",
      }),
      [state.isDragging, state.position, data.isShow]
    );

    return ReactDOM.createPortal(
      <div
        style={styles as CSSProperties}
        onMouseDown={handleMouseDown}
        id={data.id}
      >
        {children}
      </div>,
      domEl
    );
  }
);

可以看到我在 Modal.tsx 中加入了拖拽的功能铡原,代碼篇幅很長偷厦,但原理其實(shí)比較簡單,可以先看參考博客中的純 Modal 版本后在看加入拖拽代碼的版本燕刻。

這里我直接展示了完整代碼只泼,原本打算像第二篇講動效那樣介紹,但事實(shí)上兩者思路十分相似卵洗,都是通過 useEffect 監(jiān)聽鼠標(biāo)事件请唱,那么我簡單介紹下思路,便于理解:

首先我們看到有三個 dom元素 domEl过蹂、dragEl 十绑、moveEl:domEl 和參考文章中一樣,主要是彈窗出現(xiàn)的 dom酷勺,我將它加在了 APP.tsx 內(nèi)本橙;dragEl 就代表了 應(yīng)用主體 dom(這里就是 Drawing);moveEl 則是應(yīng)用組件內(nèi)部可拖拽部分脆诉,一般是 TitleBar甚亭。

由于模擬應(yīng)用贷币,我們需要記錄應(yīng)用當(dāng)前位置,所以用到了 localStorage亏狰,initPosition 初始化應(yīng)用位置役纹,通過 -1 判斷是否全屏。

state 用于記錄鼠標(biāo)數(shù)據(jù)及是否可拖拽暇唾;handleMouseDown 記錄下當(dāng)前鼠標(biāo)坐標(biāo)字管,并開啟拖拽;handleMouseMove 計(jì)算出移動位移信不,賦值給 position嘲叔,需要注意邊界情況,當(dāng)然這里我簡化了操作抽活,直接不允許出屏了硫戈;handleMouseUp 關(guān)閉拖拽;closeModal, onDrag, onDragEnd 分別是彈窗內(nèi)部關(guān)閉函數(shù)下硕,可附加的拖拽事件和停止事件丁逝。
以上就是彈框組件及拖拽的主要思路了。

UseModal

UseModal 基本和文中一致:

// UseModal.tsx
import React, { useState } from "react";

import Modal from "./Modal";

// Modal組件最基礎(chǔ)的兩個事件梭姓,open/close
export const useModal = () => {
  const [isVisible, setIsVisible] = useState(false);

  const open = () => setIsVisible(true);
  const close = () => setIsVisible(false);

  const RenderModal = ({
    children,
    data,
  }: {
    children: React.ReactChild;
    data: {
      width: number;
      height: number;
      id: string;
      moveId: string;
      isShow: boolean;
    };
  }) => (
    <React.Fragment>
      {isVisible && (
        <Modal
          data={data}
          closeModal={close}
          onDrag={() => console.log("onDrag")}
          onDragEnd={() => console.log("onDragEnd")}
        >
          {children}
        </Modal>
      )}
    </React.Fragment>
  );

  return {
    open,
    close,
    RenderModal,
  };
};

如何使用該組件我們上文已講到霜幼,如果你忘了可以回看。

至此誉尖,我們已經(jīng)完成了開篇的過程分析了罪既。

小結(jié)

本篇文章介紹了項(xiàng)目從點(diǎn)擊 Dock 呈現(xiàn)應(yīng)用到關(guān)閉應(yīng)用的過程實(shí)現(xiàn),里面有較多細(xì)節(jié)铡恕,值得反復(fù)回味與優(yōu)化琢感。

此篇相對前兩篇較長,能看到這里都是真愛(學(xué)習(xí)和我)探熔。既然如此驹针,不如給我點(diǎn)個贊吧??。

目前該項(xiàng)目已完成部分功能诀艰,包括簡單設(shè)置柬甥,基礎(chǔ)計(jì)算器,基礎(chǔ)畫板等其垄,即使是這些已有功能也有很多需要完善的地方苛蒲。

后續(xù)我會慢慢優(yōu)化,并在相應(yīng)模塊代碼優(yōu)化到一定程度時不定時更新系列文章捉捅。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末撤防,一起剝皮案震驚了整個濱河市虽风,隨后出現(xiàn)的幾起案子棒口,更是在濱河造成了極大的恐慌寄月,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件无牵,死亡現(xiàn)場離奇詭異漾肮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)茎毁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進(jìn)店門克懊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人七蜘,你說我怎么就攤上這事谭溉∠鹇保” “怎么了扮念?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我驹溃,道長柒瓣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任吠架,我火速辦了婚禮芙贫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘傍药。我一直安慰自己磺平,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布拐辽。 她就那樣靜靜地躺著拣挪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪俱诸。 梳的紋絲不亂的頭發(fā)上菠劝,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天,我揣著相機(jī)與錄音睁搭,去河邊找鬼赶诊。 笑死笼平,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的舔痪。 我是一名探鬼主播寓调,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼锄码!你這毒婦竟也來了夺英?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤滋捶,失蹤者是張志新(化名)和其女友劉穎痛悯,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體重窟,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡灸蟆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了亲族。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炒考。...
    茶點(diǎn)故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖霎迫,靈堂內(nèi)的尸體忽然破棺而出斋枢,到底是詐尸還是另有隱情,我是刑警寧澤知给,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布瓤帚,位于F島的核電站,受9級特大地震影響涩赢,放射性物質(zhì)發(fā)生泄漏戈次。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一筒扒、第九天 我趴在偏房一處隱蔽的房頂上張望怯邪。 院中可真熱鬧,春花似錦花墩、人聲如沸悬秉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽和泌。三九已至,卻和暖如春祠肥,著一層夾襖步出監(jiān)牢的瞬間武氓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留县恕,地道東北人东羹。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像弱睦,于是被迫代替她去往敵國和親百姓。 傳聞我的和親對象是個殘疾皇子渊额,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評論 2 355

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