Files
lunar-mini/server/web/static/js/pages/Wiki.js
T
gouki 2a68eae999
Build and Publish Server / build (push) Successful in 2m8s
Publish Mini Program Dev Version / publish (push) Failing after 11m17s
feat(home): 修复swiper动画、宜忌布局、月历显示、节日速查
1. 修复swiper箭头点击动画丢失问题
   - 改用固定7天窗口,滑动到边缘时整体滚动数据
   - 保持dayIndex连续,确保swiper平滑动画

2. 修复宜忌两行显示压在一起
   - 设置item固定高度32rpx和行距12rpx
   - 超出2行显示...省略号

3. 修复月历数字竖向排列问题
   - 修正wxml嵌套结构,让42个格子正确排成6行7列
   - 压缩格子高度,放大数字字体

4. 节日速查功能优化
   - 默认显示3条,右侧添加更多
2026-08-10 23:07:47 +00:00

327 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Wiki 页面组件:百科条目管理 + 历史上的今天(维基百科)同步
export default {
template: `
<div class="min-h-screen bg-gray-100">
<nav class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<div class="flex-shrink-0 flex items-center">
<h1 class="text-xl font-bold text-red-600">祈福小助手</h1>
</div>
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
<a href="/admin" class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
仪表盘
</a>
<a href="/admin/users" class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
用户管理
</a>
<a href="/admin/orders" class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
订单管理
</a>
<a href="/admin/wishes" class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
许愿管理
</a>
<a href="/admin/wiki" class="border-red-500 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
百科管理
</a>
<a href="/admin/settings" class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
系统设置
</a>
</div>
</div>
</div>
</div>
</nav>
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<!-- 标签页切换 -->
<div class="px-4 sm:px-0 mb-4">
<div class="border-b border-gray-200">
<nav class="-mb-px flex space-x-8">
<button @click="tab = 'entries'" :class="tab === 'entries' ? 'border-red-500 text-red-600' : 'border-transparent text-gray-500 hover:text-gray-700'" class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm">百科条目</button>
<button @click="tab = 'sync'" :class="tab === 'sync' ? 'border-red-500 text-red-600' : 'border-transparent text-gray-500 hover:text-gray-700'" class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm">历史上的今天(维基同步)</button>
</nav>
</div>
</div>
<!-- 百科条目管理 -->
<div class="px-4 py-2 sm:px-0" v-if="tab === 'entries'">
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<h2 class="text-lg font-medium text-gray-900">百科条目(共 {{ entries.length }} 条)</h2>
<button @click="openEditor()" class="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700">
<i class="fas fa-plus mr-1"></i>新增条目
</button>
</div>
<div class="flex flex-wrap gap-3 mb-4">
<select v-model="filterCat" @change="loadEntries" class="block rounded-md border-gray-300 border px-3 py-2 text-sm">
<option value="">全部分类</option>
<option v-for="c in categories" :key="c.key" :value="c.key">{{ c.name }}</option>
</select>
<input v-model="filterKeyword" @keyup.enter="loadEntries" placeholder="搜索标题/内容..." class="block rounded-md border-gray-300 border px-3 py-2 text-sm w-64"/>
<button @click="loadEntries" class="px-3 py-2 border border-gray-300 rounded-md text-sm hover:bg-gray-50">搜索</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">分类</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">标题</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">简介</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">排序</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">状态</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="e in entries" :key="e.id">
<td class="px-4 py-3 text-sm text-gray-500">{{ e.id }}</td>
<td class="px-4 py-3 text-sm whitespace-nowrap">{{ catName(e.category) }}</td>
<td class="px-4 py-3 text-sm font-medium text-gray-900 whitespace-nowrap">{{ e.title }}</td>
<td class="px-4 py-3 text-sm text-gray-500 max-w-xs truncate">{{ e.brief }}</td>
<td class="px-4 py-3 text-sm text-gray-500">{{ e.sort }}</td>
<td class="px-4 py-3 whitespace-nowrap">
<span :class="e.status === 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full">{{ e.status === 1 ? '显示' : '隐藏' }}</span>
</td>
<td class="px-4 py-3 text-sm whitespace-nowrap">
<button @click="openEditor(e)" class="text-red-600 hover:text-red-800 mr-3">编辑</button>
<button @click="removeEntry(e)" class="text-gray-400 hover:text-red-600">删除</button>
</td>
</tr>
<tr v-if="entries.length === 0">
<td colspan="7" class="px-4 py-8 text-center text-sm text-gray-400">暂无条目</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 历史上的今天:维基百科同步 -->
<div class="px-4 py-2 sm:px-0" v-if="tab === 'sync'">
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">维基百科数据同步</h2>
<p class="text-sm text-gray-500 mb-4">数据来源:中文维基百科「X月X日」页面(共 366 页,含大事记/出生/逝世/节假日习俗)。服务启动时若数据缺失会自动抓取;之后按配置间隔(默认 7 天)定时增量刷新。</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="bg-gray-50 rounded-lg p-4">
<div class="text-sm text-gray-500">日期覆盖</div>
<div class="text-2xl font-bold">{{ syncStatus.totalDays || 0 }} / 366 天</div>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<div class="text-sm text-gray-500">条目总数</div>
<div class="text-2xl font-bold">{{ syncStatus.totalItems || 0 }}</div>
</div>
<div class="bg-gray-50 rounded-lg p-4">
<div class="text-sm text-gray-500">同步状态</div>
<div class="text-2xl font-bold">
<span v-if="syncStatus.running" class="text-blue-600"><i class="fas fa-spinner fa-spin mr-1"></i>进行中</span>
<span v-else-if="syncStatus.lastSync && syncStatus.lastSync.status === 'success'" class="text-green-600">已完成</span>
<span v-else-if="syncStatus.lastSync && syncStatus.lastSync.status === 'partial'" class="text-yellow-600">部分失败</span>
<span v-else-if="syncStatus.lastSync && syncStatus.lastSync.status === 'failed'" class="text-red-600">失败</span>
<span v-else class="text-gray-400">未同步</span>
</div>
</div>
</div>
<div v-if="syncStatus.lastSync" class="text-sm text-gray-600 mb-4 bg-gray-50 rounded-lg p-4">
<div>最近同步:{{ formatTime(syncStatus.lastSync.startedAt) }} {{ syncStatus.lastSync.finishedAt ? formatTime(syncStatus.lastSync.finishedAt) : '进行中' }}</div>
<div class="mt-1">触发方式:{{ triggerName(syncStatus.lastSync.trigger) }} 成功 {{ syncStatus.lastSync.success }} 页 / 失败 {{ syncStatus.lastSync.failed }} 页</div>
</div>
<button @click="triggerSync" :disabled="syncStatus.running || syncing" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fas fa-sync mr-2" :class="syncStatus.running ? 'fa-spin' : ''"></i>{{ syncStatus.running ? '同步进行中(约 3-5 分钟)' : '立即全量同步' }}
</button>
<button @click="loadSyncStatus" class="ml-3 px-4 py-2 border border-gray-300 rounded-md text-sm hover:bg-gray-50">刷新状态</button>
</div>
</div>
</div>
</main>
<!-- 条目编辑弹窗 -->
<div v-if="editor.visible" class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-gray-500 bg-opacity-75" @click="editor.visible = false"></div>
<div class="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full sm:p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">{{ editor.id ? '编辑条目' : '新增条目' }}</h3>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">分类</label>
<select v-model="editor.form.category" class="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm">
<option v-for="c in categories" :key="c.key" :value="c.key">{{ c.name }}</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">标题</label>
<input v-model="editor.form.title" class="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm"/>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">简介(列表折叠时显示)</label>
<input v-model="editor.form.brief" class="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm"/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">详细内容</label>
<textarea v-model="editor.form.content" rows="6" class="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm"></textarea>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">排序(越小越靠前)</label>
<input type="number" v-model.number="editor.form.sort" class="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm"/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">状态</label>
<select v-model.number="editor.form.status" class="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm">
<option :value="1">显示</option>
<option :value="0">隐藏</option>
</select>
</div>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button @click="editor.visible = false" class="px-4 py-2 border border-gray-300 rounded-md text-sm hover:bg-gray-50">取消</button>
<button @click="saveEntry" :disabled="saving" class="px-4 py-2 border border-transparent rounded-md text-sm text-white bg-red-600 hover:bg-red-700 disabled:opacity-50">{{ saving ? '保存中...' : '保存' }}</button>
</div>
</div>
</div>
</div>
</div>
`,
data() {
return {
tab: 'entries',
categories: [
{ key: 'term', name: '二十四节气' },
{ key: 'lunarFest', name: '农历节日' },
{ key: 'solarFest', name: '公历节日' },
{ key: 'ganzhi', name: '干支生肖' },
{ key: 'termExplain', name: '黄历术语' },
{ key: 'other', name: '其他' },
],
entries: [],
filterCat: '',
filterKeyword: '',
editor: {
visible: false,
id: null,
form: { category: 'other', title: '', brief: '', content: '', sort: 0, status: 1 },
},
saving: false,
syncing: false,
syncStatus: {},
syncTimer: null,
};
},
mounted() {
this.loadEntries();
this.loadSyncStatus();
},
beforeUnmount() {
if (this.syncTimer) clearInterval(this.syncTimer);
},
methods: {
catName(key) {
const c = this.categories.find(x => x.key === key);
return c ? c.name : key;
},
formatTime(t) {
if (!t) return '';
return new Date(t).toLocaleString('zh-CN', { hour12: false });
},
triggerName(t) {
return { startup: '启动自动', cron: '定时任务', admin: '手动触发' }[t] || t;
},
async api(url, options = {}) {
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (res.status === 401) {
alert('登录已过期,请重新登录');
window.location.href = '/admin';
throw new Error('未授权');
}
const data = await res.json();
if (data.code !== 0) throw new Error(data.msg || '请求失败');
return data.data;
},
async loadEntries() {
try {
const params = new URLSearchParams();
if (this.filterCat) params.set('category', this.filterCat);
if (this.filterKeyword) params.set('keyword', this.filterKeyword);
const data = await this.api('/admin/api/wiki/entries?' + params.toString());
this.entries = data.list || [];
} catch (e) {
console.error(e);
}
},
openEditor(entry) {
this.editor.id = entry ? entry.id : null;
this.editor.form = entry
? { category: entry.category, title: entry.title, brief: entry.brief, content: entry.content, sort: entry.sort, status: entry.status }
: { category: 'other', title: '', brief: '', content: '', sort: 0, status: 1 };
this.editor.visible = true;
},
async saveEntry() {
const f = this.editor.form;
if (!f.title.trim()) { alert('标题不能为空'); return; }
this.saving = true;
try {
if (this.editor.id) {
await this.api(`/admin/api/wiki/entries/${this.editor.id}`, { method: 'PUT', body: JSON.stringify(f) });
} else {
await this.api('/admin/api/wiki/entries', { method: 'POST', body: JSON.stringify(f) });
}
this.editor.visible = false;
this.loadEntries();
} catch (e) {
alert(e.message);
} finally {
this.saving = false;
}
},
async removeEntry(entry) {
if (!confirm(`确认删除条目「${entry.title}」?`)) return;
try {
await this.api(`/admin/api/wiki/entries/${entry.id}`, { method: 'DELETE' });
this.loadEntries();
} catch (e) {
alert(e.message);
}
},
async loadSyncStatus() {
try {
this.syncStatus = await this.api('/admin/api/wiki/sync-status');
// 同步进行中时每 10 秒自动刷新
if (this.syncStatus.running && !this.syncTimer) {
this.syncTimer = setInterval(() => this.loadSyncStatus(), 10000);
} else if (!this.syncStatus.running && this.syncTimer) {
clearInterval(this.syncTimer);
this.syncTimer = null;
}
} catch (e) {
console.error(e);
}
},
async triggerSync() {
if (!confirm('全量同步将抓取 366 个维基百科页面(约 3-5 分钟),确认执行?')) return;
this.syncing = true;
try {
await this.api('/admin/api/wiki/sync', { method: 'POST' });
this.loadSyncStatus();
} catch (e) {
alert(e.message);
} finally {
this.syncing = false;
}
},
},
}