mirror of
https://github.com/mashirozx/sakura.git
synced 2024-11-26 00:38:12 +08:00
Add options framework
This commit is contained in:
parent
ef505acbb1
commit
565aeaf41f
204
app/configs/options.json
Normal file
204
app/configs/options.json
Normal file
@ -0,0 +1,204 @@
|
||||
{
|
||||
"basic.site.title": {
|
||||
"namespace": "basic.site.title",
|
||||
"public": true,
|
||||
"title": "Site title",
|
||||
"desc": "The site title",
|
||||
"type": "string",
|
||||
"default": "Theme Sakura"
|
||||
},
|
||||
"basic.site.logo": {
|
||||
"namespace": "basic.site.logo",
|
||||
"public": true,
|
||||
"title": "Site logo",
|
||||
"desc": "The site's Logo image, will display on navigation bar.",
|
||||
"type": "mediaPicker",
|
||||
"default": [
|
||||
{
|
||||
"id": 0,
|
||||
"url": "https://v3.vuejs.org/logo.png"
|
||||
}
|
||||
],
|
||||
"binds": {
|
||||
"title": "Select image for site logo.",
|
||||
"button": "Use this image",
|
||||
"type": "image",
|
||||
"multiple": false
|
||||
}
|
||||
},
|
||||
"social.github": {
|
||||
"namespace": "social.github",
|
||||
"public": true,
|
||||
"title": "Github username",
|
||||
"desc": "Your <a href=\"https://github.com\" target=\"_blank\">Github</a> username",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"thirdParty.reCaptcha.enable": {
|
||||
"namespace": "thirdParty.reCaptcha.enable",
|
||||
"public": true,
|
||||
"title": "Enable reCAPTCHA",
|
||||
"desc": "Use reCAPTCHA for anti-spam check.",
|
||||
"type": "switcher",
|
||||
"default": false,
|
||||
"binds": {
|
||||
"positiveLabel": "Enabled",
|
||||
"negativeLabel": "Disabled",
|
||||
"disabled": false
|
||||
}
|
||||
},
|
||||
"thirdParty.reCaptcha.version": {
|
||||
"namespace": "thirdParty.reCaptcha.version",
|
||||
"public": true,
|
||||
"title": "reCAPTCHA version",
|
||||
"desc": "Register your reCAPTCHA app '<a href=\"https://www.google.com/recaptcha/about/\" target=\"_blank\">here</a>', and choose a version.",
|
||||
"type": "choose",
|
||||
"default": null,
|
||||
"binds": {
|
||||
"options": [
|
||||
{
|
||||
"label": "reCAPTCHA version 3",
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"label": "reCAPTCHA version 2",
|
||||
"disabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"thirdParty.reCaptcha.siteKey": {
|
||||
"namespace": "thirdParty.reCaptcha.siteKey",
|
||||
"public": true,
|
||||
"title": "reCAPTCHA site key",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"thirdParty.reCaptcha.secretKey": {
|
||||
"namespace": "thirdParty.reCaptcha.secretKey",
|
||||
"public": false,
|
||||
"title": "reCAPTCHA secret key",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"other.hello": {
|
||||
"namespace": "other.hello",
|
||||
"public": true,
|
||||
"title": "Hello world",
|
||||
"type": "string",
|
||||
"default": "world"
|
||||
},
|
||||
"demo.string": {
|
||||
"namespace": "demo.string",
|
||||
"public": true,
|
||||
"title": "String",
|
||||
"desc": "One line string input.",
|
||||
"type": "string",
|
||||
"default": "Hello world!"
|
||||
},
|
||||
"demo.longString": {
|
||||
"namespace": "demo.longString",
|
||||
"public": true,
|
||||
"title": "Long string",
|
||||
"desc": "Textarea for long string input.",
|
||||
"type": "longString",
|
||||
"default": "\"It is the unknown we fear when we look upon death and darkness, nothing more.\"\n-- Albus Dumbledore"
|
||||
},
|
||||
"demo.switcher": {
|
||||
"namespace": "demo.switcher",
|
||||
"public": true,
|
||||
"title": "Switcher",
|
||||
"type": "switcher",
|
||||
"desc": "True/False switcher.",
|
||||
"default": true,
|
||||
"binds": {
|
||||
"positiveLabel": "current on",
|
||||
"negativeLabel": "current off",
|
||||
"disabled": false
|
||||
}
|
||||
},
|
||||
"demo.choose": {
|
||||
"namespace": "demo.choose",
|
||||
"public": true,
|
||||
"title": "Choose",
|
||||
"desc": "Choose one from options.",
|
||||
"type": "choose",
|
||||
"default": null,
|
||||
"binds": {
|
||||
"options": [
|
||||
{
|
||||
"label": "op 1",
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"label": "op 2",
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"label": "op 3",
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"label": "op 4",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"demo.selection": {
|
||||
"namespace": "demo.selection",
|
||||
"public": true,
|
||||
"title": "Selection",
|
||||
"desc": "Selection multiple items from options. max: {0: no limit, >0: limit}",
|
||||
"type": "selection",
|
||||
"default": [
|
||||
true,
|
||||
false,
|
||||
true
|
||||
],
|
||||
"binds": {
|
||||
"options": [
|
||||
{
|
||||
"label": "op 1",
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"label": "op 2",
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"label": "op 3",
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"label": "op 4",
|
||||
"disabled": true
|
||||
}
|
||||
],
|
||||
"max": 2
|
||||
}
|
||||
},
|
||||
"demo.mediaPicker": {
|
||||
"namespace": "demo.mediaPicker",
|
||||
"public": true,
|
||||
"title": "Media picker",
|
||||
"desc": "<code>type=\"image\"|\"video\"|\"audio?\"</code>, the object must include id, id=0 for remote media.",
|
||||
"type": "mediaPicker",
|
||||
"default": [
|
||||
{
|
||||
"id": 0,
|
||||
"url": "https://view.moezx.cc/images/2021/07/02/d5ab73174d18652d890e2f4d1b9bef8f.gif"
|
||||
},
|
||||
{
|
||||
"id": 0,
|
||||
"url": "https://view.moezx.cc/images/2021/07/02/a90553bf5b67770e87a89b2ce204eaa7.gif"
|
||||
}
|
||||
],
|
||||
"binds": {
|
||||
"title": "Select Media",
|
||||
"button": "Use this media",
|
||||
"type": "image",
|
||||
"multiple": true
|
||||
}
|
||||
}
|
||||
}
|
@ -31,9 +31,8 @@ class InitStateController extends BaseController
|
||||
'menus' => (new MenuController)->get_menus(),
|
||||
// 'rewrite_rules' => (new \WP_Rewrite())->rewrite_rules(),
|
||||
'index' => (new WP_Rewrite())->index,
|
||||
'config' => (new ConfigurationController)->public_options(),
|
||||
'recaptcha_site_key' => '6LfHEoEbAAAAAI5p_XBlr1WxEvrsOSNQFCQNcT79', // v2 secret key: 6LfHEoEbAAAAAIh0w2I9PCcVoa0j71mO6t7fipsj
|
||||
// 'recaptcha_site_key' => '6LdKhX8bAAAAAF5HJprXtKvg3nfBJMfgd2o007PN' // v3 secret key: 6LdKhX8bAAAAAA010EXlQ32FWoYD1J2sLb8SaYLR
|
||||
'config' => (new OptionController)->get_public_display_options(),
|
||||
// 'recaptcha_site_key' => sakura_options('thirdParty.reCaptcha.siteKey', ''), // use thirdParty.reCaptcha.siteKey
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -5,24 +5,10 @@ namespace Sakura\Controllers;
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Request;
|
||||
use WP_Error;
|
||||
use Sakura\Lib\Exception;
|
||||
use Sakura\Models\OptionModel;
|
||||
|
||||
class ConfigurationController extends BaseController
|
||||
class OptionController extends BaseController
|
||||
{
|
||||
public function public_options()
|
||||
{
|
||||
$keys = [
|
||||
// key => default value
|
||||
'title' => 'Theme Sakura',
|
||||
];
|
||||
$res = [];
|
||||
foreach ($keys as $key => $default) {
|
||||
$res[$key] = $this->sakura_options($key, $default);
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@ -49,8 +35,8 @@ class ConfigurationController extends BaseController
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array($this, 'get_config'),
|
||||
'permission_callback' => array($this, 'get_config_permissions_check'),
|
||||
'callback' => array($this, 'get_public_config'),
|
||||
'permission_callback' => array($this, 'get_public_config_permissions_check'),
|
||||
// 'args' => $this->get_collection_params(),
|
||||
),
|
||||
array(
|
||||
@ -64,6 +50,16 @@ class ConfigurationController extends BaseController
|
||||
);
|
||||
}
|
||||
|
||||
public function get_public_config(WP_REST_Request $request)
|
||||
{
|
||||
return $this->get_public_display_options();
|
||||
}
|
||||
|
||||
public function get_public_config_permissions_check(WP_REST_Request $request)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function get_config(WP_REST_Request $request)
|
||||
{
|
||||
$config = (array) OptionModel::get($this->rest_base);
|
||||
@ -85,13 +81,30 @@ class ConfigurationController extends BaseController
|
||||
|
||||
public function update_config(WP_REST_Request $request)
|
||||
{
|
||||
$original = (array) $this->get_config($request);
|
||||
$db = (array) $this->get_config($request);
|
||||
$cache = $db;
|
||||
$json = (array) self::json_validate($request->get_body());
|
||||
if (empty(array_diff($original, $json))) {
|
||||
return $original;
|
||||
$hasNoDiff = true;
|
||||
|
||||
foreach ($json as $key => $value) {
|
||||
if (array_key_exists($key, $cache)) {
|
||||
$nv = json_encode($value);
|
||||
$ov = json_encode($cache[$key]);
|
||||
if ($hasNoDiff) $hasNoDiff = $nv === $ov;
|
||||
} else {
|
||||
if ($hasNoDiff) $hasNoDiff = false;
|
||||
}
|
||||
$db[$key] = $value;
|
||||
}
|
||||
|
||||
$config = OptionModel::update($this->rest_base, $json);
|
||||
if ($hasNoDiff) {
|
||||
return [
|
||||
'code' => 'save_config_succeed',
|
||||
'message' => __('Configurations already up to date.', self::$text_domain),
|
||||
];
|
||||
}
|
||||
|
||||
$config = OptionModel::update($this->rest_base, $db);
|
||||
if (!$config) {
|
||||
return new WP_Error(
|
||||
'save_config_failure',
|
||||
@ -99,7 +112,10 @@ class ConfigurationController extends BaseController
|
||||
array('status' => 500)
|
||||
);
|
||||
} else {
|
||||
return $this->get_config($request);
|
||||
return [
|
||||
'code' => 'save_config_succeed',
|
||||
'message' => __('Configurations saved successfully.', self::$text_domain),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,10 +124,10 @@ class ConfigurationController extends BaseController
|
||||
return true;
|
||||
}
|
||||
|
||||
public function inite_theme()
|
||||
{
|
||||
$config = OptionModel::create($this->rest_base, (array)[]);
|
||||
}
|
||||
// public function inite_theme()
|
||||
// {
|
||||
// $config = OptionModel::create($this->rest_base, (array)[]);
|
||||
// }
|
||||
|
||||
public static function json_validate(string $string)
|
||||
{
|
||||
@ -150,4 +166,38 @@ class ConfigurationController extends BaseController
|
||||
// sprintf(__("No existing database saving value or default value for option '%s'.", self::$text_domain), $namespace)
|
||||
// );
|
||||
}
|
||||
|
||||
public static function get_option_json()
|
||||
{
|
||||
$options = file_get_contents(__DIR__ . "/../configs/options.json");
|
||||
return json_decode($options, true);
|
||||
}
|
||||
|
||||
public function get_public_display_options()
|
||||
{
|
||||
$output = [];
|
||||
$defaults = (array) self::get_option_json();
|
||||
// return $defaults;
|
||||
foreach ($defaults as $key => $value) {
|
||||
if ($value['public']) {
|
||||
$output[$value['namespace']] = $this->sakura_options($value['namespace'], $value['default']);
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use in admin page only
|
||||
* @return array
|
||||
*/
|
||||
public function get_all_options()
|
||||
{
|
||||
$output = [];
|
||||
$defaults = (array) self::get_option_json();
|
||||
// return $defaults;
|
||||
foreach ($defaults as $key => $value) {
|
||||
$output[$value['namespace']] = $this->sakura_options($value['namespace'], $value['default']);
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
@ -24,6 +24,6 @@ new Sakura\Routers\PagesRouter();
|
||||
|
||||
function sakura_options(string $namespace, $default)
|
||||
{
|
||||
$CF = new Sakura\Controllers\ConfigurationController();
|
||||
$CF = new Sakura\Controllers\OptionController();
|
||||
return $CF->sakura_options($namespace, $default);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ namespace Sakura\Helpers;
|
||||
|
||||
use Sakura\Helpers\ViteHelper;
|
||||
use Sakura\Controllers\InitStateController;
|
||||
use Sakura\Controllers\OptionController;
|
||||
|
||||
class AdminPageHelper extends ViteHelper
|
||||
{
|
||||
@ -46,7 +47,11 @@ class AdminPageHelper extends ViteHelper
|
||||
|
||||
wp_enqueue_script('[type:module]dev-main', self::$development_host . '/src/admin/main.ts', array(), null, true);
|
||||
|
||||
wp_localize_script('[type:module]dev-main', 'AdminColors', $this->get_admin_color_css());
|
||||
|
||||
wp_localize_script('[type:module]dev-main', 'InitState', (new InitStateController())->get_initial_state());
|
||||
|
||||
wp_localize_script('[type:module]dev-main', 'SakuraOptions', ['data' => (new OptionController())->get_all_options()]);
|
||||
}
|
||||
|
||||
public function enqueue_production_scripts()
|
||||
@ -56,9 +61,13 @@ class AdminPageHelper extends ViteHelper
|
||||
$manifest = self::get_manifest_file('admin');
|
||||
|
||||
// <script type="module" crossorigin src="http://localhost:9000/assets/index.36b06f45.js"></script>
|
||||
wp_enqueue_script('[type:module]chunk-vendors.js', $assets_base_path . $manifest[$entry_key]['file'], array(), null, false);
|
||||
wp_enqueue_script('[type:module]chunk-entrance.js', $assets_base_path . $manifest[$entry_key]['file'], array(), null, false);
|
||||
|
||||
wp_localize_script('[type:module]chunk-vendors.js', 'InitState', (new InitStateController())->get_initial_state());
|
||||
wp_localize_script('[type:module]chunk-entrance.js', 'AdminColors', $this->get_admin_color_css());
|
||||
|
||||
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());
|
||||
|
||||
// <link rel="modulepreload" href="http://localhost:9000/assets/vendor.b3a324ba.js">
|
||||
foreach ($manifest[$entry_key]['imports'] as $index => $import) {
|
||||
@ -85,4 +94,26 @@ class AdminPageHelper extends ViteHelper
|
||||
|
||||
wp_enqueue_style('fontawesome-free', 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.3/css/all.min.css');
|
||||
}
|
||||
|
||||
public function get_admin_color_css()
|
||||
{
|
||||
// {"name":"Default","url":false,"colors":["#1d2327","#2c3338","#2271b1","#72aee6"],"icon_colors":{"base":"#a7aaad","focus":"#72aee6","current":"#fff"}}
|
||||
global $_wp_admin_css_colors;
|
||||
$theme = (array) $_wp_admin_css_colors[get_user_option('admin_color')];
|
||||
// $scheme = [
|
||||
// 'dark-primary' => $theme['colors'][0],
|
||||
// 'dark-secondary' => $theme['colors'][1],
|
||||
// 'light-primary' => $theme['colors'][2],
|
||||
// 'light-secondary' => $theme['colors'][3],
|
||||
// 'icon-base' => $theme['icon_colors']['base'],
|
||||
// 'icon-focus' => $theme['icon_colors']['focus'],
|
||||
// 'icon-current' => $theme['icon_colors']['current'],
|
||||
// ];
|
||||
return $theme;
|
||||
// $css = '';
|
||||
// foreach ($scheme as $key => $value) {
|
||||
// $css .= "--{$key}:{$value};";
|
||||
// }
|
||||
// return $css;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Sakura\Helpers;
|
||||
|
||||
use Sakura\Controllers\ConfigurationController;
|
||||
use Sakura\Controllers\OptionController;
|
||||
|
||||
class SetupHelper
|
||||
{
|
||||
@ -22,7 +22,7 @@ class SetupHelper
|
||||
// count post views
|
||||
add_action('get_header', [$this, 'set_post_views']);
|
||||
// Inite config options
|
||||
add_action('after_switch_theme', [new ConfigurationController(), 'inite_theme'], 1, 2);
|
||||
add_action('after_switch_theme', [new OptionController(), 'inite_theme'], 1, 2);
|
||||
}
|
||||
|
||||
public function setup()
|
||||
|
@ -67,12 +67,7 @@ class ViteHelper extends BaseClass
|
||||
|
||||
wp_enqueue_style('normalize.css', 'https://cdn.jsdelivr.net/npm/normalize.css/normalize.css');
|
||||
|
||||
// TODO: don't use vue.js as handler
|
||||
// wp_enqueue_script('vue.js', 'https://unpkg.com/vue@next', array(), false, false);
|
||||
|
||||
// wp_localize_script('vue.js', 'InitState', (new InitStateController())->get_initial_state());
|
||||
|
||||
wp_enqueue_script('recaptcha', 'https://www.recaptcha.net/recaptcha/api.js?render=6LdKhX8bAAAAAF5HJprXtKvg3nfBJMfgd2o007PN', array(), false, true);
|
||||
wp_enqueue_script('recaptcha', 'https://www.recaptcha.net/recaptcha/api.js', array(), false, true);
|
||||
}
|
||||
|
||||
public static function script_tag_filter($tag, $handle, $src)
|
||||
|
@ -4,7 +4,7 @@ namespace Sakura\Routers;
|
||||
|
||||
use WP_REST_Controller;
|
||||
use WP_REST_Server;
|
||||
use Sakura\Controllers\ConfigurationController;
|
||||
use Sakura\Controllers\OptionController;
|
||||
use Sakura\Controllers\InitStateController;
|
||||
use Sakura\Controllers\MenuController;
|
||||
use Sakura\Controllers\PostController;
|
||||
@ -33,7 +33,9 @@ class ApiRouter extends WP_REST_Controller
|
||||
*/
|
||||
public function register_rest_routes()
|
||||
{
|
||||
add_action('rest_api_init', [new ConfigurationController(), 'register_routes']);
|
||||
// add options support
|
||||
add_action('rest_api_init', [new OptionController(), 'register_routes']);
|
||||
|
||||
add_action('rest_api_init', function () {
|
||||
// theme's initial states
|
||||
register_rest_route(
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Sakura\Utils;
|
||||
|
||||
use Rogervila\ArrayDiffMultidimensional;
|
||||
|
||||
class Tools
|
||||
{
|
||||
public static function echo_interceptor(callable $callback, ...$args)
|
||||
@ -13,15 +15,46 @@ class Tools
|
||||
return $output;
|
||||
}
|
||||
|
||||
// public function get_text_from_dom($node, $text) {
|
||||
// if (!is_null($node->childNodes)) {
|
||||
// foreach ($node->childNodes as $node) {
|
||||
// $text = get_text_from_dom($node, $text);
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// return $text . $node->textContent . ' ';
|
||||
// }
|
||||
// return $text;
|
||||
// }
|
||||
public static function get_text_from_dom($node, $text)
|
||||
{
|
||||
if (!is_null($node->childNodes)) {
|
||||
foreach ($node->childNodes as $node) {
|
||||
$text = self::get_text_from_dom($node, $text);
|
||||
}
|
||||
} else {
|
||||
return $text . $node->textContent . ' ';
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/a/3877494/8083009
|
||||
*
|
||||
* @param array $aArray1
|
||||
* @param array $aArray2
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function array_recursive_diff(array $aArray1, array $aArray2)
|
||||
{
|
||||
$aReturn = array();
|
||||
|
||||
foreach ($aArray1 as $mKey => $mValue) {
|
||||
if (array_key_exists($mKey, $aArray2)) {
|
||||
if (is_array($mValue)) {
|
||||
$aRecursiveDiff = self::array_recursive_diff($mValue, $aArray2[$mKey]);
|
||||
if (count($aRecursiveDiff)) {
|
||||
$aReturn[$mKey] = $aRecursiveDiff;
|
||||
}
|
||||
} else {
|
||||
if ($mValue != $aArray2[$mKey]) {
|
||||
$aReturn[$mKey] = $mValue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$aReturn[$mKey] = $mValue;
|
||||
}
|
||||
}
|
||||
return $aReturn;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% block admin_app %}
|
||||
<div id="app" class="sakura-options-page__app">
|
||||
<div id="app" class="sakura-options-page__app" style="{{scheme}}">
|
||||
Loading
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -24,7 +24,8 @@
|
||||
"rsync": "nodemon -e '*' --watch ./app --ignore ./app/vendor scripts/rsync.mjs",
|
||||
"rsync:composer": "nodemon --watch './composer.json' --watch './composer.lock' scripts/rsync.mjs --composer",
|
||||
"gen:icon": "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"
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl": "^1.13.2",
|
||||
@ -49,6 +50,7 @@
|
||||
"@yzfe/vue3-svgicon": "^1.0.1",
|
||||
"axios": "^0.21.1",
|
||||
"camelcase-keys": "^7.0.0",
|
||||
"chroma-js": "^2.1.2",
|
||||
"crypto-js": "^4.0.0",
|
||||
"gsap": "^3.7.0",
|
||||
"highlight.js": "^11.1.0",
|
||||
@ -67,6 +69,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^4.2.27",
|
||||
"@types/chroma-js": "^2.1.3",
|
||||
"@types/crypto-js": "^4.0.1",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/marked": "^2.0.4",
|
||||
@ -87,7 +90,6 @@
|
||||
"eslint-plugin-formatjs": "^2.17.1",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-vue": "^7.13.0",
|
||||
"foreman": "^3.0.1",
|
||||
"jest": "^27.0.6",
|
||||
"nodemon": "^2.0.12",
|
||||
"postcss-import": "^14.0.2",
|
||||
|
@ -50,4 +50,4 @@ readdirSync(iconDir).forEach((file) => {
|
||||
|
||||
const vueContent = template(importContent, dataContent)
|
||||
|
||||
writeFileSync(targetDir, vueContent)
|
||||
writeFileSync(targetDir, vueContent, { flag: 'w+' })
|
||||
|
2
scripts/options-export/.gitignore
vendored
Normal file
2
scripts/options-export/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.js
|
||||
options.ts
|
5
scripts/options-export/copy-options.mjs
Normal file
5
scripts/options-export/copy-options.mjs
Normal file
@ -0,0 +1,5 @@
|
||||
import { readFileSync, writeFileSync } from 'fs'
|
||||
|
||||
let file = readFileSync('./src/admin/options.ts', { flag: 'r' }).toString()
|
||||
file = file.replace('@/locales', './locales')
|
||||
writeFileSync('./scripts/options-export/options.ts', file, { flag: 'w+' })
|
15
scripts/options-export/dump-options.ts
Normal file
15
scripts/options-export/dump-options.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import options from './options'
|
||||
import { writeFileSync } from 'fs'
|
||||
|
||||
const exportOptions: { [key: string]: any } = {}
|
||||
|
||||
Object.keys(options).forEach((tab) => {
|
||||
options[tab].options.forEach((option) => {
|
||||
if (option.depends) delete option.depends // remove function
|
||||
exportOptions[option.namespace] = option
|
||||
})
|
||||
})
|
||||
|
||||
console.dir(exportOptions)
|
||||
|
||||
writeFileSync('./app/configs/options.json', JSON.stringify(exportOptions, null, 2), { flag: 'w+' })
|
10
scripts/options-export/locales.ts
Normal file
10
scripts/options-export/locales.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// intl.formatMessage({
|
||||
// id: 'options.basic.siteTitle',
|
||||
// defaultMessage: 'The site title',
|
||||
// })
|
||||
|
||||
export default {
|
||||
formatMessage({ id, defaultMessage }: { id: string; defaultMessage: string }) {
|
||||
return defaultMessage
|
||||
},
|
||||
}
|
@ -1,29 +1,49 @@
|
||||
<template>
|
||||
<div class="app__wrapper">
|
||||
<Core></Core>
|
||||
<div class="app__wrapper" :style="scheme">
|
||||
<div class="app__content">
|
||||
<Core></Core>
|
||||
</div>
|
||||
</div>
|
||||
<div class="messages__wrapper" :style="scheme">
|
||||
<Messages position-y="bottom"></Messages>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import scheme from './scheme'
|
||||
import Core from './Core.vue'
|
||||
import Messages from '@/components/messages/Messages.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { Core },
|
||||
components: { Core, Messages },
|
||||
setup() {
|
||||
return { scheme }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use './index';
|
||||
@use './mdc';
|
||||
@use './variables';
|
||||
.sakura-options-page__app {
|
||||
width: calc(100% - 20px);
|
||||
padding: 20px 20px 20px 0;
|
||||
@media screen and (max-width: 782px) {
|
||||
@media screen and (max-width: variables.$mobile-max-width) {
|
||||
width: calc(100% - 10px);
|
||||
padding: 10px 10px 10px 0;
|
||||
}
|
||||
> .app__wrapper {
|
||||
width: 100%;
|
||||
.app__content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
> .messages__wrapper {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 999999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -18,19 +18,28 @@
|
||||
>
|
||||
<div class="tab-page__content">
|
||||
<h1 class="row__wrapper--title">{{ options[tabKey].title }}</h1>
|
||||
<p class="row__wrapper--desc" v-if="options[tabKey].desc"> {{ options[tabKey].desc }} </p>
|
||||
<div
|
||||
class="row__wrapper--options"
|
||||
v-for="(option, optionIndex) in options[tabKey].options"
|
||||
:key="optionIndex"
|
||||
>
|
||||
<OptionItem :option="option"></OptionItem>
|
||||
</div>
|
||||
<p class="row__wrapper--desc" v-if="options[tabKey].desc" v-html="options[tabKey].desc">
|
||||
</p>
|
||||
<transition-group name="row__wrapper--options">
|
||||
<div
|
||||
class="option__wrapper"
|
||||
v-for="(option, optionIndex) in options[tabKey].options"
|
||||
:key="optionIndex"
|
||||
v-show="shouldOptionShow(option)"
|
||||
>
|
||||
<OptionItem :option="option"></OptionItem>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
</Swiper>
|
||||
<div class="buttons__wrapper">
|
||||
<NormalButton icon="fas fa-save" context="Save" :contained="true"></NormalButton>
|
||||
<NormalButton
|
||||
:icon="['fas', saving ? 'fa-spinner fa-spin' : 'fa-save'].join(' ')"
|
||||
context="Save"
|
||||
:contained="true"
|
||||
@click="handleSaveEvent"
|
||||
></NormalButton>
|
||||
<NormalButton icon="fas fa-upload" context="Import" :contained="true"></NormalButton>
|
||||
<NormalButton icon="fas fa-download" context="Export" :contained="true"></NormalButton>
|
||||
</div>
|
||||
@ -38,21 +47,14 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
Ref,
|
||||
watch,
|
||||
nextTick,
|
||||
watchEffect,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
} from 'vue'
|
||||
import { defineComponent, ref, Ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue'
|
||||
import { Swiper as SwiperInterface } from 'swiper'
|
||||
import { useInjector } from '@/hooks'
|
||||
import { useInjector, useState, useMessage } from '@/hooks'
|
||||
import store from './store'
|
||||
import options from '@/admin/options'
|
||||
import options from './options'
|
||||
import type { Option } from './options'
|
||||
import API from './api'
|
||||
import TabBar from '@/components/tabBar/TabBar.vue'
|
||||
import OptionItem from './OptionItem.vue'
|
||||
import NormalButton from '@/components/buttons/NormalButton.vue'
|
||||
@ -61,32 +63,85 @@ export default defineComponent({
|
||||
components: { TabBar, Swiper, SwiperSlide, OptionItem, NormalButton },
|
||||
setup() {
|
||||
// UI controllers
|
||||
const currentTabIndex: Ref<number> = ref(0)
|
||||
const swiperRef: Ref<SwiperInterface | null> = ref(null)
|
||||
const tabKeys = Object.keys(options)
|
||||
const tabs = tabKeys.map((key) => {
|
||||
return { context: options[key].title, icon: options[key].icon }
|
||||
return { context: options[key].title, icon: options[key].icon, key }
|
||||
})
|
||||
|
||||
let defaultCurrentTabIndex: number = 0
|
||||
if (window.location.hash) {
|
||||
const locationHashMatch = window.location.hash.match(/^#(.*)/)
|
||||
if (locationHashMatch && locationHashMatch[1] && tabKeys.indexOf(locationHashMatch[1]) > -1) {
|
||||
defaultCurrentTabIndex = tabKeys.indexOf(locationHashMatch[1])
|
||||
}
|
||||
}
|
||||
const currentTabIndex: Ref<number> = ref(defaultCurrentTabIndex)
|
||||
const swiperRef: Ref<SwiperInterface | null> = ref(null)
|
||||
|
||||
const handleSwiperEvent = (swiper: SwiperInterface) => {
|
||||
swiperRef.value = swiper
|
||||
swiper.slideTo(currentTabIndex.value)
|
||||
}
|
||||
|
||||
watch(currentTabIndex, (current) => swiperRef.value?.slideTo(current))
|
||||
watch(currentTabIndex, (current) => {
|
||||
swiperRef.value?.slideTo(current)
|
||||
window.location.hash = `#${tabs[current].key}`
|
||||
})
|
||||
|
||||
const updateAutoHeight = () => swiperRef.value?.updateAutoHeight(0)
|
||||
|
||||
// nextTick(() => updateAutoHeight())
|
||||
// watchEffect(() => updateAutoHeight())
|
||||
const updateAutoHeight = (timeout = 0) => swiperRef.value?.updateAutoHeight(timeout)
|
||||
|
||||
// auto update height
|
||||
onMounted(() => {
|
||||
const timer = setInterval(() => updateAutoHeight(), 100)
|
||||
const timer = setInterval(() => updateAutoHeight(100), 100)
|
||||
onBeforeUnmount(() => clearInterval(timer))
|
||||
})
|
||||
|
||||
// data controllers
|
||||
const { config, setConfig } = useInjector(store)
|
||||
// messages
|
||||
const addMessage = useMessage()
|
||||
|
||||
return { currentTabIndex, tabKeys, tabs, options, handleSwiperEvent }
|
||||
// data controllers
|
||||
const [saving, setSaving] = useState(false)
|
||||
const { config } = useInjector(store)
|
||||
|
||||
const handleSaveEvent = () => {
|
||||
if (saving.value) return
|
||||
setSaving(true)
|
||||
API.postConfigJson(config.value)
|
||||
.then((res) => {
|
||||
setSaving(false)
|
||||
addMessage({
|
||||
title: res.data.message,
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
setSaving(false)
|
||||
console.error(error)
|
||||
addMessage({
|
||||
title: error.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const shouldOptionShow = (option: Option) => {
|
||||
if (option.depends) {
|
||||
return option.depends(config)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
currentTabIndex,
|
||||
tabKeys,
|
||||
tabs,
|
||||
options,
|
||||
handleSwiperEvent,
|
||||
handleSaveEvent,
|
||||
shouldOptionShow,
|
||||
saving,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -110,6 +165,44 @@ export default defineComponent({
|
||||
.tab-page__content {
|
||||
width: calc(100% - 24px);
|
||||
padding: 12px;
|
||||
.row__wrapper {
|
||||
&--options {
|
||||
&-enter-active {
|
||||
transform-origin: top;
|
||||
animation: from 0.3s forwards;
|
||||
}
|
||||
&-leave-active {
|
||||
transform-origin: top;
|
||||
animation: to 0.3s forwards;
|
||||
}
|
||||
&-move {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes from {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes to {
|
||||
0% {
|
||||
height: 56px;
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
height: 0;
|
||||
transform: scaleY(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,11 +80,17 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './variables';
|
||||
.option__container {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: space-between;
|
||||
justify-content: flex-start;
|
||||
@media screen and (max-width: variables.$mobile-max-width) {
|
||||
flex-flow: column nowrap;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
> .column__wrapper {
|
||||
&--label {
|
||||
flex: 0 0 auto;
|
||||
@ -96,7 +102,7 @@ export default defineComponent({
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: space-between;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding-top: 12px;
|
||||
> .row__wrapper {
|
||||
|
@ -2,18 +2,13 @@
|
||||
@use '@material/elevation/mdc-elevation';
|
||||
@use '@material/button/mdc-button';
|
||||
@use "@material/textfield/mdc-text-field";
|
||||
// @use '@material/chips/deprecated/mdc-chips';
|
||||
// @use '@material/list/mdc-list';
|
||||
@use '@material/card/mdc-card';
|
||||
|
||||
@use "@material/tab-bar/mdc-tab-bar";
|
||||
@use "@material/tab-scroller/mdc-tab-scroller";
|
||||
@use "@material/tab-indicator/mdc-tab-indicator";
|
||||
@use "@material/tab/mdc-tab";
|
||||
// @use '@material/typography/mdc-typography';
|
||||
|
||||
@use "@material/checkbox/mdc-checkbox";
|
||||
// @use "@material/form-field/mdc-form-field";
|
||||
@use "@material/radio/mdc-radio";
|
||||
// @use "@material/switch/deprecated/mdc-switch";
|
||||
@use '@material/switch/styles';
|
22
src/admin/_scheme.scss
Normal file
22
src/admin/_scheme.scss
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@mixin global-variables {
|
||||
// Default
|
||||
--dark-primary: #1d2327;
|
||||
--dark-secondary: #2c3338;
|
||||
--light-primary: #2271b1;
|
||||
--light-secondary: #72aee6;
|
||||
--icon-base: #a7aaad;
|
||||
--icon-focus: #72aee6;
|
||||
--icon-current: #fff;
|
||||
|
||||
// Light
|
||||
--dark-primary: #e5e5e5; // menu background color
|
||||
--dark-secondary: #999; // menu focus background color
|
||||
--light-primary: #d64e07; // hot dot
|
||||
--light-secondary: #04a4cc; // button, link
|
||||
--icon-base: #999;
|
||||
--icon-focus: #ccc;
|
||||
--icon-current: #ccc;
|
||||
}
|
2
src/admin/_variables.scss
Normal file
2
src/admin/_variables.scss
Normal file
@ -0,0 +1,2 @@
|
||||
$mobile-max-width: 782px;
|
||||
$small-mobile-max-width: 466px;
|
@ -1,7 +1,7 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export default {
|
||||
postConfigJson(data: any): Promise<any> {
|
||||
postConfigJson(data: { [key: string]: any }): Promise<any> {
|
||||
return request({
|
||||
url: '/sakura/v1/config',
|
||||
method: 'POST',
|
||||
|
@ -27,6 +27,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, Ref } from 'vue'
|
||||
import { cloneDeep, remove } from 'lodash'
|
||||
import { useMessage, useIntl } from '@/hooks'
|
||||
import uniqueHash from '@/utils/uniqueHash'
|
||||
import { isUrl } from '@/utils/urlHelper'
|
||||
import NormalButton from '@/components/buttons/NormalButton.vue'
|
||||
@ -43,6 +44,9 @@ export default defineComponent({
|
||||
},
|
||||
emits: ['update:selection'],
|
||||
setup(props, { emit }) {
|
||||
const addMessage = useMessage()
|
||||
const intl = useIntl()
|
||||
|
||||
const selection: Ref<{ id: number; url: string }[]> = ref(
|
||||
props.selection as { id: number; url: string }[]
|
||||
)
|
||||
@ -103,12 +107,22 @@ export default defineComponent({
|
||||
selection.value.push({ id: 0, url })
|
||||
userInput.value = ''
|
||||
} else {
|
||||
// TODO
|
||||
console.warn('Duplicate URLs')
|
||||
addMessage({
|
||||
title: intl.formatMessage({
|
||||
id: 'messages.admin.uplicateUrls',
|
||||
defaultMessage: 'Duplicate URLs',
|
||||
}),
|
||||
type: 'warning',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// TODO
|
||||
console.warn('Invalid URL')
|
||||
addMessage({
|
||||
title: intl.formatMessage({
|
||||
id: 'messages.admin.invalidUrl',
|
||||
defaultMessage: 'Invalid URL',
|
||||
}),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,7 +130,18 @@ export default defineComponent({
|
||||
remove(selection.value, (item, itemIndex) => index === itemIndex)
|
||||
}
|
||||
|
||||
watch(selection, (value) => emit('update:selection', value), { deep: true })
|
||||
watch(
|
||||
selection,
|
||||
(value) => {
|
||||
if (!props.multiple && value.length > 1) {
|
||||
selection.value = selection.value.slice(-1)
|
||||
console.log(selection.value.length)
|
||||
}
|
||||
console.log(selection.value)
|
||||
emit('update:selection', selection.value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
return { open, add, del, userInput, selection }
|
||||
},
|
||||
@ -124,6 +149,7 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../variables';
|
||||
.picker__container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@ -148,6 +174,13 @@ export default defineComponent({
|
||||
> .button__wrapper {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@media screen and (max-width: variables.$small-mobile-max-width) {
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
> .input__wrapper {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
&--preview {
|
||||
display: flex;
|
||||
@ -179,7 +212,7 @@ export default defineComponent({
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
opacity: 1;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
&:hover {
|
||||
|
@ -4,12 +4,13 @@ import '@yzfe/svgicon/lib/svgicon.css'
|
||||
import App from './App.vue'
|
||||
import { storeProviderPlugin } from '@/hooks/store'
|
||||
import store from './store'
|
||||
import { messages } from '@/store'
|
||||
import { intlPlugin } from '../locales'
|
||||
import UiIcon from '@/components/icon/UiIcon.vue'
|
||||
import Image from '@/components/image/Image.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(storeProviderPlugin, [store])
|
||||
app.use(storeProviderPlugin, [store, messages])
|
||||
app.use(intlPlugin)
|
||||
app.use(VueSvgIconPlugin, { tagName: 'svg-icon' })
|
||||
app.component('UiIcon', UiIcon)
|
||||
|
@ -1,16 +1,21 @@
|
||||
import intl from '@/locales'
|
||||
|
||||
export interface Option {
|
||||
namespace: string
|
||||
public: boolean
|
||||
title: string
|
||||
desc?: string
|
||||
type: string
|
||||
default: any
|
||||
binds?: { [key: string]: any }
|
||||
depends?: (state: any) => boolean
|
||||
}
|
||||
export interface Options {
|
||||
[tag: string]: {
|
||||
title: string
|
||||
desc?: string
|
||||
icon: string
|
||||
options: Array<{
|
||||
namespace: string
|
||||
title: string
|
||||
desc?: string
|
||||
type: string
|
||||
default: any
|
||||
binds?: { [key: string]: any }
|
||||
}>
|
||||
options: Array<Option>
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,17 +25,201 @@ const options: Options = {
|
||||
desc: 'The basic options',
|
||||
icon: 'fas fa-address-card',
|
||||
options: [
|
||||
// basic.site.title
|
||||
{
|
||||
namespace: 'basic.siteTitle',
|
||||
title: 'Site title',
|
||||
desc: 'The site title',
|
||||
namespace: 'basic.site.title',
|
||||
public: true,
|
||||
title: intl.formatMessage({
|
||||
id: 'options.basic.site.title.title',
|
||||
defaultMessage: 'Site title',
|
||||
}),
|
||||
desc: intl.formatMessage({
|
||||
id: 'options.basic.site.title.desc',
|
||||
defaultMessage: 'The site title',
|
||||
}),
|
||||
type: 'string',
|
||||
default: 'Opps',
|
||||
default: 'Theme Sakura',
|
||||
},
|
||||
// basic.site.logo
|
||||
{
|
||||
namespace: 'basic.site.logo',
|
||||
public: true,
|
||||
title: intl.formatMessage({
|
||||
id: 'options.basic.site.logo.title',
|
||||
defaultMessage: 'Site logo',
|
||||
}),
|
||||
desc: intl.formatMessage({
|
||||
id: 'options.basic.site.logo.desc',
|
||||
defaultMessage: "The site's Logo image, will display on navigation bar.",
|
||||
}),
|
||||
type: 'mediaPicker',
|
||||
default: [{ id: 0, url: 'https://v3.vuejs.org/logo.png' }],
|
||||
binds: {
|
||||
title: intl.formatMessage({
|
||||
id: 'options.basic.site.logo.binds.title',
|
||||
defaultMessage: 'Select image for site logo.',
|
||||
}),
|
||||
button: intl.formatMessage({
|
||||
id: 'options.basic.site.logo.binds.button',
|
||||
defaultMessage: 'Use this image',
|
||||
}),
|
||||
type: 'image',
|
||||
multiple: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
social: {
|
||||
title: 'Social',
|
||||
icon: 'fas fa-users',
|
||||
options: [
|
||||
{
|
||||
namespace: 'social.github',
|
||||
public: true,
|
||||
title: 'Github username',
|
||||
desc: 'Your <a href="https://github.com" target="_blank">Github</a> username',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
thirdParty: {
|
||||
title: 'Third party services',
|
||||
icon: 'fas fa-bezier-curve',
|
||||
options: [
|
||||
// thirdParty.reCaptcha.enable
|
||||
{
|
||||
namespace: 'thirdParty.reCaptcha.enable',
|
||||
public: true,
|
||||
title: intl.formatMessage({
|
||||
id: 'options.thirdParty.reCaptcha.enable.title',
|
||||
defaultMessage: 'Enable reCAPTCHA',
|
||||
}),
|
||||
desc: intl.formatMessage({
|
||||
id: 'options.thirdParty.reCaptcha.enable.desc',
|
||||
defaultMessage: 'Use reCAPTCHA for anti-spam check.',
|
||||
}),
|
||||
type: 'switcher',
|
||||
default: false,
|
||||
binds: {
|
||||
positiveLabel: intl.formatMessage({
|
||||
id: 'options.thirdParty.reCaptcha.enable.positiveLabel',
|
||||
defaultMessage: 'Enabled',
|
||||
}),
|
||||
negativeLabel: intl.formatMessage({
|
||||
id: 'options.thirdParty.reCaptcha.enable.negativeLabel',
|
||||
defaultMessage: 'Disabled',
|
||||
}),
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
// thirdParty.reCaptcha.version
|
||||
{
|
||||
namespace: 'thirdParty.reCaptcha.version',
|
||||
public: true,
|
||||
title: intl.formatMessage({
|
||||
id: 'options.thirdParty.reCaptcha.version.title',
|
||||
defaultMessage: 'reCAPTCHA version',
|
||||
}),
|
||||
desc: intl.formatMessage({
|
||||
id: 'options.thirdParty.reCaptcha.version.desc',
|
||||
defaultMessage:
|
||||
'Register your reCAPTCHA app \'<a href="https://www.google.com/recaptcha/about/" target="_blank">here</a>\', and choose a version.',
|
||||
}),
|
||||
type: 'choose',
|
||||
default: NaN,
|
||||
binds: {
|
||||
options: [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'options.thirdParty.reCaptcha.version.label.v3',
|
||||
defaultMessage: 'reCAPTCHA version 3',
|
||||
}),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'options.thirdParty.reCaptcha.version.label.v2',
|
||||
defaultMessage: 'reCAPTCHA version 2',
|
||||
}),
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
depends: (state) => {
|
||||
return state.value['thirdParty.reCaptcha.enable']
|
||||
},
|
||||
},
|
||||
// thirdParty.reCaptcha.siteKey
|
||||
{
|
||||
namespace: 'thirdParty.reCaptcha.siteKey',
|
||||
public: true,
|
||||
title: intl.formatMessage({
|
||||
id: 'options.thirdParty.reCaptcha.siteKey.title',
|
||||
defaultMessage: 'reCAPTCHA site key',
|
||||
}),
|
||||
type: 'string',
|
||||
default: '',
|
||||
depends: (state) => {
|
||||
return state.value['thirdParty.reCaptcha.enable']
|
||||
},
|
||||
},
|
||||
// thirdParty.reCaptcha.secretKey
|
||||
{
|
||||
namespace: 'thirdParty.reCaptcha.secretKey',
|
||||
public: false,
|
||||
title: intl.formatMessage({
|
||||
id: 'options.thirdParty.reCaptcha.secretKey.title',
|
||||
defaultMessage: 'reCAPTCHA secret key',
|
||||
}),
|
||||
type: 'string',
|
||||
default: '',
|
||||
depends: (state) => {
|
||||
return state.value['thirdParty.reCaptcha.enable']
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
other: {
|
||||
title: 'Other',
|
||||
icon: 'fas fa-umbrella',
|
||||
options: [
|
||||
{
|
||||
namespace: 'other.hello',
|
||||
public: true,
|
||||
title: 'Hello world',
|
||||
type: 'string',
|
||||
default: 'world',
|
||||
},
|
||||
],
|
||||
},
|
||||
demos: {
|
||||
title: 'Demo',
|
||||
icon: 'fas fa-democrat',
|
||||
desc: '<i class="fas fa-exclamation-triangle"></i> Just components demo, comment this section in prod mode!',
|
||||
options: [
|
||||
{
|
||||
namespace: 'demo.string',
|
||||
public: true,
|
||||
title: 'String',
|
||||
desc: 'One line string input.',
|
||||
type: 'string',
|
||||
default: 'Hello world!',
|
||||
},
|
||||
{
|
||||
namespace: 'basic.switcher',
|
||||
namespace: 'demo.longString',
|
||||
public: true,
|
||||
title: 'Long string',
|
||||
desc: 'Textarea for long string input.',
|
||||
type: 'longString',
|
||||
default: `"It is the unknown we fear when we look upon death and darkness, nothing more."\n-- Albus Dumbledore`,
|
||||
},
|
||||
{
|
||||
namespace: 'demo.switcher',
|
||||
public: true,
|
||||
title: 'Switcher',
|
||||
type: 'switcher',
|
||||
desc: 'True/False switcher.',
|
||||
default: true,
|
||||
binds: {
|
||||
positiveLabel: 'current on',
|
||||
@ -39,9 +228,10 @@ const options: Options = {
|
||||
},
|
||||
},
|
||||
{
|
||||
namespace: 'basic.chooseTest',
|
||||
title: 'Choose Test',
|
||||
desc: 'wooooo',
|
||||
namespace: 'demo.choose',
|
||||
public: true,
|
||||
title: 'Choose',
|
||||
desc: 'Choose one from options.',
|
||||
type: 'choose',
|
||||
default: NaN,
|
||||
binds: {
|
||||
@ -51,13 +241,13 @@ const options: Options = {
|
||||
{ label: 'op 3', disabled: false },
|
||||
{ label: 'op 4', disabled: true },
|
||||
],
|
||||
max: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
namespace: 'basic.optionsTest',
|
||||
title: 'Option Test',
|
||||
desc: 'wooooo',
|
||||
namespace: 'demo.selection',
|
||||
public: true,
|
||||
title: 'Selection',
|
||||
desc: 'Selection multiple items from options. max: {0: no limit, >0: limit}',
|
||||
type: 'selection',
|
||||
default: [true, false, true],
|
||||
binds: {
|
||||
@ -71,16 +261,10 @@ const options: Options = {
|
||||
},
|
||||
},
|
||||
{
|
||||
namespace: 'basic.longString',
|
||||
title: 'Long string',
|
||||
desc: 'A long string',
|
||||
type: 'longString',
|
||||
default: 'Opps',
|
||||
},
|
||||
{
|
||||
namespace: 'basic.mediaPicker',
|
||||
title: 'Image picker',
|
||||
desc: 'Media picker',
|
||||
namespace: 'demo.mediaPicker',
|
||||
public: true,
|
||||
title: 'Media picker',
|
||||
desc: '<code>type="image"|"video"|"audio?"</code>, the object must include id, id=0 for remote media.',
|
||||
type: 'mediaPicker',
|
||||
default: [
|
||||
{
|
||||
@ -101,32 +285,6 @@ const options: Options = {
|
||||
},
|
||||
],
|
||||
},
|
||||
social: {
|
||||
title: 'Social',
|
||||
icon: 'fas fa-users',
|
||||
options: [
|
||||
{
|
||||
namespace: 'social.github',
|
||||
title: 'Github username',
|
||||
desc: 'Your <a href="https://github.com" target="_blank">Github</a> username',
|
||||
type: 'string',
|
||||
default: 'mashirozx',
|
||||
},
|
||||
{ namespace: 'social.weibo', title: 'Weibo username', type: 'string', default: 'mashirozx' },
|
||||
],
|
||||
},
|
||||
other: {
|
||||
title: 'Other',
|
||||
icon: 'fas fa-umbrella',
|
||||
options: [
|
||||
{
|
||||
namespace: 'other.hello',
|
||||
title: 'Hello world',
|
||||
type: 'string',
|
||||
default: 'world',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default options
|
||||
|
24
src/admin/scheme.ts
Normal file
24
src/admin/scheme.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import palette, { Scheme } from '@/utils/palette'
|
||||
|
||||
const config: { [key: string]: Scheme } = {
|
||||
Default: {
|
||||
primary: '#2271b1',
|
||||
secondary: '#72aee6',
|
||||
background: '#f0f0f1',
|
||||
surface: '#ffffff',
|
||||
error: '#d63638',
|
||||
'on-primary': '#ffffff',
|
||||
'on-secondary': '#ffffff',
|
||||
'on-background': '#1d2327',
|
||||
'on-surface': '#3c434a',
|
||||
'on-error': '#ffffff',
|
||||
},
|
||||
}
|
||||
|
||||
const { name } = (window as any).AdminColors as { [key: string]: keyof typeof config }
|
||||
|
||||
const theConfig = config[name] ?? config['Default']
|
||||
|
||||
const scheme = palette(theConfig)
|
||||
|
||||
export default scheme
|
@ -11,7 +11,9 @@ export interface OptionStore {
|
||||
}
|
||||
|
||||
export default function auth(): object {
|
||||
const [config, setConfig]: [Ref<OptionStore>, (arg: OptionStore) => void] = useState({})
|
||||
const wpLocalizeScript = (window as any).SakuraOptions?.data as OptionStore
|
||||
const initConfig = cloneDeep(wpLocalizeScript ?? {})
|
||||
const [config, setConfig] = useState(initConfig, false)
|
||||
|
||||
const updateOption = (configState: Ref<OptionStore>, key: string, value: any) => {
|
||||
const config = cloneDeep(configState.value)
|
||||
@ -19,21 +21,5 @@ export default function auth(): object {
|
||||
setConfig(config)
|
||||
}
|
||||
|
||||
// const saveOption
|
||||
// const resetOption
|
||||
// const resetAllOption
|
||||
|
||||
// const mapOption = (configState: Ref<OptionStore>) => {
|
||||
// const config = cloneDeep(configState.value)
|
||||
// const data: OptionStore = {}
|
||||
// Object.keys(options).forEach((tagKey) => {
|
||||
// const tag = options[tagKey]
|
||||
// Object.keys(tag.options).forEach((namespace) => {
|
||||
// data[tagKey][namespace].payload = config[namespace]
|
||||
// })
|
||||
// })
|
||||
// return data
|
||||
// }
|
||||
|
||||
return { config, updateOption }
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="checkbox__container">
|
||||
<div :class="['checkbox__container', { disabled: $props.disabled }]">
|
||||
<div class="mdc-checkbox mdc-checkbox--touch" :ref="setElRef" @change="handleChange">
|
||||
<input type="checkbox" class="mdc-checkbox__native-control" :id="`checkbox-${id}`" />
|
||||
<div class="mdc-checkbox__background">
|
||||
@ -86,6 +86,12 @@ export default defineComponent({
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
.label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.label {
|
||||
user-select: none;
|
||||
}
|
||||
|
185
src/components/messages/MessageNormal.vue
Normal file
185
src/components/messages/MessageNormal.vue
Normal file
@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="item__container mdc-card mdc-card--outlined">
|
||||
<div class="item__content">
|
||||
<div class="column__wrapper--icon">
|
||||
<span><i class="fas fa-info-circle"></i></span>
|
||||
</div>
|
||||
<div class="column__wrapper--content">
|
||||
<div class="row__wrapper--title">
|
||||
<div class="title__content--message">
|
||||
<div class="title">
|
||||
<span>{{ $props.message.title }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="detailed"
|
||||
:style="{ height: shouldShowDetail ? `${expandContentHeight}px` : '0px' }"
|
||||
>
|
||||
<div :class="['content', { show: shouldShowDetail }]" :ref="setExpandContentRef">
|
||||
<span>{{ $props.message.detail }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="$props.message.detail"
|
||||
:class="['title__content--collapse', { reverse: shouldShowDetail }]"
|
||||
:title="msg.showDetails"
|
||||
@click="handleShowDetailClick"
|
||||
>
|
||||
<i class="fas fa-angle-double-down"></i>
|
||||
</div>
|
||||
<div class="title__content--close" :title="msg.close" @click="handleCloseMessageEvent">
|
||||
<i class="fas fa-times-circle"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { useIntl, useInjector, useState, useElementRef, useResizeObserver } from '@/hooks'
|
||||
import { messages } from '@/store'
|
||||
import NormalButton from '@/components/buttons/NormalButton.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { NormalButton },
|
||||
props: { message: Object },
|
||||
setup(props) {
|
||||
const intl = useIntl()
|
||||
const msg = {
|
||||
dismiss: intl.formatMessage({
|
||||
id: 'messages.popup.dismiss',
|
||||
defaultMessage: 'Dismiss',
|
||||
}),
|
||||
close: intl.formatMessage({
|
||||
id: 'messages.popup.close',
|
||||
defaultMessage: 'Close',
|
||||
}),
|
||||
showDetails: intl.formatMessage({
|
||||
id: 'messages.popup.showDetails',
|
||||
defaultMessage: 'Show details',
|
||||
}),
|
||||
}
|
||||
|
||||
const { messageList, removeMessage } = useInjector(messages)
|
||||
|
||||
const handleCloseMessageEvent = () => {
|
||||
if (props.message) removeMessage(messageList, props.message.id)
|
||||
}
|
||||
|
||||
const [shouldShowDetail, setShouldShowDetail] = useState(false)
|
||||
const handleShowDetailClick = () => {
|
||||
setShouldShowDetail(!shouldShowDetail.value)
|
||||
}
|
||||
|
||||
const [expandContentRef, setExpandContentRef] = useElementRef()
|
||||
const expandContentSize = useResizeObserver(expandContentRef)
|
||||
const expandContentHeight = computed(() =>
|
||||
expandContentSize.value.height === NaN
|
||||
? 0
|
||||
: expandContentSize.value.height + expandContentSize.value.paddingTop
|
||||
)
|
||||
|
||||
return {
|
||||
msg,
|
||||
handleCloseMessageEvent,
|
||||
shouldShowDetail,
|
||||
handleShowDetailClick,
|
||||
setExpandContentRef,
|
||||
expandContentHeight,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "sass:color";
|
||||
|
||||
.item__container {
|
||||
width: var(--width);
|
||||
background: #ffffff;
|
||||
border-left: 3px solid #34d058;
|
||||
> .item__content {
|
||||
width: calc(100% - 24px);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
> .column__wrapper {
|
||||
&--icon {
|
||||
flex: 0 0 auto;
|
||||
span {
|
||||
color: #34d058;
|
||||
font-size: medium;
|
||||
}
|
||||
}
|
||||
&--content {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: flex-start;
|
||||
> .row__wrapper {
|
||||
&--title {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
> * span {
|
||||
line-height: 16px;
|
||||
}
|
||||
> .title__content {
|
||||
&--message {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
> .title {
|
||||
span {
|
||||
color: #3c434a;
|
||||
}
|
||||
}
|
||||
> .detailed {
|
||||
transition: height 0.5s cubic-bezier(0, 0, 0.3, 1);
|
||||
overflow: hidden;
|
||||
> .content {
|
||||
padding-top: 6px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
transition: transform 0.5s cubic-bezier(0, 0, 0.3, 1);
|
||||
&.show {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
span {
|
||||
color: color.adjust(#3c434a, $lightness: 30%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&--collapse {
|
||||
flex: 0 0 auto;
|
||||
transform: scaleY(1);
|
||||
transition: transform 0.5s cubic-bezier(0, 0, 0.3, 1);
|
||||
&.reverse {
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
}
|
||||
&--close {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
&--buttons {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
111
src/components/messages/Messages.vue
Normal file
111
src/components/messages/Messages.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="messages__container" :style="positionControl">
|
||||
<transition-group name="messages" tag="div">
|
||||
<div class="message__wrapper" v-for="message in messagesCalc" :key="message.id">
|
||||
<MessageNormal v-if="message.style === 'normal'" :message="message"></MessageNormal>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { useInjector } from '@/hooks'
|
||||
import { messages } from '@/store'
|
||||
import MessageNormal from './MessageNormal.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { MessageNormal },
|
||||
props: {
|
||||
positionX: { type: String, default: 'right' }, // left center right
|
||||
positionY: { type: String, default: 'top' }, // top bottom
|
||||
width: { type: String, default: '380px' },
|
||||
},
|
||||
setup(props) {
|
||||
const { messageList } = useInjector(messages)
|
||||
|
||||
const messagesCalc = computed(() => {
|
||||
if (props.positionY === 'bottom') {
|
||||
return cloneDeep(messageList.value).reverse()
|
||||
} else {
|
||||
return cloneDeep(messageList.value)
|
||||
}
|
||||
})
|
||||
|
||||
const positionControl = computed(() => {
|
||||
return {
|
||||
'--from-0': props.positionY === 'bottom' ? '100%' : '-100%',
|
||||
'--from-70': props.positionY === 'bottom' ? '-20px' : '20px',
|
||||
'--from-100': 0,
|
||||
'--to-0': 0,
|
||||
'--to-30': props.positionX === 'right' ? '-20px' : '20px',
|
||||
'--to-70': props.positionX === 'right' ? '100%' : '-100%',
|
||||
'--to-100': props.positionX === 'right' ? '100%' : '-100%',
|
||||
'--absolute-fix': props.positionY === 'bottom' ? '-100%' : '0',
|
||||
'--width': props.width,
|
||||
}
|
||||
})
|
||||
|
||||
return { messagesCalc, positionControl }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.messages__container {
|
||||
width: calc(var(--width) + 12px);
|
||||
.message__wrapper {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.messages {
|
||||
&-enter-active {
|
||||
animation: from 0.5s forwards;
|
||||
}
|
||||
&-leave-active {
|
||||
position: absolute;
|
||||
transform-origin: center center;
|
||||
animation: to 0.5s forwards;
|
||||
}
|
||||
&-move {
|
||||
transition: transform 0.3s ease;
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes from {
|
||||
0% {
|
||||
transform: translateY(var(--from-0));
|
||||
opacity: 0;
|
||||
}
|
||||
70% {
|
||||
transform: translateY(var(--from-70));
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(var(--from-100));
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes to {
|
||||
0% {
|
||||
transform: translateX(var(--to-0)) translateY(var(--absolute-fix));
|
||||
opacity: 1;
|
||||
}
|
||||
30% {
|
||||
transform: translateX(var(--to-30)) translateY(var(--absolute-fix));
|
||||
opacity: 0.8;
|
||||
}
|
||||
70% {
|
||||
transform: translateX(var(--to-70)) translateY(var(--absolute-fix));
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(var(--to-100)) translateY(var(--absolute-fix));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="radio__container">
|
||||
<div :class="['radio__container', { disabled: $props.disabled }]">
|
||||
<div class="mdc-radio" :ref="setElRef">
|
||||
<input
|
||||
class="mdc-radio__native-control"
|
||||
type="checkbox"
|
||||
:id="`radio-${id}`"
|
||||
:name="`radio-${id}`"
|
||||
:id="id"
|
||||
:name="id"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<div class="mdc-radio__background">
|
||||
@ -14,7 +14,7 @@
|
||||
</div>
|
||||
<div class="mdc-radio__ripple"></div>
|
||||
</div>
|
||||
<label class="label" :for="`radio-${id}`">{{ $props.label }}</label>
|
||||
<label class="label" :for="id">{{ $props.label }}</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -32,7 +32,7 @@ export default defineComponent({
|
||||
},
|
||||
emits: ['update:checked'],
|
||||
setup(props, { emit }) {
|
||||
const id = uniqueHash()
|
||||
const id = `radio-${uniqueHash()}`
|
||||
|
||||
const [elRef, setElRef] = useElementRef()
|
||||
|
||||
@ -84,6 +84,12 @@ export default defineComponent({
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
.label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.label {
|
||||
user-select: none;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="switcher__container">
|
||||
<div :class="['switcher__container', { disabled: $props.disabled }]">
|
||||
<button
|
||||
:id="`switch-${id}`"
|
||||
class="mdc-switch mdc-switch--unselected"
|
||||
@ -93,11 +93,20 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './theme';
|
||||
.switcher__container {
|
||||
@include theme.variables;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
.label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.label {
|
||||
user-select: none;
|
||||
padding-left: 10px;
|
||||
|
56
src/components/switcher/_theme.scss
Normal file
56
src/components/switcher/_theme.scss
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* last modify: "@material/switch": "^12.0.0-canary.9f68a932e.0"
|
||||
*/
|
||||
@use 'sass:color';
|
||||
@use '@material/theme/color-palette';
|
||||
@use '@material/theme/theme-color';
|
||||
@use '@material/theme/theme';
|
||||
@use '@material/elevation/elevation-theme';
|
||||
|
||||
@mixin variables {
|
||||
$_hairline: color-palette.$grey-300;
|
||||
$_inverse-primary: var(--mdc-theme-primary-lighter-25);
|
||||
// color.scale(theme-color.prop-value(primary), $lightness: 75%);
|
||||
|
||||
$_on-surface: color-palette.$grey-800;
|
||||
$_on-surface-variant: color-palette.$grey-700;
|
||||
$_on-surface-state-content: color-palette.$grey-900;
|
||||
$_primary-state-content: var(--mdc-theme-primary-darker-10);
|
||||
// color.scale(theme-color.prop-value(primary), $blackness: 50%);
|
||||
|
||||
--mdc-switch-disabled-selected-handle-color: #{$_on-surface};
|
||||
--mdc-switch-disabled-selected-icon-color: var(--mdc-theme-on-primary);
|
||||
--mdc-switch-disabled-selected-track-color: #{$_on-surface};
|
||||
--mdc-switch-disabled-unselected-handle-color: #{$_on-surface};
|
||||
--mdc-switch-disabled-unselected-icon-color: var(--mdc-theme-on-primary);
|
||||
--mdc-switch-disabled-unselected-track-color: #{$_on-surface};
|
||||
--mdc-switch-handle-shadow-color: #{elevation-theme.$baseline-color};
|
||||
--mdc-switch-handle-surface-color: var(--mdc-theme-surface);
|
||||
--mdc-switch-selected-focus-handle-color: #{$_primary-state-content};
|
||||
--mdc-switch-selected-focus-state-layer-color: var(--mdc-theme-primary);
|
||||
--mdc-switch-selected-focus-track-color: #{$_inverse-primary};
|
||||
--mdc-switch-selected-handle-color: var(--mdc-theme-primary);
|
||||
--mdc-switch-selected-hover-handle-color: #{$_primary-state-content};
|
||||
--mdc-switch-selected-hover-state-layer-color: var(--mdc-theme-primary);
|
||||
--mdc-switch-selected-hover-track-color: #{$_inverse-primary};
|
||||
--mdc-switch-selected-icon-color: var(--mdc-theme-on-primary);
|
||||
--mdc-switch-selected-pressed-handle-color: #{$_primary-state-content};
|
||||
--mdc-switch-selected-pressed-state-layer-color: var(--mdc-theme-primary);
|
||||
--mdc-switch-selected-pressed-track-color: #{$_inverse-primary};
|
||||
--mdc-switch-selected-track-color: #{$_inverse-primary};
|
||||
--mdc-switch-unselected-focus-handle-color: #{$_on-surface-state-content};
|
||||
--mdc-switch-unselected-focus-state-layer-color: #{$_on-surface};
|
||||
--mdc-switch-unselected-focus-track-color: #{$_hairline};
|
||||
--mdc-switch-unselected-handle-color: #{$_on-surface-variant};
|
||||
--mdc-switch-unselected-hover-handle-color: #{$_on-surface-state-content};
|
||||
--mdc-switch-unselected-hover-state-layer-color: #{$_on-surface};
|
||||
--mdc-switch-unselected-hover-track-color: #{$_hairline};
|
||||
--mdc-switch-unselected-icon-color: var(--mdc-theme-on-primary);
|
||||
--mdc-switch-unselected-pressed-handle-color: #{$_on-surface-state-content};
|
||||
--mdc-switch-unselected-pressed-state-layer-color: #{$_on-surface};
|
||||
--mdc-switch-unselected-pressed-track-color: #{$_hairline};
|
||||
--mdc-switch-unselected-track-color: #{$_hairline};
|
||||
|
||||
// --mdc-switch-disabled-selected-icon-color: GrayText;
|
||||
// --mdc-switch-disabled-unselected-icon-color: GrayText;
|
||||
}
|
@ -8,6 +8,7 @@ import useReachElementSide from './useReachElementSide'
|
||||
import { useElementRef, useElementRefs } from './useElementRef'
|
||||
import useOffsetDistance from './useOffsetDistance'
|
||||
import useMDCRipple from './mdc/useMDCRipple'
|
||||
import useMessage from './useMessage'
|
||||
|
||||
export {
|
||||
useState,
|
||||
@ -26,4 +27,5 @@ export {
|
||||
useElementRef,
|
||||
useElementRefs,
|
||||
useOffsetDistance,
|
||||
useMessage,
|
||||
}
|
||||
|
@ -2,16 +2,24 @@ import { ref, readonly, UnwrapRef, DeepReadonly, Ref } from 'vue'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
// TODO: correct return type??
|
||||
export const useState = <T>(defaultValue: T): any => {
|
||||
export const useState = <T>(
|
||||
defaultValue: T,
|
||||
shouldReadonly = true
|
||||
): [Ref<UnwrapRef<T>>, (arg: T) => void] => {
|
||||
const state = ref(defaultValue)
|
||||
const set = (value: T): void => {
|
||||
state.value = value as UnwrapRef<T>
|
||||
}
|
||||
const get = readonly(state)
|
||||
const get = (shouldReadonly ? readonly(state) : state) as Ref<UnwrapRef<T>>
|
||||
return [get, set]
|
||||
}
|
||||
|
||||
export const usePersistedState = <K, T>(key: K, defaultValue: T, cachePeriod?: number): any => {
|
||||
export const usePersistedState = <K, T>(
|
||||
key: K,
|
||||
defaultValue: T,
|
||||
shouldReadonly = true,
|
||||
cachePeriod?: number
|
||||
): [Ref<UnwrapRef<T>>, (arg: T) => void] => {
|
||||
cachePeriod = cachePeriod ?? 24 * 60 * 60
|
||||
let state = ref(defaultValue)
|
||||
|
||||
@ -42,5 +50,7 @@ export const usePersistedState = <K, T>(key: K, defaultValue: T, cachePeriod?: n
|
||||
if (pendingSet) set(pendingSet)
|
||||
})
|
||||
|
||||
return [readonly(state), set]
|
||||
const get = (shouldReadonly ? readonly(state) : state) as Ref<UnwrapRef<T>>
|
||||
|
||||
return [get, set]
|
||||
}
|
||||
|
20
src/hooks/useMessage.ts
Normal file
20
src/hooks/useMessage.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { useInjector } from '@/hooks'
|
||||
import { messages } from '@/store'
|
||||
import type { Message, MessageOptions } from '@/store/messages'
|
||||
|
||||
export default function useMessage() {
|
||||
const {
|
||||
messageList,
|
||||
addMessage,
|
||||
}: {
|
||||
messageList: Ref<Message[]>
|
||||
addMessage: (state: Ref<Message[]>, options: MessageOptions) => void
|
||||
} = useInjector(messages)
|
||||
|
||||
const _addMessage = (options: MessageOptions) => {
|
||||
addMessage(messageList, options)
|
||||
}
|
||||
|
||||
return _addMessage
|
||||
}
|
@ -86,7 +86,8 @@ export default defineComponent({
|
||||
components: { NavItem },
|
||||
setup() {
|
||||
const avatar = 'https://view.moezx.cc/images/2021/06/13/d6b010a378d392d4633008b915f98ab1.md.png'
|
||||
const logo = 'https://v3.vuejs.org/logo.png'
|
||||
const logo =
|
||||
window.InitState.config['basic.site.logo'][0]?.url || 'https://v3.vuejs.org/logo.png'
|
||||
|
||||
const [navBarItemRefs, setNavBarItemRefs] = useElementRefs()
|
||||
const [navBarWrapperRef, setNavBarWrapperRef] = useElementRef()
|
||||
|
@ -1,7 +1,28 @@
|
||||
{
|
||||
"messages.admin.invalidUrl": "Invalid URL",
|
||||
"messages.admin.uplicateUrls": "Duplicate URLs",
|
||||
"messages.popup.close": "Close",
|
||||
"messages.popup.dismiss": "Dismiss",
|
||||
"messages.popup.showDetails": "Show details",
|
||||
"messages.wordpress.applicationPasswordsEndpointNotAvailable": "ApplicationPasswords is not avaliabe in your WordPress installation, please upgrade WordPress to v5.6.0 above, or enable the ApplicationPasswords feature in v5.6.0 above installation.",
|
||||
"messages.wordpress.permalink.shouldIncludeFieldsInPost": "WordPress pages should use %slug% as the permalink.",
|
||||
"messages.wordpress.permalink.shouldIncludeFieldsInSingle": "WordPress permalink should include at least one of %post_id%, %postname%. You may set them here: {baseUrl}/wp-admin/options-permalink.php",
|
||||
"options.basic.site.logo.binds.button": "Use this image",
|
||||
"options.basic.site.logo.binds.title": "Select image for site logo.",
|
||||
"options.basic.site.logo.desc": "The site's Logo image, will display on navigation bar.",
|
||||
"options.basic.site.logo.title": "Site logo",
|
||||
"options.basic.site.title.desc": "The site title",
|
||||
"options.basic.site.title.title": "Site title",
|
||||
"options.thirdParty.reCaptcha.enable.desc": "Use reCAPTCHA for anti-spam check.",
|
||||
"options.thirdParty.reCaptcha.enable.negativeLabel": "Disabled",
|
||||
"options.thirdParty.reCaptcha.enable.positiveLabel": "Enabled",
|
||||
"options.thirdParty.reCaptcha.enable.title": "Enable reCAPTCHA",
|
||||
"options.thirdParty.reCaptcha.secretKey.title": "reCAPTCHA secret key",
|
||||
"options.thirdParty.reCaptcha.siteKey.title": "reCAPTCHA site key",
|
||||
"options.thirdParty.reCaptcha.version.desc": "Register your reCAPTCHA app '<a href=\"https://www.google.com/recaptcha/about/\" target=\"_blank\">here</a>', and choose a version.",
|
||||
"options.thirdParty.reCaptcha.version.label.v2": "reCAPTCHA version 2",
|
||||
"options.thirdParty.reCaptcha.version.label.v3": "reCAPTCHA version 3",
|
||||
"options.thirdParty.reCaptcha.version.title": "reCAPTCHA version",
|
||||
"posts.comment.composer.authorEmail.label": "Email *",
|
||||
"posts.comment.composer.authorName.label": "Nickname *",
|
||||
"posts.comment.composer.authorUrl.label": "Link",
|
||||
|
@ -1,4 +1,19 @@
|
||||
{
|
||||
"messages.admin.invalidUrl": {
|
||||
"defaultMessage": "Invalid URL"
|
||||
},
|
||||
"messages.admin.uplicateUrls": {
|
||||
"defaultMessage": "Duplicate URLs"
|
||||
},
|
||||
"messages.popup.close": {
|
||||
"defaultMessage": "Close"
|
||||
},
|
||||
"messages.popup.dismiss": {
|
||||
"defaultMessage": "Dismiss"
|
||||
},
|
||||
"messages.popup.showDetails": {
|
||||
"defaultMessage": "Show details"
|
||||
},
|
||||
"messages.wordpress.applicationPasswordsEndpointNotAvailable": {
|
||||
"defaultMessage": "ApplicationPasswords is not avaliabe in your WordPress installation, please upgrade WordPress to v5.6.0 above, or enable the ApplicationPasswords feature in v5.6.0 above installation."
|
||||
},
|
||||
@ -8,6 +23,54 @@
|
||||
"messages.wordpress.permalink.shouldIncludeFieldsInSingle": {
|
||||
"defaultMessage": "WordPress permalink should include at least one of %post_id%, %postname%. You may set them here: {baseUrl}/wp-admin/options-permalink.php"
|
||||
},
|
||||
"options.basic.site.logo.binds.button": {
|
||||
"defaultMessage": "Use this image"
|
||||
},
|
||||
"options.basic.site.logo.binds.title": {
|
||||
"defaultMessage": "Select image for site logo."
|
||||
},
|
||||
"options.basic.site.logo.desc": {
|
||||
"defaultMessage": "The site's Logo image, will display on navigation bar."
|
||||
},
|
||||
"options.basic.site.logo.title": {
|
||||
"defaultMessage": "Site logo"
|
||||
},
|
||||
"options.basic.site.title.desc": {
|
||||
"defaultMessage": "The site title"
|
||||
},
|
||||
"options.basic.site.title.title": {
|
||||
"defaultMessage": "Site title"
|
||||
},
|
||||
"options.thirdParty.reCaptcha.enable.desc": {
|
||||
"defaultMessage": "Use reCAPTCHA for anti-spam check."
|
||||
},
|
||||
"options.thirdParty.reCaptcha.enable.negativeLabel": {
|
||||
"defaultMessage": "Disabled"
|
||||
},
|
||||
"options.thirdParty.reCaptcha.enable.positiveLabel": {
|
||||
"defaultMessage": "Enabled"
|
||||
},
|
||||
"options.thirdParty.reCaptcha.enable.title": {
|
||||
"defaultMessage": "Enable reCAPTCHA"
|
||||
},
|
||||
"options.thirdParty.reCaptcha.secretKey.title": {
|
||||
"defaultMessage": "reCAPTCHA secret key"
|
||||
},
|
||||
"options.thirdParty.reCaptcha.siteKey.title": {
|
||||
"defaultMessage": "reCAPTCHA site key"
|
||||
},
|
||||
"options.thirdParty.reCaptcha.version.desc": {
|
||||
"defaultMessage": "Register your reCAPTCHA app '<a href=\"https://www.google.com/recaptcha/about/\" target=\"_blank\">here</a>', and choose a version."
|
||||
},
|
||||
"options.thirdParty.reCaptcha.version.label.v2": {
|
||||
"defaultMessage": "reCAPTCHA version 2"
|
||||
},
|
||||
"options.thirdParty.reCaptcha.version.label.v3": {
|
||||
"defaultMessage": "reCAPTCHA version 3"
|
||||
},
|
||||
"options.thirdParty.reCaptcha.version.title": {
|
||||
"defaultMessage": "reCAPTCHA version"
|
||||
},
|
||||
"posts.comment.composer.authorEmail.label": {
|
||||
"defaultMessage": "Email *"
|
||||
},
|
||||
|
@ -4,7 +4,7 @@ import '@yzfe/svgicon/lib/svgicon.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { storeProviderPlugin } from './hooks/store'
|
||||
import { auth, init, posts, comments } from './store'
|
||||
import { auth, init, posts, comments, messages } from './store'
|
||||
import { intlPlugin } from './locales'
|
||||
import UiIcon from '@/components/icon/UiIcon.vue'
|
||||
import Image from '@/components/image/Image.vue'
|
||||
@ -13,7 +13,7 @@ const theWindow = window as any
|
||||
theWindow.router = router
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(storeProviderPlugin, [auth, init, posts, comments])
|
||||
app.use(storeProviderPlugin, [auth, init, posts, comments, messages])
|
||||
app.use(router)
|
||||
app.use(intlPlugin)
|
||||
app.use(VueSvgIconPlugin, { tagName: 'svg-icon' })
|
||||
|
@ -2,5 +2,6 @@ import auth from './auth'
|
||||
import init from './init'
|
||||
import posts from './posts'
|
||||
import comments from './comments'
|
||||
import messages from './messages'
|
||||
|
||||
export { auth, init, posts, comments }
|
||||
export { auth, init, posts, comments, messages }
|
||||
|
55
src/store/messages.ts
Normal file
55
src/store/messages.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Ref } from 'vue'
|
||||
import { cloneDeep, remove } from 'lodash'
|
||||
import { useState } from '@/hooks'
|
||||
import uniqueHash from '@/utils/uniqueHash'
|
||||
|
||||
export interface Message {
|
||||
id: string
|
||||
title: string
|
||||
detail?: string
|
||||
type?: 'success' | 'warning' | 'info' | 'error'
|
||||
style?: 'normal' | 'collapse'
|
||||
options?: { [key: string]: any }
|
||||
closeTimeout?: number
|
||||
}
|
||||
|
||||
export interface MessageOptions extends Omit<Message, 'id'> {}
|
||||
|
||||
export default function msg(): object {
|
||||
const [messageList, setMessageList]: [Ref<Message[]>, (arg: Message[]) => void] = useState([])
|
||||
|
||||
const addMessage = (state: typeof messageList, options: MessageOptions) => {
|
||||
const id = `message_${uniqueHash()}`
|
||||
const _state = cloneDeep(state.value)
|
||||
const message = { ...options, id }
|
||||
message['type'] ||= 'info' // the default message type
|
||||
message['style'] ||= 'normal' // the default message type
|
||||
_state.push(message)
|
||||
setMessageList(_state)
|
||||
|
||||
if (options.closeTimeout !== undefined && options.closeTimeout <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const closeTimeout = options.closeTimeout || 3000
|
||||
|
||||
setTimeout(() => removeMessage(state, id), closeTimeout)
|
||||
}
|
||||
|
||||
const removeMessage = (state: typeof messageList, id: string) => {
|
||||
const _state = cloneDeep(state.value)
|
||||
remove(_state, (item) => item.id === id)
|
||||
setMessageList(_state)
|
||||
}
|
||||
|
||||
const clearMessage = () => {
|
||||
setMessageList([])
|
||||
}
|
||||
|
||||
return {
|
||||
messageList,
|
||||
addMessage,
|
||||
removeMessage,
|
||||
clearMessage,
|
||||
}
|
||||
}
|
39
src/utils/palette.ts
Normal file
39
src/utils/palette.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { cloneDeep } from 'lodash'
|
||||
import chroma from 'chroma-js'
|
||||
|
||||
export interface Scheme {
|
||||
// base
|
||||
primary: string
|
||||
secondary: string
|
||||
background: string
|
||||
surface: string
|
||||
error: string
|
||||
// text color of a * background
|
||||
'on-primary': string
|
||||
'on-secondary': string
|
||||
'on-background': string
|
||||
'on-surface': string
|
||||
'on-error': string
|
||||
// modifier
|
||||
'primary-lighter-25'?: string
|
||||
'primary-darker-10'?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @param scheme
|
||||
* @returns CSS variables object using directly in :style
|
||||
*/
|
||||
export default function (scheme: Scheme) {
|
||||
const modifier = {
|
||||
'primary-lighter-25': chroma(scheme.primary).brighten(2.5).hex(),
|
||||
'primary-darker-10': chroma(scheme.primary).darken(1).hex(),
|
||||
}
|
||||
const _scheme = cloneDeep(Object.assign(scheme, modifier))
|
||||
|
||||
const colors: { [key: string]: string } = {}
|
||||
Object.keys(scheme).forEach(
|
||||
(key) => (colors[`--mdc-theme-${key}`] = _scheme[key as keyof typeof scheme])
|
||||
)
|
||||
|
||||
return colors
|
||||
}
|
67
yarn.lock
67
yarn.lock
@ -1290,6 +1290,11 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.3.0"
|
||||
|
||||
"@types/chroma-js@^2.1.3":
|
||||
version "2.1.3"
|
||||
resolved "https://registry.nlark.com/@types/chroma-js/download/@types/chroma-js-2.1.3.tgz#0b03d737ff28fad10eb884e0c6cedd5ffdc4ba0a"
|
||||
integrity sha1-CwPXN/8o+tEOuITgxs7dX/3Eugo=
|
||||
|
||||
"@types/crypto-js@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.nlark.com/@types/crypto-js/download/@types/crypto-js-4.0.1.tgz#3a4bd24518b0e6c5940da4e2659eeb2ef0806963"
|
||||
@ -2271,6 +2276,13 @@ character-parser@^2.2.0:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chroma-js@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.nlark.com/chroma-js/download/chroma-js-2.1.2.tgz#1075cb9ae25bcb2017c109394168b5cf3aa500ec"
|
||||
integrity sha1-EHXLmuJbyyAXwQk5QWi1zzqlAOw=
|
||||
dependencies:
|
||||
cross-env "^6.0.3"
|
||||
|
||||
ci-info@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.nlark.com/ci-info/download/ci-info-2.0.0.tgz?cache=0&sync_timestamp=1622039942508&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fci-info%2Fdownload%2Fci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
|
||||
@ -2399,11 +2411,6 @@ commander@8:
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-8.0.0.tgz#1da2139548caef59bd23e66d18908dfb54b02258"
|
||||
integrity sha512-Xvf85aAtu6v22+E5hfVoLHqyul/jyxh91zvqk/ioJTQuJR7Z78n7H558vMPKanPSRgIEeZemT92I2g9Y8LPbSQ==
|
||||
|
||||
commander@^2.15.1:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.nlark.com/commander/download/commander-2.20.3.tgz?cache=0&sync_timestamp=1621726578455&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fcommander%2Fdownload%2Fcommander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
integrity sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz"
|
||||
@ -2475,6 +2482,13 @@ core-util-is@~1.0.0:
|
||||
resolved "https://registry.npm.taobao.org/core-util-is/download/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
|
||||
|
||||
cross-env@^6.0.3:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.npm.taobao.org/cross-env/download/cross-env-6.0.3.tgz#4256b71e49b3a40637a0ce70768a6ef5c72ae941"
|
||||
integrity sha1-Qla3HkmzpAY3oM5wdopu9ccq6UE=
|
||||
dependencies:
|
||||
cross-spawn "^7.0.0"
|
||||
|
||||
cross-spawn@^5.0.1:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.npm.taobao.org/cross-spawn/download/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
|
||||
@ -2484,7 +2498,7 @@ cross-spawn@^5.0.1:
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.npm.taobao.org/cross-spawn/download/cross-spawn-7.0.3.tgz"
|
||||
integrity sha1-9zqFudXUHQRVUcF34ogtSshXKKY=
|
||||
@ -3166,11 +3180,6 @@ esutils@^2.0.2:
|
||||
resolved "https://registry.npm.taobao.org/esutils/download/esutils-2.0.3.tgz"
|
||||
integrity sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q=
|
||||
|
||||
eventemitter3@^4.0.0:
|
||||
version "4.0.7"
|
||||
resolved "https://registry.nlark.com/eventemitter3/download/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||
integrity sha1-Lem2j2Uo1WRO9cWVJqG0oHMGFp8=
|
||||
|
||||
execa@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.nlark.com/execa/download/execa-0.7.0.tgz?cache=0&sync_timestamp=1622825396605&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fexeca%2Fdownload%2Fexeca-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
|
||||
@ -3328,21 +3337,11 @@ fn-name@^2.0.1:
|
||||
resolved "https://registry.npm.taobao.org/fn-name/download/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7"
|
||||
integrity sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.10.0:
|
||||
follow-redirects@^1.10.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.nlark.com/follow-redirects/download/follow-redirects-1.14.1.tgz?cache=0&sync_timestamp=1620555300559&other_urls=https%3A%2F%2Fregistry.nlark.com%2Ffollow-redirects%2Fdownload%2Ffollow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
|
||||
integrity sha1-2RFN7Qoc/dM04WTmZirQK/2R/0M=
|
||||
|
||||
foreman@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.nlark.com/foreman/download/foreman-3.0.1.tgz#805f28afc5a4bbaf08dbb1f5018c557dcbb8a410"
|
||||
integrity sha1-gF8or8Wku68I27H1AYxVfcu4pBA=
|
||||
dependencies:
|
||||
commander "^2.15.1"
|
||||
http-proxy "^1.17.0"
|
||||
mustache "^2.2.1"
|
||||
shell-quote "^1.6.1"
|
||||
|
||||
form-data@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npm.taobao.org/form-data/download/form-data-3.0.1.tgz?cache=0&sync_timestamp=1613410812604&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fform-data%2Fdownload%2Fform-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
|
||||
@ -3677,15 +3676,6 @@ http-proxy-agent@^4.0.1:
|
||||
agent-base "6"
|
||||
debug "4"
|
||||
|
||||
http-proxy@^1.17.0:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.npm.taobao.org/http-proxy/download/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
|
||||
integrity sha1-QBVB8FNIhLv5UmAzTnL4juOXZUk=
|
||||
dependencies:
|
||||
eventemitter3 "^4.0.0"
|
||||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
https-proxy-agent@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.npm.taobao.org/https-proxy-agent/download/https-proxy-agent-2.2.4.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhttps-proxy-agent%2Fdownload%2Fhttps-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
|
||||
@ -5039,11 +5029,6 @@ ms@^2.1.1:
|
||||
resolved "https://registry.npm.taobao.org/ms/download/ms-2.1.3.tgz?cache=0&sync_timestamp=1607433872491&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha1-V0yBOM4dK1hh8LRFedut1gxmFbI=
|
||||
|
||||
mustache@^2.2.1:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.npm.taobao.org/mustache/download/mustache-2.3.2.tgz?cache=0&sync_timestamp=1616959918003&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmustache%2Fdownload%2Fmustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5"
|
||||
integrity sha1-ptTZw/kdEzWauImoEpVPkjCj0MU=
|
||||
|
||||
nanoid@^3.1.23:
|
||||
version "3.1.23"
|
||||
resolved "https://registry.nlark.com/nanoid/download/nanoid-3.1.23.tgz"
|
||||
@ -5893,11 +5878,6 @@ require-from-string@^2.0.2:
|
||||
resolved "https://registry.npm.taobao.org/require-from-string/download/require-from-string-2.0.2.tgz"
|
||||
integrity sha1-iaf92TgmEmcxjq/hT5wy5ZjDaQk=
|
||||
|
||||
requires-port@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npm.taobao.org/requires-port/download/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||
|
||||
resize-observer-polyfill@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.npm.taobao.org/resize-observer-polyfill/download/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||
@ -6071,11 +6051,6 @@ shebang-regex@^3.0.0:
|
||||
resolved "https://registry.npm.taobao.org/shebang-regex/download/shebang-regex-3.0.0.tgz"
|
||||
integrity sha1-rhbxZE2HPsrYQ7AwexQzYtTEIXI=
|
||||
|
||||
shell-quote@^1.6.1:
|
||||
version "1.7.2"
|
||||
resolved "https://registry.nlark.com/shell-quote/download/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
|
||||
integrity sha1-Z6fQLHbJ2iT5nSCAj8re0ODgS+I=
|
||||
|
||||
shellsubstitute@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npm.taobao.org/shellsubstitute/download/shellsubstitute-1.2.0.tgz#e4f702a50c518b0f6fe98451890d705af29b6b70"
|
||||
|
Loading…
Reference in New Issue
Block a user