diff --git a/scripts/load_path.py b/scripts/load_path.py index c286587..469f9d3 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -41,6 +41,11 @@ MOD = "product_management" # any — 无需登录(仅静态资源和菜单) PATHS_ANY = [ f"/{MOD}/menu.ui", + f"/{MOD}/storefront", + f"/{MOD}/storefront/index.ui", + f"/{MOD}/storefront/api/get_categories.dspy", + f"/{MOD}/storefront/api/get_products.dspy", + f"/{MOD}/storefront/api/get_product_detail.dspy", ] # logined — 需要认证的页面和 API @@ -147,6 +152,9 @@ PATHS_LOGINED = [ f"/{MOD}/api/usage_logs.dspy", f"/{MOD}/api/usage_stats.dspy", + # Storefront purchase (requires login) + f"/{MOD}/storefront/api/purchase.dspy", + # CRUD auto-generated .dspy (new tables) f"/{MOD}/product_resource_list/get_product_resource.dspy", f"/{MOD}/product_resource_list/add_product_resource.dspy", diff --git a/wwwroot/storefront/api/get_categories.dspy b/wwwroot/storefront/api/get_categories.dspy new file mode 100644 index 0000000..bba7185 --- /dev/null +++ b/wwwroot/storefront/api/get_categories.dspy @@ -0,0 +1,16 @@ +dbname = get_module_dbname('product_management') +async with DBPools().sqlorContext(dbname) as sor: + sql = """select id, name, parent_id, icon from product_category + where status = '1' and has_product = '1'""" + + ns = params_kw.copy() + id = ns.get('id') + if id and id != '0' and id != 'null' and id != 'undefined': + sql += " and parent_id = ${id}$" + else: + sql += " and (parent_id is null or parent_id = '' or parent_id = '0')" + sql += " order by sort_order, name " + + recs = await sor.sqlExe(sql, ns) + +return json.dumps(recs or [], ensure_ascii=False) diff --git a/wwwroot/storefront/api/get_product_detail.dspy b/wwwroot/storefront/api/get_product_detail.dspy new file mode 100644 index 0000000..75258a5 --- /dev/null +++ b/wwwroot/storefront/api/get_product_detail.dspy @@ -0,0 +1,153 @@ +product_id = params_kw.get('id', '') +userid = await get_user() +dbname = get_module_dbname('product_management') + +if not product_id: + result = {'widgettype': 'Text', 'options': {'text': '请选择一个产品'}} +else: + async with DBPools().sqlorContext(dbname) as sor: + sql = """SELECT p.*, pc.name as category_name, pc.description as category_description + FROM product p + LEFT JOIN product_category pc ON p.category_id = pc.id + WHERE p.id = ${id}$ AND p.status = '1'""" + recs = await sor.sqlExe(sql, {'id': product_id}) + + if not recs: + result = {'widgettype': 'Text', 'options': {'text': '产品不存在或已下架'}} + else: + p = recs[0] + price_text = f"¥{float(p.price or 0):.2f}" if p.price else '按量计费' + detail = p.detail_intro or p.brief_intro or '暂无详细介绍' + + # Parse extra_json for additional info + extra_items = [] + extra_str = p.extra_json or '' + if extra_str: + try: + extra = json.loads(extra_str) + if isinstance(extra, dict): + for k, v in extra.items(): + extra_items.append({'key': k, 'value': str(v)}) + except: + pass + + info_rows = [ + {'label': '产品编码', 'value': p.product_code or ''}, + {'label': '产品分类', 'value': p.category_name or ''}, + {'label': '产品类型', 'value': p.product_type or ''}, + {'label': '价格', 'value': price_text}, + ] + for ei in extra_items: + info_rows.append({'label': ei['key'], 'value': ei['value']}) + + info_widgets = [] + for row in info_rows: + info_widgets.append({ + 'widgettype': 'HBox', + 'options': {'padding': '6px 0', 'gap': '8px'}, + 'subwidgets': [ + { + 'widgettype': 'Text', + 'options': { + 'text': f"{row['label']}:", + 'fontWeight': '600', + 'width': '80px', + 'fontSize': '13px' + } + }, + { + 'widgettype': 'Text', + 'options': {'text': row['value'], 'fontSize': '13px'} + } + ] + }) + + # Build action buttons + buttons = [] + if userid: + buttons.append({ + 'widgettype': 'Button', + 'options': { + 'label': '立即购买', + 'bgcolor': '#3498db', + 'color': '#fff', + 'padding': '10px 32px', + 'borderRadius': '6px', + 'fontWeight': '600' + }, + 'binds': [ + { + 'wid': 'self', + 'event': 'click', + 'actiontype': 'urlwidget', + 'target': 'PopupWindow', + 'popup_options': {'title': '购买结果', 'cwidth': 20, 'cheight': 8}, + 'options': { + 'method': 'POST', + 'url': '{{entire_url("./api/purchase.dspy")}}', + 'params': {'product_id': p.id} + } + } + ] + }) + else: + buttons.append({ + 'widgettype': 'Text', + 'options': { + 'text': '登录后即可购买', + 'fontSize': '13px', + 'color': '#999' + } + }) + + subwidgets = [ + { + 'widgettype': 'Title3', + 'options': {'text': p.product_name, 'fontWeight': '700'} + }, + { + 'widgettype': 'Text', + 'options': { + 'text': price_text, + 'fontSize': '20px', + 'fontWeight': '700', + 'color': '#e74c3c', + 'marginTop': '8px', + 'marginBottom': '16px' + } + }, + { + 'widgettype': 'VBox', + 'options': {'gap': '0', 'marginBottom': '16px'}, + 'subwidgets': info_widgets + }, + { + 'widgettype': 'Text', + 'options': { + 'text': detail, + 'fontSize': '14px', + 'lineHeight': '1.6', + 'whiteSpace': 'pre-wrap', + 'marginTop': '12px', + 'marginBottom': '20px' + } + }, + { + 'widgettype': 'HBox', + 'options': {'gap': '12px', 'marginTop': '12px'}, + 'subwidgets': buttons + } + ] + + result = { + 'widgettype': 'VBox', + 'options': { + 'width': '100%', + 'padding': '20px', + 'borderRadius': '8px', + 'border': '1px solid #e0e0e0' + }, + 'subwidgets': subwidgets + } + +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/storefront/api/get_products.dspy b/wwwroot/storefront/api/get_products.dspy new file mode 100644 index 0000000..ff6320f --- /dev/null +++ b/wwwroot/storefront/api/get_products.dspy @@ -0,0 +1,123 @@ +category_id = params_kw.get('category_id', '') +org_id = (await get_userorgid()) or '0' +dbname = get_module_dbname('product_management') + +if not category_id: + result = {'widgettype': 'Text', 'options': {'text': '请从左侧选择一个产品分类'}} +else: + async with DBPools().sqlorContext(dbname) as sor: + # Get all sub-category IDs recursively + all_ids = [category_id] + queue = [category_id] + while queue: + pid = queue.pop(0) + children = await sor.sqlExe( + "SELECT id FROM product_category WHERE parent_id = ${pid}$ AND status = '1'", + {'pid': pid} + ) + for c in (children or []): + all_ids.append(c.id) + queue.append(c.id) + + # Build IN clause + param_keys = [] + params = {'org_id': org_id} + for i, cid in enumerate(all_ids): + key = f'cid_{i}' + param_keys.append(f'${key}$') + params[key] = cid + + placeholders = ','.join(param_keys) + sql = f"""SELECT p.id, p.product_name, p.product_code, p.brief_intro, + p.product_type, p.price, p.currency, p.icon, + pc.name as category_name + FROM product p + LEFT JOIN product_category pc ON p.category_id = pc.id + WHERE p.category_id IN ({placeholders}) + AND p.org_id = ${org_id}$ + AND p.status = '1' + ORDER BY p.sort_order ASC, p.product_name ASC""" + params['org_id'] = org_id + products = await sor.sqlExe(sql, params) + + if not products: + result = {'widgettype': 'Text', 'options': {'text': '该分类下暂无产品'}} + else: + cards = [] + for p in products: + price_text = f"¥{float(p.price or 0):.2f}" if p.price else '面议' + card = { + 'widgettype': 'HBox', + 'options': { + 'padding': '12px', + 'borderRadius': '8px', + 'border': '1px solid #e0e0e0', + 'marginBottom': '8px', + 'cursor': 'pointer', + 'alignItems': 'center', + 'gap': '16px' + }, + 'binds': [ + { + 'wid': 'self', + 'event': 'click', + 'actiontype': 'urlwidget', + 'target': 'product_detail', + 'params_mapping': { + 'mapping': {'id': 'product_id'}, + 'need_other': False + }, + 'options': { + 'method': 'POST', + 'url': '{{entire_url("./api/get_product_detail.dspy")}}', + 'params': {'id': p.id} + } + } + ], + 'subwidgets': [ + { + 'widgettype': 'VBox', + 'options': {'css': 'filler', 'gap': '4px'}, + 'subwidgets': [ + { + 'widgettype': 'Title5', + 'options': {'text': p.product_name, 'fontWeight': '600'} + }, + { + 'widgettype': 'Text', + 'options': { + 'text': p.brief_intro or '暂无简介', + 'fontSize': '13px', + 'color': '#666' + } + }, + { + 'widgettype': 'Text', + 'options': { + 'text': f"{p.category_name or ''} | {p.product_code or ''}", + 'fontSize': '12px', + 'color': '#999' + } + } + ] + }, + { + 'widgettype': 'Text', + 'options': { + 'text': price_text, + 'fontSize': '16px', + 'fontWeight': '600', + 'color': '#e74c3c' + } + } + ] + } + cards.append(card) + + result = { + 'widgettype': 'VBox', + 'options': {'width': '100%', 'gap': '0'}, + 'subwidgets': cards + } + +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/storefront/api/purchase.dspy b/wwwroot/storefront/api/purchase.dspy new file mode 100644 index 0000000..69b7d33 --- /dev/null +++ b/wwwroot/storefront/api/purchase.dspy @@ -0,0 +1,56 @@ +result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}} + +try: + product_id = params_kw.get('product_id', '') + if not product_id: + raise ValueError('缺少产品ID') + + userid = await get_user() + if not userid: + raise ValueError('请先登录') + + userorgid = await get_userorgid() + dbname = get_module_dbname('product_management') + + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.sqlExe( + "SELECT * FROM product WHERE id = ${id}$ AND status = '1'", + {'id': product_id} + ) + if not recs: + raise ValueError('产品不存在或已下架') + + p = recs[0] + order_id = getID() + now = timestampstr() + order = { + 'id': order_id, + 'product_id': p.id, + 'product_code': p.product_code or '', + 'product_name': p.product_name or '', + 'buyer_id': userid, + 'buyer_org_id': userorgid or '0', + 'quantity': 1, + 'unit_price': float(p.price or 0), + 'total_price': float(p.price or 0), + 'currency': p.currency or 'CNY', + 'status': 'pending', + 'purchase_data': '{}', + 'created_at': now, + 'updated_at': now + } + await sor.C('purchase_orders', order) + + result = { + 'widgettype': 'Message', + 'options': { + 'title': '购买成功', + 'message': f"已提交购买订单: {p.product_name},订单号: {order_id}", + 'type': 'success', + 'timeout': 5 + } + } +except Exception as e: + result['options'] = {'title': '购买失败', 'message': str(e), 'type': 'error', 'timeout': 5} + +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/storefront/index.ui b/wwwroot/storefront/index.ui new file mode 100644 index 0000000..ffacef7 --- /dev/null +++ b/wwwroot/storefront/index.ui @@ -0,0 +1,123 @@ +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "padding": "0" + }, + "subwidgets": [ + { + "widgettype": "HBox", + "options": { + "width": "100%", + "alignItems": "center", + "padding": "16px 24px", + "marginBottom": "0" + }, + "subwidgets": [ + { + "widgettype": "Title2", + "options": { + "text": "产品商城", + "fontWeight": "700" + } + }, + { + "widgettype": "Filler" + }, + { + "widgettype": "Text", + "options": { + "text": "浏览产品,点击了解详情", + "fontSize": "14px" + } + } + ] + }, + { + "widgettype": "HBox", + "options": { + "width": "100%", + "css": "filler", + "gap": "0" + }, + "subwidgets": [ + { + "widgettype": "VBox", + "id": "category_sidebar", + "options": { + "width": "280px", + "minWidth": "48px", + "borderRight": "1px solid #e0e0e0", + "overflowY": "auto" + }, + "subwidgets": [ + { + "widgettype": "Tree", + "id": "category_tree", + "options": { + "title": "产品分类", + "idField": "id", + "textField": "name", + "parentField": "parent_id", + "dataurl": "{{entire_url('./api/get_categories.dspy')}}" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "product_list", + "params_mapping": { + "mapping": { + "id": "category_id" + }, + "need_other": false + }, + "options": { + "method": "POST", + "url": "{{entire_url('./api/get_products.dspy')}}" + } + } + ] + } + ] + }, + { + "widgettype": "VBox", + "options": { + "css": "filler", + "padding": "16px 24px", + "overflowY": "auto" + }, + "subwidgets": [ + { + "widgettype": "Title4", + "options": { + "text": "产品列表", + "fontWeight": "600", + "marginBottom": "12px" + } + }, + { + "widgettype": "VBox", + "id": "product_list", + "options": { + "width": "100%", + "css": "filler" + } + }, + { + "widgettype": "VBox", + "id": "product_detail", + "options": { + "width": "100%", + "marginTop": "24px" + } + } + ] + } + ] + } + ] +}