01240017
This commit is contained in:
parent
910371e9d4
commit
4600362e4d
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue