Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions app/components/dashboard/Logout.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
<script setup>
import { LogOut } from 'lucide-vue-next'
import { toast } from 'vue-sonner'

function logOut() {
localStorage.removeItem('SinkSiteToken')
navigateTo('/dashboard/login')
const { t } = useI18n()

async function logOut() {
try {
// 调用登出API
await useAPI('/api/auth/logout', {
method: 'POST',
})

// 清除本地存储的用户信息
localStorage.removeItem('SinkUser')

toast.success(t('logout.success'), {
description: t('logout.success_message'),
})

navigateTo('/dashboard/login')
} catch (e) {
console.error(e)
// 即使API调用失败,也清除本地状态并跳转
localStorage.removeItem('SinkUser')
navigateTo('/dashboard/login')
}
}
</script>

Expand Down
13 changes: 9 additions & 4 deletions app/components/dashboard/analysis/Counters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ async function getLinkCounters() {
...filters.value,
},
})
counters.value = data?.[0]
// 确保即使API返回数据不完整,所有字段也有默认值0
counters.value = {
visits: Number(data?.[0]?.visits) || 0,
visitors: Number(data?.[0]?.visitors) || 0,
referers: Number(data?.[0]?.referers) || 0,
}
}

watch([time, filters], getLinkCounters, {
Expand All @@ -45,7 +50,7 @@ onMounted(async () => {
<MousePointerClick class="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<NumberFlow class="text-2xl font-bold" :class="{ 'blur-md opacity-60': !counters.visits }" :value="counters.visits" />
<NumberFlow class="text-2xl font-bold" :value="counters.visits" />
</CardContent>
</Card>
<Card>
Expand All @@ -56,7 +61,7 @@ onMounted(async () => {
<Users class="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<NumberFlow class="text-2xl font-bold" :class="{ 'blur-md opacity-60': !counters.visitors }" :value="counters.visitors" />
<NumberFlow class="text-2xl font-bold" :value="counters.visitors" />
</CardContent>
</Card>
<Card>
Expand All @@ -67,7 +72,7 @@ onMounted(async () => {
<Flame class="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<NumberFlow class="text-2xl font-bold" :class="{ 'blur-md opacity-60': !counters.referers }" :value="counters.referers" />
<NumberFlow class="text-2xl font-bold" :value="counters.referers" />
</CardContent>
</Card>
</div>
Expand Down
27 changes: 10 additions & 17 deletions app/components/dashboard/links/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,16 @@ const dialogOpen = ref(false)

const isEdit = !!props.link.id

const EditLinkSchema = LinkSchema.pick({
url: true,
slug: true,
}).extend({
optional: LinkSchema.omit({
id: true,
url: true,
slug: true,
createdAt: true,
updatedAt: true,
title: true,
description: true,
image: true,
}).extend({
// 重新定义EditLinkSchema,确保不包含userId字段
const EditLinkSchema = z.object({
url: z.string().trim().url().max(2048),
slug: z.string().trim().max(2048).regex(new RegExp(useAppConfig().slugRegex)).default(nanoid()),
optional: z.object({
comment: z.string().trim().max(2048).optional(),
expiration: z.coerce.date().optional(),
title: z.string().trim().max(2048).optional(),
description: z.string().trim().max(2048).optional(),
image: z.string().trim().url().max(2048).optional(),
}).optional(),
})

Expand Down Expand Up @@ -85,9 +80,7 @@ async function aiSlug() {
aiSlugPending.value = true
try {
const { slug } = await useAPI('/api/link/ai', {
query: {
url: form.values.url,
},
query: { url: form.values.url },
})
form.setFieldValue('slug', slug)
}
Expand Down
15 changes: 10 additions & 5 deletions app/components/dashboard/links/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ let cursor = ''
let listComplete = false
let listError = false

const sortBy = ref('az')
const sortBy = ref('newest') // 默认按最新排序

const displayedLinks = computed(() => {
const sorted = [...links.value]
Expand All @@ -28,15 +28,20 @@ const displayedLinks = computed(() => {

async function getLinks() {
try {
const data = await useAPI('/api/link/list', {
// 调用获取当前用户链接的API
const data = await useAPI('/api/link/me', {
query: {
limit,
cursor,
},
})
links.value = links.value.concat(data.links).filter(Boolean) // Sometimes cloudflare will return null, filter out
cursor = data.cursor
listComplete = data.list_complete

// 添加新链接到列表中
links.value = links.value.concat(data.data || []).filter(Boolean)

// 更新分页信息
cursor = data.cursor || ''
listComplete = data.total <= links.value.length || !cursor
listError = false
}
catch (error) {
Expand Down
9 changes: 6 additions & 3 deletions app/components/dashboard/realtime/Chart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ async function getRealtimeStats() {
},
})

stats.value = data?.[0] || {}
// 确保visits字段有默认值0
stats.value = {
visits: Number(data?.[0]?.visits) || 0
}
}

watch([time, filters], getRealtimeStats, {
Expand All @@ -32,15 +35,15 @@ onMounted(async () => {
<template>
<Card class="md:w-80 h-72 flex flex-col p-4 md:m-2">
<div class="h-24">
<CardHeader v-if="stats.visits" class="flex flex-row justify-between items-center pb-2 space-y-0 px-0 pt-2">
<CardHeader class="flex flex-row justify-between items-center pb-2 space-y-0 px-0 pt-2">
<CardTitle class="text-sm font-medium flex items-center gap-2">
<span class="size-1.5 inline-flex animate-ping rounded-full bg-green-400 opacity-75" />
{{ $t('dashboard.visits') }}
</CardTitle>
<MousePointerClick class="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent class="px-0 pb-4">
<NumberFlow class="text-2xl font-bold" :class="{ 'blur-md opacity-60': !stats.visits }" :value="stats.visits" />
<NumberFlow class="text-2xl font-bold" :value="stats.visits" />
</CardContent>
</div>
<DashboardAnalysisViews
Expand Down
43 changes: 29 additions & 14 deletions app/components/login/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,49 @@
import { AlertCircle } from 'lucide-vue-next'
import { toast } from 'vue-sonner'
import { z } from 'zod'
// 导入UI组件
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { AutoForm } from '@/components/ui/auto-form'

const { t } = useI18n()

const LoginSchema = z.object({
token: z.string().describe('SiteToken'),
email: z.string().email(),
password: z.string().min(8),
})
const loginFieldConfig = {
token: {
email: {
inputProps: {
type: 'email',
placeholder: '[email protected]',
},
},
password: {
inputProps: {
type: 'password',
placeholder: '********',
},
},
}

const { previewMode } = useRuntimeConfig().public

async function onSubmit(form) {
try {
localStorage.setItem('SinkSiteToken', form.token)
await useAPI('/api/verify')
const response = await useAPI('/api/auth/login', {
method: 'POST',
body: form,
})

// 存储用户信息和token
localStorage.setItem('SinkUser', JSON.stringify(response.user))
localStorage.setItem('SinkSiteToken', response.token)

navigateTo('/dashboard')
}
catch (e) {
console.error(e)
toast.error(t('login.failed'), {
description: e.message,
description: e.message || t('login.invalid_credentials'),
})
}
}
Expand All @@ -51,16 +67,15 @@ async function onSubmit(form) {
:field-config="loginFieldConfig"
@submit="onSubmit"
>
<Alert v-if="previewMode">
<AlertCircle class="w-4 h-4" />
<AlertTitle>{{ $t('login.tips') }}</AlertTitle>
<AlertDescription>
{{ $t('login.preview_token') }} <code class="font-mono text-green-500">SinkCool</code> .
</AlertDescription>
</Alert>
<Button class="w-full">
{{ $t('login.submit') }}
</Button>
<div class="text-center text-sm text-muted-foreground">
{{ $t('login.dont_have_account') }}
<a href="/dashboard/register" class="text-primary hover:underline">
{{ $t('login.register_link') }}
</a>
</div>
</AutoForm>
</CardContent>
</Card>
Expand Down
115 changes: 115 additions & 0 deletions app/components/register/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<script setup>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use TypeScript in new Vue SFCs

Repository guideline: “Use TypeScript for all new code.” Switch the script to TS.

Apply this diff:

-<script setup>
+<script setup lang="ts">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<script setup>
<script setup lang="ts">
🤖 Prompt for AI Agents
In app/components/register/index.vue around line 1, the component uses a plain
<script setup> block but the repo requires TypeScript for new files; change the
block to <script setup lang="ts"> and update any local variables, props, emits
and refs to use proper TypeScript types (use
defineProps<Type>()/defineEmits<Type>() or typed refs/reactive state) and import
any needed types from Vue to satisfy the compiler; ensure the file compiles with
strict TS by fixing any implicit any types and adjusting imports/exports
accordingly.

import { AlertCircle } from 'lucide-vue-next'
import { toast } from 'vue-sonner'
import { z } from 'zod'
// 导入UI组件
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { AutoForm } from '@/components/ui/auto-form'

const { t } = useI18n()

const RegisterSchema = z.object({
username: z.string().min(3).max(50),
email: z.string().email(),
password: z.string().min(8),
})

const registerFieldConfig = {
username: {
inputProps: {
type: 'text',
placeholder: 'JohnDoe',
},
},
email: {
inputProps: {
type: 'email',
placeholder: '[email protected]',
},
},
password: {
inputProps: {
type: 'password',
placeholder: '********',
},
},
}

const { previewMode } = useRuntimeConfig().public

async function onSubmit(form) {
try {
const response = await useAPI('/api/auth/register', {
method: 'POST',
body: form,
})

// 注册成功后,自动登录用户
try {
// 调用登录API自动登录
const loginResponse = await useAPI('/api/auth/login', {
method: 'POST',
body: {
email: form.email,
password: form.password
},
})

// 存储用户信息和token
localStorage.setItem('SinkUser', JSON.stringify(loginResponse.user))
localStorage.setItem('SinkSiteToken', loginResponse.token)

toast.success(t('register.success'), {
description: t('register.success_and_logged_in'),
})

navigateTo('/dashboard')
} catch (loginError) {
// 如果自动登录失败,提示用户手动登录
toast.success(t('register.success'), {
description: t('register.success_manual_login'),
})

navigateTo('/dashboard/login')
}
}
catch (e) {
console.error(e)
toast.error(t('register.failed'), {
description: e.message,
})
}
}
</script>

<template>
<Card class="w-full max-w-sm">
<CardHeader>
<CardTitle class="text-2xl">
{{ $t('register.title') }}
</CardTitle>
<CardDescription>
{{ $t('register.description') }}
</CardDescription>
</CardHeader>
<CardContent class="grid gap-4">
<AutoForm
class="space-y-6"
:schema="RegisterSchema"
:field-config="registerFieldConfig"
@submit="onSubmit"
>
<Button class="w-full">
{{ $t('register.submit') }}
</Button>
<div class="text-center text-sm text-muted-foreground">
{{ $t('register.already_have_account') }}
<a href="/dashboard/login" class="text-primary hover:underline">
{{ $t('register.login_link') }}
</a>
</div>
</AutoForm>
</CardContent>
</Card>
</template>
2 changes: 1 addition & 1 deletion app/components/spark-ui/AnimatedList.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { computed, onMounted, ref, useSlots } from 'vue'
import { cn } from '@/lib/utils'
import { cn } from '@/utils'

const props = withDefaults(defineProps<{
class?: string
Expand Down
2 changes: 1 addition & 1 deletion app/components/spark-ui/Notification.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang='ts'>
import { cn } from '@/lib/utils'
import { cn } from '@/utils'

const props = defineProps<{
name: string
Expand Down
Loading