feat: showcase模块 - 产品展示平台(MVP)
- 4张数据表: posts/comments/likes/downloads - 5种媒体类型: music/mtv/short_video/long_video/ktv - 社交功能: 点赞(toggle)、评论(嵌套回复) - KTV付费下载: 购买记录、下载计数 - 11个dspy API端点 - 3个CRUD管理界面(posts/comments/downloads) - Feed流(类型筛选+分页)、作品详情(浏览计数+点赞状态) - load_path.py RBAC权限注册 - 符合module/db-table/crud三规范
This commit is contained in:
parent
3bae870d7c
commit
1cce05a119
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# CRUD generated directories
|
||||||
|
wwwroot/showcase_posts_list/
|
||||||
|
wwwroot/showcase_comments_list/
|
||||||
|
wwwroot/showcase_downloads_list/
|
||||||
|
# Build artifacts
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
15
init/data.json
Normal file
15
init/data.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"appcodes": [
|
||||||
|
{
|
||||||
|
"parentid": "showcase_media_type",
|
||||||
|
"parentname": "展示作品媒体类型",
|
||||||
|
"items": [
|
||||||
|
{"k": "music", "v": "音乐"},
|
||||||
|
{"k": "mtv", "v": "MTV"},
|
||||||
|
{"k": "short_video", "v": "短视频"},
|
||||||
|
{"k": "long_video", "v": "长视频"},
|
||||||
|
{"k": "ktv", "v": "KTV"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
json/showcase_comments_list.json
Normal file
16
json/showcase_comments_list.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"tblname": "showcase_comments",
|
||||||
|
"alias": "showcase_comments_list",
|
||||||
|
"title": "评论管理",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["created_at desc"],
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["reply_to_user_id"]
|
||||||
|
},
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('../api/showcase_comment_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/showcase_comment_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/showcase_comment_delete.dspy')}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
json/showcase_downloads_list.json
Normal file
25
json/showcase_downloads_list.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"tblname": "showcase_downloads",
|
||||||
|
"alias": "showcase_downloads_list",
|
||||||
|
"title": "KTV下载记录",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["created_at desc"],
|
||||||
|
"browserfields": {
|
||||||
|
"alters": {
|
||||||
|
"payment_status": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "0", "text": "待支付"},
|
||||||
|
{"value": "1", "text": "已支付"},
|
||||||
|
{"value": "2", "text": "已退款"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('../api/showcase_download_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/showcase_download_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/showcase_download_delete.dspy')}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
json/showcase_posts_list.json
Normal file
43
json/showcase_posts_list.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"tblname": "showcase_posts",
|
||||||
|
"alias": "showcase_posts_list",
|
||||||
|
"title": "展示作品管理",
|
||||||
|
"params": {
|
||||||
|
"sortby": ["created_at desc"],
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": ["model_input", "model_output"],
|
||||||
|
"alters": {
|
||||||
|
"media_type": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "music", "text": "音乐"},
|
||||||
|
{"value": "mtv", "text": "MTV"},
|
||||||
|
{"value": "short_video", "text": "短视频"},
|
||||||
|
{"value": "long_video", "text": "长视频"},
|
||||||
|
{"value": "ktv", "text": "KTV"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "0", "text": "待审核"},
|
||||||
|
{"value": "1", "text": "已发布"},
|
||||||
|
{"value": "2", "text": "已下架"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editable": {
|
||||||
|
"new_data_url": "{{entire_url('../api/showcase_post_create.dspy')}}",
|
||||||
|
"update_data_url": "{{entire_url('../api/showcase_post_update.dspy')}}",
|
||||||
|
"delete_data_url": "{{entire_url('../api/showcase_post_delete.dspy')}}"
|
||||||
|
},
|
||||||
|
"data_filter": {
|
||||||
|
"AND": [
|
||||||
|
{"field": "media_type", "op": "=", "var": "media_type_input"},
|
||||||
|
{"field": "title", "op": "LIKE", "var": "title_input"},
|
||||||
|
{"field": "status", "op": "=", "var": "status_input"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
models/showcase_comments.json
Normal file
29
models/showcase_comments.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "showcase_comments",
|
||||||
|
"title": "作品评论表",
|
||||||
|
"primary": ["id"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "post_id", "title": "作品ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "user_id", "title": "评论者ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "user_name", "title": "评论者名称", "type": "str", "length": 128},
|
||||||
|
{"name": "parent_id", "title": "父评论ID(嵌套回复)", "type": "str", "length": 32},
|
||||||
|
{"name": "reply_to_user_id", "title": "回复目标用户ID", "type": "str", "length": 32},
|
||||||
|
{"name": "reply_to_user_name", "title": "回复目标用户名", "type": "str", "length": 128},
|
||||||
|
{"name": "content", "title": "评论内容", "type": "text", "nullable": "no"},
|
||||||
|
{"name": "like_count", "title": "评论点赞数", "type": "int", "default": "0"},
|
||||||
|
{"name": "reply_count", "title": "回复数", "type": "int", "default": "0"},
|
||||||
|
{"name": "status", "title": "状态(1正常/0隐藏/2删除)", "type": "str", "length": 1, "default": "1"},
|
||||||
|
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{"name": "idx_comments_post", "idxtype": "index", "idxfields": ["post_id"]},
|
||||||
|
{"name": "idx_comments_parent", "idxtype": "index", "idxfields": ["parent_id"]},
|
||||||
|
{"name": "idx_comments_user", "idxtype": "index", "idxfields": ["user_id"]}
|
||||||
|
],
|
||||||
|
"codes": []
|
||||||
|
}
|
||||||
25
models/showcase_downloads.json
Normal file
25
models/showcase_downloads.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "showcase_downloads",
|
||||||
|
"title": "KTV付费下载记录表",
|
||||||
|
"primary": ["id"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "post_id", "title": "作品ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "user_id", "title": "购买用户ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "price", "title": "购买价格", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
|
||||||
|
{"name": "payment_status", "title": "支付状态(0待支付/1已支付/2已退款)", "type": "str", "length": 1, "default": "0"},
|
||||||
|
{"name": "download_url", "title": "下载链接", "type": "str", "length": 1024},
|
||||||
|
{"name": "download_count", "title": "已下载次数", "type": "int", "default": "0"},
|
||||||
|
{"name": "created_at", "title": "购买时间", "type": "timestamp", "nullable": "no"}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{"name": "idx_dl_post", "idxtype": "index", "idxfields": ["post_id"]},
|
||||||
|
{"name": "idx_dl_user", "idxtype": "index", "idxfields": ["user_id"]},
|
||||||
|
{"name": "idx_dl_unique", "idxtype": "unique", "idxfields": ["post_id", "user_id"]}
|
||||||
|
],
|
||||||
|
"codes": []
|
||||||
|
}
|
||||||
21
models/showcase_likes.json
Normal file
21
models/showcase_likes.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "showcase_likes",
|
||||||
|
"title": "作品点赞表",
|
||||||
|
"primary": ["id"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "post_id", "title": "作品ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "user_id", "title": "点赞用户ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "created_at", "title": "点赞时间", "type": "timestamp", "nullable": "no"}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{"name": "idx_likes_post", "idxtype": "index", "idxfields": ["post_id"]},
|
||||||
|
{"name": "idx_likes_user", "idxtype": "index", "idxfields": ["user_id"]},
|
||||||
|
{"name": "idx_likes_unique", "idxtype": "unique", "idxfields": ["post_id", "user_id"]}
|
||||||
|
],
|
||||||
|
"codes": []
|
||||||
|
}
|
||||||
50
models/showcase_posts.json
Normal file
50
models/showcase_posts.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "showcase_posts",
|
||||||
|
"title": "展示作品表",
|
||||||
|
"primary": ["id"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "author_id", "title": "作者用户ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "author_name", "title": "作者名称", "type": "str", "length": 128},
|
||||||
|
{"name": "title", "title": "作品标题", "type": "str", "length": 256, "nullable": "no"},
|
||||||
|
{"name": "description", "title": "作品描述", "type": "text"},
|
||||||
|
{"name": "media_type", "title": "媒体类型(music/mtv/short_video/long_video/ktv)", "type": "str", "length": 32, "nullable": "no"},
|
||||||
|
{"name": "content_url", "title": "主内容文件URL", "type": "str", "length": 1024},
|
||||||
|
{"name": "thumbnail_url", "title": "缩略图/封面URL", "type": "str", "length": 1024},
|
||||||
|
{"name": "duration", "title": "时长(秒)", "type": "int"},
|
||||||
|
{"name": "file_size", "title": "文件大小(bytes)", "type": "long"},
|
||||||
|
{"name": "model_name", "title": "生成模型名称", "type": "str", "length": 128},
|
||||||
|
{"name": "model_input", "title": "模型输入prompt", "type": "text"},
|
||||||
|
{"name": "model_output", "title": "模型原始输出", "type": "text"},
|
||||||
|
{"name": "tags", "title": "标签(逗号分隔)", "type": "str", "length": 512},
|
||||||
|
{"name": "category", "title": "内容分类", "type": "str", "length": 64},
|
||||||
|
{"name": "like_count", "title": "点赞数", "type": "int", "default": "0"},
|
||||||
|
{"name": "comment_count", "title": "评论数", "type": "int", "default": "0"},
|
||||||
|
{"name": "view_count", "title": "浏览数", "type": "int", "default": "0"},
|
||||||
|
{"name": "download_count", "title": "下载数", "type": "int", "default": "0"},
|
||||||
|
{"name": "is_featured", "title": "是否精选(0/1)", "type": "str", "length": 1, "default": "0"},
|
||||||
|
{"name": "status", "title": "状态(0待审核/1已发布/2已下架)", "type": "str", "length": 1, "default": "1"},
|
||||||
|
{"name": "price", "title": "付费价格(0=免费)", "type": "double", "length": 10, "dec": 2, "default": "0.00"},
|
||||||
|
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||||
|
{"name": "updated_at", "title": "更新时间", "type": "timestamp"}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{"name": "idx_posts_author", "idxtype": "index", "idxfields": ["author_id"]},
|
||||||
|
{"name": "idx_posts_media_type", "idxtype": "index", "idxfields": ["media_type"]},
|
||||||
|
{"name": "idx_posts_status", "idxtype": "index", "idxfields": ["status"]},
|
||||||
|
{"name": "idx_posts_created", "idxtype": "index", "idxfields": ["created_at"]}
|
||||||
|
],
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"field": "media_type",
|
||||||
|
"table": "appcodes_kv",
|
||||||
|
"valuefield": "k",
|
||||||
|
"textfield": "v",
|
||||||
|
"cond": "parentid='showcase_media_type'"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
17
pyproject.toml
Normal file
17
pyproject.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=45", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "showcase"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Sage展示平台 - 产品展示、社交互动、KTV付费下载"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
dependencies = [
|
||||||
|
"sqlor",
|
||||||
|
"bricks_for_python",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["showcase*"]
|
||||||
76
scripts/load_path.py
Normal file
76
scripts/load_path.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Showcase module RBAC permission registration"""
|
||||||
|
import os, sys, subprocess
|
||||||
|
|
||||||
|
# Find Sage root
|
||||||
|
SAGE_ROOT = None
|
||||||
|
for candidate in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
|
||||||
|
if os.path.isdir(os.path.join(candidate, "wwwroot")):
|
||||||
|
SAGE_ROOT = candidate
|
||||||
|
break
|
||||||
|
|
||||||
|
if not SAGE_ROOT:
|
||||||
|
print("ERROR: Sage root not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
MOD = "showcase"
|
||||||
|
SET_PERM = os.path.join(SAGE_ROOT, "set_role_perm.py")
|
||||||
|
PY = os.path.join(SAGE_ROOT, "py3", "bin", "python")
|
||||||
|
|
||||||
|
def set_perm(role, path):
|
||||||
|
cmd = [PY, SET_PERM, role, path]
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if r.returncode != 0:
|
||||||
|
print(f" WARN: {path} -> {r.stderr.strip()}")
|
||||||
|
|
||||||
|
# Public resources (no auth needed)
|
||||||
|
PATHS_ANY = [
|
||||||
|
f"/{MOD}/showcase.css",
|
||||||
|
f"/{MOD}/showcase.js",
|
||||||
|
f"/{MOD}/menu.ui",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Authenticated endpoints
|
||||||
|
PATHS_LOGINED = [
|
||||||
|
f"/{MOD}",
|
||||||
|
f"/{MOD}/index.ui",
|
||||||
|
f"/{MOD}/detail.ui",
|
||||||
|
# API endpoints
|
||||||
|
f"/{MOD}/api/showcase_feed.dspy",
|
||||||
|
f"/{MOD}/api/showcase_post_detail.dspy",
|
||||||
|
f"/{MOD}/api/showcase_post_create.dspy",
|
||||||
|
f"/{MOD}/api/showcase_post_update.dspy",
|
||||||
|
f"/{MOD}/api/showcase_post_delete.dspy",
|
||||||
|
f"/{MOD}/api/showcase_comment_create.dspy",
|
||||||
|
f"/{MOD}/api/showcase_comment_update.dspy",
|
||||||
|
f"/{MOD}/api/showcase_comment_delete.dspy",
|
||||||
|
f"/{MOD}/api/showcase_comments_list.dspy",
|
||||||
|
f"/{MOD}/api/showcase_like_toggle.dspy",
|
||||||
|
f"/{MOD}/api/showcase_download_purchase.dspy",
|
||||||
|
# CRUD generated directories
|
||||||
|
f"/{MOD}/showcase_posts_list",
|
||||||
|
f"/{MOD}/showcase_posts_list/index.ui",
|
||||||
|
f"/{MOD}/showcase_posts_list/get_showcase_posts_list.dspy",
|
||||||
|
f"/{MOD}/showcase_posts_list/add_showcase_posts_list.dspy",
|
||||||
|
f"/{MOD}/showcase_posts_list/update_showcase_posts_list.dspy",
|
||||||
|
f"/{MOD}/showcase_posts_list/delete_showcase_posts_list.dspy",
|
||||||
|
f"/{MOD}/showcase_comments_list",
|
||||||
|
f"/{MOD}/showcase_comments_list/index.ui",
|
||||||
|
f"/{MOD}/showcase_comments_list/get_showcase_comments_list.dspy",
|
||||||
|
f"/{MOD}/showcase_comments_list/add_showcase_comments_list.dspy",
|
||||||
|
f"/{MOD}/showcase_comments_list/update_showcase_comments_list.dspy",
|
||||||
|
f"/{MOD}/showcase_comments_list/delete_showcase_comments_list.dspy",
|
||||||
|
f"/{MOD}/showcase_downloads_list",
|
||||||
|
f"/{MOD}/showcase_downloads_list/index.ui",
|
||||||
|
f"/{MOD}/showcase_downloads_list/get_showcase_downloads_list.dspy",
|
||||||
|
f"/{MOD}/showcase_downloads_list/add_showcase_downloads_list.dspy",
|
||||||
|
f"/{MOD}/showcase_downloads_list/update_showcase_downloads_list.dspy",
|
||||||
|
f"/{MOD}/showcase_downloads_list/delete_showcase_downloads_list.dspy",
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"Registering RBAC permissions for {MOD}...")
|
||||||
|
for p in PATHS_ANY:
|
||||||
|
set_perm("any", p)
|
||||||
|
for p in PATHS_LOGINED:
|
||||||
|
set_perm("logined", p)
|
||||||
|
print("Done.")
|
||||||
14
showcase/__init__.py
Normal file
14
showcase/__init__.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""Showcase module - 产品展示平台"""
|
||||||
|
from .init import (
|
||||||
|
load_showcase,
|
||||||
|
create_showcase_post,
|
||||||
|
update_showcase_post,
|
||||||
|
delete_showcase_post,
|
||||||
|
create_showcase_comment,
|
||||||
|
update_showcase_comment,
|
||||||
|
delete_showcase_comment,
|
||||||
|
toggle_showcase_like,
|
||||||
|
create_showcase_download,
|
||||||
|
update_showcase_download,
|
||||||
|
delete_showcase_download,
|
||||||
|
)
|
||||||
148
showcase/init.py
Normal file
148
showcase/init.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
"""Showcase module init - 产品展示平台"""
|
||||||
|
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
from appPublic.uniqueID import getID
|
||||||
|
|
||||||
|
MODULE_NAME = "showcase"
|
||||||
|
MODULE_VERSION = "1.0.0"
|
||||||
|
DBNAME = "sage"
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
|
||||||
|
|
||||||
|
async def create_showcase_post(data):
|
||||||
|
"""创建展示作品"""
|
||||||
|
data['id'] = getID()
|
||||||
|
async with db.sqlorContext(DBNAME) as sor:
|
||||||
|
await sor.C('showcase_posts', data)
|
||||||
|
return {"status": "ok", "id": data['id']}
|
||||||
|
|
||||||
|
|
||||||
|
async def update_showcase_post(data):
|
||||||
|
"""更新展示作品"""
|
||||||
|
async with db.sqlorContext(DBNAME) as sor:
|
||||||
|
await sor.U('showcase_posts', data)
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_showcase_post(data):
|
||||||
|
"""删除展示作品"""
|
||||||
|
async with db.sqlorContext(DBNAME) as sor:
|
||||||
|
await sor.D('showcase_posts', data)
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def create_showcase_comment(data):
|
||||||
|
"""创建评论(支持嵌套回复)"""
|
||||||
|
data['id'] = getID()
|
||||||
|
async with db.sqlorContext(DBNAME) as sor:
|
||||||
|
await sor.C('showcase_comments', data)
|
||||||
|
# 更新作品评论计数
|
||||||
|
post = await sor.R('showcase_posts', {"id": data['post_id']})
|
||||||
|
if post:
|
||||||
|
new_count = (post[0].get('comment_count', 0) or 0) + 1
|
||||||
|
await sor.U('showcase_posts', {"id": data['post_id'], "comment_count": new_count})
|
||||||
|
# 如果是回复,更新父评论的reply_count
|
||||||
|
if data.get('parent_id'):
|
||||||
|
parent = await sor.R('showcase_comments', {"id": data['parent_id']})
|
||||||
|
if parent:
|
||||||
|
new_reply = (parent[0].get('reply_count', 0) or 0) + 1
|
||||||
|
await sor.U('showcase_comments', {"id": data['parent_id'], "reply_count": new_reply})
|
||||||
|
return {"status": "ok", "id": data['id']}
|
||||||
|
|
||||||
|
|
||||||
|
async def update_showcase_comment(data):
|
||||||
|
"""更新评论"""
|
||||||
|
async with db.sqlorContext(DBNAME) as sor:
|
||||||
|
await sor.U('showcase_comments', data)
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_showcase_comment(data):
|
||||||
|
"""删除评论"""
|
||||||
|
async with db.sqlorContext(DBNAME) as sor:
|
||||||
|
await sor.D('showcase_comments', data)
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def toggle_showcase_like(post_id, user_id):
|
||||||
|
"""切换点赞状态"""
|
||||||
|
async with db.sqlorContext(DBNAME) as sor:
|
||||||
|
existing = await sor.sqlExe(
|
||||||
|
"SELECT id FROM showcase_likes WHERE post_id = ${post_id}$ AND user_id = ${user_id}$",
|
||||||
|
{"post_id": post_id, "user_id": user_id}
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
# 取消点赞
|
||||||
|
await sor.D('showcase_likes', {"id": existing[0]['id']})
|
||||||
|
post = await sor.R('showcase_posts', {"id": post_id})
|
||||||
|
if post:
|
||||||
|
new_count = max(0, (post[0].get('like_count', 0) or 0) - 1)
|
||||||
|
await sor.U('showcase_posts', {"id": post_id, "like_count": new_count})
|
||||||
|
return {"status": "ok", "action": "unliked", "liked": False}
|
||||||
|
else:
|
||||||
|
# 点赞
|
||||||
|
like_data = {"id": getID(), "post_id": post_id, "user_id": user_id}
|
||||||
|
await sor.C('showcase_likes', like_data)
|
||||||
|
post = await sor.R('showcase_posts', {"id": post_id})
|
||||||
|
if post:
|
||||||
|
new_count = (post[0].get('like_count', 0) or 0) + 1
|
||||||
|
await sor.U('showcase_posts', {"id": post_id, "like_count": new_count})
|
||||||
|
return {"status": "ok", "action": "liked", "liked": True}
|
||||||
|
|
||||||
|
|
||||||
|
async def create_showcase_download(data):
|
||||||
|
"""创建KTV下载记录"""
|
||||||
|
data['id'] = getID()
|
||||||
|
async with db.sqlorContext(DBNAME) as sor:
|
||||||
|
await sor.C('showcase_downloads', data)
|
||||||
|
# 更新作品下载计数
|
||||||
|
post = await sor.R('showcase_posts', {"id": data['post_id']})
|
||||||
|
if post:
|
||||||
|
new_count = (post[0].get('download_count', 0) or 0) + 1
|
||||||
|
await sor.U('showcase_posts', {"id": data['post_id'], "download_count": new_count})
|
||||||
|
return {"status": "ok", "id": data['id']}
|
||||||
|
|
||||||
|
|
||||||
|
async def update_showcase_download(data):
|
||||||
|
"""更新下载记录"""
|
||||||
|
async with db.sqlorContext(DBNAME) as sor:
|
||||||
|
await sor.U('showcase_downloads', data)
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_showcase_download(data):
|
||||||
|
"""删除下载记录"""
|
||||||
|
async with db.sqlorContext(DBNAME) as sor:
|
||||||
|
await sor.D('showcase_downloads', data)
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
def load_showcase():
|
||||||
|
"""注册所有函数到ServerEnv"""
|
||||||
|
env = ServerEnv()
|
||||||
|
env.create_showcase_post = create_showcase_post
|
||||||
|
env.update_showcase_post = update_showcase_post
|
||||||
|
env.delete_showcase_post = delete_showcase_post
|
||||||
|
# CRUD plural aliases
|
||||||
|
env.create_showcase_posts = create_showcase_post
|
||||||
|
env.update_showcase_posts = update_showcase_post
|
||||||
|
env.delete_showcase_posts = delete_showcase_post
|
||||||
|
|
||||||
|
env.create_showcase_comment = create_showcase_comment
|
||||||
|
env.update_showcase_comment = update_showcase_comment
|
||||||
|
env.delete_showcase_comment = delete_showcase_comment
|
||||||
|
env.create_showcase_comments = create_showcase_comment
|
||||||
|
env.update_showcase_comments = update_showcase_comment
|
||||||
|
env.delete_showcase_comments = delete_showcase_comment
|
||||||
|
|
||||||
|
env.toggle_showcase_like = toggle_showcase_like
|
||||||
|
|
||||||
|
env.create_showcase_download = create_showcase_download
|
||||||
|
env.update_showcase_download = update_showcase_download
|
||||||
|
env.delete_showcase_download = delete_showcase_download
|
||||||
|
env.create_showcase_downloads = create_showcase_download
|
||||||
|
env.update_showcase_downloads = update_showcase_download
|
||||||
|
env.delete_showcase_downloads = delete_showcase_download
|
||||||
|
return True
|
||||||
16
wwwroot/api/showcase_comment_create.dspy
Normal file
16
wwwroot/api/showcase_comment_create.dspy
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
try:
|
||||||
|
user_id = await get_user()
|
||||||
|
data = {
|
||||||
|
"post_id": params_kw.get('post_id', ''),
|
||||||
|
"user_id": user_id,
|
||||||
|
"user_name": params_kw.get('user_name', ''),
|
||||||
|
"parent_id": params_kw.get('parent_id', ''),
|
||||||
|
"reply_to_user_id": params_kw.get('reply_to_user_id', ''),
|
||||||
|
"reply_to_user_name": params_kw.get('reply_to_user_name', ''),
|
||||||
|
"content": params_kw.get('content', ''),
|
||||||
|
"created_at": curDateString()
|
||||||
|
}
|
||||||
|
result = await create_showcase_comment(data)
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
5
wwwroot/api/showcase_comment_delete.dspy
Normal file
5
wwwroot/api/showcase_comment_delete.dspy
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
try:
|
||||||
|
result = await delete_showcase_comment({"id": params_kw.get('id', '')})
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
10
wwwroot/api/showcase_comment_update.dspy
Normal file
10
wwwroot/api/showcase_comment_update.dspy
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
try:
|
||||||
|
data = {"id": params_kw.get('id', '')}
|
||||||
|
for f in ['content', 'status']:
|
||||||
|
v = params_kw.get(f)
|
||||||
|
if v is not None:
|
||||||
|
data[f] = v
|
||||||
|
result = await update_showcase_comment(data)
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
16
wwwroot/api/showcase_comments_list.dspy
Normal file
16
wwwroot/api/showcase_comments_list.dspy
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
try:
|
||||||
|
db = DBPools()
|
||||||
|
post_id = params_kw.get('post_id', '')
|
||||||
|
async with db.sqlorContext('sage') as sor:
|
||||||
|
# 获取评论列表(含嵌套回复)
|
||||||
|
comments = await sor.sqlExe(
|
||||||
|
"""SELECT c.*,
|
||||||
|
(SELECT COUNT(*) FROM showcase_likes WHERE post_id = c.id) as c_like
|
||||||
|
FROM showcase_comments c
|
||||||
|
WHERE c.post_id = ${post_id}$ AND c.status = '1'
|
||||||
|
ORDER BY c.created_at ASC""",
|
||||||
|
{"post_id": post_id}
|
||||||
|
)
|
||||||
|
return json.dumps({"status": "ok", "data": list(comments)}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "data": [], "message": str(e)}, ensure_ascii=False)
|
||||||
38
wwwroot/api/showcase_download_purchase.dspy
Normal file
38
wwwroot/api/showcase_download_purchase.dspy
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
try:
|
||||||
|
user_id = await get_user()
|
||||||
|
post_id = params_kw.get('post_id', '')
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext('sage') as sor:
|
||||||
|
# 检查作品是否存在且为付费KTV
|
||||||
|
posts = await sor.R('showcase_posts', {"id": post_id})
|
||||||
|
if not posts:
|
||||||
|
return json.dumps({"status": "error", "message": "作品不存在"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
post = posts[0]
|
||||||
|
price = float(post.get('price', 0))
|
||||||
|
if price <= 0:
|
||||||
|
return json.dumps({"status": "error", "message": "该作品免费,无需购买"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 检查是否已购买
|
||||||
|
existing = await sor.sqlExe(
|
||||||
|
"SELECT id FROM showcase_downloads WHERE post_id = ${post_id}$ AND user_id = ${user_id}$ AND payment_status = '1'",
|
||||||
|
{"post_id": post_id, "user_id": user_id}
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
return json.dumps({"status": "ok", "message": "已购买", "download_url": post.get('content_url', '')}, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 创建购买记录(模拟支付成功)
|
||||||
|
dl_data = {
|
||||||
|
"post_id": post_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"price": price,
|
||||||
|
"payment_status": "1",
|
||||||
|
"download_url": post.get('content_url', ''),
|
||||||
|
"download_count": 1,
|
||||||
|
"created_at": curDateString()
|
||||||
|
}
|
||||||
|
result = await create_showcase_download(dl_data)
|
||||||
|
return json.dumps({"status": "ok", "message": "购买成功", "download_url": post.get('content_url', '')}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
35
wwwroot/api/showcase_feed.dspy
Normal file
35
wwwroot/api/showcase_feed.dspy
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
try:
|
||||||
|
db = DBPools()
|
||||||
|
media_type = params_kw.get('media_type', '')
|
||||||
|
page = int(params_kw.get('page', 1))
|
||||||
|
page_size = int(params_kw.get('page_size', 20))
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
|
||||||
|
where = "WHERE status = '1'"
|
||||||
|
bindparams = {}
|
||||||
|
if media_type:
|
||||||
|
where += " AND media_type = ${media_type}$"
|
||||||
|
bindparams['media_type'] = media_type
|
||||||
|
|
||||||
|
async with db.sqlorContext('sage') as sor:
|
||||||
|
total_r = await sor.sqlExe(
|
||||||
|
f"SELECT COUNT(*) as cnt FROM showcase_posts {where}", bindparams
|
||||||
|
)
|
||||||
|
total = total_r[0]['cnt'] if total_r else 0
|
||||||
|
|
||||||
|
posts = await sor.sqlExe(
|
||||||
|
f"""SELECT id, author_id, author_name, title, description, media_type,
|
||||||
|
content_url, thumbnail_url, duration, tags, category,
|
||||||
|
like_count, comment_count, view_count, download_count,
|
||||||
|
is_featured, price, created_at
|
||||||
|
FROM showcase_posts {where}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT {page_size} OFFSET {offset}""",
|
||||||
|
bindparams
|
||||||
|
)
|
||||||
|
return json.dumps({
|
||||||
|
"status": "ok", "data": list(posts),
|
||||||
|
"total": total, "page": page, "page_size": page_size
|
||||||
|
}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "data": [], "message": str(e)}, ensure_ascii=False)
|
||||||
7
wwwroot/api/showcase_like_toggle.dspy
Normal file
7
wwwroot/api/showcase_like_toggle.dspy
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
try:
|
||||||
|
user_id = await get_user()
|
||||||
|
post_id = params_kw.get('post_id', '')
|
||||||
|
result = await toggle_showcase_like(post_id, user_id)
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
24
wwwroot/api/showcase_post_create.dspy
Normal file
24
wwwroot/api/showcase_post_create.dspy
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
try:
|
||||||
|
user_id = await get_user()
|
||||||
|
data = {
|
||||||
|
"author_id": user_id,
|
||||||
|
"author_name": params_kw.get('author_name', ''),
|
||||||
|
"title": params_kw.get('title', ''),
|
||||||
|
"description": params_kw.get('description', ''),
|
||||||
|
"media_type": params_kw.get('media_type', 'music'),
|
||||||
|
"content_url": params_kw.get('content_url', ''),
|
||||||
|
"thumbnail_url": params_kw.get('thumbnail_url', ''),
|
||||||
|
"duration": params_kw.get('duration', 0),
|
||||||
|
"file_size": params_kw.get('file_size', 0),
|
||||||
|
"model_name": params_kw.get('model_name', ''),
|
||||||
|
"model_input": params_kw.get('model_input', ''),
|
||||||
|
"model_output": params_kw.get('model_output', ''),
|
||||||
|
"tags": params_kw.get('tags', ''),
|
||||||
|
"category": params_kw.get('category', ''),
|
||||||
|
"price": float(params_kw.get('price', 0)),
|
||||||
|
"created_at": curDateString()
|
||||||
|
}
|
||||||
|
result = await create_showcase_post(data)
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
5
wwwroot/api/showcase_post_delete.dspy
Normal file
5
wwwroot/api/showcase_post_delete.dspy
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
try:
|
||||||
|
result = await delete_showcase_post({"id": params_kw.get('id', '')})
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
41
wwwroot/api/showcase_post_detail.dspy
Normal file
41
wwwroot/api/showcase_post_detail.dspy
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
try:
|
||||||
|
db = DBPools()
|
||||||
|
post_id = params_kw.get('post_id', '')
|
||||||
|
user_id = await get_user()
|
||||||
|
|
||||||
|
async with db.sqlorContext('sage') as sor:
|
||||||
|
# 获取作品详情
|
||||||
|
posts = await sor.R('showcase_posts', {"id": post_id})
|
||||||
|
if not posts:
|
||||||
|
return json.dumps({"status": "error", "message": "作品不存在"}, ensure_ascii=False)
|
||||||
|
post = dict(posts[0])
|
||||||
|
|
||||||
|
# 增加浏览计数
|
||||||
|
new_view = (post.get('view_count', 0) or 0) + 1
|
||||||
|
await sor.U('showcase_posts', {"id": post_id, "view_count": new_view})
|
||||||
|
post['view_count'] = new_view
|
||||||
|
|
||||||
|
# 检查当前用户是否已点赞
|
||||||
|
liked = await sor.sqlExe(
|
||||||
|
"SELECT id FROM showcase_likes WHERE post_id = ${post_id}$ AND user_id = ${user_id}$",
|
||||||
|
{"post_id": post_id, "user_id": user_id}
|
||||||
|
)
|
||||||
|
post['is_liked'] = len(liked) > 0
|
||||||
|
|
||||||
|
# 检查是否已购买(付费作品)
|
||||||
|
if float(post.get('price', 0)) > 0:
|
||||||
|
bought = await sor.sqlExe(
|
||||||
|
"SELECT id FROM showcase_downloads WHERE post_id = ${post_id}$ AND user_id = ${user_id}$ AND payment_status = '1'",
|
||||||
|
{"post_id": post_id, "user_id": user_id}
|
||||||
|
)
|
||||||
|
post['is_purchased'] = len(bought) > 0
|
||||||
|
else:
|
||||||
|
post['is_purchased'] = True
|
||||||
|
|
||||||
|
# 移除敏感字段
|
||||||
|
post.pop('model_input', None)
|
||||||
|
post.pop('model_output', None)
|
||||||
|
|
||||||
|
return json.dumps({"status": "ok", "data": post}, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
14
wwwroot/api/showcase_post_update.dspy
Normal file
14
wwwroot/api/showcase_post_update.dspy
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
try:
|
||||||
|
data = {"id": params_kw.get('id', '')}
|
||||||
|
for f in ['title', 'description', 'content_url', 'thumbnail_url', 'tags',
|
||||||
|
'category', 'media_type', 'status', 'is_featured', 'price']:
|
||||||
|
v = params_kw.get(f)
|
||||||
|
if v is not None:
|
||||||
|
data[f] = v
|
||||||
|
if 'price' in data:
|
||||||
|
data['price'] = float(data['price'])
|
||||||
|
data['updated_at'] = curDateString()
|
||||||
|
result = await update_showcase_post(data)
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)
|
||||||
24
wwwroot/detail.ui
Normal file
24
wwwroot/detail.ui
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {"width": "100%", "height": "100%", "padding": "0px"},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {"width": "100%", "padding": "16px 24px", "bgcolor": "var(--bricks-card-bg, #fff)", "borderBottom": "1px solid var(--bricks-border, #eee)"},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {"label": "← 返回", "cfontsize": 0.85},
|
||||||
|
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||||||
|
"script": "history.back();"}]
|
||||||
|
},
|
||||||
|
{"widgettype": "Title", "options": {"text": "作品详情", "fontSize": "20px", "fontWeight": "bold", "flex": "1", "marginLeft": "16px"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"id": "showcase_detail_content",
|
||||||
|
"options": {"width": "100%", "flex": "1", "padding": "24px", "overflow": "auto"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
61
wwwroot/index.ui
Normal file
61
wwwroot/index.ui
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {"width": "100%", "height": "100%", "padding": "0px"},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {"width": "100%", "padding": "16px 24px", "bgcolor": "var(--bricks-card-bg, #fff)", "borderBottom": "1px solid var(--bricks-border, #eee)"},
|
||||||
|
"subwidgets": [
|
||||||
|
{"widgettype": "Title", "options": {"text": "展示平台", "fontSize": "22px", "fontWeight": "bold", "flex": "1"}},
|
||||||
|
{
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {"gap": "8px"},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {"label": "全部", "id": "btn_all", "cfontsize": 0.85},
|
||||||
|
"binds": [{"wid": "self", "event": "click", "actiontype": "registerfunction", "target": "filterByType", "options": {"type": ""}}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {"label": "🎵 音乐", "id": "btn_music", "cfontsize": 0.85},
|
||||||
|
"binds": [{"wid": "self", "event": "click", "actiontype": "registerfunction", "target": "filterByType", "options": {"type": "music"}}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {"label": "🎬 MTV", "id": "btn_mtv", "cfontsize": 0.85},
|
||||||
|
"binds": [{"wid": "self", "event": "click", "actiontype": "registerfunction", "target": "filterByType", "options": {"type": "mtv"}}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {"label": "📱 短视频", "id": "btn_short", "cfontsize": 0.85},
|
||||||
|
"binds": [{"wid": "self", "event": "click", "actiontype": "registerfunction", "target": "filterByType", "options": {"type": "short_video"}}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {"label": "🎞️ 长视频", "id": "btn_long", "cfontsize": 0.85},
|
||||||
|
"binds": [{"wid": "self", "event": "click", "actiontype": "registerfunction", "target": "filterByType", "options": {"type": "long_video"}}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {"label": "🎤 KTV", "id": "btn_ktv", "cfontsize": 0.85},
|
||||||
|
"binds": [{"wid": "self", "event": "click", "actiontype": "registerfunction", "target": "filterByType", "options": {"type": "ktv"}}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"id": "showcase_feed_container",
|
||||||
|
"options": {"width": "100%", "flex": "1", "padding": "16px 24px", "overflow": "auto"},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "ResponsableBox",
|
||||||
|
"id": "showcase_feed_grid",
|
||||||
|
"options": {"gap": "16px", "minWidth": "280px"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
72
wwwroot/showcase.css
Normal file
72
wwwroot/showcase.css
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/* Showcase platform styles */
|
||||||
|
.showcase-card {
|
||||||
|
background: var(--bricks-card-bg, #fff);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
border: 1px solid var(--bricks-border, #eee);
|
||||||
|
}
|
||||||
|
.showcase-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
|
||||||
|
}
|
||||||
|
.showcase-card .thumb {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #f0f0f0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
.showcase-card .info {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.showcase-card .title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.showcase-card .meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.showcase-card .badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.badge-music { background: #e8f5e9; color: #2e7d32; }
|
||||||
|
.badge-mtv { background: #e3f2fd; color: #1565c0; }
|
||||||
|
.badge-short_video { background: #fff3e0; color: #e65100; }
|
||||||
|
.badge-long_video { background: #fce4ec; color: #c62828; }
|
||||||
|
.badge-ktv { background: #f3e5f5; color: #6a1b9a; }
|
||||||
|
|
||||||
|
.comment-item {
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--bricks-border, #eee);
|
||||||
|
}
|
||||||
|
.comment-item .comment-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.comment-item .comment-user { font-weight: 600; font-size: 13px; }
|
||||||
|
.comment-item .comment-time { font-size: 11px; color: #999; }
|
||||||
|
.comment-item .comment-body { font-size: 14px; line-height: 1.5; }
|
||||||
|
.comment-reply {
|
||||||
|
margin-left: 32px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 2px solid var(--bricks-border, #eee);
|
||||||
|
}
|
||||||
75
wwwroot/showcase.js
Normal file
75
wwwroot/showcase.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// Showcase platform JavaScript
|
||||||
|
|
||||||
|
const MEDIA_LABELS = {
|
||||||
|
music: '🎵 音乐', mtv: '🎬 MTV', short_video: '📱 短视频',
|
||||||
|
long_video: '🎞️ 长视频', ktv: '🎤 KTV'
|
||||||
|
};
|
||||||
|
const MEDIA_ICONS = {
|
||||||
|
music: '🎵', mtv: '🎬', short_video: '📱', long_video: '🎞️', ktv: '🎤'
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentFilter = '';
|
||||||
|
|
||||||
|
async function loadFeed(mediaType) {
|
||||||
|
currentFilter = mediaType || '';
|
||||||
|
const container = document.getElementById('showcase_feed_grid');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const url = `${MODULE_PREFIX}/api/showcase_feed.dspy?media_type=${currentFilter}&page=1&page_size=30`;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url);
|
||||||
|
const result = await resp.json();
|
||||||
|
if (result.status !== 'ok' || !result.data) {
|
||||||
|
container.innerHTML = '<div style="padding:40px;text-align:center;color:#999">暂无作品</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderFeedCards(container, result.data);
|
||||||
|
} catch (e) {
|
||||||
|
container.innerHTML = '<div style="padding:40px;text-align:center;color:#c00">加载失败</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFeedCards(container, posts) {
|
||||||
|
if (!posts.length) {
|
||||||
|
container.innerHTML = '<div style="padding:40px;text-align:center;color:#999">暂无作品</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = posts.map(p => `
|
||||||
|
<div class="showcase-card" onclick="openDetail('${p.id}')">
|
||||||
|
<div class="thumb">${p.thumbnail_url ? `<img src="${p.thumbnail_url}" style="width:100%;height:100%;object-fit:cover">` : MEDIA_ICONS[p.media_type] || '📄'}</div>
|
||||||
|
<div class="info">
|
||||||
|
<span class="badge badge-${p.media_type}">${MEDIA_LABELS[p.media_type] || p.media_type}</span>
|
||||||
|
<div class="title">${escHtml(p.title)}</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span>❤ ${p.like_count || 0}</span>
|
||||||
|
<span>💬 ${p.comment_count || 0}</span>
|
||||||
|
<span>👁 ${p.view_count || 0}</span>
|
||||||
|
${parseFloat(p.price) > 0 ? `<span>💰 ¥${p.price}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(postId) {
|
||||||
|
window.location.href = `${MODULE_PREFIX}/detail.ui?post_id=${postId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register filterByType function
|
||||||
|
if (typeof registerFunction !== 'undefined') {
|
||||||
|
registerFunction('filterByType', function(opts) {
|
||||||
|
loadFeed(opts.type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-load on page ready
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
if (document.getElementById('showcase_feed_grid')) {
|
||||||
|
loadFeed('');
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user