add composition api

This commit is contained in:
Sergey Krylov 2025-01-16 08:00:54 +03:00
parent 0eafaeb27f
commit 67466483ac
7 changed files with 240 additions and 0 deletions

View File

@ -18,6 +18,10 @@
<router-link to="/store"> <router-link to="/store">
<my-button>Посты Vuex</my-button> <my-button>Посты Vuex</my-button>
</router-link> </router-link>
<router-link to="/composition">
<my-button>Посты Composition</my-button>
</router-link>
</div> </div>
</div> </div>
</template> </template>

View File

@ -0,0 +1,22 @@
import { ref, watch } from 'vue';
export default function useCreateDialog() {
const dialogVisible = ref(false);
const showCreateDialog = () => {
dialogVisible.value = true;
}
watch(dialogVisible, (newValue) => {
if (newValue) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'auto';
}
})
return {
dialogVisible,
showCreateDialog
}
}

55
src/hooks/usePosts.js Normal file
View File

@ -0,0 +1,55 @@
import axios from 'axios';
import { onMounted, ref } from 'vue';
export default function usePosts({page, limit}) {
const posts = ref([]);
const totalPages = ref(0);
const isPostLoading = ref(true);
const fetchPosts = async () => {
try {
isPostLoading.value = true;
const { data, headers } = await axios.get(`https://jsonplaceholder.typicode.com/posts`, {
params: {
_limit: limit.value,
_page: page.value
}
});
posts.value = data;
totalPages.value = Math.ceil(headers['x-total-count'] / limit.value);
} catch (e) {
alert(e.message);
} finally {
isPostLoading.value = false;
}
}
const loadMorePost = async () => {
page.value += 1;
try {
const { data, headers } = await axios.get(`https://jsonplaceholder.typicode.com/posts`, {
params: {
_limit: limit.value,
_page: page.value,
}
});
posts.value.push(...data);
totalPages.value = Math.ceil(headers['x-total-count'] / limit.value);
} catch (e) {
alert(e.message);
}
};
onMounted(fetchPosts);
return {
posts,
totalPages,
isPostLoading,
fetchPosts,
loadMorePost,
page,
}
}

View File

@ -0,0 +1,14 @@
import { computed, ref } from 'vue';
export default function useSortedAndSearchedPosts (sortedPosts) {
const searchQuery = ref('');
const sortedAndSearchedPosts = computed(() => {
return sortedPosts.value.filter((post) => post.title.toLowerCase().includes(searchQuery.value.toLowerCase()))
})
return {
searchQuery,
sortedAndSearchedPosts
}
}

View File

@ -0,0 +1,16 @@
import { computed, ref } from 'vue';
export default function useSortedPosts (posts) {
const selectedSort = ref('');
const sortedPosts = computed(() => {
return [...posts.value].sort((post1, post2) => {
return post1?.[selectedSort.value]?.localeCompare(post2?.[selectedSort.value])
})
})
return {
selectedSort,
sortedPosts
}
}

View File

@ -0,0 +1,124 @@
<template>
<div>
<h1>Страница постов (Composition API)</h1>
<my-input v-focus v-model="searchQuery" placeholder="Поиск" class="search"/>
<div class="app__btns">
<my-button @click="showCreateDialog" class="createBtn">
Создать пост
</my-button>
<my-select v-model="selectedSort" :options="sortOptions"/>
</div>
<my-dialog v-model:show="dialogVisible">
<post-form @createPost="createPost" />
</my-dialog>
<div v-if="isPostLoading" style="margin-top: 15px">Loading...</div>
<post-list v-else :posts="sortedAndSearchedPosts" @removePost="removePost" class="postList"/>
<div v-intersection="getIntersectionData()" ref="observer" class="observer"></div>
</div>
</template>
<script>
import PostForm from '@/components/PostForm.vue';
import PostList from '@/components/PostList.vue';
import axios from 'axios';
import MyPagination from '@/components/ui/Pagination.vue';
import { ref } from 'vue';
import MyButton from '@/components/ui/Button.vue';
import usePosts from '@/hooks/usePosts';
import useSortedPosts from '@/hooks/useSortedPosts';
import useSortedAndSearchedPosts from '@/hooks/useSortedAndSearchedPosts';
import useCreateDialog from '@/hooks/useCreateDialog';
export default {
components: {
MyButton,
MyPagination,
PostForm,
PostList
},
data() {
return {
dialogVisible: false,
sortOptions: [
{value: 'title', name: 'По названию'},
{value: 'body', name: 'По описанию'}
],
}
},
methods: {
getIntersectionData() {
return {
loadMorePost: this.loadMorePost,
page: this.page.value,
totalPages: this.totalPages.value,
}
},
setSelectedSort(newValue) {
console.log('selectedSort', this.selectedSort);
this.selectedSort.value = newValue;
}
},
setup(props) {
const page = ref(1);
const limit = 10;
const { isPostLoading, posts, totalPages, loadMorePost } = usePosts({ page, limit });
const { selectedSort, sortedPosts } = useSortedPosts(posts);
const { searchQuery, sortedAndSearchedPosts } = useSortedAndSearchedPosts(sortedPosts);
const {dialogVisible, showCreateDialog} = useCreateDialog();
const createPost = (newPost) => {
posts.value.push(newPost);
dialogVisible.value = false;
}
const removePost = (post) => {
posts.value = posts.value.filter(p => p.id !== post.id)
}
return {
isPostLoading,
posts,
totalPages,
selectedSort,
searchQuery,
sortedAndSearchedPosts,
dialogVisible,
showCreateDialog,
createPost,
removePost,
loadMorePost,
page,
}
}
}
</script>
<style>
.createBtn {
margin-top: 15px;
}
.postList {
margin-top: 15px;
}
.app__btns {
display: flex;
justify-content: space-between;
align-items: center;
}
.search {
margin-top: 15px;
}
.observer {
height: 0px;
background: teal;
}
</style>

View File

@ -4,6 +4,7 @@ import PostsPage from '@/pages/PostsPage.vue';
import AboutPage from '@/pages/AboutPage.vue'; import AboutPage from '@/pages/AboutPage.vue';
import PostPage from '@/pages/PostPage.vue'; import PostPage from '@/pages/PostPage.vue';
import PostsPageWithStore from '@/pages/PostsPageWithStore.vue'; import PostsPageWithStore from '@/pages/PostsPageWithStore.vue';
import PostsPageWithCompositionApi from '@/pages/PostsPageWithCompositionApi.vue';
const routes = [ const routes = [
{ {
@ -25,6 +26,10 @@ const routes = [
{ {
path: '/store', path: '/store',
component: PostsPageWithStore, component: PostsPageWithStore,
},
{
path: '/composition',
component: PostsPageWithCompositionApi,
} }
] ]