mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +08:00
person card
This commit is contained in:
@@ -192,6 +192,29 @@ export interface MediaInfo {
|
|||||||
|
|
||||||
// 详情页面
|
// 详情页面
|
||||||
detail_link?: string
|
detail_link?: string
|
||||||
|
// 其它TMDB属性
|
||||||
|
adult?: boolean
|
||||||
|
created_by?: string[]
|
||||||
|
episode_run_time?: string[]
|
||||||
|
genres?: string[]
|
||||||
|
first_air_date?: string
|
||||||
|
homepage?: string
|
||||||
|
languages?: string[]
|
||||||
|
last_air_date?: string
|
||||||
|
networks?: string[]
|
||||||
|
number_of_episodes?: number
|
||||||
|
number_of_seasons?: number
|
||||||
|
origin_countryv: string[]
|
||||||
|
original_name?: string
|
||||||
|
production_companies?: string[]
|
||||||
|
production_countries?: string[]
|
||||||
|
spoken_languages?: string[]
|
||||||
|
status?: string
|
||||||
|
tagline?: string
|
||||||
|
vote_count?: number
|
||||||
|
popularity?: number
|
||||||
|
runtime?: number
|
||||||
|
next_episode_to_air?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TMDB季信息
|
// TMDB季信息
|
||||||
@@ -253,6 +276,27 @@ export interface TmdbEpisode {
|
|||||||
guest_stars: Object[]
|
guest_stars: Object[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TMDB人特信息
|
||||||
|
export interface TmdbPerson {
|
||||||
|
// ID
|
||||||
|
id: number
|
||||||
|
|
||||||
|
// 名称
|
||||||
|
name: string
|
||||||
|
|
||||||
|
// 角色
|
||||||
|
character: string
|
||||||
|
|
||||||
|
// 图片
|
||||||
|
profile_path: string
|
||||||
|
|
||||||
|
// 性别
|
||||||
|
gender: number
|
||||||
|
|
||||||
|
// 原名
|
||||||
|
original_name: string
|
||||||
|
}
|
||||||
|
|
||||||
// 站点
|
// 站点
|
||||||
export interface Site {
|
export interface Site {
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -91,7 +91,7 @@ async function handleAddSubscribe() {
|
|||||||
}
|
}
|
||||||
else if (props.media?.type === '电视剧') {
|
else if (props.media?.type === '电视剧') {
|
||||||
// 豆瓣电视剧,只会有一季
|
// 豆瓣电视剧,只会有一季
|
||||||
const season = props.media?.season || 1
|
const season = props.media?.season ?? 1
|
||||||
// 添加订阅
|
// 添加订阅
|
||||||
addSubscribe(season)
|
addSubscribe(season)
|
||||||
}
|
}
|
||||||
@@ -371,7 +371,7 @@ const seasonsHeaders = [
|
|||||||
const getImgUrl: Ref<string> = computed(() => {
|
const getImgUrl: Ref<string> = computed(() => {
|
||||||
if (imageLoadError.value)
|
if (imageLoadError.value)
|
||||||
return noImage
|
return noImage
|
||||||
const url = props.media?.poster_path || noImage
|
const url = props.media?.poster_path ?? noImage
|
||||||
// 如果地址中包含douban则使用中转代理
|
// 如果地址中包含douban则使用中转代理
|
||||||
if (url.includes('doubanio.com'))
|
if (url.includes('doubanio.com'))
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}douban/img/${encodeURIComponent(url)}`
|
return `${import.meta.env.VITE_API_BASE_URL}douban/img/${encodeURIComponent(url)}`
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import personIcon from '@images/misc/person.png'
|
||||||
|
import type { TmdbPerson } from '@/api/types'
|
||||||
|
|
||||||
|
const personProps = defineProps({
|
||||||
|
person: Object as PropType<TmdbPerson>,
|
||||||
|
width: String,
|
||||||
|
height: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前人物
|
||||||
|
const personInfo = ref(personProps.person)
|
||||||
|
|
||||||
|
// 人物图片地址
|
||||||
|
function getPersonImage() {
|
||||||
|
if (!personInfo.value?.profile_path)
|
||||||
|
return personIcon
|
||||||
|
return `https://image.tmdb.org/t/p/w600_and_h900_bestv2${personInfo.value?.profile_path}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VHover v-bind="personProps">
|
||||||
|
<template #default="hover">
|
||||||
|
<VCard
|
||||||
|
v-bind="hover.props"
|
||||||
|
:height="personProps.height"
|
||||||
|
:width="personProps.width"
|
||||||
|
:class="{
|
||||||
|
'transition transform-cpu duration-300 scale-105': hover.isHovering,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="person-card relative transform-gpu cursor-pointer rounded text-white shadow ring-1 transition duration-150 ease-in-out scale-100 ring-gray-700"
|
||||||
|
>
|
||||||
|
<div style="padding-bottom: 150%;">
|
||||||
|
<div class="absolute inset-0 flex h-full w-full flex-col items-center p-2">
|
||||||
|
<div class="relative mt-2 mb-4 flex h-1/2 w-full justify-center">
|
||||||
|
<div class="relative w-3/4 overflow-hidden rounded-full">
|
||||||
|
<VImg :src="getPersonImage()" cover />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full truncate text-center font-bold">
|
||||||
|
{{ personInfo?.name }}
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden whitespace-normal text-center text-sm" style=" display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical;-webkit-line-clamp: 2;">
|
||||||
|
{{ personInfo?.character }}
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0 left-0 right-0 h-12 rounded-b" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.person-card {
|
||||||
|
background-image: linear-gradient(45deg, #99999b, #384359 60%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+13
-11
@@ -3,7 +3,8 @@ import MediaCardListView from '@/views/discover/MediaCardListView.vue'
|
|||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
type: Array as PropType<string[]> | PropType<string>,
|
// API路径
|
||||||
|
paths: Array as PropType<string[]> | PropType<string>,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 路由参数
|
// 路由参数
|
||||||
@@ -23,26 +24,27 @@ const titles: { [key: string]: any } = {
|
|||||||
tv_weekly_global: '全球剧集榜',
|
tv_weekly_global: '全球剧集榜',
|
||||||
movie_top250: '电影TOP250',
|
movie_top250: '电影TOP250',
|
||||||
},
|
},
|
||||||
|
credits: '演员阵容',
|
||||||
media: {
|
media: {
|
||||||
search: '搜索',
|
search: '搜索',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算API路径
|
// 计算API路径
|
||||||
function getApiPath(types: string[] | string) {
|
function getApiPath(paths: string[] | string) {
|
||||||
if (Array.isArray(types))
|
if (Array.isArray(paths))
|
||||||
return types.join('/')
|
return paths.join('/')
|
||||||
else
|
else
|
||||||
return types
|
return paths
|
||||||
}
|
}
|
||||||
|
|
||||||
// 面包屑标题
|
// 面包屑标题
|
||||||
function getTitle(types: string[] | string, title: any = '') {
|
function getTitle(paths: string[] | string, title: any = '') {
|
||||||
if (Array.isArray(types)) {
|
if (Array.isArray(paths)) {
|
||||||
if (title)
|
if (title)
|
||||||
return [titles[types[0]][types[1]], title]
|
return [titles[paths[0]][paths[1]], title]
|
||||||
|
|
||||||
return ['推荐', titles[types[0]][types[1]]]
|
return ['推荐', titles[paths[0]][paths[1]]]
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return ['发现']
|
return ['发现']
|
||||||
@@ -52,9 +54,9 @@ function getTitle(types: string[] | string, title: any = '') {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<VBreadcrumbs :items="getTitle(props.type || '', route.query?.title)" />
|
<VBreadcrumbs :items="getTitle(props.paths || '', route.query?.title)" />
|
||||||
<MediaCardListView
|
<MediaCardListView
|
||||||
:apipath="getApiPath(props.type || '')"
|
:apipath="getApiPath(props.paths || '')"
|
||||||
:params="route.query"
|
:params="route.query"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -91,7 +91,7 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/browse/:type+',
|
path: '/browse/:paths+',
|
||||||
component: () => import('../pages/browse.vue'),
|
component: () => import('../pages/browse.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function getParams() {
|
|||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取订阅列表数据
|
// 获取列表数据
|
||||||
async function fetchData({ done }: { done: any }) {
|
async function fetchData({ done }: { done: any }) {
|
||||||
try {
|
try {
|
||||||
if (!props.apipath)
|
if (!props.apipath)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import PersonCardSlideView from './PersonCardSlideView.vue'
|
||||||
import MediaCardSlideView from './MediaCardSlideView.vue'
|
import MediaCardSlideView from './MediaCardSlideView.vue'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import type { MediaInfo } from '@/api/types'
|
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%);"
|
style="background-image: linear-gradient(180deg, rgba(17, 24, 39, 47%) 0%, rgba(17, 24, 39, 100%) 100%);"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="media-header">
|
||||||
<div class="media-header">
|
<div class="media-poster">
|
||||||
<div class="media-poster">
|
<VImg :src="mediaDetail.poster_path" cover />
|
||||||
<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>
|
</div>
|
||||||
<h2>简介</h2>
|
<div class="media-title">
|
||||||
<p>{{ mediaDetail.overview }}</p>
|
<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>
|
||||||
<div class="media-overview-right" />
|
<div class="media-overview">
|
||||||
</div>
|
<div class="media-overview-left">
|
||||||
<div v-if="mediaDetail.tmdb_id">
|
<div v-if="mediaDetail.tagline" class="tagline">
|
||||||
<MediaCardSlideView :apipath="`tmdb/credits/${mediaDetail.tmdb_id}/${mediaProps.type}`">
|
{{ mediaDetail.tagline }}
|
||||||
<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>
|
</div>
|
||||||
</template>
|
<h2 v-if="mediaDetail.overview">
|
||||||
</MediaCardSlideView>
|
简介
|
||||||
</div>
|
</h2>
|
||||||
<div v-if="mediaDetail.tmdb_id">
|
<p>{{ mediaDetail.overview }}</p>
|
||||||
<MediaCardSlideView :apipath="`tmdb/recommend/${mediaDetail.tmdb_id}/${mediaProps.type}`">
|
</div>
|
||||||
<template #title="{ loaded }">
|
<div class="media-overview-right" />
|
||||||
<div v-if="loaded" class="slider-header mt-3 ms-1">
|
</div>
|
||||||
<RouterLink to="" class="slider-title">
|
<div v-if="mediaDetail.tmdb_id">
|
||||||
<span>推荐</span>
|
<PersonCardSlideView :apipath="`tmdb/credits/${mediaDetail.tmdb_id}/${mediaProps.type}`">
|
||||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
<template #title="{ loaded }">
|
||||||
</RouterLink>
|
<div v-if="loaded" class="slider-header mt-3 ms-1">
|
||||||
</div>
|
<RouterLink to="" class="slider-title">
|
||||||
</template>
|
<span>演员阵容</span>
|
||||||
</MediaCardSlideView>
|
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||||
</div>
|
</RouterLink>
|
||||||
<div v-if="mediaDetail.tmdb_id">
|
</div>
|
||||||
<MediaCardSlideView :apipath="`tmdb/similar/${mediaDetail.tmdb_id}/${mediaProps.type}`">
|
</template>
|
||||||
<template #title="{ loaded }">
|
</PersonCardSlideView>
|
||||||
<div v-if="loaded" class="slider-header mt-3 ms-1">
|
</div>
|
||||||
<RouterLink to="" class="slider-title">
|
<div v-if="mediaDetail.tmdb_id">
|
||||||
<span>类似</span>
|
<MediaCardSlideView :apipath="`tmdb/recommend/${mediaDetail.tmdb_id}/${mediaProps.type}`">
|
||||||
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
<template #title="{ loaded }">
|
||||||
</RouterLink>
|
<div v-if="loaded" class="slider-header mt-3 ms-1">
|
||||||
</div>
|
<RouterLink to="" class="slider-title">
|
||||||
</template>
|
<span>推荐</span>
|
||||||
</MediaCardSlideView>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -128,13 +130,6 @@ onMounted(() => {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
.media-header {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-header {
|
.media-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -142,12 +137,35 @@ onMounted(() => {
|
|||||||
padding-block-start: 1rem;
|
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 (min-width: 1024px) {
|
||||||
.media-overview {
|
.media-overview {
|
||||||
flex-direction: row;
|
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 (min-width: 1280px) {
|
||||||
.media-poster {
|
.media-poster {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
@@ -165,13 +183,12 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-poster {
|
.media-title {
|
||||||
width: 8rem;
|
margin-top: 1rem;
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
border-radius: .25rem;
|
flex: 1 1 0%;
|
||||||
--tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px -1px rgba(0, 0, 0, .1);
|
flex-direction: column;
|
||||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
text-align: center;
|
||||||
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 (min-width: 1280px) {
|
||||||
@@ -182,22 +199,24 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-title {
|
.media-title>h1 {
|
||||||
margin-top: 1rem;
|
font-size: 1.5rem;
|
||||||
display: flex;
|
line-height: 2rem;
|
||||||
flex: 1 1 0%;
|
font-weight: 700;
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
--tw-text-opacity: 1;
|
|
||||||
color: rgb(255 255 255/var(--tw-text-opacity));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 1280px) {
|
||||||
ul.media-crew {
|
.media-title>h1 {
|
||||||
grid-template-columns: repeat(3,minmax(0,1fr));
|
font-size: 2.25rem;
|
||||||
|
line-height: 2.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 .media-year {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
ul.media-crew {
|
ul.media-crew {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -205,10 +224,24 @@ ul.media-crew {
|
|||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
ul.media-crew {
|
||||||
|
grid-template-columns: repeat(3,minmax(0,1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.media-status {
|
.media-status {
|
||||||
margin-bottom: .5rem;
|
margin-bottom: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.media-attributes {
|
||||||
|
margin-top: .25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
.media-attributes {
|
.media-attributes {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@@ -225,16 +258,11 @@ ul.media-crew {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-attributes {
|
.media-actions {
|
||||||
font-size: .75rem;
|
position: relative;
|
||||||
line-height: 1rem;
|
margin-top: 1rem;
|
||||||
--tw-text-opacity: 1;
|
|
||||||
color: rgb(209 213 219/var(--tw-text-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-attributes {
|
|
||||||
margin-top: .25rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -253,23 +281,8 @@ ul.media-crew {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-actions {
|
.media-overview-left {
|
||||||
position: relative;
|
flex: 1 1 0%;
|
||||||
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 (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
@@ -278,8 +291,9 @@ ul.media-crew {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-overview-left {
|
.media-overview-right {
|
||||||
flex: 1 1 0%;
|
margin-top: 2rem;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
@@ -289,8 +303,23 @@ ul.media-crew {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-overview-right {
|
.slider-title {
|
||||||
margin-top: 2rem;
|
display: inline-flex;
|
||||||
width: 100%;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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