feat: add loading

This commit is contained in:
2026-09-06 23:07:57 +07:00
parent 5f52a6c554
commit 2e03b22b82
6 changed files with 82 additions and 21 deletions
+1
View File
@@ -2,4 +2,5 @@
<router-view />
<Toast position="bottom-right" />
<ConfirmDialog style="width: min(500px, 92vw)" />
<LoadingOverlay />
</template>
-5
View File
@@ -30,11 +30,6 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Select: typeof import('primevue/select')['default']
Tab: typeof import('primevue/tab')['default']
TabList: typeof import('primevue/tablist')['default']
TabPanel: typeof import('primevue/tabpanel')['default']
TabPanels: typeof import('primevue/tabpanels')['default']
Tabs: typeof import('primevue/tabs')['default']
Tag: typeof import('primevue/tag')['default']
Textarea: typeof import('primevue/textarea')['default']
Toast: typeof import('primevue/toast')['default']
+34
View File
@@ -0,0 +1,34 @@
<template>
<Teleport to="body">
<Transition name="fade">
<div
v-if="loading"
class="fixed inset-0 z-9999 flex items-center justify-center bg-white/70 dark:bg-black/60 backdrop-blur-sm"
>
<ProgressSpinner
strokeWidth="4"
fill="transparent"
animationDuration=".8s"
:style="{ width: '48px', height: '48px' }"
/>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { useLoading } from '../composables/useLoading'
const { loading } = useLoading()
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
+22
View File
@@ -0,0 +1,22 @@
import { ref } from 'vue'
const pendingCount = ref(0)
const loading = ref(false)
export function useLoading() {
function start() {
pendingCount.value++
loading.value = true
}
function stop() {
if (pendingCount.value > 0) {
pendingCount.value--
}
if (pendingCount.value === 0) {
loading.value = false
}
}
return { loading, start, stop }
}
+9 -1
View File
@@ -1,4 +1,7 @@
import axios, { type AxiosInstance } from 'axios'
import { useLoading } from '../composables/useLoading'
const { start: loadingStart, stop: loadingStop } = useLoading()
export const api: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:2000',
@@ -8,6 +11,7 @@ export const api: AxiosInstance = axios.create({
})
api.interceptors.request.use((config) => {
loadingStart()
const token = localStorage.getItem('mws_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
@@ -16,8 +20,12 @@ api.interceptors.request.use((config) => {
})
api.interceptors.response.use(
(response) => response,
(response) => {
loadingStop()
return response
},
(error) => {
loadingStop()
if (error.response?.status === 401) {
localStorage.removeItem('mws_token')
localStorage.removeItem('mws_user')
+14 -13
View File
@@ -29,10 +29,10 @@
draggable="true"
@dragstart="onDragStart(task)"
@dragend="onDragEnd"
@click.stop="toggleDesc(task.id)"
@click.stop="openEdit(task)"
>
<div class="font-medium">{{ task.title }}</div>
<div v-if="expandedDescs.has(task.id) && task.description" class="muted-note mt-1 line-clamp-2 text-[0.85rem]">{{ task.description }}</div>
<div v-if="task.description" class="muted-note mt-1 line-clamp-2 text-[0.85rem]">{{ task.description }}</div>
<div class="mt-2 text-[0.8rem]" v-if="task.assigneeName">
<span class="flex items-center gap-1 muted-note">
<i class="pi pi-user text-[0.7rem]"></i> {{ task.assigneeName }}
@@ -80,7 +80,7 @@ const route = useRoute()
const toast = useToast()
const auth = useAuthStore()
const projectId = String(route.params.id)
const projectId = computed(() => String(route.params.id))
const tasks = ref<Task[]>([])
const members = ref<{ userId: string; displayName: string }[]>([])
@@ -89,13 +89,10 @@ const dragOverStatus = ref<TaskStatus | null>(null)
const dialogVisible = ref(false)
const editingTask = ref<Task | null>(null)
const expandedDescs = ref(new Set<string>())
function toggleDesc(id: string) {
const s = new Set(expandedDescs.value)
if (s.has(id)) s.delete(id)
else s.add(id)
expandedDescs.value = s
function openEdit(task: Task) {
editingTask.value = task
dialogVisible.value = true
}
const columns = [
@@ -111,7 +108,7 @@ function tasksIn(status: TaskStatus) {
async function load() {
try {
const res = await getTasks(projectId, undefined, 1, 100)
const res = await getTasks(projectId.value, undefined, 1, 100)
tasks.value = res.items
} catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
@@ -120,7 +117,7 @@ async function load() {
async function loadMembers() {
try {
const res = await getMembers(projectId, 1, 100)
const res = await getMembers(projectId.value, 1, 100)
members.value = res.items.map((m) => ({
userId: m.userId,
displayName: m.displayName,
@@ -196,10 +193,14 @@ function formatDate(v: string) {
return new Date(v).toLocaleDateString()
}
onMounted(async () => {
watch(
() => route.params.id,
async () => {
await loadMembers()
await load()
})
},
{ immediate: true },
)
</script>
<style scoped>