Compare commits

...

8 Commits

Author SHA1 Message Date
sugarBecomer d742425cac ✨ 混淆前端打包后代码 2026-03-27 16:38:42 +08:00
sugarBecomer 5729e88687 ✨ 混淆前端打包后代码 2026-03-27 16:37:14 +08:00
sugarBecomer 0681aad27d 🐛 修复生成邀请码 2026-03-27 16:32:22 +08:00
sugarBecomer ed38bfd463 🐛 修复邮件问题 2026-03-27 16:12:34 +08:00
sugarBecomer 1c38eccfd3 🐛 请假问题 2026-03-27 00:38:15 +08:00
sugarBecomer ac6b0fa81a ✨ graph api发送邮件 2026-03-26 15:19:42 +08:00
sugarBecomer 2603aada26 ✨ smtp服务 2026-03-26 11:37:38 +08:00
sugarBecomer 223ddf4f66 ✨ 新增是否必填邀请码 2026-03-25 14:49:11 +08:00
10 changed files with 1945 additions and 308 deletions
+1094
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -39,6 +39,7 @@
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"vite": "^8.0.1",
"vite-plugin-bundle-obfuscator": "^1.11.0",
"vue-tsc": "^3.2.5"
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { message } from '@/composables/useMessage'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 15000,
timeout: 60000,
})
// ---- Token 自动刷新 ----
+45
View File
@@ -132,3 +132,48 @@ export const systemPriceApi = {
update: (data: PriceConfig) =>
request.put<any, ApiResponse>('/v1/system/config/price', data),
}
// ---- SMTP 配置 ----
export interface SmtpConfig {
authMode: 'smtp' | 'outlook_oauth2'
smtpHost: string
smtpPort: number
smtpUsername: string
smtpPassword: string
fromEmail: string
fromName: string
oauthTenant: string
oauthClientId: string
oauthClientSecret: string
oauthRedirectUrl: string
oauthRefreshToken: string
oauthScope: string
}
export interface OutlookOAuthAuthorizeURLReq {
oauthTenant: string
oauthClientId: string
oauthClientSecret: string
oauthScope: string
fromEmail: string
frontendOrigin: string
callbackBaseUrl: string
}
export interface OutlookOAuthAuthorizeURLResp {
authUrl: string
callbackUrl: string
state: string
}
export const systemSmtpApi = {
get: () =>
request.get<any, ApiResponse<SmtpConfig>>('/v1/system/config/smtp'),
update: (data: SmtpConfig) =>
request.put<any, ApiResponse>('/v1/system/config/smtp', data),
test: (toEmail: string) =>
request.post<any, ApiResponse>('/v1/system/config/smtp/test', { toEmail }),
getOutlookAuthorizeUrl: (data: OutlookOAuthAuthorizeURLReq) =>
request.post<any, ApiResponse<OutlookOAuthAuthorizeURLResp>>('/v1/system/config/smtp/oauth/outlook/authorize-url', data),
}
+4
View File
@@ -31,4 +31,8 @@ export const userApi = {
/** 获取余额变动记录 */
getBalanceRecords: (params: PageParams & { type?: string }) =>
request.get<any, ApiResponse<PageResponse<BalanceRecord>>>('/v1/user/balance/records', { params }),
/** 生成邀请码 */
generateInviteCode: () =>
request.post<any, ApiResponse>('/v1/user/generateInviteCode'),
}
File diff suppressed because it is too large Load Diff
+20 -10
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
@@ -13,20 +13,23 @@ import CaptchaDialog from '@/components/login/CaptchaDialog.vue'
const router = useRouter()
const allowRegister = ref(true)
const requireInviteCode = ref(false)
const checking = ref(true)
const schema = toTypedSchema(
const schema = computed(() => toTypedSchema(
z.object({
email: z.string().email('请输入有效邮箱').regex(/^\d+@qq\.com$/i, '仅支持QQ邮箱注册'),
username: z.string().min(2, '用户名至少2个字符').max(20, '用户名最多20个字符'),
password: z.string().min(6, '密码至少6个字符').max(20, '密码最多20个字符'),
confirmPassword: z.string().min(1, '请确认密码'),
inviteCode: z.string().optional(),
inviteCode: requireInviteCode.value
? z.string().min(1, '请输入邀请码')
: z.string().optional(),
}).refine((d) => d.password === d.confirmPassword, {
message: '两次密码不一致',
path: ['confirmPassword'],
})
)
))
const { handleSubmit, errors, defineField } = useForm({ validationSchema: schema })
@@ -45,13 +48,19 @@ const resendCaptchaOpen = ref(false)
onMounted(async () => {
try {
const res = await authApi.getPublicConfig('system.allowRegister')
if (res.code === 200 && res.data) {
allowRegister.value = res.data.value === 'true' || res.data.value === '1'
const [regRes, invRes] = await Promise.all([
authApi.getPublicConfig('system.allowRegister'),
authApi.getPublicConfig('system.requireInviteCode'),
])
if (regRes.code === 200 && regRes.data) {
allowRegister.value = regRes.data.value === 'true' || regRes.data.value === '1'
}
if (invRes.code === 200 && invRes.data) {
requireInviteCode.value = invRes.data.value === 'true' || invRes.data.value === '1'
}
} catch {
// 获取失败默认允许注册
allowRegister.value = true
requireInviteCode.value = false
} finally {
checking.value = false
}
@@ -158,8 +167,9 @@ async function onResendCaptchaConfirm(result: { captchaKey: string; dots: Array<
<p v-if="errors.confirmPassword" class="text-sm text-destructive">{{ errors.confirmPassword }}</p>
</div>
<div class="space-y-2">
<Label for="inviteCode">邀请码(可选)</Label>
<Input id="inviteCode" v-model="inviteCode" v-bind="inviteCodeAttrs" placeholder="如有邀请码请输入" />
<Label for="inviteCode">邀请码{{ requireInviteCode ? '' : '(可选)' }}</Label>
<Input id="inviteCode" v-model="inviteCode" v-bind="inviteCodeAttrs" :placeholder="requireInviteCode ? '请输入邀请码' : '如有邀请码请输入'" />
<p v-if="errors.inviteCode" class="text-sm text-destructive">{{ errors.inviteCode }}</p>
</div>
<Button type="submit" class="w-full" :disabled="submitting">
+331 -38
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { message as toast } from '@/composables/useMessage'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -8,9 +8,19 @@ import { Switch } from '@/components/ui/switch'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { MdEditor } from 'md-editor-v3'
import 'md-editor-v3/lib/style.css'
import { systemConfigApi, systemPriceApi, type PriceConfig } from '@/api/system'
import { Separator } from '@/components/ui/separator'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { systemConfigApi, systemPriceApi, systemSmtpApi, type PriceConfig, type SmtpConfig } from '@/api/system'
const defaultOAuthScope = 'offline_access https://graph.microsoft.com/Mail.Send'
const defaultOAuthRedirectUrl = 'http://localhost:3200/api/v1/mail/oauth/outlook/callback'
// ---- 公告 ----
const announcementLoading = ref(false)
const announcementSaving = ref(false)
const announcementContent = ref('')
@@ -23,8 +33,6 @@ async function fetchAnnouncement() {
const val = res.data?.value ?? ''
announcementContent.value = val
originalAnnouncement.value = val
} catch {
// 全局拦截器已处理错误提示
} finally {
announcementLoading.value = false
}
@@ -41,10 +49,8 @@ async function saveAnnouncement() {
value: announcementContent.value,
description: '系统公告',
})
toast.success('公告保存成功')
originalAnnouncement.value = announcementContent.value
} catch {
// 全局拦截器已处理错误提示
toast.success('公告保存成功')
} finally {
announcementSaving.value = false
}
@@ -55,19 +61,23 @@ function resetAnnouncement() {
toast.info('已重置')
}
// ---- 注册开关 ----
const registerLoading = ref(false)
const registerSaving = ref(false)
const allowRegister = ref(true)
const requireInviteCode = ref(false)
async function fetchRegisterConfig() {
registerLoading.value = true
try {
const res = await systemConfigApi.get('system.allowRegister')
const val = res.data?.value
allowRegister.value = val === 'true' || val === '1'
const [regRes, invRes] = await Promise.all([
systemConfigApi.get('system.allowRegister'),
systemConfigApi.get('system.requireInviteCode').catch(() => null),
])
allowRegister.value = ['true', '1'].includes(regRes.data?.value ?? '')
requireInviteCode.value = ['true', '1'].includes(invRes?.data?.value ?? '')
} catch {
allowRegister.value = true
requireInviteCode.value = false
} finally {
registerLoading.value = false
}
@@ -76,19 +86,22 @@ async function fetchRegisterConfig() {
async function saveRegisterConfig() {
registerSaving.value = true
try {
await systemConfigApi.update('system.allowRegister', {
value: allowRegister.value ? 'true' : 'false',
description: '是否允许用户注册',
})
toast.success('注册开关保存成功')
} catch {
// 全局拦截器已处理错误提示
await Promise.all([
systemConfigApi.update('system.allowRegister', {
value: allowRegister.value ? 'true' : 'false',
description: '是否允许用户注册',
}),
systemConfigApi.update('system.requireInviteCode', {
value: requireInviteCode.value ? 'true' : 'false',
description: '注册是否必须邀请码',
}),
])
toast.success('注册配置保存成功')
} finally {
registerSaving.value = false
}
}
// ---- 费率配置 ----
const priceLoading = ref(false)
const priceSaving = ref(false)
const priceConfig = ref<PriceConfig>({
@@ -106,8 +119,6 @@ async function fetchPriceConfig() {
if (res.data) {
priceConfig.value = { ...priceConfig.value, ...res.data }
}
} catch {
// 使用默认值
} finally {
priceLoading.value = false
}
@@ -118,17 +129,193 @@ async function savePriceConfig() {
try {
await systemPriceApi.update(priceConfig.value)
toast.success('费率配置保存成功')
} catch {
// 全局拦截器已处理错误提示
} finally {
priceSaving.value = false
}
}
const smtpLoading = ref(false)
const smtpSaving = ref(false)
const smtpTesting = ref(false)
const oauthAuthorizing = ref(false)
const testEmail = ref('')
const smtpConfig = ref<SmtpConfig>({
authMode: 'smtp',
smtpHost: '',
smtpPort: 587,
smtpUsername: '',
smtpPassword: '',
fromEmail: '',
fromName: '',
oauthTenant: 'consumers',
oauthClientId: '',
oauthClientSecret: '',
oauthRedirectUrl: '',
oauthRefreshToken: '',
oauthScope: defaultOAuthScope,
})
const isOAuthMode = computed(() => smtpConfig.value.authMode === 'outlook_oauth2')
function resolveApiOrigin() {
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || '/api'
return new URL(apiBaseUrl, window.location.origin).origin
}
function applyModeDefaults(mode: SmtpConfig['authMode']) {
if (mode === 'outlook_oauth2') {
smtpConfig.value.oauthTenant = 'consumers'
smtpConfig.value.oauthScope = defaultOAuthScope
smtpConfig.value.oauthRedirectUrl = defaultOAuthRedirectUrl
}
}
async function fetchSmtpConfig() {
smtpLoading.value = true
try {
const res = await systemSmtpApi.get()
if (res.data) {
smtpConfig.value = { ...smtpConfig.value, ...res.data }
smtpConfig.value.smtpPort = Number(smtpConfig.value.smtpPort) || 587
applyModeDefaults(smtpConfig.value.authMode)
}
} finally {
smtpLoading.value = false
}
}
function buildSmtpPayload(): SmtpConfig {
const payload: SmtpConfig = { ...smtpConfig.value }
if (payload.authMode === 'outlook_oauth2') {
payload.oauthTenant = 'consumers'
payload.oauthScope = defaultOAuthScope
payload.oauthRedirectUrl = defaultOAuthRedirectUrl
}
return payload
}
async function saveSmtpConfig() {
if (!smtpConfig.value.fromEmail || !smtpConfig.value.fromName) {
toast.error('请先填写发件人信息')
return
}
if (isOAuthMode.value) {
if (!smtpConfig.value.oauthClientId || !smtpConfig.value.oauthClientSecret || !smtpConfig.value.oauthRefreshToken) {
toast.error('Outlook OAuth2 模式下请先完成授权或手动填写 Refresh Token')
return
}
} else {
if (!smtpConfig.value.smtpHost || !smtpConfig.value.smtpUsername || !smtpConfig.value.smtpPassword) {
toast.error('SMTP 模式下请填写服务器、账号和密码')
return
}
}
smtpSaving.value = true
try {
await systemSmtpApi.update(buildSmtpPayload())
toast.success('邮件配置保存成功')
} finally {
smtpSaving.value = false
}
}
async function startOutlookAuthorize() {
if (!smtpConfig.value.fromEmail) {
toast.error('请先填写发件人邮箱')
return
}
if (!smtpConfig.value.oauthClientId || !smtpConfig.value.oauthClientSecret) {
toast.error('请先填写 Client ID 和 Client Secret')
return
}
oauthAuthorizing.value = true
try {
const res = await systemSmtpApi.getOutlookAuthorizeUrl({
oauthTenant: 'consumers',
oauthClientId: smtpConfig.value.oauthClientId,
oauthClientSecret: smtpConfig.value.oauthClientSecret,
oauthScope: defaultOAuthScope,
fromEmail: smtpConfig.value.fromEmail,
frontendOrigin: window.location.origin,
callbackBaseUrl: defaultOAuthRedirectUrl,
})
const authUrl = res.data?.authUrl
const callbackUrl = res.data?.callbackUrl
if (!authUrl || !callbackUrl) {
toast.error('未能生成授权地址')
return
}
smtpConfig.value.oauthRedirectUrl = callbackUrl
const popup = window.open(authUrl, 'outlook-oauth2', 'width=720,height=820')
if (!popup) {
toast.error('浏览器拦截了弹窗,请允许后重试')
return
}
const result = await new Promise<{ success: boolean; message?: string; refreshToken?: string }>((resolve, reject) => {
const timer = window.setInterval(() => {
if (popup.closed) {
window.clearInterval(timer)
window.removeEventListener('message', onMessage)
reject(new Error('授权窗口已关闭'))
}
}, 500)
function onMessage(event: MessageEvent) {
const data = event.data
if (event.origin !== resolveApiOrigin() || !data || data.source !== 'likedream-outlook-oauth') {
return
}
window.clearInterval(timer)
window.removeEventListener('message', onMessage)
resolve(data)
}
window.addEventListener('message', onMessage)
})
if (!result.success) {
toast.error(result.message || '授权失败')
return
}
if (result.refreshToken) {
smtpConfig.value.oauthRefreshToken = result.refreshToken
}
await fetchSmtpConfig()
smtpConfig.value.oauthRedirectUrl = defaultOAuthRedirectUrl
toast.success('Outlook 授权成功,Refresh Token 已保存并刷新显示')
} catch (error: any) {
toast.error(error?.message || '获取 Outlook 授权失败')
} finally {
oauthAuthorizing.value = false
}
}
async function testSmtp() {
if (!testEmail.value.trim()) {
toast.error('请输入测试收件邮箱')
return
}
smtpTesting.value = true
try {
await systemSmtpApi.test(testEmail.value.trim())
toast.success('测试邮件发送成功')
} finally {
smtpTesting.value = false
}
}
onMounted(() => {
fetchAnnouncement()
fetchRegisterConfig()
fetchPriceConfig()
fetchSmtpConfig()
})
</script>
@@ -136,21 +323,15 @@ onMounted(() => {
<div class="space-y-6">
<h2 class="text-2xl font-bold tracking-tight">系统配置</h2>
<!-- 公告编辑 -->
<Card>
<CardHeader>
<CardTitle class="text-base">系统公告</CardTitle>
</CardHeader>
<CardContent class="space-y-3">
<div v-if="announcementLoading" class="text-sm text-muted-foreground py-4 text-center">加载中...</div>
<div v-if="announcementLoading" class="py-4 text-center text-sm text-muted-foreground">加载中...</div>
<template v-else>
<MdEditor
v-model="announcementContent"
language="zh-CN"
:preview="true"
style="height: 500px"
/>
<div class="flex justify-end gap-2 mt-3">
<MdEditor v-model="announcementContent" language="zh-CN" :preview="true" style="height: 500px" />
<div class="mt-3 flex justify-end gap-2">
<Button variant="outline" size="sm" @click="resetAnnouncement">重置</Button>
<Button size="sm" :disabled="announcementSaving" @click="saveAnnouncement">
{{ announcementSaving ? '保存中...' : '保存公告' }}
@@ -160,20 +341,24 @@ onMounted(() => {
</CardContent>
</Card>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 注册开关 -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle class="text-base">注册开关配置</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div v-if="registerLoading" class="text-sm text-muted-foreground py-4 text-center">加载中...</div>
<div v-if="registerLoading" class="py-4 text-center text-sm text-muted-foreground">加载中...</div>
<template v-else>
<div class="flex items-center gap-3">
<Label>允许用户注册</Label>
<Switch v-model="allowRegister" />
<span class="text-sm text-muted-foreground">{{ allowRegister ? '已开启' : '已关闭' }}</span>
</div>
<div class="flex items-center gap-3">
<Label>注册必须邀请码</Label>
<Switch v-model="requireInviteCode" />
<span class="text-sm text-muted-foreground">{{ requireInviteCode ? '已开启' : '已关闭' }}</span>
</div>
<div class="flex justify-end">
<Button size="sm" :disabled="registerSaving" @click="saveRegisterConfig">
{{ registerSaving ? '保存中...' : '保存配置' }}
@@ -183,13 +368,12 @@ onMounted(() => {
</CardContent>
</Card>
<!-- 费率配置 -->
<Card>
<CardHeader>
<CardTitle class="text-base">费率配置</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div v-if="priceLoading" class="text-sm text-muted-foreground py-4 text-center">加载中...</div>
<div v-if="priceLoading" class="py-4 text-center text-sm text-muted-foreground">加载中...</div>
<template v-else>
<div class="space-y-3">
<div class="flex items-center gap-3">
@@ -227,5 +411,114 @@ onMounted(() => {
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle class="text-base">邮件配置</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div v-if="smtpLoading" class="py-4 text-center text-sm text-muted-foreground">加载中...</div>
<template v-else>
<div class="grid max-w-3xl grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label>发信模式</Label>
<Select
:model-value="smtpConfig.authMode"
@update:model-value="(value) => { smtpConfig.authMode = value as SmtpConfig['authMode']; applyModeDefaults(smtpConfig.authMode) }"
>
<SelectTrigger>
<SelectValue placeholder="选择邮件发信模式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="smtp">SMTP 密码 / 授权码</SelectItem>
<SelectItem value="outlook_oauth2">Outlook OAuth2</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>发件人邮箱</Label>
<Input v-model="smtpConfig.fromEmail" placeholder="例如 you@outlook.com" />
</div>
<div class="space-y-2">
<Label>发件人名称</Label>
<Input v-model="smtpConfig.fromName" placeholder="邮件中显示的发件人名称" />
</div>
</div>
<div v-if="!isOAuthMode" class="grid max-w-3xl grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label>SMTP 服务器</Label>
<Input v-model="smtpConfig.smtpHost" placeholder="例如 smtp.qq.com" />
</div>
<div class="space-y-2">
<Label>SMTP 端口</Label>
<Input v-model.number="smtpConfig.smtpPort" type="number" placeholder="例如 587" />
</div>
<div class="space-y-2">
<Label>SMTP 登录账号</Label>
<Input v-model="smtpConfig.smtpUsername" placeholder="SMTP 登录账号" />
</div>
<div class="space-y-2">
<Label>SMTP 密码 / 授权码</Label>
<Input v-model="smtpConfig.smtpPassword" type="password" placeholder="SMTP 登录密码或授权码" />
</div>
</div>
<div v-else class="space-y-4">
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
<p class="font-medium">Outlook OAuth2 + Microsoft Graph 模式</p>
<p class="mt-1">
OAuth2 模式下系统不会再走 SMTP,而是直接调用 <code>Microsoft Graph /me/sendMail</code>,
因此不会再显示 SMTP 服务器、端口和账号输入项,邮件也会以当前完成授权的微软账号身份发送。
</p>
</div>
<div class="grid max-w-3xl grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label>Client ID</Label>
<Input v-model="smtpConfig.oauthClientId" placeholder="Microsoft Entra 应用的 Client ID" />
</div>
<div class="space-y-2">
<Label>Client Secret</Label>
<Input v-model="smtpConfig.oauthClientSecret" type="password" placeholder="应用机密" />
</div>
<div class="space-y-2 md:col-span-2">
<Label>回调地址</Label>
<Input v-model="smtpConfig.oauthRedirectUrl" readonly placeholder="点击下方授权按钮后自动生成" />
</div>
<div class="space-y-2 md:col-span-2">
<Label>Refresh Token</Label>
<Input v-model="smtpConfig.oauthRefreshToken" type="password" placeholder="点击下方按钮自动回填,或手动粘贴" />
</div>
</div>
<div class="flex max-w-3xl items-center gap-3">
<Button variant="outline" :disabled="oauthAuthorizing" @click="startOutlookAuthorize">
{{ oauthAuthorizing ? '授权中...' : '一键授权并回填 Refresh Token' }}
</Button>
<span class="text-sm text-muted-foreground">会弹出微软登录窗口,固定使用 <code>consumers</code> 与 <code>Mail.Send</code> 授权并在完成后自动回填。</span>
</div>
</div>
<Separator />
<div class="flex max-w-3xl items-end gap-3">
<div class="flex-1 space-y-2">
<Label>测试收件邮箱</Label>
<Input v-model="testEmail" placeholder="输入一个邮箱地址发送测试邮件" />
</div>
<Button variant="outline" size="sm" :disabled="smtpTesting" @click="testSmtp">
{{ smtpTesting ? '发送中...' : '发送测试' }}
</Button>
</div>
<div class="flex max-w-3xl justify-end">
<Button size="sm" :disabled="smtpSaving" @click="saveSmtpConfig">
{{ smtpSaving ? '保存中...' : '保存配置' }}
</Button>
</div>
</template>
</CardContent>
</Card>
</div>
</template>
+26 -2
View File
@@ -12,12 +12,13 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { User, Lock, CreditCard, Copy, Users } from 'lucide-vue-next'
import { User, Lock, CreditCard, Copy, Users, RefreshCw } from 'lucide-vue-next'
const userStore = useUserStore()
const passwordLoading = ref(false)
const rechargeLoading = ref(false)
const cardCode = ref('')
const generateLoading = ref(false)
// 修改密码表单
const passwordSchema = toTypedSchema(
@@ -88,6 +89,20 @@ async function copyInviteCode() {
}
}
// 生成邀请码
async function handleGenerateInviteCode() {
generateLoading.value = true
try {
await userApi.generateInviteCode()
toast.success('邀请码生成成功')
userStore.fetchProfile()
} catch {
// 全局拦截器已处理错误提示
} finally {
generateLoading.value = false
}
}
onMounted(() => {
if (!userStore.profile) {
userStore.fetchProfile()
@@ -247,8 +262,17 @@ onMounted(() => {
<div class="space-y-4 max-w-md">
<div class="flex items-center gap-3">
<code class="flex-1 rounded-md border bg-muted px-4 py-2.5 text-sm font-mono">
{{ userStore.profile?.invite_code ?? '-' }}
{{ userStore.profile?.invite_code ?? '暂无邀请码' }}
</code>
<Button
variant="outline"
size="sm"
:disabled="generateLoading"
@click="handleGenerateInviteCode"
>
<RefreshCw class="mr-1.5 h-3.5 w-3.5" :class="{ 'animate-spin': generateLoading }" />
生成
</Button>
<Button
variant="outline"
size="sm"
+42 -1
View File
@@ -2,9 +2,50 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
import { fileURLToPath, URL } from 'node:url'
import vitePluginBundleObfuscator from 'vite-plugin-bundle-obfuscator'
export default defineConfig({
plugins: [vue(), tailwindcss()],
plugins: [
vue(),
tailwindcss(),
vitePluginBundleObfuscator({
apply: 'build',
log: true,
autoExcludeNodeModules: true,
threadPool: true,
options: {
compact: true,
controlFlowFlattening: true,
controlFlowFlatteningThreshold: 1,
deadCodeInjection: false,
debugProtection: false,
debugProtectionInterval: 0,
disableConsoleOutput: true,
identifierNamesGenerator: 'hexadecimal',
ignoreImports: true,
numbersToExpressions: true,
renameGlobals: false,
selfDefending: true,
simplify: true,
splitStrings: true,
splitStringsChunkLength: 5,
stringArray: true,
stringArrayCallsTransform: true,
stringArrayCallsTransformThreshold: 1,
stringArrayEncoding: ['base64'],
stringArrayIndexShift: true,
stringArrayRotate: true,
stringArrayShuffle: true,
stringArrayWrappersChainedCalls: true,
stringArrayWrappersCount: 3,
stringArrayWrappersParametersMaxCount: 4,
stringArrayWrappersType: 'function',
stringArrayThreshold: 1,
transformObjectKeys: true,
unicodeEscapeSequence: false,
},
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))