前端界面玩抽
This commit is contained in:
parent
e7fa38cf33
commit
e4287c6605
@ -188,7 +188,7 @@ class CrawlEngine:
|
|||||||
session.add(domain_model)
|
session.add(domain_model)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
self.ev.wait(60)
|
self.ev.wait(10)
|
||||||
|
|
||||||
logger.info("crawl worker stop!")
|
logger.info("crawl worker stop!")
|
||||||
|
|
||||||
|
|||||||
@ -90,7 +90,20 @@ class EvidenceEngine:
|
|||||||
|
|
||||||
# Part1 获取证据截图
|
# Part1 获取证据截图
|
||||||
logger.debug(f"开始获取 {surl} 在百度搜索中的截图")
|
logger.debug(f"开始获取 {surl} 在百度搜索中的截图")
|
||||||
img_path, tab = self.get_screenshot(target)
|
img_path, tab, has_result = self.get_screenshot(target)
|
||||||
|
if not has_result:
|
||||||
|
# 如果没有搜到结果,直接把 has_evidence 标记为 true 就行了
|
||||||
|
with Session(self.database) as session:
|
||||||
|
stmt = select(ReportUrlModel).where(ReportUrlModel.id == target["id"])
|
||||||
|
model: ReportUrlModel = session.exec(stmt).first()
|
||||||
|
if not model:
|
||||||
|
logger.error(f"{target['id']} 记录不存在,跳过...")
|
||||||
|
return None
|
||||||
|
# 更新数据
|
||||||
|
model.has_evidence = True
|
||||||
|
session.add(model)
|
||||||
|
session.commit()
|
||||||
|
return None
|
||||||
if not img_path:
|
if not img_path:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -137,7 +150,7 @@ class EvidenceEngine:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取证据截图和举报链接失败: {e}")
|
logger.error(f"获取证据截图和举报链接失败: {e}")
|
||||||
|
|
||||||
def get_screenshot(self, target: dict) -> tuple[str | None, MixTab]:
|
def get_screenshot(self, target: dict) -> tuple[str | None, MixTab, bool]:
|
||||||
"""获取搜索页面的截图,返回 img_path """
|
"""获取搜索页面的截图,返回 img_path """
|
||||||
search_keyword = target["surl"].lstrip("https://").lstrip("http://")
|
search_keyword = target["surl"].lstrip("https://").lstrip("http://")
|
||||||
tab = self.dp_engine.browser.new_tab()
|
tab = self.dp_engine.browser.new_tab()
|
||||||
@ -147,12 +160,13 @@ class EvidenceEngine:
|
|||||||
|
|
||||||
if "未找到相关结果" in tab.html:
|
if "未找到相关结果" in tab.html:
|
||||||
logger.info(f"没有关于 {search_keyword} 的数据")
|
logger.info(f"没有关于 {search_keyword} 的数据")
|
||||||
return None, tab
|
return None, tab, False
|
||||||
|
|
||||||
# 图片的存储路径
|
# 图片的存储路径
|
||||||
# 截完图先不要关闭 tab,别的地方还要用
|
# 截完图先不要关闭 tab,别的地方还要用
|
||||||
img_path = f"./imgs/{target['domain']}/{md5(target['surl'])}.png"
|
img_path = f"./imgs/{target['domain']}/{md5(target['surl'])}.png"
|
||||||
return self.do_screenshot(tab, img_path)
|
img_path, tab = self.do_screenshot(tab, img_path)
|
||||||
|
return img_path, tab, True
|
||||||
|
|
||||||
def get_wap_screenshot(self, target: dict) -> tuple[str | None, MixTab]:
|
def get_wap_screenshot(self, target: dict) -> tuple[str | None, MixTab]:
|
||||||
"""用 wap dp 再截一张 surl 本身的图"""
|
"""用 wap dp 再截一张 surl 本身的图"""
|
||||||
|
|||||||
@ -47,6 +47,18 @@ class Reporter:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
|
||||||
|
for mode in self.mode:
|
||||||
|
if mode == "pc":
|
||||||
|
self.reporters["pc"].stop()
|
||||||
|
elif mode == "wap":
|
||||||
|
self.reporters["wap"].stop()
|
||||||
|
elif mode == "site":
|
||||||
|
self.reporters["site"].stop()
|
||||||
|
else:
|
||||||
|
logger.error(f"参数错误: {mode}")
|
||||||
|
continue
|
||||||
|
|
||||||
self.status = 0
|
self.status = 0
|
||||||
self.ev.set()
|
self.ev.set()
|
||||||
|
|
||||||
|
|||||||
@ -51,6 +51,7 @@ class PcReporter(BaseReporter):
|
|||||||
def stop(self):
|
def stop(self):
|
||||||
self.status = 0
|
self.status = 0
|
||||||
self.ev.set()
|
self.ev.set()
|
||||||
|
logger.warning(f"{self.engine_name} 收到退出消息,等待当前任务完成后退出")
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
with Session(self.database) as session:
|
with Session(self.database) as session:
|
||||||
@ -214,20 +215,26 @@ class PcReporter(BaseReporter):
|
|||||||
# 获取 as、tk 值
|
# 获取 as、tk 值
|
||||||
try:
|
try:
|
||||||
get_as_tk = self.post_init(surl, token, title, q, timestamp_s)
|
get_as_tk = self.post_init(surl, token, title, q, timestamp_s)
|
||||||
|
# logger.debug(f"{get_as_tk=}")
|
||||||
get_as = get_as_tk['as']
|
get_as = get_as_tk['as']
|
||||||
get_tk = get_as_tk['tk']
|
get_tk = get_as_tk['tk']
|
||||||
|
|
||||||
# 获取验证码图片下载链接、backstr
|
# 获取验证码图片下载链接、backstr
|
||||||
get_style_result = self.get_style(get_tk, surl, token, title, q, timestamp_s)
|
get_style_result = self.get_style(get_tk, surl, token, title, q, timestamp_s)
|
||||||
|
# logger.debug(f"{get_style_result=}")
|
||||||
get_backstr = get_style_result['backstr']
|
get_backstr = get_style_result['backstr']
|
||||||
pic_download_link = get_style_result['captcha']
|
pic_download_link = get_style_result['captcha']
|
||||||
|
|
||||||
# 下载验证码图片
|
# 下载验证码图片
|
||||||
self.download_captcha(pic_download_link)
|
self.download_captcha(pic_download_link)
|
||||||
rotate_angle_rate = self.get_rotate_angle_rate()
|
rotate_angle_rate = self.get_rotate_angle_rate()
|
||||||
|
logger.debug(f"{rotate_angle_rate=}")
|
||||||
# key = self.get_key(get_as)
|
# key = self.get_key(get_as)
|
||||||
|
if not rotate_angle_rate:
|
||||||
|
return {'op': 3}
|
||||||
|
|
||||||
get_ds_tk = self.post_log(get_as, get_tk, get_backstr, rotate_angle_rate)
|
get_ds_tk = self.post_log(get_as, get_tk, get_backstr, rotate_angle_rate)
|
||||||
|
logger.debug(f"{get_ds_tk=}")
|
||||||
log_ds = get_ds_tk['ds']
|
log_ds = get_ds_tk['ds']
|
||||||
log_tk = get_ds_tk['tk']
|
log_tk = get_ds_tk['tk']
|
||||||
log_op = get_ds_tk['op']
|
log_op = get_ds_tk['op']
|
||||||
@ -238,7 +245,7 @@ class PcReporter(BaseReporter):
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'{e}')
|
logger.exception(f'{e}')
|
||||||
return {'op': 3}
|
return {'op': 3}
|
||||||
|
|
||||||
def post_init(self, surl, token, title, q, timestamp_s):
|
def post_init(self, surl, token, title, q, timestamp_s):
|
||||||
@ -316,9 +323,13 @@ class PcReporter(BaseReporter):
|
|||||||
with open('./captcha/captcha.png', 'rb') as p:
|
with open('./captcha/captcha.png', 'rb') as p:
|
||||||
picture = p.read()
|
picture = p.read()
|
||||||
slide_distance = identify_distance.rotate(image=picture)
|
slide_distance = identify_distance.rotate(image=picture)
|
||||||
|
logger.debug(f"{slide_distance=}")
|
||||||
|
if not slide_distance:
|
||||||
|
return None
|
||||||
# 旋转角度为
|
# 旋转角度为
|
||||||
# logger.info('rotate angle: ' + str(slide_distance))
|
# logger.info('rotate angle: ' + str(slide_distance))
|
||||||
rotate_angle_rate = round(slide_distance / 360, 2)
|
rotate_angle_rate = round(slide_distance / 360, 2)
|
||||||
|
logger.debug(f"{rotate_angle_rate=}")
|
||||||
# logger.info('rotate angle rate: ' + str(rotate_angle_rate))
|
# logger.info('rotate angle rate: ' + str(rotate_angle_rate))
|
||||||
return rotate_angle_rate
|
return rotate_angle_rate
|
||||||
|
|
||||||
|
|||||||
@ -46,8 +46,10 @@ class SiteReporter(BaseReporter):
|
|||||||
self.token_pattern = r'name="submit_token" value="(.*?)"'
|
self.token_pattern = r'name="submit_token" value="(.*?)"'
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
# logger.debug(f"{self.engine_name} stop called.")
|
||||||
self.status = 0
|
self.status = 0
|
||||||
self.ev.set()
|
self.ev.set()
|
||||||
|
logger.warning(f"{self.engine_name} 收到退出消息,等待当前任务完成后退出")
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""实现 PC 端的举报逻辑"""
|
"""实现 PC 端的举报逻辑"""
|
||||||
|
|||||||
@ -46,6 +46,7 @@ class WapReporter(BaseReporter):
|
|||||||
def stop(self):
|
def stop(self):
|
||||||
self.status = 0
|
self.status = 0
|
||||||
self.ev.set()
|
self.ev.set()
|
||||||
|
logger.warning(f"{self.engine_name} 收到退出消息,等待当前任务完成后退出")
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""实现 WAP 端的举报逻辑"""
|
"""实现 WAP 端的举报逻辑"""
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import base64
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
class YdmVerify(object):
|
class YdmVerify(object):
|
||||||
_custom_url = "https://www.jfbym.com/api/YmServer/customApi"
|
_custom_url = "https://www.jfbym.com/api/YmServer/customApi"
|
||||||
@ -17,4 +19,11 @@ class YdmVerify(object):
|
|||||||
"type": "90009"
|
"type": "90009"
|
||||||
}
|
}
|
||||||
resp = requests.post(self._custom_url, headers=self._headers, data=json.dumps(payload))
|
resp = requests.post(self._custom_url, headers=self._headers, data=json.dumps(payload))
|
||||||
|
logger.debug(f"{resp.json()=}")
|
||||||
|
|
||||||
|
response_data = resp.json()
|
||||||
|
if response_data.get("code") == 10002:
|
||||||
|
logger.error(f'{response_data.get("msg")}')
|
||||||
|
return None
|
||||||
|
|
||||||
return resp.json()['data']['data']
|
return resp.json()['data']['data']
|
||||||
@ -1,6 +1,7 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Query
|
from fastapi import APIRouter, Query
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from app.web.request.report_request import AddUrlsRequest, CollectEvidenceRequest, ReportRequest, GetUrlListRequest
|
from app.web.request.report_request import AddUrlsRequest, CollectEvidenceRequest, ReportRequest, GetUrlListRequest
|
||||||
from app.web.service.domain_service import DomainService
|
from app.web.service.domain_service import DomainService
|
||||||
@ -10,8 +11,11 @@ router = APIRouter(prefix="/api/urls", tags=["URL管理"])
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/v1/list")
|
@router.get("/v1/list")
|
||||||
async def get_all_urls(request: Annotated[GetUrlListRequest, Query()]):
|
def get_all_urls(request: Annotated[GetUrlListRequest, Query()]):
|
||||||
"""获取所有的URL,支持根据域名、状态进行过滤,不传则返回全部数据,支持分页"""
|
"""获取所有的URL,支持根据域名、状态进行过滤,不传则返回全部数据,支持分页"""
|
||||||
|
|
||||||
|
logger.debug(f"{request=}")
|
||||||
|
|
||||||
return ReportURLService.get_list(
|
return ReportURLService.get_list(
|
||||||
request.domain,
|
request.domain,
|
||||||
request.surl,
|
request.surl,
|
||||||
@ -25,7 +29,7 @@ async def get_all_urls(request: Annotated[GetUrlListRequest, Query()]):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/v1/add")
|
@router.post("/v1/add")
|
||||||
async def add_urls(request: AddUrlsRequest):
|
def add_urls(request: AddUrlsRequest):
|
||||||
"""
|
"""
|
||||||
手动添加 URL 到域名中,支持批量添加
|
手动添加 URL 到域名中,支持批量添加
|
||||||
格式 [
|
格式 [
|
||||||
@ -58,7 +62,7 @@ async def add_urls(request: AddUrlsRequest):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/v1/evidence")
|
@router.post("/v1/evidence")
|
||||||
async def collect_evidence(request: CollectEvidenceRequest):
|
def collect_evidence(request: CollectEvidenceRequest):
|
||||||
"""
|
"""
|
||||||
强制手动触发证据收集任务,支持批量传入,已经收集过的 URL 也要强制收集
|
强制手动触发证据收集任务,支持批量传入,已经收集过的 URL 也要强制收集
|
||||||
TODO:本来应该需要使用任务队列的,为了简单先把数据库的相关标记改为 0 ,也能达到一样的效果
|
TODO:本来应该需要使用任务队列的,为了简单先把数据库的相关标记改为 0 ,也能达到一样的效果
|
||||||
@ -68,10 +72,11 @@ async def collect_evidence(request: CollectEvidenceRequest):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/v1/report")
|
@router.post("/v1/report")
|
||||||
async def report(request: ReportRequest):
|
def report(request: ReportRequest):
|
||||||
"""举报指定的URL,支持批量传入 id 批量举报
|
"""举报指定的URL,支持批量传入 id 批量举报
|
||||||
先通过改数据库,然后等引擎自己调度实现
|
先通过改数据库,然后等引擎自己调度实现
|
||||||
"""
|
"""
|
||||||
|
logger.debug(f"{request=}")
|
||||||
return ReportURLService.batch_update_report_flag(
|
return ReportURLService.batch_update_report_flag(
|
||||||
request.ids,
|
request.ids,
|
||||||
request.report_by_one,
|
request.report_by_one,
|
||||||
|
|||||||
@ -6,10 +6,10 @@ from pydantic import BaseModel, Field
|
|||||||
class GetUrlListRequest(BaseModel):
|
class GetUrlListRequest(BaseModel):
|
||||||
domain: str = ""
|
domain: str = ""
|
||||||
surl: str = ""
|
surl: str = ""
|
||||||
is_report_by_one: Optional[bool] = False
|
is_report_by_one: Optional[int] = 2
|
||||||
is_report_by_site: Optional[bool] = False
|
is_report_by_site: Optional[int] = 2
|
||||||
is_report_by_wap: Optional[bool] = False
|
is_report_by_wap: Optional[int] = 2
|
||||||
has_evidence: Optional[bool] = False
|
has_evidence: Optional[int] = 2
|
||||||
|
|
||||||
page: int = Field(default=1, gt=0)
|
page: int = Field(default=1, gt=0)
|
||||||
size: int = Field(default=50, gt=0)
|
size: int = Field(default=50, gt=0)
|
||||||
|
|||||||
@ -15,8 +15,9 @@ class ReportURLService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_list(
|
def get_list(
|
||||||
cls, domain: str, surl: str, is_report_by_one: Optional[bool], is_report_by_site: Optional[bool],
|
cls, domain: str, surl: str, is_report_by_one: Optional[int], is_report_by_site: Optional[int],
|
||||||
is_report_by_wap: Optional[bool], has_evidence: Optional[bool], page: int, size: int):
|
is_report_by_wap: Optional[int], has_evidence: Optional[int], page: int, size: int
|
||||||
|
):
|
||||||
|
|
||||||
with Session(AppCtx.g_db_engine) as session:
|
with Session(AppCtx.g_db_engine) as session:
|
||||||
stmt = select(ReportUrlModel)
|
stmt = select(ReportUrlModel)
|
||||||
@ -27,22 +28,22 @@ class ReportURLService:
|
|||||||
if surl:
|
if surl:
|
||||||
stmt = stmt.where(ReportUrlModel.surl.like(f"%{surl}%"))
|
stmt = stmt.where(ReportUrlModel.surl.like(f"%{surl}%"))
|
||||||
total_stmt = total_stmt.where(ReportUrlModel.surl.like(f"%{surl}%"))
|
total_stmt = total_stmt.where(ReportUrlModel.surl.like(f"%{surl}%"))
|
||||||
if is_report_by_one is not None:
|
if is_report_by_one and is_report_by_one != 2:
|
||||||
stmt = stmt.where(ReportUrlModel.is_report_by_one == is_report_by_one)
|
stmt = stmt.where(ReportUrlModel.is_report_by_one == is_report_by_one)
|
||||||
total_stmt = total_stmt.where(ReportUrlModel.is_report_by_one == is_report_by_one)
|
total_stmt = total_stmt.where(ReportUrlModel.is_report_by_one == is_report_by_one)
|
||||||
if is_report_by_site is not None:
|
if is_report_by_site and is_report_by_site != 2:
|
||||||
stmt = stmt.where(ReportUrlModel.is_report_by_site == is_report_by_site)
|
stmt = stmt.where(ReportUrlModel.is_report_by_site == is_report_by_site)
|
||||||
total_stmt = total_stmt.where(ReportUrlModel.is_report_by_site == is_report_by_site)
|
total_stmt = total_stmt.where(ReportUrlModel.is_report_by_site == is_report_by_site)
|
||||||
if is_report_by_wap is not None:
|
if is_report_by_wap and is_report_by_wap != 2:
|
||||||
stmt = stmt.where(ReportUrlModel.is_report_by_wap == is_report_by_wap)
|
stmt = stmt.where(ReportUrlModel.is_report_by_wap == is_report_by_wap)
|
||||||
total_stmt = total_stmt.where(ReportUrlModel.is_report_by_wap == is_report_by_wap)
|
total_stmt = total_stmt.where(ReportUrlModel.is_report_by_wap == is_report_by_wap)
|
||||||
if has_evidence is not None:
|
if has_evidence and has_evidence != 2:
|
||||||
stmt = stmt.where(ReportUrlModel.has_evidence == has_evidence)
|
stmt = stmt.where(ReportUrlModel.has_evidence == has_evidence)
|
||||||
total_stmt = total_stmt.where(ReportUrlModel.has_evidence == has_evidence)
|
total_stmt = total_stmt.where(ReportUrlModel.has_evidence == has_evidence)
|
||||||
|
|
||||||
# 设置分页
|
# 设置分页
|
||||||
stmt = stmt.offset((page - 1) * size).limit(size)
|
stmt = stmt.offset((page - 1) * size).limit(size)
|
||||||
|
# logger.debug(f"{str(stmt)=}")
|
||||||
try:
|
try:
|
||||||
total = session.exec(total_stmt).first()
|
total = session.exec(total_stmt).first()
|
||||||
urls = session.exec(stmt).all()
|
urls = session.exec(stmt).all()
|
||||||
@ -103,10 +104,12 @@ class ReportURLService:
|
|||||||
stmt = update(ReportUrlModel).where(ReportUrlModel.id.in_(ids))
|
stmt = update(ReportUrlModel).where(ReportUrlModel.id.in_(ids))
|
||||||
if report_by_wap:
|
if report_by_wap:
|
||||||
stmt = stmt.values(is_report_by_wap=False)
|
stmt = stmt.values(is_report_by_wap=False)
|
||||||
elif report_by_site:
|
if report_by_site:
|
||||||
stmt = stmt.values(is_report_by_site=False)
|
stmt = stmt.values(is_report_by_site=False)
|
||||||
elif report_by_one:
|
if report_by_one:
|
||||||
stmt = stmt.values(is_report_by_one=False)
|
stmt = stmt.values(is_report_by_one=False)
|
||||||
|
|
||||||
|
logger.debug(f"{str(stmt)=}")
|
||||||
session.exec(stmt)
|
session.exec(stmt)
|
||||||
session.commit()
|
session.commit()
|
||||||
return ApiResult.ok(len(ids))
|
return ApiResult.ok(len(ids))
|
||||||
|
|||||||
7
fe/components.d.ts
vendored
7
fe/components.d.ts
vendored
@ -12,16 +12,23 @@ declare module 'vue' {
|
|||||||
EditDomainDialog: typeof import('./src/components/EditDomainDialog.vue')['default']
|
EditDomainDialog: typeof import('./src/components/EditDomainDialog.vue')['default']
|
||||||
ImportDomainDialog: typeof import('./src/components/ImportDomainDialog.vue')['default']
|
ImportDomainDialog: typeof import('./src/components/ImportDomainDialog.vue')['default']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
NForm: typeof import('naive-ui')['NForm']
|
NForm: typeof import('naive-ui')['NForm']
|
||||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||||
|
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
||||||
|
NGrid: typeof import('naive-ui')['NGrid']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||||
NModal: typeof import('naive-ui')['NModal']
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
NPagination: typeof import('naive-ui')['NPagination']
|
NPagination: typeof import('naive-ui')['NPagination']
|
||||||
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
NTag: typeof import('naive-ui')['NTag']
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { RouterLink, RouterView } from 'vue-router'
|
import { RouterLink, RouterView } from 'vue-router'
|
||||||
import { NLayout, NMessageProvider, NConfigProvider, NLayoutHeader, NLayoutSider, NMenu } from 'naive-ui'
|
import {
|
||||||
|
NLayout,
|
||||||
|
NMessageProvider,
|
||||||
|
NConfigProvider,
|
||||||
|
NLayoutHeader,
|
||||||
|
NLayoutSider,
|
||||||
|
NMenu,
|
||||||
|
} from 'naive-ui'
|
||||||
import { type MenuOption } from 'naive-ui'
|
import { type MenuOption } from 'naive-ui'
|
||||||
import { List, ColorWand } from '@vicons/ionicons5'
|
import { List, ColorWand } from '@vicons/ionicons5'
|
||||||
|
|
||||||
@ -30,14 +37,26 @@ const menuOpts: MenuOption[] = [
|
|||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
<n-layout class="h-screen">
|
<n-layout class="h-screen">
|
||||||
<!-- header -->
|
<!-- header -->
|
||||||
<n-layout-header class="h-16 p-5" style="background-color: oklch(62.3% 0.214 259.815); color: white" bordered>
|
<n-layout-header
|
||||||
|
class="h-16 p-5"
|
||||||
|
style="background-color: oklch(62.3% 0.214 259.815); color: white"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
<span class="font-bold text-xl">BAIDU Reporter</span>
|
<span class="font-bold text-xl">BAIDU Reporter</span>
|
||||||
</n-layout-header>
|
</n-layout-header>
|
||||||
|
|
||||||
<n-layout position="absolute" has-sider style="top: 64px">
|
<n-layout position="absolute" has-sider style="top: 64px">
|
||||||
<!-- sidebar -->
|
<!-- sidebar -->
|
||||||
<n-layout-sider width="8%" show-trigger show-collapsed-content :collapsed-width="64"
|
<n-layout-sider
|
||||||
content-style="padding: 8px; text-align:center;" :native-scrollbar="false" bordered collapse-mode="width">
|
width="8%"
|
||||||
|
show-trigger
|
||||||
|
show-collapsed-content
|
||||||
|
:collapsed-width="64"
|
||||||
|
content-style="padding: 8px; text-align:center;"
|
||||||
|
:native-scrollbar="false"
|
||||||
|
bordered
|
||||||
|
collapse-mode="width"
|
||||||
|
>
|
||||||
<n-menu :indent="24" :options="menuOpts" @update:value="handleUpdateValue" />
|
<n-menu :indent="24" :options="menuOpts" @update:value="handleUpdateValue" />
|
||||||
</n-layout-sider>
|
</n-layout-sider>
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, inject } from 'vue'
|
import { ref, inject } from 'vue'
|
||||||
import { NModal, NForm, NFormItem, NInputNumber, NCheckbox, NInput, NButton, NButtonGroup, useMessage } from 'naive-ui'
|
import {
|
||||||
|
NModal,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NInputNumber,
|
||||||
|
NCheckbox,
|
||||||
|
NInput,
|
||||||
|
NButton,
|
||||||
|
NButtonGroup,
|
||||||
|
useMessage,
|
||||||
|
} from 'naive-ui'
|
||||||
import type { FormRules } from 'naive-ui'
|
import type { FormRules } from 'naive-ui'
|
||||||
import type { AxiosInstance } from 'axios'
|
import type { AxiosInstance } from 'axios'
|
||||||
|
|
||||||
const model = defineModel<boolean>("show", { required: true })
|
const model = defineModel<boolean>('show', { required: true })
|
||||||
const emit = defineEmits(['success'])
|
const emit = defineEmits(['success'])
|
||||||
|
|
||||||
const axios = inject("axios") as AxiosInstance
|
const axios = inject('axios') as AxiosInstance
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const interval = ref<number>(1440) // 默认1天(1440分钟)
|
const interval = ref<number>(1440) // 默认1天(1440分钟)
|
||||||
@ -19,34 +29,37 @@ const rules: FormRules = {
|
|||||||
interval: [
|
interval: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
type: "number",
|
type: 'number',
|
||||||
message: '请输入采集间隔',
|
message: '请输入采集间隔',
|
||||||
trigger: ['blur', 'change']
|
trigger: ['blur', 'change'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'number',
|
type: 'number',
|
||||||
min: 1,
|
min: 1,
|
||||||
message: '采集间隔必须大于0',
|
message: '采集间隔必须大于0',
|
||||||
trigger: ['blur', 'change'],
|
trigger: ['blur', 'change'],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
domains: [
|
domains: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: '请输入域名',
|
message: '请输入域名',
|
||||||
trigger: ['blur', 'change']
|
trigger: ['blur', 'change'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
validator: (_, value: string) => {
|
validator: (_, value: string) => {
|
||||||
if (!value.trim()) return true // 空值由 required 规则处理
|
if (!value.trim()) return true // 空值由 required 规则处理
|
||||||
const domainList = value.split(/[\n,]/).map(d => d.trim()).filter(d => d)
|
const domainList = value
|
||||||
|
.split(/[\n,]/)
|
||||||
|
.map((d) => d.trim())
|
||||||
|
.filter((d) => d)
|
||||||
if (domainList.length === 0) return false
|
if (domainList.length === 0) return false
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
message: '域名格式不正确',
|
message: '域名格式不正确',
|
||||||
trigger: ['blur', 'change']
|
trigger: ['blur', 'change'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
@ -60,13 +73,13 @@ const handleConfirm = async () => {
|
|||||||
// 处理域名输入,支持换行和逗号分隔
|
// 处理域名输入,支持换行和逗号分隔
|
||||||
const domainList = domains.value
|
const domainList = domains.value
|
||||||
.split(/[\n,]/) // 同时支持换行和逗号分隔
|
.split(/[\n,]/) // 同时支持换行和逗号分隔
|
||||||
.map(domain => domain.trim()) // 去除每个域名的前后空格
|
.map((domain) => domain.trim()) // 去除每个域名的前后空格
|
||||||
.filter(domain => domain) // 过滤空字符串
|
.filter((domain) => domain) // 过滤空字符串
|
||||||
|
|
||||||
const response = await axios.post('/api/domain/v1/add', {
|
const response = await axios.post('/api/domain/v1/add', {
|
||||||
domains: domainList,
|
domains: domainList,
|
||||||
crawl_interval: interval.value,
|
crawl_interval: interval.value,
|
||||||
crawl_now: startImmediately.value
|
crawl_now: startImmediately.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.data.code === 20000) {
|
if (response.data.code === 20000) {
|
||||||
@ -93,20 +106,29 @@ const handleClose = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-modal v-model:show="model" preset="card" title="手动添加" :mask-closable="false" style="width: 600px">
|
<n-modal
|
||||||
|
v-model:show="model"
|
||||||
|
preset="card"
|
||||||
|
title="手动添加"
|
||||||
|
:mask-closable="false"
|
||||||
|
style="width: 600px"
|
||||||
|
>
|
||||||
<n-form size="small" ref="formRef" :model="{ interval, domains }" :rules="rules">
|
<n-form size="small" ref="formRef" :model="{ interval, domains }" :rules="rules">
|
||||||
<n-form-item path="interval" label="采集间隔(分钟)">
|
<n-form-item path="interval" label="采集间隔(分钟)">
|
||||||
<n-input-number v-model:value="interval" :min="1" />
|
<n-input-number v-model:value="interval" :min="1" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item path="domains" label="域名列表">
|
<n-form-item path="domains" label="域名列表">
|
||||||
<n-input v-model:value="domains" type="textarea" :rows="10" placeholder="请输入域名,支持换行或英文逗号分隔" />
|
<n-input
|
||||||
|
v-model:value="domains"
|
||||||
|
type="textarea"
|
||||||
|
:rows="10"
|
||||||
|
placeholder="请输入域名,支持换行或英文逗号分隔"
|
||||||
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item label="采集选项">
|
<n-form-item label="采集选项">
|
||||||
<n-checkbox v-model:checked="startImmediately">
|
<n-checkbox v-model:checked="startImmediately"> 立即开始采集 </n-checkbox>
|
||||||
立即开始采集
|
|
||||||
</n-checkbox>
|
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<template #action>
|
<template #action>
|
||||||
|
|||||||
@ -1,88 +1,96 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, defineProps, defineEmits, inject, defineModel, computed, watch } from 'vue';
|
import { ref, defineProps, defineEmits, inject, defineModel, computed, watch } from 'vue'
|
||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui'
|
||||||
import type { AxiosInstance } from 'axios';
|
import type { AxiosInstance } from 'axios'
|
||||||
import type { DataTableRowKey } from 'naive-ui';
|
import type { DataTableRowKey } from 'naive-ui'
|
||||||
|
|
||||||
const show = defineModel<boolean>('show');
|
const show = defineModel<boolean>('show')
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
// 要修改的域名 ID 列表
|
// 要修改的域名 ID 列表
|
||||||
domainIds: DataTableRowKey[] | null;
|
domainIds: DataTableRowKey[] | null
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'success'): void;
|
(e: 'success'): void
|
||||||
(e: 'close'): void;
|
(e: 'close'): void
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const axios = inject('axios') as AxiosInstance;
|
const axios = inject('axios') as AxiosInstance
|
||||||
const message = useMessage();
|
const message = useMessage()
|
||||||
|
|
||||||
// 采集间隔需要用户输入
|
// 采集间隔需要用户输入
|
||||||
const crawlInterval = ref<number | null>(null);
|
const crawlInterval = ref<number | null>(null)
|
||||||
const loading = ref(false);
|
const loading = ref(false)
|
||||||
|
|
||||||
// 动态计算弹窗标题
|
// 动态计算弹窗标题
|
||||||
const dialogTitle = computed(() => {
|
const dialogTitle = computed(() => {
|
||||||
const count = props.domainIds?.length || 0;
|
const count = props.domainIds?.length || 0
|
||||||
if (count > 1) {
|
if (count > 1) {
|
||||||
return `批量修改 ${count} 个域名的采集间隔`;
|
return `批量修改 ${count} 个域名的采集间隔`
|
||||||
} else if (count === 1) {
|
} else if (count === 1) {
|
||||||
return '修改域名采集间隔';
|
return '修改域名采集间隔'
|
||||||
}
|
}
|
||||||
return '修改采集间隔'; // 应该不会出现,但作为备用
|
return '修改采集间隔' // 应该不会出现,但作为备用
|
||||||
});
|
})
|
||||||
|
|
||||||
// 弹窗显示时,重置 crawlInterval
|
// 弹窗显示时,重置 crawlInterval
|
||||||
watch(show, (newShow) => {
|
watch(show, (newShow) => {
|
||||||
if (newShow) {
|
if (newShow) {
|
||||||
crawlInterval.value = null; // 每次打开都清空,强制用户输入
|
crawlInterval.value = null // 每次打开都清空,强制用户输入
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (crawlInterval.value === null || crawlInterval.value < 1) {
|
if (crawlInterval.value === null || crawlInterval.value < 1) {
|
||||||
message.error('请输入有效的采集间隔(大于等于1的整数)');
|
message.error('请输入有效的采集间隔(大于等于1的整数)')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.domainIds || props.domainIds.length === 0) {
|
if (!props.domainIds || props.domainIds.length === 0) {
|
||||||
message.error('没有指定要修改的域名');
|
message.error('没有指定要修改的域名')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true
|
||||||
|
|
||||||
const response = (await axios.post('/api/domain/v1/update', {
|
const response = (
|
||||||
|
await axios.post('/api/domain/v1/update', {
|
||||||
domain_ids: props.domainIds, // 直接使用传入的 ID
|
domain_ids: props.domainIds, // 直接使用传入的 ID
|
||||||
crawl_interval: crawlInterval.value
|
crawl_interval: crawlInterval.value,
|
||||||
})).data;
|
})
|
||||||
|
).data
|
||||||
|
|
||||||
if (response.code !== 20000) {
|
if (response.code !== 20000) {
|
||||||
message.error(`更新失败:${response.message}`);
|
message.error(`更新失败:${response.message}`)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success('更新成功');
|
message.success('更新成功')
|
||||||
emit('success');
|
emit('success')
|
||||||
show.value = false; // 关闭弹窗
|
show.value = false // 关闭弹窗
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新失败', error);
|
console.error('更新失败', error)
|
||||||
message.error(`更新失败:${error}`);
|
message.error(`更新失败:${error}`)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
show.value = false;
|
show.value = false
|
||||||
emit('close'); // 触发 close 事件
|
emit('close') // 触发 close 事件
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-modal v-model:show="show" preset="dialog" :title="dialogTitle" :loading="loading" @close="handleClose">
|
<n-modal
|
||||||
|
v-model:show="show"
|
||||||
|
preset="dialog"
|
||||||
|
:title="dialogTitle"
|
||||||
|
:loading="loading"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
<!-- 批量编辑提示 -->
|
<!-- 批量编辑提示 -->
|
||||||
<div v-if="(domainIds?.length || 0) > 1" class="mb-4 text-orange-500">
|
<div v-if="(domainIds?.length || 0) > 1" class="mb-4 text-orange-500">
|
||||||
你正在批量修改 {{ domainIds?.length }} 个域名的采集间隔。
|
你正在批量修改 {{ domainIds?.length }} 个域名的采集间隔。
|
||||||
@ -90,8 +98,13 @@ const handleClose = () => {
|
|||||||
|
|
||||||
<n-form>
|
<n-form>
|
||||||
<n-form-item label="采集间隔(分钟)" required>
|
<n-form-item label="采集间隔(分钟)" required>
|
||||||
<n-input-number v-model:value="crawlInterval" :min="1" :step="1" style="width: 100%"
|
<n-input-number
|
||||||
placeholder="请输入采集间隔" />
|
v-model:value="crawlInterval"
|
||||||
|
:min="1"
|
||||||
|
:step="1"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="请输入采集间隔"
|
||||||
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<template #action>
|
<template #action>
|
||||||
|
|||||||
@ -1,14 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, inject } from 'vue'
|
import { ref, inject } from 'vue'
|
||||||
import { NModal, NForm, NFormItem, NInputNumber, NCheckbox, NUpload, NButton, NButtonGroup, useMessage } from 'naive-ui'
|
import {
|
||||||
|
NModal,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
|
NInputNumber,
|
||||||
|
NCheckbox,
|
||||||
|
NUpload,
|
||||||
|
NButton,
|
||||||
|
NButtonGroup,
|
||||||
|
useMessage,
|
||||||
|
} from 'naive-ui'
|
||||||
import type { FormRules } from 'naive-ui'
|
import type { FormRules } from 'naive-ui'
|
||||||
import type { AxiosInstance } from 'axios'
|
import type { AxiosInstance } from 'axios'
|
||||||
import type { UploadFileInfo } from 'naive-ui'
|
import type { UploadFileInfo } from 'naive-ui'
|
||||||
|
|
||||||
const model = defineModel<boolean>("show", { required: true })
|
const model = defineModel<boolean>('show', { required: true })
|
||||||
const emit = defineEmits(['success'])
|
const emit = defineEmits(['success'])
|
||||||
|
|
||||||
const axios = inject("axios") as AxiosInstance
|
const axios = inject('axios') as AxiosInstance
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const interval = ref(1440) // 默认1天(1440分钟)
|
const interval = ref(1440) // 默认1天(1440分钟)
|
||||||
@ -19,24 +29,24 @@ const formRef = ref<InstanceType<typeof NForm> | null>(null)
|
|||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
interval: [
|
interval: [
|
||||||
{
|
{
|
||||||
type: "number",
|
type: 'number',
|
||||||
required: true,
|
required: true,
|
||||||
message: '请输入采集间隔',
|
message: '请输入采集间隔',
|
||||||
trigger: ['blur', 'change']
|
trigger: ['blur', 'change'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'number',
|
type: 'number',
|
||||||
min: 1,
|
min: 1,
|
||||||
message: '采集间隔必须大于0',
|
message: '采集间隔必须大于0',
|
||||||
trigger: ['blur', 'change']
|
trigger: ['blur', 'change'],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
fileList: [
|
fileList: [
|
||||||
{
|
{
|
||||||
type: "array",
|
type: 'array',
|
||||||
required: true,
|
required: true,
|
||||||
message: '请选择文件',
|
message: '请选择文件',
|
||||||
trigger: ['change']
|
trigger: ['change'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
validator: (_, value: UploadFileInfo[]) => {
|
validator: (_, value: UploadFileInfo[]) => {
|
||||||
@ -44,9 +54,9 @@ const rules: FormRules = {
|
|||||||
return !!value[0].file
|
return !!value[0].file
|
||||||
},
|
},
|
||||||
message: '文件无效',
|
message: '文件无效',
|
||||||
trigger: ['change']
|
trigger: ['change'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
@ -69,8 +79,8 @@ const handleConfirm = async () => {
|
|||||||
|
|
||||||
const response = await axios.post('/api/domain/v1/import', formData, {
|
const response = await axios.post('/api/domain/v1/import', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.data.code === 20000) {
|
if (response.data.code === 20000) {
|
||||||
@ -97,9 +107,21 @@ const handleClose = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-modal v-model:show="model" preset="card" title="通过文件导入" :mask-closable="false" style="width: 600px">
|
<n-modal
|
||||||
<n-form size="small" ref="formRef" :model="{ interval, fileList }" :rules="rules" label-placement="left"
|
v-model:show="model"
|
||||||
label-width="200">
|
preset="card"
|
||||||
|
title="通过文件导入"
|
||||||
|
:mask-closable="false"
|
||||||
|
style="width: 600px"
|
||||||
|
>
|
||||||
|
<n-form
|
||||||
|
size="small"
|
||||||
|
ref="formRef"
|
||||||
|
:model="{ interval, fileList }"
|
||||||
|
:rules="rules"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="200"
|
||||||
|
>
|
||||||
<n-form-item path="interval" label="采集间隔(分钟)">
|
<n-form-item path="interval" label="采集间隔(分钟)">
|
||||||
<n-input-number v-model:value="interval" :min="1" />
|
<n-input-number v-model:value="interval" :min="1" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@ -109,9 +131,7 @@ const handleClose = () => {
|
|||||||
</n-upload>
|
</n-upload>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="采集选项">
|
<n-form-item label="采集选项">
|
||||||
<n-checkbox v-model:checked="startImmediately">
|
<n-checkbox v-model:checked="startImmediately"> 立即开始采集 </n-checkbox>
|
||||||
立即开始采集
|
|
||||||
</n-checkbox>
|
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<template #action>
|
<template #action>
|
||||||
|
|||||||
@ -6,10 +6,10 @@ import {createPinia} from 'pinia'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
import axios from "axios"
|
import axios from 'axios'
|
||||||
|
|
||||||
import "vfonts/Lato.css"
|
import 'vfonts/Lato.css'
|
||||||
import "vfonts/IBMPlexMono.css"
|
import 'vfonts/IBMPlexMono.css'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ app.use(router)
|
|||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
timeout: 9000,
|
timeout: 9000,
|
||||||
timeoutErrorMessage: "E_NETWORK_TIMEOUT",
|
timeoutErrorMessage: 'E_NETWORK_TIMEOUT',
|
||||||
})
|
})
|
||||||
app.provide('axios', axiosInstance)
|
app.provide('axios', axiosInstance)
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +1,43 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { onMounted, inject, ref, computed } from "vue";
|
import { onMounted, inject, ref, computed } from 'vue'
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import type { AxiosInstance } from "axios";
|
import type { AxiosInstance } from 'axios'
|
||||||
import { type DataTableColumn, type DataTableRowKey, useMessage, useDialog } from "naive-ui";
|
import { type DataTableColumn, type DataTableRowKey, useMessage, useDialog } from 'naive-ui'
|
||||||
import ImportDomainDialog from "../components/ImportDomainDialog.vue";
|
import ImportDomainDialog from '../components/ImportDomainDialog.vue'
|
||||||
import AddDomainDialog from "../components/AddDomainDialog.vue";
|
import AddDomainDialog from '../components/AddDomainDialog.vue'
|
||||||
import EditDomainDialog from "../components/EditDomainDialog.vue";
|
import EditDomainDialog from '../components/EditDomainDialog.vue'
|
||||||
import { convertTimestampToDate } from "@/utils/common";
|
import { convertTimestampToDate } from '@/utils/common'
|
||||||
|
|
||||||
const axios = inject("axios") as AxiosInstance;
|
const axios = inject('axios') as AxiosInstance
|
||||||
const message = useMessage();
|
const message = useMessage()
|
||||||
const dialog = useDialog();
|
const dialog = useDialog()
|
||||||
const route = useRoute();
|
const route = useRoute()
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
const showImportDialog = ref(false);
|
// 筛选条件
|
||||||
const showAddDialog = ref(false);
|
const filterForm = ref({
|
||||||
const showEditDialog = ref(false);
|
domain: '',
|
||||||
|
status: null as number | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态选项
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '全部', value: null },
|
||||||
|
{ label: 'READY', value: 1 },
|
||||||
|
{ label: 'QUEUE', value: 2 },
|
||||||
|
{ label: 'CRAWLING', value: 3 },
|
||||||
|
{ label: 'PAUSE', value: 999 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const showImportDialog = ref(false)
|
||||||
|
const showAddDialog = ref(false)
|
||||||
|
const showEditDialog = ref(false)
|
||||||
|
|
||||||
// 当前正在编辑的域名 ID 列表(单个或批量)
|
// 当前正在编辑的域名 ID 列表(单个或批量)
|
||||||
const editingDomainIds = ref<DataTableRowKey[] | null>(null);
|
const editingDomainIds = ref<DataTableRowKey[] | null>(null)
|
||||||
|
|
||||||
// 选中行的 Key
|
// 选中行的 Key
|
||||||
const checkedRowKeys = ref<DataTableRowKey[]>([]);
|
const checkedRowKeys = ref<DataTableRowKey[]>([])
|
||||||
|
|
||||||
// 分页相关状态
|
// 分页相关状态
|
||||||
const pagination = ref({
|
const pagination = ref({
|
||||||
@ -32,35 +47,42 @@ const pagination = ref({
|
|||||||
showSizePicker: true,
|
showSizePicker: true,
|
||||||
pageSizes: [10, 20, 50, 100, 200, 500, 1000],
|
pageSizes: [10, 20, 50, 100, 200, 500, 1000],
|
||||||
onChange: (page: number) => {
|
onChange: (page: number) => {
|
||||||
pagination.value.page = page;
|
pagination.value.page = page
|
||||||
updateUrlParams();
|
updateUrlParams()
|
||||||
getDomainList();
|
getDomainList()
|
||||||
},
|
},
|
||||||
onUpdatePageSize: (pageSize: number) => {
|
onUpdatePageSize: (pageSize: number) => {
|
||||||
pagination.value.pageSize = pageSize;
|
pagination.value.pageSize = pageSize
|
||||||
pagination.value.page = 1;
|
pagination.value.page = 1
|
||||||
updateUrlParams();
|
updateUrlParams()
|
||||||
getDomainList();
|
getDomainList()
|
||||||
}
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
// 更新URL参数
|
// 更新URL参数
|
||||||
const updateUrlParams = () => {
|
const updateUrlParams = () => {
|
||||||
router.push({
|
router.push({
|
||||||
query: {
|
query: {
|
||||||
page: pagination.value.page,
|
page: pagination.value.page,
|
||||||
size: pagination.value.pageSize
|
size: pagination.value.pageSize,
|
||||||
|
domain: filterForm.value.domain || undefined,
|
||||||
|
status: filterForm.value.status || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化分页参数
|
// 初始化分页参数
|
||||||
const initPagination = () => {
|
const initPagination = () => {
|
||||||
const page = Number(route.query.page) || 1;
|
const page = Number(route.query.page) || 1
|
||||||
const size = Number(route.query.size) || 50;
|
const size = Number(route.query.size) || 50
|
||||||
pagination.value.page = page;
|
const domain = route.query.domain as string || ''
|
||||||
pagination.value.pageSize = size;
|
const status = route.query.status ? Number(route.query.status) : null
|
||||||
};
|
|
||||||
|
pagination.value.page = page
|
||||||
|
pagination.value.pageSize = size
|
||||||
|
filterForm.value.domain = domain
|
||||||
|
filterForm.value.status = status
|
||||||
|
}
|
||||||
|
|
||||||
const columns: Array<DataTableColumn> = [
|
const columns: Array<DataTableColumn> = [
|
||||||
{
|
{
|
||||||
@ -78,32 +100,32 @@ const columns: Array<DataTableColumn> = [
|
|||||||
title: '状态',
|
title: '状态',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
let statusText = '';
|
let statusText = ''
|
||||||
let statusType = '';
|
let statusType = ''
|
||||||
|
|
||||||
switch (row.status) {
|
switch (row.status) {
|
||||||
case 1:
|
case 1:
|
||||||
statusText = 'READY';
|
statusText = 'READY'
|
||||||
statusType = 'success';
|
statusType = 'success'
|
||||||
break;
|
break
|
||||||
case 2:
|
case 2:
|
||||||
statusText = 'QUENE';
|
statusText = 'QUENE'
|
||||||
statusType = 'warning';
|
statusType = 'warning'
|
||||||
break;
|
break
|
||||||
case 3:
|
case 3:
|
||||||
statusText = 'CRAWLING';
|
statusText = 'CRAWLING'
|
||||||
statusType = 'info';
|
statusType = 'info'
|
||||||
break;
|
break
|
||||||
case 999:
|
case 999:
|
||||||
statusText = "PAUSE";
|
statusText = 'PAUSE'
|
||||||
statusType = "error";
|
statusType = 'error'
|
||||||
break;
|
break
|
||||||
default:
|
default:
|
||||||
statusText = 'UNKNOWN';
|
statusText = 'UNKNOWN'
|
||||||
statusType = 'error';
|
statusType = 'error'
|
||||||
}
|
}
|
||||||
|
|
||||||
return <n-tag type={statusType}>{statusText}</n-tag>;
|
return <n-tag type={statusType}>{statusText}</n-tag>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -113,44 +135,50 @@ const columns: Array<DataTableColumn> = [
|
|||||||
<n-tooltip>
|
<n-tooltip>
|
||||||
{{
|
{{
|
||||||
trigger: () => <span>{row.crawl_interval}</span>,
|
trigger: () => <span>{row.crawl_interval}</span>,
|
||||||
default: () => `约 ${row.crawl_interval as number / 60 / 24} 天`
|
default: () => `约 ${(row.crawl_interval as number) / 60 / 24} 天`,
|
||||||
}}
|
}}
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "最近采集时间",
|
title: '最近采集时间',
|
||||||
key: "latest_crawl_time",
|
key: 'latest_crawl_time',
|
||||||
render: (row) => convertTimestampToDate(row.latest_crawl_time as number),
|
render: (row) => convertTimestampToDate(row.latest_crawl_time as number),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "操作",
|
title: '操作',
|
||||||
key: "action",
|
key: 'action',
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<n-button size="small" type="primary" onClick={() => handleEdit(row)}>编辑</n-button>
|
<n-button size="small" type="primary" onClick={() => handleEdit(row)}>
|
||||||
<n-button size="small" type="info" onClick={() => handleSingleCrawl(row)}>立即采集</n-button>
|
编辑
|
||||||
<n-button size="small" type="error" onClick={() => handleDelete(row)}>删除</n-button>
|
</n-button>
|
||||||
|
<n-button size="small" type="info" onClick={() => handleSingleCrawl(row)}>
|
||||||
|
立即采集
|
||||||
|
</n-button>
|
||||||
|
<n-button size="small" type="error" onClick={() => handleDelete(row)}>
|
||||||
|
删除
|
||||||
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const domains = ref([])
|
const domains = ref([])
|
||||||
|
|
||||||
// 是否有选中的行
|
// 是否有选中的行
|
||||||
const hasSelectedRows = computed(() => checkedRowKeys.value.length > 0);
|
const hasSelectedRows = computed(() => checkedRowKeys.value.length > 0)
|
||||||
|
|
||||||
// 处理选中行变化
|
// 处理选中行变化
|
||||||
const handleCheck = (rowKeys: DataTableRowKey[]) => {
|
const handleCheck = (rowKeys: DataTableRowKey[]) => {
|
||||||
checkedRowKeys.value = rowKeys;
|
checkedRowKeys.value = rowKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量删除域名 */
|
/** 批量删除域名 */
|
||||||
const handleBatchDelete = () => {
|
const handleBatchDelete = () => {
|
||||||
if (!hasSelectedRows.value) {
|
if (!hasSelectedRows.value) {
|
||||||
message.warning('请至少选择一个域名');
|
message.warning('请至少选择一个域名')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const removeSurl = ref(false)
|
const removeSurl = ref(false)
|
||||||
dialog.warning({
|
dialog.warning({
|
||||||
@ -158,67 +186,69 @@ const handleBatchDelete = () => {
|
|||||||
content: () => (
|
content: () => (
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-2">确定要删除选中的 {checkedRowKeys.value.length} 个域名吗?</div>
|
<div class="mb-2">确定要删除选中的 {checkedRowKeys.value.length} 个域名吗?</div>
|
||||||
<n-checkbox v-model:checked={removeSurl.value}>
|
<n-checkbox v-model:checked={removeSurl.value}>同时删除所有关联的 SURL</n-checkbox>
|
||||||
同时删除所有关联的 SURL
|
|
||||||
</n-checkbox>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
positiveText: '确定',
|
positiveText: '确定',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: async () => {
|
onPositiveClick: async () => {
|
||||||
try {
|
try {
|
||||||
const response = (await axios.post('/api/domain/v1/delete', {
|
const response = (
|
||||||
|
await axios.post('/api/domain/v1/delete', {
|
||||||
domain_ids: checkedRowKeys.value,
|
domain_ids: checkedRowKeys.value,
|
||||||
remove_surl: removeSurl.value
|
remove_surl: removeSurl.value,
|
||||||
})).data
|
})
|
||||||
|
).data
|
||||||
if (response.code !== 20000) {
|
if (response.code !== 20000) {
|
||||||
message.error(`批量删除域名失败,错误:${response.message}`)
|
message.error(`批量删除域名失败,错误:${response.message}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
message.success('批量删除成功')
|
message.success('批量删除成功')
|
||||||
checkedRowKeys.value = []; // 清空选择
|
checkedRowKeys.value = [] // 清空选择
|
||||||
getDomainList()
|
getDomainList()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('批量删除域名失败', error)
|
console.error('批量删除域名失败', error)
|
||||||
message.error(`批量删除域名失败,错误:${error}`)
|
message.error(`批量删除域名失败,错误:${error}`)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量修改采集间隔 */
|
/** 批量修改采集间隔 */
|
||||||
const handleBatchEdit = () => {
|
const handleBatchEdit = () => {
|
||||||
if (!hasSelectedRows.value) {
|
if (!hasSelectedRows.value) {
|
||||||
message.warning('请至少选择一个域名');
|
message.warning('请至少选择一个域名')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
editingDomainIds.value = [...checkedRowKeys.value]; // 设置要编辑的 ID 为当前选中的 ID
|
editingDomainIds.value = [...checkedRowKeys.value] // 设置要编辑的 ID 为当前选中的 ID
|
||||||
showEditDialog.value = true;
|
showEditDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量立即采集 */
|
/** 批量立即采集 */
|
||||||
const handleBatchCrawl = async () => {
|
const handleBatchCrawl = async () => {
|
||||||
if (!hasSelectedRows.value) {
|
if (!hasSelectedRows.value) {
|
||||||
message.warning('请至少选择一个域名');
|
message.warning('请至少选择一个域名')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// TODO: 确认后端是否有 /api/domain/v1/crawl 接口
|
// TODO: 确认后端是否有 /api/domain/v1/crawl 接口
|
||||||
const response = (await axios.post('/api/domain/v1/crawl', {
|
const response = (
|
||||||
domain_ids: checkedRowKeys.value
|
await axios.post('/api/domain/v1/crawl', {
|
||||||
})).data;
|
domain_ids: checkedRowKeys.value,
|
||||||
|
})
|
||||||
|
).data
|
||||||
|
|
||||||
if (response.code !== 20000) {
|
if (response.code !== 20000) {
|
||||||
message.error(`批量触发采集失败:${response.message}`);
|
message.error(`批量触发采集失败:${response.message}`)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success('批量触发采集成功,已加入队列');
|
message.success('批量触发采集成功,已加入队列')
|
||||||
checkedRowKeys.value = []; // 清空选择
|
checkedRowKeys.value = [] // 清空选择
|
||||||
getDomainList(); // 刷新列表查看状态变化
|
getDomainList() // 刷新列表查看状态变化
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('批量触发采集失败', error);
|
console.error('批量触发采集失败', error)
|
||||||
message.error(`批量触发采集失败:${error}`);
|
message.error(`批量触发采集失败:${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,20 +256,22 @@ const handleBatchCrawl = async () => {
|
|||||||
const handleSingleCrawl = async (row: any) => {
|
const handleSingleCrawl = async (row: any) => {
|
||||||
try {
|
try {
|
||||||
// 调用与批量采集相同的接口,但只传单个 ID
|
// 调用与批量采集相同的接口,但只传单个 ID
|
||||||
const response = (await axios.post('/api/domain/v1/crawl', {
|
const response = (
|
||||||
domain_ids: [row.id]
|
await axios.post('/api/domain/v1/crawl', {
|
||||||
})).data;
|
domain_ids: [row.id],
|
||||||
|
})
|
||||||
|
).data
|
||||||
|
|
||||||
if (response.code !== 20000) {
|
if (response.code !== 20000) {
|
||||||
message.error(`触发采集失败:${response.message}`);
|
message.error(`触发采集失败:${response.message}`)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success(`域名 ${row.domain} 已加入采集队列`);
|
message.success(`域名 ${row.domain} 已加入采集队列`)
|
||||||
getDomainList(); // 刷新列表查看状态变化
|
getDomainList() // 刷新列表查看状态变化
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('触发采集失败', error);
|
console.error('触发采集失败', error)
|
||||||
message.error(`触发采集失败:${error}`);
|
message.error(`触发采集失败:${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,58 +284,61 @@ const handleDelete = async (row: any) => {
|
|||||||
content: () => (
|
content: () => (
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-2">确定要删除域名 {row.domain} 吗?</div>
|
<div class="mb-2">确定要删除域名 {row.domain} 吗?</div>
|
||||||
<n-checkbox v-model:checked={removeSurl.value}>
|
<n-checkbox v-model:checked={removeSurl.value}>同时删除所有关联的 SURL</n-checkbox>
|
||||||
同时删除所有关联的 SURL
|
|
||||||
</n-checkbox>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
positiveText: '确定',
|
positiveText: '确定',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: async () => {
|
onPositiveClick: async () => {
|
||||||
try {
|
try {
|
||||||
const response = (await axios.post('/api/domain/v1/delete', {
|
const response = (
|
||||||
|
await axios.post('/api/domain/v1/delete', {
|
||||||
domain_ids: [row.id],
|
domain_ids: [row.id],
|
||||||
remove_surl: removeSurl.value
|
remove_surl: removeSurl.value,
|
||||||
})).data
|
})
|
||||||
|
).data
|
||||||
if (response.code !== 20000) {
|
if (response.code !== 20000) {
|
||||||
message.error(`删除域名失败,错误:${response.message}`)
|
message.error(`删除域名失败,错误:${response.message}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
message.success('删除成功')
|
message.success('删除成功')
|
||||||
// 如果删除的是选中的行,也从 checkedRowKeys 中移除
|
// 如果删除的是选中的行,也从 checkedRowKeys 中移除
|
||||||
const index = checkedRowKeys.value.findIndex(key => key === row.id);
|
const index = checkedRowKeys.value.findIndex((key) => key === row.id)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
checkedRowKeys.value.splice(index, 1);
|
checkedRowKeys.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
getDomainList()
|
getDomainList()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除域名失败', error)
|
console.error('删除域名失败', error)
|
||||||
message.error(`删除域名失败,错误:${error}`)
|
message.error(`删除域名失败,错误:${error}`)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取域名列表 */
|
/** 获取域名列表 */
|
||||||
const getDomainList = async () => {
|
const getDomainList = async () => {
|
||||||
try {
|
try {
|
||||||
const response = (await axios.get('/api/domain/v1/list', {
|
const response = (
|
||||||
|
await axios.get('/api/domain/v1/list', {
|
||||||
params: {
|
params: {
|
||||||
page: pagination.value.page,
|
page: pagination.value.page,
|
||||||
size: pagination.value.pageSize
|
size: pagination.value.pageSize,
|
||||||
}
|
domain: filterForm.value.domain || undefined,
|
||||||
})).data;
|
status: filterForm.value.status || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).data
|
||||||
if (response.code !== 20000) {
|
if (response.code !== 20000) {
|
||||||
message.error(`获取域名列表失败,错误:${response.message}`);
|
message.error(`获取域名列表失败,错误:${response.message}`)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domains.value = response.data.rows;
|
domains.value = response.data.rows
|
||||||
pagination.value.itemCount = response.data.total;
|
pagination.value.itemCount = response.data.total
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取域名列表失败', error);
|
console.error('获取域名列表失败', error)
|
||||||
message.error(`获取域名列表失败,错误:${error}`);
|
message.error(`获取域名列表失败,错误:${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,22 +352,40 @@ const handleAddSuccess = () => {
|
|||||||
|
|
||||||
/** 编辑域名 */
|
/** 编辑域名 */
|
||||||
const handleEdit = (row: any) => {
|
const handleEdit = (row: any) => {
|
||||||
editingDomainIds.value = [row.id]; // 设置要编辑的 ID 为当前行的 ID
|
editingDomainIds.value = [row.id] // 设置要编辑的 ID 为当前行的 ID
|
||||||
showEditDialog.value = true;
|
showEditDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEditSuccess = () => {
|
const handleEditSuccess = () => {
|
||||||
getDomainList();
|
getDomainList()
|
||||||
const editedCount = editingDomainIds.value?.length || 0;
|
const editedCount = editingDomainIds.value?.length || 0
|
||||||
editingDomainIds.value = null; // 清空正在编辑的 ID
|
editingDomainIds.value = null // 清空正在编辑的 ID
|
||||||
// 如果是批量编辑,成功后清空表格的选择
|
// 如果是批量编辑,成功后清空表格的选择
|
||||||
if (editedCount > 1) {
|
if (editedCount > 1) {
|
||||||
checkedRowKeys.value = [];
|
checkedRowKeys.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置筛选
|
||||||
|
const resetFilter = () => {
|
||||||
|
filterForm.value = {
|
||||||
|
domain: '',
|
||||||
|
status: null,
|
||||||
|
}
|
||||||
|
pagination.value.page = 1
|
||||||
|
updateUrlParams()
|
||||||
|
getDomainList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用筛选
|
||||||
|
const applyFilter = () => {
|
||||||
|
pagination.value.page = 1
|
||||||
|
updateUrlParams()
|
||||||
|
getDomainList()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
initPagination();
|
initPagination()
|
||||||
await getDomainList()
|
await getDomainList()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -347,6 +400,20 @@ onMounted(async () => {
|
|||||||
<n-button type="info" @click="handleBatchCrawl" :disabled="!hasSelectedRows">立即采集</n-button>
|
<n-button type="info" @click="handleBatchCrawl" :disabled="!hasSelectedRows">立即采集</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选表单 -->
|
||||||
|
<n-form inline :model="filterForm" class="mb-4 p-4 bg-gray-50 rounded-lg">
|
||||||
|
<n-form-item label="域名" path="domain">
|
||||||
|
<n-input v-model:value="filterForm.domain" placeholder="请输入域名" clearable @keydown.enter="applyFilter" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="状态" path="status">
|
||||||
|
<n-select v-model:value="filterForm.status" :options="statusOptions" placeholder="请选择状态" style="width: 200px" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item>
|
||||||
|
<n-button type="primary" @click="applyFilter">筛选</n-button>
|
||||||
|
<n-button class="ml-2" @click="resetFilter">重置</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
|
||||||
<n-data-table :columns="columns" :data="domains" :row-key="(row: any) => row.id" :checked-row-keys="checkedRowKeys"
|
<n-data-table :columns="columns" :data="domains" :row-key="(row: any) => row.id" :checked-row-keys="checkedRowKeys"
|
||||||
@update:checked-row-keys="handleCheck" size="small" />
|
@update:checked-row-keys="handleCheck" size="small" />
|
||||||
|
|
||||||
@ -360,5 +427,4 @@ onMounted(async () => {
|
|||||||
<AddDomainDialog v-model:show="showAddDialog" @success="handleAddSuccess" />
|
<AddDomainDialog v-model:show="showAddDialog" @success="handleAddSuccess" />
|
||||||
<EditDomainDialog v-model:show="showEditDialog" :domain-ids="editingDomainIds" @success="handleEditSuccess"
|
<EditDomainDialog v-model:show="showEditDialog" :domain-ids="editingDomainIds" @success="handleEditSuccess"
|
||||||
@close="editingDomainIds = null" />
|
@close="editingDomainIds = null" />
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,7 +1,379 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="tsx">
|
||||||
|
import { onMounted, inject, ref, computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import type { AxiosInstance } from 'axios'
|
||||||
|
import { type DataTableColumn, type DataTableRowKey, useMessage, useDialog, NDropdown } from 'naive-ui'
|
||||||
|
import { convertTimestampToDate } from '@/utils/common'
|
||||||
|
|
||||||
|
const axios = inject('axios') as AxiosInstance
|
||||||
|
const message = useMessage()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 搜索条件
|
||||||
|
const searchForm = ref({
|
||||||
|
domain: '',
|
||||||
|
surl: '',
|
||||||
|
is_report_by_one: 2,
|
||||||
|
is_report_by_site: 2,
|
||||||
|
is_report_by_wap: 2,
|
||||||
|
has_evidence: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选项数据
|
||||||
|
const options = [
|
||||||
|
{ label: '全部', value: 2 },
|
||||||
|
{ label: '是', value: 1 },
|
||||||
|
{ label: '否', value: 0 },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 分页相关状态
|
||||||
|
const pagination = ref({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
itemCount: 0,
|
||||||
|
showSizePicker: true,
|
||||||
|
pageSizes: [10, 20, 50, 100, 200, 500, 1000],
|
||||||
|
onChange: (page: number) => {
|
||||||
|
pagination.value.page = page
|
||||||
|
updateUrlParams()
|
||||||
|
getUrlList()
|
||||||
|
},
|
||||||
|
onUpdatePageSize: (pageSize: number) => {
|
||||||
|
pagination.value.pageSize = pageSize
|
||||||
|
pagination.value.page = 1
|
||||||
|
updateUrlParams()
|
||||||
|
getUrlList()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新URL参数
|
||||||
|
const updateUrlParams = () => {
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
page: pagination.value.page,
|
||||||
|
size: pagination.value.pageSize,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化分页参数
|
||||||
|
const initPagination = () => {
|
||||||
|
const page = Number(route.query.page) || 1
|
||||||
|
const size = Number(route.query.size) || 50
|
||||||
|
pagination.value.page = page
|
||||||
|
pagination.value.pageSize = size
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中行的 Key
|
||||||
|
const checkedRowKeys = ref<DataTableRowKey[]>([])
|
||||||
|
|
||||||
|
// 举报选项
|
||||||
|
const reportOptions = [
|
||||||
|
{
|
||||||
|
label: '全部渠道',
|
||||||
|
key: 'all',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'PC渠道',
|
||||||
|
key: 'pc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'SITE渠道',
|
||||||
|
key: 'site',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'WAP渠道',
|
||||||
|
key: 'wap',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const columns: Array<DataTableColumn> = [
|
||||||
|
{
|
||||||
|
type: 'selection',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '#',
|
||||||
|
key: 'id',
|
||||||
|
minWidth: 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '域名',
|
||||||
|
key: 'domain',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'SURL',
|
||||||
|
key: 'surl',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Q',
|
||||||
|
key: 'q',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Token',
|
||||||
|
key: 'token',
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '已通过PC举报',
|
||||||
|
key: 'is_report_by_one',
|
||||||
|
render: (row) => (
|
||||||
|
<n-tag type={row.is_report_by_one ? 'success' : 'default'}>
|
||||||
|
{row.is_report_by_one ? '是' : '否'}
|
||||||
|
</n-tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '已通过site举报',
|
||||||
|
key: 'is_report_by_site',
|
||||||
|
render: (row) => (
|
||||||
|
<n-tag type={row.is_report_by_site ? 'success' : 'default'}>
|
||||||
|
{row.is_report_by_site ? '是' : '否'}
|
||||||
|
</n-tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '已通过WAP举报',
|
||||||
|
key: 'is_report_by_wap',
|
||||||
|
render: (row) => (
|
||||||
|
<n-tag type={row.is_report_by_wap ? 'success' : 'default'}>
|
||||||
|
{row.is_report_by_wap ? '是' : '否'}
|
||||||
|
</n-tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '已收集证据',
|
||||||
|
key: 'has_evidence',
|
||||||
|
render: (row) => (
|
||||||
|
<n-tag type={row.has_evidence ? 'success' : 'default'}>
|
||||||
|
{row.has_evidence ? '是' : '否'}
|
||||||
|
</n-tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
render: (row) => {
|
||||||
|
return (
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<n-dropdown
|
||||||
|
trigger="click"
|
||||||
|
options={reportOptions}
|
||||||
|
onSelect={(key: string) => handleSingleReport(row, key)}
|
||||||
|
>
|
||||||
|
<n-button size="small" type="primary">举报</n-button>
|
||||||
|
</n-dropdown>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="info"
|
||||||
|
onClick={() => handleSingleCollectEvidence(row)}
|
||||||
|
>
|
||||||
|
收集证据
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const urls = ref([])
|
||||||
|
|
||||||
|
// 获取URL列表
|
||||||
|
const getUrlList = async () => {
|
||||||
|
try {
|
||||||
|
const response = (
|
||||||
|
await axios.get('/api/urls/v1/list', {
|
||||||
|
params: {
|
||||||
|
...searchForm.value,
|
||||||
|
page: pagination.value.page,
|
||||||
|
size: pagination.value.pageSize,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).data
|
||||||
|
|
||||||
|
if (response.code !== 20000) {
|
||||||
|
message.error(`获取URL列表失败:${response.message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
urls.value = response.data.data
|
||||||
|
console.log('response.data.total:', response.data.total)
|
||||||
|
pagination.value.itemCount = response.data.total
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取URL列表失败', error)
|
||||||
|
message.error(`获取URL列表失败:${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.value.page = 1
|
||||||
|
getUrlList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.value = {
|
||||||
|
domain: '',
|
||||||
|
surl: '',
|
||||||
|
is_report_by_one: 2,
|
||||||
|
is_report_by_site: 2,
|
||||||
|
is_report_by_wap: 2,
|
||||||
|
has_evidence: 2,
|
||||||
|
}
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理举报
|
||||||
|
const handleReport = async (ids: number[], option: string) => {
|
||||||
|
// 检查是否有证据
|
||||||
|
const selectedUrls = urls.value.filter((url: any) => ids.includes(url.id))
|
||||||
|
const hasNoEvidence = selectedUrls.some((url: any) => !url.has_evidence)
|
||||||
|
|
||||||
|
if (hasNoEvidence) {
|
||||||
|
message.warning('请先收集证据后再进行举报')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = (await axios.post('/api/urls/v1/report', {
|
||||||
|
ids,
|
||||||
|
report_by_one: option === 'all' || option === 'pc',
|
||||||
|
report_by_site: option === 'all' || option === 'site',
|
||||||
|
report_by_wap: option === 'all' || option === 'wap',
|
||||||
|
})).data
|
||||||
|
|
||||||
|
if (response.code !== 20000) {
|
||||||
|
message.error(`举报失败:${response.message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success('操作成功,已修改SURL状态,等待引擎调度')
|
||||||
|
getUrlList()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('举报失败', error)
|
||||||
|
message.error(`举报失败:${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理收集证据
|
||||||
|
const handleCollectEvidence = async (ids: number[]) => {
|
||||||
|
try {
|
||||||
|
const response = (await axios.post('/api/urls/v1/evidence', {
|
||||||
|
ids,
|
||||||
|
})).data
|
||||||
|
|
||||||
|
if (response.code !== 20000) {
|
||||||
|
message.error(`收集证据失败:${response.message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success('操作成功,已修改SURL状态,等待引擎调度')
|
||||||
|
getUrlList()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('收集证据失败', error)
|
||||||
|
message.error(`收集证据失败:${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理单个URL的举报
|
||||||
|
const handleSingleReport = (row: any, option: string) => {
|
||||||
|
handleReport([row.id], option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理单个URL的收集证据
|
||||||
|
const handleSingleCollectEvidence = (row: any) => {
|
||||||
|
handleCollectEvidence([row.id])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理批量举报
|
||||||
|
const handleBatchReport = (option: string) => {
|
||||||
|
if (checkedRowKeys.value.length === 0) {
|
||||||
|
message.warning('请至少选择一个URL')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleReport(checkedRowKeys.value as number[], option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理批量收集证据
|
||||||
|
const handleBatchCollectEvidence = () => {
|
||||||
|
if (checkedRowKeys.value.length === 0) {
|
||||||
|
message.warning('请至少选择一个URL')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleCollectEvidence(checkedRowKeys.value as number[])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否有选中的行
|
||||||
|
const hasSelectedRows = computed(() => checkedRowKeys.value.length > 0)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initPagination()
|
||||||
|
getUrlList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<p class="text-2xl">DomainManager</p>
|
<div class="p-4">
|
||||||
|
<h1 class="text-2xl mb-4">URL管理</h1>
|
||||||
|
|
||||||
|
<!-- 搜索表单 -->
|
||||||
|
<n-card class="mb-4">
|
||||||
|
<n-form :model="searchForm" label-placement="left" label-width="auto" require-mark-placement="right-hanging">
|
||||||
|
<n-grid :cols="24" :x-gap="24">
|
||||||
|
<n-form-item-gi :span="8" label="域名">
|
||||||
|
<n-input v-model:value="searchForm.domain" placeholder="请输入域名" @keyup.enter="handleSearch" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :span="8" label="SURL">
|
||||||
|
<n-input v-model:value="searchForm.surl" placeholder="请输入SURL" @keyup.enter="handleSearch" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :span="8" label="是否已通过PC举报">
|
||||||
|
<n-select v-model:value="searchForm.is_report_by_one" :options="options" placeholder="请选择" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :span="8" label="是否已通过site举报">
|
||||||
|
<n-select v-model:value="searchForm.is_report_by_site" :options="options" placeholder="请选择" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :span="8" label="是否已通过WAP举报">
|
||||||
|
<n-select v-model:value="searchForm.is_report_by_wap" :options="options" placeholder="请选择" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :span="8" label="是否已收集证据">
|
||||||
|
<n-select v-model:value="searchForm.has_evidence" :options="options" placeholder="请选择" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
</n-grid>
|
||||||
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
|
<n-button @click="handleReset">重置</n-button>
|
||||||
|
<n-button type="primary" @click="handleSearch">搜索</n-button>
|
||||||
|
</div>
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<n-card>
|
||||||
|
<div class="mb-4" v-if="hasSelectedRows">
|
||||||
|
<n-space>
|
||||||
|
<n-dropdown trigger="click" :options="reportOptions" @select="handleBatchReport">
|
||||||
|
<n-button type="primary">批量举报 ({{ checkedRowKeys.length }})</n-button>
|
||||||
|
</n-dropdown>
|
||||||
|
<n-button type="info" @click="handleBatchCollectEvidence">
|
||||||
|
批量收集证据 ({{ checkedRowKeys.length }})
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-data-table :columns="columns" :data="urls" :bordered="false" :row-key="(row: any) => row.id"
|
||||||
|
:checked-row-keys="checkedRowKeys" @update:checked-row-keys="checkedRowKeys = $event" />
|
||||||
|
|
||||||
|
<div class="flex justify-center mt-4">
|
||||||
|
<n-pagination v-model:page="pagination.page" :item-count="pagination.itemCount" :page-size="pagination.pageSize"
|
||||||
|
:show-size-picker="pagination.showSizePicker" :page-sizes="pagination.pageSizes"
|
||||||
|
:on-update:page-size="pagination.onUpdatePageSize" :on-change="pagination.onChange" />
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user