Add new color settings

This commit is contained in:
Pig Fang 2019-12-04 16:45:09 +08:00
parent 8aec8e5028
commit ed9d856c43
22 changed files with 258 additions and 104 deletions

View File

@ -118,7 +118,7 @@ class AdminController extends Controller
return redirect('/admin'); return redirect('/admin');
} }
public function customize(Request $request, \App\Services\Webpack $webpack) public function customize(Request $request)
{ {
$homepage = Option::form('homepage', OptionForm::AUTO_DETECT, function ($form) { $homepage = Option::form('homepage', OptionForm::AUTO_DETECT, function ($form) {
$form->text('home_pic_url')->hint(); $form->text('home_pic_url')->hint();
@ -150,18 +150,40 @@ class AdminController extends Controller
$form->textarea('custom_js', 'JavaScript')->rows(6); $form->textarea('custom_js', 'JavaScript')->rows(6);
})->addMessage()->handle(); })->addMessage()->handle();
if ($request->isMethod('post') && $request->input('action') == 'color') { if ($request->isMethod('post') && $request->input('action') === 'color') {
$color = $this->validate($request, ['color' => 'required'])['color']; $navbar = $request->input('navbar');
option(['color_scheme' => $color]); if ($navbar) {
option(['navbar_color' => $navbar]);
}
$sidebar = $request->input('sidebar');
if ($sidebar) {
option(['sidebar_color' => $sidebar]);
}
} }
return view('admin.customize', [ return view('admin.customize', [
'colors' => ['blue', 'yellow', 'green', 'purple', 'red', 'black'], 'colors' => [
'skins_css' => $webpack->url('skins/_all-skins.min.css'), 'navbar' => [
'primary', 'secondary', 'success', 'danger', 'indigo',
'purple', 'pink', 'teal', 'cyan', 'dark', 'gray',
'fuchsia', 'maroon', 'olive', 'navy',
'lime', 'light', 'warning', 'white', 'orange',
],
'sidebar' => [
'primary', 'warning', 'info', 'danger', 'success', 'indigo',
'navy', 'purple', 'fuchsia', 'pink', 'maroon', 'orange',
'lime', 'teal', 'olive',
],
],
'forms' => [ 'forms' => [
'homepage' => $homepage, 'homepage' => $homepage,
'custom_js_css' => $customJsCss, 'custom_js_css' => $customJsCss,
], ],
'extra' => [
'navbar' => option('navbar_color'),
'sidebar' => option('sidebar_color'),
],
]); ]);
} }

View File

@ -41,15 +41,25 @@ class HeadComposer
public function applyThemeColor(View $view) public function applyThemeColor(View $view)
{ {
$colors = [ $colors = [
'blue' => '#3c8dbc', 'primary' => '#007bff',
'yellow' => '#f39c12', 'secondary' => '#6c757d',
'green' => '#00a65a', 'success' => '#28a745',
'purple' => '#605ca8', 'warning' => '#ffc107',
'red' => '#dd4b39', 'danger' => '#dc3545',
'black' => '#ffffff', 'navy' => '#001f3f',
'olive' => '#3d9970',
'lime' => '#01ff70',
'fuchsia' => '#f012be',
'maroon' => '#d81b60',
'indigo' => '#6610f2',
'purple' => '#6f42c1',
'pink' => '#e83e8c',
'orange' => '#fd7e14',
'teal' => '#20c997',
'cyan' => '#17a2b8',
'gray' => '#6c757d',
]; ];
preg_match('/skin-(\w+)?(?:-light)?/', option('color_scheme'), $matches); $view->with('theme_color', Arr::get($colors, option('navbar_color')));
$view->with('theme_color', Arr::get($colors, $matches[1]));
} }
public function seo(View $view) public function seo(View $view)

View File

@ -53,6 +53,7 @@ class SideMenuComposer
$classes = []; $classes = [];
if ($isActive) { if ($isActive) {
$item['active'] = true;
$classes[] = 'active menu-open'; $classes[] = 'active menu-open';
} }

View File

@ -12,9 +12,12 @@ class ViewServiceProvider extends ServiceProvider
public function boot(Webpack $webpack) public function boot(Webpack $webpack)
{ {
View::composer(['home', '*.base', 'shared.header'], function ($view) { View::composer(['home', '*.base', 'shared.header'], function ($view) {
$lightColors = ['light', 'warning', 'white', 'orange'];
$color = option('navbar_color');
$view->with([ $view->with([
'site_name' => option_localized('site_name'), 'site_name' => option_localized('site_name'),
'color_scheme' => option('color_scheme'), 'navbar_color' => $color,
'color_mode' => in_array($color, $lightColors) ? 'light' : 'dark',
]); ]);
}); });
@ -32,6 +35,10 @@ class ViewServiceProvider extends ServiceProvider
View::composer('shared.user-menu', Composers\UserMenuComposer::class); View::composer('shared.user-menu', Composers\UserMenuComposer::class);
View::composer('shared.sidebar', function ($view) {
$view->with('sidebar_color', option('sidebar_color'));
});
View::composer('shared.side-menu', Composers\SideMenuComposer::class); View::composer('shared.side-menu', Composers\SideMenuComposer::class);
View::composer('shared.user-panel', Composers\UserPanelComposer::class); View::composer('shared.user-panel', Composers\UserPanelComposer::class);

View File

@ -11,7 +11,6 @@ return [
'ip_get_method' => '0', 'ip_get_method' => '0',
'api_type' => 'false', 'api_type' => 'false',
'announcement' => 'Welcome to Blessing Skin {version}!', 'announcement' => 'Welcome to Blessing Skin {version}!',
'color_scheme' => 'skin-blue',
'home_pic_url' => './app/bg.png', 'home_pic_url' => './app/bg.png',
'custom_css' => '', 'custom_css' => '',
'custom_js' => '', 'custom_js' => '',
@ -58,4 +57,6 @@ return [
'content_policy' => '', 'content_policy' => '',
'transparent_navbar' => 'false', 'transparent_navbar' => 'false',
'status_code_for_private' => '403', 'status_code_for_private' => '403',
'navbar_color' => 'cyan',
'sidebar_color' => 'dark-maroon',
]; ];

View File

@ -23,6 +23,7 @@
"admin-lte": "^3.0.1", "admin-lte": "^3.0.1",
"echarts": "^4.5.0", "echarts": "^4.5.0",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"rxjs": "^6.5.3",
"skinview3d": "^1.1.0", "skinview3d": "^1.1.0",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-good-table": "^2.18.1", "vue-good-table": "^2.18.1",
@ -89,7 +90,7 @@
"printWidth": 80, "printWidth": 80,
"semi": false, "semi": false,
"singleQuote": true, "singleQuote": true,
"trailingComma": "es5", "trailingComma": "all",
"arrowParens": "avoid", "arrowParens": "avoid",
"tabWidth": 2 "tabWidth": 2
}, },

View File

@ -5,3 +5,8 @@
#chart #chart
width 100% width 100%
height 400px height 400px
.btn-color
width 40px
height 20px
cursor pointer

View File

@ -10,25 +10,16 @@ body
.navbar-brand .navbar-brand
font-family Minecraft, 'Segoe UI', 'Microsoft Yahei', 'Microsoft Jhenghei', sans-serif font-family Minecraft, 'Segoe UI', 'Microsoft Yahei', 'Microsoft Jhenghei', sans-serif
border-right 0
.navbar .navbar
transition color 0.25s ease-in-out, border-color 0.25s ease-in-out, background-color 0.25s ease-in-out transition color 0.25s ease-in-out, border-color 0.25s ease-in-out, background-color 0.25s ease-in-out
.transparent .transparent
.navbar-brand, .navbar-header > a .navbar-brand
color #5e5e5e color #5e5e5e
&.navbar &.navbar
background-color rgba(255, 255, 255, 0.2) background-color rgba(255, 255, 255, 0.2)
.navbar-nav li a
border 0
color #5e5e5e
&:hover
background rgba(0, 0, 0, 0.2)
color #f6f6f6
.splash .splash
width 80% width 80%
height 50% height 50%

View File

@ -1,16 +1,83 @@
function handler(event: Event): void { /* eslint-disable object-curly-newline */
document.body.className = import { fromEvent, merge, of, partition } from 'rxjs'
document.body.className.replace( import { filter, map, pairwise } from 'rxjs/operators'
/skin-\w+(?:-light)?/,
// eslint-disable-next-line no-extra-parens export function registerNavbarPicker(
(event.target as HTMLInputElement).value, navbar: HTMLElement,
picker: HTMLDivElement,
init: string,
): void {
const color$ = fromEvent(picker, 'click')
.pipe(
map(event => (event.target as HTMLElement)),
filter(
(element): element is HTMLInputElement => element.tagName === 'INPUT',
),
map(element => element.value),
) )
merge(of(init), color$)
.pipe(pairwise())
.subscribe(([previous, current]) => {
navbar.classList.replace(`navbar-${previous}`, `navbar-${current}`)
})
const [light$, dark$] = partition(
color$,
color => ['light', 'warning', 'white', 'orange', 'lime'].includes(color),
)
light$.subscribe(() => {
// DO NOT use `classList.replace`.
navbar.classList.remove('navbar-dark')
navbar.classList.add('navbar-light')
})
dark$.subscribe(() => {
// DO NOT use `classList.replace`.
navbar.classList.remove('navbar-light')
navbar.classList.add('navbar-dark')
})
} }
const table = document.querySelector('#change-color') const navbar = document.querySelector<HTMLElement>('.wrapper > nav')
const picker = document.querySelector<HTMLDivElement>('#navbar-color-picker')
/* istanbul ignore next */ /* istanbul ignore next */
if (table) { if (navbar && picker) {
table.addEventListener('change', handler) registerNavbarPicker(navbar, picker, blessing.extra.navbar || 'white')
} }
export default handler export function registerSidebarPicker(
sidebar: HTMLElement,
{ dark, light }: { dark: HTMLDivElement, light: HTMLDivElement },
init: string,
): void {
const color$ = merge(
fromEvent(dark, 'click'),
fromEvent(light, 'click'),
).pipe(
map(event => (event.target as HTMLElement)),
filter(
(element): element is HTMLInputElement => element.tagName === 'INPUT',
),
map(element => element.value),
)
merge(of(init), color$)
.pipe(pairwise())
.subscribe(([previous, current]) => {
sidebar.classList.replace(`sidebar-${previous}`, `sidebar-${current}`)
})
}
const sidebar = document.querySelector<HTMLElement>('.main-sidebar')
const darkPicker = document
.querySelector<HTMLDivElement>('#sidebar-dark-picker')
const lightPicker = document
.querySelector<HTMLDivElement>('#sidebar-light-picker')
/* istanbul ignore next */
if (sidebar && darkPicker && lightPicker) {
registerSidebarPicker(
sidebar,
{ dark: darkPicker, light: lightPicker },
blessing.extra.sidebar || 'dark-primary',
)
}

View File

@ -1,10 +1,52 @@
import handler from '@/views/admin/Customization' import {
registerNavbarPicker,
registerSidebarPicker,
} from '@/views/admin/Customization'
test('preview color', () => { test('preview navbar color', () => {
document.body.classList.add('skin-blue') const nav = document.createElement('nav')
const target = document.createElement('input') nav.className = 'navbar-primary navbar-dark'
target.value = 'skin-purple' const picker = document.createElement('div')
handler({ target } as any as Event) picker.innerHTML = `
<label><input type="radio" name="navbar" value="cyan"></label>
<label><input type="radio" name="navbar" value="orange"></label>
`
const cyan = picker.querySelector<HTMLInputElement>('[value=cyan]')!
const orange = picker.querySelector<HTMLInputElement>('[value=orange]')!
expect(document.body.classList.contains('skin-purple')).toBeTrue() registerNavbarPicker(nav, picker, 'primary')
cyan.click()
expect(nav.className).toContain('navbar-cyan')
expect(nav.className).toContain('navbar-dark')
expect(nav.className).not.toContain('navbar-light')
orange.click()
expect(nav.className).toContain('navbar-orange')
expect(nav.className).not.toContain('navbar-cyan')
expect(nav.className).toContain('navbar-light')
expect(nav.className).not.toContain('navbar-dark')
})
test('preview sidebar color', () => {
const sidebar = document.createElement('aside')
sidebar.className = 'sidebar-dark-primary'
const darkPicker = document.createElement('div')
darkPicker.innerHTML = `
<label><input type="radio" name="sidebar" value="dark-cyan"></label>`
const darkCyan = darkPicker.querySelector<HTMLInputElement>('[value="dark-cyan"]')!
const lightPicker = document.createElement('div')
lightPicker.innerHTML = `
<label><input type="radio" name="sidebar" value="light-cyan"></label>`
const lightCyan = lightPicker.querySelector<HTMLInputElement>('[value="light-cyan"]')!
registerSidebarPicker(
sidebar,
{ dark: darkPicker, light: lightPicker },
'dark-primary',
)
darkCyan.click()
expect(sidebar.className).toContain('sidebar-dark-cyan')
lightCyan.click()
expect(sidebar.className).toContain('sidebar-light-cyan')
}) })

View File

@ -71,18 +71,10 @@ customize:
success: Theme color updated. success: Theme color updated.
colors: colors:
blue: Blue (Default) navbar: Top Navigation Bar
blue-light: Blue Light sidebar:
yellow: Yellow dark: Sidebar (Dark)
yellow-light: Yellow Light light: Sidebar (Light)
green: Green
green-light: Green Light
purple: Purple
purple-light: Purple Light
red: Red
red-light: Red Light
black: Black
black-light: Black Light
i18n: i18n:
add: Add New Language Line add: Add New Language Line

View File

@ -10,6 +10,7 @@
- Added "Status" page. - Added "Status" page.
- Added support of customizing UI text. - Added support of customizing UI text.
- Spanish support (Greatly thanks [@poopingpenis](https://github.com/poopingpenis)) - Spanish support (Greatly thanks [@poopingpenis](https://github.com/poopingpenis))
- Brand new website theme color settings.
## Tweaked ## Tweaked

View File

@ -10,6 +10,7 @@
- 新增「运行状态」页面 - 新增「运行状态」页面
- 支持自定义 UI 文本 - 支持自定义 UI 文本
- 西班牙语支持(感谢 [@poopingpenis](https://github.com/poopingpenis) - 西班牙语支持(感谢 [@poopingpenis](https://github.com/poopingpenis)
- 全新的站点配色设置
## 调整 ## 调整

View File

@ -12,45 +12,46 @@
{{ trans('admin.customize.change-color.title') }} {{ trans('admin.customize.change-color.title') }}
</h3> </h3>
</div> </div>
<div class="card-body p-0"> <div class="card-body">
<table class="table table-striped bring-up nth-2-center" id="change-color"> <div id="navbar-color-picker">
<tbody> <p>{{ trans('admin.customize.colors.navbar') }}</p>
{% for color in colors %} <div>
<tr> {% for color in colors.navbar %}
<td>{{ trans("admin.customize.colors.#{color}") }}</td> <label>
<td> <input type="radio" class="d-none" name="navbar" value="{{ color }}">
<label> <div class="btn-color bg-{{ color }} display-inline-block
<input rounded-pill mr-2 mb-1 elevation-2"
type="radio" ></div>
name="color" </label>
value="skin-{{ color }}"
style="display: none;"
>
<span class="btn bg-{{ color }} btn-xs">
<i class="far fa-eye"></i>
</span>
</label>
</td>
</tr>
<tr>
<td>{{ trans("admin.customize.colors.#{color}-light") }}</td>
<td>
<label>
<input
type="radio"
name="color"
value="skin-{{ color }}-light"
style="display: none;"
>
<span class="btn bg-{{ color }} btn-xs">
<i class="far fa-eye"></i>
</span>
</label>
</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </div>
</table> </div>
<div id="sidebar-dark-picker">
<p>{{ trans('admin.customize.colors.sidebar.dark') }}</p>
<div>
{% for color in colors.sidebar %}
<label>
<input type="radio" class="d-none" name="sidebar" value="dark-{{ color }}">
<div class="btn-color bg-{{ color }} display-inline-block
rounded-pill mr-2 mb-1 elevation-2"
></div>
</label>
{% endfor %}
</div>
</div>
<div id="sidebar-light-picker">
<p>{{ trans('admin.customize.colors.sidebar.light') }}</p>
<div>
{% for color in colors.sidebar %}
<label>
<input type="radio" class="d-none" name="sidebar" value="light-{{ color }}">
<div class="btn-color bg-{{ color }} display-inline-block
rounded-pill mr-2 mb-1 elevation-2"
></div>
</label>
{% endfor %}
</div>
</div>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<input <input
@ -70,5 +71,7 @@
{% endblock %} {% endblock %}
{% block before_foot %} {% block before_foot %}
<link rel="stylesheet" href="{{ skins_css }}"> <script>
blessing.extra = {{ extra|json_encode|raw }}
</script>
{% endblock %} {% endblock %}

View File

@ -20,7 +20,7 @@
<div id="fixed-bg" style="background-image: url('{{ home_pic_url }}')"></div> <div id="fixed-bg" style="background-image: url('{{ home_pic_url }}')"></div>
{% endif %} {% endif %}
<nav class="navbar navbar-expand fixed-top navbar-light navbar-white <nav class="navbar navbar-expand fixed-top navbar-{{ navbar_color }} navbar-{{ color_mode }}
ml-0 {{ transparent_navbar ? 'transparent' }}" ml-0 {{ transparent_navbar ? 'transparent' }}"
> >
<div class="container"> <div class="container">

View File

@ -1,4 +1,4 @@
<nav class="main-header navbar navbar-expand navbar-white navbar-light"> <nav class="main-header navbar navbar-expand navbar-{{ navbar_color }} navbar-{{ color_mode }}">
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#"> <a class="nav-link" data-widget="pushmenu" href="#">

View File

@ -1,6 +1,6 @@
<li class="nav-item {{ item.classes|join(' ') }}"> <li class="nav-item {{ item.classes|join(' ') }}">
{% if item.children %} {% if item.children %}
<a class="nav-link" href="#"> <a class="nav-link {{ item.active ? 'active' }}" href="#">
<i class="nav-icon fas {{ item.icon }}"></i> <i class="nav-icon fas {{ item.icon }}"></i>
<p>{{ trans(item.title) }}</p> <p>{{ trans(item.title) }}</p>
<i class="right fas fa-angle-left"></i> <i class="right fas fa-angle-left"></i>
@ -13,7 +13,7 @@
{% else %} {% else %}
<a <a
href="{{ url(item.link) }}" href="{{ url(item.link) }}"
class="nav-link" class="nav-link {{ item.active ? 'active' }}"
{% if attribute(item, 'new-tab') %} {% if attribute(item, 'new-tab') %}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"

View File

@ -1,4 +1,4 @@
<aside class="main-sidebar sidebar-dark-primary elevation-3"> <aside class="main-sidebar sidebar-{{ sidebar_color }} elevation-3">
<a href="{{ url('/') }}" class="brand-link text-center"> <a href="{{ url('/') }}" class="brand-link text-center">
<span class="brand-text font-weight-light">{{ site_name }}</span> <span class="brand-text font-weight-light">{{ site_name }}</span>
</a> </a>

View File

@ -7,7 +7,7 @@
<body class="hold-transition layout-top-nav"> <body class="hold-transition layout-top-nav">
<div class="wrapper"> <div class="wrapper">
<nav class="main-header navbar navbar-expand navbar-light navbar-white ml-0"> <nav class="main-header navbar navbar-expand navbar-{{ navbar_color }} navbar-{{ color_mode }} ml-0">
<div class="container"> <div class="container">
<div class="navbar-header"> <div class="navbar-header">
<a href="{{ url('/') }}" class="navbar-brand">{{ site_name }}</a> <a href="{{ url('/') }}" class="navbar-brand">{{ site_name }}</a>

View File

@ -27,9 +27,11 @@ class AdminFormsTest extends BrowserKitTestCase
{ {
// Change color // Change color
$this->visit('/admin/customize') $this->visit('/admin/customize')
->select('skin-purple', 'color') ->select('orange', 'navbar')
->select('light-fuchsia', 'sidebar')
->press('submit_color'); ->press('submit_color');
$this->assertEquals('skin-purple', option('color_scheme')); $this->assertEquals('orange', option('navbar_color'));
$this->assertEquals('light-fuchsia', option('sidebar_color'));
$this->visit('/admin/customize') $this->visit('/admin/customize')
->type('url', 'home_pic_url') ->type('url', 'home_pic_url')

View File

@ -31,7 +31,8 @@ class SideMenuComposerTest extends TestCase
$crawler = new Crawler($this->get('/user/oauth/manage')->getContent()); $crawler = new Crawler($this->get('/user/oauth/manage')->getContent());
$this->assertCount(1, $crawler->filter('aside .nav-treeview')); $this->assertCount(1, $crawler->filter('aside .nav-treeview'));
$this->assertCount(2, $crawler->filter('aside .active')); $this->assertCount(2, $crawler->filter('aside .nav-item.active'));
$this->assertCount(2, $crawler->filter('aside .nav-link.active'));
} }
public function testCollectPluginConfigs() public function testCollectPluginConfigs()

View File

@ -8413,6 +8413,13 @@ rxjs@^6.4.0:
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
rxjs@^6.5.3:
version "6.5.3"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a"
integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==
dependencies:
tslib "^1.9.0"
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2" version "5.1.2"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"