feat:新增演示

This commit is contained in:
2026-10-18 02:51:44 -07:00
parent 9b64f9fc0b
commit 961b5ca754
5 changed files with 85 additions and 3 deletions
+2
View File
@@ -41,6 +41,8 @@ SCREEN_MAX_MB=10
# 回收站里的大屏保留多少天,超过后自动彻底删除
TRASH_RETENTION_DAYS=30
THUMBNAIL_DIR=data/thumbnails
# 未登录演示:展示这个用户 ID 名下的大屏(用该账号登录后在编辑器里做即可);留空则没有演示数据
DEMO_OWNER_ID=
# 模型写的单条数据查询的超时(秒)
DATA_QUERY_TIMEOUT=10
+45 -3
View File
@@ -2,17 +2,59 @@
from typing import Annotated, Any
from fastapi import APIRouter, Query
from fastapi import APIRouter, Query, Response
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.config import settings
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
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}")
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(
default_factory=lambda: float(os.getenv("DATA_QUERY_TIMEOUT", "10"))
+16
View File
@@ -109,6 +109,22 @@ class ScreenDetail(BaseModel):
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):
revision: int
updated_at: datetime
+15
View File
@@ -428,6 +428,21 @@ class ScreenService:
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:
"""只复制草稿和封面,不复制发布状态和历史"""
source = await self.get(screen_id, owner_id)