feat: add Excel export for leads and content
- export_leads.dspy: generate xlsx with openpyxl, base64 return - export_content.dspy: generate xlsx for content list - admin.ui: add export section with download buttons - load_path.py: register export endpoints for RBAC - pyproject.toml: add openpyxl dependency
This commit is contained in:
parent
4495e9589b
commit
d98ef560c0
@ -11,6 +11,7 @@ dependencies = [
|
||||
"sqlor",
|
||||
"bricks_for_python",
|
||||
"requests",
|
||||
"openpyxl",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@ -92,6 +92,9 @@ PATHS = [
|
||||
# DingTalk
|
||||
f"/{MOD}/api/submit_approval.dspy",
|
||||
f"/{MOD}/api/dingtalk_callback.dspy",
|
||||
# Export
|
||||
f"/{MOD}/api/export_leads.dspy",
|
||||
f"/{MOD}/api/export_content.dspy",
|
||||
]
|
||||
|
||||
def run(role, paths):
|
||||
|
||||
@ -309,6 +309,66 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"gap": "12px",
|
||||
"marginTop": "16px",
|
||||
"padding": "12px",
|
||||
"css": "card",
|
||||
"alignItems": "center"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "📥 数据导出",
|
||||
"fontSize": "16px",
|
||||
"fontWeight": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Filler"
|
||||
},
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"id": "btn_export_leads",
|
||||
"options": {
|
||||
"label": "导出商机线索",
|
||||
"css": "primary"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "script",
|
||||
"options": {
|
||||
"code": "var btn=this;btn.setLabel('生成中...');fetch(bricks.absurl('/cms/api/export_leads.dspy',btn)).then(function(r){return r.json()}).then(function(d){if(d.status==='ok'){var bin=atob(d.data);var bytes=new Uint8Array(bin.length);for(var i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);var blob=new Blob([bytes],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});var url=URL.createObjectURL(blob);var a=document.createElement('a');a.href=url;a.download=d.filename;a.click();URL.revokeObjectURL(url);btn.setLabel('导出商机线索')}else{btn.setLabel('导出失败');setTimeout(function(){btn.setLabel('导出商机线索')},2000)}}).catch(function(){btn.setLabel('网络错误');setTimeout(function(){btn.setLabel('导出商机线索')},2000)})"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"id": "btn_export_content",
|
||||
"options": {
|
||||
"label": "导出内容列表",
|
||||
"css": "primary"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "script",
|
||||
"options": {
|
||||
"code": "var btn=this;btn.setLabel('生成中...');fetch(bricks.absurl('/cms/api/export_content.dspy',btn)).then(function(r){return r.json()}).then(function(d){if(d.status==='ok'){var bin=atob(d.data);var bytes=new Uint8Array(bin.length);for(var i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);var blob=new Blob([bytes],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});var url=URL.createObjectURL(blob);var a=document.createElement('a');a.href=url;a.download=d.filename;a.click();URL.revokeObjectURL(url);btn.setLabel('导出内容列表')}else{btn.setLabel('导出失败');setTimeout(function(){btn.setLabel('导出内容列表')},2000)}}).catch(function(){btn.setLabel('网络错误');setTimeout(function(){btn.setLabel('导出内容列表')},2000)})"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "sage_main_content",
|
||||
|
||||
63
wwwroot/api/export_content.dspy
Normal file
63
wwwroot/api/export_content.dspy
Normal file
@ -0,0 +1,63 @@
|
||||
|
||||
import io
|
||||
import base64
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('cms')
|
||||
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
rows = await sor.R('cms_content', {'sort': 'sort_order asc, created_at desc'})
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = '内容列表'
|
||||
|
||||
# Headers
|
||||
headers = ['标题', '类型', '摘要', '状态', '标签', '排序', '创建时间', '发布时间']
|
||||
header_font = Font(bold=True, color='FFFFFF')
|
||||
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
|
||||
thin_border = Border(
|
||||
left=Side(style='thin'), right=Side(style='thin'),
|
||||
top=Side(style='thin'), bottom=Side(style='thin')
|
||||
)
|
||||
|
||||
for col, h in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=h)
|
||||
cell.font = header_font
|
||||
cell.fill = header_fill
|
||||
cell.alignment = Alignment(horizontal='center')
|
||||
cell.border = thin_border
|
||||
|
||||
# Mappings
|
||||
type_map = {'banner': 'Banner', 'product': '产品', 'case': '案例', 'news': '新闻'}
|
||||
status_map = {'draft': '草稿', 'pending': '待审批', 'approved': '已审批', 'published': '已发布', 'archived': '已归档'}
|
||||
|
||||
# Data rows
|
||||
for i, row in enumerate(rows, 2):
|
||||
ws.cell(row=i, column=1, value=row.get('title', ''))
|
||||
ws.cell(row=i, column=2, value=type_map.get(row.get('content_type', ''), row.get('content_type', '')))
|
||||
ws.cell(row=i, column=3, value=row.get('summary_text', '')[:200] if row.get('summary_text') else '')
|
||||
ws.cell(row=i, column=4, value=status_map.get(row.get('status', ''), row.get('status', '')))
|
||||
ws.cell(row=i, column=5, value=row.get('tags', ''))
|
||||
ws.cell(row=i, column=6, value=row.get('sort_order', 0))
|
||||
ws.cell(row=i, column=7, value=str(row.get('created_at', ''))[:19])
|
||||
ws.cell(row=i, column=8, value=str(row.get('published_at', ''))[:19] if row.get('published_at') else '')
|
||||
for col in range(1, 9):
|
||||
ws.cell(row=i, column=col).border = thin_border
|
||||
|
||||
# Auto-width
|
||||
col_widths = [30, 10, 40, 10, 20, 8, 20, 20]
|
||||
for i, w in enumerate(col_widths, 1):
|
||||
col_letter = chr(64 + i) if i <= 26 else 'A'
|
||||
ws.column_dimensions[col_letter].width = w
|
||||
|
||||
# Save to buffer and encode
|
||||
buf = io.BytesIO()
|
||||
wb.save(buf)
|
||||
b64 = base64.b64encode(buf.getvalue()).decode()
|
||||
|
||||
filename = f'cms_content_{curDateString()}.xlsx'
|
||||
return {'status': 'ok', 'filename': filename, 'data': b64, 'total': len(rows)}
|
||||
64
wwwroot/api/export_leads.dspy
Normal file
64
wwwroot/api/export_leads.dspy
Normal file
@ -0,0 +1,64 @@
|
||||
|
||||
import io
|
||||
import base64
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('cms')
|
||||
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
rows = await sor.R('cms_leads', {'sort': 'created_at desc'})
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = '商机线索'
|
||||
|
||||
# Headers
|
||||
headers = ['姓名', '公司', '电话', '邮箱', '行业', '地区', '来源', '感兴趣产品', '状态', '留言', '创建时间']
|
||||
header_font = Font(bold=True, color='FFFFFF')
|
||||
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
|
||||
thin_border = Border(
|
||||
left=Side(style='thin'), right=Side(style='thin'),
|
||||
top=Side(style='thin'), bottom=Side(style='thin')
|
||||
)
|
||||
|
||||
for col, h in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=h)
|
||||
cell.font = header_font
|
||||
cell.fill = header_fill
|
||||
cell.alignment = Alignment(horizontal='center')
|
||||
cell.border = thin_border
|
||||
|
||||
# Status mapping
|
||||
status_map = {'new': '新建', 'contacted': '已联系', 'qualified': '已确认', 'converted': '已转化', 'closed': '已关闭'}
|
||||
source_map = {'website': '官网', 'phone': '电话', 'referral': '推荐', 'ai_extract': 'AI抽取'}
|
||||
|
||||
# Data rows
|
||||
for i, row in enumerate(rows, 2):
|
||||
ws.cell(row=i, column=1, value=row.get('name', ''))
|
||||
ws.cell(row=i, column=2, value=row.get('company', ''))
|
||||
ws.cell(row=i, column=3, value=row.get('phone', ''))
|
||||
ws.cell(row=i, column=4, value=row.get('email', ''))
|
||||
ws.cell(row=i, column=5, value=row.get('industry', ''))
|
||||
ws.cell(row=i, column=6, value=row.get('region', ''))
|
||||
ws.cell(row=i, column=7, value=source_map.get(row.get('source', ''), row.get('source', '')))
|
||||
ws.cell(row=i, column=8, value=row.get('interest_products', ''))
|
||||
ws.cell(row=i, column=9, value=status_map.get(row.get('status', ''), row.get('status', '')))
|
||||
ws.cell(row=i, column=10, value=row.get('message', ''))
|
||||
ws.cell(row=i, column=11, value=str(row.get('created_at', ''))[:19])
|
||||
for col in range(1, 12):
|
||||
ws.cell(row=i, column=col).border = thin_border
|
||||
|
||||
# Auto-width
|
||||
for col in range(1, 12):
|
||||
ws.column_dimensions[chr(64 + col) if col <= 26 else 'A'].width = 15
|
||||
|
||||
# Save to buffer and encode
|
||||
buf = io.BytesIO()
|
||||
wb.save(buf)
|
||||
b64 = base64.b64encode(buf.getvalue()).decode()
|
||||
|
||||
filename = f'shangji_leads_{curDateString()}.xlsx'
|
||||
return {'status': 'ok', 'filename': filename, 'data': b64, 'total': len(rows)}
|
||||
Loading…
x
Reference in New Issue
Block a user