403 lines
15 KiB
TypeScript
403 lines
15 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import { useRouter } from 'next/navigation';
|
||
import { useAuth } from '@/lib/auth';
|
||
import { ProtectedRoute } from '@/lib/auth';
|
||
import { limqRequest } from '@/lib/api';
|
||
import { TeamSelector } from '@/app/components/ui/TeamSelector';
|
||
import { ProjectSelector } from '@/app/components/ui/ProjectSelector';
|
||
|
||
interface ShortUrlData {
|
||
originalUrl: string;
|
||
customSlug?: string;
|
||
title: string;
|
||
description?: string;
|
||
tags?: string[];
|
||
teamId: string;
|
||
projectId: string;
|
||
domain: string;
|
||
}
|
||
|
||
export default function CreateShortUrlPage() {
|
||
return (
|
||
<ProtectedRoute>
|
||
<CreateShortUrlForm />
|
||
</ProtectedRoute>
|
||
);
|
||
}
|
||
|
||
function CreateShortUrlForm() {
|
||
const router = useRouter();
|
||
const { user } = useAuth();
|
||
|
||
const [formData, setFormData] = useState<ShortUrlData>({
|
||
originalUrl: '',
|
||
customSlug: '',
|
||
title: '',
|
||
description: '',
|
||
tags: [],
|
||
teamId: '',
|
||
projectId: '',
|
||
domain: 'googleads.link',
|
||
});
|
||
|
||
const [tagInput, setTagInput] = useState('');
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [success, setSuccess] = useState(false);
|
||
|
||
// 使用 useEffect 在加载时添加用户信息到表单数据中
|
||
useEffect(() => {
|
||
if (user) {
|
||
console.log('当前用户:', user.email);
|
||
// 可以在这里添加用户相关数据到表单中
|
||
}
|
||
}, [user]);
|
||
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||
const { name, value } = e.target;
|
||
setFormData(prev => ({
|
||
...prev,
|
||
[name]: value
|
||
}));
|
||
};
|
||
|
||
const handleTagKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||
if (e.key === 'Enter' && tagInput.trim()) {
|
||
e.preventDefault();
|
||
addTag();
|
||
}
|
||
};
|
||
|
||
const addTag = () => {
|
||
if (tagInput.trim() && !formData.tags?.includes(tagInput.trim())) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
tags: [...(prev.tags || []), tagInput.trim()]
|
||
}));
|
||
setTagInput('');
|
||
}
|
||
};
|
||
|
||
const removeTag = (tagToRemove: string) => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
tags: prev.tags?.filter(tag => tag !== tagToRemove)
|
||
}));
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setIsSubmitting(true);
|
||
setError(null);
|
||
|
||
try {
|
||
// 验证必填字段
|
||
if (!formData.originalUrl) {
|
||
throw new Error('原始 URL 是必填项');
|
||
}
|
||
|
||
if (!formData.title) {
|
||
throw new Error('标题是必填项');
|
||
}
|
||
|
||
if (!formData.teamId) {
|
||
throw new Error('团队是必填项');
|
||
}
|
||
|
||
if (!formData.projectId) {
|
||
throw new Error('项目是必填项');
|
||
}
|
||
|
||
if (!formData.domain) {
|
||
throw new Error('域名是必填项');
|
||
}
|
||
|
||
// 按照API要求构建请求数据
|
||
const requestData = {
|
||
type: "shorturl",
|
||
attributes: {
|
||
// 可以添加任何额外属性,但attributes不能为空
|
||
icon: ""
|
||
},
|
||
shortUrl: {
|
||
url: formData.originalUrl,
|
||
slug: formData.customSlug || undefined,
|
||
title: formData.title,
|
||
name: formData.title,
|
||
description: formData.description || "",
|
||
domain: formData.domain
|
||
},
|
||
teamId: formData.teamId,
|
||
projectId: formData.projectId,
|
||
tagIds: formData.tags && formData.tags.length > 0 ? formData.tags : undefined
|
||
};
|
||
|
||
// 调用 API 创建 shorturl 资源
|
||
const response = await limqRequest('resource/shorturl', 'POST', requestData as unknown as Record<string, unknown>);
|
||
|
||
console.log('创建成功:', response);
|
||
setSuccess(true);
|
||
|
||
// 2秒后跳转到链接列表页面
|
||
setTimeout(() => {
|
||
router.push('/links');
|
||
}, 2000);
|
||
|
||
} catch (err) {
|
||
console.error('创建短链接失败:', err);
|
||
setError(err instanceof Error ? err.message : '创建短链接失败,请稍后重试');
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="container mx-auto px-4 py-8 max-w-3xl">
|
||
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
||
<div className="border-b border-gray-200 bg-blue-50 px-6 py-4">
|
||
<h1 className="text-xl font-medium text-gray-900">Create Short URL</h1>
|
||
<p className="mt-1 text-sm text-gray-600">
|
||
Create a new short URL resource for tracking and analytics
|
||
</p>
|
||
</div>
|
||
|
||
{error && (
|
||
<div className="bg-red-50 border-l-4 border-red-500 p-4 m-6">
|
||
<div className="flex">
|
||
<div className="flex-shrink-0">
|
||
<svg className="h-5 w-5 text-red-500" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||
</svg>
|
||
</div>
|
||
<div className="ml-3">
|
||
<p className="text-sm text-red-700">{error}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{success && (
|
||
<div className="bg-green-50 border-l-4 border-green-500 p-4 m-6">
|
||
<div className="flex">
|
||
<div className="flex-shrink-0">
|
||
<svg className="h-5 w-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||
</svg>
|
||
</div>
|
||
<div className="ml-3">
|
||
<p className="text-sm text-green-700">
|
||
短链接创建成功!正在跳转...
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||
{/* 标题 */}
|
||
<div>
|
||
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
|
||
标题 <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="title"
|
||
name="title"
|
||
value={formData.title}
|
||
onChange={handleChange}
|
||
placeholder="例如:产品发布活动"
|
||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
{/* 原始 URL */}
|
||
<div>
|
||
<label htmlFor="originalUrl" className="block text-sm font-medium text-gray-700">
|
||
原始 URL <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="url"
|
||
id="originalUrl"
|
||
name="originalUrl"
|
||
value={formData.originalUrl}
|
||
onChange={handleChange}
|
||
placeholder="https://example.com/your-long-url"
|
||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
{/* 自定义短链接 */}
|
||
<div>
|
||
<label htmlFor="customSlug" className="block text-sm font-medium text-gray-700">
|
||
自定义短链接 <span className="text-gray-500">(可选)</span>
|
||
</label>
|
||
<div className="flex mt-1 rounded-md shadow-sm">
|
||
<span className="inline-flex items-center px-3 py-2 text-sm text-gray-500 border border-r-0 border-gray-300 rounded-l-md bg-gray-50">
|
||
{formData.domain}/
|
||
</span>
|
||
<input
|
||
type="text"
|
||
id="customSlug"
|
||
name="customSlug"
|
||
value={formData.customSlug}
|
||
onChange={handleChange}
|
||
placeholder="custom-slug"
|
||
className="flex-1 block w-full min-w-0 px-3 py-2 border border-gray-300 rounded-none rounded-r-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||
/>
|
||
</div>
|
||
<p className="mt-1 text-xs text-gray-500">
|
||
留空将生成随机短链接
|
||
</p>
|
||
</div>
|
||
|
||
{/* 域名 */}
|
||
<div>
|
||
<label htmlFor="domain" className="block text-sm font-medium text-gray-700">
|
||
域名 <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="domain"
|
||
name="domain"
|
||
value={formData.domain}
|
||
onChange={handleChange}
|
||
placeholder="例如:googleads.link"
|
||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
{/* 团队选择 */}
|
||
<div>
|
||
<label htmlFor="teamId" className="block text-sm font-medium text-gray-700">
|
||
团队 <span className="text-red-500">*</span>
|
||
</label>
|
||
<div className="mt-1">
|
||
<TeamSelector
|
||
value={formData.teamId}
|
||
onChange={(teamId) => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
teamId: teamId as string,
|
||
// 当团队变更时清除已选项目
|
||
projectId: ''
|
||
}));
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 项目选择 */}
|
||
<div>
|
||
<label htmlFor="projectId" className="block text-sm font-medium text-gray-700">
|
||
项目 <span className="text-red-500">*</span>
|
||
</label>
|
||
<div className="mt-1">
|
||
<ProjectSelector
|
||
teamId={formData.teamId}
|
||
value={formData.projectId}
|
||
onChange={(projectId) => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
projectId: projectId as string
|
||
}));
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 描述 */}
|
||
<div>
|
||
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
|
||
描述 <span className="text-gray-500">(可选)</span>
|
||
</label>
|
||
<textarea
|
||
id="description"
|
||
name="description"
|
||
value={formData.description}
|
||
onChange={handleChange}
|
||
rows={3}
|
||
placeholder="对此链接的简短描述"
|
||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||
/>
|
||
</div>
|
||
|
||
{/* 标签 */}
|
||
<div>
|
||
<label htmlFor="tagInput" className="block text-sm font-medium text-gray-700">
|
||
标签 <span className="text-gray-500">(可选)</span>
|
||
</label>
|
||
<div className="flex mt-1 rounded-md shadow-sm">
|
||
<input
|
||
type="text"
|
||
id="tagInput"
|
||
value={tagInput}
|
||
onChange={(e) => setTagInput(e.target.value)}
|
||
onKeyDown={handleTagKeyDown}
|
||
placeholder="添加标签并按 Enter"
|
||
className="flex-1 block w-full min-w-0 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={addTag}
|
||
className="inline-flex items-center px-3 py-2 text-sm font-medium text-white border border-transparent rounded-r-md shadow-sm bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||
>
|
||
添加
|
||
</button>
|
||
</div>
|
||
|
||
{formData.tags && formData.tags.length > 0 && (
|
||
<div className="flex flex-wrap gap-2 mt-2">
|
||
{formData.tags.map(tag => (
|
||
<span key={tag} className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-blue-100 rounded-full text-blue-800">
|
||
{tag}
|
||
<button
|
||
type="button"
|
||
onClick={() => removeTag(tag)}
|
||
className="flex-shrink-0 ml-1 text-blue-500 rounded-full hover:text-blue-700 focus:outline-none"
|
||
>
|
||
<span className="sr-only">删除标签 {tag}</span>
|
||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||
</svg>
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 提交按钮 */}
|
||
<div className="flex justify-end pt-4 border-t border-gray-200">
|
||
<button
|
||
type="button"
|
||
onClick={() => router.back()}
|
||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 mr-3"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={isSubmitting}
|
||
className="inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||
>
|
||
{isSubmitting ? (
|
||
<>
|
||
<svg className="w-5 h-5 mr-2 -ml-1 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||
</svg>
|
||
处理中...
|
||
</>
|
||
) : '创建短链接'}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|