Add mobile support (Header, Drawer, ThumbList)
@ -26,6 +26,53 @@
|
||||
"multiple": false
|
||||
}
|
||||
},
|
||||
"homepage.slogan": {
|
||||
"namespace": "homepage.slogan",
|
||||
"public": true,
|
||||
"title": "Slogan",
|
||||
"desc": "The slogan text (with typewriter effect), recommend 10-20 characters.",
|
||||
"type": "string",
|
||||
"default": "Hello World!"
|
||||
},
|
||||
"homepage.quote": {
|
||||
"namespace": "homepage.quote",
|
||||
"public": true,
|
||||
"title": "Quote",
|
||||
"desc": "The quote text (behinds the slogan).",
|
||||
"type": "longString",
|
||||
"default": "The most beautiful things in the world cannot be seen or even touched. \nThey must be felt with the heart."
|
||||
},
|
||||
"homepage.signature": {
|
||||
"namespace": "homepage.signature",
|
||||
"public": true,
|
||||
"title": "Signature",
|
||||
"desc": "The signature text (follows the quote).",
|
||||
"type": "string",
|
||||
"default": "—Helen Keller"
|
||||
},
|
||||
"homepage.cover.image": {
|
||||
"namespace": "homepage.cover.image",
|
||||
"public": true,
|
||||
"title": "Cover image",
|
||||
"desc": "Homepage cover image.",
|
||||
"type": "mediaPicker",
|
||||
"default": [
|
||||
{
|
||||
"id": 0,
|
||||
"url": "https://view.moezx.cc/images/2021/06/19/ca4748651c3c67e7e4c29c34fb13bc33.jpg"
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"url": "https://view.moezx.cc/images/2021/07/21/c21fcdbf4cf09674537d928884863ecc.jpg"
|
||||
}
|
||||
],
|
||||
"binds": {
|
||||
"title": "Select image for homepage cover.",
|
||||
"button": "Use this image",
|
||||
"type": "image",
|
||||
"multiple": true
|
||||
}
|
||||
},
|
||||
"social.github": {
|
||||
"namespace": "social.github",
|
||||
"public": true,
|
||||
|
@ -5,7 +5,7 @@
|
||||
define('SAKURA_VERSION', wp_get_theme()->get('Version'));
|
||||
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');
|
||||
|
||||
// PHP loaders
|
||||
|
@ -67,7 +67,7 @@ class AdminPageHelper extends ViteHelper
|
||||
|
||||
wp_localize_script('[type:module]chunk-entrance.js', 'InitState', (new InitStateController())->get_initial_state());
|
||||
|
||||
wp_localize_script('[type:module]chunk-entrance.js', 'SakuraOptions', (new OptionController())->get_all_options());
|
||||
wp_localize_script('[type:module]chunk-entrance.js', 'SakuraOptions', ['data' => (new OptionController())->get_all_options()]);
|
||||
|
||||
// <link rel="modulepreload" href="http://localhost:9000/assets/vendor.b3a324ba.js">
|
||||
foreach ($manifest[$entry_key]['imports'] as $index => $import) {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Sakura\Helpers;
|
||||
|
||||
use Sakura\Controllers\OptionController;
|
||||
// use Sakura\Controllers\OptionController;
|
||||
|
||||
class SetupHelper
|
||||
{
|
||||
@ -22,7 +22,8 @@ class SetupHelper
|
||||
// count post views
|
||||
add_action('get_header', [$this, 'set_post_views']);
|
||||
// Inite config options
|
||||
add_action('after_switch_theme', [new OptionController(), 'inite_theme'], 1, 2);
|
||||
// won't need anymore with options?
|
||||
// add_action('after_switch_theme', [new OptionController(), 'inite_theme'], 1, 2);
|
||||
}
|
||||
|
||||
public function setup()
|
||||
|
12
docs/dev.md
@ -1,7 +1,17 @@
|
||||
# Dev configurations
|
||||
|
||||
.env.development
|
||||
|
||||
```
|
||||
```env
|
||||
SSH_KEY_PATH='~/.ssh/id_rsa'
|
||||
SSH_REMOTE_HOST='root@8.8.8.8'
|
||||
SSH_REMOTE_WORK_DIR='/var/www/html/wp-contents/themes/sakura-next'
|
||||
```
|
||||
|
||||
add this rewrite rule to Nginx:
|
||||
|
||||
```nginx
|
||||
location /src/assets {
|
||||
rewrite ^/(.*)$ http://localhost:9000/$1 redirect;
|
||||
}
|
||||
```
|
||||
|
35
package.json
@ -25,29 +25,29 @@
|
||||
"rsync:composer": "nodemon --watch './composer.json' --watch './composer.lock' scripts/rsync.mjs --composer",
|
||||
"icon": "yarn svgo -f ./src/assets/icons/ui/ && node scripts/import-svg-icons.mjs && eslint \"src/components/icon/**/*.{ts,js,json,vue}\" --fix && prettier \"src/components/icon/**/*.{ts,js,json,vue}\" --write",
|
||||
"mdc": "node scripts/mdc-upgrade.mjs",
|
||||
"options": "node scripts/options-export/copy-options.mjs && yarn tsc scripts/options-export/dump-options.ts && node scripts/options-export/dump-options.js"
|
||||
"options": "node scripts/options-export/copy-options.mjs && yarn tsc scripts/options-export/dump-options.ts && node scripts/options-export/dump-options.js && eslint \"src/admin/optionsType.ts\" --fix && prettier \"src/admin/optionsType.ts\" --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl": "^1.13.2",
|
||||
"@material/button": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/card": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/checkbox": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/chips": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/dialog": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/elevation": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/form-field": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/list": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/menu": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/radio": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/ripple": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/switch": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/tab-bar": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/textfield": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/theme": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/typography": "^12.0.0-canary.9f68a932e.0",
|
||||
"@material/button": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/card": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/checkbox": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/chips": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/dialog": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/elevation": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/form-field": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/menu": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/radio": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/ripple": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/switch": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/tab-bar": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/textfield": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/theme": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@material/typography": "^12.0.0-canary.90e08fc6b.0",
|
||||
"@vueuse/core": "^5.1.3",
|
||||
"@yzfe/svgicon": "^1.0.1",
|
||||
"@yzfe/vue3-svgicon": "^1.0.1",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^0.21.1",
|
||||
"camelcase-keys": "^7.0.0",
|
||||
"chroma-js": "^2.1.2",
|
||||
@ -58,6 +58,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^2.1.3",
|
||||
"normalize.css": "^8.0.1",
|
||||
"perfect-scrollbar": "^1.5.2",
|
||||
"sass": "^1.35.1",
|
||||
"sass-loader": "^12.1.0",
|
||||
"snakecase-keys": "^4.0.2",
|
||||
|
@ -8,7 +8,12 @@ const template = (importContent, dataContent) => `
|
||||
<!-- # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -->
|
||||
<!-- # Generated by scripts/import-svg-icons.js -->
|
||||
<template>
|
||||
<svg-icon :data="svg[$props.name]" original></svg-icon>
|
||||
<svg-icon
|
||||
:data="svg[$props.name]"
|
||||
:width="$props.width"
|
||||
:height="$props.height"
|
||||
original
|
||||
></svg-icon>
|
||||
</template>
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
@ -17,6 +22,8 @@ const template = (importContent, dataContent) => `
|
||||
name: 'Icon',
|
||||
props: {
|
||||
name: { type: String },
|
||||
width: { type: String, default: '100%' },
|
||||
height: { type: String, default: '100%' },
|
||||
},
|
||||
setup(){
|
||||
const svg = {
|
||||
|
@ -3,13 +3,29 @@ import { writeFileSync } from 'fs'
|
||||
|
||||
const exportOptions: { [key: string]: any } = {}
|
||||
|
||||
const optionTypesTemplate = (fill: string) => {
|
||||
return `
|
||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
// Generated by scripts/options-export/dump-options.ts
|
||||
export interface SakuraOptions {
|
||||
${fill}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
let types = ''
|
||||
|
||||
Object.keys(options).forEach((tab) => {
|
||||
options[tab].options.forEach((option) => {
|
||||
if (option.depends) delete option.depends // remove function
|
||||
exportOptions[option.namespace] = option
|
||||
types += `'${option.namespace}': any, `
|
||||
})
|
||||
})
|
||||
|
||||
console.dir(exportOptions)
|
||||
|
||||
const optionTypes = optionTypesTemplate(types)
|
||||
|
||||
writeFileSync('./app/configs/options.json', JSON.stringify(exportOptions, null, 2), { flag: 'w+' })
|
||||
writeFileSync('./src/admin/optionsType.ts', optionTypes, { flag: 'w+' })
|
||||
|
@ -21,8 +21,8 @@ export interface Options {
|
||||
|
||||
const options: Options = {
|
||||
basic: {
|
||||
title: 'Basic',
|
||||
desc: 'The basic options',
|
||||
title: intl.formatMessage({ id: 'options.basicTitle', defaultMessage: 'Basic' }),
|
||||
desc: intl.formatMessage({ id: 'options.basicDesc', defaultMessage: 'The basic options' }),
|
||||
icon: 'fas fa-address-card',
|
||||
options: [
|
||||
// basic.site.title
|
||||
@ -69,6 +69,95 @@ const options: Options = {
|
||||
},
|
||||
],
|
||||
},
|
||||
homepage: {
|
||||
title: intl.formatMessage({ id: 'options.homepageTitle', defaultMessage: 'Homepage' }),
|
||||
desc: intl.formatMessage({ id: 'options.homepageDesc', defaultMessage: 'Homepage options' }),
|
||||
icon: 'fas fa-home',
|
||||
options: [
|
||||
// homepage.slogan
|
||||
{
|
||||
namespace: 'homepage.slogan',
|
||||
public: true,
|
||||
title: intl.formatMessage({
|
||||
id: 'options.homepage.slogan.title',
|
||||
defaultMessage: 'Slogan',
|
||||
}),
|
||||
desc: intl.formatMessage({
|
||||
id: 'options.homepage.slogan.desc',
|
||||
defaultMessage: 'The slogan text (with typewriter effect), recommend 10-20 characters.',
|
||||
}),
|
||||
type: 'string',
|
||||
default: 'Hello World!',
|
||||
},
|
||||
// homepage.quote
|
||||
{
|
||||
namespace: 'homepage.quote',
|
||||
public: true,
|
||||
title: intl.formatMessage({
|
||||
id: 'options.homepage.quote.title',
|
||||
defaultMessage: 'Quote',
|
||||
}),
|
||||
desc: intl.formatMessage({
|
||||
id: 'options.homepage.signature.desc',
|
||||
defaultMessage: 'The quote text (behinds the slogan).',
|
||||
}),
|
||||
type: 'longString',
|
||||
default:
|
||||
'The most beautiful things in the world cannot be seen or even touched. \nThey must be felt with the heart.',
|
||||
},
|
||||
// homepage.signature
|
||||
{
|
||||
namespace: 'homepage.signature',
|
||||
public: true,
|
||||
title: intl.formatMessage({
|
||||
id: 'options.homepage.signature.title',
|
||||
defaultMessage: 'Signature',
|
||||
}),
|
||||
desc: intl.formatMessage({
|
||||
id: 'options.homepage.signature.desc',
|
||||
defaultMessage: 'The signature text (follows the quote).',
|
||||
}),
|
||||
type: 'string',
|
||||
default: '—Helen Keller',
|
||||
},
|
||||
// homepage.cover.image
|
||||
{
|
||||
namespace: 'homepage.cover.image',
|
||||
public: true,
|
||||
title: intl.formatMessage({
|
||||
id: 'options.homepage.cover.image.title',
|
||||
defaultMessage: 'Cover images',
|
||||
}),
|
||||
desc: intl.formatMessage({
|
||||
id: 'options.homepage.cover.image.desc',
|
||||
defaultMessage: 'Homepage cover images. Will enable slide show with multiple selections.',
|
||||
}),
|
||||
type: 'mediaPicker',
|
||||
default: [
|
||||
{
|
||||
id: 0,
|
||||
url: 'https://view.moezx.cc/images/2021/06/19/ca4748651c3c67e7e4c29c34fb13bc33.jpg',
|
||||
},
|
||||
{
|
||||
id: 0,
|
||||
url: 'https://view.moezx.cc/images/2021/07/21/c21fcdbf4cf09674537d928884863ecc.jpg',
|
||||
},
|
||||
],
|
||||
binds: {
|
||||
title: intl.formatMessage({
|
||||
id: 'options.homepage.cover.image.binds.title',
|
||||
defaultMessage: 'Select image for homepage cover.',
|
||||
}),
|
||||
button: intl.formatMessage({
|
||||
id: 'options.homepage.cover.image.binds.button',
|
||||
defaultMessage: 'Use this image',
|
||||
}),
|
||||
type: 'image',
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
social: {
|
||||
title: 'Social',
|
||||
icon: 'fas fa-users',
|
||||
@ -171,7 +260,8 @@ const options: Options = {
|
||||
],
|
||||
},
|
||||
thirdParty: {
|
||||
title: 'Third party services',
|
||||
title: 'Third party',
|
||||
desc: 'The third party services options',
|
||||
icon: 'fas fa-bezier-curve',
|
||||
options: [
|
||||
// thirdParty.reCaptcha.enable
|
||||
|
27
src/admin/optionsType.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
// Generated by scripts/options-export/dump-options.ts
|
||||
export interface SakuraOptions {
|
||||
'basic.site.title': any
|
||||
'basic.site.logo': any
|
||||
'homepage.slogan': any
|
||||
'homepage.quote': any
|
||||
'homepage.signature': any
|
||||
'homepage.cover.image': any
|
||||
'social.github': any
|
||||
'social.gitlab': any
|
||||
'social.twitter': any
|
||||
'social.weibo': any
|
||||
'social.facebook': any
|
||||
'social.stackoverflow': any
|
||||
'thirdParty.reCaptcha.enable': any
|
||||
'thirdParty.reCaptcha.version': any
|
||||
'thirdParty.reCaptcha.siteKey': any
|
||||
'thirdParty.reCaptcha.secretKey': any
|
||||
'other.hello': any
|
||||
'demo.string': any
|
||||
'demo.longString': any
|
||||
'demo.switcher': any
|
||||
'demo.choose': any
|
||||
'demo.selection': any
|
||||
'demo.mediaPicker': any
|
||||
}
|
2
src/assets/icons/ui/social.email.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1626852417721" class="icon" viewBox="0 0 1385 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2021" xmlns:xlink="http://www.w3.org/1999/xlink" width="270.5078125" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }
|
||||
</style></defs><path d="M1226.571294 36.442353h-1090.258823c-74.992941 0-136.312471 54.211765-136.312471 120.470588v722.82353c0 66.258824 61.319529 120.470588 136.312471 120.470588h1090.258823c74.992941 0 136.312471-54.211765 136.312471-120.470588v-722.82353c0-66.258824-61.319529-120.470588-136.312471-120.470588z m0 240.941176l-545.129412 301.176471-545.129411-301.176471v-120.470588l545.129411 301.176471 545.129412-301.176471v120.470588z" fill="#FFCB05" p-id="2022"></path></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/icons/ui/social.telegram.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M1024 512c0 281.6-230.4 512-512 512S0 793.6 0 512 230.4 0 512 0s512 230.4 512 512z" fill="#36AAE8"/><path d="m696.32 778.24 102.4-486.4c10.24-40.96-15.36-61.44-46.08-51.2l-604.16 230.4c-40.96 15.36-40.96 40.96-5.12 51.2l153.6 51.2 363.52-230.4c15.36-10.24 30.72-5.12 20.48 5.12L389.12 614.4l-10.24 158.72c15.36 0 25.6-5.12 30.72-15.36l76.8-71.68 158.72 117.76c25.6 15.36 46.08 5.12 51.2-25.6z" fill="#FFF"/></svg>
|
After Width: | Height: | Size: 524 B |
@ -1 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M70.13 125.364h883.737v828.503H70.131z" fill="#1296db"/><path d="M513.159 1174.801c-268.49.663-510.524-161.06-613.258-408.949s-46.452-532.948 143.165-722.51S518.406-203.22 765.742-99.823a663.079 663.079 0 0 1 409.06 613.092c0 365.26-295.666 660.87-661.643 661.532zm145.816-956.479-20.547 2.651c-12.593 2.652-25.186 6.628-37.116 11.268a164.817 164.817 0 0 0-94.118 112.014c-6.628 25.849-6.628 53.024 0 78.21-25.187 0-50.373-3.314-74.234-9.942a487.712 487.712 0 0 1-202.873-98.757 305.718 305.718 0 0 1-49.103-48.385 135.267 135.267 0 0 1-15.244-17.896 185.419 185.419 0 0 0-17.896 43.083 172.66 172.66 0 0 0 37.117 151.781c8.617 10.605 21.873 17.233 30.489 26.513a43.8 43.8 0 0 1-22.535-1.989 163.16 163.16 0 0 1-35.129-9.942l-17.895-6.628a159.956 159.956 0 0 0 78.21 143.165c15.907 11.93 34.466 19.222 54.35 21.873-10.605 9.28-57.664 5.302-74.234 3.314 16.57 50.373 55.676 89.478 105.441 106.711 15.907 5.965 32.477 8.616 49.047 8.616a218.172 218.172 0 0 1-45.07 29.164 358.797 358.797 0 0 1-99.42 36.454c-15.245 3.314-31.152 2.651-48.385 5.302a236.952 236.952 0 0 1-53.024-1.988l14.581 8.616c15.245 9.28 30.49 17.233 47.06 23.861 31.814 13.256 64.291 23.861 97.431 31.815 78.21 16.57 159.79 13.919 236.676-7.954 173.71-54.35 283.735-181.608 325.546-369.18 6.628-35.13 9.28-71.583 7.954-107.375l26.512-21.872a278.1 278.1 0 0 0 56.338-64.955 330.849 330.849 0 0 1-94.118 25.85c6.628-3.315 12.594-7.954 18.559-13.257 25.186-20.547 43.745-47.721 53.687-78.21l-20.547 11.267a269.208 269.208 0 0 1-86.164 28.5 167.358 167.358 0 0 0-130.628-52.36v.662h-.718z" fill="#fff"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" style="enable-background:new 0 0 400 400" xml:space="preserve"><circle cx="200" cy="200" r="200" style="fill:#1b9df0"/><path d="M163.4 305.5c88.7 0 137.2-73.5 137.2-137.2 0-2.1 0-4.2-.1-6.2 9.4-6.8 17.6-15.3 24.1-25-8.6 3.8-17.9 6.4-27.7 7.6 10-6 17.6-15.4 21.2-26.7-9.3 5.5-19.6 9.5-30.6 11.7-8.8-9.4-21.3-15.2-35.2-15.2-26.6 0-48.2 21.6-48.2 48.2 0 3.8.4 7.5 1.3 11-40.1-2-75.6-21.2-99.4-50.4-4.1 7.1-6.5 15.4-6.5 24.2 0 16.7 8.5 31.5 21.5 40.1-7.9-.2-15.3-2.4-21.8-6v.6c0 23.4 16.6 42.8 38.7 47.3-4 1.1-8.3 1.7-12.7 1.7-3.1 0-6.1-.3-9.1-.9 6.1 19.2 23.9 33.1 45 33.5-16.5 12.9-37.3 20.6-59.9 20.6-3.9 0-7.7-.2-11.5-.7 21.1 13.8 46.5 21.8 73.7 21.8" style="fill:#fff"/></svg>
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 739 B |
@ -1 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M727.6 492.2c-34.8-6.7-17.9-25.1-17.9-25.1s34.1-55.5-6.8-95.9c-50.6-49.9-173.7 6.3-173.7 6.3-46.9 14.4-34.5-6.6-27.9-42.2 0-42-14.5-113-139.7-71.2-125.1 42.3-232.4 190.2-232.4 190.2-74.6 98.2-64.8 174.2-64.8 174.2C83 796.3 263.6 842.3 404.1 853.3c147.8 11.4 347.3-50.2 407.7-177 60.7-127-49.2-177.2-84.2-184.1zM415.1 806.1c-146.7 6.7-265.4-65.8-265.4-162.4 0-96.7 118.7-174.2 265.4-180.9 146.9-6.8 265.7 53 265.7 149.5 0 96.4-118.8 187.2-265.7 193.8z" fill="#E52429"/><path d="M799.5 433.1c12 0 22-8.7 23.8-20 .2-.9.3-1.6.3-2.5 18-159.9-132.8-132.3-132.8-132.3-13.3 0-24.1 10.6-24.1 24.1 0 13.2 10.7 23.8 24.1 23.8 108.3-23.6 84.5 83.3 84.5 83.3-.1 13 10.8 23.6 24.2 23.6z" fill="#F49500"/><path d="M782 154.1c-52.1-12.1-105.8-1.6-120.8 1.2-1.2.2-2.3 1.2-3.3 1.3-.5.2-.9.6-.9.6-14.8 4.2-25.6 17.6-25.6 33.6 0 19 15.6 34.7 35.1 34.7 0 0 18.9-2.5 31.8-7.5 12.7-5.1 120.8-3.7 174.4 85.1 29.2 64.9 12.9 108.3 10.8 115.3 0 0-7 16.9-7 33.5 0 19.1 15.6 31.3 35.1 31.3 16.2 0 29.8-2.2 33.8-29.3h.2c57.7-189.4-70.4-278.4-163.6-299.8z" fill="#F49500"/><path d="M385.9 526.3c-147.6 17-130.5 153.4-130.5 153.4s-1.5 43.2 39.5 65.2c86.3 46.2 175.3 18.2 220.1-39 45-57.1 18.7-196.5-129.1-179.6z"/><path d="M348.7 717.8c-27.6 3.1-49.8-12.6-49.8-35.3 0-22.6 19.7-46.4 47.2-49.2 31.6-3 52.2 15 52.2 37.8.2 22.7-22.2 43.6-49.6 46.7zm87-73.1c-9.4 6.9-20.9 6-25.7-2.4-5.1-8.1-3.2-20.9 6.2-27.7 10.9-8.1 22.3-5.7 27.3 2.4 4.8 8.1 1.3 20.5-7.8 27.7z" fill="#FFF"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 30"><g fill="none" fill-rule="evenodd"><path d="M2.715 20.11c0 4.203 5.6 7.613 12.506 7.613 6.907 0 12.506-3.41 12.506-7.613s-5.6-7.612-12.506-7.612c-6.907 0-12.506 3.409-12.506 7.612" fill="#fefefe"/><path d="M15.513 27.102c-6.114.59-11.39-2.111-11.788-6.035-.397-3.922 4.239-7.581 10.35-8.172 6.115-.591 11.39 2.11 11.789 6.032.395 3.924-4.238 7.584-10.35 8.175M27.74 14.078c-.521-.152-.878-.255-.604-.924.59-1.45.65-2.701.011-3.593-1.2-1.675-4.48-1.584-8.239-.045 0-.001-1.18.505-.878-.41.579-1.818.49-3.34-.409-4.219-2.039-1.995-7.464.075-12.115 4.62C2.023 12.914 0 16.523 0 19.643c0 5.97 7.831 9.598 15.492 9.598 10.043 0 16.724-5.702 16.724-10.231 0-2.737-2.358-4.29-4.476-4.932" fill="#d52c2b"/><path d="M34.409 3.154C31.984.526 28.405-.476 25.103.21c-.764.16-1.251.894-1.088 1.64.162.747.914 1.224 1.678 1.063 2.35-.487 4.891.226 6.617 2.093a6.687 6.687 0 0 1 1.452 6.647 1.373 1.373 0 0 0 .91 1.74c.743.235 1.54-.162 1.782-.888V12.5a9.393 9.393 0 0 0-2.045-9.346" fill="#e79115"/><path d="M30.684 6.44c-1.181-1.28-2.923-1.766-4.532-1.432a1.188 1.188 0 0 0-.935 1.413c.14.64.787 1.053 1.442.913v.002a2.366 2.366 0 0 1 2.217.698c.578.626.733 1.479.484 2.227h.002a1.187 1.187 0 0 0 .783 1.5c.64.199 1.326-.142 1.532-.768a4.57 4.57 0 0 0-.993-4.553" fill="#e79115"/><path d="M15.85 19.996c-.213.358-.686.53-1.057.38-.364-.146-.479-.545-.27-.897.212-.349.666-.52 1.03-.378.369.131.501.535.297.895m-1.947 2.445c-.593.921-1.859 1.325-2.812.9-.94-.418-1.217-1.49-.626-2.388.583-.897 1.808-1.295 2.754-.907.958.4 1.263 1.463.684 2.395m2.22-6.526c-2.909-.741-6.197.676-7.46 3.183-1.287 2.555-.042 5.392 2.897 6.32 3.043.96 6.632-.512 7.88-3.269 1.23-2.697-.306-5.474-3.317-6.234" fill="#060101"/></g></svg>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 1.5 KiB |
1
src/assets/icons/ui/social.youtube.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M711.111 512 375.467 705.422V318.578L711.11 512zM1024 694.044V329.956S1024 153.6 847.644 153.6H176.356S0 153.6 0 329.956v364.088S0 870.4 176.356 870.4h665.6c5.688 0 182.044 0 182.044-176.356" fill="#D81E06"/></svg>
|
After Width: | Height: | Size: 325 B |
BIN
src/assets/masks/dot.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
@ -41,6 +41,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- // TODO: use tags instead of button, button is useless! -->
|
||||
<div class="row__wrapper--button" @click="handleViewPostDetailEvent">
|
||||
<div class="button__wrapper">
|
||||
<NormalButton icon="fab fa-readme" :context="buttonContext"></NormalButton>
|
||||
@ -54,15 +55,13 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue'
|
||||
import { useIntl, useRouter, useElementRef, useMDCRipple } from '@/hooks'
|
||||
import postFilter from '@/utils/filters/postFilter'
|
||||
import linkHandler from '@/utils/linkHandler'
|
||||
import NormalButton from '@/components/buttons/NormalButton.vue'
|
||||
// import { post as postMock } from '@/mocks/postContentMock' // mock
|
||||
|
||||
export default defineComponent({
|
||||
components: { NormalButton },
|
||||
props: {
|
||||
post: { type: Object /*, default: () => postMock*/ },
|
||||
data: { type: Object },
|
||||
type: { type: String, default: 'normal' }, // normal | reverse | mobile
|
||||
},
|
||||
setup(props) {
|
||||
@ -77,7 +76,7 @@ export default defineComponent({
|
||||
defaultMessage: 'Read More',
|
||||
})
|
||||
|
||||
const data = computed(() => (props.post ? postFilter(props.post as Post, 'thumbList') : null))
|
||||
const data = props.data
|
||||
|
||||
const handleViewPostDetailEvent = () => {
|
||||
if (data) {
|
||||
@ -98,7 +97,7 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
@use '@/styles/mixins/text';
|
||||
@use '@/styles/mixins/tags';
|
||||
@use '@/styles/mixins/skeleton';
|
||||
// @use '@/styles/mixins/skeleton';
|
||||
|
||||
.card__container {
|
||||
// TODO: sizing in parent
|
||||
@ -123,7 +122,7 @@ export default defineComponent({
|
||||
border-radius: 10px 0 0 10px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
@include skeleton.skeleton-loading;
|
||||
// @include skeleton.skeleton-loading;
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
32
src/components/cards/postThumbCards/PostThumbCardIndex.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<PostThumbCardMobile v-if="isMobile" :data="data" :type="$props.type"></PostThumbCardMobile>
|
||||
<PostThumbCardClassic v-else :data="data"></PostThumbCardClassic>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue'
|
||||
import { useWindowResize } from '@/hooks'
|
||||
import postFilter from '@/utils/filters/postFilter'
|
||||
import PostThumbCardClassic from './PostThumbCardClassic.vue'
|
||||
import PostThumbCardMobile from './PostThumbCardMobile.vue'
|
||||
// import { post as postMock } from '@/mocks/postContentMock' // mock
|
||||
|
||||
export default defineComponent({
|
||||
components: { PostThumbCardClassic, PostThumbCardMobile },
|
||||
props: {
|
||||
post: { type: Object /*, default: () => postMock*/ },
|
||||
type: { type: String, default: 'normal' }, // normal | reverse | mobile
|
||||
},
|
||||
setup(props) {
|
||||
const windowSize = useWindowResize()
|
||||
const isMobile = computed(() => windowSize.value.innerWidth <= 840)
|
||||
|
||||
const data = computed(() => (props.post ? postFilter(props.post as Post, 'thumbList') : null))
|
||||
|
||||
return {
|
||||
data,
|
||||
isMobile,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
146
src/components/cards/postThumbCards/PostThumbCardMobile.vue
Normal file
@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="card__container">
|
||||
<div class="row__wrapper--thumbnail">
|
||||
<Image
|
||||
class="image"
|
||||
:src="data.featureImage.thumbnail"
|
||||
:alt="data.title"
|
||||
placeholder="https://via.placeholder.com/1024x768"
|
||||
:draggable="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="row__wrapper--title">
|
||||
<span>{{ data.title }}</span>
|
||||
</div>
|
||||
<div class="row__wrapper--statistics">
|
||||
<div class="column__wrapper--read_count">
|
||||
<span><i class="fab fa-hotjar"></i> {{ data.readCount }}</span>
|
||||
</div>
|
||||
<div class="column__wrapper--comment_count">
|
||||
<span> <i class="far fa-comment-dots"></i> {{ data.commentCount }}</span>
|
||||
</div>
|
||||
<div class="column__wrapper--word_count">
|
||||
<span><i class="fas fa-pen-nib"></i> {{ data.wordCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row__wrapper--abstract">
|
||||
<span>{{ data.excerpt }} </span>
|
||||
</div>
|
||||
<div class="row__wrapper--tags">
|
||||
<div class="tags__container">
|
||||
<div
|
||||
class="tag__wrapper"
|
||||
v-for="(tag, index) in ['vue', 'javascript', 'php', 'wordpress']"
|
||||
:key="index"
|
||||
>
|
||||
<div class="tag yolk">
|
||||
<span class="text">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue'
|
||||
import { useIntl, useRouter } from '@/hooks'
|
||||
import linkHandler from '@/utils/linkHandler'
|
||||
import NormalButton from '@/components/buttons/NormalButton.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { NormalButton },
|
||||
props: {
|
||||
data: { type: Object /*, default: () => postMock*/ },
|
||||
type: { type: String, default: 'normal' }, // normal | reverse | mobile
|
||||
},
|
||||
setup(props) {
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
|
||||
const buttonContext = intl.formatMessage({
|
||||
id: 'posts.readMore',
|
||||
defaultMessage: 'Read More',
|
||||
})
|
||||
|
||||
const data = props.data
|
||||
|
||||
const handleViewPostDetailEvent = () => {
|
||||
if (data) {
|
||||
linkHandler.handleClickLink({ url: data.value?.link ?? '', router, target: '_blank' })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
buttonContext,
|
||||
handleViewPostDetailEvent,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '@/styles/mixins/text';
|
||||
@use '@/styles/mixins/tags';
|
||||
|
||||
.card__container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
> * {
|
||||
width: calc(100% - 24px);
|
||||
}
|
||||
> .row__wrapper {
|
||||
&--thumbnail {
|
||||
width: 100%;
|
||||
}
|
||||
&--tags {
|
||||
max-height: 16px;
|
||||
overflow: hidden;
|
||||
align-items: flex-start;
|
||||
.tags__container {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
.tag__wrapper {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
@include tags.tag-style;
|
||||
}
|
||||
}
|
||||
}
|
||||
&--title {
|
||||
line-height: 30px;
|
||||
font-size: x-large; // 24
|
||||
font-weight: bold;
|
||||
}
|
||||
&--abstract {
|
||||
line-height: 22px;
|
||||
font-size: medium; // 16
|
||||
@include text.line-number-limit(4);
|
||||
}
|
||||
&--statistics {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
> * {
|
||||
cursor: pointer;
|
||||
> span {
|
||||
line-height: 12px;
|
||||
font-size: small;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,7 +1,12 @@
|
||||
<!-- # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -->
|
||||
<!-- # Generated by scripts/import-svg-icons.js -->
|
||||
<template>
|
||||
<svg-icon :data="svg[$props.name]" original></svg-icon>
|
||||
<svg-icon
|
||||
:data="svg[$props.name]"
|
||||
:width="$props.width"
|
||||
:height="$props.height"
|
||||
original
|
||||
></svg-icon>
|
||||
</template>
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
@ -24,16 +29,20 @@ import socialPixiv from '@/assets/icons/ui/social.pixiv.svg'
|
||||
import socialQq from '@/assets/icons/ui/social.qq.svg'
|
||||
import socialQzone from '@/assets/icons/ui/social.qzone.svg'
|
||||
import socialStackoverflow from '@/assets/icons/ui/social.stackoverflow.svg'
|
||||
import socialTelegram from '@/assets/icons/ui/social.telegram.svg'
|
||||
import socialTwitch from '@/assets/icons/ui/social.twitch.svg'
|
||||
import socialTwitter from '@/assets/icons/ui/social.twitter.svg'
|
||||
import socialWeibo from '@/assets/icons/ui/social.weibo.svg'
|
||||
import socialWeichat from '@/assets/icons/ui/social.weichat.svg'
|
||||
import socialYoutube from '@/assets/icons/ui/social.youtube.svg'
|
||||
import socialZhihu from '@/assets/icons/ui/social.zhihu.svg'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Icon',
|
||||
props: {
|
||||
name: { type: String },
|
||||
width: { type: String, default: '100%' },
|
||||
height: { type: String, default: '100%' },
|
||||
},
|
||||
setup() {
|
||||
const svg = {
|
||||
@ -56,10 +65,12 @@ export default defineComponent({
|
||||
'social.qq': socialQq,
|
||||
'social.qzone': socialQzone,
|
||||
'social.stackoverflow': socialStackoverflow,
|
||||
'social.telegram': socialTelegram,
|
||||
'social.twitch': socialTwitch,
|
||||
'social.twitter': socialTwitter,
|
||||
'social.weibo': socialWeibo,
|
||||
'social.weichat': socialWeichat,
|
||||
'social.youtube': socialYoutube,
|
||||
'social.zhihu': socialZhihu,
|
||||
}
|
||||
return {
|
||||
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="post-thumb-list__container" :ref="setListContainerRef">
|
||||
<div class="post-thumb-card__wrapper" v-for="(post, index) in postList" :key="index">
|
||||
<span>{{ index }}: {{ post.id }}</span>
|
||||
<PostThumbCardClassic
|
||||
<!-- <span>{{ index }}: {{ post.id }}</span> -->
|
||||
<PostThumbCardIndex
|
||||
:post="post"
|
||||
:type="index % 2 ? 'normal' : 'reverse'"
|
||||
></PostThumbCardClassic>
|
||||
></PostThumbCardIndex>
|
||||
</div>
|
||||
<div class="loader__wrapper" v-show="fetchStatus === 'fetching'">
|
||||
<BookLoader></BookLoader>
|
||||
@ -19,11 +19,11 @@
|
||||
import { defineComponent, computed, onMounted, Ref } from 'vue'
|
||||
import { useInjector, useState, useElementRef, useReachElementSide } from '@/hooks'
|
||||
import { posts } from '@/store'
|
||||
import PostThumbCardClassic from '@/components/cards/postThumbCards/PostThumbCardClassic.vue'
|
||||
import PostThumbCardIndex from '@/components/cards/postThumbCards/PostThumbCardIndex.vue'
|
||||
import BookLoader from '@/components/loader/BookLoader.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { PostThumbCardClassic, BookLoader },
|
||||
components: { PostThumbCardIndex, BookLoader },
|
||||
props: {
|
||||
namespace: { type: String, default: 'homepage' },
|
||||
page: { type: Number, default: 1 },
|
||||
|
@ -76,7 +76,7 @@ export default defineComponent({
|
||||
const [expandContentRef, setExpandContentRef] = useElementRef()
|
||||
const expandContentSize = useResizeObserver(expandContentRef)
|
||||
const expandContentHeight = computed(() =>
|
||||
expandContentSize.value.height === NaN
|
||||
isNaN(expandContentSize.value.height)
|
||||
? 0
|
||||
: expandContentSize.value.height + expandContentSize.value.paddingTop
|
||||
)
|
||||
|
27
src/hooks/lib/usePerfectScrollbar.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { ref, Ref, watch, onBeforeUnmount } from 'vue'
|
||||
import PerfectScrollbar from 'perfect-scrollbar'
|
||||
|
||||
const usePerfectScrollbar = <El>(
|
||||
elementRef: El extends Element ? Element : Ref<Element | null>,
|
||||
options: PerfectScrollbar.Options = {}
|
||||
) => {
|
||||
const mdcRef: Ref<PerfectScrollbar | null> = ref(null)
|
||||
|
||||
if (elementRef instanceof Element) {
|
||||
mdcRef.value = new PerfectScrollbar(elementRef, options)
|
||||
} else {
|
||||
watch(elementRef, (element) => {
|
||||
if (element) {
|
||||
mdcRef.value = new PerfectScrollbar(element, options)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
mdcRef.value?.destroy()
|
||||
})
|
||||
|
||||
return mdcRef
|
||||
}
|
||||
|
||||
export default usePerfectScrollbar
|
24
src/hooks/mdc/useMDCList.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ref, Ref, watch, onBeforeUnmount } from 'vue'
|
||||
import { MDCList } from '@material/list'
|
||||
|
||||
const useMDCList = <El>(elementRef: El extends Element ? Element : Ref<Element | null>) => {
|
||||
const mdcRef: Ref<MDCList | null> = ref(null)
|
||||
|
||||
if (elementRef instanceof Element) {
|
||||
mdcRef.value = MDCList.attachTo(elementRef)
|
||||
} else {
|
||||
watch(elementRef, (element) => {
|
||||
if (element) {
|
||||
mdcRef.value = MDCList.attachTo(element)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
mdcRef.value?.destroy()
|
||||
})
|
||||
|
||||
return mdcRef
|
||||
}
|
||||
|
||||
export default useMDCList
|
@ -1,11 +1,23 @@
|
||||
/**
|
||||
* https://codepen.io/gavra/pen/tEpzn
|
||||
*/
|
||||
import { ref, watch, Ref } from 'vue'
|
||||
const useTypewriterEffect = (strings: string[], speed = 100): [Ref<string>, () => void] => {
|
||||
/**
|
||||
* @param strings
|
||||
* @param speed
|
||||
* @param callback function to call when done
|
||||
* @returns [textRef, do(), done?]
|
||||
*/
|
||||
const useTypewriterEffect = (
|
||||
strings: string[],
|
||||
separator = '/n',
|
||||
speed = 100,
|
||||
callback?: () => void
|
||||
): [Ref<string>, () => void, () => void, Ref<boolean>] => {
|
||||
const textRef = ref('')
|
||||
const done = ref(false)
|
||||
|
||||
const typewriterEffect = () => {
|
||||
textRef.value = '' // reset
|
||||
done.value = false
|
||||
|
||||
const aText = strings
|
||||
const iSpeed = speed // time delay of print out
|
||||
let iIndex = 0 // start printing array at this posision
|
||||
@ -13,28 +25,39 @@ const useTypewriterEffect = (strings: string[], speed = 100): [Ref<string>, () =
|
||||
const iScrollAt = 20 // start scrolling up at this many lines
|
||||
let iTextPos = 0 // initialise text position
|
||||
let iRow // initialise current row
|
||||
let beforeText = '' // cache last line
|
||||
|
||||
const typewriter = () => {
|
||||
iRow = Math.max(0, iIndex - iScrollAt)
|
||||
// const destination = element
|
||||
|
||||
while (iRow < iIndex) {
|
||||
textRef.value += aText[iRow++] + '<br />'
|
||||
textRef.value += aText[iRow++] + separator
|
||||
}
|
||||
textRef.value = aText[iIndex].substring(0, iTextPos) // + '_'
|
||||
if (iTextPos++ == iArrLength) {
|
||||
textRef.value = beforeText + aText[iIndex].substring(0, iTextPos) // + '_'
|
||||
if (iTextPos++ === iArrLength) {
|
||||
iTextPos = 0
|
||||
iIndex++
|
||||
if (iIndex != aText.length) {
|
||||
if (iIndex !== aText.length) {
|
||||
iArrLength = aText[iIndex].length
|
||||
window.setTimeout(typewriter, 500)
|
||||
beforeText = textRef.value + separator
|
||||
}
|
||||
} else {
|
||||
window.setTimeout(typewriter, iSpeed)
|
||||
}
|
||||
if (iRow === iIndex - 1 && iIndex === aText.length) {
|
||||
done.value = true
|
||||
if (callback) callback()
|
||||
}
|
||||
}
|
||||
typewriter()
|
||||
}
|
||||
|
||||
return [textRef, typewriterEffect]
|
||||
const clearTextRef = () => {
|
||||
textRef.value = ''
|
||||
}
|
||||
|
||||
return [textRef, typewriterEffect, clearTextRef, done]
|
||||
}
|
||||
|
||||
export default useTypewriterEffect
|
||||
|
@ -1,41 +1,206 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<section class="header__wrapper">
|
||||
<div>
|
||||
<!-- PC -->
|
||||
<div v-if="!isMobile" class="page">
|
||||
<header class="header__wrapper">
|
||||
<Header></Header>
|
||||
</section>
|
||||
<section class="main__wrapper">
|
||||
</header>
|
||||
<div class="header__placeholder" v-if="$props.headerPlaceholder"></div>
|
||||
<section class="content__wrapper">
|
||||
<slot></slot>
|
||||
</section>
|
||||
<section class="footer__wrapper">
|
||||
<footer class="footer__wrapper">
|
||||
<Footer></Footer>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
<!-- Mobile -->
|
||||
<div v-else :class="['page', 'mobile', { 'show-drawer': shouldDrawerOpen }]">
|
||||
<header class="header__wrapper">
|
||||
<HeaderMobile :open="shouldDrawerOpen" @toggle="handleMDrawerToggleEvent"></HeaderMobile>
|
||||
<div class="fake-after" @click="handleClickFakeAfterEvent"></div>
|
||||
</header>
|
||||
<div class="header__placeholder mdc-elevation--z4" v-if="$props.headerPlaceholder"></div>
|
||||
<section class="content__wrapper mdc-elevation--z4">
|
||||
<slot></slot>
|
||||
<footer class="footer__wrapper">
|
||||
<Footer></Footer>
|
||||
</footer>
|
||||
<div class="fake-after" @click="handleClickFakeAfterEvent"></div>
|
||||
</section>
|
||||
<aside class="drawer__wrapper">
|
||||
<NavDrawer></NavDrawer>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { defineComponent, computed, onUnmounted, onDeactivated } from 'vue'
|
||||
import { throttle, xor } from 'lodash'
|
||||
import { useState, useWindowResize } from '@/hooks'
|
||||
import getScrollbarWidth from '@/utils/getScrollbarWidth'
|
||||
import Header from '@/layouts/components/header/Header.vue'
|
||||
import Footer from '@/layouts/components/footer/Footer.vue'
|
||||
import HeaderMobile from '@/layouts/components/header/HeaderMobile.vue'
|
||||
import NavDrawer from '@/layouts/components/header/NavDrawer.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LayoutBase',
|
||||
components: { Header, Footer },
|
||||
components: { Header, Footer, HeaderMobile, NavDrawer },
|
||||
props: { headerPlaceholder: { type: Boolean, default: true } },
|
||||
setup() {
|
||||
const windowSize = useWindowResize()
|
||||
const isMobile = computed(() => windowSize.value.innerWidth <= 600)
|
||||
const [shouldDrawerOpen, setShouldDrawerOpen] = useState(false)
|
||||
|
||||
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)`
|
||||
}
|
||||
}
|
||||
const toggleDrawer = throttle(
|
||||
() => {
|
||||
setShouldDrawerOpen(!shouldDrawerOpen.value)
|
||||
if (shouldDrawerOpen.value) {
|
||||
addScrollLock()
|
||||
} else {
|
||||
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,
|
||||
{
|
||||
trailing: false,
|
||||
}
|
||||
)
|
||||
|
||||
const handleMDrawerToggleEvent = () => {
|
||||
toggleDrawer()
|
||||
}
|
||||
|
||||
const handleClickFakeAfterEvent = () => {
|
||||
toggleDrawer()
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
setShouldDrawerOpen(false)
|
||||
removeScrollLock()
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
setShouldDrawerOpen(false)
|
||||
removeScrollLock()
|
||||
})
|
||||
|
||||
return { isMobile, handleMDrawerToggleEvent, shouldDrawerOpen, handleClickFakeAfterEvent }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$drawer-width: 260px;
|
||||
.page {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.header__wrapper {
|
||||
position: var(--header-position, sticky);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
z-index: 2;
|
||||
}
|
||||
.header__placeholder {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
visibility: hidden;
|
||||
}
|
||||
.content__wrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
}
|
||||
&.mobile {
|
||||
.header__wrapper {
|
||||
overflow-x: hidden; // hide box shadow
|
||||
height: 60px; // left the gap for box shadow
|
||||
::v-deep() {
|
||||
.toggler__wrapper {
|
||||
z-index: 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content__wrapper {
|
||||
position: relative;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.main__wrapper {
|
||||
position: relative;
|
||||
.header__wrapper,
|
||||
.content__wrapper {
|
||||
transition: transform 0.5s;
|
||||
> .fake-after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
content: '';
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s, width 0.1s 0.5s, height 0.1s 0.5s;
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
.drawer__wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: #{$drawer-width};
|
||||
height: 100%;
|
||||
background: #ffffff;
|
||||
visibility: hidden;
|
||||
transition: all 0.5s;
|
||||
z-index: 0;
|
||||
}
|
||||
&.show-drawer {
|
||||
.drawer__wrapper {
|
||||
visibility: visible;
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
.content__wrapper,
|
||||
.header__wrapper {
|
||||
transform: translate3d(#{$drawer-width}, 0, 0);
|
||||
> .fake-after {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
}
|
||||
.header__wrapper {
|
||||
> .fake-after {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -17,14 +17,19 @@
|
||||
<div class="content__wrapper">
|
||||
<div class="content__container">
|
||||
<div class="slogan__wrapper">
|
||||
<h1 class="typewriter">{{ sloganText }}</h1>
|
||||
<h1 class="typewriter">{{ sloganText }}<span class="cursor"> </span></h1>
|
||||
</div>
|
||||
<div class="dialog__wrapper">
|
||||
<div class="signature__wrapper">
|
||||
<div :class="['dialog__wrapper', { show: shouldShowSignatureDialog }]">
|
||||
<div class="quote__wrapper" v-if="quote">
|
||||
<span>
|
||||
<i class="fas fa-quote-left"></i>
|
||||
You got to put the past behind you before you can move on.
|
||||
<i class="fas fa-quote-right"></i>
|
||||
<i class="icon fas fa-quote-left"></i>
|
||||
{{ quote }}
|
||||
<i class="icon fas fa-quote-right"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="signature__wrapper" v-if="signature">
|
||||
<span>
|
||||
{{ signature }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="social-media__wrapper">
|
||||
@ -33,25 +38,44 @@
|
||||
v-for="(item, index) in socialMedia"
|
||||
:key="index"
|
||||
>
|
||||
<UiIcon :name="item.name"></UiIcon>
|
||||
<UiIcon :name="`social.${item.name}`"></UiIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mask__layer"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch } from 'vue'
|
||||
import { defineComponent, watch, onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'
|
||||
import { gsap } from 'gsap'
|
||||
import { useTypewriterEffect } from '@/hooks'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
import { useElementRef } from '@/hooks'
|
||||
import { useTypewriterEffect, useState, useElementRef } from '@/hooks'
|
||||
import sakuraOptions from '@/utils/sakuraOptions'
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const slogan = 'Hello, world!'
|
||||
// sakura options data
|
||||
const socialMedia = Object.keys(sakuraOptions)
|
||||
.map((key) => {
|
||||
if (/^social\./.test(key)) {
|
||||
const match = key.match(/^social\.(.*)$/)
|
||||
if (!match || !sakuraOptions[key]) return null
|
||||
return {
|
||||
name: match[1],
|
||||
value: sakuraOptions[key],
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter((i) => i)
|
||||
|
||||
const slogan = sakuraOptions['homepage.slogan']
|
||||
const quote = sakuraOptions['homepage.quote']
|
||||
const signature = sakuraOptions['homepage.signature']
|
||||
|
||||
const [parallaxContainerRef, setParallaxContainerRef] = useElementRef()
|
||||
|
||||
@ -63,6 +87,7 @@ export default defineComponent({
|
||||
scrollTrigger: {
|
||||
trigger: layersEloement[0],
|
||||
start: 'top top',
|
||||
end: 'bottom top',
|
||||
scrub: true,
|
||||
},
|
||||
y: '20%',
|
||||
@ -70,32 +95,45 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
const [sloganText, doSloganEffect] = useTypewriterEffect([slogan])
|
||||
const [shouldShowSignatureDialog, setShouldShowSignatureDialog] = useState(false)
|
||||
const showSignatureDialog = () => {
|
||||
window.setTimeout(() => setShouldShowSignatureDialog(true), 700)
|
||||
}
|
||||
|
||||
const [sloganText, doSloganEffect, clearText] = useTypewriterEffect(
|
||||
slogan.split(' '),
|
||||
' ',
|
||||
100,
|
||||
showSignatureDialog
|
||||
)
|
||||
|
||||
const [isSloganEffectCalled, setIsSloganEffectCalled] = useState(false)
|
||||
|
||||
const initeSloganEffect = () => {
|
||||
if (!isSloganEffectCalled.value) {
|
||||
doSloganEffect()
|
||||
setIsSloganEffectCalled(true)
|
||||
}
|
||||
}
|
||||
|
||||
const clearSloganEffect = () => {
|
||||
clearText()
|
||||
setShouldShowSignatureDialog(false)
|
||||
setIsSloganEffectCalled(false)
|
||||
}
|
||||
|
||||
onMounted(() => window.setTimeout(() => initeSloganEffect(), 1000))
|
||||
onActivated(() => window.setTimeout(() => initeSloganEffect(), 1000))
|
||||
onUnmounted(() => clearSloganEffect())
|
||||
onDeactivated(() => clearSloganEffect())
|
||||
|
||||
const handleImageError = () => {
|
||||
window.setTimeout(() => doSloganEffect(), 1000)
|
||||
// window.setTimeout(() => doSloganEffect(), 1000)
|
||||
}
|
||||
const handleImageLoad = () => {
|
||||
window.setTimeout(() => doSloganEffect(), 1000)
|
||||
// window.setTimeout(() => doSloganEffect(), 1000)
|
||||
}
|
||||
|
||||
const config = (window as any).InitState.config
|
||||
const socialMedia = Object.keys(config)
|
||||
.map((key) => {
|
||||
if (/^social\./.test(key)) {
|
||||
const match = key.match(/^social\.(.*)$/)
|
||||
if (!match || !config[key]) return null
|
||||
return {
|
||||
name: match[1],
|
||||
value: config[key],
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter((i) => i)
|
||||
console.log(socialMedia)
|
||||
|
||||
const styleController = () => {
|
||||
return {}
|
||||
}
|
||||
@ -103,17 +141,20 @@ export default defineComponent({
|
||||
return {
|
||||
setParallaxContainerRef,
|
||||
styleController,
|
||||
slogan,
|
||||
sloganText,
|
||||
quote,
|
||||
signature,
|
||||
handleImageError,
|
||||
handleImageLoad,
|
||||
sloganText,
|
||||
socialMedia,
|
||||
shouldShowSignatureDialog,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$mobile-view-max-width: 800px;
|
||||
.cover__container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -122,6 +163,7 @@ export default defineComponent({
|
||||
.background__wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
.image__wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -138,25 +180,40 @@ export default defineComponent({
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
.slogan__wrapper {
|
||||
font-size: 36px;
|
||||
text-transform: uppercase;
|
||||
color: #ffffff;
|
||||
.typewriter {
|
||||
overflow: hidden;
|
||||
border-right: 0.15em solid orange;
|
||||
white-space: nowrap;
|
||||
// position: relative;
|
||||
margin: 0 auto;
|
||||
letter-spacing: 0.15em;
|
||||
animation: blink-caret 0.75s step-end infinite;
|
||||
max-width: 80vw;
|
||||
align-self: center;
|
||||
font-size: 60px;
|
||||
text-transform: uppercase;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
.cursor {
|
||||
position: relative;
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0.15em;
|
||||
height: 1.2em;
|
||||
background: orange;
|
||||
animation: blink-caret 1s step-end infinite;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: $mobile-view-max-width) {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
@keyframes blink-caret {
|
||||
from,
|
||||
to {
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
50% {
|
||||
border-color: orange;
|
||||
background: orange;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -170,7 +227,16 @@ export default defineComponent({
|
||||
// letter-spacing: 0.1em;
|
||||
font-size: 16px;
|
||||
line-height: 30px;
|
||||
// align-self: flex-start;
|
||||
align-self: flex-start;
|
||||
visibility: hidden;
|
||||
// width: 100%;
|
||||
@media screen and (max-width: $mobile-view-max-width) {
|
||||
display: none;
|
||||
}
|
||||
&.show {
|
||||
visibility: visible;
|
||||
animation: fadeIn /* animate.css */ 0.8s;
|
||||
}
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@ -181,24 +247,60 @@ export default defineComponent({
|
||||
border-style: solid;
|
||||
border-color: transparent transparent rgba(0, 0, 0, 0.5) transparent;
|
||||
}
|
||||
.quote__wrapper {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
// text-align-last: right;
|
||||
white-space: pre;
|
||||
.icon {
|
||||
&:first-child {
|
||||
padding-right: 6px;
|
||||
}
|
||||
&:last-child {
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.signature__wrapper {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
text-align: right;
|
||||
}
|
||||
.social-media__wrapper {
|
||||
margin-top: 6px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 20px;
|
||||
.social-media-item__wrapper {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.mask__layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
&:before {
|
||||
background-image: url('@/assets/masks/dot.png');
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="footer__container">
|
||||
<div class="row__wrapper">
|
||||
<div class="sakura-icon__wrapper fa-spin">
|
||||
<ui-icon name="sakura"></ui-icon>
|
||||
<ui-icon name="ic.sakura"></ui-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row__wrapper">
|
||||
|
@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<header class="header__container mdc-elevation--z4">
|
||||
<div class="header__container mdc-elevation--z4">
|
||||
<div class="header__content">
|
||||
<div class="logo__wrapper">
|
||||
<img class="logo" :src="logo" alt="logo" @load="computeShouldHideNavItemList" />
|
||||
<img
|
||||
class="logo"
|
||||
:src="logo"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
@load="computeShouldHideNavItemList"
|
||||
/>
|
||||
</div>
|
||||
<div class="nav__wrapper" :ref="setNavBarWrapperRef" @resize="handleNavBarWrapperResizeEvent">
|
||||
<div class="nav__ul nav__ul--parent" :ref="setNavBarItemRefs">
|
||||
@ -64,11 +70,12 @@
|
||||
<img class="avatar" :src="avatar" alt="avatar" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, computed } from 'vue'
|
||||
import { defineComponent, ref, watch, computed, onMounted } from 'vue'
|
||||
import { debounce, cloneDeep } from 'lodash'
|
||||
import {
|
||||
useElementRef,
|
||||
useElementRefs,
|
||||
@ -77,17 +84,15 @@ import {
|
||||
useMDCRipple,
|
||||
} from '@/hooks'
|
||||
import { init } from '@/store'
|
||||
import sakuraOptions from '@/utils/sakuraOptions'
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
import NavItem from '@/layouts/components/header/NavItem.vue'
|
||||
import { debounce, cloneDeep } from 'lodash'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Header',
|
||||
components: { NavItem },
|
||||
setup() {
|
||||
const avatar = 'https://view.moezx.cc/images/2021/06/13/d6b010a378d392d4633008b915f98ab1.md.png'
|
||||
const logo =
|
||||
window.InitState.config['basic.site.logo'][0]?.url || 'https://v3.vuejs.org/logo.png'
|
||||
const logo = sakuraOptions['basic.site.logo'][0]?.url || 'https://v3.vuejs.org/logo.png'
|
||||
|
||||
const [navBarItemRefs, setNavBarItemRefs] = useElementRefs()
|
||||
const [navBarWrapperRef, setNavBarWrapperRef] = useElementRef()
|
||||
@ -109,6 +114,8 @@ export default defineComponent({
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => computeShouldHideNavItemList())
|
||||
|
||||
const navBarWrapperSize = useResizeObserver(navBarWrapperRef)
|
||||
|
||||
watch(navBarWrapperSize, () => {
|
||||
|
140
src/layouts/components/header/HeaderMobile.vue
Normal file
@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="nav__container mdc-elevation--z4">
|
||||
<div class="nav__content">
|
||||
<div class="column__wrapper--toggler toggler__wrapper" @click="handleToggleEvent">
|
||||
<div :class="['toggler', { active: $props.open }]">
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column__wrapper--logo">
|
||||
<div class="logo__wrapper" v-if="logo">
|
||||
<img class="logo" :src="logo" alt="logo" draggable="false" />
|
||||
</div>
|
||||
<div class="sitename" v-if="sitename">{{ sitename }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import sakuraOptions from '@/utils/sakuraOptions'
|
||||
|
||||
export default defineComponent({
|
||||
props: { open: Boolean },
|
||||
emits: ['toggle'],
|
||||
setup(props, { emit }) {
|
||||
const handleToggleEvent = () => {
|
||||
emit('toggle', !props.open)
|
||||
}
|
||||
const logo = sakuraOptions['basic.site.logo'][0]?.url
|
||||
const sitename = sakuraOptions['basic.site.title']
|
||||
|
||||
return { handleToggleEvent, logo, sitename }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// @use 'sass:math';
|
||||
.nav__container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> .nav__content {
|
||||
width: calc(100% - 12px * 2);
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
> .column__wrapper {
|
||||
&--toggler {
|
||||
$width: 30px;
|
||||
$height: 3px;
|
||||
$color: var(--toggler-color, #333333);
|
||||
> .toggler {
|
||||
position: relative;
|
||||
width: #{$width};
|
||||
height: #{$width};
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> span {
|
||||
transition: all 0.2s ease-in-out;
|
||||
width: #{$width};
|
||||
height: #{$height};
|
||||
background: #{$color};
|
||||
position: relative;
|
||||
display: block;
|
||||
&::before {
|
||||
transition: all 0.2s ease-in-out;
|
||||
content: '';
|
||||
display: block;
|
||||
background: #{$color};
|
||||
height: #{$height};
|
||||
width: #{$width};
|
||||
position: absolute;
|
||||
// top: -16px;
|
||||
transform: rotate(0deg);
|
||||
transform-origin: 13%;
|
||||
top: -8px;
|
||||
}
|
||||
&::after {
|
||||
transition: all 0.2s ease-in-out;
|
||||
content: '';
|
||||
display: block;
|
||||
background: #{$color};
|
||||
height: #{$height};
|
||||
width: #{$width};
|
||||
position: absolute;
|
||||
transform: rotate(0deg);
|
||||
transform-origin: 13%;
|
||||
top: 8px;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
> span {
|
||||
transition: all 0.2s ease-in-out;
|
||||
background: transparent;
|
||||
&::before {
|
||||
transition: all 0.2s ease-in-out;
|
||||
transform: rotate(45deg);
|
||||
// width: #{$width / math.sin(45deg)};
|
||||
}
|
||||
&::after {
|
||||
transition: all 0.2s ease-in-out;
|
||||
transform: rotate(-45deg);
|
||||
// width: #{$width / math.sin(45deg)};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&--logo {
|
||||
display: flex;
|
||||
flex-flow: row-reverse nowrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
> .logo__wrapper {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
> .logo {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
> .sitename {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
211
src/layouts/components/header/NavDrawer.vue
Normal file
@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div class="drawer__container" :ref="setScrollContainerRef">
|
||||
<div class="drawer__content">
|
||||
<div class="row__wrapper--avatar">
|
||||
<Image
|
||||
src="https://view.moezx.cc/images/2021/06/13/d6b010a378d392d4633008b915f98ab1.md.png"
|
||||
placeholder=""
|
||||
:avatar="true"
|
||||
alt=""
|
||||
:draggable="false"
|
||||
></Image>
|
||||
</div>
|
||||
<div class="row__wrapper--signature">Hello world...</div>
|
||||
<div class="row__wrapper--social"> social</div>
|
||||
<div class="row__wrapper--search">
|
||||
<div class="background">
|
||||
<input class="input" type="search" name="search" placeholder="Search..." required="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row__wrapper--menu">
|
||||
<div
|
||||
:class="['ul__wrapper', { active: currentActive === parentIndex }]"
|
||||
v-for="(parent, parentIndex) in navItems"
|
||||
:key="parentIndex"
|
||||
>
|
||||
<div class="ul__content--tag" @click="handleClickParentEvent($event, parentIndex)">
|
||||
<NavItem
|
||||
:context="parent.title"
|
||||
:prefix="parent.icon"
|
||||
:url="parent.child.length > 0 ? '' : parent.url"
|
||||
:suffix="parent.child.length > 0 ? 'fas fa-chevron-down' : ''"
|
||||
></NavItem>
|
||||
</div>
|
||||
<div class="ul__content--child">
|
||||
<div class="li__wrapper" v-for="(child, childIndex) in parent.child" :key="childIndex">
|
||||
<div class="li__content--tag">
|
||||
<NavItem :context="child.title" :prefix="child.icon" :url="child.url"></NavItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from 'vue'
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { useElementRef, useInjector } from '@/hooks'
|
||||
import usePerfectScrollbar from '@/hooks/lib/usePerfectScrollbar'
|
||||
import { init } from '@/store'
|
||||
import NavItem from '@/layouts/components/header/NavItem.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { NavItem },
|
||||
setup() {
|
||||
const { initState } = useInjector(init)
|
||||
|
||||
const navItems = computed(() => {
|
||||
const items: any = []
|
||||
const origin = camelcaseKeys(initState.value.menus)['headerMenu'] as Array<any>
|
||||
origin.forEach((parent) => {
|
||||
if (parent.parent === 0) {
|
||||
const item = cloneDeep(parent)
|
||||
item['child'] = []
|
||||
origin.forEach((child) => {
|
||||
if (child.parent === parent.id) {
|
||||
item['child'].push(child)
|
||||
}
|
||||
})
|
||||
items.push(item)
|
||||
}
|
||||
})
|
||||
return items
|
||||
})
|
||||
|
||||
const [scrollContainerRef, setScrollContainerRef] = useElementRef()
|
||||
const ps = usePerfectScrollbar(scrollContainerRef, { suppressScrollX: true })
|
||||
|
||||
const currentActive = ref(NaN)
|
||||
const total = computed(() => navItems.value.length)
|
||||
|
||||
const changeCurrentActive = (i: number) => {
|
||||
if (i === currentActive.value) currentActive.value = NaN
|
||||
else if (i < total.value && i >= 0) currentActive.value = i
|
||||
}
|
||||
|
||||
const handleClickParentEvent = (event: Event, i: number) => {
|
||||
// if has child
|
||||
if (navItems.value[i].child.length > 0) {
|
||||
changeCurrentActive(i)
|
||||
const collapseContainer = (event.currentTarget as HTMLElement).parentElement
|
||||
const collapseContent = (event.currentTarget as HTMLElement)?.nextElementSibling
|
||||
if (collapseContent instanceof HTMLElement) {
|
||||
collapseContainer?.style.setProperty(
|
||||
'--collapse-height',
|
||||
`${collapseContent.scrollHeight}px`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.log('open')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
navItems,
|
||||
setScrollContainerRef,
|
||||
currentActive,
|
||||
handleClickParentEvent,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drawer__container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
> .drawer__content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
> .row__wrapper {
|
||||
&--avatar {
|
||||
margin-top: 50px;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
&--signature {
|
||||
text-align: center;
|
||||
color: #333333;
|
||||
font-weight: 900;
|
||||
font-family: sans-serif;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
// &--social {
|
||||
// }
|
||||
&--search {
|
||||
width: 100%;
|
||||
.background {
|
||||
width: 100%;
|
||||
border-top: 1px solid rgba(153, 153, 153, 0.3);
|
||||
border-bottom: 1px solid rgba(153, 153, 153, 0.3);
|
||||
.input {
|
||||
width: 100%;
|
||||
border: unset;
|
||||
padding: 8px 12px 8px 30px;
|
||||
outline: none;
|
||||
color: #666666;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&--menu {
|
||||
width: 100%;
|
||||
> .ul__wrapper {
|
||||
width: 100%;
|
||||
> .ul__content {
|
||||
&--tag {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
background: rgba(2, 1, 1, 0);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
&--child {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
background: rgba(95, 93, 93, 0);
|
||||
transition: all 0.3s;
|
||||
> .li__wrapper {
|
||||
> .li__content--tag {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
.ul__content {
|
||||
&--tag {
|
||||
background: rgba(2, 1, 1, 0.05);
|
||||
}
|
||||
&--child {
|
||||
max-height: var(--collapse-height);
|
||||
background: rgba(95, 93, 93, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
::v-deep() {
|
||||
.nav-item__content {
|
||||
justify-content: space-between;
|
||||
.context {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,6 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import { VueSvgIconPlugin } from '@yzfe/vue3-svgicon'
|
||||
import '@yzfe/svgicon/lib/svgicon.css'
|
||||
import 'animate.css/animate.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { storeProviderPlugin } from './hooks/store'
|
||||
|
@ -1 +1,4 @@
|
||||
@use 'mdc';
|
||||
@use './mixins/perfect-scrollbar';
|
||||
|
||||
@include perfect-scrollbar.core-styles;
|
||||
|
127
src/styles/mixins/_perfect-scrollbar.scss
Normal file
@ -0,0 +1,127 @@
|
||||
@mixin core-styles {
|
||||
/**
|
||||
* https://github.com/mdbootstrap/perfect-scrollbar/blob/master/css/perfect-scrollbar.css
|
||||
*/
|
||||
|
||||
/*
|
||||
* Container style
|
||||
*/
|
||||
.ps {
|
||||
overflow: hidden !important;
|
||||
overflow-anchor: none;
|
||||
-ms-overflow-style: none;
|
||||
touch-action: auto;
|
||||
-ms-touch-action: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* Scrollbar rail styles
|
||||
*/
|
||||
.ps__rail-x {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: background-color 0.2s linear, opacity 0.2s linear;
|
||||
-webkit-transition: background-color 0.2s linear, opacity 0.2s linear;
|
||||
height: 15px;
|
||||
/* there must be 'bottom' or 'top' for ps__rail-x */
|
||||
bottom: 0px;
|
||||
/* please don't change 'position' */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ps__rail-y {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: background-color 0.2s linear, opacity 0.2s linear;
|
||||
-webkit-transition: background-color 0.2s linear, opacity 0.2s linear;
|
||||
width: 15px;
|
||||
/* there must be 'right' or 'left' for ps__rail-y */
|
||||
right: 0;
|
||||
/* please don't change 'position' */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ps--active-x > .ps__rail-x,
|
||||
.ps--active-y > .ps__rail-y {
|
||||
display: block;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ps:hover > .ps__rail-x,
|
||||
.ps:hover > .ps__rail-y,
|
||||
.ps--focus > .ps__rail-x,
|
||||
.ps--focus > .ps__rail-y,
|
||||
.ps--scrolling-x > .ps__rail-x,
|
||||
.ps--scrolling-y > .ps__rail-y {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ps .ps__rail-x:hover,
|
||||
.ps .ps__rail-y:hover,
|
||||
.ps .ps__rail-x:focus,
|
||||
.ps .ps__rail-y:focus,
|
||||
.ps .ps__rail-x.ps--clicking,
|
||||
.ps .ps__rail-y.ps--clicking {
|
||||
background-color: #eee;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/*
|
||||
* Scrollbar thumb styles
|
||||
*/
|
||||
.ps__thumb-x {
|
||||
background-color: #aaa;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s linear, height 0.2s ease-in-out;
|
||||
-webkit-transition: background-color 0.2s linear, height 0.2s ease-in-out;
|
||||
height: 6px;
|
||||
/* there must be 'bottom' for ps__thumb-x */
|
||||
bottom: 2px;
|
||||
/* please don't change 'position' */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ps__thumb-y {
|
||||
background-color: #aaa;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s linear, width 0.2s ease-in-out;
|
||||
-webkit-transition: background-color 0.2s linear, width 0.2s ease-in-out;
|
||||
width: 6px;
|
||||
/* there must be 'right' for ps__thumb-y */
|
||||
right: 2px;
|
||||
/* please don't change 'position' */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ps__rail-x:hover > .ps__thumb-x,
|
||||
.ps__rail-x:focus > .ps__thumb-x,
|
||||
.ps__rail-x.ps--clicking .ps__thumb-x {
|
||||
background-color: #999;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
.ps__rail-y:hover > .ps__thumb-y,
|
||||
.ps__rail-y:focus > .ps__thumb-y,
|
||||
.ps__rail-y.ps--clicking .ps__thumb-y {
|
||||
background-color: #999;
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
/* MS supports */
|
||||
@supports (-ms-overflow-style: none) {
|
||||
.ps {
|
||||
overflow: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
|
||||
.ps {
|
||||
overflow: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* fixup */
|
||||
.ps__rail-y {
|
||||
left: auto !important;
|
||||
}
|
||||
}
|
15
src/utils/getScrollBarWidth.ts
Normal file
@ -0,0 +1,15 @@
|
||||
const getScrollbarWidth = () => {
|
||||
let div1 = document.createElement('div')
|
||||
let div2 = document.createElement('div')
|
||||
div1.style.width = div2.style.width = div1.style.height = div2.style.height = '100px'
|
||||
div1.style.overflow = 'scroll'
|
||||
div2.style.overflow = 'hidden'
|
||||
document.body.appendChild(div1)
|
||||
document.body.appendChild(div2)
|
||||
const scrollbarWidth = Math.abs(div1.scrollHeight - div2.scrollHeight)
|
||||
document.body.removeChild(div1)
|
||||
document.body.removeChild(div2)
|
||||
return scrollbarWidth
|
||||
}
|
||||
|
||||
export default getScrollbarWidth
|
9
src/utils/sakuraOptions.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { SakuraOptions as SakuraOptionsAbstract } from '@/admin/optionsType'
|
||||
|
||||
export interface SakuraOptions extends SakuraOptionsAbstract {
|
||||
[namespace: string]: any
|
||||
}
|
||||
|
||||
const config = (window as any).InitState.config as SakuraOptions
|
||||
|
||||
export default config
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Base class="base">
|
||||
<Base :headerPlaceholder="false">
|
||||
<div class="main__content">
|
||||
<div class="cover__wrapper">
|
||||
<Cover></Cover>
|
||||
@ -23,14 +23,10 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.base {
|
||||
--header-position: fixed;
|
||||
}
|
||||
.main__content {
|
||||
.cover__wrapper {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
// background: url('https://via.placeholder.com/233');
|
||||
}
|
||||
.content__wrapper {
|
||||
width: 100%;
|
||||
|
@ -32,10 +32,13 @@ export default defineConfig({
|
||||
hmr: {
|
||||
// TODO: .env
|
||||
protocol: 'ws',
|
||||
// host: '192.168.28.26',
|
||||
host: 'localhost',
|
||||
port: 9000,
|
||||
},
|
||||
fs: {
|
||||
// This maybe MDC's incorrect absolute path
|
||||
allow: ['./', './node_modules/', '/node_modules/'],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
target: 'modules',
|
||||
@ -55,6 +58,9 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['gsap', 'marked', 'gsap/ScrollTrigger', 'highlight.js', '@vueuse/core'],
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
|