Merge pull request 'feat:新增演示' (#13) from feat/0.3.0 into main
my-v0-server / deploy (push) Successful in 49s

Reviewed-on: #13
This commit was merged in pull request #13.
This commit is contained in:
2026-10-18 18:23:22 +08:00
5 changed files with 85 additions and 3 deletions
+2
View File
@@ -41,6 +41,8 @@ SCREEN_MAX_MB=10
# 回收站里的大屏保留多少天,超过后自动彻底删除 # 回收站里的大屏保留多少天,超过后自动彻底删除
TRASH_RETENTION_DAYS=30 TRASH_RETENTION_DAYS=30
THUMBNAIL_DIR=data/thumbnails THUMBNAIL_DIR=data/thumbnails
# 未登录演示:展示这个用户 ID 名下的大屏(用该账号登录后在编辑器里做即可);留空则没有演示数据
DEMO_OWNER_ID=
# 模型写的单条数据查询的超时(秒) # 模型写的单条数据查询的超时(秒)
DATA_QUERY_TIMEOUT=10 DATA_QUERY_TIMEOUT=10
+45 -3
View File
@@ -2,17 +2,59 @@
from typing import Annotated, Any from typing import Annotated, Any
from fastapi import APIRouter, Query from fastapi import APIRouter, Query, Response
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from app.api.screens import Service from app.api.screens import Service, thumbnail_url
from app.common.response import ApiResponse, success_response from app.common.response import ApiResponse, success_response
from app.config import settings
from app.core.exceptions import BizException from app.core.exceptions import BizException
from app.schemas.saved_screen import PublicScreen from app.schemas.saved_screen import (
DemoScreen,
DemoScreenList,
PublicScreen,
PublishBrief,
)
from app.services.screens import LIVE_VARS_MAX_CHARS, public_page from app.services.screens import LIVE_VARS_MAX_CHARS, public_page
router = APIRouter(prefix="/public", tags=["Public"]) router = APIRouter(prefix="/public", tags=["Public"])
# 演示数据的条数上限和浏览器缓存时长(秒)
DEMO_LIMIT = 20
DEMO_CACHE_SECONDS = 60
@router.get("/demo/screens")
async def list_demo_screens(
service: Service, response: Response
) -> ApiResponse[DemoScreenList]:
"""未登录演示:演示账号(DEMO_OWNER_ID)下的大屏,页面去掉数据源 headers;没配账号时为空"""
owner_id = settings.demo_owner_id
if owner_id is None:
return success_response(DemoScreenList(items=[]))
records = await service.list_demo(owner_id, DEMO_LIMIT)
response.headers["Cache-Control"] = f"public, max-age={DEMO_CACHE_SECONDS}"
return success_response(
DemoScreenList(
items=[
DemoScreen(
screen_id=record.screen_id,
name=record.name,
page=public_page(record.page),
thumbnail_url=thumbnail_url(record.thumbnail),
revision=record.revision,
updated_at=record.updated_at,
publish=None
if record.publish is None
else PublishBrief.model_validate(
record.publish.model_dump(exclude={"revision"})
),
)
for record in records
]
)
)
@router.get("/screens/{share_id}") @router.get("/screens/{share_id}")
async def get_public_screen( async def get_public_screen(
+7
View File
@@ -133,6 +133,13 @@ class Settings:
) )
) )
# 演示账号的用户 ID:未登录访问时展示这个账号下的大屏,不配置则没有演示数据
demo_owner_id: int | None = field(
default_factory=lambda: (
int(value) if (value := os.getenv("DEMO_OWNER_ID", "").strip()) else None
)
)
# 模型写的单条数据查询的超时(秒) # 模型写的单条数据查询的超时(秒)
data_query_timeout: float = field( data_query_timeout: float = field(
default_factory=lambda: float(os.getenv("DATA_QUERY_TIMEOUT", "10")) default_factory=lambda: float(os.getenv("DATA_QUERY_TIMEOUT", "10"))
+16
View File
@@ -109,6 +109,22 @@ class ScreenDetail(BaseModel):
publish: PublishBrief | None publish: PublishBrief | None
class DemoScreen(BaseModel):
"""未登录演示用的大屏:带页面,前端据此在内存里还原一份可随意修改的大屏列表"""
screen_id: str
name: str
page: dict[str, Any]
thumbnail_url: str | None
revision: int
updated_at: datetime
publish: PublishBrief | None
class DemoScreenList(BaseModel):
items: list[DemoScreen]
class ScreenSaved(BaseModel): class ScreenSaved(BaseModel):
revision: int revision: int
updated_at: datetime updated_at: datetime
+15
View File
@@ -428,6 +428,21 @@ class ScreenService:
deleted=deleted, deleted=deleted,
) )
async def list_demo(self, owner_id: int, limit: int) -> list[ScreenRecord]:
"""演示账号下没删除的大屏(最近修改在前),带页面"""
summaries, _ = await self._store.search(
owner_id,
keyword=None,
status=None,
sort="updated_at",
descending=True,
limit=limit,
offset=0,
deleted=False,
)
records = [await self._store.get(item.screen_id, owner_id) for item in summaries]
return [record for record in records if record is not None]
async def copy_screen(self, screen_id: str, owner_id: int, name: str | None) -> str: async def copy_screen(self, screen_id: str, owner_id: int, name: str | None) -> str:
"""只复制草稿和封面,不复制发布状态和历史""" """只复制草稿和封面,不复制发布状态和历史"""
source = await self.get(screen_id, owner_id) source = await self.get(screen_id, owner_id)