Add mobile support (Header, Drawer, ThumbList)

next
mashirozx 2021-07-21 17:35:45 +08:00
parent cc350c833e
commit 1d97b87a69
41 changed files with 2811 additions and 2762 deletions

View File

@ -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,

View File

@ -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

View File

@ -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) {

View File

@ -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()

View File

@ -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;
}
```

View File

@ -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",

View File

@ -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 = {

View File

@ -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+' })

View File

@ -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

View 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
}

View 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

View 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

View File

@ -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

View File

@ -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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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%;

View 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>

View 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>

View File

@ -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 {

View File

@ -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 },

View File

@ -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
)

View 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

View 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

View File

@ -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

View File

@ -1,41 +1,206 @@
<template>
<div class="page">
<section class="header__wrapper">
<Header></Header>
</section>
<section class="main__wrapper">
<slot></slot>
</section>
<section class="footer__wrapper">
<Footer></Footer>
</section>
<div>
<!-- PC -->
<div v-if="!isMobile" class="page">
<header class="header__wrapper">
<Header></Header>
</header>
<div class="header__placeholder" v-if="$props.headerPlaceholder"></div>
<section class="content__wrapper">
<slot></slot>
<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%;
z-index: 1;
height: 48px;
z-index: 2;
}
.main__wrapper {
.header__placeholder {
width: 100%;
height: 48px;
visibility: hidden;
}
.content__wrapper {
position: relative;
z-index: 0;
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;
}
.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>

View File

@ -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">&nbsp;</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;
}
@keyframes blink-caret {
from,
to {
border-color: transparent;
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;
}
}
50% {
border-color: orange;
@media screen and (max-width: $mobile-view-max-width) {
white-space: normal;
}
@keyframes blink-caret {
from,
to {
background: transparent;
}
50% {
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>

View File

@ -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">

View File

@ -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, () => {

View 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>

View 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>

View File

@ -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'

View File

@ -1 +1,4 @@
@use 'mdc';
@use './mixins/perfect-scrollbar';
@include perfect-scrollbar.core-styles;

View 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;
}
}

View 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

View 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

View File

@ -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%;

View File

@ -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: {

4052
yarn.lock

File diff suppressed because it is too large Load Diff