Compare commits

...

9 Commits

Author SHA1 Message Date
Tim
fa2ffaa64a fix: viditor样式失效 #586 2025-08-18 11:27:18 +08:00
Tim
3037c856d0 fix: viditor样式失效 #586 2025-08-18 11:27:13 +08:00
Tim
239f1f8c84 Merge pull request #617 from CH-122/fix/mobile-invite-ui
fix: 优化邀请链接组件样式,增加文本换行支持;调整积分商城页面内边距
2025-08-18 10:55:40 +08:00
CH-122
ac303184c4 fix: 优化邀请链接组件样式,增加文本换行支持;调整积分商城页面内边距 2025-08-18 10:32:55 +08:00
Tim
7f16bbdb94 Merge pull request #607 from nagisa77/feature/coin_store
支持积分商城 & 邀请码
2025-08-18 02:20:59 +08:00
tim
f1c83b0f68 fix: 更新提示 2025-08-18 02:19:43 +08:00
tim
22c2b1564d feat: ui 优化+弹窗 2025-08-18 02:18:04 +08:00
tim
628d28c12d feat: 注册流程重构 2025-08-18 02:06:48 +08:00
Tim
2577992ee3 Merge pull request #613 from nagisa77/codex/implement-invitation-link-functionality
feat: add invite link generation and copy
2025-08-18 01:24:05 +08:00
9 changed files with 90 additions and 20 deletions

View File

@@ -48,12 +48,13 @@ public class AuthController {
} }
if (req.getInviteToken() != null && !req.getInviteToken().isEmpty()) { if (req.getInviteToken() != null && !req.getInviteToken().isEmpty()) {
if (!inviteService.validate(req.getInviteToken())) { if (!inviteService.validate(req.getInviteToken())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token")); return ResponseEntity.badRequest().body(Map.of("error", "邀请码使用次数过多"));
} }
try { try {
User user = userService.registerWithInvite( User user = userService.registerWithInvite(
req.getUsername(), req.getEmail(), req.getPassword()); req.getUsername(), req.getEmail(), req.getPassword());
inviteService.consume(req.getInviteToken()); inviteService.consume(req.getInviteToken());
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"token", jwtService.generateToken(user.getUsername()), "token", jwtService.generateToken(user.getUsername()),
"reason_code", "INVITE_APPROVED" "reason_code", "INVITE_APPROVED"
@@ -78,10 +79,26 @@ public class AuthController {
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) { public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
boolean ok = userService.verifyCode(req.getUsername(), req.getCode()); boolean ok = userService.verifyCode(req.getUsername(), req.getCode());
if (ok) { if (ok) {
return ResponseEntity.ok(Map.of( Optional<User> userOpt = userService.findByUsername(req.getUsername());
"message", "Verified", if (userOpt.isEmpty()) {
"token", jwtService.generateReasonToken(req.getUsername()) return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials"));
)); }
User user = userOpt.get();
if (user.isApproved()) {
return ResponseEntity.ok(Map.of(
"message", "Verified and isApproved",
"reason_code", "VERIFIED_AND_APPROVED",
"token", jwtService.generateToken(req.getUsername())
));
} else {
return ResponseEntity.ok(Map.of(
"message", "Verified",
"reason_code", "VERIFIED",
"token", jwtService.generateReasonToken(req.getUsername())
));
}
} }
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code")); return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
} }

View File

@@ -77,7 +77,7 @@ public class UserService {
public User registerWithInvite(String username, String email, String password) { public User registerWithInvite(String username, String email, String password) {
User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT); User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT);
user.setVerified(true); user.setVerified(true);
user.setVerificationCode(null); user.setVerificationCode(genCode());
return userRepository.save(user); return userRepository.save(user);
} }

View File

@@ -8,6 +8,13 @@
/> />
<NotificationSettingPopup :visible="showNotificationPopup" @close="closeNotificationPopup" /> <NotificationSettingPopup :visible="showNotificationPopup" @close="closeNotificationPopup" />
<MedalPopup :visible="showMedalPopup" :medals="newMedals" @close="closeMedalPopup" /> <MedalPopup :visible="showMedalPopup" :medals="newMedals" @close="closeMedalPopup" />
<ActivityPopup
:visible="showInviteCodePopup"
:icon="inviteCodeIcon"
text="邀请码活动开始了,速来参与大伙们🔥🔥🔥"
@close="closeInviteCodePopup"
/>
</div> </div>
</template> </template>
@@ -21,7 +28,10 @@ const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl const API_BASE_URL = config.public.apiBaseUrl
const showMilkTeaPopup = ref(false) const showMilkTeaPopup = ref(false)
const showInviteCodePopup = ref(false)
const milkTeaIcon = ref('') const milkTeaIcon = ref('')
const inviteCodeIcon = ref('')
const showNotificationPopup = ref(false) const showNotificationPopup = ref(false)
const showMedalPopup = ref(false) const showMedalPopup = ref(false)
const newMedals = ref([]) const newMedals = ref([])
@@ -30,6 +40,9 @@ onMounted(async () => {
await checkMilkTeaActivity() await checkMilkTeaActivity()
if (showMilkTeaPopup.value) return if (showMilkTeaPopup.value) return
await checkInviteCodeActivity()
if (showInviteCodePopup.value) return
await checkNotificationSetting() await checkNotificationSetting()
if (showNotificationPopup.value) return if (showNotificationPopup.value) return
@@ -53,12 +66,38 @@ const checkMilkTeaActivity = async () => {
// ignore network errors // ignore network errors
} }
} }
const checkInviteCodeActivity = async () => {
if (!process.client) return
if (localStorage.getItem('inviteCodeActivityPopupShown')) return
try {
const res = await fetch(`${API_BASE_URL}/api/activities`)
if (res.ok) {
const list = await res.json()
const a = list.find((i) => i.type === 'INVITE_POINTS' && !i.ended)
if (a) {
inviteCodeIcon.value = a.icon
showInviteCodePopup.value = true
}
}
} catch (e) {
// ignore network errors
}
}
const closeInviteCodePopup = () => {
if (!process.client) return
localStorage.setItem('inviteCodeActivityPopupShown', 'true')
showInviteCodePopup.value = false
}
const closeMilkTeaPopup = () => { const closeMilkTeaPopup = () => {
if (!process.client) return if (!process.client) return
localStorage.setItem('milkTeaActivityPopupShown', 'true') localStorage.setItem('milkTeaActivityPopupShown', 'true')
showMilkTeaPopup.value = false showMilkTeaPopup.value = false
checkNotificationSetting() checkNotificationSetting()
} }
const checkNotificationSetting = async () => { const checkNotificationSetting = async () => {
if (!process.client) return if (!process.client) return
if (!authState.loggedIn) return if (!authState.loggedIn) return

View File

@@ -6,16 +6,16 @@
<span class="invite-code-description-title-text">邀请规则说明</span> <span class="invite-code-description-title-text">邀请规则说明</span>
</div> </div>
<div class="invite-code-description-content"> <div class="invite-code-description-content">
<p>邀请好友注册并登录每次可以获得500积分</p> <p>邀请好友注册并登录每次可以获得500积分🎉🎉🎉</p>
<p>邀请链接的有效期为1个月</p> <p>邀请链接的有效期为1个月</p>
<p>每一个邀请链接的邀请人数上限为3人</p> <p>每一个邀请链接的邀请人数上限为3人</p>
<p>通过邀请链接注册无需注册审核</p> <p>通过邀请链接注册无需注册审核</p>
<p>每人每天仅能生产3个邀请链接</p> <p>每人每天仅能生产1个邀请链接</p>
</div> </div>
</div> </div>
<div v-if="inviteLink" class="invite-code-link-content"> <div v-if="inviteLink" class="invite-code-link-content">
<p> <p class="invite-code-link-content-text">
邀请链接{{ inviteLink }} 邀请链接{{ inviteLink }}
<span @click="copyLink"><i class="fas fa-copy copy-icon"></i></span> <span @click="copyLink"><i class="fas fa-copy copy-icon"></i></span>
</p> </p>
@@ -48,9 +48,9 @@ onMounted(async () => {
isLoadingUser.value = true isLoadingUser.value = true
user.value = await fetchCurrentUser() user.value = await fetchCurrentUser()
isLoadingUser.value = false isLoadingUser.value = false
if (user.value) { // if (user.value) {
await fetchInvite(false) // await fetchInvite(false)
} // }
}) })
const fetchInvite = async (showToast = true) => { const fetchInvite = async (showToast = true) => {
@@ -171,6 +171,10 @@ const copyLink = async () => {
opacity: 0.8; opacity: 0.8;
} }
.invite-code-link-content-text {
word-break: break-all;
}
.copy-icon { .copy-icon {
cursor: pointer; cursor: pointer;
margin-left: 5px; margin-left: 5px;

View File

@@ -12,7 +12,6 @@ export default defineNuxtConfig({
twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '', twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '',
}, },
}, },
// 确保 Vditor 样式在 global.css 覆盖前加载
css: ['vditor/dist/index.css', '~/assets/fonts.css', '~/assets/global.css'], css: ['vditor/dist/index.css', '~/assets/fonts.css', '~/assets/global.css'],
app: { app: {
pageTransition: { name: 'page', mode: 'out-in' }, pageTransition: { name: 'page', mode: 'out-in' },

View File

@@ -145,7 +145,7 @@ onMounted(async () => {
} }
.activity-card-normal-right { .activity-card-normal-right {
width: calc(100% - 150px); width: 100%;
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {

View File

@@ -63,6 +63,7 @@ const pointRules = [
'评论:每天前四条评论可获 10 积分,你的帖子被评论也可获 10 积分', '评论:每天前四条评论可获 10 积分,你的帖子被评论也可获 10 积分',
'帖子被点赞:每次 10 积分', '帖子被点赞:每次 10 积分',
'评论被点赞:每次 10 积分', '评论被点赞:每次 10 积分',
'邀请好友加入可获得 500 积分/次,注意需要使用邀请链接注册',
] ]
const goods = ref([]) const goods = ref([])
@@ -128,7 +129,7 @@ const submitRedeem = async () => {
<style scoped> <style scoped>
.point-mall-page { .point-mall-page {
padding-left: 20px; padding: 0 20px;
max-width: var(--page-max-width); max-width: var(--page-max-width);
background-color: var(--background-color); background-color: var(--background-color);
margin: 0 auto; margin: 0 auto;

View File

@@ -96,6 +96,7 @@ import { discordAuthorize } from '~/utils/discord'
import { githubAuthorize } from '~/utils/github' import { githubAuthorize } from '~/utils/github'
import { googleAuthorize } from '~/utils/google' import { googleAuthorize } from '~/utils/google'
import { twitterAuthorize } from '~/utils/twitter' import { twitterAuthorize } from '~/utils/twitter'
import { loadCurrentUser, setToken } from '~/utils/auth'
const route = useRoute() const route = useRoute()
const config = useRuntimeConfig() const config = useRuntimeConfig()
@@ -160,6 +161,7 @@ const sendVerification = async () => {
username: username.value, username: username.value,
email: email.value, email: email.value,
password: password.value, password: password.value,
inviteToken: inviteToken.value,
}), }),
}) })
isWaitingForEmailSent.value = false isWaitingForEmailSent.value = false
@@ -192,11 +194,18 @@ const verifyCode = async () => {
}) })
const data = await res.json() const data = await res.json()
if (res.ok) { if (res.ok) {
if (registerMode.value === 'WHITELIST') { if (data.reason_code === 'VERIFIED_AND_APPROVED') {
navigateTo(`/signup-reason?token=${data.token}`, { replace: true }) toast.success('注册成功')
} else { setToken(data.token)
toast.success('注册成功,请登录') loadCurrentUser()
navigateTo('/login', { replace: true }) navigateTo('/', { replace: true })
} else if (data.reason_code === 'VERIFIED') {
if (registerMode.value === 'WHITELIST') {
navigateTo(`/signup-reason?token=${data.token}`, { replace: true })
} else {
toast.success('注册成功,请登录')
navigateTo('/login', { replace: true })
}
} }
} else { } else {
toast.error(data.error || '注册失败') toast.error(data.error || '注册失败')

View File

@@ -2,6 +2,7 @@ import Vditor from 'vditor'
import { getToken, authState } from './auth' import { getToken, authState } from './auth'
import { searchUsers, fetchFollowings, fetchAdmins } from './user' import { searchUsers, fetchFollowings, fetchAdmins } from './user'
import { tiebaEmoji } from './tiebaEmoji' import { tiebaEmoji } from './tiebaEmoji'
import '~/assets/global.css'
export function getEditorTheme() { export function getEditorTheme() {
return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'classic' return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'classic'