diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a332f66 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..cce9005 --- /dev/null +++ b/init/data.json @@ -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"} + ] + } + ] +} diff --git a/json/showcase_comments_list.json b/json/showcase_comments_list.json new file mode 100644 index 0000000..9314a0f --- /dev/null +++ b/json/showcase_comments_list.json @@ -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')}}" + } + } +} diff --git a/json/showcase_downloads_list.json b/json/showcase_downloads_list.json new file mode 100644 index 0000000..31be9c1 --- /dev/null +++ b/json/showcase_downloads_list.json @@ -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')}}" + } + } +} diff --git a/json/showcase_posts_list.json b/json/showcase_posts_list.json new file mode 100644 index 0000000..9167d1e --- /dev/null +++ b/json/showcase_posts_list.json @@ -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"} + ] + } + } +} diff --git a/models/showcase_comments.json b/models/showcase_comments.json new file mode 100644 index 0000000..865de94 --- /dev/null +++ b/models/showcase_comments.json @@ -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": [] +} diff --git a/models/showcase_downloads.json b/models/showcase_downloads.json new file mode 100644 index 0000000..11df280 --- /dev/null +++ b/models/showcase_downloads.json @@ -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": [] +} diff --git a/models/showcase_likes.json b/models/showcase_likes.json new file mode 100644 index 0000000..e8ee153 --- /dev/null +++ b/models/showcase_likes.json @@ -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": [] +} diff --git a/models/showcase_posts.json b/models/showcase_posts.json new file mode 100644 index 0000000..5f7ba39 --- /dev/null +++ b/models/showcase_posts.json @@ -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'" + } + ] +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fcee9b0 --- /dev/null +++ b/pyproject.toml @@ -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*"] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..2d59f88 --- /dev/null +++ b/scripts/load_path.py @@ -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.") diff --git a/showcase/__init__.py b/showcase/__init__.py new file mode 100644 index 0000000..0b5ee7e --- /dev/null +++ b/showcase/__init__.py @@ -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, +) diff --git a/showcase/init.py b/showcase/init.py new file mode 100644 index 0000000..5e50a54 --- /dev/null +++ b/showcase/init.py @@ -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 diff --git a/wwwroot/api/showcase_comment_create.dspy b/wwwroot/api/showcase_comment_create.dspy new file mode 100644 index 0000000..795a984 --- /dev/null +++ b/wwwroot/api/showcase_comment_create.dspy @@ -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) diff --git a/wwwroot/api/showcase_comment_delete.dspy b/wwwroot/api/showcase_comment_delete.dspy new file mode 100644 index 0000000..bd3cc61 --- /dev/null +++ b/wwwroot/api/showcase_comment_delete.dspy @@ -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) diff --git a/wwwroot/api/showcase_comment_update.dspy b/wwwroot/api/showcase_comment_update.dspy new file mode 100644 index 0000000..baec605 --- /dev/null +++ b/wwwroot/api/showcase_comment_update.dspy @@ -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) diff --git a/wwwroot/api/showcase_comments_list.dspy b/wwwroot/api/showcase_comments_list.dspy new file mode 100644 index 0000000..8b6878b --- /dev/null +++ b/wwwroot/api/showcase_comments_list.dspy @@ -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) diff --git a/wwwroot/api/showcase_download_purchase.dspy b/wwwroot/api/showcase_download_purchase.dspy new file mode 100644 index 0000000..a9fec2c --- /dev/null +++ b/wwwroot/api/showcase_download_purchase.dspy @@ -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) diff --git a/wwwroot/api/showcase_feed.dspy b/wwwroot/api/showcase_feed.dspy new file mode 100644 index 0000000..07cfaec --- /dev/null +++ b/wwwroot/api/showcase_feed.dspy @@ -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) diff --git a/wwwroot/api/showcase_like_toggle.dspy b/wwwroot/api/showcase_like_toggle.dspy new file mode 100644 index 0000000..c0dca91 --- /dev/null +++ b/wwwroot/api/showcase_like_toggle.dspy @@ -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) diff --git a/wwwroot/api/showcase_post_create.dspy b/wwwroot/api/showcase_post_create.dspy new file mode 100644 index 0000000..d0b01c0 --- /dev/null +++ b/wwwroot/api/showcase_post_create.dspy @@ -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) diff --git a/wwwroot/api/showcase_post_delete.dspy b/wwwroot/api/showcase_post_delete.dspy new file mode 100644 index 0000000..ddbeac8 --- /dev/null +++ b/wwwroot/api/showcase_post_delete.dspy @@ -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) diff --git a/wwwroot/api/showcase_post_detail.dspy b/wwwroot/api/showcase_post_detail.dspy new file mode 100644 index 0000000..9ec6619 --- /dev/null +++ b/wwwroot/api/showcase_post_detail.dspy @@ -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) diff --git a/wwwroot/api/showcase_post_update.dspy b/wwwroot/api/showcase_post_update.dspy new file mode 100644 index 0000000..662582b --- /dev/null +++ b/wwwroot/api/showcase_post_update.dspy @@ -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) diff --git a/wwwroot/detail.ui b/wwwroot/detail.ui new file mode 100644 index 0000000..aa12893 --- /dev/null +++ b/wwwroot/detail.ui @@ -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"} + } + ] +} diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..1af8760 --- /dev/null +++ b/wwwroot/index.ui @@ -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"} + } + ] + } + ] +} diff --git a/wwwroot/showcase.css b/wwwroot/showcase.css new file mode 100644 index 0000000..fc051fd --- /dev/null +++ b/wwwroot/showcase.css @@ -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); +} diff --git a/wwwroot/showcase.js b/wwwroot/showcase.js new file mode 100644 index 0000000..800c7ee --- /dev/null +++ b/wwwroot/showcase.js @@ -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 = '
暂无作品
'; + return; + } + renderFeedCards(container, result.data); + } catch (e) { + container.innerHTML = '
加载失败
'; + } +} + +function renderFeedCards(container, posts) { + if (!posts.length) { + container.innerHTML = '
暂无作品
'; + return; + } + container.innerHTML = posts.map(p => ` +
+
${p.thumbnail_url ? `` : MEDIA_ICONS[p.media_type] || '📄'}
+
+ ${MEDIA_LABELS[p.media_type] || p.media_type} +
${escHtml(p.title)}
+
+ ❤ ${p.like_count || 0} + 💬 ${p.comment_count || 0} + 👁 ${p.view_count || 0} + ${parseFloat(p.price) > 0 ? `💰 ¥${p.price}` : ''} +
+
+
+ `).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,'>'); +} + +// 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(''); + } +});