panhui 2 years ago
parent
commit
5b2c94ef38

+ 61 - 0
package-lock.json

@@ -15,6 +15,7 @@
         "qs": "^6.11.2",
         "vant": "^4.8.0",
         "vue": "^3.3.4",
+        "vue-i18n": "^9.8.0",
         "vue-router": "^4.2.5"
       },
       "devDependencies": {
@@ -503,6 +504,47 @@
       "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
       "dev": true
     },
+    "node_modules/@intlify/core-base": {
+      "version": "9.8.0",
+      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.8.0.tgz",
+      "integrity": "sha512-UxaSZVZ1DwqC/CltUZrWZNaWNhfmKtfyV4BJSt/Zt4Or/fZs1iFj0B+OekYk1+MRHfIOe3+x00uXGQI4PbO/9g==",
+      "dependencies": {
+        "@intlify/message-compiler": "9.8.0",
+        "@intlify/shared": "9.8.0"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
+    "node_modules/@intlify/message-compiler": {
+      "version": "9.8.0",
+      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.8.0.tgz",
+      "integrity": "sha512-McnYWhcoYmDJvssVu6QGR0shqlkJuL1HHdi5lK7fNqvQqRYaQ4lSLjYmZxwc8tRNMdIe9/KUKfyPxU9M6yCtNQ==",
+      "dependencies": {
+        "@intlify/shared": "9.8.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
+    "node_modules/@intlify/shared": {
+      "version": "9.8.0",
+      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.8.0.tgz",
+      "integrity": "sha512-TmgR0RCLjzrSo+W3wT0ALf9851iFMlVI9EYNGeWvZFUQTAJx0bvfsMlPdgVtV1tDNRiAfhkFsMKu6jtUY1ZLKQ==",
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
     "node_modules/@jridgewell/gen-mapping": {
       "version": "0.3.3",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@@ -3875,6 +3917,25 @@
         "eslint": ">=6.0.0"
       }
     },
+    "node_modules/vue-i18n": {
+      "version": "9.8.0",
+      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.8.0.tgz",
+      "integrity": "sha512-Izho+6PYjejsTq2mzjcRdBZ5VLRQoSuuexvR8029h5CpN03FYqiqBrShMyf2I1DKkN6kw/xmujcbvC+4QybpsQ==",
+      "dependencies": {
+        "@intlify/core-base": "9.8.0",
+        "@intlify/shared": "9.8.0",
+        "@vue/devtools-api": "^6.5.0"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
     "node_modules/vue-router": {
       "version": "4.2.5",
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz",

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
     "@vueuse/core": "^10.6.1",
     "axios": "^1.6.2",
     "less": "^4.2.0",
+    "mitt": "^3.0.1",
     "pinia": "^2.1.7",
     "qs": "^6.11.2",
     "vant": "^4.8.0",

+ 25 - 0
src/App.vue

@@ -2,6 +2,31 @@
     <RouterView class="container h-full bg-[#11131E]" />
 </template>
 
+<script setup>
+import { onMounted, onBeforeUnmount } from 'vue'
+import { emitter } from '@/utils/eventBus'
+import { showConfirmDialog } from 'vant'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+const promptLogin = async () => {
+    showConfirmDialog({
+        title: '登录查看更多精彩'
+        // message: '登录查看更多精彩'
+    }).then(() => {
+        router.push('/login')
+    })
+}
+
+onMounted(() => {
+    emitter.on('promptLogin', promptLogin)
+})
+
+onBeforeUnmount(() => {
+    emitter.off('promptLogin', promptLogin)
+})
+</script>
+
 <style lang="less" scoped>
 .container {
     min-height: 100vh;

BIN
src/assets/icon_inter_white.png


BIN
src/assets/png-jindou-bg.png


+ 0 - 100
src/components/BuyPage copy.vue

@@ -1,100 +0,0 @@
-<template>
-    <van-popup class="buy-page" v-model:show="show" closeable round position="bottom" :style="{ maxHeight: '70%' }">
-        <div class="news-title text-white text-lg AlimamaShuHeiTi relative">
-            <span class="z-[1] relative">購買金豆</span>
-            <img class="w-[78px] h-[16px] absolute bottom-0 left-0 z-0" src="@/assets/png-xiantiao1.png" alt="" />
-        </div>
-        <div class="money text-xs text-white mt-[5px]">帳戶餘額:789金豆</div>
-
-        <div class="choose-list flex flex-wrap justify-between">
-            <div class="choose-btn active flex flex-col items-center justify-center">
-                <div class="val text-white">
-                    <span class="text-[16px]">¥</span>
-                    <span class="text-[26px]">19.9</span>
-                </div>
-                <div class="sub text-xs text-white">1990金豆+<span class="text-[#FFEF00]">送888金豆</span></div>
-                <div class="btn-tag">首充福利</div>
-            </div>
-            <div v-for="i in 3" :key="i" class="choose-btn flex flex-col items-center justify-center">
-                <div class="val text-white">
-                    <span class="text-[16px]">¥</span>
-                    <span class="text-[26px]">19.9</span>
-                </div>
-                <div class="sub text-xs text-white">1990金豆+<span class="text-[#FFEF00]">送888金豆</span></div>
-                <div class="btn-tag">粉丝福利</div>
-            </div>
-        </div>
-
-        <div class="tips text-[10px] text-[#61657A] text-center pt-[90px] pb-[18px]">
-            虛擬商品購買後不可退換,青少年請在家長陪同下購買
-        </div>
-
-        <div class="btn">
-            <van-button class="AlimamaShuHeiTi" type="primary" block round>立即支付</van-button>
-        </div>
-    </van-popup>
-</template>
-
-<script setup>
-import { ref } from 'vue'
-const show = ref(false)
-// const props = defineProps({
-//     show: {
-//         type: Boolean,
-//         default: false
-//     }
-// })
-// const emit = defineEmits(['update:show'])
-function changeShow(val) {
-    show.value = val
-}
-defineExpose({ changeShow })
-</script>
-
-<style lang="less" scoped>
-.buy-page {
-    --van-popup-background: #20223c;
-    padding: 21px 18px;
-}
-
-.choose-btn {
-    width: calc(50vw - 27px);
-    height: 88px;
-    background: #2d3059;
-    border-radius: 8px;
-    border: 1px solid #525686;
-    margin-top: 18px;
-    display: inline-flex;
-    position: relative;
-
-    .btn-tag {
-        font-size: 12px;
-        color: #ffffff;
-        line-height: 18px;
-        position: absolute;
-        top: 0;
-        left: 0;
-        width: 62px;
-        height: 18px;
-        background: #7f82af;
-        border-radius: 8px 0px 8px 0px;
-        text-align: center;
-    }
-
-    &.active {
-        background: #2d3059 linear-gradient(318deg, rgba(255, 87, 186, 0.1) 0%, rgba(255, 87, 186, 0.2) 100%);
-        border-radius: 8px;
-        border: 2px solid #ff57ba;
-        .sub {
-            span {
-                color: #fff !important;
-            }
-        }
-        .btn-tag {
-            background: #2d3059 linear-gradient(-318deg, #ff3b20, #ff57ba);
-            left: -2px;
-            top: -1px;
-        }
-    }
-}
-</style>

+ 59 - 0
src/components/VideoHistory.vue

@@ -0,0 +1,59 @@
+<template>
+    <van-popup class="histroy-page" v-model:show="show" round position="bottom" :style="{ maxHeight: '70%' }">
+        <div class="title text-white flex overflow-hidden whitespace-nowrap">
+            <div class="van-ellipsis">少帅,夫人的深情你不少帅,夫人的深情你不…</div>
+            <div>-第1集</div>
+        </div>
+        <div class="btns">
+            <van-button type="primary" size="mini" round>1-5</van-button>
+            <van-button type="primary" size="mini" round>6-10</van-button>
+        </div>
+
+        <div class="video-list">
+            <div class="video-info">
+                <van-image
+                    width="calc(33vw - 20.66px)"
+                    height="140"
+                    fit="cover"
+                    radius="12"
+                    src="https://p9.itc.cn/images01/20200808/f27ca5bb0a1c4e2d9b9df893f273a83c.jpeg"
+                />
+            </div>
+        </div>
+    </van-popup>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+const show = ref(false)
+// const props = defineProps({
+//     show: {
+//         type: Boolean,
+//         default: false
+//     }
+// })
+// const emit = defineEmits(['update:show'])
+function changeShow(val) {
+    show.value = val
+}
+defineExpose({ changeShow })
+</script>
+
+<style lang="less" scoped>
+.histroy-page {
+    background: linear-gradient(180deg, rgba(17, 19, 30, 0.8) 0%, rgba(17, 19, 30, 0) 100%);
+    border-radius: 16px 16px 0px 0px;
+    backdrop-filter: blur(4px);
+    padding: 21px 18px;
+}
+
+.btns {
+    .van-button {
+        width: 72px;
+        font-size: 14px;
+    }
+    .van-button + .van-button {
+        margin-left: 16px;
+    }
+}
+</style>

+ 21 - 11
src/components/VideoLine.vue

@@ -1,31 +1,41 @@
 <template>
-    <div class="video-info flex">
-        <van-image
-            width="110"
-            height="70"
-            fit="cover"
-            radius="12"
-            src="https://p9.itc.cn/images01/20200808/f27ca5bb0a1c4e2d9b9df893f273a83c.jpeg"
-        />
+    <div class="video-info flex" @click="goDetail">
+        <van-image width="110" height="70" fit="cover" radius="12" :src="info.cover" />
 
         <div class="content">
-            <div class="title text-white van-multi-ellipsis--l2 text-sm">少帥,夫人的深情你不配</div>
+            <div class="title text-white van-multi-ellipsis--l2 text-sm">{{ info.title }}</div>
             <div class="text-[#00FFE4] text-xs" v-if="history">观看至第1集</div>
             <div class="text text-xs text-[#61657A] flex justify-between">
-                <span>虐恋·92集</span>
-                <span>803.0万播放</span>
+                <span>虐恋·{{ info.totalEpisodes }}集</span>
+                <span> {{ info.playCount }}播放</span>
             </div>
         </div>
     </div>
 </template>
 
 <script setup>
+import { useRouter } from 'vue-router'
 const props = defineProps({
     history: {
         type: Boolean,
         default: false
+    },
+    info: {
+        type: Object,
+        default: () => {
+            return {}
+        }
     }
 })
+const router = useRouter()
+function goDetail() {
+    router.push({
+        name: 'video',
+        query: {
+            id: props.info.id
+        }
+    })
+}
 </script>
 
 <style lang="less" scoped>

+ 28 - 11
src/components/VideoSmall.vue

@@ -1,21 +1,38 @@
 <template>
-    <div class="video-info inline-block">
+    <div @click="goDetail" class="video-info inline-block">
         <div class="top relative">
-            <van-image
-                width="calc(33vw - 21.33px)"
-                height="140"
-                fit="cover"
-                radius="12"
-                src="https://p9.itc.cn/images01/20200808/f27ca5bb0a1c4e2d9b9df893f273a83c.jpeg"
-            />
-            <div class="top-text text-[10px] text-white p-[8px] absolute bottom-0 left-0 right-0">803.0万播放</div>
+            <van-image width="calc(33vw - 21.33px)" height="140" fit="cover" radius="12" :src="info.cover" />
+            <div class="top-text text-[10px] text-white p-[8px] absolute bottom-0 left-0 right-0">
+                {{ info.playCount }}播放
+            </div>
         </div>
         <div class="content">
-            <div class="text-sm text-white van-ellipsis">少帥,夫人的情深你不配</div>
-            <div class="text-xs text-[#61657A]">虐恋·92集</div>
+            <div class="text-sm text-white van-ellipsis">{{ info.title }}</div>
+            <div class="text-xs text-[#61657A]">虐恋·{{ info.totalEpisodes }}集</div>
         </div>
     </div>
 </template>
+<script setup>
+import { useRouter } from 'vue-router'
+const props = defineProps({
+    info: {
+        type: Object,
+        default: () => {
+            return {}
+        }
+    }
+})
+
+const router = useRouter()
+function goDetail() {
+    router.push({
+        name: 'video',
+        query: {
+            id: props.info.id
+        }
+    })
+}
+</script>
 
 <style lang="less" scoped>
 .video-info {

+ 17 - 1
src/locales/zh-tw.json

@@ -27,5 +27,21 @@
     "home": "首頁",
     "watching": "追劇",
     "mine": "我的"
-  }
+  },
+  "login": {
+    "email": "郵箱",
+    "emailPla": "请输入郵箱",
+    "username": "用戶名",
+    "usernamePla": "请输入用戶名",
+    "password": "密碼",
+    "passwordPla": "請輸入密碼",
+    "password2": "再次輸入密碼",
+    "password2Pla": "請再次輸入密碼",
+    "register": "注册成功",
+    "login": "登入",
+    "goRegister": "暫無帳號,立即注册",
+    "sure": "點擊同意注册協定",
+    "loginsucess": "登入成功"
+  },
+  "loading": "加载中..."
 }

+ 1 - 0
src/main.js

@@ -21,6 +21,7 @@ app.use(createPinia())
 app.use(router)
 app.use(vant)
 app.use(http)
+
 app.use(i18n)
 
 app.mount('#app')

+ 42 - 6
src/router/index.js

@@ -1,4 +1,6 @@
 import { createRouter, createWebHistory } from 'vue-router'
+import { useUserStore } from '../stores/user'
+import { emitter } from '../utils/eventBus'
 
 const router = createRouter({
     history: createWebHistory(import.meta.env.BASE_URL),
@@ -17,23 +19,21 @@ const router = createRouter({
                     name: 'home',
                     component: () => import('@/views/HomeView.vue'),
                     meta: {
-                        menuPage: true
+                        allowGuest: true
                     }
                 },
                 {
                     path: '/watching',
                     name: 'watching',
                     component: () => import('@/views/WatchingView.vue'),
-                    meta: {
-                        menuPage: true
-                    }
+                    meta: {}
                 },
                 {
                     path: '/mine',
                     name: 'mine',
                     component: () => import('@/views/MineView.vue'),
                     meta: {
-                        menuPage: true
+                        allowGuest: true
                     }
                 }
             ]
@@ -56,7 +56,26 @@ const router = createRouter({
         {
             path: '/login',
             name: 'login',
-            component: () => import('@/views/LoginView.vue')
+            component: () => import('@/views/LoginView.vue'),
+            meta: {
+                allowGuest: true
+            }
+        },
+        {
+            path: '/register',
+            name: 'register',
+            component: () => import('@/views/RegisterView.vue'),
+            meta: {
+                allowGuest: true
+            }
+        },
+        {
+            path: '/video',
+            name: 'video',
+            component: () => import('@/views/VideoView.vue'),
+            meta: {
+                allowGuest: true
+            }
         }
     ],
     scrollBehavior(to, from, savedPosition) {
@@ -67,5 +86,22 @@ const router = createRouter({
         }
     }
 })
+router.beforeEach(async (to, from, next) => {
+    const { user, get: getUser } = useUserStore()
+    if (!to.meta.allowGuest && !user) {
+        try {
+            await getUser()
+            next()
+        } catch (error) {
+            next(false)
+            emitter.emit('promptLogin')
+        }
+    } else {
+        if (!user) {
+            getUser().catch(() => {})
+        }
+        next()
+    }
+})
 
 export default router

+ 82 - 0
src/stores/user.js

@@ -0,0 +1,82 @@
+import { ref } from 'vue'
+import { defineStore } from 'pinia'
+import { http } from '@/plugins/http'
+async function setAfCuid(cuid) {
+    // if (Capacitor.isNativePlatform()) {
+    //     AppsFlyer.setCustomerUserId({ cuid })
+    // }
+}
+async function logLogin() {}
+async function logRegistraion(methods = 'username') {}
+export const useUserStore = defineStore('user', () => {
+    const user = ref(null)
+    const login = (username, password) => {
+        return new Promise((resolve, reject) => {
+            http.post('/api/auth/login', { username, password })
+                .then(res => {
+                    http.setToken(res.token)
+                    return get()
+                })
+                .then(res => {
+                    resolve()
+                })
+                .catch(e => {
+                    if (e.message) {
+                        reject(e.message)
+                    } else if (e.errors.length > 0) {
+                        reject(e.errors[0].message)
+                    } else {
+                        reject(e)
+                    }
+                })
+        })
+    }
+    const register = (email, username, password, invitor) => {
+        return new Promise((resolve, reject) => {
+            http.post('api/auth/register', {
+                email,
+                username,
+                password
+            })
+                .then(res => {
+                    http.setToken(res.token)
+                    return get()
+                })
+                .then(res => {
+                    resolve()
+                })
+                .catch(e => {
+                    if (e.message) {
+                        reject(e.message)
+                    } else if (e.errors.length > 0) {
+                        reject(e.errors[0].message)
+                    } else {
+                        reject(e)
+                    }
+                })
+            // http.post('/auth/v1/userRegister', { username, password, invitor })
+            //     .then(res => {
+            //         http.setToken(res.token)
+            //         user.value = res.user
+            //         setAfCuid(res.user.id)
+            //         logLogin()
+            //         if (res.newRegister) logRegistraion()
+            //         resolve(res)
+            //     })
+            //     .catch(e => {
+            //         reject(e)
+            //     })
+        })
+    }
+    const get = async () => {
+        if (!http.token.value) return Promise.reject()
+        return http.get('/user/my').then(res => {
+            user.value = res
+        })
+    }
+    const logout = () => {
+        http.setToken(null)
+        user.value = null
+    }
+    return { user, login, get, register, logout }
+})

+ 4 - 0
src/utils/eventBus.js

@@ -0,0 +1,4 @@
+import mitt from 'mitt'
+
+const emitter = mitt()
+export { emitter }

+ 38 - 19
src/views/HomeView.vue

@@ -19,7 +19,7 @@
                 <img class="w-78px] h-[16px] absolute bottom-0 left-0 z-0" src="@/assets/png-xiantiao1.png" alt="" />
             </div>
             <div class="news-list">
-                <video-small v-for="i in 10" :key="i"></video-small>
+                <video-small v-for="(item, index) in seriesList" :key="index" :info="item"></video-small>
             </div>
         </div>
 
@@ -27,27 +27,18 @@
             <div class="hot-title">{{ $t('home.hot') }}</div>
             <img src="@/assets/hotImg.png" alt="" class="hot-img" />
             <div class="hot-list">
-                <video-line v-for="i in 3" :key="i"></video-line>
+                <video-line v-for="(item, index) in hotList" :key="index" :info="item"></video-line>
             </div>
             <img src="@/assets/pbg-bg.png" alt="" class="hot-bg" />
         </div>
 
-        <div class="py-6 px-[18px]">
-            <div class="news-title text-white text-lg AlimamaShuHeiTi relative">
-                <span class="z-[1] relative">戰神</span>
-                <img class="w-[44px] h-[16px] absolute bottom-0 left-0 z-0" src="@/assets/png-xiantiao1.png" alt="" />
-            </div>
-            <div class="news-list">
-                <video-small v-for="i in 10" :key="i"></video-small>
-            </div>
-        </div>
-        <div class="py-6 px-[18px]">
+        <div class="py-6 px-[18px]" v-for="(item, index) in categories" :key="index">
             <div class="news-title text-white text-lg AlimamaShuHeiTi relative">
-                <span class="z-[1] relative">甜寵</span>
+                <span class="z-[1] relative">{{ item.name }}</span>
                 <img class="w-[44px] h-[16px] absolute bottom-0 left-0 z-0" src="@/assets/png-xiantiao1.png" alt="" />
             </div>
             <div class="news-list">
-                <video-small v-for="i in 10" :key="i"></video-small>
+                <video-small v-for="(data, dataIndex) in item.datas" :key="dataIndex" :info="data"></video-small>
             </div>
         </div>
         <div class="py-6 px-[18px]">
@@ -56,7 +47,7 @@
                 <img class="w-[78px] h-[16px] absolute bottom-0 left-0 z-0" src="@/assets/png-xiantiao1.png" alt="" />
             </div>
             <div class="perfect-list">
-                <video-small v-for="i in 10" :key="i"></video-small>
+                <video-small v-for="(item, index) in seriesList" :key="index" :info="item"></video-small>
             </div>
         </div>
     </div>
@@ -65,6 +56,34 @@
 <script setup>
 import VideoSmall from '../components/VideoSmall.vue'
 import VideoLine from '../components/VideoLine.vue'
+import { http } from '@/plugins/http'
+import { ref } from 'vue'
+
+const seriesList = ref([])
+const hotList = ref([])
+http.get('/api/series').then(res => {
+    seriesList.value = res.data
+    hotList.value = res.data.slice(0, 3)
+})
+
+async function getSeries(data = {}) {
+    return http.get('/api/series', data).then(res => {
+        return Promise.resolve(res.data)
+    })
+}
+
+const categories = ref([])
+http.get('/api/categories').then(res => {
+    res.data.forEach(async item => {
+        let data = await getSeries({
+            categories: item.id
+        })
+        categories.value.push({
+            ...item,
+            datas: data
+        })
+    })
+})
 </script>
 
 <style lang="less" scoped>
@@ -105,11 +124,11 @@ import VideoLine from '../components/VideoLine.vue'
     .hot-title {
         font-size: 18px;
         font-weight: 600;
-        color: #aeffaf;
+        color: #00ffe4;
         line-height: 18px;
-        background: linear-gradient(133deg, #c1ff8f 0%, #00ffe4 100%);
-        -webkit-background-clip: text;
-        -webkit-text-fill-color: transparent;
+        // background: linear-gradient(133deg, #c1ff8f 0%, #00ffe4 100%);
+        // -webkit-background-clip: text;
+        // -webkit-text-fill-color: transparent;
         position: relative;
         z-index: 1;
         padding: 16px 14px;

+ 90 - 54
src/views/LoginView.vue

@@ -5,65 +5,70 @@
             <div class="text-white text-[26px] AlimamaShuHeiTi">Hi,歡迎來到走馬短劇</div>
 
             <!-- 可以使用 CellGroup 作为容器 -->
-            <van-cell-group :border="false">
-                <van-field label-align="top" clearable v-model="form.mail" label="郵箱" placeholder="请输入郵箱">
-                    <template #left-icon>
-                        <img class="left-icon" src="@/assets/login_icon_zhanghao.png" alt="" />
-                    </template>
-                    <template #label>
-                        <div class="text-white text-[14px]">郵箱</div>
-                    </template>
-                </van-field>
-
-                <van-field
-                    label-align="top"
-                    clearable
-                    v-model="form.code"
-                    label="驗證碼"
-                    placeholder="請輸入驗證碼"
-                    v-if="loginCode"
-                >
-                    <template #left-icon>
-                        <img class="left-icon" src="@/assets/login_icon_yanzhengma.png" alt="" />
-                    </template>
-                    <template #label>
-                        <div class="text-white text-[14px]">驗證碼</div>
-                    </template>
-                    <template #button>
-                        <van-button class="px-[11px]" size="mini" round type="primary">獲取驗證碼</van-button>
-                    </template>
-                </van-field>
-
-                <van-field
-                    label-align="top"
-                    type="password"
-                    clearable
-                    v-model="form.password"
-                    label="密碼"
-                    placeholder="請輸入密碼"
-                    v-else
-                >
-                    <template #left-icon>
-                        <img class="left-icon" src="@/assets/login_icon_mima.png" alt="" />
-                    </template>
-                    <template #label>
-                        <div class="text-white text-[14px]">密碼</div>
-                    </template>
-                </van-field>
-
-                <!-- <div @click="loginCode = !loginCode" class="text-[#61657A] text-right text-xs underline pt-[10px]">
+
+            <van-form @submit="onSubmit">
+                <van-cell-group :border="false">
+                    <van-field
+                        label-align="top"
+                        clearable
+                        v-model="form.username"
+                        label="用户名"
+                        :placeholder="$t('login.usernamePla')"
+                        :rules="[
+                            {
+                                required: true,
+                                message: $t('login.usernamePla')
+                            }
+                        ]"
+                    >
+                        <template #left-icon>
+                            <img class="left-icon" src="@/assets/login_icon_zhanghao.png" alt="" />
+                        </template>
+                        <template #label>
+                            <div class="text-white text-[14px]">{{ $t('login.username') }}</div>
+                        </template>
+                    </van-field>
+
+                    <van-field
+                        label-align="top"
+                        type="password"
+                        clearable
+                        v-model="form.password"
+                        label="密碼"
+                        :placeholder="$t('login.passwordPla')"
+                        :rules="[
+                            {
+                                required: true,
+                                message: $t('login.passwordPla')
+                            }
+                        ]"
+                    >
+                        <template #left-icon>
+                            <img class="left-icon" src="@/assets/login_icon_mima.png" alt="" />
+                        </template>
+                        <template #label>
+                            <div class="text-white text-[14px]">{{ $t('login.password') }}</div>
+                        </template>
+                    </van-field>
+
+                    <!-- <div @click="loginCode = !loginCode" class="text-[#61657A] text-right text-xs underline pt-[10px]">
                     {{ loginCode ? '密碼登录' : '验证码登录' }}
                 </div> -->
 
-                <div class="btn py-[64px] px-[17px]">
-                    <van-button type="primary" class="AlimamaShuHeiTi" block round>登入</van-button>
+                    <div class="btn py-[64px] px-[17px]">
+                        <van-button type="primary" class="AlimamaShuHeiTi" native-type="submit" block round>{{
+                            $t('login.login')
+                        }}</van-button>
 
-                    <div class="text-[#61657A] text-sm mt-[20px] text-center">暫無帳號,立即注册</div>
-                </div>
-            </van-cell-group>
+                        <div class="text-[#61657A] text-sm mt-[20px] text-center" @click="goRegister">
+                            {{ $t('login.goRegister') }}
+                        </div>
+                    </div>
+                </van-cell-group>
+            </van-form>
 
             <div class="rule flex justify-center van-safe-area-bottom">
-                <van-checkbox v-model="checked">點擊同意注册協定</van-checkbox>
+                <van-checkbox v-model="checked">{{ $t('login.sure') }}</van-checkbox>
             </div>
         </div>
     </div>
@@ -71,9 +76,13 @@
 
 <script setup>
 import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { showLoadingToast, closeToast, showNotify } from 'vant'
+import { useUserStore } from '../stores/user'
+import { useI18n } from 'vue-i18n'
 
 const form = ref({
-    mail: '',
+    username: '',
     password: '',
     code: ''
 })
@@ -81,6 +90,33 @@ const form = ref({
 const checked = ref(false)
 
 const loginCode = ref(false)
+
+const router = useRouter()
+function goRegister() {
+    router.push({
+        name: 'register'
+    })
+}
+
+const { t } = useI18n()
+const { login } = useUserStore()
+function onSubmit() {
+    showLoadingToast({
+        message: t('loading'),
+        forbidClick: true,
+        duration: -1
+    })
+    login(form.value.username, form.value.password)
+        .then(res => {
+            closeToast()
+            showNotify({ type: 'success', message: t('login.loginsucess') })
+            router.back()
+        })
+        .catch(e => {
+            closeToast()
+            showNotify({ type: 'warning', message: e })
+        })
+}
 </script>
 
 <style lang="less" scoped>

+ 214 - 0
src/views/RegisterView.vue

@@ -0,0 +1,214 @@
+<template>
+    <div class="login">
+        <img src="@/assets/png-denglu-bg.png" class="top-bg !absolute w-full h-[200px] top-0 left-0 !z-0" alt="" />
+        <div class="login-content h-full">
+            <div class="text-white text-[26px] AlimamaShuHeiTi">Hi,歡迎來到走馬短劇</div>
+
+            <!-- 可以使用 CellGroup 作为容器 -->
+            <van-form @submit="onSubmit">
+                <van-cell-group :border="false">
+                    <van-field
+                        label-align="top"
+                        clearable
+                        v-model="form.email"
+                        :rules="[
+                            {
+                                required: true,
+                                pattern: /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/,
+                                message: '请输入正确内容'
+                            }
+                        ]"
+                        :label="$t('login.email')"
+                        :placeholder="$t('login.emailPla')"
+                    >
+                        <template #left-icon>
+                            <img class="left-icon" src="@/assets/login_icon_zhanghao.png" alt="" />
+                        </template>
+                        <template #label>
+                            <div class="text-white text-[14px]">{{ $t('login.email') }}</div>
+                        </template>
+                    </van-field>
+                    <van-field
+                        label-align="top"
+                        clearable
+                        v-model="form.username"
+                        :label="$t('login.username')"
+                        :placeholder="$t('login.usernamePla')"
+                        :rules="[
+                            {
+                                required: true,
+                                message: $t('login.usernamePla')
+                            }
+                        ]"
+                    >
+                        <template #left-icon>
+                            <img class="left-icon" src="@/assets/login_icon_zhanghao.png" alt="" />
+                        </template>
+                        <template #label>
+                            <div class="text-white text-[14px]">{{ $t('login.username') }}</div>
+                        </template>
+                    </van-field>
+
+                    <van-field
+                        label-align="top"
+                        type="password"
+                        clearable
+                        v-model="form.password"
+                        :label="$t('login.password')"
+                        :placeholder="$t('login.passwordPla')"
+                        :rules="[
+                            {
+                                required: true,
+                                message: $t('login.passwordPla')
+                            }
+                        ]"
+                    >
+                        <template #left-icon>
+                            <img class="left-icon" src="@/assets/login_icon_mima.png" alt="" />
+                        </template>
+                        <template #label>
+                            <div class="text-white text-[14px]">{{ $t('login.password') }}</div>
+                        </template>
+                    </van-field>
+
+                    <van-field
+                        label-align="top"
+                        type="password"
+                        clearable
+                        v-model="form.password2"
+                        label="密碼"
+                        :placeholder="$t('login.password2Pla')"
+                        :rules="[
+                            {
+                                validator,
+                                message: '两次密码输入不一致'
+                            }
+                        ]"
+                    >
+                        <template #left-icon>
+                            <img class="left-icon" src="@/assets/login_icon_mima.png" alt="" />
+                        </template>
+                        <template #label>
+                            <div class="text-white text-[14px]">{{ $t('login.password2') }}</div>
+                        </template>
+                    </van-field>
+
+                    <!-- <div @click="loginCode = !loginCode" class="text-[#61657A] text-right text-xs underline pt-[10px]">
+                    {{ loginCode ? '密碼登录' : '验证码登录' }}
+                </div> -->
+
+                    <div class="btn py-[64px] px-[17px]">
+                        <van-button type="primary" class="AlimamaShuHeiTi" block round native-type="submit"
+                            >注册</van-button
+                        >
+
+                        <div class="text-[#61657A] text-sm mt-[20px] text-center" @click="goLogin">
+                            已有帳號,立即登入
+                        </div>
+                    </div>
+                </van-cell-group>
+            </van-form>
+
+            <div class="rule flex justify-center van-safe-area-bottom">
+                <van-checkbox v-model="checked">點擊同意注册協定</van-checkbox>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { http } from '@/plugins/http'
+import { useUserStore } from '../stores/user'
+import { showNotify } from 'vant'
+import { useRouter } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+import { showLoadingToast, closeToast } from 'vant'
+
+const form = ref({
+    email: '',
+    username: '',
+    password: '',
+    password2: ''
+})
+
+const checked = ref(false)
+
+const loginCode = ref(false)
+
+function validator(val) {
+    return form.value.password === val
+}
+
+const router = useRouter()
+function goLogin() {
+    router.back()
+}
+const { t } = useI18n()
+
+const { register } = useUserStore()
+showLoadingToast({
+    message: t('loading'),
+    forbidClick: true
+})
+function onSubmit() {
+    register(form.value.email, form.value.username, form.value.password).then(res => {
+        closeToast()
+        showNotify({ type: 'success', message: t('login.register') })
+        router.go(-2)
+    })
+}
+</script>
+
+<style lang="less" scoped>
+.login {
+    .login-content {
+        position: relative;
+        z-index: 1;
+        padding: 30px;
+    }
+}
+
+.van-cell-group {
+    --van-cell-group-background: transparent;
+    --van-cell-background: transparent;
+    --van-cell-border-color: #4d61657a;
+    --van-field-placeholder-text-color: #61657a;
+    /deep/.van-field__label--top {
+        margin-bottom: 14px;
+    }
+
+    /deep/.van-cell__value {
+        line-height: 44px;
+    }
+    .van-cell {
+        --van-cell-value-color: #fff;
+        --van-cell-font-size: 14px;
+        --van-field-input-text-color: #fff;
+        --van-field-clear-icon-color: #404455;
+        padding: 40px 0 0;
+        &::after {
+            left: 0;
+            right: 0;
+        }
+    }
+
+    .left-icon {
+        width: 24px;
+        height: 24px;
+    }
+}
+
+.rule {
+    // position: fixed;
+    // bottom: 30px;
+    // left: 50%;
+    // transform: translateX(-50%);
+    .van-checkbox {
+        --van-checkbox-size: 16px;
+        --van-checkbox-border-color: #61657a;
+        --van-checkbox-label-color: #61657a;
+        font-size: 12px;
+    }
+}
+</style>

+ 73 - 0
src/views/VideoView.vue

@@ -0,0 +1,73 @@
+<template>
+    <div class="view van-safe-area-bottom">
+        <van-sticky @change="changeSticky">
+            <van-nav-bar :class="{ isFixed: isFixed }" title="第一集" left-text="" left-arrow @click-left="back" />
+        </van-sticky>
+
+        <div class="view-bottom flex justify-between z-20 items-center van-safe-area-bottom h-[49px] px-[18px]">
+            <span class="text-base text-white">{{ seriesInfo.title }}-第1集</span>
+            <van-button type="primary" class="btn" :icon="moreImg" icon-position="right" size="mini" round
+                >选集</van-button
+            >
+        </div>
+
+        <video-history></video-history>
+    </div>
+</template>
+
+<script setup>
+import { useRouter } from 'vue-router'
+import { ref } from 'vue'
+import moreImg from '@/assets/icon_inter_white.png'
+import VideoHistory from '../components/VideoHistory.vue'
+import { http } from '@/plugins/http'
+import { useRoute } from 'vue-router'
+
+const router = useRouter()
+function back() {
+    router.back()
+}
+
+const isFixed = ref(false)
+function changeSticky(fixed) {
+    isFixed.value = fixed
+}
+
+const route = useRoute()
+const seriesInfo = ref({})
+http.get('/api/series/' + route.query.id).then(res => {
+    seriesInfo.value = res
+})
+
+http.get('/api/episodes', {
+    seriesId: route.query.id,
+    orderBy: 'episodeNum',
+    order: 'episodeNum'
+})
+</script>
+
+<style lang="less" scoped>
+.van-nav-bar {
+    --van-nav-bar-background: transparent;
+    --van-border-width: 0;
+    --van-nav-bar-title-text-color: #fff;
+    --van-nav-bar-icon-color: #fff;
+
+    &.isFixed {
+        --van-nav-bar-background: #11131e;
+    }
+}
+
+.view-bottom {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
+
+    .btn {
+        --van-button-mini-font-size: 14px;
+        --van-button-mini-padding: 0 12px;
+        height: 26px;
+    }
+}
+</style>

+ 9 - 4
yarn.lock

@@ -180,7 +180,7 @@
 
 "@intlify/core-base@9.8.0":
   version "9.8.0"
-  resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.8.0.tgz#969ca59f55084e23e968ec0bfe71678774e568ec"
+  resolved "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.8.0.tgz"
   integrity sha512-UxaSZVZ1DwqC/CltUZrWZNaWNhfmKtfyV4BJSt/Zt4Or/fZs1iFj0B+OekYk1+MRHfIOe3+x00uXGQI4PbO/9g==
   dependencies:
     "@intlify/message-compiler" "9.8.0"
@@ -188,7 +188,7 @@
 
 "@intlify/message-compiler@9.8.0":
   version "9.8.0"
-  resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.8.0.tgz#587d69b302f9b8130a4a949b0ab4add519761787"
+  resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.8.0.tgz"
   integrity sha512-McnYWhcoYmDJvssVu6QGR0shqlkJuL1HHdi5lK7fNqvQqRYaQ4lSLjYmZxwc8tRNMdIe9/KUKfyPxU9M6yCtNQ==
   dependencies:
     "@intlify/shared" "9.8.0"
@@ -196,7 +196,7 @@
 
 "@intlify/shared@9.8.0":
   version "9.8.0"
-  resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.8.0.tgz#62adf8f6ef67c8eba6cf8d521e248f3503f237d3"
+  resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.8.0.tgz"
   integrity sha512-TmgR0RCLjzrSo+W3wT0ALf9851iFMlVI9EYNGeWvZFUQTAJx0bvfsMlPdgVtV1tDNRiAfhkFsMKu6jtUY1ZLKQ==
 
 "@jridgewell/gen-mapping@^0.3.2":
@@ -1435,6 +1435,11 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2:
   dependencies:
     brace-expansion "^1.1.7"
 
+mitt@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
+  integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
+
 ms@2.1.2, ms@^2.1.1:
   version "2.1.2"
   resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
@@ -2073,7 +2078,7 @@ vue-eslint-parser@^9.3.1:
 
 vue-i18n@^9.8.0:
   version "9.8.0"
-  resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.8.0.tgz#54339daf377a31b234b027c5158e774728b6bc24"
+  resolved "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.8.0.tgz"
   integrity sha512-Izho+6PYjejsTq2mzjcRdBZ5VLRQoSuuexvR8029h5CpN03FYqiqBrShMyf2I1DKkN6kw/xmujcbvC+4QybpsQ==
   dependencies:
     "@intlify/core-base" "9.8.0"