mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	feat: add mobile support
This commit is contained in:
		@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -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 />}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
								
								app/icons/close.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/icons/close.svg
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 981 B  | 
							
								
								
									
										25
									
								
								app/icons/menu.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/icons/menu.svg
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.2 KiB  | 
@@ -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>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user