Refactor UUID generation, remove unused logger and encryption utilities, and clean up request handling

- Updated `generateUUID` function for improved readability and performance.
- Deleted `logger.ts`, `other.ts`, `request.ts`, `storage.ts`, `tansParams.ts`, and `validate.ts` as they were no longer needed.
- Simplified TypeScript configuration by removing unnecessary paths and aliases.
- Enhanced Vite configuration for better project structure and maintainability.
This commit is contained in:
DEV_DSW
2026-04-17 15:38:08 +08:00
parent b1dea9a5c2
commit 79bea4f107
360 changed files with 14495 additions and 30856 deletions

View File

@@ -0,0 +1,174 @@
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>
);
}

View File

@@ -0,0 +1,56 @@
import type { ReactNode } from 'react';
type DialogSurfaceProps = {
open: boolean;
title: string;
widthClassName?: string;
onClose: () => void;
children: ReactNode;
};
export default function DialogSurface({
open,
title,
widthClassName = 'max-w-[560px]',
onClose,
children,
}: DialogSurfaceProps) {
if (!open) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/45 px-4 py-6 backdrop-blur-[2px]"
onClick={onClose}
>
<div
className={[
'w-full rounded-[20px] bg-[#F4F3EB] shadow-[0_25px_50px_-12px_rgba(0,0,0,0.2)] dark:bg-[#1f1f22]',
widthClassName,
].join(' ')}
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-start justify-between border-b border-black/6 px-6 py-5 dark:border-white/6">
<h2
className="text-[20px] font-normal tracking-tight text-[#171717] dark:text-[#f3f4f6]"
style={{ fontFamily: "Georgia, Cambria, 'Times New Roman', Times, serif" }}
>
{title}
</h2>
<button
type="button"
className="mt-0.5 rounded-full p-1 text-[#99A0AE] transition-colors hover:text-[#171717] dark:hover:text-[#f3f4f6]"
onClick={onClose}
aria-label="关闭弹窗"
>
<svg viewBox="0 0 24 24" className="h-5 w-5 fill-none stroke-current" strokeWidth="1.8">
<path d="M6 6L18 18M18 6L6 18" strokeLinecap="round" />
</svg>
</button>
</div>
<div className="px-6 pb-6 pt-5">{children}</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,203 @@
import { useEffect, useState } from 'react';
import { hotelStaffTypeMappingListUsingPost } from '../../../api/typeMapping';
import type { RoomTypeMapping } from '../../../api/types';
import type { TaskOperationInput } from '../../../stores';
import DialogSurface from './DialogSurface';
type TaskOperationDialogProps = {
open: boolean;
onClose: () => void;
onConfirm: (options: TaskOperationInput) => Promise<void>;
};
type RoomTypeMappingLike = RoomTypeMapping & {
dyHotSpringName?: string;
};
const OPERATION_OPTIONS: Array<{ label: string; value: 'open' | 'close' }> = [
{ label: '开启', value: 'open' },
{ label: '关闭', value: 'close' },
];
function normalizeRoomType(item: RoomTypeMappingLike): RoomTypeMappingLike {
return {
...item,
dyHotSpringName: item.dyHotSpringName ?? item.dyHotSrpingName ?? '',
dyHotSrpingName: item.dyHotSrpingName ?? item.dyHotSpringName ?? '',
};
}
export default function TaskOperationDialog({
open,
onClose,
onConfirm,
}: TaskOperationDialogProps) {
const [roomList, setRoomList] = useState<RoomTypeMappingLike[]>([]);
const [roomType, setRoomType] = useState('');
const [startTime, setStartTime] = useState('');
const [endTime, setEndTime] = useState('');
const [operation, setOperation] = useState<'open' | 'close' | ''>('');
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
setRoomType('');
setStartTime('');
setEndTime('');
setOperation('');
setError(null);
setSubmitting(false);
setLoading(true);
void (async () => {
try {
const response = await hotelStaffTypeMappingListUsingPost({ body: {} as RoomTypeMapping });
setRoomList(Array.isArray(response?.data) ? response.data.map((item: RoomTypeMappingLike) => normalizeRoomType(item)) : []);
} catch (requestError) {
setRoomList([]);
setError(requestError instanceof Error ? requestError.message : String(requestError));
} finally {
setLoading(false);
}
})();
}, [open]);
async function handleConfirm(): Promise<void> {
if (!roomType) {
setError('请选择房型');
return;
}
if (!startTime || !endTime) {
setError('请选择日期范围');
return;
}
if (startTime > endTime) {
setError('开始日期不能晚于结束日期');
return;
}
if (!operation) {
setError('请选择操作');
return;
}
setSubmitting(true);
setError(null);
try {
await onConfirm({
roomType,
startTime,
endTime,
operation,
roomList: roomList.map((item) => ({ ...item })),
});
} catch (confirmError) {
setError(confirmError instanceof Error ? confirmError.message : String(confirmError));
setSubmitting(false);
return;
}
setSubmitting(false);
}
return (
<DialogSurface open={open} title="渠道房型操作" widthClassName="max-w-[480px]" onClose={onClose}>
<div className="space-y-4">
<label className="block space-y-2">
<span className="text-[14px] font-medium text-[#4B4B4B] dark:text-[#9ca3af]"></span>
<select
value={roomType}
onChange={(event) => setRoomType(event.target.value)}
className="h-[48px] w-full rounded-[12px] border border-transparent bg-[#EDECE4] px-4 text-[14px] text-[#171717] outline-none transition-colors focus:border-[#3B6DE8] dark:bg-[#222225] dark:text-[#f3f4f6]"
disabled={loading || submitting}
>
<option value="">{loading ? '正在加载房型...' : '请选择房型'}</option>
{roomList.map((item) => (
<option key={item.id || item.pmsName} value={item.id}>
{item.pmsName || item.id}
</option>
))}
</select>
</label>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<label className="block space-y-2">
<span className="text-[14px] font-medium text-[#4B4B4B] dark:text-[#9ca3af]"></span>
<input
type="date"
value={startTime}
onChange={(event) => setStartTime(event.target.value)}
className="h-[48px] w-full rounded-[12px] border border-transparent bg-[#EDECE4] px-4 text-[14px] text-[#171717] outline-none transition-colors focus:border-[#3B6DE8] dark:bg-[#222225] dark:text-[#f3f4f6]"
disabled={submitting}
/>
</label>
<label className="block space-y-2">
<span className="text-[14px] font-medium text-[#4B4B4B] dark:text-[#9ca3af]"></span>
<input
type="date"
value={endTime}
onChange={(event) => setEndTime(event.target.value)}
className="h-[48px] w-full rounded-[12px] border border-transparent bg-[#EDECE4] px-4 text-[14px] text-[#171717] outline-none transition-colors focus:border-[#3B6DE8] dark:bg-[#222225] dark:text-[#f3f4f6]"
disabled={submitting}
/>
</label>
</div>
<label className="block space-y-2">
<span className="text-[14px] font-medium text-[#4B4B4B] dark:text-[#9ca3af]"></span>
<select
value={operation}
onChange={(event) => setOperation(event.target.value as 'open' | 'close' | '')}
className="h-[48px] w-full rounded-[12px] border border-transparent bg-[#EDECE4] px-4 text-[14px] text-[#171717] outline-none transition-colors focus:border-[#3B6DE8] dark:bg-[#222225] dark:text-[#f3f4f6]"
disabled={submitting}
>
<option value=""></option>
{OPERATION_OPTIONS.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</label>
<div className="rounded-[12px] border border-dashed border-black/5 bg-[#E8E6DE]/30 px-4 py-3 text-[12px] leading-6 text-[#6B7280] dark:border-[#2a2a2d] dark:bg-[#222225]/50 dark:text-gray-400">
沿 `typeMapping` `dyHotSrpingName / dyHotSpringName`
</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={loading || submitting}
>
{submitting ? '执行中...' : '确认'}
</button>
</div>
</div>
</DialogSurface>
);
}

View File

@@ -0,0 +1,2 @@
export { default as AddChannelDialog } from './AddChannelDialog';
export { default as TaskOperationDialog } from './TaskOperationDialog';