Browse Source

Update frontend page allowlist to include '/test' and update SQLite database

as
npmrun 1 week ago
parent
commit
d500a013db
  1. 3
      app/components/TopNav.vue
  2. 14
      app/components/index/CardDetailModal.vue
  3. 28
      app/components/index/CardFormModal.vue
  4. 10
      app/components/index/LeftSidebar.vue
  5. 154
      app/config/cardTypes.ts
  6. 1125
      app/pages/collect/index copy.vue
  7. 1355
      app/pages/collect/index.vue
  8. 1402
      app/pages/index/index.vue
  9. 0
      app/pages/test/index.vue
  10. 2
      packages/common/config/index.ts
  11. BIN
      packages/drizzle-pkg/db.sqlite

3
app/components/TopNav.vue

@ -9,7 +9,8 @@ const router = useRouter()
const { loggedIn, user, clear, initialized } = useAuthSession()
const links: any = [
{ to: "/collect", label: "收藏" },
{ to: "/test", label: "测试" },
]
const searchQuery = ref((route.query.q as string) ?? '')

14
app/components/index/CardDetailModal.vue

@ -1,9 +1,10 @@
<script setup lang="ts">
import type { CategoryNode } from './CategoryTreeNode.vue'
import { getTypeLabel, type CardType } from '~/config/cardTypes'
export interface CardDetail {
id: number
type: 'text' | 'image' | 'image-text' | 'portfolio' | 'project'
type: CardType
image?: string
images?: string[]
title: string
@ -47,16 +48,7 @@ const categoryName = computed(() => {
return find(props.categories)
})
const typeLabel = computed(() => {
const map: Record<string, string> = {
text: '纯文本',
image: '图片',
'image-text': '图文',
portfolio: '图集',
project: '项目',
}
return map[props.card?.type ?? ''] ?? props.card?.type ?? ''
})
const typeLabel = computed(() => getTypeLabel(props.card?.type ?? ''))
const imageCount = computed(() => props.card?.images?.length ?? (props.card?.image ? 1 : 0))

28
app/components/index/CardFormModal.vue

@ -2,14 +2,7 @@
import type { CategoryNode } from './CategoryTreeNode.vue'
import type { CardDetail } from './CardDetailModal.vue'
import { request } from '~/utils/http/factory'
const CARD_TYPES = [
{ value: 'image-text', label: '图文', icon: 'lucide:image-plus' },
{ value: 'image', label: '图片', icon: 'lucide:image' },
{ value: 'text', label: '纯文本', icon: 'lucide:text' },
{ value: 'portfolio', label: '图集', icon: 'lucide:layout-grid' },
{ value: 'project', label: '项目', icon: 'lucide:folder-kanban' },
] as const
import { cardTypeRegistry, CARD_TYPE_OPTIONS, type CardType } from '~/config/cardTypes'
const ASPECT_PRESETS = [
{ label: '竖图 3:4', value: 0.75 },
@ -65,17 +58,12 @@ const isEdit = computed(() => props.mode === 'edit')
const titleText = computed(() => isEdit.value ? '编辑卡片' : '新增卡片')
const needsImage = computed(() =>
formType.value === 'image' || formType.value === 'image-text' || formType.value === 'project'
)
const needsMultiImage = computed(() => formType.value === 'portfolio')
const needsTags = computed(() => formType.value === 'project')
const needsDescription = computed(() =>
formType.value === 'text' || formType.value === 'image-text' || formType.value === 'portfolio' || formType.value === 'project'
)
//
const formFields = computed(() => cardTypeRegistry[formType.value as CardType]?.formFields ?? {})
const needsImage = computed(() => formFields.value.image ?? false)
const needsMultiImage = computed(() => formFields.value.multiImage ?? false)
const needsTags = computed(() => formFields.value.tags ?? false)
const needsDescription = computed(() => formFields.value.description ?? false)
// Flatten category tree for select
interface FlatCat { id: string; name: string; depth: number }
@ -245,7 +233,7 @@ async function onSubmit() {
<label class="cf-label">类型</label>
<div class="cf-type-grid">
<button
v-for="ct in CARD_TYPES"
v-for="ct in CARD_TYPE_OPTIONS"
:key="ct.value"
type="button"
class="cf-type-btn"

10
app/components/index/LeftSidebar.vue

@ -17,7 +17,6 @@ const props = withDefaults(defineProps<{
drawerOpen?: boolean
width?: string
side?: 'left' | 'right'
loggedIn?: boolean
}>(), {
mode: 'rail',
drawerOpen: false,
@ -90,7 +89,6 @@ const ctxItems = computed<ContextMenuItem[]>(() => [
])
function onTreeContextMenu(node: CategoryNode, event: MouseEvent) {
if (!props.loggedIn) return
if (node.id === '__uncategorized__') return
ctxNode.value = node
ctxX.value = event.clientX
@ -157,7 +155,6 @@ function onAddRoot() {
分类
</span>
<button
v-if="loggedIn"
type="button"
class="cat-add"
title="新建分类"
@ -172,7 +169,7 @@ function onAddRoot() {
<div v-if="categories.length === 0" class="empty-tip">
<Icon name="lucide:folder-open" class="empty-icon" />
<span>还没有分类</span>
<button v-if="loggedIn" type="button" class="empty-btn" @click="onAddRoot">
<button type="button" class="empty-btn" @click="onAddRoot">
新建第一个
</button>
</div>
@ -190,7 +187,6 @@ function onAddRoot() {
</div>
<IndexContextMenu
v-if="loggedIn"
:visible="ctxVisible"
:x="ctxX"
:y="ctxY"
@ -234,7 +230,6 @@ function onAddRoot() {
分类
</span>
<button
v-if="loggedIn"
type="button"
class="cat-add"
title="新建分类"
@ -249,7 +244,7 @@ function onAddRoot() {
<div v-if="categories.length === 0" class="empty-tip">
<Icon name="lucide:folder-open" class="empty-icon" />
<span>还没有分类</span>
<button v-if="loggedIn" type="button" class="empty-btn" @click="onAddRoot">
<button type="button" class="empty-btn" @click="onAddRoot">
新建第一个
</button>
</div>
@ -267,7 +262,6 @@ function onAddRoot() {
</div>
<IndexContextMenu
v-if="loggedIn"
:visible="ctxVisible"
:x="ctxX"
:y="ctxY"

154
app/config/cardTypes.ts

@ -0,0 +1,154 @@
/**
*
*
*
* 1. CardTypes
* 2. cardTypeRegistry
* 3.
*
* index.vue / CardFormModal / CardDetailModal
*/
import type { Component } from 'vue'
import WaterfallCard from '~/components/index/WaterfallCard.vue'
import WaterfallTextCard from '~/components/index/WaterfallTextCard.vue'
import WaterfallImageCard from '~/components/index/WaterfallImageCard.vue'
import WaterfallPortfolioCard from '~/components/index/WaterfallPortfolioCard.vue'
import WaterfallProjectCard from '~/components/index/WaterfallProjectCard.vue'
// ============ CardType 常量(与 drizzle schema 保持同步) ============
export const CardTypes = [
'text',
'image',
'image-text',
'portfolio',
'project',
] as const
export type CardType = (typeof CardTypes)[number]
// ============ 前端卡片数据接口 ============
export interface CardItemLike {
id?: number
type: CardType
image?: string
images?: string[]
title: string
description?: string
tags?: string[]
aspectRatio: number
categoryId?: string | null
createdAt?: string
}
// ============ 类型配置 ============
export interface CardTypeFormFields {
/** 是否需要单张图片 */
image?: boolean
/** 是否需要多张图片 */
multiImage?: boolean
/** 是否需要描述字段 */
description?: boolean
/** 是否需要标签选择 */
tags?: boolean
}
export interface CardTypeConfig {
/** 瀑布流渲染组件 */
component: Component
/** 中文标签 */
label: string
/** 图标名 (lucide:xxx) */
icon: string
/** 表单字段需求 */
formFields: CardTypeFormFields
/** 从 CardItemLike 提取该组件需要的 props */
mapProps: (item: CardItemLike) => Record<string, unknown>
}
// ============ 注册表 ============
export const cardTypeRegistry: Record<CardType, CardTypeConfig> = {
text: {
component: WaterfallTextCard,
label: '纯文本',
icon: 'lucide:text',
formFields: { description: true },
mapProps: (item) => ({
title: item.title,
description: item.description ?? '',
aspectRatio: item.aspectRatio,
}),
},
image: {
component: WaterfallImageCard,
label: '图片',
icon: 'lucide:image',
formFields: { image: true },
mapProps: (item) => ({
image: item.image ?? '',
title: item.title,
aspectRatio: item.aspectRatio,
}),
},
'image-text': {
component: WaterfallCard,
label: '图文',
icon: 'lucide:image-plus',
formFields: { image: true, description: true },
mapProps: (item) => ({
image: item.image ?? '',
title: item.title,
description: item.description ?? '',
aspectRatio: item.aspectRatio,
}),
},
portfolio: {
component: WaterfallPortfolioCard,
label: '图集',
icon: 'lucide:layout-grid',
formFields: { multiImage: true, description: true },
mapProps: (item) => ({
images: item.images ?? [],
title: item.title,
description: item.description ?? '',
aspectRatio: item.aspectRatio,
}),
},
project: {
component: WaterfallProjectCard,
label: '项目',
icon: 'lucide:folder-kanban',
formFields: { image: true, description: true, tags: true },
mapProps: (item) => ({
image: item.image ?? '',
title: item.title,
description: item.description ?? '',
tags: item.tags ?? [],
aspectRatio: item.aspectRatio,
}),
},
}
// ============ 派生数据(供表单等使用) ============
/** 类型选项列表(用于表单类型选择器) */
export const CARD_TYPE_OPTIONS = CardTypes.map((t) => ({
value: t,
label: cardTypeRegistry[t].label,
icon: cardTypeRegistry[t].icon,
}))
/** 获取类型中文标签 */
export function getTypeLabel(type: CardType | string): string {
return cardTypeRegistry[type as CardType]?.label ?? type
}
/**
* CardType
*
*/
if (import.meta.dev) {
for (const t of CardTypes) {
if (!cardTypeRegistry[t]) {
console.warn(`[cardTypes] 注册表缺少类型 "${t}" 的配置`)
}
}
}

1125
app/pages/collect/index copy.vue

File diff suppressed because it is too large

1355
app/pages/collect/index.vue

File diff suppressed because it is too large

1402
app/pages/index/index.vue

File diff suppressed because it is too large

0
app/pages/about/index.vue → app/pages/test/index.vue

2
packages/common/config/index.ts

@ -29,5 +29,5 @@ export const API_ALLOWLIST: RouteRule[] = [
export const FRONTEND_LOGIN_PATH = "/auth/login"
export const FRONTEND_REGISTER_PATH = "/auth/register"
export const FRONTEND_PAGE_ALLOWLIST = new Set(["/", FRONTEND_LOGIN_PATH, FRONTEND_REGISTER_PATH, '/about'])
export const FRONTEND_PAGE_ALLOWLIST = new Set(["/", FRONTEND_LOGIN_PATH, FRONTEND_REGISTER_PATH, '/test'])
export const FRONTEND_PAGE_GUEST_ONLY = new Set([FRONTEND_LOGIN_PATH, FRONTEND_REGISTER_PATH])

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.
Loading…
Cancel
Save