- 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.
204 lines
7.4 KiB
TypeScript
204 lines
7.4 KiB
TypeScript
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>
|
||
);
|
||
}
|