feat(courseware): add protected PPTX viewer
This commit is contained in:
@@ -0,0 +1,583 @@
|
||||
<?php
|
||||
$is_player = $selected_deck !== null;
|
||||
$caption_colors = array('#e86a5c', '#2f9e99', '#f0a04b');
|
||||
include('template/' . $OJ_TEMPLATE . '/header.php');
|
||||
?>
|
||||
|
||||
<style>
|
||||
body#MainBg-C.cw-catalog-page {
|
||||
background: #f3eadc !important;
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
to bottom,
|
||||
rgba(120, 90, 50, 0.05) 0,
|
||||
rgba(120, 90, 50, 0.05) 1px,
|
||||
transparent 1px,
|
||||
transparent 32px
|
||||
) !important;
|
||||
}
|
||||
|
||||
body#MainBg-C.cw-player-page {
|
||||
background: #1b2430 !important;
|
||||
}
|
||||
|
||||
#main.ui.main.container.cw-main {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 24px 48px !important;
|
||||
}
|
||||
|
||||
.cw-player-page #main.ui.main.container.cw-main {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.cw-player-page .footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cw-hero {
|
||||
text-align: center;
|
||||
padding: 28px 0 8px;
|
||||
}
|
||||
|
||||
.cw-hero h1 {
|
||||
margin: 0;
|
||||
font-size: 3.4rem;
|
||||
font-weight: 800;
|
||||
color: #e86a5c;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.cw-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 28px;
|
||||
max-width: 1180px;
|
||||
margin: 28px auto 0;
|
||||
}
|
||||
|
||||
.cw-card {
|
||||
display: block;
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
text-decoration: none !important;
|
||||
color: inherit;
|
||||
box-shadow: 0 14px 32px rgba(80, 50, 20, 0.12);
|
||||
transform: translateY(0);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.cw-card:hover,
|
||||
.cw-card:focus {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(80, 50, 20, 0.16);
|
||||
}
|
||||
|
||||
.cw-card-cover {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background:
|
||||
radial-gradient(circle at 20% 20%, rgba(255,255,255,0.35), transparent 42%),
|
||||
linear-gradient(160deg, #f7d3c8, #f6efe4 55%, #d8efe9);
|
||||
}
|
||||
|
||||
.cw-card-meta {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.cw-card-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.cw-empty {
|
||||
max-width: 640px;
|
||||
margin: 40px auto 0;
|
||||
padding: 28px 24px;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border-radius: 18px;
|
||||
color: #4b3b2a;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.cw-theater {
|
||||
min-height: calc(100vh - 77px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 18px 20px 24px;
|
||||
color: #f4f1ea;
|
||||
}
|
||||
|
||||
.cw-back {
|
||||
align-self: flex-start;
|
||||
color: #f4f1ea !important;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cw-stage {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 72px minmax(0, 1fr) 72px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.cw-nav-btn {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
background: #5b6570;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.cw-nav-btn.next {
|
||||
background: #e86a5c;
|
||||
}
|
||||
|
||||
.cw-nav-btn.is-disabled,
|
||||
.cw-nav-btn:disabled {
|
||||
opacity: 0.28;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cw-frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 220px);
|
||||
aspect-ratio: 16 / 9;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cw-frame canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cw-status {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #3b3228;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
background: #fff8ee;
|
||||
}
|
||||
|
||||
.cw-status.is-error {
|
||||
color: #9b2c2c;
|
||||
}
|
||||
|
||||
.cw-status[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cw-pager {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.cw-pager-num {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.cw-pager-num em {
|
||||
font-style: normal;
|
||||
color: #e86a5c;
|
||||
border-bottom: 4px solid #e86a5c;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.cw-dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.cw-dots button {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: #7b8490;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cw-dots button.is-current {
|
||||
background: #e86a5c;
|
||||
}
|
||||
|
||||
.cw-dots button:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.cw-mobile-nav {
|
||||
display: none;
|
||||
justify-content: space-between;
|
||||
padding: 8px 8px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.cw-hero h1 {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
|
||||
.cw-stage {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.cw-frame {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.cw-mobile-nav {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.body.classList.add('<?php echo $is_player ? 'cw-player-page' : 'cw-catalog-page'; ?>');
|
||||
var main = document.getElementById('main');
|
||||
if (main) {
|
||||
main.classList.add('cw-main');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php if ($is_player) { ?>
|
||||
<div class="cw-theater">
|
||||
<a class="cw-back" href="courseware.php"><?php echo htmlspecialchars($MSG_COURSEWARE_BACK, ENT_QUOTES, 'UTF-8'); ?></a>
|
||||
<div class="cw-stage">
|
||||
<button type="button" class="cw-nav-btn desktop-only" id="cw-prev" aria-label="<?php echo htmlspecialchars($MSG_COURSEWARE_PREV, ENT_QUOTES, 'UTF-8'); ?>" disabled aria-disabled="true">‹</button>
|
||||
<div class="cw-frame" id="cw-frame">
|
||||
<canvas id="cw-canvas"></canvas>
|
||||
<div class="cw-status" id="cw-status" role="status" aria-live="polite"><?php echo htmlspecialchars($MSG_COURSEWARE_OPENING, ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
</div>
|
||||
<button type="button" class="cw-nav-btn next desktop-only" id="cw-next" aria-label="<?php echo htmlspecialchars($MSG_COURSEWARE_NEXT, ENT_QUOTES, 'UTF-8'); ?>" disabled aria-disabled="true">›</button>
|
||||
</div>
|
||||
<div class="cw-mobile-nav">
|
||||
<button type="button" class="cw-nav-btn" id="cw-prev-m" aria-label="<?php echo htmlspecialchars($MSG_COURSEWARE_PREV, ENT_QUOTES, 'UTF-8'); ?>" disabled aria-disabled="true">‹</button>
|
||||
<button type="button" class="cw-nav-btn next" id="cw-next-m" aria-label="<?php echo htmlspecialchars($MSG_COURSEWARE_NEXT, ENT_QUOTES, 'UTF-8'); ?>" disabled aria-disabled="true">›</button>
|
||||
</div>
|
||||
<div class="cw-pager">
|
||||
<div class="cw-pager-num" aria-live="polite"><em id="cw-page-now"><?php echo intval($current_page); ?></em> / <span id="cw-page-total"><?php echo htmlspecialchars($MSG_COURSEWARE_PAGE_UNKNOWN, ENT_QUOTES, 'UTF-8'); ?></span></div>
|
||||
<div class="cw-dots" id="cw-dots" role="tablist"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="<?php echo $path_fix; ?>include/pptxviewjs/jszip.min.js"></script>
|
||||
<script src="<?php echo $path_fix; ?>include/pptxviewjs/PptxViewJS.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var slug = <?php echo json_encode($selected_deck['slug'], JSON_UNESCAPED_UNICODE); ?>;
|
||||
var page = <?php echo intval($current_page); ?>;
|
||||
var desiredPage = page;
|
||||
var total = 0;
|
||||
var totalKnown = false;
|
||||
var ready = false;
|
||||
var failed = false;
|
||||
var viewer = null;
|
||||
var navQueue = Promise.resolve();
|
||||
var navBusy = false;
|
||||
var canvas = document.getElementById('cw-canvas');
|
||||
var statusEl = document.getElementById('cw-status');
|
||||
var buttons = {
|
||||
prev: [document.getElementById('cw-prev'), document.getElementById('cw-prev-m')],
|
||||
next: [document.getElementById('cw-next'), document.getElementById('cw-next-m')]
|
||||
};
|
||||
var msg = {
|
||||
unknown: <?php echo json_encode($MSG_COURSEWARE_PAGE_UNKNOWN, JSON_UNESCAPED_UNICODE); ?>,
|
||||
pageN: <?php echo json_encode($MSG_COURSEWARE_PAGE_N, JSON_UNESCAPED_UNICODE); ?>,
|
||||
login: <?php echo json_encode($MSG_COURSEWARE_ERR_LOGIN, JSON_UNESCAPED_UNICODE); ?>,
|
||||
missing: <?php echo json_encode($MSG_COURSEWARE_ERR_NOT_FOUND, JSON_UNESCAPED_UNICODE); ?>,
|
||||
broken: <?php echo json_encode($MSG_COURSEWARE_ERR_BROKEN, JSON_UNESCAPED_UNICODE); ?>,
|
||||
player: <?php echo json_encode($MSG_COURSEWARE_ERR_PLAYER, JSON_UNESCAPED_UNICODE); ?>
|
||||
};
|
||||
|
||||
function setStatus(text, isError) {
|
||||
if (!text) {
|
||||
statusEl.hidden = true;
|
||||
statusEl.removeAttribute('role');
|
||||
statusEl.setAttribute('role', 'status');
|
||||
statusEl.setAttribute('aria-live', 'polite');
|
||||
return;
|
||||
}
|
||||
statusEl.hidden = false;
|
||||
statusEl.textContent = text;
|
||||
statusEl.classList.toggle('is-error', !!isError);
|
||||
statusEl.setAttribute('role', isError ? 'alert' : 'status');
|
||||
statusEl.setAttribute('aria-live', isError ? 'assertive' : 'polite');
|
||||
}
|
||||
|
||||
function setBtnState(list, disabled) {
|
||||
list.forEach(function (btn) {
|
||||
if (!btn) {
|
||||
return;
|
||||
}
|
||||
btn.disabled = !!disabled;
|
||||
btn.setAttribute('aria-disabled', disabled ? 'true' : 'false');
|
||||
btn.classList.toggle('is-disabled', !!disabled);
|
||||
});
|
||||
}
|
||||
|
||||
function clampPage(value) {
|
||||
var next = parseInt(value, 10);
|
||||
if (isNaN(next) || next < 1) {
|
||||
next = 1;
|
||||
}
|
||||
if (totalKnown && total >= 1 && next > total) {
|
||||
next = total;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function shownPage() {
|
||||
if (viewer && ready && !failed) {
|
||||
return viewer.getCurrentSlideIndex() + 1;
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
function updateChrome() {
|
||||
if (viewer && ready) {
|
||||
page = viewer.getCurrentSlideIndex() + 1;
|
||||
total = viewer.getSlideCount() || total;
|
||||
totalKnown = total >= 1;
|
||||
}
|
||||
document.getElementById('cw-page-now').textContent = String(page);
|
||||
document.getElementById('cw-page-total').textContent = totalKnown ? String(total) : msg.unknown;
|
||||
var disableNav = !ready || failed;
|
||||
setBtnState(buttons.prev, disableNav || desiredPage <= 1);
|
||||
setBtnState(buttons.next, disableNav || (totalKnown && desiredPage >= total));
|
||||
var dots = document.getElementById('cw-dots');
|
||||
if (totalKnown && dots.childNodes.length !== total) {
|
||||
dots.innerHTML = '';
|
||||
for (var i = 1; i <= total; i++) {
|
||||
var dot = document.createElement('button');
|
||||
dot.type = 'button';
|
||||
dot.setAttribute('aria-label', msg.pageN.replace('%s', String(i)));
|
||||
(function (index) {
|
||||
dot.addEventListener('click', function (event) {
|
||||
event.stopPropagation();
|
||||
go(index);
|
||||
});
|
||||
})(i);
|
||||
dots.appendChild(dot);
|
||||
}
|
||||
}
|
||||
Array.prototype.forEach.call(dots.children, function (dot, idx) {
|
||||
dot.classList.toggle('is-current', idx + 1 === page);
|
||||
dot.disabled = !ready || failed;
|
||||
dot.setAttribute('aria-disabled', (!ready || failed) ? 'true' : 'false');
|
||||
});
|
||||
if (window.history && window.history.replaceState) {
|
||||
window.history.replaceState(null, '', 'courseware.php?deck=' + encodeURIComponent(slug) + '&page=' + page);
|
||||
}
|
||||
}
|
||||
|
||||
function go(nextPage) {
|
||||
if (!viewer || !ready || failed) {
|
||||
return;
|
||||
}
|
||||
desiredPage = clampPage(nextPage);
|
||||
if (navBusy) {
|
||||
return;
|
||||
}
|
||||
navBusy = true;
|
||||
navQueue = navQueue.then(function pump() {
|
||||
if (!viewer || !ready || failed) {
|
||||
navBusy = false;
|
||||
return;
|
||||
}
|
||||
var target = clampPage(desiredPage);
|
||||
var current = shownPage();
|
||||
if (target === current) {
|
||||
page = current;
|
||||
navBusy = false;
|
||||
updateChrome();
|
||||
return;
|
||||
}
|
||||
return viewer.goToSlide(target - 1).then(function () {
|
||||
page = shownPage();
|
||||
updateChrome();
|
||||
if (clampPage(desiredPage) !== page) {
|
||||
return pump();
|
||||
}
|
||||
navBusy = false;
|
||||
});
|
||||
}).catch(function () {
|
||||
navBusy = false;
|
||||
setStatus(msg.broken, true);
|
||||
});
|
||||
}
|
||||
|
||||
function sizeCanvas() {
|
||||
var frame = document.getElementById('cw-frame');
|
||||
var rect = frame.getBoundingClientRect();
|
||||
var ratio = window.devicePixelRatio || 1;
|
||||
canvas.width = Math.max(1, Math.floor(rect.width * ratio));
|
||||
canvas.height = Math.max(1, Math.floor(rect.height * ratio));
|
||||
}
|
||||
|
||||
function fail(kind) {
|
||||
failed = true;
|
||||
ready = false;
|
||||
if (kind === 'login') {
|
||||
setStatus(msg.login, true);
|
||||
} else if (kind === 'missing') {
|
||||
setStatus(msg.missing, true);
|
||||
} else if (kind === 'player') {
|
||||
setStatus(msg.player, true);
|
||||
} else {
|
||||
setStatus(msg.broken, true);
|
||||
}
|
||||
updateChrome();
|
||||
}
|
||||
|
||||
try {
|
||||
if (!window.Chart || !window.JSZip || !window.PptxViewJS || !window.PptxViewJS.PPTXViewer) {
|
||||
fail('player');
|
||||
return;
|
||||
}
|
||||
|
||||
sizeCanvas();
|
||||
viewer = new PptxViewJS.PPTXViewer({
|
||||
canvas: canvas,
|
||||
slideSizeMode: 'fit',
|
||||
backgroundColor: '#ffffff'
|
||||
});
|
||||
viewer.on('slideChanged', updateChrome);
|
||||
viewer.on('renderComplete', function () {
|
||||
if (!failed) {
|
||||
setStatus('');
|
||||
updateChrome();
|
||||
}
|
||||
});
|
||||
|
||||
viewer.loadFromArrayBuffer = function (buffer) {
|
||||
return this.loadFile(buffer);
|
||||
};
|
||||
|
||||
fetch('courseware-file.php?deck=' + encodeURIComponent(slug), { credentials: 'same-origin' }).then(function (res) {
|
||||
if (res.status === 401) {
|
||||
throw { kind: 'login' };
|
||||
}
|
||||
if (res.status === 404 || !res.ok) {
|
||||
throw { kind: 'missing' };
|
||||
}
|
||||
return res.arrayBuffer();
|
||||
}).then(function (buf) {
|
||||
return viewer.loadFromArrayBuffer(buf);
|
||||
}).then(function () {
|
||||
total = viewer.getSlideCount() || 0;
|
||||
totalKnown = total >= 1;
|
||||
if (!totalKnown) {
|
||||
throw { kind: 'broken' };
|
||||
}
|
||||
ready = true;
|
||||
page = clampPage(page);
|
||||
desiredPage = page;
|
||||
return viewer.goToSlide(page - 1);
|
||||
}).then(function () {
|
||||
setStatus('');
|
||||
updateChrome();
|
||||
}).catch(function (err) {
|
||||
fail(err && err.kind ? err.kind : 'broken');
|
||||
});
|
||||
} catch (err) {
|
||||
fail('player');
|
||||
}
|
||||
|
||||
buttons.prev.forEach(function (btn) {
|
||||
if (btn) btn.addEventListener('click', function () { go(desiredPage - 1); });
|
||||
});
|
||||
buttons.next.forEach(function (btn) {
|
||||
if (btn) btn.addEventListener('click', function () { go(desiredPage + 1); });
|
||||
});
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
go(desiredPage - 1);
|
||||
}
|
||||
if (event.key === 'ArrowRight' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
go(desiredPage + 1);
|
||||
}
|
||||
if (event.key === 'Escape') window.location.href = 'courseware.php';
|
||||
});
|
||||
document.getElementById('cw-frame').addEventListener('click', function (event) {
|
||||
var rect = this.getBoundingClientRect();
|
||||
var x = event.clientX - rect.left;
|
||||
if (x < rect.width * 0.4) go(desiredPage - 1);
|
||||
if (x > rect.width * 0.6) go(desiredPage + 1);
|
||||
});
|
||||
window.addEventListener('resize', function () {
|
||||
sizeCanvas();
|
||||
if (viewer && ready && !failed) viewer.render();
|
||||
});
|
||||
updateChrome();
|
||||
})();
|
||||
</script>
|
||||
<?php } else { ?>
|
||||
<div class="cw-hero">
|
||||
<h1><?php echo htmlspecialchars($MSG_COURSEWARE, ENT_QUOTES, 'UTF-8'); ?></h1>
|
||||
</div>
|
||||
|
||||
<?php if (empty($decks)) { ?>
|
||||
<div class="cw-empty"><?php echo htmlspecialchars($MSG_COURSEWARE_EMPTY, ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<?php } else { ?>
|
||||
<div class="cw-grid">
|
||||
<?php foreach ($decks as $index => $deck) {
|
||||
$bar = $caption_colors[$index % count($caption_colors)];
|
||||
?>
|
||||
<a class="cw-card" href="courseware.php?deck=<?php echo rawurlencode($deck['slug']); ?>">
|
||||
<div class="cw-card-cover" aria-hidden="true"></div>
|
||||
<div class="cw-card-meta" style="background:<?php echo $bar; ?>">
|
||||
<div class="cw-card-title"><?php echo htmlspecialchars($deck['title'], ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
</div>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
|
||||
<?php include('template/' . $OJ_TEMPLATE . '/footer.php'); ?>
|
||||
@@ -1,193 +0,0 @@
|
||||
<?php include('template/' . $OJ_TEMPLATE . '/header.php'); ?>
|
||||
<script src="include/vditor-adapter.js"></script>
|
||||
|
||||
<style>
|
||||
.docs-shell {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
min-height: calc(100vh - 180px);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.docs-sidebar,
|
||||
.docs-content {
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.08);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.docs-sidebar {
|
||||
width: 300px;
|
||||
padding: 18px 0;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.docs-sidebar-header {
|
||||
padding: 0 20px 14px;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.docs-sidebar-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.docs-sidebar-header p {
|
||||
margin: 8px 0 0;
|
||||
color: #6b7280;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.docs-nav {
|
||||
padding: 12px;
|
||||
max-height: calc(100vh - 260px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.docs-nav a {
|
||||
display: block;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
color: #1f2937;
|
||||
transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
.docs-nav a:hover,
|
||||
.docs-nav a:focus {
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
color: #1d4ed8;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.docs-nav a.active {
|
||||
background: linear-gradient(135deg, #1d4ed8, #2563eb);
|
||||
color: #fff;
|
||||
box-shadow: 0 12px 30px rgba(37, 99, 235, 0.24);
|
||||
}
|
||||
|
||||
.docs-empty {
|
||||
padding: 14px 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.docs-content {
|
||||
flex: 1;
|
||||
padding: 22px 28px 28px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.docs-content-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.docs-content-header.docs-content-header-compact {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.docs-content-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.docs-content-header span {
|
||||
color: #6b7280;
|
||||
font-size: 0.92rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.docs-render {
|
||||
min-height: 320px;
|
||||
color: #111827;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.docs-shell {
|
||||
flex-direction: column;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.docs-sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.docs-nav {
|
||||
max-height: 260px;
|
||||
}
|
||||
|
||||
.docs-content {
|
||||
padding: 18px 18px 22px;
|
||||
}
|
||||
|
||||
.docs-content-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.docs-content-header span {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="docs-shell">
|
||||
<aside class="docs-sidebar">
|
||||
<div class="docs-sidebar-header">
|
||||
<h2>文档目录</h2>
|
||||
</div>
|
||||
<div class="docs-nav">
|
||||
<?php if (empty($doc_entries)) { ?>
|
||||
<div class="docs-empty">当前 doc 目录下还没有 Markdown 文档。</div>
|
||||
<?php } else { ?>
|
||||
<?php foreach ($doc_entries as $doc_entry) { ?>
|
||||
<a
|
||||
href="docs.php?file=<?php echo rawurlencode($doc_entry['file']); ?>"
|
||||
class="<?php if ($doc_entry['file'] === $selected_file) echo 'active'; ?>"
|
||||
>
|
||||
<?php echo htmlspecialchars($doc_entry['title'], ENT_QUOTES, 'UTF-8'); ?>
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="docs-content">
|
||||
<div class="docs-content-header<?php if (!$show_selected_title) echo ' docs-content-header-compact'; ?>">
|
||||
<?php if ($show_selected_title) { ?>
|
||||
<h1><?php echo htmlspecialchars($selected_title, ENT_QUOTES, 'UTF-8'); ?></h1>
|
||||
<?php } ?>
|
||||
<span><?php echo empty($selected_file) ? '未选择文档' : htmlspecialchars($selected_file, ENT_QUOTES, 'UTF-8'); ?></span>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($selected_markdown)) { ?>
|
||||
<div id="doc-render" class="docs-render md"><?php echo htmlspecialchars($selected_markdown, ENT_NOQUOTES, 'UTF-8'); ?></div>
|
||||
<?php } else { ?>
|
||||
<div class="ui info message">没有可显示的文档内容。</div>
|
||||
<?php } ?>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
if (!$('#doc-render').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
HustOJVditor.renderMarkdownBlocks('#doc-render', {
|
||||
useTextContent: true
|
||||
}).catch(function (error) {
|
||||
console.error('Failed to render docs markdown.', error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include('template/' . $OJ_TEMPLATE . '/footer.php'); ?>
|
||||
@@ -1,7 +1,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="<?php echo $OJ_CDN_URL.$path_fix."template/$OJ_TEMPLATE"?>/css/semantic.min.js"></script>
|
||||
<?php if (empty($OJ_COURSEWARE_PAGE)) { ?>
|
||||
<script src="<?php echo $path_fix."template/$OJ_TEMPLATE"?>/css/Chart.min.js"></script>
|
||||
<?php } ?>
|
||||
<style>
|
||||
.footer {
|
||||
line-height: 1.4285em;
|
||||
|
||||
@@ -192,6 +192,9 @@ a.label:hover {
|
||||
console.log('\n %c Theme By %c Baoshuo ( @renbaoshuo ) %c https://baoshuo.ren %c\n', 'color: #fadfa3; background: #000000; padding:5px 0;', 'background: #fadfa3; padding:5px 0;', 'background: #ffbf33; padding:5px 0;', '');
|
||||
console.log('\n GitHub Homepage: https://github.com/zhblue/hustoj \n Document: https://zhblue.github.io/hustoj \n Bug report URL: https://github.com/zhblue/hustoj/issues \n \n%c ★ Please give us a star on GitHub! ★ %c \n', 'color: red;', '')
|
||||
</script>
|
||||
<?php if (!empty($OJ_COURSEWARE_PAGE)) { ?>
|
||||
<script src="<?php echo $path_fix."template/$OJ_TEMPLATE"?>/css/Chart.min.js"></script>
|
||||
<?php } ?>
|
||||
</head>
|
||||
|
||||
<?php
|
||||
@@ -216,6 +219,8 @@ a.label:hover {
|
||||
<!-- 问题 -->
|
||||
<a class="item <?php if ($url=="problemset.php") echo "active";?>"
|
||||
href="<?php echo $path_fix?>problemset.php"><i class="list icon"></i><?php echo $MSG_PROBLEMS?> </a>
|
||||
<a class="item <?php if ($url=="courseware.php") echo "active";?>"
|
||||
href="<?php echo $path_fix?>courseware.php"><i class="clone icon"></i><?php echo $MSG_COURSEWARE; ?></a>
|
||||
<!-- 竞赛/作业 -->
|
||||
<a class="item <?php if ($url=="contest.php") echo "active";?>" href="<?php echo $path_fix?>contest.php<?php if(isset($_SESSION[$OJ_NAME."_user_id"])) echo "?my" ?>" ><i
|
||||
class="trophy icon"></i> <?php echo $MSG_CONTEST?></a>
|
||||
|
||||
Reference in New Issue
Block a user