From 0ed9a15a6a7d595f8a62c10cf82f5cdfda634e86 Mon Sep 17 00:00:00 2001
From: zhangjiahuichenxi
Date: Tue, 19 May 2026 19:51:41 +0800
Subject: [PATCH 1/4] fix admin dev runtime and avatar handling
---
.env.example | 8 +-
.gitignore | 3 +
docs/src/advanced/contribution.md | 4 +-
docs/src/en/advanced/contribution.md | 4 +-
packages/admin/index.html | 3 +-
packages/admin/src/App.jsx | 103 +-
packages/admin/src/components/Header.jsx | 107 +-
packages/admin/src/components/icon/index.js | 53 +-
packages/admin/src/index.jsx | 4 +-
packages/admin/src/pages/forgot/index.jsx | 9 +-
packages/admin/src/pages/login/index.jsx | 12 +-
.../admin/src/pages/manage-comments/index.jsx | 2 +-
.../admin/src/pages/manage-comments/utils.js | 34 +-
packages/admin/src/pages/profile/index.jsx | 9 +-
packages/admin/src/pages/register/index.jsx | 9 +-
packages/admin/src/pages/user/index.jsx | 2 +-
packages/admin/src/style/custom.css | 944 ++++++++++++++++++
packages/admin/src/style/index.css | 4 +
packages/admin/src/style/style.css | 3 +-
packages/admin/src/utils/ui.js | 7 +
packages/admin/vite.config.ts | 4 +-
packages/server/development.js | 17 +
packages/server/src/service/akismet.js | 3 +-
.../server/src/service/markdown/mathjax.js | 2 +
pnpm-lock.yaml | 24 +-
25 files changed, 1233 insertions(+), 141 deletions(-)
create mode 100644 packages/admin/src/style/custom.css
create mode 100644 packages/admin/src/style/index.css
create mode 100644 packages/admin/src/utils/ui.js
diff --git a/.env.example b/.env.example
index cb264e27de6..db311bc258f 100644
--- a/.env.example
+++ b/.env.example
@@ -1,7 +1,11 @@
+SQLITE_PATH=./data
+JWT_TOKEN=waline-dev-jwt-token
+
+# PostgreSQL example
POSTGRES_DATABASE=
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_HOST=
-PORSTGRES_PORT=
+POSTGRES_PORT=
POSTGRES_PREFIX=
-POSTGRES_SSL=
\ No newline at end of file
+POSTGRES_SSL=
diff --git a/.gitignore b/.gitignore
index 792ddb9c8e3..2fe2e27667b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,6 @@ dist/
# npm config file
.npmrc
+
+# other
+data/
\ No newline at end of file
diff --git a/docs/src/advanced/contribution.md b/docs/src/advanced/contribution.md
index 9235a4fde1d..21509999f05 100644
--- a/docs/src/advanced/contribution.md
+++ b/docs/src/advanced/contribution.md
@@ -49,7 +49,9 @@ order: -1
::: tip
- 为了使 `@waline/server` 能在本地正常运行,你需要配置必要的本地环境变量至 `.env`。
+ 当你没有配置任何存储环境变量时,`pnpm run server:dev` 会默认回退到 `./data` 下的本地 SQLite 数据库。
+
+ 如果你想在本地使用其他存储后端,请在 `.env` 中配置对应的环境变量。
在 `.env.example` 我们准备了示例供你参考
diff --git a/docs/src/en/advanced/contribution.md b/docs/src/en/advanced/contribution.md
index d2ac8a96bf8..b8ec099cc39 100644
--- a/docs/src/en/advanced/contribution.md
+++ b/docs/src/en/advanced/contribution.md
@@ -49,7 +49,9 @@ If you want to contribute to waline, here is a guide.
::: tip
- In order to run `@waline/server` locally, you need to configure some local environment variables to `.env`.
+ `pnpm run server:dev` will fall back to a local SQLite database at `./data` when no storage environment variables are set.
+
+ If you want to use another storage backend locally, configure the required environment variables in `.env`.
We provide an example for you in `.env.example`.
diff --git a/packages/admin/index.html b/packages/admin/index.html
index 05e15ad74a1..35eb9bfcc24 100644
--- a/packages/admin/index.html
+++ b/packages/admin/index.html
@@ -6,7 +6,8 @@
Waline Management System
diff --git a/packages/admin/src/App.jsx b/packages/admin/src/App.jsx
index e8e5ca39c65..586535c8717 100644
--- a/packages/admin/src/App.jsx
+++ b/packages/admin/src/App.jsx
@@ -10,6 +10,7 @@ import Profile from './pages/profile/index.jsx';
import Register from './pages/register/index.jsx';
import User from './pages/user/index.jsx';
import { store } from './store/index.js';
+import { getUiPath } from './utils/ui.js';
const Access = (props) => {
const user = useSelector((state) => state.user);
@@ -18,15 +19,18 @@ const Access = (props) => {
const meta = props.meta ?? {};
const basename = props.basename ?? '';
const emptyUser = !user?.objectId;
+ const currentPath = location.pathname.replace(basename, '') || '/';
+ const redirectPath = currentPath.startsWith('/') ? currentPath : `/${currentPath}`;
+ const buildPath = (path) => `${basename}${path}`.replaceAll(/\/+/gu, '/');
if (emptyUser) {
- return (location.href = `${basename}/ui/login?redirect=${location.pathname.replace(basename, '')}`);
+ return (location.href = `${buildPath(getUiPath('login'))}?redirect=${redirectPath}`);
}
const noPermission = meta.auth ? props.meta.auth !== user.type : false;
if (noPermission) {
- return (location.href = `${basename}/ui/profile`);
+ return (location.href = buildPath(getUiPath('profile')));
}
}, [user, props.meta, props.basename]);
@@ -36,52 +40,61 @@ const Access = (props) => {
export default function App() {
const match = location.pathname.match(/(.*?)\/ui/u);
const basePath = match ? match[1] : '/';
+ const homePath = getUiPath();
+ const userPath = getUiPath('user');
+ const migrationPath = getUiPath('migration');
+ const loginPath = getUiPath('login');
+ const registerPath = getUiPath('register');
+ const forgotPath = getUiPath('forgot');
+ const profilePath = getUiPath('profile');
return (
-
-
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
- } />
- } />
- } />
-
-
-
- }
- />
-
-
+
+
+
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ } />
+ } />
+ } />
+
+
+
+ }
+ />
+
+
+
);
}
diff --git a/packages/admin/src/components/Header.jsx b/packages/admin/src/components/Header.jsx
index df081ad5361..da74e8ddf2a 100644
--- a/packages/admin/src/components/Header.jsx
+++ b/packages/admin/src/components/Header.jsx
@@ -5,6 +5,8 @@ import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router';
import { LANGUAGE_OPTIONS } from '../locales/index.js';
+import { buildAvatar } from '../pages/manage-comments/utils.js';
+import { getUiPath } from '../utils/ui.js';
export default function Header() {
const dispatch = useDispatch();
@@ -42,54 +44,75 @@ export default function Header() {
const onLogout = (event) => {
event.preventDefault();
dispatch.user.logout();
- navigate('/ui/login');
+ navigate(getUiPath('login'));
};
+ const siteName = window.SITE_NAME || 'Waline';
+
+ const navItems = [
+ { to: getUiPath(), label: t('comment') },
+ { to: getUiPath('user'), label: t('user') },
+ { to: getUiPath('migration'), label: t('migration') },
+ ];
+
return [
-
- {user?.type === 'administrator' ? (
-
- ) : null}
-
-
-
-
- {user?.type ? (
-
- {user.display_name}
+
,
+ ,
latestVersion ? (
+
+`;
+const githubSvg = ``;
+const googleSvg = ``;
+const huaweiSvg = ``;
+const oidcSvg = `
+`;
+const qqSvg = ``;
+const twitterSvg = ``;
+const weiboSvg = ``;
+
+const createIcon = (svg) =>
+ function Icon(props) {
+ return React.createElement('span', {
+ ...props,
+ dangerouslySetInnerHTML: { __html: svg },
+ });
+ };
+
+export const github = createIcon(githubSvg);
+export const twitter = createIcon(twitterSvg);
+export const facebook = createIcon(facebookSvg);
+export const weibo = createIcon(weiboSvg);
+export const qq = createIcon(qqSvg);
+export const oidc = createIcon(oidcSvg);
+export const google = createIcon(googleSvg);
+export const huawei = createIcon(huaweiSvg);
diff --git a/packages/admin/src/index.jsx b/packages/admin/src/index.jsx
index 2b46895fa4a..b96387b8aeb 100644
--- a/packages/admin/src/index.jsx
+++ b/packages/admin/src/index.jsx
@@ -6,7 +6,7 @@ import { store } from './store/index.js';
// oxlint-disable-next-line import/no-unassigned-import
import './i18n.js';
-import './style/index.scss';
+import './style/index.css';
const run = async () => {
await Promise.race([
@@ -42,7 +42,9 @@ const run = async () => {
const container = document.createElement('div');
+ document.body.classList.add('waline-admin-body');
container.style.height = '100%';
+ container.className = 'waline-admin-root';
document.body.append(container);
const root = createRoot(container);
diff --git a/packages/admin/src/pages/forgot/index.jsx b/packages/admin/src/pages/forgot/index.jsx
index 8cc3891fd89..cdb124557dc 100644
--- a/packages/admin/src/pages/forgot/index.jsx
+++ b/packages/admin/src/pages/forgot/index.jsx
@@ -4,6 +4,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router';
import Header from '../../components/Header.jsx';
+import { getUiPath } from '../../utils/ui.js';
export default function Forgot() {
const { t } = useTranslation();
@@ -16,7 +17,7 @@ export default function Forgot() {
useEffect(() => {
// if logged
if (user?.objectId) {
- navigate('/ui', { replace: true });
+ navigate(getUiPath(), { replace: true });
}
}, [navigate, user?.objectId]);
@@ -36,7 +37,7 @@ export default function Forgot() {
email,
});
alert(t('find password success! please go to your mailbox to reset it!'));
- navigate('/ui/login');
+ navigate(getUiPath('login'));
} catch {
setError(t('find password error! try again later'));
} finally {
@@ -85,8 +86,8 @@ export default function Forgot() {
- {t('back to home')} •{' '}
- {t('register.login')}
+ {t('back to home')} •{' '}
+ {t('register.login')}
diff --git a/packages/admin/src/pages/login/index.jsx b/packages/admin/src/pages/login/index.jsx
index c5f35caab98..eb46ffb80f5 100644
--- a/packages/admin/src/pages/login/index.jsx
+++ b/packages/admin/src/pages/login/index.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router';
@@ -6,7 +6,9 @@ import { Link, useNavigate } from 'react-router';
import Header from '../../components/Header.jsx';
// oxlint-disable-next-line import/no-namespace
import * as Icons from '../../components/icon/index.js';
+import { useCaptcha } from '../../components/useCaptcha.js';
import { get2FAToken } from '../../services/user.js';
+import { getUiPath } from '../../utils/ui.js';
export default function Login() {
const { t } = useTranslation();
@@ -33,7 +35,7 @@ export default function Login() {
const isAdmin = user.type === 'administrator';
- const defaultRedirect = isAdmin ? '/ui' : '/ui/profile';
+ const defaultRedirect = isAdmin ? getUiPath() : getUiPath('profile');
const redirect = isAdmin && query.get('redirect') ? query.get('redirect') : defaultRedirect;
navigate(redirect.replaceAll(/\/+/gu, '/'));
@@ -177,7 +179,7 @@ export default function Login() {
{t('remember me')}
- {t('forgot password')}
+ {t('forgot password')}
@@ -190,8 +192,8 @@ export default function Login() {
- {t('back to home')} •{' '}
- {t('register')}
+ {t('back to home')} •{' '}
+ {t('register')}
diff --git a/packages/admin/src/pages/manage-comments/index.jsx b/packages/admin/src/pages/manage-comments/index.jsx
index d286781df10..362d8eaefd3 100644
--- a/packages/admin/src/pages/manage-comments/index.jsx
+++ b/packages/admin/src/pages/manage-comments/index.jsx
@@ -500,7 +500,7 @@ export default function ManageComments() {

{
+ const label = (typeof name === 'string' ? name.trim().charAt(0) : '').toUpperCase() || 'W';
+ const fontSize = Math.round(size * 0.42);
+ const svg = `
`;
-export const buildAvatar = (email = '', avatar = '') => {
- if (avatar) return avatar;
- const normalizedEmail = typeof email === 'string' ? email : '';
+ return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
+};
+
+const isDefaultAvatar = (avatar = '') => {
+ if (!avatar) {
+ return true;
+ }
+
+ return /https?:\/\/seccdn\.libravatar\.org\/avatar\//u.test(avatar);
+};
+
+export const buildAvatar = (name = '', email = '', avatar = '') => {
+ if (!isDefaultAvatar(avatar)) {
+ return avatar;
+ }
- return `https://sdn.geekzu.org/avatar/${md5(normalizedEmail)}?s=40&r=G&d=`;
+ return buildFallbackAvatarDataUri(name);
};
export const getPostUrl = (url) => (window.SITE_URL ?? '') + url;
diff --git a/packages/admin/src/pages/profile/index.jsx b/packages/admin/src/pages/profile/index.jsx
index cf5bdd04fd0..672b21c71fa 100644
--- a/packages/admin/src/pages/profile/index.jsx
+++ b/packages/admin/src/pages/profile/index.jsx
@@ -7,6 +7,7 @@ import Header from '../../components/Header.jsx';
// oxlint-disable-next-line import/no-namespace
import * as Icons from '../../components/icon/index.js';
import { updateProfile } from '../../services/user.js';
+import { buildAvatar } from '../manage-comments/utils.js';
import TwoFactorAuth from './twoFactorAuth.jsx';
export default function Profile() {
@@ -111,9 +112,11 @@ export default function Profile() {
{user.display_name}
diff --git a/packages/admin/src/pages/register/index.jsx b/packages/admin/src/pages/register/index.jsx
index 9721bc7fd80..54f2ce76946 100644
--- a/packages/admin/src/pages/register/index.jsx
+++ b/packages/admin/src/pages/register/index.jsx
@@ -5,6 +5,7 @@ import { Link, useNavigate } from 'react-router';
import Header from '../../components/Header.jsx';
import { useCaptcha } from '../../components/useCaptcha.js';
+import { getUiPath } from '../../utils/ui.js';
export default function Register() {
const { t } = useTranslation();
@@ -20,7 +21,7 @@ export default function Register() {
useEffect(() => {
if (user?.objectId) {
- navigate('/ui', { replace: true });
+ navigate(getUiPath(), { replace: true });
}
}, [navigate, user?.objectId]);
@@ -64,7 +65,7 @@ export default function Register() {
alert(t('register success! please go to your mailbox to verify it!'));
}
- navigate('/ui/login');
+ navigate(getUiPath('login'));
} catch (err) {
setError(err.message);
} finally {
@@ -157,8 +158,8 @@ export default function Register() {
- {t('back to home')} •{' '}
- {t('register.login')}
+ {t('back to home')} •{' '}
+ {t('register.login')}
diff --git a/packages/admin/src/pages/user/index.jsx b/packages/admin/src/pages/user/index.jsx
index fe4eaaa9097..d140a1d6ba6 100644
--- a/packages/admin/src/pages/user/index.jsx
+++ b/packages/admin/src/pages/user/index.jsx
@@ -150,7 +150,7 @@ export default function User() {

p:first-child,
+.col-tb-3 > h2,
+.col-tb-3 > p + p {
+ color: var(--wa-text);
+}
+
+.typecho-list-operate {
+ margin-bottom: 18px;
+ padding: 16px 18px;
+ border: 1px solid var(--wa-border);
+ border-radius: var(--wa-radius-lg);
+ background: var(--wa-surface-strong);
+}
+
+.typecho-list-operate form,
+.typecho-list-operate .operate,
+.typecho-list-operate .search {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.typecho-list-operate form {
+ justify-content: space-between;
+ flex-wrap: wrap;
+}
+
+input[type='text'],
+input[type='password'],
+input[type='email'],
+textarea,
+select {
+ border: 1px solid var(--wa-border-strong);
+ border-radius: var(--wa-radius-sm);
+ background: #fff;
+ color: var(--wa-text);
+ box-shadow: inset 0 1px 0 rgb(255 255 255 / 6%);
+}
+
+input[type='text']:focus,
+input[type='password']:focus,
+input[type='email']:focus,
+textarea:focus,
+select:focus {
+ border-color: rgb(25 118 255 / 45%);
+ outline: none;
+ box-shadow:
+ 0 0 0 4px rgb(25 118 255 / 10%),
+ inset 0 1px 0 rgb(255 255 255 / 8%);
+}
+
+select {
+ min-height: 42px;
+ padding: 0 12px;
+}
+
+.btn {
+ border: 1px solid var(--wa-border);
+ border-radius: 999px;
+ background: #fff;
+ color: var(--wa-text);
+ font-weight: 600;
+}
+
+.btn:hover {
+ background: var(--wa-surface-soft);
+}
+
+.primary {
+ border-color: transparent;
+ background: linear-gradient(135deg, #0f6bff, #3692ff);
+ color: #fff;
+ box-shadow: 0 10px 24px rgb(25 118 255 / 18%);
+}
+
+.primary:hover {
+ background: linear-gradient(135deg, #005fde, #2488ff);
+}
+
+.dropdown-menu {
+ min-width: 180px;
+ padding: 8px;
+ border: 1px solid var(--wa-border);
+ border-radius: 16px;
+ background: rgb(255 255 255 / 96%);
+ box-shadow: var(--wa-shadow-soft);
+ backdrop-filter: blur(18px);
+}
+
+.dropdown-menu button:hover {
+ border-radius: 10px;
+ background: var(--wa-accent-soft);
+ color: var(--wa-accent-strong);
+}
+
+.typecho-table-wrap,
+.typecho-content-panel section,
+.col-tb-3,
+.typecho-login {
+ border: 1px solid var(--wa-border);
+ border-radius: 24px;
+ background: var(--wa-surface-strong);
+ box-shadow: var(--wa-shadow-soft);
+}
+
+.typecho-table-wrap {
+ overflow: hidden;
+}
+
+.typecho-list-table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+}
+
+.typecho-list-table th {
+ border-bottom: 1px solid var(--wa-border);
+ background: #f6faff;
+ color: var(--wa-text-soft);
+ font-size: 0.82rem;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.typecho-list-table th,
+.typecho-list-table td {
+ padding: 18px 16px;
+ border-color: var(--wa-border);
+ vertical-align: top;
+}
+
+.typecho-list-table tbody tr:hover {
+ background: #fbfdff;
+}
+
+.comment-content,
+.comment-body,
+.comment-head,
+.comment-meta,
+.comment-date {
+ color: var(--wa-text);
+}
+
+.comment-date {
+ margin-bottom: 10px;
+ color: var(--wa-text-soft);
+}
+
+.comment-action button,
+.comment-action span {
+ margin-right: 12px;
+ font-weight: 600;
+}
+
+button.operate-delete,
+button.operate-button-delete {
+ color: var(--wa-danger);
+}
+
+button.operate-edit,
+button.operate-reply,
+button.operate-approved,
+button.operate-waiting,
+button.operate-spam,
+button.operate-sticky,
+button.operate-administrator,
+button.operate-guest,
+button.operate-label {
+ color: var(--wa-accent-strong);
+}
+
+.typecho-content-panel section {
+ padding: 24px;
+}
+
+.typecho-content-panel h3,
+.typecho-content-panel h2,
+.col-tb-3 h2 {
+ color: var(--wa-text);
+}
+
+.col-tb-3 {
+ padding: 24px;
+ text-align: center;
+}
+
+.profile-avatar,
+.comment-avatar img,
+.user-avatar img,
+.avatar {
+ border: 3px solid rgb(255 255 255 / 85%);
+ border-radius: 50%;
+ box-shadow: 0 12px 28px rgb(44 86 134 / 14%);
+}
+
+.profile-avatar {
+ width: 88px;
+ height: 88px;
+ object-fit: cover;
+}
+
+.account-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+
+.account-item {
+ margin-right: 0;
+}
+
+.account-item > a {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 48px;
+ height: 48px;
+ border: 1px solid var(--wa-border);
+ border-radius: 14px;
+ background: #fff;
+ box-shadow: var(--wa-shadow-soft);
+}
+
+.account-item > a:hover {
+ transform: translateY(-1px);
+}
+
+.typecho-login-wrap {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: calc(100% - 110px);
+ padding: 36px 24px 48px;
+}
+
+.typecho-login {
+ width: min(460px, 100%);
+ padding: 32px;
+}
+
+.typecho-login form p {
+ margin-bottom: 16px;
+}
+
+.social-accounts {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 14px;
+ margin-top: 24px;
+}
+
+.social-accounts a {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 48px;
+ height: 48px;
+ border: 1px solid var(--wa-border);
+ border-radius: 14px;
+ background: #fff;
+}
+
+.more-link {
+ margin-top: 22px;
+ text-align: center;
+ color: var(--wa-text-soft);
+}
+
+.message.popup.notice {
+ left: 50%;
+ width: min(560px, calc(100% - 32px));
+ margin-left: 0;
+ transform: translateX(-50%);
+}
+
+.message.popup.notice ul {
+ padding: 14px 18px;
+ border: 1px solid rgb(201 73 73 / 16%);
+ border-radius: 16px;
+ background: rgb(255 244 244 / 96%);
+ box-shadow: var(--wa-shadow-soft);
+}
+
+.typecho-pager li.current button {
+ background: var(--wa-accent-soft);
+ color: var(--wa-accent-strong);
+}
+
+.typecho-pager button:hover {
+ background: var(--wa-surface-soft);
+}
+
+@media (width <= 960px) {
+ .typecho-head-nav {
+ padding-inline: 16px;
+ }
+
+ .waline-admin-header,
+ .main .body.container,
+ .typecho-login {
+ padding: 20px;
+ border-radius: 24px;
+ }
+
+ .waline-admin-header {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .waline-admin-nav,
+ .typecho-head-nav .operate {
+ width: 100%;
+ }
+
+ .typecho-head-nav .operate {
+ justify-content: space-between;
+ flex-wrap: wrap;
+ }
+
+ .main {
+ padding: 20px 16px 36px;
+ }
+
+ .typecho-page-title h2 {
+ font-size: 1.6rem;
+ }
+
+ .typecho-list-operate form,
+ .typecho-list-operate .operate,
+ .typecho-list-operate .search {
+ align-items: stretch;
+ flex-direction: column;
+ }
+}
+
+.account-item {
+ position: relative;
+ display: inline-block;
+ margin-right: 16px;
+}
+
+.account-item .account-unbind svg {
+ position: absolute;
+ top: -3px;
+ right: -3px;
+
+ display: none;
+
+ border: 1px solid #999;
+ border-radius: 50%;
+
+ background: #fff;
+
+ cursor: pointer;
+}
+
+.account-item:hover .account-unbind svg {
+ display: block;
+}
+
+.account-item.github path,
+.account-item.twitter circle,
+.account-item.facebook path,
+.account-item.weibo circle,
+.account-item.qq path,
+.account-item.oidc path,
+.account-item.huawei path {
+ fill: #808080;
+}
+
+.account-item.github:hover path,
+.account-item.github.bind path {
+ fill: #1b1f23;
+}
+
+.account-item.twitter:hover circle,
+.account-item.twitter.bind circle {
+ fill: #2daae1;
+}
+
+.account-item.facebook:hover path,
+.account-item.facebook.bind path {
+ fill: #1877f2;
+}
+
+.account-item.weibo:hover circle,
+.account-item.weibo.bind circle {
+ fill: #d34237;
+}
+
+.account-item.qq:hover path,
+.account-item.qq.bind path {
+ fill: #259be0;
+}
+
+.account-item.oidc:hover path,
+.account-item.oidc.bind path {
+ fill: #ff9626;
+}
+
+.account-item.huawei:hover path,
+.account-item.huawei.bind path {
+ fill: #f00;
+}
+
+.user-page-account-item {
+ margin-right: 6px;
+ opacity: 0.5;
+}
+
+.user-page-account-item.bind {
+ opacity: 1;
+}
+
+.user-page-account-item svg {
+ width: 16px;
+ height: 16px;
+}
+
+/* remain for historical reasons */
+.vemoji,
+.wl-emoji {
+ display: inline-block;
+ vertical-align: middle;
+ width: 1.3rem;
+ margin: 0.25rem;
+}
+
+.social-accounts a + a {
+ margin-left: 16px;
+}
+
+.upgrade-tips {
+ padding: 0 10px;
+ border-bottom: 1px solid rgb(212 167 44 / 40%);
+
+ background: #fff8c5;
+ color: #24292f;
+
+ line-height: 36px;
+}
+
+.typecho-pager button {
+ display: block;
+
+ padding: 0 10px;
+ border: none;
+ border-radius: 2px;
+
+ background: none;
+ color: inherit;
+
+ font: inherit;
+
+ cursor: pointer;
+}
+
+.typecho-pager button:hover {
+ background: #e9e9e6;
+ text-decoration: none;
+}
+
+.typecho-pager li.current button {
+ background: #e9e9e6;
+ color: #444;
+}
+
+.typecho-head-nav .operate button {
+ display: inline-block;
+
+ margin-left: -1px;
+ padding: 0 20px;
+ border: 1px solid #383d45;
+ border-width: 0 1px;
+
+ background: none;
+ color: #bbb;
+
+ font: inherit;
+ line-height: 36px;
+
+ cursor: pointer;
+}
+
+.typecho-head-nav .operate button:hover {
+ background-color: #202328;
+ color: #fff;
+}
+
+.dropdown-menu button {
+ display: block;
+
+ width: 100%;
+ padding: 5px 12px;
+ border: none;
+
+ background: none;
+ color: #666;
+
+ font: inherit;
+ text-align: left;
+
+ cursor: pointer;
+}
+
+.dropdown-menu button:hover {
+ background: #f6f6f3;
+ text-decoration: none;
+}
+
+button.operate-delete,
+button.operate-button-delete {
+ color: #b94a48;
+}
+
+button.operate-edit {
+ color: #070;
+}
+
+button.operate-reply {
+ color: #545c30;
+}
+
+.typecho-list-operate button:hover {
+ text-decoration: none;
+}
+
+.account-unbind {
+ padding: 0;
+ border: none;
+ background: none;
+ cursor: pointer;
+}
+
+.profile-avatar-btn {
+ display: block;
+
+ padding: 0;
+ border: none;
+
+ background: none;
+
+ cursor: pointer;
+}
diff --git a/packages/admin/src/style/index.css b/packages/admin/src/style/index.css
new file mode 100644
index 00000000000..3e12030a2dc
--- /dev/null
+++ b/packages/admin/src/style/index.css
@@ -0,0 +1,4 @@
+@import './normalize.css';
+@import './style.css';
+@import './grid.css';
+@import './custom.css';
diff --git a/packages/admin/src/style/style.css b/packages/admin/src/style/style.css
index d124f058354..a4f93f85466 100644
--- a/packages/admin/src/style/style.css
+++ b/packages/admin/src/style/style.css
@@ -437,8 +437,7 @@ select {
}
#typecho-nav-list .parent a:hover,
-#typecho-nav-list .focus .parent a,
-#typecho-nav-list .root:hover .parent a {
+#typecho-nav-list .focus .parent a {
background: #202328;
color: #fff;
text-decoration: none;
diff --git a/packages/admin/src/utils/ui.js b/packages/admin/src/utils/ui.js
new file mode 100644
index 00000000000..e6877789f37
--- /dev/null
+++ b/packages/admin/src/utils/ui.js
@@ -0,0 +1,7 @@
+export const uiBase = /\/ui(?:\/|$)/u.test(location.pathname) ? '/ui' : '';
+
+export const getUiPath = (path = '') => {
+ const normalizedPath = path ? `/${path.replace(/^\/+/u, '')}` : '';
+
+ return `${uiBase}${normalizedPath}` || '/';
+};
diff --git a/packages/admin/vite.config.ts b/packages/admin/vite.config.ts
index cecb05f8fab..a9a06b2f0bc 100644
--- a/packages/admin/vite.config.ts
+++ b/packages/admin/vite.config.ts
@@ -24,10 +24,10 @@ export default defineConfig({
},
server: {
+ host: '127.0.0.1',
port: 9010,
proxy: {
- '/token': 'http://localhost:9090',
- '/user': 'http://localhost:9090',
+ '/api': 'http://127.0.0.1:9090',
},
},
});
diff --git a/packages/server/development.js b/packages/server/development.js
index 8c0f83b1cf4..d5abc01058a 100644
--- a/packages/server/development.js
+++ b/packages/server/development.js
@@ -5,6 +5,23 @@ require('dotenv').config({
quiet: true,
});
+const hasStorageEnv = [
+ 'LEAN_KEY',
+ 'MONGO_DB',
+ 'PG_DB',
+ 'POSTGRES_DATABASE',
+ 'SQLITE_PATH',
+ 'MYSQL_DB',
+ 'TIDB_DB',
+ 'GITHUB_TOKEN',
+ 'TCB_ENV',
+].some((envName) => Boolean(process.env[envName]));
+
+if (!hasStorageEnv) {
+ process.env.SQLITE_PATH = path.join(__dirname, '../../data');
+ process.env.JWT_TOKEN ||= 'waline-dev-jwt-token';
+}
+
const watcher = require('think-watcher');
const Application = require('thinkjs');
diff --git a/packages/server/src/service/akismet.js b/packages/server/src/service/akismet.js
index b708c783d75..7d688deb3b4 100644
--- a/packages/server/src/service/akismet.js
+++ b/packages/server/src/service/akismet.js
@@ -2,8 +2,7 @@ const Akismet = require('akismet');
const DEFAULT_KEY = '70542d86693e';
-// oxlint-disable-next-line func-names
-module.exports = async function (comment, blog) {
+module.exports = function akismet(comment, blog) {
let { AKISMET_KEY, SITE_URL } = process.env;
if (!AKISMET_KEY) {
diff --git a/packages/server/src/service/markdown/mathjax.js b/packages/server/src/service/markdown/mathjax.js
index 76d4ac187a2..636b731fa69 100644
--- a/packages/server/src/service/markdown/mathjax.js
+++ b/packages/server/src/service/markdown/mathjax.js
@@ -1,4 +1,5 @@
const { liteAdaptor } = require('mathjax-full/js/adaptors/liteAdaptor.js');
+const { RegisterHTMLHandler } = require('mathjax-full/js/handlers/html.js');
const { AllPackages } = require('mathjax-full/js/input/tex/AllPackages.js');
const { TeX } = require('mathjax-full/js/input/tex.js');
const { mathjax } = require('mathjax-full/js/mathjax');
@@ -9,6 +10,7 @@ const { escapeHtml } = require('./utils');
const mathjaxPlugin = (md) => {
const adaptor = liteAdaptor();
+ RegisterHTMLHandler(adaptor);
const packages = AllPackages.sort();
const tex = new TeX({ packages });
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b70899c3694..6af26aedafa 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,7 +14,9 @@ overrides:
apidoc>esbuild-loader: ^4.4.3
patchedDependencies:
- apidoc: 14f1b3501c5015248e143b5b4216cb0f70cff2b6340214529fd5a63c8d59cffb
+ apidoc:
+ hash: 14f1b3501c5015248e143b5b4216cb0f70cff2b6340214529fd5a63c8d59cffb
+ path: patches/apidoc.patch
importers:
@@ -304,7 +306,7 @@ importers:
version: 4.0.5
ip2region:
specifier: ^2.3.0
- version: 2.3.0(@types/node@24.0.0)
+ version: 2.3.0(@types/node@24.12.4)
jsdom:
specifier: ^19.0.0
version: 19.0.0
@@ -358,7 +360,7 @@ importers:
version: 1.1.7
think-model-mysql2:
specifier: ^2.0.0
- version: 2.0.0(@types/node@24.0.0)
+ version: 2.0.0(@types/node@24.12.4)
think-model-postgresql:
specifier: 1.1.7
version: 1.1.7
@@ -13502,9 +13504,9 @@ snapshots:
ip-address@10.2.0: {}
- ip2region@2.3.0(@types/node@24.0.0):
+ ip2region@2.3.0(@types/node@24.12.4):
dependencies:
- '@types/node': 24.0.0
+ '@types/node': 24.12.4
is-alphabetical@2.0.1: {}
@@ -14477,9 +14479,9 @@ snapshots:
muggle-string@0.4.1: {}
- mysql2@3.22.3(@types/node@24.0.0):
+ mysql2@3.22.3(@types/node@24.12.4):
dependencies:
- '@types/node': 24.0.0
+ '@types/node': 24.12.4
aws-ssl-profiles: 1.1.2
denque: 2.1.0
generate-function: 2.3.1
@@ -16243,12 +16245,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
- think-model-mysql2@2.0.0(@types/node@24.0.0):
+ think-model-mysql2@2.0.0(@types/node@24.12.4):
dependencies:
think-debounce: 1.0.4
think-helper: 1.1.4
think-model-abstract: 1.6.5
- think-mysql: think-mysql2@2.0.0(@types/node@24.0.0)
+ think-mysql: think-mysql2@2.0.0(@types/node@24.12.4)
transitivePeerDependencies:
- '@types/node'
- supports-color
@@ -16308,10 +16310,10 @@ snapshots:
- mongodb-extjson
- snappy
- think-mysql2@2.0.0(@types/node@24.0.0):
+ think-mysql2@2.0.0(@types/node@24.12.4):
dependencies:
debug: 2.6.9
- mysql: mysql2@3.22.3(@types/node@24.0.0)
+ mysql: mysql2@3.22.3(@types/node@24.12.4)
think-debounce: 1.0.4
think-helper: 1.1.4
think-instance: 1.0.2
From 9167d9e5f72c7d943f024528efdce55dd7e3f64b Mon Sep 17 00:00:00 2001
From: zhangjiahuichenxi
Date: Tue, 19 May 2026 20:27:47 +0800
Subject: [PATCH 2/4] fix admin access redirects in effect
---
.gitignore | 3 ++-
packages/admin/src/App.jsx | 8 +++++---
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/.gitignore b/.gitignore
index 2fe2e27667b..59d755a90fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,4 +37,5 @@ dist/
.npmrc
# other
-data/
\ No newline at end of file
+data/
+error.log
diff --git a/packages/admin/src/App.jsx b/packages/admin/src/App.jsx
index 586535c8717..49b8405f163 100644
--- a/packages/admin/src/App.jsx
+++ b/packages/admin/src/App.jsx
@@ -24,13 +24,15 @@ const Access = (props) => {
const buildPath = (path) => `${basename}${path}`.replaceAll(/\/+/gu, '/');
if (emptyUser) {
- return (location.href = `${buildPath(getUiPath('login'))}?redirect=${redirectPath}`);
+ location.href = `${buildPath(getUiPath('login'))}?redirect=${redirectPath}`;
+ return;
}
- const noPermission = meta.auth ? props.meta.auth !== user.type : false;
+ const noPermission = meta.auth ? meta.auth !== user.type : false;
if (noPermission) {
- return (location.href = buildPath(getUiPath('profile')));
+ location.href = buildPath(getUiPath('profile'));
+ return;
}
}, [user, props.meta, props.basename]);
From 7317a24cc594e64e0429be476412ec7332292bbf Mon Sep 17 00:00:00 2001
From: zhangjiahuichenxi
Date: Tue, 19 May 2026 21:46:22 +0800
Subject: [PATCH 3/4] sync server ui with local admin updates
---
packages/admin/src/pages/profile/index.jsx | 7 +-
packages/admin/src/style/custom.css | 157 +++++++++++++++-----
packages/admin/src/style/style.css | 1 -
packages/server/src/middleware/dashboard.js | 33 +++-
4 files changed, 157 insertions(+), 41 deletions(-)
diff --git a/packages/admin/src/pages/profile/index.jsx b/packages/admin/src/pages/profile/index.jsx
index 672b21c71fa..35da0bb08e5 100644
--- a/packages/admin/src/pages/profile/index.jsx
+++ b/packages/admin/src/pages/profile/index.jsx
@@ -111,7 +111,12 @@ export default function Profile() {
-