x1ongzhu 2 tahun lalu
induk
melakukan
b7a1c000c6
7 mengubah file dengan 222 tambahan dan 225 penghapusan
  1. 1 0
      package.json
  2. 149 0
      src/components/common/LoginForm/index.vue
  3. 2 1
      src/components/common/index.ts
  4. 21 181
      src/views/page/Home.vue
  5. 1 1
      tsconfig.json
  6. 43 42
      vite.config.ts
  7. 5 0
      yarn.lock

+ 1 - 0
package.json

@@ -24,6 +24,7 @@
   },
   "dependencies": {
     "@traptitech/markdown-it-katex": "^3.6.0",
+    "@vicons/tabler": "^0.12.0",
     "@vueuse/core": "^9.13.0",
     "highlight.js": "^11.7.0",
     "html2canvas": "^1.4.1",

+ 149 - 0
src/components/common/LoginForm/index.vue

@@ -0,0 +1,149 @@
+<template>
+    <n-form ref="loginForm" :model="form" :rules="rules" :show-label="false">
+        <n-form-item ref="phoneRef" path="phone" label="">
+            <n-input v-model:value="form.phone" :input-props="{ type: 'tel' }" @keydown.enter.prevent>
+                <template #prefix>
+                    <n-icon size="24" class="input-icon">
+                        <user />
+                    </n-icon>
+                </template>
+            </n-input>
+        </n-form-item>
+        <n-form-item path="code" label="">
+            <n-input-group style="align-items: center">
+                <n-input v-model:value="form.code" :input-props="{ type: 'tel' }">
+                    <template #prefix>
+                        <n-icon size="24" class="input-icon">
+                            <Lock />
+                        </n-icon>
+                    </template>
+                </n-input>
+                <div style="min-width: 20px"></div>
+                <n-button :disabled="countDown > 0" type="tertiary" secondary @click="sendVerify">
+                    {{ countDown > 0 ? `${countDown}秒后重新发送` : '发送验证码' }}
+                </n-button>
+            </n-input-group>
+        </n-form-item>
+
+        <div class="check">
+            <n-checkbox v-model:checked="agree">
+                已阅读同意
+                <span class="prim" @click.stop="">《用户服务协议》</span>和
+                <span class="prim" @click.stop="">《平台隐私协》</span>
+            </n-checkbox>
+        </div>
+
+        <div class="submit">
+            <n-button @click="submit" block type="primary" size="large" :loading="loading" circle> 登录 </n-button>
+        </div>
+    </n-form>
+</template>
+<script setup lang="ts">
+import { ref, Ref } from 'vue'
+import { NForm, NFormItem, NInput, NInputGroup, NCheckbox, NButton, NIcon, useMessage, FormInst } from 'naive-ui'
+import { User, Lock } from '@vicons/tabler'
+import { fetchSendVerify } from '../../../api'
+import { useStorage } from '@vueuse/core'
+import { useAuthStore } from '@/store'
+
+const emit = defineEmits(['success'])
+const ms = useMessage()
+const authStore = useAuthStore()
+const form = ref({
+    phone: '',
+    code: ''
+})
+const rules = {
+    phone: [
+        {
+            validator(rule: any, value: any) {
+                if (!value) {
+                    return new Error('请输入手机号')
+                } else if (!/^1[3-9]\d{9}$/.test(value)) {
+                    return new Error('手机号格式错误')
+                }
+
+                return true
+            },
+            trigger: ['submit']
+        }
+    ],
+    code: [
+        {
+            validator(rule: any, value: any) {
+                if (!value) {
+                    return new Error('请输入验证码')
+                } else if (!/^\d{4}$/.test(value)) {
+                    return new Error('验证码格式错误')
+                }
+
+                return true
+            },
+            trigger: ['submit']
+        }
+    ]
+}
+const countDown = ref(0)
+const agree = useStorage('agree', false)
+const loading = ref(false)
+const loginForm: Ref<FormInst | null> = ref(null)
+
+async function sendVerify() {
+    if (!/^1[3-9]\d{9}$/.test(form.value.phone)) {
+        ms.error('请输入正确的手机号')
+        return
+    }
+    if (!agree.value) {
+        ms.error('请先阅读并同意用户服务协议和平台隐私协议')
+        return
+    }
+    try {
+        await fetchSendVerify(form.value.phone)
+        countDown.value = 60
+        const timer = setInterval(() => {
+            countDown.value--
+            if (countDown.value <= 0) {
+                clearInterval(timer)
+            }
+        }, 1000)
+    } catch (e: any) {
+        ms.error(e.message)
+    }
+}
+
+async function submit() {
+    try {
+        await loginForm.value?.validate()
+    } catch (e: any) {
+        return
+    }
+    if (!agree.value) {
+        ms.error('请先阅读并同意用户服务协议和平台隐私协议')
+        return
+    }
+    try {
+        loading.value = true
+        await authStore.phoneLogin(form.value.phone, form.value.code)
+        emit('success')
+    } catch (e: any) {
+        ms.error(e.message)
+    } finally {
+        loading.value = false
+    }
+}
+</script>
+<style lang="less" scoped>
+.input-icon {
+    color: var(--n-text-color) !important;
+    margin-right: 12px;
+}
+.btn-send {
+    margin-left: 20px !important;
+}
+.submit {
+    margin-top: 40px;
+    .n-button {
+        height: 40px;
+    }
+}
+</style>

+ 2 - 1
src/components/common/index.ts

@@ -6,5 +6,6 @@ import VipCard from './VipCard/index.vue'
 import Share from './Share/index.vue'
 import Setting from './Setting/index.vue'
 import PromptStore from './PromptStore/index.vue'
+import LoginForm from './LoginForm/index.vue'
 
-export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore, VipCard, Share }
+export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore, VipCard, Share, LoginForm }

+ 21 - 181
src/views/page/Home.vue

@@ -1,5 +1,5 @@
 <template>
-    <div class="page h-full flex flex-col" :style="{ backgroundImage: `url(${inPC ? pcBg : h5Bg})` }">
+    <div class="page h-full flex flex-col" :style="{ backgroundImage: `url(${desktopMode ? pcBg : h5Bg})` }">
         <n-page-header>
             <template #title>
                 <span class="title">CHILLGPT </span>
@@ -10,7 +10,7 @@
             <template #extra>
                 <user-avatar avatarType="small" v-if="isLogin" />
                 <template v-else>
-                    <n-button v-if="inPC" type="primary" round @click="showLogin = true"> 登录 </n-button>
+                    <n-button v-if="desktopMode" type="primary" round @click="showLogin = true"> 登录 </n-button>
                     <n-button v-else @click="$router.push('/login')">登录</n-button>
                 </template>
             </template>
@@ -25,7 +25,7 @@
 
             <n-card
                 class="vip"
-                v-if="inPC"
+                v-if="desktopMode"
                 title="会员权益"
                 content-style="padding:0"
                 header-style="text-align:center;padding:0;height:44px;line-height:44px;font-size: 16px;font-family: AlimamaShuHeiTi;"
@@ -112,90 +112,45 @@
                 header-style="text-align:center;padding:30px;font-size:24px;"
             >
                 <div class="login-box">
-                    <n-form ref="formRef" :model="form" :rules="rules" :show-label="false">
-                        <n-form-item ref="phoneRef" path="phone" label="">
-                            <n-input v-model:value="form.phone" :input-props="{ type: 'tel' }" @keydown.enter.prevent>
-                                <template #prefix>
-                                    <img src="@/assets/user.png" class="input-pre" alt="" />
-                                </template>
-                            </n-input>
-                        </n-form-item>
-                        <n-form-item path="code" label="">
-                            <n-input-group>
-                                <n-input
-                                    v-model:value="form.code"
-                                    :input-props="{ type: 'tel' }"
-                                    @input="handlePasswordInput"
-                                    @keydown.enter.prevent
-                                >
-                                    <template #prefix>
-                                        <img src="@/assets/code.png" class="input-pre" alt="" />
-                                    </template>
-                                </n-input>
-                                <n-button class="send" :disabled="isSend" type="tertiary" secondary @click.stop="send">
-                                    {{ isSend ? `已发送(${sendNum}S)` : '发送验证码' }}
-                                </n-button>
-                            </n-input-group>
-                        </n-form-item>
-
-                        <div class="check">
-                            <n-checkbox v-model:checked="checked">
-                                已阅读同意 <span class="prim" @click.stop="">《用户服务协议》</span>和<span
-                                    class="prim"
-                                    @click.stop=""
-                                    >《平台隐私协》</span
-                                >
-                            </n-checkbox>
-                        </div>
-
-                        <div class="submit">
-                            <n-button @click="submit" block type="primary" size="large" :loading="loading" circle>
-                                登录
-                            </n-button>
-                        </div>
-                    </n-form>
+                    <login-form @success="onLogin"></login-form>
                 </div>
             </n-card>
         </n-modal>
     </div>
 </template>
 
-<script setup>
-import {
-    NForm,
-    NFormItem,
-    NInput,
-    NInputGroup,
-    NCheckbox,
-    NAvatar,
-    NButton,
-    NPageHeader,
-    NCard,
-    useMessage,
-    NGrid,
-    NGridItem,
-    NModal
-} from 'naive-ui'
+<script setup lang="ts">
+import { NAvatar, NButton, NPageHeader, NCard, NGrid, NGridItem, NModal } from 'naive-ui'
 import h5Bg from '@/assets/png-shouye.jpg'
 import pcBg from '@/assets/png-bg.jpg'
 import logo from '@/assets/logo.png'
 import { useUserStore } from '@/store'
 import { useRouter } from 'vue-router'
 import { ref, computed } from 'vue'
-import { http } from '@/plugins/http'
-import { UserAvatar } from '@/components/common'
+import { UserAvatar, LoginForm } from '@/components/common'
+import { useWindowSize } from '@vueuse/core'
 
-const inPC = window.innerWidth >= 1024
+const { width } = useWindowSize()
+const desktopMode = computed(() => {
+    return width.value > 768
+})
 
+const router = useRouter()
 const userStore = useUserStore()
+const showLogin = ref(false)
+
 const isLogin = computed(() => {
     return !!userStore.userInfo.id
 })
 
-const router = useRouter()
+const onLogin = () => {
+    showLogin.value = false
+    router.replace({ name: 'Chat' })
+}
+
 function goChat() {
     if (!userStore.userInfo.id) {
-        if (inPC) {
+        if (desktopMode.value) {
             showLogin.value = true
         } else {
             router.push('/login')
@@ -204,121 +159,6 @@ function goChat() {
         router.push('/chat')
     }
 }
-
-const showLogin = ref(false)
-const form = ref({
-    phone: '',
-    code: ''
-})
-const formRef = ref(null)
-const message = useMessage()
-function submit() {
-    loading.value = true
-    formRef.value
-        .validate()
-        .then(() => {
-            if (checked.value) {
-                userStore
-                    .loginByCode(form.value.phone, form.value.code)
-                    .then(() => {
-                        loading.value = false
-                        message.success('登录成功')
-                        showLogin.value = false
-                        userStore.getUserInfo()
-                    })
-                    .catch(e => {
-                        loading.value = false
-                        console.log(e)
-                        if (e && e.message) {
-                            this.message.error(e.message)
-                            setTime(0)
-                            form.value.code = ''
-                        }
-                    })
-            } else {
-                loading.value = false
-                message.error('请先阅读并同意协议')
-            }
-        })
-        .catch(() => {
-            loading.value = false
-        })
-}
-
-const rules = ref({
-    phone: [
-        {
-            validator(rule, value) {
-                if (!value) {
-                    return new Error('请输入手机号')
-                } else if (!/^[1][3,4,5,6,7,8,9][0-9]{9}$/.test(value)) {
-                    return new Error('手机号格式错误')
-                }
-
-                return true
-            },
-            trigger: ['blur']
-        }
-    ],
-    code: [
-        {
-            required: true,
-            message: '请输入验证码',
-            trigger: ['blur']
-        }
-    ]
-})
-const checked = ref(false)
-const phoneRef = ref(null)
-const loading = ref(false)
-function send() {
-    loading.value = true
-    phoneRef.value
-        ?.validate()
-        .then(errors => {
-            isSend.value = true
-            setTime(60)
-            http.post(
-                '/api/sms/sendVerify',
-                {
-                    phone: form.value.phone
-                },
-                {
-                    body: 'json'
-                }
-            )
-                .then(res => {
-                    loading.value = false
-                    form.value.msgCode = res
-                })
-                .catch(e => {
-                    loading.value = false
-                    setTime(0)
-                })
-        })
-        .catch(() => {
-            loading.value = false
-        })
-}
-
-const isSend = ref(false)
-const sendNum = ref(60)
-const timer = ref(null)
-function setTime(num) {
-    sendNum.value = num
-    if (timer.value) {
-        clearTimeout(timer.value)
-    }
-
-    if (num <= 0) {
-        isSend.value = false
-        return
-    } else {
-        timer.value = setTimeout(() => {
-            setTime(num - 1)
-        }, 1000)
-    }
-}
 </script>
 
 <style lang="less" scoped>

+ 1 - 1
tsconfig.json

@@ -19,7 +19,7 @@
         },
         "types": ["vite/client", "node", "naive-ui/volar"],
         "typeRoots": ["./node_modules/@types", "./src/typings"],
-        "allowJs": false
+        "allowJs": false,
     },
     "exclude": ["node_modules", "dist", "service"]
 }

+ 43 - 42
vite.config.ts

@@ -5,51 +5,52 @@ import vue from '@vitejs/plugin-vue'
 import { VitePWA } from 'vite-plugin-pwa'
 
 function setupPlugins(env: ImportMetaEnv): PluginOption[] {
-  return [
-    vue(),
-    env.VITE_GLOB_APP_PWA === 'true' && VitePWA({
-      injectRegister: 'auto',
-      manifest: {
-        name: 'chatGPT',
-        short_name: 'chatGPT',
-        icons: [
-          { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
-          { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
-        ],
-      },
-    }),
-  ]
+    return [
+        vue(),
+        env.VITE_GLOB_APP_PWA === 'true' &&
+            VitePWA({
+                injectRegister: 'auto',
+                manifest: {
+                    name: 'chatGPT',
+                    short_name: 'chatGPT',
+                    icons: [
+                        { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
+                        { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' }
+                    ]
+                }
+            })
+    ]
 }
 
-export default defineConfig((env) => {
-  const viteEnv = loadEnv(env.mode, process.cwd()) as unknown as ImportMetaEnv
+export default defineConfig(env => {
+    const viteEnv = loadEnv(env.mode, process.cwd()) as unknown as ImportMetaEnv
 
-  return {
-    resolve: {
-      alias: {
-        '@': path.resolve(process.cwd(), 'src'),
-      },
-    },
-    plugins: setupPlugins(viteEnv),
-    server: {
-      host: '0.0.0.0',
-      port: 1002,
-      open: false,
-    },
-    css: {
-        preprocessorOptions: {
-            less: {
-                javascriptEnabled: true,
-                additionalData: '@import "@/styles/common.less";'
+    return {
+        resolve: {
+            alias: {
+                '@': path.resolve(process.cwd(), 'src')
+            }
+        },
+        plugins: setupPlugins(viteEnv),
+        server: {
+            host: '0.0.0.0',
+            port: 5004,
+            open: false
+        },
+        css: {
+            preprocessorOptions: {
+                less: {
+                    javascriptEnabled: true,
+                    additionalData: '@import "@/styles/common.less";'
+                }
+            }
+        },
+        build: {
+            reportCompressedSize: false,
+            sourcemap: false,
+            commonjsOptions: {
+                ignoreTryCatch: false
             }
         }
-    },
-    build: {
-      reportCompressedSize: false,
-      sourcemap: false,
-      commonjsOptions: {
-        ignoreTryCatch: false,
-      },
-    },
-  }
+    }
 })

+ 5 - 0
yarn.lock

@@ -1758,6 +1758,11 @@
     "@typescript-eslint/types" "5.58.0"
     eslint-visitor-keys "^3.3.0"
 
+"@vicons/tabler@^0.12.0":
+  version "0.12.0"
+  resolved "https://registry.npmmirror.com/@vicons/tabler/-/tabler-0.12.0.tgz#11924d2288e9346d47b44dd643ac20e72a32e089"
+  integrity sha512-3+wUFuxb7e8OzZ8Wryct1pzfA2vyoF4lwW98O9s27ZrfCGaJGNmqG+q8A7vQ92Mf+COCgxpK+rhNPTtTvaU6qw==
+
 "@vitejs/plugin-vue@^4.0.0":
   version "4.1.0"
   resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.1.0.tgz"