Compare commits
10
Commits
c64c9479a9
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35dcc24096 | ||
|
|
4c4de7a5cb | ||
|
|
cb67026821 | ||
|
|
93dcffc983 | ||
|
|
837bd4df76 | ||
|
|
26e1690f84 | ||
|
|
bd88ca76de | ||
|
|
912eebddee | ||
|
|
6cbacab98b | ||
|
|
f7539a33b0 |
+6
-5
@@ -2,7 +2,7 @@ from flask import Flask
|
|||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from .extensions import db, migrate
|
from .extensions import db, migrate
|
||||||
from .config import DevelopmentConfig, ProductionConfig, TestingConfig
|
from .config import DevelopmentConfig, ProductionConfig, TestingConfig
|
||||||
from .blueprints import torrent_bp, book_bp, music_bp, file_bp, video_bp, image_bp
|
from .blueprints import torrent_bp, book_bp, music_bp, file_bp, video_bp, image_bp, asmr_bp
|
||||||
|
|
||||||
|
|
||||||
def create_app(config_name="development"):
|
def create_app(config_name="development"):
|
||||||
@@ -10,14 +10,14 @@ def create_app(config_name="development"):
|
|||||||
CORS(app)
|
CORS(app)
|
||||||
app.config.from_object(DevelopmentConfig)
|
app.config.from_object(DevelopmentConfig)
|
||||||
|
|
||||||
match config_name:
|
if config_name == 'production':
|
||||||
case 'production':
|
|
||||||
app.config.from_object(ProductionConfig)
|
app.config.from_object(ProductionConfig)
|
||||||
case 'testing':
|
elif config_name == 'testing':
|
||||||
app.config.from_object(TestingConfig)
|
app.config.from_object(TestingConfig)
|
||||||
case _:
|
else:
|
||||||
app.config.from_object(DevelopmentConfig)
|
app.config.from_object(DevelopmentConfig)
|
||||||
|
|
||||||
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
migrate.init_app(app, db)
|
migrate.init_app(app, db)
|
||||||
|
|
||||||
@@ -27,5 +27,6 @@ def create_app(config_name="development"):
|
|||||||
app.register_blueprint(file_bp.bp, url_prefix='/api/file')
|
app.register_blueprint(file_bp.bp, url_prefix='/api/file')
|
||||||
app.register_blueprint(video_bp.bp, url_prefix='/api/video')
|
app.register_blueprint(video_bp.bp, url_prefix='/api/video')
|
||||||
app.register_blueprint(image_bp.bp, url_prefix='/api/image')
|
app.register_blueprint(image_bp.bp, url_prefix='/api/image')
|
||||||
|
app.register_blueprint(asmr_bp.bp, url_prefix='/api/asmr')
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from flask import Blueprint, Response, jsonify
|
||||||
|
|
||||||
|
from app.services.asmr_service import AsmrService
|
||||||
|
|
||||||
|
bp = Blueprint("asmr", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/")
|
||||||
|
def index():
|
||||||
|
asmr_object = AsmrService()
|
||||||
|
return jsonify({"code": 200, "message": asmr_object.get_all()})
|
||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
IGNORE_DIRS = {
|
IGNORE_DIRS = {
|
||||||
"System Volume Information",
|
"System Volume Information",
|
||||||
"$RECYCLE.BIN",
|
"$RECYCLE.BIN",
|
||||||
@@ -8,7 +9,7 @@ IGNORE_DIRS = {
|
|||||||
"done_images",
|
"done_images",
|
||||||
"女同"
|
"女同"
|
||||||
}
|
}
|
||||||
SPECIAL_PREFIXES = {"T28", "FC2"}
|
SPECIAL_PREFIXES = {"T28", "FC2", "T38"}
|
||||||
SEPARATORS = {"♀ ", "♀ ", "+"}
|
SEPARATORS = {"♀ ", "♀ ", "+"}
|
||||||
SPECIAL_DIRS = {"川村まや", "上原亜衣", "上原志織"}
|
SPECIAL_DIRS = {"川村まや", "上原亜衣", "上原志織"}
|
||||||
IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.svg'}
|
IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.svg'}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
class AsmrService:
|
||||||
|
API_URL = "https://www.asmrgay.com/api/fs/list"
|
||||||
|
base_params = {
|
||||||
|
"path": "/asmr/中文音声",
|
||||||
|
"password": "",
|
||||||
|
"per_page": 30,
|
||||||
|
"refresh": False
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_all(self):
|
||||||
|
return [self.API_URL]
|
||||||
@@ -11,7 +11,7 @@ from app.models import Setting
|
|||||||
|
|
||||||
class BookService:
|
class BookService:
|
||||||
ALLOWED_CONTENT_TYPES = ['"application/octet-stream"', "application/octet-stream", "text/plain",
|
ALLOWED_CONTENT_TYPES = ['"application/octet-stream"', "application/octet-stream", "text/plain",
|
||||||
"application/zip"]
|
"application/zip", "application/vnd.rar"]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
setting_record = Setting.query.filter_by(name="book_download").first()
|
setting_record = Setting.query.filter_by(name="book_download").first()
|
||||||
@@ -51,6 +51,7 @@ class BookService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def legitimate_naming(name: str) -> str:
|
def legitimate_naming(name: str) -> str:
|
||||||
"""将文件名中的非法字符替换为合法字符"""
|
"""将文件名中的非法字符替换为合法字符"""
|
||||||
|
name = name.rstrip(" .")
|
||||||
replacements = {":": ":", "<": "《", ">": "》", "/": " ", "\\": " ", "?": "?"}
|
replacements = {":": ":", "<": "《", ">": "》", "/": " ", "\\": " ", "?": "?"}
|
||||||
for old, new in replacements.items():
|
for old, new in replacements.items():
|
||||||
name = name.replace(old, new)
|
name = name.replace(old, new)
|
||||||
@@ -64,12 +65,18 @@ class BookService:
|
|||||||
return target
|
return target
|
||||||
|
|
||||||
def post_page(self, name, url): # 帖子页面
|
def post_page(self, name, url): # 帖子页面
|
||||||
print(f"详情页面 {self.setting["baseUrl"]}{url}")
|
full_url = f"{self.setting['baseUrl']}{url}"
|
||||||
response = self.session.get(f"{self.setting["baseUrl"]}{url}")
|
try:
|
||||||
|
response = self.session.get(full_url)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
print(f"详情页面状态码:{str(response.status_code)}")
|
except requests.RequestException as e:
|
||||||
|
yield f"event: error\ndata: 请求帖子详情失败 {url}: {str(e)}\n\n"
|
||||||
|
return
|
||||||
|
print(f"详情页面 {str(response.status_code)} {full_url}")
|
||||||
|
|
||||||
soup = BeautifulSoup(response.text, 'lxml')
|
soup = BeautifulSoup(response.text, 'lxml')
|
||||||
resource_boxs = soup.select('ignore_js_op')
|
resource_boxs = soup.select('ignore_js_op')
|
||||||
|
|
||||||
for i in resource_boxs:
|
for i in resource_boxs:
|
||||||
download_dir_path = self.download_path / self.setting["targetDate"]
|
download_dir_path = self.download_path / self.setting["targetDate"]
|
||||||
self.create_folder(download_dir_path, name)
|
self.create_folder(download_dir_path, name)
|
||||||
@@ -81,8 +88,7 @@ class BookService:
|
|||||||
if privilege_level > self.setting["privilegeLevel"]:
|
if privilege_level > self.setting["privilegeLevel"]:
|
||||||
print("下载失败,权限等级不够")
|
print("下载失败,权限等级不够")
|
||||||
return
|
return
|
||||||
yield from self.download_file(f"{self.setting["baseUrl"]}{i.select_one(
|
yield from self.download_file(f"{self.setting['baseUrl']}{i.select_one('a')['href']}", name, i.select_one('a').string)
|
||||||
'a')['href']}", name, i.select_one('a').string)
|
|
||||||
# yield from self.download_file(f"{self.setting["baseUrl"]}{i.select_one(
|
# yield from self.download_file(f"{self.setting["baseUrl"]}{i.select_one(
|
||||||
# 'a')['href']}", name, self.legitimate_naming(i.select_one('a').string))
|
# 'a')['href']}", name, self.legitimate_naming(i.select_one('a').string))
|
||||||
except RetryError as e:
|
except RetryError as e:
|
||||||
@@ -97,10 +103,12 @@ class BookService:
|
|||||||
print(f'跳转页面了:{file_url}')
|
print(f'跳转页面了:{file_url}')
|
||||||
response = self.session.get(file_url, headers={"referer": file_url}, stream=True)
|
response = self.session.get(file_url, headers={"referer": file_url}, stream=True)
|
||||||
print(f"下载状态码:{response.status_code} {file_url}")
|
print(f"下载状态码:{response.status_code} {file_url}")
|
||||||
|
response.raise_for_status()
|
||||||
print(dir_name + "/" + file_name)
|
print(dir_name + "/" + file_name)
|
||||||
content_type = response.headers.get('Content-Type', '')
|
content_type = response.headers.get('Content-Type', '')
|
||||||
if content_type not in self.ALLOWED_CONTENT_TYPES:
|
if content_type not in self.ALLOWED_CONTENT_TYPES:
|
||||||
yield f"event: mes_error\ndata: {self.setting["targetDate"]} {file_name}\n\n"
|
yield f"event: mes_error\ndata: {self.setting['targetDate']} {file_name}\n\n"
|
||||||
|
print(content_type)
|
||||||
print("不是可下载文件")
|
print("不是可下载文件")
|
||||||
return
|
return
|
||||||
target_dir = self.download_path / self.setting["targetDate"] / dir_name / file_name
|
target_dir = self.download_path / self.setting["targetDate"] / dir_name / file_name
|
||||||
@@ -109,7 +117,7 @@ class BookService:
|
|||||||
for chunk in response.iter_content(chunk_size=1024):
|
for chunk in response.iter_content(chunk_size=1024):
|
||||||
if chunk:
|
if chunk:
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
yield f"data: {self.setting["targetDate"]} {file_name}\n\n"
|
yield f"data: {self.setting['targetDate']} {file_name}\n\n"
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
yield f"event: error\ndata: {self.setting['targetDate']} {file_name} 网络错误\n\n"
|
yield f"event: error\ndata: {self.setting['targetDate']} {file_name} 网络错误\n\n"
|
||||||
raise # 触发 tenacity 重试
|
raise # 触发 tenacity 重试
|
||||||
@@ -122,7 +130,7 @@ class BookService:
|
|||||||
yield f"data: 不允许下载当天的\n\n"
|
yield f"data: 不允许下载当天的\n\n"
|
||||||
return None
|
return None
|
||||||
# yield f"data: 当前页面 {page}\n\n"
|
# yield f"data: 当前页面 {page}\n\n"
|
||||||
url = f"{self.setting["baseUrl"]}forum.php?mod={self.setting["mod"]}&fid={self.setting["fid"]}&page={page}" # 版块页面地址
|
url = f"{self.setting['baseUrl']}forum.php?mod={self.setting['mod']}&fid={self.setting['fid']}&page={page}" # 版块页面地址
|
||||||
response = self.session.get(url) # 请求
|
response = self.session.get(url) # 请求
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
soup = BeautifulSoup(response.text, 'lxml') # 解析
|
soup = BeautifulSoup(response.text, 'lxml') # 解析
|
||||||
@@ -193,6 +201,9 @@ class BookService:
|
|||||||
else:
|
else:
|
||||||
return element.select_one(".by em span").string
|
return element.select_one(".by em span").string
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# 下载
|
||||||
|
# ------------------------------------------------------------
|
||||||
def book_download(self):
|
def book_download(self):
|
||||||
print(os.getenv('aa'))
|
print(os.getenv('aa'))
|
||||||
self.create_folder(self.download_path, self.setting["targetDate"]) # 创建文件夹
|
self.create_folder(self.download_path, self.setting["targetDate"]) # 创建文件夹
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ class FileService:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
new_path = os.path.join(os.path.dirname(entry.path), new_name)
|
new_path = os.path.join(os.path.dirname(entry.path), new_name)
|
||||||
|
try:
|
||||||
os.rename(entry.path, new_path)
|
os.rename(entry.path, new_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
self.result_list.insert(0, new_path)
|
self.result_list.insert(0, new_path)
|
||||||
# return new_path
|
# return new_path
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
from typing import Optional, Callable, List
|
from typing import Optional, Callable, List, Union
|
||||||
|
|
||||||
|
|
||||||
class FolderService:
|
class FolderService:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
paths: List[str] | str,
|
paths: Union[List[str], str],
|
||||||
ignore_dirs: Optional[set] = None,
|
ignore_dirs: Optional[set] = None,
|
||||||
folder_callback: Optional[Callable] = None,
|
folder_callback: Optional[Callable] = None,
|
||||||
file_callback: Optional[Callable] = None,
|
file_callback: Optional[Callable] = None,
|
||||||
|
|||||||
@@ -67,3 +67,9 @@ export function torrent_statistics(): ResponseData {
|
|||||||
url: "/torrent/statistics",
|
url: "/torrent/statistics",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function asmr(): ResponseData {
|
||||||
|
return request({
|
||||||
|
url: "/asmr/",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,26 +10,32 @@ const {isCollapse} = storeToRefs(isCollapseStore)
|
|||||||
<template>
|
<template>
|
||||||
<el-aside :width="!isCollapse?'200px':'auto'">
|
<el-aside :width="!isCollapse?'200px':'auto'">
|
||||||
<el-scrollbar>
|
<el-scrollbar>
|
||||||
<el-menu default-active="2" :collapse="isCollapse" router>
|
<el-menu :default-active="$route.path" :collapse="isCollapse" router>
|
||||||
<el-menu-item index="1" route="index">
|
<el-menu-item index="/index" route="index">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Menu/>
|
<Menu/>
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>首页</span>
|
<span>首页</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="2" route="about">
|
<el-menu-item index="/about" route="about">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Menu/>
|
<Menu/>
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>统计</span>
|
<span>统计</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="3" disabled>
|
<el-menu-item index="/byte-to-hex" route="byte-to-hex">
|
||||||
|
<el-icon>
|
||||||
|
<Menu/>
|
||||||
|
</el-icon>
|
||||||
|
<span>字节对象转Hex</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="4" disabled>
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<document/>
|
<document/>
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>Navigator Three</span>
|
<span>Navigator Three</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="4">
|
<el-menu-item index="5">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<setting/>
|
<setting/>
|
||||||
</el-icon>
|
</el-icon>
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ const router = createRouter({
|
|||||||
name: 'About',
|
name: 'About',
|
||||||
component: () => import('../views/AboutView.vue'),
|
component: () => import('../views/AboutView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'byte-to-hex',
|
||||||
|
name: 'ByteToHex',
|
||||||
|
component: () => import('../views/ByteToHexView.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ interface RequestCache {
|
|||||||
axios.defaults.headers["Content-Type"] = "application/json; charset=UTF-8"
|
axios.defaults.headers["Content-Type"] = "application/json; charset=UTF-8"
|
||||||
|
|
||||||
const service: AxiosInstance = axios.create({
|
const service: AxiosInstance = axios.create({
|
||||||
baseURL: "http://192.168.1.32:5000/api/",
|
baseURL: "http://43.159.32.210:5000/api/",
|
||||||
// baseURL: "/api",
|
// baseURL: "/api",
|
||||||
timeout: 10000,
|
timeout: 30000,
|
||||||
})
|
})
|
||||||
|
|
||||||
service.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
service.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||||
|
|||||||
@@ -0,0 +1,380 @@
|
|||||||
|
<template>
|
||||||
|
<el-main>
|
||||||
|
<el-card>
|
||||||
|
|
||||||
|
<h1>🔑 字节对象 → Hex 密钥</h1>
|
||||||
|
<p class="sub">
|
||||||
|
将键为数字索引的 JSON 对象转换为 32 位十六进制字符串(适用于 AES‑128 密钥)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<label for="jsonInput">输入 JSON 对象</label>
|
||||||
|
<textarea
|
||||||
|
id="jsonInput"
|
||||||
|
v-model="inputText"
|
||||||
|
placeholder='例如:{"0":14,"1":3,"2":121,...}'
|
||||||
|
@keydown.ctrl.enter="performConversion"
|
||||||
|
@keydown.meta.enter="performConversion"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button @click="performConversion">🔄 转换</button>
|
||||||
|
<button class="secondary" :disabled="!copyEnabled" @click="copyResult">
|
||||||
|
📋 复制 Hex
|
||||||
|
</button>
|
||||||
|
<span class="copy-feedback" :class="{ show: copyFeedbackVisible }"
|
||||||
|
>已复制!</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-area">
|
||||||
|
<div class="result-label">
|
||||||
|
<span>转换结果</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="result-box"
|
||||||
|
:class="resultClass"
|
||||||
|
v-text="resultText"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-hint">
|
||||||
|
💡 点击
|
||||||
|
<button class="link-btn" @click="loadExample">加载示例</button>
|
||||||
|
快速体验
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, onMounted} from 'vue'
|
||||||
|
|
||||||
|
// 示例数据
|
||||||
|
const EXAMPLE_OBJ: Record<string, number> = {
|
||||||
|
'0': 14,
|
||||||
|
'1': 3,
|
||||||
|
'2': 121,
|
||||||
|
'3': 117,
|
||||||
|
'4': 7,
|
||||||
|
'5': 245,
|
||||||
|
'6': 22,
|
||||||
|
'7': 34,
|
||||||
|
'8': 136,
|
||||||
|
'9': 163,
|
||||||
|
'10': 201,
|
||||||
|
'11': 114,
|
||||||
|
'12': 195,
|
||||||
|
'13': 230,
|
||||||
|
'14': 109,
|
||||||
|
'15': 204,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const inputText = ref<string>('')
|
||||||
|
const resultText = ref<string>('等待转换…')
|
||||||
|
const resultClass = ref<string>('') // 用于 success / error
|
||||||
|
const copyFeedbackVisible = ref<boolean>(false)
|
||||||
|
const copyEnabled = ref<boolean>(false)
|
||||||
|
|
||||||
|
// 核心转换函数
|
||||||
|
function convertToHex(obj: Record<string, unknown>): string {
|
||||||
|
// 获取所有键并转为数字排序
|
||||||
|
const keys = Object.keys(obj)
|
||||||
|
.map((k) => Number(k))
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
|
||||||
|
// 检查是否有非数字键
|
||||||
|
if (keys.some((k) => isNaN(k))) {
|
||||||
|
throw new Error('对象包含非数字键,请确保键为 "0", "1", … 格式')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查长度是否为16
|
||||||
|
if (keys.length !== 16) {
|
||||||
|
throw new Error(`期望 16 个字节,实际获得 ${keys.length} 个`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按顺序取值并转为两位十六进制
|
||||||
|
const hex = keys
|
||||||
|
.map((key) => {
|
||||||
|
const val = obj[key]
|
||||||
|
if (typeof val !== 'number' || isNaN(val)) {
|
||||||
|
throw new Error(`键 "${key}" 对应的值不是有效数字 (${val})`)
|
||||||
|
}
|
||||||
|
if (val < 0 || val > 255) {
|
||||||
|
throw new Error(`键 "${key}" 的值 ${val} 超出字节范围 (0-255)`)
|
||||||
|
}
|
||||||
|
return val.toString(16).padStart(2, '0')
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
return hex
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行转换
|
||||||
|
function performConversion(): void {
|
||||||
|
const raw = inputText.value.trim()
|
||||||
|
if (!raw) {
|
||||||
|
resultText.value = '⚠️ 请输入 JSON 数据'
|
||||||
|
resultClass.value = 'error'
|
||||||
|
copyEnabled.value = false
|
||||||
|
copyFeedbackVisible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(raw)
|
||||||
|
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
||||||
|
throw new Error('输入必须是 JSON 对象(不是数组或原始类型)')
|
||||||
|
}
|
||||||
|
const hex = convertToHex(obj)
|
||||||
|
resultText.value = hex
|
||||||
|
resultClass.value = 'success'
|
||||||
|
copyEnabled.value = true
|
||||||
|
copyFeedbackVisible.value = false
|
||||||
|
} catch (err: any) {
|
||||||
|
resultText.value = `❌ 转换失败:${err.message}`
|
||||||
|
resultClass.value = 'error'
|
||||||
|
copyEnabled.value = false
|
||||||
|
copyFeedbackVisible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载示例
|
||||||
|
function loadExample(): void {
|
||||||
|
inputText.value = JSON.stringify(EXAMPLE_OBJ, null, 2)
|
||||||
|
performConversion() // 自动转换
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制结果
|
||||||
|
function copyResult(): void {
|
||||||
|
const text = resultText.value
|
||||||
|
if (!text || resultClass.value === 'error') return
|
||||||
|
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(text)
|
||||||
|
.then(() => {
|
||||||
|
copyFeedbackVisible.value = true
|
||||||
|
setTimeout(() => (copyFeedbackVisible.value = false), 2000)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
fallbackCopy(text)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
fallbackCopy(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy(text: string): void {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = text
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
copyFeedbackVisible.value = true
|
||||||
|
setTimeout(() => (copyFeedbackVisible.value = false), 2000)
|
||||||
|
} catch (_) {
|
||||||
|
alert('复制失败,请手动复制')
|
||||||
|
}
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载示例
|
||||||
|
onMounted(() => {
|
||||||
|
loadExample()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
// 变量(可根据需要调整)
|
||||||
|
$primary: #3b82f6;
|
||||||
|
$primary-hover: #2563eb;
|
||||||
|
$bg: #f6f8fa;
|
||||||
|
$card-bg: #ffffff;
|
||||||
|
$text-dark: #1f2937;
|
||||||
|
$text-gray: #6b7280;
|
||||||
|
$border: #d1d5db;
|
||||||
|
$radius: 16px;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
//height: calc(100vh - 211px + 60px);
|
||||||
|
height: calc(100vh - 141px);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: $text-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub {
|
||||||
|
color: $text-gray;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #374151;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
outline: 1px solid $border;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-family: 'Menlo', 'Cascadia Code', 'Consolas', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
resize: none;
|
||||||
|
transition: border 0.2s, background 0.2s;
|
||||||
|
background: #fafbfc;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: $primary;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 18px 0 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: $primary;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, transform 0.1s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $primary-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: $text-dark;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #d1d5db;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.link-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: $primary;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: underline;
|
||||||
|
display: inline;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $primary-hover;
|
||||||
|
background: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-feedback {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #10b981;
|
||||||
|
margin-left: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-area {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-box {
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-family: 'Menlo', 'Cascadia Code', 'Consolas', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
word-break: break-all;
|
||||||
|
min-height: 48px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
color: $text-dark;
|
||||||
|
transition: background 0.2s, border-color 0.2s;
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: #fee2e2;
|
||||||
|
border-color: #fca5a5;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background: #ecfdf5;
|
||||||
|
border-color: #6ee7b7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text-gray;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, onUnmounted} from "vue";
|
import {ref, onUnmounted} from "vue";
|
||||||
import {
|
import {
|
||||||
|
asmr,
|
||||||
file_rename,
|
file_rename,
|
||||||
music_rename,
|
music_rename,
|
||||||
image_hasTorrent,
|
image_hasTorrent,
|
||||||
@@ -38,7 +39,8 @@ const buttons = [
|
|||||||
{id: 5, title: "判断视频是否有torrent", handle: video_hasTorrent},
|
{id: 5, title: "判断视频是否有torrent", handle: video_hasTorrent},
|
||||||
{id: 6, title: "判断图片是否有torrent", handle: image_hasTorrent},
|
{id: 6, title: "判断图片是否有torrent", handle: image_hasTorrent},
|
||||||
{id: 7, title: "音乐文件重命名", handle: music_rename},
|
{id: 7, title: "音乐文件重命名", handle: music_rename},
|
||||||
{id: 8, title: "种子统计", handle: torrent_statistics}
|
{id: 8, title: "种子统计", handle: torrent_statistics},
|
||||||
|
{id: 9, title: "asmr", handle: asmr}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ function closeAlert(id: number) {
|
|||||||
const handleClick = async (btnObj: BtnItem) => {
|
const handleClick = async (btnObj: BtnItem) => {
|
||||||
if (btnObj.title === "断开") return
|
if (btnObj.title === "断开") return
|
||||||
if (btnObj.title === "小说下载") {
|
if (btnObj.title === "小说下载") {
|
||||||
connect("http://192.168.1.32:5000/api/book/download")
|
connect("http://43.159.32.210:5000/api/book/download")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -64,7 +66,10 @@ const handleClick = async (btnObj: BtnItem) => {
|
|||||||
}))
|
}))
|
||||||
messages.value.push({id: Date.now() + messages.value.length, text: '已完成'})
|
messages.value.push({id: Date.now() + messages.value.length, text: '已完成'})
|
||||||
messages.value.reverse()
|
messages.value.reverse()
|
||||||
} finally {
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user