fix: 统一系统化待办跨设备布局 #695
@ -29,6 +29,20 @@ class SystemizationTodos extends Controller
|
||||
'none' => '无提醒',
|
||||
];
|
||||
private const OWNER_OPTIONS = ['人参', '晨曦', '常仪', '待定'];
|
||||
private const COLUMN_WIDTHS_SETTING = 'business.systemization_todos.column_widths';
|
||||
private const COLUMN_WIDTH_KEYS = [
|
||||
'type',
|
||||
'section',
|
||||
'sequence',
|
||||
'title',
|
||||
'owner',
|
||||
'assignee',
|
||||
'start_date',
|
||||
'due_date',
|
||||
'reminder',
|
||||
'status',
|
||||
'progress_notes',
|
||||
];
|
||||
private const OWNER_ALIASES = [
|
||||
'人参' => '人参',
|
||||
'苏木' => '人参',
|
||||
@ -205,6 +219,7 @@ class SystemizationTodos extends Controller
|
||||
$filters,
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
|
||||
)),
|
||||
'todoColumnWidths' => $this->columnWidths(),
|
||||
'todoSummary' => [
|
||||
'total' => $totalCount,
|
||||
'incomplete' => $incompleteCount,
|
||||
@ -224,10 +239,57 @@ class SystemizationTodos extends Controller
|
||||
'import' => route('business.systemization-todos.import'),
|
||||
'history' => route('business.systemization-todos.history'),
|
||||
'historyExport' => route('business.systemization-todos.history.export'),
|
||||
'columnWidths' => route('business.systemization-todos.column-widths'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function saveColumnWidths(Request $request): JsonResponse
|
||||
{
|
||||
$ruleKeys = implode(',', self::COLUMN_WIDTH_KEYS);
|
||||
$data = $request->validate([
|
||||
'widths' => ['required', 'array:' . $ruleKeys],
|
||||
'widths.*' => ['required', 'integer', 'min:64', 'max:720'],
|
||||
]);
|
||||
$widths = [];
|
||||
foreach (self::COLUMN_WIDTH_KEYS as $key) {
|
||||
$widths[$key] = (int) $data['widths'][$key];
|
||||
}
|
||||
|
||||
setting([
|
||||
self::COLUMN_WIDTHS_SETTING => json_encode(
|
||||
$widths,
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
|
||||
),
|
||||
])->save();
|
||||
|
||||
return response()->json(['ok' => true, 'widths' => $widths]);
|
||||
}
|
||||
|
||||
private function columnWidths(): ?array
|
||||
{
|
||||
$encoded = setting(self::COLUMN_WIDTHS_SETTING);
|
||||
$widths = is_string($encoded) ? json_decode($encoded, true) : $encoded;
|
||||
if (! is_array($widths)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = [];
|
||||
foreach (self::COLUMN_WIDTH_KEYS as $key) {
|
||||
$width = $widths[$key] ?? null;
|
||||
if (! is_int($width) && ! ctype_digit((string) $width)) {
|
||||
return null;
|
||||
}
|
||||
$width = (int) $width;
|
||||
if ($width < 64 || $width > 720) {
|
||||
return null;
|
||||
}
|
||||
$normalized[$key] = $width;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private function hierarchySequenceMap(): array
|
||||
{
|
||||
$todos = SystemizationTodo::query()
|
||||
|
||||
BIN
public/fonts/caiwu-ui/DroidSansFallback.ttf
Normal file
BIN
public/fonts/caiwu-ui/DroidSansFallback.ttf
Normal file
Binary file not shown.
10
resources/assets/sass/app.css
vendored
10
resources/assets/sass/app.css
vendored
@ -41,6 +41,14 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "CaiWu CJK";
|
||||
src: url("/public/fonts/caiwu-ui/DroidSansFallback.ttf") format("truetype");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@ -1063,7 +1071,7 @@ form {
|
||||
|
||||
body {
|
||||
@apply text-black;
|
||||
font-family: "JetBrains Mono", "Microsoft YaHei", "PingFang SC", monospace;
|
||||
font-family: "JetBrains Mono", "CaiWu CJK", monospace;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
@ -19,11 +19,27 @@
|
||||
.sys-todo-summary__value { color:#171717; font-size:1rem; font-weight:700; font-variant-numeric:tabular-nums; }
|
||||
.sys-todo-summary__item.is-warning .sys-todo-summary__value { color:#935f07; }
|
||||
.sys-todo-summary__item.is-success .sys-todo-summary__value { color:#53793e; }
|
||||
.sys-todo-filter { display:flex; flex-wrap:wrap; gap:.65rem .55rem; align-items:end; margin:0 0 .85rem; padding:.7rem; border:1px solid #e6dfd2; border-radius:.65rem; background:#fcfaf5; }
|
||||
.sys-todo-filter label { display:grid; flex:1 1 7.4rem; gap:.25rem; min-width:0; color:#595959; font-size:.72rem; }
|
||||
.sys-todo-filter label.sys-todo-filter__keyword { flex:2.2 1 16rem; }
|
||||
.sys-todo-filter { display:grid; grid-template-columns:repeat(6,minmax(8.5rem,1fr)); gap:.65rem .55rem; align-items:end; margin:0 0 .85rem; padding:.7rem; border:1px solid #e6dfd2; border-radius:.65rem; background:#fcfaf5; }
|
||||
.sys-todo-filter > label { display:grid; grid-column:span 1; gap:.25rem; min-width:0; color:#595959; font-size:.72rem; }
|
||||
.sys-todo-filter > label.sys-todo-filter__keyword { grid-column:span 3; }
|
||||
.sys-todo-filter input, .sys-todo-filter select { width:100%; height:2.125rem; padding:0 .6rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fff; color:#171717; font:inherit; font-size:.8125rem; }
|
||||
.sys-todo-filter__multi { display:grid; flex:1 1 8.2rem; gap:.25rem; min-width:0; color:#595959; font-size:.72rem; }
|
||||
.sys-todo-filter > label input,
|
||||
.sys-todo-filter > label select { -webkit-appearance:none; appearance:none; }
|
||||
.sys-todo-page select:not([multiple]) {
|
||||
padding-right:2rem;
|
||||
background-color:#fff;
|
||||
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%23856632' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-repeat:no-repeat;
|
||||
background-position:right .7rem center;
|
||||
background-size:.7rem .47rem;
|
||||
}
|
||||
.sys-todo-page select:not([multiple])::-ms-expand { display:none; }
|
||||
.sys-todo-page select:focus-visible,
|
||||
.sys-todo-filter input:focus-visible,
|
||||
.sys-todo-field input:focus-visible,
|
||||
.sys-todo-field textarea:focus-visible { border-color:var(--sys-gold); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.18); }
|
||||
.sys-todo-page input[type="search"]::-webkit-search-cancel-button { -webkit-appearance:none; display:none; }
|
||||
.sys-todo-filter__multi { display:grid; grid-column:span 1; gap:.25rem; min-width:0; color:#595959; font-size:.72rem; }
|
||||
.sys-todo-filter__multi details { position:relative; min-width:0; }
|
||||
.sys-todo-filter__multi > details > summary { position:relative; display:flex; align-items:center; min-height:2.125rem; padding:0 1.8rem 0 .6rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fff; color:#171717; cursor:pointer; list-style:none; }
|
||||
.sys-todo-filter__multi > details > summary::-webkit-details-marker { display:none; }
|
||||
@ -37,7 +53,7 @@
|
||||
.sys-todo-filter__multi-menu label:hover { background:var(--sys-gold-soft); }
|
||||
.sys-todo-filter__multi-menu input[type="checkbox"] { flex:0 0 auto; width:1rem; height:1rem; padding:0; accent-color:var(--sys-gold); }
|
||||
.sys-todo-filter__multi-empty { padding:.65rem; color:#777; font-size:.75rem; text-align:center; }
|
||||
.sys-todo-filter__actions { display:flex; flex:0 0 auto; gap:.45rem; }
|
||||
.sys-todo-filter__actions { display:flex; grid-column:span 2; justify-content:flex-end; gap:.45rem; }
|
||||
.sys-todo-table-wrap { overflow-x:auto; overflow-y:visible; border:1px solid var(--sys-border); border-radius:.65rem; background:#fff; scrollbar-width:none; }
|
||||
.sys-todo-table-wrap::-webkit-scrollbar { height:0; }
|
||||
.sys-todo-table { width:108rem; min-width:108rem; table-layout:fixed; border-collapse:separate; border-spacing:0; font-size:.75rem; }
|
||||
@ -152,6 +168,31 @@
|
||||
.sys-todo-owner-checks label { display:flex; align-items:center; gap:.35rem; min-height:1.6rem; color:#333; font-size:.76rem; cursor:pointer; }
|
||||
.sys-todo-owner-checks label:hover { color:var(--sys-gold); }
|
||||
.sys-todo-owner-checks input[type="checkbox"] { flex:0 0 auto; width:1rem; height:1rem; min-height:0; padding:0; accent-color:var(--sys-gold); }
|
||||
.sys-todo-page input[type="checkbox"] {
|
||||
-webkit-appearance:none;
|
||||
appearance:none;
|
||||
display:inline-grid;
|
||||
flex:0 0 1rem;
|
||||
place-items:center;
|
||||
width:1rem;
|
||||
height:1rem;
|
||||
min-height:0;
|
||||
padding:0;
|
||||
border:1px solid #bfb3a0;
|
||||
border-radius:.24rem;
|
||||
background-color:#fff;
|
||||
background-image:none;
|
||||
cursor:pointer;
|
||||
}
|
||||
.sys-todo-page input[type="checkbox"]:checked {
|
||||
border-color:var(--sys-gold);
|
||||
background-color:var(--sys-gold);
|
||||
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.2 6.4 4.8 9 9.8 3.3' fill='none' stroke='%23FFFFFF' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-repeat:no-repeat;
|
||||
background-position:center;
|
||||
background-size:.72rem;
|
||||
}
|
||||
.sys-todo-page input[type="checkbox"]:focus-visible { outline:2px solid var(--sys-gold); outline-offset:2px; }
|
||||
.sys-todo-upload-zone { display:flex; flex-wrap:wrap; align-items:center; gap:.45rem .65rem; min-height:4.5rem; padding:.7rem; border:1px dashed #cfc3ad; border-radius:.55rem; background:#fcfaf5; }
|
||||
.sys-todo-upload-zone input[type="file"] { position:absolute; width:1px; height:1px; padding:0; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
|
||||
.sys-todo-upload-button { display:inline-flex; align-items:center; justify-content:center; min-height:2rem; padding:0 .75rem; border:1px solid var(--sys-gold); border-radius:.4rem; background:var(--sys-gold); color:#fff; font-size:.78rem; cursor:pointer; }
|
||||
@ -239,9 +280,22 @@
|
||||
.sys-todo-image-dialog__body button[hidden] { visibility:hidden; }
|
||||
.sys-todo-image-dialog__foot { display:flex; justify-content:flex-end; padding:.55rem .85rem; background:#1d1d1f; }
|
||||
.sys-todo-image-dialog__foot a { color:#f0e4bf; font-size:.75rem; }
|
||||
@media (max-width:1300px) { .sys-todo-summary { grid-template-columns:repeat(2,minmax(0,1fr)); } }
|
||||
@media (max-width:900px) { .sys-todo-filter label, .sys-todo-filter label.sys-todo-filter__keyword { flex-basis:9rem; } .sys-todo-filter__actions { width:100%; } .sys-todo-filter__actions .sys-todo-page__button { flex:1; } }
|
||||
@media (max-width:720px) { .sys-todo-summary { grid-template-columns:repeat(2,minmax(0,1fr)); } .sys-todo-filter label, .sys-todo-filter label.sys-todo-filter__keyword { flex-basis:10rem; } .sys-todo-field--2,.sys-todo-field--3,.sys-todo-field--4,.sys-todo-field--6 { grid-column:span 12; } .sys-todo-combined-grid { grid-column:span 12; grid-template-columns:1fr; } }
|
||||
@media (max-width:1100px) {
|
||||
.sys-todo-summary { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
||||
.sys-todo-filter { grid-template-columns:repeat(3,minmax(8.5rem,1fr)); }
|
||||
.sys-todo-filter > label.sys-todo-filter__keyword { grid-column:span 2; }
|
||||
.sys-todo-filter__actions { grid-column:span 3; }
|
||||
}
|
||||
@media (max-width:720px) {
|
||||
.sys-todo-filter { grid-template-columns:minmax(0,1fr); }
|
||||
.sys-todo-filter > label,
|
||||
.sys-todo-filter > label.sys-todo-filter__keyword,
|
||||
.sys-todo-filter__multi,
|
||||
.sys-todo-filter__actions { grid-column:1 / -1; }
|
||||
.sys-todo-filter__actions .sys-todo-page__button { flex:1; }
|
||||
.sys-todo-field--2,.sys-todo-field--3,.sys-todo-field--4,.sys-todo-field--6 { grid-column:span 12; }
|
||||
.sys-todo-combined-grid { grid-column:span 12; grid-template-columns:1fr; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="biz-card-head">
|
||||
@ -961,6 +1015,8 @@
|
||||
var routes = @json($todoRoutes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
var positionGroups = @json($todoPositionGroups, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
var contentOptions = @json($todoContentOptions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
var serverColumnWidths = @json($todoColumnWidths, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
var csrfToken = @json(csrf_token());
|
||||
var currentTodoId = null;
|
||||
var currentGroupTodoId = null;
|
||||
var currentTodoTitle = '';
|
||||
@ -979,7 +1035,8 @@
|
||||
var scrollRail = document.querySelector('[data-sys-todo-scrollrail]');
|
||||
var scrollRailSpacer = document.querySelector('[data-sys-todo-scrollrail-spacer]');
|
||||
var scrollRailFrame = null;
|
||||
var columnStorageKey = 'caiwu-systemization-todo-column-widths-v1';
|
||||
var columnStorageKey = 'caiwu-systemization-todo-column-widths-v2';
|
||||
var columnWidthSaveTimer = null;
|
||||
var defaultColumnWidths = todoColumns.map(function (column) {
|
||||
return Number(column.getAttribute('data-default-width') || 100);
|
||||
});
|
||||
@ -1137,31 +1194,72 @@
|
||||
todoTable.style.minWidth = total + 'px';
|
||||
todoTable.style.setProperty('--sys-todo-type-width', columnWidths[0] + 'px');
|
||||
if (persist) {
|
||||
try {
|
||||
window.localStorage.setItem(columnStorageKey, JSON.stringify(columnWidths));
|
||||
} catch (error) {
|
||||
// Column resizing still works when browser storage is unavailable.
|
||||
}
|
||||
persistColumnWidths(columnWidths);
|
||||
}
|
||||
scheduleScrollRailSync();
|
||||
}
|
||||
|
||||
function columnWidthsFromPreference(preference) {
|
||||
if (!preference) return null;
|
||||
var values = Array.isArray(preference)
|
||||
? preference
|
||||
: todoColumns.map(function (column) {
|
||||
return preference[column.getAttribute('data-sys-todo-col')];
|
||||
});
|
||||
if (values.length !== todoColumns.length
|
||||
|| values.some(function (value) { return value === null || value === undefined || value === ''; })) {
|
||||
return null;
|
||||
}
|
||||
return values.map(Number);
|
||||
}
|
||||
|
||||
function persistColumnWidths(widths) {
|
||||
try {
|
||||
window.localStorage.setItem(columnStorageKey, JSON.stringify(widths));
|
||||
} catch (error) {
|
||||
// The shared server preference remains available when local storage is blocked.
|
||||
}
|
||||
if (columnWidthSaveTimer !== null) {
|
||||
window.clearTimeout(columnWidthSaveTimer);
|
||||
}
|
||||
columnWidthSaveTimer = window.setTimeout(function () {
|
||||
columnWidthSaveTimer = null;
|
||||
var payload = {};
|
||||
todoColumns.forEach(function (column, index) {
|
||||
payload[column.getAttribute('data-sys-todo-col')] = widths[index];
|
||||
});
|
||||
window.fetch(routes.columnWidths, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ widths: payload })
|
||||
}).catch(function () {
|
||||
// Keep the current browser usable; the next resize retries the shared save.
|
||||
});
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function resetColumnWidth(index) {
|
||||
var widths = columnWidths.slice();
|
||||
widths[index] = defaultColumnWidths[index];
|
||||
applyColumnWidths(widths, true);
|
||||
}
|
||||
|
||||
try {
|
||||
var storedColumnWidths = JSON.parse(window.localStorage.getItem(columnStorageKey) || 'null');
|
||||
if (Array.isArray(storedColumnWidths) && storedColumnWidths.length === todoColumns.length) {
|
||||
applyColumnWidths(storedColumnWidths, false);
|
||||
} else {
|
||||
applyColumnWidths(defaultColumnWidths, false);
|
||||
var initialColumnWidths = columnWidthsFromPreference(serverColumnWidths);
|
||||
if (initialColumnWidths === null) {
|
||||
try {
|
||||
initialColumnWidths = columnWidthsFromPreference(
|
||||
JSON.parse(window.localStorage.getItem(columnStorageKey) || 'null')
|
||||
);
|
||||
} catch (error) {
|
||||
initialColumnWidths = null;
|
||||
}
|
||||
} catch (error) {
|
||||
applyColumnWidths(defaultColumnWidths, false);
|
||||
}
|
||||
applyColumnWidths(initialColumnWidths || defaultColumnWidths, false);
|
||||
|
||||
todoColumnResizers.forEach(function (handle) {
|
||||
var columnName = handle.getAttribute('data-sys-todo-col-resizer');
|
||||
|
||||
@ -789,6 +789,8 @@ Route::prefix('workspace/other/systemization-todos')
|
||||
Route::post('import', [SystemizationTodos::class, 'importWorkbook'])
|
||||
->middleware('import')
|
||||
->name('import');
|
||||
Route::post('column-widths', [SystemizationTodos::class, 'saveColumnWidths'])
|
||||
->name('column-widths');
|
||||
Route::post('/', [SystemizationTodos::class, 'store'])->name('store');
|
||||
Route::get('history', [SystemizationTodos::class, 'history'])->name('history');
|
||||
Route::patch('{todo}/category', [SystemizationTodos::class, 'updateCategory'])
|
||||
|
||||
@ -175,6 +175,52 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
$this->assertSoftDeleted('business_systemization_todos', ['id' => $todo->id]);
|
||||
}
|
||||
|
||||
public function test_column_widths_are_saved_as_a_shared_page_preference(): void
|
||||
{
|
||||
$this->loginAs();
|
||||
|
||||
$widths = [
|
||||
'type' => 96,
|
||||
'section' => 228,
|
||||
'sequence' => 98,
|
||||
'title' => 360,
|
||||
'owner' => 112,
|
||||
'assignee' => 116,
|
||||
'start_date' => 120,
|
||||
'due_date' => 120,
|
||||
'reminder' => 118,
|
||||
'status' => 96,
|
||||
'progress_notes' => 280,
|
||||
];
|
||||
|
||||
$this->postJson(route('business.systemization-todos.column-widths'), [
|
||||
'widths' => $widths,
|
||||
])->assertOk()
|
||||
->assertJson([
|
||||
'ok' => true,
|
||||
'widths' => $widths,
|
||||
]);
|
||||
|
||||
$response = $this->get(route('business.pages.show', [
|
||||
'section' => 'other',
|
||||
'page' => 'systemization-todos',
|
||||
]))->assertOk();
|
||||
$content = $response->viewData('content');
|
||||
|
||||
$this->assertSame($widths, $content['todoColumnWidths']);
|
||||
$this->assertSame(
|
||||
route('business.systemization-todos.column-widths'),
|
||||
$content['todoRoutes']['columnWidths'],
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
'serverColumnWidths = ' . json_encode(
|
||||
$widths,
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
|
||||
),
|
||||
(string) $response->getContent(),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_long_progress_and_notes_keep_the_standard_table_row_layout(): void
|
||||
{
|
||||
$this->loginAs();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user