feat: 新增知资札记工具及相关功能

1. 新增知资札记完整站点:包含首页、详情页、管理后台
2. 添加站点图标、鉴权配置与后端API鉴权逻辑
3. 在市场页面新增知资札记入口跳转
4. 新增经典乡村音乐播放列表文件
5. 临时添加养生谚语批量导入脚本
This commit is contained in:
yangxiangyuan
2026-07-19 20:45:41 +08:00
parent 6a043687f5
commit 421a8aa1f7
20 changed files with 3752 additions and 14 deletions
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#162A46"/>
<text x="16" y="22" text-anchor="middle" font-size="18" font-family="sans-serif" fill="white" font-weight="bold">札</text>
</svg>

After

Width:  |  Height:  |  Size: 271 B

+2 -3
View File
@@ -25,6 +25,7 @@
<button id="m-tab-weekly" class="subtab">指数周报</button>
<button id="m-tab-news" class="subtab">全球新闻</button>
<button id="m-tab-wisdom" class="subtab">理论与实践</button>
<button id="m-tab-zhizi" class="subtab">知资札记</button>
<button id="m-tab-tools" class="subtab">工具箱</button>
<div id="m-status" class="status status-inline" style="display:none"></div>
</div>
@@ -125,9 +126,7 @@
</div>
</section>
<section id="m-panel-wisdom" class="panel" style="display:none">
<iframe id="wisdom-frame" src="/tools/markets/wallstreet_wisdom.html?v=10" style="width:100%;height:calc(100vh - 140px);border:none;"></iframe>
</section>
<!-- 理论与实践:点击tab直接新窗口打开 -->
<section id="m-panel-tools" class="panel" style="display:none">
<div class="tools-container">
+13 -11
View File
@@ -1710,6 +1710,7 @@ const switchPanel = which => {
const wkTab = el('m-tab-weekly')
const newsTab = el('m-tab-news')
const wisdomTab = el('m-tab-wisdom')
const zhiziTab = el('m-tab-zhizi')
const toolsTab = el('m-tab-tools')
const rt = el('m-panel-realtime')
const ar = el('m-panel-archive')
@@ -1720,36 +1721,35 @@ const switchPanel = which => {
const tools = el('m-panel-tools')
if (!rtTab || !arTab || !calTab || !wkTab || !rt || !ar || !cal || !wk) return
if (which === 'archive') {
rtTab.classList.remove('active'); arTab.classList.add('active'); calTab.classList.remove('active'); wkTab.classList.remove('active'); if (newsTab) newsTab.classList.remove('active'); if (wisdomTab) wisdomTab.classList.remove('active'); if (toolsTab) toolsTab.classList.remove('active')
rtTab.classList.remove('active'); arTab.classList.add('active'); calTab.classList.remove('active'); wkTab.classList.remove('active'); if (newsTab) newsTab.classList.remove('active'); if (wisdomTab) wisdomTab.classList.remove('active'); if (zhiziTab) zhiziTab.classList.remove('active'); if (toolsTab) toolsTab.classList.remove('active')
rt.style.display = 'none'; ar.style.display = ''; cal.style.display = 'none'; wk.style.display = 'none'; if (news) news.style.display = 'none'; if (wisdom) wisdom.style.display = 'none'; if (tools) tools.style.display = 'none'
loadArchive()
} else if (which === 'calendar') {
rtTab.classList.remove('active'); arTab.classList.remove('active'); calTab.classList.add('active'); wkTab.classList.remove('active'); if (newsTab) newsTab.classList.remove('active'); if (wisdomTab) wisdomTab.classList.remove('active'); if (toolsTab) toolsTab.classList.remove('active')
rtTab.classList.remove('active'); arTab.classList.remove('active'); calTab.classList.add('active'); wkTab.classList.remove('active'); if (newsTab) newsTab.classList.remove('active'); if (wisdomTab) wisdomTab.classList.remove('active'); if (zhiziTab) zhiziTab.classList.remove('active'); if (toolsTab) toolsTab.classList.remove('active')
rt.style.display = 'none'; ar.style.display = 'none'; cal.style.display = ''; wk.style.display = 'none'; if (news) news.style.display = 'none'; if (wisdom) wisdom.style.display = 'none'; if (tools) tools.style.display = 'none'
if (!calendarState.month || !Array.isArray(calendarState.days) || calendarState.days.length === 0) loadSecurityCalendar(false)
} else if (which === 'weekly') {
rtTab.classList.remove('active'); arTab.classList.remove('active'); calTab.classList.remove('active'); wkTab.classList.add('active'); if (newsTab) newsTab.classList.remove('active'); if (wisdomTab) wisdomTab.classList.remove('active'); if (toolsTab) toolsTab.classList.remove('active')
rtTab.classList.remove('active'); arTab.classList.remove('active'); calTab.classList.remove('active'); wkTab.classList.add('active'); if (newsTab) newsTab.classList.remove('active'); if (wisdomTab) wisdomTab.classList.remove('active'); if (zhiziTab) zhiziTab.classList.remove('active'); if (toolsTab) toolsTab.classList.remove('active')
rt.style.display = 'none'; ar.style.display = 'none'; cal.style.display = 'none'; wk.style.display = ''; if (news) news.style.display = 'none'; if (wisdom) wisdom.style.display = 'none'; if (tools) tools.style.display = 'none'
if (!weeklyState.weekStart || !Array.isArray(weeklyState.rows) || weeklyState.rows.length === 0) {
setWeeklyIdleState('请选择周后点击“加载”读取历史,或点“刷新并补存”主动更新。', { clearData: false })
}
} else if (which === 'news') {
rtTab.classList.remove('active'); arTab.classList.remove('active'); calTab.classList.remove('active'); wkTab.classList.remove('active'); if (wisdomTab) wisdomTab.classList.remove('active'); if (toolsTab) toolsTab.classList.remove('active')
rtTab.classList.remove('active'); arTab.classList.remove('active'); calTab.classList.remove('active'); wkTab.classList.remove('active'); if (wisdomTab) wisdomTab.classList.remove('active'); if (zhiziTab) zhiziTab.classList.remove('active'); if (toolsTab) toolsTab.classList.remove('active')
if (newsTab) newsTab.classList.add('active')
rt.style.display = 'none'; ar.style.display = 'none'; cal.style.display = 'none'; wk.style.display = 'none'; if (news) news.style.display = ''; if (wisdom) wisdom.style.display = 'none'; if (tools) tools.style.display = 'none'
loadGlobalNews(false)
} else if (which === 'wisdom') {
rtTab.classList.remove('active'); arTab.classList.remove('active'); calTab.classList.remove('active'); wkTab.classList.remove('active')
if (newsTab) newsTab.classList.remove('active')
if (wisdomTab) wisdomTab.classList.add('active')
rt.style.display = 'none'; ar.style.display = 'none'; cal.style.display = 'none'; wk.style.display = 'none'
if (news) news.style.display = 'none'
if (wisdom) wisdom.style.display = ''
if (tools) tools.style.display = 'none'
window.open('https://traesite.umersoft.com:8975/tools/markets/wallstreet_wisdom.html', '_blank')
return
} else if (which === 'zhizi') {
window.open('https://traesite.umersoft.com:8975/tools/zhizi_notes/index.html', '_blank')
return
} else if (which === 'tools') {
rtTab.classList.remove('active'); arTab.classList.remove('active'); calTab.classList.remove('active'); wkTab.classList.remove('active')
if (newsTab) newsTab.classList.remove('active')
if (wisdomTab) wisdomTab.classList.remove('active')
if (zhiziTab) zhiziTab.classList.remove('active')
if (toolsTab) toolsTab.classList.add('active')
rt.style.display = 'none'; ar.style.display = 'none'; cal.style.display = 'none'; wk.style.display = 'none'
if (news) news.style.display = 'none'
@@ -1758,6 +1758,7 @@ const switchPanel = which => {
} else {
rtTab.classList.add('active'); arTab.classList.remove('active'); calTab.classList.remove('active'); wkTab.classList.remove('active'); if (newsTab) newsTab.classList.remove('active')
if (wisdomTab) wisdomTab.classList.remove('active')
if (zhiziTab) zhiziTab.classList.remove('active')
if (toolsTab) toolsTab.classList.remove('active')
rt.style.display = ''; ar.style.display = 'none'; cal.style.display = 'none'; wk.style.display = 'none'; if (news) news.style.display = 'none'
if (wisdom) wisdom.style.display = 'none'
@@ -1790,6 +1791,7 @@ document.addEventListener('DOMContentLoaded', () => {
const stWk = el('m-tab-weekly'); if (stWk) stWk.addEventListener('click', () => switchPanel('weekly'))
const stNews = el('m-tab-news'); if (stNews) stNews.addEventListener('click', () => switchPanel('news'))
const stWisdom = el('m-tab-wisdom'); if (stWisdom) stWisdom.addEventListener('click', () => switchPanel('wisdom'))
const stZhizi = el('m-tab-zhizi'); if (stZhizi) stZhizi.addEventListener('click', () => switchPanel('zhizi'))
const stTools = el('m-tab-tools'); if (stTools) stTools.addEventListener('click', () => switchPanel('tools'))
const cbp = el('cal-btn-prev'); if (cbp) cbp.addEventListener('click', () => navigateMonthCalendar(-1))
const cbc = el('cal-btn-current'); if (cbc) cbc.addEventListener('click', () => { const i = el('cal-month'); if (i) i.value = todayMonthLocal(); loadSecurityCalendar(false) })
@@ -0,0 +1,182 @@
[
{
"title": "He Stopped Loving Her Today",
"artist": "George Jones",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/01%20-%20He%20Stopped%20Loving%20Her%20Today%20-%20George%20Jones.mp3",
"cover": ""
},
{
"title": "I Walk the Line",
"artist": "Johnny Cash",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/02%20-%20I%20Walk%20the%20Line%20-%20Johnny%20Cash.mp3",
"cover": ""
},
{
"title": "Coal Miner's Daughter",
"artist": "Loretta Lynn",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/04%20-%20Coal%20Miner%27s%20Daughter%20-%20Loretta%20Lynn.mp3",
"cover": ""
},
{
"title": "Your Cheatin' Heart",
"artist": "Hank Williams",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/05%20-%20Your%20Cheatin%27%20Heart%20-%20Hank%20Williams.mp3",
"cover": ""
},
{
"title": "The Dance",
"artist": "Garth Brooks",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/07%20-%20The%20Dance%20-%20Garth%20Brooks.mp3",
"cover": ""
},
{
"title": "Friends in Low Places",
"artist": "Garth Brooks",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/09%20-%20Friends%20in%20Low%20Places%20-%20Garth%20Brooks.mp3",
"cover": ""
},
{
"title": "Okie from Muskogee",
"artist": "Merle Haggard",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/13%20-%20Okie%20from%20Muskogee%20-%20Merle%20Haggard.mp3",
"cover": ""
},
{
"title": "On the Road Again",
"artist": "Willie Nelson",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/14%20-%20On%20the%20Road%20Again%20-%20Willie%20Nelson.mp3",
"cover": ""
},
{
"title": "The Gambler",
"artist": "Kenny Rogers",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/16%20-%20The%20Gambler%20-%20Kenny%20Rogers.mp3",
"cover": ""
},
{
"title": "I Fall to Pieces",
"artist": "Patsy Cline",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/17%20-%20I%20Fall%20to%20Pieces%20-%20Patsy%20Cline.mp3",
"cover": ""
},
{
"title": "Whiskey River",
"artist": "Willie Nelson",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/18%20-%20Whiskey%20River%20-%20Willie%20Nelson.mp3",
"cover": ""
},
{
"title": "A Boy Named Sue",
"artist": "Johnny Cash",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/19%20-%20A%20Boy%20Named%20Sue%20-%20Johnny%20Cash.mp3",
"cover": ""
},
{
"title": "Lucille",
"artist": "Kenny Rogers",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/20%20-%20Lucille%20-%20Kenny%20Rogers.mp3",
"cover": ""
},
{
"title": "Harper Valley PTA",
"artist": "Jeannie C. Riley",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/21%20-%20Harper%20Valley%20PTA%20-%20Jeannie%20C.%20Riley.mp3",
"cover": ""
},
{
"title": "The Night the Lights Went Out in Georgia",
"artist": "Vicki Lawrence",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/22%20-%20The%20Night%20the%20Lights%20Went%20Out%20in%20Georgia%20-%20Vicki%20Lawrence.mp3",
"cover": ""
},
{
"title": "Thank God I'm a Country Boy",
"artist": "John Denver",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/25%20-%20Thank%20God%20I%27m%20a%20Country%20Boy%20-%20John%20Denver.mp3",
"cover": ""
},
{
"title": "Rocky Top",
"artist": "Osborne Brothers",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/26%20-%20Rocky%20Top%20-%20Osborne%20Brothers.mp3",
"cover": ""
},
{
"title": "Achy Breaky Heart",
"artist": "Billy Ray Cyrus",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/29%20-%20Achy%20Breaky%20Heart%20-%20Billy%20Ray%20Cyrus.mp3",
"cover": ""
},
{
"title": "Amazed",
"artist": "Lonestar",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/30%20-%20Amazed%20-%20Lonestar.mp3",
"cover": ""
},
{
"title": "Breathe",
"artist": "Faith Hill",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/31%20-%20Breathe%20-%20Faith%20Hill.mp3",
"cover": ""
},
{
"title": "Before He Cheats",
"artist": "Carrie Underwood",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/32%20-%20Before%20He%20Cheats%20-%20Carrie%20Underwood.mp3",
"cover": ""
},
{
"title": "Need You Now",
"artist": "Lady Antebellum",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/33%20-%20Need%20You%20Now%20-%20Lady%20Antebellum.mp3",
"cover": ""
},
{
"title": "Body Like a Back Road",
"artist": "Sam Hunt",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/35%20-%20Body%20Like%20a%20Back%20Road%20-%20Sam%20Hunt.mp3",
"cover": ""
},
{
"title": "Meant to Be",
"artist": "Bebe Rexha Florida Georgia Line",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/36%20-%20Meant%20to%20Be%20-%20Bebe%20Rexha%20Florida%20Georgia%20Line.mp3",
"cover": ""
},
{
"title": "Live Like You Were Dying",
"artist": "Tim McGraw",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/40%20-%20Live%20Like%20You%20Were%20Dying%20-%20Tim%20McGraw.mp3",
"cover": ""
},
{
"title": "Chicken Fried",
"artist": "Zac Brown Band",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/42%20-%20Chicken%20Fried%20-%20Zac%20Brown%20Band.mp3",
"cover": ""
},
{
"title": "Five O'Clock Somewhere",
"artist": "Alan Jackson Jimmy Buffett",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/43%20-%20Five%20O%27Clock%20Somewhere%20-%20Alan%20Jackson%20Jimmy%20Buffett.mp3",
"cover": ""
},
{
"title": "Redneck Woman",
"artist": "Gretchen Wilson",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/44%20-%20Redneck%20Woman%20-%20Gretchen%20Wilson.mp3",
"cover": ""
},
{
"title": "Check Yes or No",
"artist": "George Strait",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/46%20-%20Check%20Yes%20or%20No%20-%20George%20Strait.mp3",
"cover": ""
},
{
"title": "I Can Love You Like That",
"artist": "John Michael Montgomery",
"src": "https://android-o1-images.oss-accelerate.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/external_storage/classic-country-music/mp3/50%20-%20I%20Can%20Love%20You%20Like%20That%20-%20John%20Michael%20Montgomery.mp3",
"cover": ""
}
]
+245
View File
@@ -0,0 +1,245 @@
/* ═══════════════════════════════════════════════════════════
知资札记 - 后台CMS管理样式
主色:#162A46 墨蓝 | 删除红:#D94452
═══════════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; }
body {
font-family: 'Microsoft YaHei', 'Source Han Sans CN', -apple-system, sans-serif;
color: #333; background: #F5F6FA; line-height: 1.6;
}
a { text-decoration: none; color: inherit; }
button { cursor: pointer; font-family: inherit; border: none; outline: none; }
input, select { font-family: inherit; outline: none; }
/* ── 布局 ── */
.za-layout { display: flex; min-height: 100vh; }
/* ── 左侧菜单 ── */
.za-sidebar {
width: 220px; min-width: 220px; background: #162A46; color: #fff;
padding: 20px 0; position: fixed; top: 0; left: 0; bottom: 0;
overflow-y: auto; z-index: 100;
}
.za-sidebar .sb-logo {
font-size: 1.1rem; font-weight: 700; letter-spacing: 2px;
padding: 0 20px 20px; border-bottom: 1px solid rgba(255,255,255,0.1);
margin-bottom: 12px;
}
.za-sidebar .sb-menu { list-style: none; }
.za-sidebar .sb-menu li a {
display: flex; align-items: center; gap: 8px;
padding: 10px 20px; color: rgba(255,255,255,0.7);
font-size: 0.9rem; transition: all 0.2s;
}
.za-sidebar .sb-menu li a:hover,
.za-sidebar .sb-menu li a.active { color: #fff; background: rgba(255,255,255,0.08); }
.za-sidebar .sb-menu .sub-menu { padding-left: 16px; }
.za-sidebar .sb-menu .sub-menu a { font-size: 0.82rem; padding: 7px 20px; }
/* ── 主内容区 ── */
.za-main { flex: 1; margin-left: 220px; padding: 20px; }
/* ── 顶部操作栏 ── */
.za-toolbar {
background: #fff; border-radius: 8px; padding: 16px;
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.06);
display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
}
.za-toolbar .search-box {
display: flex; align-items: center; border: 1px solid #E0E4E8;
border-radius: 6px; padding: 6px 12px; flex: 1; min-width: 200px;
}
.za-toolbar .search-box input {
border: none; flex: 1; font-size: 0.85rem; background: transparent;
}
.za-toolbar .search-box button { background: transparent; color: #999; font-size: 0.9rem; }
.za-toolbar select {
border: 1px solid #E0E4E8; border-radius: 6px;
padding: 7px 12px; font-size: 0.85rem; background: #fff;
}
.za-toolbar .btn {
padding: 7px 16px; border-radius: 6px; font-size: 0.85rem;
font-weight: 500; transition: all 0.2s;
}
.za-toolbar .btn-primary { background: #162A46; color: #fff; }
.za-toolbar .btn-primary:hover { background: #1E3A5F; }
.za-toolbar .btn-danger { background: #D94452; color: #fff; }
.za-toolbar .btn-danger:hover { background: #C0392B; }
.za-toolbar .btn-outline { background: #fff; color: #162A46; border: 1px solid #162A46; }
.za-toolbar .btn-outline:hover { background: #F0F4F8; }
.za-toolbar .btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* ── 表格 ── */
.za-table-wrap {
background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.06);
overflow: hidden;
}
.za-table {
width: 100%; border-collapse: collapse; font-size: 0.85rem;
}
.za-table thead { background: #F8FAFB; }
.za-table th {
padding: 12px 14px; text-align: left; font-weight: 600;
color: #666; border-bottom: 2px solid #E0E4E8;
white-space: nowrap; font-size: 0.8rem;
}
.za-table td {
padding: 10px 14px; border-bottom: 1px solid #F0F2F5;
vertical-align: middle;
}
.za-table tbody tr:hover { background: #F8FAFB; }
.za-table .cell-title {
max-width: 240px; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; cursor: default;
}
.za-table .cell-cover {
width: 60px; height: 40px; border-radius: 4px; overflow: hidden;
background: #E8EDF2; display: flex; align-items: center; justify-content: center;
}
.za-table .cell-cover img { width: 100%; height: 100%; object-fit: cover; }
.za-table .cell-cover .text-icon {
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
background: #162A46; color: #fff; font-size: 0.8rem; font-weight: 700;
}
.za-table .type-badge {
display: inline-block; padding: 2px 8px; border-radius: 10px;
font-size: 0.72rem; font-weight: 500;
}
.za-table .type-badge.text { background: #E8F0FE; color: #1E3A5F; }
.za-table .type-badge.image { background: #E8F5F0; color: #467974; }
.za-table .status-badge {
display: inline-block; padding: 2px 10px; border-radius: 10px;
font-size: 0.72rem; font-weight: 500;
}
.za-table .status-badge.normal { background: #E8F5E9; color: #2E7D32; }
.za-table .status-badge.offline { background: #FFF3E0; color: #E65100; }
.za-table .actions { display: flex; gap: 6px; }
.za-table .actions button {
padding: 4px 10px; border-radius: 4px; font-size: 0.75rem;
transition: all 0.2s;
}
.za-table .actions .act-preview { background: #E8F0FE; color: #1E3A5F; }
.za-table .actions .act-preview:hover { background: #D0E0FC; }
.za-table .actions .act-edit { background: #E8F5F0; color: #467974; }
.za-table .actions .act-edit:hover { background: #D0EBE0; }
.za-table .actions .act-toggle { background: #FFF3E0; color: #E65100; }
.za-table .actions .act-toggle:hover { background: #FFE0B2; }
.za-table .actions .act-delete { background: #FFEBEE; color: #D94452; }
.za-table .actions .act-delete:hover { background: #FFCDD2; }
/* ── 分页 ── */
.za-pagination {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 16px; background: #fff; border-top: 1px solid #F0F2F5;
font-size: 0.85rem; color: #666;
}
.za-pagination .page-btns { display: flex; gap: 4px; align-items: center; }
.za-pagination .page-btns button {
padding: 5px 10px; border-radius: 4px; font-size: 0.8rem;
background: #fff; border: 1px solid #E0E4E8; color: #333;
transition: all 0.2s;
}
.za-pagination .page-btns button:hover:not(:disabled) { background: #F0F4F8; }
.za-pagination .page-btns button.active { background: #162A46; color: #fff; border-color: #162A46; }
.za-pagination .page-btns button:disabled { opacity: 0.4; cursor: not-allowed; }
.za-pagination select { border: 1px solid #E0E4E8; border-radius: 4px; padding: 4px 8px; }
/* ── 弹窗 ── */
.za-modal-overlay {
position: fixed; inset: 0; z-index: 5000;
background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center;
}
.za-modal-overlay.hidden { display: none; }
.za-modal {
background: #fff; border-radius: 12px; padding: 24px;
max-width: 460px; width: 90%; box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
.za-modal h3 { margin-bottom: 12px; color: #333; font-size: 1.05rem; }
.za-modal .modal-body { font-size: 0.85rem; color: #666; margin-bottom: 20px; line-height: 1.6; }
.za-modal .modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
.za-modal .btn {
padding: 8px 20px; border-radius: 6px; font-size: 0.85rem; font-weight: 500;
}
.za-modal .btn-primary { background: #162A46; color: #fff; }
.za-modal .btn-primary:hover { background: #1E3A5F; }
.za-modal .btn-danger { background: #D94452; color: #fff; }
.za-modal .btn-danger:hover { background: #C0392B; }
.za-modal .btn-cancel { background: #F0F2F5; color: #666; }
.za-modal .btn-cancel:hover { background: #E0E4E8; }
/* ── Toast ── */
.za-toast {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
z-index: 9999; padding: 10px 24px; border-radius: 8px;
font-size: 0.85rem; color: #fff; opacity: 0;
transition: opacity 0.3s; pointer-events: none;
}
.za-toast.show { opacity: 1; }
.za-toast.info { background: #3B82F6; }
.za-toast.success { background: #10B981; }
.za-toast.error { background: #EF4444; }
/* ── 新增/编辑表单弹窗 ── */
.za-modal.form-modal { max-width: 700px; max-height: 85vh; overflow-y: auto; }
.za-modal .form-group { margin-bottom: 14px; }
.za-modal .form-group label {
display: block; font-size: 0.82rem; font-weight: 600;
color: #555; margin-bottom: 4px;
}
.za-modal .form-group input,
.za-modal .form-group select,
.za-modal .form-group textarea {
width: 100%; padding: 8px 12px; border: 1px solid #E0E4E8;
border-radius: 6px; font-size: 0.85rem; transition: border-color 0.2s;
}
.za-modal .form-group input:focus,
.za-modal .form-group textarea:focus,
.za-modal .form-group select:focus { border-color: #162A46; }
.za-modal .form-group textarea { min-height: 200px; resize: vertical; font-family: inherit; }
.za-modal .form-group .tag-input {
display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
border: 1px solid #E0E4E8; border-radius: 6px; padding: 6px;
min-height: 38px;
}
.za-modal .form-group .tag-input .tag-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 8px; border-radius: 10px; font-size: 0.75rem;
background: #E8F0FE; color: #1E3A5F;
}
.za-modal .form-group .tag-input .tag-chip .remove-tag {
background: transparent; color: #1E3A5F; font-size: 0.8rem; padding: 0; cursor: pointer;
}
.za-modal .form-group .tag-input input {
border: none; flex: 1; min-width: 80px; padding: 4px; font-size: 0.82rem;
}
/* ── 图片上传预览 ── */
.za-modal .form-group .upload-area { margin-top: 4px; }
.za-modal .form-group .upload-preview {
display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px;
}
.za-modal .form-group .preview-item {
position: relative; width: 80px; height: 80px;
border-radius: 6px; overflow: hidden; border: 1px solid #E0E4E8;
}
.za-modal .form-group .preview-item img {
width: 100%; height: 100%; object-fit: cover;
}
.za-modal .form-group .preview-item .preview-remove {
position: absolute; top: 2px; right: 2px;
width: 18px; height: 18px; border-radius: 50%;
background: rgba(0,0,0,0.5); color: #fff;
font-size: 0.7rem; display: flex; align-items: center; justify-content: center;
padding: 0; line-height: 1;
}
/* ── 响应式 ── */
@media (max-width: 900px) {
.za-sidebar { display: none; }
.za-main { margin-left: 0; }
.za-table { font-size: 0.78rem; }
.za-table th, .za-table td { padding: 8px 8px; }
.za-table .cell-title { max-width: 120px; }
}
+104
View File
@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>知资札记 · 后台管理</title>
<link rel="icon" href="/icons/zhizi_notes.svg" type="image/svg+xml" />
<link rel="stylesheet" href="/common/common.css" />
<link rel="stylesheet" href="/tools/zhizi_notes/admin.css" />
</head>
<body>
<!-- Toast -->
<div id="za-toast" class="za-toast"></div>
<!-- ═══════════════ 布局 ═══════════════ -->
<div class="za-layout">
<!-- 左侧菜单 -->
<aside class="za-sidebar">
<div class="sb-logo">知资札记</div>
<ul class="sb-menu">
<li><a href="#">📊 数据仪表盘</a></li>
<li><a href="#" class="active">📄 图文内容管理</a>
<ul class="sub-menu">
<li><a href="#" class="active">全部存档列表</a></li>
<li><a href="#" id="za-menu-new">+ 新增存档</a></li>
<li><a href="#">🖼 图片资源库</a></li>
</ul>
</li>
<li><a href="#">🏷 分类标签管理</a></li>
<li><a href="#">👤 用户管理</a></li>
<li><a href="#">🕷 爬虫任务日志</a></li>
<li><a href="#">⚙ 系统设置</a></li>
</ul>
</aside>
<!-- 主内容区 -->
<main class="za-main">
<!-- 顶部操作栏 -->
<div class="za-toolbar">
<div class="search-box">
<input id="za-search" type="text" placeholder="搜索标题/标签…" />
<button id="za-search-btn">🔍</button>
</div>
<select id="za-filter-category">
<option value="">全部分类</option>
</select>
<select id="za-filter-type">
<option value="">全部类型</option>
<option value="text">纯文字</option>
<option value="image">图文</option>
</select>
<select id="za-filter-status">
<option value="">全部状态</option>
<option value="normal">正常</option>
<option value="offline">已下架</option>
</select>
<button class="btn btn-outline" id="za-btn-batch-offline" disabled>批量下架</button>
<button class="btn btn-danger" id="za-btn-batch-delete" disabled>批量删除</button>
<button class="btn btn-outline" id="za-btn-batch-export" disabled>批量导出</button>
<button class="btn btn-primary" id="za-btn-new">+ 新增存档</button>
</div>
<!-- 数据表格 -->
<div class="za-table-wrap">
<table class="za-table">
<thead>
<tr>
<th style="width:40px"><input type="checkbox" id="za-select-all" title="全选" /></th>
<th style="width:70px">封面</th>
<th>标题</th>
<th style="width:90px">分类</th>
<th style="width:90px">类型</th>
<th style="width:100px">来源</th>
<th style="width:130px">创建时间</th>
<th style="width:60px">阅读</th>
<th style="width:70px">状态</th>
<th style="width:200px">操作</th>
</tr>
</thead>
<tbody id="za-tbody"></tbody>
</table>
<!-- 分页 -->
<div class="za-pagination" id="za-pagination">
<div>
<span id="za-page-info">共 0 条</span>
<span style="margin-left:12px">每页</span>
<select id="za-page-size">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<span>条</span>
</div>
<div class="page-btns" id="za-page-btns"></div>
</div>
</div>
</main>
</div>
<script src="/tools/zhizi_notes/admin.js"></script>
</body>
</html>
+696
View File
@@ -0,0 +1,696 @@
/**
* 知资札记 - 后台CMS管理逻辑
* 图文内容管理列表页面
*/
(function () {
'use strict';
const API_BASE = '/api/zhizi_notes';
const $ = function (sel) { return document.querySelector(sel); };
const $$ = function (sel) { return document.querySelectorAll(sel); };
// ── 状态 ──
var adminState = {
page: 1,
pageSize: 20,
totalPages: 0,
total: 0,
filter: { search: '', category: '', contentType: '', status: '' },
selectedIds: new Set(),
editingId: null
};
// ── Toast ──
function showToast(msg, type) {
type = type || 'info';
var t = $('#za-toast');
if (!t) return;
t.textContent = msg;
t.className = 'za-toast ' + type + ' show';
clearTimeout(t._timer);
t._timer = setTimeout(function () { t.classList.remove('show'); }, type === 'error' ? 4000 : 2000);
}
// ── 链接清洗 ──
function clearAllLinks(html) {
if (!html || typeof html !== 'string') return '';
return html.replace(/<a\b[^>]*>(.*?)<\/a>/gi, '$1');
}
// ── API ──
async function apiGet(path) {
try {
var res = await fetch(API_BASE + path, { credentials: 'same-origin' });
return await res.json();
} catch (e) { return { ok: false, error: e.message }; }
}
async function apiPost(path, body) {
try {
var res = await fetch(API_BASE + path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(body)
});
return await res.json();
} catch (e) { return { ok: false, error: e.message }; }
}
async function apiPut(path, body) {
try {
var res = await fetch(API_BASE + path, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(body)
});
return await res.json();
} catch (e) { return { ok: false, error: e.message }; }
}
async function apiDelete(path) {
try {
var res = await fetch(API_BASE + path, { method: 'DELETE', credentials: 'same-origin' });
return await res.json();
} catch (e) { return { ok: false, error: e.message }; }
}
// ── 加载列表 ──
async function loadTableData() {
var params = new URLSearchParams();
params.set('page', adminState.page);
params.set('pageSize', adminState.pageSize);
if (adminState.filter.search) params.set('search', adminState.filter.search);
if (adminState.filter.category) params.set('category', adminState.filter.category);
if (adminState.filter.contentType) params.set('contentType', adminState.filter.contentType);
if (adminState.filter.status) params.set('status', adminState.filter.status);
var data = await apiGet('/articles?' + params.toString());
if (!data.ok) { showToast('加载失败:' + (data.error || '未知错误'), 'error'); return; }
adminState.totalPages = data.pagination ? data.pagination.totalPages : 0;
adminState.total = data.pagination ? data.pagination.total : 0;
adminState.selectedIds.clear();
renderTable(data.data || []);
renderPagination();
}
// ── 渲染表格 ──
function renderTable(articles) {
var tbody = $('#za-tbody');
if (!tbody) return;
tbody.innerHTML = '';
if (articles.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" style="text-align:center;padding:40px;color:#999">暂无数据</td></tr>';
return;
}
articles.forEach(function (a) {
var tr = document.createElement('tr');
// 复选框
var tdCb = document.createElement('td');
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.setAttribute('data-id', a.id);
cb.addEventListener('change', function () { toggleSelect(a.id, cb.checked); });
tdCb.appendChild(cb);
// 封面
var tdCover = document.createElement('td');
var coverDiv = document.createElement('div');
coverDiv.className = 'cell-cover';
if (a.cover_url) {
var img = document.createElement('img');
img.src = a.cover_url; img.alt = '';
coverDiv.appendChild(img);
} else {
coverDiv.innerHTML = '<div class="text-icon">文</div>';
}
tdCover.appendChild(coverDiv);
// 标题
var tdTitle = document.createElement('td');
var titleDiv = document.createElement('div');
titleDiv.className = 'cell-title';
titleDiv.textContent = a.title;
titleDiv.title = a.title;
tdTitle.appendChild(titleDiv);
// 分类
var tdCat = document.createElement('td');
tdCat.textContent = a.category;
// 内容类型
var tdType = document.createElement('td');
var typeSpan = document.createElement('span');
typeSpan.className = 'type-badge ' + (a.content_type === 'text' ? 'text' : 'image');
var imgCount = a.image_count || 0;
typeSpan.textContent = a.content_type === 'text' ? '纯文字' : '图文(' + imgCount + '张)';
tdType.appendChild(typeSpan);
// 来源
var tdSrc = document.createElement('td');
tdSrc.textContent = a.source || '-';
tdSrc.style.maxWidth = '100px';
tdSrc.style.overflow = 'hidden';
tdSrc.style.textOverflow = 'ellipsis';
tdSrc.style.whiteSpace = 'nowrap';
// 创建时间
var tdTime = document.createElement('td');
tdTime.textContent = (a.created_at || '').slice(0, 16);
tdTime.style.whiteSpace = 'nowrap';
// 阅读量
var tdViews = document.createElement('td');
tdViews.textContent = a.views || 0;
// 状态
var tdStatus = document.createElement('td');
var statusSpan = document.createElement('span');
statusSpan.className = 'status-badge ' + (a.status === 'normal' ? 'normal' : 'offline');
statusSpan.textContent = a.status === 'normal' ? '正常' : '已下架';
tdStatus.appendChild(statusSpan);
// 操作
var tdActions = document.createElement('td');
tdActions.className = 'actions';
var btnPreview = document.createElement('button');
btnPreview.className = 'act-preview';
btnPreview.textContent = '预览';
btnPreview.addEventListener('click', function () { window.open('/tools/zhizi_notes/detail.html?id=' + a.id, '_blank'); });
var btnEdit = document.createElement('button');
btnEdit.className = 'act-edit';
btnEdit.textContent = '编辑';
btnEdit.addEventListener('click', function () { openEditForm(a.id); });
var btnToggle = document.createElement('button');
btnToggle.className = 'act-toggle';
btnToggle.textContent = a.status === 'normal' ? '下架' : '上架';
btnToggle.addEventListener('click', function () { toggleStatus(a.id, a.status); });
var btnDel = document.createElement('button');
btnDel.className = 'act-delete';
btnDel.textContent = '删除';
btnDel.addEventListener('click', function () { confirmDelete(a.id, a.title); });
tdActions.appendChild(btnPreview);
tdActions.appendChild(btnEdit);
tdActions.appendChild(btnToggle);
tdActions.appendChild(btnDel);
tr.appendChild(tdCb);
tr.appendChild(tdCover);
tr.appendChild(tdTitle);
tr.appendChild(tdCat);
tr.appendChild(tdType);
tr.appendChild(tdSrc);
tr.appendChild(tdTime);
tr.appendChild(tdViews);
tr.appendChild(tdStatus);
tr.appendChild(tdActions);
tbody.appendChild(tr);
});
}
// ── 分页 ──
function renderPagination() {
var container = $('#za-pagination');
if (!container) return;
var info = $('#za-page-info');
if (info) info.textContent = '共 ' + adminState.total + ' 条,第 ' + adminState.page + '/' + adminState.totalPages + ' 页';
var btns = $('#za-page-btns');
if (!btns) return;
btns.innerHTML = '';
var total = adminState.totalPages;
var cur = adminState.page;
var prevBtn = document.createElement('button');
prevBtn.textContent = '上一页';
prevBtn.disabled = cur <= 1;
prevBtn.addEventListener('click', function () { if (cur > 1) { adminState.page--; loadTableData(); } });
btns.appendChild(prevBtn);
var start = Math.max(1, cur - 2);
var end = Math.min(total, cur + 2);
for (var i = start; i <= end; i++) {
var pageBtn = document.createElement('button');
pageBtn.textContent = i;
if (i === cur) pageBtn.classList.add('active');
pageBtn.addEventListener('click', function (p) {
return function () { adminState.page = p; loadTableData(); };
}(i));
btns.appendChild(pageBtn);
}
var nextBtn = document.createElement('button');
nextBtn.textContent = '下一页';
nextBtn.disabled = cur >= total;
nextBtn.addEventListener('click', function () { if (cur < total) { adminState.page++; loadTableData(); } });
btns.appendChild(nextBtn);
}
// ── 选择行 ──
function toggleSelect(id, checked) {
if (checked) adminState.selectedIds.add(id);
else adminState.selectedIds.delete(id);
updateBatchBtns();
}
function toggleSelectAll(checked) {
$$('#za-tbody input[type="checkbox"]').forEach(function (cb) {
cb.checked = checked;
var id = parseInt(cb.getAttribute('data-id'));
if (checked) adminState.selectedIds.add(id);
else adminState.selectedIds.delete(id);
});
updateBatchBtns();
}
function updateBatchBtns() {
var hasSelection = adminState.selectedIds.size > 0;
var btns = $$('.za-toolbar .btn:disabled, .za-toolbar .btn:not(:disabled)');
$$('#za-btn-batch-offline, #za-btn-batch-delete, #za-btn-batch-export').forEach(function (btn) {
btn.disabled = !hasSelection;
});
}
// ── 批量操作 ──
async function batchAction(action) {
if (adminState.selectedIds.size === 0) { showToast('请先选择文章', 'info'); return; }
var ids = Array.from(adminState.selectedIds);
var labels = { delete: '删除', offline: '下架', online: '上架' };
var label = labels[action] || action;
if (action === 'delete') {
showConfirm('确认批量' + label, '确定要批量' + label + ' ' + ids.length + ' 篇文章吗?此操作不可恢复。', async function () {
var data = await apiPost('/articles/batch', { ids: ids, action: action });
if (data.ok) { showToast('批量' + label + '成功', 'success'); loadTableData(); }
else showToast('操作失败:' + (data.error || '未知错误'), 'error');
});
} else {
var data = await apiPost('/articles/batch', { ids: ids, action: action });
if (data.ok) { showToast('批量' + label + '成功', 'success'); loadTableData(); }
else showToast('操作失败:' + (data.error || '未知错误'), 'error');
}
}
// ── 状态切换 ──
async function toggleStatus(id, currentStatus) {
var newStatus = currentStatus === 'normal' ? 'offline' : 'normal';
var label = newStatus === 'normal' ? '上架' : '下架';
var data = await apiPut('/articles/' + id, { status: newStatus });
if (data.ok) { showToast(label + '成功', 'success'); loadTableData(); }
else showToast('操作失败:' + (data.error || '未知错误'), 'error');
}
// ── 删除确认 ──
function confirmDelete(id, title) {
showConfirm('确认删除', '确定要永久删除文章「' + title + '」吗?关联图片也会一并删除,此操作不可恢复。', async function () {
var data = await apiDelete('/articles/' + id);
if (data.ok) { showToast('删除成功', 'success'); loadTableData(); }
else showToast('删除失败:' + (data.error || '未知错误'), 'error');
});
}
// ── 确认弹窗 ──
function showConfirm(title, message, onConfirm) {
var overlay = document.createElement('div');
overlay.className = 'za-modal-overlay';
overlay.innerHTML = '<div class="za-modal">' +
'<h3>' + escapeHtml(title) + '</h3>' +
'<div class="modal-body">' + escapeHtml(message) + '</div>' +
'<div class="modal-actions">' +
'<button class="btn btn-cancel" id="za-confirm-cancel">取消</button>' +
'<button class="btn btn-danger" id="za-confirm-ok">确认删除</button>' +
'</div></div>';
document.body.appendChild(overlay);
overlay.querySelector('#za-confirm-cancel').addEventListener('click', function () { overlay.remove(); });
overlay.querySelector('#za-confirm-ok').addEventListener('click', function () {
overlay.remove();
if (onConfirm) onConfirm();
});
overlay.addEventListener('click', function (e) { if (e.target === overlay) overlay.remove(); });
}
// ── 上传文件 ──
async function uploadFiles(files, articleId) {
var formData = new FormData();
formData.append('articleId', articleId || 0);
for (var i = 0; i < files.length; i++) {
formData.append('images', files[i]);
}
try {
var res = await fetch(API_BASE + '/upload', { method: 'POST', credentials: 'same-origin', body: formData });
return await res.json();
} catch (e) { return { ok: false, error: e.message }; }
}
// ── 新增/编辑表单 ──
async function openEditForm(id) {
adminState.editingId = id;
var article = null;
if (id) {
var data = await apiGet('/articles/' + id);
if (data.ok) article = data.data;
else { showToast('加载文章失败', 'error'); return; }
}
var overlay = document.createElement('div');
overlay.className = 'za-modal-overlay';
overlay.innerHTML = '<div class="za-modal form-modal">' +
'<h3>' + (id ? '编辑存档' : '新增存档') + '</h3>' +
'<div class="form-group"><label>标题</label><input id="za-form-title" value="' + escapeHtml(article ? article.title : '') + '" placeholder="输入文章标题" /></div>' +
'<div class="form-group"><label>分类</label><select id="za-form-category"><option value="">选择分类</option></select></div>' +
'<div class="form-group"><label>来源备注</label><input id="za-form-source" value="' + escapeHtml(article ? article.source : '') + '" placeholder="来源信息(纯文字)" /></div>' +
'<div class="form-group"><label>标签</label><div class="tag-input" id="za-form-tags"><input id="za-tag-input" placeholder="输入标签后回车" /></div></div>' +
'<div class="form-group"><label>正文</label><textarea id="za-form-content" placeholder="粘贴正文内容(自动清除外链)">' + escapeHtml(article ? article.content : '') + '</textarea></div>' +
'<div class="form-group"><label>内容类型</label><select id="za-form-contentType"><option value="text">纯文字</option><option value="image">图文</option></select></div>' +
'<div class="form-group"><label>置顶</label><select id="za-form-pinned"><option value="0">否</option><option value="1">是</option></select></div>' +
'<div class="form-group"><label>图片上传</label>' +
'<div class="upload-area" id="za-upload-area">' +
'<input type="file" id="za-file-input" accept="image/*" multiple style="display:none" />' +
'<button type="button" class="btn btn-outline" id="za-upload-btn">选择图片(可多选)</button>' +
'<div id="za-upload-preview" class="upload-preview"></div>' +
'</div>' +
'</div>' +
'<div class="modal-actions">' +
'<button class="btn btn-cancel" id="za-form-cancel">取消</button>' +
'<button class="btn btn-primary" id="za-form-save">保存</button>' +
'</div></div>';
document.body.appendChild(overlay);
// 填充分类
var cats = await apiGet('/categories');
var catSelect = overlay.querySelector('#za-form-category');
if (cats.ok && cats.data) {
cats.data.forEach(function (c) {
var opt = document.createElement('option');
opt.value = c.name; opt.textContent = c.name;
if (article && article.category === c.name) opt.selected = true;
catSelect.appendChild(opt);
});
}
if (article) {
overlay.querySelector('#za-form-contentType').value = article.content_type || 'text';
overlay.querySelector('#za-form-pinned').value = article.is_pinned ? '1' : '0';
}
// 标签管理
var tagContainer = overlay.querySelector('#za-form-tags');
var tagInput = overlay.querySelector('#za-tag-input');
var currentTags = [];
try { if (article && article.tags_json) currentTags = JSON.parse(article.tags_json); } catch (e) {}
function renderTagChips() {
var existing = tagContainer.querySelectorAll('.tag-chip');
existing.forEach(function (el) { el.remove(); });
currentTags.forEach(function (t, i) {
var chip = document.createElement('span');
chip.className = 'tag-chip';
chip.innerHTML = t + '<button class="remove-tag" data-idx="' + i + '">×</button>';
chip.querySelector('.remove-tag').addEventListener('click', function () {
currentTags.splice(i, 1);
renderTagChips();
});
tagContainer.insertBefore(chip, tagInput);
});
}
renderTagChips();
tagInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
var val = tagInput.value.trim();
if (val && currentTags.indexOf(val) === -1) {
currentTags.push(val);
renderTagChips();
}
tagInput.value = '';
}
});
// ── 图片上传逻辑 ──
var selectedFiles = [];
var removedImageIds = []; // 编辑时删除的已有图片ID
var uploadPreview = overlay.querySelector('#za-upload-preview');
var fileInput = overlay.querySelector('#za-file-input');
var uploadBtn = overlay.querySelector('#za-upload-btn');
// 编辑时加载已有图片
if (article && article.images) {
article.images.forEach(function (img) {
var item = document.createElement('div');
item.className = 'preview-item';
item.innerHTML = '<img src="/uploads/zhizi_notes/' + img.filename + '" alt="" />' +
'<button class="preview-remove" data-filename="' + img.filename + '" data-id="' + (img.id || '') + '">×</button>';
uploadPreview.appendChild(item);
});
}
// 为所有已有图片的删除按钮绑定事件
uploadPreview.querySelectorAll('.preview-remove').forEach(function (btn) {
btn.addEventListener('click', function () {
var id = btn.getAttribute('data-id');
if (id) removedImageIds.push(parseInt(id));
btn.closest('.preview-item').remove();
});
});
uploadBtn.addEventListener('click', function () { fileInput.click(); });
fileInput.addEventListener('change', function () {
if (!fileInput.files) return;
Array.from(fileInput.files).forEach(function (file, idx) {
if (!file.type.startsWith('image/')) return;
selectedFiles.push(file);
var reader = new FileReader();
reader.onload = (function (i) {
return function (e) {
var item = document.createElement('div');
item.className = 'preview-item';
item.innerHTML = '<img src="' + e.target.result + '" alt="" />' +
'<button class="preview-remove">×</button>';
item.querySelector('.preview-remove').addEventListener('click', function () {
selectedFiles[i] = null;
item.remove();
});
uploadPreview.appendChild(item);
};
})(idx);
reader.readAsDataURL(file);
});
fileInput.value = '';
});
overlay.querySelector('#za-form-cancel').addEventListener('click', function () { overlay.remove(); });
overlay.querySelector('#za-form-save').addEventListener('click', async function () {
var saveBtn = overlay.querySelector('#za-form-save');
saveBtn.disabled = true;
saveBtn.textContent = '保存中…';
var title = overlay.querySelector('#za-form-title').value.trim();
if (!title) { showToast('标题不能为空', 'error'); saveBtn.disabled = false; saveBtn.textContent = '保存'; return; }
// 先保存文章获取 ID
var rawContent = overlay.querySelector('#za-form-content').value;
var content = clearAllLinks(rawContent);
var body = {
title: title,
content: content,
category: overlay.querySelector('#za-form-category').value || '未分类',
source: overlay.querySelector('#za-form-source').value.trim(),
tags: currentTags,
contentType: overlay.querySelector('#za-form-contentType').value,
isPinned: overlay.querySelector('#za-form-pinned').value === '1'
};
var result;
if (id) {
result = await apiPut('/articles/' + id, body);
result.data = { id: id };
} else {
result = await apiPost('/articles', body);
}
if (!result.ok) {
showToast('保存失败:' + (result.error || '未知错误'), 'error');
saveBtn.disabled = false;
saveBtn.textContent = '保存';
return;
}
var savedId = result.data ? result.data.id : id;
if (!savedId) savedId = id;
// 编辑时删除已被移除的图片
if (id && removedImageIds.length > 0) {
for (var k = 0; k < removedImageIds.length; k++) {
await apiDelete('/images/' + removedImageIds[k]);
}
}
// 过滤已删除的文件(标记为 null)
var filesToUpload = selectedFiles.filter(function (f) { return f !== null; });
// 上传图片
if (filesToUpload.length > 0 && savedId) {
saveBtn.textContent = '上传图片中…';
var uploadResult = await uploadFiles(filesToUpload, savedId);
if (!uploadResult.ok) {
showToast('图片上传失败:' + (uploadResult.error || '未知错误'), 'error');
saveBtn.disabled = false;
saveBtn.textContent = '保存';
return;
}
// 更新封面为新上传的第一张图
if (uploadResult.data && uploadResult.data.length > 0) {
body.coverUrl = uploadResult.data[0].url;
await apiPut('/articles/' + savedId, body);
}
}
showToast(id ? '更新成功' : '创建成功', 'success');
overlay.remove();
loadTableData();
});
overlay.addEventListener('click', function (e) { if (e.target === overlay) overlay.remove(); });
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ── 导出 ──
async function exportSelected() {
if (adminState.selectedIds.size === 0) { showToast('请先选择文章', 'info'); return; }
var ids = Array.from(adminState.selectedIds);
try {
var res = await fetch(API_BASE + '/export', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ ids: ids })
});
if (res.ok) {
var blob = await res.blob();
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url; a.download = '知资札记_导出.txt'; a.click();
URL.revokeObjectURL(url);
showToast('导出成功', 'success');
} else {
showToast('导出失败', 'error');
}
} catch (e) {
showToast('导出失败:' + e.message, 'error');
}
}
// ── 初始化 ──
function init() {
// 全选
var selectAllCb = $('#za-select-all');
if (selectAllCb) {
selectAllCb.addEventListener('change', function () { toggleSelectAll(selectAllCb.checked); });
}
// 搜索
var searchInput = $('#za-search');
var searchBtn = $('#za-search-btn');
if (searchBtn && searchInput) {
searchBtn.addEventListener('click', function () {
adminState.filter.search = searchInput.value.trim();
adminState.page = 1;
loadTableData();
});
searchInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
adminState.filter.search = searchInput.value.trim();
adminState.page = 1;
loadTableData();
}
});
}
// 分类筛选
var catFilter = $('#za-filter-category');
if (catFilter) {
catFilter.addEventListener('change', function () {
adminState.filter.category = catFilter.value;
adminState.page = 1;
loadTableData();
});
}
// 内容类型筛选
var typeFilter = $('#za-filter-type');
if (typeFilter) {
typeFilter.addEventListener('change', function () {
adminState.filter.contentType = typeFilter.value;
adminState.page = 1;
loadTableData();
});
}
// 状态筛选
var statusFilter = $('#za-filter-status');
if (statusFilter) {
statusFilter.addEventListener('change', function () {
adminState.filter.status = statusFilter.value;
adminState.page = 1;
loadTableData();
});
}
// 每页条数
var pageSizeSel = $('#za-page-size');
if (pageSizeSel) {
pageSizeSel.addEventListener('change', function () {
adminState.pageSize = parseInt(pageSizeSel.value) || 20;
adminState.page = 1;
loadTableData();
});
}
// 批量操作按钮
var btnBatchOffline = $('#za-btn-batch-offline');
var btnBatchDelete = $('#za-btn-batch-delete');
var btnBatchExport = $('#za-btn-batch-export');
if (btnBatchOffline) btnBatchOffline.addEventListener('click', function () { batchAction('offline'); });
if (btnBatchDelete) btnBatchDelete.addEventListener('click', function () { batchAction('delete'); });
if (btnBatchExport) btnBatchExport.addEventListener('click', function () { exportSelected(); });
// 新增按钮
var btnNew = $('#za-btn-new');
if (btnNew) btnNew.addEventListener('click', function () { openEditForm(null); });
// 加载分类选项
loadFilterOptions();
loadTableData();
}
async function loadFilterOptions() {
var cats = await apiGet('/categories');
var catFilter = $('#za-filter-category');
if (catFilter && cats.ok && cats.data) {
cats.data.forEach(function (c) {
var opt = document.createElement('option');
opt.value = c.name; opt.textContent = c.name;
catFilter.appendChild(opt);
});
}
}
document.addEventListener('DOMContentLoaded', init);
})();
+15
View File
@@ -0,0 +1,15 @@
{
"_comment": "知资札记工具的前端鉴权配置",
"enable_auth_comment": "true:启用鉴权;false:暂时对所有访问者开放",
"enable_auth": true,
"max_age_days_comment": "鉴权Cookie有效期(天),范围建议1~365",
"max_age_days": 30,
"cookieName_comment": "鉴权Cookie名称",
"cookieName": "zhizi_notes_gate",
"guidParam_comment": "URL中的GUID参数名",
"guidParam": "guid",
"guidValue_comment": "GUID参数值(留空表示不启用GUID)",
"guidValue": "b1c4ejKa0TAD7MQSIEBf94RMOG6SJ2-nUTJNUEiw",
"requireGuid_comment": "true:强制需要GUID参数才能访问;false:允许仅凭token或已有会话访问",
"requireGuid": false
}
+96
View File
@@ -0,0 +1,96 @@
/* ═══════════════════════════════════════════════════════════
知资札记 - 文章详情页样式
复用 zhizi_notes.css 变量体系
═══════════════════════════════════════════════════════════ */
/* ── 布局 ── */
.zd-layout {
margin-top: 56px; min-height: calc(100vh - 56px);
max-width: 900px; margin-left: auto; margin-right: auto;
padding: 0 20px;
}
.zd-main { padding: 30px 0 60px; }
/* ── 文章头部 ── */
.zd-header { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
.zd-header h1 {
font-size: 1.6rem; font-weight: 700; color: var(--text);
line-height: 1.5; margin-bottom: 12px;
}
.zd-meta {
display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
font-size: 0.82rem; color: var(--text-light);
}
.zd-meta .zd-cat {
display: inline-block; padding: 2px 10px; border-radius: 10px;
background: var(--primary); color: #fff; font-size: 0.75rem;
}
.zd-meta .fav-btn {
background: transparent; color: var(--text-light); font-size: 1.1rem;
padding: 2px 4px; transition: all var(--transition); border: none; cursor: pointer;
}
.zd-meta .fav-btn.favorited { color: #E74C3C; }
.zd-meta .fav-btn:hover { color: #E74C3C; }
/* ── 标签 ── */
.zd-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.zd-tag {
padding: 3px 10px; border-radius: 12px; font-size: 0.75rem;
background: var(--bg-tag); color: var(--text-secondary);
}
/* ── 封面图 ── */
#zd-cover {
margin-bottom: 24px; border-radius: var(--radius);
overflow: hidden; max-height: 400px;
}
#zd-cover img { width: 100%; height: 100%; object-fit: cover; }
/* ── 正文 ── */
.zd-content {
font-size: 0.95rem; line-height: 1.85; color: var(--text);
word-break: break-word; white-space: pre-wrap;
}
.zd-content p { margin-bottom: 1em; }
.zd-content img { max-width: 100%; border-radius: 6px; margin: 12px 0; }
.zd-content blockquote {
border-left: 3px solid var(--accent); padding: 8px 16px;
margin: 12px 0; color: var(--text-secondary);
background: var(--bg);
}
.zd-content h1,.zd-content h2,.zd-content h3,.zd-content h4 {
margin: 1.2em 0 0.6em; font-weight: 600;
}
.zd-content h2 { font-size: 1.2rem; }
.zd-content h3 { font-size: 1.05rem; }
.zd-content ul,.zd-content ol { padding-left: 1.5em; margin-bottom: 1em; }
.zd-content li { margin-bottom: 0.3em; }
.zd-content pre {
background: var(--bg); padding: 12px 16px; border-radius: 6px;
overflow-x: auto; font-size: 0.85rem; margin: 12px 0;
}
.zd-content code {
background: var(--bg-tag); padding: 2px 6px; border-radius: 4px;
font-size: 0.88em;
}
.zd-content pre code { background: transparent; padding: 0; }
/* ── 图片集 ── */
.zd-gallery { margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border); }
.zd-gallery h3 { font-size: 1rem; color: var(--text); margin-bottom: 14px; }
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
.gallery-item {
border-radius: var(--radius); overflow: hidden;
background: var(--bg-tag); cursor: pointer;
transition: transform var(--transition);
}
.gallery-item:hover { transform: scale(1.02); }
.gallery-item img { width: 100%; height: 180px; object-fit: cover; display: block; }
/* ── 响应式 ── */
@media (max-width: 767px) {
.zd-layout { padding: 0 12px; }
.zd-header h1 { font-size: 1.3rem; }
.zd-content { font-size: 0.9rem; }
.gallery-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
}
+79
View File
@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>知资札记 · 文章详情</title>
<link rel="icon" href="/icons/zhizi_notes.svg" type="image/svg+xml" />
<link rel="stylesheet" href="/common/common.css" />
<link rel="stylesheet" href="/tools/zhizi_notes/zhizi_notes.css" />
<link rel="stylesheet" href="/tools/zhizi_notes/detail.css" />
</head>
<body>
<!-- Toast -->
<div id="zd-toast" class="zs-toast"></div>
<!-- ═══════════════ 顶部 Header ═══════════════ -->
<header class="zs-header">
<button class="hamburger" id="zd-back" title="返回" style="display:flex">←</button>
<div class="logo">知资札记</div>
<nav class="nav-menu">
<button data-nav="home">首页</button>
</nav>
<div class="header-right">
<button id="zs-theme-btn" class="theme-btn" title="切换暗黑模式">🌙</button>
</div>
</header>
<!-- ═══════════════ 主体 ═══════════════ -->
<div class="zd-layout">
<main class="zd-main">
<!-- 加载中 -->
<div id="zd-loading" class="zs-loading">
<div class="spinner"></div>
<div>加载中…</div>
</div>
<!-- 文章内容 -->
<article id="zd-article" style="display:none">
<!-- 标题区 -->
<header class="zd-header">
<h1 id="zd-title"></h1>
<div class="zd-meta">
<span id="zd-category" class="zd-cat"></span>
<span id="zd-source"></span>
<span id="zd-date"></span>
<span id="zd-views"></span>
<button id="zd-fav-btn" class="fav-btn" title="收藏">♡</button>
</div>
<div id="zd-tags" class="zd-tags"></div>
</header>
<!-- 封面图 -->
<div id="zd-cover" style="display:none">
<img id="zd-cover-img" alt="" />
</div>
<!-- 正文 -->
<div id="zd-content" class="zd-content"></div>
<!-- 图片集 -->
<div id="zd-gallery" class="zd-gallery" style="display:none">
<h3>附图</h3>
<div id="zd-gallery-grid" class="gallery-grid"></div>
</div>
</article>
<!-- 错误 -->
<div id="zd-error" class="zs-empty" style="display:none">
<div id="zd-error-msg">文章不存在或已被删除</div>
<div style="margin-top:12px">
<a href="/tools/zhizi_notes/index.html" style="color:var(--accent)">← 返回首页</a>
</div>
</div>
</main>
</div>
<script src="/tools/zhizi_notes/detail.js"></script>
</body>
</html>
+224
View File
@@ -0,0 +1,224 @@
/**
* 知资札记 - 文章详情页逻辑
*/
(function () {
'use strict';
const API_BASE = '/api/zhizi_notes';
const STORAGE_KEY = 'zhizi_notes_theme';
const $ = function (sel) { return document.querySelector(sel); };
let articleId = null;
let isFavorited = false;
// ── 解析 URL 参数 ──
function getParam(name) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
}
// ── Toast ──
function showToast(msg, type) {
type = type || 'info';
const t = $('#zd-toast');
if (!t) return;
t.textContent = msg;
t.className = 'zs-toast ' + type + ' show';
clearTimeout(t._timer);
t._timer = setTimeout(function () { t.classList.remove('show'); }, type === 'error' ? 4000 : 2000);
}
// ── 暗黑模式 ──
function initTheme() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
const btn = $('#zs-theme-btn');
if (btn) {
btn.textContent = saved === 'dark' ? '☀️' : '🌙';
btn.addEventListener('click', function () {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
if (isDark) {
document.documentElement.removeAttribute('data-theme');
localStorage.setItem(STORAGE_KEY, 'light');
btn.textContent = '🌙';
} else {
document.documentElement.setAttribute('data-theme', 'dark');
localStorage.setItem(STORAGE_KEY, 'dark');
btn.textContent = '☀️';
}
});
}
}
// ── API ──
async function apiGet(path) {
try {
const res = await fetch(API_BASE + path, { credentials: 'same-origin' });
return await res.json();
} catch (e) { return { ok: false, error: e.message }; }
}
async function apiPost(path, body) {
try {
const res = await fetch(API_BASE + path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(body)
});
return await res.json();
} catch (e) { return { ok: false, error: e.message }; }
}
// ── 加载详情 ──
async function loadDetail() {
articleId = getParam('id');
if (!articleId) {
showError('缺少文章ID');
return;
}
const data = await apiGet('/articles/' + articleId);
const loading = $('#zd-loading');
if (loading) loading.style.display = 'none';
if (!data.ok) {
showError(data.error || '文章不存在');
return;
}
renderArticle(data.data);
}
function showError(msg) {
const loading = $('#zd-loading');
const error = $('#zd-error');
if (loading) loading.style.display = 'none';
if (error) {
error.style.display = 'block';
$('#zd-error-msg').textContent = msg;
}
}
// ── 渲染文章 ──
function renderArticle(article) {
const articleEl = $('#zd-article');
if (!articleEl) return;
articleEl.style.display = 'block';
// 标题
$('#zd-title').textContent = article.title;
document.title = article.title + ' · 知资札记';
// 元信息
$('#zd-category').textContent = article.category || '未分类';
$('#zd-source').textContent = article.source ? '来源:' + article.source : '';
$('#zd-date').textContent = (article.created_at || '').slice(0, 10);
$('#zd-views').textContent = (article.views || 0) + ' 阅读';
// 收藏按钮
isFavorited = article.fav_count > 0;
updateFavBtn();
const favBtn = $('#zd-fav-btn');
if (favBtn) {
favBtn.addEventListener('click', function () { toggleFavorite(); });
}
// 标签
const tagsEl = $('#zd-tags');
try {
const tags = JSON.parse(article.tags_json || '[]');
if (tags.length > 0 && tagsEl) {
tagsEl.innerHTML = '';
tags.forEach(function (t) {
const span = document.createElement('span');
span.className = 'zd-tag';
span.textContent = t;
tagsEl.appendChild(span);
});
} else if (tagsEl) {
tagsEl.style.display = 'none';
}
} catch (e) { if (tagsEl) tagsEl.style.display = 'none'; }
// 封面图
if (article.cover_url) {
const cover = $('#zd-cover');
const coverImg = $('#zd-cover-img');
if (cover) cover.style.display = 'block';
if (coverImg) coverImg.src = article.cover_url;
}
// 正文
const contentEl = $('#zd-content');
if (contentEl) {
contentEl.innerHTML = article.content || article.content_plain || '';
}
// 图片集
if (article.images && article.images.length > 0) {
const gallery = $('#zd-gallery');
const grid = $('#zd-gallery-grid');
if (gallery) gallery.style.display = 'block';
if (grid) {
grid.innerHTML = '';
article.images.forEach(function (img) {
const item = document.createElement('div');
item.className = 'gallery-item';
const imgEl = document.createElement('img');
imgEl.src = '/uploads/zhizi_notes/' + img.filename;
imgEl.alt = img.original_name || '';
imgEl.loading = 'lazy';
item.appendChild(imgEl);
grid.appendChild(item);
});
}
}
}
// ── 收藏切换 ──
async function toggleFavorite() {
const data = await apiPost('/articles/' + articleId + '/toggle_favorite', {});
if (data.ok) {
isFavorited = data.data ? data.data.favorited : !isFavorited;
updateFavBtn();
showToast(isFavorited ? '已收藏' : '已取消收藏', isFavorited ? 'success' : 'info');
} else {
showToast('操作失败:' + (data.error || '未知错误'), 'error');
}
}
function updateFavBtn() {
const btn = $('#zd-fav-btn');
if (!btn) return;
if (isFavorited) {
btn.classList.add('favorited');
btn.textContent = '♥';
} else {
btn.classList.remove('favorited');
btn.textContent = '♡';
}
}
// ── 初始化 ──
function init() {
initTheme();
loadDetail();
// 返回按钮
const backBtn = $('#zd-back');
if (backBtn) {
backBtn.addEventListener('click', function () {
if (document.referrer && document.referrer.indexOf('/tools/zhizi_notes') !== -1) {
history.back();
} else {
location.href = '/tools/zhizi_notes/index.html';
}
});
}
}
document.addEventListener('DOMContentLoaded', init);
})();
+147
View File
@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>知资札记 · 证券投资图文存档站</title>
<link rel="icon" href="/icons/zhizi_notes.svg" type="image/svg+xml" />
<link rel="stylesheet" href="/common/common.css" />
<link rel="stylesheet" href="/tools/zhizi_notes/zhizi_notes.css" />
</head>
<body>
<!-- Toast -->
<div id="zs-toast" class="zs-toast"></div>
<!-- 侧边栏遮罩(手机) -->
<div id="zs-sidebar-overlay" class="zs-sidebar-overlay"></div>
<!-- ═══════════════ 顶部 Header ═══════════════ -->
<header class="zs-header">
<button id="zs-hamburger" class="hamburger" title="菜单">☰</button>
<div class="logo">知资札记</div>
<nav class="nav-menu">
<button class="active" data-nav="home">首页</button>
<button data-nav="archive">分类归档</button>
<button data-nav="favorites">我的收藏</button>
<button data-nav="submit">提交存档</button>
</nav>
<div class="header-right">
<div class="search-box">
<input id="zs-search" type="text" placeholder="搜索标题/标签…" />
<button id="zs-search-btn" title="搜索">🔍</button>
</div>
<button id="zs-theme-btn" class="theme-btn" title="切换暗黑模式">🌙</button>
</div>
</header>
<!-- ═══════════════ 主体布局 ═══════════════ -->
<div class="zs-layout">
<!-- 左侧筛选栏 -->
<aside id="zs-sidebar" class="zs-sidebar">
<!-- 内容分类 -->
<div class="sidebar-section">
<div class="sidebar-title">内容分类</div>
<div id="zs-categories">
<button class="cat-item active" data-cat="">全部</button>
</div>
</div>
<!-- 热门标签 -->
<div class="sidebar-section">
<div class="sidebar-title">热门标签</div>
<div id="zs-tags" class="tag-cloud"></div>
</div>
<!-- 时间筛选 -->
<div class="sidebar-section">
<div class="sidebar-title">时间筛选</div>
<div class="time-filter">
<button class="time-item active" data-time="">全部</button>
<button class="time-item" data-time="week">本周存档</button>
<button class="time-item" data-time="month">本月</button>
<button class="time-item" data-time="quarter">季度</button>
</div>
</div>
<!-- 内容类型 -->
<div class="sidebar-section">
<div class="sidebar-title">内容类型</div>
<div class="type-toggle">
<button class="type-btn active" data-type="">全部</button>
<button class="type-btn" data-type="text">纯文字</button>
<button class="type-btn" data-type="image">图文多图</button>
</div>
</div>
<!-- 站点统计 -->
<div class="sidebar-section">
<div class="sidebar-title">站点统计</div>
<div class="stats-card">
<div class="stat-row"><span>总存档文章</span><span class="stat-val" id="zs-stat-articles">0</span></div>
<div class="stat-row"><span>总存档图片</span><span class="stat-val" id="zs-stat-images">0</span></div>
</div>
</div>
</aside>
<!-- 中间主内容区 -->
<main class="zs-main">
<!-- 视图切换 + 筛选标签 -->
<div class="view-toggle">
<button class="active" data-view="card">📐 瀑布卡片</button>
<button data-view="list">📋 简洁列表</button>
<div id="zs-filter-tags" class="filter-tags" style="flex:1;margin-left:8px"></div>
</div>
<!-- 卡片网格 -->
<div id="zs-card-grid" class="card-grid"></div>
<!-- 加载状态 -->
<div id="zs-loading" class="zs-loading" style="display:none">
<div class="spinner"></div>
<div>加载中…</div>
</div>
<!-- 空状态 -->
<div id="zs-empty" class="zs-empty" style="display:none">
<div>暂无存档内容</div>
<div style="font-size:0.8rem;margin-top:4px">点击右侧「提交新存档」开始记录</div>
</div>
</main>
<!-- 右侧辅助栏 -->
<aside class="zs-rightbar">
<!-- 站长精选 -->
<div class="right-section">
<div class="right-title">站长精选</div>
<div id="zs-pinned">
<div class="pinned-item" style="color:var(--text-light)">暂无置顶</div>
</div>
</div>
<!-- 最新入库 -->
<div class="right-section">
<div class="right-title">最新入库</div>
<div id="zs-latest">
<div class="latest-item" style="color:var(--text-light)">暂无存档</div>
</div>
</div>
<!-- 分类占比 -->
<div class="right-section">
<div class="right-title">分类占比</div>
<div id="zs-chart">
<div style="color:var(--text-light);font-size:0.8rem">暂无数据</div>
</div>
</div>
<!-- 提交存档 -->
<div class="right-section">
<button id="zs-submit-btn" class="submit-btn">提交新存档</button>
</div>
</aside>
</div>
<!-- 脚本 -->
<script src="/tools/zhizi_notes/zhizi_notes.js"></script>
</body>
</html>
+416
View File
@@ -0,0 +1,416 @@
/* ═══════════════════════════════════════════════════════════════════
知资札记 - 全局样式
主色:#162A46 墨蓝 | 辅助色:#467974 青灰
日间背景:#F7F9FC | 暗黑背景:#0F1726
═══════════════════════════════════════════════════════════════════ */
/* ── CSS 变量(日间默认) ── */
:root {
--primary: #162A46;
--primary-light: #1E3A5F;
--primary-dark: #0F1D33;
--accent: #467974;
--accent-light: #5A9690;
--text: #333;
--text-secondary: #666;
--text-light: #999;
--bg: #F7F9FC;
--bg-card: #FFFFFF;
--bg-sidebar: #FFFFFF;
--bg-header: #162A46;
--bg-tag: #E8EDF2;
--bg-tag-hover: #D0D8E0;
--border: #E0E4E8;
--shadow: 0 1px 3px rgba(0,0,0,0.08);
--shadow-hover: 0 4px 16px rgba(0,0,0,0.12);
--radius: 8px;
--transition: 0.2s ease;
--font: 'Microsoft YaHei', 'Source Han Sans CN', -apple-system, sans-serif;
--line-height: 1.75;
}
/* ── 暗黑模式 ── */
[data-theme="dark"] {
--primary: #1E3A5F;
--primary-light: #2A4D7A;
--primary-dark: #0F1D33;
--accent: #5A9690;
--accent-light: #6DB0A9;
--text: #E0E4E8;
--text-secondary: #A0A8B4;
--text-light: #6B7280;
--bg: #0F1726;
--bg-card: #1A2332;
--bg-sidebar: #1A2332;
--bg-header: #0F1D33;
--bg-tag: #1E2D3D;
--bg-tag-hover: #2A3D52;
--border: #2A3D52;
--shadow: 0 1px 3px rgba(0,0,0,0.3);
--shadow-hover: 0 4px 16px rgba(0,0,0,0.5);
}
/* ── 全局重置 ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; scroll-behavior: smooth; }
body {
font-family: var(--font);
color: var(--text);
background: var(--bg);
line-height: var(--line-height);
min-height: 100vh;
transition: background var(--transition), color var(--transition);
}
a { text-decoration: none; color: inherit; }
img { max-width: 100%; display: block; }
button { cursor: pointer; font-family: inherit; border: none; outline: none; }
input, select, textarea { font-family: inherit; outline: none; }
/* ── 顶部 Header ── */
.zs-header {
position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
background: var(--bg-header); color: #fff;
height: 56px; display: flex; align-items: center;
padding: 0 20px; gap: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.zs-header .logo {
font-size: 1.25rem; font-weight: 700;
letter-spacing: 2px; white-space: nowrap;
user-select: none;
}
.zs-header .nav-menu {
display: flex; gap: 4px; flex: 1; justify-content: center;
}
.zs-header .nav-menu button {
background: transparent; color: rgba(255,255,255,0.75);
padding: 6px 16px; border-radius: 6px; font-size: 0.9rem;
transition: all var(--transition);
}
.zs-header .nav-menu button:hover,
.zs-header .nav-menu button.active { color: #fff; background: rgba(255,255,255,0.12); }
.zs-header .header-right { display: flex; align-items: center; gap: 10px; }
.zs-header .search-box {
display: flex; align-items: center; background: rgba(255,255,255,0.12);
border-radius: 20px; padding: 4px 12px; gap: 6px;
}
.zs-header .search-box input {
background: transparent; border: none; color: #fff;
width: 160px; font-size: 0.85rem;
}
.zs-header .search-box input::placeholder { color: rgba(255,255,255,0.5); }
.zs-header .search-box button {
background: transparent; color: rgba(255,255,255,0.7);
font-size: 1rem; padding: 2px;
}
.zs-header .theme-btn {
background: rgba(255,255,255,0.12); color: #fff;
width: 34px; height: 34px; border-radius: 50%;
font-size: 1.1rem; display: flex; align-items: center; justify-content: center;
transition: all var(--transition);
}
.zs-header .theme-btn:hover { background: rgba(255,255,255,0.2); }
/* ── 汉堡菜单按钮(手机) ── */
.hamburger {
display: none; background: transparent; color: #fff;
font-size: 1.5rem; width: 36px; height: 36px;
align-items: center; justify-content: center;
}
/* ── 主体布局 ── */
.zs-layout {
display: flex; margin-top: 56px; min-height: calc(100vh - 56px);
max-width: 1400px; margin-left: auto; margin-right: auto;
}
/* ── 左侧筛选栏 ── */
.zs-sidebar {
width: 220px; min-width: 220px; background: var(--bg-sidebar);
border-right: 1px solid var(--border);
padding: 16px 12px; overflow-y: auto;
position: sticky; top: 56px; height: calc(100vh - 56px);
transition: transform var(--transition);
}
.zs-sidebar .sidebar-section { margin-bottom: 20px; }
.zs-sidebar .sidebar-title {
font-size: 0.8rem; color: var(--text-light);
text-transform: uppercase; letter-spacing: 1px;
margin-bottom: 8px; padding-left: 4px;
}
.zs-sidebar .cat-item {
display: block; width: 100%; text-align: left;
padding: 8px 12px; border-radius: 6px; font-size: 0.9rem;
color: var(--text-secondary); background: transparent;
transition: all var(--transition);
}
.zs-sidebar .cat-item:hover { background: var(--bg-tag); color: var(--text); }
.zs-sidebar .cat-item.active { background: var(--primary); color: #fff; }
.zs-sidebar .tag-cloud { display: flex; flex-wrap: wrap; gap: 6px; }
.zs-sidebar .tag-item {
padding: 4px 10px; border-radius: 12px; font-size: 0.78rem;
background: var(--bg-tag); color: var(--text-secondary);
transition: all var(--transition);
}
.zs-sidebar .tag-item:hover { background: var(--bg-tag-hover); color: var(--text); }
.zs-sidebar .tag-item.active { background: var(--accent); color: #fff; }
.zs-sidebar .time-filter { display: flex; flex-direction: column; gap: 4px; }
.zs-sidebar .time-item {
display: block; width: 100%; text-align: left;
padding: 6px 12px; border-radius: 6px; font-size: 0.85rem;
color: var(--text-secondary); background: transparent;
transition: all var(--transition);
}
.zs-sidebar .time-item:hover { background: var(--bg-tag); }
.zs-sidebar .time-item.active { background: var(--accent); color: #fff; }
.zs-sidebar .type-toggle { display: flex; gap: 6px; }
.zs-sidebar .type-btn {
flex: 1; padding: 6px 8px; border-radius: 6px; font-size: 0.8rem;
background: var(--bg-tag); color: var(--text-secondary);
transition: all var(--transition);
}
.zs-sidebar .type-btn.active { background: var(--accent); color: #fff; }
.zs-sidebar .stats-card {
background: var(--bg); border-radius: var(--radius);
padding: 12px; font-size: 0.85rem;
}
.zs-sidebar .stats-card .stat-row {
display: flex; justify-content: space-between;
padding: 4px 0; color: var(--text-secondary);
}
.zs-sidebar .stats-card .stat-val { font-weight: 700; color: var(--primary); }
/* ── 中间主内容区 ── */
.zs-main {
flex: 1; padding: 20px; min-width: 0;
}
.zs-main .view-toggle {
display: flex; align-items: center; gap: 8px; margin-bottom: 16px;
}
.zs-main .view-toggle button {
padding: 6px 14px; border-radius: 6px; font-size: 0.85rem;
background: var(--bg-tag); color: var(--text-secondary);
transition: all var(--transition);
}
.zs-main .view-toggle button.active { background: var(--primary); color: #fff; }
.zs-main .filter-tags {
display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px;
}
.zs-main .filter-tag {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 10px; border-radius: 12px; font-size: 0.78rem;
background: var(--accent); color: #fff;
}
.zs-main .filter-tag .remove-filter {
background: transparent; color: rgba(255,255,255,0.7);
font-size: 0.85rem; line-height: 1; padding: 0;
}
.zs-main .filter-tag .remove-filter:hover { color: #fff; }
/* ── 卡片网格视图 ── */
.card-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.card-grid.list-view {
grid-template-columns: 1fr;
}
/* ── 图文卡片 ── */
.zs-card {
background: var(--bg-card); border-radius: var(--radius);
box-shadow: var(--shadow); overflow: hidden;
transition: all var(--transition); cursor: pointer;
position: relative;
}
.zs-card:hover { box-shadow: var(--shadow-hover); transform: translateY(-2px); }
.zs-card .card-cover {
height: 180px; overflow: hidden; position: relative;
background: var(--bg-tag);
}
.zs-card .card-cover img {
width: 100%; height: 100%; object-fit: cover;
}
.zs-card .card-cover .text-badge {
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, var(--primary), var(--accent));
color: #fff; font-size: 2.5rem; font-weight: 700;
}
.zs-card .card-badge {
position: absolute; bottom: 8px; right: 8px;
background: rgba(0,0,0,0.6); color: #fff;
padding: 2px 8px; border-radius: 10px; font-size: 0.72rem;
}
.zs-card .card-body { padding: 14px; }
.zs-card .card-title {
font-size: 0.95rem; font-weight: 600; color: var(--text);
line-height: 1.5; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; margin-bottom: 8px;
}
.zs-card .card-source {
font-size: 0.78rem; color: var(--text-light); margin-bottom: 6px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.zs-card .card-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; }
.zs-card .card-tag {
padding: 2px 8px; border-radius: 10px; font-size: 0.7rem;
background: var(--bg-tag); color: var(--text-secondary);
}
.zs-card .card-footer {
display: flex; align-items: center; justify-content: space-between;
font-size: 0.75rem; color: var(--text-light);
border-top: 1px solid var(--border); padding-top: 10px;
}
.zs-card .card-footer .fav-btn {
background: transparent; color: var(--text-light); font-size: 1rem;
padding: 2px 4px; transition: all var(--transition);
}
.zs-card .card-footer .fav-btn.favorited { color: #E74C3C; }
.zs-card .card-footer .fav-btn:hover { color: #E74C3C; }
/* ── 列表视图卡片 ── */
.list-view .zs-card {
display: flex; flex-direction: row;
}
.list-view .zs-card .card-cover {
width: 240px; min-width: 240px; height: auto; min-height: 160px;
}
.list-view .zs-card .card-body { flex: 1; }
/* ── 右侧辅助栏 ── */
.zs-rightbar {
width: 260px; min-width: 260px; background: var(--bg-sidebar);
border-left: 1px solid var(--border); padding: 16px 12px;
overflow-y: auto; position: sticky; top: 56px;
height: calc(100vh - 56px);
}
.zs-rightbar .right-section { margin-bottom: 20px; }
.zs-rightbar .right-title {
font-size: 0.82rem; font-weight: 600; color: var(--text);
margin-bottom: 10px; padding-bottom: 6px;
border-bottom: 2px solid var(--accent);
}
.zs-rightbar .pinned-item {
padding: 8px 0; border-bottom: 1px solid var(--border);
cursor: pointer; transition: color var(--transition);
}
.zs-rightbar .pinned-item:hover { color: var(--accent); }
.zs-rightbar .pinned-item .pinned-title {
font-size: 0.85rem; font-weight: 500;
display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
overflow: hidden;
}
.zs-rightbar .pinned-item .pinned-date {
font-size: 0.72rem; color: var(--text-light); margin-top: 2px;
}
.zs-rightbar .latest-item {
padding: 6px 0; font-size: 0.82rem; color: var(--text-secondary);
cursor: pointer; transition: color var(--transition);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.zs-rightbar .latest-item:hover { color: var(--accent); }
.zs-rightbar .chart-bar {
display: flex; align-items: center; gap: 8px; margin-bottom: 6px;
font-size: 0.75rem;
}
.zs-rightbar .chart-bar .bar-label { width: 60px; text-align: right; color: var(--text-secondary); }
.zs-rightbar .chart-bar .bar-track {
flex: 1; height: 8px; background: var(--bg-tag); border-radius: 4px; overflow: hidden;
}
.zs-rightbar .chart-bar .bar-fill {
height: 100%; background: var(--accent); border-radius: 4px;
transition: width 0.5s ease;
}
.zs-rightbar .chart-bar .bar-count { width: 30px; color: var(--text-light); }
.zs-rightbar .submit-btn {
display: block; width: 100%; padding: 10px; border-radius: var(--radius);
background: var(--primary); color: #fff; font-size: 0.9rem; font-weight: 600;
text-align: center; transition: all var(--transition);
}
.zs-rightbar .submit-btn:hover { background: var(--primary-light); }
/* ── 加载状态 ── */
.zs-loading {
text-align: center; padding: 40px; color: var(--text-light);
}
.zs-loading .spinner {
width: 32px; height: 32px; border: 3px solid var(--border);
border-top-color: var(--accent); border-radius: 50%;
animation: spin 0.8s linear infinite; margin: 0 auto 12px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.zs-empty {
text-align: center; padding: 60px 20px; color: var(--text-light); font-size: 0.95rem;
}
/* ── 提示 Toast ── */
.zs-toast {
position: fixed; top: 70px; left: 50%; transform: translateX(-50%);
z-index: 9999; padding: 10px 24px; border-radius: 8px;
font-size: 0.9rem; color: #fff; opacity: 0;
transition: opacity 0.3s ease; pointer-events: none;
}
.zs-toast.show { opacity: 1; }
.zs-toast.info { background: #3B82F6; }
.zs-toast.success { background: #10B981; }
.zs-toast.error { background: #EF4444; }
/* ── 模态弹窗(交互详情/确认) ── */
.zs-modal-overlay {
position: fixed; inset: 0; z-index: 5000;
background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center;
transition: opacity var(--transition);
}
.zs-modal-overlay.hidden { display: none; }
.zs-modal {
background: var(--bg-card); border-radius: 12px; padding: 24px;
max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
.zs-modal h3 { margin-bottom: 16px; color: var(--text); }
.zs-modal .modal-body { font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 20px; }
.zs-modal .modal-body pre {
background: var(--bg); padding: 12px; border-radius: 6px;
font-size: 0.78rem; overflow-x: auto; white-space: pre-wrap; word-break: break-all;
}
.zs-modal .modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
.zs-modal .btn {
padding: 8px 20px; border-radius: 6px; font-size: 0.85rem; font-weight: 500;
transition: all var(--transition);
}
.zs-modal .btn-primary { background: var(--primary); color: #fff; }
.zs-modal .btn-primary:hover { background: var(--primary-light); }
.zs-modal .btn-danger { background: #D94452; color: #fff; }
.zs-modal .btn-danger:hover { background: #C0392B; }
.zs-modal .btn-cancel { background: var(--bg-tag); color: var(--text-secondary); }
.zs-modal .btn-cancel:hover { background: var(--bg-tag-hover); }
/* ── 响应式 ── */
@media (max-width: 1199px) {
.zs-rightbar { display: none; }
.zs-sidebar { width: 200px; min-width: 200px; }
.card-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
.list-view .zs-card .card-cover { width: 180px; min-width: 180px; }
}
@media (max-width: 767px) {
.zs-layout { flex-direction: column; }
.zs-sidebar {
position: fixed; top: 56px; left: 0; bottom: 0; z-index: 900;
width: 260px; transform: translateX(-100%);
box-shadow: 4px 0 16px rgba(0,0,0,0.15);
}
.zs-sidebar.open { transform: translateX(0); }
.zs-sidebar-overlay {
display: none; position: fixed; inset: 0; z-index: 899;
background: rgba(0,0,0,0.4);
}
.zs-sidebar-overlay.show { display: block; }
.hamburger { display: flex; }
.zs-header .nav-menu { display: none; }
.zs-header .search-box input { width: 100px; }
.zs-main { padding: 12px; }
.card-grid { grid-template-columns: 1fr; }
.list-view .zs-card { flex-direction: column; }
.list-view .zs-card .card-cover { width: 100%; min-width: auto; height: 180px; }
}
+594
View File
@@ -0,0 +1,594 @@
/**
* 知资札记 - C端前端逻辑
* 证券投资图文存档站
*/
(function () {
'use strict';
// ── 常量 ──
const API_BASE = '/api/zhizi_notes';
const STORAGE_KEY = 'zhizi_notes_theme';
const STORAGE_VIEW = 'zhizi_notes_view';
const STORAGE_FAVORITES = 'zhizi_notes_favorites';
// ── 状态 ──
let state = {
page: 1,
pageSize: 12,
totalPages: 0,
loading: false,
done: false,
viewMode: localStorage.getItem(STORAGE_VIEW) || 'card',
filter: { category: '', tag: '', timeRange: '', contentType: '', search: '' },
favorites: new Set()
};
// ── DOM 缓存 ──
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
// ── 链接清洗函数 ──
window.clearAllLink = function (htmlStr) {
if (!htmlStr || typeof htmlStr !== 'string') return '';
return htmlStr.replace(/<a\b[^>]*>(.*?)<\/a>/gi, '$1');
};
// ── Toast 提示 ──
function showToast(msg, type) {
type = type || 'info';
const t = $('#zs-toast');
if (!t) return;
t.textContent = msg;
t.className = 'zs-toast ' + type + ' show';
const duration = type === 'error' ? 4000 : 2000;
clearTimeout(t._timer);
t._timer = setTimeout(function () { t.classList.remove('show'); }, duration);
}
// ── 暗黑模式 ──
function initTheme() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
const btn = $('#zs-theme-btn');
if (btn) {
btn.textContent = saved === 'dark' ? '☀️' : '🌙';
btn.addEventListener('click', toggleTheme);
}
}
function toggleTheme() {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
if (isDark) {
document.documentElement.removeAttribute('data-theme');
localStorage.setItem(STORAGE_KEY, 'light');
$('#zs-theme-btn').textContent = '🌙';
} else {
document.documentElement.setAttribute('data-theme', 'dark');
localStorage.setItem(STORAGE_KEY, 'dark');
$('#zs-theme-btn').textContent = '☀️';
}
}
// ── API 请求 ──
async function apiGet(path) {
try {
const res = await fetch(API_BASE + path, { credentials: 'same-origin' });
return await res.json();
} catch (e) {
return { ok: false, error: e.message };
}
}
async function apiPost(path, body) {
try {
const res = await fetch(API_BASE + path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(body)
});
return await res.json();
} catch (e) {
return { ok: false, error: e.message };
}
}
// ── 加载文章列表 ──
async function loadArticles(reset) {
if (reset) {
state.page = 1; state.done = false;
const grid = $('#zs-card-grid');
if (grid) grid.innerHTML = '';
}
if (state.loading || state.done) return;
state.loading = true;
showLoading(true);
const params = new URLSearchParams();
params.set('page', state.page);
params.set('pageSize', state.pageSize);
if (state.filter.category) params.set('category', state.filter.category);
if (state.filter.tag) params.set('tag', state.filter.tag);
if (state.filter.timeRange) params.set('timeRange', state.filter.timeRange);
if (state.filter.contentType) params.set('contentType', state.filter.contentType);
if (state.filter.search) params.set('search', state.filter.search);
const data = await apiGet('/articles?' + params.toString());
showLoading(false);
state.loading = false;
if (!data.ok) {
showToast('加载失败:' + (data.error || '未知错误'), 'error');
return;
}
const articles = data.data || [];
const pag = data.pagination || {};
state.totalPages = pag.totalPages || 0;
if (state.page >= state.totalPages) state.done = true;
if (articles.length === 0 && state.page === 1) {
showEmpty(true);
} else {
showEmpty(false);
renderCards(articles);
state.page++;
}
}
function showLoading(show) {
const el = $('#zs-loading');
if (el) el.style.display = show ? 'block' : 'none';
}
function showEmpty(show) {
const el = $('#zs-empty');
if (el) el.style.display = show ? 'block' : 'none';
}
// ── 渲染卡片 ──
function renderCards(articles) {
const grid = $('#zs-card-grid');
if (!grid) return;
articles.forEach(function (article) {
const card = createCard(article);
grid.appendChild(card);
});
lazyLoadImages();
}
function createCard(article) {
const card = document.createElement('div');
card.className = 'zs-card';
card.setAttribute('data-id', article.id);
card.addEventListener('click', function (e) {
if (e.target.closest('.fav-btn')) return;
openDetail(article.id);
});
// 封面区
const cover = document.createElement('div');
cover.className = 'card-cover';
if (article.cover_url) {
const img = document.createElement('img');
img.setAttribute('data-src', article.cover_url);
img.setAttribute('alt', article.title);
img.className = 'lazy-img';
cover.appendChild(img);
} else {
const badge = document.createElement('div');
badge.className = 'text-badge';
badge.textContent = '文';
cover.appendChild(badge);
}
// 角标
const imgCount = article.image_count || 0;
const badge = document.createElement('span');
badge.className = 'card-badge';
badge.textContent = article.content_type === 'image' ? '含' + imgCount + '张附图' : '纯文字';
cover.appendChild(badge);
// 正文区
const body = document.createElement('div');
body.className = 'card-body';
const title = document.createElement('div');
title.className = 'card-title';
title.textContent = article.title;
const source = document.createElement('div');
source.className = 'card-source';
source.textContent = article.source || '';
// 标签
const tagsRow = document.createElement('div');
tagsRow.className = 'card-tags';
try {
const tags = JSON.parse(article.tags_json || '[]');
tags.forEach(function (t) {
const span = document.createElement('span');
span.className = 'card-tag';
span.textContent = t;
tagsRow.appendChild(span);
});
} catch (e) {}
// 底部
const footer = document.createElement('div');
footer.className = 'card-footer';
const meta = document.createElement('span');
meta.textContent = (article.created_at || '').slice(0, 10) + ' · ' + (article.views || 0) + ' 阅读';
const favBtn = document.createElement('button');
favBtn.className = 'fav-btn';
favBtn.setAttribute('data-id', article.id);
favBtn.title = '收藏';
if (state.favorites.has(article.id)) {
favBtn.classList.add('favorited');
favBtn.textContent = '♥';
} else {
favBtn.textContent = '♡';
}
favBtn.addEventListener('click', function (e) { e.stopPropagation(); toggleFavorite(article.id, favBtn); });
footer.appendChild(meta);
footer.appendChild(favBtn);
body.appendChild(title);
body.appendChild(source);
body.appendChild(tagsRow);
body.appendChild(footer);
card.appendChild(cover);
card.appendChild(body);
return card;
}
// ── 图片懒加载 ──
function lazyLoadImages() {
const imgs = document.querySelectorAll('img.lazy-img[data-src]');
if (!('IntersectionObserver' in window)) {
imgs.forEach(function (img) { img.src = img.getAttribute('data-src'); img.removeAttribute('data-src'); });
return;
}
const observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.getAttribute('data-src');
img.removeAttribute('data-src');
img.classList.remove('lazy-img');
observer.unobserve(img);
}
});
}, { rootMargin: '100px' });
imgs.forEach(function (img) { observer.observe(img); });
}
// ── 滚动触底加载 ──
let scrollTimer = null;
function onScroll() {
if (scrollTimer) return;
scrollTimer = setTimeout(function () {
scrollTimer = null;
if (state.loading || state.done) return;
const scrollY = window.scrollY + window.innerHeight;
const docH = document.documentElement.scrollHeight;
if (scrollY >= docH - 300) loadArticles(false);
}, 200);
}
// ── 收藏切换 ──
async function toggleFavorite(articleId, btn) {
if (btn._pending) return;
btn._pending = true;
btn.style.opacity = '0.5';
try {
const data = await apiPost('/articles/' + articleId + '/toggle_favorite', {});
if (data.ok) {
if (data.data && data.data.favorited) {
state.favorites.add(articleId);
btn.classList.add('favorited');
btn.textContent = '♥';
showToast('已收藏', 'success');
} else {
state.favorites.delete(articleId);
btn.classList.remove('favorited');
btn.textContent = '♡';
showToast('已取消收藏', 'info');
}
} else {
showToast('操作失败:' + (data.error || '未知错误'), 'error');
}
} catch (e) {
showToast('网络错误', 'error');
}
btn._pending = false;
btn.style.opacity = '1';
}
// ── 打开详情页 ──
function openDetail(id) {
window.open('/tools/zhizi_notes/detail.html?id=' + id, '_blank');
}
// ── 筛选逻辑 ──
function setFilter(key, value) {
if (key === 'category') {
if (state.filter.category === value) value = '';
} else if (key === 'tag') {
if (state.filter.tag === value) value = '';
} else if (key === 'timeRange') {
if (state.filter.timeRange === value) value = '';
} else if (key === 'contentType') {
if (state.filter.contentType === value) value = '';
}
state.filter[key] = value;
updateFilterUI();
loadArticles(true);
}
function setSearch(value) {
state.filter.search = value;
loadArticles(true);
}
function updateFilterUI() {
// 分类
$$('.zs-sidebar .cat-item').forEach(function (el) {
var cat = el.getAttribute('data-cat');
el.classList.toggle('active', cat === state.filter.category);
});
// 标签
$$('.zs-sidebar .tag-item').forEach(function (el) {
var tag = el.getAttribute('data-tag');
el.classList.toggle('active', tag === state.filter.tag);
});
// 时间
$$('.zs-sidebar .time-item').forEach(function (el) {
var tr = el.getAttribute('data-time');
el.classList.toggle('active', tr === state.filter.timeRange);
});
// 图文类型
$$('.zs-sidebar .type-btn').forEach(function (el) {
var ct = el.getAttribute('data-type');
el.classList.toggle('active', ct === state.filter.contentType);
});
// 筛选标签显示
renderFilterTags();
}
function renderFilterTags() {
var container = $('#zs-filter-tags');
if (!container) return;
container.innerHTML = '';
var items = [];
if (state.filter.category) items.push({ label: state.filter.category, key: 'category' });
if (state.filter.tag) items.push({ label: state.filter.tag, key: 'tag' });
if (state.filter.timeRange) {
var labels = { week: '本周', month: '本月', quarter: '本季度' };
items.push({ label: labels[state.filter.timeRange] || state.filter.timeRange, key: 'timeRange' });
}
if (state.filter.contentType) {
items.push({ label: state.filter.contentType === 'text' ? '纯文字' : '图文多图', key: 'contentType' });
}
items.forEach(function (item) {
var span = document.createElement('span');
span.className = 'filter-tag';
span.textContent = item.label;
var rm = document.createElement('button');
rm.className = 'remove-filter';
rm.textContent = '×';
rm.addEventListener('click', function () { setFilter(item.key, ''); });
span.appendChild(rm);
container.appendChild(span);
});
}
// ── 视图切换 ──
function setViewMode(mode) {
state.viewMode = mode;
localStorage.setItem(STORAGE_VIEW, mode);
var grid = $('#zs-card-grid');
if (grid) grid.className = mode === 'list' ? 'card-grid list-view' : 'card-grid';
$$('.zs-main .view-toggle button').forEach(function (btn) {
btn.classList.toggle('active', btn.getAttribute('data-view') === mode);
});
}
// ── 加载右侧栏 ──
async function loadRightbar() {
// 置顶
try {
var pinned = await apiGet('/articles/pinned');
var pinnedContainer = $('#zs-pinned');
if (pinnedContainer && pinned.ok && pinned.data) {
pinnedContainer.innerHTML = '';
pinned.data.forEach(function (a) {
var div = document.createElement('div');
div.className = 'pinned-item';
div.innerHTML = '<div class="pinned-title">' + escapeHtml(a.title) + '</div><div class="pinned-date">' + (a.created_at || '').slice(0, 10) + '</div>';
div.addEventListener('click', function () { openDetail(a.id); });
pinnedContainer.appendChild(div);
});
}
} catch (e) {}
// 最新
try {
var latest = await apiGet('/articles/latest');
var latestContainer = $('#zs-latest');
if (latestContainer && latest.ok && latest.data) {
latestContainer.innerHTML = '';
latest.data.forEach(function (a) {
var div = document.createElement('div');
div.className = 'latest-item';
div.textContent = a.title;
div.addEventListener('click', function () { openDetail(a.id); });
latestContainer.appendChild(div);
});
}
} catch (e) {}
// 统计数据
try {
var stats = await apiGet('/stats');
if (stats.ok && stats.data) {
var totalArticles = $('#zs-stat-articles');
var totalImages = $('#zs-stat-images');
if (totalArticles) totalArticles.textContent = stats.data.totalArticles || 0;
if (totalImages) totalImages.textContent = stats.data.totalImages || 0;
// 分类占比
var chartContainer = $('#zs-chart');
if (chartContainer && stats.data.catStats) {
var maxCount = Math.max.apply(null, stats.data.catStats.map(function (c) { return c.c; }));
chartContainer.innerHTML = '';
stats.data.catStats.forEach(function (c) {
var pct = maxCount > 0 ? Math.round((c.c / maxCount) * 100) : 0;
var row = document.createElement('div');
row.className = 'chart-bar';
row.innerHTML = '<span class="bar-label">' + escapeHtml(c.category) + '</span>' +
'<span class="bar-track"><span class="bar-fill" style="width:' + pct + '%"></span></span>' +
'<span class="bar-count">' + c.c + '</span>';
chartContainer.appendChild(row);
});
}
}
} catch (e) {}
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ── 加载分类/标签 ──
async function loadSidebarFilters() {
// 分类
try {
var cats = await apiGet('/categories');
var catContainer = $('#zs-categories');
if (catContainer && cats.ok && cats.data) {
catContainer.innerHTML = '';
cats.data.forEach(function (c) {
var btn = document.createElement('button');
btn.className = 'cat-item';
btn.setAttribute('data-cat', c.name);
btn.textContent = c.name;
btn.addEventListener('click', function () { setFilter('category', c.name); });
catContainer.appendChild(btn);
});
}
} catch (e) {}
// 标签
try {
var tags = await apiGet('/tags');
var tagContainer = $('#zs-tags');
if (tagContainer && tags.ok && tags.data) {
tagContainer.innerHTML = '';
tags.data.forEach(function (t) {
var btn = document.createElement('button');
btn.className = 'tag-item';
btn.setAttribute('data-tag', t.name);
btn.textContent = t.name + '(' + (t.article_count || 0) + ')';
btn.addEventListener('click', function () { setFilter('tag', t.name); });
tagContainer.appendChild(btn);
});
}
} catch (e) {}
}
// ── 加载收藏状态 ──
async function loadFavorites() {
try {
var data = await apiGet('/favorites');
if (data.ok && data.data) {
state.favorites = new Set();
data.data.forEach(function (a) { state.favorites.add(a.id); });
}
} catch (e) {}
}
// ── 初始化 ──
function init() {
initTheme();
// 视图切换
setViewMode(state.viewMode);
$$('.zs-main .view-toggle button').forEach(function (btn) {
btn.addEventListener('click', function () { setViewMode(btn.getAttribute('data-view')); });
});
// 分类筛选
$$('.zs-sidebar .cat-item').forEach(function (btn) {
btn.addEventListener('click', function () { setFilter('category', btn.getAttribute('data-cat')); });
});
// 标签筛选
// (动态生成的,在 loadSidebarFilters 中绑定)
// 时间筛选
$$('.zs-sidebar .time-item').forEach(function (btn) {
btn.addEventListener('click', function () { setFilter('timeRange', btn.getAttribute('data-time')); });
});
// 图文类型筛选
$$('.zs-sidebar .type-btn').forEach(function (btn) {
btn.addEventListener('click', function () { setFilter('contentType', btn.getAttribute('data-type')); });
});
// 搜索
var searchInput = $('#zs-search');
var searchBtn = $('#zs-search-btn');
if (searchBtn && searchInput) {
searchBtn.addEventListener('click', function () { setSearch(searchInput.value.trim()); });
searchInput.addEventListener('keydown', function (e) { if (e.key === 'Enter') setSearch(searchInput.value.trim()); });
}
// 滚动加载
window.addEventListener('scroll', onScroll, { passive: true });
// 汉堡菜单
var hamburger = $('#zs-hamburger');
var sidebar = $('#zs-sidebar');
var overlay = $('#zs-sidebar-overlay');
if (hamburger && sidebar) {
hamburger.addEventListener('click', function () {
var open = sidebar.classList.contains('open');
if (open) {
sidebar.classList.remove('open');
if (overlay) overlay.classList.remove('show');
} else {
sidebar.classList.add('open');
if (overlay) overlay.classList.add('show');
}
});
if (overlay) {
overlay.addEventListener('click', function () {
sidebar.classList.remove('open');
overlay.classList.remove('show');
});
}
}
// 提交存档按钮
var submitBtn = $('#zs-submit-btn');
if (submitBtn) {
submitBtn.addEventListener('click', function () {
window.open('/tools/zhizi_notes/admin.html', '_blank');
});
}
// 初始加载
loadSidebarFilters();
loadFavorites().then(function () { loadArticles(true); });
loadRightbar();
}
// ── 启动 ──
document.addEventListener('DOMContentLoaded', init);
})();