Skip to content
This repository was archived by the owner on Jan 7, 2026. It is now read-only.

Commit 5e59e13

Browse files
BobLiu0518Copilot
andauthored
feat: base implement (#1)
* feat: base implement * fix: fix css prop name error Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat: remove db schema * feat(db): transform undefined to null * feat: add example dotenv file * feat: use sha256 instead of uuidv4 for filename * feat: check login state in middleware Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat: add register field validation * feat: translate error message to chinese * feat: remove useless localstorage * refactor: use nuxt full-stack type safety function --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 2251683 commit 5e59e13

26 files changed

+1575
-59
lines changed

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DATABASE_URL=postgres://username:password@127.0.0.1:5432/database
2+
JWT_SECRET=your-secret-key
3+
FFMPEG_PATH=/usr/local/bin/ffmpeg
4+
FFPROBE_PATH=/usr/local/bin/ffprobe

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ logs
2222
.env
2323
.env.*
2424
!.env.example
25+
26+
# Uploaded files
27+
/public/uploads

.husky/commit-msg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1+
#!/bin/sh
2+
. "$(dirname "$0")/_/husky.sh"
3+
14
npx --no -- commitlint --edit "$1"

.husky/pre-commit

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1+
#!/bin/sh
2+
. "$(dirname "$0")/_/husky.sh"
3+
14
npx lint-staged

.prettierrc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Config } from 'prettier';
1+
import type { Config } from 'prettier';
22

33
const config: Config = {
44
printWidth: 100,

app/app.vue

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<template>
2-
<div>
3-
<NuxtRouteAnnouncer />
4-
<NuxtWelcome />
5-
</div>
2+
<NuxtLayout>
3+
<NuxtPage />
4+
</NuxtLayout>
65
</template>
6+
7+
<script setup lang="ts"></script>

app/assets/css/main.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
*,
2+
*::before,
3+
*::after {
4+
box-sizing: border-box;
5+
}
6+
7+
body {
8+
margin: 0;
9+
font-family:
10+
'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
11+
'微软雅黑', Arial, sans-serif;
12+
}

app/layouts/default.vue

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<template>
2+
<div class="common-layout">
3+
<el-container>
4+
<el-header>
5+
<el-menu mode="horizontal" :router="true" :ellipsis="false" class="header-menu">
6+
<el-menu-item index="/">
7+
<span class="logo">VideoHub</span>
8+
</el-menu-item>
9+
<div class="flex-grow" />
10+
<el-menu-item index="/">首页</el-menu-item>
11+
<el-menu-item index="/upload">上传视频</el-menu-item>
12+
<template v-if="!isLoggedIn">
13+
<el-menu-item index="/login">登录</el-menu-item>
14+
<el-menu-item index="/register">注册</el-menu-item>
15+
</template>
16+
<template v-else>
17+
<el-sub-menu index="user">
18+
<template #title>用户中心</template>
19+
<el-menu-item @click="logout">退出登录</el-menu-item>
20+
</el-sub-menu>
21+
</template>
22+
</el-menu>
23+
</el-header>
24+
<el-main>
25+
<slot />
26+
</el-main>
27+
</el-container>
28+
</div>
29+
</template>
30+
31+
<script setup>
32+
const token = useCookie('token');
33+
const isLoggedIn = computed(() => !!token.value);
34+
35+
const logout = () => {
36+
token.value = null;
37+
const user = useCookie('user');
38+
user.value = null;
39+
navigateTo('/login');
40+
};
41+
</script>
42+
43+
<style scoped>
44+
.el-header {
45+
padding: 0;
46+
}
47+
.header-menu {
48+
padding: 0 20px;
49+
align-items: center;
50+
}
51+
.logo {
52+
font-size: 20px;
53+
font-weight: bold;
54+
color: #409eff;
55+
}
56+
.flex-grow {
57+
flex-grow: 1;
58+
}
59+
.el-main {
60+
background-color: #f5f7fa;
61+
min-height: calc(100vh - 60px);
62+
}
63+
</style>

app/pages/index.vue

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<template>
2+
<div class="video-grid">
3+
<el-row :gutter="20">
4+
<el-col
5+
v-for="video in videos"
6+
:key="video.id"
7+
:xs="24"
8+
:sm="12"
9+
:md="8"
10+
:lg="6"
11+
class="video-col"
12+
>
13+
<el-card
14+
class="video-card"
15+
:body-style="{ padding: '0px' }"
16+
@click="navigateTo(`/video/${video.id}`)"
17+
>
18+
<div class="thumbnail-container">
19+
<img :src="video.thumbnail_url" class="thumbnail" />
20+
<div class="duration-tag">{{ formatDuration(video.duration) }}</div>
21+
</div>
22+
<div class="video-info">
23+
<h3 class="video-title">{{ video.title }}</h3>
24+
<div class="video-meta">
25+
<span class="uploader">{{ video.uploader }}</span>
26+
</div>
27+
</div>
28+
</el-card>
29+
</el-col>
30+
</el-row>
31+
</div>
32+
</template>
33+
34+
<script setup lang="ts">
35+
const { data: videos } = await useFetch('/api/videos');
36+
37+
const formatDuration = (seconds: number) => {
38+
const m = Math.floor(seconds / 60);
39+
const s = Math.floor(seconds % 60);
40+
return `${m}:${s.toString().padStart(2, '0')}`;
41+
};
42+
</script>
43+
44+
<style scoped>
45+
.video-grid {
46+
max-width: 1200px;
47+
margin: 0 auto;
48+
padding: 20px;
49+
}
50+
.video-col {
51+
margin-bottom: 20px;
52+
}
53+
.video-card {
54+
cursor: pointer;
55+
transition: transform 0.3s;
56+
border-radius: 8px;
57+
overflow: hidden;
58+
}
59+
.video-card:hover {
60+
transform: translateY(-5px);
61+
}
62+
.thumbnail-container {
63+
position: relative;
64+
width: 100%;
65+
padding-top: 56.25%; /* 16:9 Aspect Ratio */
66+
}
67+
.thumbnail {
68+
position: absolute;
69+
top: 0;
70+
left: 0;
71+
width: 100%;
72+
height: 100%;
73+
object-fit: cover;
74+
}
75+
.duration-tag {
76+
position: absolute;
77+
bottom: 8px;
78+
right: 8px;
79+
background: rgba(0, 0, 0, 0.7);
80+
color: white;
81+
padding: 2px 6px;
82+
border-radius: 4px;
83+
font-size: 12px;
84+
}
85+
.video-info {
86+
padding: 12px;
87+
}
88+
.video-title {
89+
margin: 0;
90+
font-size: 16px;
91+
font-weight: 600;
92+
line-height: 1.4;
93+
height: 2.8em;
94+
overflow: hidden;
95+
display: -webkit-box;
96+
-webkit-line-clamp: 2;
97+
-webkit-box-orient: vertical;
98+
}
99+
.video-meta {
100+
margin-top: 8px;
101+
font-size: 13px;
102+
color: #909399;
103+
}
104+
</style>

app/pages/login.vue

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<template>
2+
<div class="auth-container">
3+
<el-card class="auth-card">
4+
<template #header>
5+
<h2 class="auth-title">登录</h2>
6+
</template>
7+
<el-form label-position="top" @submit.prevent="handleLogin">
8+
<el-form-item label="用户名">
9+
<el-input v-model="username" placeholder="请输入用户名" />
10+
</el-form-item>
11+
<el-form-item label="密码">
12+
<el-input
13+
v-model="password"
14+
type="password"
15+
show-password
16+
placeholder="请输入密码"
17+
/>
18+
</el-form-item>
19+
<el-button
20+
type="primary"
21+
native-type="submit"
22+
class="submit-btn"
23+
:loading="loading"
24+
>
25+
登录
26+
</el-button>
27+
<div class="auth-footer">
28+
还没有账号? <NuxtLink to="/register">立即注册</NuxtLink>
29+
</div>
30+
</el-form>
31+
</el-card>
32+
</div>
33+
</template>
34+
35+
<script setup lang="ts">
36+
const username = ref('');
37+
const password = ref('');
38+
const loading = ref(false);
39+
40+
const handleLogin = async () => {
41+
loading.value = true;
42+
try {
43+
const { token, user } = await $fetch('/api/auth/login', {
44+
method: 'POST',
45+
body: { username: username.value, password: password.value },
46+
});
47+
48+
const tokenCookie = useCookie('token');
49+
const userCookie = useCookie<typeof user>('user');
50+
tokenCookie.value = token;
51+
userCookie.value = user;
52+
53+
ElMessage.success('登录成功');
54+
navigateTo('/');
55+
} catch (error: unknown) {
56+
const e = error as { data?: { statusMessage?: string } };
57+
ElMessage.error(e.data?.statusMessage || '登录失败');
58+
} finally {
59+
loading.value = false;
60+
}
61+
};
62+
</script>
63+
64+
<style scoped>
65+
.auth-container {
66+
display: flex;
67+
justify-content: center;
68+
align-items: center;
69+
padding-top: 60px;
70+
}
71+
.auth-card {
72+
width: 100%;
73+
max-width: 400px;
74+
border-radius: 8px;
75+
}
76+
.auth-title {
77+
margin: 0;
78+
text-align: center;
79+
font-size: 20px;
80+
}
81+
.submit-btn {
82+
width: 100%;
83+
margin-top: 10px;
84+
}
85+
.auth-footer {
86+
margin-top: 16px;
87+
text-align: center;
88+
font-size: 14px;
89+
color: #606266;
90+
}
91+
.auth-footer a {
92+
color: #409eff;
93+
text-decoration: none;
94+
}
95+
</style>

0 commit comments

Comments
 (0)