Merge upstream main into feat/500-api-key-ip-whitelist

This commit is contained in:
RWDai
2026-05-20 10:26:56 +08:00
501 changed files with 47013 additions and 3667 deletions
+32 -3
View File
@@ -132,6 +132,13 @@
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="text-sm font-medium text-foreground">{{ announcement.title }}</span>
<Badge
v-if="announcement.requires_ack"
variant="outline"
class="text-[10px] px-1.5 py-0"
>
必读
</Badge>
<Pin
v-if="announcement.is_pinned"
class="w-3.5 h-3.5 text-muted-foreground flex-shrink-0"
@@ -240,6 +247,13 @@
:class="getIconColor(announcement.type)"
/>
<span class="font-medium text-sm">{{ announcement.title }}</span>
<Badge
v-if="announcement.requires_ack"
variant="outline"
class="text-[10px] shrink-0"
>
必读
</Badge>
<Pin
v-if="announcement.is_pinned"
class="w-3.5 h-3.5 text-muted-foreground shrink-0"
@@ -433,6 +447,18 @@
class="cursor-pointer text-sm"
>置顶公告</Label>
</div>
<div class="flex items-center gap-2">
<input
id="requires-ack"
v-model="formData.requires_ack"
type="checkbox"
class="h-4 w-4 rounded border-gray-300 cursor-pointer"
>
<Label
for="requires-ack"
class="cursor-pointer text-sm"
>必读确认</Label>
</div>
<div
v-if="editingAnnouncement"
class="flex items-center gap-2"
@@ -611,7 +637,8 @@ const formData = ref({
type: 'info' as 'info' | 'warning' | 'maintenance' | 'important',
priority: 0,
is_pinned: false,
is_active: true
is_active: true,
requires_ack: false
})
onMounted(() => {
@@ -663,7 +690,8 @@ function openCreateDialog() {
type: 'info',
priority: 0,
is_pinned: false,
is_active: true
is_active: true,
requires_ack: false
}
dialogOpen.value = true
}
@@ -676,7 +704,8 @@ function openEditDialog(announcement: Announcement) {
type: announcement.type,
priority: announcement.priority,
is_pinned: announcement.is_pinned,
is_active: announcement.is_active
is_active: announcement.is_active,
requires_ack: !!announcement.requires_ack
}
dialogOpen.value = true
}
+155
View File
@@ -0,0 +1,155 @@
<template>
<div class="space-y-6 pb-8">
<div>
<h1 class="text-2xl font-semibold text-foreground">
我的邀请
</h1>
<p class="mt-1 text-sm text-muted-foreground">
分享邀请码后,符合规则的返利会进入赠款余额
</p>
</div>
<div
v-if="loading"
class="rounded-lg border border-border bg-card p-6 text-sm text-muted-foreground"
>
正在加载...
</div>
<template v-else-if="dashboard">
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<Card class="p-5">
<p class="text-xs text-muted-foreground">
总邀请
</p>
<p class="mt-2 text-2xl font-semibold">
{{ dashboard.summary.total_invites }}
</p>
</Card>
<Card class="p-5">
<p class="text-xs text-muted-foreground">
有效邀请
</p>
<p class="mt-2 text-2xl font-semibold">
{{ dashboard.summary.effective_invites }}
</p>
</Card>
<Card class="p-5">
<p class="text-xs text-muted-foreground">
已发返利
</p>
<p class="mt-2 text-2xl font-semibold">
{{ formatUsd(dashboard.summary.paid_reward_usd) }}
</p>
</Card>
</div>
<Card class="p-5">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-[240px_1fr]">
<div>
<Label class="text-xs text-muted-foreground">
邀请码
</Label>
<div class="mt-2 flex items-center gap-2">
<code class="rounded-lg border border-border bg-muted px-3 py-2 font-mono text-sm">
{{ dashboard.invite_code }}
</code>
<Button
type="button"
variant="outline"
size="sm"
@click="copyToClipboard(dashboard.invite_code)"
>
<Copy class="mr-2 h-4 w-4" />
复制
</Button>
</div>
</div>
<div>
<Label class="text-xs text-muted-foreground">
邀请链接
</Label>
<div class="mt-2 flex min-w-0 items-center gap-2">
<Input
:model-value="dashboard.invitation_link"
readonly
class="min-w-0"
/>
<Button
type="button"
variant="outline"
size="sm"
class="shrink-0"
@click="copyToClipboard(dashboard.invitation_link)"
>
<Copy class="mr-2 h-4 w-4" />
复制
</Button>
</div>
</div>
</div>
</Card>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<Card class="p-5">
<p class="text-xs text-muted-foreground">
待发返利
</p>
<p class="mt-2 text-xl font-semibold">
{{ formatUsd(dashboard.summary.pending_reward_usd) }}
</p>
</Card>
<Card class="p-5">
<p class="text-xs text-muted-foreground">
已冲回返利
</p>
<p class="mt-2 text-xl font-semibold">
{{ formatUsd(dashboard.summary.reversed_reward_usd) }}
</p>
</Card>
</div>
</template>
<div
v-else
class="rounded-lg border border-border bg-card p-6 text-sm text-muted-foreground"
>
邀请数据暂不可用
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { Copy } from 'lucide-vue-next'
import { referralApi, type ReferralDashboardResponse } from '@/api/referrals'
import { Button, Card, Input, Label } from '@/components/ui'
import { useClipboard } from '@/composables/useClipboard'
import { useToast } from '@/composables/useToast'
const dashboard = ref<ReferralDashboardResponse | null>(null)
const loading = ref(false)
const { copyToClipboard } = useClipboard()
const { error: showError } = useToast()
function formatUsd(value: number): string {
return `$${Number(value || 0).toFixed(2)}`
}
async function loadReferralDashboard() {
loading.value = true
try {
dashboard.value = await referralApi.getMyReferral()
} catch {
dashboard.value = null
showError('加载邀请数据失败')
} finally {
loading.value = false
}
}
onMounted(() => {
void loadReferralDashboard()
})
</script>
+1 -1
View File
@@ -387,7 +387,7 @@
<!-- eslint-disable vue/no-v-html -->
<div
class="oauth-icon shrink-0"
v-html="getOAuthIcon(p.provider_type)"
v-html="getOAuthIcon(p.provider_type, p.icon_url)"
/>
<!-- eslint-enable vue/no-v-html -->
<div class="min-w-0">