mirror of
https://github.com/Jerryplusy/rc-plugin.git
synced 2025-10-14 16:19:18 +00:00
🎈 pref:提升 UI 复用性
This commit is contained in:
parent
17dbaa3290
commit
4540a69e1b
@ -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;
|
||||
}
|
||||
}
|
||||
|
47
server/components/common/ConfigItem.jsx
Normal file
47
server/components/common/ConfigItem.jsx
Normal 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>
|
||||
);
|
@ -2,9 +2,32 @@ 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';
|
||||
|
||||
export default function Bili() {
|
||||
const [config, setConfig] = useState({
|
||||
// 定义配置项
|
||||
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,
|
||||
@ -17,56 +40,31 @@ export default function Bili() {
|
||||
biliCDN: 0,
|
||||
biliDownloadMethod: 0,
|
||||
biliResolution: 5
|
||||
});
|
||||
};
|
||||
|
||||
export default function Bili() {
|
||||
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"
|
||||
{BILI_CONFIG.toggles.map(item => (
|
||||
<ConfigToggle
|
||||
key={item.key}
|
||||
label={item.label}
|
||||
checked={config[item.key]}
|
||||
onChange={(value) => handleConfigChange(item.key, value)}
|
||||
/>
|
||||
</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>
|
||||
))}
|
||||
</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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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 key,kimi接口申请: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,218 +178,113 @@ 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>
|
||||
|
||||
{/* 路径和大小限制配置 */ }
|
||||
<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>
|
||||
{/* 基础配置 */}
|
||||
{renderInputGroup(GENERIC_CONFIG.basicInputs)}
|
||||
|
||||
{/* 代理配置 */}
|
||||
<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>
|
||||
<h4 className="font-semibold mt-6 mb-4">代理设置</h4>
|
||||
{renderInputGroup(GENERIC_CONFIG.proxyInputs)}
|
||||
|
||||
{/* 其他基础配置 */ }
|
||||
<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>
|
||||
{/* 流媒体配置 */}
|
||||
<h4 className="font-semibold mt-6 mb-4">流媒体设置</h4>
|
||||
{renderInputGroup(GENERIC_CONFIG.streamInputs)}
|
||||
|
||||
{/* 并发和定时配置 */ }
|
||||
<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>
|
||||
{/* 并发配置 */}
|
||||
<h4 className="font-semibold mt-6 mb-4">并发设置</h4>
|
||||
{renderInputGroup(GENERIC_CONFIG.concurrencyInputs)}
|
||||
|
||||
{/* DeepL API配置 */}
|
||||
<div className="form-control w-full mb-6">
|
||||
<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">DeepL API地址</span>
|
||||
<span className="label-text">{item.label}</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={ config.deeplApiUrls }
|
||||
onChange={ (e) => setConfig({ ...config, deeplApiUrls: e.target.value }) }
|
||||
placeholder="请输入DeepL API地址,多个地址用逗号分隔..."
|
||||
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">
|
||||
<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"
|
||||
{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)}
|
||||
/>
|
||||
</label>
|
||||
{item.hint && (
|
||||
<span className="text-xs text-base-content/70 ml-2">
|
||||
NCQQ不用开启,其他ICQQ、LLO需要开启
|
||||
{item.hint}
|
||||
</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"
|
||||
{/* 其他配置 */}
|
||||
<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">
|
||||
{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 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"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -297,7 +293,7 @@ export default function Generic() {
|
||||
<div className="flex justify-end gap-4">
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
onClick={ handleReset }
|
||||
onClick={() => setConfig(DEFAULT_CONFIG)}
|
||||
disabled={loading}
|
||||
>
|
||||
重置
|
||||
|
@ -2,9 +2,53 @@ 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';
|
||||
|
||||
export default function Ncm() {
|
||||
const [config, setConfig] = useState({
|
||||
// 定义配置项
|
||||
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,
|
||||
@ -13,46 +57,30 @@ export default function Ncm() {
|
||||
neteaseCloudAPIServer: '',
|
||||
neteaseCloudAudioQuality: 'exhigh',
|
||||
neteaseUserId: ''
|
||||
});
|
||||
};
|
||||
|
||||
export default function Ncm() {
|
||||
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>
|
||||
|
||||
{/* 开关配置部分 */}
|
||||
<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>
|
||||
</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"
|
||||
/>
|
||||
</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 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">
|
||||
{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">
|
||||
{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>
|
||||
</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}
|
||||
>
|
||||
重置
|
||||
|
@ -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';
|
||||
|
||||
export default function Tiktok() {
|
||||
const [config, setConfig] = useState({
|
||||
// 定义配置项
|
||||
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(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">
|
||||
{TIKTOK_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>
|
||||
{item.hint && (
|
||||
<span className="label-text-alt text-xs text-base-content/70">
|
||||
格式:odin_tt=xxx;passport_fe_beating_status=xxx;...
|
||||
{item.hint}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<textarea
|
||||
value={config.douyinCookie}
|
||||
onChange={(e) => setConfig({ ...config, douyinCookie: 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="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>
|
||||
{item.hint && (
|
||||
<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">
|
||||
是否显示视频评论
|
||||
{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}
|
||||
>
|
||||
重置
|
||||
|
@ -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';
|
||||
|
||||
export default function Youtube() {
|
||||
const [config, setConfig] = useState({
|
||||
// 定义配置项
|
||||
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(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}
|
||||
>
|
||||
重置
|
||||
|
@ -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();
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user