Add vue admin panel

This commit is contained in:
mashirozx 2021-07-14 16:54:49 +08:00
parent 64bc72864b
commit 4efaa8413a
18 changed files with 321 additions and 76 deletions

View File

@ -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)
// );
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -80,4 +80,9 @@ export default defineComponent({
.tab-bar__container {
width: 100%;
}
.mdc-tab__icon {
font-size: 14px;
width: 14px;
height: 14px;
}
</style>

View File

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