feat(admin-ui): 引入 Toast 反馈(Ant Design Pro 风格基线)
This commit is contained in:
@@ -1,8 +1,63 @@
|
|||||||
/*
|
/*
|
||||||
* SaaSShop Admin Components
|
* SaaSShop Admin Components
|
||||||
* 说明:用于承接 Blade 中零散的 inline style,便于后续统一美化与治理。
|
* 说明:用于承接 Blade 中零散的 inline style,便于后续统一美化与治理。
|
||||||
|
*
|
||||||
|
* 美化方向:Ant Design Pro 风格(规整、企业中后台范式)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* Toast(渐进增强:配合 admin.js 展示更友好的反馈提示) */
|
||||||
|
.toast-container{
|
||||||
|
position:fixed;
|
||||||
|
top:16px;
|
||||||
|
right:16px;
|
||||||
|
z-index:999;
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:10px;
|
||||||
|
width:min(420px, calc(100vw - 32px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast{
|
||||||
|
background:#111827;
|
||||||
|
border:1px solid #334155;
|
||||||
|
border-left-width:4px;
|
||||||
|
border-radius:12px;
|
||||||
|
padding:12px 12px;
|
||||||
|
color:#e5e7eb;
|
||||||
|
box-shadow:0 14px 30px rgba(0,0,0,.35);
|
||||||
|
display:flex;
|
||||||
|
align-items:flex-start;
|
||||||
|
justify-content:space-between;
|
||||||
|
gap:12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-success{border-left-color:#16a34a;}
|
||||||
|
.toast-warning{border-left-color:#f59e0b;}
|
||||||
|
.toast-error{border-left-color:#ef4444;}
|
||||||
|
|
||||||
|
.toast-content{
|
||||||
|
font-size:13px;
|
||||||
|
line-height:1.45;
|
||||||
|
word-break:break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close{
|
||||||
|
appearance:none;
|
||||||
|
border:none;
|
||||||
|
background:transparent;
|
||||||
|
color:#94a3b8;
|
||||||
|
font-size:18px;
|
||||||
|
line-height:1;
|
||||||
|
cursor:pointer;
|
||||||
|
padding:2px 6px;
|
||||||
|
border-radius:8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close:hover{
|
||||||
|
background:#1f2937;
|
||||||
|
color:#e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
.form-inline-row{
|
.form-inline-row{
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
|
|||||||
@@ -43,6 +43,79 @@
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// 通用:将后端 flash 信息同步到 toast(更像 Ant Design Pro 的反馈方式)
|
||||||
|
// 说明:渐进增强。页面仍保留原本的提示块,不依赖 JS。
|
||||||
|
(function () {
|
||||||
|
var container = qs('[data-role="toast-container"]');
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var flashNodes = document.querySelectorAll('[data-flash]');
|
||||||
|
if (!flashNodes || flashNodes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createToast(type, text) {
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.className = 'toast toast-' + type;
|
||||||
|
div.setAttribute('role', 'status');
|
||||||
|
|
||||||
|
var content = document.createElement('div');
|
||||||
|
content.className = 'toast-content';
|
||||||
|
content.textContent = text;
|
||||||
|
|
||||||
|
var close = document.createElement('button');
|
||||||
|
close.type = 'button';
|
||||||
|
close.className = 'toast-close';
|
||||||
|
close.textContent = '×';
|
||||||
|
close.addEventListener('click', function () {
|
||||||
|
try {
|
||||||
|
container.removeChild(div);
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
div.appendChild(content);
|
||||||
|
div.appendChild(close);
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
flashNodes.forEach(function (node) {
|
||||||
|
var type = node.getAttribute('data-flash') || 'info';
|
||||||
|
var text = (node.textContent || '').trim();
|
||||||
|
if (!text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(createToast(type, text));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自动消失(success/warning),error 保留更久
|
||||||
|
setTimeout(function () {
|
||||||
|
try {
|
||||||
|
var toasts = container.querySelectorAll('.toast');
|
||||||
|
toasts.forEach(function (t) {
|
||||||
|
var cls = t.className || '';
|
||||||
|
var isError = cls.indexOf('toast-error') >= 0;
|
||||||
|
if (!isError) {
|
||||||
|
container.removeChild(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
}, 4500);
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
try {
|
||||||
|
var toasts = container.querySelectorAll('.toast');
|
||||||
|
toasts.forEach(function (t) {
|
||||||
|
try {
|
||||||
|
container.removeChild(t);
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
}, 9000);
|
||||||
|
})();
|
||||||
|
|
||||||
// 续费缺订阅治理:订单详情页“绑定订阅ID”输入框,小交互增强:
|
// 续费缺订阅治理:订单详情页“绑定订阅ID”输入框,小交互增强:
|
||||||
// - 输入后按 Enter 直接提交
|
// - 输入后按 Enter 直接提交
|
||||||
// - 自动聚焦,减少点击
|
// - 自动聚焦,减少点击
|
||||||
|
|||||||
@@ -69,6 +69,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main class="content">
|
<main class="content">
|
||||||
|
<div class="toast-container" data-role="toast-container" aria-live="polite" aria-relevant="additions" aria-atomic="true"></div>
|
||||||
|
|
||||||
<div class="top">
|
<div class="top">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="page-title">@yield('page_title', '总台管理')</h1>
|
<h1 class="page-title">@yield('page_title', '总台管理')</h1>
|
||||||
@@ -77,13 +79,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if(session('success'))
|
@if(session('success'))
|
||||||
<div class="flash">{{ session('success') }}</div>
|
<div class="flash" data-flash="success">{{ session('success') }}</div>
|
||||||
@endif
|
@endif
|
||||||
@if(session('warning'))
|
@if(session('warning'))
|
||||||
<div class="warning">{{ session('warning') }}</div>
|
<div class="warning" data-flash="warning">{{ session('warning') }}</div>
|
||||||
@endif
|
@endif
|
||||||
@if(session('error'))
|
@if(session('error'))
|
||||||
<div class="error-box">{{ session('error') }}</div>
|
<div class="error-box" data-flash="error">{{ session('error') }}</div>
|
||||||
@endif
|
@endif
|
||||||
@if($errors->any())
|
@if($errors->any())
|
||||||
<div class="error-box">
|
<div class="error-box">
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AdminComponentsCssToastStylesShouldExistTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function loginAsPlatformAdmin(): void
|
||||||
|
{
|
||||||
|
$this->seed();
|
||||||
|
|
||||||
|
$this->post('/admin/login', [
|
||||||
|
'email' => 'platform.admin@demo.local',
|
||||||
|
'password' => 'Platform@123456',
|
||||||
|
])->assertRedirect('/admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_admin_components_css_should_contain_toast_styles(): void
|
||||||
|
{
|
||||||
|
$this->loginAsPlatformAdmin();
|
||||||
|
|
||||||
|
$css = file_get_contents(public_path('css/admin-components.css'));
|
||||||
|
$this->assertIsString($css);
|
||||||
|
|
||||||
|
$this->assertStringContainsString(".toast-container{\n", $css);
|
||||||
|
$this->assertStringContainsString(".toast{\n", $css);
|
||||||
|
$this->assertStringContainsString(".toast-success", $css);
|
||||||
|
$this->assertStringContainsString(".toast-warning", $css);
|
||||||
|
$this->assertStringContainsString(".toast-error", $css);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user