mirror of
https://github.com/rachelos/we-mp-rss.git
synced 2026-08-30 18:01:55 +08:00
1.4.9-Fix
This commit is contained in:
+2
-1
@@ -22,4 +22,5 @@ demo.txt
|
||||
test.yaml
|
||||
.venv
|
||||
.idea
|
||||
.trae
|
||||
.trae
|
||||
test.py
|
||||
@@ -0,0 +1,384 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Body
|
||||
from sqlalchemy.orm import Session
|
||||
from core.auth import get_current_user_or_ak
|
||||
from core.database import get_db
|
||||
from core.db import DB
|
||||
from core.models.filter_rule import FilterRule
|
||||
from core.models.base import DATA_STATUS
|
||||
from .base import success_response, error_response
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
|
||||
router = APIRouter(prefix="/filter-rules", tags=["过滤规则管理"])
|
||||
|
||||
|
||||
class FilterRuleCreate(BaseModel):
|
||||
mp_id: str # JSON字符串,存储多个公众号ID数组
|
||||
rule_name: str
|
||||
remove_ids: Optional[List[str]] = None
|
||||
remove_classes: Optional[List[str]] = None
|
||||
remove_selectors: Optional[List[str]] = None
|
||||
remove_attributes: Optional[List[dict]] = None
|
||||
remove_regex: Optional[List[str]] = None
|
||||
remove_normal_tag: Optional[int] = 0
|
||||
priority: Optional[int] = 0
|
||||
|
||||
|
||||
class FilterRuleUpdate(BaseModel):
|
||||
rule_name: Optional[str] = None
|
||||
remove_ids: Optional[List[str]] = None
|
||||
remove_classes: Optional[List[str]] = None
|
||||
remove_selectors: Optional[List[str]] = None
|
||||
remove_attributes: Optional[List[dict]] = None
|
||||
remove_regex: Optional[List[str]] = None
|
||||
remove_normal_tag: Optional[int] = None
|
||||
status: Optional[int] = None
|
||||
priority: Optional[int] = None
|
||||
|
||||
|
||||
@router.get("", summary="获取过滤规则列表")
|
||||
async def get_filter_rules(
|
||||
mp_id: str = Query(None, description="公众号ID,不传则返回所有"),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
offset: int = Query(0, ge=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user_or_ak)
|
||||
):
|
||||
"""获取过滤规则列表,支持按公众号筛选"""
|
||||
try:
|
||||
query = db.query(FilterRule)
|
||||
if mp_id:
|
||||
query = query.filter(FilterRule.mp_id == mp_id)
|
||||
|
||||
total = query.count()
|
||||
rules = query.order_by(FilterRule.priority.desc(), FilterRule.created_at.desc()).limit(limit).offset(offset).all()
|
||||
|
||||
rules_list = []
|
||||
for rule in rules:
|
||||
# 解析 mp_id JSON 字符串为数组
|
||||
mp_ids = []
|
||||
try:
|
||||
if rule.mp_id:
|
||||
mp_ids = json.loads(rule.mp_id) if rule.mp_id.startswith('[') else [rule.mp_id]
|
||||
except:
|
||||
mp_ids = [rule.mp_id] if rule.mp_id else []
|
||||
|
||||
rules_list.append({
|
||||
"id": rule.id,
|
||||
"mp_id": rule.mp_id,
|
||||
"mp_ids": mp_ids,
|
||||
"is_global": len(mp_ids) == 0, # 标记是否为全局规则
|
||||
"rule_name": rule.rule_name,
|
||||
"remove_ids": rule.remove_ids or [],
|
||||
"remove_classes": rule.remove_classes or [],
|
||||
"remove_selectors": rule.remove_selectors or [],
|
||||
"remove_attributes": rule.remove_attributes or [],
|
||||
"remove_regex": rule.remove_regex or [],
|
||||
"remove_normal_tag": rule.remove_normal_tag or 0,
|
||||
"status": rule.status,
|
||||
"priority": rule.priority,
|
||||
"created_at": rule.created_at.isoformat() if rule.created_at else None,
|
||||
"updated_at": rule.updated_at.isoformat() if rule.updated_at else None
|
||||
})
|
||||
|
||||
return success_response(data={
|
||||
"list": rules_list,
|
||||
"page": {
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"total": total
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f"获取过滤规则列表错误: {str(e)}")
|
||||
# 如果表不存在,返回空列表
|
||||
if "no such table" in str(e).lower() or "filter_rules" in str(e).lower():
|
||||
return success_response(data={
|
||||
"list": [],
|
||||
"page": {"limit": limit, "offset": offset, "total": 0}
|
||||
})
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=error_response(code=50001, message=f"获取过滤规则列表失败: {str(e)}")
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{rule_id}", summary="获取过滤规则详情")
|
||||
async def get_filter_rule(
|
||||
rule_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user_or_ak)
|
||||
):
|
||||
"""获取单个过滤规则详情"""
|
||||
try:
|
||||
rule = db.query(FilterRule).filter(FilterRule.id == rule_id).first()
|
||||
if not rule:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=error_response(code=40401, message="过滤规则不存在")
|
||||
)
|
||||
|
||||
# 解析 mp_id JSON 字符串为数组
|
||||
mp_ids = []
|
||||
try:
|
||||
if rule.mp_id:
|
||||
mp_ids = json.loads(rule.mp_id) if rule.mp_id.startswith('[') else [rule.mp_id]
|
||||
except:
|
||||
mp_ids = [rule.mp_id] if rule.mp_id else []
|
||||
|
||||
return success_response(data={
|
||||
"id": rule.id,
|
||||
"mp_id": rule.mp_id,
|
||||
"mp_ids": mp_ids,
|
||||
"is_global": len(mp_ids) == 0, # 标记是否为全局规则
|
||||
"rule_name": rule.rule_name,
|
||||
"remove_ids": rule.remove_ids or [],
|
||||
"remove_classes": rule.remove_classes or [],
|
||||
"remove_selectors": rule.remove_selectors or [],
|
||||
"remove_attributes": rule.remove_attributes or [],
|
||||
"remove_regex": rule.remove_regex or [],
|
||||
"remove_normal_tag": rule.remove_normal_tag or 0,
|
||||
"status": rule.status,
|
||||
"priority": rule.priority,
|
||||
"created_at": rule.created_at.isoformat() if rule.created_at else None,
|
||||
"updated_at": rule.updated_at.isoformat() if rule.updated_at else None
|
||||
})
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"获取过滤规则详情错误: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=error_response(code=50001, message="获取过滤规则详情失败")
|
||||
)
|
||||
|
||||
|
||||
@router.post("", summary="创建过滤规则")
|
||||
async def create_filter_rule(
|
||||
rule: FilterRuleCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user_or_ak)
|
||||
):
|
||||
"""为指定公众号创建过滤规则,支持多公众号"""
|
||||
try:
|
||||
new_rule = FilterRule(
|
||||
mp_id=rule.mp_id,
|
||||
rule_name=rule.rule_name,
|
||||
remove_ids=rule.remove_ids,
|
||||
remove_classes=rule.remove_classes,
|
||||
remove_selectors=rule.remove_selectors,
|
||||
remove_attributes=rule.remove_attributes,
|
||||
remove_regex=rule.remove_regex,
|
||||
remove_normal_tag=rule.remove_normal_tag or 0,
|
||||
priority=rule.priority or 0,
|
||||
status=DATA_STATUS.ACTIVE,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now()
|
||||
)
|
||||
db.add(new_rule)
|
||||
db.commit()
|
||||
db.refresh(new_rule)
|
||||
|
||||
return success_response(data={
|
||||
"id": new_rule.id,
|
||||
"mp_id": new_rule.mp_id,
|
||||
"rule_name": new_rule.rule_name,
|
||||
"message": "过滤规则创建成功"
|
||||
})
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"创建过滤规则错误: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=error_response(code=50001, message="创建过滤规则失败")
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{rule_id}", summary="更新过滤规则")
|
||||
async def update_filter_rule(
|
||||
rule_id: int,
|
||||
rule: FilterRuleUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user_or_ak)
|
||||
):
|
||||
"""更新过滤规则"""
|
||||
try:
|
||||
existing_rule = db.query(FilterRule).filter(FilterRule.id == rule_id).first()
|
||||
if not existing_rule:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=error_response(code=40401, message="过滤规则不存在")
|
||||
)
|
||||
|
||||
update_data = rule.dict(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(existing_rule, key, value)
|
||||
|
||||
existing_rule.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
return success_response(data={
|
||||
"id": existing_rule.id,
|
||||
"message": "过滤规则更新成功"
|
||||
})
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"更新过滤规则错误: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=error_response(code=50001, message="更新过滤规则失败")
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{rule_id}", summary="删除过滤规则")
|
||||
async def delete_filter_rule(
|
||||
rule_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user_or_ak)
|
||||
):
|
||||
"""删除过滤规则"""
|
||||
try:
|
||||
rule = db.query(FilterRule).filter(FilterRule.id == rule_id).first()
|
||||
if not rule:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=error_response(code=40401, message="过滤规则不存在")
|
||||
)
|
||||
|
||||
db.delete(rule)
|
||||
db.commit()
|
||||
|
||||
return success_response(data={
|
||||
"id": rule_id,
|
||||
"message": "过滤规则删除成功"
|
||||
})
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"删除过滤规则错误: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=error_response(code=50001, message="删除过滤规则失败")
|
||||
)
|
||||
|
||||
|
||||
@router.get("/mp/{mp_id}/active", summary="获取公众号的启用规则")
|
||||
async def get_active_rules_for_mp(
|
||||
mp_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user_or_ak)
|
||||
):
|
||||
"""获取指定公众号的所有启用的过滤规则(支持多公众号匹配和全局规则)"""
|
||||
try:
|
||||
# 获取所有启用的规则,然后在 Python 层面过滤
|
||||
rules = db.query(FilterRule).filter(
|
||||
FilterRule.status == DATA_STATUS.ACTIVE
|
||||
).order_by(FilterRule.priority.desc()).all()
|
||||
|
||||
rules_list = []
|
||||
for rule in rules:
|
||||
# 解析 mp_id JSON,检查是否包含指定的 mp_id 或者是全局规则(空数组)
|
||||
try:
|
||||
mp_ids = json.loads(rule.mp_id) if rule.mp_id and rule.mp_id.startswith('[') else ([rule.mp_id] if rule.mp_id else [])
|
||||
except:
|
||||
mp_ids = [rule.mp_id] if rule.mp_id else []
|
||||
|
||||
# 匹配条件:mp_id 在列表中,或者是全局规则(空数组)
|
||||
if not mp_ids or mp_id in mp_ids:
|
||||
rules_list.append({
|
||||
"id": rule.id,
|
||||
"rule_name": rule.rule_name,
|
||||
"remove_ids": rule.remove_ids or [],
|
||||
"remove_classes": rule.remove_classes or [],
|
||||
"remove_selectors": rule.remove_selectors or [],
|
||||
"remove_attributes": rule.remove_attributes or [],
|
||||
"remove_regex": rule.remove_regex or [],
|
||||
"remove_normal_tag": rule.remove_normal_tag or 0,
|
||||
"priority": rule.priority
|
||||
})
|
||||
|
||||
return success_response(data=rules_list)
|
||||
except Exception as e:
|
||||
print(f"获取公众号过滤规则错误: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=error_response(code=50001, message="获取公众号过滤规则失败")
|
||||
)
|
||||
|
||||
|
||||
def get_filter_rules_for_mp(mp_id: str) -> list:
|
||||
"""
|
||||
获取指定公众号的所有启用过滤规则(供内部调用)
|
||||
返回规则列表,用于HTML过滤
|
||||
支持多公众号匹配和全局规则(空mp_id数组)
|
||||
"""
|
||||
session = DB.get_session()
|
||||
try:
|
||||
rules = session.query(FilterRule).filter(
|
||||
FilterRule.status == DATA_STATUS.ACTIVE
|
||||
).order_by(FilterRule.priority.desc()).all()
|
||||
|
||||
# 在 Python 层面过滤匹配的规则
|
||||
matched_rules = []
|
||||
for rule in rules:
|
||||
try:
|
||||
mp_ids = json.loads(rule.mp_id) if rule.mp_id and rule.mp_id.startswith('[') else ([rule.mp_id] if rule.mp_id else [])
|
||||
except:
|
||||
mp_ids = [rule.mp_id] if rule.mp_id else []
|
||||
|
||||
# 匹配条件:mp_id 在列表中,或者是全局规则(空数组)
|
||||
if not mp_ids or mp_id in mp_ids:
|
||||
matched_rules.append(rule)
|
||||
|
||||
return matched_rules
|
||||
except Exception as e:
|
||||
print(f"获取过滤规则失败: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
def apply_filter_rules(html_content: str, mp_id: str) -> str:
|
||||
"""
|
||||
对HTML内容应用指定公众号的过滤规则
|
||||
|
||||
Args:
|
||||
html_content: 原始HTML内容
|
||||
mp_id: 公众号ID
|
||||
|
||||
Returns:
|
||||
过滤后的HTML内容
|
||||
"""
|
||||
if not html_content or not mp_id:
|
||||
return html_content
|
||||
|
||||
rules = get_filter_rules_for_mp(mp_id)
|
||||
if not rules:
|
||||
return html_content
|
||||
|
||||
from tools.htmltools import htmltools
|
||||
|
||||
filtered_content = html_content
|
||||
for rule in rules:
|
||||
try:
|
||||
filtered_content = htmltools.clean_html(
|
||||
filtered_content,
|
||||
remove_ids=rule.remove_ids or [],
|
||||
remove_classes=rule.remove_classes or [],
|
||||
remove_selectors=rule.remove_selectors or [],
|
||||
remove_attributes=rule.remove_attributes or [],
|
||||
remove_regx=rule.remove_regex or [],
|
||||
remove_normal_tag=bool(rule.remove_normal_tag)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"应用过滤规则失败 (规则ID: {rule.id}): {str(e)}")
|
||||
continue
|
||||
|
||||
return filtered_content
|
||||
+3
-1
@@ -11,11 +11,13 @@ server:
|
||||
#启动JOB定时任务,默认为True
|
||||
enable_job: ${ENABLE_JOB:-True}
|
||||
#代码修改自动重启服务,默认为False
|
||||
auto_reload: ${AUTO_RELOAD:-True}
|
||||
auto_reload: ${AUTO_RELOAD:-False}
|
||||
#最大线程数 默认2个线程,不建议超过4个线程
|
||||
threads: ${THREADS:-1}
|
||||
#通过web方式授权二维码 默认False
|
||||
auth_web: ${WERSS_AUTH_WEB:-True}
|
||||
#是否发送授权二维通知 默认True
|
||||
send_code: ${WERSS_SEND_CODE:-True}
|
||||
|
||||
|
||||
#数据库连接 例如db: mysql+pymysql://<username>:<password>@<host>/we-rss?charset=utf8mb4
|
||||
|
||||
+12
-4
@@ -144,16 +144,15 @@ class Db:
|
||||
except ValueError:
|
||||
return now_ts
|
||||
return now_ts
|
||||
|
||||
def _to_unix_millis(value, fallback_seconds) -> int:
|
||||
now_ts = int(datetime.now().timestamp())
|
||||
now_ts = datetime.now().timestamp() # 保留小数精度
|
||||
# 确保 fallback_seconds 是有效的秒级时间戳
|
||||
if fallback_seconds is None:
|
||||
fallback_seconds = now_ts
|
||||
if isinstance(fallback_seconds, (int, float)):
|
||||
fallback_seconds = int(fallback_seconds)
|
||||
if fallback_seconds > 1_000_000_000_000:
|
||||
fallback_seconds = int(fallback_seconds / 1000)
|
||||
fallback_seconds = fallback_seconds / 1000
|
||||
# 不转换为int,保留小数部分
|
||||
else:
|
||||
fallback_seconds = now_ts
|
||||
|
||||
@@ -205,6 +204,15 @@ class Db:
|
||||
if art.content_html is None:
|
||||
from tools.fix import fix_html
|
||||
art.content_html = fix_html(art.content)
|
||||
|
||||
# 应用过滤规则
|
||||
try:
|
||||
from apis.filter_rule import apply_filter_rules
|
||||
if art.mp_id and art.content_html:
|
||||
art.content_html = apply_filter_rules(art.content_html, art.mp_id)
|
||||
except Exception as e:
|
||||
print_warning(f"应用过滤规则失败: {e}")
|
||||
|
||||
from core.models.base import DATA_STATUS
|
||||
art.status=DATA_STATUS.ACTIVE
|
||||
session.add(art)
|
||||
|
||||
@@ -14,5 +14,7 @@ from .access_key import AccessKey
|
||||
from .cascade_node import CascadeNode, CascadeSyncLog
|
||||
# 导入级联任务分配模型
|
||||
from .cascade_task_allocation import CascadeTaskAllocation
|
||||
# 导入过滤规则模型
|
||||
from .filter_rule import FilterRule
|
||||
# 导入基础模型
|
||||
from .base import *
|
||||
@@ -0,0 +1,27 @@
|
||||
from .base import Base, Column, String, Integer, DateTime, Text, JSON
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class FilterRule(Base):
|
||||
"""HTML过滤规则模型 - 为指定公众号配置HTML过滤规则"""
|
||||
__tablename__ = 'filter_rules'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
mp_id = Column(String(255), nullable=False, index=True, comment='公众号ID')
|
||||
rule_name = Column(String(255), nullable=False, comment='规则名称')
|
||||
|
||||
# 过滤规则配置
|
||||
remove_ids = Column(JSON, nullable=True, comment='要移除的ID列表')
|
||||
remove_classes = Column(JSON, nullable=True, comment='要移除的class列表')
|
||||
remove_selectors = Column(JSON, nullable=True, comment='CSS选择器列表')
|
||||
remove_attributes = Column(JSON, nullable=True, comment='属性过滤列表 [{"name": "attr_name", "value": "attr_value", "eq": true}]')
|
||||
remove_regex = Column(JSON, nullable=True, comment='正则表达式列表')
|
||||
remove_normal_tag = Column(Integer, default=0, comment='是否移除常见HTML元素(0-否, 1-是)')
|
||||
|
||||
status = Column(Integer, default=1, comment='状态(1-启用, 2-禁用)')
|
||||
priority = Column(Integer, default=0, comment='优先级,数字越大优先级越高')
|
||||
|
||||
created_at = Column(DateTime, default=datetime.now, comment='创建时间')
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
|
||||
|
||||
from_attributes = True
|
||||
+5
-1
@@ -1,3 +1,5 @@
|
||||
from nt import environ
|
||||
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
@@ -277,7 +279,9 @@ class WxGather:
|
||||
setStatus(False)
|
||||
from core.queue import TaskQueue
|
||||
TaskQueue.clear_queue()
|
||||
threading.Thread(target=send_wx_code,args=(f"公众号平台登录失效,请重新登录",)).start()
|
||||
import os
|
||||
if str(os.getenv('WE_RSS.AUTH',False))!="True" and cfg.get("server.send_code")=="True":
|
||||
threading.Thread(target=send_wx_code,args=(f"公众号平台登录失效,请重新登录",)).start()
|
||||
# send_wx_code(f"公众号平台登录失效,请重新登录")
|
||||
raise Exception(error)
|
||||
# raise Exception(error)
|
||||
|
||||
@@ -5,6 +5,7 @@ from core.models.feed import Feed
|
||||
from core.models.message_task import MessageTask
|
||||
from core.models.cascade_node import CascadeNode, CascadeSyncLog
|
||||
from core.models.cascade_task_allocation import CascadeTaskAllocation
|
||||
from core.models.filter_rule import FilterRule
|
||||
from core.db import Db,DB
|
||||
from core.config import cfg
|
||||
from core.auth import pwd_context
|
||||
|
||||
@@ -21,6 +21,7 @@ from apis.tools import router as tools_router
|
||||
from apis.github_update import router as github_router
|
||||
from apis.cascade import router as cascade_router
|
||||
from apis.env_exception import router as env_exception_router
|
||||
from apis.filter_rule import router as filter_rule_router
|
||||
from views import router as views_router
|
||||
import apis
|
||||
import os
|
||||
@@ -93,6 +94,7 @@ api_router.include_router(tools_router)
|
||||
api_router.include_router(github_router)
|
||||
api_router.include_router(cascade_router)
|
||||
api_router.include_router(env_exception_router)
|
||||
api_router.include_router(filter_rule_router)
|
||||
|
||||
resource_router = APIRouter(prefix="/static")
|
||||
resource_router.include_router(res_router)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import http from './http'
|
||||
|
||||
export interface FilterRule {
|
||||
id: number
|
||||
mp_id: string // JSON字符串,存储多个公众号ID
|
||||
mp_ids: string[] // 解析后的公众号ID数组
|
||||
is_global: boolean // 是否为全局规则
|
||||
rule_name: string
|
||||
remove_ids: string[]
|
||||
remove_classes: string[]
|
||||
remove_selectors: string[]
|
||||
remove_attributes: Array<{ name: string; value?: string; eq?: boolean }>
|
||||
remove_regex: string[]
|
||||
remove_normal_tag: number
|
||||
status: number
|
||||
priority: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface FilterRuleCreateParams {
|
||||
mp_id: string // JSON字符串,存储多个公众号ID
|
||||
rule_name: string
|
||||
remove_ids?: string[]
|
||||
remove_classes?: string[]
|
||||
remove_selectors?: string[]
|
||||
remove_attributes?: Array<{ name: string; value?: string; eq?: boolean }>
|
||||
remove_regex?: string[]
|
||||
remove_normal_tag?: number
|
||||
priority?: number
|
||||
}
|
||||
|
||||
export interface FilterRuleUpdateParams {
|
||||
rule_name?: string
|
||||
remove_ids?: string[]
|
||||
remove_classes?: string[]
|
||||
remove_selectors?: string[]
|
||||
remove_attributes?: Array<{ name: string; value?: string; eq?: boolean }>
|
||||
remove_regex?: string[]
|
||||
remove_normal_tag?: number
|
||||
status?: number
|
||||
priority?: number
|
||||
}
|
||||
|
||||
export interface FilterRuleListResult {
|
||||
code: number
|
||||
data: {
|
||||
list: FilterRule[]
|
||||
page: {
|
||||
limit: number
|
||||
offset: number
|
||||
total: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取过滤规则列表
|
||||
export const getFilterRules = (params?: { mp_id?: string; limit?: number; offset?: number }) => {
|
||||
return http.get<FilterRuleListResult>('/wx/filter-rules', { params })
|
||||
}
|
||||
|
||||
// 获取单个过滤规则详情
|
||||
export const getFilterRule = (ruleId: number) => {
|
||||
return http.get<{ code: number; data: FilterRule }>(`/wx/filter-rules/${ruleId}`)
|
||||
}
|
||||
|
||||
// 创建过滤规则
|
||||
export const createFilterRule = (data: FilterRuleCreateParams) => {
|
||||
return http.post<{ code: number; data: { id: number; message: string } }>('/wx/filter-rules', data)
|
||||
}
|
||||
|
||||
// 更新过滤规则
|
||||
export const updateFilterRule = (ruleId: number, data: FilterRuleUpdateParams) => {
|
||||
return http.put<{ code: number; data: { id: number; message: string } }>(`/wx/filter-rules/${ruleId}`, data)
|
||||
}
|
||||
|
||||
// 删除过滤规则
|
||||
export const deleteFilterRule = (ruleId: number) => {
|
||||
return http.delete<{ code: number; data: { id: number; message: string } }>(`/wx/filter-rules/${ruleId}`)
|
||||
}
|
||||
|
||||
// 获取公众号的启用规则
|
||||
export const getActiveRulesForMp = (mpId: string) => {
|
||||
return http.get<{ code: number; data: FilterRule[] }>(`/wx/filter-rules/mp/${mpId}/active`)
|
||||
}
|
||||
@@ -29,6 +29,12 @@
|
||||
</template>
|
||||
消息任务
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/filter-rules">
|
||||
<template #icon>
|
||||
<icon-filter />
|
||||
</template>
|
||||
过滤规则
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/configs">
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
|
||||
@@ -25,8 +25,9 @@ const loading = ref(false)
|
||||
const mpList = ref<MpItem[]>([])
|
||||
const selectedMps = ref<MpItem[]>([])
|
||||
const currentPage = ref(0)
|
||||
const totalCount = ref(0)
|
||||
const hasMore = ref(true)
|
||||
const pageSize = 10
|
||||
const pageSize = 20
|
||||
|
||||
const filteredMps = computed(() => {
|
||||
return mpList.value.filter(mp =>
|
||||
@@ -65,8 +66,9 @@ const fetchMps = async (reset = true) => {
|
||||
mpList.value = [...mpList.value, ...newMps]
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
hasMore.value = res.list.length === pageSize
|
||||
// 更新总数并判断是否还有更多数据
|
||||
totalCount.value = res.total || 0
|
||||
hasMore.value = mpList.value.length < totalCount.value
|
||||
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -193,10 +195,13 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div v-if="hasMore" class="load-more">
|
||||
<a-button type="text" @click="loadMore" :loading="loading">
|
||||
加载更多
|
||||
<a-button type="text" @click="loadMore" :loading="loading" :disabled="loading">
|
||||
加载更多 ({{ mpList.length }}/{{ totalCount }})
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-else-if="mpList.length > 0" class="load-more no-more">
|
||||
已加载全部 {{ totalCount }} 个公众号
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-space>
|
||||
</a-card>
|
||||
@@ -251,4 +256,9 @@ h4 {
|
||||
justify-content: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -12,6 +12,8 @@ import ConfigDetail from '../views/ConfigDetail.vue'
|
||||
import MessageTaskList from '../views/MessageTaskList.vue'
|
||||
import MessageTaskForm from '../views/MessageTaskForm.vue'
|
||||
import NovelReader from '../views/NovelReader.vue'
|
||||
import FilterRuleList from '../views/FilterRuleList.vue'
|
||||
import FilterRuleForm from '../views/FilterRuleForm.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -172,6 +174,34 @@ const routes = [
|
||||
permissions: ['admin']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'filter-rules',
|
||||
name: 'FilterRuleList',
|
||||
component: FilterRuleList,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
permissions: ['wechat:manage']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'filter-rules/add',
|
||||
name: 'FilterRuleAdd',
|
||||
component: FilterRuleForm,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
permissions: ['wechat:manage']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'filter-rules/edit/:id',
|
||||
name: 'FilterRuleEdit',
|
||||
component: FilterRuleForm,
|
||||
props: true,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
permissions: ['wechat:manage']
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import {
|
||||
getFilterRule,
|
||||
createFilterRule,
|
||||
updateFilterRule,
|
||||
type FilterRule,
|
||||
type FilterRuleCreateParams,
|
||||
type FilterRuleUpdateParams
|
||||
} from '@/api/filterRule'
|
||||
import MpMultiSelect from '@/components/MpMultiSelect.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
const showMpSelector = ref(false)
|
||||
const mpSelectorRef = ref<InstanceType<typeof MpMultiSelect> | null>(null)
|
||||
|
||||
// 表单数据
|
||||
const formData = ref<{
|
||||
mps_id: any[] // 选中的公众号数组
|
||||
rule_name: string
|
||||
remove_ids: string
|
||||
remove_classes: string
|
||||
remove_selectors: string
|
||||
remove_attributes: Array<{ name: string; value: string; eq: boolean }>
|
||||
remove_regex: string
|
||||
remove_normal_tag: boolean
|
||||
priority: number
|
||||
}>({
|
||||
mps_id: [],
|
||||
rule_name: '',
|
||||
remove_ids: '',
|
||||
remove_classes: '',
|
||||
remove_selectors: '',
|
||||
remove_attributes: [],
|
||||
remove_regex: '',
|
||||
remove_normal_tag: false,
|
||||
priority: 0
|
||||
})
|
||||
|
||||
// 新增属性行
|
||||
const addAttributeRow = () => {
|
||||
formData.value.remove_attributes.push({ name: '', value: '', eq: false })
|
||||
}
|
||||
|
||||
// 删除属性行
|
||||
const removeAttributeRow = (index: number) => {
|
||||
formData.value.remove_attributes.splice(index, 1)
|
||||
}
|
||||
|
||||
// 获取规则详情
|
||||
const fetchRuleDetail = async (id: number) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getFilterRule(id)
|
||||
const rule = res
|
||||
// 解析 mp_id JSON 字符串
|
||||
let mpIds: any[] = []
|
||||
try {
|
||||
if (rule.mp_id) {
|
||||
mpIds = rule.mp_id.startsWith('[')
|
||||
? JSON.parse(rule.mp_id)
|
||||
: rule.mp_id.split(',').map((id: string) => ({ id: id.trim() }))
|
||||
}
|
||||
} catch {
|
||||
mpIds = rule.mp_id ? [{ id: rule.mp_id }] : []
|
||||
}
|
||||
|
||||
formData.value = {
|
||||
mps_id: mpIds,
|
||||
rule_name: rule.rule_name,
|
||||
remove_ids: (rule.remove_ids || []).join('\n'),
|
||||
remove_classes: (rule.remove_classes || []).join('\n'),
|
||||
remove_selectors: (rule.remove_selectors || []).join('\n'),
|
||||
remove_attributes: rule.remove_attributes || [],
|
||||
remove_regex: (rule.remove_regex || []).join('\n'),
|
||||
remove_normal_tag: !!rule.remove_normal_tag,
|
||||
priority: rule.priority || 0
|
||||
}
|
||||
|
||||
// 初始化选择器数据
|
||||
nextTick(() => {
|
||||
if (mpSelectorRef.value && mpIds.length > 0) {
|
||||
mpSelectorRef.value.parseSelected(mpIds)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
Message.error('获取规则详情失败')
|
||||
router.back()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.value.rule_name.trim()) {
|
||||
Message.warning('请输入规则名称')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
// 提取公众号ID数组并转为JSON字符串,如果为空则传空数组
|
||||
const mpIdsArray = formData.value.mps_id.map((mp: any) => mp.id?.toString() || mp.toString())
|
||||
const mpIdJson = mpIdsArray.length > 0 ? JSON.stringify(mpIdsArray) : '[]'
|
||||
|
||||
const data: FilterRuleCreateParams | FilterRuleUpdateParams = {
|
||||
mp_id: mpIdJson,
|
||||
rule_name: formData.value.rule_name.trim(),
|
||||
remove_ids: formData.value.remove_ids
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean),
|
||||
remove_classes: formData.value.remove_classes
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean),
|
||||
remove_selectors: formData.value.remove_selectors
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean),
|
||||
remove_attributes: formData.value.remove_attributes.filter(a => a.name.trim()),
|
||||
remove_regex: formData.value.remove_regex
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean),
|
||||
remove_normal_tag: formData.value.remove_normal_tag ? 1 : 0,
|
||||
priority: formData.value.priority
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await updateFilterRule(Number(route.params.id), data)
|
||||
Message.success('更新成功')
|
||||
} else {
|
||||
await createFilterRule(data as FilterRuleCreateParams)
|
||||
Message.success('创建成功')
|
||||
}
|
||||
router.push('/filter-rules')
|
||||
} catch (error: any) {
|
||||
Message.error(error?.response?.data?.message || '操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (isEdit.value) {
|
||||
fetchRuleDetail(Number(route.params.id))
|
||||
} else if (route.query.mp_id) {
|
||||
// 从URL参数获取预选公众号
|
||||
formData.value.mps_id = [{ id: route.query.mp_id as string }]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-spin :loading="loading">
|
||||
<div class="filter-rule-form">
|
||||
<a-page-header
|
||||
:title="isEdit ? '编辑过滤规则' : '添加过滤规则'"
|
||||
subtitle="配置HTML内容过滤规则"
|
||||
@back="handleCancel"
|
||||
/>
|
||||
|
||||
<a-card :loading="loading">
|
||||
<a-form :model="formData" layout="vertical" @submit-success="handleSubmit">
|
||||
<a-form-item label="选择公众号" field="mps_id">
|
||||
<a-space>
|
||||
<a-input
|
||||
:model-value="(formData.mps_id||[]).map((mp: any) => mp.id?.toString() || mp.toString()).join(',')"
|
||||
placeholder="不选择则对所有公众号生效"
|
||||
readonly
|
||||
style="width: 300px"
|
||||
/>
|
||||
<a-button @click="showMpSelector = true">选择</a-button>
|
||||
</a-space>
|
||||
<template #extra>
|
||||
<span class="form-tip">可选择多个公众号,不选择则作为全局规则对所有公众号生效</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="规则名称" field="rule_name" :rules="[{ required: true, message: '请输入规则名称' }]">
|
||||
<a-input v-model="formData.rule_name" placeholder="例如:移除广告元素" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="优先级" field="priority">
|
||||
<a-input-number v-model="formData.priority" :min="0" :max="100" placeholder="数字越大优先级越高" />
|
||||
<template #extra>
|
||||
<span class="form-tip">数字越大优先级越高,同一公众号的规则按优先级依次执行</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider>过滤规则配置</a-divider>
|
||||
|
||||
<a-form-item label="移除ID元素">
|
||||
<a-textarea
|
||||
v-model="formData.remove_ids"
|
||||
placeholder="每行一个ID,例如: ad-banner, footer-nav"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
/>
|
||||
<template #extra>
|
||||
<span class="form-tip">按元素ID移除,每行一个</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="移除Class元素">
|
||||
<a-textarea
|
||||
v-model="formData.remove_classes"
|
||||
placeholder="每行一个class名称,例如: ad-container, recommend-box"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
/>
|
||||
<template #extra>
|
||||
<span class="form-tip">按CSS class移除,每行一个</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="CSS选择器">
|
||||
<a-textarea
|
||||
v-model="formData.remove_selectors"
|
||||
placeholder="每行一个CSS选择器,例如: div.ad-wrapper, .recommend-list > li"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
/>
|
||||
<template #extra>
|
||||
<span class="form-tip">使用CSS选择器精确定位元素,每行一个</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="属性过滤">
|
||||
<div class="attribute-list">
|
||||
<div v-for="(attr, index) in formData.remove_attributes" :key="index" class="attribute-row">
|
||||
<a-input v-model="attr.name" placeholder="属性名" style="width: 120px" />
|
||||
<a-input v-model="attr.value" placeholder="属性值(可选)" style="flex: 1" />
|
||||
<a-checkbox v-model="attr.eq">精确匹配</a-checkbox>
|
||||
<a-button type="text" status="danger" @click="removeAttributeRow(index)">
|
||||
<template #icon><icon-delete /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
<a-button type="dashed" long @click="addAttributeRow">
|
||||
<template #icon><icon-plus /></template>
|
||||
添加属性条件
|
||||
</a-button>
|
||||
</div>
|
||||
<template #extra>
|
||||
<span class="form-tip">根据元素属性过滤,如 data-type="ad"</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="正则表达式">
|
||||
<a-textarea
|
||||
v-model="formData.remove_regex"
|
||||
placeholder="每行一个正则表达式,例如: <div class="ad">.*?</div>"
|
||||
:auto-size="{ minRows: 2, maxRows: 6 }"
|
||||
/>
|
||||
<template #extra>
|
||||
<span class="form-tip">使用正则表达式移除内容,每行一个。谨慎使用,确保表达式正确</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="移除常见HTML元素">
|
||||
<a-switch v-model="formData.remove_normal_tag" />
|
||||
<template #extra>
|
||||
<span class="form-tip">自动移除 script、style 标签和 HTML 注释等常见元素</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit" :loading="submitting">
|
||||
{{ isEdit ? '保存修改' : '创建规则' }}
|
||||
</a-button>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 公众号选择器模态框 -->
|
||||
<a-modal
|
||||
v-model:visible="showMpSelector"
|
||||
title="选择公众号"
|
||||
:footer="false"
|
||||
width="800px"
|
||||
>
|
||||
<MpMultiSelect
|
||||
ref="mpSelectorRef"
|
||||
v-model="formData.mps_id"
|
||||
/>
|
||||
<template #footer>
|
||||
<a-button type="primary" @click="showMpSelector = false">确定</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-rule-form {
|
||||
padding: 16px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.attribute-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.attribute-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-rule-form {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.attribute-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.attribute-row .arco-input {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,380 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import {
|
||||
getFilterRules,
|
||||
deleteFilterRule,
|
||||
updateFilterRule,
|
||||
type FilterRule
|
||||
} from '@/api/filterRule'
|
||||
import { getSubscriptions, type Subscription } from '@/api/subscription'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const loading = ref(false)
|
||||
const ruleList = ref<FilterRule[]>([])
|
||||
const mpList = ref<Subscription[]>([])
|
||||
const selectedMpId = ref<string>('')
|
||||
const pagination = ref({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const isMobile = ref(window.innerWidth < 768)
|
||||
|
||||
// 获取公众号列表
|
||||
const fetchMpList = async () => {
|
||||
try {
|
||||
const res = await getSubscriptions({ page: 0, pageSize: 100 })
|
||||
mpList.value = res.list || []
|
||||
} catch (error) {
|
||||
console.error('获取公众号列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取规则列表
|
||||
const fetchRuleList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: { mp_id?: string; limit: number; offset: number } = {
|
||||
limit: pagination.value.pageSize,
|
||||
offset: (pagination.value.current - 1) * pagination.value.pageSize
|
||||
}
|
||||
if (selectedMpId.value) {
|
||||
params.mp_id = selectedMpId.value
|
||||
}
|
||||
const res = await getFilterRules(params)
|
||||
ruleList.value = res.list || []
|
||||
pagination.value.total = res.page?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取规则列表失败:', error)
|
||||
Message.error('获取规则列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取公众号名称(支持单个或多个)
|
||||
const getMpNames = (mpIds: string[] | string) => {
|
||||
if (!mpIds) return '全局规则'
|
||||
const ids = Array.isArray(mpIds) ? mpIds : [mpIds]
|
||||
if (ids.length === 0) return '全局规则'
|
||||
const names = ids.map(id => {
|
||||
const mp = mpList.value.find(m => m.id === id)
|
||||
return mp?.mp_name || id
|
||||
})
|
||||
return names.join(', ')
|
||||
}
|
||||
|
||||
// 获取第一个公众号名称(用于头像显示)
|
||||
const getFirstMpName = (mpIds: string[] | string) => {
|
||||
if (!mpIds) return '全'
|
||||
const ids = Array.isArray(mpIds) ? mpIds : [mpIds]
|
||||
if (ids.length === 0) return '全'
|
||||
const mp = mpList.value.find(m => m.id === ids[0])
|
||||
return mp?.mp_name?.charAt(0) || ids[0]?.charAt(0) || '?'
|
||||
}
|
||||
|
||||
// 处理筛选变化
|
||||
const handleFilterChange = () => {
|
||||
pagination.value.current = 1
|
||||
fetchRuleList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number) => {
|
||||
pagination.value.current = page
|
||||
fetchRuleList()
|
||||
}
|
||||
|
||||
// 加载更多(移动端)
|
||||
const handleLoadMore = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
pagination.value.current += 1
|
||||
const params: { mp_id?: string; limit: number; offset: number } = {
|
||||
limit: pagination.value.pageSize,
|
||||
offset: (pagination.value.current - 1) * pagination.value.pageSize
|
||||
}
|
||||
if (selectedMpId.value) {
|
||||
params.mp_id = selectedMpId.value
|
||||
}
|
||||
const res = await getFilterRules(params)
|
||||
ruleList.value = [...ruleList.value, ...res.list]
|
||||
pagination.value.total = res.page?.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 添加规则
|
||||
const handleAdd = () => {
|
||||
router.push('/filter-rules/add')
|
||||
}
|
||||
|
||||
// 编辑规则
|
||||
const handleEdit = (id: number) => {
|
||||
router.push(`/filter-rules/edit/${id}`)
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
const handleToggleStatus = async (rule: FilterRule) => {
|
||||
const newStatus = rule.status === 1 ? 2 : 1
|
||||
try {
|
||||
await updateFilterRule(rule.id, { status: newStatus })
|
||||
Message.success(newStatus === 1 ? '规则已启用' : '规则已禁用')
|
||||
fetchRuleList()
|
||||
} catch (error) {
|
||||
Message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除规则
|
||||
const handleDelete = (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这条过滤规则吗?删除后无法恢复',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteFilterRule(id)
|
||||
Message.success('删除成功')
|
||||
fetchRuleList()
|
||||
} catch (error) {
|
||||
Message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 预览规则配置
|
||||
const previewRule = (rule: FilterRule) => {
|
||||
const config: string[] = []
|
||||
if (rule.remove_ids?.length) config.push(`移除ID: ${rule.remove_ids.join(', ')}`)
|
||||
if (rule.remove_classes?.length) config.push(`移除Class: ${rule.remove_classes.join(', ')}`)
|
||||
if (rule.remove_selectors?.length) config.push(`CSS选择器: ${rule.remove_selectors.join(', ')}`)
|
||||
if (rule.remove_regex?.length) config.push(`正则: ${rule.remove_regex.length}条`)
|
||||
if (rule.remove_normal_tag) config.push('移除常见元素')
|
||||
return config.length ? config.join(' | ') : '无过滤配置'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchMpList()
|
||||
// 从路由参数获取mp_id
|
||||
if (route.query.mp_id) {
|
||||
selectedMpId.value = route.query.mp_id as string
|
||||
}
|
||||
fetchRuleList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-spin :loading="loading">
|
||||
<div class="filter-rule-list">
|
||||
<div class="header">
|
||||
<h2>HTML过滤规则</h2>
|
||||
<a-select
|
||||
v-model="selectedMpId"
|
||||
placeholder="选择公众号筛选"
|
||||
allow-clear
|
||||
style="width: 200px; margin-right: 10px;"
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-option value="">全部公众号</a-option>
|
||||
<a-option
|
||||
v-for="mp in mpList"
|
||||
:key="mp.id"
|
||||
:value="mp.id"
|
||||
:label="mp.mp_name"
|
||||
/>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><icon-plus /></template>
|
||||
添加规则
|
||||
</a-button>
|
||||
</div>
|
||||
<a-alert type="info" closable style="margin-bottom: 16px">
|
||||
过滤规则用于在采集文章后自动清理HTML内容,可移除广告、无关元素等。每个公众号可配置多条规则,按优先级依次执行。
|
||||
</a-alert>
|
||||
|
||||
<div v-if="!isMobile" class="table-wrapper">
|
||||
<a-table
|
||||
:data="ruleList"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
@page-change="handlePageChange"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="公众号" :width="200">
|
||||
<template #cell="{ record }">
|
||||
<div class="mp-info">
|
||||
<a-avatar :size="24" :style="{ backgroundColor: '#3370ff' }">
|
||||
{{ getFirstMpName(record.mp_ids) }}
|
||||
</a-avatar>
|
||||
<a-tooltip :content="getMpNames(record.mp_ids)">
|
||||
<span class="mp-name">{{ getMpNames(record.mp_ids) }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="规则名称" data-index="rule_name" :width="180" />
|
||||
<a-table-column title="过滤配置" ellipsis>
|
||||
<template #cell="{ record }">
|
||||
<a-tooltip :content="previewRule(record)">
|
||||
<span class="rule-preview">{{ previewRule(record) }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="优先级" data-index="priority" :width="80" align="center" />
|
||||
<a-table-column title="状态" :width="100" align="center">
|
||||
<template #cell="{ record }">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
{{ record.status === 1 ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" :width="200" align="center">
|
||||
<template #cell="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" type="primary" @click="handleEdit(record.id)">编辑</a-button>
|
||||
<a-button
|
||||
size="mini"
|
||||
:type="record.status === 1 ? 'outline' : 'primary'"
|
||||
:status="record.status === 1 ? 'warning' : 'success'"
|
||||
@click="handleToggleStatus(record)"
|
||||
>
|
||||
{{ record.status === 1 ? '禁用' : '启用' }}
|
||||
</a-button>
|
||||
<a-button size="mini" status="danger" @click="handleDelete(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<a-list v-else :bordered="false">
|
||||
<a-list-item v-for="item in ruleList" :key="item.id">
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<div class="mobile-title">
|
||||
<span>{{ item.rule_name }}</span>
|
||||
<a-tag :color="item.status === 1 ? 'green' : 'red'" size="small">
|
||||
{{ item.status === 1 ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="mobile-desc">
|
||||
<div>公众号: {{ getMpNames(item.mp_ids) }}</div>
|
||||
<div class="rule-preview">{{ previewRule(item) }}</div>
|
||||
<div>优先级: {{ item.priority }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a-space direction="vertical" size="mini">
|
||||
<a-button size="mini" type="primary" long @click="handleEdit(item.id)">编辑</a-button>
|
||||
<a-button
|
||||
size="mini"
|
||||
:type="item.status === 1 ? 'outline' : 'primary'"
|
||||
:status="item.status === 1 ? 'warning' : 'success'"
|
||||
long
|
||||
@click="handleToggleStatus(item)"
|
||||
>
|
||||
{{ item.status === 1 ? '禁用' : '启用' }}
|
||||
</a-button>
|
||||
<a-button size="mini" status="danger" long @click="handleDelete(item.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-list-item>
|
||||
<template #footer>
|
||||
<div v-if="ruleList.length < pagination.total" class="load-more">
|
||||
<a-button long @click="handleLoadMore" :loading="loading">加载更多</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-rule-list {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header .arco-btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-wrapper :deep(.arco-table-container) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mp-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mp-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rule-preview {
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mobile-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mobile-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.mobile-desc div {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
width: 120px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-rule-list {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,6 +18,13 @@
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="editMp(record)">编辑</a-button>
|
||||
<a-button
|
||||
size="mini"
|
||||
type="outline"
|
||||
@click="goToFilterRules(record.id)"
|
||||
>
|
||||
规则
|
||||
</a-button>
|
||||
<a-button
|
||||
size="mini"
|
||||
status="danger"
|
||||
@@ -63,8 +70,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getSubscriptions, addSubscription, updateSubscription, deleteSubscription } from '@/api/subscription'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const headers = { Authorization: `Bearer ${getToken()}` }
|
||||
|
||||
@@ -145,6 +156,10 @@ const deleteMp = async (id) => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const goToFilterRules = (mpId: string) => {
|
||||
router.push({ path: '/filter-rules', query: { mp_id: mpId } })
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
pagination.current = page
|
||||
loadData()
|
||||
|
||||
Reference in New Issue
Block a user