Browse Source

refactor(config): enhance UI layout and improve user experience in admin and profile settings

- Updated the layout of the admin configuration and user profile pages for better visual appeal and usability.
- Introduced rounded borders and background enhancements for containers and cards.
- Improved text visibility with highlighted styles and added informative descriptions.
- Consolidated action buttons into sticky footers for easier access to save functionality.
- Removed redundant elements and streamlined the form structure for a cleaner interface.

This update significantly enhances the user experience by providing a more modern and intuitive design.
main
npmrun 3 weeks ago
parent
commit
2e4fe22513
  1. 61
      app/pages/me/admin/config/index.vue
  2. 23
      app/pages/me/profile/index.vue
  3. 106
      docs/superpowers/specs/2026-04-21-personal-home-ejs-design.md
  4. BIN
      packages/drizzle-pkg/db.sqlite

61
app/pages/me/admin/config/index.vue

@ -145,22 +145,26 @@ async function sendTestEmail() {
</script> </script>
<template> <template>
<UContainer class="py-8 space-y-8 max-w-4xl"> <UContainer class="!max-w-none w-full space-y-6 py-8">
<h1 class="text-2xl font-semibold"> <div class="rounded-2xl border border-default/70 bg-elevated/30 px-5 py-4 sm:px-6">
应用配置 <h1 class="text-2xl font-semibold text-highlighted">
</h1> 应用配置
<p class="text-sm text-muted"> </h1>
全局设置站点名称开放注册媒体孤儿自动清扫与评论通知邮件 <p class="mt-1 text-sm text-muted">
</p> 管理全局设置站点名称注册开关媒体自动清扫与评论通知邮件
</p>
</div>
<UCard> <UCard class="rounded-2xl border border-default/70 shadow-sm">
<template #header> <template #header>
全局项 <div class="font-semibold text-highlighted">
全局项
</div>
</template> </template>
<div v-if="loading" class="text-muted"> <div v-if="loading" class="text-muted">
加载中 加载中
</div> </div>
<div v-else class="space-y-4 max-w-xl"> <div v-else class="space-y-4">
<UFormField label="站点名称"> <UFormField label="站点名称">
<UInput v-model="siteName" maxlength="64" placeholder="Person Panel" /> <UInput v-model="siteName" maxlength="64" placeholder="Person Panel" />
</UFormField> </UFormField>
@ -188,20 +192,31 @@ async function sendTestEmail() {
step="1" step="1"
/> />
</UFormField> </UFormField>
<UButton :loading="saving" @click="save">
保存
</UButton>
</div> </div>
</UCard> </UCard>
<UCard> <UCard class="rounded-2xl border border-default/70 shadow-sm">
<template #header> <template #header>
评论通知邮件 <div class="flex flex-wrap items-center justify-between gap-3">
<div class="font-semibold text-highlighted">
评论通知邮件
</div>
<UButton
color="neutral"
variant="soft"
size="sm"
:loading="testingEmail"
:disabled="loading || saving || !commentEmailConfigReady"
@click="sendTestEmail"
>
发送测试邮件
</UButton>
</div>
</template> </template>
<div v-if="loading" class="text-muted"> <div v-if="loading" class="text-muted">
加载中 加载中
</div> </div>
<div v-else class="space-y-4 max-w-xl"> <div v-else class="space-y-4">
<UFormField <UFormField
label="启用评论邮件通知" label="启用评论邮件通知"
description="关闭后将跳过评论邮件发送。" description="关闭后将跳过评论邮件发送。"
@ -258,15 +273,13 @@ async function sendTestEmail() {
> >
<UInput v-model="commentSmtpPass" type="password" placeholder="smtp password" /> <UInput v-model="commentSmtpPass" type="password" placeholder="smtp password" />
</UFormField> </UFormField>
<div class="flex gap-3">
<UButton :loading="saving" @click="save">
保存
</UButton>
<UButton color="neutral" variant="soft" :loading="testingEmail" @click="sendTestEmail">
发送测试邮件
</UButton>
</div>
</div> </div>
</UCard> </UCard>
<div class="sticky bottom-3 z-[1] flex flex-wrap justify-end gap-3 border-t border-default/60 bg-default/70 pt-4 backdrop-blur">
<UButton :loading="saving" :disabled="loading" size="lg" icon="i-lucide-save" @click="save">
保存配置
</UButton>
</div>
</UContainer> </UContainer>
</template> </template>

23
app/pages/me/profile/index.vue

@ -268,10 +268,15 @@ async function save() {
</script> </script>
<template> <template>
<UContainer class="py-8 max-w-2xl space-y-6"> <UContainer class="!max-w-none w-full space-y-6 py-8">
<h1 class="text-2xl font-semibold"> <div class="rounded-2xl border border-default/70 bg-elevated/30 px-5 py-4 sm:px-6">
个人资料 <h1 class="text-2xl font-semibold text-highlighted">
</h1> 个人资料
</h1>
<p class="mt-1 text-sm text-muted">
管理你的公开展示信息通知偏好和个人资料修改后记得保存
</p>
</div>
<div class="relative min-h-[28rem]"> <div class="relative min-h-[28rem]">
<div <div
v-show="loading" v-show="loading"
@ -284,7 +289,7 @@ async function save() {
</div> </div>
<UForm <UForm
:state="state" :state="state"
class="space-y-4" class="space-y-5 rounded-2xl border border-default/70 bg-default/40 p-4 shadow-sm backdrop-blur-[1px] sm:p-6 lg:p-8"
:class="loading ? 'pointer-events-none select-none opacity-50' : ''" :class="loading ? 'pointer-events-none select-none opacity-50' : ''"
@submit.prevent="save" @submit.prevent="save"
> >
@ -440,9 +445,11 @@ async function save() {
> >
<UTextarea v-model="state.linksJson" :rows="8" class="w-full font-mono text-sm" /> <UTextarea v-model="state.linksJson" :rows="8" class="w-full font-mono text-sm" />
</UFormField> </UFormField>
<UButton type="submit" :loading="saving" :disabled="loading"> <div class="sticky bottom-3 z-[1] flex justify-end border-t border-default/60 bg-default/70 pt-4 backdrop-blur">
保存 <UButton type="submit" :loading="saving" :disabled="loading" size="lg" icon="i-lucide-save">
</UButton> 保存资料
</UButton>
</div>
</UForm> </UForm>
</div> </div>

106
docs/superpowers/specs/2026-04-21-personal-home-ejs-design.md

@ -1,106 +0,0 @@
# 设计:个人主页可选 EJS 整页渲染(多页可扩展)
**日期**:2026-04-21
**状态**:已定稿(修订:根路径解耦 + 多页自建)
**修订说明**
1. **`/@:publicSlug`** 不被 EJS 覆盖;**始终** Vue 默认主页。
2. 个性化整页使用 **独立路由族**,**不**与 Nuxt 根路由抢请求。
3. **不止一条**:支持 **多条** EJS 页面;主页所有者在控制台 **自行添加 / 编辑 / 禁用 / 删除**(具体交互由实现计划细化)。**不再**假设全局仅有一个 `custom` 字符串模板字段。
## 1. 背景与目标
当前公开个人根路径 `/@:publicSlug`**Nuxt 页面** `app/pages/@[publicSlug]/index.vue`**`layout: public`** 渲染,视觉与结构受站内组件体系约束。
**目标**:
- **`/@:publicSlug`**:**不覆盖、不劫持**,**始终** Vue。
- 在 **`@` 命名空间**下增加 **固定前缀 + 动态页键**,由服务端在用户启用时输出 **完整 HTML**(EJS),条数 **多条**,键由用户 **添加时指定**(在约束内)。
- 模板内可链到 `/@slug/posts/...`、`timeline`、`about` 等现有 Vue 子路由。
- **模板能力(已确认)**:**完整 EJS**(含任意 `<% %>`);等价于在服务器上以进程身份执行 **用户可控代码**,**信任模型与滥用责任由运营侧承担**;本版 **不做** 沙箱化必选。
## 2. 产品规则(已确认)
| 维度 | 规则 |
|------|------|
| 根路径 | **`/@:publicSlug`** **仅** Vue。 |
| 个性化页 URL | **`/@:publicSlug/<PAGES_PREFIX>/<pageKey>`**(及同义尾部 `/`)。`<PAGES_PREFIX>` 为 **全站常量**(建议 `p`,短且易记;实现层 `PUBLIC_EJS_PAGES_PREFIX`),**不得**与用户内容的第一段自由混淆——访客能一眼看出是「自定义页族」。`<pageKey>` 为 **单段** 路径(见下条)。 |
| `pageKey` | 由用户在添加页面时设定;**约束**:长度、字符集(建议 `[a-z0-9-]` 小写 storage、展示可折叠)、**保留字** 拒绝(含与站内 **一级** 路径同名者,如 `posts`、`timeline`、`about`、`reading`、**以及前缀自身** `p` 若会产生歧义时由实现明确——至少禁止 `pageKey` 为空、`.`、`..`)。**同一 `publicSlug``pageKey` 唯一**。 |
| 条数上限 | **每用户可配置页面数上限**(具体数字实现定,如 20~50),写入控制台与错误提示;本 spec 不定死数字。 |
| 命中条件 | 用户 **`publicSlug` 有效**;存在一行 **`pageKey` 匹配** 且 **启用****`ejsSource` 非空**;否则对该 URL **404**。 |
| 子路由 | `/@:publicSlug/posts/...`、`timeline`、`about` 等 **始终** Vue;EJS 仅命中 **`/<PAGES_PREFIX>/:pageKey`** 双段结构,**不** 向下吞更深路径(若未来要「子路径」,另开设计)。 |
| 编辑权限 | **仅** 对应用户可在控制台 **增删改** 各页的 `pageKey`(或仅创建时可写、创建后只读——产品二选一,默认 **创建后可改 key** 易破坏外链,建议 **创建后 key 只读****改 key 视为新页 + 旧 URL 301**,实现计划定夺)。 |
| 数据真相 | 注入 EJS 的公开数据与 **`GET /api/public/profile/:publicSlug`** **语义一致**,通过 **共享 server 层** 组装;并额外注入 **当前页** 元信息(见 4.2)。 |
## 3. 非目标(本版明确不做)
- **不** 覆盖 **`/@:publicSlug`**。
- **不** 用 EJS 接管除 **`/<PAGES_PREFIX>/:pageKey`** 以外的 `/@slug/**`
- **不** 在本版要求 **沙箱**;不将「受限模板」作为交付前提。
- **不** 用仅客户端 `v-html` 冒充 **整页 SSR HTML** 主路径。
## 4. 架构(推荐实现)
### 4.1 Nitro 短路:匹配 **`/@…/p/:pageKey`**
**Nitro** 侧(`server/middleware` 或顺序明确的钩子 / 专用 route):
1. 路径匹配 **`^\/@(?<publicSlug>[^/]+)\/<PAGES_PREFIX>\/(?<pageKey>[^/]+)\/?$`**(`<PAGES_PREFIX>` 与常量一致)。
2. `publicSlug` 解析用户;按 **`(userId, pageKey)`** 查 **自定义页表**
3. 命中且启用:组装 `locals`,**`ejs.render`**,**`text/html`**,结束响应。
4. **未命中**:**显式 404**(不要落到无关 Vue 页)。
**与根路径**:**`/@slug`** 永不进入本分支。
### 4.2 数据与注入
- **profile 单一来源**:**`getPublicProfilePayloadForHome(publicSlug)`**(名称可调整)与 **`GET /api/public/profile/:publicSlug`** 共用。
- **`ejs.render(template, locals)`** 建议包含:
- **`profile`**:与公开 API **`data`** 同构;
- **`site`**:站名、公开根 URL;
- **`page`**:`{ key, path }`,`path` 为当前页规范路径(含前缀),便于模板内自链;
- **URL 辅助**:**`profileRootPath`**、`buildSitePath('posts')`** 等(与现有路由约定一致);
- **安全向 helper**:`escapeHtml`、日期格式化等(与选项 A 一致,不消灭任意 EJS 能力)。
- **禁止项**:不向 `locals` 注入无界 **`require`**;**`ejs` 选项** 避免引入磁盘任意读;模板仅存 **DB**,文件名不传真实 FS **`root`**。
### 4.3 存储
- **独立表**(推荐):**`user_ejs_pages`**(名可微调):`user_id`、`page_key`、`ejs_source`、`enabled`、`created_at`、`updated_at`;**唯一约束 `(user_id, page_key)`**。
- **不** 再依赖用户表上单字段 **`homeEjsSource`** 作为唯一真相(若历史上存在,迁移为表或弃用)。
- **单页模板体积**、**全站每用户页数** 限流在服务层强制。
### 4.4 可选:站内发现
- Vue 根主页可列出「自定义页」入口(只展示 `enabled` 的页);**本 spec 不强制定 UI**。
## 5. 安全与滥用(选项 A 下的最低工程线)
- **单请求** 渲染 **超时**、**单模板体积** 上限、**每用户页数** 上限。
- 访客 **500/错误页** 不泄露栈;**预览/保存** 可对主人返回语法错误。
- 运营可 **禁用某页****封禁用户** 使对应 URL **404**
## 6. 错误与兼容性
- 运行时错误:**日志** + 访客安全页。
- **不** 做「自动删除页」类降级,除非产品后续明确要求并文案提示。
## 7. 测试
- **单元测试**:`ejs.render` + 固定 `locals`;`pageKey` 与保留字校验。
- **集成 / 手测**
- 同一用户两条不同 `pageKey` 均可 200;
- 禁用或删除后 **404**
- **`/@slug`**、**`/@slug/posts/...`** 仍为 Vue。
## 8. 依赖
- **`ejs`**;如有 **`@types/ejs`** 则 dev。
## 9. 与早期「仅 `/custom`」草案的关系
- 早期单一路径 **`/@slug/custom`** 视为 **仅 `pageKey = 'custom'` 的特殊情况**;现以 **通用多页模型** 为准。若产品仍需 **固定别名**(例如默认生成一页 `welcome`),可在实现时 **种子一行**,**不**再单独写死单一路由处理器(除非兼容迁移需要)。
---
**下一实现阶段**:见 `docs/superpowers/plans/`**implementation plan**(`writing-plans` 产出)。

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.
Loading…
Cancel
Save