feat: add loading
This commit is contained in:
@@ -2,4 +2,5 @@
|
|||||||
<router-view />
|
<router-view />
|
||||||
<Toast position="bottom-right" />
|
<Toast position="bottom-right" />
|
||||||
<ConfirmDialog style="width: min(500px, 92vw)" />
|
<ConfirmDialog style="width: min(500px, 92vw)" />
|
||||||
|
<LoadingOverlay />
|
||||||
</template>
|
</template>
|
||||||
Vendored
-5
@@ -30,11 +30,6 @@ declare module 'vue' {
|
|||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
Select: typeof import('primevue/select')['default']
|
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']
|
Tag: typeof import('primevue/tag')['default']
|
||||||
Textarea: typeof import('primevue/textarea')['default']
|
Textarea: typeof import('primevue/textarea')['default']
|
||||||
Toast: typeof import('primevue/toast')['default']
|
Toast: typeof import('primevue/toast')['default']
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
@@ -1,4 +1,7 @@
|
|||||||
import axios, { type AxiosInstance } from 'axios'
|
import axios, { type AxiosInstance } from 'axios'
|
||||||
|
import { useLoading } from '../composables/useLoading'
|
||||||
|
|
||||||
|
const { start: loadingStart, stop: loadingStop } = useLoading()
|
||||||
|
|
||||||
export const api: AxiosInstance = axios.create({
|
export const api: AxiosInstance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:2000',
|
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) => {
|
api.interceptors.request.use((config) => {
|
||||||
|
loadingStart()
|
||||||
const token = localStorage.getItem('mws_token')
|
const token = localStorage.getItem('mws_token')
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
@@ -16,8 +20,12 @@ api.interceptors.request.use((config) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => {
|
||||||
|
loadingStop()
|
||||||
|
return response
|
||||||
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
loadingStop()
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
localStorage.removeItem('mws_token')
|
localStorage.removeItem('mws_token')
|
||||||
localStorage.removeItem('mws_user')
|
localStorage.removeItem('mws_user')
|
||||||
|
|||||||
@@ -29,10 +29,10 @@
|
|||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onDragStart(task)"
|
@dragstart="onDragStart(task)"
|
||||||
@dragend="onDragEnd"
|
@dragend="onDragEnd"
|
||||||
@click.stop="toggleDesc(task.id)"
|
@click.stop="openEdit(task)"
|
||||||
>
|
>
|
||||||
<div class="font-medium">{{ task.title }}</div>
|
<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">
|
<div class="mt-2 text-[0.8rem]" v-if="task.assigneeName">
|
||||||
<span class="flex items-center gap-1 muted-note">
|
<span class="flex items-center gap-1 muted-note">
|
||||||
<i class="pi pi-user text-[0.7rem]"></i> {{ task.assigneeName }}
|
<i class="pi pi-user text-[0.7rem]"></i> {{ task.assigneeName }}
|
||||||
@@ -80,7 +80,7 @@ const route = useRoute()
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
const projectId = String(route.params.id)
|
const projectId = computed(() => String(route.params.id))
|
||||||
const tasks = ref<Task[]>([])
|
const tasks = ref<Task[]>([])
|
||||||
const members = ref<{ userId: string; displayName: string }[]>([])
|
const members = ref<{ userId: string; displayName: string }[]>([])
|
||||||
|
|
||||||
@@ -89,13 +89,10 @@ const dragOverStatus = ref<TaskStatus | null>(null)
|
|||||||
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const editingTask = ref<Task | null>(null)
|
const editingTask = ref<Task | null>(null)
|
||||||
const expandedDescs = ref(new Set<string>())
|
|
||||||
|
|
||||||
function toggleDesc(id: string) {
|
function openEdit(task: Task) {
|
||||||
const s = new Set(expandedDescs.value)
|
editingTask.value = task
|
||||||
if (s.has(id)) s.delete(id)
|
dialogVisible.value = true
|
||||||
else s.add(id)
|
|
||||||
expandedDescs.value = s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -111,7 +108,7 @@ function tasksIn(status: TaskStatus) {
|
|||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const res = await getTasks(projectId, undefined, 1, 100)
|
const res = await getTasks(projectId.value, undefined, 1, 100)
|
||||||
tasks.value = res.items
|
tasks.value = res.items
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||||
@@ -120,7 +117,7 @@ async function load() {
|
|||||||
|
|
||||||
async function loadMembers() {
|
async function loadMembers() {
|
||||||
try {
|
try {
|
||||||
const res = await getMembers(projectId, 1, 100)
|
const res = await getMembers(projectId.value, 1, 100)
|
||||||
members.value = res.items.map((m) => ({
|
members.value = res.items.map((m) => ({
|
||||||
userId: m.userId,
|
userId: m.userId,
|
||||||
displayName: m.displayName,
|
displayName: m.displayName,
|
||||||
@@ -196,10 +193,14 @@ function formatDate(v: string) {
|
|||||||
return new Date(v).toLocaleDateString()
|
return new Date(v).toLocaleDateString()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
watch(
|
||||||
await loadMembers()
|
() => route.params.id,
|
||||||
await load()
|
async () => {
|
||||||
})
|
await loadMembers()
|
||||||
|
await load()
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user