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

View File

@ -1,20 +1,40 @@
import { SearchFilters } from "./SearchFilter"; import { SearchFilters } from "./SearchFilter";
import { useQueryClient } from '@tanstack/react-query';
export function Header() { export function Header() {
const handleSearch = (value: string) => {
};
const handleFilterChange = () => {
};
return ( return (
<header className=" bg-gradient-to-r from-primary to-primary-50 p-6"> <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> <h1 className="text-3xl font-bold text-white"></h1>
<div className="mt-4 text-blue-50"> <div className="mt-4 text-blue-50">
<p className="text-base opacity-90"> <p className="text-base opacity-90">
</p> </p>
<div className="mt-2 text-sm opacity-80 flex gap-6"> <div className="mt-2 text-sm opacity-80 flex flex-wrap gap-x-6 gap-y-2">
<span></span> <span></span>
<span></span> <span></span>
<span></span> <span></span>
</div> </div>
</div> </div>
<SearchFilters className="mt-4"></SearchFilters> <SearchFilters
className="mt-4"
searchTerm=""
filterCategory={null}
filterStatus={null}
onSearchChange={handleSearch}
onCategoryChange={handleFilterChange}
onStatusChange={handleFilterChange}
/>
</div>
</header> </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 { interface SearchFiltersProps {
searchTerm: string; searchTerm: string;
@ -7,8 +9,14 @@ interface SearchFiltersProps {
onCategoryChange: (value: string) => void; onCategoryChange: (value: string) => void;
filterStatus: string; filterStatus: string;
onStatusChange: (value: string) => void; onStatusChange: (value: string) => void;
className?: string className?: string;
isLoading?: boolean;
} }
const LoadingIndicator = () => (
<Spin size="small" className="ml-2" />
);
export function SearchFilters({ export function SearchFilters({
searchTerm, searchTerm,
onSearchChange, onSearchChange,
@ -16,69 +24,67 @@ export function SearchFilters({
onCategoryChange, onCategoryChange,
filterStatus, filterStatus,
onStatusChange, onStatusChange,
className className,
isLoading = false
}: SearchFiltersProps) { }: SearchFiltersProps) {
return ( const [form] = Form.useForm();
<div className={`flex flex-col sm:flex-row items-center gap-6 ${className}`}>
<div className="flex-1 w-full"> // 统一处理表单初始值
<div className="relative"> const initialValues = {
<MagnifyingGlassIcon className="h-5 w-5 text-[#041E42] absolute left-4 top-1/2 transform -translate-y-1/2" /> search: searchTerm,
<input category: filterCategory,
type="text" status: filterStatus
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 useEffect(() => {
placeholder:text-tertiary-400 form.setFieldsValue(initialValues);
focus:outline-none focus:border-[#00308F] focus:ring-2 focus:ring-[#00308F]/20" }, [searchTerm, filterCategory, filterStatus, form]);
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
/>
</div>
</div>
<FilterDropdowns
filterCategory={filterCategory}
onCategoryChange={onCategoryChange}
filterStatus={filterStatus}
onStatusChange={onStatusChange}
/>
</div>
);
}
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 ( return (
<div className="flex flex-col sm:flex-row gap-4"> <Form
<select form={form}
className={selectClassName} layout="vertical"
value={filterCategory} className={className}
onChange={(e) => onCategoryChange(e.target.value)} initialValues={initialValues}
> >
<option value="all">All Categories</option> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<option value="complaint">Complaints</option> <Form.Item name="search" noStyle>
<option value="suggestion">Suggestions</option> <Input
<option value="request">Requests</option> prefix={<SearchOutlined />}
<option value="feedback">Feedback</option> placeholder="搜索关键词、发件人或单位..."
</select> onChange={(e) => onSearchChange(e.target.value)}
<select allowClear
className={selectClassName} suffix={isLoading ? <LoadingIndicator /> : null}
value={filterStatus} />
onChange={(e) => onStatusChange(e.target.value)} </Form.Item>
>
<option value="all">All Status</option> <Form.Item name="category" noStyle>
<option value="pending">Pending</option> <Select
<option value="in-progress">In Progress</option> className="w-full"
<option value="resolved">Resolved</option> onChange={onCategoryChange}
</select> 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> </div>
</Form>
); );
} }

View File

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

View File

@ -404,6 +404,9 @@ importers:
tailwind-merge: tailwind-merge:
specifier: ^2.6.0 specifier: ^2.6.0
version: 2.6.0 version: 2.6.0
usehooks-ts:
specifier: ^3.1.0
version: 3.1.0(react@18.2.0)
uuid: uuid:
specifier: ^10.0.0 specifier: ^10.0.0
version: 10.0.0 version: 10.0.0
@ -6800,6 +6803,12 @@ packages:
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 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: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -14251,6 +14260,11 @@ snapshots:
dependencies: dependencies:
react: 18.2.0 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-deprecate@1.0.2: {}
util@0.12.5: util@0.12.5: