mirror of
https://github.com/mashirozx/sakura.git
synced 2024-11-22 14:58:14 +08:00
Add vue admin panel
This commit is contained in:
parent
64bc72864b
commit
4efaa8413a
@ -5,10 +5,24 @@ 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
|
||||
{
|
||||
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.
|
||||
*
|
||||
@ -23,7 +37,7 @@ class ConfigurationController extends BaseController
|
||||
/**
|
||||
* Registers the routes for comments.
|
||||
*
|
||||
* @since 4.7.0
|
||||
* @since 5.0.0
|
||||
*
|
||||
* @see register_rest_route()
|
||||
*/
|
||||
@ -39,12 +53,6 @@ class ConfigurationController extends BaseController
|
||||
'permission_callback' => array($this, 'get_config_permissions_check'),
|
||||
// 'args' => $this->get_collection_params(),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array($this, 'create_config'),
|
||||
'permission_callback' => array($this, 'create_config_permissions_check'),
|
||||
// 'args' => $this->get_endpoint_args_for_item_schema(WP_REST_Server::CREATABLE),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array($this, 'update_config'),
|
||||
@ -75,7 +83,7 @@ class ConfigurationController extends BaseController
|
||||
return true;
|
||||
}
|
||||
|
||||
public function create_config(WP_REST_Request $request)
|
||||
public function update_config(WP_REST_Request $request)
|
||||
{
|
||||
$original = (array) $this->get_config($request);
|
||||
$json = (array) self::json_validate($request->get_body());
|
||||
@ -83,8 +91,7 @@ class ConfigurationController extends BaseController
|
||||
return $original;
|
||||
}
|
||||
|
||||
$config = OptionModel::create($this->rest_base, $json);
|
||||
$config = $config ? $config : OptionModel::update($this->rest_base, $json);
|
||||
$config = OptionModel::update($this->rest_base, $json);
|
||||
if (!$config) {
|
||||
return new WP_Error(
|
||||
'save_config_failure',
|
||||
@ -96,25 +103,51 @@ class ConfigurationController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
public function create_config_permissions_check(WP_REST_Request $request)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function update_config(WP_REST_Request $request)
|
||||
{
|
||||
return $this->create_config($request);
|
||||
}
|
||||
|
||||
public function update_config_permissions_check(WP_REST_Request $request)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function inite_theme()
|
||||
{
|
||||
$config = OptionModel::create($this->rest_base, (array)[]);
|
||||
}
|
||||
|
||||
public static function json_validate(string $string)
|
||||
{
|
||||
$json = json_decode($string);
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
public function set_key_value(string $key, $value)
|
||||
{
|
||||
$json = (array) OptionModel::get($this->rest_base);
|
||||
if (!$json) {
|
||||
return new WP_Error(
|
||||
'no_such_option',
|
||||
__('Maybe you should save the configuration bufore using it.', self::$text_domain),
|
||||
array('status' => 500)
|
||||
);
|
||||
}
|
||||
$json[$key] = $value;
|
||||
$config = OptionModel::update($this->rest_base, $json);
|
||||
$config = $config ? $config : OptionModel::create($this->rest_base, $json);
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function sakura_options(string $namespace, $default)
|
||||
{
|
||||
$config = (array) OptionModel::get($this->rest_base);
|
||||
if (array_key_exists($namespace, $config)) {
|
||||
return $config[$namespace];
|
||||
} else {
|
||||
$this->set_key_value($namespace, $default);
|
||||
return $default;
|
||||
}
|
||||
// translators: %s: $namespace */
|
||||
// throw new Exception(
|
||||
// sprintf(__("No existing database saving value or default value for option '%s'.", self::$text_domain), $namespace)
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,6 @@ namespace Sakura\Controllers;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Request;
|
||||
use WP_Rewrite;
|
||||
use Sakura\Controllers\MenuController;
|
||||
use Sakura\Controllers\CommentController;
|
||||
|
||||
class InitStateController extends BaseController
|
||||
{
|
||||
@ -33,6 +31,7 @@ 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
|
||||
);
|
||||
|
@ -11,13 +11,19 @@ define('SAKURA_DEVEPLOMENT_HOST', 'http://127.0.0.1:9000');
|
||||
// PHP loaders
|
||||
require_once(__DIR__ . '/loader.php');
|
||||
|
||||
new \Sakura\Helpers\SetupHelper();
|
||||
new \Sakura\Helpers\WhoopsHelper();
|
||||
new \Sakura\Helpers\ViteHelper();
|
||||
new \Sakura\Helpers\AdminPageHelper();
|
||||
new \Sakura\Helpers\CustomMenuMetaFieldsHelper();
|
||||
new \Sakura\Helpers\CommentHelper();
|
||||
new \Sakura\Helpers\PostQueryHelper('post');
|
||||
new Sakura\Helpers\SetupHelper();
|
||||
new Sakura\Helpers\WhoopsHelper();
|
||||
new Sakura\Helpers\ViteHelper();
|
||||
new Sakura\Helpers\AdminPageHelper();
|
||||
new Sakura\Helpers\CustomMenuMetaFieldsHelper();
|
||||
new Sakura\Helpers\CommentHelper();
|
||||
new Sakura\Helpers\PostQueryHelper('post');
|
||||
|
||||
new \Sakura\Routers\ApiRouter();
|
||||
new \Sakura\Routers\PagesRouter();
|
||||
new Sakura\Routers\ApiRouter();
|
||||
new Sakura\Routers\PagesRouter();
|
||||
|
||||
function sakura_options(string $namespace, $default)
|
||||
{
|
||||
$CF = new Sakura\Controllers\ConfigurationController();
|
||||
return $CF->sakura_options($namespace, $default);
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Sakura\Helpers;
|
||||
|
||||
use Sakura\Controllers\ConfigurationController;
|
||||
|
||||
class SetupHelper
|
||||
{
|
||||
public function __construct()
|
||||
@ -19,6 +21,8 @@ class SetupHelper
|
||||
add_filter('excerpt_length', [$this, 'changes_post_excerpt_length'], 10);
|
||||
// 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);
|
||||
}
|
||||
|
||||
public function setup()
|
||||
|
@ -11,7 +11,7 @@ class OptionModel extends BaseModel
|
||||
return self::$namespace . "_{$key}";
|
||||
}
|
||||
|
||||
public static function create(string $key, $value)
|
||||
public static function create(string $key, $value)
|
||||
{
|
||||
return add_option(self::the_key($key), $value);
|
||||
}
|
||||
|
@ -1 +1,3 @@
|
||||
Elevation & shadows: <https://material.io/archive/guidelines/material-design/elevation-shadows.html>
|
||||
|
||||
Typegraphy: <https://material.io/design/typography/the-type-system.html#type-scale>
|
||||
|
@ -55,6 +55,7 @@
|
||||
"sass": "^1.35.1",
|
||||
"sass-loader": "^12.1.0",
|
||||
"snakecase-keys": "^4.0.2",
|
||||
"swiper": "^6.7.5",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^3.1.4",
|
||||
"vue-intl": "^6.0.6",
|
||||
|
@ -1,23 +1,27 @@
|
||||
<template>
|
||||
<div class="app__wrapper">
|
||||
<Layout></Layout>
|
||||
<Core></Core>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import Layout from './Layout.vue'
|
||||
import Core from './Core.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { Layout },
|
||||
components: { Core },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use './styles/index';
|
||||
@use './index';
|
||||
.sakura-options-page__app {
|
||||
width: calc(100% - 20px);
|
||||
padding: 20px 20px 20px 0;
|
||||
@media screen and (max-width: 782px) {
|
||||
width: calc(100% - 10px);
|
||||
padding: 10px 10px 10px 0;
|
||||
}
|
||||
> .app__wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
91
src/admin/Core.vue
Normal file
91
src/admin/Core.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="layout mdc-card mdc-card--outlined">
|
||||
<div class="tab-bar__wrapper">
|
||||
<TabBar v-model:current="currentTabIndex" :items="tabs"></TabBar>
|
||||
</div>
|
||||
<Swiper
|
||||
class="tab-page__wrapper"
|
||||
:slidesPerView="1"
|
||||
:spaceBetween="50"
|
||||
:allowTouchMove="false"
|
||||
:autoHeight="true"
|
||||
@swiper="handleSwiperEvent"
|
||||
>
|
||||
<SwiperSlide
|
||||
class="tab-page__container"
|
||||
v-for="(tabKey, tabKeyIndex) in tabKeys"
|
||||
:key="tabKeyIndex"
|
||||
>
|
||||
<div class="tab-page__content mdc-typography">
|
||||
<h1 class="mdc-typography--headline5">{{ options[tabKey].title }}</h1>
|
||||
<div
|
||||
class="row__wrapper--options"
|
||||
v-for="(option, optionIndex) in options[tabKey].options"
|
||||
:key="optionIndex"
|
||||
>
|
||||
{{ option.namespace }}
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
</Swiper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, Ref, watch, nextTick } from 'vue'
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue'
|
||||
import { Swiper as SwiperInterface } from 'swiper'
|
||||
import { useInjector } from '@/hooks'
|
||||
import store from './store'
|
||||
import options from '@/admin/options'
|
||||
import TabBar from '@/components/tabBar/TabBar.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { TabBar, Swiper, SwiperSlide },
|
||||
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 }
|
||||
})
|
||||
const handleSwiperEvent = (swiper: SwiperInterface) => {
|
||||
swiperRef.value = swiper
|
||||
}
|
||||
|
||||
watch(currentTabIndex, (current) => swiperRef.value?.slideTo(current))
|
||||
nextTick(() => swiperRef.value?.updateAutoHeight(100))
|
||||
|
||||
// data controllers
|
||||
const { config, setConfig } = useInjector(store)
|
||||
|
||||
return { currentTabIndex, tabKeys, tabs, options, handleSwiperEvent }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep() {
|
||||
@import 'swiper/swiper';
|
||||
}
|
||||
|
||||
.layout {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
> .tab-bar__wrapper {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
> .tab-page__wrapper {
|
||||
width: 100%;
|
||||
.tab-page__container {
|
||||
width: 100%;
|
||||
.tab-page__content {
|
||||
width: calc(100% - 24px);
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,36 +0,0 @@
|
||||
<template>
|
||||
<div class="layout mdc-card mdc-card--outlined">
|
||||
<div class="tab-bar__wrapper">
|
||||
<TabBar></TabBar>
|
||||
</div>
|
||||
<br /><br /><br /><br /><br /><br /><br />
|
||||
<br /><br /><br /><br /><br /><br /><br />
|
||||
<br /><br /><br /><br /><br /><br /><br />
|
||||
<br /><br /><br /><br /><br /><br /><br />
|
||||
<br /><br /><br /><br /><br /><br /><br />
|
||||
<br /><br /><br /><br /><br /><br /><br />
|
||||
<br /><br /><br /><br /><br /><br /><br />
|
||||
<br /><br /><br /><br /><br /><br /><br />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import TabBar from '@/components/tabBar/TabBar.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { TabBar },
|
||||
setup() {},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
> .tab-bar__wrapper {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
</style>
|
24
src/admin/OptionItem.vue
Normal file
24
src/admin/OptionItem.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="option__container">
|
||||
<h1 class="mdc-typography--headline6">{{ $props.namespace }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { useInjector } from '@/hooks'
|
||||
import store from './store'
|
||||
|
||||
const defaultOption = {}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
options: { type: Object, default: () => defaultOption },
|
||||
},
|
||||
emits: [],
|
||||
setup(props, { emit }) {
|
||||
const { config, setConfig } = useInjector(store)
|
||||
return {}
|
||||
},
|
||||
})
|
||||
</script>
|
@ -1,11 +1,12 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export default {
|
||||
postConfigJson(config: any): Promise<any> {
|
||||
postConfigJson(data: any): Promise<any> {
|
||||
return request({
|
||||
url: '/sakura/v1/config',
|
||||
method: 'POST',
|
||||
data: config,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: data,
|
||||
})
|
||||
},
|
||||
}
|
@ -11,3 +11,4 @@
|
||||
@use "@material/tab-scroller/mdc-tab-scroller";
|
||||
@use "@material/tab-indicator/mdc-tab-indicator";
|
||||
@use "@material/tab/mdc-tab";
|
||||
@use '@material/typography/mdc-typography';
|
@ -3,13 +3,13 @@ import { VueSvgIconPlugin } from '@yzfe/vue3-svgicon'
|
||||
import '@yzfe/svgicon/lib/svgicon.css'
|
||||
import App from './App.vue'
|
||||
import { storeProviderPlugin } from '@/hooks/store'
|
||||
// import { auth, init, posts, comments } from './store'
|
||||
import store 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, [auth, init, posts, comments])
|
||||
app.use(storeProviderPlugin, [store])
|
||||
app.use(intlPlugin)
|
||||
app.use(VueSvgIconPlugin, { tagName: 'svg-icon' })
|
||||
app.component('UiIcon', UiIcon)
|
||||
|
51
src/admin/options.ts
Normal file
51
src/admin/options.ts
Normal file
@ -0,0 +1,51 @@
|
||||
export interface Options {
|
||||
[tag: string]: {
|
||||
title: string
|
||||
icon: string
|
||||
options: Array<{
|
||||
namespace: string
|
||||
type: string
|
||||
default: any
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
const options: Options = {
|
||||
basic: {
|
||||
title: 'Basic',
|
||||
icon: 'fas fa-address-card',
|
||||
options: [
|
||||
{
|
||||
namespace: 'basic.siteTitle',
|
||||
type: 'string',
|
||||
default: 'Opps',
|
||||
},
|
||||
{
|
||||
namespace: 'basic.userName',
|
||||
type: 'string',
|
||||
default: 'Mashiro',
|
||||
},
|
||||
],
|
||||
},
|
||||
social: {
|
||||
title: 'Social',
|
||||
icon: 'fas fa-users',
|
||||
options: [
|
||||
{ namespace: 'social.github', type: 'string', default: 'mashirozx' },
|
||||
{ namespace: 'social.weibo', type: 'string', default: 'mashirozx' },
|
||||
],
|
||||
},
|
||||
other: {
|
||||
title: 'Other',
|
||||
icon: 'fas fa-umbrella',
|
||||
options: [
|
||||
{
|
||||
namespace: 'other.hello',
|
||||
type: 'string',
|
||||
default: 'world',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default options
|
39
src/admin/store.ts
Normal file
39
src/admin/store.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Ref } from 'vue'
|
||||
import { useState } from '@/hooks'
|
||||
import API from '@/api'
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
import intl from '@/locales'
|
||||
import options, { Options } from './options'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
export interface OptionStore {
|
||||
[namespace: string]: any
|
||||
}
|
||||
|
||||
export default function auth(): object {
|
||||
const [config, setConfig]: [Ref<OptionStore>, (arg: OptionStore) => void] = useState({})
|
||||
|
||||
const updateOption = (configState: Ref<OptionStore>, key: string, value: any) => {
|
||||
const config = cloneDeep(configState.value)
|
||||
config[key] = value
|
||||
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, setConfig }
|
||||
}
|
@ -80,4 +80,9 @@ export default defineComponent({
|
||||
.tab-bar__container {
|
||||
width: 100%;
|
||||
}
|
||||
.mdc-tab__icon {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
</style>
|
||||
|
20
yarn.lock
20
yarn.lock
@ -2788,6 +2788,13 @@ dom-serializer@^1.0.1:
|
||||
domhandler "^4.2.0"
|
||||
entities "^2.0.0"
|
||||
|
||||
dom7@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npm.taobao.org/dom7/download/dom7-3.0.0.tgz#b861ce5d67a6becd7aaa3ad02942ff14b1240331"
|
||||
integrity sha1-uGHOXWemvs16qjrQKUL/FLEkAzE=
|
||||
dependencies:
|
||||
ssr-window "^3.0.0-alpha.1"
|
||||
|
||||
domelementtype@1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.npm.taobao.org/domelementtype/download/domelementtype-1.3.1.tgz?cache=0&sync_timestamp=1617298554829&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdomelementtype%2Fdownload%2Fdomelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
|
||||
@ -6235,6 +6242,11 @@ sprintf-js@~1.0.2:
|
||||
resolved "https://registry.npm.taobao.org/sprintf-js/download/sprintf-js-1.0.3.tgz"
|
||||
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
||||
|
||||
ssr-window@^3.0.0, ssr-window@^3.0.0-alpha.1:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npm.taobao.org/ssr-window/download/ssr-window-3.0.0.tgz#fd5b82801638943e0cc704c4691801435af7ac37"
|
||||
integrity sha1-/VuCgBY4lD4MxwTEaRgBQ1r3rDc=
|
||||
|
||||
stable@^0.1.8:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.npm.taobao.org/stable/download/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
|
||||
@ -6438,6 +6450,14 @@ svgo@^1.3.2:
|
||||
unquote "~1.1.1"
|
||||
util.promisify "~1.0.0"
|
||||
|
||||
swiper@^6.7.5:
|
||||
version "6.7.5"
|
||||
resolved "https://registry.nlark.com/swiper/download/swiper-6.7.5.tgz?cache=0&sync_timestamp=1625145077063&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fswiper%2Fdownload%2Fswiper-6.7.5.tgz#8f150c7281919b7d6bea00889e9dc16448e92986"
|
||||
integrity sha1-jxUMcoGRm31r6gCInp3BZEjpKYY=
|
||||
dependencies:
|
||||
dom7 "^3.0.0"
|
||||
ssr-window "^3.0.0"
|
||||
|
||||
symbol-tree@^3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.npm.taobao.org/symbol-tree/download/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
|
Loading…
Reference in New Issue
Block a user