mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-09 14:31:35 +08:00
person card
This commit is contained in:
@@ -43,7 +43,7 @@ function getParams() {
|
||||
return params
|
||||
}
|
||||
|
||||
// 获取订阅列表数据
|
||||
// 获取列表数据
|
||||
async function fetchData({ done }: { done: any }) {
|
||||
try {
|
||||
if (!props.apipath)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PersonCardSlideView from './PersonCardSlideView.vue'
|
||||
import MediaCardSlideView from './MediaCardSlideView.vue'
|
||||
import api from '@/api'
|
||||
import type { MediaInfo } from '@/api/types'
|
||||
@@ -39,72 +40,73 @@ onMounted(() => {
|
||||
style="background-image: linear-gradient(180deg, rgba(17, 24, 39, 47%) 0%, rgba(17, 24, 39, 100%) 100%);"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-header">
|
||||
<div class="media-poster">
|
||||
<VImg :src="mediaDetail.poster_path" cover />
|
||||
</div>
|
||||
<div class="media-title">
|
||||
<div class="media-status" />
|
||||
<h1 class="media-title">
|
||||
{{ mediaDetail.title }}
|
||||
<span class="media-year">
|
||||
({{ mediaDetail.year }})
|
||||
</span>
|
||||
</h1>
|
||||
<span class="media-attributes">
|
||||
<span>分级</span>
|
||||
<span>时长</span>
|
||||
<span>风格</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-actions" />
|
||||
</div>
|
||||
<div class="media-overview">
|
||||
<div class="media-overview-left">
|
||||
<div class="tagline">
|
||||
标签
|
||||
<div class="media-header">
|
||||
<div class="media-poster">
|
||||
<VImg :src="mediaDetail.poster_path" cover />
|
||||
</div>
|
||||
<h2>简介</h2>
|
||||
<p>{{ mediaDetail.overview }}</p>
|
||||
<div class="media-title">
|
||||
<div class="media-status" />
|
||||
<h1 class="media-title">
|
||||
{{ mediaDetail.title }}
|
||||
<span class="media-year">
|
||||
({{ mediaDetail.year }})
|
||||
</span>
|
||||
</h1>
|
||||
<span class="media-attributes">
|
||||
<span>{{ mediaDetail.runtime }}</span>
|
||||
<span>{{ mediaDetail.genres }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-actions" />
|
||||
</div>
|
||||
<div class="media-overview-right" />
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<MediaCardSlideView :apipath="`tmdb/credits/${mediaDetail.tmdb_id}/${mediaProps.type}`">
|
||||
<template #title="{ loaded }">
|
||||
<div v-if="loaded" class="slider-header mt-3 ms-1">
|
||||
<RouterLink to="" class="slider-title">
|
||||
<span>演员阵容</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
</RouterLink>
|
||||
<div class="media-overview">
|
||||
<div class="media-overview-left">
|
||||
<div v-if="mediaDetail.tagline" class="tagline">
|
||||
{{ mediaDetail.tagline }}
|
||||
</div>
|
||||
</template>
|
||||
</MediaCardSlideView>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<MediaCardSlideView :apipath="`tmdb/recommend/${mediaDetail.tmdb_id}/${mediaProps.type}`">
|
||||
<template #title="{ loaded }">
|
||||
<div v-if="loaded" class="slider-header mt-3 ms-1">
|
||||
<RouterLink to="" class="slider-title">
|
||||
<span>推荐</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
</MediaCardSlideView>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<MediaCardSlideView :apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`">
|
||||
<template #title="{ loaded }">
|
||||
<div v-if="loaded" class="slider-header mt-3 ms-1">
|
||||
<RouterLink to="" class="slider-title">
|
||||
<span>类似</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
</MediaCardSlideView>
|
||||
<h2 v-if="mediaDetail.overview">
|
||||
简介
|
||||
</h2>
|
||||
<p>{{ mediaDetail.overview }}</p>
|
||||
</div>
|
||||
<div class="media-overview-right" />
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<PersonCardSlideView :apipath="`tmdb/credits/${mediaDetail.tmdb_id}/${mediaProps.type}`">
|
||||
<template #title="{ loaded }">
|
||||
<div v-if="loaded" class="slider-header mt-3 ms-1">
|
||||
<RouterLink to="" class="slider-title">
|
||||
<span>演员阵容</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
</PersonCardSlideView>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<MediaCardSlideView :apipath="`tmdb/recommend/${mediaDetail.tmdb_id}/${mediaProps.type}`">
|
||||
<template #title="{ loaded }">
|
||||
<div v-if="loaded" class="slider-header mt-3 ms-1">
|
||||
<RouterLink to="" class="slider-title">
|
||||
<span>推荐</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
</MediaCardSlideView>
|
||||
</div>
|
||||
<div v-if="mediaDetail.tmdb_id">
|
||||
<MediaCardSlideView :apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`">
|
||||
<template #title="{ loaded }">
|
||||
<div v-if="loaded" class="slider-header mt-3 ms-1">
|
||||
<RouterLink to="" class="slider-title">
|
||||
<span>类似</span>
|
||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
</MediaCardSlideView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -128,13 +130,6 @@ onMounted(() => {
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.media-header {
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.media-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -142,12 +137,35 @@ onMounted(() => {
|
||||
padding-block-start: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.media-header {
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.media-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.media-overview {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.media-poster {
|
||||
width: 8rem;
|
||||
overflow: hidden;
|
||||
border-radius: .25rem;
|
||||
--tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1);
|
||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.media-poster {
|
||||
margin-right: 1rem;
|
||||
@@ -165,13 +183,12 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.media-poster {
|
||||
width: 8rem;
|
||||
overflow: hidden;
|
||||
border-radius: .25rem;
|
||||
--tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1);
|
||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
.media-title {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
@@ -182,22 +199,24 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.media-title {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255/var(--tw-text-opacity));
|
||||
.media-title>h1 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
ul.media-crew {
|
||||
grid-template-columns: repeat(3,minmax(0,1fr));
|
||||
@media (min-width: 1280px) {
|
||||
.media-title>h1 {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h1 .media-year {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
ul.media-crew {
|
||||
margin-top: 1.5rem;
|
||||
display: grid;
|
||||
@@ -205,10 +224,24 @@ ul.media-crew {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
ul.media-crew {
|
||||
grid-template-columns: repeat(3,minmax(0,1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.media-status {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.media-attributes {
|
||||
margin-top: .25rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.media-attributes {
|
||||
margin-top: 0;
|
||||
@@ -225,16 +258,11 @@ ul.media-crew {
|
||||
}
|
||||
}
|
||||
|
||||
.media-attributes {
|
||||
font-size: .75rem;
|
||||
line-height: 1rem;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(209 213 219/var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.media-attributes {
|
||||
margin-top: .25rem;
|
||||
.media-actions {
|
||||
position: relative;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -253,23 +281,8 @@ ul.media-crew {
|
||||
}
|
||||
}
|
||||
|
||||
.media-actions {
|
||||
position: relative;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.media-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255/var(--tw-text-opacity));
|
||||
.media-overview-left {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
@@ -278,8 +291,9 @@ ul.media-crew {
|
||||
}
|
||||
}
|
||||
|
||||
.media-overview-left {
|
||||
flex: 1 1 0%;
|
||||
.media-overview-right {
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
@@ -289,8 +303,23 @@ ul.media-crew {
|
||||
}
|
||||
}
|
||||
|
||||
.media-overview-right {
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
.slider-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.75rem;
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(209 213 219/var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
@media (min-width: 640px){
|
||||
.slider-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
152
src/views/discover/PersonCardListView.vue
Normal file
152
src/views/discover/PersonCardListView.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api'
|
||||
import type { TmdbPerson } from '@/api/types'
|
||||
import PersonCard from '@/components/cards/PersonCard.vue'
|
||||
import NoDataFound from '@/components/NoDataFound.vue'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
apipath: String,
|
||||
})
|
||||
|
||||
// 判断是否有滚动条
|
||||
function hasScroll() {
|
||||
return (
|
||||
document.body.scrollHeight
|
||||
- (window.innerHeight || document.documentElement.clientHeight)
|
||||
> 2
|
||||
)
|
||||
}
|
||||
|
||||
// 当前页码
|
||||
const page = ref(1)
|
||||
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// 是否加载完成
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<TmdbPerson[]>([])
|
||||
const currData = ref<TmdbPerson[]>([])
|
||||
|
||||
// 获取列表数据
|
||||
async function fetchData({ done }: { done: any }) {
|
||||
try {
|
||||
if (!props.apipath)
|
||||
return
|
||||
|
||||
// 如果正在加载中,直接返回
|
||||
if (loading.value) {
|
||||
done('ok')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
|
||||
// 加载到满屏或者加载出错
|
||||
if (!hasScroll()) {
|
||||
// 加载多次
|
||||
while (!hasScroll()) {
|
||||
// 请求API
|
||||
currData.value = await api.get(props.apipath)
|
||||
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done('ok')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
|
||||
// 页码+1
|
||||
page.value++
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 加载一次
|
||||
// 请求API
|
||||
currData.value = await api.get(props.apipath)
|
||||
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done('ok')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 合并数据
|
||||
dataList.value = [...dataList.value, ...currData.value]
|
||||
|
||||
// 页码+1
|
||||
page.value++
|
||||
}
|
||||
|
||||
// 取消加载中
|
||||
loading.value = false
|
||||
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
|
||||
// 返回加载失败
|
||||
done('error')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="!isRefreshed"
|
||||
class="mt-12 w-full text-center text-gray-500 text-sm flex flex-col items-center"
|
||||
>
|
||||
<VProgressCircular
|
||||
size="48"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
<VInfiniteScroll
|
||||
mode="intersect"
|
||||
side="end"
|
||||
:items="dataList"
|
||||
class="overflow-hidden"
|
||||
@load="fetchData"
|
||||
>
|
||||
<template #loading />
|
||||
<div
|
||||
v-if="dataList.length > 0"
|
||||
class="grid gap-4 grid-media-card mx-3"
|
||||
tabindex="0"
|
||||
>
|
||||
<PersonCard
|
||||
v-for="data in dataList"
|
||||
:key="data.id"
|
||||
:media="data"
|
||||
/>
|
||||
</div>
|
||||
<NoDataFound
|
||||
v-if="dataList.length === 0 && isRefreshed"
|
||||
error-code="500"
|
||||
error-title="出错啦!"
|
||||
error-description="无法获取到媒体信息,请检查网络连接。"
|
||||
/>
|
||||
</VInfiniteScroll>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-media-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||
}
|
||||
</style>
|
||||
87
src/views/discover/PersonCardSlideView.vue
Normal file
87
src/views/discover/PersonCardSlideView.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script lang="ts" setup>
|
||||
import PersionCard from '@/components/cards/PersonCard.vue'
|
||||
import api from '@/api'
|
||||
import type { TmdbPerson } from '@/api/types'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
apipath: String,
|
||||
})
|
||||
|
||||
// 组件加载完成
|
||||
const componentLoaded = ref(false)
|
||||
|
||||
// 数据列表
|
||||
const dataList = ref<TmdbPerson[]>([])
|
||||
|
||||
// 获取订阅列表数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
if (!props.apipath)
|
||||
return
|
||||
|
||||
dataList.value = await api.get(props.apipath)
|
||||
componentLoaded.value = true
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时获取数据
|
||||
onMounted(fetchData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot
|
||||
name="title"
|
||||
:loaded="componentLoaded"
|
||||
/>
|
||||
<VSlideGroup show-arrows="false">
|
||||
<template #prev>
|
||||
<VBtn
|
||||
class="rounded-circle shadow-none"
|
||||
icon="mdi-chevron-left"
|
||||
color="grey"
|
||||
/>
|
||||
</template>
|
||||
<VSlideGroupItem
|
||||
v-for="data in dataList"
|
||||
:key="data.id"
|
||||
>
|
||||
<PersionCard
|
||||
:key="data.id"
|
||||
:person="data"
|
||||
height="15rem"
|
||||
width="10rem"
|
||||
/>
|
||||
</VSlideGroupItem>
|
||||
<template #next>
|
||||
<VBtn
|
||||
class="rounded-circle shadow-none"
|
||||
icon="mdi-chevron-right"
|
||||
color="grey"
|
||||
/>
|
||||
</template>
|
||||
</VSlideGroup>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-slide-group .v-card {
|
||||
@apply m-2;
|
||||
}
|
||||
|
||||
.v-slide-group__prev {
|
||||
@apply absolute right-11;
|
||||
|
||||
z-index: 3;
|
||||
margin-block-start: -40px;
|
||||
}
|
||||
|
||||
.v-slide-group__next {
|
||||
@apply absolute right-1;
|
||||
|
||||
z-index: 3;
|
||||
margin-block-start: -40px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user