Add blackboard

This commit is contained in:
the1812 2021-12-17 08:56:16 +08:00
parent eac68c2866
commit 3e6bc26ba4
15 changed files with 486 additions and 3 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,38 @@
<template>
<div class="home-redesign-base">
<slot />
</div>
</template>
<script lang="ts">
export default Vue.extend({
})
</script>
<style lang="scss">
@import "common";
.home-redesign-base {
--home-base-color: #fff;
--home-background-color: #fff;
--home-color: #000;
--home-max-width: var(--home-max-width-override, 1440px);
--home-card-radius: 12px;
--home-card-shadow: 0 0 0 1px #8881, 0 4px 12px 0 rgba(0, 0, 0, 0.05);
background-color: var(--home-base-color);
color: var(--home-color);
font-size: 12px;
min-height: 100vh;
line-height: normal;
@include v-center();
body.dark & {
--home-base-color: #181818;
--home-background-color: #282828;
--home-color: #eee;
--home-card-shadow: 0 0 0 1px #8882, 0 4px 12px 0 rgba(0, 0, 0, 0.2);
}
}
html {
scroll-behavior: smooth;
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<HomeRedesignBase>
<div class="fresh-home">
<div class="fresh-home-content-layout">
<FreshLayoutItem
v-for="layout of layouts"
:key="layout.name"
:item="layout"
/>
</div>
</div>
</HomeRedesignBase>
</template>
<script lang="ts">
import HomeRedesignBase from '../HomeRedesignBase.vue'
import FreshLayoutItem from './FreshLayoutItem.vue'
import { layouts } from './layouts/layouts'
export default Vue.extend({
components: {
HomeRedesignBase,
FreshLayoutItem,
},
data() {
return {
layouts,
}
},
})
</script>
<style lang="scss">
@import "common";
.fresh-home {
padding: 24px;
width: 100%;
max-width: var(--home-max-width, unset);
@include v-stretch();
&-header {
@include h-center();
justify-content: space-between;
margin-bottom: 16px;
&-title {
color: var(--home-color);
font-weight: bold;
font-size: 20px;
}
&-more {
@include semibold();
.be-icon {
transition: .3s ease-out;
margin-right: 6px;
}
&:hover .be-icon {
transform: rotate(0.5turn);
}
}
}
&-content-layout {
@include h-stretch();
flex-wrap: wrap;
gap: 12px 24px;
}
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<div
class="fresh-home-content-layout-item"
:class="{
grow: item.grow,
linebreak: options.linebreak,
}"
>
<component :is="item.component" />
</div>
</template>
<script lang="ts">
import { getComponentSettings } from '@/core/settings'
import { FreshLayoutItemSettings } from './layouts/fresh-layout-item'
const options = getComponentSettings('freshHome').options as {
layoutOptions: Record<string, FreshLayoutItemSettings>
}
export default Vue.extend({
props: {
item: {
required: true,
type: Object,
},
},
data() {
return {
options: options.layoutOptions[this.item.name] ?? {},
}
},
})
</script>
<style lang="scss">
.fresh-home {
&-content-layout-item {
flex: 0 0 auto;
&.linebreak {
flex: 1 0 100%;
}
&.grow {
flex: 1 0 0;
}
}
}
</style>

View File

@ -0,0 +1,34 @@
import { ComponentMetadata } from '@/components/types'
import { contentLoaded } from '@/core/life-cycle'
import { mountVueComponent } from '@/core/utils'
import { homeUrls } from '../urls'
export const component: ComponentMetadata = {
name: 'freshHome',
displayName: '清爽首页',
description: '使用重新设计的清爽风格首页替换原本的首页.',
urlInclude: homeUrls,
tags: [
componentsTags.style,
],
entry: () => {
contentLoaded(async () => {
const FreshHome = await import('./FreshHome.vue')
const freshHome = mountVueComponent(FreshHome)
document.body.appendChild(freshHome.$el)
})
},
options: {
layoutOptions: {
displayName: '版块设置',
defaultValue: {},
hidden: true,
},
},
instantStyles: [
{
name: 'fresh-home-hide-original',
style: () => import('../hide-original.scss'),
},
],
}

View File

@ -0,0 +1,212 @@
<template>
<div class="fresh-home-blackboard">
<div class="fresh-home-header">
<div class="fresh-home-header-title">
活动
</div>
<a class="fresh-home-header-more" href="https://www.bilibili.com/blackboard/x/act_list/" target="_blank">
<VButton round>
<VIcon icon="mdi-dots-horizontal" :size="18"></VIcon>
更多
</VButton>
</a>
</div>
<input
v-for="(b, i) of blackboards"
:id="'blackboard' + i"
:key="i"
class="fresh-home-blackboard-radio"
type="radio"
name="blackboard"
:checked="i === 0"
:data-index="i"
/>
<div class="fresh-home-blackboard-cards">
<a
v-for="(b, i) of blackboards"
:key="i"
class="fresh-home-blackboard-card"
target="_blank"
:href="b.url"
:title="b.title"
>
<DpiImage
class="fresh-home-blackboard-card-image"
:src="b.imageUrl"
:alt="b.title"
:size="{ width: 500, height: 250 }"
:intersection="{ root: cardsContainer }"
/>
<div class="fresh-home-blackboard-card-title" :title="b.title">
{{ b.title }}
</div>
</a>
</div>
<div class="fresh-home-blackboard-jump-dots">
<label
v-for="(b, i) of blackboards"
:key="i"
:for="'blackboard' + i"
>
<div class="fresh-home-blackboard-jump-dot"></div>
</label>
</div>
</div>
</template>
<script lang="ts">
import {
VButton,
VIcon,
DpiImage,
} from '@/ui'
import { getBlackboards } from './api'
export default Vue.extend({
components: {
VButton,
VIcon,
DpiImage,
},
data() {
return {
blackboards: [],
timer: 0,
}
},
computed: {
cardsContainer() {
return this.$el.querySelector('.fresh-home-blackboard-cards')
},
},
async created() {
const blackboards = await getBlackboards()
this.blackboards = blackboards.filter(b => !b.isAd)
},
mounted() {
const radioClass = 'fresh-home-blackboard-radio'
this.timer = window.setInterval(() => {
if (!document.hasFocus() || this.$el.matches(':hover')) {
return
}
const currentIndex = parseInt(
dq(`.${radioClass}:checked`).getAttribute('data-index'),
)
let targetIndex: number
if (currentIndex === this.blackboards.length - 1) {
targetIndex = 0
} else {
targetIndex = currentIndex + 1
}
(dq(
`.${radioClass}[data-index='${targetIndex}']`,
) as HTMLInputElement).checked = true
}, 5000)
},
beforeDestroy() {
if (this.timer) {
window.clearInterval(this.timer)
}
},
})
</script>
<style lang="scss">
@import "common";
$max-card-count: 16;
.fresh-home {
&-blackboard {
position: relative;
&,
& * {
transition: .2s ease-out;
}
&-cards {
display: flex;
--blackboard-width: 350px;
--blackboard-height: 250px;
--image-height: 197px;
width: var(--blackboard-width);
height: var(--blackboard-height);
box-shadow: var(--home-card-shadow);
border-radius: var(--home-card-radius);
background-color: var(--home-background-color);
overflow: hidden;
}
&-card {
flex: 0 0 auto;
width: 100%;
height: 100%;
position: relative;
display: block;
transition: 0.8s cubic-bezier(0.44, 0.29, 0.13, 1);
color: inherit;
body.dark & {
color: inherit !important;
}
&-image {
width: 100%;
height: var(--image-height);
object-fit: fill;
display: block;
border-radius: 12px;
transition-duration: inherit;
}
&-title {
$bottom: 16px;
position: absolute;
bottom: $bottom;
left: 50%;
transform: translateX(-50%);
padding: 0 16px;
font-size: 14px;
font-weight: bold;
line-height: calc(var(--blackboard-height) - var(--image-height) - #{$bottom});
border-radius: 14px;
max-width: var(--blackboard-width);
box-sizing: border-box;
@include single-line();
}
}
&-radio {
display: none;
@for $i from 1 to $max-card-count {
&:checked:nth-of-type(#{$i})
~ .fresh-home-blackboard-jump-dots
label:nth-child(#{$i})
.fresh-home-blackboard-jump-dot {
background-color: var(--theme-color);
width: 40px;
}
&:checked:nth-of-type(#{$i}) ~ .fresh-home-blackboard-cards .fresh-home-blackboard-card {
transform: translateX(calc(-1 * #{$i - 1} * var(--blackboard-width))) scale(0.9);
&:nth-of-type(#{$i}) {
transform: translateX(calc(-1 * #{$i - 1} * var(--blackboard-width)));
img {
border-radius: var(--home-card-radius) var(--home-card-radius) 0 0;
}
}
}
}
}
&-jump-dots {
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
@include h-center();
label {
display: block;
padding: 8px 6px;
cursor: pointer;
}
}
&-jump-dot {
background-color: #8884;
box-sizing: border-box;
width: 20px;
height: 8px;
border-radius: 8px;
}
}
}
</style>

View File

@ -0,0 +1,24 @@
import { getJson } from '@/core/ajax'
export interface Blackboard {
url: string
title: string
isAd: boolean
imageUrl: string
}
export const getBlackboards = async (): Promise<Blackboard[]> => {
const locId = 4694
const api = `https://api.bilibili.com/x/web-show/res/locs?pf=0&ids=${locId}`
const { code, message, data } = await getJson(api)
if (code !== 0) {
throw new Error(`获取活动卡片失败: ${message}`)
}
const list: any[] = data[locId]
return list.map(it => ({
url: it.url,
title: it.name,
// isAd: it.is_ad_loc,
isAd: it.res_id !== locId,
imageUrl: it.pic,
} as Blackboard))
}

View File

@ -0,0 +1,7 @@
import { FreshLayoutItem } from '../fresh-layout-item'
export const blackboard: FreshLayoutItem = {
name: 'blackboard',
displayName: '活动',
component: () => import('./Blackboard.vue').then(m => m.default),
}

View File

@ -0,0 +1,10 @@
import { Executable, VueModule, WithName } from '@/core/common-types'
export interface FreshLayoutItem extends WithName {
component: Executable<VueModule>
grow?: boolean
}
export interface FreshLayoutItemSettings {
name: string
linebreak: boolean
}

View File

@ -0,0 +1,7 @@
import { registerAndGetData } from '@/plugins/data'
import { blackboard } from './blackboard/blackboard'
const builtInLayouts = [
blackboard,
]
export const [layouts] = registerAndGetData('homeRedesign.fresh.layouts', [...builtInLayouts])

View File

@ -0,0 +1,18 @@
.international-home > :not(.international-header),
.international-header .b-wrap,
.international-footer,
#app > .bili-wrapper,
#app > .elevator-module,
#app > .bili-header-m.stardust-common > .bili-wrapper,
.bili-header-m .head-banner .head-content .head-logo,
#i_cecream .bili-header__channel,
#i_cecream > :not(.bili-header) {
position: fixed;
visibility: hidden;
top: 200vh;
left: 0;
height: 0 !important;
padding: 0 !important;
margin: 0 !important;
overflow: hidden !important;
}

View File

@ -0,0 +1,4 @@
export const homeUrls = [
/^https:\/\/www\.bilibili\.com\/$/,
/^https:\/\/www\.bilibili\.com\/index\.html$/,
]

View File

@ -1,4 +1,6 @@
let eventAttached = false
// FIXME: 需要重构, 不要直接对具体组件有行为依赖
export const disableProfilePopup = () => {
if (document.URL.replace(window.location.search, '') === 'https://t.bilibili.com/') {
(async () => {

View File

@ -1,5 +1,6 @@
<template>
<img
v-bind="$attrs"
:width="width"
:height="height"
:srcset="srcset"

View File

@ -1,6 +1,9 @@
@mixin shadow {
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.05);
// box-shadow: #0002 0 1px 10px 1px;
body.dark & {
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.2);
}
}
@mixin h-center($gap: 0) {
display: flex;
@ -69,14 +72,12 @@
background-color: #fff;
color: black;
border-radius: $radius;
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #8882;
box-sizing: border-box;
// box-shadow: rgba(0, 0, 0, 0.12) 0 4px 8px 0px;
@include shadow();
body.dark & {
background-color: #282828;
color: #eee;
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.2);
}
}
@mixin popup($radius: 8px) {
@ -119,6 +120,9 @@
stroke: #eee;
}
}
@mixin semibold {
font-weight: 600;
}
@mixin default-background-color {
background-color: #fff;
body.dark & {