This commit is contained in:
longdayi 2025-01-24 00:17:43 +08:00
parent 910371e9d4
commit 4600362e4d
5 changed files with 280 additions and 184 deletions

View File

@ -33,8 +33,8 @@
"@nice/client": "workspace:^",
"@nice/common": "workspace:^",
"@nice/iconer": "workspace:^",
"@nice/ui": "workspace:^",
"@nice/theme": "workspace:^",
"@nice/ui": "workspace:^",
"@tanstack/query-async-storage-persister": "^5.51.9",
"@tanstack/react-query": "^5.51.21",
"@tanstack/react-query-persist-client": "^5.51.9",
@ -70,6 +70,7 @@
"superjson": "^2.2.1",
"swiper": "^11.2.1",
"tailwind-merge": "^2.6.0",
"usehooks-ts": "^3.1.0",
"uuid": "^10.0.0",
"yjs": "^13.6.20",
"zod": "^3.23.8"

View File

@ -1,20 +1,40 @@
import { SearchFilters } from "./SearchFilter";
import { useQueryClient } from '@tanstack/react-query';
export function Header() {
return (
<header className=" bg-gradient-to-r from-primary to-primary-50 p-6">
<h1 className="text-3xl font-bold text-white"></h1>
<div className="mt-4 text-blue-50">
<p className="text-base opacity-90">
</p>
<div className="mt-2 text-sm opacity-80 flex gap-6">
<span></span>
<span></span>
<span></span>
</div>
</div>
<SearchFilters className="mt-4"></SearchFilters>
</header>
);
const handleSearch = (value: string) => {
};
const handleFilterChange = () => {
};
return (
<header className="bg-gradient-to-r from-primary to-primary-50 p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-bold text-white"></h1>
<div className="mt-4 text-blue-50">
<p className="text-base opacity-90">
</p>
<div className="mt-2 text-sm opacity-80 flex flex-wrap gap-x-6 gap-y-2">
<span></span>
<span></span>
<span></span>
</div>
</div>
<SearchFilters
className="mt-4"
searchTerm=""
filterCategory={null}
filterStatus={null}
onSearchChange={handleSearch}
onCategoryChange={handleFilterChange}
onStatusChange={handleFilterChange}
/>
</div>
</header>
);
}

View File

@ -1,4 +1,6 @@
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { SearchOutlined } from '@ant-design/icons';
import { Form, Input, Select, Spin } from 'antd';
import { useEffect } from 'react';
interface SearchFiltersProps {
searchTerm: string;
@ -7,8 +9,14 @@ interface SearchFiltersProps {
onCategoryChange: (value: string) => void;
filterStatus: string;
onStatusChange: (value: string) => void;
className?: string
className?: string;
isLoading?: boolean;
}
const LoadingIndicator = () => (
<Spin size="small" className="ml-2" />
);
export function SearchFilters({
searchTerm,
onSearchChange,
@ -16,69 +24,67 @@ export function SearchFilters({
onCategoryChange,
filterStatus,
onStatusChange,
className
className,
isLoading = false
}: SearchFiltersProps) {
const [form] = Form.useForm();
// 统一处理表单初始值
const initialValues = {
search: searchTerm,
category: filterCategory,
status: filterStatus
};
useEffect(() => {
form.setFieldsValue(initialValues);
}, [searchTerm, filterCategory, filterStatus, form]);
return (
<div className={`flex flex-col sm:flex-row items-center gap-6 ${className}`}>
<div className="flex-1 w-full">
<div className="relative">
<MagnifyingGlassIcon className="h-5 w-5 text-[#041E42] absolute left-4 top-1/2 transform -translate-y-1/2" />
<input
type="text"
placeholder="Search by keyword, sender, or unit..."
className="w-full h-[46px] pl-12 pr-4 rounded-lg
bg-white shadow-sm transition-all duration-200
placeholder:text-tertiary-400
focus:outline-none focus:border-[#00308F] focus:ring-2 focus:ring-[#00308F]/20"
value={searchTerm}
<Form
form={form}
layout="vertical"
className={className}
initialValues={initialValues}
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Form.Item name="search" noStyle>
<Input
prefix={<SearchOutlined />}
placeholder="搜索关键词、发件人或单位..."
onChange={(e) => onSearchChange(e.target.value)}
allowClear
suffix={isLoading ? <LoadingIndicator /> : null}
/>
</div>
</Form.Item>
<Form.Item name="category" noStyle>
<Select
className="w-full"
onChange={onCategoryChange}
options={[
{ value: 'all', label: '所有分类' },
{ value: 'complaint', label: '投诉' },
{ value: 'suggestion', label: '建议' },
{ value: 'request', label: '请求' },
{ value: 'feedback', label: '反馈' }
]}
/>
</Form.Item>
<Form.Item name="status" noStyle>
<Select
className="w-full"
onChange={onStatusChange}
options={[
{ value: 'all', label: '所有状态' },
{ value: 'pending', label: '待处理' },
{ value: 'in-progress', label: '处理中' },
{ value: 'resolved', label: '已解决' }
]}
/>
</Form.Item>
</div>
<FilterDropdowns
filterCategory={filterCategory}
onCategoryChange={onCategoryChange}
filterStatus={filterStatus}
onStatusChange={onStatusChange}
/>
</div>
</Form>
);
}
function FilterDropdowns({
filterCategory,
onCategoryChange,
filterStatus,
onStatusChange,
}: Pick<SearchFiltersProps, 'filterCategory' | 'onCategoryChange' | 'filterStatus' | 'onStatusChange'>) {
const selectClassName = `min-w-[160px] h-[46px] px-4 rounded-lg
bg-white shadow-sm transition-all duration-200
text-[#041E42] font-medium
focus:outline-none focus:border-[#00308F] focus:ring-2 focus:ring-[#00308F]/20
hover:border-[#00308F]`;
return (
<div className="flex flex-col sm:flex-row gap-4">
<select
className={selectClassName}
value={filterCategory}
onChange={(e) => onCategoryChange(e.target.value)}
>
<option value="all">All Categories</option>
<option value="complaint">Complaints</option>
<option value="suggestion">Suggestions</option>
<option value="request">Requests</option>
<option value="feedback">Feedback</option>
</select>
<select
className={selectClassName}
value={filterStatus}
onChange={(e) => onStatusChange(e.target.value)}
>
<option value="all">All Status</option>
<option value="pending">Pending</option>
<option value="in-progress">In Progress</option>
<option value="resolved">Resolved</option>
</select>
</div>
);
}

View File

@ -1,120 +1,175 @@
import { useState } from 'react'
import { Input, Button, Card, Steps, Tag, Spin, message } from 'antd'
import { SearchOutlined, SafetyCertificateOutlined } from '@ant-design/icons'
interface FeedbackStatus {
status: 'pending' | 'in-progress' | 'resolved'
ticketId: string
submittedDate: string
lastUpdate: string
title: string
status: 'pending' | 'in-progress' | 'resolved'
ticketId: string
submittedDate: string
lastUpdate: string
title: string
}
const { Step } = Steps
export default function LetterProgressPage() {
const [feedbackId, setFeedbackId] = useState('')
const [status, setStatus] = useState<FeedbackStatus | null>(null)
const [feedbackId, setFeedbackId] = useState('')
const [status, setStatus] = useState<FeedbackStatus | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
// Mock data - In production this would come from an API
const mockLookup = () => {
setStatus({
status: 'in-progress',
ticketId: 'USAF-2025-0123',
submittedDate: '2025-01-15',
lastUpdate: '2025-01-21',
title: 'Aircraft Maintenance Schedule Inquiry'
})
const validateInput = () => {
if (!feedbackId.trim()) {
setError('请输入有效的问题编号')
return false
}
if (!/^USAF-\d{4}-\d{4}$/.test(feedbackId)) {
setError('问题编号格式不正确应为USAF-YYYY-NNNN')
return false
}
setError('')
return true
}
return (
<div className="min-h-screen bg-gradient-to-b from-slate-100 to-slate-200">
{/* Header */}
<header className="bg-[#00308F] bg-gradient-to-r from-[#00308F] to-[#0353A4] p-6 text-white">
<h1 className="text-2xl font-semibold mb-3">
USAF Feedback Progress Tracking
</h1>
<div className="text-lg opacity-90">
<p></p>
</div>
</header>
const mockLookup = () => {
if (!validateInput()) return
{/* Main Content */}
<main className="container mx-auto px-4 py-8">
{/* Search Section */}
<div className="space-y-4 mb-8">
<label
htmlFor="feedbackId"
className="block text-lg font-medium text-[#1F4E79]"
>
Enter Feedback ID
</label>
<div className="flex gap-4">
<input
id="feedbackId"
type="text"
value={feedbackId}
onChange={(e) => setFeedbackId(e.target.value)}
placeholder="e.g. USAF-2025-0123"
className="flex-1 p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-[#00538B] focus:border-transparent"
/>
<button
onClick={mockLookup}
className="px-6 py-3 bg-[#00538B] text-white rounded-md hover:bg-[#1F4E79] transition-colors duration-200"
>
Track
</button>
</div>
</div>
{/* Results Section */}
{status && (
<div className="bg-white rounded-lg p-6">
<div className="space-y-6">
<div className="flex items-center justify-between border-b pb-4">
<h2 className="text-xl font-semibold text-[#1F4E79]">
Feedback Details
</h2>
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-[#00538B]">
{status.status.toUpperCase()}
</span>
</div>
setLoading(true)
setTimeout(() => {
setStatus({
status: 'in-progress',
ticketId: feedbackId,
submittedDate: '2025-01-15',
lastUpdate: '2025-01-21',
title: 'Aircraft Maintenance Schedule Inquiry'
})
setLoading(false)
}, 1000)
}
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-tertiary-300">Ticket ID</p>
<p className="font-medium">{status.ticketId}</p>
</div>
<div>
<p className="text-sm text-tertiary-300">Submitted Date</p>
<p className="font-medium">{status.submittedDate}</p>
</div>
<div>
<p className="text-sm text-tertiary-300">Last Update</p>
<p className="font-medium">{status.lastUpdate}</p>
</div>
<div>
<p className="text-sm text-tertiary-300">Title</p>
<p className="font-medium">{status.title}</p>
</div>
</div>
const getStatusColor = (status: string) => {
switch (status) {
case 'pending':
return 'orange'
case 'in-progress':
return 'blue'
case 'resolved':
return 'green'
default:
return 'gray'
}
}
{/* Progress Timeline */}
<div className="mt-8">
<div className="relative">
<div className="absolute w-full h-1 bg-gray-200 top-4"></div>
<div className="relative flex justify-between">
{['Submitted', 'In Review', 'Resolved'].map((step, index) => (
<div key={step} className="text-center">
<div className={`w-8 h-8 mx-auto rounded-full flex items-center justify-center ${index <= 1 ? 'bg-[#00538B] text-white' : 'bg-gray-200'
}`}>
{index + 1}
</div>
<div className="mt-2 text-sm font-medium">{step}</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
)}
</main>
return (
<div className="min-h-screen bg-white" style={{ fontFamily: 'Arial, sans-serif' }}>
{/* Header */}
<header className="bg-[#003366] p-8 text-white">
<div className="container mx-auto flex items-center">
<SafetyCertificateOutlined className="text-4xl mr-4" />
<div>
<h1 className="text-3xl font-bold mb-2">USAF Feedback Tracking System</h1>
<p className="text-lg">Enter your ticket ID to track progress</p>
</div>
</div>
)
</header>
{/* Main Content */}
<main className="container mx-auto px-4 py-8">
{/* Search Section */}
<Card className="mb-8 border border-gray-200">
<div className="space-y-4">
<label className="block text-lg font-medium text-[#003366]">
Ticket ID
</label>
<div className="flex gap-4">
<Input
prefix={<SearchOutlined className="text-[#003366]" />}
size="large"
value={feedbackId}
onChange={(e) => setFeedbackId(e.target.value)}
placeholder="e.g. USAF-2025-0123"
status={error ? 'error' : ''}
className="border border-gray-300"
/>
<Button
type="primary"
size="large"
icon={<SearchOutlined />}
loading={loading}
onClick={mockLookup}
style={{ backgroundColor: '#003366', borderColor: '#003366' }}
>
Track
</Button>
</div>
{error && <p className="text-red-600 text-sm">{error}</p>}
</div>
</Card>
{/* Results Section */}
{status && (
<Card>
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between border-b pb-4">
<h2 className="text-xl font-semibold text-[#003366]">
Ticket Details
</h2>
<Tag
color={getStatusColor(status.status)}
className="font-bold uppercase"
>
{status.status}
</Tag>
</div>
{/* Details Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div>
<p className="text-sm text-gray-600">Ticket ID</p>
<p className="font-medium text-[#003366]">{status.ticketId}</p>
</div>
<div>
<p className="text-sm text-gray-600">Submitted Date</p>
<p className="font-medium text-[#003366]">{status.submittedDate}</p>
</div>
<div>
<p className="text-sm text-gray-600">Last Update</p>
<p className="font-medium text-[#003366]">{status.lastUpdate}</p>
</div>
<div>
<p className="text-sm text-gray-600">Subject</p>
<p className="font-medium text-[#003366]">{status.title}</p>
</div>
</div>
{/* Progress Timeline */}
<div className="mt-8">
<Steps
current={status.status === 'pending' ? 0 : status.status === 'in-progress' ? 1 : 2}
className="usa-progress"
>
<Step
title="Submitted"
description="Ticket received"
icon={<SafetyCertificateOutlined />}
/>
<Step
title="In Progress"
description="Under review"
icon={<SafetyCertificateOutlined />}
/>
<Step
title="Resolved"
description="Ticket completed"
icon={<SafetyCertificateOutlined />}
/>
</Steps>
</div>
</div>
</Card>
)}
</main>
</div>
)
}

View File

@ -404,6 +404,9 @@ importers:
tailwind-merge:
specifier: ^2.6.0
version: 2.6.0
usehooks-ts:
specifier: ^3.1.0
version: 3.1.0(react@18.2.0)
uuid:
specifier: ^10.0.0
version: 10.0.0
@ -6800,6 +6803,12 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
usehooks-ts@3.1.0:
resolution: {integrity: sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==}
engines: {node: '>=16.15.0'}
peerDependencies:
react: ^16.8.0 || ^17 || ^18
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -14251,6 +14260,11 @@ snapshots:
dependencies:
react: 18.2.0
usehooks-ts@3.1.0(react@18.2.0):
dependencies:
lodash.debounce: 4.0.8
react: 18.2.0
util-deprecate@1.0.2: {}
util@0.12.5: