mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-06-09 11:39:31 +08:00
fix: 迁移部分页面为setup
This commit is contained in:
@@ -37,8 +37,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { API_BASE_URL, toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { getToken } from '~/utils/auth'
|
import { getToken } from '~/utils/auth'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
medals: {
|
medals: {
|
||||||
|
|||||||
@@ -26,49 +26,43 @@
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { API_BASE_URL } from '~/main'
|
|
||||||
import Dropdown from '~/components/Dropdown.vue'
|
import Dropdown from '~/components/Dropdown.vue'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
name: 'CategorySelect',
|
|
||||||
components: { Dropdown },
|
|
||||||
props: {
|
|
||||||
modelValue: { type: [String, Number], default: '' },
|
modelValue: { type: [String, Number], default: '' },
|
||||||
options: { type: Array, default: () => [] },
|
options: { type: Array, default: () => [] },
|
||||||
},
|
})
|
||||||
emits: ['update:modelValue'],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const providedOptions = ref(Array.isArray(props.options) ? [...props.options] : [])
|
|
||||||
|
|
||||||
watch(
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const providedOptions = ref(Array.isArray(props.options) ? [...props.options] : [])
|
||||||
|
watch(
|
||||||
() => props.options,
|
() => props.options,
|
||||||
(val) => {
|
(val) => {
|
||||||
providedOptions.value = Array.isArray(val) ? [...val] : []
|
providedOptions.value = Array.isArray(val) ? [...val] : []
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/categories`)
|
const res = await fetch(`${API_BASE_URL}/api/categories`)
|
||||||
if (!res.ok) return []
|
if (!res.ok) return []
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return [{ id: '', name: '无分类' }, ...data]
|
return [{ id: '', name: '无分类' }, ...data]
|
||||||
}
|
}
|
||||||
|
|
||||||
const isImageIcon = (icon) => {
|
const isImageIcon = (icon) => {
|
||||||
if (!icon) return false
|
if (!icon) return false
|
||||||
return /^https?:\/\//.test(icon) || icon.startsWith('/')
|
return /^https?:\/\//.test(icon) || icon.startsWith('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = computed({
|
const selected = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (v) => emit('update:modelValue', v),
|
set: (v) => emit('update:modelValue', v),
|
||||||
})
|
})
|
||||||
|
|
||||||
return { fetchCategories, selected, isImageIcon, providedOptions }
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -88,11 +88,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import VueEasyLightbox from 'vue-easy-lightbox'
|
import VueEasyLightbox from 'vue-easy-lightbox'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { API_BASE_URL, toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { authState, getToken } from '~/utils/auth'
|
import { authState, getToken } from '~/utils/auth'
|
||||||
import { handleMarkdownClick, renderMarkdown } from '~/utils/markdown'
|
import { handleMarkdownClick, renderMarkdown } from '~/utils/markdown'
|
||||||
import { getMedalTitle } from '~/utils/medal'
|
import { getMedalTitle } from '~/utils/medal'
|
||||||
@@ -100,13 +100,11 @@ import TimeManager from '~/utils/time'
|
|||||||
import BaseTimeline from '~/components/BaseTimeline.vue'
|
import BaseTimeline from '~/components/BaseTimeline.vue'
|
||||||
import CommentEditor from '~/components/CommentEditor.vue'
|
import CommentEditor from '~/components/CommentEditor.vue'
|
||||||
import DropdownMenu from '~/components/DropdownMenu.vue'
|
import DropdownMenu from '~/components/DropdownMenu.vue'
|
||||||
import LoginOverlay from '~/components/LoginOverlay.vue'
|
|
||||||
import ReactionsGroup from '~/components/ReactionsGroup.vue'
|
import ReactionsGroup from '~/components/ReactionsGroup.vue'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
const CommentItem = {
|
const props = defineProps({
|
||||||
name: 'CommentItem',
|
|
||||||
emits: ['deleted'],
|
|
||||||
props: {
|
|
||||||
comment: {
|
comment: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -119,39 +117,42 @@ const CommentItem = {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
setup(props, { emit }) {
|
|
||||||
const router = useRouter()
|
const emit = defineEmits(['deleted'])
|
||||||
const showReplies = ref(props.level === 0 ? true : props.defaultShowReplies)
|
|
||||||
watch(
|
const router = useRouter()
|
||||||
|
const showReplies = ref(props.level === 0 ? true : props.defaultShowReplies)
|
||||||
|
watch(
|
||||||
() => props.defaultShowReplies,
|
() => props.defaultShowReplies,
|
||||||
(val) => {
|
(val) => {
|
||||||
showReplies.value = props.level === 0 ? true : val
|
showReplies.value = props.level === 0 ? true : val
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
const showEditor = ref(false)
|
const showEditor = ref(false)
|
||||||
const editorWrapper = ref(null)
|
const editorWrapper = ref(null)
|
||||||
const isWaitingForReply = ref(false)
|
const isWaitingForReply = ref(false)
|
||||||
const lightboxVisible = ref(false)
|
const lightboxVisible = ref(false)
|
||||||
const lightboxIndex = ref(0)
|
const lightboxIndex = ref(0)
|
||||||
const lightboxImgs = ref([])
|
const lightboxImgs = ref([])
|
||||||
const loggedIn = computed(() => authState.loggedIn)
|
const loggedIn = computed(() => authState.loggedIn)
|
||||||
const countReplies = (list) => list.reduce((sum, r) => sum + 1 + countReplies(r.reply || []), 0)
|
const countReplies = (list) => list.reduce((sum, r) => sum + 1 + countReplies(r.reply || []), 0)
|
||||||
const replyCount = computed(() => countReplies(props.comment.reply || []))
|
const replyCount = computed(() => countReplies(props.comment.reply || []))
|
||||||
const toggleReplies = () => {
|
|
||||||
|
const toggleReplies = () => {
|
||||||
showReplies.value = !showReplies.value
|
showReplies.value = !showReplies.value
|
||||||
}
|
}
|
||||||
const toggleEditor = () => {
|
|
||||||
|
const toggleEditor = () => {
|
||||||
showEditor.value = !showEditor.value
|
showEditor.value = !showEditor.value
|
||||||
if (showEditor.value) {
|
if (showEditor.value) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editorWrapper.value?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
editorWrapper.value?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并所有子回复为一个扁平数组
|
const flattenReplies = (list) => {
|
||||||
const flattenReplies = (list) => {
|
|
||||||
let result = []
|
let result = []
|
||||||
for (const r of list) {
|
for (const r of list) {
|
||||||
result.push(r)
|
result.push(r)
|
||||||
@@ -160,24 +161,24 @@ const CommentItem = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
const replyList = computed(() => {
|
const replyList = computed(() => {
|
||||||
if (props.level < 1) {
|
if (props.level < 1) {
|
||||||
return props.comment.reply
|
return props.comment.reply
|
||||||
}
|
}
|
||||||
|
|
||||||
return flattenReplies(props.comment.reply || [])
|
return flattenReplies(props.comment.reply || [])
|
||||||
})
|
})
|
||||||
|
|
||||||
const isAuthor = computed(() => authState.username === props.comment.userName)
|
const isAuthor = computed(() => authState.username === props.comment.userName)
|
||||||
const isAdmin = computed(() => authState.role === 'ADMIN')
|
const isAdmin = computed(() => authState.role === 'ADMIN')
|
||||||
const commentMenuItems = computed(() =>
|
const commentMenuItems = computed(() =>
|
||||||
isAuthor.value || isAdmin.value
|
isAuthor.value || isAdmin.value
|
||||||
? [{ text: '删除评论', color: 'red', onClick: () => deleteComment() }]
|
? [{ text: '删除评论', color: 'red', onClick: () => deleteComment() }]
|
||||||
: [],
|
: [],
|
||||||
)
|
)
|
||||||
const deleteComment = async () => {
|
const deleteComment = async () => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) {
|
if (!token) {
|
||||||
toast.error('请先登录')
|
toast.error('请先登录')
|
||||||
@@ -195,8 +196,8 @@ const CommentItem = {
|
|||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const submitReply = async (parentUserName, text, clear) => {
|
const submitReply = async (parentUserName, text, clear) => {
|
||||||
if (!text.trim()) return
|
if (!text.trim()) return
|
||||||
isWaitingForReply.value = true
|
isWaitingForReply.value = true
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
@@ -256,14 +257,16 @@ const CommentItem = {
|
|||||||
} finally {
|
} finally {
|
||||||
isWaitingForReply.value = false
|
isWaitingForReply.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const copyCommentLink = () => {
|
|
||||||
|
const copyCommentLink = () => {
|
||||||
const link = `${location.origin}${location.pathname}#comment-${props.comment.id}`
|
const link = `${location.origin}${location.pathname}#comment-${props.comment.id}`
|
||||||
navigator.clipboard.writeText(link).then(() => {
|
navigator.clipboard.writeText(link).then(() => {
|
||||||
toast.success('已复制')
|
toast.success('已复制')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const handleContentClick = (e) => {
|
|
||||||
|
const handleContentClick = (e) => {
|
||||||
handleMarkdownClick(e)
|
handleMarkdownClick(e)
|
||||||
if (e.target.tagName === 'IMG') {
|
if (e.target.tagName === 'IMG') {
|
||||||
const container = e.target.parentNode
|
const container = e.target.parentNode
|
||||||
@@ -272,42 +275,7 @@ const CommentItem = {
|
|||||||
lightboxIndex.value = imgs.indexOf(e.target.src)
|
lightboxIndex.value = imgs.indexOf(e.target.src)
|
||||||
lightboxVisible.value = true
|
lightboxVisible.value = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return {
|
|
||||||
showReplies,
|
|
||||||
toggleReplies,
|
|
||||||
showEditor,
|
|
||||||
toggleEditor,
|
|
||||||
submitReply,
|
|
||||||
copyCommentLink,
|
|
||||||
renderMarkdown,
|
|
||||||
isWaitingForReply,
|
|
||||||
commentMenuItems,
|
|
||||||
deleteComment,
|
|
||||||
lightboxVisible,
|
|
||||||
lightboxIndex,
|
|
||||||
lightboxImgs,
|
|
||||||
handleContentClick,
|
|
||||||
loggedIn,
|
|
||||||
replyCount,
|
|
||||||
replyList,
|
|
||||||
getMedalTitle,
|
|
||||||
editorWrapper,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CommentItem.components = {
|
|
||||||
CommentItem,
|
|
||||||
CommentEditor,
|
|
||||||
BaseTimeline,
|
|
||||||
ReactionsGroup,
|
|
||||||
DropdownMenu,
|
|
||||||
VueEasyLightbox,
|
|
||||||
LoginOverlay,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CommentItem
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -11,36 +11,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import ActivityPopup from '~/components/ActivityPopup.vue'
|
import ActivityPopup from '~/components/ActivityPopup.vue'
|
||||||
import MedalPopup from '~/components/MedalPopup.vue'
|
import MedalPopup from '~/components/MedalPopup.vue'
|
||||||
import NotificationSettingPopup from '~/components/NotificationSettingPopup.vue'
|
import NotificationSettingPopup from '~/components/NotificationSettingPopup.vue'
|
||||||
import { API_BASE_URL } from '~/main'
|
import { API_BASE_URL } from '~/main'
|
||||||
import { authState } from '~/utils/auth'
|
import { authState } from '~/utils/auth'
|
||||||
|
|
||||||
export default {
|
const showMilkTeaPopup = ref(false)
|
||||||
name: 'GlobalPopups',
|
const milkTeaIcon = ref('')
|
||||||
components: { ActivityPopup, MedalPopup, NotificationSettingPopup },
|
const showNotificationPopup = ref(false)
|
||||||
data() {
|
const showMedalPopup = ref(false)
|
||||||
return {
|
const newMedals = ref([])
|
||||||
showMilkTeaPopup: false,
|
|
||||||
milkTeaIcon: '',
|
|
||||||
showNotificationPopup: false,
|
|
||||||
showMedalPopup: false,
|
|
||||||
newMedals: [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
await this.checkMilkTeaActivity()
|
|
||||||
if (this.showMilkTeaPopup) return
|
|
||||||
|
|
||||||
await this.checkNotificationSetting()
|
onMounted(async () => {
|
||||||
if (this.showNotificationPopup) return
|
await checkMilkTeaActivity()
|
||||||
|
if (showMilkTeaPopup.value) return
|
||||||
|
|
||||||
await this.checkNewMedals()
|
await checkNotificationSetting()
|
||||||
},
|
if (showNotificationPopup.value) return
|
||||||
methods: {
|
|
||||||
async checkMilkTeaActivity() {
|
await checkNewMedals()
|
||||||
|
})
|
||||||
|
|
||||||
|
const checkMilkTeaActivity = async () => {
|
||||||
if (!process.client) return
|
if (!process.client) return
|
||||||
if (localStorage.getItem('milkTeaActivityPopupShown')) return
|
if (localStorage.getItem('milkTeaActivityPopupShown')) return
|
||||||
try {
|
try {
|
||||||
@@ -49,33 +43,33 @@ export default {
|
|||||||
const list = await res.json()
|
const list = await res.json()
|
||||||
const a = list.find((i) => i.type === 'MILK_TEA' && !i.ended)
|
const a = list.find((i) => i.type === 'MILK_TEA' && !i.ended)
|
||||||
if (a) {
|
if (a) {
|
||||||
this.milkTeaIcon = a.icon
|
milkTeaIcon.value = a.icon
|
||||||
this.showMilkTeaPopup = true
|
showMilkTeaPopup.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore network errors
|
// ignore network errors
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
closeMilkTeaPopup() {
|
const closeMilkTeaPopup = () => {
|
||||||
if (!process.client) return
|
if (!process.client) return
|
||||||
localStorage.setItem('milkTeaActivityPopupShown', 'true')
|
localStorage.setItem('milkTeaActivityPopupShown', 'true')
|
||||||
this.showMilkTeaPopup = false
|
showMilkTeaPopup.value = false
|
||||||
this.checkNotificationSetting()
|
checkNotificationSetting()
|
||||||
},
|
}
|
||||||
async checkNotificationSetting() {
|
const checkNotificationSetting = async () => {
|
||||||
if (!process.client) return
|
if (!process.client) return
|
||||||
if (!authState.loggedIn) return
|
if (!authState.loggedIn) return
|
||||||
if (localStorage.getItem('notificationSettingPopupShown')) return
|
if (localStorage.getItem('notificationSettingPopupShown')) return
|
||||||
this.showNotificationPopup = true
|
showNotificationPopup.value = true
|
||||||
},
|
}
|
||||||
closeNotificationPopup() {
|
const closeNotificationPopup = () => {
|
||||||
if (!process.client) return
|
if (!process.client) return
|
||||||
localStorage.setItem('notificationSettingPopupShown', 'true')
|
localStorage.setItem('notificationSettingPopupShown', 'true')
|
||||||
this.showNotificationPopup = false
|
showNotificationPopup.value = false
|
||||||
this.checkNewMedals()
|
checkNewMedals()
|
||||||
},
|
}
|
||||||
async checkNewMedals() {
|
const checkNewMedals = async () => {
|
||||||
if (!process.client) return
|
if (!process.client) return
|
||||||
if (!authState.loggedIn || !authState.userId) return
|
if (!authState.loggedIn || !authState.userId) return
|
||||||
try {
|
try {
|
||||||
@@ -85,21 +79,19 @@ export default {
|
|||||||
const seen = JSON.parse(localStorage.getItem('seenMedals') || '[]')
|
const seen = JSON.parse(localStorage.getItem('seenMedals') || '[]')
|
||||||
const m = medals.filter((i) => i.completed && !seen.includes(i.type))
|
const m = medals.filter((i) => i.completed && !seen.includes(i.type))
|
||||||
if (m.length > 0) {
|
if (m.length > 0) {
|
||||||
this.newMedals = m
|
newMedals.value = m
|
||||||
this.showMedalPopup = true
|
showMedalPopup.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore errors
|
// ignore errors
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
closeMedalPopup() {
|
const closeMedalPopup = () => {
|
||||||
if (!process.client) return
|
if (!process.client) return
|
||||||
const seen = new Set(JSON.parse(localStorage.getItem('seenMedals') || '[]'))
|
const seen = new Set(JSON.parse(localStorage.getItem('seenMedals') || '[]'))
|
||||||
this.newMedals.forEach((m) => seen.add(m.type))
|
newMedals.value.forEach((m) => seen.add(m.type))
|
||||||
localStorage.setItem('seenMedals', JSON.stringify([...seen]))
|
localStorage.setItem('seenMedals', JSON.stringify([...seen]))
|
||||||
this.showMedalPopup = false
|
showMedalPopup.value = false
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -123,49 +123,48 @@
|
|||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { themeState, cycleTheme, ThemeMode } from '~/utils/theme'
|
import { themeState, cycleTheme, ThemeMode } from '~/utils/theme'
|
||||||
import { authState } from '~/utils/auth'
|
import { authState } from '~/utils/auth'
|
||||||
import { fetchUnreadCount, notificationState } from '~/utils/notification'
|
import { fetchUnreadCount, notificationState } from '~/utils/notification'
|
||||||
import { ref, computed, watch, onMounted } from 'vue'
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import { API_BASE_URL } from '~/main'
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
name: 'MenuComponent',
|
|
||||||
props: {
|
|
||||||
visible: {
|
visible: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
async setup(props, { emit }) {
|
|
||||||
const router = useRouter()
|
|
||||||
const categories = ref([])
|
|
||||||
const tags = ref([])
|
|
||||||
const categoryOpen = ref(true)
|
|
||||||
const tagOpen = ref(true)
|
|
||||||
const isLoadingCategory = ref(false)
|
|
||||||
const isLoadingTag = ref(false)
|
|
||||||
const categoryData = ref([])
|
|
||||||
const tagData = ref([])
|
|
||||||
|
|
||||||
const fetchCategoryData = async () => {
|
const emit = defineEmits(['item-click'])
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const categoryOpen = ref(true)
|
||||||
|
const tagOpen = ref(true)
|
||||||
|
const isLoadingCategory = ref(false)
|
||||||
|
const isLoadingTag = ref(false)
|
||||||
|
const categoryData = ref([])
|
||||||
|
const tagData = ref([])
|
||||||
|
|
||||||
|
const fetchCategoryData = async () => {
|
||||||
isLoadingCategory.value = true
|
isLoadingCategory.value = true
|
||||||
const res = await fetch(`${API_BASE_URL}/api/categories`)
|
const res = await fetch(`${API_BASE_URL}/api/categories`)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
categoryData.value = data
|
categoryData.value = data
|
||||||
isLoadingCategory.value = false
|
isLoadingCategory.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchTagData = async () => {
|
const fetchTagData = async () => {
|
||||||
isLoadingTag.value = true
|
isLoadingTag.value = true
|
||||||
const res = await fetch(`${API_BASE_URL}/api/tags?limit=10`)
|
const res = await fetch(`${API_BASE_URL}/api/tags?limit=10`)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
tagData.value = data
|
tagData.value = data
|
||||||
isLoadingTag.value = false
|
isLoadingTag.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconClass = computed(() => {
|
const iconClass = computed(() => {
|
||||||
switch (themeState.mode) {
|
switch (themeState.mode) {
|
||||||
case ThemeMode.DARK:
|
case ThemeMode.DARK:
|
||||||
return 'fas fa-moon'
|
return 'fas fa-moon'
|
||||||
@@ -174,74 +173,53 @@ export default {
|
|||||||
default:
|
default:
|
||||||
return 'fas fa-desktop'
|
return 'fas fa-desktop'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const unreadCount = computed(() => notificationState.unreadCount)
|
const unreadCount = computed(() => notificationState.unreadCount)
|
||||||
const showUnreadCount = computed(() => (unreadCount.value > 99 ? '99+' : unreadCount.value))
|
const showUnreadCount = computed(() => (unreadCount.value > 99 ? '99+' : unreadCount.value))
|
||||||
const shouldShowStats = computed(() => authState.role === 'ADMIN')
|
const shouldShowStats = computed(() => authState.role === 'ADMIN')
|
||||||
|
|
||||||
const updateCount = async () => {
|
const updateCount = async () => {
|
||||||
if (authState.loggedIn) {
|
if (authState.loggedIn) {
|
||||||
await fetchUnreadCount()
|
await fetchUnreadCount()
|
||||||
} else {
|
} else {
|
||||||
notificationState.unreadCount = 0
|
notificationState.unreadCount = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await updateCount()
|
await updateCount()
|
||||||
watch(() => authState.loggedIn, updateCount)
|
watch(() => authState.loggedIn, updateCount)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleHomeClick = () => {
|
const handleHomeClick = () => {
|
||||||
router.push('/').then(() => {
|
router.push('/').then(() => {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleItemClick = () => {
|
const handleItemClick = () => {
|
||||||
if (window.innerWidth <= 768) emit('item-click')
|
if (window.innerWidth <= 768) emit('item-click')
|
||||||
}
|
}
|
||||||
|
|
||||||
const isImageIcon = (icon) => {
|
const isImageIcon = (icon) => {
|
||||||
if (!icon) return false
|
if (!icon) return false
|
||||||
return /^https?:\/\//.test(icon) || icon.startsWith('/')
|
return /^https?:\/\//.test(icon) || icon.startsWith('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
const gotoCategory = (c) => {
|
const gotoCategory = (c) => {
|
||||||
const value = encodeURIComponent(c.id ?? c.name)
|
const value = encodeURIComponent(c.id ?? c.name)
|
||||||
router.push({ path: '/', query: { category: value } })
|
router.push({ path: '/', query: { category: value } })
|
||||||
handleItemClick()
|
handleItemClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
const gotoTag = (t) => {
|
const gotoTag = (t) => {
|
||||||
const value = encodeURIComponent(t.id ?? t.name)
|
const value = encodeURIComponent(t.id ?? t.name)
|
||||||
router.push({ path: '/', query: { tags: value } })
|
router.push({ path: '/', query: { tags: value } })
|
||||||
handleItemClick()
|
handleItemClick()
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([fetchCategoryData(), fetchTagData()])
|
|
||||||
|
|
||||||
return {
|
|
||||||
categoryData,
|
|
||||||
tagData,
|
|
||||||
categoryOpen,
|
|
||||||
tagOpen,
|
|
||||||
isLoadingCategory,
|
|
||||||
isLoadingTag,
|
|
||||||
iconClass,
|
|
||||||
unreadCount,
|
|
||||||
showUnreadCount,
|
|
||||||
shouldShowStats,
|
|
||||||
cycleTheme,
|
|
||||||
handleHomeClick,
|
|
||||||
handleItemClick,
|
|
||||||
isImageIcon,
|
|
||||||
gotoCategory,
|
|
||||||
gotoTag,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all([fetchCategoryData(), fetchTagData()])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -57,49 +57,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { API_BASE_URL, toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { fetchCurrentUser, getToken } from '~/utils/auth'
|
import { fetchCurrentUser, getToken } from '~/utils/auth'
|
||||||
import BaseInput from '~/components/BaseInput.vue'
|
import BaseInput from '~/components/BaseInput.vue'
|
||||||
import BasePopup from '~/components/BasePopup.vue'
|
import BasePopup from '~/components/BasePopup.vue'
|
||||||
import LevelProgress from '~/components/LevelProgress.vue'
|
import LevelProgress from '~/components/LevelProgress.vue'
|
||||||
import ProgressBar from '~/components/ProgressBar.vue'
|
import ProgressBar from '~/components/ProgressBar.vue'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
export default {
|
const info = ref({ redeemCount: 0, ended: false })
|
||||||
name: 'MilkTeaActivityComponent',
|
const user = ref(null)
|
||||||
components: { ProgressBar, LevelProgress, BaseInput, BasePopup },
|
const dialogVisible = ref(false)
|
||||||
data() {
|
const contact = ref('')
|
||||||
return {
|
const loading = ref(false)
|
||||||
info: { redeemCount: 0, ended: false },
|
const isLoadingUser = ref(true)
|
||||||
user: null,
|
|
||||||
dialogVisible: false,
|
onMounted(async () => {
|
||||||
contact: '',
|
await loadInfo()
|
||||||
loading: false,
|
isLoadingUser.value = true
|
||||||
isLoadingUser: true,
|
user.value = await fetchCurrentUser()
|
||||||
}
|
isLoadingUser.value = false
|
||||||
},
|
})
|
||||||
async mounted() {
|
const loadInfo = async () => {
|
||||||
await this.loadInfo()
|
|
||||||
this.isLoadingUser = true
|
|
||||||
this.user = await fetchCurrentUser()
|
|
||||||
this.isLoadingUser = false
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async loadInfo() {
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/activities/milk-tea`)
|
const res = await fetch(`${API_BASE_URL}/api/activities/milk-tea`)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.info = await res.json()
|
info.value = await res.json()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
openDialog() {
|
const openDialog = () => {
|
||||||
this.dialogVisible = true
|
dialogVisible.value = true
|
||||||
},
|
}
|
||||||
closeDialog() {
|
const closeDialog = () => {
|
||||||
this.dialogVisible = false
|
dialogVisible.value = false
|
||||||
},
|
}
|
||||||
async submitRedeem() {
|
const submitRedeem = async () => {
|
||||||
if (!this.contact) return
|
if (!contact.value) return
|
||||||
this.loading = true
|
loading.value = true
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
const res = await fetch(`${API_BASE_URL}/api/activities/milk-tea/redeem`, {
|
const res = await fetch(`${API_BASE_URL}/api/activities/milk-tea/redeem`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -107,7 +102,7 @@ export default {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ contact: this.contact }),
|
body: JSON.stringify({ contact: contact.value }),
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
@@ -116,14 +111,12 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
toast.success('兑换成功!')
|
toast.success('兑换成功!')
|
||||||
}
|
}
|
||||||
this.dialogVisible = false
|
dialogVisible.value = false
|
||||||
await this.loadInfo()
|
await loadInfo()
|
||||||
} else {
|
} else {
|
||||||
toast.error('兑换失败')
|
toast.error('兑换失败')
|
||||||
}
|
}
|
||||||
this.loading = false
|
loading.value = false
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -46,11 +46,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { API_BASE_URL, toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { authState, getToken } from '~/utils/auth'
|
import { authState, getToken } from '~/utils/auth'
|
||||||
import { reactionEmojiMap } from '~/utils/reactions'
|
import { reactionEmojiMap } from '~/utils/reactions'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const reactions = ref(props.modelValue)
|
||||||
|
const reactionTypes = ref([])
|
||||||
|
|
||||||
let cachedTypes = null
|
let cachedTypes = null
|
||||||
const fetchTypes = async () => {
|
const fetchTypes = async () => {
|
||||||
@@ -71,66 +76,61 @@ const fetchTypes = async () => {
|
|||||||
return cachedTypes
|
return cachedTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
name: 'ReactionsGroup',
|
|
||||||
props: {
|
|
||||||
modelValue: { type: Array, default: () => [] },
|
modelValue: { type: Array, default: () => [] },
|
||||||
contentType: { type: String, required: true },
|
contentType: { type: String, required: true },
|
||||||
contentId: { type: [Number, String], required: true },
|
contentId: { type: [Number, String], required: true },
|
||||||
},
|
})
|
||||||
emits: ['update:modelValue'],
|
|
||||||
setup(props, { emit }) {
|
watch(
|
||||||
const reactions = ref(props.modelValue)
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(v) => (reactions.value = v),
|
(v) => (reactions.value = v),
|
||||||
)
|
)
|
||||||
|
|
||||||
const reactionTypes = ref([])
|
onMounted(async () => {
|
||||||
onMounted(async () => {
|
|
||||||
reactionTypes.value = await fetchTypes()
|
reactionTypes.value = await fetchTypes()
|
||||||
})
|
})
|
||||||
|
|
||||||
const counts = computed(() => {
|
const counts = computed(() => {
|
||||||
const c = {}
|
const c = {}
|
||||||
for (const r of reactions.value) {
|
for (const r of reactions.value) {
|
||||||
c[r.type] = (c[r.type] || 0) + 1
|
c[r.type] = (c[r.type] || 0) + 1
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
})
|
})
|
||||||
|
|
||||||
const totalCount = computed(() => Object.values(counts.value).reduce((a, b) => a + b, 0))
|
const totalCount = computed(() => Object.values(counts.value).reduce((a, b) => a + b, 0))
|
||||||
const likeCount = computed(() => counts.value['LIKE'] || 0)
|
const likeCount = computed(() => counts.value['LIKE'] || 0)
|
||||||
|
|
||||||
const userReacted = (type) =>
|
const userReacted = (type) =>
|
||||||
reactions.value.some((r) => r.type === type && r.user === authState.username)
|
reactions.value.some((r) => r.type === type && r.user === authState.username)
|
||||||
|
|
||||||
const displayedReactions = computed(() => {
|
const displayedReactions = computed(() => {
|
||||||
return Object.entries(counts.value)
|
return Object.entries(counts.value)
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
.map(([type]) => ({ type }))
|
.map(([type]) => ({ type }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const panelTypes = computed(() => reactionTypes.value.filter((t) => t !== 'LIKE'))
|
const panelTypes = computed(() => reactionTypes.value.filter((t) => t !== 'LIKE'))
|
||||||
|
|
||||||
const panelVisible = ref(false)
|
const panelVisible = ref(false)
|
||||||
let hideTimer = null
|
let hideTimer = null
|
||||||
const openPanel = () => {
|
const openPanel = () => {
|
||||||
clearTimeout(hideTimer)
|
clearTimeout(hideTimer)
|
||||||
panelVisible.value = true
|
panelVisible.value = true
|
||||||
}
|
}
|
||||||
const scheduleHide = () => {
|
const scheduleHide = () => {
|
||||||
clearTimeout(hideTimer)
|
clearTimeout(hideTimer)
|
||||||
hideTimer = setTimeout(() => {
|
hideTimer = setTimeout(() => {
|
||||||
panelVisible.value = false
|
panelVisible.value = false
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
const cancelHide = () => {
|
const cancelHide = () => {
|
||||||
clearTimeout(hideTimer)
|
clearTimeout(hideTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleReaction = async (type) => {
|
const toggleReaction = async (type) => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) {
|
if (!token) {
|
||||||
toast.error('请先登录')
|
toast.error('请先登录')
|
||||||
@@ -199,23 +199,6 @@ export default {
|
|||||||
emit('update:modelValue', reactions.value)
|
emit('update:modelValue', reactions.value)
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
reactionEmojiMap,
|
|
||||||
counts,
|
|
||||||
totalCount,
|
|
||||||
likeCount,
|
|
||||||
displayedReactions,
|
|
||||||
panelTypes,
|
|
||||||
panelVisible,
|
|
||||||
openPanel,
|
|
||||||
scheduleHide,
|
|
||||||
cancelHide,
|
|
||||||
toggleReaction,
|
|
||||||
userReacted,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -36,33 +36,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useIsMobile } from '~/utils/screen'
|
import { useIsMobile } from '~/utils/screen'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import Dropdown from '~/components/Dropdown.vue'
|
import Dropdown from '~/components/Dropdown.vue'
|
||||||
import { API_BASE_URL } from '~/main'
|
|
||||||
import { stripMarkdown } from '~/utils/markdown'
|
import { stripMarkdown } from '~/utils/markdown'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
export default {
|
const emit = defineEmits(['close'])
|
||||||
name: 'SearchDropdown',
|
|
||||||
components: { Dropdown },
|
|
||||||
emits: ['close'],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const router = useRouter()
|
|
||||||
const keyword = ref('')
|
|
||||||
const selected = ref(null)
|
|
||||||
const results = ref([])
|
|
||||||
const dropdown = ref(null)
|
|
||||||
const isMobile = useIsMobile()
|
|
||||||
|
|
||||||
const toggle = () => {
|
const router = useRouter()
|
||||||
|
const keyword = ref('')
|
||||||
|
const selected = ref(null)
|
||||||
|
const results = ref([])
|
||||||
|
const dropdown = ref(null)
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
dropdown.value.toggle()
|
dropdown.value.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClose = () => emit('close')
|
const onClose = () => emit('close')
|
||||||
|
|
||||||
const fetchResults = async (kw) => {
|
const fetchResults = async (kw) => {
|
||||||
if (!kw) return []
|
if (!kw) return []
|
||||||
const res = await fetch(`${API_BASE_URL}/api/search/global?keyword=${encodeURIComponent(kw)}`)
|
const res = await fetch(`${API_BASE_URL}/api/search/global?keyword=${encodeURIComponent(kw)}`)
|
||||||
if (!res.ok) return []
|
if (!res.ok) return []
|
||||||
@@ -76,25 +74,25 @@ export default {
|
|||||||
postId: r.postId,
|
postId: r.postId,
|
||||||
}))
|
}))
|
||||||
return results.value
|
return results.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const highlight = (text) => {
|
const highlight = (text) => {
|
||||||
text = stripMarkdown(text)
|
text = stripMarkdown(text)
|
||||||
if (!keyword.value) return text
|
if (!keyword.value) return text
|
||||||
const reg = new RegExp(keyword.value, 'gi')
|
const reg = new RegExp(keyword.value, 'gi')
|
||||||
const res = text.replace(reg, (m) => `<span class="highlight">${m}</span>`)
|
const res = text.replace(reg, (m) => `<span class="highlight">${m}</span>`)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
user: 'fas fa-user',
|
user: 'fas fa-user',
|
||||||
post: 'fas fa-file-alt',
|
post: 'fas fa-file-alt',
|
||||||
comment: 'fas fa-comment',
|
comment: 'fas fa-comment',
|
||||||
category: 'fas fa-folder',
|
category: 'fas fa-folder',
|
||||||
tag: 'fas fa-hashtag',
|
tag: 'fas fa-hashtag',
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(selected, (val) => {
|
watch(selected, (val) => {
|
||||||
if (!val) return
|
if (!val) return
|
||||||
const opt = results.value.find((r) => r.id === val)
|
const opt = results.value.find((r) => r.id === val)
|
||||||
if (!opt) return
|
if (!opt) return
|
||||||
@@ -113,21 +111,7 @@ export default {
|
|||||||
}
|
}
|
||||||
selected.value = null
|
selected.value = null
|
||||||
keyword.value = ''
|
keyword.value = ''
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
|
||||||
keyword,
|
|
||||||
selected,
|
|
||||||
fetchResults,
|
|
||||||
highlight,
|
|
||||||
iconMap,
|
|
||||||
isMobile,
|
|
||||||
dropdown,
|
|
||||||
onClose,
|
|
||||||
toggle,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -28,42 +28,41 @@
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { API_BASE_URL, toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import Dropdown from '~/components/Dropdown.vue'
|
import Dropdown from '~/components/Dropdown.vue'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
export default {
|
const emit = defineEmits(['update:modelValue'])
|
||||||
name: 'TagSelect',
|
const props = defineProps({
|
||||||
components: { Dropdown },
|
|
||||||
props: {
|
|
||||||
modelValue: { type: Array, default: () => [] },
|
modelValue: { type: Array, default: () => [] },
|
||||||
creatable: { type: Boolean, default: false },
|
creatable: { type: Boolean, default: false },
|
||||||
options: { type: Array, default: () => [] },
|
options: { type: Array, default: () => [] },
|
||||||
},
|
})
|
||||||
emits: ['update:modelValue'],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const localTags = ref([])
|
|
||||||
const providedTags = ref(Array.isArray(props.options) ? [...props.options] : [])
|
|
||||||
|
|
||||||
watch(
|
const localTags = ref([])
|
||||||
|
const providedTags = ref(Array.isArray(props.options) ? [...props.options] : [])
|
||||||
|
|
||||||
|
watch(
|
||||||
() => props.options,
|
() => props.options,
|
||||||
(val) => {
|
(val) => {
|
||||||
providedTags.value = Array.isArray(val) ? [...val] : []
|
providedTags.value = Array.isArray(val) ? [...val] : []
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const mergedOptions = computed(() => {
|
const mergedOptions = computed(() => {
|
||||||
const arr = [...providedTags.value, ...localTags.value]
|
const arr = [...providedTags.value, ...localTags.value]
|
||||||
return arr.filter((v, i, a) => a.findIndex((t) => t.id === v.id) === i)
|
return arr.filter((v, i, a) => a.findIndex((t) => t.id === v.id) === i)
|
||||||
})
|
})
|
||||||
|
|
||||||
const isImageIcon = (icon) => {
|
const isImageIcon = (icon) => {
|
||||||
if (!icon) return false
|
if (!icon) return false
|
||||||
return /^https?:\/\//.test(icon) || icon.startsWith('/')
|
return /^https?:\/\//.test(icon) || icon.startsWith('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildTagsUrl = (kw = '') => {
|
const buildTagsUrl = (kw = '') => {
|
||||||
const base = API_BASE_URL || (process.client ? window.location.origin : '')
|
const base = API_BASE_URL || (process.client ? window.location.origin : '')
|
||||||
const url = new URL('/api/tags', base)
|
const url = new URL('/api/tags', base)
|
||||||
|
|
||||||
@@ -71,9 +70,9 @@ export default {
|
|||||||
url.searchParams.set('limit', '10')
|
url.searchParams.set('limit', '10')
|
||||||
|
|
||||||
return url.toString()
|
return url.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchTags = async (kw = '') => {
|
const fetchTags = async (kw = '') => {
|
||||||
const defaultOption = { id: 0, name: '无标签' }
|
const defaultOption = { id: 0, name: '无标签' }
|
||||||
|
|
||||||
// 1) 先拼 URL(自动兜底到 window.location.origin)
|
// 1) 先拼 URL(自动兜底到 window.location.origin)
|
||||||
@@ -91,11 +90,7 @@ export default {
|
|||||||
// 3) 合并、去重、可创建
|
// 3) 合并、去重、可创建
|
||||||
let options = [...data, ...localTags.value]
|
let options = [...data, ...localTags.value]
|
||||||
|
|
||||||
if (
|
if (props.creatable && kw && !options.some((t) => t.name.toLowerCase() === kw.toLowerCase())) {
|
||||||
props.creatable &&
|
|
||||||
kw &&
|
|
||||||
!options.some((t) => t.name.toLowerCase() === kw.toLowerCase())
|
|
||||||
) {
|
|
||||||
options.push({ id: `__create__:${kw}`, name: `创建"${kw}"` })
|
options.push({ id: `__create__:${kw}`, name: `创建"${kw}"` })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,9 +98,9 @@ export default {
|
|||||||
|
|
||||||
// 4) 最终结果
|
// 4) 最终结果
|
||||||
return [defaultOption, ...options]
|
return [defaultOption, ...options]
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = computed({
|
const selected = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (v) => {
|
set: (v) => {
|
||||||
if (Array.isArray(v)) {
|
if (Array.isArray(v)) {
|
||||||
@@ -131,11 +126,7 @@ export default {
|
|||||||
}
|
}
|
||||||
emit('update:modelValue', v)
|
emit('update:modelValue', v)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return { fetchTags, selected, isImageIcon, mergedOptions }
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,12 +1 @@
|
|||||||
import { useRuntimeConfig } from '#app'
|
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
|
|
||||||
export const API_BASE_URL = config.public.apiBaseUrl
|
|
||||||
export const GOOGLE_CLIENT_ID = config.public.googleClientId
|
|
||||||
export const GITHUB_CLIENT_ID = config.public.githubClientId
|
|
||||||
export const DISCORD_CLIENT_ID = config.public.discordClientId
|
|
||||||
export const TWITTER_CLIENT_ID = config.public.twitterClientId
|
|
||||||
|
|
||||||
// 重新导出 toast 功能,使用 composable 方式
|
|
||||||
export { toast } from './composables/useToast'
|
export { toast } from './composables/useToast'
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ import { use } from 'echarts/core'
|
|||||||
import { CanvasRenderer } from 'echarts/renderers'
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import VChart from 'vue-echarts'
|
import VChart from 'vue-echarts'
|
||||||
import { API_BASE_URL } from '~/main'
|
|
||||||
import { getToken } from '~/utils/auth'
|
import { getToken } from '~/utils/auth'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
use([LineChart, TitleComponent, TooltipComponent, GridComponent, DataZoomComponent, CanvasRenderer])
|
use([LineChart, TitleComponent, TooltipComponent, GridComponent, DataZoomComponent, CanvasRenderer])
|
||||||
|
|
||||||
|
|||||||
@@ -29,35 +29,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { API_BASE_URL } from '~/main'
|
|
||||||
import TimeManager from '~/utils/time'
|
import TimeManager from '~/utils/time'
|
||||||
import MilkTeaActivityComponent from '~/components/MilkTeaActivityComponent.vue'
|
import MilkTeaActivityComponent from '~/components/MilkTeaActivityComponent.vue'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
export default {
|
const activities = ref([])
|
||||||
name: 'ActivityListPageView',
|
const isLoadingActivities = ref(false)
|
||||||
components: { MilkTeaActivityComponent },
|
|
||||||
data() {
|
onMounted(async () => {
|
||||||
return {
|
isLoadingActivities.value = true
|
||||||
activities: [],
|
|
||||||
TimeManager,
|
|
||||||
isLoadingActivities: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
this.isLoadingActivities = true
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/activities`)
|
const res = await fetch(`${API_BASE_URL}/api/activities`)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.activities = await res.json()
|
activities.value = await res.json()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoadingActivities = false
|
isLoadingActivities.value = false
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -23,105 +23,99 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { API_BASE_URL, toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import BaseInput from '~/components/BaseInput.vue'
|
import BaseInput from '~/components/BaseInput.vue'
|
||||||
export default {
|
const config = useRuntimeConfig()
|
||||||
name: 'ForgotPasswordPageView',
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
components: { BaseInput },
|
|
||||||
data() {
|
const step = ref(0)
|
||||||
return {
|
const email = ref('')
|
||||||
step: 0,
|
const code = ref('')
|
||||||
email: '',
|
const password = ref('')
|
||||||
code: '',
|
const token = ref('')
|
||||||
password: '',
|
const emailError = ref('')
|
||||||
token: '',
|
const passwordError = ref('')
|
||||||
emailError: '',
|
const isSending = ref(false)
|
||||||
passwordError: '',
|
const isVerifying = ref(false)
|
||||||
isSending: false,
|
const isResetting = ref(false)
|
||||||
isVerifying: false,
|
|
||||||
isResetting: false,
|
onMounted(() => {
|
||||||
|
if (route.query.email) {
|
||||||
|
email.value = decodeURIComponent(route.query.email)
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
mounted() {
|
const sendCode = async () => {
|
||||||
if (this.$route.query.email) {
|
if (!email.value) {
|
||||||
this.email = decodeURIComponent(this.$route.query.email)
|
emailError.value = '邮箱不能为空'
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async sendCode() {
|
|
||||||
if (!this.email) {
|
|
||||||
this.emailError = '邮箱不能为空'
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.isSending = true
|
isSending.value = true
|
||||||
const res = await fetch(`${API_BASE_URL}/api/auth/forgot/send`, {
|
const res = await fetch(`${API_BASE_URL}/api/auth/forgot/send`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ email: this.email }),
|
body: JSON.stringify({ email: email.value }),
|
||||||
})
|
})
|
||||||
this.isSending = false
|
isSending.value = false
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success('验证码已发送')
|
toast.success('验证码已发送')
|
||||||
this.step = 1
|
step.value = 1
|
||||||
} else {
|
} else {
|
||||||
toast.error('请填写已注册邮箱')
|
toast.error('请填写已注册邮箱')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.isSending = false
|
isSending.value = false
|
||||||
toast.error('发送失败')
|
toast.error('发送失败')
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
async verifyCode() {
|
const verifyCode = async () => {
|
||||||
try {
|
try {
|
||||||
this.isVerifying = true
|
isVerifying.value = true
|
||||||
const res = await fetch(`${API_BASE_URL}/api/auth/forgot/verify`, {
|
const res = await fetch(`${API_BASE_URL}/api/auth/forgot/verify`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ email: this.email, code: this.code }),
|
body: JSON.stringify({ email: email.value, code: code.value }),
|
||||||
})
|
})
|
||||||
this.isVerifying = false
|
isVerifying.value = false
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.token = data.token
|
token.value = data.token
|
||||||
this.step = 2
|
step.value = 2
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.error || '验证失败')
|
toast.error(data.error || '验证失败')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.isVerifying = false
|
isVerifying.value = false
|
||||||
toast.error('验证失败')
|
toast.error('验证失败')
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
async resetPassword() {
|
const resetPassword = async () => {
|
||||||
if (!this.password) {
|
if (!password.value) {
|
||||||
this.passwordError = '密码不能为空'
|
passwordError.value = '密码不能为空'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.isResetting = true
|
isResetting.value = true
|
||||||
const res = await fetch(`${API_BASE_URL}/api/auth/forgot/reset`, {
|
const res = await fetch(`${API_BASE_URL}/api/auth/forgot/reset`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ token: this.token, password: this.password }),
|
body: JSON.stringify({ token: token.value, password: password.value }),
|
||||||
})
|
})
|
||||||
this.isResetting = false
|
isResetting.value = false
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success('密码已重置')
|
toast.success('密码已重置')
|
||||||
this.$router.push('/login')
|
router.push('/login')
|
||||||
} else if (data.field === 'password') {
|
} else if (data.field === 'password') {
|
||||||
this.passwordError = data.error
|
passwordError.value = data.error
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.error || '重置失败')
|
toast.error(data.error || '重置失败')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.isResetting = false
|
isResetting.value = false
|
||||||
toast.error('重置失败')
|
toast.error('重置失败')
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -111,35 +111,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import ArticleCategory from '~/components/ArticleCategory.vue'
|
import ArticleCategory from '~/components/ArticleCategory.vue'
|
||||||
import ArticleTags from '~/components/ArticleTags.vue'
|
import ArticleTags from '~/components/ArticleTags.vue'
|
||||||
import CategorySelect from '~/components/CategorySelect.vue'
|
import CategorySelect from '~/components/CategorySelect.vue'
|
||||||
import SearchDropdown from '~/components/SearchDropdown.vue'
|
import SearchDropdown from '~/components/SearchDropdown.vue'
|
||||||
import TagSelect from '~/components/TagSelect.vue'
|
import TagSelect from '~/components/TagSelect.vue'
|
||||||
import { API_BASE_URL } from '~/main'
|
|
||||||
import { getToken } from '~/utils/auth'
|
import { getToken } from '~/utils/auth'
|
||||||
import { useScrollLoadMore } from '~/utils/loadMore'
|
import { useScrollLoadMore } from '~/utils/loadMore'
|
||||||
import { stripMarkdown } from '~/utils/markdown'
|
import { stripMarkdown } from '~/utils/markdown'
|
||||||
import { useIsMobile } from '~/utils/screen'
|
import { useIsMobile } from '~/utils/screen'
|
||||||
import TimeManager from '~/utils/time'
|
import TimeManager from '~/utils/time'
|
||||||
|
|
||||||
export default {
|
useHead({
|
||||||
name: 'HomePageView',
|
|
||||||
components: {
|
|
||||||
CategorySelect,
|
|
||||||
TagSelect,
|
|
||||||
ArticleTags,
|
|
||||||
ArticleCategory,
|
|
||||||
SearchDropdown,
|
|
||||||
ClientOnly: () =>
|
|
||||||
import('vue').then((m) =>
|
|
||||||
m.defineAsyncComponent(() => import('vue').then(() => ({ template: '<slot />' }))),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
async setup() {
|
|
||||||
useHead({
|
|
||||||
title: 'OpenIsle - 全面开源的自由社区',
|
title: 'OpenIsle - 全面开源的自由社区',
|
||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
@@ -148,42 +133,41 @@ export default {
|
|||||||
'OpenIsle 是一个开放的技术与交流社区,致力于为开发者、技术爱好者和创作者们提供一个自由、友好、包容的讨论与协作环境。我们鼓励用户在这里分享知识、交流经验、提出问题、展示作品,并共同推动技术进步与社区成长。',
|
'OpenIsle 是一个开放的技术与交流社区,致力于为开发者、技术爱好者和创作者们提供一个自由、友好、包容的讨论与协作环境。我们鼓励用户在这里分享知识、交流经验、提出问题、展示作品,并共同推动技术进步与社区成长。',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
const selectedCategory = ref('')
|
|
||||||
const selectedTags = ref([])
|
|
||||||
const route = useRoute()
|
|
||||||
const tagOptions = ref([])
|
|
||||||
const categoryOptions = ref([])
|
|
||||||
const isLoadingPosts = ref(false)
|
|
||||||
const topics = ref(['最新回复', '最新', '排行榜' /*, '热门', '类别'*/])
|
|
||||||
const selectedTopic = ref(
|
|
||||||
route.query.view === 'ranking'
|
|
||||||
? '排行榜'
|
|
||||||
: route.query.view === 'latest'
|
|
||||||
? '最新'
|
|
||||||
: '最新回复',
|
|
||||||
)
|
|
||||||
const articles = ref([])
|
|
||||||
const page = ref(0)
|
|
||||||
const pageSize = 10
|
|
||||||
const isMobile = useIsMobile()
|
|
||||||
const allLoaded = ref(false)
|
|
||||||
|
|
||||||
const selectedCategorySet = (category) => {
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
const selectedCategory = ref('')
|
||||||
|
const selectedTags = ref([])
|
||||||
|
const route = useRoute()
|
||||||
|
const tagOptions = ref([])
|
||||||
|
const categoryOptions = ref([])
|
||||||
|
const isLoadingPosts = ref(false)
|
||||||
|
const topics = ref(['最新回复', '最新', '排行榜' /*, '热门', '类别'*/])
|
||||||
|
const selectedTopic = ref(
|
||||||
|
route.query.view === 'ranking' ? '排行榜' : route.query.view === 'latest' ? '最新' : '最新回复',
|
||||||
|
)
|
||||||
|
const articles = ref([])
|
||||||
|
const page = ref(0)
|
||||||
|
const pageSize = 10
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
const allLoaded = ref(false)
|
||||||
|
|
||||||
|
const selectedCategorySet = (category) => {
|
||||||
const c = decodeURIComponent(category)
|
const c = decodeURIComponent(category)
|
||||||
selectedCategory.value = isNaN(c) ? c : Number(c)
|
selectedCategory.value = isNaN(c) ? c : Number(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedTagsSet = (tags) => {
|
const selectedTagsSet = (tags) => {
|
||||||
const t = Array.isArray(tags) ? tags.join(',') : tags
|
const t = Array.isArray(tags) ? tags.join(',') : tags
|
||||||
selectedTags.value = t
|
selectedTags.value = t
|
||||||
.split(',')
|
.split(',')
|
||||||
.filter((v) => v)
|
.filter((v) => v)
|
||||||
.map((v) => decodeURIComponent(v))
|
.map((v) => decodeURIComponent(v))
|
||||||
.map((v) => (isNaN(v) ? v : Number(v)))
|
.map((v) => (isNaN(v) ? v : Number(v)))
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const query = route.query
|
const query = route.query
|
||||||
const category = query.category
|
const category = query.category
|
||||||
const tags = query.tags
|
const tags = query.tags
|
||||||
@@ -194,9 +178,9 @@ export default {
|
|||||||
if (tags) {
|
if (tags) {
|
||||||
selectedTagsSet(tags)
|
selectedTagsSet(tags)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query,
|
() => route.query,
|
||||||
() => {
|
() => {
|
||||||
const query = route.query
|
const query = route.query
|
||||||
@@ -205,9 +189,9 @@ export default {
|
|||||||
category && selectedCategorySet(category)
|
category && selectedCategorySet(category)
|
||||||
tags && selectedTagsSet(tags)
|
tags && selectedTagsSet(tags)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const loadOptions = async () => {
|
const loadOptions = async () => {
|
||||||
if (selectedCategory.value && !isNaN(selectedCategory.value)) {
|
if (selectedCategory.value && !isNaN(selectedCategory.value)) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/categories/${selectedCategory.value}`)
|
const res = await fetch(`${API_BASE_URL}/api/categories/${selectedCategory.value}`)
|
||||||
@@ -233,9 +217,9 @@ export default {
|
|||||||
}
|
}
|
||||||
tagOptions.value = arr
|
tagOptions.value = arr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildUrl = () => {
|
const buildUrl = () => {
|
||||||
let url = `${API_BASE_URL}/api/posts?page=${page.value}&pageSize=${pageSize}`
|
let url = `${API_BASE_URL}/api/posts?page=${page.value}&pageSize=${pageSize}`
|
||||||
if (selectedCategory.value) {
|
if (selectedCategory.value) {
|
||||||
url += `&categoryId=${selectedCategory.value}`
|
url += `&categoryId=${selectedCategory.value}`
|
||||||
@@ -246,9 +230,9 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildRankUrl = () => {
|
const buildRankUrl = () => {
|
||||||
let url = `${API_BASE_URL}/api/posts/ranking?page=${page.value}&pageSize=${pageSize}`
|
let url = `${API_BASE_URL}/api/posts/ranking?page=${page.value}&pageSize=${pageSize}`
|
||||||
if (selectedCategory.value) {
|
if (selectedCategory.value) {
|
||||||
url += `&categoryId=${selectedCategory.value}`
|
url += `&categoryId=${selectedCategory.value}`
|
||||||
@@ -259,9 +243,9 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildReplyUrl = () => {
|
const buildReplyUrl = () => {
|
||||||
let url = `${API_BASE_URL}/api/posts/latest-reply?page=${page.value}&pageSize=${pageSize}`
|
let url = `${API_BASE_URL}/api/posts/latest-reply?page=${page.value}&pageSize=${pageSize}`
|
||||||
if (selectedCategory.value) {
|
if (selectedCategory.value) {
|
||||||
url += `&categoryId=${selectedCategory.value}`
|
url += `&categoryId=${selectedCategory.value}`
|
||||||
@@ -272,9 +256,9 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchPosts = async (reset = false) => {
|
const fetchPosts = async (reset = false) => {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
page.value = 0
|
page.value = 0
|
||||||
allLoaded.value = false
|
allLoaded.value = false
|
||||||
@@ -315,9 +299,9 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchRanking = async (reset = false) => {
|
const fetchRanking = async (reset = false) => {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
page.value = 0
|
page.value = 0
|
||||||
allLoaded.value = false
|
allLoaded.value = false
|
||||||
@@ -358,9 +342,9 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchLatestReply = async (reset = false) => {
|
const fetchLatestReply = async (reset = false) => {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
page.value = 0
|
page.value = 0
|
||||||
allLoaded.value = false
|
allLoaded.value = false
|
||||||
@@ -401,9 +385,9 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchContent = async (reset = false) => {
|
const fetchContent = async (reset = false) => {
|
||||||
if (selectedTopic.value === '排行榜') {
|
if (selectedTopic.value === '排行榜') {
|
||||||
await fetchRanking(reset)
|
await fetchRanking(reset)
|
||||||
} else if (selectedTopic.value === '最新回复') {
|
} else if (selectedTopic.value === '最新回复') {
|
||||||
@@ -411,36 +395,21 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
await fetchPosts(reset)
|
await fetchPosts(reset)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
useScrollLoadMore(fetchContent)
|
|
||||||
|
|
||||||
watch([selectedCategory, selectedTags], () => {
|
|
||||||
fetchContent(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(selectedTopic, () => {
|
|
||||||
fetchContent(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
const sanitizeDescription = (text) => stripMarkdown(text)
|
|
||||||
|
|
||||||
await Promise.all([loadOptions(), fetchContent()])
|
|
||||||
|
|
||||||
return {
|
|
||||||
topics,
|
|
||||||
selectedTopic,
|
|
||||||
articles,
|
|
||||||
sanitizeDescription,
|
|
||||||
isLoadingPosts,
|
|
||||||
selectedCategory,
|
|
||||||
selectedTags,
|
|
||||||
tagOptions,
|
|
||||||
categoryOptions,
|
|
||||||
isMobile,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useScrollLoadMore(fetchContent)
|
||||||
|
|
||||||
|
watch([selectedCategory, selectedTags], () => {
|
||||||
|
fetchContent(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(selectedTopic, () => {
|
||||||
|
fetchContent(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
const sanitizeDescription = (text) => stripMarkdown(text)
|
||||||
|
|
||||||
|
await Promise.all([loadOptions(), fetchContent()])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -51,8 +51,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { API_BASE_URL, toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { setToken, loadCurrentUser } from '~/utils/auth'
|
import { setToken, loadCurrentUser } from '~/utils/auth'
|
||||||
import { googleAuthorize } from '~/utils/google'
|
import { googleAuthorize } from '~/utils/google'
|
||||||
import { githubAuthorize } from '~/utils/github'
|
import { githubAuthorize } from '~/utils/github'
|
||||||
@@ -60,27 +60,20 @@ import { discordAuthorize } from '~/utils/discord'
|
|||||||
import { twitterAuthorize } from '~/utils/twitter'
|
import { twitterAuthorize } from '~/utils/twitter'
|
||||||
import BaseInput from '~/components/BaseInput.vue'
|
import BaseInput from '~/components/BaseInput.vue'
|
||||||
import { registerPush } from '~/utils/push'
|
import { registerPush } from '~/utils/push'
|
||||||
export default {
|
const config = useRuntimeConfig()
|
||||||
name: 'LoginPageView',
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
components: { BaseInput },
|
|
||||||
setup() {
|
const username = ref('')
|
||||||
return { googleAuthorize }
|
const password = ref('')
|
||||||
},
|
const isWaitingForLogin = ref(false)
|
||||||
data() {
|
|
||||||
return {
|
const submitLogin = async () => {
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
isWaitingForLogin: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async submitLogin() {
|
|
||||||
try {
|
try {
|
||||||
this.isWaitingForLogin = true
|
isWaitingForLogin.value = true
|
||||||
const res = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
const res = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username: this.username, password: this.password }),
|
body: JSON.stringify({ username: username.value, password: password.value }),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok && data.token) {
|
if (res.ok && data.token) {
|
||||||
@@ -88,35 +81,33 @@ export default {
|
|||||||
await loadCurrentUser()
|
await loadCurrentUser()
|
||||||
toast.success('登录成功')
|
toast.success('登录成功')
|
||||||
registerPush()
|
registerPush()
|
||||||
this.$router.push('/')
|
router.push('/')
|
||||||
} else if (data.reason_code === 'NOT_VERIFIED') {
|
} else if (data.reason_code === 'NOT_VERIFIED') {
|
||||||
toast.info('当前邮箱未验证,已经为您重新发送验证码')
|
toast.info('当前邮箱未验证,已经为您重新发送验证码')
|
||||||
this.$router.push({ path: '/signup', query: { verify: 1, u: this.username } })
|
router.push({ path: '/signup', query: { verify: 1, u: username.value } })
|
||||||
} else if (data.reason_code === 'IS_APPROVING') {
|
} else if (data.reason_code === 'IS_APPROVING') {
|
||||||
toast.info('您的注册正在审批中, 请留意邮件')
|
toast.info('您的注册正在审批中, 请留意邮件')
|
||||||
this.$router.push('/')
|
router.push('/')
|
||||||
} else if (data.reason_code === 'NOT_APPROVED') {
|
} else if (data.reason_code === 'NOT_APPROVED') {
|
||||||
this.$router.push('/signup-reason?token=' + data.token)
|
router.push('/signup-reason?token=' + data.token)
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.error || '登录失败')
|
toast.error(data.error || '登录失败')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('登录失败')
|
toast.error('登录失败')
|
||||||
} finally {
|
} finally {
|
||||||
this.isWaitingForLogin = false
|
isWaitingForLogin.value = false
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
loginWithGithub() {
|
const loginWithGithub = () => {
|
||||||
githubAuthorize()
|
githubAuthorize()
|
||||||
},
|
}
|
||||||
loginWithDiscord() {
|
const loginWithDiscord = () => {
|
||||||
discordAuthorize()
|
discordAuthorize()
|
||||||
},
|
}
|
||||||
loginWithTwitter() {
|
const loginWithTwitter = () => {
|
||||||
twitterAuthorize()
|
twitterAuthorize()
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -478,10 +478,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { API_BASE_URL } from '~/main'
|
|
||||||
import BaseTimeline from '~/components/BaseTimeline.vue'
|
import BaseTimeline from '~/components/BaseTimeline.vue'
|
||||||
import BasePlaceholder from '~/components/BasePlaceholder.vue'
|
import BasePlaceholder from '~/components/BasePlaceholder.vue'
|
||||||
import NotificationContainer from '~/components/NotificationContainer.vue'
|
import NotificationContainer from '~/components/NotificationContainer.vue'
|
||||||
@@ -491,26 +490,21 @@ import { toast } from '~/main'
|
|||||||
import { stripMarkdownLength } from '~/utils/markdown'
|
import { stripMarkdownLength } from '~/utils/markdown'
|
||||||
import TimeManager from '~/utils/time'
|
import TimeManager from '~/utils/time'
|
||||||
import { reactionEmojiMap } from '~/utils/reactions'
|
import { reactionEmojiMap } from '~/utils/reactions'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
export default {
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
name: 'MessagePageView',
|
const router = useRouter()
|
||||||
components: { BaseTimeline, BasePlaceholder, NotificationContainer },
|
const route = useRoute()
|
||||||
setup() {
|
const notifications = ref([])
|
||||||
const router = useRouter()
|
const isLoadingMessage = ref(false)
|
||||||
const route = useRoute()
|
const selectedTab = ref(
|
||||||
const notifications = ref([])
|
|
||||||
const isLoadingMessage = ref(false)
|
|
||||||
const selectedTab = ref(
|
|
||||||
['all', 'unread', 'control'].includes(route.query.tab) ? route.query.tab : 'unread',
|
['all', 'unread', 'control'].includes(route.query.tab) ? route.query.tab : 'unread',
|
||||||
)
|
)
|
||||||
const notificationPrefs = ref([])
|
const notificationPrefs = ref([])
|
||||||
const filteredNotifications = computed(() =>
|
const filteredNotifications = computed(() =>
|
||||||
selectedTab.value === 'all'
|
selectedTab.value === 'all' ? notifications.value : notifications.value.filter((n) => !n.read),
|
||||||
? notifications.value
|
)
|
||||||
: notifications.value.filter((n) => !n.read),
|
|
||||||
)
|
|
||||||
|
|
||||||
const markRead = async (id) => {
|
const markRead = async (id) => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
const n = notifications.value.find((n) => n.id === id)
|
const n = notifications.value.find((n) => n.id === id)
|
||||||
if (!n || n.read) return
|
if (!n || n.read) return
|
||||||
@@ -523,9 +517,9 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
fetchUnreadCount()
|
fetchUnreadCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const markAllRead = async () => {
|
const markAllRead = async () => {
|
||||||
// 除了 REGISTER_REQUEST 类型消息
|
// 除了 REGISTER_REQUEST 类型消息
|
||||||
const idsToMark = notifications.value
|
const idsToMark = notifications.value
|
||||||
.filter((n) => n.type !== 'REGISTER_REQUEST' && !n.read)
|
.filter((n) => n.type !== 'REGISTER_REQUEST' && !n.read)
|
||||||
@@ -549,9 +543,9 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
toast.success('已读所有消息')
|
toast.success('已读所有消息')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
POST_VIEWED: 'fas fa-eye',
|
POST_VIEWED: 'fas fa-eye',
|
||||||
COMMENT_REPLY: 'fas fa-reply',
|
COMMENT_REPLY: 'fas fa-reply',
|
||||||
POST_REVIEWED: 'fas fa-shield-alt',
|
POST_REVIEWED: 'fas fa-shield-alt',
|
||||||
@@ -566,9 +560,9 @@ export default {
|
|||||||
REGISTER_REQUEST: 'fas fa-user-clock',
|
REGISTER_REQUEST: 'fas fa-user-clock',
|
||||||
ACTIVITY_REDEEM: 'fas fa-coffee',
|
ACTIVITY_REDEEM: 'fas fa-coffee',
|
||||||
MENTION: 'fas fa-at',
|
MENTION: 'fas fa-at',
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchNotifications = async () => {
|
const fetchNotifications = async () => {
|
||||||
try {
|
try {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -712,13 +706,13 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchPrefs = async () => {
|
const fetchPrefs = async () => {
|
||||||
notificationPrefs.value = await fetchNotificationPreferences()
|
notificationPrefs.value = await fetchNotificationPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
const togglePref = async (pref) => {
|
const togglePref = async (pref) => {
|
||||||
const ok = await updateNotificationPreference(pref.type, !pref.enabled)
|
const ok = await updateNotificationPreference(pref.type, !pref.enabled)
|
||||||
if (ok) {
|
if (ok) {
|
||||||
pref.enabled = !pref.enabled
|
pref.enabled = !pref.enabled
|
||||||
@@ -727,9 +721,9 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const approve = async (id, nid) => {
|
const approve = async (id, nid) => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) return
|
if (!token) return
|
||||||
const res = await fetch(`${API_BASE_URL}/api/admin/users/${id}/approve`, {
|
const res = await fetch(`${API_BASE_URL}/api/admin/users/${id}/approve`, {
|
||||||
@@ -742,9 +736,9 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reject = async (id, nid) => {
|
const reject = async (id, nid) => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) return
|
if (!token) return
|
||||||
const res = await fetch(`${API_BASE_URL}/api/admin/users/${id}/reject`, {
|
const res = await fetch(`${API_BASE_URL}/api/admin/users/${id}/reject`, {
|
||||||
@@ -757,9 +751,9 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error('操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatType = (t) => {
|
const formatType = (t) => {
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case 'POST_VIEWED':
|
case 'POST_VIEWED':
|
||||||
return '帖子被查看'
|
return '帖子被查看'
|
||||||
@@ -794,31 +788,12 @@ export default {
|
|||||||
default:
|
default:
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchNotifications()
|
fetchNotifications()
|
||||||
fetchPrefs()
|
fetchPrefs()
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
|
||||||
notifications,
|
|
||||||
formatType,
|
|
||||||
isLoadingMessage,
|
|
||||||
stripMarkdownLength,
|
|
||||||
markRead,
|
|
||||||
approve,
|
|
||||||
reject,
|
|
||||||
TimeManager,
|
|
||||||
selectedTab,
|
|
||||||
filteredNotifications,
|
|
||||||
markAllRead,
|
|
||||||
authState,
|
|
||||||
notificationPrefs,
|
|
||||||
togglePref,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -88,41 +88,31 @@ import LoginOverlay from '~/components/LoginOverlay.vue'
|
|||||||
import PostEditor from '~/components/PostEditor.vue'
|
import PostEditor from '~/components/PostEditor.vue'
|
||||||
import PostTypeSelect from '~/components/PostTypeSelect.vue'
|
import PostTypeSelect from '~/components/PostTypeSelect.vue'
|
||||||
import TagSelect from '~/components/TagSelect.vue'
|
import TagSelect from '~/components/TagSelect.vue'
|
||||||
import { API_BASE_URL, toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { authState, getToken } from '~/utils/auth'
|
import { authState, getToken } from '~/utils/auth'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
export default {
|
const title = ref('')
|
||||||
name: 'NewPostPageView',
|
const content = ref('')
|
||||||
components: {
|
const selectedCategory = ref('')
|
||||||
PostEditor,
|
const selectedTags = ref([])
|
||||||
CategorySelect,
|
const postType = ref('NORMAL')
|
||||||
TagSelect,
|
const prizeIcon = ref('')
|
||||||
LoginOverlay,
|
const prizeIconFile = ref(null)
|
||||||
PostTypeSelect,
|
const tempPrizeIcon = ref('')
|
||||||
AvatarCropper,
|
const showPrizeCropper = ref(false)
|
||||||
FlatPickr,
|
const prizeName = ref('')
|
||||||
},
|
const prizeCount = ref(1)
|
||||||
setup() {
|
const prizeDescription = ref('')
|
||||||
const title = ref('')
|
const endTime = ref(null)
|
||||||
const content = ref('')
|
const startTime = ref(null)
|
||||||
const selectedCategory = ref('')
|
const dateConfig = { enableTime: true, time_24hr: true, dateFormat: 'Y-m-d H:i' }
|
||||||
const selectedTags = ref([])
|
const isWaitingPosting = ref(false)
|
||||||
const postType = ref('NORMAL')
|
const isAiLoading = ref(false)
|
||||||
const prizeIcon = ref('')
|
const isLogin = computed(() => authState.loggedIn)
|
||||||
const prizeIconFile = ref(null)
|
|
||||||
const tempPrizeIcon = ref('')
|
|
||||||
const showPrizeCropper = ref(false)
|
|
||||||
const prizeName = ref('')
|
|
||||||
const prizeCount = ref(1)
|
|
||||||
const prizeDescription = ref('')
|
|
||||||
const endTime = ref(null)
|
|
||||||
const startTime = ref(null)
|
|
||||||
const dateConfig = { enableTime: true, time_24hr: true, dateFormat: 'Y-m-d H:i' }
|
|
||||||
const isWaitingPosting = ref(false)
|
|
||||||
const isAiLoading = ref(false)
|
|
||||||
const isLogin = computed(() => authState.loggedIn)
|
|
||||||
|
|
||||||
const onPrizeIconChange = (e) => {
|
const onPrizeIconChange = (e) => {
|
||||||
const file = e.target.files[0]
|
const file = e.target.files[0]
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
@@ -132,18 +122,18 @@ export default {
|
|||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPrizeCropped = ({ file, url }) => {
|
const onPrizeCropped = ({ file, url }) => {
|
||||||
prizeIconFile.value = file
|
prizeIconFile.value = file
|
||||||
prizeIcon.value = url
|
prizeIcon.value = url
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(prizeCount, (val) => {
|
watch(prizeCount, (val) => {
|
||||||
if (!val || val < 1) prizeCount.value = 1
|
if (!val || val < 1) prizeCount.value = 1
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadDraft = async () => {
|
const loadDraft = async () => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) return
|
if (!token) return
|
||||||
try {
|
try {
|
||||||
@@ -162,11 +152,11 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadDraft)
|
onMounted(loadDraft)
|
||||||
|
|
||||||
const clearPost = async () => {
|
const clearPost = async () => {
|
||||||
title.value = ''
|
title.value = ''
|
||||||
content.value = ''
|
content.value = ''
|
||||||
selectedCategory.value = ''
|
selectedCategory.value = ''
|
||||||
@@ -196,9 +186,9 @@ export default {
|
|||||||
toast.error('云端草稿清空失败, 请稍后重试')
|
toast.error('云端草稿清空失败, 请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveDraft = async () => {
|
const saveDraft = async () => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) {
|
if (!token) {
|
||||||
toast.error('请先登录')
|
toast.error('请先登录')
|
||||||
@@ -227,8 +217,8 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('保存失败')
|
toast.error('保存失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const ensureTags = async (token) => {
|
const ensureTags = async (token) => {
|
||||||
for (let i = 0; i < selectedTags.value.length; i++) {
|
for (let i = 0; i < selectedTags.value.length; i++) {
|
||||||
const t = selectedTags.value[i]
|
const t = selectedTags.value[i]
|
||||||
if (typeof t === 'string' && t.startsWith('__new__:')) {
|
if (typeof t === 'string' && t.startsWith('__new__:')) {
|
||||||
@@ -257,9 +247,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const aiGenerate = async () => {
|
const aiGenerate = async () => {
|
||||||
if (!content.value.trim()) {
|
if (!content.value.trim()) {
|
||||||
toast.error('内容为空,无法优化')
|
toast.error('内容为空,无法优化')
|
||||||
return
|
return
|
||||||
@@ -289,9 +279,9 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
isAiLoading.value = false
|
isAiLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitPost = async () => {
|
const submitPost = async () => {
|
||||||
if (!title.value.trim()) {
|
if (!title.value.trim()) {
|
||||||
toast.error('标题不能为空')
|
toast.error('标题不能为空')
|
||||||
return
|
return
|
||||||
@@ -391,32 +381,6 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
isWaitingPosting.value = false
|
isWaitingPosting.value = false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
selectedCategory,
|
|
||||||
selectedTags,
|
|
||||||
postType,
|
|
||||||
prizeIcon,
|
|
||||||
prizeCount,
|
|
||||||
endTime,
|
|
||||||
submitPost,
|
|
||||||
saveDraft,
|
|
||||||
clearPost,
|
|
||||||
isWaitingPosting,
|
|
||||||
aiGenerate,
|
|
||||||
isAiLoading,
|
|
||||||
isLogin,
|
|
||||||
onPrizeIconChange,
|
|
||||||
onPrizeCropped,
|
|
||||||
showPrizeCropper,
|
|
||||||
tempPrizeIcon,
|
|
||||||
dateConfig,
|
|
||||||
prizeName,
|
|
||||||
prizeDescription,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -35,33 +35,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import PostEditor from '~/components/PostEditor.vue'
|
import PostEditor from '~/components/PostEditor.vue'
|
||||||
import CategorySelect from '~/components/CategorySelect.vue'
|
import CategorySelect from '~/components/CategorySelect.vue'
|
||||||
import TagSelect from '~/components/TagSelect.vue'
|
import TagSelect from '~/components/TagSelect.vue'
|
||||||
import { API_BASE_URL, toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { getToken, authState } from '~/utils/auth'
|
import { getToken, authState } from '~/utils/auth'
|
||||||
import LoginOverlay from '~/components/LoginOverlay.vue'
|
import LoginOverlay from '~/components/LoginOverlay.vue'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
export default {
|
const title = ref('')
|
||||||
name: 'EditPostPageView',
|
const content = ref('')
|
||||||
components: { PostEditor, CategorySelect, TagSelect, LoginOverlay },
|
const selectedCategory = ref('')
|
||||||
setup() {
|
const selectedTags = ref([])
|
||||||
const title = ref('')
|
const isWaitingPosting = ref(false)
|
||||||
const content = ref('')
|
const isAiLoading = ref(false)
|
||||||
const selectedCategory = ref('')
|
const isLogin = computed(() => authState.loggedIn)
|
||||||
const selectedTags = ref([])
|
|
||||||
const isWaitingPosting = ref(false)
|
|
||||||
const isAiLoading = ref(false)
|
|
||||||
const isLogin = computed(() => authState.loggedIn)
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const postId = route.params.id
|
const postId = route.params.id
|
||||||
|
|
||||||
const loadPost = async () => {
|
const loadPost = async () => {
|
||||||
try {
|
try {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
const res = await fetch(`${API_BASE_URL}/api/posts/${postId}`, {
|
const res = await fetch(`${API_BASE_URL}/api/posts/${postId}`, {
|
||||||
@@ -77,18 +75,18 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('加载失败')
|
toast.error('加载失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadPost)
|
onMounted(loadPost)
|
||||||
|
|
||||||
const clearPost = () => {
|
const clearPost = () => {
|
||||||
title.value = ''
|
title.value = ''
|
||||||
content.value = ''
|
content.value = ''
|
||||||
selectedCategory.value = ''
|
selectedCategory.value = ''
|
||||||
selectedTags.value = []
|
selectedTags.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensureTags = async (token) => {
|
const ensureTags = async (token) => {
|
||||||
for (let i = 0; i < selectedTags.value.length; i++) {
|
for (let i = 0; i < selectedTags.value.length; i++) {
|
||||||
const t = selectedTags.value[i]
|
const t = selectedTags.value[i]
|
||||||
if (typeof t === 'string' && t.startsWith('__new__:')) {
|
if (typeof t === 'string' && t.startsWith('__new__:')) {
|
||||||
@@ -117,9 +115,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const aiGenerate = async () => {
|
const aiGenerate = async () => {
|
||||||
if (!content.value.trim()) {
|
if (!content.value.trim()) {
|
||||||
toast.error('内容为空,无法优化')
|
toast.error('内容为空,无法优化')
|
||||||
return
|
return
|
||||||
@@ -149,9 +147,9 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
isAiLoading.value = false
|
isAiLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitPost = async () => {
|
const submitPost = async () => {
|
||||||
if (!title.value.trim()) {
|
if (!title.value.trim()) {
|
||||||
toast.error('标题不能为空')
|
toast.error('标题不能为空')
|
||||||
return
|
return
|
||||||
@@ -197,24 +195,9 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
isWaitingPosting.value = false
|
isWaitingPosting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const cancelEdit = () => {
|
const cancelEdit = () => {
|
||||||
router.push(`/posts/${postId}`)
|
router.push(`/posts/${postId}`)
|
||||||
}
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
selectedCategory,
|
|
||||||
selectedTags,
|
|
||||||
submitPost,
|
|
||||||
clearPost,
|
|
||||||
cancelEdit,
|
|
||||||
isWaitingPosting,
|
|
||||||
aiGenerate,
|
|
||||||
isAiLoading,
|
|
||||||
isLogin,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -64,95 +64,92 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import AvatarCropper from '~/components/AvatarCropper.vue'
|
import AvatarCropper from '~/components/AvatarCropper.vue'
|
||||||
import BaseInput from '~/components/BaseInput.vue'
|
import BaseInput from '~/components/BaseInput.vue'
|
||||||
import Dropdown from '~/components/Dropdown.vue'
|
import Dropdown from '~/components/Dropdown.vue'
|
||||||
import { API_BASE_URL, toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { fetchCurrentUser, getToken, setToken } from '~/utils/auth'
|
import { fetchCurrentUser, getToken, setToken } from '~/utils/auth'
|
||||||
export default {
|
const config = useRuntimeConfig()
|
||||||
name: 'SettingsPageView',
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
components: { BaseInput, Dropdown, AvatarCropper },
|
const router = useRouter()
|
||||||
data() {
|
const username = ref('')
|
||||||
return {
|
const introduction = ref('')
|
||||||
username: '',
|
const usernameError = ref('')
|
||||||
introduction: '',
|
const avatar = ref('')
|
||||||
usernameError: '',
|
const avatarFile = ref(null)
|
||||||
avatar: '',
|
const tempAvatar = ref('')
|
||||||
avatarFile: null,
|
const showCropper = ref(false)
|
||||||
tempAvatar: '',
|
const role = ref('')
|
||||||
showCropper: false,
|
const publishMode = ref('DIRECT')
|
||||||
role: '',
|
const passwordStrength = ref('LOW')
|
||||||
publishMode: 'DIRECT',
|
const aiFormatLimit = ref(3)
|
||||||
passwordStrength: 'LOW',
|
const registerMode = ref('DIRECT')
|
||||||
aiFormatLimit: 3,
|
const isLoadingPage = ref(false)
|
||||||
registerMode: 'DIRECT',
|
const isSaving = ref(false)
|
||||||
isLoadingPage: false,
|
|
||||||
isSaving: false,
|
onMounted(async () => {
|
||||||
}
|
isLoadingPage.value = true
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
this.isLoadingPage = true
|
|
||||||
const user = await fetchCurrentUser()
|
const user = await fetchCurrentUser()
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
this.username = user.username
|
username.value = user.username
|
||||||
this.introduction = user.introduction || ''
|
introduction.value = user.introduction || ''
|
||||||
this.avatar = user.avatar
|
avatar.value = user.avatar
|
||||||
this.role = user.role
|
role.value = user.role
|
||||||
if (this.role === 'ADMIN') {
|
if (role.value === 'ADMIN') {
|
||||||
this.loadAdminConfig()
|
loadAdminConfig()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error('请先登录')
|
toast.error('请先登录')
|
||||||
this.$router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
this.isLoadingPage = false
|
isLoadingPage.value = false
|
||||||
},
|
})
|
||||||
methods: {
|
|
||||||
onAvatarChange(e) {
|
const onAvatarChange = (e) => {
|
||||||
const file = e.target.files[0]
|
const file = e.target.files[0]
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
this.tempAvatar = reader.result
|
tempAvatar.value = reader.result
|
||||||
this.showCropper = true
|
showCropper.value = true
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onCropped({ file, url }) {
|
const onCropped = ({ file, url }) => {
|
||||||
this.avatarFile = file
|
avatarFile.value = file
|
||||||
this.avatar = url
|
avatar.value = url
|
||||||
},
|
}
|
||||||
fetchPublishModes() {
|
const fetchPublishModes = () => {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{ id: 'DIRECT', name: '直接发布', icon: 'fas fa-bolt' },
|
{ id: 'DIRECT', name: '直接发布', icon: 'fas fa-bolt' },
|
||||||
{ id: 'REVIEW', name: '审核后发布', icon: 'fas fa-search' },
|
{ id: 'REVIEW', name: '审核后发布', icon: 'fas fa-search' },
|
||||||
])
|
])
|
||||||
},
|
}
|
||||||
fetchPasswordStrengths() {
|
const fetchPasswordStrengths = () => {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{ id: 'LOW', name: '低', icon: 'fas fa-lock-open' },
|
{ id: 'LOW', name: '低', icon: 'fas fa-lock-open' },
|
||||||
{ id: 'MEDIUM', name: '中', icon: 'fas fa-lock' },
|
{ id: 'MEDIUM', name: '中', icon: 'fas fa-lock' },
|
||||||
{ id: 'HIGH', name: '高', icon: 'fas fa-user-shield' },
|
{ id: 'HIGH', name: '高', icon: 'fas fa-user-shield' },
|
||||||
])
|
])
|
||||||
},
|
}
|
||||||
fetchAiLimits() {
|
const fetchAiLimits = () => {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{ id: 3, name: '3次' },
|
{ id: 3, name: '3次' },
|
||||||
{ id: 5, name: '5次' },
|
{ id: 5, name: '5次' },
|
||||||
{ id: 10, name: '10次' },
|
{ id: 10, name: '10次' },
|
||||||
{ id: -1, name: '无限' },
|
{ id: -1, name: '无限' },
|
||||||
])
|
])
|
||||||
},
|
}
|
||||||
fetchRegisterModes() {
|
const fetchRegisterModes = () => {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{ id: 'DIRECT', name: '直接注册', icon: 'fas fa-user-check' },
|
{ id: 'DIRECT', name: '直接注册', icon: 'fas fa-user-check' },
|
||||||
{ id: 'WHITELIST', name: '白名单邀请制', icon: 'fas fa-envelope' },
|
{ id: 'WHITELIST', name: '白名单邀请制', icon: 'fas fa-envelope' },
|
||||||
])
|
])
|
||||||
},
|
}
|
||||||
async loadAdminConfig() {
|
const loadAdminConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
const res = await fetch(`${API_BASE_URL}/api/admin/config`, {
|
const res = await fetch(`${API_BASE_URL}/api/admin/config`, {
|
||||||
@@ -160,31 +157,31 @@ export default {
|
|||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
this.publishMode = data.publishMode
|
publishMode.value = data.publishMode
|
||||||
this.passwordStrength = data.passwordStrength
|
passwordStrength.value = data.passwordStrength
|
||||||
this.aiFormatLimit = data.aiFormatLimit
|
aiFormatLimit.value = data.aiFormatLimit
|
||||||
this.registerMode = data.registerMode
|
registerMode.value = data.registerMode
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
async save() {
|
const save = async () => {
|
||||||
this.isSaving = true
|
isSaving.value = true
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let token = getToken()
|
let token = getToken()
|
||||||
this.usernameError = ''
|
usernameError.value = ''
|
||||||
if (!this.username) {
|
if (!username.value) {
|
||||||
this.usernameError = '用户名不能为空'
|
usernameError.value = '用户名不能为空'
|
||||||
}
|
}
|
||||||
if (this.usernameError) {
|
if (usernameError.value) {
|
||||||
toast.error(this.usernameError)
|
toast.error(usernameError.value)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (this.avatarFile) {
|
if (avatarFile.value) {
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
form.append('file', this.avatarFile)
|
form.append('file', avatarFile.value)
|
||||||
const res = await fetch(`${API_BASE_URL}/api/users/me/avatar`, {
|
const res = await fetch(`${API_BASE_URL}/api/users/me/avatar`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
@@ -192,7 +189,7 @@ export default {
|
|||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.avatar = data.url
|
avatar.value = data.url
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.error || '上传失败')
|
toast.error(data.error || '上传失败')
|
||||||
break
|
break
|
||||||
@@ -201,7 +198,7 @@ export default {
|
|||||||
const res = await fetch(`${API_BASE_URL}/api/users/me`, {
|
const res = await fetch(`${API_BASE_URL}/api/users/me`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||||
body: JSON.stringify({ username: this.username, introduction: this.introduction }),
|
body: JSON.stringify({ username: username.value, introduction: introduction.value }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
@@ -213,24 +210,22 @@ export default {
|
|||||||
setToken(data.token)
|
setToken(data.token)
|
||||||
token = data.token
|
token = data.token
|
||||||
}
|
}
|
||||||
if (this.role === 'ADMIN') {
|
if (role.value === 'ADMIN') {
|
||||||
await fetch(`${API_BASE_URL}/api/admin/config`, {
|
await fetch(`${API_BASE_URL}/api/admin/config`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
publishMode: this.publishMode,
|
publishMode: publishMode.value,
|
||||||
passwordStrength: this.passwordStrength,
|
passwordStrength: passwordStrength.value,
|
||||||
aiFormatLimit: this.aiFormatLimit,
|
aiFormatLimit: aiFormatLimit.value,
|
||||||
registerMode: this.registerMode,
|
registerMode: registerMode.value,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
toast.success('保存成功')
|
toast.success('保存成功')
|
||||||
} while (!this.isSaving)
|
} while (!isSaving.value)
|
||||||
|
|
||||||
this.isSaving = false
|
isSaving.value = false
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -18,36 +18,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import BaseInput from '~/components/BaseInput.vue'
|
import BaseInput from '~/components/BaseInput.vue'
|
||||||
import { API_BASE_URL, toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
export default {
|
const reason = ref('')
|
||||||
name: 'SignupReasonPageView',
|
const error = ref('')
|
||||||
components: { BaseInput },
|
const isWaitingForRegister = ref(false)
|
||||||
data() {
|
const token = ref('')
|
||||||
return {
|
|
||||||
reason: '',
|
onMounted(() => {
|
||||||
error: '',
|
token.value = route.query.token || ''
|
||||||
isWaitingForRegister: false,
|
if (!token.value) {
|
||||||
token: '',
|
router.push('/signup')
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
mounted() {
|
|
||||||
this.token = this.$route.query.token || ''
|
const submit = async () => {
|
||||||
if (!this.token) {
|
if (!reason.value || reason.value.trim().length < 20) {
|
||||||
this.$router.push('/signup')
|
error.value = '请至少输入20个字'
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async submit() {
|
|
||||||
if (!this.reason || this.reason.trim().length < 20) {
|
|
||||||
this.error = '请至少输入20个字'
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.isWaitingForRegister = true
|
isWaitingForRegister.value = true
|
||||||
const res = await fetch(`${API_BASE_URL}/api/auth/reason`, {
|
const res = await fetch(`${API_BASE_URL}/api/auth/reason`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -58,23 +54,21 @@ export default {
|
|||||||
reason: this.reason,
|
reason: this.reason,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
this.isWaitingForRegister = false
|
isWaitingForRegister.value = false
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success('注册理由已提交,请等待审核')
|
toast.success('注册理由已提交,请等待审核')
|
||||||
this.$router.push('/')
|
router.push('/')
|
||||||
} else if (data.reason_code === 'INVALID_CREDENTIALS') {
|
} else if (data.reason_code === 'INVALID_CREDENTIALS') {
|
||||||
toast.error('登录已过期,请重新登录')
|
toast.error('登录已过期,请重新登录')
|
||||||
this.$router.push('/login')
|
router.push('/login')
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.error || '提交失败')
|
toast.error(data.error || '提交失败')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.isWaitingForRegister = false
|
isWaitingForRegister.value = false
|
||||||
toast.error('提交失败')
|
toast.error('提交失败')
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -89,115 +89,110 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import BaseInput from '~/components/BaseInput.vue'
|
import BaseInput from '~/components/BaseInput.vue'
|
||||||
import { API_BASE_URL, toast } from '~/main'
|
import { toast } from '~/main'
|
||||||
import { discordAuthorize } from '~/utils/discord'
|
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'
|
||||||
export default {
|
const config = useRuntimeConfig()
|
||||||
name: 'SignupPageView',
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
components: { BaseInput },
|
const emailStep = ref(0)
|
||||||
setup() {
|
const email = ref('')
|
||||||
return { googleAuthorize }
|
const username = ref('')
|
||||||
},
|
const password = ref('')
|
||||||
data() {
|
const registerMode = ref('DIRECT')
|
||||||
return {
|
const emailError = ref('')
|
||||||
emailStep: 0,
|
const usernameError = ref('')
|
||||||
email: '',
|
const passwordError = ref('')
|
||||||
username: '',
|
const code = ref('')
|
||||||
password: '',
|
const isWaitingForEmailSent = ref(false)
|
||||||
registerMode: 'DIRECT',
|
const isWaitingForEmailVerified = ref(false)
|
||||||
emailError: '',
|
|
||||||
usernameError: '',
|
onMounted(async () => {
|
||||||
passwordError: '',
|
username.value = route.query.u || ''
|
||||||
code: '',
|
|
||||||
isWaitingForEmailSent: false,
|
|
||||||
isWaitingForEmailVerified: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
this.username = this.$route.query.u || ''
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/config`)
|
const res = await fetch(`${API_BASE_URL}/api/config`)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
this.registerMode = data.registerMode
|
registerMode.value = data.registerMode
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
if (this.$route.query.verify) {
|
if (route.query.verify) {
|
||||||
this.emailStep = 1
|
emailStep.value = 1
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
methods: {
|
|
||||||
clearErrors() {
|
const clearErrors = () => {
|
||||||
this.emailError = ''
|
emailError.value = ''
|
||||||
this.usernameError = ''
|
usernameError.value = ''
|
||||||
this.passwordError = ''
|
passwordError.value = ''
|
||||||
},
|
}
|
||||||
async sendVerification() {
|
|
||||||
this.clearErrors()
|
const sendVerification = async () => {
|
||||||
|
clearErrors()
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
if (!emailRegex.test(this.email)) {
|
if (!emailRegex.test(email.value)) {
|
||||||
this.emailError = '邮箱格式不正确'
|
emailError.value = '邮箱格式不正确'
|
||||||
}
|
}
|
||||||
if (!this.password || this.password.length < 6) {
|
if (!password.value || password.value.length < 6) {
|
||||||
this.passwordError = '密码至少6位'
|
passwordError.value = '密码至少6位'
|
||||||
}
|
}
|
||||||
if (!this.username) {
|
if (!username.value) {
|
||||||
this.usernameError = '用户名不能为空'
|
usernameError.value = '用户名不能为空'
|
||||||
}
|
}
|
||||||
if (this.emailError || this.passwordError || this.usernameError) {
|
if (emailError.value || passwordError.value || usernameError.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.isWaitingForEmailSent = true
|
isWaitingForEmailSent.value = true
|
||||||
const res = await fetch(`${API_BASE_URL}/api/auth/register`, {
|
const res = await fetch(`${API_BASE_URL}/api/auth/register`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username: this.username,
|
username: username.value,
|
||||||
email: this.email,
|
email: email.value,
|
||||||
password: this.password,
|
password: password.value,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
this.isWaitingForEmailSent = false
|
isWaitingForEmailSent.value = false
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.emailStep = 1
|
emailStep.value = 1
|
||||||
toast.success('验证码已发送,请查看邮箱')
|
toast.success('验证码已发送,请查看邮箱')
|
||||||
} else if (data.field) {
|
} else if (data.field) {
|
||||||
if (data.field === 'username') this.usernameError = data.error
|
if (data.field === 'username') usernameError.value = data.error
|
||||||
if (data.field === 'email') this.emailError = data.error
|
if (data.field === 'email') emailError.value = data.error
|
||||||
if (data.field === 'password') this.passwordError = data.error
|
if (data.field === 'password') passwordError.value = data.error
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.error || '发送失败')
|
toast.error(data.error || '发送失败')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('发送失败')
|
toast.error('发送失败')
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
async verifyCode() {
|
|
||||||
|
const verifyCode = async () => {
|
||||||
try {
|
try {
|
||||||
this.isWaitingForEmailVerified = true
|
isWaitingForEmailVerified.value = true
|
||||||
const res = await fetch(`${API_BASE_URL}/api/auth/verify`, {
|
const res = await fetch(`${API_BASE_URL}/api/auth/verify`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
code: this.code,
|
code: code.value,
|
||||||
username: this.username,
|
username: username.value,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
if (this.registerMode === 'WHITELIST') {
|
if (registerMode.value === 'WHITELIST') {
|
||||||
this.$router.push('/signup-reason?token=' + data.token)
|
router.push('/signup-reason?token=' + data.token)
|
||||||
} else {
|
} else {
|
||||||
toast.success('注册成功,请登录')
|
toast.success('注册成功,请登录')
|
||||||
this.$router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.error || '注册失败')
|
toast.error(data.error || '注册失败')
|
||||||
@@ -205,19 +200,17 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error('注册失败')
|
toast.error('注册失败')
|
||||||
} finally {
|
} finally {
|
||||||
this.isWaitingForEmailVerified = false
|
isWaitingForEmailVerified.value = false
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
signupWithGithub() {
|
const signupWithGithub = () => {
|
||||||
githubAuthorize()
|
githubAuthorize()
|
||||||
},
|
}
|
||||||
signupWithDiscord() {
|
const signupWithDiscord = () => {
|
||||||
discordAuthorize()
|
discordAuthorize()
|
||||||
},
|
}
|
||||||
signupWithTwitter() {
|
const signupWithTwitter = () => {
|
||||||
twitterAuthorize()
|
twitterAuthorize()
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user