mirror of
https://github.com/mashirozx/sakura.git
synced 2024-12-12 09:54:35 +08:00
Add mobile compatibility
This commit is contained in:
parent
214689a41b
commit
d82fc5fb99
@ -100,7 +100,7 @@ class OptionController extends BaseController
|
|||||||
if ($hasNoDiff) {
|
if ($hasNoDiff) {
|
||||||
return [
|
return [
|
||||||
'code' => 'save_config_succeed',
|
'code' => 'save_config_succeed',
|
||||||
'message' => __('Configurations already up to date.', self::$text_domain),
|
'message' => __('Configuration is already up to date.', self::$text_domain),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,13 +108,13 @@ class OptionController extends BaseController
|
|||||||
if (!$config) {
|
if (!$config) {
|
||||||
return new WP_Error(
|
return new WP_Error(
|
||||||
'save_config_failure',
|
'save_config_failure',
|
||||||
__('Unable to save configuration.', self::$text_domain),
|
__('Unable to save the configuration.', self::$text_domain),
|
||||||
array('status' => 500)
|
array('status' => 500)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
'code' => 'save_config_succeed',
|
'code' => 'save_config_succeed',
|
||||||
'message' => __('Configurations saved successfully.', self::$text_domain),
|
'message' => __('Configuration saved successfully.', self::$text_domain),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
define('SAKURA_VERSION', wp_get_theme()->get('Version'));
|
define('SAKURA_VERSION', wp_get_theme()->get('Version'));
|
||||||
define('SAKURA_TEXT_DOMAIN', wp_get_theme()->get('TextDomain'));
|
define('SAKURA_TEXT_DOMAIN', wp_get_theme()->get('TextDomain'));
|
||||||
|
|
||||||
define('SAKURA_DEVEPLOMENT', true);
|
define('SAKURA_DEVEPLOMENT', false);
|
||||||
define('SAKURA_DEVEPLOMENT_HOST', 'http://127.0.0.1:9000');
|
define('SAKURA_DEVEPLOMENT_HOST', 'http://127.0.0.1:9000');
|
||||||
|
|
||||||
// PHP loaders
|
// PHP loaders
|
||||||
|
17
src/@types/declarations.d.ts
vendored
17
src/@types/declarations.d.ts
vendored
@ -53,7 +53,20 @@ interface WPPostAbstract {
|
|||||||
categories: [number?]
|
categories: [number?]
|
||||||
categoriesMeta: { [key: string]: any }
|
categoriesMeta: { [key: string]: any }
|
||||||
tags: [number?]
|
tags: [number?]
|
||||||
tagsMeta: { [key: string]: any }
|
tagsMeta: {
|
||||||
|
[key: string]: {
|
||||||
|
count: number
|
||||||
|
description: string
|
||||||
|
filter: string
|
||||||
|
name: string
|
||||||
|
parent: number
|
||||||
|
slug: string
|
||||||
|
taxonomy: string
|
||||||
|
termGroup: number
|
||||||
|
termId: number
|
||||||
|
termTaxonomyId: number
|
||||||
|
}
|
||||||
|
}
|
||||||
commentCount: number
|
commentCount: number
|
||||||
viewCount: number
|
viewCount: number
|
||||||
wordsCount: number
|
wordsCount: number
|
||||||
@ -117,3 +130,5 @@ interface CommentStore {
|
|||||||
pagination: Pagination
|
pagination: Pagination
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare type FetchingStatus = 'inite' | 'cached' | 'pending' | 'success' | 'error' | 'empty'
|
||||||
|
15
src/App.vue
15
src/App.vue
@ -4,15 +4,19 @@
|
|||||||
<component :is="Component" :key="$route.fullPath"></component>
|
<component :is="Component" :key="$route.fullPath"></component>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
|
<div class="messages__wrapper">
|
||||||
|
<Messages position-y="bottom" position-x="left"></Messages>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { init } from '@/store'
|
import { init } from '@/store'
|
||||||
import { useInjector } from '@/hooks'
|
import { useInjector } from '@/hooks'
|
||||||
|
import Messages from '@/components/messages/Messages.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'App',
|
components: { Messages },
|
||||||
setup() {
|
setup() {
|
||||||
const { fetchWpJson } = useInjector(init)
|
const { fetchWpJson } = useInjector(init)
|
||||||
fetchWpJson()
|
fetchWpJson()
|
||||||
@ -21,5 +25,12 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '@/styles/index';
|
@use '@/styles/global';
|
||||||
|
|
||||||
|
.messages__wrapper {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 999999;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -47,10 +47,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, Ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
import { defineComponent, ref, Ref, watch } from 'vue'
|
||||||
import { Swiper, SwiperSlide } from 'swiper/vue'
|
import { Swiper, SwiperSlide } from 'swiper/vue'
|
||||||
import { Swiper as SwiperInterface } from 'swiper'
|
import { Swiper as SwiperInterface } from 'swiper'
|
||||||
import { useInjector, useState, useMessage } from '@/hooks'
|
import { useInjector, useState, useMessage, useIntervalWatcher } from '@/hooks'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
import options from './options'
|
import options from './options'
|
||||||
import type { Option } from './options'
|
import type { Option } from './options'
|
||||||
@ -90,11 +90,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const updateAutoHeight = (timeout = 0) => swiperRef.value?.updateAutoHeight(timeout)
|
const updateAutoHeight = (timeout = 0) => swiperRef.value?.updateAutoHeight(timeout)
|
||||||
|
|
||||||
// auto update height
|
useIntervalWatcher(() => updateAutoHeight(100), 100)
|
||||||
onMounted(() => {
|
|
||||||
const timer = setInterval(() => updateAutoHeight(100), 100)
|
|
||||||
onBeforeUnmount(() => clearInterval(timer))
|
|
||||||
})
|
|
||||||
|
|
||||||
// messages
|
// messages
|
||||||
const addMessage = useMessage()
|
const addMessage = useMessage()
|
||||||
@ -213,7 +209,7 @@ export default defineComponent({
|
|||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px;
|
padding: 0 12px 12px 12px;
|
||||||
width: calc(100% - 24px);
|
width: calc(100% - 24px);
|
||||||
@include polyfills.flex-gap(12px, 'row wrap');
|
@include polyfills.flex-gap(12px, 'row wrap');
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="option__container">
|
<div class="option__container">
|
||||||
<h3 class="column__wrapper--label"> {{ title }} </h3>
|
<h3 class="column__wrapper--label">
|
||||||
|
{{ title }}
|
||||||
|
<span class="restore" :title="msg.restoreTitle" @click="handleRestoreEvent"
|
||||||
|
><i class="fas fa-redo-alt"></i
|
||||||
|
></span>
|
||||||
|
</h3>
|
||||||
<div class="column__wrapper--main">
|
<div class="column__wrapper--main">
|
||||||
<div class="row__wrapper--option">
|
<div class="row__wrapper--option">
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
@ -43,7 +48,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, watch } from 'vue'
|
import { defineComponent, ref, watch } from 'vue'
|
||||||
import { useInjector } from '@/hooks'
|
import { cloneDeep } from 'lodash'
|
||||||
|
import { useInjector, useIntl } from '@/hooks'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
import validator from './validator'
|
import validator from './validator'
|
||||||
import OutlinedInput from '@/components/inputs/OutlinedInput.vue'
|
import OutlinedInput from '@/components/inputs/OutlinedInput.vue'
|
||||||
@ -60,9 +66,16 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
emits: [],
|
emits: [],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const msg = {
|
||||||
|
restoreTitle: intl.formatMessage({
|
||||||
|
id: 'admin.restore.title',
|
||||||
|
defaultMessage: 'Restore this option to default.',
|
||||||
|
}),
|
||||||
|
}
|
||||||
const { namespace, type, title, desc, binds } = props.option
|
const { namespace, type, title, desc, binds } = props.option
|
||||||
const { config, updateOption } = useInjector(store)
|
const { config, updateOption } = useInjector(store)
|
||||||
const optionResultRef = ref(config.value[namespace] ?? props.option.default)
|
const optionResultRef = ref(config.value[namespace] ?? cloneDeep(props.option).default)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
optionResultRef,
|
optionResultRef,
|
||||||
@ -74,7 +87,11 @@ export default defineComponent({
|
|||||||
{ immediate: true, deep: true }
|
{ immediate: true, deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
return { config, optionResultRef, type, title, desc, binds }
|
const handleRestoreEvent = () => {
|
||||||
|
optionResultRef.value = cloneDeep(props.option).default
|
||||||
|
}
|
||||||
|
|
||||||
|
return { msg, config, optionResultRef, type, title, desc, binds, handleRestoreEvent }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -96,6 +113,19 @@ export default defineComponent({
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
|
@media screen and (max-width: variables.$mobile-max-width) {
|
||||||
|
margin-block-start: 0;
|
||||||
|
margin-block-end: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
> .restore {
|
||||||
|
padding-left: 6px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: var(--mdc-theme-secondary, #1d2327);
|
||||||
|
:hover {
|
||||||
|
color: var(--mdc-theme-primary, #6200ee);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&--main {
|
&--main {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -112,6 +142,7 @@ export default defineComponent({
|
|||||||
&--desc {
|
&--desc {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #646970;
|
color: #646970;
|
||||||
|
// margin-block-end: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,14 +139,21 @@ export default defineComponent({
|
|||||||
(value) => {
|
(value) => {
|
||||||
if (!props.multiple && value.length > 1) {
|
if (!props.multiple && value.length > 1) {
|
||||||
selection.value = selection.value.slice(-1)
|
selection.value = selection.value.slice(-1)
|
||||||
console.log(selection.value.length)
|
// console.log(selection.value.length)
|
||||||
}
|
}
|
||||||
console.log(selection.value)
|
// console.log(selection.value)
|
||||||
emit('update:selection', selection.value)
|
emit('update:selection', selection.value)
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.selection as { id: number; url: string }[],
|
||||||
|
(selectionProp) => {
|
||||||
|
selection.value = selectionProp
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return { open, add, del, userInput, selection }
|
return { open, add, del, userInput, selection }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -183,9 +190,6 @@ export default defineComponent({
|
|||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@include polyfills.flex-gap(12px, 'row wrap');
|
@include polyfills.flex-gap(12px, 'row wrap');
|
||||||
> .input__wrapper {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&--preview {
|
&--preview {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Ref } from 'vue'
|
import { Ref } from 'vue'
|
||||||
import { useState } from '@/hooks'
|
import { useState } from '@/hooks'
|
||||||
import API from '@/api'
|
// import API from '@/api'
|
||||||
import camelcaseKeys from 'camelcase-keys'
|
// import camelcaseKeys from 'camelcase-keys'
|
||||||
import intl from '@/locales'
|
// import intl from '@/locales'
|
||||||
import options, { Options } from './options'
|
// import options, { Options } from './options'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
|
|
||||||
export interface OptionStore {
|
export interface OptionStore {
|
||||||
|
@ -2,21 +2,25 @@
|
|||||||
<div class="card__container mdc-card mdc-elevation--z8" :type="$props.type" v-if="data">
|
<div class="card__container mdc-card mdc-elevation--z8" :type="$props.type" v-if="data">
|
||||||
<div class="card__content mdc-card__primary-action" :ref="setContentRef">
|
<div class="card__content mdc-card__primary-action" :ref="setContentRef">
|
||||||
<div class="ripple__mask mdc-card__ripple"></div>
|
<div class="ripple__mask mdc-card__ripple"></div>
|
||||||
<div class="thumbnail__wrapper" @click="handleViewPostDetailEvent">
|
<div class="thumbnail__wrapper">
|
||||||
<Image
|
<Link :url="$props.data.link">
|
||||||
class="image"
|
<Image
|
||||||
:src="$props.data.featureImage.thumbnail"
|
class="image"
|
||||||
:alt="$props.data.title"
|
:src="$props.data.featureImage.thumbnail"
|
||||||
placeholder="https://via.placeholder.com/1024x768"
|
:alt="$props.data.title"
|
||||||
:draggable="false"
|
placeholder="https://via.placeholder.com/1024x768"
|
||||||
/>
|
:draggable="false"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div class="details__wrapper">
|
<div class="details__wrapper">
|
||||||
<div class="row__wrapper--date">
|
<div class="row__wrapper--date">
|
||||||
<span><i class="far fa-clock"></i> {{ $props.data.publistTime }}</span>
|
<span><i class="far fa-clock"></i> {{ $props.data.publistTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row__wrapper--title" @click="handleViewPostDetailEvent">
|
<div class="row__wrapper--title">
|
||||||
<span>{{ $props.data.title }}</span>
|
<Link :url="$props.data.link">
|
||||||
|
<span>{{ $props.data.title }}</span>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div class="row__wrapper--info">
|
<div class="row__wrapper--info">
|
||||||
<div class="column__wrapper--read_count">
|
<div class="column__wrapper--read_count">
|
||||||
@ -32,19 +36,20 @@
|
|||||||
<div class="row__wrapper--abstruct">
|
<div class="row__wrapper--abstruct">
|
||||||
<span>{{ $props.data.excerpt }} </span>
|
<span>{{ $props.data.excerpt }} </span>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="row__wrapper--tags">
|
<div class="row__wrapper--tags" v-if="$props.data.tags.length > 0">
|
||||||
<div class="tags__container">
|
<div class="tags__container">
|
||||||
<div class="tag__wrapper" v-for="(tag, index) in tags" :key="index">
|
<div class="tag__wrapper" v-for="(tag, index) in $props.data.tags" :key="index">
|
||||||
<div class="tag yolk">
|
<Link :to="{ name: 'TagArchive', params: { tag: tag.slug } }">
|
||||||
<span class="text">{{ tag }}</span>
|
<NormalChip :context="tag.name"></NormalChip>
|
||||||
</div>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div>
|
||||||
<!-- // TODO: use tags instead of button, button is useless! -->
|
<div class="row__wrapper--button">
|
||||||
<div class="row__wrapper--button" @click="handleViewPostDetailEvent">
|
|
||||||
<div class="button__wrapper">
|
<div class="button__wrapper">
|
||||||
<NormalButton icon="fab fa-readme" :context="buttonContext"></NormalButton>
|
<Link :url="$props.data.link">
|
||||||
|
<NormalButton icon="fab fa-readme" :context="buttonContext"></NormalButton>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -55,11 +60,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed } from 'vue'
|
import { defineComponent, computed } from 'vue'
|
||||||
import { useIntl, useRouter, useElementRef, useMDCRipple } from '@/hooks'
|
import { useIntl, useRouter, useElementRef, useMDCRipple } from '@/hooks'
|
||||||
import linkHandler from '@/utils/linkHandler'
|
|
||||||
import NormalButton from '@/components/buttons/NormalButton.vue'
|
import NormalButton from '@/components/buttons/NormalButton.vue'
|
||||||
|
import NormalChip from '@/components/chips/NormalChip.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { NormalButton },
|
components: { NormalButton, NormalChip },
|
||||||
props: {
|
props: {
|
||||||
data: { type: Object },
|
data: { type: Object },
|
||||||
type: { type: String, default: 'normal' }, // normal | reverse | mobile
|
type: { type: String, default: 'normal' }, // normal | reverse | mobile
|
||||||
@ -76,13 +81,8 @@ export default defineComponent({
|
|||||||
defaultMessage: 'Read More',
|
defaultMessage: 'Read More',
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleViewPostDetailEvent = () => {
|
|
||||||
linkHandler.handleClickLink({ url: props.data?.link ?? '', router, target: '_blank' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buttonContext,
|
buttonContext,
|
||||||
handleViewPostDetailEvent,
|
|
||||||
setContentRef,
|
setContentRef,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -96,11 +96,11 @@ export default defineComponent({
|
|||||||
@use '@/styles/mixins/polyfills';
|
@use '@/styles/mixins/polyfills';
|
||||||
|
|
||||||
.card__container {
|
.card__container {
|
||||||
// TODO: sizing in parent
|
|
||||||
width: 780px;
|
width: 780px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
user-select: none;
|
||||||
.card__content {
|
.card__content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -151,7 +151,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
&--title {
|
&--title {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
> span {
|
span {
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
font-size: large;
|
font-size: large;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -186,7 +186,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
&--tags {
|
&--tags {
|
||||||
max-height: 16px;
|
max-height: 32px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
.tags__container {
|
.tags__container {
|
||||||
@ -200,7 +200,9 @@ export default defineComponent({
|
|||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@include tags.tag-style;
|
.router-link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card__container">
|
<div class="card__container">
|
||||||
<div class="row__wrapper--thumbnail" @click="handleViewPostDetailEvent">
|
<div class="row__wrapper--thumbnail">
|
||||||
<Image
|
<Link :url="$props.data.link">
|
||||||
class="image"
|
<Image
|
||||||
:src="$props.data.featureImage.thumbnail"
|
class="image"
|
||||||
:alt="$props.data.title"
|
:src="$props.data.featureImage.thumbnail"
|
||||||
placeholder="https://via.placeholder.com/1024x768"
|
:alt="$props.data.title"
|
||||||
:draggable="false"
|
placeholder="https://via.placeholder.com/1024x768"
|
||||||
/>
|
:draggable="false"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div class="row__wrapper--title" @click="handleViewPostDetailEvent">
|
<div class="row__wrapper--title">
|
||||||
<span>{{ $props.data.title }}</span>
|
<Link :url="$props.data.link">
|
||||||
|
<span>{{ $props.data.title }}</span>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div class="row__wrapper--statistics">
|
<div class="row__wrapper--statistics">
|
||||||
<div class="column__wrapper--read_count">
|
<div class="column__wrapper--read_count">
|
||||||
@ -26,16 +30,12 @@
|
|||||||
<div class="row__wrapper--abstract">
|
<div class="row__wrapper--abstract">
|
||||||
<span>{{ $props.data.excerpt }} </span>
|
<span>{{ $props.data.excerpt }} </span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row__wrapper--tags">
|
<div class="row__wrapper--tags" v-if="$props.data.tags.length > 0">
|
||||||
<div class="tags__container">
|
<div class="tags__container">
|
||||||
<div
|
<div class="tag__wrapper" v-for="(tag, index) in $props.data.tags" :key="index">
|
||||||
class="tag__wrapper"
|
<Link :to="{ name: 'TagArchive', params: { tag: tag.slug } }">
|
||||||
v-for="(tag, index) in ['vue', 'javascript', 'php', 'wordpress']"
|
<NormalChip :context="tag.name"></NormalChip>
|
||||||
:key="index"
|
</Link>
|
||||||
>
|
|
||||||
<div class="tag yolk">
|
|
||||||
<span class="text">{{ tag }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -45,11 +45,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed } from 'vue'
|
import { defineComponent, computed } from 'vue'
|
||||||
import { useIntl, useRouter } from '@/hooks'
|
import { useIntl, useRouter } from '@/hooks'
|
||||||
import linkHandler from '@/utils/linkHandler'
|
import NormalChip from '@/components/chips/NormalChip.vue'
|
||||||
import NormalButton from '@/components/buttons/NormalButton.vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { NormalButton },
|
components: { NormalChip },
|
||||||
props: {
|
props: {
|
||||||
data: { type: Object /*, default: () => postMock*/ },
|
data: { type: Object /*, default: () => postMock*/ },
|
||||||
type: { type: String, default: 'normal' }, // normal | reverse | mobile
|
type: { type: String, default: 'normal' }, // normal | reverse | mobile
|
||||||
@ -63,13 +62,8 @@ export default defineComponent({
|
|||||||
defaultMessage: 'Read More',
|
defaultMessage: 'Read More',
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleViewPostDetailEvent = () => {
|
|
||||||
linkHandler.handleClickLink({ url: props.data?.link ?? '', router, target: '_blank' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buttonContext,
|
buttonContext,
|
||||||
handleViewPostDetailEvent,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -77,7 +71,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '@/styles/mixins/text';
|
@use '@/styles/mixins/text';
|
||||||
@use '@/styles/mixins/tags';
|
|
||||||
@use '@/styles/mixins/polyfills';
|
@use '@/styles/mixins/polyfills';
|
||||||
|
|
||||||
.card__container {
|
.card__container {
|
||||||
@ -86,6 +79,7 @@ export default defineComponent({
|
|||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
@include polyfills.flex-gap(12px, 'column nowrap');
|
@include polyfills.flex-gap(12px, 'column nowrap');
|
||||||
> * {
|
> * {
|
||||||
width: calc(100% - 24px);
|
width: calc(100% - 24px);
|
||||||
@ -95,7 +89,7 @@ export default defineComponent({
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
&--tags {
|
&--tags {
|
||||||
max-height: 16px;
|
max-height: 32px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
.tags__container {
|
.tags__container {
|
||||||
@ -109,7 +103,6 @@ export default defineComponent({
|
|||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@include tags.tag-style;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,9 +42,13 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// watch(resultRef,result=>{
|
watch(
|
||||||
// if()
|
() => props.result,
|
||||||
// })
|
(resultProp) => {
|
||||||
|
resultRef.value = resultProp
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
return { resultRef, isMax }
|
return { resultRef, isMax }
|
||||||
},
|
},
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="single-content__wrapper" v-if="postData">
|
<div class="single-content__wrapper">
|
||||||
<div class="featuer-image__wrapper">
|
<div class="featuer-image__wrapper" v-if="postData.publistTimeBrief">
|
||||||
<FeatureImage :data="postData"></FeatureImage>
|
<FeatureImage :data="postData"></FeatureImage>
|
||||||
</div>
|
</div>
|
||||||
<div class="article__wrapper">
|
<div class="article__wrapper" v-if="postData.content">
|
||||||
<Article :content="postData.content"></Article>
|
<Article :content="postData.content"></Article>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-loader__wrapper" v-show="postFetchStatus === 'fetching'">
|
<div class="content-loader__wrapper" v-show="postFetchStatus === 'pending'">
|
||||||
<BookLoader></BookLoader>
|
<BookLoader></BookLoader>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment__wrapper">
|
<div class="comment__wrapper" v-if="postId">
|
||||||
<Comment :postId="postId"></Comment>
|
<Comment :postId="postId"></Comment>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed } from 'vue'
|
import { defineComponent, computed } from 'vue'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
import contentHandler from './utils/contentHandler'
|
import contentHandler from './utils/contentHandler'
|
||||||
import FeatureImage from './components/FeatureImage.vue'
|
import FeatureImage from './components/FeatureImage.vue'
|
||||||
import Article from './components/Article.vue'
|
import Article from './components/Article.vue'
|
||||||
@ -32,7 +33,8 @@ export default defineComponent({
|
|||||||
setup(props) {
|
setup(props) {
|
||||||
const { postData, postFetchStatus } = contentHandler(props)
|
const { postData, postFetchStatus } = contentHandler(props)
|
||||||
const postId = computed(() => {
|
const postId = computed(() => {
|
||||||
return postData.value?.id
|
if (isEmpty(postData.value)) return false
|
||||||
|
return (postData.value as Post)?.id
|
||||||
})
|
})
|
||||||
return { postData, postFetchStatus, postId }
|
return { postData, postFetchStatus, postId }
|
||||||
},
|
},
|
||||||
@ -41,6 +43,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '@/styles/mixins/tags';
|
@use '@/styles/mixins/tags';
|
||||||
|
@use '@/styles/mixins/sizes';
|
||||||
@use '@/styles/mixins/skeleton';
|
@use '@/styles/mixins/skeleton';
|
||||||
.single-content__wrapper {
|
.single-content__wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -51,15 +54,11 @@ export default defineComponent({
|
|||||||
.featuer-image__wrapper {
|
.featuer-image__wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.article__wrapper {
|
.article__wrapper,
|
||||||
width: 100%;
|
|
||||||
max-width: 800px;
|
|
||||||
padding-top: 24px;
|
|
||||||
}
|
|
||||||
.comment__wrapper {
|
.comment__wrapper {
|
||||||
width: 100%;
|
width: calc(100% - 12px * 2);
|
||||||
max-width: 800px;
|
max-width: #{sizes.$post-main-content-max-width}; // 800px
|
||||||
padding-top: 24px;
|
padding: 24px 12px 0 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -31,17 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-box">
|
<div class="flex-box">
|
||||||
<div class="column__wrapper--publish">
|
<div class="column__wrapper--publish">
|
||||||
<span>{{ $props.data.publistTime }}</span>
|
<span>{{ $props.data.publistTimeBrief }}</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-box">
|
|
||||||
<div class="column__wrapper--words">
|
|
||||||
<span>{{ $props.data.wordCount }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-box">
|
|
||||||
<div class="column__wrapper--reads">
|
|
||||||
<span>{{ $props.data.readCount }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -74,6 +64,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '@/styles/mixins/text';
|
@use '@/styles/mixins/text';
|
||||||
|
@use '@/styles/mixins/sizes';
|
||||||
@use '@/styles/mixins/polyfills';
|
@use '@/styles/mixins/polyfills';
|
||||||
.feature-image__container {
|
.feature-image__container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -89,20 +80,21 @@ export default defineComponent({
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
&--image {
|
// &--image {
|
||||||
}
|
// }
|
||||||
&--pattern {
|
&--pattern {
|
||||||
background: yellowgreen;
|
background: yellowgreen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.post-info__wrapper {
|
.post-info__wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: #{sizes.$post-main-content-max-width}; // 800px
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
margin: 0 12px;
|
||||||
> * {
|
> * {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
@ -116,8 +108,8 @@ export default defineComponent({
|
|||||||
line-height: 48px;
|
line-height: 48px;
|
||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
// @include text.line-number-limit(1);
|
@include text.line-number-limit(4);
|
||||||
// @include text.text-shadow-offset;
|
@include text.text-shadow-offset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&--info {
|
&--info {
|
||||||
@ -164,14 +156,10 @@ export default defineComponent({
|
|||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&--author {
|
// &--author {
|
||||||
}
|
// }
|
||||||
&--publish {
|
// &--publish {
|
||||||
}
|
// }
|
||||||
&--words {
|
|
||||||
}
|
|
||||||
&--reads {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,12 +24,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, watch, computed, toRefs, onMounted, nextTick, ref } from 'vue'
|
import { defineComponent, watch, computed, toRefs, onMounted, nextTick, ref, Comment } from 'vue'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
import camelcaseKeys from 'camelcase-keys'
|
import camelcaseKeys from 'camelcase-keys'
|
||||||
import { useInjector, useState, useRoute } from '@/hooks'
|
import { useInjector, useState, useRoute, useMessage, useIntl } from '@/hooks'
|
||||||
import { comments } from '@/store'
|
import { comments } from '@/store'
|
||||||
import API from '@/api'
|
import API from '@/api'
|
||||||
|
import axiosErrorHandler from '@/utils/axiosErrorHandler'
|
||||||
import CommentList from './CommentList.vue'
|
import CommentList from './CommentList.vue'
|
||||||
import Pagination from '@/components/pagination/Pagination.vue'
|
import Pagination from '@/components/pagination/Pagination.vue'
|
||||||
import Composer from './Composer.vue'
|
import Composer from './Composer.vue'
|
||||||
@ -40,6 +41,8 @@ export default defineComponent({
|
|||||||
postId: Number,
|
postId: Number,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
|
const addMessage = useMessage()
|
||||||
|
const intl = useIntl()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
// const commentPagination = {
|
// const commentPagination = {
|
||||||
// hash: route.hash, // TODO: support nested
|
// hash: route.hash, // TODO: support nested
|
||||||
@ -51,7 +54,7 @@ export default defineComponent({
|
|||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [perPage, setPerpage] = useState(10)
|
const [perPage, setPerpage] = useState(10)
|
||||||
const [totalPage, setTotalPage] = useState(1)
|
const [totalPage, setTotalPage] = useState(1)
|
||||||
const [commentData, setCommentData] = useState([])
|
const [commentData, setCommentData] = useState([] as Comment[])
|
||||||
|
|
||||||
const namespace = computed(() => `comment-for-post-${postId.value}`)
|
const namespace = computed(() => `comment-for-post-${postId.value}`)
|
||||||
|
|
||||||
@ -92,18 +95,27 @@ export default defineComponent({
|
|||||||
API.Sakura.v1
|
API.Sakura.v1
|
||||||
.createComment({ authorEmail, authorName, authorUrl, content, parent, post })
|
.createComment({ authorEmail, authorName, authorUrl, content, parent, post })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const _commentData = cloneDeep(commentData.value)
|
const _commentData = cloneDeep(commentData.value) as Comment[]
|
||||||
_commentData.push(camelcaseKeys(res.data))
|
_commentData.push(camelcaseKeys(res.data))
|
||||||
setCommentData(_commentData)
|
setCommentData(_commentData)
|
||||||
console.log(res.data, commentData.value)
|
// console.log(res.data, commentData.value)
|
||||||
|
addMessage({
|
||||||
|
type: 'success',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'messages.comment.submit.success',
|
||||||
|
defaultMessage: 'Comment post successfully.',
|
||||||
|
}),
|
||||||
|
})
|
||||||
composerRef.value?.clearInputContent()
|
composerRef.value?.clearInputContent()
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error.response) {
|
const titleMsg = intl.formatMessage({
|
||||||
console.error(error.response)
|
id: 'messages.comment.submit.error',
|
||||||
} else {
|
defaultMessage: 'Comment post failure.',
|
||||||
console.error(error)
|
})
|
||||||
}
|
const errorMsg = axiosErrorHandler(error).msg
|
||||||
|
console.log(errorMsg)
|
||||||
|
addMessage({ type: 'error', title: titleMsg, detail: errorMsg, closeTimeout: 0 })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,38 +11,40 @@
|
|||||||
></OutlinedTextarea>
|
></OutlinedTextarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="row__wrapper--profile">
|
<div class="row__wrapper--profile">
|
||||||
<div class="column__wrapper--avatar">
|
<div class="flex-box">
|
||||||
<div class="avatar__wrapper mdc-elevation--z1">
|
<div class="column__wrapper--avatar">
|
||||||
<Image :src="avatar" placeholder="" :avatar="false" alt="" :draggable="false"></Image>
|
<div class="avatar__wrapper mdc-elevation--z1">
|
||||||
|
<Image :src="avatar" placeholder="" :avatar="false" alt="" :draggable="false"></Image>
|
||||||
|
</div>
|
||||||
|
<div class="icon__wrapper avatar__wrapper mdc-elevation--z2">
|
||||||
|
<span class="gravatar">
|
||||||
|
<!-- <i class="fab fa-qq"></i> -->
|
||||||
|
<i class="fab fa-google"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="icon__wrapper avatar__wrapper mdc-elevation--z2">
|
<div class="column__wrapper--input username">
|
||||||
<span class="gravatar">
|
<OutlinedInput
|
||||||
<!-- <i class="fab fa-qq"></i> -->
|
v-model:content="inputAuthorName"
|
||||||
<i class="fab fa-google"></i>
|
leadingIcon="fas fa-user"
|
||||||
</span>
|
:label="messages.nickname"
|
||||||
|
></OutlinedInput>
|
||||||
|
</div>
|
||||||
|
<div class="column__wrapper--input">
|
||||||
|
<OutlinedInput
|
||||||
|
v-model:content="inputAuthorEmail"
|
||||||
|
leadingIcon="fas fa-envelope"
|
||||||
|
:label="messages.email"
|
||||||
|
@blur="handleEmailInputBlurEvent"
|
||||||
|
></OutlinedInput>
|
||||||
|
</div>
|
||||||
|
<div class="column__wrapper--input">
|
||||||
|
<OutlinedInput
|
||||||
|
v-model:content="inputAuthorUrl"
|
||||||
|
leadingIcon="fas fa-home"
|
||||||
|
:label="messages.link"
|
||||||
|
></OutlinedInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="column__wrapper--input">
|
|
||||||
<OutlinedInput
|
|
||||||
v-model:content="inputAuthorName"
|
|
||||||
leadingIcon="fas fa-user"
|
|
||||||
:label="messages.nickname"
|
|
||||||
></OutlinedInput>
|
|
||||||
</div>
|
|
||||||
<div class="column__wrapper--input">
|
|
||||||
<OutlinedInput
|
|
||||||
v-model:content="inputAuthorEmail"
|
|
||||||
leadingIcon="fas fa-envelope"
|
|
||||||
:label="messages.email"
|
|
||||||
@blur="handleEmailInputBlurEvent"
|
|
||||||
></OutlinedInput>
|
|
||||||
</div>
|
|
||||||
<div class="column__wrapper--input">
|
|
||||||
<OutlinedInput
|
|
||||||
v-model:content="inputAuthorUrl"
|
|
||||||
leadingIcon="fas fa-home"
|
|
||||||
:label="messages.link"
|
|
||||||
></OutlinedInput>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="row__wrapper--options"></div> -->
|
<!-- <div class="row__wrapper--options"></div> -->
|
||||||
@ -164,47 +166,70 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
&--profile {
|
&--profile {
|
||||||
display: flex;
|
> .flex-box {
|
||||||
flex-flow: row nowrap;
|
position: relative;
|
||||||
justify-content: space-between;
|
display: flex;
|
||||||
align-items: center;
|
flex-flow: row nowrap;
|
||||||
@include polyfills.flex-gap(12px, 'row nowrap');
|
justify-content: space-between;
|
||||||
.column__wrapper {
|
align-items: center;
|
||||||
&--avatar {
|
@include polyfills.flex-gap(12px, 'row nowrap');
|
||||||
flex: 0 0 auto;
|
@media screen and (max-width: 800px) {
|
||||||
position: relative;
|
flex-flow: column nowrap;
|
||||||
> .avatar__wrapper {
|
@include polyfills.flex-gap-unset('row nowrap');
|
||||||
width: 56px;
|
@include polyfills.flex-gap(12px, 'column nowrap');
|
||||||
height: 56px;
|
}
|
||||||
border-radius: 50%;
|
.column__wrapper {
|
||||||
overflow: hidden;
|
&--avatar {
|
||||||
}
|
flex: 0 0 auto;
|
||||||
> .icon__wrapper {
|
position: relative;
|
||||||
position: absolute;
|
@media screen and (max-width: 800px) {
|
||||||
right: 0;
|
position: absolute;
|
||||||
bottom: 0;
|
top: 0;
|
||||||
width: 20px;
|
right: 0;
|
||||||
height: 20px;
|
transform: scale(0.8);
|
||||||
background: #03a9f4;
|
}
|
||||||
border-radius: 50%;
|
> .avatar__wrapper {
|
||||||
display: flex;
|
width: 56px;
|
||||||
align-items: center;
|
height: 56px;
|
||||||
justify-content: center;
|
border-radius: 50%;
|
||||||
span {
|
overflow: hidden;
|
||||||
width: 12px;
|
}
|
||||||
height: 12px;
|
> .icon__wrapper {
|
||||||
color: #fff;
|
position: absolute;
|
||||||
line-height: 12px;
|
right: 0;
|
||||||
font-size: small;
|
bottom: 0;
|
||||||
&.gravatar {
|
width: 20px;
|
||||||
transform: rotate(270deg);
|
height: 20px;
|
||||||
|
background: #03a9f4;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
span {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 12px;
|
||||||
|
font-size: small;
|
||||||
|
&.gravatar {
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&--input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
@media screen and (max-width: 800px) {
|
||||||
|
&.username {
|
||||||
|
::v-deep() {
|
||||||
|
.mdc-text-field__input {
|
||||||
|
width: calc(100% - 80px);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
&--input {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
import { computed, onMounted, Ref, nextTick } from 'vue'
|
import { computed, onMounted, Ref, nextTick } from 'vue'
|
||||||
import { useInjector, useState, useIntl, useRoute } from '@/hooks'
|
import { isEmpty } from 'lodash'
|
||||||
import { posts } from '@/store'
|
import { useInjector, useState, useIntl, useRoute, useMessage, useCommonMessages } from '@/hooks'
|
||||||
|
import { posts, messages } from '@/store'
|
||||||
import { GetPostParams, GetPageParams } from '@/api/Wp/v2' // interfaces
|
import { GetPostParams, GetPageParams } from '@/api/Wp/v2' // interfaces
|
||||||
import postFilter from '@/utils/filters/postFilter'
|
import postFilter from '@/utils/filters/postFilter'
|
||||||
|
|
||||||
|
export type Content = Post | {}
|
||||||
|
|
||||||
export default function setup(props: {
|
export default function setup(props: {
|
||||||
readonly singleType?: string | undefined
|
readonly singleType?: string | undefined
|
||||||
readonly pageType?: string | undefined
|
readonly pageType?: string | undefined
|
||||||
}) {
|
}) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const addMessage = useMessage()
|
||||||
|
const commonMessages = useCommonMessages()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const [fetchStatus, setFetchStatus] = useState('fetching')
|
const [fetchStatus, setFetchStatus] = useState('inite' as FetchingStatus)
|
||||||
const [content, setContent]: [Ref<Post>, (attr: any) => any] = useState(null)
|
const [content, setContent] = useState({} as Content)
|
||||||
const {
|
const {
|
||||||
postsStore,
|
postsStore,
|
||||||
fetchPost,
|
fetchPost,
|
||||||
@ -19,13 +24,22 @@ export default function setup(props: {
|
|||||||
getPostsList,
|
getPostsList,
|
||||||
}: { postsStore: Ref<PostStore>; [key: string]: any } = useInjector(posts) // TODO: fix useInjector return type
|
}: { postsStore: Ref<PostStore>; [key: string]: any } = useInjector(posts) // TODO: fix useInjector return type
|
||||||
|
|
||||||
|
// TODO: [bug] https://github.com/mashirozx/sakura-next/issues/148
|
||||||
|
// console.log('[DEBUG]', route.params)
|
||||||
|
// addMessage({
|
||||||
|
// title: '[DEBUG] contentHandler',
|
||||||
|
// detail: JSON.stringify(route.params),
|
||||||
|
// type: 'info',
|
||||||
|
// closeTimeout: 0,
|
||||||
|
// })
|
||||||
|
|
||||||
const { slug, postId, postname } = route.params
|
const { slug, postId, postname } = route.params
|
||||||
const isSingle = props.singleType ? true : false
|
const isSingle = props.singleType ? true : false
|
||||||
const namespace = isSingle ? `single-${postId || postname}` : `page-${slug}`
|
const namespace = isSingle ? `single-${postId || postname}` : `page-${slug}`
|
||||||
|
|
||||||
// get parsed post content
|
// get parsed post content
|
||||||
const data = computed(() =>
|
const data = computed(() =>
|
||||||
content.value ? postFilter(content.value, isSingle ? 'single' : 'page') : null
|
!isEmpty(content.value) ? postFilter(content.value as Post, isSingle ? 'single' : 'page') : {}
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultFetchOpts: GetPostParams | GetPageParams = { page: 1, perPage: 1 }
|
const defaultFetchOpts: GetPostParams | GetPageParams = { page: 1, perPage: 1 }
|
||||||
@ -33,42 +47,62 @@ export default function setup(props: {
|
|||||||
if (postId) {
|
if (postId) {
|
||||||
defaultFetchOpts['include'] = Number(postId)
|
defaultFetchOpts['include'] = Number(postId)
|
||||||
} else if (postname) {
|
} else if (postname) {
|
||||||
defaultFetchOpts['slug'] = postname
|
defaultFetchOpts['slug'] = postname as string
|
||||||
} else if (slug) {
|
} else if (slug) {
|
||||||
defaultFetchOpts['slug'] = slug
|
defaultFetchOpts['slug'] = slug as string
|
||||||
} else if (isSingle) {
|
} else if (isSingle) {
|
||||||
throw new Error(
|
// TODO: should wait router https://github.com/mashirozx/sakura-next/issues/148
|
||||||
intl.formatMessage(
|
const errorMsg = intl.formatMessage(
|
||||||
{
|
{
|
||||||
id: 'messages.wordpress.permalink.shouldIncludeFieldsInSingle',
|
id: 'messages.wordpress.permalink.shouldIncludeFieldsInSingle',
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
'WordPress permalink should include at least one of %post_id%, %postname%.\nYou may set them here: {baseUrl}/wp-admin/options-permalink.php',
|
'WordPress permalink should include at least one of %post_id%, %postname%.\nYou may set them here: {baseUrl}/wp-admin/options-permalink.php',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
baseUrl: window.location.origin,
|
baseUrl: window.location.origin,
|
||||||
}
|
}
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
addMessage({
|
||||||
|
title: commonMessages.javascriptErrorTitle,
|
||||||
|
detail: errorMsg,
|
||||||
|
type: 'error',
|
||||||
|
closeTimeout: 0,
|
||||||
|
})
|
||||||
|
console.error(errorMsg)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
const errorMsg = intl.formatMessage({
|
||||||
intl.formatMessage({
|
id: 'messages.wordpress.permalink.shouldIncludeFieldsInPost',
|
||||||
id: 'messages.wordpress.permalink.shouldIncludeFieldsInPost',
|
defaultMessage: 'WordPress pages should use %slug% as the permalink.',
|
||||||
defaultMessage: 'WordPress pages should use %slug% as the permalink.',
|
})
|
||||||
})
|
addMessage({
|
||||||
)
|
title: commonMessages.javascriptErrorTitle,
|
||||||
|
detail: errorMsg,
|
||||||
|
type: 'error',
|
||||||
|
closeTimeout: 0,
|
||||||
|
})
|
||||||
|
console.error(errorMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchContent = () => {
|
const fetchContent = () => {
|
||||||
const fetchOption = isSingle ? fetchPost : fetchPage
|
const fetchOption = isSingle ? fetchPost : fetchPage
|
||||||
setFetchStatus('fetching')
|
if (fetchStatus.value !== 'cached') {
|
||||||
|
setFetchStatus('pending')
|
||||||
|
}
|
||||||
fetchOption({
|
fetchOption({
|
||||||
state: postsStore,
|
state: postsStore,
|
||||||
namespace,
|
namespace,
|
||||||
opts: { ...defaultFetchOpts },
|
opts: { ...defaultFetchOpts },
|
||||||
}).then(() => {
|
addMessage,
|
||||||
getContent()
|
|
||||||
setFetchStatus('done')
|
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
getContent()
|
||||||
|
setFetchStatus('success')
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setFetchStatus('error')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getContent = () => {
|
const getContent = () => {
|
||||||
@ -84,8 +118,17 @@ export default function setup(props: {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
getContent()
|
getContent()
|
||||||
// setFetchStatus('refreshing')
|
if ((content.value as Post)?.content) {
|
||||||
// TODO: use a transparent mask (or just a popup) to show: 'refeshing content', when it fails or timeout, show popup. If the postsStore is empty, show BookLoader. In other words, BookLoader should only be displayed when real fetching API.
|
setFetchStatus('cached')
|
||||||
|
const msg = intl.formatMessage({
|
||||||
|
id: 'messages.postContent.cache.found',
|
||||||
|
defaultMessage: 'Fetching the latest post content...',
|
||||||
|
})
|
||||||
|
addMessage({
|
||||||
|
type: 'info',
|
||||||
|
title: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
fetchContent()
|
fetchContent()
|
||||||
})
|
})
|
||||||
|
52
src/components/link/Link.vue
Normal file
52
src/components/link/Link.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="$props.url === null">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<router-link v-else-if="to" :to="to">
|
||||||
|
<slot></slot>
|
||||||
|
</router-link>
|
||||||
|
<a v-else href="https://google.com" target="_blank">
|
||||||
|
<slot></slot>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, computed } from 'vue'
|
||||||
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
|
import type { RouterLinkTo } from './types'
|
||||||
|
import linkHandler from '@/utils/linkHandler'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
url: String,
|
||||||
|
routerName: String,
|
||||||
|
routerParams: Object,
|
||||||
|
routerPath: String,
|
||||||
|
routerQuery: Object,
|
||||||
|
to: Object,
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const to = computed(() => {
|
||||||
|
if (props.to) return props.to as RouteLocationRaw
|
||||||
|
const _to: RouterLinkTo = {}
|
||||||
|
if (props.url && linkHandler.isInternal(props.url || '')) {
|
||||||
|
_to['path'] = linkHandler.internalLinkRouterPath(props.url)
|
||||||
|
return _to
|
||||||
|
} else if (props.routerName || props.routerPath) {
|
||||||
|
if (props.routerName) {
|
||||||
|
_to['name'] = props.routerName
|
||||||
|
} else {
|
||||||
|
_to['path'] = props.routerPath
|
||||||
|
_to['params'] = props.routerParams ?? undefined
|
||||||
|
}
|
||||||
|
_to['query'] = props.routerQuery ?? undefined
|
||||||
|
return _to
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { to }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
6
src/components/link/types.ts
Normal file
6
src/components/link/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface RouterLinkTo {
|
||||||
|
path?: string
|
||||||
|
name?: string
|
||||||
|
params?: { [key: string]: any }
|
||||||
|
query?: { [key: string]: any }
|
||||||
|
}
|
@ -7,7 +7,7 @@
|
|||||||
:type="index % 2 ? 'normal' : 'reverse'"
|
:type="index % 2 ? 'normal' : 'reverse'"
|
||||||
></PostThumbCardIndex>
|
></PostThumbCardIndex>
|
||||||
</div>
|
</div>
|
||||||
<div class="loader__wrapper" v-show="fetchStatus === 'fetching'">
|
<div class="loader__wrapper" v-show="fetchStatus === 'pending'">
|
||||||
<BookLoader></BookLoader>
|
<BookLoader></BookLoader>
|
||||||
</div>
|
</div>
|
||||||
<div class="last-page__wrapper" v-show="isTheLastPage">no more</div>
|
<div class="last-page__wrapper" v-show="isTheLastPage">no more</div>
|
||||||
@ -17,7 +17,14 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed, onMounted, Ref } from 'vue'
|
import { defineComponent, computed, onMounted, Ref } from 'vue'
|
||||||
import { useInjector, useState, useElementRef, useReachElementSide } from '@/hooks'
|
import {
|
||||||
|
useInjector,
|
||||||
|
useState,
|
||||||
|
useElementRef,
|
||||||
|
useReachElementSide,
|
||||||
|
useMessage,
|
||||||
|
useIntl,
|
||||||
|
} from '@/hooks'
|
||||||
import { posts } from '@/store'
|
import { posts } from '@/store'
|
||||||
import PostThumbCardIndex from '@/components/cards/postThumbCards/PostThumbCardIndex.vue'
|
import PostThumbCardIndex from '@/components/cards/postThumbCards/PostThumbCardIndex.vue'
|
||||||
import BookLoader from '@/components/loader/BookLoader.vue'
|
import BookLoader from '@/components/loader/BookLoader.vue'
|
||||||
@ -37,8 +44,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
|
const addMessage = useMessage()
|
||||||
|
const intl = useIntl()
|
||||||
const [listContainerRef, setListContainerRef] = useElementRef()
|
const [listContainerRef, setListContainerRef] = useElementRef()
|
||||||
const [fetchStatus, setFetchStatus] = useState('fetching')
|
const [fetchStatus, setFetchStatus] = useState('inite' as FetchingStatus)
|
||||||
const [currentPage, setCurrentPage] = useState(props.page)
|
const [currentPage, setCurrentPage] = useState(props.page)
|
||||||
const [postList, setPostList]: [Ref<Post[]>, (attr: any) => any] = useState([] as Post[])
|
const [postList, setPostList]: [Ref<Post[]>, (attr: any) => any] = useState([] as Post[])
|
||||||
const {
|
const {
|
||||||
@ -66,7 +75,9 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetch = async () => {
|
const fetch = async () => {
|
||||||
setFetchStatus('fetching')
|
if (fetchStatus.value !== 'cached') {
|
||||||
|
setFetchStatus('pending')
|
||||||
|
}
|
||||||
fetchPost({
|
fetchPost({
|
||||||
state: postsStore,
|
state: postsStore,
|
||||||
namespace: props.namespace,
|
namespace: props.namespace,
|
||||||
@ -76,10 +87,15 @@ export default defineComponent({
|
|||||||
context: 'embed',
|
context: 'embed',
|
||||||
...props.fetchParameters,
|
...props.fetchParameters,
|
||||||
},
|
},
|
||||||
}).then(() => {
|
addMessage,
|
||||||
get()
|
|
||||||
setFetchStatus('done')
|
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
get()
|
||||||
|
setFetchStatus('success')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setFetchStatus('error')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
@ -108,11 +124,21 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// this will only work when set to cache post store
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
get()
|
get()
|
||||||
// setFetchStatus('done')
|
if (postList.value.length > 0) {
|
||||||
// TODO: use a transparent mask (or just a popup) to show: 'refeshing content', when it fails or timeout, show popup. If the postsStore is empty, show BookLoader. In other words, BookLoader should only be displayed when real fetching API.
|
setFetchStatus('cached')
|
||||||
}, 500) // postsStore injection may not be OK when mounted
|
const msg = intl.formatMessage({
|
||||||
|
id: 'messages.postList.cache.found',
|
||||||
|
defaultMessage: 'Fetching the latest post list...',
|
||||||
|
})
|
||||||
|
addMessage({
|
||||||
|
type: 'info',
|
||||||
|
title: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 0) // postsStore injection may not be OK when mounted
|
||||||
fetch()
|
fetch()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -141,20 +141,20 @@ export default defineComponent({
|
|||||||
|
|
||||||
$i: 2;
|
$i: 2;
|
||||||
@while $i < 6 {
|
@while $i < 6 {
|
||||||
$delay: $i * 15 - 30;
|
$delay: $i * 15% - 30%;
|
||||||
@keyframes page-#{$i} {
|
@keyframes page-#{$i} {
|
||||||
#{0 + $delay}% {
|
#{0% + $delay} {
|
||||||
transform: rotateY(180deg);
|
transform: rotateY(180deg);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
#{20 + $delay}% {
|
#{20% + $delay} {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
#{35 + $delay}%,
|
#{35% + $delay},
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
#{50 + $delay}%,
|
#{50% + $delay},
|
||||||
100% {
|
100% {
|
||||||
transform: rotateY(0deg);
|
transform: rotateY(0deg);
|
||||||
}
|
}
|
||||||
|
@ -11,14 +11,6 @@
|
|||||||
<div class="title">
|
<div class="title">
|
||||||
<span>{{ $props.message.title }}</span>
|
<span>{{ $props.message.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="detailed"
|
|
||||||
:style="{ height: shouldShowDetail ? `${expandContentHeight}px` : '0px' }"
|
|
||||||
>
|
|
||||||
<div :class="['content', { show: shouldShowDetail }]" :ref="setExpandContentRef">
|
|
||||||
<span>{{ $props.message.detail }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="$props.message.detail"
|
v-if="$props.message.detail"
|
||||||
@ -32,6 +24,16 @@
|
|||||||
<i class="fas fa-times-circle"></i>
|
<i class="fas fa-times-circle"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row__wrapper--detail">
|
||||||
|
<div
|
||||||
|
class="detailed"
|
||||||
|
:style="{ maxHeight: shouldShowDetail ? `${expandContentHeight}px` : '0px' }"
|
||||||
|
>
|
||||||
|
<div class="content" :ref="setExpandContentRef">
|
||||||
|
<span>{{ $props.message.detail }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -114,6 +116,7 @@ export default defineComponent({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use "sass:color";
|
@use "sass:color";
|
||||||
@use '@/styles/mixins/polyfills';
|
@use '@/styles/mixins/polyfills';
|
||||||
|
@use '@/styles/mixins/text';
|
||||||
|
|
||||||
.item__container {
|
.item__container {
|
||||||
--text-color: #3c434a;
|
--text-color: #3c434a;
|
||||||
@ -131,13 +134,14 @@ export default defineComponent({
|
|||||||
&[type='error'] {
|
&[type='error'] {
|
||||||
--highlight-color: #f93154; // danger
|
--highlight-color: #f93154; // danger
|
||||||
}
|
}
|
||||||
width: var(--width);
|
width: var(--msg-width);
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
border-left: 3px solid var(--highlight-color, #757575);
|
border-left: 3px solid var(--highlight-color, #757575);
|
||||||
> .item__content {
|
> .item__content {
|
||||||
width: calc(100% - 24px);
|
width: calc(100% - 24px);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
> .flex-box {
|
> .flex-box {
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
align-items: space-between;
|
align-items: space-between;
|
||||||
@ -157,16 +161,18 @@ export default defineComponent({
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
// overflow-wrap: anywhere;
|
||||||
> .row__wrapper {
|
> .row__wrapper {
|
||||||
&--title {
|
&--title {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@include polyfills.flex-gap(12px, 'row nowrap');
|
@include polyfills.flex-gap(12px, 'row nowrap');
|
||||||
|
width: calc(100% + 12px);
|
||||||
> * span {
|
> * span {
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
|
@include text.word-break;
|
||||||
}
|
}
|
||||||
> .title__content {
|
> .title__content {
|
||||||
&--message {
|
&--message {
|
||||||
@ -177,35 +183,36 @@ export default defineComponent({
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
> .detailed {
|
|
||||||
transition: height 0.5s cubic-bezier(0, 0, 0.3, 1);
|
|
||||||
overflow: hidden;
|
|
||||||
> .content {
|
|
||||||
padding-top: 6px;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
transform: scaleY(0);
|
|
||||||
transform-origin: top;
|
|
||||||
transition: transform 0.5s cubic-bezier(0, 0, 0.3, 1);
|
|
||||||
&.show {
|
|
||||||
transform: scaleY(1);
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
color: var(--text-color-lighter-30);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&--collapse {
|
&--collapse {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
transform: scaleY(1);
|
transform: scaleY(1);
|
||||||
transition: transform 0.5s cubic-bezier(0, 0, 0.3, 1);
|
transition: transform 0.5s cubic-bezier(0, 0, 0.3, 1);
|
||||||
|
cursor: pointer;
|
||||||
&.reverse {
|
&.reverse {
|
||||||
transform: scaleY(-1);
|
transform: scaleY(-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&--close {
|
&--close {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&--detail {
|
||||||
|
> .detailed {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 0;
|
||||||
|
transition: max-height 0.3s ease-in-out;
|
||||||
|
overflow: hidden;
|
||||||
|
> .content {
|
||||||
|
padding-top: 6px;
|
||||||
|
width: 100%;
|
||||||
|
span {
|
||||||
|
color: var(--text-color-lighter-30);
|
||||||
|
@include text.word-break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ export default defineComponent({
|
|||||||
props: {
|
props: {
|
||||||
positionX: { type: String, default: 'right' }, // left center right
|
positionX: { type: String, default: 'right' }, // left center right
|
||||||
positionY: { type: String, default: 'top' }, // top bottom
|
positionY: { type: String, default: 'top' }, // top bottom
|
||||||
width: { type: String, default: '380px' },
|
// width: { type: String, default: '380px' },
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { messageList } = useInjector(messages)
|
const { messageList } = useInjector(messages)
|
||||||
@ -43,7 +43,7 @@ export default defineComponent({
|
|||||||
'--to-70': props.positionX === 'right' ? '100%' : '-100%',
|
'--to-70': props.positionX === 'right' ? '100%' : '-100%',
|
||||||
'--to-100': props.positionX === 'right' ? '100%' : '-100%',
|
'--to-100': props.positionX === 'right' ? '100%' : '-100%',
|
||||||
'--absolute-fix': props.positionY === 'bottom' ? '-100%' : '0',
|
'--absolute-fix': props.positionY === 'bottom' ? '-100%' : '0',
|
||||||
'--width': props.width,
|
// '--width': props.width,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -54,7 +54,17 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.messages__container {
|
.messages__container {
|
||||||
width: calc(var(--width) + 12px);
|
--msg-width: var(--message-width, 380px);
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
--msg-width: var(--message-width, 300px);
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
--msg-width: var(--message-width, 250px);
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 360px) {
|
||||||
|
--msg-width: var(--message-width, 80vw);
|
||||||
|
}
|
||||||
|
width: calc(var(--msg-width) + 12px);
|
||||||
.message__wrapper {
|
.message__wrapper {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ export default defineComponent({
|
|||||||
.map((item, index) => index === props.result)
|
.map((item, index) => index === props.result)
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: watcher's bug on deep mode: https://github.com/vuejs/vue/issues/2164
|
// watcher's bug on deep mode: https://github.com/vuejs/vue/issues/2164
|
||||||
const cacheArrayRef = computed(() => cloneDeep(arrayRef.value))
|
const cacheArrayRef = computed(() => cloneDeep(arrayRef.value))
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -76,6 +76,14 @@ export default defineComponent({
|
|||||||
.map((item, index) => index === props.result))
|
.map((item, index) => index === props.result))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.result,
|
||||||
|
(resultProp) => {
|
||||||
|
arrayRef.value = cloneDeep(arrayRef.value).map((item) => false)
|
||||||
|
if (resultProp !== NaN) arrayRef.value[resultProp] = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return { arrayRef }
|
return { arrayRef }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
role="switch"
|
role="switch"
|
||||||
:aria-checked="checked"
|
:aria-checked="checked"
|
||||||
:ref="setElRef"
|
:ref="setElRef"
|
||||||
@click="handleChange"
|
|
||||||
>
|
>
|
||||||
<div class="mdc-switch__track"></div>
|
<div class="mdc-switch__track"></div>
|
||||||
<div class="mdc-switch__handle-track">
|
<div class="mdc-switch__handle-track">
|
||||||
@ -35,7 +34,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, watch } from 'vue'
|
import { defineComponent, ref, watch } from 'vue'
|
||||||
import uniqueHash from '@/utils/uniqueHash'
|
import uniqueHash from '@/utils/uniqueHash'
|
||||||
import { useElementRef } from '@/hooks'
|
import { useElementRef, useIntervalWatcher } from '@/hooks'
|
||||||
import useMDCSwitch from '@/hooks/mdc/useMDCSwitch'
|
import useMDCSwitch from '@/hooks/mdc/useMDCSwitch'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -54,17 +53,17 @@ export default defineComponent({
|
|||||||
|
|
||||||
const checked = ref(props.checked)
|
const checked = ref(props.checked)
|
||||||
|
|
||||||
const handleChange = () => {
|
useIntervalWatcher(() => {
|
||||||
if (MDCSwitchRef.value) {
|
if (MDCSwitchRef.value && MDCSwitchRef.value.selected !== checked.value) {
|
||||||
checked.value = !MDCSwitchRef.value.selected
|
checked.value = MDCSwitchRef.value.selected
|
||||||
}
|
}
|
||||||
}
|
}, 100)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.checked,
|
() => props.checked,
|
||||||
(value) => {
|
(value) => {
|
||||||
checked.value = value
|
checked.value = value
|
||||||
if (MDCSwitchRef.value) MDCSwitchRef.value.selected = !value
|
if (MDCSwitchRef.value) MDCSwitchRef.value.selected = value
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
@ -87,7 +86,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
watch(checked, (value) => emit('update:checked', value))
|
watch(checked, (value) => emit('update:checked', value))
|
||||||
|
|
||||||
return { id, setElRef, handleChange, checked }
|
return { id, setElRef, checked }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -8,8 +8,11 @@ import useReachElementSide from './useReachElementSide'
|
|||||||
import { useElementRef, useElementRefs } from './useElementRef'
|
import { useElementRef, useElementRefs } from './useElementRef'
|
||||||
import useOffsetDistance from './useOffsetDistance'
|
import useOffsetDistance from './useOffsetDistance'
|
||||||
import useMDCRipple from './mdc/useMDCRipple'
|
import useMDCRipple from './mdc/useMDCRipple'
|
||||||
import useMessage from './useMessage'
|
import useMessage, { useCommonMessages } from './useMessage'
|
||||||
import useTypewriterEffect from './useTypewriterEffect'
|
import useTypewriterEffect from './useTypewriterEffect'
|
||||||
|
import useIntervalWatcher from './useIntervalWatcher'
|
||||||
|
import useKeepAliveWindowScrollTop from './useKeepAliveWindowScrollTop'
|
||||||
|
import useWindowScrollLock from './useWindowScrollLock'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useState,
|
useState,
|
||||||
@ -29,5 +32,9 @@ export {
|
|||||||
useElementRefs,
|
useElementRefs,
|
||||||
useOffsetDistance,
|
useOffsetDistance,
|
||||||
useMessage,
|
useMessage,
|
||||||
|
useCommonMessages,
|
||||||
useTypewriterEffect,
|
useTypewriterEffect,
|
||||||
|
useIntervalWatcher,
|
||||||
|
useKeepAliveWindowScrollTop,
|
||||||
|
useWindowScrollLock,
|
||||||
}
|
}
|
||||||
|
18
src/hooks/useIntervalWatcher.ts
Normal file
18
src/hooks/useIntervalWatcher.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { onMounted, onUnmounted, onActivated, onDeactivated } from 'vue'
|
||||||
|
export default function useIntervalWatcher(func: () => void, interval = 100): void {
|
||||||
|
let timer = NaN
|
||||||
|
const addWatcher = () => {
|
||||||
|
if (timer) return
|
||||||
|
timer = window.setInterval(func, interval)
|
||||||
|
}
|
||||||
|
const removeWatcher = () => {
|
||||||
|
if (!timer) return
|
||||||
|
window.clearInterval(timer)
|
||||||
|
timer = NaN
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => addWatcher())
|
||||||
|
onActivated(() => addWatcher())
|
||||||
|
onUnmounted(() => removeWatcher())
|
||||||
|
onDeactivated(() => removeWatcher())
|
||||||
|
}
|
27
src/hooks/useKeepAliveWindowScrollTop.ts
Normal file
27
src/hooks/useKeepAliveWindowScrollTop.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { onDeactivated, watch, onActivated } from 'vue'
|
||||||
|
import { useWindowScroll } from '@vueuse/core'
|
||||||
|
import { useState } from '@/hooks'
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const { scrollTop, scrollLeft } = (function () {
|
||||||
|
const { x, y } = useWindowScroll()
|
||||||
|
return { scrollTop: y, scrollLeft: x }
|
||||||
|
})()
|
||||||
|
|
||||||
|
const [scrollTopCache, setScrollTopCache] = useState(0)
|
||||||
|
const [isScrollTopSet, setIsScrollTopSet] = useState(false)
|
||||||
|
|
||||||
|
watch(scrollTop, (value) => {
|
||||||
|
if (!isScrollTopSet.value) return
|
||||||
|
setScrollTopCache(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
window.scrollTo(scrollLeft.value ?? 0, scrollTopCache.value)
|
||||||
|
setIsScrollTopSet(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
setIsScrollTopSet(false)
|
||||||
|
})
|
||||||
|
}
|
@ -1,16 +1,24 @@
|
|||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { useInjector } from '@/hooks'
|
import { useInjector, useIntl } from '@/hooks'
|
||||||
import { messages } from '@/store'
|
import { messages } from '@/store'
|
||||||
import type { Message, MessageOptions } from '@/store/messages'
|
import type { Message, MessageOptions } from '@/store/messages'
|
||||||
|
|
||||||
export default function useMessage() {
|
/**
|
||||||
const {
|
* deprecated
|
||||||
messageList,
|
*/
|
||||||
addMessage,
|
export interface UseMessageInjecter {
|
||||||
}: {
|
messageList: Ref<Message[]>
|
||||||
messageList: Ref<Message[]>
|
addMessage: (state: Ref<Message[]>, options: MessageOptions) => void
|
||||||
addMessage: (state: Ref<Message[]>, options: MessageOptions) => void
|
}
|
||||||
} = useInjector(messages)
|
|
||||||
|
/**
|
||||||
|
* @param useMessageInjecter (deprecated)
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function useMessage(useMessageInjecter?: UseMessageInjecter) {
|
||||||
|
const { messageList, addMessage }: UseMessageInjecter = useMessageInjecter
|
||||||
|
? useMessageInjecter
|
||||||
|
: useInjector(messages)
|
||||||
|
|
||||||
const _addMessage = (options: MessageOptions) => {
|
const _addMessage = (options: MessageOptions) => {
|
||||||
addMessage(messageList, options)
|
addMessage(messageList, options)
|
||||||
@ -18,3 +26,13 @@ export default function useMessage() {
|
|||||||
|
|
||||||
return _addMessage
|
return _addMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useCommonMessages = () => {
|
||||||
|
const intl = useIntl()
|
||||||
|
return {
|
||||||
|
javascriptErrorTitle: intl.formatMessage({
|
||||||
|
id: 'messages.commonMessages.javascriptErrorTitle',
|
||||||
|
defaultMessage: 'Opps, something when wrong!',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
29
src/hooks/useWindowScrollLock.ts
Normal file
29
src/hooks/useWindowScrollLock.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { onUnmounted, onDeactivated } from 'vue'
|
||||||
|
import getScrollbarWidth from '@/utils/getScrollbarWidth'
|
||||||
|
export default function () {
|
||||||
|
const removeScrollLock = () => {
|
||||||
|
const body = document.querySelector('body')
|
||||||
|
// TODO: add a fake scroll bar element
|
||||||
|
if (body instanceof HTMLElement) {
|
||||||
|
body.style.overflow = 'auto'
|
||||||
|
body.style.width = '100%'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const addScrollLock = () => {
|
||||||
|
const body = document.querySelector('body')
|
||||||
|
if (body instanceof HTMLElement) {
|
||||||
|
body.style.overflow = 'hidden'
|
||||||
|
body.style.width = `calc(100% - ${String(getScrollbarWidth())}px)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
removeScrollLock()
|
||||||
|
})
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
removeScrollLock()
|
||||||
|
})
|
||||||
|
|
||||||
|
return [removeScrollLock, addScrollLock]
|
||||||
|
}
|
@ -36,9 +36,13 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed, onUnmounted, onDeactivated } from 'vue'
|
import { defineComponent, computed, onUnmounted, onDeactivated } from 'vue'
|
||||||
import { throttle, xor } from 'lodash'
|
import { throttle } from 'lodash'
|
||||||
import { useState, useWindowResize } from '@/hooks'
|
import {
|
||||||
import getScrollbarWidth from '@/utils/getScrollbarWidth'
|
useState,
|
||||||
|
useWindowResize,
|
||||||
|
useKeepAliveWindowScrollTop,
|
||||||
|
useWindowScrollLock,
|
||||||
|
} from '@/hooks'
|
||||||
import Header from '@/layouts/components/header/Header.vue'
|
import Header from '@/layouts/components/header/Header.vue'
|
||||||
import Footer from '@/layouts/components/footer/Footer.vue'
|
import Footer from '@/layouts/components/footer/Footer.vue'
|
||||||
import HeaderMobile from '@/layouts/components/header/HeaderMobile.vue'
|
import HeaderMobile from '@/layouts/components/header/HeaderMobile.vue'
|
||||||
@ -49,25 +53,13 @@ export default defineComponent({
|
|||||||
components: { Header, Footer, HeaderMobile, NavDrawer },
|
components: { Header, Footer, HeaderMobile, NavDrawer },
|
||||||
props: { headerPlaceholder: { type: Boolean, default: true } },
|
props: { headerPlaceholder: { type: Boolean, default: true } },
|
||||||
setup() {
|
setup() {
|
||||||
|
useKeepAliveWindowScrollTop()
|
||||||
const windowSize = useWindowResize()
|
const windowSize = useWindowResize()
|
||||||
const isMobile = computed(() => windowSize.value.innerWidth <= 600)
|
const isMobile = computed(() => windowSize.value.innerWidth <= 600)
|
||||||
const [shouldDrawerOpen, setShouldDrawerOpen] = useState(false)
|
const [shouldDrawerOpen, setShouldDrawerOpen] = useState(false)
|
||||||
|
|
||||||
const removeScrollLock = () => {
|
const [removeScrollLock, addScrollLock] = useWindowScrollLock()
|
||||||
const body = document.querySelector('body')
|
|
||||||
// TODO: add a fake scroll bar element
|
|
||||||
if (body instanceof HTMLElement) {
|
|
||||||
body.style.overflow = 'auto'
|
|
||||||
body.style.width = '100%'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const addScrollLock = () => {
|
|
||||||
const body = document.querySelector('body')
|
|
||||||
if (body instanceof HTMLElement) {
|
|
||||||
body.style.overflow = 'hidden'
|
|
||||||
body.style.width = `calc(100% - ${String(getScrollbarWidth())}px)`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const toggleDrawer = throttle(
|
const toggleDrawer = throttle(
|
||||||
() => {
|
() => {
|
||||||
setShouldDrawerOpen(!shouldDrawerOpen.value)
|
setShouldDrawerOpen(!shouldDrawerOpen.value)
|
||||||
@ -76,14 +68,6 @@ export default defineComponent({
|
|||||||
} else {
|
} else {
|
||||||
removeScrollLock()
|
removeScrollLock()
|
||||||
}
|
}
|
||||||
// const body = document.querySelector('body')
|
|
||||||
// if (body instanceof HTMLElement) {
|
|
||||||
// body.style.overflow = xor(['hidden', 'auto'], [body.style.overflow])[0]
|
|
||||||
// body.style.width = xor(
|
|
||||||
// [`calc(100% - ${String(getScrollbarWidth())}px)`, '100%'],
|
|
||||||
// [body.style.width]
|
|
||||||
// )[0]
|
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
500,
|
500,
|
||||||
{
|
{
|
||||||
@ -101,12 +85,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
setShouldDrawerOpen(false)
|
setShouldDrawerOpen(false)
|
||||||
removeScrollLock()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
setShouldDrawerOpen(false)
|
setShouldDrawerOpen(false)
|
||||||
removeScrollLock()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return { isMobile, handleMDrawerToggleEvent, shouldDrawerOpen, handleClickFakeAfterEvent }
|
return { isMobile, handleMDrawerToggleEvent, shouldDrawerOpen, handleClickFakeAfterEvent }
|
||||||
@ -115,6 +97,11 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use '@/styles/app';
|
||||||
|
::v-deep() {
|
||||||
|
@include app.global;
|
||||||
|
}
|
||||||
|
|
||||||
$drawer-width: 260px;
|
$drawer-width: 260px;
|
||||||
.page {
|
.page {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
<NavItem
|
<NavItem
|
||||||
:context="parent.title"
|
:context="parent.title"
|
||||||
:prefix="parent.icon"
|
:prefix="parent.icon"
|
||||||
:url="parent.child.length > 0 ? '' : parent.url"
|
:url="parent.child.length > 0 ? null : parent.url"
|
||||||
:suffix="parent.child.length > 0 ? 'fas fa-chevron-down' : ''"
|
:suffix="parent.child.length > 0 ? 'fas fa-chevron-down' : ''"
|
||||||
></NavItem>
|
></NavItem>
|
||||||
</div>
|
</div>
|
||||||
@ -175,6 +175,18 @@ export default defineComponent({
|
|||||||
height: 36px;
|
height: 36px;
|
||||||
background: rgba(2, 1, 1, 0);
|
background: rgba(2, 1, 1, 0);
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
::v-deep() {
|
||||||
|
.nav-item__content {
|
||||||
|
.icon--suffix {
|
||||||
|
transform: scale(0.6);
|
||||||
|
transform-origin: right;
|
||||||
|
i {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&--child {
|
&--child {
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
@ -193,6 +205,15 @@ export default defineComponent({
|
|||||||
.ul__content {
|
.ul__content {
|
||||||
&--tag {
|
&--tag {
|
||||||
background: rgba(2, 1, 1, 0.05);
|
background: rgba(2, 1, 1, 0.05);
|
||||||
|
::v-deep() {
|
||||||
|
.nav-item__content {
|
||||||
|
.icon--suffix {
|
||||||
|
i {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&--child {
|
&--child {
|
||||||
max-height: var(--collapse-height);
|
max-height: var(--collapse-height);
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="nav-item__container mdc-list-item" :ref="setContainerRef" @click="handleClickEvent">
|
<Link class="link__container" :url="$props.url">
|
||||||
<div class="mdc-list-item__ripple"></div>
|
<div class="nav-item__container mdc-list-item" :ref="setContainerRef">
|
||||||
<span class="nav-item__content mdc-list-item__text">
|
<div class="mdc-list-item__ripple"></div>
|
||||||
<span class="icon icon--prefix" v-if="prefix">
|
<span class="nav-item__content mdc-list-item__text">
|
||||||
<i :class="prefix"></i>
|
<span class="icon icon--prefix" v-if="prefix">
|
||||||
|
<i :class="prefix"></i>
|
||||||
|
</span>
|
||||||
|
<span class="context">{{ context }}</span>
|
||||||
|
<span class="icon icon--suffix" v-if="suffix">
|
||||||
|
<i :class="suffix"></i>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="context">{{ context }}</span>
|
</div>
|
||||||
<span class="icon icon--suffix" v-if="suffix">
|
</Link>
|
||||||
<i :class="suffix"></i>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -44,34 +46,38 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.nav-item__container {
|
.link__container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: pointer;
|
.nav-item__container {
|
||||||
position: relative;
|
|
||||||
&.mdc-list-item {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
.nav-item__content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
width: 100%;
|
||||||
flex-flow: row nowrap;
|
cursor: pointer;
|
||||||
justify-content: center;
|
position: relative;
|
||||||
align-items: center;
|
&.mdc-list-item {
|
||||||
padding: 0 24px;
|
padding-left: 0;
|
||||||
span {
|
padding-right: 0;
|
||||||
color: #5f6368;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
.icon {
|
.nav-item__content {
|
||||||
&--prefix {
|
width: 100%;
|
||||||
padding-right: 12px;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
span {
|
||||||
|
color: #5f6368;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
&--suffix {
|
.icon {
|
||||||
padding-left: 12px;
|
&--prefix {
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
&--suffix {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import { auth, init, posts, comments, messages } from './store'
|
|||||||
import { intlPlugin } from './locales'
|
import { intlPlugin } from './locales'
|
||||||
import UiIcon from '@/components/icon/UiIcon.vue'
|
import UiIcon from '@/components/icon/UiIcon.vue'
|
||||||
import Image from '@/components/image/Image.vue'
|
import Image from '@/components/image/Image.vue'
|
||||||
|
import Link from '@/components/link/Link.vue'
|
||||||
|
|
||||||
const theWindow = window as any
|
const theWindow = window as any
|
||||||
theWindow.router = router
|
theWindow.router = router
|
||||||
@ -20,4 +21,5 @@ app.use(intlPlugin)
|
|||||||
app.use(VueSvgIconPlugin, { tagName: 'svg-icon' })
|
app.use(VueSvgIconPlugin, { tagName: 'svg-icon' })
|
||||||
app.component('UiIcon', UiIcon)
|
app.component('UiIcon', UiIcon)
|
||||||
app.component('Image', Image)
|
app.component('Image', Image)
|
||||||
|
app.component('Link', Link)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
@ -15,8 +15,9 @@ interface FetchParams {
|
|||||||
|
|
||||||
export default function comments(): object {
|
export default function comments(): object {
|
||||||
const defaultCommentStore: CommentStore = {}
|
const defaultCommentStore: CommentStore = {}
|
||||||
const [commentStore, setCommentStore]: [Ref<CommentStore>, (arg: CommentStore) => void] =
|
const [commentStore, setCommentStore] = false
|
||||||
usePersistedState('commentStore', defaultCommentStore)
|
? usePersistedState('commentStore', defaultCommentStore)
|
||||||
|
: useState(defaultCommentStore)
|
||||||
|
|
||||||
const resHandler = (
|
const resHandler = (
|
||||||
state: FetchParams['state'],
|
state: FetchParams['state'],
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Ref } from 'vue'
|
|
||||||
import { cloneDeep, remove } from 'lodash'
|
import { cloneDeep, remove } from 'lodash'
|
||||||
import { useState } from '@/hooks'
|
import { useState } from '@/hooks'
|
||||||
import uniqueHash from '@/utils/uniqueHash'
|
import uniqueHash from '@/utils/uniqueHash'
|
||||||
@ -31,7 +30,7 @@ export default function msg(): object {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeTimeout = options.closeTimeout || 3000
|
const closeTimeout = options.closeTimeout || 6000
|
||||||
|
|
||||||
setTimeout(() => removeMessage(state, id), closeTimeout)
|
setTimeout(() => removeMessage(state, id), closeTimeout)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { usePersistedState, useState } from '@/hooks'
|
import { usePersistedState, useState } from '@/hooks'
|
||||||
|
import type { MessageOptions } from '@/store/messages'
|
||||||
import camelcaseKeys from 'camelcase-keys'
|
import camelcaseKeys from 'camelcase-keys'
|
||||||
import { AxiosResponse } from 'axios' // interface
|
import { AxiosResponse } from 'axios' // interface
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
@ -7,11 +8,14 @@ import API from '@/api'
|
|||||||
import { GetPostParams, GetPageParams } from '@/api/Wp/v2' // interface
|
import { GetPostParams, GetPageParams } from '@/api/Wp/v2' // interface
|
||||||
import { getPagination } from '@/utils/filters/paginationFilter'
|
import { getPagination } from '@/utils/filters/paginationFilter'
|
||||||
import logger from '@/utils/logger'
|
import logger from '@/utils/logger'
|
||||||
|
import axiosErrorHandler from '@/utils/axiosErrorHandler'
|
||||||
|
import intl from '@/locales'
|
||||||
|
|
||||||
interface FetchParams {
|
export interface FetchParams {
|
||||||
state: Ref<PostStore>
|
state: Ref<PostStore>
|
||||||
namespace: string
|
namespace: string
|
||||||
opts: GetPostParams | GetPageParams
|
opts: GetPostParams | GetPageParams
|
||||||
|
addMessage: (options: MessageOptions) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function posts(): object {
|
export default function posts(): object {
|
||||||
@ -24,7 +28,9 @@ export default function posts(): object {
|
|||||||
data: {},
|
data: {},
|
||||||
list: {},
|
list: {},
|
||||||
}
|
}
|
||||||
const [postsStore, setPostsStore] = usePersistedState('postsStore', defaultStore)
|
const [postsStore, setPostsStore] = false
|
||||||
|
? usePersistedState('postsStore', defaultStore)
|
||||||
|
: useState(defaultStore)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common method of handling API response of Array(WP_POST)
|
* Common method of handling API response of Array(WP_POST)
|
||||||
@ -94,11 +100,8 @@ export default function posts(): object {
|
|||||||
/**
|
/**
|
||||||
* Fetch posts list from API /wp-json/wp/v2/posts
|
* Fetch posts list from API /wp-json/wp/v2/posts
|
||||||
* TODO: what's the correct type of readonly (state)?
|
* TODO: what's the correct type of readonly (state)?
|
||||||
* @param state
|
|
||||||
* @param type
|
|
||||||
* @param axiosOptions
|
|
||||||
*/
|
*/
|
||||||
const fetchPost = async ({ state, namespace, opts }: FetchParams) => {
|
const fetchPost = async ({ state, namespace, opts, addMessage }: FetchParams) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
API.Wp.v2
|
API.Wp.v2
|
||||||
.getPosts(opts as GetPostParams)
|
.getPosts(opts as GetPostParams)
|
||||||
@ -108,6 +111,12 @@ export default function posts(): object {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger('error', error)
|
logger('error', error)
|
||||||
|
const errorMsgTitle = intl.formatMessage({
|
||||||
|
id: 'messages.posts.fetchPostError',
|
||||||
|
defaultMessage: 'Failed to fetch post content.',
|
||||||
|
})
|
||||||
|
const errorMsg = axiosErrorHandler(error).msg
|
||||||
|
addMessage({ type: 'error', title: errorMsgTitle, detail: errorMsg, closeTimeout: 0 })
|
||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -116,11 +125,8 @@ export default function posts(): object {
|
|||||||
/**
|
/**
|
||||||
* Fetch posts list from API /wp-json/wp/v2/posts
|
* Fetch posts list from API /wp-json/wp/v2/posts
|
||||||
* TODO: what's the correct type of readonly (state)?
|
* TODO: what's the correct type of readonly (state)?
|
||||||
* @param state
|
|
||||||
* @param type
|
|
||||||
* @param axiosOptions
|
|
||||||
*/
|
*/
|
||||||
const fetchPage = async ({ state, namespace, opts }: FetchParams) => {
|
const fetchPage = async ({ state, namespace, opts, addMessage }: FetchParams) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
API.Wp.v2
|
API.Wp.v2
|
||||||
.getPages(opts as GetPageParams)
|
.getPages(opts as GetPageParams)
|
||||||
@ -130,6 +136,12 @@ export default function posts(): object {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger('error', error)
|
logger('error', error)
|
||||||
|
const errorMsgTitle = intl.formatMessage({
|
||||||
|
id: 'messages.posts.fetchPageError',
|
||||||
|
defaultMessage: 'Failed to fetch page content.',
|
||||||
|
})
|
||||||
|
const errorMsg = axiosErrorHandler(error).msg
|
||||||
|
addMessage({ type: 'error', title: errorMsgTitle, detail: errorMsg, closeTimeout: 0 })
|
||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
9
src/styles/_app.scss
Normal file
9
src/styles/_app.scss
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
@mixin global {
|
||||||
|
a,
|
||||||
|
a:hover,
|
||||||
|
a:focus,
|
||||||
|
a:active {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
@ -35,3 +35,41 @@
|
|||||||
@error "The second paramater $flex-flow is set to be '#{$flex-flow}', which is illegal.";
|
@error "The second paramater $flex-flow is set to be '#{$flex-flow}', which is illegal.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin _flex-gap-unset($row: true) {
|
||||||
|
$margin: 0;
|
||||||
|
$transform: 0;
|
||||||
|
@if $row {
|
||||||
|
margin-left: $transform;
|
||||||
|
margin-right: $transform;
|
||||||
|
} @else {
|
||||||
|
margin-top: $transform;
|
||||||
|
margin-bottom: $transform;
|
||||||
|
}
|
||||||
|
> * {
|
||||||
|
@if $row {
|
||||||
|
margin-left: $margin;
|
||||||
|
margin-right: $margin;
|
||||||
|
} @else {
|
||||||
|
margin-top: $margin;
|
||||||
|
margin-bottom: $margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unset flex-gap, used in @media screen width rules
|
||||||
|
@mixin flex-gap-unset($flex-flow: 'row nowrap') {
|
||||||
|
@if $flex-flow== 'row nowrap' or $flex-flow== 'row-reverse nowrap' {
|
||||||
|
@include _flex-gap-unset(true);
|
||||||
|
} @else if $flex-flow== 'column nowrap' or $flex-flow== 'column-reverse nowrap' {
|
||||||
|
@include _flex-gap-unset(false);
|
||||||
|
} @else if $flex-flow== 'row wrap' or $flex-flow== 'row-reverse wrap' {
|
||||||
|
@include _flex-gap-unset(true);
|
||||||
|
@include _flex-gap-unset(false);
|
||||||
|
} @else if $flex-flow== 'column wrap' or $flex-flow== 'column-reverse wrap' {
|
||||||
|
@include _flex-gap-unset(true);
|
||||||
|
@include _flex-gap-unset(false);
|
||||||
|
} @else {
|
||||||
|
@error "The second paramater $flex-flow is set to be '#{$flex-flow}', which is illegal.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1
src/styles/mixins/_sizes.scss
Normal file
1
src/styles/mixins/_sizes.scss
Normal file
@ -0,0 +1 @@
|
|||||||
|
$post-main-content-max-width: 800px;
|
@ -11,3 +11,8 @@
|
|||||||
padding: 10px 10px;
|
padding: 10px 10px;
|
||||||
transform: translate(-10px, 10px);
|
transform: translate(-10px, 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin word-break {
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
13
src/utils/axiosErrorHandler.ts
Normal file
13
src/utils/axiosErrorHandler.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export default function (error: any) {
|
||||||
|
if (error.response) {
|
||||||
|
return {
|
||||||
|
type: 'Response Error',
|
||||||
|
msg: error.response.data.message ?? error.response.data, // Standard WP_Rest_Error
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: 'Request Error',
|
||||||
|
msg: error.message, // eg. network error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import intl from '@/locales'
|
import intl from '@/locales'
|
||||||
import timeFormater from '@/utils/timeFormater'
|
|
||||||
import htmlStringInnerText from '@/utils/htmlStringInnerText'
|
import htmlStringInnerText from '@/utils/htmlStringInnerText'
|
||||||
import camelcaseKeys from 'camelcase-keys'
|
import camelcaseKeys from 'camelcase-keys'
|
||||||
import publishTime from './publishTime'
|
import publishTime from './publishTime'
|
||||||
@ -9,6 +8,7 @@ export default function (post: Post, type: 'single' | 'page' | 'thumbList') {
|
|||||||
const title = post.title.rendered
|
const title = post.title.rendered
|
||||||
|
|
||||||
const publistTime = publishTime(post.date)
|
const publistTime = publishTime(post.date)
|
||||||
|
const publistTimeBrief = publishTime(post.date, true)
|
||||||
|
|
||||||
const readCount = intl.formatMessage(
|
const readCount = intl.formatMessage(
|
||||||
{
|
{
|
||||||
@ -55,9 +55,12 @@ export default function (post: Post, type: 'single' | 'page' | 'thumbList') {
|
|||||||
const content = post.content ?? ''
|
const content = post.content ?? ''
|
||||||
const link = post.link
|
const link = post.link
|
||||||
|
|
||||||
|
const tags = post.tagsMeta ? camelcaseKeys(post.tagsMeta) : []
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
id,
|
id,
|
||||||
publistTime,
|
publistTime,
|
||||||
|
publistTimeBrief,
|
||||||
title,
|
title,
|
||||||
readCount,
|
readCount,
|
||||||
commentCount,
|
commentCount,
|
||||||
@ -67,6 +70,7 @@ export default function (post: Post, type: 'single' | 'page' | 'thumbList') {
|
|||||||
author,
|
author,
|
||||||
content,
|
content,
|
||||||
link,
|
link,
|
||||||
|
tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -1,22 +1,39 @@
|
|||||||
import intl from '@/locales'
|
import intl from '@/locales'
|
||||||
import timeFormater from '@/utils/timeFormater'
|
import timeFormater from '@/utils/timeFormater'
|
||||||
|
|
||||||
export default function (publishTime: string) {
|
export default function (publishTime: string, brief = false) {
|
||||||
const publistTimeDate = new timeFormater(publishTime)
|
const publistTimeDate = new timeFormater(publishTime)
|
||||||
const publistTime = publistTimeDate.moreThanOneYear()
|
if (brief) {
|
||||||
? intl.formatMessage(
|
return publistTimeDate.moreThanOneYear()
|
||||||
{
|
? intl.formatMessage(
|
||||||
id: 'posts.postTimeOn',
|
{
|
||||||
defaultMessage: 'Post on {publistTimeDate, date, long}',
|
id: 'posts.postTimeOn.brief',
|
||||||
},
|
defaultMessage: '{publistTimeDate, date, long}',
|
||||||
{ publistTimeDate: publistTimeDate.getDate() }
|
},
|
||||||
)
|
{ publistTimeDate: publistTimeDate.getDate() }
|
||||||
: intl.formatMessage(
|
)
|
||||||
{
|
: intl.formatMessage(
|
||||||
id: 'posts.postTimeSince',
|
{
|
||||||
defaultMessage: 'Post {duration} ago',
|
id: 'posts.postTimeSince',
|
||||||
},
|
defaultMessage: '{duration} ago',
|
||||||
{ duration: publistTimeDate.getReadableTimeFromNow() }
|
},
|
||||||
)
|
{ duration: publistTimeDate.getReadableTimeFromNowBrief() }
|
||||||
return publistTime
|
)
|
||||||
|
} else {
|
||||||
|
return publistTimeDate.moreThanOneYear()
|
||||||
|
? intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'posts.postTimeOn.full',
|
||||||
|
defaultMessage: 'Post on {publistTimeDate, date, long}',
|
||||||
|
},
|
||||||
|
{ publistTimeDate: publistTimeDate.getDate() }
|
||||||
|
)
|
||||||
|
: intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'posts.postTimeSince',
|
||||||
|
defaultMessage: 'Post {duration} ago',
|
||||||
|
},
|
||||||
|
{ duration: publistTimeDate.getReadableTimeFromNow() }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Router } from 'vue-router'
|
import type { Router } from 'vue-router'
|
||||||
import logger from './logger'
|
import logger from './logger'
|
||||||
|
|
||||||
export default class linkHandler {
|
export default class linkHandler {
|
||||||
@ -20,7 +20,8 @@ export default class linkHandler {
|
|||||||
|
|
||||||
public static internalLinkRouterPath(url: string) {
|
public static internalLinkRouterPath(url: string) {
|
||||||
if (this.isInternal(url)) {
|
if (this.isInternal(url)) {
|
||||||
const { pathname, search, hash } = this.urlParser(url)
|
const parsed = this.urlParser(url)
|
||||||
|
const { pathname, search, hash } = this.urlParser(parsed.href)
|
||||||
return pathname + search + hash
|
return pathname + search + hash
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Not internal link')
|
throw new Error('Not internal link')
|
||||||
@ -38,10 +39,9 @@ export default class linkHandler {
|
|||||||
}) {
|
}) {
|
||||||
logger('log', 'linkHandler: ', url)
|
logger('log', 'linkHandler: ', url)
|
||||||
if (this.isInternal(url)) {
|
if (this.isInternal(url)) {
|
||||||
const parsed = this.urlParser(url)
|
|
||||||
// TODO: why not import? cause vue codes cannot pass the jest test...
|
// TODO: why not import? cause vue codes cannot pass the jest test...
|
||||||
// router = router ?? ((window as any).router as Router)
|
// router = router ?? ((window as any).router as Router)
|
||||||
router.push(this.internalLinkRouterPath(parsed.href))
|
router.push(this.internalLinkRouterPath(url))
|
||||||
// window.setTimeout(() => (window.location.hash = parsed.hash))
|
// window.setTimeout(() => (window.location.hash = parsed.hash))
|
||||||
} else {
|
} else {
|
||||||
console.log('open: ', this.urlParser(url).href)
|
console.log('open: ', this.urlParser(url).href)
|
||||||
|
@ -15,7 +15,7 @@ export default class timeFormater {
|
|||||||
this.timestampFromNow = this.now - this.timestamp
|
this.timestampFromNow = this.now - this.timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
public getReadableTimeFromNow() {
|
public getTimeFromNow() {
|
||||||
const gap = this.timestampFromNow
|
const gap = this.timestampFromNow
|
||||||
let num: number = 0
|
let num: number = 0
|
||||||
let unit: Unit = 'second'
|
let unit: Unit = 'second'
|
||||||
@ -38,9 +38,20 @@ export default class timeFormater {
|
|||||||
num = gap / (365 * 24 * 60 * 60 * 1000)
|
num = gap / (365 * 24 * 60 * 60 * 1000)
|
||||||
unit = 'year'
|
unit = 'year'
|
||||||
}
|
}
|
||||||
|
return { num, unit }
|
||||||
|
}
|
||||||
|
|
||||||
|
public getReadableTimeFromNow() {
|
||||||
|
const { num, unit } = this.getTimeFromNow()
|
||||||
return intl.formatRelativeTime(Math.floor(num), unit, { style: 'narrow' })
|
return intl.formatRelativeTime(Math.floor(num), unit, { style: 'narrow' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getReadableTimeFromNowBrief() {
|
||||||
|
const { num, unit } = this.getTimeFromNow()
|
||||||
|
const _num = Math.floor(num)
|
||||||
|
return timeFormater.commonUnites(_num)[unit]
|
||||||
|
}
|
||||||
|
|
||||||
public getFormatTime(
|
public getFormatTime(
|
||||||
opts: FormatDateOptions = { year: 'numeric', month: 'numeric', day: 'numeric' }
|
opts: FormatDateOptions = { year: 'numeric', month: 'numeric', day: 'numeric' }
|
||||||
) {
|
) {
|
||||||
@ -60,4 +71,56 @@ export default class timeFormater {
|
|||||||
public getDate() {
|
public getDate() {
|
||||||
return this.date
|
return this.date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static commonUnites = (num: number) => {
|
||||||
|
const year = intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'app.common.units.year',
|
||||||
|
defaultMessage:
|
||||||
|
'{num, plural, =0 {just now} =1 {1 year} other {{num, number, ::compact-short} years}}',
|
||||||
|
},
|
||||||
|
{ num }
|
||||||
|
)
|
||||||
|
const month = intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'app.common.units.year',
|
||||||
|
defaultMessage:
|
||||||
|
'{num, plural, =0 {just now} =1 {1 month} other {{num, number, ::compact-short} monthes}}',
|
||||||
|
},
|
||||||
|
{ num }
|
||||||
|
)
|
||||||
|
const day = intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'app.common.units.year',
|
||||||
|
defaultMessage:
|
||||||
|
'{num, plural, =0 {just now} =1 {1 day} other {{num, number, ::compact-short} days}}',
|
||||||
|
},
|
||||||
|
{ num }
|
||||||
|
)
|
||||||
|
const hour = intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'app.common.units.year',
|
||||||
|
defaultMessage:
|
||||||
|
'{num, plural, =0 {just now} =1 {1 hour} other {{num, number, ::compact-short} hours}}',
|
||||||
|
},
|
||||||
|
{ num }
|
||||||
|
)
|
||||||
|
const minute = intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'app.common.units.year',
|
||||||
|
defaultMessage:
|
||||||
|
'{num, plural, =0 {just now} =1 {1 minute} other {{num, number, ::compact-short} minutes}}',
|
||||||
|
},
|
||||||
|
{ num }
|
||||||
|
)
|
||||||
|
const second = intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'app.common.units.year',
|
||||||
|
defaultMessage:
|
||||||
|
'{num, plural, =0 {just now} =1 {1 second} other {{num, number, ::compact-short} seconds}}',
|
||||||
|
},
|
||||||
|
{ num }
|
||||||
|
)
|
||||||
|
return { year, month, day, hour, minute, second }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user