Author SHA1 Message Date
gouki e0b447ddb1 fix(admin): 修复后台前端 Vue/Inertia 加载失败导致白屏
Build and Publish Server / build (push) Successful in 1m21s
- 原 unpkg 的 @inertiajs/vue3 UMD 路径 404,Inertia undefined
- 改用 import map + esm.sh ESM CDN(自动解析依赖树)
- vue 用含模板编译器的完整版 dist/vue.esm-browser.prod.js
- inertia 用 ?bundle&external=vue,保证与 import map 的 vue 单实例
- 解决 runtime-only vue 无法编译 template 字符串导致的白屏
2026-08-11 22:54:39 +00:00
gouki d175777d75 fix(admin): 浏览器直接访问后台时渲染登录页而非401 JSON
Build and Publish Server / build (push) Successful in 2m11s
- AdminAuth 中间件:GET 页面请求未登录时返回 login 页,API 请求仍返回 401
- 新增 Login.js 登录页组件(密码输入 + 登录后跳回原页面)
- index.html 注册 login 页面路由
2026-08-11 21:23:09 +00:00
gouki 0256f4e7f3 fix(home): 修复节日速查显示问题
Publish Mini Program Dev Version / publish (push) Successful in 45s
1. 节日速查显示最近10条(之前只显示3条)
2. 修复右侧'还有XX天'溢出问题
   - fest-left 添加 flex-shrink: 0 和 min-width
   - 确保文字不会溢出屏幕
2026-08-11 19:16:49 +00:00
gouki 9ccab93c5b fix(solar-terms): 修复 loadTerms 缺少闭合括号导致 CI 上传失败
Publish Mini Program Dev Version / publish (push) Successful in 3m11s
2026-08-11 12:08:00 +00:00
7 changed files with 107 additions and 19 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ Page({
const termTime = dayInfo.solarTerm ? (SOLAR_TERM_TIMES[dayInfo.solarTerm] || '') : '';
const isFirstOrFifteen = this.data.highlightLunar && (dayInfo.lunarDay === 1 || dayInfo.lunarDay === 15);
const nextBuddhist = this.data.showBuddhist ? getNextBuddhistFestival(y, m, d) : null;
const upcoming = getUpcomingFestivals(5, y, m, d);
const upcoming = getUpcomingFestivals(10, y, m, d);
return {
key: fmt(y, m, d),
dayInfo, almanac, legalHoliday, workday, termTime,
+3 -3
View File
@@ -123,14 +123,14 @@
</view>
</view>
<!-- 节日速查(默认显示3条,点击查看更多) -->
<!-- 节日速查(显示最近10条,点击查看更多) -->
<view class="card">
<view class="fest-head">
<text class="section-title">节日速查</text>
<text class="fest-more" bindtap="openZoom" data-type="fest-year" data-date="{{dayItem.key}}">更多 >></text>
</view>
<view class="fest-quick" wx:if="{{dayItem.upcoming.length > 0}}">
<view class="fest-quick-item" wx:for="{{dayItem.upcoming.slice(0, 3)}}" wx:key="name" catchtap="goFestival" data-date="{{item.solarDate}}">
<view class="fest-quick" wx:if="{{dayItem.upcoming && dayItem.upcoming.length > 0}}">
<view class="fest-quick-item" wx:for="{{dayItem.upcoming}}" wx:key="name" catchtap="goFestival" data-date="{{item.solarDate}}">
<text class="fest-name">{{item.name}}</text>
<text class="fest-date">{{item.solarMonth}}月{{item.solarDay}}日</text>
<text class="fest-left {{item.daysLeft === 0 ? 'is-today' : ''}}">{{item.daysLeft === 0 ? '今天' : item.daysLeft + '天后'}}</text>
+3
View File
@@ -491,6 +491,9 @@
font-size: 24rpx;
color: #C41E3A;
font-weight: 500;
flex-shrink: 0;
min-width: 100rpx;
text-align: right;
}
.fest-left.is-today {
+2
View File
@@ -32,6 +32,8 @@ Page({
// 按日期排序
terms.sort((a, b) => new Date(a.date) - new Date(b.date));
this.setData({ terms });
},
onShareAppMessage() {
return {
title: '二十四节气 - 节气查询',
+17 -10
View File
@@ -104,6 +104,7 @@ func Auth() gin.HandlerFunc {
}
// AdminAuth 管理员认证中间件:要求携带 role=admin 的 JWTAuthorization 头或 HttpOnly Cookie
// 浏览器导航(GET 页面请求)未登录时渲染登录页;API 请求未登录时返回 401 JSON
func AdminAuth() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := ""
@@ -115,18 +116,24 @@ func AdminAuth() gin.HandlerFunc {
tokenString, _ = c.Cookie(service.AdminTokenCookie)
}
if tokenString == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "需要管理员权限",
})
c.Abort()
return
authed := false
if tokenString != "" {
cfg := config.Load()
userService := service.NewUserService()
authed = userService.IsAdminToken(tokenString, cfg.JWT.Secret)
}
cfg := config.Load()
userService := service.NewUserService()
if !userService.IsAdminToken(tokenString, cfg.JWT.Secret) {
if !authed {
// 页面导航(GET 且非 /admin/api/):返回登录页
if c.Request.Method == http.MethodGet && !strings.HasPrefix(c.Request.URL.Path, "/admin/api/") {
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "管理员登录",
"page": "login",
"url": c.Request.URL.Path,
})
c.Abort()
return
}
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"msg": "需要管理员权限",
+13 -5
View File
@@ -5,20 +5,28 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .title }} - 祈福小助手管理后台</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/@inertiajs/vue3@1.0.0/dist/index.umd.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Vue + Inertia 通过 import map 从 esm.sh 加载(ESM,自动解析依赖树,国内可访问) -->
<script type="importmap">
{
"imports": {
"vue": "https://esm.sh/vue@3.4.21/dist/vue.esm-browser.prod.js",
"@inertiajs/vue3": "https://esm.sh/@inertiajs/vue3@1.0.0?bundle&external=vue"
}
}
</script>
</head>
<body class="bg-gray-100">
<div id="app" data-page='{"component":"{{ .page }}","props":{},"url":"{{ .url }}","version":""}'></div>
<script>
const { createApp, h } = Vue;
const { createInertiaApp } = Inertia;
<script type="module">
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
createInertiaApp({
resolve: name => {
const pages = {
login: () => import('/static/js/pages/Login.js'),
dashboard: () => import('/static/js/pages/Dashboard.js'),
users: () => import('/static/js/pages/Users.js'),
orders: () => import('/static/js/pages/Orders.js'),
+68
View File
@@ -0,0 +1,68 @@
// Login 管理员登录页
export default {
template: `
<div class="min-h-screen bg-gray-100 flex items-center justify-center px-4">
<div class="max-w-sm w-full bg-white rounded-lg shadow-md p-8">
<div class="text-center mb-6">
<h1 class="text-2xl font-bold text-red-600">祈福小助手</h1>
<p class="text-gray-500 text-sm mt-1">管理后台登录</p>
</div>
<form @submit.prevent="submit">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">管理密码</label>
<input
v-model="password"
type="password"
autocomplete="current-password"
placeholder="请输入 ADMIN_PASSWORD"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent"
:disabled="loading"
/>
</div>
<div v-if="error" class="mb-4 text-sm text-red-600">{{ error }}</div>
<button
type="submit"
:disabled="loading || !password"
class="w-full bg-red-600 text-white py-2 rounded-md hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition"
>
{{ loading ? '登录中...' : '登 录' }}
</button>
</form>
</div>
</div>
`,
data() {
return {
password: '',
loading: false,
error: ''
};
},
methods: {
async submit() {
if (!this.password || this.loading) return;
this.loading = true;
this.error = '';
try {
const res = await fetch('/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: this.password })
});
const data = await res.json();
if (data.code === 0) {
// Cookie 已由服务端写入,跳回原本要访问的页面
window.location.href = window.location.pathname === '/admin/login'
? '/admin/'
: window.location.pathname;
} else {
this.error = data.msg || '登录失败';
}
} catch (e) {
this.error = '网络错误,请重试';
} finally {
this.loading = false;
}
}
}
};