diff --git a/CHANGELOG.md b/CHANGELOG.md index 96e99ff..1c4d2c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ 本仓库自 **v1.0.0** 起作为正式版维护;此前历史提交已归档重置。 +## [1.0.2] - 2026-08-23 + +### 新增 + +- 中英文界面(`@nuxtjs/i18n`),语言切换与主题切换独立菜单 +- 移动端导航:汉堡菜单 + 分组用户菜单(语言 / 外观 / 账户) +- Docker 忘记密码:`docker exec pichost reset-password`(随机密码,见 `server/cli/`) + +### 改进 + +- 统计页工具栏与图库布局(桌面单行、移动端适配) +- 上传卡片视觉优化(背景图、插图、悬停边框动效) +- Logo 与上传区图片压缩,减小体积 + ## [1.0.1] - 2026-08-22 ### 修复 @@ -33,5 +47,6 @@ - **v1.1.0**(计划中):S3、Cloudflare R2 及更多对象存储后端 +[1.0.2]: https://github.com/O96u/PicHost/releases/tag/v1.0.2 [1.0.1]: https://github.com/O96u/PicHost/releases/tag/v1.0.1 [1.0.0]: https://github.com/O96u/PicHost/releases/tag/v1.0.0 diff --git a/Dockerfile b/Dockerfile index 8d603d4..881bed0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,8 +20,11 @@ ENV NODE_ENV=production \ DATA_DIR=/data COPY --from=build /app/.output ./.output +COPY server/cli/reset-password.mjs ./server/cli/reset-password.mjs +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh EXPOSE 6892 VOLUME /data -CMD ["node", ".output/server/index.mjs"] +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/README.en.md b/README.en.md index c106506..282dcf3 100644 --- a/README.en.md +++ b/README.en.md @@ -68,6 +68,15 @@ docker run -d \ Default port: **6892**. Open `http://:6892` in your browser and complete the setup wizard. +**Forgot password** (requires `docker exec` / server access): + +```bash +docker exec pichost reset-password # admin only, when there is exactly one +docker exec pichost reset-password # admin or regular user +``` + +A random password is printed. Fails if the user does not exist. + If you cloned the repo, you can also use `docker compose up -d` (see `docker-compose.yml`). ### Local development @@ -185,7 +194,8 @@ npm test # Unit tests | Version | Plan | |---------|------| -| **v1.0.1** | Settings version display, mobile upload preferences layout fix (current) | +| **v1.0.2** | i18n, mobile menus, stats/upload UI, Docker password reset (current) | +| **v1.0.1** | Settings version display, mobile upload preferences layout fix | | **v1.0.0** | Local storage, multi-user, API, Twikoo | | **v1.1.0** | S3, Cloudflare R2, and more object storage backends | diff --git a/README.md b/README.md index 8c27971..76948e0 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,20 @@ docker run -d \ 默认端口 **6892**。浏览器打开 `http://<主机IP>:6892`,按引导创建管理员即可。 +**忘记密码**(需能执行 `docker exec`,相当于服务器权限): + +```bash +# 无参数:重置管理员(仅当系统中只有一个管理员时) +docker exec pichost reset-password + +# 指定用户名:可重置管理员或普通用户 +docker exec pichost reset-password 用户名 +``` + +终端会打印随机新密码;用户不存在时会报错。登录后请到「修改密码」更换。 + +本地:`npm run reset-password` 或 `npm run reset-password -- 用户名` + 已克隆仓库时也可用 `docker compose up -d`(见仓库内 `docker-compose.yml`)。 ### 本地开发 @@ -185,7 +199,8 @@ npm test # 单元测试 | 版本 | 计划 | |------|------| -| **v1.0.1** | 设置页版本展示、移动端上传偏好布局修复(当前) | +| **v1.0.2** | 中英文、移动端菜单、统计/上传 UI、Docker 重置密码(当前) | +| **v1.0.1** | 设置页版本展示、移动端上传偏好布局修复 | | **v1.0.0** | 本地存储、多用户、API、Twikoo | | **v1.1.0** | S3、Cloudflare R2 等对象存储后端 | diff --git a/app/app.vue b/app/app.vue index 0d2789a..96d0ecc 100644 --- a/app/app.vue +++ b/app/app.vue @@ -1,8 +1,10 @@ diff --git a/app/assets/css/main.css b/app/assets/css/main.css index 3329b45..7dfffc1 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -59,14 +59,31 @@ border-radius: 1rem; border-width: 2px; border-style: dashed; - border-color: var(--ui-border); - background-color: color-mix(in oklab, var(--ui-bg-elevated) 50%, transparent); + background-color: color-mix(in oklab, var(--ui-bg-elevated) 72%, transparent); padding: 2.5rem; + transition: + border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), + background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.upload-card-surface:not(.upload-card-surface--panel) { + border-color: var(--ui-border); +} + +.upload-card-surface:not(.upload-card-surface--panel):hover, +.upload-card-surface:not(.upload-card-surface--panel).upload-card-surface--active { + border-color: var(--ui-primary); + background-color: color-mix(in oklab, var(--ui-primary) 10%, var(--ui-bg-elevated) 72%); + box-shadow: 0 0 0 1px color-mix(in oklab, var(--ui-primary) 25%, transparent), + 0 8px 24px color-mix(in oklab, var(--ui-primary) 18%, transparent); } .upload-card-surface--panel { border-style: solid; + border-color: var(--ui-border); background-color: var(--ui-bg-elevated); + background-image: none; box-shadow: var(--ui-shadow-sm); padding: 0; } diff --git a/app/assets/image/logo-dark.png b/app/assets/image/logo-dark.png index cbd6f2c..19eed6d 100644 Binary files a/app/assets/image/logo-dark.png and b/app/assets/image/logo-dark.png differ diff --git a/app/assets/image/logo-light.png b/app/assets/image/logo-light.png index 7868e6f..b0f1bdc 100644 Binary files a/app/assets/image/logo-light.png and b/app/assets/image/logo-light.png differ diff --git a/app/assets/image/upload-card-bg.webp b/app/assets/image/upload-card-bg.webp new file mode 100644 index 0000000..2966455 Binary files /dev/null and b/app/assets/image/upload-card-bg.webp differ diff --git a/app/assets/image/upload-card-hero.webp b/app/assets/image/upload-card-hero.webp new file mode 100644 index 0000000..e9c94fd Binary files /dev/null and b/app/assets/image/upload-card-hero.webp differ diff --git a/app/components/AdminLoginGate.vue b/app/components/AdminLoginGate.vue index b9ebcbb..5f959c6 100644 --- a/app/components/AdminLoginGate.vue +++ b/app/components/AdminLoginGate.vue @@ -9,6 +9,7 @@ const emit = defineEmits<{ const router = useRouter() const { login, fetchStatus, authStatus } = useAuth() const toast = useToast() +const { t } = useI18n() const username = ref('') const password = ref('') @@ -47,10 +48,10 @@ async function submit() { password.value = '' secret.value = '' emit('success') - toast.add({ title: '登录成功', color: 'success' }) + toast.add({ title: t('auth.loginSuccess'), color: 'success' }) } else { toast.add({ - title: result.error ?? (legacyMode.value ? '登录失败,请检查密钥' : '用户名或密码错误'), + title: result.error ?? (legacyMode.value ? t('auth.loginFailedSecret') : t('auth.loginFailedCredentials')), color: 'error' }) } @@ -61,7 +62,10 @@ async function submit() { @@ -127,12 +131,12 @@ async function submit() { for="login-username" class="block text-sm font-medium text-default" > - 用户名 + {{ t('auth.username') }} - 密码 + {{ t('auth.password') }} - 没有账号? + {{ t('auth.noAccount') }} - 注册 + {{ t('auth.register') }}

- 未登录时无法访问上传和管理功能 + {{ t('auth.loginFooter') }}

diff --git a/app/components/AppShell.vue b/app/components/AppShell.vue index db11b5d..8a8a92e 100644 --- a/app/components/AppShell.vue +++ b/app/components/AppShell.vue @@ -4,59 +4,27 @@ import logoLight from '~/assets/image/logo-light.png' import logoDark from '~/assets/image/logo-dark.png' const route = useRoute() -const colorMode = useColorMode() const { logout, user, isAdmin } = useAuth() +const { t } = useI18n() + +const mobileNavOpen = ref(false) +const mobileUserOpen = ref(false) const navItems = computed(() => { const items = [ - { label: 'API', to: '/api', icon: 'i-lucide-code-2' }, - { label: '统计', to: '/stats', icon: 'i-lucide-chart-column' } + { label: t('nav.api'), to: '/api', icon: 'i-lucide-code-2' }, + { label: t('nav.stats'), to: '/stats', icon: 'i-lucide-chart-column' } ] if (isAdmin.value) { - items.push({ label: '设置', to: '/settings', icon: 'i-lucide-settings' }) + items.push({ label: t('nav.settings'), to: '/settings', icon: 'i-lucide-settings' }) } return items }) -const themeIcon = computed(() => { - if (colorMode.preference === 'system') return 'i-lucide-monitor' - return colorMode.preference === 'dark' ? 'i-lucide-moon' : 'i-lucide-sun' -}) - -const themeItems = computed(() => [[ - { - label: '浅色', - icon: 'i-lucide-sun', - type: 'checkbox', - checked: colorMode.preference === 'light', - onUpdateChecked(checked: boolean) { - if (checked) colorMode.preference = 'light' - } - }, - { - label: '深色', - icon: 'i-lucide-moon', - type: 'checkbox', - checked: colorMode.preference === 'dark', - onUpdateChecked(checked: boolean) { - if (checked) colorMode.preference = 'dark' - } - }, - { - label: '跟随系统', - icon: 'i-lucide-monitor', - type: 'checkbox', - checked: colorMode.preference === 'system', - onUpdateChecked(checked: boolean) { - if (checked) colorMode.preference = 'system' - } - } -]]) - -const userMenuItems = computed(() => { +const accountMenuItems = computed(() => { const items: DropdownMenuItem[] = [ { - label: '修改密码', + label: t('nav.changePassword'), icon: 'i-lucide-key-round', to: '/account/password' } @@ -64,27 +32,39 @@ const userMenuItems = computed(() => { if (isAdmin.value) { items.push({ - label: '系统设置', + label: t('nav.systemSettings'), icon: 'i-lucide-settings', to: '/settings' }) } items.push({ - label: '退出登录', + label: t('nav.logout'), icon: 'i-lucide-log-out', onSelect() { void handleLogout() } }) - return [items] + return items }) +const desktopUserMenuItems = computed(() => [ + accountMenuItems.value +]) + function isNavActive(to: string) { return route.path.startsWith(to) } +watch(mobileNavOpen, (open) => { + if (open) mobileUserOpen.value = false +}) + +watch(mobileUserOpen, (open) => { + if (open) mobileNavOpen.value = false +}) + async function handleLogout() { await logout() } @@ -93,83 +73,94 @@ async function handleLogout() {