瀏覽代碼

feat: add mobile support

Yidadaa 2 年之前
父節點
當前提交
1fae774bb2
共有 8 個文件被更改,包括 144 次插入39 次删除
  1. 43 19
      app/components/home.module.scss
  2. 26 14
      app/components/home.tsx
  3. 10 2
      app/components/settings.tsx
  4. 3 3
      app/components/window.scss
  5. 11 0
      app/globals.scss
  6. 21 0
      app/icons/close.svg
  7. 25 0
      app/icons/menu.svg
  8. 5 1
      app/layout.tsx

+ 43 - 19
app/components/home.module.scss

@@ -20,14 +20,12 @@
 
 .container {
   @include container();
-
-  max-width: 1080px;
-  max-height: 864px;
 }
 
 .tight-container {
   --window-width: 100vw;
   --window-height: 100vh;
+  --window-content-width: calc(var(--window-width) - var(--sidebar-width));
 
   @include container();
 
@@ -44,6 +42,43 @@
   box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05);
 }
 
+.window-content {
+  width: var(--window-content-width);
+  height: 100%;
+}
+
+.mobile {
+  display: none;
+}
+
+@media only screen and (max-width: 600px) {
+  .container {
+    min-width: unset;
+    min-height: unset;
+    border: 0;
+    border-radius: 0;
+  }
+
+  .sidebar {
+    position: absolute;
+    top: -100%;
+    z-index: 999;
+    border-bottom-left-radius: 20px;
+    border-bottom-right-radius: 20px;
+    height: 80vh;
+    box-shadow: var(--shadow);
+    transition: all ease 0.3s;
+  }
+
+  .sidebar-show {
+    top: 0;
+  }
+
+  .mobile {
+    display: block;
+  }
+}
+
 .sidebar-header {
   position: relative;
   padding-top: 20px;
@@ -72,7 +107,6 @@
 }
 
 .chat-list {
-  width: 260px;
 }
 
 .chat-item {
@@ -159,13 +193,8 @@
   display: inline-flex;
 }
 
-.sidebar-action:last-child {
-  margin-left: 15px;
-}
-
-.window-content {
-  width: var(--window-content-width);
-  height: 100%;
+.sidebar-action:not(:last-child) {
+  margin-right: 15px;
 }
 
 .chat {
@@ -193,7 +222,7 @@
 }
 
 .chat-message-container {
-  max-width: 80%;
+  max-width: var(--message-max-width);
   display: flex;
   flex-direction: column;
   align-items: flex-start;
@@ -227,6 +256,7 @@
 }
 
 .chat-message-item {
+  box-sizing: border-box;
   max-width: 100%;
   margin-top: 10px;
   border-radius: 10px;
@@ -255,9 +285,6 @@
   color: #aaa;
 }
 
-.chat-message-action-button {
-}
-
 .chat-input-panel {
   position: absolute;
   bottom: 20px;
@@ -272,15 +299,12 @@
   flex: 1;
 }
 
-.chat-input-panel-multi {
-}
-
 .chat-input {
   height: 100%;
   width: 100%;
   border-radius: 10px;
   border: var(--border-in-light);
-  box-shadow: var(--card-shadow);
+  box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
   background-color: var(--white);
   color: var(--black);
   font-family: inherit;

+ 26 - 14
app/components/home.tsx

@@ -21,22 +21,13 @@ import BotIcon from "../icons/bot.svg";
 import AddIcon from "../icons/add.svg";
 import DeleteIcon from "../icons/delete.svg";
 import LoadingIcon from "../icons/three-dots.svg";
+import MenuIcon from "../icons/menu.svg";
+import CloseIcon from "../icons/close.svg";
 
 import { Message, SubmitKey, useChatStore, Theme } from "../store";
 import { Settings } from "./settings";
 import dynamic from "next/dynamic";
 
-export const LazySettings = dynamic(
-  async () => await (await import("./settings")).Settings,
-  {
-    loading: () => (
-      <div className="">
-        <LoadingIcon />
-      </div>
-    ),
-  }
-);
-
 export function Markdown(props: { content: string }) {
   return (
     <ReactMarkdown remarkPlugins={[RemarkMath]} rehypePlugins={[RehypeKatex]}>
@@ -134,7 +125,7 @@ function useSubmitHandler() {
   };
 }
 
-export function Chat() {
+export function Chat(props: { showSideBar?: () => void }) {
   type RenderMessage = Message & { preview?: boolean };
 
   const session = useChatStore((state) => state.currentSession());
@@ -200,6 +191,14 @@ export function Chat() {
           </div>
         </div>
         <div className={styles["window-actions"]}>
+          <div className={styles["window-action-button"] + " " + styles.mobile}>
+            <IconButton
+              icon={<MenuIcon />}
+              bordered
+              title="查看消息列表"
+              onClick={props?.showSideBar}
+            />
+          </div>
           <div className={styles["window-action-button"]}>
             <IconButton
               icon={<BrainIcon />}
@@ -300,6 +299,7 @@ function useSwitchTheme() {
 export function Home() {
   const [createNewSession] = useChatStore((state) => [state.newSession]);
   const loading = !useChatStore?.persist?.hasHydrated();
+  const [showSideBar, setShowSideBar] = useState(true);
 
   // settings
   const [openSettings, setOpenSettings] = useState(false);
@@ -322,7 +322,9 @@ export function Home() {
         config.tightBorder ? styles["tight-container"] : styles.container
       }`}
     >
-      <div className={styles.sidebar}>
+      <div
+        className={styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`}
+      >
         <div className={styles["sidebar-header"]}>
           <div className={styles["sidebar-title"]}>ChatGPT Next</div>
           <div className={styles["sidebar-sub-title"]}>
@@ -342,6 +344,12 @@ export function Home() {
 
         <div className={styles["sidebar-tail"]}>
           <div className={styles["sidebar-actions"]}>
+            <div className={styles["sidebar-action"] + " " + styles.mobile}>
+              <IconButton
+                icon={<CloseIcon />}
+                onClick={() => setShowSideBar(!showSideBar)}
+              />
+            </div>
             <div className={styles["sidebar-action"]}>
               <IconButton
                 icon={<SettingsIcon />}
@@ -365,7 +373,11 @@ export function Home() {
       </div>
 
       <div className={styles["window-content"]}>
-        {openSettings ? <LazySettings /> : <Chat key="chat" />}
+        {openSettings ? (
+          <Settings closeSettings={() => setOpenSettings(false)} />
+        ) : (
+          <Chat key="chat" showSideBar={() => setShowSideBar(true)} />
+        )}
       </div>
     </div>
   );

+ 10 - 2
app/components/settings.tsx

@@ -5,15 +5,15 @@ import EmojiPicker, { Emoji, Theme as EmojiTheme } from "emoji-picker-react";
 import styles from "./settings.module.scss";
 
 import ResetIcon from "../icons/reload.svg";
+import CloseIcon from "../icons/close.svg";
 
 import { List, ListItem, Popover } from "./ui-lib";
 
 import { IconButton } from "./button";
 import { SubmitKey, useChatStore, Theme } from "../store";
 import { Avatar } from "./home";
-import dynamic from "next/dynamic";
 
-export function Settings() {
+export function Settings(props: { closeSettings: () => void }) {
   const [showEmojiPicker, setShowEmojiPicker] = useState(false);
   const [config, updateConfig, resetConfig] = useChatStore((state) => [
     state.config,
@@ -29,6 +29,14 @@ export function Settings() {
           <div className={styles["window-header-sub-title"]}>设置选项</div>
         </div>
         <div className={styles["window-actions"]}>
+          <div className={styles["window-action-button"]}>
+            <IconButton
+              icon={<CloseIcon />}
+              onClick={props.closeSettings}
+              bordered
+              title="重置所有选项"
+            />
+          </div>
           <div className={styles["window-action-button"]}>
             <IconButton
               icon={<ResetIcon />}

+ 3 - 3
app/components/window.scss

@@ -12,9 +12,9 @@
   font-weight: bolder;
   overflow: hidden;
   text-overflow: ellipsis;
-  display: -webkit-box;
-  -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;
+  white-space: nowrap;
+  display: block;
+  max-width: 50vw;
 }
 
 .window-header-sub-title {

+ 11 - 0
app/globals.scss

@@ -46,6 +46,17 @@
   --window-height: 90vh;
   --sidebar-width: 300px;
   --window-content-width: calc(var(--window-width) - var(--sidebar-width));
+  --message-max-width: 80%;
+}
+
+@media only screen and (max-width: 600px) {
+  :root {
+    --window-width: 100vw;
+    --window-height: 100vh;
+    --sidebar-width: 100vw;
+    --window-content-width: var(--window-width);
+    --message-max-width: 100%;
+  }
 }
 
 @media (prefers-color-scheme: dark) {

+ 21 - 0
app/icons/close.svg

@@ -0,0 +1,21 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
+  height="16" viewBox="0 0 16 16" fill="none">
+  <defs>
+    <rect id="path_0" x="0" y="0" width="16" height="16" />
+  </defs>
+  <g opacity="1" transform="translate(0 0)  rotate(0 8 8)">
+    <mask id="bg-mask-0" fill="white">
+      <use xlink:href="#path_0"></use>
+    </mask>
+    <g mask="url(#bg-mask-0)">
+      <path id="路径 1"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2.6666666666666665 2.6666666666666665)  rotate(0 5.333333333333333 5.333333333333333)"
+        d="M0,0L10.67,10.67 " />
+      <path id="路径 2"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2.6666666666666665 2.6666666666666665)  rotate(0 5.333333333333333 5.333333333333333)"
+        d="M0,10.67L10.67,0 " />
+    </g>
+  </g>
+</svg>

+ 25 - 0
app/icons/menu.svg

@@ -0,0 +1,25 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
+  height="16" viewBox="0 0 16 16" fill="none">
+  <defs>
+    <rect id="path_0" x="0" y="0" width="16" height="16" />
+  </defs>
+  <g opacity="1" transform="translate(0 0)  rotate(0 8 8)">
+    <mask id="bg-mask-0" fill="white">
+      <use xlink:href="#path_0"></use>
+    </mask>
+    <g mask="url(#bg-mask-0)">
+      <path id="路径 1"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2.649903333333333 3.983233333333333)  rotate(0 5.333331666666666 0)"
+        d="M0,0L10.67,0 " />
+      <path id="路径 2"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2.649903333333333 7.983233333333333)  rotate(0 5.333331666666666 0)"
+        d="M0,0L10.67,0 " />
+      <path id="路径 3"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2.649903333333333 11.983233333333333)  rotate(0 5.333331666666666 0)"
+        d="M0,0L10.67,0 " />
+    </g>
+  </g>
+</svg>

+ 5 - 1
app/layout.tsx

@@ -12,7 +12,11 @@ export default function RootLayout({
   children: React.ReactNode;
 }) {
   return (
-    <html lang="en">
+    <html lang="zh-Hans-CN">
+      <meta
+        name="viewport"
+        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
+      />
       <body>{children}</body>
     </html>
   );