🎈 pref:提升 UI 复用性

This commit is contained in:
zhiyu1998 2024-11-25 11:24:00 +08:00
parent 17dbaa3290
commit 4540a69e1b
8 changed files with 628 additions and 820 deletions

View File

@ -29,9 +29,7 @@ export class WebUI extends plugin {
this.isOpenWebUI = this.toolsConfig.isOpenWebUI;
}
async rWebSwitch(e) {
config.updateField("tools", "isOpenWebUI", !this.isOpenWebUI);
const realIsOpenWebUI = config.getConfig("tools").isOpenWebUI;
async initData(e, realIsOpenWebUI) {
if (realIsOpenWebUI) {
Promise.all([getBotStatus(e), getBotVersionInfo(e), getBotLoginInfo(e)]).then(values => {
const status = values[0].data;
@ -44,13 +42,20 @@ export class WebUI extends plugin {
})
})
}
}
async rWebSwitch(e) {
config.updateField("tools", "isOpenWebUI", !this.isOpenWebUI);
const realIsOpenWebUI = config.getConfig("tools").isOpenWebUI;
// 初始化数据
await this.initData(e, realIsOpenWebUI);
// 这里有点延迟,需要写反
e.reply(`R插件 WebUI${ realIsOpenWebUI ? "开启\n🚀 请重启以启动 WebUI" : "关闭" }`);
e.reply(`R插件可视化面板${ realIsOpenWebUI ? "✅已开启" : "❌已关闭" },重启后生效`);
return true;
}
async rWebStatus(e) {
e.reply(`R插件 WebUI${ this.toolsConfig.isOpenWebUI ? "开启" : "关闭" }`);
e.reply(`R插件可视化面板${ this.toolsConfig.isOpenWebUI ? "开启" : "关闭" }`);
return true;
}
}

View File

@ -0,0 +1,47 @@
import React from 'react';
export const ConfigToggle = ({ label, checked, onChange }) => (
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">{label}</span>
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className="toggle toggle-primary"
/>
</label>
</div>
);
export const ConfigInput = ({ label, value, onChange, type = "text", placeholder = "" }) => (
<div className="form-control">
<label className="label">
<span className="label-text">{label}</span>
</label>
<input
type={type}
value={value}
onChange={(e) => onChange(type === "number" ? parseInt(e.target.value) : e.target.value)}
placeholder={placeholder}
className="input input-bordered"
/>
</div>
);
export const ConfigSelect = ({ label, value, onChange, options }) => (
<div className="form-control">
<label className="label">
<span className="label-text">{label}</span>
</label>
<select
className="select select-bordered"
value={value}
onChange={(e) => onChange(parseInt(e.target.value))}
>
{options.map(item => (
<option key={item.value} value={item.value}>{item.label}</option>
))}
</select>
</div>
);

View File

@ -2,71 +2,69 @@ import { useState, useEffect } from 'react';
import { BILI_CDN_SELECT_LIST, BILI_DOWNLOAD_METHOD, BILI_RESOLUTION_LIST } from "../../../constants/constant.js";
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
import Toast from "../toast.jsx";
import { ConfigToggle, ConfigInput, ConfigSelect } from '../common/ConfigItem';
//
const BILI_CONFIG = {
toggles: [
{ key: 'biliDisplayCover', label: '显示封面' },
{ key: 'biliDisplayInfo', label: '显示视频信息' },
{ key: 'biliDisplayIntro', label: '显示简介' },
{ key: 'biliDisplayOnline', label: '显示在线人数' },
{ key: 'biliDisplaySummary', label: '显示总结' },
{ key: 'biliUseBBDown', label: '使用BBDown' },
],
inputs: [
{ key: 'biliSessData', label: 'SESSDATA', type: 'text', placeholder: '请输入Bilibili SESSDATA' },
{ key: 'biliDuration', label: '视频时长限制(秒)', type: 'number' },
{ key: 'biliIntroLenLimit', label: '简介长度限制', type: 'number' },
],
selects: [
{ key: 'biliCDN', label: 'CDN选择', options: BILI_CDN_SELECT_LIST },
{ key: 'biliDownloadMethod', label: '下载方式', options: BILI_DOWNLOAD_METHOD },
{ key: 'biliResolution', label: '视频画质', options: BILI_RESOLUTION_LIST },
]
};
//
const DEFAULT_CONFIG = {
biliSessData: '',
biliDuration: 480,
biliIntroLenLimit: 50,
biliDisplayCover: true,
biliDisplayInfo: true,
biliDisplayIntro: true,
biliDisplayOnline: true,
biliDisplaySummary: false,
biliUseBBDown: false,
biliCDN: 0,
biliDownloadMethod: 0,
biliResolution: 5
};
export default function Bili() {
const [config, setConfig] = useState({
biliSessData: '',
biliDuration: 480,
biliIntroLenLimit: 50,
biliDisplayCover: true,
biliDisplayInfo: true,
biliDisplayIntro: true,
biliDisplayOnline: true,
biliDisplaySummary: false,
biliUseBBDown: false,
biliCDN: 0,
biliDownloadMethod: 0,
biliResolution: 5
});
const [config, setConfig] = useState(DEFAULT_CONFIG);
const [loading, setLoading] = useState(false);
//
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
biliSessData: yamlConfig.biliSessData || '',
biliDuration: yamlConfig.biliDuration || 480,
biliIntroLenLimit: yamlConfig.biliIntroLenLimit || 50,
biliDisplayCover: yamlConfig.biliDisplayCover ?? true,
biliDisplayInfo: yamlConfig.biliDisplayInfo ?? true,
biliDisplayIntro: yamlConfig.biliDisplayIntro ?? true,
biliDisplayOnline: yamlConfig.biliDisplayOnline ?? true,
biliDisplaySummary: yamlConfig.biliDisplaySummary ?? false,
biliUseBBDown: yamlConfig.biliUseBBDown ?? false,
biliCDN: yamlConfig.biliCDN || 0,
biliDownloadMethod: yamlConfig.biliDownloadMethod || 0,
biliResolution: yamlConfig.biliResolution || 5
const newConfig = {};
Object.keys(DEFAULT_CONFIG).forEach(key => {
newConfig[key] = yamlConfig[key] ?? DEFAULT_CONFIG[key];
});
setConfig(newConfig);
}
};
loadConfig();
}, []);
//
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig({
biliSessData: config.biliSessData,
biliDuration: config.biliDuration,
biliIntroLenLimit: config.biliIntroLenLimit,
biliDisplayCover: config.biliDisplayCover,
biliDisplayInfo: config.biliDisplayInfo,
biliDisplayIntro: config.biliDisplayIntro,
biliDisplayOnline: config.biliDisplayOnline,
biliDisplaySummary: config.biliDisplaySummary,
biliUseBBDown: config.biliUseBBDown,
biliCDN: config.biliCDN,
biliDownloadMethod: config.biliDownloadMethod,
biliResolution: config.biliResolution
});
const success = await updateYamlConfig(config);
if (success) {
// 使 daisyUI toast
document.getElementById('toast-success').classList.remove('hidden');
setTimeout(() => {
document.getElementById('toast-success').classList.add('hidden');
@ -79,212 +77,67 @@ export default function Bili() {
}
};
//
const handleReset = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
biliSessData: yamlConfig.biliSessData || '',
biliDuration: yamlConfig.biliDuration || 480,
biliIntroLenLimit: yamlConfig.biliIntroLenLimit || 50,
biliDisplayCover: yamlConfig.biliDisplayCover ?? true,
biliDisplayInfo: yamlConfig.biliDisplayInfo ?? true,
biliDisplayIntro: yamlConfig.biliDisplayIntro ?? true,
biliDisplayOnline: yamlConfig.biliDisplayOnline ?? true,
biliDisplaySummary: yamlConfig.biliDisplaySummary ?? false,
biliUseBBDown: yamlConfig.biliUseBBDown ?? false,
biliCDN: yamlConfig.biliCDN || 0,
biliDownloadMethod: yamlConfig.biliDownloadMethod || 0,
biliResolution: yamlConfig.biliResolution || 5
});
}
const handleConfigChange = (key, value) => {
setConfig(prev => ({ ...prev, [key]: value }));
};
return (
<div className="p-6 mx-auto container">
{/* 成功提示 */}
<Toast id="toast-success" />
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Bilibili 配置</h2>
{/* 基础配置部分 */}
<div className="card bg-base-200 shadow-xl mb-6">
<div className="card-body">
<h3 className="card-title mb-4">基础配置</h3>
{/* SESSDATA配置 */}
<div className="form-control w-full mb-4">
<label className="label">
<span className="label-text">SESSDATA</span>
</label>
<input
type="text"
value={config.biliSessData}
onChange={(e) => setConfig({ ...config, biliSessData: e.target.value })}
placeholder="请输入Bilibili SESSDATA"
className="input input-bordered w-full"
/>
</div>
{/* 数值配置部分 */}
{/* 输入框配置 */}
<div className="grid md:grid-cols-2 gap-4 mb-4">
<div className="form-control">
<label className="label">
<span className="label-text">视频时长限制</span>
</label>
<input
type="number"
value={config.biliDuration}
onChange={(e) => setConfig({ ...config, biliDuration: parseInt(e.target.value) })}
className="input input-bordered"
{BILI_CONFIG.inputs.map(item => (
<ConfigInput
key={item.key}
label={item.label}
type={item.type}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
placeholder={item.placeholder}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">简介长度限制</span>
</label>
<input
type="number"
value={config.biliIntroLenLimit}
onChange={(e) => setConfig({ ...config, biliIntroLenLimit: parseInt(e.target.value) })}
className="input input-bordered"
/>
</div>
))}
</div>
{/* 开关配置部分 */}
{/* 开关配置 */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">显示封面</span>
<input
type="checkbox"
checked={config.biliDisplayCover}
onChange={(e) => setConfig({ ...config, biliDisplayCover: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">显示视频信息</span>
<input
type="checkbox"
checked={config.biliDisplayInfo}
onChange={(e) => setConfig({ ...config, biliDisplayInfo: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">显示简介</span>
<input
type="checkbox"
checked={config.biliDisplayIntro}
onChange={(e) => setConfig({ ...config, biliDisplayIntro: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">显示在线人数</span>
<input
type="checkbox"
checked={config.biliDisplayOnline}
onChange={(e) => setConfig({ ...config, biliDisplayOnline: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">显示总结</span>
<input
type="checkbox"
checked={config.biliDisplaySummary}
onChange={(e) => setConfig({ ...config, biliDisplaySummary: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">使用BBDown</span>
<input
type="checkbox"
checked={config.biliUseBBDown}
onChange={(e) => setConfig({ ...config, biliUseBBDown: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
{BILI_CONFIG.toggles.map(item => (
<ConfigToggle
key={item.key}
label={item.label}
checked={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
/>
))}
</div>
{/* 下拉选择配置部分 */}
{/* 选择框配置 */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="form-control">
<label className="label">
<span className="label-text">CDN选择</span>
</label>
<select
className="select select-bordered"
value={config.biliCDN}
onChange={(e) => setConfig({ ...config, biliCDN: parseInt(e.target.value) })}>
{
BILI_CDN_SELECT_LIST.map(item => {
return (
<option value={ item.value }>{ item.label }</option>
)
})
}
</select>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">下载方式</span>
</label>
<select
className="select select-bordered"
value={config.biliDownloadMethod}
onChange={(e) => setConfig({ ...config, biliDownloadMethod: parseInt(e.target.value) })}>
{
BILI_DOWNLOAD_METHOD.map(item => {
return (
<option value={ item.value }>{ item.label }</option>
)
})
}
</select>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">视频画质</span>
</label>
<select
className="select select-bordered"
value={config.biliResolution}
onChange={(e) => setConfig({ ...config, biliResolution: parseInt(e.target.value) })}>
{
BILI_RESOLUTION_LIST.map(item => {
return (
<option value={ item.value }>{ item.label }</option>
)
})
}
</select>
</div>
{BILI_CONFIG.selects.map(item => (
<ConfigSelect
key={item.key}
label={item.label}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
options={item.options}
/>
))}
</div>
</div>
</div>
{/* 保存按钮 */}
{/* 操作按钮 */}
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={handleReset}
onClick={() => setConfig(DEFAULT_CONFIG)}
disabled={loading}
>
重置
@ -299,5 +152,5 @@ export default function Bili() {
</div>
</div>
</div>
)
);
}

View File

@ -1,69 +1,170 @@
import { useState, useEffect } from 'react';
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
import Toast from "../toast.jsx";
import { ConfigToggle, ConfigInput, ConfigSelect } from '../common/ConfigItem';
import { AI_MODEL_LIST } from "../../../constants/constant.js";
//
const GENERIC_CONFIG = {
basicInputs: [
{
key: 'defaultPath',
label: '视频保存路径',
type: 'text',
placeholder: '请输入视频保存路径...',
defaultValue: './data/rcmp4/'
},
{
key: 'videoSizeLimit',
label: '视频大小限制MB',
type: 'number',
hint: '超过限制转为群文件',
defaultValue: 70
}
],
proxyInputs: [
{
key: 'proxyAddr',
label: '魔法地址',
type: 'text',
placeholder: '请输入代理地址...',
defaultValue: '127.0.0.1'
},
{
key: 'proxyPort',
label: '魔法端口',
type: 'text',
placeholder: '请输入代理端口...',
defaultValue: '7890'
}
],
streamInputs: [
{
key: 'identifyPrefix',
label: '识别前缀',
type: 'text',
placeholder: '请输入识别前缀...',
defaultValue: ''
},
{
key: 'streamDuration',
label: '视频最大时长(秒)',
type: 'number',
defaultValue: 10
}
],
concurrencyInputs: [
{
key: 'queueConcurrency',
label: '队列并发数',
type: 'number',
hint: '仅影响B站下载',
defaultValue: 1
},
{
key: 'videoDownloadConcurrency',
label: '视频下载并发数',
type: 'number',
defaultValue: 1
}
],
textareas: [
{
key: 'deeplApiUrls',
label: 'DeepL API地址',
placeholder: '请输入DeepL API地址多个地址用逗号分隔...',
defaultValue: ''
}
],
toggles: [
{
key: 'streamCompatibility',
label: '兼容模式',
hint: 'NCQQ不用开启其他ICQQ、LLO需要开启',
defaultValue: false
}
],
otherInputs: [
{
key: 'xiaohongshuCookie',
label: '小红书Cookie',
type: 'text',
placeholder: '请输入小红书的Cookie...',
defaultValue: ''
},
{
key: 'autoclearTrashtime',
label: '自动清理时间',
type: 'text',
placeholder: '请输入Cron表达式...',
hint: 'Cron表达式',
defaultValue: '0 0 8 * * ?'
}
],
aiInputs: [
{
key: 'aiBaseURL',
label: 'AI接口地址',
type: 'text',
placeholder: '请输入AI接口地址...',
defaultValue: '',
hint: '用于识图的接口kimi默认接口为https://api.moonshot.cn其他服务商自己填写'
},
{
key: 'aiApiKey',
label: 'API Key',
type: 'text',
placeholder: '请输入API Key...',
defaultValue: '',
hint: '用于识图的api keykimi接口申请https://platform.moonshot.cn/console/api-keys'
}
],
aiSelects: [
{
key: 'aiModel',
label: 'AI模型',
options: [
{ value: 'moonshot-v1-8k', label: 'Moonshot V1 8K' },
{ value: 'moonshot-v1-32k', label: 'Moonshot V1 32K' },
{ value: 'moonshot-v1-128k', label: 'Moonshot V1 128K' },
//
],
defaultValue: 'moonshot-v1-8k',
hint: '模型使用kimi不用填写其他要填写'
}
]
};
//
const DEFAULT_CONFIG = Object.values(GENERIC_CONFIG).reduce((acc, group) => {
group.forEach(item => {
acc[item.key] = item.defaultValue;
});
return acc;
}, {});
export default function Generic() {
const [config, setConfig] = useState({
defaultPath: './data/rcmp4/',
videoSizeLimit: 70,
proxyAddr: '127.0.0.1',
proxyPort: '7890',
identifyPrefix: '',
streamDuration: 10,
streamCompatibility: false,
queueConcurrency: 1,
videoDownloadConcurrency: 1,
autoclearTrashtime: '0 0 8 * * ?',
xiaohongshuCookie: '',
deeplApiUrls: ''
});
const [config, setConfig] = useState(DEFAULT_CONFIG);
const [loading, setLoading] = useState(false);
//
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
defaultPath: yamlConfig.defaultPath || './data/rcmp4/',
videoSizeLimit: yamlConfig.videoSizeLimit || 70,
proxyAddr: yamlConfig.proxyAddr || '127.0.0.1',
proxyPort: yamlConfig.proxyPort || '7890',
identifyPrefix: yamlConfig.identifyPrefix || '',
streamDuration: yamlConfig.streamDuration || 10,
streamCompatibility: yamlConfig.streamCompatibility ?? false,
queueConcurrency: yamlConfig.queueConcurrency || 1,
videoDownloadConcurrency: yamlConfig.videoDownloadConcurrency || 1,
autoclearTrashtime: yamlConfig.autoclearTrashtime || '0 0 8 * * ?',
xiaohongshuCookie: yamlConfig.xiaohongshuCookie || '',
deeplApiUrls: yamlConfig.deeplApiUrls || ''
const newConfig = {};
Object.keys(DEFAULT_CONFIG).forEach(key => {
newConfig[key] = yamlConfig[key] ?? DEFAULT_CONFIG[key];
});
setConfig(newConfig);
}
};
loadConfig();
}, []);
//
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig({
defaultPath: config.defaultPath,
videoSizeLimit: config.videoSizeLimit,
proxyAddr: config.proxyAddr,
proxyPort: config.proxyPort,
identifyPrefix: config.identifyPrefix,
streamDuration: config.streamDuration,
streamCompatibility: config.streamCompatibility,
queueConcurrency: config.queueConcurrency,
videoDownloadConcurrency: config.videoDownloadConcurrency,
autoclearTrashtime: config.autoclearTrashtime,
xiaohongshuCookie: config.xiaohongshuCookie,
deeplApiUrls: config.deeplApiUrls
});
const success = await updateYamlConfig(config);
if (success) {
document.getElementById('generic-toast-success').classList.remove('hidden');
setTimeout(() => {
@ -77,237 +178,132 @@ export default function Generic() {
}
};
//
const handleReset = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
defaultPath: yamlConfig.defaultPath || './data/rcmp4/',
videoSizeLimit: yamlConfig.videoSizeLimit || 70,
proxyAddr: yamlConfig.proxyAddr || '127.0.0.1',
proxyPort: yamlConfig.proxyPort || '7890',
identifyPrefix: yamlConfig.identifyPrefix || '',
streamDuration: yamlConfig.streamDuration || 10,
streamCompatibility: yamlConfig.streamCompatibility ?? false,
queueConcurrency: yamlConfig.queueConcurrency || 1,
videoDownloadConcurrency: yamlConfig.videoDownloadConcurrency || 1,
autoclearTrashtime: yamlConfig.autoclearTrashtime || '0 0 8 * * ?',
xiaohongshuCookie: yamlConfig.xiaohongshuCookie || '',
deeplApiUrls: yamlConfig.deeplApiUrls || '',
});
}
const handleConfigChange = (key, value) => {
setConfig(prev => ({ ...prev, [key]: value }));
};
//
const renderInputGroup = (inputs, title) => (
<div className="grid md:grid-cols-2 gap-4 mb-6">
{inputs.map(item => (
<div key={item.key} className="form-control">
<ConfigInput
label={item.label}
type={item.type}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
placeholder={item.placeholder}
/>
{item.hint && (
<span className="text-xs text-base-content/70 mt-1">
{item.hint}
</span>
)}
</div>
))}
</div>
);
return (
<div className="p-6 mx-auto container">
{/* 成功提示 */}
<Toast id="generic-toast-success" />
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6">通用配置</h2>
{/* 基础配置部分 */}
<div className="card bg-base-200 shadow-xl mb-6">
<div className="card-body">
<h3 className="card-title mb-4">基础配置</h3>
{/* 路径和大小限制配置 */ }
{/* 基础配置 */}
{renderInputGroup(GENERIC_CONFIG.basicInputs)}
{/* 代理配置 */}
<h4 className="font-semibold mt-6 mb-4">代理设置</h4>
{renderInputGroup(GENERIC_CONFIG.proxyInputs)}
{/* 流媒体配置 */}
<h4 className="font-semibold mt-6 mb-4">流媒体设置</h4>
{renderInputGroup(GENERIC_CONFIG.streamInputs)}
{/* 并发配置 */}
<h4 className="font-semibold mt-6 mb-4">并发设置</h4>
{renderInputGroup(GENERIC_CONFIG.concurrencyInputs)}
{/* DeepL API配置 */}
<h4 className="font-semibold mt-6 mb-4">API设置</h4>
{GENERIC_CONFIG.textareas.map(item => (
<div key={item.key} className="form-control w-full mb-6">
<label className="label">
<span className="label-text">{item.label}</span>
</label>
<textarea
value={config[item.key]}
onChange={(e) => handleConfigChange(item.key, e.target.value)}
placeholder={item.placeholder}
className="textarea textarea-bordered h-24"
/>
</div>
))}
{/* 开关配置 */}
{GENERIC_CONFIG.toggles.map(item => (
<div key={item.key} className="form-control mb-6">
<ConfigToggle
label={item.label}
checked={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
/>
{item.hint && (
<span className="text-xs text-base-content/70 ml-2">
{item.hint}
</span>
)}
</div>
))}
{/* 其他配置 */}
<h4 className="font-semibold mt-6 mb-4">其他设置</h4>
{renderInputGroup(GENERIC_CONFIG.otherInputs)}
{/* AI配置 */}
<h4 className="font-semibold mt-6 mb-4">AI设置</h4>
{renderInputGroup(GENERIC_CONFIG.aiInputs)}
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="form-control">
<label className="label">
<span className="label-text">视频保存路径</span>
</label>
<input
type="text"
value={ config.defaultPath }
onChange={ (e) => setConfig({ ...config, defaultPath: e.target.value }) }
placeholder="请输入视频保存路径..."
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">视频大小限制MB</span>
<span className="label-text-alt text-xs">超过限制转为群文件</span>
</label>
<input
type="number"
value={ config.videoSizeLimit }
onChange={ (e) => setConfig({
...config,
videoSizeLimit: parseInt(e.target.value)
}) }
className="input input-bordered"
/>
</div>
</div>
{/* 代理配置 */ }
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="form-control">
<label className="label">
<span className="label-text">代理地址</span>
</label>
<input
type="text"
value={ config.proxyAddr }
onChange={ (e) => setConfig({ ...config, proxyAddr: e.target.value }) }
placeholder="请输入代理地址..."
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">代理端口</span>
</label>
<input
type="text"
value={ config.proxyPort }
onChange={ (e) => setConfig({ ...config, proxyPort: e.target.value }) }
placeholder="请输入代理端口..."
className="input input-bordered"
/>
</div>
</div>
{/* 其他基础配置 */ }
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="form-control">
<label className="label">
<span className="label-text">识别前缀</span>
</label>
<input
type="text"
value={ config.identifyPrefix }
onChange={ (e) => setConfig({ ...config, identifyPrefix: e.target.value }) }
placeholder="请输入识别前缀..."
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">视频最大时长</span>
</label>
<input
type="number"
value={ config.streamDuration }
onChange={ (e) => setConfig({
...config,
streamDuration: parseInt(e.target.value)
}) }
className="input input-bordered"
/>
</div>
</div>
{/* 并发和定时配置 */ }
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="form-control">
<label className="label">
<span className="label-text">队列并发数</span>
<span className="label-text-alt text-xs">仅影响B站下载</span>
</label>
<input
type="number"
value={ config.queueConcurrency }
onChange={ (e) => setConfig({
...config,
queueConcurrency: parseInt(e.target.value)
}) }
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">视频下载并发数</span>
</label>
<input
type="number"
value={ config.videoDownloadConcurrency }
onChange={ (e) => setConfig({
...config,
videoDownloadConcurrency: parseInt(e.target.value)
}) }
className="input input-bordered"
/>
</div>
</div>
{/* DeepL API配置 */ }
<div className="form-control w-full mb-6">
<label className="label">
<span className="label-text">DeepL API地址</span>
</label>
<textarea
value={ config.deeplApiUrls }
onChange={ (e) => setConfig({ ...config, deeplApiUrls: e.target.value }) }
placeholder="请输入DeepL API地址多个地址用逗号分隔..."
className="textarea textarea-bordered h-24"
/>
</div>
{/* 开关配置 */ }
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">兼容模式</span>
<input
type="checkbox"
checked={ config.streamCompatibility }
onChange={ (e) => setConfig({ ...config, streamCompatibility: e.target.checked }) }
className="toggle toggle-primary"
/>
</label>
<span className="text-xs text-base-content/70 ml-2">
NCQQ不用开启其他ICQQLLO需要开启
</span>
</div>
{/* 小红书 Cookie */ }
<div className="form-control w-full mt-6">
<label className="label">
<span className="label-text">小红书Cookie</span>
</label>
<input
type="text"
value={ config.xiaohongshuCookie }
onChange={ (e) => setConfig({ ...config, xiaohongshuCookie: e.target.value }) }
placeholder="请输入小红书的Cookie..."
className="input input-bordered"
/>
</div>
{/* 定时清理配置 */ }
<div className="form-control w-full mt-6">
<label className="label">
<span className="label-text">自动清理时间</span>
<span className="label-text-alt text-xs">Cron表达式</span>
</label>
<input
type="text"
value={ config.autoclearTrashtime }
onChange={ (e) => setConfig({ ...config, autoclearTrashtime: e.target.value }) }
placeholder="请输入Cron表达式..."
className="input input-bordered"
/>
{GENERIC_CONFIG.aiSelects.map(item => (
<div key={item.key} className="form-control">
<ConfigSelect
label={item.label}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
options={item.options}
/>
{item.hint && (
<span className="text-xs text-base-content/70 mt-1">
{item.hint}
</span>
)}
</div>
))}
</div>
</div>
</div>
{/* 保存按钮 */ }
{/* 保存按钮 */}
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={ handleReset }
disabled={ loading }
onClick={() => setConfig(DEFAULT_CONFIG)}
disabled={loading}
>
重置
</button>
<button
className="btn btn-primary"
onClick={ handleSave }
disabled={ loading }
onClick={handleSave}
disabled={loading}
>
{ loading ? <span className="loading loading-spinner"></span> : '保存配置' }
{loading ? <span className="loading loading-spinner"></span> : '保存配置'}
</button>
</div>
</div>

View File

@ -2,57 +2,85 @@ import { useState, useEffect } from 'react';
import { NETEASECLOUD_QUALITY_LIST } from "../../../constants/constant.js";
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
import Toast from "../toast.jsx";
import { ConfigToggle, ConfigInput, ConfigSelect } from '../common/ConfigItem';
//
const NCM_CONFIG = {
textareas: [
{
key: 'neteaseCookie',
label: 'Cookie',
placeholder: '请输入网易云Cookie...'
}
],
inputs: [
{
key: 'neteaseCloudAPIServer',
label: '自建API服务器地址',
type: 'text',
placeholder: '请输入API服务器地址...'
},
{
key: 'neteaseUserId',
label: '用户ID',
type: 'text',
placeholder: '网易云用户ID',
hint: '不要手动更改!'
},
{
key: 'songRequestMaxList',
label: '点歌最大列表数',
type: 'number'
}
],
toggles: [
{ key: 'useLocalNeteaseAPI', label: '使用自建API' },
{ key: 'useNeteaseSongRequest', label: '开启点歌功能' },
{ key: 'isSendVocal', label: '发送群语音' }
],
selects: [
{
key: 'neteaseCloudAudioQuality',
label: '音频质量',
options: NETEASECLOUD_QUALITY_LIST
}
]
};
//
const DEFAULT_CONFIG = {
useLocalNeteaseAPI: false,
useNeteaseSongRequest: false,
isSendVocal: true,
songRequestMaxList: 10,
neteaseCookie: '',
neteaseCloudAPIServer: '',
neteaseCloudAudioQuality: 'exhigh',
neteaseUserId: ''
};
export default function Ncm() {
const [config, setConfig] = useState({
useLocalNeteaseAPI: false,
useNeteaseSongRequest: false,
isSendVocal: true,
songRequestMaxList: 10,
neteaseCookie: '',
neteaseCloudAPIServer: '',
neteaseCloudAudioQuality: 'exhigh',
neteaseUserId: ''
});
const [config, setConfig] = useState(DEFAULT_CONFIG);
const [loading, setLoading] = useState(false);
//
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
useLocalNeteaseAPI: yamlConfig.useLocalNeteaseAPI ?? false,
useNeteaseSongRequest: yamlConfig.useNeteaseSongRequest ?? false,
isSendVocal: yamlConfig.isSendVocal ?? true,
songRequestMaxList: yamlConfig.songRequestMaxList || 10,
neteaseCookie: yamlConfig.neteaseCookie || '',
neteaseCloudAPIServer: yamlConfig.neteaseCloudAPIServer || '',
neteaseCloudAudioQuality: yamlConfig.neteaseCloudAudioQuality || 'exhigh',
neteaseUserId: yamlConfig.neteaseUserId || ''
const newConfig = {};
Object.keys(DEFAULT_CONFIG).forEach(key => {
newConfig[key] = yamlConfig[key] ?? DEFAULT_CONFIG[key];
});
setConfig(newConfig);
}
};
loadConfig();
}, []);
//
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig({
useLocalNeteaseAPI: config.useLocalNeteaseAPI,
useNeteaseSongRequest: config.useNeteaseSongRequest,
isSendVocal: config.isSendVocal,
songRequestMaxList: config.songRequestMaxList,
neteaseCookie: config.neteaseCookie,
neteaseCloudAPIServer: config.neteaseCloudAPIServer,
neteaseCloudAudioQuality: config.neteaseCloudAudioQuality,
neteaseUserId: config.neteaseUserId
});
const success = await updateYamlConfig(config);
if (success) {
document.getElementById('ncm-toast-success').classList.remove('hidden');
setTimeout(() => {
@ -66,141 +94,79 @@ export default function Ncm() {
}
};
//
const handleReset = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
useLocalNeteaseAPI: yamlConfig.useLocalNeteaseAPI ?? false,
useNeteaseSongRequest: yamlConfig.useNeteaseSongRequest ?? false,
isSendVocal: yamlConfig.isSendVocal ?? true,
songRequestMaxList: yamlConfig.songRequestMaxList || 10,
neteaseCookie: yamlConfig.neteaseCookie || '',
neteaseCloudAPIServer: yamlConfig.neteaseCloudAPIServer || '',
neteaseCloudAudioQuality: yamlConfig.neteaseCloudAudioQuality || 'exhigh',
neteaseUserId: yamlConfig.neteaseUserId || ''
});
}
const handleConfigChange = (key, value) => {
setConfig(prev => ({ ...prev, [key]: value }));
};
return (
<div className="p-6 mx-auto container">
{/* 成功提示 */}
<Toast id="ncm-toast-success" />
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6">网易云音乐配置</h2>
{/* 基础配置部分 */}
<div className="card bg-base-200 shadow-xl mb-6">
<div className="card-body">
<h3 className="card-title mb-4">基础配置</h3>
{/* 文本输入配置 */}
<div className="space-y-4 mb-6">
<div className="form-control w-full">
{/* 文本配置 */}
{NCM_CONFIG.textareas.map(item => (
<div key={item.key} className="form-control w-full mb-6">
<label className="label">
<span className="label-text">Cookie</span>
<span className="label-text">{item.label}</span>
</label>
<textarea
value={config.neteaseCookie}
onChange={(e) => setConfig({ ...config, neteaseCookie: e.target.value })}
placeholder="请输入网易云Cookie..."
value={config[item.key]}
onChange={(e) => handleConfigChange(item.key, e.target.value)}
placeholder={item.placeholder}
className="textarea textarea-bordered h-24"
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text">自建API服务器地址</span>
</label>
<input
type="text"
value={config.neteaseCloudAPIServer}
onChange={(e) => setConfig({ ...config, neteaseCloudAPIServer: e.target.value })}
placeholder="请输入API服务器地址..."
className="input input-bordered w-full"
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text">用户ID</span>
<span className="label-text-alt text-xs text-warning">不要手动更改</span>
</label>
<input
type="text"
value={config.neteaseUserId}
onChange={(e) => setConfig({ ...config, neteaseUserId: e.target.value })}
placeholder="网易云用户ID"
className="input input-bordered w-full"
/>
</div>
))}
{/* 输入框配置 */}
<div className="space-y-4 mb-6">
{NCM_CONFIG.inputs.map(item => (
<div key={item.key} className="form-control w-full">
<ConfigInput
label={item.label}
type={item.type}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
placeholder={item.placeholder}
/>
{item.hint && (
<span className="text-xs text-warning mt-1">
{item.hint}
</span>
)}
</div>
))}
</div>
{/* 开关配置部分 */}
{/* 开关配置 */}
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">使用自建API</span>
<input
type="checkbox"
checked={config.useLocalNeteaseAPI}
onChange={(e) => setConfig({ ...config, useLocalNeteaseAPI: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">开启点歌功能</span>
<input
type="checkbox"
checked={config.useNeteaseSongRequest}
onChange={(e) => setConfig({ ...config, useNeteaseSongRequest: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">发送群语音</span>
<input
type="checkbox"
checked={config.isSendVocal}
onChange={(e) => setConfig({ ...config, isSendVocal: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
</div>
{NCM_CONFIG.toggles.map(item => (
<ConfigToggle
key={item.key}
label={item.label}
checked={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
/>
))}
</div>
{/* 其他配置 */}
{/* 选择框配置 */}
<div className="grid md:grid-cols-2 gap-4">
<div className="form-control">
<label className="label">
<span className="label-text">点歌最大列表数</span>
</label>
<input
type="number"
value={config.songRequestMaxList}
onChange={(e) => setConfig({ ...config, songRequestMaxList: parseInt(e.target.value) })}
className="input input-bordered"
{NCM_CONFIG.selects.map(item => (
<ConfigSelect
key={item.key}
label={item.label}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
options={item.options}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">音频质量</span>
</label>
<select
className="select select-bordered"
value={config.neteaseCloudAudioQuality}
onChange={(e) => setConfig({ ...config, neteaseCloudAudioQuality: e.target.value })}>
{NETEASECLOUD_QUALITY_LIST.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
))}
</div>
</div>
</div>
@ -209,7 +175,7 @@ export default function Ncm() {
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={handleReset}
onClick={() => setConfig(DEFAULT_CONFIG)}
disabled={loading}
>
重置

View File

@ -1,42 +1,61 @@
import { useState, useEffect } from 'react';
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
import Toast from "../toast.jsx";
import { ConfigToggle } from '../common/ConfigItem';
//
const TIKTOK_CONFIG = {
textareas: [
{
key: 'douyinCookie',
label: 'Cookie',
placeholder: '请输入抖音Cookie...',
hint: '格式odin_tt=xxx;passport_fe_beating_status=xxx;...'
}
],
toggles: [
{
key: 'douyinCompression',
label: '视频压缩',
hint: '开启后使用压缩格式,加速视频发送'
},
{
key: 'douyinComments',
label: '显示评论',
hint: '是否显示视频评论'
}
]
};
//
const DEFAULT_CONFIG = {
douyinCookie: '',
douyinCompression: true,
douyinComments: false
};
export default function Tiktok() {
const [config, setConfig] = useState({
douyinCookie: '',
douyinCompression: true,
douyinComments: false
});
const [config, setConfig] = useState(DEFAULT_CONFIG);
const [loading, setLoading] = useState(false);
//
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
douyinCookie: yamlConfig.douyinCookie || '',
douyinCompression: yamlConfig.douyinCompression ?? true,
douyinComments: yamlConfig.douyinComments ?? false
const newConfig = {};
Object.keys(DEFAULT_CONFIG).forEach(key => {
newConfig[key] = yamlConfig[key] ?? DEFAULT_CONFIG[key];
});
setConfig(newConfig);
}
};
loadConfig();
}, []);
//
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig({
douyinCookie: config.douyinCookie,
douyinCompression: config.douyinCompression,
douyinComments: config.douyinComments
});
const success = await updateYamlConfig(config);
if (success) {
document.getElementById('tiktok-toast-success').classList.remove('hidden');
setTimeout(() => {
@ -50,77 +69,57 @@ export default function Tiktok() {
}
};
//
const handleReset = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
douyinCookie: yamlConfig.douyinCookie || '',
douyinCompression: yamlConfig.douyinCompression ?? true,
douyinComments: yamlConfig.douyinComments ?? false
});
}
const handleConfigChange = (key, value) => {
setConfig(prev => ({ ...prev, [key]: value }));
};
return (
<div className="p-6 mx-auto container">
{/* 成功提示 */}
<Toast id="tiktok-toast-success" />
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6">抖音配置</h2>
{/* 基础配置部分 */}
<div className="card bg-base-200 shadow-xl mb-6">
<div className="card-body">
<h3 className="card-title mb-4">基础配置</h3>
{/* Cookie配置 */}
<div className="form-control w-full mb-6">
<label className="label">
<span className="label-text">Cookie</span>
<span className="label-text-alt text-xs text-base-content/70">
格式odin_tt=xxx;passport_fe_beating_status=xxx;...
</span>
</label>
<textarea
value={config.douyinCookie}
onChange={(e) => setConfig({ ...config, douyinCookie: e.target.value })}
placeholder="请输入抖音Cookie..."
className="textarea textarea-bordered h-24"
/>
</div>
{TIKTOK_CONFIG.textareas.map(item => (
<div key={item.key} className="form-control w-full mb-6">
<label className="label">
<span className="label-text">{item.label}</span>
{item.hint && (
<span className="label-text-alt text-xs text-base-content/70">
{item.hint}
</span>
)}
</label>
<textarea
value={config[item.key]}
onChange={(e) => handleConfigChange(item.key, e.target.value)}
placeholder={item.placeholder}
className="textarea textarea-bordered h-24"
/>
</div>
))}
{/* 开关配置部分 */}
{/* 开关配置 */}
<div className="grid md:grid-cols-2 gap-4">
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">视频压缩</span>
<input
type="checkbox"
checked={config.douyinCompression}
onChange={(e) => setConfig({ ...config, douyinCompression: e.target.checked })}
className="toggle toggle-primary"
{TIKTOK_CONFIG.toggles.map(item => (
<div key={item.key}>
<ConfigToggle
label={item.label}
checked={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
/>
</label>
<span className="text-xs text-base-content/70 ml-2">
开启后使用压缩格式加速视频发送
</span>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">显示评论</span>
<input
type="checkbox"
checked={config.douyinComments}
onChange={(e) => setConfig({ ...config, douyinComments: e.target.checked })}
className="toggle toggle-primary"
/>
</label>
<span className="text-xs text-base-content/70 ml-2">
是否显示视频评论
</span>
</div>
{item.hint && (
<span className="text-xs text-base-content/70 ml-2">
{item.hint}
</span>
)}
</div>
))}
</div>
</div>
</div>
@ -129,7 +128,7 @@ export default function Tiktok() {
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={handleReset}
onClick={() => setConfig(DEFAULT_CONFIG)}
disabled={loading}
>
重置

View File

@ -1,46 +1,51 @@
import { useState, useEffect } from 'react';
import { BILI_CDN_SELECT_LIST, YOUTUBE_GRAPHICS_LIST } from "../../../constants/constant.js";
import { YOUTUBE_GRAPHICS_LIST } from "../../../constants/constant.js";
import { readYamlConfig, updateYamlConfig } from '../../utils/yamlHelper';
import Toast from "../toast.jsx";
import { ConfigInput, ConfigSelect } from '../common/ConfigItem';
//
const YOUTUBE_CONFIG = {
inputs: [
{ key: 'youtubeCookiePath', label: 'Cookie文件路径', type: 'text', placeholder: '请输入Cookie.txt文件路径...' },
{ key: 'youtubeClipTime', label: '最大截取时长(秒)', type: 'number', hint: '建议不超过5分钟' },
{ key: 'youtubeDuration', label: '视频时长限制(秒)', type: 'number', hint: '建议不超过30分钟' }
],
selects: [
{ key: 'youtubeGraphicsOptions', label: '下载画质', options: YOUTUBE_GRAPHICS_LIST, hint: '0为原画' }
]
};
//
const DEFAULT_CONFIG = {
youtubeGraphicsOptions: 720,
youtubeClipTime: 0,
youtubeDuration: 480,
youtubeCookiePath: ''
};
export default function Youtube() {
const [config, setConfig] = useState({
youtubeGraphicsOptions: 720,
youtubeClipTime: 0,
youtubeDuration: 480,
youtubeCookiePath: ''
});
const [config, setConfig] = useState(DEFAULT_CONFIG);
const [loading, setLoading] = useState(false);
//
useEffect(() => {
const loadConfig = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
youtubeGraphicsOptions: yamlConfig.youtubeGraphicsOptions || 720,
youtubeClipTime: yamlConfig.youtubeClipTime || 0,
youtubeDuration: yamlConfig.youtubeDuration || 480,
youtubeCookiePath: yamlConfig.youtubeCookiePath || ''
const newConfig = {};
Object.keys(DEFAULT_CONFIG).forEach(key => {
newConfig[key] = yamlConfig[key] ?? DEFAULT_CONFIG[key];
});
setConfig(newConfig);
}
};
loadConfig();
}, []);
//
const handleSave = async () => {
setLoading(true);
try {
const success = await updateYamlConfig({
youtubeGraphicsOptions: config.youtubeGraphicsOptions,
youtubeClipTime: config.youtubeClipTime,
youtubeDuration: config.youtubeDuration,
youtubeCookiePath: config.youtubeCookiePath
});
const success = await updateYamlConfig(config);
if (success) {
document.getElementById('youtube-toast-success').classList.remove('hidden');
setTimeout(() => {
@ -54,90 +59,46 @@ export default function Youtube() {
}
};
//
const handleReset = async () => {
const yamlConfig = await readYamlConfig();
if (yamlConfig) {
setConfig({
youtubeGraphicsOptions: yamlConfig.youtubeGraphicsOptions || 720,
youtubeClipTime: yamlConfig.youtubeClipTime || 0,
youtubeDuration: yamlConfig.youtubeDuration || 480,
youtubeCookiePath: yamlConfig.youtubeCookiePath || ''
});
}
const handleConfigChange = (key, value) => {
setConfig(prev => ({ ...prev, [key]: value }));
};
return (
<div className="p-6 mx-auto container">
{/* 成功提示 */}
<Toast id="youtube-toast-success" />
<div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6">YouTube 配置</h2>
{/* 基础配置部分 */}
<div className="card bg-base-200 shadow-xl mb-6">
<div className="card-body">
<h3 className="card-title mb-4">基础配置</h3>
{/* Cookie路径配置 */}
<div className="form-control w-full mb-6">
<label className="label">
<span className="label-text">Cookie文件路径</span>
</label>
<input
type="text"
value={config.youtubeCookiePath}
onChange={(e) => setConfig({ ...config, youtubeCookiePath: e.target.value })}
placeholder="请输入Cookie.txt文件路径..."
className="input input-bordered w-full"
/>
{/* 输入框配置 */}
<div className="grid md:grid-cols-2 gap-4 mb-4">
{YOUTUBE_CONFIG.inputs.map(item => (
<ConfigInput
key={item.key}
label={item.label}
type={item.type}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
placeholder={item.placeholder}
/>
))}
</div>
{/* 数值配置部分 */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="form-control">
<label className="label">
<span className="label-text">下载画质</span>
<span className="label-text-alt text-xs">0为原画</span>
</label>
<select
className="select select-bordered"
value={config.youtubeGraphicsOptions}
onChange={(e) => setConfig({ ...config, youtubeGraphicsOptions: parseInt(e.target.value) })}>
{
YOUTUBE_GRAPHICS_LIST.map(item => {
return (
<option value={ item.value }>{ item.label }</option>
)
})
}
</select>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">最大截取时长</span>
<span className="label-text-alt text-xs">建议不超过5分钟</span>
</label>
<input
type="number"
value={config.youtubeClipTime}
onChange={(e) => setConfig({ ...config, youtubeClipTime: parseInt(e.target.value) })}
className="input input-bordered"
{/* 选择框配置 */}
<div className="grid md:grid-cols-2 gap-4">
{YOUTUBE_CONFIG.selects.map(item => (
<ConfigSelect
key={item.key}
label={item.label}
value={config[item.key]}
onChange={(value) => handleConfigChange(item.key, value)}
options={item.options}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">视频时长限制</span>
<span className="label-text-alt text-xs">建议不超过30分钟</span>
</label>
<input
type="number"
value={config.youtubeDuration}
onChange={(e) => setConfig({ ...config, youtubeDuration: parseInt(e.target.value) })}
className="input input-bordered"
/>
</div>
))}
</div>
</div>
</div>
@ -146,7 +107,7 @@ export default function Youtube() {
<div className="flex justify-end gap-4">
<button
className="btn btn-ghost"
onClick={handleReset}
onClick={() => setConfig(DEFAULT_CONFIG)}
disabled={loading}
>
重置

View File

@ -1,36 +1,14 @@
import { spawn } from 'child_process';
logger.info(`[R插件][Next.js监测], 父进程 PID: ${process.pid}`);
logger.mark(`[R插件][WebUI], 父进程 PID: ${process.pid}`);
let nextjsProcess = null;
// 构建应用程序
export const buildNextJs = () => {
logger.info(logger.yellow('[R插件][Next.js监测],正在构建 Next.js 应用...'));
return new Promise((resolve, reject) => {
const buildProcess = spawn('pnpm', ['run', 'build'], {
cwd: './plugins/rconsole-plugin/server',
stdio: 'ignore',
shell: true,
});
buildProcess.on('close', (code) => {
if (code === 0) {
logger.info(logger.yellow('[R插件][Next.js监测],构建完成。'));
resolve();
} else {
logger.error(`[R插件][Next.js监测],构建失败,退出码:${code}`);
reject(new Error('Build failed'));
}
});
});
};
// 启动子进程运行 Next.js
export const startNextJs = (mode = 'start') => {
const script = mode === 'start' ? 'start' : 'dev';
logger.info(logger.yellow(`[R插件][Next.js监测],启动 Next.js ${mode} 进程...`));
logger.info(logger.yellow(`[R插件][WebUI监测],启动 WebUI ${mode} 进程...`));
nextjsProcess = spawn('pnpm', ['run', script], {
cwd: './plugins/rconsole-plugin', // 指定工作目录
@ -40,19 +18,22 @@ export const startNextJs = (mode = 'start') => {
// 子进程异常退出时捕获信号
nextjsProcess.on('close', (code) => {
logger.error(`[R插件][Next.js监测]Next.js 进程发生异常 ${code}`);
logger.error(`[R插件][WebUI监测]WebUI 进程发生异常 ${code}`);
nextjsProcess = null;
});
nextjsProcess.on('error', (err) => {
logger.error(`[R插件][Next.js监测] 子进程错误: ${err.message}`);
logger.error(`[R插件][WebUI监测] 子进程错误: ${err.message}`);
});
};
// 捕获父进程退出信号
const cleanup = () => {
logger.info(logger.yellow('[R插件][Next.js监测] 父进程退出,终止子进程...'));
export const cleanup = () => {
logger.info(logger.yellow('[R插件][WebUI监测] 父进程退出,终止子进程...'));
if (nextjsProcess) {
nextjsProcess.kill(); // 终止子进程
// 终止子进程
nextjsProcess.kill();
nextjsProcess = null;
}
process.exit();
};