- Added a new task store in `src-react/stores/task.ts` to manage tasks and their statuses. - Implemented functions for creating, executing, and retrying tasks, along with handling task progress and completion. - Introduced persistence for tasks using IPC. - Created utility functions for normalizing room types and building subtasks. - Added a new CSS file for global styles in `src-react/styles.css`. - Created runtime types in `src-react/types/runtime.ts` and exported them. - Updated the main entry points for Vue and React applications to support dynamic framework loading. - Refactored chat model interfaces and utility functions into `src/shared/chat-model.ts`. - Updated TypeScript configuration to include paths for React components and types. - Enhanced Vite configuration to support both Vue and React frameworks.
175 lines
7.1 KiB
TypeScript
175 lines
7.1 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import type { ChannelItem } from '../../../stores';
|
|
import DialogSurface from './DialogSurface';
|
|
|
|
type AddChannelDialogProps = {
|
|
open: boolean;
|
|
loading: boolean;
|
|
availableChannels: ChannelItem[];
|
|
initialSelected: ChannelItem[];
|
|
onClose: () => void;
|
|
onConfirm: (items: ChannelItem[]) => Promise<void>;
|
|
};
|
|
|
|
export default function AddChannelDialog({
|
|
open,
|
|
loading,
|
|
availableChannels,
|
|
initialSelected,
|
|
onClose,
|
|
onConfirm,
|
|
}: AddChannelDialogProps) {
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [localSelected, setLocalSelected] = useState<ChannelItem[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
setSearchQuery('');
|
|
setError(null);
|
|
setSubmitting(false);
|
|
setLocalSelected(initialSelected.map((item) => ({ ...item })));
|
|
}, [open, initialSelected]);
|
|
|
|
const query = searchQuery.trim().toLowerCase();
|
|
const existingChannelUrls = new Set(localSelected.map((item) => item.channelUrl));
|
|
const candidateChannels = availableChannels.filter((item) => !existingChannelUrls.has(item.channelUrl));
|
|
const filteredChannels = (query
|
|
? candidateChannels.filter((item) => (
|
|
item.channelName.toLowerCase().includes(query)
|
|
|| item.channelUrl.toLowerCase().includes(query)
|
|
))
|
|
: candidateChannels)
|
|
.slice(0, 8);
|
|
|
|
function addChannel(item: ChannelItem): void {
|
|
setLocalSelected((current) => {
|
|
if (current.some((channel) => channel.channelUrl === item.channelUrl)) return current;
|
|
return [...current, { ...item }];
|
|
});
|
|
setSearchQuery('');
|
|
}
|
|
|
|
function removeChannel(id: string): void {
|
|
setLocalSelected((current) => current.filter((item) => item.id !== id));
|
|
}
|
|
|
|
async function handleConfirm(): Promise<void> {
|
|
setSubmitting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
await onConfirm(localSelected.map((item) => ({ ...item })));
|
|
} catch (confirmError) {
|
|
setError(confirmError instanceof Error ? confirmError.message : String(confirmError));
|
|
setSubmitting(false);
|
|
return;
|
|
}
|
|
|
|
setSubmitting(false);
|
|
}
|
|
|
|
return (
|
|
<DialogSurface open={open} title="关联渠道" onClose={onClose}>
|
|
<div className="space-y-5">
|
|
<div className="space-y-2">
|
|
<div className="text-[14px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80">搜索添加渠道</div>
|
|
<input
|
|
value={searchQuery}
|
|
onChange={(event) => setSearchQuery(event.target.value)}
|
|
placeholder="输入渠道名称或链接"
|
|
className="h-[48px] w-full rounded-[12px] border border-transparent bg-[#EDECE4] px-4 text-[14px] text-[#171717] outline-none transition-colors placeholder:text-[#99A0AE] focus:border-[#3B6DE8] dark:bg-[#222225] dark:text-[#f3f4f6]"
|
|
/>
|
|
|
|
<div className="min-h-[52px] rounded-[12px] border border-black/5 bg-[#E8E6DE]/30 p-2 dark:border-[#2a2a2d] dark:bg-[#222225]/50">
|
|
{loading ? (
|
|
<div className="px-2 py-3 text-[13px] text-[#99A0AE] dark:text-gray-500">正在加载可用渠道...</div>
|
|
) : filteredChannels.length > 0 ? (
|
|
<div className="space-y-1">
|
|
{filteredChannels.map((item) => (
|
|
<button
|
|
key={item.channelUrl}
|
|
type="button"
|
|
className="flex w-full flex-col rounded-[10px] px-3 py-2 text-left transition-colors hover:bg-[#E8E6DE] dark:hover:bg-[#2a2a2d]"
|
|
onClick={() => addChannel(item)}
|
|
>
|
|
<span className="text-[13px] font-medium text-[#171717] dark:text-[#f3f4f6]">{item.channelName}</span>
|
|
<span className="truncate text-[12px] text-[#99A0AE] dark:text-gray-500">{item.channelUrl}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="px-2 py-3 text-[13px] text-[#99A0AE] dark:text-gray-500">
|
|
{availableChannels.length === 0 ? '暂无可用渠道,请先检查脚本配置。' : '没有匹配到可添加的渠道。'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="text-[14px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80">已选渠道</div>
|
|
<div className="max-h-[240px] space-y-2 overflow-y-auto pr-1">
|
|
{localSelected.length > 0 ? (
|
|
localSelected.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="flex items-center justify-between gap-3 rounded-[12px] border border-black/5 bg-[#E8E6DE]/50 p-3 dark:border-[#2a2a2d] dark:bg-[#222225]"
|
|
>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate text-[13px] font-medium text-[#171717] dark:text-[#f3f4f6]">{item.channelName}</div>
|
|
<div className="truncate text-[12px] text-[#99A0AE] dark:text-gray-500">{item.channelUrl}</div>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
className="shrink-0 rounded-full p-1 text-[#99A0AE] transition-colors hover:text-[#ef4444]"
|
|
onClick={() => removeChannel(item.id)}
|
|
aria-label={`移除渠道 ${item.channelName}`}
|
|
>
|
|
<svg viewBox="0 0 24 24" className="h-[18px] w-[18px] fill-none stroke-current" strokeWidth="1.8">
|
|
<path d="M3 6H21M8 6V4H16V6M7 6L8 19H16L17 6" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="rounded-[12px] border border-dashed border-black/5 bg-[#E8E6DE]/30 px-4 py-6 text-center text-[13px] text-[#99A0AE] dark:border-[#2a2a2d] dark:bg-[#222225]/50 dark:text-gray-500">
|
|
未选择任何渠道
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{error ? (
|
|
<div className="rounded-[12px] border border-[#f2c7cd] bg-[#fff5f6] px-4 py-3 text-[13px] text-[#c24150] dark:border-[#4b2229] dark:bg-[#2b1c1f] dark:text-[#ffb4bf]">
|
|
{error}
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="flex justify-end gap-3 pt-2">
|
|
<button
|
|
type="button"
|
|
className="h-[40px] rounded-full bg-[#EDECE4] px-6 text-[13px] font-semibold text-[#4B4B4B] transition-colors hover:bg-[#E5E4DC] dark:bg-[#222225] dark:text-gray-200"
|
|
onClick={onClose}
|
|
disabled={submitting}
|
|
>
|
|
取消
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="h-[40px] rounded-full bg-[#2B7FFF] px-6 text-[13px] font-semibold text-white transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
|
|
onClick={() => {
|
|
void handleConfirm();
|
|
}}
|
|
disabled={submitting}
|
|
>
|
|
{submitting ? '保存中...' : '确认'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</DialogSurface>
|
|
);
|
|
}
|