Files
001code-html--cocos/scratch-gui/src/playground/works-plaza.ejs
2026-09-07 09:55:36 +08:00

1753 lines
71 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>作品广场 - 灵丌AI - 001CODE</title>
<style>
:root {
--bg: #f0f4f9; --ink: #0f172a; --muted: #64748b;
--ai-cyan: #00CCFF; --ai-accent: #00CCFF; --line: #e2e8f0;
--nav-h: 56px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { min-height: 100%; }
body {
background: var(--bg); color: var(--ink);
font-family: "Noto Sans SC", "PingFang SC", system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
}
.cd-nav {
position: sticky; top: 0; z-index: 50; height: var(--nav-h);
background: #fff; border-bottom: 1px solid var(--line);
display: flex; align-items: center; padding: 0 16px; gap: 12px;
}
.cd-nav__back {
display: inline-flex; align-items: center; gap: 8px;
padding: 8px 16px 8px 10px; border-radius: 12px;
border: 1.5px solid #7dd3fc;
background: linear-gradient(135deg, #f0fbff, #f0fbff);
text-decoration: none; color: #0003FF; font-size: 14px; font-weight: 700;
transition: all 0.2s; flex-shrink: 0;
box-shadow: 0 1px 3px rgba(0, 3, 255, 0.12);
}
.cd-nav__back svg {
flex-shrink: 0; width: 18px; height: 18px;
padding: 5px; box-sizing: content-box;
border-radius: 8px; background: #fff;
border: 1px solid #bae6fd; color: #0003FF;
}
.cd-nav__back:hover {
border-color: #60a5fa;
background: linear-gradient(135deg, #e0f7ff, #e0f7ff);
color: #0003FF;
box-shadow: 0 2px 8px rgba(0, 3, 255, 0.18);
}
.cd-nav__back:hover svg { border-color: #7dd3fc; background: #f0fbff; }
.cd-nav__logo {
display: inline-flex; align-items: center; justify-content: center;
flex-shrink: 0; height: 48px; width: 48px; text-decoration: none;
transition: transform 0.2s, opacity 0.2s;
}
.cd-nav__logo img {
height: 48px; width: 48px; object-fit: contain; display: block;
}
.cd-nav__logo:hover { transform: scale(1.08); opacity: 0.92; }
.cd-nav__course {
flex: 1; min-width: 0;
font-size: 15px; font-weight: 700; color: #0b1a4a;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cd-nav__right {
display: flex; align-items: center; gap: 8px; flex-shrink: 0; margin-left: auto;
}
.cd-nav__btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 7px 12px; border-radius: 18px; border: 1px solid var(--line);
background: #f8fafc; color: #475569; font-size: 12px; font-weight: 600;
text-decoration: none; cursor: pointer; font-family: inherit;
transition: all 0.2s; white-space: nowrap;
}
.cd-nav__btn:hover, .cd-nav__btn.is-active {
border-color: #7dd3fc; background: #f0fbff; color: #0003FF;
}
.cd-nav__user {
display: none; align-items: center; gap: 8px;
padding: 3px 10px 3px 3px; border-radius: 22px;
border: 1px solid var(--line); background: #fff;
color: inherit; max-width: 180px;
}
.cd-nav__avatar {
width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0;
background: linear-gradient(135deg, #e0f7ff, #7dd3fc);
display: flex; align-items: center; justify-content: center; font-size: 14px;
overflow: hidden;
}
.cd-nav__avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cd-nav__user-meta { min-width: 0; line-height: 1.2; }
.cd-nav__user-name {
font-size: 12px; font-weight: 600; color: #0b1a4a;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 80px;
}
.cd-nav__user-score { font-size: 10px; color: #0003FF; font-weight: 500; text-decoration: none; }
.cd-nav__user-score:hover { text-decoration: underline; }
.cd-nav__login {
display: none; padding: 7px 14px; border-radius: 18px; border: none;
background: linear-gradient(90deg, rgba(0, 204, 255, 1) 0%, rgba(0, 3, 255, 1) 100%);
color: #fff; font-size: 12px; font-weight: 600; text-decoration: none;
}
.page { max-width: 1200px; margin: 0 auto; padding: 28px 24px 48px; }
.page-hero {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 16px; margin-bottom: 24px; flex-wrap: wrap;
}
.page-hero__title { font-size: 28px; font-weight: 800; color: #0b1a4a; margin-bottom: 8px; }
.page-hero__sub { font-size: 14px; color: var(--muted); line-height: 1.6; }
.scope-tabs {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px; border-radius: 12px; background: #fff; border: 1px solid var(--line);
flex-shrink: 0;
}
.scope-tab {
padding: 8px 14px; border-radius: 9px; border: none;
background: transparent; color: #64748b; font-size: 13px; font-weight: 700;
cursor: pointer; font-family: inherit; transition: all 0.2s; white-space: nowrap;
}
.scope-tab:hover { color: #0003FF; }
.scope-tab.active {
background: linear-gradient(135deg, rgba(0,204,255,0.14), rgba(0,3,255,0.1));
color: #0003FF;
}
.filter-row {
display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 22px;
}
.filter-chip {
padding: 8px 14px; border-radius: 999px; border: 1.5px solid #e2e8f0;
background: #fff; color: #475569; font-size: 13px; font-weight: 600;
cursor: pointer; font-family: inherit; transition: all 0.2s;
}
.filter-chip:hover { border-color: #7dd3fc; color: #0003FF; }
.filter-chip.active {
border-color: #0003FF; color: #0003FF;
background: linear-gradient(135deg, rgba(0,204,255,0.12), rgba(0,3,255,0.08));
}
.works-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px;
}
.work-card {
display: flex; flex-direction: column;
border-radius: 14px; overflow: hidden; border: 1px solid #e8ecf1;
background: #fff; transition: all 0.25s; cursor: pointer; text-align: left;
font-family: inherit; padding: 0; width: 100%; height: 100%;
}
.work-card:hover {
transform: translateY(-3px); box-shadow: 0 10px 28px rgba(0,3,255,0.12);
}
.work-card__media {
width: 100%; aspect-ratio: 4/3; object-fit: cover; display: block; flex-shrink: 0;
background: linear-gradient(135deg, #e0f7ff, #7dd3fc);
}
.work-card__media-wrap {
position: relative; width: 100%; aspect-ratio: 4/3; flex-shrink: 0;
background: linear-gradient(135deg, #e0f7ff, #7dd3fc); overflow: hidden;
}
.work-card__media-wrap .work-card__media {
width: 100%; height: 100%; aspect-ratio: auto;
}
.work-card__media-wrap video.work-card__media {
background: #0f172a; object-fit: cover;
}
.work-card__play {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
width: 44px; height: 44px; border-radius: 50%;
background: rgba(15, 23, 42, 0.55); color: #fff;
display: flex; align-items: center; justify-content: center;
pointer-events: none; backdrop-filter: blur(2px);
}
.work-card__play svg { width: 18px; height: 18px; margin-left: 2px; }
.preview-modal {
display: none; position: fixed; inset: 0; z-index: 300;
background: rgba(15, 23, 42, 0.72); align-items: center; justify-content: center;
padding: 24px; box-sizing: border-box;
}
.preview-modal.is-open { display: flex; }
.preview-modal__panel {
position: relative; width: min(920px, 100%); max-height: calc(100vh - 48px);
background: #0f172a; border-radius: 16px; overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.35);
display: flex; flex-direction: column;
}
.preview-modal__head {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 14px 16px; background: #fff; border-bottom: 1px solid var(--line);
}
.preview-modal__title {
font-size: 15px; font-weight: 700; color: #0b1a4a;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.preview-modal__close {
flex-shrink: 0; width: 32px; height: 32px; border-radius: 8px;
border: 1px solid #e2e8f0; background: #f8fafc; color: #475569;
cursor: pointer; font-size: 18px; line-height: 1; font-family: inherit;
}
.preview-modal__close:hover { border-color: #7dd3fc; color: #0003FF; }
.preview-modal__body {
flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center;
padding: 12px; background: #0b1220;
}
.preview-modal__body img,
.preview-modal__body video {
max-width: 100%; max-height: calc(100vh - 160px);
border-radius: 8px; display: block;
}
.preview-modal__body img { object-fit: contain; background: transparent; }
.preview-modal__body video { width: 100%; background: #000; }
.preview-modal__empty {
color: #94a3b8; font-size: 14px; font-weight: 600; padding: 40px 16px; text-align: center;
}
/* 音频 / 语音 / 音乐播放器(类音乐 App */
.audio-player {
width: min(380px, 100%);
border-radius: 24px;
overflow: hidden;
background: #fff;
box-shadow: 0 24px 56px rgba(15, 23, 42, 0.32);
padding: 28px 24px 24px;
box-sizing: border-box;
color: #0b1a4a;
}
.audio-player__art {
position: relative;
width: 168px; height: 168px;
margin: 0 auto 22px;
border-radius: 50%;
background:
radial-gradient(circle at 35% 30%, rgba(255,255,255,0.55), transparent 42%),
linear-gradient(145deg, #00CCFF, #0003FF 55%, #4f46e5);
box-shadow:
0 0 0 10px rgba(0, 3, 255, 0.08),
0 18px 36px rgba(0, 3, 255, 0.35);
display: flex; align-items: center; justify-content: center;
transition: transform 0.35s ease;
}
.audio-player.is-playing .audio-player__art {
animation: audioDiscSpin 8s linear infinite;
}
@keyframes audioDiscSpin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.audio-player__art-hole {
width: 28px; height: 28px; border-radius: 50%;
background: #fff;
box-shadow: inset 0 0 0 6px rgba(0, 3, 255, 0.18);
}
.audio-player__art-ring {
position: absolute; inset: 18px; border-radius: 50%;
border: 1px solid rgba(255,255,255,0.28);
pointer-events: none;
}
.audio-player__meta { text-align: center; margin-bottom: 18px; }
.audio-player__badge {
display: inline-flex; align-items: center;
padding: 3px 10px; border-radius: 999px; margin-bottom: 10px;
background: rgba(0,3,255,0.1); color: #0003FF;
font-size: 11px; font-weight: 700;
}
.audio-player__title {
font-size: 18px; font-weight: 800; color: #0b1a4a; line-height: 1.35;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.audio-player__sub {
margin-top: 6px; color: #64748b; font-size: 13px; font-weight: 500;
}
.audio-player__wave {
display: flex; align-items: flex-end; justify-content: space-between;
height: 44px; width: 100%;
margin: 0 0 14px; padding: 0 2px; box-sizing: border-box;
}
.audio-player__wave span {
flex: 0 0 auto; width: 3px; min-width: 3px; max-width: 3px;
min-height: 4px; height: 4px; border-radius: 999px;
background: #7dd3fc;
transform-origin: bottom center;
}
.audio-player.is-playing .audio-player__wave span {
background: linear-gradient(90deg, rgba(0, 204, 255, 1) 0%, rgba(0, 3, 255, 1) 100%);
}
.audio-player__progress-wrap { margin-bottom: 8px; }
.audio-player__progress {
-webkit-appearance: none; appearance: none;
width: 100%; height: 6px; border-radius: 999px;
background: #e0f7ff; outline: none; cursor: pointer;
}
.audio-player__progress::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 14px; height: 14px; border-radius: 50%;
background: #0003FF; border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(0, 3, 255, 0.45);
cursor: pointer;
}
.audio-player__progress::-moz-range-thumb {
width: 14px; height: 14px; border-radius: 50%;
background: #0003FF; border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(0, 3, 255, 0.45);
cursor: pointer;
}
.audio-player__time {
display: flex; justify-content: space-between;
font-size: 11px; color: #94a3b8; font-weight: 600;
margin-bottom: 16px;
}
.audio-player__controls {
display: flex; align-items: center; justify-content: center; gap: 18px;
}
.audio-player__btn {
width: 44px; height: 44px; border-radius: 50%;
border: 1px solid #e2e8f0; background: #f8fafc;
color: #475569; cursor: pointer; display: inline-flex;
align-items: center; justify-content: center;
font-family: inherit; transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}
.audio-player__btn:hover { border-color: #7dd3fc; color: #0003FF; background: #f0fbff; }
.audio-player__btn--play {
width: 64px; height: 64px;
border: none;
background: linear-gradient(90deg, rgba(0, 204, 255, 1) 0%, rgba(0, 3, 255, 1) 100%);
color: #fff;
box-shadow: 0 10px 24px rgba(0, 3, 255, 0.4);
}
/* 覆盖通用 .audio-player__btn:hover避免白底白图标 */
.audio-player__btn.audio-player__btn--play:hover {
border: none;
background: linear-gradient(135deg, #b56af8, #7476f4);
color: #fff;
filter: none;
transform: translateY(-2px) scale(1.05);
box-shadow:
0 14px 30px rgba(0, 3, 255, 0.5),
0 0 0 6px rgba(0, 204, 255, 0.16);
}
.audio-player__btn.audio-player__btn--play:active {
transform: translateY(0) scale(0.98);
box-shadow: 0 8px 18px rgba(0, 3, 255, 0.38);
}
.audio-player__btn svg { width: 22px; height: 22px; display: block; }
.audio-player__btn--play svg { width: 28px; height: 28px; margin-left: 2px; fill: currentColor; }
.audio-player__btn--play.is-playing svg { margin-left: 0; }
.audio-player audio { display: none; }
/* 绘本预览阅读器 */
.book-reader {
width: 100%; max-width: 720px; max-height: calc(100vh - 140px);
display: flex; flex-direction: column; gap: 12px;
background: #fff; border-radius: 14px; overflow: hidden;
}
.book-reader__stage {
position: relative; flex: 1; min-height: 0;
display: flex; align-items: center; justify-content: center;
background: linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%);
padding: 16px 56px;
}
.book-reader__img {
max-width: 100%; max-height: min(52vh, 420px);
border-radius: 10px; object-fit: contain; display: block;
box-shadow: 0 8px 28px rgba(15, 23, 42, 0.12);
}
.book-reader__placeholder {
color: #94a3b8; font-size: 14px; font-weight: 600; padding: 48px 16px; text-align: center;
}
.book-reader__nav {
position: absolute; top: 50%; transform: translateY(-50%);
width: 40px; height: 40px; border-radius: 50%;
border: 1px solid #e2e8f0; background: rgba(255,255,255,0.95);
color: #475569; cursor: pointer; font-size: 22px; line-height: 1;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);
font-family: inherit;
}
.book-reader__nav:hover { border-color: #7dd3fc; color: #0003FF; }
.book-reader__nav:disabled { opacity: 0.35; cursor: default; }
.book-reader__nav--prev { left: 10px; }
.book-reader__nav--next { right: 10px; }
.book-reader__footer {
padding: 0 18px 16px; display: flex; flex-direction: column; gap: 8px;
}
.book-reader__page-no {
font-size: 12px; font-weight: 700; color: #0003FF;
}
.book-reader__text {
font-size: 14px; line-height: 1.7; color: #0b1a4a;
max-height: 7.2em; overflow: auto;
}
.book-reader__dots {
display: flex; justify-content: center; gap: 6px; flex-wrap: wrap; padding-bottom: 2px;
}
.book-reader__dot {
width: 8px; height: 8px; border-radius: 50%; border: none; padding: 0;
background: #d8b4fe; cursor: pointer; opacity: 0.45;
}
.book-reader__dot.is-active { opacity: 1; background: #0003FF; transform: scale(1.15); }
.work-card__body {
display: flex; flex-direction: column; flex: 1; min-height: 0;
padding: 12px 14px 12px;
}
.work-card__type {
display: inline-flex; align-self: flex-start; padding: 2px 8px; border-radius: 6px; margin-bottom: 8px;
font-size: 11px; font-weight: 600; background: rgba(0,3,255,0.08); color: #0003FF;
}
.work-card__name {
font-size: 14px; font-weight: 700; color: #0b1a4a; margin-bottom: 4px;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; line-height: 1.4; min-height: 2.8em;
}
.work-card__meta { font-size: 12px; color: #94a3b8; }
.work-card__author {
font-size: 12px; color: #64748b; margin-top: 4px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.work-card__meta-row { margin-top: 2px; }
.empty-state {
text-align: center; padding: 64px 20px; color: #94a3b8;
background: #fff; border-radius: 16px; border: 1px dashed #e2e8f0;
}
.empty-state__title { font-size: 16px; font-weight: 700; color: #0b1a4a; margin-bottom: 8px; }
.empty-state__sub { font-size: 13px; line-height: 1.6; margin-bottom: 18px; }
.empty-state__btn {
display: inline-flex; padding: 10px 20px; border-radius: 10px; border: none;
background: linear-gradient(90deg, rgba(0, 204, 255, 1) 0%, rgba(0, 3, 255, 1) 100%); color: #fff;
font-size: 13px; font-weight: 700; text-decoration: none; cursor: pointer; font-family: inherit;
}
.work-card__footer {
display: flex; align-items: center; gap: 8px;
margin-top: auto; padding-top: 12px;
border-top: 1px solid #f1f5f9;
font-size: 11px; color: #94a3b8;
}
.work-card__publish {
display: inline-flex; align-items: center; justify-content: center;
padding: 5px 10px; border-radius: 8px; border: 1px solid rgba(0,3,255,0.25);
background: #f0fbff; color: #0003FF; cursor: pointer;
font-family: inherit; font-size: 11px; font-weight: 600;
transition: all 0.15s; flex-shrink: 0;
}
.work-card__publish:hover {
background: linear-gradient(90deg, rgba(0, 204, 255, 1) 0%, rgba(0, 3, 255, 1) 100%);
border-color: transparent; color: #fff;
}
.work-card__publish:disabled { opacity: 0.55; cursor: wait; }
.work-card__published {
display: inline-flex; align-items: center; justify-content: center;
padding: 5px 10px; border-radius: 8px;
background: #f1f5f9; color: #94a3b8;
font-size: 11px; font-weight: 600; flex-shrink: 0;
}
.work-card__delete {
display: inline-flex; align-items: center; justify-content: center;
margin-left: auto; padding: 5px 10px; border-radius: 8px; border: 1px solid #fecaca;
background: #fff; color: #dc2626; cursor: pointer;
font-family: inherit; font-size: 11px; font-weight: 600;
transition: all 0.15s; flex-shrink: 0;
}
.work-card__delete:hover { background: #fef2f2; border-color: #f87171; }
.work-card__delete:disabled { opacity: 0.55; cursor: wait; }
.toast {
position: fixed; top: 18px; left: 50%; transform: translateX(-50%) translateY(-12px);
z-index: 200; padding: 10px 16px; border-radius: 10px;
background: rgba(15, 23, 42, 0.92); color: #fff;
font-size: 13px; font-weight: 600; pointer-events: none;
opacity: 0; transition: opacity 0.2s, transform 0.2s;
max-width: calc(100vw - 32px); box-shadow: 0 8px 24px rgba(15,23,42,0.18);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast.is-error { background: rgba(185, 28, 28, 0.94); }
.pager {
display: flex; align-items: center; justify-content: center; gap: 12px;
margin-top: 28px;
}
.pager__btn {
padding: 8px 16px; border-radius: 10px; border: 1px solid #e2e8f0;
background: #fff; color: #475569; font-size: 13px; font-weight: 600;
cursor: pointer; font-family: inherit;
}
.pager__btn:hover:not(:disabled) { border-color: #7dd3fc; color: #0003FF; }
.pager__btn:disabled { opacity: 0.45; cursor: not-allowed; }
.pager__info { font-size: 13px; color: #94a3b8; font-weight: 600; }
.loading-box {
grid-column: 1 / -1; text-align: center; padding: 48px 16px; color: #64748b; font-size: 14px;
}
.loading-spin {
width: 36px; height: 36px; margin: 0 auto 12px; border-radius: 50%;
border: 3px solid #e0f7ff; border-top-color: #0003FF;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 640px) {
.page { padding: 20px 16px 40px; }
.page-hero__title { font-size: 22px; }
.cd-nav__user-meta { display: none; }
}
</style>
</head>
<body>
<header class="cd-nav">
<a class="cd-nav__back" href="/ai.html" title="返回" onclick="if (document.referrer) { history.back(); return false; }">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
返回
</a>
<a class="cd-nav__logo" href="/ai.html" title="灵丌AI">
<img src="../images/aigc/logo.png" alt="灵丌AI" />
</a>
<div class="cd-nav__course">作品广场</div>
<div class="cd-nav__right">
<a class="cd-nav__btn is-active" href="/works-plaza.html">作品广场</a>
<button type="button" class="cd-nav__btn" id="navMineBtn">我的作品</button>
<a class="cd-nav__login" id="navLoginBtn" href="/login.html">请登录</a>
<div class="cd-nav__user" id="navUser">
<div class="cd-nav__avatar" id="navAvatar"><img src="../images/aigc/ai_programming_avatar_01.png" alt="头像" /></div>
<div class="cd-nav__user-meta">
<div class="cd-nav__user-name" id="navUserName">加载中</div>
<a class="cd-nav__user-score" href="/ai-points.html">积分 <strong id="navScore">--</strong></a>
</div>
</div>
</div>
</header>
<div class="page">
<div class="page-hero">
<div>
<h1 class="page-hero__title" id="pageTitle">作品广场</h1>
<p class="page-hero__sub" id="pageSub">看看大家用 AI 创作了什么,发现灵感、互相学习</p>
</div>
<div class="scope-tabs" role="tablist" aria-label="作品范围">
<button type="button" class="scope-tab active" id="scopePlaza" data-scope="plaza">广场</button>
<button type="button" class="scope-tab" id="scopeMine" data-scope="mine">我的作品</button>
</div>
</div>
<div class="filter-row" id="filterRow">
<button type="button" class="filter-chip active" data-type="">全部</button>
<button type="button" class="filter-chip" data-type="image">图片</button>
<button type="button" class="filter-chip" data-type="book">绘本</button>
<button type="button" class="filter-chip" data-type="video">视频</button>
<button type="button" class="filter-chip" data-type="tts">语音</button>
<button type="button" class="filter-chip" data-type="music">音乐</button>
</div>
<div class="works-grid" id="worksGrid"></div>
<div class="pager" id="pager" style="display:none;">
<button type="button" class="pager__btn" id="pagerPrev">上一页</button>
<span class="pager__info" id="pagerInfo">1 / 1</span>
<button type="button" class="pager__btn" id="pagerNext">下一页</button>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<div class="preview-modal" id="previewModal" aria-hidden="true">
<div class="preview-modal__panel" role="dialog" aria-modal="true" aria-labelledby="previewModalTitle">
<div class="preview-modal__head">
<div class="preview-modal__title" id="previewModalTitle">作品预览</div>
<button type="button" class="preview-modal__close" id="previewModalClose" aria-label="关闭">×</button>
</div>
<div class="preview-modal__body" id="previewModalBody"></div>
</div>
</div>
<script>
var currentFilter = '';
var currentPage = 1;
var pageSize = 20;
var totalCount = 0;
var loading = false;
var worksCache = [];
var deletingIds = {};
var publishingIds = {};
var toastTimer = null;
var listScope = 'plaza'; // plaza | mine
function resolveBaseUrl() {
return (window.CONFIG && window.CONFIG.DataServerBaseUrl) ||
"<%= htmlWebpackPlugin.options.dataServerBaseUrl %>";
}
function authHeaders() {
var headers = { 'Content-Type': 'application/json' };
var token = localStorage.getItem('access_token');
if (token) headers.Authorization = 'Bearer ' + token;
return headers;
}
function escapeHtml(str) {
return String(str == null ? '' : str)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
/** 读取 ai.html 缓存头像,仅展示 */
function applyNavAvatar(avatarEl) {
var el = avatarEl || document.getElementById('navAvatar');
if (!el) return;
var n = 1;
try {
var parsed = parseInt(localStorage.getItem('ai_programming_avatar'), 10);
if (parsed >= 1 && parsed <= 12) n = parsed;
} catch (e) {}
var pad = n < 10 ? ('0' + n) : String(n);
var src = '../images/aigc/ai_programming_avatar_' + pad + '.png';
var img = el.querySelector('img');
if (!img) {
img = document.createElement('img');
img.alt = '头像';
el.textContent = '';
el.appendChild(img);
}
img.src = src;
}
function pickApiData(result) {
if (!result || typeof result !== 'object') return {};
if (result.data && typeof result.data === 'object' && !Array.isArray(result.data)) return result.data;
if (result.obj && typeof result.obj === 'object' && !Array.isArray(result.obj)) return result.obj;
return result;
}
function formatTime(iso) {
if (!iso) return '';
try {
var d = new Date(iso);
if (isNaN(d.getTime())) return '';
var m = d.getMonth() + 1;
var day = d.getDate();
return d.getFullYear() + '/' + m + '/' + day;
} catch (e) { return ''; }
}
function placeholderThumb(type) {
var map = {
text: '../images/aigc/icon-ai-wenben.png',
image: '../images/aigc/icon-ai-huitu.png',
book: '../images/aigc/icon-ai-huiben.png',
video: '../images/aigc/icon-ai-shiping.png',
audio: '../images/aigc/icon-ai-yuying.png',
tts: '../images/aigc/icon-ai-yuying.png',
music: '../images/aigc/icon-ai-music.png'
};
return map[type] || '../images/aigc/icon-ai-huitu.png';
}
function getWorkContent(item) {
if (!item) return '';
var content = item.content;
if (content == null) return '';
if (typeof content === 'string') return content.trim();
return '';
}
function parseBookPages(content) {
if (content == null || content === '') return [];
var pages = content;
if (typeof content === 'string') {
var trimmed = content.trim();
if (!trimmed) return [];
if (trimmed.charAt(0) !== '[' && trimmed.charAt(0) !== '{') return [];
try {
pages = JSON.parse(trimmed);
} catch (e) {
return [];
}
}
if (pages && !Array.isArray(pages) && Array.isArray(pages.pages)) {
pages = pages.pages;
}
if (!Array.isArray(pages)) return [];
return pages.map(function(p) {
if (!p || typeof p !== 'object') return null;
var image = p.image_url || p.url || p.content || '';
var text = p.text || p.desc || p.description || '';
if (!image && !text) return null;
return { image_url: String(image || ''), text: String(text || '') };
}).filter(Boolean);
}
function isBookWorkType(type) {
var t = String(type || '').toLowerCase();
return t === 'book' || t === 'huiben' || t === 'picture_book' || t === 'picturebook';
}
function isVideoUrl(url) {
return !!url && /\.(mp4|webm|mov|m4v)(\?|$)/i.test(url);
}
function isImageUrl(url) {
return !!url && /\.(png|jpe?g|webp|gif|bmp)(\?|$)/i.test(url);
}
function isAudioUrl(url) {
return !!url && /\.(mp3|wav|ogg|m4a|aac)(\?|$)/i.test(url);
}
function resolveMediaKind(item) {
var type = String((item && item.work_type) || '').toLowerCase();
var bookPages = parseBookPages(item && item.content);
// 绘本优先work_type 为 book或 content 是 [{image_url,text}, ...]
if (isBookWorkType(type) || bookPages.length) return 'book';
var content = getWorkContent(item);
if (type === 'video' || isVideoUrl(content)) return 'video';
if (type === 'image' || isImageUrl(content)) return 'image';
if (type === 'audio' || type === 'tts' || type === 'music' || isAudioUrl(content)) return 'audio';
if (type === 'text') return 'text';
// 无明确类型时,按 content URL 推断OSS 结果常无扩展名,偏视频
if (content && /^https?:\/\//i.test(content) && /dashscope|aliyuncs|\.oss-/i.test(content)) {
return 'video';
}
return type || 'unknown';
}
function resolveBookCover(item) {
var pages = parseBookPages(item && item.content);
if (pages.length) return pages[0].image_url || '';
return '';
}
function resolveThumb(item) {
// 封面统一优先使用 thumbnail_url视频除外见 renderCardMedia
if (item && item.thumbnail_url) return item.thumbnail_url;
var kind = resolveMediaKind(item);
var content = getWorkContent(item);
if (kind === 'image' && content) return content;
if (kind === 'book') {
var cover = resolveBookCover(item);
if (cover) return cover;
}
return placeholderThumb(item && item.work_type);
}
function resolveVideoSrc(item) {
var content = getWorkContent(item);
if (content && /^https?:\/\//i.test(content)) return content;
if (content && isVideoUrl(content)) return content;
return '';
}
function renderCardMedia(item) {
var kind = resolveMediaKind(item);
var thumb = resolveThumb(item);
var playIcon =
'<span class="work-card__play" aria-hidden="true">' +
'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>' +
'</span>';
// 视频:一律用视频元素展示第一帧(后端不返回预览图)
if (kind === 'video') {
var videoSrc = resolveVideoSrc(item);
if (videoSrc) {
return '<div class="work-card__media-wrap">' +
'<video class="work-card__media" src="' + escapeHtml(videoSrc) +
'" muted playsinline preload="metadata" data-video-cover="1"></video>' +
playIcon +
'</div>';
}
return '<div class="work-card__media-wrap">' +
'<img class="work-card__media" src="' + escapeHtml(placeholderThumb('video')) + '" alt="" />' +
playIcon +
'</div>';
}
return '<div class="work-card__media-wrap">' +
'<img class="work-card__media" src="' + escapeHtml(thumb) + '" alt="" onerror="this.src=\'' + placeholderThumb(item.work_type) + '\'" />' +
(kind === 'audio' ? playIcon : '') +
'</div>';
}
/** 强制解码视频第一帧作为封面(部分浏览器 currentTime=0 会黑屏) */
function hydrateVideoCovers(root) {
var scope = root || document;
var videos = scope.querySelectorAll('video.work-card__media[data-video-cover="1"]');
Array.prototype.forEach.call(videos, function(video) {
if (video.getAttribute('data-cover-bound') === '1') return;
video.setAttribute('data-cover-bound', '1');
var fallbackToPlaceholder = function() {
var wrap = video.parentElement;
if (!wrap) return;
var img = document.createElement('img');
img.className = 'work-card__media';
img.alt = '';
img.src = placeholderThumb('video');
try { wrap.replaceChild(img, video); } catch (e) {}
};
var seekFirstFrame = function() {
try {
if (!isFinite(video.duration) || video.duration <= 0) {
video.currentTime = 0.1;
return;
}
video.currentTime = Math.min(0.1, Math.max(0.01, video.duration * 0.01));
} catch (e) {}
};
video.addEventListener('loadeddata', seekFirstFrame);
video.addEventListener('loadedmetadata', seekFirstFrame);
video.addEventListener('seeked', function() {
try { video.pause(); } catch (e) {}
});
video.addEventListener('error', fallbackToPlaceholder);
if (video.readyState >= 1) seekFirstFrame();
});
}
function closePreviewModal() {
stopPlazaWaveVisual();
var modal = document.getElementById('previewModal');
var body = document.getElementById('previewModalBody');
if (!modal) return;
var video = body && body.querySelector('video');
var audio = body && body.querySelector('audio');
if (video) {
try { video.pause(); } catch (e) {}
}
if (audio) {
try { audio.pause(); } catch (e) {}
}
bookPreviewPages = [];
bookPreviewIndex = 0;
if (body) body.innerHTML = '';
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden', 'true');
}
var bookPreviewPages = [];
var bookPreviewIndex = 0;
var PLAZA_WAVE_BAR_COUNT = 20;
var plazaWave = {
bars: [],
raf: 0,
audioCtx: null,
analyser: null,
source: null,
dataArray: null,
boundAudio: null,
useAnalyser: false,
silentFrames: 0
};
function getAudioTypeLabel(item) {
var t = String((item && item.work_type) || '').toLowerCase();
if (t === 'music') return '音乐';
if (t === 'tts' || t === 'voice') return '语音';
if (t === 'audio') return '音频';
return (item && item.work_type_display) || '音频';
}
function ensurePlazaWaveBars(el) {
if (!el) return [];
if (el.children.length !== PLAZA_WAVE_BAR_COUNT) {
var html = '';
for (var i = 0; i < PLAZA_WAVE_BAR_COUNT; i++) {
html += '<span style="flex:0 0 auto;width:3px;min-width:3px;max-width:3px;"></span>';
}
el.innerHTML = html;
}
plazaWave.bars = Array.prototype.slice.call(el.children);
return plazaWave.bars;
}
function setPlazaWaveIdle() {
var bars = plazaWave.bars || [];
for (var i = 0; i < bars.length; i++) bars[i].style.height = '4px';
}
function stopPlazaWaveVisual() {
if (plazaWave.raf) {
cancelAnimationFrame(plazaWave.raf);
plazaWave.raf = 0;
}
plazaWave.silentFrames = 0;
var player = document.getElementById('plazaAudioPlayer');
if (player) player.classList.remove('is-playing');
setPlazaWaveIdle();
}
function paintPlazaAnalyserWave() {
var bars = plazaWave.bars;
var data = plazaWave.dataArray;
if (!bars.length || !data || !plazaWave.analyser) return 0;
plazaWave.analyser.getByteFrequencyData(data);
var step = Math.max(1, Math.floor(data.length / bars.length));
var sum = 0;
for (var i = 0; i < bars.length; i++) {
var v = data[i * step] || 0;
sum += v;
var boost = 0.55 + 0.45 * Math.sin((i / Math.max(1, bars.length - 1)) * Math.PI);
bars[i].style.height = Math.max(8, Math.min(100, (v / 255) * 100 * boost + 8)) + '%';
}
return sum / Math.max(1, bars.length);
}
function paintPlazaFallbackWave(now) {
var bars = plazaWave.bars;
if (!bars.length) return;
var t = (now || performance.now()) / 1000;
for (var i = 0; i < bars.length; i++) {
var n = i / Math.max(1, bars.length - 1);
var envelope = Math.sin(n * Math.PI);
var wobble =
Math.sin(t * 7.2 + i * 0.55) * 0.35 +
Math.sin(t * 11.5 + i * 0.9) * 0.25 +
Math.sin(t * 3.4 + i * 0.2) * 0.15;
bars[i].style.height = Math.max(8, Math.min(100, 12 + envelope * 58 + (wobble + 0.5) * 28 * envelope)) + '%';
}
}
function plazaWaveTick(now) {
var audioEl = document.getElementById('plazaPreviewAudio');
if (!audioEl || audioEl.paused || audioEl.ended) {
stopPlazaWaveVisual();
return;
}
if (plazaWave.useAnalyser) {
var avg = paintPlazaAnalyserWave();
if (avg < 1.5) {
plazaWave.silentFrames += 1;
if (plazaWave.silentFrames > 12) {
plazaWave.useAnalyser = false;
paintPlazaFallbackWave(now);
}
} else {
plazaWave.silentFrames = 0;
}
} else {
paintPlazaFallbackWave(now);
}
plazaWave.raf = requestAnimationFrame(plazaWaveTick);
}
function ensurePlazaAudioGraph(audioEl) {
if (!audioEl) return false;
// 每次打开新 audio 节点需重建图(旧 source 不可复用)
if (plazaWave.source && plazaWave.boundAudio === audioEl) return !!plazaWave.analyser;
try {
if (plazaWave.source) {
try { plazaWave.source.disconnect(); } catch (e) {}
plazaWave.source = null;
plazaWave.analyser = null;
}
var Ctx = window.AudioContext || window.webkitAudioContext;
if (!Ctx) return false;
plazaWave.audioCtx = plazaWave.audioCtx || new Ctx();
plazaWave.analyser = plazaWave.audioCtx.createAnalyser();
plazaWave.analyser.fftSize = 128;
plazaWave.analyser.smoothingTimeConstant = 0.78;
plazaWave.source = plazaWave.audioCtx.createMediaElementSource(audioEl);
plazaWave.source.connect(plazaWave.analyser);
plazaWave.analyser.connect(plazaWave.audioCtx.destination);
plazaWave.dataArray = new Uint8Array(plazaWave.analyser.frequencyBinCount);
plazaWave.boundAudio = audioEl;
return true;
} catch (err) {
console.warn('[works-plaza] analyser init failed', err);
plazaWave.analyser = null;
plazaWave.source = null;
plazaWave.boundAudio = null;
return false;
}
}
function startPlazaWaveVisual() {
var audioEl = document.getElementById('plazaPreviewAudio');
var player = document.getElementById('plazaAudioPlayer');
var waveEl = document.getElementById('plazaAudioWave');
if (!audioEl || !player || !waveEl) return;
ensurePlazaWaveBars(waveEl);
player.classList.add('is-playing');
plazaWave.silentFrames = 0;
if (plazaWave.audioCtx && plazaWave.audioCtx.state === 'suspended') {
plazaWave.audioCtx.resume().catch(function() {});
}
plazaWave.useAnalyser = ensurePlazaAudioGraph(audioEl);
if (plazaWave.raf) cancelAnimationFrame(plazaWave.raf);
plazaWave.raf = requestAnimationFrame(plazaWaveTick);
}
function bindPlazaAudioPlayerEvents(audioEl) {
if (!audioEl || audioEl.getAttribute('data-wave-bound') === '1') return;
audioEl.setAttribute('data-wave-bound', '1');
audioEl.addEventListener('play', function() {
startPlazaWaveVisual();
updatePlazaPlayButton(true);
});
audioEl.addEventListener('playing', function() {
startPlazaWaveVisual();
updatePlazaPlayButton(true);
});
audioEl.addEventListener('pause', function() {
stopPlazaWaveVisual();
updatePlazaPlayButton(false);
});
audioEl.addEventListener('ended', function() {
stopPlazaWaveVisual();
updatePlazaPlayButton(false);
var progress = document.getElementById('plazaAudioProgress');
if (progress) progress.value = '0';
var cur = document.getElementById('plazaAudioCurrent');
if (cur) cur.textContent = '0:00';
});
audioEl.addEventListener('emptied', function() {
stopPlazaWaveVisual();
updatePlazaPlayButton(false);
});
audioEl.addEventListener('timeupdate', syncPlazaAudioProgress);
audioEl.addEventListener('loadedmetadata', syncPlazaAudioProgress);
}
function formatAudioTime(sec) {
if (!isFinite(sec) || sec < 0) return '0:00';
var s = Math.floor(sec);
var m = Math.floor(s / 60);
var r = s % 60;
return m + ':' + (r < 10 ? '0' + r : r);
}
function syncPlazaAudioProgress() {
var audioEl = document.getElementById('plazaPreviewAudio');
var progress = document.getElementById('plazaAudioProgress');
var cur = document.getElementById('plazaAudioCurrent');
var dur = document.getElementById('plazaAudioDuration');
if (!audioEl) return;
var duration = isFinite(audioEl.duration) ? audioEl.duration : 0;
var current = isFinite(audioEl.currentTime) ? audioEl.currentTime : 0;
if (progress) {
progress.max = duration > 0 ? String(duration) : '0';
progress.value = String(current);
var pct = duration > 0 ? (current / duration) * 100 : 0;
progress.style.background =
'linear-gradient(90deg, #00CCFF 0%, #0003FF ' + pct + '%, #e0f7ff ' + pct + '%)';
}
if (cur) cur.textContent = formatAudioTime(current);
if (dur) dur.textContent = formatAudioTime(duration);
}
function updatePlazaPlayButton(playing) {
var btn = document.getElementById('plazaAudioPlayBtn');
if (!btn) return;
btn.classList.toggle('is-playing', !!playing);
btn.setAttribute('aria-label', playing ? '暂停' : '播放');
btn.innerHTML = playing
? '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 5h4v14H6zm8 0h4v14h-4z"/></svg>'
: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>';
}
function togglePlazaAudioPlay() {
var audioEl = document.getElementById('plazaPreviewAudio');
if (!audioEl) return;
if (audioEl.paused) {
var p = audioEl.play();
if (p && typeof p.catch === 'function') p.catch(function() {});
} else {
audioEl.pause();
}
}
function seekPlazaAudio(deltaSec) {
var audioEl = document.getElementById('plazaPreviewAudio');
if (!audioEl) return;
var next = (audioEl.currentTime || 0) + deltaSec;
var duration = isFinite(audioEl.duration) ? audioEl.duration : 0;
if (duration > 0) next = Math.max(0, Math.min(duration, next));
else next = Math.max(0, next);
try { audioEl.currentTime = next; } catch (e) {}
syncPlazaAudioProgress();
}
function onPlazaProgressInput(e) {
var audioEl = document.getElementById('plazaPreviewAudio');
var progress = e && e.target;
if (!audioEl || !progress) return;
var val = Number(progress.value);
if (!isFinite(val)) return;
try { audioEl.currentTime = val; } catch (err) {}
syncPlazaAudioProgress();
}
function renderAudioPlayer(item, content) {
var label = getAudioTypeLabel(item);
var title = (item && item.title) || '未命名作品';
var author = (item && item.username) || '';
return '<div class="audio-player" id="plazaAudioPlayer">' +
'<div class="audio-player__art" aria-hidden="true">' +
'<div class="audio-player__art-ring"></div>' +
'<div class="audio-player__art-hole"></div>' +
'</div>' +
'<div class="audio-player__meta">' +
'<div class="audio-player__badge">' + escapeHtml(label) + '</div>' +
'<div class="audio-player__title" title="' + escapeHtml(title) + '">' + escapeHtml(title) + '</div>' +
'<div class="audio-player__sub">' + (author ? ('@' + escapeHtml(author)) : '灵丌AI 作品') + '</div>' +
'</div>' +
'<div class="audio-player__wave" id="plazaAudioWave"></div>' +
'<div class="audio-player__progress-wrap">' +
'<input class="audio-player__progress" id="plazaAudioProgress" type="range" min="0" max="0" value="0" step="0.01" />' +
'</div>' +
'<div class="audio-player__time">' +
'<span id="plazaAudioCurrent">0:00</span>' +
'<span id="plazaAudioDuration">0:00</span>' +
'</div>' +
'<div class="audio-player__controls">' +
'<button type="button" class="audio-player__btn" id="plazaAudioRewind" aria-label="快退10秒">' +
'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M11.99 5V1l-5 5 5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6h-2c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>' +
'</button>' +
'<button type="button" class="audio-player__btn audio-player__btn--play" id="plazaAudioPlayBtn" aria-label="播放">' +
'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>' +
'</button>' +
'<button type="button" class="audio-player__btn" id="plazaAudioForward" aria-label="快进10秒">' +
'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 5V1l5 5-5 5V7c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6h2c0 4.42-3.58 8-8 8s-8-3.58-8-8 3.58-8 8-8z"/></svg>' +
'</button>' +
'</div>' +
'<audio id="plazaPreviewAudio" src="' + escapeHtml(content) + '" playsinline preload="metadata"></audio>' +
'</div>';
}
function openAudioPlayer(item, content) {
var body = document.getElementById('previewModalBody');
if (!body || !content) return;
stopPlazaWaveVisual();
plazaWave.source = null;
plazaWave.analyser = null;
plazaWave.boundAudio = null;
body.innerHTML = renderAudioPlayer(item, content);
var waveEl = document.getElementById('plazaAudioWave');
var audioEl = document.getElementById('plazaPreviewAudio');
var playBtn = document.getElementById('plazaAudioPlayBtn');
var rewindBtn = document.getElementById('plazaAudioRewind');
var forwardBtn = document.getElementById('plazaAudioForward');
var progress = document.getElementById('plazaAudioProgress');
ensurePlazaWaveBars(waveEl);
setPlazaWaveIdle();
updatePlazaPlayButton(false);
bindPlazaAudioPlayerEvents(audioEl);
if (playBtn) playBtn.addEventListener('click', togglePlazaAudioPlay);
if (rewindBtn) rewindBtn.addEventListener('click', function() { seekPlazaAudio(-10); });
if (forwardBtn) forwardBtn.addEventListener('click', function() { seekPlazaAudio(10); });
if (progress) {
progress.addEventListener('input', onPlazaProgressInput);
progress.addEventListener('change', onPlazaProgressInput);
}
try { audioEl.crossOrigin = 'anonymous'; } catch (e) {}
audioEl.onerror = function() {
try {
audioEl.onerror = null;
audioEl.removeAttribute('crossorigin');
audioEl.crossOrigin = null;
audioEl.src = content;
} catch (e2) {}
};
// 打开后自动播放
var playPromise = audioEl.play();
if (playPromise && typeof playPromise.catch === 'function') {
playPromise.catch(function() {
updatePlazaPlayButton(false);
});
}
}
function renderBookReaderPage() {
var body = document.getElementById('previewModalBody');
if (!body || !bookPreviewPages.length) return;
var total = bookPreviewPages.length;
var idx = Math.max(0, Math.min(bookPreviewIndex, total - 1));
bookPreviewIndex = idx;
var page = bookPreviewPages[idx] || {};
var img = page.image_url || '';
var text = page.text || '';
var imgHtml = img
? '<img class="book-reader__img" src="' + escapeHtml(img) + '" alt="第' + (idx + 1) + '页" />'
: '<div class="book-reader__placeholder">本页暂无插图</div>';
var dots = bookPreviewPages.map(function(_, i) {
return '<button type="button" class="book-reader__dot' + (i === idx ? ' is-active' : '') +
'" data-book-page="' + i + '" aria-label="第' + (i + 1) + '页"></button>';
}).join('');
body.innerHTML =
'<div class="book-reader">' +
'<div class="book-reader__stage">' +
'<button type="button" class="book-reader__nav book-reader__nav--prev" data-book-nav="prev"' +
(idx <= 0 ? ' disabled' : '') + ' aria-label="上一页"></button>' +
imgHtml +
'<button type="button" class="book-reader__nav book-reader__nav--next" data-book-nav="next"' +
(idx >= total - 1 ? ' disabled' : '') + ' aria-label="下一页"></button>' +
'</div>' +
'<div class="book-reader__footer">' +
'<div class="book-reader__page-no">第 ' + (idx + 1) + ' / ' + total + ' 页</div>' +
'<div class="book-reader__text">' + escapeHtml(text || '(本页暂无文字)') + '</div>' +
'<div class="book-reader__dots">' + dots + '</div>' +
'</div>' +
'</div>';
}
function openBookPreview(item) {
bookPreviewPages = parseBookPages(item && item.content);
bookPreviewIndex = 0;
var body = document.getElementById('previewModalBody');
if (!bookPreviewPages.length) {
if (body) body.innerHTML = '<div class="preview-modal__empty">暂无可预览的绘本内容</div>';
return;
}
renderBookReaderPage();
}
function openWorkPreview(item) {
if (!item) return;
var modal = document.getElementById('previewModal');
var body = document.getElementById('previewModalBody');
var titleEl = document.getElementById('previewModalTitle');
if (!modal || !body) return;
var kind = resolveMediaKind(item);
var content = getWorkContent(item);
var type = String((item && item.work_type) || kind || '').toLowerCase();
if (titleEl) titleEl.textContent = item.title || item.work_type_display || '作品预览';
// 无可用媒体(如图片/内容缺失)时,跳转到对应创作页
var hasBook = kind === 'book' && parseBookPages(item && item.content).length > 0;
var hasMedia = !!(
hasBook ||
(kind === 'video' && content) ||
(kind === 'image' && content) ||
(kind === 'audio' && content) ||
(kind === 'text' && content) ||
(content && /^https?:\/\//i.test(content))
);
if (!hasMedia) {
location.href = createToolUrlByType(type || currentFilter);
return;
}
if (kind === 'book') {
openBookPreview(item);
} else if (kind === 'video' && content) {
body.innerHTML = '<video src="' + escapeHtml(content) + '" controls autoplay playsinline preload="metadata"></video>';
} else if (kind === 'image' && content) {
body.innerHTML = '<img src="' + escapeHtml(content) + '" alt="' + escapeHtml(item.title || '作品预览') + '" />';
} else if (kind === 'audio' && content) {
openAudioPlayer(item, content);
} else if (kind === 'text' && content) {
body.innerHTML =
'<div style="width:100%;max-height:calc(100vh - 160px);overflow:auto;padding:20px;background:#fff;border-radius:8px;color:#0b1a4a;line-height:1.7;white-space:pre-wrap;">' +
escapeHtml(content) +
'</div>';
} else if (content && /^https?:\/\//i.test(content)) {
// 未知类型但有 URL按图片尝试失败由浏览器处理
if (isVideoUrl(content)) {
body.innerHTML = '<video src="' + escapeHtml(content) + '" controls autoplay playsinline></video>';
} else {
body.innerHTML = '<img src="' + escapeHtml(content) + '" alt="作品预览" />';
}
} else {
location.href = createToolUrlByType(type || currentFilter);
return;
}
modal.classList.add('is-open');
modal.setAttribute('aria-hidden', 'false');
}
function totalPages() {
return Math.max(1, Math.ceil((Number(totalCount) || 0) / pageSize));
}
function isLoggedIn() {
return !!localStorage.getItem('access_token');
}
function goLogin() {
try { localStorage.setItem('returnUrl', location.pathname + location.search); } catch (e) {}
location.href = '/login.html';
}
function showToast(msg, type) {
var el = document.getElementById('toast');
if (!el) return;
el.textContent = msg || '';
el.classList.toggle('is-error', type === 'error');
el.classList.add('show');
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function() { el.classList.remove('show'); }, 2000);
}
function updateScopeUi() {
var isMine = listScope === 'mine';
var titleEl = document.getElementById('pageTitle');
var subEl = document.getElementById('pageSub');
var plazaTab = document.getElementById('scopePlaza');
var mineTab = document.getElementById('scopeMine');
var navPlaza = document.querySelector('.cd-nav__btn[href="/works-plaza.html"]');
var navMine = document.getElementById('navMineBtn');
if (titleEl) titleEl.textContent = isMine ? '我的作品' : '作品广场';
if (subEl) {
subEl.textContent = isMine
? '查看你已发布的作品,支持按类型筛选'
: '看看大家用 AI 创作了什么,发现灵感、互相学习';
}
if (plazaTab) plazaTab.classList.toggle('active', !isMine);
if (mineTab) mineTab.classList.toggle('active', isMine);
if (navPlaza) navPlaza.classList.toggle('is-active', !isMine);
if (navMine) navMine.classList.toggle('is-active', isMine);
document.title = (isMine ? '我的作品' : '作品广场') + ' - 灵丌AI - 001CODE';
}
function setListScope(scope, opts) {
var next = scope === 'mine' ? 'mine' : 'plaza';
if (next === 'mine' && !isLoggedIn()) {
showToast('请先登录后查看我的作品', 'error');
setTimeout(goLogin, 600);
return;
}
if (listScope === next && !(opts && opts.forceReload)) return;
listScope = next;
currentPage = 1;
updateScopeUi();
loadWorks();
}
function isPublicWork(w) {
if (!w) return false;
var v = w.is_public;
return v === true || v === 1 || v === '1' || String(v).toLowerCase() === 'true';
}
async function publishWork(workId, btn) {
var id = Number(workId);
if (!id || publishingIds[id]) return;
if (!isLoggedIn()) {
showToast('请先登录后再发布', 'error');
setTimeout(goLogin, 600);
return;
}
if (!window.confirm('确定将作品发布到广场吗?发布后所有人可见。')) return;
publishingIds[id] = true;
if (btn) {
btn.disabled = true;
btn.textContent = '发布中…';
}
try {
var res = await fetch(resolveBaseUrl() + '/api/ai-explorer/works/publish/', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({ id: id })
});
var result = await res.json().catch(function() { return null; });
if (res.status === 401 || res.status === 403) {
showToast('登录已失效,请重新登录', 'error');
setTimeout(goLogin, 600);
return;
}
if (!res.ok || (result && result.success === false)) {
throw new Error((result && (result.message || result.detail)) || ('HTTP ' + res.status));
}
for (var i = 0; i < worksCache.length; i++) {
if (Number(worksCache[i].id) === id) {
worksCache[i].is_public = true;
worksCache[i].status = 'published';
worksCache[i].status_display = '已发布';
break;
}
}
showToast('发布成功,在通过审核后即可广场展示', 'success');
renderWorks();
} catch (err) {
console.error('[ai-works] publish failed', err);
showToast((err && err.message) || '发布失败,请稍后重试', 'error');
if (btn) {
btn.disabled = false;
btn.textContent = '发布';
}
} finally {
delete publishingIds[id];
}
}
async function deleteWork(workId, btn) {
var id = Number(workId);
if (!id || deletingIds[id]) return;
if (!isLoggedIn()) {
showToast('请先登录后再删除', 'error');
setTimeout(goLogin, 600);
return;
}
if (!window.confirm('确定删除这个作品吗?删除后不可恢复。')) return;
deletingIds[id] = true;
if (btn) btn.disabled = true;
try {
var res = await fetch(resolveBaseUrl() + '/api/ai-explorer/works/delete/', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({ work_id: id })
});
var result = await res.json().catch(function() { return null; });
if (res.status === 401 || res.status === 403) {
showToast('登录已失效,请重新登录', 'error');
setTimeout(goLogin, 600);
return;
}
if (!res.ok || (result && result.success === false)) {
throw new Error((result && (result.message || result.detail)) || ('HTTP ' + res.status));
}
worksCache = worksCache.filter(function(item) { return Number(item.id) !== id; });
totalCount = Math.max(0, (Number(totalCount) || 0) - 1);
showToast((result && result.message) || '删除成功');
if (!worksCache.length && currentPage > 1 && totalCount > 0) {
currentPage -= 1;
loadWorks();
return;
}
if (!worksCache.length && totalCount > 0) {
loadWorks();
return;
}
renderWorks();
} catch (err) {
console.error('[ai-works] delete failed', err);
showToast((err && err.message) || '删除失败,请稍后重试', 'error');
if (btn) btn.disabled = false;
} finally {
delete deletingIds[id];
}
}
function renderLoading() {
document.getElementById('worksGrid').innerHTML =
'<div class="loading-box"><div class="loading-spin"></div>作品加载中...</div>';
document.getElementById('pager').style.display = 'none';
}
function renderError(msg) {
document.getElementById('worksGrid').innerHTML =
'<div class="empty-state" style="grid-column:1/-1;">' +
'<div class="empty-state__title">加载失败</div>' +
'<div class="empty-state__sub">' + escapeHtml(msg || '请稍后重试') + '</div>' +
'<button type="button" class="empty-state__btn" onclick="loadWorks()">重新加载</button>' +
'</div>';
document.getElementById('pager').style.display = 'none';
}
function renderPager() {
var pager = document.getElementById('pager');
var pages = totalPages();
if (!worksCache.length && totalCount === 0) {
pager.style.display = 'none';
return;
}
pager.style.display = 'flex';
document.getElementById('pagerInfo').textContent = currentPage + ' / ' + pages + ' · 共 ' + totalCount + ' 件';
document.getElementById('pagerPrev').disabled = currentPage <= 1 || loading;
document.getElementById('pagerNext').disabled = currentPage >= pages || loading;
}
function createToolUrlByType(type) {
var t = String(type || '').toLowerCase();
if (t === 'image' || t === 'img' || t === 'draw') return '/ai-draw.html';
if (t === 'book' || t === 'huiben' || t === 'picture_book' || t === 'picturebook') return '/ai-huiben.html';
if (t === 'video') return '/ai-video.html';
if (t === 'tts' || t === 'voice' || t === 'audio') return '/ai-voice.html';
if (t === 'music') return '/ai-music.html';
if (t === 'text') return '/ai-text.html';
return '/ai.html';
}
function createToolLabelByType(type) {
var t = String(type || '').toLowerCase();
if (t === 'image' || t === 'img' || t === 'draw') return '去 AI 绘图';
if (t === 'book' || t === 'huiben' || t === 'picture_book' || t === 'picturebook') return '去 AI 绘本';
if (t === 'video') return '去 AI 视频';
if (t === 'tts' || t === 'voice' || t === 'audio') return '去 AI 语音';
if (t === 'music') return '去 AI 音乐';
if (t === 'text') return '去 AI 文本';
return '去创作';
}
function renderWorks() {
var grid = document.getElementById('worksGrid');
if (!worksCache.length) {
var createHref = createToolUrlByType(currentFilter);
var createLabel = createToolLabelByType(currentFilter);
grid.innerHTML =
'<div class="empty-state" style="grid-column:1/-1;">' +
'<div class="empty-state__title">' + (listScope === 'mine' ? '还没有已发布作品' : '暂无公开作品') + '</div>' +
'<div class="empty-state__sub">' +
(listScope === 'mine'
? '去创作工具箱生成作品并发布后,会显示在这里'
: '换个分类看看,或去创作工具箱生成并发布作品') +
'</div>' +
'<a class="empty-state__btn" href="' + createHref + '">' + createLabel + '</a>' +
'</div>';
renderPager();
return;
}
grid.innerHTML = worksCache.map(function(w) {
var typeLabel = w.work_type_display || w.work_type || '作品';
var title = w.title || '未命名作品';
var author = w.username || '匿名';
var time = formatTime(w.created_at);
var meta = (w.model_name ? w.model_name + ' · ' : '') + time;
var publishBtn = isPublicWork(w)
? '<span class="work-card__published">已发布</span>'
: ('<button type="button" class="work-card__publish" data-work-id="' + escapeHtml(w.id) + '" title="发布到广场">发布</button>');
var footerHtml = listScope === 'mine'
? ('<div class="work-card__footer">' +
publishBtn +
'<button type="button" class="work-card__delete" data-work-id="' + escapeHtml(w.id) + '" title="删除作品">删除</button>' +
'</div>')
: '';
return '<div class="work-card" data-id="' + escapeHtml(w.id) + '">' +
renderCardMedia(w) +
'<div class="work-card__body">' +
'<span class="work-card__type">' + escapeHtml(typeLabel) + '</span>' +
'<div class="work-card__name">' + escapeHtml(title) + '</div>' +
'<div class="work-card__author">@' + escapeHtml(author) + '</div>' +
'<div class="work-card__meta work-card__meta-row">' + escapeHtml(meta) + '</div>' +
footerHtml +
'</div>' +
'</div>';
}).join('');
hydrateVideoCovers(grid);
renderPager();
}
async function loadWorks() {
if (loading) return;
if (listScope === 'mine' && !isLoggedIn()) {
listScope = 'plaza';
updateScopeUi();
}
loading = true;
renderLoading();
try {
var payload = {
page: currentPage,
page_size: pageSize
};
if (currentFilter) payload.work_type = currentFilter;
var endpoint = listScope === 'mine'
? '/api/ai-explorer/works/mine/'
: '/api/ai-explorer/works/list/';
var res = await fetch(resolveBaseUrl() + endpoint, {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify(payload)
});
var result = await res.json().catch(function() { return null; });
if (res.status === 401 || res.status === 403) {
showToast('登录已失效,请重新登录', 'error');
setTimeout(goLogin, 600);
return;
}
if (!res.ok || (result && result.success === false)) {
throw new Error((result && (result.message || result.detail)) || ('HTTP ' + res.status));
}
var data = pickApiData(result);
totalCount = Number(data.count) || 0;
worksCache = Array.isArray(data.results) ? data.results : [];
var pages = totalPages();
if (currentPage > pages) {
currentPage = pages;
loading = false;
return loadWorks();
}
renderWorks();
} catch (err) {
console.error('[ai-works] list failed', err);
renderError((err && err.message) || '作品列表加载失败');
} finally {
loading = false;
renderPager();
}
}
document.getElementById('worksGrid').addEventListener('click', function(e) {
var publishBtn = e.target.closest('.work-card__publish');
if (publishBtn) {
e.preventDefault();
e.stopPropagation();
publishWork(publishBtn.getAttribute('data-work-id'), publishBtn);
return;
}
var deleteBtn = e.target.closest('.work-card__delete');
if (deleteBtn) {
e.preventDefault();
e.stopPropagation();
deleteWork(deleteBtn.getAttribute('data-work-id'), deleteBtn);
return;
}
var card = e.target.closest('.work-card');
if (!card) return;
var id = card.getAttribute('data-id');
var item = worksCache.find(function(w) { return String(w.id) === String(id); });
if (item) openWorkPreview(item);
});
(function initPreviewModal() {
var modal = document.getElementById('previewModal');
var closeBtn = document.getElementById('previewModalClose');
var body = document.getElementById('previewModalBody');
if (closeBtn) closeBtn.addEventListener('click', closePreviewModal);
if (modal) {
modal.addEventListener('click', function(e) {
if (e.target === modal) closePreviewModal();
});
}
if (body) {
body.addEventListener('click', function(e) {
var navBtn = e.target.closest('[data-book-nav]');
if (navBtn && !navBtn.disabled) {
var dir = navBtn.getAttribute('data-book-nav');
if (dir === 'prev' && bookPreviewIndex > 0) {
bookPreviewIndex -= 1;
renderBookReaderPage();
} else if (dir === 'next' && bookPreviewIndex < bookPreviewPages.length - 1) {
bookPreviewIndex += 1;
renderBookReaderPage();
}
return;
}
var dot = e.target.closest('[data-book-page]');
if (dot) {
var pageIdx = Number(dot.getAttribute('data-book-page'));
if (!isNaN(pageIdx) && pageIdx !== bookPreviewIndex) {
bookPreviewIndex = pageIdx;
renderBookReaderPage();
}
}
});
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closePreviewModal();
return;
}
if (!modal || !modal.classList.contains('is-open') || !bookPreviewPages.length) return;
if (e.key === 'ArrowLeft' && bookPreviewIndex > 0) {
bookPreviewIndex -= 1;
renderBookReaderPage();
} else if (e.key === 'ArrowRight' && bookPreviewIndex < bookPreviewPages.length - 1) {
bookPreviewIndex += 1;
renderBookReaderPage();
}
});
})();
document.getElementById('filterRow').addEventListener('click', function(e) {
var btn = e.target.closest('.filter-chip');
if (!btn || loading) return;
currentFilter = btn.getAttribute('data-type') || '';
currentPage = 1;
document.querySelectorAll('.filter-chip').forEach(function(c) {
c.classList.toggle('active', c === btn);
});
loadWorks();
});
document.querySelector('.scope-tabs').addEventListener('click', function(e) {
var tab = e.target.closest('.scope-tab');
if (!tab || loading) return;
setListScope(tab.getAttribute('data-scope') || 'plaza');
});
document.getElementById('navMineBtn').addEventListener('click', function() {
if (loading) return;
setListScope('mine');
});
document.querySelector('.cd-nav__btn[href="/works-plaza.html"]').addEventListener('click', function(e) {
if (listScope === 'plaza') return;
e.preventDefault();
setListScope('plaza');
});
document.getElementById('pagerPrev').addEventListener('click', function() {
if (currentPage <= 1 || loading) return;
currentPage -= 1;
loadWorks();
});
document.getElementById('pagerNext').addEventListener('click', function() {
if (currentPage >= totalPages() || loading) return;
currentPage += 1;
loadWorks();
});
(function initNavUser() {
var loginBtn = document.getElementById('navLoginBtn');
var userBox = document.getElementById('navUser');
var token = localStorage.getItem('access_token');
if (!token) {
loginBtn.style.display = 'inline-flex';
userBox.style.display = 'none';
return;
}
loginBtn.style.display = 'none';
userBox.style.display = 'inline-flex';
document.getElementById('navUserName').textContent = localStorage.getItem('username') || '用户';
applyNavAvatar(document.getElementById('navAvatar'));
try {
var cachedPoints = localStorage.getItem('ai_points_balance');
document.getElementById('navScore').textContent = (cachedPoints != null && cachedPoints !== '') ? cachedPoints : '--';
} catch (e) {}
fetch(resolveBaseUrl() + '/api/ai-explorer/userinfo/', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({})
}).then(function(res) {
return res.json().catch(function() { return null; }).then(function(result) {
return { status: res.status, ok: res.ok, result: result };
});
}).then(function(payload) {
if (payload.status === 403 || payload.status === 401) {
showToast('登录失效,请重新登录', 'error');
try {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
} catch (e) {}
setTimeout(function() { goLogin(); }, 2000);
return;
}
if (!payload.ok || !payload.result || payload.result.success === false) {
var errMsg = (payload.result && (payload.result.message || payload.result.detail || payload.result.error)) ||
('获取用户信息失败' + (payload.status ? '' + payload.status + '' : ''));
showToast(errMsg, 'error');
return;
}
var data = (payload.result && payload.result.data) || payload.result || {};
var name = data.full_name || data.username || localStorage.getItem('username') || '用户';
document.getElementById('navUserName').textContent = name;
applyNavAvatar(document.getElementById('navAvatar'));
var score = data.score != null ? data.score : (data.points != null ? data.points : null);
var scoreEl = document.getElementById('navScore');
if (scoreEl && score != null) scoreEl.textContent = String(score);
}).catch(function(err) {
showToast((err && err.message) || '获取用户信息失败,请稍后重试', 'error');
});
})();
updateScopeUi();
loadWorks();
</script>
</body>
</html>