mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-08 16:11:05 +08:00
Compare commits
1 Commits
codex/upda
...
codex/add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10ae877b3b |
@@ -1,31 +0,0 @@
|
||||
package com.openisle.config;
|
||||
|
||||
import com.openisle.model.PointGood;
|
||||
import com.openisle.repository.PointGoodRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Initialize default point mall goods. */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PointGoodInitializer implements CommandLineRunner {
|
||||
private final PointGoodRepository pointGoodRepository;
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
if (pointGoodRepository.count() == 0) {
|
||||
PointGood g1 = new PointGood();
|
||||
g1.setName("GPT Plus 1 个月");
|
||||
g1.setCost(20000);
|
||||
g1.setImage("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/chatgpt.png");
|
||||
pointGoodRepository.save(g1);
|
||||
|
||||
PointGood g2 = new PointGood();
|
||||
g2.setName("奶茶");
|
||||
g2.setCost(5000);
|
||||
g2.setImage("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/coffee.png");
|
||||
pointGoodRepository.save(g2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.dto.PointGoodDto;
|
||||
import com.openisle.dto.PointRedeemRequest;
|
||||
import com.openisle.mapper.PointGoodMapper;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.service.PointMallService;
|
||||
import com.openisle.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** REST controller for point mall. */
|
||||
@RestController
|
||||
@RequestMapping("/api/point-goods")
|
||||
@RequiredArgsConstructor
|
||||
public class PointMallController {
|
||||
private final PointMallService pointMallService;
|
||||
private final UserService userService;
|
||||
private final PointGoodMapper pointGoodMapper;
|
||||
|
||||
@GetMapping
|
||||
public List<PointGoodDto> list() {
|
||||
return pointMallService.listGoods().stream()
|
||||
.map(pointGoodMapper::toDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/redeem")
|
||||
public Map<String, Integer> redeem(@RequestBody PointRedeemRequest req, Authentication auth) {
|
||||
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
|
||||
int point = pointMallService.redeem(user, req.getGoodId(), req.getContact());
|
||||
return Map.of("point", point);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.openisle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Point mall good info. */
|
||||
@Data
|
||||
public class PointGoodDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private int cost;
|
||||
private String image;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.openisle.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Request to redeem a point mall good. */
|
||||
@Data
|
||||
public class PointRedeemRequest {
|
||||
private Long goodId;
|
||||
private String contact;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.openisle.mapper;
|
||||
|
||||
import com.openisle.dto.PointGoodDto;
|
||||
import com.openisle.model.PointGood;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Mapper for point mall goods. */
|
||||
@Component
|
||||
public class PointGoodMapper {
|
||||
public PointGoodDto toDto(PointGood good) {
|
||||
PointGoodDto dto = new PointGoodDto();
|
||||
dto.setId(good.getId());
|
||||
dto.setName(good.getName());
|
||||
dto.setCost(good.getCost());
|
||||
dto.setImage(good.getImage());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.openisle.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/** Item available in the point mall. */
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Table(name = "point_goods")
|
||||
public class PointGood {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int cost;
|
||||
|
||||
private String image;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.openisle.repository;
|
||||
|
||||
import com.openisle.model.PointGood;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/** Repository for point mall goods. */
|
||||
public interface PointGoodRepository extends JpaRepository<PointGood, Long> {
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.openisle.service;
|
||||
|
||||
import com.openisle.exception.FieldException;
|
||||
import com.openisle.exception.NotFoundException;
|
||||
import com.openisle.model.PointGood;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.repository.PointGoodRepository;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Service for point mall operations. */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PointMallService {
|
||||
private final PointGoodRepository pointGoodRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
public List<PointGood> listGoods() {
|
||||
return pointGoodRepository.findAll();
|
||||
}
|
||||
|
||||
public int redeem(User user, Long goodId, String contact) {
|
||||
PointGood good = pointGoodRepository.findById(goodId)
|
||||
.orElseThrow(() -> new NotFoundException("Good not found"));
|
||||
if (user.getPoint() < good.getCost()) {
|
||||
throw new FieldException("point", "Insufficient points");
|
||||
}
|
||||
user.setPoint(user.getPoint() - good.getCost());
|
||||
userRepository.save(user);
|
||||
notificationService.createActivityRedeemNotifications(user, good.getName() + ": " + contact);
|
||||
return user.getPoint();
|
||||
}
|
||||
}
|
||||
@@ -56,19 +56,6 @@
|
||||
<i class="menu-item-icon fas fa-chart-line"></i>
|
||||
<span class="menu-item-text">站点统计</span>
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
v-if="authState.loggedIn"
|
||||
class="menu-item"
|
||||
exact-active-class="selected"
|
||||
to="/points"
|
||||
@click="handleItemClick"
|
||||
>
|
||||
<i class="menu-item-icon fas fa-coins"></i>
|
||||
<span class="menu-item-text">
|
||||
积分商城
|
||||
<span v-if="myPoint !== null" class="point-count">{{ myPoint }}</span>
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="menu-section">
|
||||
@@ -143,7 +130,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { authState, fetchCurrentUser } from '~/utils/auth'
|
||||
import { authState } from '~/utils/auth'
|
||||
import { fetchUnreadCount, notificationState } from '~/utils/notification'
|
||||
import { useIsMobile } from '~/utils/screen'
|
||||
import { cycleTheme, ThemeMode, themeState } from '~/utils/theme'
|
||||
@@ -160,7 +147,6 @@ const emit = defineEmits(['item-click'])
|
||||
|
||||
const categoryOpen = ref(true)
|
||||
const tagOpen = ref(true)
|
||||
const myPoint = ref(null)
|
||||
|
||||
/** ✅ 用 useAsyncData 替换原生 fetch,避免 SSR+CSR 二次请求 */
|
||||
const {
|
||||
@@ -205,15 +191,6 @@ const unreadCount = computed(() => notificationState.unreadCount)
|
||||
const showUnreadCount = computed(() => (unreadCount.value > 99 ? '99+' : unreadCount.value))
|
||||
const shouldShowStats = computed(() => authState.role === 'ADMIN')
|
||||
|
||||
const loadPoint = async () => {
|
||||
if (authState.loggedIn) {
|
||||
const user = await fetchCurrentUser()
|
||||
myPoint.value = user ? user.point : null
|
||||
} else {
|
||||
myPoint.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const updateCount = async () => {
|
||||
if (authState.loggedIn) {
|
||||
await fetchUnreadCount()
|
||||
@@ -223,15 +200,9 @@ const updateCount = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([updateCount(), loadPoint()])
|
||||
// 登录态变化时再拉一次未读数和积分;与 useAsyncData 无关
|
||||
watch(
|
||||
() => authState.loggedIn,
|
||||
() => {
|
||||
updateCount()
|
||||
loadPoint()
|
||||
},
|
||||
)
|
||||
await updateCount()
|
||||
// 登录态变化时再拉一次未读数;与 useAsyncData 无关
|
||||
watch(() => authState.loggedIn, updateCount)
|
||||
})
|
||||
|
||||
const handleItemClick = () => {
|
||||
@@ -321,12 +292,6 @@ const gotoTag = (t) => {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.point-count {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.menu-item-icon {
|
||||
margin-right: 10px;
|
||||
opacity: 0.5;
|
||||
|
||||
@@ -40,22 +40,30 @@
|
||||
兑换
|
||||
</div>
|
||||
<div v-else class="redeem-button disabled">兑换</div>
|
||||
<RedeemPopup
|
||||
:visible="dialogVisible"
|
||||
v-model="contact"
|
||||
:loading="loading"
|
||||
@close="closeDialog"
|
||||
@submit="submitRedeem"
|
||||
/>
|
||||
<BasePopup :visible="dialogVisible" @close="closeDialog">
|
||||
<div class="redeem-dialog-content">
|
||||
<BaseInput
|
||||
textarea=""
|
||||
rows="5"
|
||||
v-model="contact"
|
||||
placeholder="联系方式 (手机号/Email/微信/instagram/telegram等, 务必注明来源)"
|
||||
/>
|
||||
<div class="redeem-actions">
|
||||
<div class="redeem-submit-button" @click="submitRedeem" :disabled="loading">提交</div>
|
||||
<div class="redeem-cancel-button" @click="closeDialog">取消</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePopup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { toast } from '~/main'
|
||||
import { fetchCurrentUser, getToken } from '~/utils/auth'
|
||||
import BaseInput from '~/components/BaseInput.vue'
|
||||
import BasePopup from '~/components/BasePopup.vue'
|
||||
import LevelProgress from '~/components/LevelProgress.vue'
|
||||
import ProgressBar from '~/components/ProgressBar.vue'
|
||||
import RedeemPopup from '~/components/RedeemPopup.vue'
|
||||
const config = useRuntimeConfig()
|
||||
const API_BASE_URL = config.public.apiBaseUrl
|
||||
|
||||
@@ -177,6 +185,56 @@ const submitRedeem = async () => {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.redeem-dialog-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
background-color: var(--background-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.redeem-actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.redeem-submit-button {
|
||||
background-color: var(--primary-color);
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.redeem-submit-button:disabled {
|
||||
background-color: var(--primary-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.redeem-submit-button:hover {
|
||||
background-color: var(--primary-color-hover);
|
||||
}
|
||||
|
||||
.redeem-submit-button:disabled:hover {
|
||||
background-color: var(--primary-color-disabled);
|
||||
}
|
||||
|
||||
.redeem-cancel-button {
|
||||
color: var(--primary-color);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.redeem-cancel-button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.user-level-text {
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
@@ -189,5 +247,9 @@ const submitRedeem = async () => {
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.redeem-dialog-content {
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<BasePopup :visible="visible" @close="onClose">
|
||||
<div class="redeem-dialog-content">
|
||||
<BaseInput
|
||||
textarea
|
||||
rows="5"
|
||||
v-model="innerContact"
|
||||
placeholder="联系方式 (手机号/Email/微信/instagram/telegram等, 务必注明来源)"
|
||||
/>
|
||||
<div class="redeem-actions">
|
||||
<div class="redeem-submit-button" @click="submit" :disabled="loading">提交</div>
|
||||
<div class="redeem-cancel-button" @click="onClose">取消</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePopup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import BaseInput from '~/components/BaseInput.vue'
|
||||
import BasePopup from '~/components/BasePopup.vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
loading: { type: Boolean, default: false },
|
||||
modelValue: { type: String, default: '' },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'submit', 'close'])
|
||||
|
||||
const innerContact = ref(props.modelValue)
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
innerContact.value = v
|
||||
},
|
||||
)
|
||||
watch(innerContact, (v) => emit('update:modelValue', v))
|
||||
|
||||
const submit = () => {
|
||||
emit('submit')
|
||||
}
|
||||
const onClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.redeem-dialog-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
background-color: var(--background-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.redeem-actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.redeem-submit-button {
|
||||
background-color: var(--primary-color);
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.redeem-submit-button:disabled {
|
||||
background-color: var(--primary-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.redeem-submit-button:hover {
|
||||
background-color: var(--primary-color-hover);
|
||||
}
|
||||
|
||||
.redeem-submit-button:disabled:hover {
|
||||
background-color: var(--primary-color-disabled);
|
||||
}
|
||||
|
||||
.redeem-cancel-button {
|
||||
color: var(--primary-color);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.redeem-cancel-button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.redeem-dialog-content {
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,7 @@ import { googleAuthWithToken } from '~/utils/google'
|
||||
onMounted(async () => {
|
||||
const hash = new URLSearchParams(window.location.hash.substring(1))
|
||||
const idToken = hash.get('id_token')
|
||||
const state = hash.get('state') || ''
|
||||
if (idToken) {
|
||||
await googleAuthWithToken(
|
||||
idToken,
|
||||
@@ -18,6 +19,7 @@ onMounted(async () => {
|
||||
(token) => {
|
||||
navigateTo(`/signup-reason?token=${token}`, { replace: true })
|
||||
},
|
||||
state,
|
||||
)
|
||||
} else {
|
||||
navigateTo('/login', { replace: true })
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
|
||||
<div class="other-login-page-content">
|
||||
<div class="login-page-button" @click="googleAuthorize">
|
||||
<div class="login-page-button" @click="googleAuthorize()">
|
||||
<img class="login-page-button-icon" src="../assets/icons/google.svg" alt="Google Logo" />
|
||||
<div class="login-page-button-text">Google 登录</div>
|
||||
</div>
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
<template>
|
||||
<div class="point-mall-page">
|
||||
<section class="rules">
|
||||
<div class="section-title">🎉 积分规则</div>
|
||||
<div class="section-content">
|
||||
<div class="section-item" v-for="(rule, idx) in pointRules" :key="idx">{{ rule }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="point-info">
|
||||
<p v-if="authState.loggedIn && point !== null">
|
||||
<span><i class="fas fa-coins coin-icon"></i></span>我的积分:<span class="point-value">{{
|
||||
point
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section class="goods">
|
||||
<div class="goods-item" v-for="(good, idx) in goods" :key="idx">
|
||||
<img class="goods-item-image" :src="good.image" alt="good.name" />
|
||||
<div class="goods-item-name">{{ good.name }}</div>
|
||||
<div class="goods-item-cost">
|
||||
<i class="fas fa-coins"></i>
|
||||
{{ good.cost }} 积分
|
||||
</div>
|
||||
<div class="goods-item-button" @click="openRedeem(good)">兑换</div>
|
||||
</div>
|
||||
</section>
|
||||
<RedeemPopup
|
||||
:visible="dialogVisible"
|
||||
v-model="contact"
|
||||
:loading="loading"
|
||||
@close="closeRedeem"
|
||||
@submit="submitRedeem"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { authState, fetchCurrentUser, getToken } from '~/utils/auth'
|
||||
import { toast } from '~/main'
|
||||
import RedeemPopup from '~/components/RedeemPopup.vue'
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const API_BASE_URL = config.public.apiBaseUrl
|
||||
|
||||
const point = ref(null)
|
||||
|
||||
const pointRules = [
|
||||
'发帖:每天前两次,每次 30 积分',
|
||||
'评论:每天前四条评论可获 10 积分,你的帖子被评论也可获 10 积分',
|
||||
'帖子被点赞:每次 10 积分',
|
||||
'评论被点赞:每次 10 积分',
|
||||
]
|
||||
|
||||
const goods = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const contact = ref('')
|
||||
const loading = ref(false)
|
||||
const selectedGood = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
if (authState.loggedIn) {
|
||||
const user = await fetchCurrentUser()
|
||||
point.value = user ? user.point : null
|
||||
}
|
||||
await loadGoods()
|
||||
})
|
||||
|
||||
const loadGoods = async () => {
|
||||
const res = await fetch(`${API_BASE_URL}/api/point-goods`)
|
||||
if (res.ok) {
|
||||
goods.value = await res.json()
|
||||
}
|
||||
}
|
||||
|
||||
const openRedeem = (good) => {
|
||||
selectedGood.value = good
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const closeRedeem = () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
const submitRedeem = async () => {
|
||||
if (!selectedGood.value || !contact.value) return
|
||||
loading.value = true
|
||||
const token = getToken()
|
||||
const res = await fetch(`${API_BASE_URL}/api/point-goods/redeem`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ goodId: selectedGood.value.id, contact: contact.value }),
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
point.value = data.point
|
||||
toast.success('兑换成功!')
|
||||
dialogVisible.value = false
|
||||
contact.value = ''
|
||||
} else {
|
||||
toast.error('兑换失败')
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.point-mall-page {
|
||||
padding-left: 20px;
|
||||
max-width: var(--page-max-width);
|
||||
background-color: var(--background-color);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.point-info {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.point-value {
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.coin-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.rules,
|
||||
.goods {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.goods {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border: 1px solid var(--normal-border-color);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-item-name {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.goods-item-image {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-bottom: 1px solid var(--normal-border-color);
|
||||
}
|
||||
|
||||
.goods-item-cost {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.goods-item-button {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 7px 10px;
|
||||
border-radius: 10px;
|
||||
width: calc(100% - 40px);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.goods-item-button:hover {
|
||||
background-color: var(--primary-color-hover);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@@ -28,7 +28,6 @@ const reason = ref('')
|
||||
const error = ref('')
|
||||
const isWaitingForRegister = ref(false)
|
||||
const token = ref('')
|
||||
const route = useRoute()
|
||||
|
||||
onMounted(async () => {
|
||||
token.value = route.query.token || ''
|
||||
@@ -51,8 +50,8 @@ const submit = async () => {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: token.value,
|
||||
reason: reason.value,
|
||||
token: this.token,
|
||||
reason: this.reason,
|
||||
}),
|
||||
})
|
||||
isWaitingForRegister.value = false
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
|
||||
<div class="other-signup-page-content">
|
||||
<div class="signup-page-button" @click="googleAuthorize">
|
||||
<div class="signup-page-button" @click="signupWithGoogle">
|
||||
<img class="signup-page-button-icon" src="~/assets/icons/google.svg" alt="Google Logo" />
|
||||
<div class="signup-page-button-text">Google 注册</div>
|
||||
</div>
|
||||
@@ -98,6 +98,7 @@ import { googleAuthorize } from '~/utils/google'
|
||||
import { twitterAuthorize } from '~/utils/twitter'
|
||||
const config = useRuntimeConfig()
|
||||
const API_BASE_URL = config.public.apiBaseUrl
|
||||
const route = useRoute()
|
||||
const emailStep = ref(0)
|
||||
const email = ref('')
|
||||
const username = ref('')
|
||||
@@ -109,9 +110,11 @@ const passwordError = ref('')
|
||||
const code = ref('')
|
||||
const isWaitingForEmailSent = ref(false)
|
||||
const isWaitingForEmailVerified = ref(false)
|
||||
const inviteToken = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
username.value = route.query.u || ''
|
||||
inviteToken.value = route.query.invite_token || ''
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/config`)
|
||||
if (res.ok) {
|
||||
@@ -156,6 +159,7 @@ const sendVerification = async () => {
|
||||
username: username.value,
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
inviteToken: inviteToken.value,
|
||||
}),
|
||||
})
|
||||
isWaitingForEmailSent.value = false
|
||||
@@ -184,6 +188,7 @@ const verifyCode = async () => {
|
||||
body: JSON.stringify({
|
||||
code: code.value,
|
||||
username: username.value,
|
||||
inviteToken: inviteToken.value,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
@@ -203,14 +208,17 @@ const verifyCode = async () => {
|
||||
isWaitingForEmailVerified.value = false
|
||||
}
|
||||
}
|
||||
const signupWithGoogle = () => {
|
||||
googleAuthorize(inviteToken.value)
|
||||
}
|
||||
const signupWithGithub = () => {
|
||||
githubAuthorize()
|
||||
githubAuthorize(inviteToken.value)
|
||||
}
|
||||
const signupWithDiscord = () => {
|
||||
discordAuthorize()
|
||||
discordAuthorize(inviteToken.value)
|
||||
}
|
||||
const signupWithTwitter = () => {
|
||||
twitterAuthorize()
|
||||
twitterAuthorize(inviteToken.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function googleGetIdToken() {
|
||||
})
|
||||
}
|
||||
|
||||
export function googleAuthorize() {
|
||||
export function googleAuthorize(state = '') {
|
||||
const config = useRuntimeConfig()
|
||||
const GOOGLE_CLIENT_ID = config.public.googleClientId
|
||||
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
||||
@@ -31,18 +31,25 @@ export function googleAuthorize() {
|
||||
}
|
||||
const redirectUri = `${WEBSITE_BASE_URL}/google-callback`
|
||||
const nonce = Math.random().toString(36).substring(2)
|
||||
const url = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${GOOGLE_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=id_token&scope=openid%20email%20profile&nonce=${nonce}`
|
||||
const url =
|
||||
`https://accounts.google.com/o/oauth2/v2/auth?client_id=${GOOGLE_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
||||
`&response_type=id_token&scope=openid%20email%20profile&nonce=${nonce}&state=${state}`
|
||||
window.location.href = url
|
||||
}
|
||||
|
||||
export async function googleAuthWithToken(idToken, redirect_success, redirect_not_approved) {
|
||||
export async function googleAuthWithToken(
|
||||
idToken,
|
||||
redirect_success,
|
||||
redirect_not_approved,
|
||||
state = '',
|
||||
) {
|
||||
try {
|
||||
const config = useRuntimeConfig()
|
||||
const API_BASE_URL = config.public.apiBaseUrl
|
||||
const res = await fetch(`${API_BASE_URL}/api/auth/google`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ idToken }),
|
||||
body: JSON.stringify({ idToken, state }),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (res.ok && data.token) {
|
||||
|
||||
Reference in New Issue
Block a user