优化,准备添加用户选择导出可视化微信记录
This commit is contained in:
parent
8330cb974a
commit
fe3f2dc6e6
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,35 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: 帮助定位问题所在
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**问题描述**
|
||||
请在此处提供对问题的详细描述。
|
||||
|
||||
**复现步骤**
|
||||
请提供重现问题所需的步骤。(执行的命令)
|
||||
|
||||
1. 步骤 1
|
||||
2. 步骤 2
|
||||
3. 步骤 3
|
||||
|
||||
**预期行为**
|
||||
请清楚地描述您预期的行为。
|
||||
|
||||
**实际行为**
|
||||
请描述实际的行为和问题出现的地方。
|
||||
|
||||
**环境信息**
|
||||
- pywxdump版本:
|
||||
- 操作系统版本:
|
||||
- python版本:
|
||||
- 微信版本:
|
||||
|
||||
|
||||
|
||||
**其他信息**
|
||||
请提供任何与问题相关的其他信息(文字,截图等)。
|
42
.github/workflows/auto-sync-gitee.yml
vendored
42
.github/workflows/auto-sync-gitee.yml
vendored
@ -1,42 +0,0 @@
|
||||
#on:
|
||||
# push:
|
||||
# branches: [master]
|
||||
#name: Mirror GitHub Repos to Gitee
|
||||
#jobs:
|
||||
# run:
|
||||
# name: Sync-GitHub-to-Gitee
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Mirror the Github repos to Gitee.
|
||||
# uses: Yikun/hub-mirror-action@master
|
||||
# with:
|
||||
# src: github/xaoyaoo
|
||||
# dst: gitee/xaoyaoo
|
||||
# dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
|
||||
# dst_token: ${{ secrets.GITEE_TOKEN }}
|
||||
# force_update: true
|
||||
# src_account_type: org
|
||||
# dst_account_type: user
|
||||
# mappings: "dashboard=>dashboards"
|
||||
# static_list: "trader"
|
||||
# cache_path: /github/workspace/hub-mirror-cache
|
||||
|
||||
name: Hello World Action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ] # 触发条件:当主分支有新的推送时
|
||||
|
||||
jobs:
|
||||
hello-job:
|
||||
runs-on: ubuntu-latest # 运行环境:最新的 Ubuntu 系统
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3 # 检出代码
|
||||
|
||||
- name: Print Hello Message
|
||||
run: echo "Hello, world!" # 执行命令,打印消息
|
||||
|
||||
- name: Print Date
|
||||
run: date # 执行命令,打印当前日期
|
134
.github/workflows/publish.yml
vendored
134
.github/workflows/publish.yml
vendored
@ -1,134 +0,0 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
# 当master分支有push时,触发action
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # 以 'v' 开头的标签触发工作流程
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish Pypi and Create Release
|
||||
if: github.repository == 'xaoyaoo/PyWxDump' # 仅在指定仓库的 tag 触发工作流程
|
||||
# 此作业在 Linux 上运行
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository # 检出仓库
|
||||
uses: actions/checkout@v2 # 使用 GitHub 官方的 checkout action
|
||||
|
||||
- name: Set git fetch depth # 设置 git fetch 深度
|
||||
run: |
|
||||
git fetch --prune --unshallow # 获取完整的 git 历史记录
|
||||
|
||||
- name: Set up Python # 设置 Python 环境
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.8'
|
||||
cache: 'pip' # caching pip dependencies
|
||||
- run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
python -m pip install --upgrade twine
|
||||
pip install pyinstaller
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Set up Node.js # 设置 Node.js 环境
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Build Web UI # 构建 Web UI
|
||||
run: |
|
||||
cd ..
|
||||
git clone https://github.com/xaoyaoo/wxdump_web.git
|
||||
Compress-Archive -Path wxdump_web -DestinationPath wxdump_web.zip
|
||||
Compress-Archive -Path PyWxDump -DestinationPath PyWxDump.zip
|
||||
cd wxdump_web
|
||||
npm list -g
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: copy web ui to pywxdump/ui/web
|
||||
run: |
|
||||
cd ..
|
||||
ls -l wxdump_web/dist
|
||||
cp -r wxdump_web/dist PyWxDump/pywxdump/ui/web
|
||||
ls -l PyWxDump/pywxdump/ui/web
|
||||
cd PyWxDump
|
||||
|
||||
# - name: Build Export UI # 构建导出的 Web UI
|
||||
# run: |
|
||||
# cd ..
|
||||
# cd wxdump_web
|
||||
# cp src/main.ts src/t.ts
|
||||
# cp src/main.ts.export src/main.ts
|
||||
# npm install
|
||||
# npm run build
|
||||
#
|
||||
# - name: copy Export UI and Export UI to pywxdump/ui/web and pywxdump/ui/export
|
||||
# run: |
|
||||
# cd ..
|
||||
# ls -l wxdump_web/dist
|
||||
# cp -r wxdump_web/dist PyWxDump/pywxdump/ui/export
|
||||
# ls -l PyWxDump/pywxdump/ui/export
|
||||
# cd PyWxDump
|
||||
|
||||
- name: Build package # 构建包
|
||||
run: |
|
||||
python -m build
|
||||
pip install -U .
|
||||
|
||||
- name: Generate File pywxdump.spec # 生成 pywxdump.spec 文件
|
||||
run: |
|
||||
python tests/build_exe.py
|
||||
ls
|
||||
ls dist
|
||||
cat dist/pywxdump.spec
|
||||
|
||||
- name: Build Executable
|
||||
run: |
|
||||
pyinstaller --clean --distpath=dist dist/pywxdump.spec
|
||||
|
||||
- name: test
|
||||
run: |
|
||||
ls -l dist
|
||||
ls -l "${{ github.workspace }}"
|
||||
|
||||
- name: Zip Executable
|
||||
run: |
|
||||
cd ..
|
||||
ls
|
||||
Compress-Archive -Path PyWxDump/dist/*.exe,PyWxDump/dist/*.whl -DestinationPath exe_whl.zip
|
||||
Compress-Archive -Path PyWxDump.zip,wxdump_web.zip -DestinationPath Source.zip
|
||||
ls
|
||||
cp exe_whl.zip PyWxDump/dist/exe_whl.zip
|
||||
cp Source.zip PyWxDump/dist/Source.zip
|
||||
ls PyWxDump/dist
|
||||
cd PyWxDump
|
||||
|
||||
- name: Publish package with Twine # 使用 Twine 发布到 PyPI
|
||||
run: |
|
||||
twine upload dist/*.whl dist/*.tar.gz
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref.tag }}
|
||||
body: |
|
||||
[Auto Release] Update PyWxDump to ${{ github.ref }}
|
||||
详细更新日志请查看 [CHANGELOG.md](https://github.com/xaoyaoo/PyWxDump/blob/master/doc/CHANGELOG.md)
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: |
|
||||
dist/*.exe
|
||||
dist/*.whl
|
||||
dist/exe_whl.zip
|
||||
dist/Source.zip
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -29,3 +29,4 @@ dist-ssr
|
||||
*.local
|
||||
/pywxdump/ui/web/*
|
||||
/pywxdump/ui/web/assets/*
|
||||
/pywxdump/wxdump_work
|
||||
|
@ -5,6 +5,7 @@
|
||||
# Author: xaoyaoo
|
||||
# Date: 2024/04/20
|
||||
# -------------------------------------------------------------------------------
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
from pywxdump.db import DBHandler
|
||||
@ -39,5 +40,135 @@ def export_json(wxid, outpath, db_config, my_wxid="我", indent=4):
|
||||
return True, f"导出成功: {outpath}"
|
||||
|
||||
|
||||
def export_json_mini(wxid, outpath, db_config, my_wxid="我", indent=4):
|
||||
# 确保输出目录存在
|
||||
if not os.path.exists(outpath):
|
||||
outpath = os.path.join(os.getcwd(), "export_mini" + os.sep + wxid)
|
||||
os.makedirs(outpath, exist_ok=True)
|
||||
|
||||
db = DBHandler(db_config, my_wxid)
|
||||
|
||||
# 获取消息总数
|
||||
count = db.get_msgs_count(wxid)
|
||||
chatCount = count.get(wxid, 0)
|
||||
if chatCount == 0:
|
||||
return False, "没有聊天记录"
|
||||
|
||||
users = {}
|
||||
page_size = chatCount + 1 # 保持与原函数一致的分页逻辑
|
||||
|
||||
for i in range(0, chatCount, page_size):
|
||||
start_index = i
|
||||
data, users_t = db.get_msgs(wxid, start_index, page_size)
|
||||
users.update(users_t) # 合并用户信息
|
||||
|
||||
if not data:
|
||||
continue
|
||||
|
||||
# 构建简化数据
|
||||
mini_data = []
|
||||
for msg in data:
|
||||
# 获取昵称(优先用备注,没有则用昵称,最后用wxid)
|
||||
user_info = users.get(msg.get("talker"), {})
|
||||
nickname = user_info.get("remark") or user_info.get("nickname") or msg.get("talker")
|
||||
|
||||
mini_msg = {
|
||||
"nickname": nickname,
|
||||
"message": msg.get("msg", ""),
|
||||
"time": msg.get("CreateTime", "")
|
||||
}
|
||||
mini_data.append(mini_msg)
|
||||
|
||||
# 保存简化后的文件
|
||||
save_path = os.path.join(outpath, f"{wxid}_mini_{i}_{i + page_size}.json")
|
||||
with open(save_path, "w", encoding="utf-8") as f:
|
||||
json.dump(mini_data, f, ensure_ascii=False, indent=indent)
|
||||
|
||||
return True, f"简化版导出成功: {outpath}"
|
||||
|
||||
|
||||
def export_json_mini_time_limit(wxid, outpath, db_config, my_wxid="我",
|
||||
start_createtime=None, end_createtime=None, indent=4):
|
||||
"""
|
||||
带时间过滤的简化版聊天记录导出
|
||||
|
||||
:param start_createtime: 开始时间(格式:2025-4-30 16:55:01)
|
||||
:param end_createtime: 结束时间(格式:2025-4-30 16:55:01)
|
||||
"""
|
||||
# 创建输出目录
|
||||
if not os.path.exists(outpath):
|
||||
outpath = os.path.join(os.getcwd(), "export_mini" + os.sep + wxid)
|
||||
os.makedirs(outpath, exist_ok=True)
|
||||
|
||||
# 初始化数据库连接
|
||||
db = DBHandler(db_config, my_wxid)
|
||||
|
||||
# 时间格式转换
|
||||
def str_to_timestamp(time_str):
|
||||
if not time_str:
|
||||
return None
|
||||
try:
|
||||
dt = datetime.datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
|
||||
return int(dt.timestamp())
|
||||
except ValueError:
|
||||
raise ValueError(f"无效时间格式: {time_str},示例: 2025-04-30 16:55:01")
|
||||
|
||||
start_ts = str_to_timestamp(start_createtime)
|
||||
end_ts = str_to_timestamp(end_createtime)
|
||||
|
||||
# 获取消息数据(带时间过滤)
|
||||
all_data = []
|
||||
users = {}
|
||||
page_size = 5000 # 每次获取5000条
|
||||
start_index = 0
|
||||
|
||||
while True:
|
||||
# 获取分页数据(自动包含时间过滤条件)
|
||||
data, users_t = db.get_msgs(
|
||||
wxid,
|
||||
start_index=start_index,
|
||||
page_size=page_size,
|
||||
start_createtime=start_ts,
|
||||
end_createtime=end_ts
|
||||
)
|
||||
|
||||
if not data:
|
||||
break
|
||||
|
||||
all_data.extend(data)
|
||||
users.update(users_t)
|
||||
start_index += page_size
|
||||
|
||||
if not all_data:
|
||||
return False, "指定时间段内没有聊天记录"
|
||||
|
||||
# 构建简化数据结构
|
||||
mini_data = []
|
||||
for msg in all_data:
|
||||
talker = msg.get("talker")
|
||||
user_info = users.get(talker, {})
|
||||
|
||||
mini_msg = {
|
||||
"sender": user_info.get("remark") or user_info.get("nickname") or talker,
|
||||
"content": msg.get("msg", ""),
|
||||
"timestamp": msg.get("CreateTime")
|
||||
}
|
||||
mini_data.append(mini_msg)
|
||||
|
||||
# 生成带时间范围的文件名
|
||||
time_suffix = ""
|
||||
if start_createtime or end_createtime:
|
||||
start_part = start_createtime.replace(" ", "_").replace(":", "-") if start_createtime else "all"
|
||||
end_part = end_createtime.replace(" ", "_").replace(":", "-") if end_createtime else "now"
|
||||
time_suffix = f"_{start_part}_to_{end_part}"
|
||||
|
||||
save_path = os.path.join(outpath, f"{wxid}_mini{time_suffix}.json")
|
||||
with open(save_path, "w", encoding="utf-8") as f:
|
||||
json.dump(mini_data, f, ensure_ascii=False, indent=indent)
|
||||
|
||||
return True, f"导出成功: {save_path}"
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
|
@ -5,6 +5,7 @@
|
||||
# Author: xaoyaoo
|
||||
# Date: 2024/01/02
|
||||
# -------------------------------------------------------------------------------
|
||||
import datetime
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
@ -22,6 +23,7 @@ from pywxdump.db import DBHandler
|
||||
from pywxdump.db.utils import download_file, dat2img
|
||||
|
||||
from .export import export_csv, export_json, export_html
|
||||
from .export.exportJSON import export_json_mini, export_json_mini_time_limit
|
||||
from .rjson import ReJson, RqJson
|
||||
from .utils import error9999, gc, asyncError9999, rs_loger
|
||||
|
||||
@ -134,11 +136,18 @@ def get_msgs(wxid: str = Body(...), start: int = Body(...), limit: int = Body(..
|
||||
"""
|
||||
|
||||
my_wxid = gc.get_conf(gc.at, "last")
|
||||
|
||||
if not my_wxid: return ReJson(1001, body="my_wxid is required")
|
||||
db_config = gc.get_conf(my_wxid, "db_config")
|
||||
|
||||
db = DBHandler(db_config, my_wxid=my_wxid)
|
||||
msgs, users = db.get_msgs(wxids=wxid, start_index=start, page_size=limit)
|
||||
|
||||
start_createtime = datetime.datetime.strptime("2025-04-28 00:54:33",
|
||||
"%Y-%m-%d %H:%M:%S").timestamp()
|
||||
end_createtime = datetime.datetime.now().timestamp()
|
||||
msgs, users = db.get_msgs(wxids=wxid, start_index=start, page_size=limit,) #
|
||||
|
||||
|
||||
return ReJson(0, {"msg_list": msgs, "user_list": users})
|
||||
|
||||
|
||||
@ -457,13 +466,16 @@ def get_export_json(wxid: str = Body(..., embed=True)):
|
||||
if not os.path.exists(outpath):
|
||||
os.makedirs(outpath)
|
||||
|
||||
code, ret = export_json(wxid, outpath, db_config, my_wxid=my_wxid)
|
||||
code, ret = export_json_mini_time_limit(wxid, outpath, db_config, my_wxid=my_wxid,start_createtime="2025-4-29 00:00:00", end_createtime="2025-4-30 00:00:00")
|
||||
if code:
|
||||
return ReJson(0, ret)
|
||||
else:
|
||||
return ReJson(2001, body=ret)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ExportHtmlRequest(BaseModel):
|
||||
wxid: str
|
||||
|
||||
|
@ -367,7 +367,7 @@ class MainApi(BaseSubMainClass):
|
||||
def console_run():
|
||||
# 检查是否需要显示帮助信息
|
||||
if len(sys.argv) == 1:
|
||||
sys.argv.append(MainUi.mode)
|
||||
sys.argv.append(MainApi.mode)
|
||||
elif len(sys.argv) == 2 and sys.argv[1] not in models.keys():
|
||||
sys.argv.append('-h')
|
||||
main_parser.print_help()
|
||||
|
@ -59,6 +59,8 @@ class DBHandler(MicroHandler, MediaHandler, OpenIMContactHandler, PublicMsgHandl
|
||||
"talker": talker, "room_name": StrTalker, "msg": msg, "src": src, "extra": {},
|
||||
"CreateTime": CreateTime, }
|
||||
"""
|
||||
|
||||
|
||||
msgs0, wxid_list0 = self.get_msg_list(wxids=wxids, start_index=start_index, page_size=page_size,
|
||||
msg_type=msg_type,
|
||||
msg_sub_type=msg_sub_type, start_createtime=start_createtime,
|
||||
|
@ -103,9 +103,16 @@ class MsgHandler(DatabaseBase):
|
||||
f"{sql_sub_type}"
|
||||
f"{sql_start_createtime}"
|
||||
f"{sql_end_createtime}"
|
||||
f"ORDER BY CreateTime ASC LIMIT ?,?"
|
||||
f"ORDER BY CreateTime ASC LIMIT ? OFFSET ?"
|
||||
)
|
||||
param = param + (start_index, page_size)
|
||||
|
||||
param = param + ( page_size,start_index)
|
||||
# # 测试
|
||||
# print(sql + "\n" + " ".join([str(i) for i in param]))
|
||||
# print(sql + "\n" + " ".join([str(i) for i in param]))
|
||||
# print(sql + "\n" + " ".join([str(i) for i in param]))
|
||||
|
||||
|
||||
result = self.execute(sql, param)
|
||||
if not result:
|
||||
return [], []
|
||||
|
28
pywxdump/ui/.github/workflows/auto_build.yml
vendored
Normal file
28
pywxdump/ui/.github/workflows/auto_build.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: Build and Deploy Vue App
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- web
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build the app
|
||||
run: npm run build
|
||||
|
||||
- name: Cheak dist
|
||||
run: ls dist
|
34
pywxdump/ui/.gitignore
vendored
Normal file
34
pywxdump/ui/.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
tsconfig.app.tsbuildinfo
|
||||
tsconfig.node.tsbuildinfo
|
||||
|
||||
/public/data/
|
3
pywxdump/ui/.vscode/extensions.json
vendored
Normal file
3
pywxdump/ui/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
1
pywxdump/ui/README.md
Normal file
1
pywxdump/ui/README.md
Normal file
@ -0,0 +1 @@
|
||||
## 这是[PyWxDump](https://github.com/xaoyaoo/PyWxDump)的Web版,用于在浏览器中查看微信聊天记录。
|
6
pywxdump/ui/env.d.ts
vendored
Normal file
6
pywxdump/ui/env.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module 'vue3-markdown-it';
|
||||
declare module '@/utils/axios.js' {
|
||||
import http from '@/utils/axios.js';
|
||||
export default http;
|
||||
}
|
22
pywxdump/ui/index.html
Normal file
22
pywxdump/ui/index.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="./favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyWxDump</title>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="./data.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" style="height: 100%;"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
3075
pywxdump/ui/package-lock.json
generated
Normal file
3075
pywxdump/ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
pywxdump/ui/package.json
Normal file
40
pywxdump/ui/package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "wxdump_web",
|
||||
"version": "2.4.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/axios": "^0.14.0",
|
||||
"axios": "^1.6.3",
|
||||
"cors": "^2.8.5",
|
||||
"echarts": "^5.5.0",
|
||||
"echarts-wordcloud": "^2.1.0",
|
||||
"element-plus": "^2.4.4",
|
||||
"markdown-it": "^14.0.0",
|
||||
"v3-infinite-loading": "^1.3.1",
|
||||
"vue": "^3.3.11",
|
||||
"vue-echarts": "^6.6.9",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue3-markdown-it": "^1.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/markdown-it": "^13.0.7",
|
||||
"@types/node": "^18.19.3",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vue/tsconfig": "^0.5.0",
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"sass": "^1.69.7",
|
||||
"typescript": "~5.3.0",
|
||||
"vite": "^5.0.10",
|
||||
"vue-tsc": "^1.8.25"
|
||||
}
|
||||
}
|
5
pywxdump/ui/public/data.js
Normal file
5
pywxdump/ui/public/data.js
Normal file
@ -0,0 +1,5 @@
|
||||
localStorage.setItem('isUseLocalData', 'f') // 't' : 'f'
|
||||
const local_msg_count = 772
|
||||
const local_mywxid = ''
|
||||
const local_user_list = {}
|
||||
const local_msg_list = []
|
BIN
pywxdump/ui/public/favicon.ico
Normal file
BIN
pywxdump/ui/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 264 KiB |
221
pywxdump/ui/src/App.vue
Normal file
221
pywxdump/ui/src/App.vue
Normal file
@ -0,0 +1,221 @@
|
||||
<script setup lang="ts">
|
||||
import chatIcon from "@/assets/icon/ChatIcon.vue";
|
||||
import StatisticsIcon from "@/assets/icon/StatisticsIcon.vue";
|
||||
import CleanupIcon from "@/assets/icon/CleanupIcon.vue";
|
||||
import ToolsIcon from "@/assets/icon/ToolsIcon.vue";
|
||||
import AboutIcon from "@/assets/icon/AboutIcon.vue";
|
||||
import HelpIcon from "@/assets/icon/HelpIcon.vue";
|
||||
import SettingIcon from "@/assets/icon/SettingIcon.vue";
|
||||
// import CollapseIcon from "@/assets/icon/CollapseIcon.vue";
|
||||
import HomeIcon from "@/assets/icon/HomeIcon.vue";
|
||||
import ContactsIcon from "@/assets/icon/ContactsIcon.vue";
|
||||
import MomentsIcon from "@/assets/icon/MomentsIcon.vue";
|
||||
import FavoriteIcon from "@/assets/icon/FavoriteIcon.vue";
|
||||
import CollapseOpenIcon from "@/assets/icon/CollapseOpenIcon.vue";
|
||||
import CollapseCloseIcon from "@/assets/icon/CollapseCloseIcon.vue";
|
||||
|
||||
import {RouterLink, RouterView} from 'vue-router'
|
||||
import {ref, onMounted, withCtx, watch} from 'vue'
|
||||
import router from "@/router";
|
||||
import {is_db_init, is_use_local_data} from "@/utils/common_utils";
|
||||
import ChatRecordsMain from "@/components/chat/ChatRecordsMain.vue";
|
||||
|
||||
const isCollapse = ref(true);
|
||||
|
||||
const is_local_data = ref(true);
|
||||
|
||||
onMounted(() => {
|
||||
// localStorage.setItem('isDbInit', "t");
|
||||
is_local_data.value = localStorage.getItem('isUseLocalData') === 't';
|
||||
console.log("is_local_data", is_local_data.value);
|
||||
if(!is_local_data.value) {
|
||||
is_db_init();
|
||||
}
|
||||
})
|
||||
// watch(isDbInit, (val) => {
|
||||
// localStorage.setItem('isDbInit', val);
|
||||
// })
|
||||
|
||||
const handleOpen = (key: string, keyPath: string[]) => {
|
||||
// console.log(key, keyPath)
|
||||
}
|
||||
const handleClose = (key: string, keyPath: string[]) => {
|
||||
// console.log(key, keyPath)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="export-main" v-if="is_local_data">
|
||||
<chat-records-main wxid="wxid_test"/>
|
||||
</div>
|
||||
<div id="appbg" v-else>
|
||||
<el-container class="layout-container-demo" style="height: 100%;background:none;">
|
||||
<el-aside :width="isCollapse ? '64px' : '160px'">
|
||||
<el-container class="sidebar-container">
|
||||
<el-menu default-active="1" class="el-menu-vertical-demo" :collapse="isCollapse" :router='true'
|
||||
:collapse-transition="false" :show-timeout="0" :hide-timeout="0">
|
||||
|
||||
<el-radio-group v-model="isCollapse"
|
||||
style="margin-bottom: 20px;margin-top: 10px;margin-left: 10px;max-height: 30px">
|
||||
<el-radio-button :label="false" v-if="isCollapse">
|
||||
<collapse-open-icon></collapse-open-icon>
|
||||
</el-radio-button>
|
||||
<el-radio-button :label="true" v-else>
|
||||
<collapse-close-icon></collapse-close-icon>
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<el-menu-item index='/home'>
|
||||
<home-icon></home-icon>
|
||||
<template #title>首页</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index='/chat'>
|
||||
<chat-icon></chat-icon>
|
||||
<template #title>聊天查看</template>
|
||||
</el-menu-item>
|
||||
|
||||
<!-- <el-menu-item index='/contacts'>-->
|
||||
<!-- <contacts-icon></contacts-icon>-->
|
||||
<!-- <template #title>好友管理</template>-->
|
||||
<!-- </el-menu-item>-->
|
||||
<!-- <el-menu-item index='/moments'>-->
|
||||
<!-- <moments-icon></moments-icon>-->
|
||||
<!-- <template #title>朋友圈</template>-->
|
||||
<!-- </el-menu-item>-->
|
||||
<!-- <el-menu-item index='/favorite'>-->
|
||||
<!-- <favorite-icon></favorite-icon>-->
|
||||
<!-- <template #title>收藏管理</template>-->
|
||||
<!-- </el-menu-item>-->
|
||||
|
||||
<el-menu-item index='/statistics'>
|
||||
<statistics-icon></statistics-icon>
|
||||
<template #title>统计分析</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index='/cleanup'>
|
||||
<cleanup-icon></cleanup-icon>
|
||||
<template #title>文件清理</template>
|
||||
</el-menu-item>
|
||||
<el-sub-menu index='/tools'>
|
||||
<template #title>
|
||||
<tools-icon></tools-icon>
|
||||
<span>实用工具</span>
|
||||
</template>
|
||||
<el-menu-item index='/wxinfo'>账号信息</el-menu-item>
|
||||
<el-menu-item index='/bias'>基址偏移</el-menu-item>
|
||||
<el-menu-item index='/decrypt'>解密数据</el-menu-item>
|
||||
<el-menu-item index='/merge'>数据库合并</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
|
||||
<el-menu default-active="1" class="el-menu-vertical-demo" :collapse="isCollapse" @open="handleOpen"
|
||||
@close="handleClose" :router='true'>
|
||||
<el-menu-item index='/about'>
|
||||
<about-icon></about-icon>
|
||||
<template #title>关于我们</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index='/help'>
|
||||
<help-icon></help-icon>
|
||||
<template #title>帮助中心</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index='/setting'>
|
||||
<setting-icon></setting-icon>
|
||||
<template #title>更多设置</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-container>
|
||||
</el-aside>
|
||||
|
||||
<el-main>
|
||||
<RouterView/>
|
||||
</el-main>
|
||||
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.export-main {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#appbg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
header {
|
||||
line-height: 1.5;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
place-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: left;
|
||||
margin-left: -1rem;
|
||||
font-size: 1rem;
|
||||
|
||||
padding: 1rem 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-vertical-demo:not(.el-menu--collapse) {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.layout-container-demo .el-header {
|
||||
position: relative;
|
||||
background-color: var(--el-color-primary-light-7);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.layout-container-demo .el-aside {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.layout-container-demo .el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.layout-container-demo .el-main {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.layout-container-demo .toolbar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
62
pywxdump/ui/src/api/base.ts
Normal file
62
pywxdump/ui/src/api/base.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import http from "@/utils/axios.js";
|
||||
|
||||
|
||||
// const is_local_data = false;
|
||||
const is_local_data = localStorage.getItem('isUseLocalData') === 't';
|
||||
|
||||
export const apiVersion = () => {
|
||||
return http.get('/api/rs/version').then((res: any) => {
|
||||
return res;
|
||||
}).catch((err: any) => {
|
||||
console.log(err);
|
||||
return '';
|
||||
})
|
||||
}
|
||||
|
||||
export const api_db_init = async () => {
|
||||
const t = await http.get('/api/rs/is_init')
|
||||
console.log("is_db_init", !!t);
|
||||
return !!t;
|
||||
}
|
||||
|
||||
export const api_img = (url: string) => {
|
||||
if (is_local_data) {
|
||||
return `./imgsrc?src=${url}`;
|
||||
}
|
||||
return `/api/rs/imgsrc?src=${url}`;
|
||||
}
|
||||
export const api_audio = (url: string) => {
|
||||
if (is_local_data) {
|
||||
return `./audio?src=${url}`;
|
||||
}
|
||||
return `/api/rs/audio?src=${url}`;
|
||||
}
|
||||
|
||||
export const api_video = (url: string) => {
|
||||
if (is_local_data) {
|
||||
return `./video?src=${url}`;
|
||||
}
|
||||
return `/api/rs/video?src=${url}`;
|
||||
}
|
||||
|
||||
export const api_file = (url: string) => {
|
||||
if (is_local_data) {
|
||||
return `./file?src=${url}`;
|
||||
}
|
||||
return `/api/rs/file?src=${url}`;
|
||||
}
|
||||
|
||||
// file_info
|
||||
export const api_file_info = (url: string) => {
|
||||
if (is_local_data) {
|
||||
return `./file_info?src=${url}`;
|
||||
}
|
||||
return http.post('/api/rs/file_info', {
|
||||
'file_path': url,
|
||||
}).then((res: any) => {
|
||||
return res;
|
||||
}).catch((err: any) => {
|
||||
console.log(err);
|
||||
return '';
|
||||
})
|
||||
}
|
114
pywxdump/ui/src/api/chat.ts
Normal file
114
pywxdump/ui/src/api/chat.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import http from "@/utils/axios.js";
|
||||
import {ElNotification} from "element-plus";
|
||||
|
||||
const is_local_data = localStorage.getItem('isUseLocalData') === 't';
|
||||
// 编辑器禁用检查
|
||||
|
||||
const l_msg_count = local_msg_count
|
||||
const l_user_list = local_user_list
|
||||
const l_msg_list = local_msg_list
|
||||
const l_mywxid = local_mywxid
|
||||
|
||||
// user list 部分
|
||||
export const apiUserList = (word: string = '', wxids: string[] = [], labels: string[] = []) => {
|
||||
if (is_local_data) {
|
||||
return l_user_list;
|
||||
}
|
||||
return http.post('/api/rs/user_list', {
|
||||
'word': word,
|
||||
'wxids': wxids,
|
||||
'labels': labels
|
||||
}).then((res: any) => {
|
||||
return res;
|
||||
}).catch((err: any) => {
|
||||
console.log(err);
|
||||
return '';
|
||||
})
|
||||
}
|
||||
|
||||
export const apiUserSessionList = () => {
|
||||
return http.post('/api/rs/user_session_list', {})
|
||||
.then((res: any) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log(err);
|
||||
return [];
|
||||
})
|
||||
}
|
||||
export const apiMyWxid = () => {
|
||||
if (is_local_data) {
|
||||
return l_mywxid;
|
||||
}
|
||||
return http.get('/api/rs/mywxid').then((res: any) => {
|
||||
return res.my_wxid;
|
||||
}).catch((err: any) => {
|
||||
console.log(err);
|
||||
return '';
|
||||
})
|
||||
}
|
||||
|
||||
// 消息部分
|
||||
|
||||
export const apiRealTime = () => {
|
||||
return http.post('/api/ls/realtimemsg', {}).then((res: any) => {
|
||||
console.log(res);
|
||||
// 滚动消息提醒
|
||||
ElNotification({
|
||||
title: 'Success',
|
||||
message: '获取实时消息成功!',
|
||||
type: 'success',
|
||||
})
|
||||
return true;
|
||||
}).catch((err: any) => {
|
||||
console.log(err);
|
||||
ElNotification({
|
||||
title: 'Error',
|
||||
message: '获取实时消息失败!',
|
||||
type: 'error',
|
||||
})
|
||||
return false;
|
||||
})
|
||||
}
|
||||
|
||||
export const apiMsgCount = (wxids: string[]) => {
|
||||
return http.post('/api/rs/msg_count', {
|
||||
"wxids": wxids
|
||||
}).then((res: any) => {
|
||||
return res;
|
||||
}).catch((err: any) => {
|
||||
console.log(err);
|
||||
return '';
|
||||
})
|
||||
}
|
||||
export const apiMsgCountSolo = (wxid: string) => {
|
||||
if (is_local_data) {
|
||||
return l_msg_count;
|
||||
}
|
||||
return apiMsgCount([wxid]).then((res: any) => {
|
||||
return res[wxid] || 0;
|
||||
}).catch((err: any) => {
|
||||
console.log(err);
|
||||
return 0;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const apiMsgs = (wxid: string, start: number, limit: number) => {
|
||||
if (is_local_data) {
|
||||
return {
|
||||
'msg_list': l_msg_list || [],
|
||||
'user_list': l_user_list || [],
|
||||
}
|
||||
}
|
||||
return http.post('/api/rs/msg_list', {
|
||||
'start': start,
|
||||
'limit': limit,
|
||||
'wxid': wxid,
|
||||
}).then((res: any) => {
|
||||
return res;
|
||||
}).catch((err: any) => {
|
||||
console.log(err);
|
||||
return '';
|
||||
})
|
||||
}
|
40
pywxdump/ui/src/api/stat.ts
Normal file
40
pywxdump/ui/src/api/stat.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import http from "@/utils/axios.js";
|
||||
|
||||
const is_local_data = localStorage.getItem('isUseLocalData') === 't';
|
||||
// user list 部分
|
||||
export const apiDateCount = (wxid: string = '', start_time: number = 0, end_time: number = 0) => {
|
||||
return http.post('/api/rs/date_count', {
|
||||
'wxid': wxid,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
}).then((res: any) => {
|
||||
return res;
|
||||
}).catch((err: any) => {
|
||||
console.log(err);
|
||||
return '';
|
||||
})
|
||||
}
|
||||
export const apiTalkerCount = (top: number = 10, start_time: number = 0, end_time: number = 0) => {
|
||||
return http.post('/api/rs/top_talker_count', {
|
||||
'top': top,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
}).then((res: any) => {
|
||||
return res;
|
||||
}).catch((err: any) => {
|
||||
console.log(err);
|
||||
return '';
|
||||
})
|
||||
}
|
||||
|
||||
export const apiWordcloud = (target: string = "") => {
|
||||
return http.post('/api/rs/wordcloud', {
|
||||
target: target,
|
||||
}).then((res: any) => {
|
||||
return res;
|
||||
}).catch((err: any) => {
|
||||
console.log(err);
|
||||
return '';
|
||||
})
|
||||
}
|
||||
|
86
pywxdump/ui/src/assets/base.css
Normal file
86
pywxdump/ui/src/assets/base.css
Normal file
@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
} */
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
10
pywxdump/ui/src/assets/icon/AboutIcon.vue
Normal file
10
pywxdump/ui/src/assets/icon/AboutIcon.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1704181424728" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="11271" width="200" height="200">
|
||||
<path
|
||||
d="M491.776 352c12.864 0 52.48-6.144 65.152-18.304a61.056 61.056 0 0 0 18.88-45.504 61.248 61.248 0 0 0-18.88-45.184 62.464 62.464 0 0 0-45.248-18.56 61.312 61.312 0 0 0-44.48 18.304 61.312 61.312 0 0 0-18.688 45.44c0 18.432 6.336 33.664 18.88 45.76 12.608 12.032 11.904 18.048 24.384 18.048zM512 32C246.912 32 32 246.848 32 512s214.912 480 480 480c265.152 0 480-214.848 480-480S777.152 32 512 32z m0 896a416 416 0 1 1 0-832 416 416 0 0 1 0 832z m39.488-278.528l-14.912 14.848 39.744-210.816c0.256-1.408-0.192-2.816-0.128-4.224v-0.64a30.592 30.592 0 0 0-9.408-22.72l-0.256-0.448c-0.192-0.192-0.448-0.192-0.64-0.384a33.28 33.28 0 0 0-12.992-7.424c-1.664-0.512-3.008-1.472-4.8-1.728-1.216-0.192-2.432 0.128-3.712 0.064l-3.136 0.064a34.24 34.24 0 0 0-29.504 17.216L425.472 519.488a33.152 33.152 0 1 0 47.04 46.976l14.848-14.848-39.68 210.816c-0.256 1.472 0.192 2.816 0.128 4.288v0.64a30.272 30.272 0 0 0 9.408 22.72l0.256 0.384 0.64 0.448a35.456 35.456 0 0 0 12.992 7.36c1.6 0.512 2.944 1.408 4.736 1.728 1.28 0.192 2.496-0.192 3.776-0.064l3.136-0.128a34.048 34.048 0 0 0 29.504-17.152l86.272-86.272a33.152 33.152 0 1 0-47.04-46.912z"
|
||||
fill="" p-id="11272"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
13
pywxdump/ui/src/assets/icon/ChatIcon.vue
Normal file
13
pywxdump/ui/src/assets/icon/ChatIcon.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1722526818775" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="1411" width="200" height="200">
|
||||
<path
|
||||
d="M129.792 887.168c-2.816 0-5.76-0.512-8.576-1.536-10.24-3.584-17.024-13.312-17.024-24.064V727.552H77.696C34.816 727.552 0 688.64 0 640.768V150.784C0 102.912 34.816 64 77.696 64h625.408C745.984 64 780.8 102.912 780.8 150.784v489.856c0 47.872-34.816 86.784-77.696 86.784H601.6c-14.08 0-25.6-11.52-25.6-25.6s11.52-25.6 25.6-25.6h101.504c14.336 0 26.496-16.384 26.496-35.584V150.784C729.6 131.456 717.44 115.2 703.104 115.2H77.696C63.36 115.2 51.2 131.456 51.2 150.784v489.856c0 19.328 12.16 35.584 26.496 35.584h52.096c14.08 0 25.6 11.52 25.6 25.6v87.808l84.864-103.936c4.864-6.016 12.16-9.472 19.84-9.472h163.328c14.08 0 25.6 11.52 25.6 25.6s-11.52 25.6-25.6 25.6H272.256l-122.624 150.144c-4.864 6.272-12.288 9.6-19.84 9.6z"
|
||||
p-id="1412"></path>
|
||||
<path
|
||||
d="M928.896 960c-6.656 0-13.312-2.688-18.176-7.552l-79.36-79.872H546.688c-33.28 0-60.288-27.136-60.288-60.544V532.352c0-30.592 23.808-60.544 48.128-60.544h131.968c14.08 0 25.6 11.52 25.6 25.6s-11.52 25.6-25.6 25.6h-125.44c-1.536 2.176-3.456 5.76-3.456 9.344v279.68c0 5.12 4.096 9.344 9.088 9.344h295.296c6.784 0 13.312 2.688 18.176 7.552l43.136 43.392v-25.344c0-14.08 11.52-25.6 25.6-25.6h34.688c4.992 0 9.088-4.224 9.088-9.344V532.352c0-5.12-4.096-9.344-9.088-9.344h-119.808c-14.08 0-25.6-11.52-25.6-25.6s11.52-25.6 25.6-25.6h119.808c33.28 0 60.288 27.136 60.288 60.544v279.68c0 33.408-27.008 60.544-60.288 60.544h-9.088V934.4a25.6384 25.6384 0 0 1-25.6 25.6z"
|
||||
p-id="1413"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
10
pywxdump/ui/src/assets/icon/CleanupIcon.vue
Normal file
10
pywxdump/ui/src/assets/icon/CleanupIcon.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1713444801401" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="4641" width="200" height="200">
|
||||
<path
|
||||
d="M426.688 94.144a32 32 0 0 0-32 32v138.688h-288a32 32 0 0 0-32 32v170.688a32 32 0 0 0 32 32h32v353.792a32 32 0 0 0 32 32h682.624a32 32 0 0 0 32-32V499.52h32a32 32 0 0 0 32-32V296.832a32 32 0 0 0-32-32h-288V126.144a32 32 0 0 0-32-32H426.688z m394.624 727.168h-106.624v-97.792a32 32 0 1 0-64 0v97.792H544V723.2a32 32 0 1 0-64 0v98.176H373.312v-97.792a32 32 0 1 0-64 0v97.792H202.688v-320h618.624v320zM458.688 296.832V158.144h106.624v138.688a32 32 0 0 0 32 32h288v106.688H138.688V328.832h288a32 32 0 0 0 32-32z"
|
||||
fill="#333333" p-id="4642"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
10
pywxdump/ui/src/assets/icon/CollapseCloseIcon.vue
Normal file
10
pywxdump/ui/src/assets/icon/CollapseCloseIcon.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1722527189048" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="2087" width="200" height="200">
|
||||
<path
|
||||
d="M813.056 1024c-10.5472 0-21.1968-3.1744-29.184-9.4208L163.84 534.6304c-16.1792-12.4928-16.1792-32.768 0-45.2608L783.7696 9.4208c16.1792-12.4928 42.2912-12.4928 58.4704 0s16.1792 32.768 0 45.2608L251.4944 512l590.6432 457.4208c16.1792 12.4928 16.1792 32.768 0 45.2608-7.9872 6.144-18.5344 9.3184-29.0816 9.3184z"
|
||||
p-id="2088"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
13
pywxdump/ui/src/assets/icon/CollapseIcon.vue
Normal file
13
pywxdump/ui/src/assets/icon/CollapseIcon.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1704182542260" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="16755" width="200" height="200">
|
||||
<path d="M896 0l128 0 0 128-128 0 0-128Z" fill="#1296db" p-id="16756"></path>
|
||||
<path d="M0 0l704 0 0 128-704 0 0-128Z" fill="#1296db" p-id="16757"></path>
|
||||
<path d="M896 448l128 0 0 128-128 0 0-128Z" fill="#1296db" p-id="16758"></path>
|
||||
<path d="M0 448l704 0 0 128-704 0 0-128Z" fill="#1296db" p-id="16759"></path>
|
||||
<path d="M896 896l128 0 0 128-128 0 0-128Z" fill="#1296db" p-id="16760"></path>
|
||||
<path d="M0 896l704 0 0 128-704 0 0-128Z" fill="#1296db" p-id="16761"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
10
pywxdump/ui/src/assets/icon/CollapseOpenIcon.vue
Normal file
10
pywxdump/ui/src/assets/icon/CollapseOpenIcon.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1722527233092" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="2249" width="200" height="200">
|
||||
<path
|
||||
d="M193.1264 1024c10.5472 0 21.1968-3.1744 29.184-9.4208l619.9296-479.9488c16.1792-12.4928 16.1792-32.768 0-45.2608L222.3104 9.4208C206.1312-3.072 180.0192-3.072 163.84 9.4208s-16.1792 32.768 0 45.2608L754.5856 512 163.84 969.4208c-16.1792 12.4928-16.1792 32.768 0 45.2608 8.0896 6.144 18.6368 9.3184 29.2864 9.3184z"
|
||||
p-id="2250"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
20
pywxdump/ui/src/assets/icon/ContactsIcon.vue
Normal file
20
pywxdump/ui/src/assets/icon/ContactsIcon.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1722526340718" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="3332" width="200" height="200">
|
||||
<path
|
||||
d="M394 108.1c-130.9 0-237 106.1-237 237s106.1 237 237 237 237-106.1 237-237-106.1-237-237-237z m0 391.3c-85.2 0-154.3-69.1-154.3-154.3S308.8 190.8 394 190.8s154.3 69.1 154.3 154.3S479.2 499.4 394 499.4z"
|
||||
fill="#242424" p-id="3333"></path>
|
||||
<path
|
||||
d="M393.5 581c151.1 0 273.6 122.5 273.6 273.6H749c0-196.3-159.2-355.5-355.5-355.5S38 658.3 38 854.6h81.9c0-151.1 122.5-273.6 273.6-273.6z"
|
||||
fill="#242424" p-id="3334"></path>
|
||||
<path d="M78.9 855.4m-40.9 0a40.9 40.9 0 1 0 81.8 0 40.9 40.9 0 1 0-81.8 0Z" fill="#242424" p-id="3335"></path>
|
||||
<path d="M708.1 855.4m-40.9 0a40.9 40.9 0 1 0 81.8 0 40.9 40.9 0 1 0-81.8 0Z" fill="#242424" p-id="3336"></path>
|
||||
<path d="M663.4 541.2m-40.9 0a40.9 40.9 0 1 0 81.8 0 40.9 40.9 0 1 0-81.8 0Z" fill="#242424" p-id="3337"></path>
|
||||
<path d="M643.1 149.2m-40.9 0a40.9 40.9 0 1 0 81.8 0 40.9 40.9 0 1 0-81.8 0Z" fill="#242424" p-id="3338"></path>
|
||||
<path
|
||||
d="M989 854.6c0-143.7-85.3-267.5-208-323.6 54.8-43.4 90-110.6 90-185.9 0-127.8-101.2-232-227.9-236.8v81.8c81 4.7 145.2 72.9 145.2 155 0 78.8-56.1 146.7-132.3 156.1-5.6 0.7-12.9 7.4-12.9 13v60.6c0-20 3.1-0.6 7.2-0.3l5 7c143.3 8.7 251.8 127.6 251.8 273.1h0.1v0.8c0 22.6 18.3 40.9 40.9 40.9S989 878 989 855.4v-0.8z"
|
||||
fill="#242424" p-id="3339"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
25
pywxdump/ui/src/assets/icon/FavoriteIcon.vue
Normal file
25
pywxdump/ui/src/assets/icon/FavoriteIcon.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<!-- <svg t="1722527105629" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"-->
|
||||
<!-- p-id="1917" width="200" height="200">-->
|
||||
<!-- <path-->
|
||||
<!-- d="M637.184 998.4h-485.12c-13.312 0-24.064-10.88-24.064-24.32V49.92c0-13.44 10.752-24.32 24.064-24.32s24.064 10.88 24.064 24.32v899.84h461.184c13.312 0 24.064 10.88 24.064 24.32-0.128 13.44-10.88 24.32-24.192 24.32z"-->
|
||||
<!-- p-id="1918"></path>-->
|
||||
<!-- <path-->
|
||||
<!-- d="M872.064 779.52c-13.312 0-24.064-10.88-24.064-24.32V74.24H308.992v105.344h445.568c13.312 0 24.064 10.88 24.064 24.32s-10.752 24.32-24.064 24.32H285.056c-13.312 0-24.064-10.88-24.064-24.32V49.92c0-13.44 10.752-24.32 24.064-24.32h587.008C885.248 25.6 896 36.48 896 49.92V755.2c0 13.44-10.752 24.32-23.936 24.32z"-->
|
||||
<!-- p-id="1919"></path>-->
|
||||
<!-- <path-->
|
||||
<!-- d="M695.936 382.336h-410.88c-13.312 0-24.064-10.88-24.064-24.32 0-13.44 10.752-24.32 24.064-24.32h410.88c13.312 0 24.064 10.88 24.064 24.32-0.128 13.44-10.88 24.32-24.064 24.32zM578.56 536.32H285.056c-13.312 0-24.064-10.88-24.064-24.32s10.752-24.32 24.064-24.32H578.56c13.312 0 24.064 10.88 24.064 24.32s-10.88 24.32-24.064 24.32zM461.184 690.304H285.056c-13.312 0-24.064-10.88-24.064-24.32s10.752-24.32 24.064-24.32h176.128c13.312 0 24.064 10.88 24.064 24.32s-10.88 24.32-24.064 24.32zM638.72 996.48c-3.456 0-6.912-0.768-10.24-2.176a25.9328 25.9328 0 0 1-15.232-23.68V755.2c0-13.44 10.752-24.32 24.064-24.32h232.832c9.984 0 19.456 6.656 23.04 16 3.712 9.728 1.152 20.608-6.4 27.648l-230.656 215.04c-4.992 4.48-11.136 6.912-17.408 6.912z m22.528-216.96v139.136L810.496 779.52H661.248z"-->
|
||||
<!-- p-id="1920"></path>-->
|
||||
<!-- </svg>-->
|
||||
<svg t="1722527337412" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="2411" width="200" height="200">
|
||||
<path
|
||||
d="M512 985.6c-7.296 0-14.464-3.072-19.584-9.088l-384-454.272c-92.928-109.952-92.928-289.024 0-398.976C154.752 68.48 216.576 38.4 282.624 38.4s128 30.08 174.208 84.864l146.304 173.184a25.6 25.6 0 0 1-3.072 36.096 25.6 25.6 0 0 1-36.096-3.072L417.792 156.288C381.44 113.28 333.44 89.6 282.624 89.6c-50.688 0-98.688 23.68-135.04 66.688-77.568 91.776-77.568 241.152 0 332.928l384 454.272A25.6 25.6 0 0 1 512 985.6z"
|
||||
p-id="2412"></path>
|
||||
<path
|
||||
d="M512 985.6c-5.888 0-11.648-2.048-16.512-6.016-10.752-9.088-12.16-25.216-3.072-36.096l384-454.272c77.568-91.776 77.568-241.152 0-332.928C840.064 113.28 792.064 89.6 741.376 89.6c-50.816 0-98.688 23.68-135.04 66.688-9.088 10.752-25.216 12.16-36.096 3.072s-12.16-25.216-3.072-36.096C613.376 68.48 675.2 38.4 741.376 38.4c66.048 0 128 30.08 174.208 84.864 92.928 109.952 92.928 289.024 0 398.976l-384 454.272c-5.12 6.016-12.288 9.088-19.584 9.088z"
|
||||
p-id="2413"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
13
pywxdump/ui/src/assets/icon/HelpIcon.vue
Normal file
13
pywxdump/ui/src/assets/icon/HelpIcon.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1704181514907" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="13165" width="200" height="200">
|
||||
<path
|
||||
d="M512 120C296 120 120 296 120 512s176 392 392 392 392-176 392-392S728 120 512 120z m0 736c-190.4 0-344-153.6-344-344S321.6 168 512 168 856 321.6 856 512 702.4 856 512 856z"
|
||||
p-id="13166"></path>
|
||||
<path
|
||||
d="M467.2 656h94.4v91.2h-94.4zM624 313.6c-32-22.4-70.4-33.6-118.4-33.6-35.2 0-65.6 8-91.2 24-38.4 25.6-59.2 67.2-62.4 126.4h91.2c0-17.6 4.8-33.6 14.4-49.6 9.6-16 27.2-24 51.2-24 24 0 41.6 6.4 51.2 19.2 9.6 12.8 14.4 27.2 14.4 43.2 0 14.4-4.8 27.2-12.8 38.4-4.8 6.4-11.2 12.8-17.6 19.2l-22.4 17.6C499.2 512 484.8 528 480 540.8s-8 38.4-9.6 73.6h84.8c0-16 1.6-28.8 4.8-36.8 3.2-12.8 11.2-24 24-33.6l22.4-17.6c22.4-17.6 38.4-32 46.4-43.2 12.8-19.2 20.8-41.6 20.8-68.8-3.2-44.8-17.6-76.8-49.6-100.8z"
|
||||
p-id="13167"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
5
pywxdump/ui/src/assets/icon/HomeIcon.vue
Normal file
5
pywxdump/ui/src/assets/icon/HomeIcon.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1722526872404" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1580" width="200" height="200"><path d="M852.096 1024H612.224c-16.128 0-29.312-14.336-29.312-32s13.056-32 29.312-32h210.56V471.296c0-17.664 13.056-32 29.312-32h62.592L518.912 73.6 111.104 439.296h60.8c16.128 0 29.312 14.336 29.312 32V960H409.6c16.128 0 29.312 14.336 29.312 32S425.856 1024 409.6 1024H171.904c-16.128 0-29.312-14.336-29.312-32V503.296H29.312c-12.416 0-23.424-8.448-27.52-21.248-4.096-12.672-0.512-27.008 8.96-35.584L500.608 7.296c10.88-9.728 26.624-9.6 37.376 0.256l475.52 439.296c9.344 8.704 12.8 22.784 8.576 35.456-4.224 12.672-15.232 20.992-27.52 20.992h-113.408V992c0.256 17.664-12.928 32-29.056 32z" p-id="1581"></path><path d="M612.224 1024h-0.256c-16.128-0.128-29.184-14.592-29.056-32.256l1.92-277.888H453.504c-16.128 0-29.312-14.336-29.312-32s13.056-32 29.312-32H614.4c7.808 0 15.232 3.456 20.736 9.472 5.504 6.016 8.576 14.208 8.448 22.784l-2.176 310.144c0 17.536-13.056 31.744-29.184 31.744z" p-id="1582"></path></svg>
|
||||
</el-icon>
|
||||
</template>
|
19
pywxdump/ui/src/assets/icon/MomentsIcon.vue
Normal file
19
pywxdump/ui/src/assets/icon/MomentsIcon.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1722526765465" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="6678" width="200" height="200">
|
||||
<path
|
||||
d="M512 1024C229.6832 1024 0 794.3168 0 512S229.6832 0 512 0s512 229.6832 512 512-229.6832 512-512 512z m0-958.5664C263.168 65.4336 60.7232 265.728 60.7232 512S263.168 958.5664 512 958.5664 963.2768 758.272 963.2768 512 760.832 65.4336 512 65.4336z"
|
||||
p-id="6679"></path>
|
||||
<path
|
||||
d="M512 704.512c-105.2672 0-190.8736-86.3232-190.8736-192.512 0-106.0864 85.6064-192.4096 190.8736-192.4096S702.8736 405.9136 702.8736 512 617.2672 704.512 512 704.512z m0-319.5904c-71.7824 0-130.2528 57.0368-130.2528 127.0784S440.1152 639.0784 512 639.0784c71.7824 0 130.2528-57.0368 130.2528-127.0784S583.7824 384.9216 512 384.9216z"
|
||||
p-id="6680"></path>
|
||||
<path
|
||||
d="M961.536 384.9216H512c-16.7936 0-30.3104-14.6432-30.3104-32.6656s13.6192-32.6656 30.3104-32.6656h449.536c16.7936 0 30.3104 14.6432 30.3104 32.6656s-13.5168 32.6656-30.3104 32.6656zM672.5632 992.0512c-16.7936 0-30.3104-14.6432-30.3104-32.6656V512c0-18.0224 13.6192-32.6656 30.3104-32.6656 16.7936 0 30.3104 14.6432 30.3104 32.6656v447.3856c0 18.0224-13.5168 32.6656-30.3104 32.6656zM512 704.512H78.5408c-16.7936 0-30.3104-14.6432-30.3104-32.6656s13.6192-32.6656 30.3104-32.6656H512c16.7936 0 30.3104 14.6432 30.3104 32.6656S528.7936 704.512 512 704.512zM351.4368 544.6656c-16.7936 0-30.3104-14.6432-30.3104-32.6656V80.5888c0-18.0224 13.6192-32.6656 30.3104-32.6656 16.7936 0 30.3104 14.6432 30.3104 32.6656V512c0 18.0224-13.5168 32.6656-30.3104 32.6656z"
|
||||
p-id="6681"></path>
|
||||
<path
|
||||
d="M385.024 445.1328c-8.192 0-16.2816-3.584-22.3232-10.5472-11.3664-13.312-10.6496-33.9968 1.7408-46.1824L682.2912 72.0896c12.288-12.1856 31.5392-11.3664 42.9056 1.8432 11.3664 13.312 10.6496 33.9968-1.7408 46.1824L405.6064 436.4288c-5.8368 5.8368-13.2096 8.704-20.5824 8.704zM945.4592 745.984c-7.3728 0-14.7456-2.8672-20.5824-8.704L607.0272 420.9664c-12.288-12.288-13.1072-32.9728-1.7408-46.1824 11.3664-13.312 30.5152-14.1312 42.9056-1.8432l317.8496 316.3136c12.288 12.288 13.1072 32.9728 1.7408 46.1824-5.9392 6.9632-14.1312 10.5472-22.3232 10.5472zM305.5616 976.0768c-8.192 0-16.2816-3.4816-22.3232-10.5472-11.3664-13.312-10.6496-33.9968 1.7408-46.1824l306.4832-305.0496c12.288-12.1856 31.5392-11.4688 42.9056 1.8432 11.3664 13.312 10.6496 33.9968-1.7408 46.1824L326.144 967.3728c-5.8368 5.8368-13.2096 8.704-20.5824 8.704zM385.024 671.0272c-7.3728 0-14.7456-2.8672-20.5824-8.704L57.9584 357.376c-12.288-12.288-13.1072-32.9728-1.7408-46.1824 11.3664-13.312 30.5152-14.1312 42.9056-1.8432L405.6064 614.4c12.288 12.288 13.1072 32.9728 1.7408 46.1824-6.0416 6.9632-14.1312 10.4448-22.3232 10.4448z"
|
||||
p-id="6682"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
13
pywxdump/ui/src/assets/icon/SettingIcon.vue
Normal file
13
pywxdump/ui/src/assets/icon/SettingIcon.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1704181557524" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="14155" width="200" height="200">
|
||||
<path
|
||||
d="M913.94086 431.828053c-4.878101-25.783223-20.693298-42.455951-40.947598-42.455951l-3.511987 0c-54.731532 0-99.248422-44.515866-99.248422-99.262748 0-17.294898 8.299013-37.015032 8.619308-37.798884 10.097986-22.722514 2.349511-50.567699-18.078751-64.859193l-102.701057-57.183374-1.509377-0.738827c-20.545942-8.909927-48.667419-3.207042-63.987337 12.753465-11.086499 11.434423-49.306986 44.037982-78.471213 44.037982-29.543873 0-67.849294-33.257451-78.992075-44.908816-15.293311-16.077164-43.12417-22.112624-63.902402-13.218046l-106.35733 58.272171-1.596358 1.016143c-20.430308 14.234189-28.207435 42.078351-18.165732 64.713884 0.346901 0.827855 8.676613 20.387329 8.676613 37.914518 0 54.746882-44.51689 99.262748-99.247398 99.262748l-4.149507 0c-19.617803 0-35.434024 16.671705-40.309054 42.455951-0.363274 1.814322-8.590656 45.446052-8.590656 80.429821 0 34.938744 8.227382 78.555124 8.590656 80.355119 4.875031 25.799596 20.691251 42.48665 40.946574 42.48665l3.511987 0c54.730509 0 99.247398 44.51689 99.247398 99.247398 0 17.411555-8.328689 37.058011-8.647961 37.812187-10.069333 22.766516-2.349511 50.567699 18.021445 64.787562l100.756775 56.531528 1.538029 0.696872c20.836561 9.17087 49.01432 3.191692 64.250326-13.464663 14.07353-15.207353 52.207036-46.782489 80.20981-46.782489 30.354332 0 69.445652 35.347043 80.706113 47.76691 10.387581 11.376095 26.349111 18.22713 42.687218 18.22713 7.631818 0 14.857383-1.511423 21.474081-4.354168l104.4724-57.574277 1.538029-0.989537c20.428262-14.276145 28.206412-42.077328 18.138102-64.727187-0.348947-0.842181-8.677637-20.402679-8.677637-37.928844 0-54.730509 44.51689-99.247398 99.248422-99.247398l4.093225 0c19.644409 0 35.488259-16.687054 40.365336-42.48665 0.347924-1.799996 8.588609-45.416376 8.588609-80.355119C922.529469 477.274104 914.288784 433.642374 913.94086 431.828053M862.982258 512.257873c0 22.605857-4.498454 51.655474-6.559393 63.785745-82.09781 6.732331-145.738245 75.335802-145.738245 158.303422 0 23.419386 7.430226 45.851281 11.377118 56.169277l-89.12076 49.216935c-4.38282-4.584412-17.325597-17.644869-34.939767-30.762631-30.93557-22.925129-60.595077-34.635845-88.106664-34.635845-27.278273 0-56.703443 11.420097-87.493703 34.05563-17.528212 12.768815-30.296003 25.479301-34.765805 30.18037l-85.724407-47.997154c4.179183-10.839883 11.405771-32.982182 11.405771-56.226582 0-82.96762-63.640436-151.571091-145.70857-158.303422-2.089591-12.130272-6.588045-41.178865-6.588045-63.785745 0-22.650883 4.498454-51.713802 6.588045-63.844074 82.068134-6.718005 145.70857-75.335802 145.70857-158.303422 0-23.288402-7.429203-45.792952-11.376095-56.095599l91.325985-50.190099c3.975545 3.976568 17.005302 16.730033 34.82311 29.411867 30.355355 21.663392 59.260685 32.633235 86.016049 32.633235 26.494421 0 55.19509-10.766205 85.346807-32.009018 17.963117-12.623505 30.964222-25.203008 34.736129-28.757974l87.900979 48.825009c-3.975545 10.244318-11.405771 32.676214-11.405771 56.18258 0 82.96762 63.640436 151.585417 145.738245 158.303422C858.483804 460.5717 862.982258 489.7523 862.982258 512.257873"
|
||||
fill="#231F20" p-id="14156"></path>
|
||||
<path
|
||||
d="M510.215866 365.633445c-80.530105 0-146.056494 65.527412-146.056494 146.057517 0 80.543408 65.527412 146.043191 146.056494 146.043191 80.530105 0 146.057517-65.499783 146.057517-146.043191C656.273383 431.160857 590.74597 365.633445 510.215866 365.633445M596.725148 511.690962c0 47.693232-38.799678 86.491887-86.509283 86.491887-47.708582 0-86.479607-38.798655-86.479607-86.491887 0-47.665603 38.771025-86.479607 86.479607-86.479607C557.925471 425.212378 596.725148 464.025359 596.725148 511.690962"
|
||||
fill="#231F20" p-id="14157"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
13
pywxdump/ui/src/assets/icon/StatisticsIcon.vue
Normal file
13
pywxdump/ui/src/assets/icon/StatisticsIcon.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1704181237272" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="10274" width="200" height="200">
|
||||
<path
|
||||
d="M833.6 867.2H192.4c-18.5 0-33.4-15-33.4-33.4V192.6c0-18.5 15-33.4 33.4-33.4s33.4 15 33.4 33.4v607.7h607.7c18.5 0 33.4 15 33.4 33.4 0.1 18.5-14.9 33.5-33.3 33.5z"
|
||||
fill="#202A35" p-id="10275"></path>
|
||||
<path
|
||||
d="M281.6 711.1c-8.6 0-17.1-3.3-23.6-9.8-13.1-13.1-13.1-34.2 0-47.3l164.5-164.5c13-13 34-13 47-0.3l98.3 96.1 171.8-181.6c12.7-13.4 33.9-14 47.3-1.3 13.4 12.7 14 33.9 1.3 47.3L593 656c-6.2 6.5-14.7 10.3-23.6 10.5-8.8 0-17.6-3.3-24-9.5l-99-96.8-141.1 141.1c-6.5 6.5-15.1 9.8-23.7 9.8z"
|
||||
fill="#202A35" p-id="10276"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
10
pywxdump/ui/src/assets/icon/ToolsIcon.vue
Normal file
10
pywxdump/ui/src/assets/icon/ToolsIcon.vue
Normal file
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-icon>
|
||||
<svg t="1704181120854" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="9265" width="200" height="200">
|
||||
<path
|
||||
d="M166.2976 995.1232c-36.2496 0-70.2464-14.1312-95.8464-39.7312-52.8384-52.8384-52.8384-139.0592 0-191.8976l242.4832-242.4832c6.7584-6.7584 15.7696-10.4448 25.3952-10.4448s18.6368 3.6864 25.3952 10.4448c2.048 2.048 4.7104 3.072 7.3728 3.072 1.6384 0 4.5056-0.4096 6.9632-2.8672 2.048-2.048 3.2768-4.7104 3.2768-7.3728 0-1.4336-0.4096-4.5056-2.8672-6.9632-6.9632-6.9632-10.6496-15.7696-10.6496-25.3952s3.6864-18.6368 10.4448-25.3952l75.9808-75.9808c13.9264-13.9264 36.6592-13.9264 50.5856 0l28.8768 28.8768L804.6592 137.6256l12.4928-46.2848c4.7104-17.408 17.8176-30.72 35.4304-35.84l74.5472-21.0944c13.1072-4.3008 27.0336-1.024 36.864 8.6016l18.6368 18.6368c9.8304 9.8304 13.1072 23.9616 8.3968 37.0688l-21.0944 74.1376c-5.12 17.408-18.0224 30.3104-35.4304 35.4304h-0.4096l-46.2848 12.4928-270.9504 271.1552 28.8768 28.8768c6.7584 6.7584 10.4448 15.7696 10.4448 25.3952s-3.6864 18.6368-10.4448 25.3952l-75.9808 75.9808c-6.7584 6.7584-15.7696 10.4448-25.3952 10.4448s-18.6368-3.6864-25.3952-10.4448c-2.048-2.048-4.5056-3.072-7.3728-3.072-1.6384 0-4.5056 0.4096-6.9632 2.8672l-0.2048 0.2048c-2.4576 2.4576-2.8672 5.5296-2.8672 7.168s0.4096 4.5056 2.8672 7.168c14.1312 14.1312 14.1312 36.864 0.2048 50.7904L262.3488 955.392c-25.8048 25.6-59.8016 39.7312-96.0512 39.7312z m172.2368-441.7536L99.5328 792.3712c-36.864 36.864-36.864 97.0752 0 134.144 17.8176 17.8176 41.5744 27.8528 66.7648 27.8528h0.2048c25.1904 0 49.152-9.8304 66.9696-27.8528l239.0016-239.2064c-7.5776-9.0112-11.6736-20.2752-11.6736-32.3584 0-13.7216 5.3248-26.624 15.1552-36.2496 9.6256-9.6256 22.3232-14.9504 35.84-15.1552h0.2048c11.8784 0 23.3472 4.096 32.3584 11.6736l68.8128-68.8128-54.0672-54.4768L866.5088 183.9104l56.7296-15.36c3.8912-1.2288 6.3488-3.4816 7.3728-7.168l20.8896-73.1136-14.1312-14.1312-73.3184 20.8896c-3.6864 1.024-6.144 3.4816-7.168 7.168l-15.36 56.9344-307.8144 307.4048-54.272-54.272-68.8128 69.0176c7.3728 9.0112 11.4688 20.2752 11.6736 32.1536 0 13.7216-5.3248 26.624-15.1552 36.4544-9.4208 9.6256-22.3232 14.9504-35.84 15.1552h-0.2048c-12.0832-0.2048-23.3472-4.3008-32.5632-11.6736zM826.7776 994.9184c-42.5984 0-85.1968-16.1792-117.76-48.7424-35.6352-35.6352-53.0432-84.7872-47.9232-134.9632 1.4336-12.9024-2.6624-23.9616-11.6736-33.1776l-71.0656-71.68 126.5664-126.5664 71.4752 71.4752c9.216 9.216 20.2752 13.1072 33.1776 11.6736 49.9712-5.3248 99.328 12.0832 134.9632 47.9232 31.3344 31.3344 48.7424 73.1136 48.7424 117.76 0 44.4416-17.2032 86.2208-48.7424 117.76-32.5632 32.3584-75.1616 48.5376-117.76 48.5376zM636.1088 706.56l42.3936 42.5984c17.8176 17.8176 26.2144 41.3696 23.3472 66.3552-3.8912 37.888 9.216 74.9568 36.0448 101.7856 48.9472 48.9472 128.6144 48.9472 177.5616 0 23.7568-23.7568 36.6592-55.0912 36.6592-88.6784s-13.1072-65.1264-36.6592-88.6784c-27.0336-27.0336-64.1024-40.1408-101.7856-36.0448-24.9856 2.6624-48.5376-5.7344-66.3552-23.552l-42.3936-42.3936-68.8128 68.608zM311.7056 431.9232l-28.4672-28.0576c-9.216-9.216-20.48-12.4928-32.768-9.8304-49.3568 10.24-100.9664-0.4096-141.9264-29.2864-52.4288-36.6592-81.3056-96.4608-77.4144-160.3584v-0.4096c1.024-12.6976 8.192-23.3472 19.6608-28.8768 11.4688-5.5296 24.576-4.3008 35.0208 3.072l88.6784 62.0544 49.152-70.4512-88.8832-62.2592c-10.6496-7.5776-15.9744-19.6608-14.5408-32.1536 1.4336-12.4928 9.216-22.7328 20.48-27.8528 58.9824-25.3952 125.1328-18.6368 177.3568 18.0224 60.0064 41.984 89.088 115.3024 73.9328 186.7776-2.4576 12.4928 0.8192 23.7568 9.8304 32.768L430.08 313.344l-118.3744 118.5792zM258.2528 352.256c19.6608 0 38.5024 7.5776 53.0432 21.9136l60.8256-60.8256c-18.2272-18.6368-25.3952-43.2128-20.2752-69.2224 11.6736-55.5008-10.8544-112.4352-57.344-144.9984-37.2736-26.0096-83.7632-32.5632-126.5664-18.2272l84.1728 58.9824c15.7696 11.0592 19.456 32.768 8.3968 48.3328l-56.5248 80.6912c-5.3248 7.5776-13.312 12.6976-22.3232 14.336-9.216 1.6384-18.432-0.4096-26.0096-5.7344l-83.968-58.9824c0.8192 45.056 22.9376 86.4256 60.416 112.64 32.3584 22.7328 71.2704 30.72 109.9776 22.7328 5.3248-1.2288 10.8544-1.6384 16.1792-1.6384z m-78.848-108.3392z m-117.3504-32.1536z m165.0688-47.104z"
|
||||
fill="#333333" p-id="9266"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
BIN
pywxdump/ui/src/assets/img/qq.png
Normal file
BIN
pywxdump/ui/src/assets/img/qq.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
BIN
pywxdump/ui/src/assets/img/qrcode_gh.jpg
Normal file
BIN
pywxdump/ui/src/assets/img/qrcode_gh.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
1
pywxdump/ui/src/assets/logo.svg
Normal file
1
pywxdump/ui/src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 276 B |
35
pywxdump/ui/src/assets/main.css
Normal file
35
pywxdump/ui/src/assets/main.css
Normal file
@ -0,0 +1,35 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
54
pywxdump/ui/src/components/chat/ChatRecords.vue
Normal file
54
pywxdump/ui/src/components/chat/ChatRecords.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import ChatRecprdsHeader from '@/components/chat/ChatRecprdsHeader.vue';
|
||||
import ChatRecordsMain from '@/components/chat/ChatRecordsMain.vue';
|
||||
import {ref, defineProps, nextTick, onMounted, watch} from 'vue';
|
||||
import http from "@/utils/axios.js";
|
||||
import ChatExportMain from "@/components/chatBackup/ChatExportMain.vue";
|
||||
import {apiMsgCount} from "@/api/chat";
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
wxid: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
// 导出聊天记录页面是否显示
|
||||
const is_export = ref(false);
|
||||
const onExport = (exporting: boolean) => {
|
||||
is_export.value = exporting;
|
||||
}
|
||||
// end 导出聊天记录页面是否显示
|
||||
|
||||
// start 监测wxid变化,初始化数据
|
||||
const init = () => {
|
||||
is_export.value = false;
|
||||
}
|
||||
watch(() => props.wxid, async (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
init();
|
||||
}
|
||||
});
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
// end 监测wxid变化,初始化数据
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container>
|
||||
<el-header style="height: 40px; max-height: 40px; width: 100%;background-color: #d2d2fa;padding-top: 5px;">
|
||||
<ChatRecprdsHeader :wxid="wxid" @exporting="onExport"/>
|
||||
</el-header>
|
||||
<el-main style="height: calc(100vh - 40px);padding: 0;margin: 0;background-color: #f5f5f5;">
|
||||
<ChatExportMain v-if="is_export" :wxid="wxid"/>
|
||||
<ChatRecordsMain v-else :wxid="wxid"/>
|
||||
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
346
pywxdump/ui/src/components/chat/ChatRecordsMain.vue
Normal file
346
pywxdump/ui/src/components/chat/ChatRecordsMain.vue
Normal file
@ -0,0 +1,346 @@
|
||||
<script setup lang="ts">
|
||||
import {defineProps, ref, onMounted, watch, nextTick, defineExpose} from "vue";
|
||||
import http from '@/utils/axios.js';
|
||||
import MessageText from '@/components/chat/message/MessageText.vue';
|
||||
import MessageImg from '@/components/chat/message/MessageImg.vue';
|
||||
import MessageVideo from '@/components/chat/message/MessageVideo.vue';
|
||||
import MessageAudio from '@/components/chat/message/MessageAudio.vue';
|
||||
import MessageFile from '@/components/chat/message/MessageFile.vue';
|
||||
import MessageEmoji from '@/components/chat/message/MessageEmoji.vue'
|
||||
import MessageOther from "@/components/chat/message/MessageOther.vue";
|
||||
import {apiMsgCountSolo, apiMsgs, apiMyWxid} from "@/api/chat";
|
||||
import type {msg, User, UserList} from "@/utils/common_utils";
|
||||
import {api_audio, api_img, api_video} from "@/api/base";
|
||||
// v3 无限滚动 https://vue3-infinite-loading.netlify.app/api/props.html#distance
|
||||
import InfiniteLoading from "v3-infinite-loading";
|
||||
import "v3-infinite-loading/lib/style.css";
|
||||
|
||||
// 这里的 props 是从父组件传递过来的
|
||||
const props = defineProps({
|
||||
wxid: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
// 定义变量
|
||||
const messages = ref<msg[]>([]);
|
||||
const userlist = ref<UserList>({});
|
||||
const msg_loading = ref(false);
|
||||
const my_wxid = ref('');
|
||||
const start = ref(0);
|
||||
const limit = ref(100);
|
||||
const min_id = ref(0);
|
||||
const max_id = ref(0);
|
||||
const msg_count = ref(0);
|
||||
|
||||
// 这部分为构造消息的发送时间和头像
|
||||
const _direction = (message: any) => {
|
||||
|
||||
if (message.talker == '我') {
|
||||
message.talker = my_wxid.value;
|
||||
}
|
||||
const sendname = (message: msg) => {
|
||||
const user = userlist.value[message.talker];
|
||||
return user?.remark || user?.nickname || user?.account || message.talker;
|
||||
}
|
||||
return `${sendname(message)} [${message.type_name}] ${message.CreateTime}`;
|
||||
}
|
||||
|
||||
const get_head_url = (message: any) => {
|
||||
if (message.talker == '我') {
|
||||
message.talker = my_wxid.value;
|
||||
}
|
||||
if (!userlist.value.hasOwnProperty(message.talker)) {
|
||||
return '';
|
||||
}
|
||||
return api_img(userlist.value[message.talker].headImgUrl);
|
||||
}
|
||||
// END 这部分为构造消息的发送时间和头像
|
||||
|
||||
// 获取聊天记录
|
||||
const fetchData = async (scroll: String = '') => {
|
||||
if (msg_loading.value) {
|
||||
console.log("正在获取消息,请稍后再试!")
|
||||
return;
|
||||
}
|
||||
if (props.wxid == '') {
|
||||
console.log("wxid 为空, 请检查!")
|
||||
return;
|
||||
}
|
||||
try {
|
||||
msg_loading.value = true;
|
||||
if (start.value < 0) {
|
||||
start.value = 0;
|
||||
}
|
||||
console.log('fetchData', props.wxid, start.value, limit.value)
|
||||
const body_data = await apiMsgs(props.wxid, start.value, limit.value);
|
||||
|
||||
// messages.value = [];
|
||||
// messages.value = body_data.msg_list.concat(messages.value);
|
||||
messages.value = body_data.msg_list
|
||||
if (messages.value.length == 0) {
|
||||
msg_loading.value = false;
|
||||
return body_data;
|
||||
}
|
||||
userlist.value = Object.assign(userlist.value, body_data.user_list);
|
||||
// 去重
|
||||
messages.value = messages.value.filter((item, index, array) => {
|
||||
return index === 0 || item.id !== array[index - 1].id;
|
||||
});
|
||||
// 排序
|
||||
messages.value.sort((a, b) => {
|
||||
return a.id - b.id
|
||||
});
|
||||
|
||||
min_id.value = messages.value[0].id;
|
||||
max_id.value = messages.value[messages.value.length - 1].id;
|
||||
|
||||
if (scroll == "top") {
|
||||
scrollToId(messages.value[0].id);
|
||||
} else if (scroll == "bottom") {
|
||||
scrollToId(messages.value[messages.value.length - 1].id);
|
||||
}
|
||||
|
||||
msg_loading.value = false;
|
||||
return body_data;
|
||||
} catch (error) {
|
||||
msg_loading.value = false;
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
// 上述为网络请求部分
|
||||
|
||||
// 初始加载数据
|
||||
|
||||
// END 获取聊天记录
|
||||
|
||||
// 监听 userData 中 username 的变化
|
||||
const init = async () => {
|
||||
try {
|
||||
messages.value = [];
|
||||
userlist.value = {};
|
||||
msg_loading.value = false;
|
||||
limit.value = limit.value || 100;
|
||||
min_id.value = 0;
|
||||
max_id.value = 0;
|
||||
|
||||
my_wxid.value = await apiMyWxid();
|
||||
msg_count.value = await apiMsgCountSolo(props.wxid);
|
||||
if (msg_count.value <= 0) {
|
||||
return;
|
||||
}
|
||||
// 切换最后一页
|
||||
console.log('msg_count.value', msg_count.value, limit.value)
|
||||
start.value = Math.floor((msg_count.value - 1) / limit.value) * limit.value || 0
|
||||
await fetchData("bottom");
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
watch(() => props.wxid, (newUsername, oldUsername) => {
|
||||
console.log('username changed: ', oldUsername, newUsername);
|
||||
init();
|
||||
});
|
||||
watch(() => start.value, (newVal, oldVal) => {
|
||||
console.log('msg start changed: ', oldVal, newVal);
|
||||
if (newVal > oldVal) {
|
||||
// 说明是向后滚动
|
||||
fetchData("top");
|
||||
} else {
|
||||
fetchData("bottom");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
// 移动到底部请求获取全部数据
|
||||
const loadMoreTop = async ($state: any) => {
|
||||
try {
|
||||
if (start.value == 0) {
|
||||
$state.complete();
|
||||
return;
|
||||
}
|
||||
start.value = start.value - limit.value;
|
||||
if (start.value < 0) {
|
||||
start.value = 0;
|
||||
}
|
||||
// await fetchData("bottom");
|
||||
$state.loaded();
|
||||
} catch (error) {
|
||||
console.log("Error fetching Top data:", error)
|
||||
$state.error();
|
||||
}
|
||||
};
|
||||
|
||||
const loadMoreBottom = async ($state: any) => {
|
||||
try {
|
||||
if (start.value + limit.value > msg_count.value) {
|
||||
$state.complete();
|
||||
return;
|
||||
}
|
||||
start.value = start.value + limit.value;
|
||||
// await fetchData("top");
|
||||
$state.loaded();
|
||||
} catch (error) {
|
||||
console.log("Error fetching data:", error)
|
||||
$state.error();
|
||||
}
|
||||
};
|
||||
// END 循环请求获取全部数据
|
||||
|
||||
|
||||
// 分页管理相关的变量
|
||||
const handleLimitChange = (val: number) => {
|
||||
limit.value = val;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
start.value = (val - 1) * limit.value;
|
||||
// fetchData();
|
||||
};
|
||||
// END 分页管理相关的变量
|
||||
|
||||
// 滚动到指定位置 id
|
||||
const scrollToId = (id: number) => {
|
||||
nextTick(() => {
|
||||
const element = document.getElementById(`message-${id}`);
|
||||
console.log(`scrollToId: message-${id}`)
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
// END 滚动到指定位置 id
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="chat" v-if="messages.length>0 && msg_count>0">
|
||||
<el-container class="chat-records-main-container">
|
||||
<el-main class="chat-records-main-main">
|
||||
<!-- <div class="infinite-container">-->
|
||||
<!-- <InfiniteLoading @infinite="loadMoreTop" :top="true" :firstload="false">-->
|
||||
<!-- <template #spinner>-->
|
||||
<!-- <span class="spinner-text">加载中...</span>-->
|
||||
<!-- </template>-->
|
||||
<!-- <template #complete>-->
|
||||
<!-- <span class="complete-text">没有更多啦</span>-->
|
||||
<!-- </template>-->
|
||||
<!-- <template #error="{ retry }">-->
|
||||
<!-- <button @click="retry" class="retry-button">错误</button>-->
|
||||
<!-- </template>-->
|
||||
<!-- </InfiniteLoading>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div class="message" v-for="(message,index) in messages" :key="index" :id="`message-${message.id}`">
|
||||
<!-- 文字消息 -->
|
||||
<MessageText v-if="message.type_name == '文本'" :is_sender="message.is_sender"
|
||||
:direction="_direction(message)" :headUrl="get_head_url(message)"
|
||||
:content="message.msg"></MessageText>
|
||||
<!-- 图片消息 -->
|
||||
<MessageImg v-else-if="message.type_name == '图片'" :is_sender="message.is_sender"
|
||||
:direction="_direction(message)" :headUrl="get_head_url(message)"
|
||||
:src="api_img(message.src)"></MessageImg>
|
||||
<!-- 表情消息 -->
|
||||
<MessageEmoji v-else-if="message.type_name == '动画表情'" :is_sender="message.is_sender"
|
||||
:direction="_direction(message)" :headUrl="get_head_url(message)"
|
||||
:src="api_img(message.src)"></MessageEmoji>
|
||||
<!-- 视频消息 -->
|
||||
<MessageVideo v-else-if="message.type_name == '视频'" :is_sender="message.is_sender"
|
||||
:direction="_direction(message)" :headUrl="get_head_url(message)"
|
||||
:src="api_video(message.src)"></MessageVideo>
|
||||
<!-- 文件消息 -->
|
||||
<MessageFile v-else-if="message.type_name == '文件'" :is_sender="message.is_sender"
|
||||
:direction="_direction(message)" :headUrl="get_head_url(message)"
|
||||
:src="message.src"></MessageFile>
|
||||
<!-- 语音消息 -->
|
||||
<MessageAudio v-else-if="message.type_name == '语音'" :is_sender="message.is_sender"
|
||||
:direction="_direction(message)" :headUrl="get_head_url(message)"
|
||||
:src="api_audio(message.src)"
|
||||
:msg="message.msg"></MessageAudio>
|
||||
<!-- 其他消息 -->
|
||||
<MessageOther v-else :is_sender="message.is_sender" :direction="_direction(message)"
|
||||
:headUrl="get_head_url(message)" :content="message.msg"></MessageOther>
|
||||
</div>
|
||||
<!-- <div class="infinite-container">-->
|
||||
<!-- <InfiniteLoading @infinite="loadMoreBottom" :top="false" :firstload="false">-->
|
||||
<!-- <template #spinner>-->
|
||||
<!-- <span class="spinner-text">加载中...</span>-->
|
||||
<!-- </template>-->
|
||||
<!-- <template #complete>-->
|
||||
<!-- <span class="complete-text">没有更多啦</span>-->
|
||||
<!-- </template>-->
|
||||
<!-- <template #error="{ retry }">-->
|
||||
<!-- <button @click="retry" class="retry-button">错误</button>-->
|
||||
<!-- </template>-->
|
||||
<!-- </InfiniteLoading>-->
|
||||
<!-- </div>-->
|
||||
</el-main>
|
||||
<el-footer height="20px" class="chat-records-main-footer">
|
||||
<el-pagination background small layout="sizes, prev, pager, next, jumper" :total="msg_count"
|
||||
:page-size="limit" :page-sizes="[50,100, 200, 300, 500]" @size-change="handleLimitChange"
|
||||
:current-page="Math.floor(start / limit + 1)" @current-change="handleCurrentChange"
|
||||
/>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</div>
|
||||
<el-skeleton v-else-if="messages.length<=0 && msg_count>0" :rows="30" animated/>
|
||||
<el-empty description="无记录" v-else/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
#chat {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - 15px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chat-records-main-container {
|
||||
height: calc(100% - 15px);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chat-records-main-main {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.message:last-of-type {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-records-main-footer {
|
||||
display: grid;
|
||||
place-items: center; /* 居中对齐 */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.infinite-container {
|
||||
display: grid;
|
||||
place-items: center; /* 居中对齐 */
|
||||
|
||||
.spinner-text,
|
||||
.complete-text,
|
||||
.retry-button {
|
||||
font-size: 16px;
|
||||
color: #9d09f3;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
201
pywxdump/ui/src/components/chat/ChatRecprdsHeader.vue
Normal file
201
pywxdump/ui/src/components/chat/ChatRecprdsHeader.vue
Normal file
@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import {defineEmits, defineProps, nextTick, onMounted, ref, watch} from 'vue';
|
||||
import http from "@/utils/axios.js";
|
||||
import {ElTable, ElNotification, ElMessage, ElMessageBox} from "element-plus";
|
||||
import {apiMsgCount, apiMsgCountSolo, apiRealTime, apiUserList} from "@/api/chat";
|
||||
import {gen_show_name, type User} from "@/utils/common_utils";
|
||||
import UserInfoShow from "@/components/chat/components/UserInfoShow.vue";
|
||||
|
||||
const props = defineProps({
|
||||
wxid: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
const msg_count = ref<number>(0);
|
||||
const userinfo = ref<User>({
|
||||
wxid: '',
|
||||
nOrder: 0,
|
||||
nUnReadCount: 0,
|
||||
strNickName: '',
|
||||
nStatus: 0,
|
||||
nIsSend: 0,
|
||||
strContent: '',
|
||||
nMsgLocalID: 0,
|
||||
nMsgStatus: 0,
|
||||
nTime: '',
|
||||
nMsgType: 0,
|
||||
nMsgSubType: 0,
|
||||
nickname: '',
|
||||
remark: '',
|
||||
account: '',
|
||||
describe: '',
|
||||
headImgUrl: '',
|
||||
ExtraBuf: {
|
||||
"个性签名": "",
|
||||
"企微属性": "",
|
||||
"公司名称": "",
|
||||
"国": "",
|
||||
"备注图片": "",
|
||||
"备注图片2": "",
|
||||
"市": "",
|
||||
"性别[1男2女]": 0,
|
||||
"手机号": "",
|
||||
"朋友圈背景": "",
|
||||
"省": ""
|
||||
},
|
||||
LabelIDList: [],
|
||||
extra: null
|
||||
});
|
||||
|
||||
// 请求数据,赋值 START
|
||||
const req_user_info = async () => {
|
||||
// 请求数据 用户信息
|
||||
try {
|
||||
const body_data = await apiUserList("", [props.wxid]);
|
||||
userinfo.value.wxid = props.wxid;
|
||||
userinfo.value.remark = body_data[props.wxid]?.remark;
|
||||
userinfo.value.account = body_data[props.wxid]?.account;
|
||||
userinfo.value.describe = body_data[props.wxid]?.describe;
|
||||
userinfo.value.headImgUrl = body_data[props.wxid]?.headImgUrl;
|
||||
userinfo.value.nickname = body_data[props.wxid]?.nickname;
|
||||
userinfo.value.LabelIDList = body_data[props.wxid]?.LabelIDList;
|
||||
userinfo.value.ExtraBuf = body_data[props.wxid]?.ExtraBuf;
|
||||
userinfo.value.extra = body_data[props.wxid]?.extra;
|
||||
return body_data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching data wxid2user :', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const req_msg_count = async () => {
|
||||
try {
|
||||
msg_count.value = 0;
|
||||
const body_data = await apiMsgCountSolo(props.wxid);
|
||||
msg_count.value = body_data || 0;
|
||||
return body_data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching data msg_count:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// 请求数据,赋值 END
|
||||
|
||||
// 初始调用函数 START
|
||||
const init = () => {
|
||||
is_export.value = false;
|
||||
req_user_info();
|
||||
req_msg_count();
|
||||
}
|
||||
onMounted(() => {
|
||||
console.log('ChatRecprdsHeader onMounted', props.wxid)
|
||||
init();
|
||||
});
|
||||
watch(() => props.wxid, async (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
init();
|
||||
}
|
||||
});
|
||||
// 初始调用函数 END
|
||||
|
||||
// 弹窗展示更多信息 START
|
||||
const is_show_more = ref(false);
|
||||
// 获取实时消息 START
|
||||
|
||||
const is_getting_real_time_msg = ref(false);
|
||||
const get_real_time_msg = async () => {
|
||||
if (is_getting_real_time_msg.value) {
|
||||
console.log("正在获取实时消息,请稍后再试!")
|
||||
return;
|
||||
}
|
||||
is_getting_real_time_msg.value = true;
|
||||
try {
|
||||
const body_data = await apiRealTime();
|
||||
is_getting_real_time_msg.value = false;
|
||||
return body_data;
|
||||
} catch (error) {
|
||||
is_getting_real_time_msg.value = false;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// 获取实时消息 END
|
||||
|
||||
// 导出消息按钮,并传递是否导出给父组件 START
|
||||
const is_export = ref(false);
|
||||
const emits = defineEmits(['exporting']);
|
||||
const export_button = (val: boolean) => {
|
||||
// 提交参数 is_export 给父组件
|
||||
emits('exporting', val);
|
||||
is_export.value = val;
|
||||
}
|
||||
// 导出消息按钮,并传递是否导出给父组件 END
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<el-row :gutter="5" style="width: 100%;">
|
||||
<el-col :span="6" style="white-space: nowrap;">
|
||||
<el-text class="label_color mx-1" truncated>wxid:</el-text> 
|
||||
<el-text class="data_color mx-1" truncated :title="userinfo?.wxid">{{ userinfo?.wxid }}</el-text>
|
||||
</el-col>
|
||||
<el-col :span="6" style="white-space: nowrap;">
|
||||
<el-text class="label_color mx-1" truncated>名称:</el-text> 
|
||||
<el-text class="data_color mx-1" truncated title="show_name">{{ gen_show_name(userinfo) }}</el-text>
|
||||
</el-col>
|
||||
<el-col :span="5" style="white-space: nowrap;">
|
||||
<el-text class="label_color mx-1" truncated>数量:</el-text> 
|
||||
<el-text class="data_color mx-1" truncated :title="msg_count">{{ msg_count }}</el-text>
|
||||
</el-col>
|
||||
<el-col :span="2" style="white-space: nowrap;">
|
||||
<el-text class="button_color mx-1 underline" truncated @click="is_show_more=!is_show_more"> 详细信息</el-text>
|
||||
</el-col>
|
||||
<el-col :span="2" style="white-space: nowrap;">
|
||||
<el-text v-if="!is_export" class="button_color mx-1 underline" truncated @click="export_button(true);">导出备份
|
||||
</el-text>
|
||||
<el-text v-if="is_export" class="button_color mx-1 underline" truncated @click="export_button(false);">聊天查看
|
||||
</el-text>
|
||||
</el-col>
|
||||
<el-col :span="3" style="white-space: nowrap;">
|
||||
<el-text class="button_color mx-1 underline" truncated @click="get_real_time_msg();">实时消息
|
||||
<template v-if="is_getting_real_time_msg" style="color: #00bd7e">...</template>
|
||||
</el-text>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-dialog v-model="is_show_more" title="详细信息" width="600" center>
|
||||
<user-info-show :userinfo="userinfo" :show_all="true"></user-info-show>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.label_color {
|
||||
color: #333; /* 调整字体颜色 */
|
||||
font-size: 15px;
|
||||
padding-left: 15px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.data_color {
|
||||
color: #08488c;
|
||||
background-color: #f4f4f4; /* 调整背景颜色 */
|
||||
font-size: 15px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
font-weight: bold; /* 使用 bold 表示加粗 */
|
||||
white-space: nowrap;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.button_color {
|
||||
color: #0048ff; /* 调整字体颜色 */
|
||||
font-size: 15px;
|
||||
padding-left: 15px;
|
||||
padding-right: 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
122
pywxdump/ui/src/components/chat/ContactsList.vue
Normal file
122
pywxdump/ui/src/components/chat/ContactsList.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import {defineEmits, onMounted, ref} from 'vue';
|
||||
import http from '@/utils/axios.js';
|
||||
import {apiUserList, apiUserSessionList} from "@/api/chat";
|
||||
import UserInfoShow from "@/components/chat/components/UserInfoShow.vue";
|
||||
import {gen_show_name, type User} from "@/utils/common_utils";
|
||||
import {api_img} from "@/api/base";
|
||||
|
||||
// "wxid": strUsrName, "nOrder": nOrder, "nUnReadCount": nUnReadCount, "strNickName": strNickName,
|
||||
// "nStatus": nStatus, "nIsSend": nIsSend, "strContent": strContent, "nMsgLocalID": nMsgLocalID,
|
||||
// "nMsgStatus": nMsgStatus, "nTime": nTime, "nMsgType": nMsgType, "nMsgSubType": nMsgSubType,
|
||||
// "nickname": NickName, "remark": Remark, "account": Alias,
|
||||
// "describe": describe, "headImgUrl": bigHeadImgUrl if bigHeadImgUrl else "",
|
||||
// "ExtraBuf": ExtraBuf, "LabelIDList": tuple(LabelIDList)
|
||||
|
||||
|
||||
const tableData = ref([]);
|
||||
|
||||
|
||||
// 初始化请求session数据 START
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
tableData.value = await apiUserSessionList();
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
onMounted(fetchData);
|
||||
// END 初始化请求session数据 END
|
||||
|
||||
// 搜索框以及按钮 START
|
||||
const search_word = ref('');
|
||||
const search = async () => {
|
||||
try {
|
||||
// console.log(body_data);
|
||||
if (search_word.value === '') {
|
||||
tableData.value = await apiUserSessionList();
|
||||
return;
|
||||
}
|
||||
console.log(search_word.value);
|
||||
tableData.value = []
|
||||
const ret = await apiUserList(search_word.value);
|
||||
if (ret !== null && typeof ret === 'object') {
|
||||
Object.entries(ret).forEach(([key, value]) => {
|
||||
tableData.value.push(value);
|
||||
});
|
||||
}
|
||||
// for (const key in ret) {
|
||||
// if (ret.hasOwnProperty(key)) {
|
||||
// tableData.value.push(ret[key]);
|
||||
// }
|
||||
// }
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// END 搜索框以及按钮 END
|
||||
|
||||
// 处理user数据 传递给父组件 START
|
||||
const emits = defineEmits(['wxid']);
|
||||
|
||||
const handleCurrentChange = (val: User | undefined) => {
|
||||
// 触发自定义事件,并传递数据
|
||||
if (val !== undefined) {
|
||||
// 处理user数据
|
||||
// 判断val是否有wxid
|
||||
if (val.wxid !== undefined) {
|
||||
console.log('wxid:', val.wxid);
|
||||
emits('wxid', val.wxid);
|
||||
}
|
||||
}
|
||||
}
|
||||
// END 处理user数据 传递给父组件 END
|
||||
|
||||
|
||||
// 生成显示的name
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 搜索框以及按钮 -->
|
||||
<div style="padding: 10px 10px;">
|
||||
<el-input placeholder="请输入关键字" v-model="search_word" @keyup.enter="search"
|
||||
style="width: 170px;margin-left: 15px;"></el-input>
|
||||
<el-button type="primary" @click="search" style="width: 50px;">搜索</el-button>
|
||||
</div>
|
||||
<!-- 这是联系人的list -->
|
||||
<el-table :data="tableData" stripe style="width: 100%" max-height="100%" height="100%" highlight-current-row loading="lazy"
|
||||
@current-change="handleCurrentChange">
|
||||
<el-table-column width="57">
|
||||
<template v-slot="{ row }">
|
||||
<el-avatar :size="33" :src="api_img(row.headImgUrl)" v-if="row.headImgUrl!==''"></el-avatar>
|
||||
<el-avatar :size="33" v-else>群</el-avatar>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="190">
|
||||
<template v-slot="{ row }">
|
||||
<el-tooltip class="item" effect="light" placement="right">
|
||||
<div slot="content" class="tips">
|
||||
<span>{{ gen_show_name(row) }}</span> <br>
|
||||
<span v-if="row.nTime" style="color: #909399; font-size: 12px;">{{ row.nTime }}</span>
|
||||
</div>
|
||||
<template #content>
|
||||
<user-info-show :userinfo="row" :show_all="false" style="max-width: 600px"></user-info-show>
|
||||
</template>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 允许提示内容换行 */
|
||||
.el-tooltip__popper .popper__content {
|
||||
white-space: pre-line; /* 允许换行 */
|
||||
}
|
||||
</style>
|
111
pywxdump/ui/src/components/chat/components/UserInfoShow.vue
Normal file
111
pywxdump/ui/src/components/chat/components/UserInfoShow.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {api_img} from "@/api/base";
|
||||
|
||||
const props = defineProps({
|
||||
userinfo: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
show_all: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
const tableRowClassName = ({
|
||||
row,
|
||||
rowIndex,
|
||||
}: {
|
||||
row: any
|
||||
rowIndex: number
|
||||
}) => {
|
||||
console.log(row.wxid, props.userinfo.extra.owner.wxid, row.wxid == props.userinfo.extra.owner.wxid)
|
||||
if (row.wxid == props.userinfo.extra.owner.wxid) {
|
||||
console.log("table-success-row")
|
||||
return 'table-success-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="show_all" style="max-width: 560px">
|
||||
<!-- 用于详细信息里面的更多信息显示 -->
|
||||
<div>
|
||||
<el-divider content-position="center">基本信息</el-divider>
|
||||
<span>wxid:{{ userinfo.wxid }}<br></span>
|
||||
<span>账号:{{ userinfo.account }}<br></span>
|
||||
<span>昵称:{{ userinfo.nickname }}<br></span>
|
||||
<span>备注:{{ userinfo.remark }}<br></span>
|
||||
</div>
|
||||
<div>
|
||||
<el-divider content-position="center">账号信息</el-divider>
|
||||
<span>性别:{{
|
||||
userinfo.ExtraBuf['性别[1男2女]'] == 1 ? '男' : userinfo.ExtraBuf['性别[1男2女]'] == 2 ? '女' : ''
|
||||
}}<br></span>
|
||||
<span>手机:{{ userinfo.ExtraBuf['手机号'] }}<br></span>
|
||||
<span>标签:{{ userinfo.LabelIDList.join('/') }}<br></span>
|
||||
<span>描述:{{ userinfo.describe }}<br></span>
|
||||
<span>个签:{{ userinfo.ExtraBuf['个性签名'] }}<br></span>
|
||||
<span>国家:{{ userinfo.ExtraBuf['国'] }}<br></span>
|
||||
<span>省份:{{ userinfo.ExtraBuf['省'] }}<br></span>
|
||||
<span>市名:{{ userinfo.ExtraBuf['市'] }}<br></span>
|
||||
</div>
|
||||
<div>
|
||||
<el-divider content-position="center">其他信息</el-divider>
|
||||
<span>公司:{{ userinfo.ExtraBuf['公司名称'] }}<br></span>
|
||||
<span>企微:{{ userinfo.ExtraBuf['企微属性'] }}<br></span>
|
||||
<span>朋友圈背景:<br></span>
|
||||
<el-image v-if="userinfo.ExtraBuf['朋友圈背景']" :src="api_img(userinfo.ExtraBuf['朋友圈背景'])" alt="朋友圈背景"
|
||||
style="max-width: 200px;max-height: 200px"
|
||||
:preview-src-list="[api_img(userinfo.ExtraBuf['朋友圈背景'])]" :hide-on-click-modal="true"/>
|
||||
</div>
|
||||
<div v-if="userinfo.extra">
|
||||
<el-divider content-position="center">群聊信息</el-divider>
|
||||
<span>群主: {{ userinfo.extra.owner.wxid }}<br></span>
|
||||
<span>群成员:<br></span>
|
||||
<el-table :data="Object.values(userinfo.extra.wxid2userinfo)" style="width: 100%"
|
||||
:row-class-name="tableRowClassName">
|
||||
<el-table-column prop="wxid" label="wxid"/>
|
||||
<el-table-column prop="account" label="账号"/>
|
||||
<el-table-column prop="nickname" label="昵称"/>
|
||||
<el-table-column prop="remark" label="备注"/>
|
||||
<el-table-column prop="roomNickname" label="群昵称"/>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="max-width: 560px">
|
||||
<span>wxid:{{ userinfo.wxid }}<br></span>
|
||||
<span>账号:{{ userinfo.account }}<br></span>
|
||||
<span>昵称:{{ userinfo.nickname }}<br></span>
|
||||
<span v-if="userinfo.remark">备注:{{ userinfo.remark }}<br></span>
|
||||
<span v-if="userinfo.ExtraBuf && userinfo.ExtraBuf['性别[1男2女]']">性别:{{
|
||||
userinfo.ExtraBuf['性别[1男2女]'] == 1 ? '男' : userinfo.ExtraBuf['性别[1男2女]'] == 2 ? '女' : ''
|
||||
}}<br></span>
|
||||
<span v-if="userinfo.ExtraBuf && userinfo.ExtraBuf['手机号']">手机:{{ userinfo.ExtraBuf['手机号'] }}<br></span>
|
||||
<span v-if="userinfo.LabelIDList&&userinfo.LabelIDList.length>0">标签:{{
|
||||
userinfo.LabelIDList.join('/')
|
||||
}}<br></span>
|
||||
<span v-if="userinfo.describe">描述:{{ userinfo.describe }}<br></span>
|
||||
<span v-if="userinfo.ExtraBuf && userinfo.ExtraBuf['个性签名']">个签:{{ userinfo.ExtraBuf['个性签名'] }}<br></span>
|
||||
<span v-if="userinfo.ExtraBuf && userinfo.ExtraBuf['国']">国家:{{ userinfo.ExtraBuf['国'] }}<br></span>
|
||||
<span v-if="userinfo.ExtraBuf && userinfo.ExtraBuf['省']">省份:{{ userinfo.ExtraBuf['省'] }}<br></span>
|
||||
<span v-if="userinfo.ExtraBuf && userinfo.ExtraBuf['市']">市名:{{ userinfo.ExtraBuf['市'] }}<br></span>
|
||||
<span v-if="userinfo.ExtraBuf && userinfo.ExtraBuf['公司名称']">公司:{{ userinfo.ExtraBuf['公司名称'] }}<br></span>
|
||||
<span v-if="userinfo.ExtraBuf && userinfo.ExtraBuf['企微属性']">企微:{{ userinfo.ExtraBuf['企微属性'] }}<br></span>
|
||||
<span v-if="userinfo.ExtraBuf && userinfo.ExtraBuf['朋友圈背景']">朋友圈背景:<br></span>
|
||||
<el-image v-if="userinfo.ExtraBuf && userinfo.ExtraBuf['朋友圈背景']"
|
||||
:src="api_img(userinfo.ExtraBuf['朋友圈背景'])"
|
||||
style="max-width: 200px;max-height: 200px" alt="朋友圈背景"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-success-row {
|
||||
--el-table-tr-bg-color: var(--el-color-success-light-9);
|
||||
}
|
||||
</style>
|
216
pywxdump/ui/src/components/chat/message/MessageAudio.vue
Normal file
216
pywxdump/ui/src/components/chat/message/MessageAudio.vue
Normal file
@ -0,0 +1,216 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {defineProps, onMounted, ref} from "vue";
|
||||
// import http from '@/router/axios.js';
|
||||
import ElTextarea from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
is_sender: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
headUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
msg: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
})
|
||||
// const audioSrc = ref("");
|
||||
|
||||
// onMounted(async () => {
|
||||
// // audioSrc.value = await read_audio_base64(props.src);
|
||||
// });
|
||||
//
|
||||
// const read_audio_base64 = async (src: string) => {
|
||||
//
|
||||
// // try {
|
||||
// // const body_data = await http.post('/rs_api/audio', {
|
||||
// // 'savePath': src,
|
||||
// // });
|
||||
// // return body_data;
|
||||
// // } catch (error) {
|
||||
// // console.error('Error fetching data:', error);
|
||||
// // return "";
|
||||
// // }
|
||||
// return src;
|
||||
// }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-content">
|
||||
<!-- recordContent 聊天记录数组-->
|
||||
<!-- 对方 -->
|
||||
<div class="word" v-if="!is_sender">
|
||||
<img :src="headUrl">
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
|
||||
<audio controls style="background-color:#fff ">
|
||||
<source :src="src" type="audio/wav">
|
||||
</audio>
|
||||
<el-textarea
|
||||
:rows="1"
|
||||
:readonly="true"
|
||||
:value="msg"
|
||||
style="width: 100%;"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- 我的 -->
|
||||
<div class="word-my" v-else>
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
|
||||
<audio controls style="background-color:#95EC69;">
|
||||
<source :src="src" type="audio/wav">
|
||||
</audio>
|
||||
<el-textarea
|
||||
:rows="1"
|
||||
:readonly="true"
|
||||
:value="msg"
|
||||
style="width: 100%;"/>
|
||||
</div>
|
||||
<img :src="headUrl">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.chat-content {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.word {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 10px;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chat_img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::before {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-right: 10px solid #FFF;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.word-my {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 90%;
|
||||
margin-left: 10px;
|
||||
text-align: right;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
background: #95EC69;
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.chat_img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::after {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-left: 10px solid #95EC69;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-image__error .image-slot {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.demo-image__error .image-slot .el-icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.demo-image__error .el-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
</style>
|
206
pywxdump/ui/src/components/chat/message/MessageEmoji.vue
Normal file
206
pywxdump/ui/src/components/chat/message/MessageEmoji.vue
Normal file
@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="chat-content">
|
||||
<!-- recordContent 聊天记录数组-->
|
||||
<!-- 对方 -->
|
||||
<div class="word" v-if="!is_sender">
|
||||
<img :src="headUrl">
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
|
||||
<div class="demo-image__preview">
|
||||
<el-image
|
||||
style="max-width: 150px; max-height: 150px"
|
||||
:src=src
|
||||
:zoom-rate="1.2"
|
||||
:max-scale="7"
|
||||
:min-scale="0.2"
|
||||
:preview-src-list="[src]"
|
||||
:hide-on-click-modal="true"
|
||||
:initial-index="4"
|
||||
fit="cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- 我的 -->
|
||||
<div class="word-my" v-else>
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
|
||||
<div class="demo-image__preview">
|
||||
<!-- <image :src="src"></image> -->
|
||||
<el-image
|
||||
style="max-width: 150px; max-height: 150px"
|
||||
:src=src
|
||||
:zoom-rate="1.2"
|
||||
:max-scale="7"
|
||||
:min-scale="0.2"
|
||||
:preview-src-list="[src]"
|
||||
:hide-on-click-modal="true"
|
||||
:initial-index="4"
|
||||
fit="cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- <img class="chat_img" :src="src" alt="">-->
|
||||
</div>
|
||||
<img :src="headUrl">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {defineProps, onMounted, ref} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
is_sender: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
headUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.chat-content {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.word {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 10px;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chat_img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::before {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-right: 10px solid #FFF;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.word-my {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 90%;
|
||||
margin-left: 10px;
|
||||
text-align: right;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
background: #95EC69;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.chat_img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::after {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-left: 10px solid #95EC69;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-image__error .image-slot {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.demo-image__error .image-slot .el-icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.demo-image__error .el-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
</style>
|
223
pywxdump/ui/src/components/chat/message/MessageFile.vue
Normal file
223
pywxdump/ui/src/components/chat/message/MessageFile.vue
Normal file
@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div class="chat-content">
|
||||
<!-- recordContent 聊天记录数组-->
|
||||
<!-- 对方 -->
|
||||
<div class="word" v-if="!is_sender">
|
||||
<img :src="headUrl">
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
<div style="float: left">
|
||||
<el-card shadow="hover" style="width:fit-content;">文件 : <a :href="videoSrc" download>{{
|
||||
file_info.file_name }}</a>
|
||||
<div>
|
||||
文件大小:<span>{{ file_info.file_size }}{{ file_info.file_size_unit }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 我的 -->
|
||||
<div class="word-my" v-else>
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
|
||||
<div style="float: right">
|
||||
<el-card shadow="hover" style="width:fit-content;">文件 : <a :href="videoSrc" download>{{
|
||||
file_info.file_name }}</a>
|
||||
<div>
|
||||
文件大小:<span>{{ file_info.file_size }}{{ file_info.file_size_unit }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
<img :src="headUrl">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, onMounted, reactive, ref } from "vue";
|
||||
import http from '@/utils/axios.js';
|
||||
import {api_file, api_file_info} from "@/api/base";
|
||||
|
||||
const props = defineProps({
|
||||
is_sender: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
headUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const videoSrc = ref("");
|
||||
const file_info = reactive({
|
||||
file_name: String,
|
||||
file_size: Number,
|
||||
file_size_unit: String,
|
||||
});
|
||||
onMounted(async () => {
|
||||
console.log('文件加载中')
|
||||
const file_info_resp = await api_file_info(props.src);
|
||||
// console.log(file_info_resp)
|
||||
file_info.file_name = file_info_resp.file_name;
|
||||
file_info.file_size = Number(file_info_resp.file_size) / 1024;
|
||||
if (file_info.file_size < 1024) {
|
||||
file_info.file_size_unit = "kb";
|
||||
} else {
|
||||
file_info.file_size = file_info.file_size / 1024;
|
||||
file_info.file_size_unit = "mb";
|
||||
}
|
||||
file_info.file_size = Number(file_info_resp.file_size) / 1024;
|
||||
// 字符串转float
|
||||
// var file_size = file_info_resp.file_size/1024;
|
||||
videoSrc.value = api_file(props.src);
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-content {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.word {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 10px;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chat_img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::before {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-right: 10px solid #FFF;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.word-my {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 90%;
|
||||
margin-left: 10px;
|
||||
text-align: right;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
background: #95EC69;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.chat_img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::after {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-left: 10px solid #95EC69;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-image__error .image-slot {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.demo-image__error .image-slot .el-icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.demo-image__error .el-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.demo-video__preview {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
</style>
|
||||
|
207
pywxdump/ui/src/components/chat/message/MessageImg.vue
Normal file
207
pywxdump/ui/src/components/chat/message/MessageImg.vue
Normal file
@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="chat-content">
|
||||
<!-- recordContent 聊天记录数组-->
|
||||
<!-- 对方 -->
|
||||
<div class="word" v-if="!is_sender">
|
||||
<img :src="headUrl">
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
|
||||
<div class="demo-image__preview">
|
||||
<el-image
|
||||
style="max-width: 150px; max-height: 150px"
|
||||
:src="src"
|
||||
:zoom-rate="1.2"
|
||||
:max-scale="7"
|
||||
:min-scale="0.2"
|
||||
:initial-index="4"
|
||||
:preview-src-list="[src]"
|
||||
:hide-on-click-modal="true"
|
||||
fit="cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- 我的 -->
|
||||
<div class="word-my" v-else>
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
|
||||
<div class="demo-image__preview">
|
||||
<el-image
|
||||
style="max-width: 150px; max-height: 150px"
|
||||
:src="src"
|
||||
:zoom-rate="1.2"
|
||||
:max-scale="7"
|
||||
:min-scale="0.2"
|
||||
:initial-index="4"
|
||||
:preview-src-list="[src]"
|
||||
:hide-on-click-modal="true"
|
||||
fit="cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- <img class="chat_img" :src="imgSrc" alt="">-->
|
||||
</div>
|
||||
<img :src="headUrl">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {defineProps, onMounted, ref} from "vue";
|
||||
import http from '@/utils/axios.js';
|
||||
|
||||
const props = defineProps({
|
||||
is_sender: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
headUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.chat-content {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.word {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 10px;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chat_img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::before {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-right: 10px solid #FFF;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.word-my {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 90%;
|
||||
margin-left: 10px;
|
||||
text-align: right;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
background: #95EC69;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.chat_img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::after {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-left: 10px solid #95EC69;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-image__error .image-slot {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.demo-image__error .image-slot .el-icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.demo-image__error .el-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
</style>
|
166
pywxdump/ui/src/components/chat/message/MessageOther.vue
Normal file
166
pywxdump/ui/src/components/chat/message/MessageOther.vue
Normal file
@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="chat-content">
|
||||
<!-- recordContent 聊天记录数组-->
|
||||
<!-- 对方 -->
|
||||
<div class="word" v-if="!is_sender">
|
||||
<img :src="headUrl">
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
<div class="info-content" v-html="sanitizeHTML(content)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 我的 -->
|
||||
<div class="word-my" v-else>
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
<div class="info-content" v-html="sanitizeHTML(content)"></div>
|
||||
</div>
|
||||
<img :src="headUrl">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {defineProps} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
is_sender: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
headUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const sanitizeHTML = (html: any) => {
|
||||
// Use DOMParser to parse the HTML and then serialize it to a trusted HTML string
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
return doc.body.innerHTML;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.chat-content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.word {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 10px;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
display: inline-block;
|
||||
|
||||
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::before {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-right: 10px solid #FFF;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.word-my {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 90%;
|
||||
margin-left: 10px;
|
||||
text-align: right;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
background: #95EC69;
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
|
||||
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::after {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-left: 10px solid #95EC69;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
164
pywxdump/ui/src/components/chat/message/MessageText.vue
Normal file
164
pywxdump/ui/src/components/chat/message/MessageText.vue
Normal file
@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="chat-content">
|
||||
<!-- recordContent 聊天记录数组-->
|
||||
<!-- 对方 -->
|
||||
<div class="word" v-if="!is_sender">
|
||||
<img :src="headUrl">
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
<div class="info-content" v-html="sanitizeHTML(content)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 我的 -->
|
||||
<div class="word-my" v-else>
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
<div class="info-content" v-html="sanitizeHTML(content)"></div>
|
||||
</div>
|
||||
<img :src="headUrl">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {defineProps} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
is_sender: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
headUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const sanitizeHTML = (html) => {
|
||||
// Use DOMParser to parse the HTML and then serialize it to a trusted HTML string
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
return doc.body.innerHTML;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.chat-content {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.word {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 10px;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
display: inline-block;
|
||||
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::before {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-right: 10px solid #FFF;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.word-my {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 90%;
|
||||
margin-left: 10px;
|
||||
text-align: right;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
background: #95EC69;
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
|
||||
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::after {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-left: 10px solid #95EC69;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
193
pywxdump/ui/src/components/chat/message/MessageVideo.vue
Normal file
193
pywxdump/ui/src/components/chat/message/MessageVideo.vue
Normal file
@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="chat-content">
|
||||
<!-- recordContent 聊天记录数组-->
|
||||
<!-- 对方 -->
|
||||
<div class="word" v-if="!is_sender">
|
||||
<img :src="headUrl">
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
<div class="demo-video__preview">
|
||||
<video controls width="30%">
|
||||
<source :src=src type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 我的 -->
|
||||
<div class="word-my" v-else>
|
||||
<div class="info">
|
||||
<p class="time">{{ direction }}</p>
|
||||
|
||||
<div class="demo-video__preview">
|
||||
<video controls width="50%">
|
||||
<source :src=src type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
<img :src="headUrl">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {defineProps, onMounted, ref} from "vue";
|
||||
import http from '@/utils/axios.js';
|
||||
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
is_sender: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
headUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.chat-content {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.word {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 10px;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chat_img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::before {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-right: 10px solid #FFF;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.word-my {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 90%;
|
||||
margin-left: 10px;
|
||||
text-align: right;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: rgba(51, 51, 51, 0.8);
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-top: -5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
background: #95EC69;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.chat_img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
//小三角形
|
||||
.info-content::after {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: 8px;
|
||||
content: '';
|
||||
border-left: 10px solid #95EC69;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-image__error .image-slot {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.demo-image__error .image-slot .el-icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.demo-image__error .el-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
.demo-video__preview {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
</style>
|
105
pywxdump/ui/src/components/chatBackup/ChatExportMain.vue
Normal file
105
pywxdump/ui/src/components/chatBackup/ChatExportMain.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, defineProps, nextTick, watch, type Ref} from 'vue';
|
||||
import ChatRecprdsHeader from "@/components/chat/ChatRecprdsHeader.vue";
|
||||
import DateTimeSelect from "@/components/utils/DateTimeSelect.vue";
|
||||
import http from '@/utils/axios.js';
|
||||
import {type Action, ElMessage, ElMessageBox} from "element-plus";
|
||||
import ExportENDB from "@/components/chatBackup/ExportENDB.vue";
|
||||
import ExportDEDB from "@/components/chatBackup/ExportDEDB.vue";
|
||||
import ExportCSV from "@/components/chatBackup/ExportCSV.vue";
|
||||
import ExportJSON from "@/components/chatBackup/ExportJSON.vue";
|
||||
import ExportHTML from "@/components/chatBackup/ExportHTML.vue";
|
||||
import ExportPDF from "@/components/chatBackup/ExportPDF.vue";
|
||||
import ExportDOCX from "@/components/chatBackup/ExportDOCX.vue";
|
||||
|
||||
const props = defineProps({
|
||||
wxid: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.wxid, (newVal: string, oldVal: String) => {
|
||||
console.log(newVal);
|
||||
});
|
||||
|
||||
const exportType: Ref<string> = ref(''); // 导出类型
|
||||
const result = ref(''); // 用于显示返回值
|
||||
|
||||
|
||||
const setting = {
|
||||
'endb': {
|
||||
brief: '加密文件',
|
||||
detail: "导出的内容为微信加密数据库。可还原回微信,但会覆盖微信后续消息。(全程不解密,所以数据安全)",
|
||||
},
|
||||
'dedb': {
|
||||
brief: '解密文件',
|
||||
detail: "导出的文件为解密后的sqlite数据库,并且会自动合并msg和media数据库为同一个,但是无法还原回微信。",
|
||||
},
|
||||
'csv': {
|
||||
brief: 'csv',
|
||||
detail: "只包含文本,但是可以用excel软件(wps,office)打开。",
|
||||
},
|
||||
'json': {
|
||||
brief: 'json',
|
||||
detail: "只包含文本,可用于数据分析,情感分析等方面。",
|
||||
},
|
||||
'html': {
|
||||
brief: 'html-测试中',
|
||||
detail: "主要用于浏览器可视化查看。",
|
||||
},
|
||||
'pdf': {
|
||||
brief: 'pdf-开发中',
|
||||
detail: "pdf版本。",
|
||||
},
|
||||
'docx': {
|
||||
brief: 'docx-开发中',
|
||||
detail: "docx版本。",
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="chat_export" style="background-color: #d2d2fa;padding:0;">
|
||||
|
||||
<!-- 分割线 -->
|
||||
<el-main style="overflow-y: auto; height: calc(100vh - 65px);padding: 0">
|
||||
<div style="background-color: #d2d2fa;height: calc(100vh - 65px); display: grid; place-items: center; ">
|
||||
<div
|
||||
style="background-color: #fff; width: 70%; height: 70%; border-radius: 10px; padding: 20px; overflow: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="font-size: 20px; font-weight: bold;">导出与备份(未完待续...)</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<!-- <el-button style="margin-right: 10px;" @click="exportData">导出</el-button>-->
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
导出类型:
|
||||
<el-select placeholder="请选择导出类型" style="width: 50%;" v-model="exportType">
|
||||
<el-option :label="value.brief" :value="index" v-for="(value,index) in setting" :key="index">
|
||||
{{ value.brief }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
<br/><br/>
|
||||
<span v-if="exportType">
|
||||
{{ setting[exportType].detail }}
|
||||
</span>
|
||||
</div>
|
||||
<el-divider/>
|
||||
<ExportENDB v-if="exportType=='endb'" :wxid="props.wxid"/>
|
||||
<ExportDEDB v-if="exportType=='dedb'" :wxid="props.wxid"/>
|
||||
<ExportCSV v-if="exportType=='csv'" :wxid="props.wxid"/>
|
||||
<ExportJSON v-if="exportType=='json'" :wxid="props.wxid"/>
|
||||
<ExportHTML v-if="exportType=='html'" :wxid="props.wxid"/>
|
||||
<ExportPDF v-if="exportType=='pdf'" :wxid="props.wxid"/>
|
||||
<ExportDOCX v-if="exportType=='docx'" :wxid="props.wxid"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
61
pywxdump/ui/src/components/chatBackup/ExportCSV.vue
Normal file
61
pywxdump/ui/src/components/chatBackup/ExportCSV.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {defineProps, ref, watch} from "vue";
|
||||
import http from "@/utils/axios.js";
|
||||
import DateTimeSelect from "@/components/utils/DateTimeSelect.vue";
|
||||
|
||||
const props = defineProps({
|
||||
wxid: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
watch(() => props.wxid, (newVal: string, oldVal: String) => {
|
||||
console.log(newVal);
|
||||
});
|
||||
// 上述代码是监听props.wxid的变化,当props.wxid变化时,会打印新值。
|
||||
|
||||
// const datetime = ref([]);
|
||||
const Result = ref("");
|
||||
|
||||
const requestExport = async () => {
|
||||
Result.value = "正在处理中...";
|
||||
try {
|
||||
Result.value = await http.post('/api/rs/export_csv', {
|
||||
'wxid': props.wxid,
|
||||
// 'datetime': datetime.value,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data msg_count:', error);
|
||||
Result.value = "请求失败\n" + error;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 处理时间选择器的数据
|
||||
// const handDatetimeChildData = (val: any) => {
|
||||
// datetime.value = val;
|
||||
// }
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- <div>-->
|
||||
<!-- <strong>时间(默认全部):</strong>-->
|
||||
<!-- <DateTimeSelect @datetime="handDatetimeChildData"/>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div style="position: relative;">
|
||||
<el-button type="primary" @click="requestExport()">导出</el-button>
|
||||
</div>
|
||||
<el-divider/>
|
||||
<!-- 结果显示 -->
|
||||
<el-input type="textarea" :rows="6" readonly placeholder="" v-model="Result"
|
||||
style="width: 100%;"></el-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
63
pywxdump/ui/src/components/chatBackup/ExportDEDB.vue
Normal file
63
pywxdump/ui/src/components/chatBackup/ExportDEDB.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {defineProps, ref, watch} from "vue";
|
||||
import http from "@/utils/axios.js";
|
||||
|
||||
const props = defineProps({
|
||||
wxid: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
watch(() => props.wxid, (newVal: string, oldVal: String) => {
|
||||
console.log(newVal);
|
||||
});
|
||||
// 上述代码是监听props.wxid的变化,当props.wxid变化时,会打印新值。
|
||||
|
||||
const wx_path = ref("");
|
||||
const key = ref("");
|
||||
const Result = ref("");
|
||||
|
||||
const requestExport = async () => {
|
||||
Result.value = "正在处理中...";
|
||||
try {
|
||||
Result.value = await http.post('/api/rs/export_dedb', {
|
||||
'key': key.value,
|
||||
'wx_path': wx_path.value,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data msg_count:', error);
|
||||
Result.value = "请求失败\n" + error;
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
密钥(可选):
|
||||
<el-input placeholder="密钥[可为空,空表示使用默认的,无默认会报错]"
|
||||
v-model="key"
|
||||
style="width: 75%;"></el-input>
|
||||
<br><br>
|
||||
微信文件夹路径(可选):
|
||||
<el-input placeholder="微信文件夹路径[可为空,空表示使用默认的,无默认会报错](eg: C:\****\WeChat Files\wxid_**** )"
|
||||
v-model="wx_path"
|
||||
style="width: 70%;"></el-input>
|
||||
<br><br>
|
||||
|
||||
<div style="position: relative;">
|
||||
<el-button type="primary" @click="requestExport()">导出</el-button>
|
||||
</div>
|
||||
<el-divider/>
|
||||
<!-- 结果显示 -->
|
||||
<el-input type="textarea" :rows="6" readonly placeholder="" v-model="Result"
|
||||
style="width: 100%;"></el-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
31
pywxdump/ui/src/components/chatBackup/ExportDOCX.vue
Normal file
31
pywxdump/ui/src/components/chatBackup/ExportDOCX.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {defineProps, watch} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
wxid: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.wxid, (newVal: string, oldVal: String) => {
|
||||
console.log(newVal);
|
||||
});
|
||||
|
||||
const requestExport = () => {
|
||||
console.log('requestExport');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<span>{{props.wxid}}</span><br>
|
||||
<el-button type="primary" @click="requestExport()">导出</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
54
pywxdump/ui/src/components/chatBackup/ExportENDB.vue
Normal file
54
pywxdump/ui/src/components/chatBackup/ExportENDB.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {defineProps, ref, watch} from "vue";
|
||||
import http from "@/utils/axios.js";
|
||||
|
||||
const props = defineProps({
|
||||
wxid: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
watch(() => props.wxid, (newVal: string, oldVal: String) => {
|
||||
console.log(newVal);
|
||||
});
|
||||
// 上述代码是监听props.wxid的变化,当props.wxid变化时,会打印新值。
|
||||
|
||||
const wx_path = ref("");
|
||||
const Result = ref("");
|
||||
|
||||
const requestExport = async () => {
|
||||
Result.value = "请求中...";
|
||||
try {
|
||||
Result.value = await http.post('/api/rs/export_endb', {
|
||||
'wx_path': wx_path.value,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data msg_count:', error);
|
||||
Result.value = "请求失败\n"+error;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
微信文件夹路径(可选):
|
||||
<el-input placeholder="微信文件夹路径[可为空,空表示使用默认的,无默认会报错](eg: C:\****\WeChat Files\wxid_**** )"
|
||||
v-model="wx_path"
|
||||
style="width: 70%;"></el-input>
|
||||
<br><br>
|
||||
<div style="position: relative;">
|
||||
<el-button type="primary" @click="requestExport()">导出</el-button>
|
||||
</div>
|
||||
<el-divider/>
|
||||
<!-- 结果显示 -->
|
||||
<el-input type="textarea" :rows="6" readonly placeholder="" v-model="Result"
|
||||
style="width: 100%;"></el-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
62
pywxdump/ui/src/components/chatBackup/ExportHTML.vue
Normal file
62
pywxdump/ui/src/components/chatBackup/ExportHTML.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {defineProps, ref, watch} from "vue";
|
||||
import http from "@/utils/axios.js";
|
||||
import DateTimeSelect from "@/components/utils/DateTimeSelect.vue";
|
||||
|
||||
const props = defineProps({
|
||||
wxid: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
watch(() => props.wxid, (newVal: string, oldVal: String) => {
|
||||
console.log(newVal);
|
||||
});
|
||||
// 上述代码是监听props.wxid的变化,当props.wxid变化时,会打印新值。
|
||||
|
||||
const datetime = ref([]);
|
||||
const Result = ref("");
|
||||
|
||||
const requestExport = async () => {
|
||||
Result.value = "正在处理中...";
|
||||
try {
|
||||
Result.value = await http.post('/api/rs/export_html', {
|
||||
'wxid': props.wxid,
|
||||
// 'datetime': datetime.value,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data msg_count:', error);
|
||||
Result.value = "请求失败\n" + error;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 处理时间选择器的数据
|
||||
const handDatetimeChildData = (val: any) => {
|
||||
datetime.value = val;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- <div>-->
|
||||
<!-- <strong>时间(默认全部):</strong>-->
|
||||
<!-- <DateTimeSelect @datetime="handDatetimeChildData"/>-->
|
||||
<!-- </div>-->
|
||||
<span>使用说明:(1)根据 https://blog.csdn.net/meser88/article/details/130229417 进行设置</span><br/>
|
||||
<span>(2)打开导出的文件夹位置,使用(1)设置的浏览器打开 index.html 文件</span>
|
||||
<div style="position: relative;">
|
||||
<el-button type="primary" @click="requestExport()">导出</el-button>
|
||||
</div>
|
||||
<el-divider/>
|
||||
<!-- 结果显示 -->
|
||||
<el-input type="textarea" :rows="6" readonly placeholder="" v-model="Result"
|
||||
style="width: 100%;"></el-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
61
pywxdump/ui/src/components/chatBackup/ExportJSON.vue
Normal file
61
pywxdump/ui/src/components/chatBackup/ExportJSON.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {defineProps, ref, watch} from "vue";
|
||||
import http from "@/utils/axios.js";
|
||||
import DateTimeSelect from "@/components/utils/DateTimeSelect.vue";
|
||||
|
||||
const props = defineProps({
|
||||
wxid: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
watch(() => props.wxid, (newVal: string, oldVal: String) => {
|
||||
console.log(newVal);
|
||||
});
|
||||
// 上述代码是监听props.wxid的变化,当props.wxid变化时,会打印新值。
|
||||
|
||||
const datetime = ref([]);
|
||||
const Result = ref("");
|
||||
|
||||
const requestExport = async () => {
|
||||
Result.value = "正在处理中...";
|
||||
try {
|
||||
Result.value = await http.post('/api/rs/export_json', {
|
||||
'wxid': props.wxid,
|
||||
// 'datetime': datetime.value,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching data msg_count:', error);
|
||||
Result.value = "请求失败\n" + error;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 处理时间选择器的数据
|
||||
const handDatetimeChildData = (val: any) => {
|
||||
datetime.value = val;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- <div>-->
|
||||
<!-- <strong>时间(默认全部):</strong>-->
|
||||
<!-- <DateTimeSelect @datetime="handDatetimeChildData"/>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div style="position: relative;">
|
||||
<el-button type="primary" @click="requestExport()">导出</el-button>
|
||||
</div>
|
||||
<el-divider/>
|
||||
<!-- 结果显示 -->
|
||||
<el-input type="textarea" :rows="6" readonly placeholder="" v-model="Result"
|
||||
style="width: 100%;"></el-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
31
pywxdump/ui/src/components/chatBackup/ExportPDF.vue
Normal file
31
pywxdump/ui/src/components/chatBackup/ExportPDF.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {defineProps, watch} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
wxid: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.wxid, (newVal: string, oldVal: String) => {
|
||||
console.log(newVal);
|
||||
});
|
||||
|
||||
const requestExport = () => {
|
||||
console.log('requestExport');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<span>{{props.wxid}}</span><br>
|
||||
<el-button type="primary" @click="requestExport()">导出</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
7
pywxdump/ui/src/components/icons/IconCommunity.vue
Normal file
7
pywxdump/ui/src/components/icons/IconCommunity.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
pywxdump/ui/src/components/icons/IconDocumentation.vue
Normal file
7
pywxdump/ui/src/components/icons/IconDocumentation.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
pywxdump/ui/src/components/icons/IconEcosystem.vue
Normal file
7
pywxdump/ui/src/components/icons/IconEcosystem.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
pywxdump/ui/src/components/icons/IconSupport.vue
Normal file
7
pywxdump/ui/src/components/icons/IconSupport.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
19
pywxdump/ui/src/components/icons/IconTooling.vue
Normal file
19
pywxdump/ui/src/components/icons/IconTooling.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
173
pywxdump/ui/src/components/stats/ContactStats.vue
Normal file
173
pywxdump/ui/src/components/stats/ContactStats.vue
Normal file
@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import * as echarts from "echarts";
|
||||
import {onMounted, ref, shallowRef} from "vue";
|
||||
import {apiDateCount, apiTalkerCount, apiWordcloud} from "@/api/stat";
|
||||
import {apiUserList} from "@/api/chat";
|
||||
import {gen_show_name, type User} from "@/utils/common_utils";
|
||||
import DateTimeSelect from "@/components/utils/DateTimeSelect.vue";
|
||||
import ColorSelect from "@/components/utils/ColorSelect.vue";
|
||||
import ChartInit from "@/components/stats/components/ChartInit.vue";
|
||||
|
||||
|
||||
// https://echarts.apache.org/examples/en/editor.html
|
||||
|
||||
interface gender_face {
|
||||
男: number
|
||||
女: number
|
||||
未知: number
|
||||
}
|
||||
|
||||
const user = ref<{ [key: string]: User }>({});
|
||||
const gender_data = ref<gender_face>({});
|
||||
const signature_count_dict = ref<{ [key: string]: number }>({});
|
||||
|
||||
const is_update = ref(false);
|
||||
const chart_option = ref({
|
||||
backgroundColor: "",
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
title: {
|
||||
left: 'center',
|
||||
text: '联系人画像'
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: {}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '性别',
|
||||
type: 'pie',
|
||||
radius: ["10%", "20%"],
|
||||
center: ['50%', '200px'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
data: <any>[]
|
||||
}, {
|
||||
name: '个性签名词云',
|
||||
type: 'wordCloud',
|
||||
sizeRange: [15, 80],
|
||||
rotationRange: [0, 0],
|
||||
rotationStep: 45,
|
||||
gridSize: 8,
|
||||
shape: 'cardioid',
|
||||
keepAspect: false,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
drawOutOfBound: false,
|
||||
textStyle: {
|
||||
normal: {
|
||||
color: function () {
|
||||
return 'rgb(' + [
|
||||
Math.round(Math.random() * 160),
|
||||
Math.round(Math.random() * 160),
|
||||
Math.round(Math.random() * 160)
|
||||
].join(',') + ')';
|
||||
},
|
||||
fontFamily: 'sans-serif',
|
||||
fontWeight: 'normal'
|
||||
},
|
||||
emphasis: {
|
||||
shadowBlur: 10,
|
||||
shadowColor: '#333'
|
||||
}
|
||||
},
|
||||
data: <any>[]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
const get_data = async () => {
|
||||
user.value = await apiUserList();
|
||||
signature_count_dict.value = await apiWordcloud("signature");
|
||||
|
||||
let gender_data1 = {'男': 0, '女': 0, '未知': 0};
|
||||
let province_data: { [key: string]: number } = {};
|
||||
let city_data: { [key: string]: number } = {};
|
||||
let signature_data: { [key: string]: number } = {};
|
||||
for (let key in user.value) {
|
||||
let u = user.value[key];
|
||||
let ExtraBuf = u.ExtraBuf;
|
||||
if (ExtraBuf) {
|
||||
if (ExtraBuf["性别[1男2女]"] == 1) {
|
||||
gender_data1['男'] += 1;
|
||||
} else if (ExtraBuf["性别[1男2女]"] == 2) {
|
||||
gender_data1['女'] += 1
|
||||
} else {
|
||||
gender_data1['未知'] += 1
|
||||
}
|
||||
} else {
|
||||
gender_data1['未知'] += 1
|
||||
}
|
||||
}
|
||||
gender_data.value = gender_data1;
|
||||
}
|
||||
|
||||
// 刷新图表 START
|
||||
const refreshChart = async (is_get_data: boolean = true) => {
|
||||
if (is_get_data) {
|
||||
await get_data();
|
||||
}
|
||||
// 渲染图表
|
||||
chart_option.value.series[0].data = [
|
||||
{'value': gender_data.value["男"], 'name': '男', itemStyle: {color: '#4F6FE8'}},
|
||||
{'value': gender_data.value["女"], 'name': '女', itemStyle: {color: '#FF6347'}}
|
||||
]
|
||||
|
||||
chart_option.value.series[1].data = Object.keys(signature_count_dict.value).map((key) => {
|
||||
return {name: key, value: signature_count_dict.value[key]}
|
||||
});
|
||||
is_update.value = !is_update.value;
|
||||
}
|
||||
// 刷新图表 END
|
||||
|
||||
onMounted(() => {
|
||||
refreshChart();
|
||||
});
|
||||
|
||||
|
||||
// 搜索联系人相关 END
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-layout" style="background-color: #d2d2fa;height: 100%;width: 100%;">
|
||||
<el-container style="height: 100%;width: 100%;">
|
||||
<el-header :height="'80px'" style="width: 100%;">
|
||||
<strong>颜色设置:</strong>
|
||||
bg:
|
||||
<color-select
|
||||
@updateColors="(val:any)=>{val?chart_option.backgroundColor=val:'';refreshChart(false)}"></color-select>
|
||||
</el-header>
|
||||
|
||||
<el-main style="height: calc(100% - 100px);width: 100%;">
|
||||
<chart-init :option="chart_option" :update="is_update" class="charts_main"/>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.charts_main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
227
pywxdump/ui/src/components/stats/DateChatHeatmapStats.vue
Normal file
227
pywxdump/ui/src/components/stats/DateChatHeatmapStats.vue
Normal file
@ -0,0 +1,227 @@
|
||||
<script setup lang="ts">
|
||||
import * as echarts from "echarts";
|
||||
import {onMounted, ref, shallowRef} from "vue";
|
||||
import {apiDateCount, apiTalkerCount} from "@/api/stat";
|
||||
import {apiUserList} from "@/api/chat";
|
||||
import {gen_show_name, type User} from "@/utils/common_utils";
|
||||
import DateTimeSelect from "@/components/utils/DateTimeSelect.vue";
|
||||
import ColorSelect from "@/components/utils/ColorSelect.vue";
|
||||
import NumberInput from "@/components/utils/NumberInput.vue";
|
||||
import ChartInit from "@/components/stats/components/ChartInit.vue";
|
||||
|
||||
// https://echarts.apache.org/examples/en/editor.html
|
||||
|
||||
interface CountData {
|
||||
sender_count: number
|
||||
receiver_count: number
|
||||
total_count: number
|
||||
}
|
||||
|
||||
interface calendar_face {
|
||||
top: number
|
||||
left: number
|
||||
orient: string
|
||||
range: string
|
||||
dayLabel: any
|
||||
monthLabel: any
|
||||
yearLabel: any
|
||||
}
|
||||
|
||||
interface series_face {
|
||||
type: string
|
||||
coordinateSystem: string
|
||||
calendarIndex: number
|
||||
data: any[]
|
||||
}
|
||||
|
||||
|
||||
const datetime = ref([0, 0]);
|
||||
const word = ref("");
|
||||
const loading = ref(false);
|
||||
const user_options = ref<User[]>([]);
|
||||
|
||||
const date_count_data = ref<any>({});
|
||||
const top_user = ref<{ [key: string]: User }>({});
|
||||
const top_user_count = ref<{ [key: string]: CountData }>({});
|
||||
|
||||
const is_update = ref(false);
|
||||
const chart_option = ref({
|
||||
backgroundColor: "#ffffff",
|
||||
title: {
|
||||
left: 'center',
|
||||
text: '聊天记录(不包括群聊)'
|
||||
},
|
||||
toolbox: {
|
||||
feature: {saveAsImage: {}}
|
||||
},
|
||||
tooltip: {
|
||||
position: 'top',
|
||||
formatter: function (p: any) {
|
||||
return p.data[0] + '<br>聊天数量:' + p.data[1];
|
||||
}
|
||||
},
|
||||
visualMap: {
|
||||
min: 0,
|
||||
max: 500,
|
||||
calculable: true,
|
||||
orient: 'vertical',
|
||||
right: '0',
|
||||
top: 'center'
|
||||
},
|
||||
calendar: <calendar_face[]>[],
|
||||
series: <series_face[]>[],
|
||||
|
||||
});
|
||||
|
||||
|
||||
const get_date_count_data = async () => {
|
||||
// {"2024-12-20":{ "sender_count": sender_count, "receiver_count": receiver_count, "total_count": total_count },....}
|
||||
date_count_data.value = await apiDateCount(word.value, datetime.value[0] / 1000, datetime.value[1] / 1000);
|
||||
|
||||
// 根据key排序
|
||||
date_count_data.value = Object.fromEntries(Object.entries(date_count_data.value).sort());
|
||||
|
||||
}
|
||||
|
||||
const get_top_user_count = async () => {
|
||||
// {"wxid":{ "sender_count": sender_count, "receiver_count": receiver_count, "total_count": total_count },....}
|
||||
const body_data = await apiTalkerCount();
|
||||
top_user.value = await apiUserList("", Object.keys(body_data));
|
||||
top_user_count.value = body_data;
|
||||
// 根据total_count排序
|
||||
top_user_count.value = Object.fromEntries(Object.entries(top_user_count.value).sort((a, b) => b[1].total_count - a[1].total_count));
|
||||
}
|
||||
|
||||
// 刷新图表 START
|
||||
const refreshChart = async (is_get_data: boolean = true) => {
|
||||
if (is_get_data) {
|
||||
await get_date_count_data();
|
||||
}
|
||||
// 渲染图表
|
||||
let min_date = Object.keys(date_count_data.value)[0];
|
||||
let max_date = Object.keys(date_count_data.value)[Object.keys(date_count_data.value).length - 1];
|
||||
|
||||
let min_year = parseInt(min_date.split("-")[0]);
|
||||
let max_year = parseInt(max_date.split("-")[0]);
|
||||
|
||||
chart_option.value.calendar = [];
|
||||
chart_option.value.series = [];
|
||||
for (let i = min_year; i < max_year + 1; i++) {
|
||||
chart_option.value.calendar.push({
|
||||
top: 100,
|
||||
left: 50 + 200 * (i - min_year),
|
||||
orient: 'vertical',
|
||||
range: i.toString(),
|
||||
dayLabel: {
|
||||
margin: 5, firstDay: 1, nameMap: ['日', '一', '二', '三', '四', '五', '六'],
|
||||
},
|
||||
monthLabel: {
|
||||
margin: 5,
|
||||
nameMap: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"]
|
||||
},
|
||||
yearLabel: {"color": "#000"}
|
||||
});
|
||||
chart_option.value.series.push({
|
||||
type: 'heatmap', coordinateSystem: 'calendar', calendarIndex: i - min_year, data: []
|
||||
});
|
||||
}
|
||||
|
||||
// refreshData();
|
||||
Object.keys(date_count_data.value).map(date => {
|
||||
let year = parseInt(date.split("-")[0]);
|
||||
let index = year - min_year;
|
||||
chart_option.value.series[index].data.push([date, date_count_data.value[date].total_count]);
|
||||
});
|
||||
is_update.value = !is_update.value;
|
||||
}
|
||||
// 刷新图表 END
|
||||
|
||||
onMounted(() => {
|
||||
get_top_user_count();
|
||||
refreshChart();
|
||||
});
|
||||
|
||||
|
||||
// 搜索联系人相关 START
|
||||
const search_user = async (query: string) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
if (query === '') {
|
||||
user_options.value = [];
|
||||
return;
|
||||
}
|
||||
const body_data = await apiUserList(query);
|
||||
loading.value = false;
|
||||
user_options.value = Object.values(body_data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const set_top_user = async (wxid: string) => {
|
||||
try {
|
||||
word.value = wxid;
|
||||
await refreshChart();
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// 搜索联系人相关 END
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-layout" style="background-color: #d2d2fa;height: 100%;width: 100%;">
|
||||
<el-container style="height: 100%;width: 100%;">
|
||||
<el-header :height="'80px'" style="width: 100%;">
|
||||
<strong>时间(默认全部):</strong>
|
||||
<DateTimeSelect @datetime="(val: any) => {datetime = val;}"/>
|
||||
<el-select
|
||||
v-model="word"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
placeholder="输入想查看的联系人"
|
||||
remote-show-suffix
|
||||
clearable
|
||||
:remote-method="search_user"
|
||||
:loading="loading"
|
||||
style="width: 240px"
|
||||
>
|
||||
<el-option v-for="item in user_options" :key="item.wxid" :label="gen_show_name(item)" :value="item.wxid"/>
|
||||
</el-select>
|
||||
<el-button type="primary" @click="refreshChart">查看</el-button>
|
||||
 
|
||||
<strong>颜色设置:</strong>
|
||||
bg:
|
||||
<color-select
|
||||
@updateColors="(val:any)=>{val?chart_option.backgroundColor=val:'';refreshChart(false)}"></color-select>
|
||||
min:
|
||||
<number-input :n="chart_option.visualMap.min" :step="100"
|
||||
@updateNumber="(val:any)=>{val?chart_option.visualMap.min=val:'';refreshChart(false)}"></number-input>
|
||||
max:
|
||||
<number-input :n="chart_option.visualMap.max" :step="100"
|
||||
@updateNumber="(val:any)=>{val?chart_option.visualMap.max=val:'';refreshChart(false)}"></number-input>
|
||||
<br>
|
||||
<strong>top10[总:(收/发)]:</strong>
|
||||
<template v-for="wxid in Object.keys(top_user_count)" :key="wxid">
|
||||
<el-button type="primary" plain @click="set_top_user(wxid)" size="small">
|
||||
{{ gen_show_name(top_user[wxid]) }} [{{ top_user_count[wxid]?.total_count }}({{
|
||||
top_user_count[wxid]?.receiver_count
|
||||
}}/{{ top_user_count[wxid]?.sender_count }})]
|
||||
</el-button>
|
||||
</template>
|
||||
</el-header>
|
||||
|
||||
<el-main style="height: calc(100% - 100px);width: 100%;">
|
||||
<chart-init :option="chart_option" :update="is_update"/>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
326
pywxdump/ui/src/components/stats/DateChatStats.vue
Normal file
326
pywxdump/ui/src/components/stats/DateChatStats.vue
Normal file
@ -0,0 +1,326 @@
|
||||
<script setup lang="ts">
|
||||
import * as echarts from "echarts";
|
||||
import {onMounted, ref, shallowRef} from "vue";
|
||||
import {apiDateCount, apiTalkerCount} from "@/api/stat";
|
||||
import {apiUserList} from "@/api/chat";
|
||||
import {gen_show_name, type User} from "@/utils/common_utils";
|
||||
import DateTimeSelect from "@/components/utils/DateTimeSelect.vue";
|
||||
import ColorSelect from "@/components/utils/ColorSelect.vue";
|
||||
import ChartInit from "@/components/stats/components/ChartInit.vue";
|
||||
|
||||
// https://echarts.apache.org/examples/en/editor.html
|
||||
|
||||
interface CountData {
|
||||
sender_count: number
|
||||
receiver_count: number
|
||||
total_count: number
|
||||
}
|
||||
|
||||
const date_count_data = ref<{ [key: string]: CountData }>({});
|
||||
|
||||
const datetime = ref([0, 0]);
|
||||
const word = ref("");
|
||||
const loading = ref(false);
|
||||
const user_options = ref<User[]>([]);
|
||||
|
||||
const top_user = ref<{ [key: string]: User }>({});
|
||||
const top_user_count = ref<{ [key: string]: CountData }>({});
|
||||
|
||||
const is_update = ref(false);
|
||||
|
||||
const colors = [
|
||||
{
|
||||
"color": '#ffeab6',
|
||||
"areaStyle": new echarts.graphic.LinearGradient(0, 0, 0, 1,
|
||||
[{offset: 0, color: "rgba(255,234,182,0)"},
|
||||
{offset: 1, color: "rgba(255,234,182,0)"}])
|
||||
}, {
|
||||
"color": '#c0ffc2',
|
||||
"areaStyle": new echarts.graphic.LinearGradient(0, 0, 0, 1,
|
||||
[{offset: 0, color: "rgba(192,255,194,0)"},
|
||||
{offset: 1, color: "rgba(192,255,194,0)"}])
|
||||
}, {
|
||||
"color": '#a1d9ff',
|
||||
"areaStyle": new echarts.graphic.LinearGradient(0, 0, 0, 1,
|
||||
[{offset: 0, color: "rgba(161,217,255,0)"},
|
||||
{offset: 1, color: "rgba(161,217,255,0)"}])
|
||||
}, {
|
||||
"color": '#D37373',
|
||||
"areaStyle": new echarts.graphic.LinearGradient(0, 0, 0, 1,
|
||||
[{offset: 0, color: "rgba(192,255,194,0)"},
|
||||
{offset: 1, color: "rgba(192,255,194,0)"}])
|
||||
}, {
|
||||
"color": '#e4ecf6',
|
||||
"areaStyle": new echarts.graphic.LinearGradient(0, 0, 0, 1,
|
||||
[{offset: 0, color: "rgba(161,217,255,0)"},
|
||||
{offset: 1, color: "rgba(161,217,255,0)"}])
|
||||
}, {
|
||||
"color": 'rgba(185,4,245,0.44)',
|
||||
"areaStyle": new echarts.graphic.LinearGradient(0, 0, 0, 1,
|
||||
[{offset: 0, color: "rgba(161,217,255,0)"},
|
||||
{offset: 1, color: "rgba(161,217,255,0)"}])
|
||||
}
|
||||
];
|
||||
const bg_color = ref("");
|
||||
|
||||
const chart_option = ref({
|
||||
backgroundColor: bg_color.value,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
position: function (pt: any) {
|
||||
return [pt[0], '90%'];
|
||||
},
|
||||
formatter: function (params: any) {
|
||||
let date = params[0].name;
|
||||
// let total_count = params[0].value;
|
||||
// let sender_count = params[1].value;
|
||||
// let receiver_count = params[2].value;
|
||||
let total_count = date_count_data.value[date].total_count;
|
||||
let sender_count = date_count_data.value[date].sender_count;
|
||||
let receiver_count = date_count_data.value[date].receiver_count;
|
||||
return `${date}<br>
|
||||
聊天记录数量:${total_count}<br>
|
||||
发送数量:${sender_count}(${(sender_count / total_count * 100).toFixed(2)}%)<br>
|
||||
接收数量:${receiver_count}(${(receiver_count / total_count * 100).toFixed(2)}%) `
|
||||
}
|
||||
},
|
||||
title: {
|
||||
left: 'center',
|
||||
text: '日聊天记录(不包括群聊)'
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: 'none'
|
||||
},
|
||||
saveAsImage: {}
|
||||
}
|
||||
},
|
||||
dataZoom: [
|
||||
{type: 'inside', start: 0, end: 100},
|
||||
{start: 0, end: 100}
|
||||
],
|
||||
legend: {
|
||||
right: '1%', // 设置图例位于右侧,距离右边边缘 5%
|
||||
top: '5%', // 设置图例位于上方
|
||||
orient: 'vertical' // 设置图例为垂直排列
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category', // x 轴类型为分类
|
||||
boundaryGap: false, // x 轴两端不留空白间隙
|
||||
data: <any>[], // x 轴的数据,这里使用了 TypeScript 的泛型表示尚未填充数据
|
||||
},
|
||||
yAxis: [
|
||||
{type: 'value', boundaryGap: ['100%', '10%'], name: '数量', axisLabel: {formatter: '{value}'}},
|
||||
{
|
||||
type: 'value', boundaryGap: [0, '100%'], name: '百分比', position: 'right', max: 200,
|
||||
axisLabel: {formatter: '{value} %'}
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '聊天记录数量',
|
||||
type: 'line',
|
||||
symbol: 'none',
|
||||
sampling: 'lttb',
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {color: colors[0].color},
|
||||
areaStyle: {color: colors[0].areaStyle},
|
||||
data: <any>[]
|
||||
}, {
|
||||
name: '发送数量',
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
show: false,
|
||||
symbol: 'none',
|
||||
sampling: 'lttb',
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {color: colors[1].color},
|
||||
areaStyle: {color: colors[1].areaStyle},
|
||||
data: <any>[]
|
||||
}, {
|
||||
name: '接收数量',
|
||||
type: 'line',
|
||||
symbol: 'none',
|
||||
show: false,
|
||||
showSymbol: false,
|
||||
sampling: 'lttb',
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {color: colors[2].color},
|
||||
areaStyle: {color: colors[2].areaStyle},
|
||||
data: <any>[]
|
||||
}, {
|
||||
name: '发送数量bar',
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
barWidth: '50%',
|
||||
yAxisIndex: 1,
|
||||
itemStyle: {color: colors[3].color},
|
||||
areaStyle: {color: colors[3].areaStyle},
|
||||
data: <any>[]
|
||||
}, {
|
||||
name: '接收数量bar',
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
barWidth: '50%',
|
||||
yAxisIndex: 1,
|
||||
itemStyle: {color: colors[4].color},
|
||||
areaStyle: {color: colors[4].areaStyle},
|
||||
data: <any>[]
|
||||
}, {
|
||||
name: '分界线',
|
||||
type: 'line',
|
||||
symbol: 'none',
|
||||
sampling: 'lttb',
|
||||
yAxisIndex: 1,
|
||||
itemStyle: {color: colors[5].color},
|
||||
areaStyle: {color: colors[5].areaStyle},
|
||||
data: <any>[]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const update_chart_option = () => {
|
||||
for (let i = 0; i < chart_option.value.series.length; i++) {
|
||||
chart_option.value.series[i].itemStyle.color = colors[i].color;
|
||||
chart_option.value.series[i].areaStyle.color = colors[i].areaStyle;
|
||||
}
|
||||
chart_option.value.backgroundColor = bg_color.value;
|
||||
}
|
||||
|
||||
const get_date_count_data = async () => {
|
||||
// {"2024-12-20":{ "sender_count": sender_count, "receiver_count": receiver_count, "total_count": total_count },....}
|
||||
date_count_data.value = await apiDateCount(word.value, datetime.value[0] / 1000, datetime.value[1] / 1000);
|
||||
// 根据key排序
|
||||
date_count_data.value = Object.fromEntries(Object.entries(date_count_data.value).sort());
|
||||
}
|
||||
|
||||
const get_top_user_count = async () => {
|
||||
// {"wxid":{ "sender_count": sender_count, "receiver_count": receiver_count, "total_count": total_count },....}
|
||||
const body_data = await apiTalkerCount();
|
||||
top_user.value = await apiUserList("", Object.keys(body_data));
|
||||
top_user_count.value = body_data;
|
||||
// 根据total_count排序
|
||||
top_user_count.value = Object.fromEntries(Object.entries(top_user_count.value).sort((a, b) => b[1].total_count - a[1].total_count));
|
||||
}
|
||||
|
||||
// 刷新图表 START
|
||||
const refreshChart = async (is_get_data: boolean = true) => {
|
||||
if (is_get_data) {
|
||||
await get_date_count_data();
|
||||
}
|
||||
// refreshData();
|
||||
chart_option.value.xAxis.data = Object.keys(date_count_data.value);
|
||||
chart_option.value.series[0].data = Object.values(date_count_data.value).map((item: any) => item.total_count);
|
||||
// chart_option.value.series[1].data = Object.values(date_count_data.value).map((item: any) => item.sender_count);
|
||||
// chart_option.value.series[2].data = Object.values(date_count_data.value).map((item: any) => item.receiver_count);
|
||||
chart_option.value.series[3].data = Object.values(date_count_data.value).map((item: any) => item.sender_count / item.total_count * 100);
|
||||
chart_option.value.series[4].data = Object.values(date_count_data.value).map((item: any) => item.receiver_count / item.total_count * 100);
|
||||
chart_option.value.series[5].data = Object.values(date_count_data.value).map((item: any) => 50);
|
||||
// 渲染图表
|
||||
is_update.value = !is_update.value;
|
||||
}
|
||||
// 刷新图表 END
|
||||
|
||||
onMounted(() => {
|
||||
get_top_user_count();
|
||||
refreshChart();
|
||||
});
|
||||
|
||||
|
||||
// 搜索联系人相关 START
|
||||
const search_user = async (query: string) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
if (query === '') {
|
||||
user_options.value = [];
|
||||
return;
|
||||
}
|
||||
const body_data = await apiUserList(query);
|
||||
loading.value = false;
|
||||
user_options.value = Object.values(body_data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const search_change = async () => {
|
||||
try {
|
||||
console.log('search_change:', word.value);
|
||||
// await get_data();
|
||||
await refreshChart();
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const set_top_user = async (wxid: string) => {
|
||||
try {
|
||||
word.value = wxid;
|
||||
await search_change();
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// 搜索联系人相关 END
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-layout" style="background-color: #d2d2fa;height: 100%;width: 100%;">
|
||||
<el-container style="height: 100%;width: 100%;">
|
||||
<el-header :height="'80px'" style="width: 100%;">
|
||||
<strong>时间(默认全部):</strong>
|
||||
<DateTimeSelect @datetime="(val: any) => {datetime = val;}"/>
|
||||
<el-select
|
||||
v-model="word"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
placeholder="输入想查看的联系人"
|
||||
remote-show-suffix
|
||||
clearable
|
||||
:remote-method="search_user"
|
||||
:loading="loading"
|
||||
style="width: 240px"
|
||||
>
|
||||
<el-option v-for="item in user_options" :key="item.wxid" :label="gen_show_name(item)" :value="item.wxid"/>
|
||||
</el-select>
|
||||
<el-button type="primary" @click="search_change">查看</el-button>
|
||||
|
||||
<strong>颜色设置:</strong>
|
||||
bg:
|
||||
<color-select
|
||||
@updateColors="(val:any)=>{val?chart_option.backgroundColor=val:'';refreshChart(false)}"></color-select>
|
||||
|
||||
<template v-for="(color, index) in chart_option.series" :key="index">
|
||||
c{{ index + 1 }}
|
||||
<color-select
|
||||
@updateColors="(val:any)=>{val?chart_option.series[index].itemStyle.color=val:'';refreshChart(false)}"
|
||||
></color-select>
|
||||
</template>
|
||||
<el-button @click="update_chart_option();refreshChart(false);" size="small">重置</el-button>
|
||||
<br>
|
||||
<strong>top10[总:(收/发)]:</strong>
|
||||
<template v-for="wxid in Object.keys(top_user_count)" :key="wxid">
|
||||
<el-button type="primary" plain @click="set_top_user(wxid)" size="small">
|
||||
{{ gen_show_name(top_user[wxid]) }} [{{ top_user_count[wxid]?.total_count }}({{
|
||||
top_user_count[wxid]?.receiver_count
|
||||
}}/{{ top_user_count[wxid]?.sender_count }})]
|
||||
</el-button>
|
||||
</template>
|
||||
</el-header>
|
||||
|
||||
<el-main style="height: calc(100% - 100px);width: 100%;">
|
||||
<chart-init :option="chart_option" :update="is_update" id="charts_main"/>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
46
pywxdump/ui/src/components/stats/components/ChartInit.vue
Normal file
46
pywxdump/ui/src/components/stats/components/ChartInit.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import * as echarts from "echarts";
|
||||
import 'echarts-wordcloud';
|
||||
import {getCurrentInstance, onMounted, ref, shallowRef, watch,} from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
option: any,
|
||||
update: boolean
|
||||
}>();
|
||||
const chart_options = ref<any>(props.option);
|
||||
const Chart = shallowRef<any>(null)
|
||||
const init = () => {
|
||||
let t = getCurrentInstance()?.proxy?.$refs.chart_main;
|
||||
if (t instanceof HTMLElement) {
|
||||
chart_options.value = props.option;
|
||||
Chart.value = echarts.init(t);
|
||||
Chart.value.clear();
|
||||
Chart.value.setOption(chart_options.value, true);
|
||||
} else {
|
||||
console.error('chart_main is not HTMLElement');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
})
|
||||
|
||||
watch(() => props.update, async (newVal, oldVal) => {
|
||||
chart_options.value = props.option;
|
||||
Chart.value.clear();
|
||||
Chart.value.setOption(chart_options.value, true);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chart-div" ref="chart_main"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chart-div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
19
pywxdump/ui/src/components/utils/ColorSelect.vue
Normal file
19
pywxdump/ui/src/components/utils/ColorSelect.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
|
||||
const color = ref('');
|
||||
const updateColors = (val: string) => {
|
||||
emit('updateColors', val);
|
||||
}
|
||||
|
||||
const emit = defineEmits(['updateColors']);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-color-picker v-model="color" @change="updateColors" size="small"></el-color-picker>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
135
pywxdump/ui/src/components/utils/DateTimeSelect.vue
Normal file
135
pywxdump/ui/src/components/utils/DateTimeSelect.vue
Normal file
@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import {defineEmits, ref, watch} from 'vue';
|
||||
|
||||
const defaultTime: [Date, Date] = [
|
||||
new Date(2000, 1, 1, 0, 0, 0),
|
||||
new Date(2000, 2, 1, 23, 59, 59),
|
||||
] // '12:00:00', '08:00:00'
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
text: '全部',
|
||||
value: () => {
|
||||
const end = new Date();
|
||||
const start = new Date(2010, 0, 1, 0, 0, 0);
|
||||
return [start, end]
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近一周',
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
|
||||
return [start, end]
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近一个月',
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
|
||||
return [start, end]
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近三个月',
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
|
||||
return [start, end]
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近半年',
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 180)
|
||||
return [start, end]
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '最近一年',
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 365)
|
||||
return [start, end]
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '去年一整年',
|
||||
value: () => {
|
||||
const start = new Date()
|
||||
start.setFullYear(start.getFullYear() - 1)
|
||||
start.setMonth(0)
|
||||
start.setDate(1)
|
||||
start.setHours(0, 0, 0)
|
||||
const end = new Date()
|
||||
end.setFullYear(end.getFullYear() - 1)
|
||||
end.setMonth(11)
|
||||
end.setDate(31)
|
||||
end.setHours(23, 59, 59)
|
||||
return [start, end]
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const datetime = ref([Date, Date])
|
||||
const emits = defineEmits(['datetime']);
|
||||
|
||||
// 向父组件传递数据
|
||||
// 检测datetime的变化
|
||||
watch(() => datetime.value, (newVal: any, oldVal: any) => {
|
||||
let start;
|
||||
let end;
|
||||
if (newVal) {
|
||||
start = newVal[0].getTime();
|
||||
end = newVal[1].getTime();
|
||||
} else {
|
||||
start = 0;
|
||||
end = 0;
|
||||
}
|
||||
// 向父组件传递数据
|
||||
emits('datetime', [start, end]);
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <div class="block">-->
|
||||
<el-date-picker
|
||||
v-model="datetime"
|
||||
type="datetimerange"
|
||||
:shortcuts="shortcuts"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
:default-time="defaultTime"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
/>
|
||||
<!-- </div>-->
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.block {
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
border-right: solid 1px var(--el-border-color);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.block:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.block .demonstration {
|
||||
display: block;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
383
pywxdump/ui/src/components/utils/DbInitComponent.vue
Normal file
383
pywxdump/ui/src/components/utils/DbInitComponent.vue
Normal file
@ -0,0 +1,383 @@
|
||||
<script setup lang="ts">
|
||||
import http from "@/utils/axios.js";
|
||||
import ProgressBar from "@/components/utils/ProgressBar.vue";
|
||||
import {defineEmits, onMounted, ref, watch} from "vue";
|
||||
import {ElTable, ElTableColumn, ElMessage, ElMessageBox} from "element-plus";
|
||||
import type {Action} from 'element-plus'
|
||||
import router from "@/router";
|
||||
|
||||
interface wxinfo {
|
||||
pid: string;
|
||||
version: string;
|
||||
account: string;
|
||||
mobile: string;
|
||||
nickname: string;
|
||||
mail: string;
|
||||
wxid: string;
|
||||
wx_dir: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
const percentage = ref(0);
|
||||
const startORstop = ref(-1); // 用于进度条的开始和停止 0表示0% 1表示100%
|
||||
|
||||
const init_type = ref("");
|
||||
|
||||
const is_init = ref(false);
|
||||
const wxinfoData = ref<wxinfo[]>([]);
|
||||
|
||||
const oneWx = ref("");
|
||||
const decryping = ref(false);
|
||||
const isErrorShow = ref(false);
|
||||
const isUseKey = ref("false");
|
||||
|
||||
const merge_path = ref("");
|
||||
const wx_path = ref("");
|
||||
const key = ref("");
|
||||
const my_wxid = ref("");
|
||||
|
||||
const local_wxids = ref([]);
|
||||
|
||||
const db_init = (init: boolean) => {
|
||||
if (init) {
|
||||
localStorage.setItem('isDbInit', "t");
|
||||
router.push('/');
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '初始化成功!',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ** 是否使用key的初始化** START
|
||||
const init_key = async () => {
|
||||
if (decryping.value) {
|
||||
console.log("正在解密中,请稍后再试!")
|
||||
return;
|
||||
}
|
||||
decryping.value = true;
|
||||
try {
|
||||
decryping.value = true;
|
||||
startORstop.value = 0; // 进度条开始
|
||||
let reqdata = {
|
||||
"wx_path": wx_path.value,
|
||||
"key": key.value,
|
||||
"my_wxid": my_wxid.value
|
||||
}
|
||||
const body_data = await http.post('/api/ls/init_key', reqdata);
|
||||
is_init.value = body_data.is_init;
|
||||
if (body_data.is_init) {
|
||||
percentage.value = 100; // 进度条 100%
|
||||
}
|
||||
decryping.value = false;
|
||||
db_init(body_data.is_init);
|
||||
} catch (error) {
|
||||
percentage.value = 0; // 进度条 0%
|
||||
isErrorShow.value = true;
|
||||
decryping.value = false;
|
||||
ElMessageBox.alert(error, 'error', {
|
||||
confirmButtonText: '确认',
|
||||
callback: (action: Action) => {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: `action: ${action}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
return [];
|
||||
}
|
||||
decryping.value = false;
|
||||
}
|
||||
|
||||
const init_nokey = async () => {
|
||||
try {
|
||||
let reqdata = {
|
||||
"wx_path": wx_path.value,
|
||||
"merge_path": merge_path.value,
|
||||
"my_wxid": my_wxid.value
|
||||
}
|
||||
const body_data = await http.post('/api/ls/init_nokey', reqdata);
|
||||
is_init.value = body_data.is_init;
|
||||
if (body_data.is_init) {
|
||||
percentage.value = 100; // 进度条 100%
|
||||
}
|
||||
decryping.value = false;
|
||||
db_init(body_data.is_init);
|
||||
// emits('isAutoShow', body_data.is_init);
|
||||
} catch (error) {
|
||||
percentage.value = 0; // 进度条 0%
|
||||
isErrorShow.value = true;
|
||||
decryping.value = false;
|
||||
ElMessageBox.alert(error, 'error', {
|
||||
confirmButtonText: '确认',
|
||||
callback: (action: Action) => {
|
||||
init_type.value = "";// 刷新
|
||||
},
|
||||
})
|
||||
// console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
decryping.value = false;
|
||||
}
|
||||
// ** 是否使用key的初始化** END
|
||||
|
||||
// ** 使用上次数据部分** START
|
||||
const selectLastWx = async (row: wxinfo) => {
|
||||
// console.log(row)
|
||||
my_wxid.value = row.wxid;
|
||||
}
|
||||
|
||||
const get_init_last_local_wxid = async () => {
|
||||
try {
|
||||
const body_data = await http.post('/api/ls/init_last_local_wxid'); //[ 'wx1234567890', 'wx0987654321' ]
|
||||
local_wxids.value = body_data.local_wxids.map((item: string) => {
|
||||
return {wxid: item}
|
||||
});
|
||||
if (local_wxids.value.length === 1) {
|
||||
my_wxid.value = local_wxids.value[0].wxid;
|
||||
// console.log("init_last")
|
||||
await init_last();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const init_last = async () => {
|
||||
try {
|
||||
let reqdata = {
|
||||
"wx_path": wx_path.value,
|
||||
"merge_path": merge_path.value,
|
||||
"my_wxid": my_wxid.value
|
||||
}
|
||||
const body_data = await http.post('/api/ls/init_last', reqdata);
|
||||
is_init.value = body_data.is_init;
|
||||
if (body_data.is_init) {
|
||||
percentage.value = 100; // 进度条 100%
|
||||
decryping.value = false;
|
||||
db_init(body_data.is_init);
|
||||
// emits('isAutoShow', body_data.is_init);
|
||||
} else {
|
||||
isErrorShow.value = true;
|
||||
decryping.value = false;
|
||||
ElMessageBox.alert("未发现上次的设置数据!", 'error', {
|
||||
confirmButtonText: '确认',
|
||||
callback: (action: Action) => {
|
||||
init_type.value = "";// 刷新
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
decryping.value = false;
|
||||
} catch (error) {
|
||||
// percentage.value = 0; // 进度条 0%
|
||||
isErrorShow.value = true;
|
||||
decryping.value = false;
|
||||
ElMessageBox.alert(error, 'error', {
|
||||
confirmButtonText: '确认',
|
||||
callback: (action: Action) => {
|
||||
init_type.value = "";
|
||||
},
|
||||
})
|
||||
// console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
decryping.value = false;
|
||||
}
|
||||
|
||||
// ** 使用上次数据部分** END
|
||||
|
||||
// **自动解密微信部分** START 查看有多少个微信正在登录 , 并调用init_key解密初始化
|
||||
const get_wxinfo = async () => {
|
||||
try {
|
||||
wxinfoData.value = await http.post('/api/ls/wxinfo');
|
||||
if (wxinfoData.value.length === 1) {
|
||||
selectWx(wxinfoData.value[0]);
|
||||
oneWx.value = " (检测到只有一个微信,将在5秒后自动选择) ";
|
||||
setTimeout(okWx, 5000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const selectWx = async (row: wxinfo) => {
|
||||
merge_path.value = "";
|
||||
wx_path.value = row.wx_dir;
|
||||
key.value = row.key;
|
||||
my_wxid.value = row.wxid;
|
||||
}
|
||||
|
||||
const okWx = () => {
|
||||
if (wx_path.value === '' && key.value === '' && my_wxid.value === '') {
|
||||
console.log("请填写完整信息! ")
|
||||
return;
|
||||
}
|
||||
if (decryping.value) {
|
||||
console.log("正在解密...,请稍后再试!")
|
||||
return;
|
||||
}
|
||||
init_key();
|
||||
}
|
||||
|
||||
// **自动解密微信部分** END 查看有多少个微信正在登录 , 并调用init_key解密初始化
|
||||
|
||||
|
||||
// 监测isAutoShow是否为aoto,如果是则执行get_wxinfo
|
||||
watch(init_type, (val) => {
|
||||
if (val === 'auto') {
|
||||
get_wxinfo();
|
||||
} else if (val === 'custom') {
|
||||
// init();
|
||||
} else if (val === 'last') {
|
||||
get_init_last_local_wxid();
|
||||
// init_last();
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="background-color: #d2d2fa; height: 100%; display: flex; justify-content: center; align-items: center;">
|
||||
<!-- 上次数据 -->
|
||||
<div v-if="init_type==='last'">
|
||||
<div
|
||||
style="background-color: #fff; width: 90%;min-width: 800px; height: 80%; border-radius: 10px; padding: 20px; overflow: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="font-size: 20px; font-weight: bold;">选择要查看的微信</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<el-table :data="local_wxids" @current-change="selectLastWx" highlight-current-row style="width: 100%">
|
||||
<el-table-column :min-width="50" prop="wxid" label="微信原始id"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<el-button style="margin-right: 10px;margin-top: 10px;width: 100%;" type="success" @click="init_last">确定
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END -->
|
||||
|
||||
<!-- 自动解密和显示 -->
|
||||
<div v-else-if="init_type==='auto'">
|
||||
|
||||
<!-- <el-progress v-if="decryping && !isErrorShow" type="dashboard" :percentage="percentage" :color="colors"/>-->
|
||||
<div v-if="decryping">
|
||||
<ProgressBar v-if="decryping" :startORstop="startORstop"/>
|
||||
</div>
|
||||
<div v-else
|
||||
style="background-color: #fff; width: 90%;min-width: 800px; height: 80%; border-radius: 10px; padding: 20px; overflow: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="font-size: 20px; font-weight: bold;">选择要查看的微信(会清空work下对应wxid数据)</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<el-table :data="wxinfoData" @current-change="selectWx" highlight-current-row style="width: 100%">
|
||||
<el-table-column :min-width="30" prop="pid" label="进程id"></el-table-column>
|
||||
<el-table-column :min-width="40" prop="version" label="微信版本"></el-table-column>
|
||||
<el-table-column :min-width="40" prop="account" label="账号"></el-table-column>
|
||||
<el-table-column :min-width="40" prop="nickname" label="昵称"></el-table-column>
|
||||
<el-table-column :min-width="50" prop="wxid" label="微信原始id"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<el-button style="margin-right: 10px;margin-top: 10px;width: 100%;" type="success" @click="okWx">确定{{
|
||||
oneWx
|
||||
}}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END -->
|
||||
|
||||
<!-- 用于自定义参数 -->
|
||||
<div v-else-if="init_type==='custom'">
|
||||
<div v-if="decryping">
|
||||
<ProgressBar v-if="decryping" :startORstop="startORstop"/>
|
||||
</div>
|
||||
<div v-else
|
||||
style="background-color: #fff; width: 80%;min-width: 800px; height: 70%; border-radius: 10px; padding: 20px; overflow: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="font-size: 20px; font-weight: bold;">自定义-文件位置</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<!-- <el-button style="margin-right: 10px;" @click="exportData">导出</el-button>-->
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<!-- 单选按钮 -->
|
||||
<input type="radio" v-model="isUseKey" value="true"/> 使用 KEY
|
||||
<input type="radio" v-model="isUseKey" value="false"/> 不使用 KEY
|
||||
<div v-if="isUseKey=='false'">
|
||||
说明:1、表示数据库已解密并合并<br>2、合并后的数据库需要包含(MediaMSG,MSG,MicroMsg,OpenIMMsg)这些数据库合并的内容<br>
|
||||
</div>
|
||||
<div v-if="isUseKey=='true'">
|
||||
说明:1、自动根据key解密微信文件夹下的数据库<br>2、必须保证key正确,否则解密失败<br>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider> <!-- 分割线-->
|
||||
|
||||
<div v-if="isUseKey=='true'">
|
||||
<label>密钥key(必填): </label>
|
||||
<el-input placeholder="密钥key (64位)" v-model="key" style="width: 80%;"></el-input>
|
||||
<br>
|
||||
</div>
|
||||
<div v-if="isUseKey=='false'">
|
||||
<label>merge_all.db 文件路径(必填,非文件夹): </label>
|
||||
<el-input placeholder="(MediaMSG.db,MSG.db,MicroMsg.db,OpenIMMsg.db)合并后的数据库" v-model="merge_path"
|
||||
style="width: 80%;"></el-input>
|
||||
<br>
|
||||
</div>
|
||||
<label>微信文件夹路径(必填): </label>
|
||||
<el-input placeholder="C:\***\WeChat Files\wxid_*******" v-model="wx_path" style="width: 80%;"></el-input>
|
||||
<br>
|
||||
<label>微信原始id(必填): </label>
|
||||
<el-input placeholder="wxid_*******" v-model="my_wxid" style="width: 80%;"></el-input>
|
||||
<br>
|
||||
|
||||
<el-button v-if="isUseKey=='true'" style="margin-top: 10px;width: 100%;" type="success" @click="init_key">
|
||||
确定
|
||||
</el-button>
|
||||
<el-button v-if="isUseKey=='false'" style="margin-top: 10px;width: 100%;" type="success" @click="init_nokey">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END -->
|
||||
|
||||
|
||||
<!-- 初始选择界面 -->
|
||||
<div v-else-if="init_type === ''" style="display: flex; justify-content: space-between;">
|
||||
<label
|
||||
style="width: 200px; height: 150px; background-color: #fff; display: flex; flex-direction: column; align-items: center; border-radius: 10px; margin-right: 20px;">
|
||||
<input type="radio" v-model="init_type" value="last"/>
|
||||
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%;">
|
||||
<div>使用历史数据</div>
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
style="width: 200px; height: 150px; background-color: #fff; display: flex; flex-direction: column; align-items: center; border-radius: 10px; margin-right: 20px;">
|
||||
<input type="radio" v-model="init_type" value="auto"/>
|
||||
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%;">
|
||||
<div>自动解密已登录微信</div>
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
style="width: 200px; height: 150px; background-color: #fff; display: flex; flex-direction: column; align-items: center; border-radius: 10px;">
|
||||
<input type="radio" v-model="init_type" value="custom"/>
|
||||
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%;">
|
||||
<div>自定义文件位置</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<!-- END -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
25
pywxdump/ui/src/components/utils/NumberInput.vue
Normal file
25
pywxdump/ui/src/components/utils/NumberInput.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
n: Number,
|
||||
step: Number
|
||||
}>();
|
||||
|
||||
const value = ref(props.n);
|
||||
|
||||
const updateNumber = (val: bigint) => {
|
||||
emit('updateNumber', val);
|
||||
}
|
||||
const emit = defineEmits(['updateNumber']);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-input-number v-model="value" @change="updateNumber" size="small" :step="step"
|
||||
style="width: 100px;"></el-input-number>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
56
pywxdump/ui/src/components/utils/ProgressBar.vue
Normal file
56
pywxdump/ui/src/components/utils/ProgressBar.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import {defineEmits, defineProps, ref} from 'vue';
|
||||
|
||||
// 进度条
|
||||
const percentage = ref(0);
|
||||
|
||||
const timeout = ref(500);
|
||||
const colors = [
|
||||
{color: '#f56c6c', percentage: 20},
|
||||
{color: '#e6a23c', percentage: 40},
|
||||
{color: '#5cb87a', percentage: 60},
|
||||
{color: '#1989fa', percentage: 80},
|
||||
{color: '#6f7ad3', percentage: 100},
|
||||
]
|
||||
const last_time = ref(new Date().getTime());
|
||||
|
||||
// END 进度条
|
||||
const props = defineProps({
|
||||
startORstop: {
|
||||
type: Number,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
const updateProgress = () => {
|
||||
if (props.startORstop === 1) {
|
||||
percentage.value = 100;
|
||||
return;
|
||||
}
|
||||
|
||||
last_time.value = new Date().getTime();
|
||||
if (percentage.value >= 99) {
|
||||
return;
|
||||
}
|
||||
if (percentage.value >= 60) {
|
||||
timeout.value = timeout.value + 50;
|
||||
}
|
||||
percentage.value = percentage.value + 1;
|
||||
// 调用自身并计算下一个延迟时长
|
||||
setTimeout(updateProgress, timeout.value);
|
||||
};
|
||||
|
||||
// 监听开始和停止
|
||||
updateProgress();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="progress-bar">
|
||||
<el-progress type="dashboard" :percentage="percentage" :color="colors"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
28
pywxdump/ui/src/main.ts
Normal file
28
pywxdump/ui/src/main.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import {createApp} from 'vue'
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import Markdown from 'vue3-markdown-it';
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
|
||||
// import Appexport from "@/Appexport.vue";
|
||||
// const app = createApp(Appexport)
|
||||
|
||||
app.use(ElementPlus)
|
||||
app.use(Markdown)
|
||||
|
||||
app.provide('msg_path', '');
|
||||
app.provide('micro_path', '');
|
||||
app.provide('media_path', '');
|
||||
app.provide('filestorage_path', '');
|
||||
app.provide('user_list', []);
|
||||
|
||||
app.mount('#app')
|
97
pywxdump/ui/src/router/index.ts
Normal file
97
pywxdump/ui/src/router/index.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import {createRouter, createWebHashHistory} from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'index',
|
||||
component: () => import((`@/views/IndexView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/db_init',
|
||||
name: 'db_init',
|
||||
component: () => import((`@/views/DbInitView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
component: () => import((`@/views/HomeView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
component: () => import((`@/views/ChatView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/contacts',
|
||||
name: 'contacts',
|
||||
component: () => import((`@/views/ContactsView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/moments',
|
||||
name: 'moments',
|
||||
component: () => import((`@/views/MomentsView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/favorite',
|
||||
name: 'favorite',
|
||||
component: () => import((`@/views/FavoriteView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/cleanup',
|
||||
name: 'cleanup',
|
||||
component: () => import((`@/views/CleanupView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/statistics',
|
||||
name: 'statistics',
|
||||
component: () => import((`@/views/StatisticsView.vue`))
|
||||
},
|
||||
|
||||
// 专业工具
|
||||
{
|
||||
path: '/wxinfo',
|
||||
name: 'wxinfo',
|
||||
component: () => import((`@/views/tools/WxinfoView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/bias',
|
||||
name: 'bias',
|
||||
component: () => import((`@/views/tools/BiasView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/merge',
|
||||
name: 'merge',
|
||||
component: () => import((`@/views/tools/MergeView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/decrypt',
|
||||
name: 'decrypt',
|
||||
component: () => import((`@/views/tools/DecryptView.vue`))
|
||||
},
|
||||
|
||||
// 其他 关于、帮助、设置
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import((`@/views/other/AboutView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/help',
|
||||
name: 'help',
|
||||
component: () => import((`@/views/other/HelpView.vue`))
|
||||
},
|
||||
{
|
||||
path: '/setting',
|
||||
name: 'setting',
|
||||
component: () => import((`@/views/other/SettingView.vue`))
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
export default router
|
76
pywxdump/ui/src/utils/axios.js
Normal file
76
pywxdump/ui/src/utils/axios.js
Normal file
@ -0,0 +1,76 @@
|
||||
// 创建一个 axios 实例
|
||||
import axios from 'axios'
|
||||
import { to_initview } from '@/utils/common_utils'
|
||||
// import {inject, onMounted} from 'vue';
|
||||
|
||||
const params = process.env.NODE_ENV === 'development' ? {
|
||||
baseURL: 'http://127.0.0.1:5000', // 根据你的实际情况设置基础URL
|
||||
withCredentials: true, // 表示跨域请求时是否需要使用凭证,开启后,后端服务器要设置允许开启
|
||||
} : {
|
||||
withCredentials: true, // 表示跨域请求时是否需要使用凭证,开启后,后端服务器要设置允许开启
|
||||
}
|
||||
|
||||
const http = axios.create(params)
|
||||
|
||||
// 请求拦截器
|
||||
http.interceptors.request.use(
|
||||
(config) => {
|
||||
// 在发送请求之前做些什么,比如添加请求头
|
||||
config.headers['Content-Type'] = 'application/json' // 根据您的需求设置其他请求头
|
||||
// config.headers['msg_path'] = inject("msg_path");
|
||||
// config.headers['micro_path'] = inject("micro_path");
|
||||
// config.headers['media_path'] = inject("media_path");
|
||||
// config.headers['filestorage_path'] = inject("filestorage_path");
|
||||
// 补全路径
|
||||
// console.log('config.url', config.url);
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
// 对请求错误做些什么
|
||||
console.log('Error Message:', error.message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
http.interceptors.response.use(
|
||||
(response) => {
|
||||
// 对响应数据做点什么
|
||||
if (response.data.code === 0) {
|
||||
// 如果后端返回的状态码是0 ,说明接口请求成功
|
||||
// 这里直接返回后端返回的数据
|
||||
return response.data.body
|
||||
} else if (response.data.code === 1001 && 'my_wxid is required' in response.data.body) {
|
||||
// 如果后端返回的状态码是1001,说明用户未登录
|
||||
// 这里直接返回后端返回的数据
|
||||
// 跳转到登录页面
|
||||
to_initview();
|
||||
return Promise.reject(response.data)
|
||||
} else {
|
||||
// 如果不是 200,说明接口请求失败,弹出后端给的错误提示
|
||||
console.error('Error Message:', response.data)
|
||||
return Promise.reject(response.data)
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// 对响应错误做点什么
|
||||
if (error.response) {
|
||||
// 请求已发出,但服务器响应的状态码不在 2xx 范围内
|
||||
console.error('HTTP Error Response:', error.response.status)
|
||||
} else if (error.request) {
|
||||
// 请求已发出,但没有收到响应
|
||||
console.error('No response received:', error.request)
|
||||
} else {
|
||||
// 发送请求时发生了一些事情,触发了错误
|
||||
console.error('Error sending request:', error.message)
|
||||
}
|
||||
|
||||
// 把url+参数+错误传递给调用者
|
||||
return Promise.reject({
|
||||
message: error.message,
|
||||
url: error.config.url,
|
||||
params: error.config.params
|
||||
})
|
||||
}
|
||||
)
|
||||
export default http
|
82
pywxdump/ui/src/utils/common_utils.ts
Normal file
82
pywxdump/ui/src/utils/common_utils.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import {ElMessage} from "element-plus";
|
||||
import router from "@/router";
|
||||
import http from '@/utils/axios.js';
|
||||
import {api_db_init} from "@/api/base";
|
||||
|
||||
export interface ExtraBuf {
|
||||
"个性签名": string
|
||||
"企微属性": string
|
||||
"公司名称": string
|
||||
"国": string
|
||||
"备注图片": string
|
||||
"备注图片2": string
|
||||
"市": string
|
||||
"性别[1男2女]": number
|
||||
"手机号": string
|
||||
"朋友圈背景": string
|
||||
"省": string
|
||||
}
|
||||
|
||||
export interface User {
|
||||
wxid: string
|
||||
nOrder: number
|
||||
nUnReadCount: number
|
||||
strNickName: string
|
||||
nStatus: number
|
||||
nIsSend: number
|
||||
strContent: string
|
||||
nMsgLocalID: number
|
||||
nMsgStatus: number
|
||||
nTime: string
|
||||
nMsgType: number
|
||||
nMsgSubType: number
|
||||
nickname: string
|
||||
remark: string
|
||||
account: string
|
||||
describe: string
|
||||
headImgUrl: string
|
||||
ExtraBuf: ExtraBuf
|
||||
LabelIDList: string[],
|
||||
extra: object | null
|
||||
}
|
||||
|
||||
export interface UserList {
|
||||
[key: string]: User
|
||||
}
|
||||
|
||||
export interface msg {
|
||||
id: number
|
||||
MsgSvrID: string
|
||||
type_name: string
|
||||
is_sender: number
|
||||
talker: string
|
||||
room_name: string
|
||||
msg: string
|
||||
src: string
|
||||
CreateTime: string
|
||||
extra: {}
|
||||
}
|
||||
|
||||
// {"id": _id, "MsgSvrID": str(MsgSvrID), "type_name": type_name, "is_sender": IsSender,
|
||||
// "talker": talker, "room_name": StrTalker, "msg": msg, "src": src, "extra": {},
|
||||
// "CreateTime": CreateTime, }
|
||||
|
||||
export const to_initview = () => {
|
||||
router.push({name: 'db_init'});
|
||||
ElMessage.error('请先初始化数据');
|
||||
}
|
||||
|
||||
export const is_db_init = async () => {
|
||||
const t = await api_db_init();
|
||||
localStorage.setItem('isDbInit', t ? 't' : 'f');
|
||||
!t ? to_initview() : null;
|
||||
return t;
|
||||
}
|
||||
|
||||
export const is_use_local_data = () => {
|
||||
return localStorage.getItem('isUseLocalData') === 't';
|
||||
}
|
||||
|
||||
export const gen_show_name = (userinfo: User) => {
|
||||
return userinfo?.remark || userinfo?.nickname || userinfo?.strNickName || userinfo?.account || userinfo?.wxid || '未知';
|
||||
}
|
47
pywxdump/ui/src/views/ChatView.vue
Normal file
47
pywxdump/ui/src/views/ChatView.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import ContactsList from '@/components/chat/ContactsList.vue';
|
||||
import ChatRecords from '@/components/chat/ChatRecords.vue';
|
||||
import {onMounted, ref} from "vue";
|
||||
import IndexView from "@/views/IndexView.vue";
|
||||
import {apiVersion} from "@/api/base";
|
||||
import {is_db_init} from "@/utils/common_utils";
|
||||
|
||||
const wxid = ref('');
|
||||
|
||||
onMounted(() => {
|
||||
apiVersion().then((data: string) => {
|
||||
console.log("API version: " + data);
|
||||
}).catch((error: string) => {
|
||||
console.error('Error fetching API version:', error);
|
||||
});
|
||||
is_db_init();
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div id="chat_view" class="common-layout">
|
||||
<div>
|
||||
<el-container>
|
||||
<!-- 这是左边的list -->
|
||||
<el-aside width="auto" style="overflow-y: auto; height: calc(100vh);">
|
||||
<ContactsList @wxid="(val: any) => { wxid = val;}"/>
|
||||
</el-aside>
|
||||
<!-- END 这是左边的list -->
|
||||
|
||||
<!--这是右边的具体聊天记录-->
|
||||
<div v-if="wxid != ''" style="height: calc(100vh);width: 100%;">
|
||||
<ChatRecords :wxid="wxid"/>
|
||||
</div>
|
||||
|
||||
<div v-else style="width: 100%;height: 100%">
|
||||
<IndexView/>
|
||||
</div>
|
||||
<!-- END 这是右边的具体聊天记录 -->
|
||||
</el-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
27
pywxdump/ui/src/views/CleanupView.vue
Normal file
27
pywxdump/ui/src/views/CleanupView.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
import HomeView from "@/views/HomeView.vue";
|
||||
import ContactsList from "@/components/chat/ContactsList.vue";
|
||||
import ChatExportMain from "@/components/chatBackup/ChatExportMain.vue";
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="background-color: #d2d2fa; height: 100vh; display: grid; place-items: center; ">
|
||||
<h2 style="text-align: center">欢迎使用<a href="https://github.com/xaoyaoo/PyWxDump.git">PyWxDump</a>聊天记录查看工具!
|
||||
</h2>
|
||||
<h3 style="text-align: center">
|
||||
微信存储空间清理,减少微信占用空间<br>通过选择某个人或群,把这群里的聊天记录中涉及的图片、视频、文件、语音等的媒体文件找出来<br>
|
||||
以群对话为单位有选择性的(比如时间段)或按群会话批量从电脑的缓存中清除。
|
||||
</h3>
|
||||
<h3 style="text-align: center">
|
||||
打开电脑微信,点击左下角的菜单,选择设置->通用设置->存储空间管理->清理空间,即可查看微信占用的空间,点击清理即可清理微信占用的空间。<br>
|
||||
如果这些自带功能无法满足需要,请提交issue,我会增加点新的功能。
|
||||
</h3>
|
||||
<p>如需提前体验更多功能,请多多支持,多多鼓励!</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
29
pywxdump/ui/src/views/ContactsView.vue
Normal file
29
pywxdump/ui/src/views/ContactsView.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import http from '@/utils/axios.js';
|
||||
import {onMounted, ref} from "vue";
|
||||
import {apiVersion} from "@/api/base";
|
||||
|
||||
const version = ref(''); // 用于显示返回值
|
||||
|
||||
const getVersion = () => {
|
||||
apiVersion().then((data: string) => {
|
||||
version.value = data;
|
||||
}).catch((error: string) => {
|
||||
console.error('Error fetching API version:', error);
|
||||
});
|
||||
}
|
||||
onMounted(getVersion);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="background-color: #d2d2fa; height: 100vh; display: grid; place-items: center; ">
|
||||
<h2 style="text-align: center">欢迎使用<a href="https://github.com/xaoyaoo/PyWxDump.git">PyWxDump</a>聊天记录查看工具!
|
||||
<p style="text-align: center">当前版本:{{ version }}</p>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
11
pywxdump/ui/src/views/DbInitView.vue
Normal file
11
pywxdump/ui/src/views/DbInitView.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import DbInitComponent from "@/components/utils/DbInitComponent.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<db-init-component></db-init-component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
30
pywxdump/ui/src/views/FavoriteView.vue
Normal file
30
pywxdump/ui/src/views/FavoriteView.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import http from '@/utils/axios.js';
|
||||
import {onMounted, ref} from "vue";
|
||||
import {apiVersion} from "@/api/base";
|
||||
|
||||
const version = ref(''); // 用于显示返回值
|
||||
|
||||
const getVersion = () => {
|
||||
apiVersion().then((data: string) => {
|
||||
version.value = data;
|
||||
}).catch((error: string) => {
|
||||
console.error('Error fetching API version:', error);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(getVersion);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="background-color: #d2d2fa; height: 100vh; display: grid; place-items: center; ">
|
||||
<h2 style="text-align: center">欢迎使用<a href="https://github.com/xaoyaoo/PyWxDump.git">PyWxDump</a>聊天记录查看工具!
|
||||
<p style="text-align: center">当前版本:{{ version }}</p>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
30
pywxdump/ui/src/views/HomeView.vue
Normal file
30
pywxdump/ui/src/views/HomeView.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import http from '@/utils/axios.js';
|
||||
import {onMounted, ref} from "vue";
|
||||
import {apiVersion} from "@/api/base";
|
||||
|
||||
const version = ref(''); // 用于显示返回值
|
||||
|
||||
const getVersion = () => {
|
||||
apiVersion().then((data: string) => {
|
||||
version.value = data;
|
||||
}).catch((error: string) => {
|
||||
console.error('Error fetching API version:', error);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(getVersion);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="background-color: #d2d2fa; height: 100vh; display: grid; place-items: center; ">
|
||||
<h2 style="text-align: center">欢迎使用<a href="https://github.com/xaoyaoo/PyWxDump.git">PyWxDump</a>聊天记录查看工具!
|
||||
<p style="text-align: center">当前版本:{{ version }}</p>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
30
pywxdump/ui/src/views/IndexView.vue
Normal file
30
pywxdump/ui/src/views/IndexView.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import http from '@/utils/axios.js';
|
||||
import {onMounted, ref} from "vue";
|
||||
import {apiVersion} from "@/api/base";
|
||||
|
||||
const version = ref(''); // 用于显示返回值
|
||||
|
||||
const getVersion = () => {
|
||||
apiVersion().then((data: string) => {
|
||||
version.value = data;
|
||||
}).catch((error: string) => {
|
||||
console.error('Error fetching API version:', error);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(getVersion);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="background-color: #d2d2fa; height: 100vh; display: grid; place-items: center; ">
|
||||
<h2 style="text-align: center">欢迎使用<a href="https://github.com/xaoyaoo/PyWxDump.git">PyWxDump</a>聊天记录查看工具!
|
||||
<p style="text-align: center">当前版本:{{ version }}</p>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
30
pywxdump/ui/src/views/MomentsView.vue
Normal file
30
pywxdump/ui/src/views/MomentsView.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import http from '@/utils/axios.js';
|
||||
import {onMounted, ref} from "vue";
|
||||
import {apiVersion} from "@/api/base";
|
||||
|
||||
const version = ref(''); // 用于显示返回值
|
||||
|
||||
const getVersion = () => {
|
||||
apiVersion().then((data: string) => {
|
||||
version.value = data;
|
||||
}).catch((error: string) => {
|
||||
console.error('Error fetching API version:', error);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(getVersion);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="background-color: #d2d2fa; height: 100vh; display: grid; place-items: center; ">
|
||||
<h2 style="text-align: center">欢迎使用<a href="https://github.com/xaoyaoo/PyWxDump.git">PyWxDump</a>聊天记录查看工具!
|
||||
<p style="text-align: center">当前版本:{{ version }}</p>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
48
pywxdump/ui/src/views/StatisticsView.vue
Normal file
48
pywxdump/ui/src/views/StatisticsView.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import DateChatStats from "@/components/stats/DateChatStats.vue";
|
||||
import {ref} from "vue";
|
||||
import ContactStats from "@/components/stats/ContactStats.vue";
|
||||
import DateChatHeatmapStats from "@/components/stats/DateChatHeatmapStats.vue";
|
||||
|
||||
const mene_selected = ref("date_chat_heatmap");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <h2 style="text-align: center">欢迎使用<a href="https://github.com/xaoyaoo/PyWxDump.git">PyWxDump</a>聊天记录查看工具!-->
|
||||
<!-- </h2>-->
|
||||
<!-- <h3 style="text-align: center">-->
|
||||
<!-- 用于统计微信聊天记录,包括聊天记录数量、聊天记录总字数、聊天记录总图片数量<br>聊天记录总视频数量、聊天记录总文件数量、聊天记录总语音数量等等。-->
|
||||
<!-- 行成统计报表,方便用户查看自己的聊天记录情况-->
|
||||
<!-- </h3>-->
|
||||
<div class="common-layout" style="height: 100vh;width: 100%;background-color: #d2d2fa;">
|
||||
<el-container style="height: calc(100vh);width: 100%;">
|
||||
<el-aside width="120px" style="height: 100%;">
|
||||
<el-menu style="height: 100%;background-color: #F7F7F7;color:#262626;"
|
||||
:default-active="mene_selected"
|
||||
class="el-menu-vertical-demo"
|
||||
@select="(val: string)=>{mene_selected = val}"
|
||||
>
|
||||
<el-menu-item index="date_chat_heatmap">
|
||||
<span>聊天热力图</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="date_chat_count">
|
||||
<span>日聊天数据</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="contact_stats">
|
||||
<span>联系人画像</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<el-main style="height: 100%;width: 100%;margin: 0;padding: 0;">
|
||||
<date-chat-stats v-if="mene_selected=='date_chat_count'"/>
|
||||
<date-chat-heatmap-stats v-if="mene_selected=='date_chat_heatmap'"/>
|
||||
<contact-stats v-if="mene_selected=='contact_stats'"/>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
67
pywxdump/ui/src/views/other/AboutView.vue
Normal file
67
pywxdump/ui/src/views/other/AboutView.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1 id="-center-pywxdump-center-" style="text-align: center">
|
||||
PyWxDump<a @click="check_update" target="_blank" style="float: right; margin-right: 30px;">检查更新</a>
|
||||
</h1>
|
||||
<!-- 在右上角添加按钮, “检查更新” -->
|
||||
|
||||
<Markdown :source="source" style="background-color: #d2d2fa;"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Markdown from 'vue3-markdown-it';
|
||||
import http from '@/utils/axios.js';
|
||||
import {type Action, ElMessage, ElMessageBox} from "element-plus";
|
||||
import {onMounted, ref} from "vue";
|
||||
|
||||
const check_update = async () => {
|
||||
try {
|
||||
|
||||
const body_data = await http.post('/api/rs/check_update');
|
||||
const latest_version = body_data.latest_version;
|
||||
const msg = body_data.msg;
|
||||
const url = body_data.latest_url;
|
||||
const showtext = `${msg}:${latest_version} \n ${url || ''}`;
|
||||
|
||||
ElMessageBox.alert(showtext, 'info', {
|
||||
confirmButtonText: '确认',
|
||||
callback: (action: Action) => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: `action: ${action}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
// console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const source = ref("# 加载中")
|
||||
|
||||
const get_readme_md = async () => {
|
||||
try {
|
||||
|
||||
const body_data = await http.post('/api/rs/get_readme');
|
||||
source.value = body_data;
|
||||
} catch (error) {
|
||||
// console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
get_readme_md()
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.about {
|
||||
background-color: #d2d2fa;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
33
pywxdump/ui/src/views/other/HelpView.vue
Normal file
33
pywxdump/ui/src/views/other/HelpView.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1 id="-center-pywxdump-center-" style="text-align: center">
|
||||
PyWxDump UserGuide & FAQ
|
||||
</h1>
|
||||
<br><br><br><br><br><br>
|
||||
<!-- <Markdown :source="source" />-->
|
||||
<h2 style="text-align: center">
|
||||
<a href="https://github.com/xaoyaoo/PyWxDump/blob/master/doc/UserGuide.md" target="_blank">使用教程</a>
|
||||
<br>
|
||||
<a href="https://github.com/xaoyaoo/PyWxDump/blob/master/doc/FAQ.md" target="_blank">常见问题</a>
|
||||
<br>
|
||||
<a href="https://github.com/xaoyaoo/PyWxDump/blob/master/doc/CE%E8%8E%B7%E5%8F%96%E5%9F%BA%E5%9D%80.md"
|
||||
target="_blank">CE获取基址</a>
|
||||
<br>
|
||||
<a href="https://github.com/xaoyaoo/PyWxDump/blob/master/doc/wx%E6%95%B0%E6%8D%AE%E5%BA%93%E7%AE%80%E8%BF%B0.md"
|
||||
target="_blank">wx数据库简述</a>
|
||||
<br>
|
||||
<a href="https://github.com/xaoyaoo/PyWxDump/blob/master/doc/MAC%E6%95%B0%E6%8D%AE%E5%BA%93%E8%A7%A3%E5%AF%86.md"
|
||||
target="_blank">MAC数据库解密</a>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Markdown from 'vue3-markdown-it';
|
||||
|
||||
const source = "";
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
53
pywxdump/ui/src/views/other/SettingView.vue
Normal file
53
pywxdump/ui/src/views/other/SettingView.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import DbInitView from "@/views/DbInitView.vue";
|
||||
import {ref} from "vue";
|
||||
import DbInitComponent from "@/components/utils/DbInitComponent.vue";
|
||||
|
||||
const setting_selected = ref("")
|
||||
|
||||
const MeneSelect = (val: any) => {
|
||||
setting_selected.value = val
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-layout" style="height: 100vh;width: 100%;background-color: #F7F7F7;">
|
||||
<el-container>
|
||||
<el-header style="height: 35px; max-height: 35px; width: 100%;">
|
||||
<h2 style="text-align: center">欢迎使用<a
|
||||
href="https://github.com/xaoyaoo/PyWxDump.git">PyWxDump</a>聊天记录查看工具!
|
||||
<span style="font-size: 14px">(如需提前体验更多功能请开通超级vip)</span>
|
||||
</h2>
|
||||
</el-header>
|
||||
<el-container style="height: calc(100vh - 35px);width: 100%;">
|
||||
|
||||
<el-aside width="200px" style="height: 100%;">
|
||||
<el-menu style="height: 100%;background-color: #F7F7F7;color:#262626;"
|
||||
default-active="2"
|
||||
class="el-menu-vertical-demo"
|
||||
@select="MeneSelect"
|
||||
>
|
||||
<el-menu-item index="-1" disabled>
|
||||
<span style="color: #043bea">设置</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="db_init">
|
||||
<span>初始化设置</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="3">
|
||||
<span></span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<el-main style="height: 100%;max-height: 100%;width: 100%;margin: 0;padding: 0;">
|
||||
<db-init-component v-if="setting_selected=='db_init'"></db-init-component>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
75
pywxdump/ui/src/views/tools/BiasView.vue
Normal file
75
pywxdump/ui/src/views/tools/BiasView.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
import http from '@/utils/axios.js';
|
||||
|
||||
const mobile = ref<string>('');
|
||||
const name = ref<string>('');
|
||||
const account = ref<string>('');
|
||||
const key = ref<string>('');
|
||||
const wxdbPath = ref<string>('');
|
||||
|
||||
const result = ref<string>(''); // 结果
|
||||
|
||||
const decrypt = async () => {
|
||||
try {
|
||||
// key与wxdbPath二选一
|
||||
if (key.value === '' && wxdbPath.value === '') {
|
||||
result.value = 'key与wxdbPath必须填写一个';
|
||||
return;
|
||||
}
|
||||
result.value = await http.post('/api/ls/biasaddr', {
|
||||
mobile: mobile.value,
|
||||
name: name.value,
|
||||
account: account.value,
|
||||
key: key.value,
|
||||
wxdbPath: wxdbPath.value
|
||||
});
|
||||
result.value = "{版本号:昵称,账号,手机号,邮箱,KEY}\n"+result.value;
|
||||
} catch (error) {
|
||||
result.value = 'Error fetching data: \n' + error;
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="background-color: #d2d2fa; height: 100vh; display: grid; place-items: center; ">
|
||||
<div style="background-color: #fff; width: 70%; height: 70%; border-radius: 10px; padding: 20px; overflow: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="font-size: 20px; font-weight: bold;">基址偏移</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<!-- <el-button style="margin-right: 10px;" @click="exportData">导出</el-button>-->
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<label>手机号: </label>
|
||||
<el-input placeholder="请输入手机号" v-model="mobile" style="width: 80%;"></el-input>
|
||||
<br>
|
||||
<label>昵称: </label>
|
||||
<el-input placeholder="请输入昵称" v-model="name" style="width: 80%;"></el-input>
|
||||
<br>
|
||||
<label>微信账号: </label>
|
||||
<el-input placeholder="请输入微信号" v-model="account" style="width: 80%;"></el-input>
|
||||
<br>
|
||||
<label>密钥(key): </label>
|
||||
<el-input placeholder="请输入密钥(key)(可选)" v-model="key" style="width: 80%;"></el-input>
|
||||
<br>
|
||||
<label>微信数据库路径: </label>
|
||||
<el-input placeholder="请输入微信数据库路径(可选)" v-model="wxdbPath" style="width: 75%;"></el-input>
|
||||
<br>
|
||||
<el-button style="margin-top: 10px;width: 50%;" type="success" @click="decrypt">偏移</el-button>
|
||||
<!-- 分割线 -->
|
||||
<el-divider></el-divider>
|
||||
<!-- 分割线 -->
|
||||
<el-input type="textarea" :rows="10" readonly placeholder="输出结果" v-model="result"
|
||||
style="width: 100%;color: #00bd7e;"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
60
pywxdump/ui/src/views/tools/DecryptView.vue
Normal file
60
pywxdump/ui/src/views/tools/DecryptView.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
import http from '@/utils/axios.js';
|
||||
|
||||
const wxdbPath = ref<string>('');
|
||||
const key = ref<string>('');
|
||||
const outPath = ref<string>('');
|
||||
const decryptResult = ref<string>('');
|
||||
|
||||
const decrypt = async () => {
|
||||
try {
|
||||
decryptResult.value = await http.post('/api/ls/decrypt', {
|
||||
wxdbPath: wxdbPath.value,
|
||||
key: key.value,
|
||||
outPath: outPath.value
|
||||
});
|
||||
} catch (error) {
|
||||
decryptResult.value = 'Error fetching data: \n' + error;
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="background-color: #d2d2fa; height: 100vh; display: grid; place-items: center; ">
|
||||
<div style="background-color: #fff; width: 70%; height: 70%; border-radius: 10px; padding: 20px; overflow: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="font-size: 20px; font-weight: bold;">解密-微信数据库</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<!-- <el-button style="margin-right: 10px;" @click="exportData">导出</el-button>-->
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<label>密钥(key): </label>
|
||||
<el-input placeholder="请输入密钥(key)" v-model="key" style="width: 82%;"></el-input>
|
||||
<br>
|
||||
<label>微信数据库路径: </label>
|
||||
<el-input placeholder="请输入微信数据库路径" v-model="wxdbPath" style="width: 80%;"></el-input>
|
||||
<br>
|
||||
<label>解密后输出文件夹路径: </label>
|
||||
<el-input placeholder="请输入解密后输出文件夹路径" v-model="outPath" style="width: 75%;"></el-input>
|
||||
<br>
|
||||
|
||||
<el-button style="margin-top: 10px;width: 50%;" type="success" @click="decrypt">解密</el-button>
|
||||
<!-- 分割线 -->
|
||||
<el-divider></el-divider>
|
||||
<!-- 分割线 -->
|
||||
<el-input type="textarea" :rows="10" readonly placeholder="解密后数据库路径" v-model="decryptResult"
|
||||
style="width: 100%;"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
53
pywxdump/ui/src/views/tools/MergeView.vue
Normal file
53
pywxdump/ui/src/views/tools/MergeView.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
import http from '@/utils/axios.js';
|
||||
|
||||
const dbPath = ref<string>('');
|
||||
const outPath = ref<string>('');
|
||||
const Result = ref<string>('');
|
||||
|
||||
const decrypt = async () => {
|
||||
try {
|
||||
Result.value = await http.post('/api/ls/merge', {
|
||||
dbPath: dbPath.value,
|
||||
outPath: outPath.value
|
||||
});
|
||||
} catch (error) {
|
||||
Result.value = 'Error fetching data: \n' + error;
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="background-color: #d2d2fa; height: 100vh; display: grid; place-items: center; ">
|
||||
<div style="background-color: #fff; width: 70%; height: 70%; border-radius: 10px; padding: 20px; overflow: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="font-size: 20px; font-weight: bold;">合并-微信数据库</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<!-- <el-button style="margin-right: 10px;" @click="exportData">导出</el-button>-->
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<label>数据库路径: </label>
|
||||
<el-input placeholder="数据库路径(文件夹,并且确保文件夹下的db文件已经解密):" v-model="dbPath" style="width: 80%;"></el-input>
|
||||
<br>
|
||||
<label>微信数据库路径: </label>
|
||||
<el-input placeholder="输出合并后的数据库路径" v-model="outPath" style="width: 80%;"></el-input>
|
||||
<br>
|
||||
<el-button style="margin-top: 10px;width: 50%;" type="success" @click="decrypt">合并</el-button>
|
||||
<!-- 分割线 -->
|
||||
<el-divider></el-divider>
|
||||
<!-- 分割线 -->
|
||||
<el-input type="textarea" :rows="10" readonly placeholder="合并后数据库路径" v-model="Result"
|
||||
style="width: 100%;"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
95
pywxdump/ui/src/views/tools/WxinfoView.vue
Normal file
95
pywxdump/ui/src/views/tools/WxinfoView.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import {defineComponent, reactive, ref, onMounted, toRefs} from 'vue'
|
||||
import {ElNotification, ElTable, ElTableColumn} from 'element-plus'
|
||||
import http from '@/utils/axios.js';
|
||||
|
||||
interface wxinfo {
|
||||
pid: string;
|
||||
version: string;
|
||||
account: string;
|
||||
mobile: string;
|
||||
name: string;
|
||||
mail: string;
|
||||
wxid: string;
|
||||
filePath: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
const wxinfoData = ref<wxinfo[]>([]);
|
||||
|
||||
const get_wxinfo = async () => {
|
||||
try {
|
||||
wxinfoData.value = await http.post('/api/ls/wxinfo');
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
onMounted(get_wxinfo); // 初始化时获取数据
|
||||
|
||||
const refreshData = async () => {
|
||||
await get_wxinfo();
|
||||
};
|
||||
|
||||
const exportData = () => {
|
||||
const csvContent = convertToCSV(wxinfoData.value);
|
||||
downloadCSV(csvContent, 'wxinfo_data.csv');
|
||||
};
|
||||
|
||||
const convertToCSV = (data: wxinfo[]) => {
|
||||
const header = Object.keys(data[0]).join(',');
|
||||
const rows = data.map((item) => Object.values(item).join(','));
|
||||
return `${header}\n${rows.join('\n')}`;
|
||||
};
|
||||
|
||||
const downloadCSV = (csvContent: string, fileName: string) => {
|
||||
const blob = new Blob([new Uint8Array([0xEF, 0xBB, 0xBF]), csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
|
||||
if (navigator.msSaveBlob) {
|
||||
navigator.msSaveBlob(blob, fileName);
|
||||
} else {
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.setAttribute('download', fileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
ElNotification.success({
|
||||
title: 'Success',
|
||||
message: 'CSV file exported successfully!',
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="background-color: #d2d2fa; height: 100vh; display: grid; place-items: center; ">
|
||||
<div style="background-color: #fff; width: 90%; height: 80%; border-radius: 10px; padding: 20px; overflow: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="font-size: 20px; font-weight: bold;">微信信息(已经登录)</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<el-button style="margin-right: 10px;" @click="refreshData">刷新</el-button>
|
||||
<el-button style="margin-right: 10px;" @click="exportData">导出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<el-table :data="wxinfoData" style="width: 100%">
|
||||
<el-table-column :min-width="30" prop="pid" label="进程id"></el-table-column>
|
||||
<el-table-column :min-width="40" prop="version" label="微信版本"></el-table-column>
|
||||
<el-table-column :min-width="40" prop="account" label="账号"></el-table-column>
|
||||
<el-table-column :min-width="45" prop="mobile" label="手机号"></el-table-column>
|
||||
<el-table-column :min-width="40" prop="nickname" label="昵称"></el-table-column>
|
||||
<el-table-column :min-width="30" prop="mail" label="邮箱"></el-table-column>
|
||||
<el-table-column :min-width="50" prop="wxid" label="微信原始id"></el-table-column>
|
||||
<el-table-column prop="wx_dir" label="微信文件夹路径"></el-table-column>
|
||||
<el-table-column prop="key" label="密钥(key)"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
13
pywxdump/ui/tsconfig.app.json
Normal file
13
pywxdump/ui/tsconfig.app.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
18
pywxdump/ui/tsconfig.json
Normal file
18
pywxdump/ui/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"node",
|
||||
"axios"
|
||||
],
|
||||
"allowJs": true
|
||||
}
|
||||
}
|
17
pywxdump/ui/tsconfig.node.json
Normal file
17
pywxdump/ui/tsconfig.node.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
53
pywxdump/ui/vite.config.ts
Normal file
53
pywxdump/ui/vite.config.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import {fileURLToPath, URL} from 'node:url'
|
||||
|
||||
import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
base: './',
|
||||
server: {
|
||||
// 访问项目的IP地址,可以设置为“0.0.0.0”来使项目外部可访问
|
||||
host: "0.0.0.0",
|
||||
// 访问项目的端口号
|
||||
port: 8080,
|
||||
// 自动启动浏览器
|
||||
open: false,
|
||||
// 配置反向代理处理跨域请求
|
||||
proxy: {
|
||||
"/api/ls": {
|
||||
target: "http://127.0.0.1:5000",
|
||||
changeOrigin: true, //是否跨域
|
||||
// rewrite: (path) => path.replace(/^\/mis/, ""), //因为后端接口有mis前缀,所以不需要替换
|
||||
// ws: true, //是否代理 websockets
|
||||
// secure: true, //是否https接口
|
||||
},
|
||||
"/api/rs": {
|
||||
target: "http://127.0.0.1:5000",
|
||||
changeOrigin: true, //是否跨域
|
||||
// rewrite: (path) => path.replace(/^\/mis/, ""), //因为后端接口有mis前缀,所以不需要替换
|
||||
// ws: true, //是否代理 websockets
|
||||
// secure: true, //是否https接口
|
||||
},
|
||||
},
|
||||
},
|
||||
// build: {
|
||||
// rollupOptions: {
|
||||
// output: {
|
||||
// chunkFileNames: 'js-[name]-[hash].js',
|
||||
// entryFileNames: 'js-[name]-[hash].js',
|
||||
// assetFileNames: '[ext]-[name]-[hash][extname]',
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user