Your Name f02b3f4098 feat: 前端对接 Profile/Messages/Settings 页面 API
- 3 消息页 + 2 资料编辑页 + 3 设置页 + 2 资料展示页
- api.ts 新增 Profile/Messages/ChangePassword 等类型和方法
- SSEContext 事件映射修复 + 断线重连修复
- 剩余页面加 USE_MOCK 双模式,52/55 页面已完成

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 10:27:59 +08:00

685 lines
27 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { USE_MOCK } from '@/contexts/AuthContext'
import { api } from '@/lib/api'
import { useToast } from '@/components/ui/Toast'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import {
ArrowLeft,
Building2,
Phone,
Mail,
Search,
Loader2,
X,
Check,
CreditCard,
ShieldCheck,
AlertCircle,
ChevronRight
} from 'lucide-react'
// 模拟企业查询结果
interface CompanySearchResult {
companyName: string
shortName: string
businessLicense: string
legalPerson: string
registeredCapital: string
establishDate: string
businessScope: string
address: string
status: string
}
// 模拟企业数据库
const mockCompanyDatabase: CompanySearchResult[] = [
{
companyName: '上海星辰文化传媒有限公司',
shortName: '星辰传媒',
businessLicense: '91310000MA1FL8XXXX',
legalPerson: '张三',
registeredCapital: '500万人民币',
establishDate: '2020-03-15',
businessScope: '文化传媒、广告设计、互联网信息服务',
address: '上海市浦东新区张江高科技园区xxx路xxx号',
status: '在营',
},
{
companyName: '北京蓝海数字科技有限公司',
shortName: '蓝海科技',
businessLicense: '91110000MA2BK9YYYY',
legalPerson: '李四',
registeredCapital: '1000万人民币',
establishDate: '2018-06-20',
businessScope: '软件开发、信息技术服务、数据处理',
address: '北京市海淀区中关村科技园xxx号',
status: '在营',
},
{
companyName: '杭州云创网络技术有限公司',
shortName: '云创网络',
businessLicense: '91330000MA3CL7ZZZZ',
legalPerson: '王五',
registeredCapital: '300万人民币',
establishDate: '2021-01-10',
businessScope: '网络技术开发、电子商务、广告服务',
address: '浙江省杭州市西湖区xxx路xxx号',
status: '在营',
},
{
companyName: '深圳创意无限广告有限公司',
shortName: '创意无限',
businessLicense: '91440000MA4DM8AAAA',
legalPerson: '赵六',
registeredCapital: '200万人民币',
establishDate: '2019-09-05',
businessScope: '广告设计、品牌策划、市场营销',
address: '广东省深圳市南山区xxx路xxx号',
status: '在营',
},
{
companyName: '成都天府传媒集团有限公司',
shortName: '天府传媒',
businessLicense: '91510000MA5EN9BBBB',
legalPerson: '钱七',
registeredCapital: '2000万人民币',
establishDate: '2015-12-01',
businessScope: '广播电视节目制作、影视策划、新媒体运营',
address: '四川省成都市高新区xxx路xxx号',
status: '在营',
},
]
// 认证状态
type VerifyStatus = 'unverified' | 'pending' | 'verified'
// 认证方式
type VerifyMethod = 'bank' | 'legalPerson'
// 当前公司数据
const mockCurrentCompany = {
companyName: '上海星辰文化传媒有限公司',
shortName: '星辰传媒',
businessLicense: '91310000MA1FL8XXXX',
legalPerson: '张三',
registeredCapital: '500万人民币',
establishDate: '2020-03-15',
businessScope: '文化传媒、广告设计、互联网信息服务',
address: '上海市浦东新区张江高科技园区xxx路xxx号',
contactPhone: '021-12345678',
contactEmail: 'contact@starmedia.com',
verifyStatus: 'verified' as VerifyStatus,
// 对公账户信息(验证通过后显示)
bankInfo: {
bankName: '中国工商银行上海浦东支行',
accountNumber: '6222****1234',
},
}
export default function AgencyCompanyPage() {
const router = useRouter()
const toast = useToast()
const [isEditing, setIsEditing] = useState(false)
const [formData, setFormData] = useState(mockCurrentCompany)
const [isSaving, setIsSaving] = useState(false)
// 企业查询相关状态
const [showSearch, setShowSearch] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [isSearching, setIsSearching] = useState(false)
const [searchResults, setSearchResults] = useState<CompanySearchResult[]>([])
// 认证相关状态
const [showVerifyModal, setShowVerifyModal] = useState(false)
const [verifyMethod, setVerifyMethod] = useState<VerifyMethod | null>(null)
const [verifyStep, setVerifyStep] = useState(1)
const [verifyCode, setVerifyCode] = useState('')
const [isVerifying, setIsVerifying] = useState(false)
// 搜索企业
const handleSearch = async () => {
if (!searchQuery.trim()) return
setIsSearching(true)
await new Promise(resolve => setTimeout(resolve, 800))
const results = mockCompanyDatabase.filter(company =>
company.companyName.includes(searchQuery) ||
company.shortName.includes(searchQuery) ||
company.businessLicense.includes(searchQuery)
)
setSearchResults(results)
setIsSearching(false)
}
// 选择企业
const handleSelectCompany = (company: CompanySearchResult) => {
setFormData({
...formData,
companyName: company.companyName,
shortName: company.shortName,
businessLicense: company.businessLicense,
legalPerson: company.legalPerson,
registeredCapital: company.registeredCapital,
establishDate: company.establishDate,
businessScope: company.businessScope,
address: company.address,
verifyStatus: 'unverified', // 选择新公司后需要重新验证
})
setShowSearch(false)
setSearchQuery('')
setSearchResults([])
}
// 开始验证
const handleStartVerify = (method: VerifyMethod) => {
setVerifyMethod(method)
setVerifyStep(2)
}
// 提交验证
const handleSubmitVerify = async () => {
if (!verifyCode.trim()) {
toast.error('请输入验证信息')
return
}
setIsVerifying(true)
await new Promise(resolve => setTimeout(resolve, 1500))
// 模拟验证成功
setFormData({ ...formData, verifyStatus: 'verified' })
setIsVerifying(false)
setShowVerifyModal(false)
setVerifyMethod(null)
setVerifyStep(1)
setVerifyCode('')
toast.success('企业认证成功')
}
const handleSave = async () => {
setIsSaving(true)
if (USE_MOCK) {
// Mock 模式:模拟保存延迟
await new Promise(resolve => setTimeout(resolve, 1000))
} else {
// TODO: 后端企业信息保存 API 待实现,暂时使用 mock 行为
await new Promise(resolve => setTimeout(resolve, 1000))
}
setIsSaving(false)
setIsEditing(false)
toast.success('公司信息已保存')
}
// 认证状态显示
const renderVerifyStatus = () => {
switch (formData.verifyStatus) {
case 'verified':
return (
<div className="flex items-center gap-3 p-4 rounded-xl bg-accent-green/10">
<div className="w-10 h-10 rounded-full bg-accent-green/20 flex items-center justify-center">
<ShieldCheck size={20} className="text-accent-green" />
</div>
<div>
<p className="font-medium text-accent-green"></p>
<p className="text-sm text-text-secondary"></p>
</div>
</div>
)
case 'pending':
return (
<div className="flex items-center gap-3 p-4 rounded-xl bg-accent-amber/10">
<div className="w-10 h-10 rounded-full bg-accent-amber/20 flex items-center justify-center">
<Loader2 size={20} className="text-accent-amber animate-spin" />
</div>
<div>
<p className="font-medium text-accent-amber"></p>
<p className="text-sm text-text-secondary">...</p>
</div>
</div>
)
default:
return (
<div className="space-y-4">
<div className="flex items-center gap-3 p-4 rounded-xl bg-accent-coral/10">
<div className="w-10 h-10 rounded-full bg-accent-coral/20 flex items-center justify-center">
<AlertCircle size={20} className="text-accent-coral" />
</div>
<div className="flex-1">
<p className="font-medium text-accent-coral"></p>
<p className="text-sm text-text-secondary">使</p>
</div>
</div>
<Button variant="primary" className="w-full" onClick={() => setShowVerifyModal(true)}>
<ShieldCheck size={16} />
</Button>
</div>
)
}
}
return (
<div className="space-y-6">
{/* 顶部导航 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
type="button"
onClick={() => router.back()}
className="p-2 rounded-lg hover:bg-bg-elevated transition-colors"
>
<ArrowLeft size={20} className="text-text-secondary" />
</button>
<div>
<h1 className="text-2xl font-bold text-text-primary"></h1>
<p className="text-sm text-text-secondary mt-0.5"></p>
</div>
</div>
{!isEditing ? (
<Button variant="primary" onClick={() => setIsEditing(true)}>
</Button>
) : (
<div className="flex gap-3">
<Button variant="secondary" onClick={() => { setIsEditing(false); setShowSearch(false) }}>
</Button>
<Button variant="primary" onClick={handleSave} disabled={isSaving}>
{isSaving ? '保存中...' : '保存'}
</Button>
</div>
)}
</div>
{/* 企业查询面板 */}
{showSearch && (
<Card className="border-2 border-accent-indigo">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Search size={18} className="text-accent-indigo" />
</span>
<button
type="button"
onClick={() => { setShowSearch(false); setSearchResults([]) }}
className="p-1 rounded hover:bg-bg-elevated transition-colors"
>
<X size={18} className="text-text-tertiary" />
</button>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-text-secondary">
</p>
<div className="flex gap-3">
<div className="flex-1">
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="请输入公司名称或统一社会信用代码"
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<Button variant="primary" onClick={handleSearch} disabled={isSearching || !searchQuery.trim()}>
{isSearching ? (
<>
<Loader2 size={16} className="animate-spin" />
</>
) : (
<>
<Search size={16} />
</>
)}
</Button>
</div>
{searchResults.length > 0 && (
<div className="space-y-2 max-h-64 overflow-y-auto">
<p className="text-sm text-text-tertiary"> {searchResults.length} </p>
{searchResults.map((company) => (
<button
key={company.businessLicense}
type="button"
onClick={() => handleSelectCompany(company)}
className="w-full p-4 rounded-xl bg-bg-elevated hover:bg-bg-page border border-transparent hover:border-accent-indigo transition-all text-left"
>
<div className="flex items-start justify-between">
<div>
<p className="font-medium text-text-primary">{company.companyName}</p>
<p className="text-sm text-text-secondary mt-1">
: {company.legalPerson} · {company.registeredCapital}
</p>
<p className="text-xs text-text-tertiary mt-1 font-mono">
{company.businessLicense}
</p>
</div>
<span className="px-2 py-1 text-xs rounded bg-accent-green/15 text-accent-green">
{company.status}
</span>
</div>
</button>
))}
</div>
)}
{searchResults.length === 0 && searchQuery && !isSearching && (
<div className="text-center py-8 text-text-tertiary">
<Building2 size={32} className="mx-auto mb-2 opacity-50" />
<p></p>
</div>
)}
</CardContent>
</Card>
)}
{/* 认证弹窗 */}
{showVerifyModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<Card className="w-full max-w-lg mx-4">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<ShieldCheck size={18} className="text-accent-indigo" />
</span>
<button
type="button"
onClick={() => { setShowVerifyModal(false); setVerifyMethod(null); setVerifyStep(1); setVerifyCode('') }}
className="p-1 rounded hover:bg-bg-elevated transition-colors"
>
<X size={18} className="text-text-tertiary" />
</button>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{verifyStep === 1 ? (
<>
<p className="text-sm text-text-secondary">
<span className="text-text-primary font-medium">{formData.companyName}</span>
</p>
{/* 对公打款验证 */}
<button
type="button"
onClick={() => handleStartVerify('bank')}
className="w-full p-4 rounded-xl bg-bg-elevated hover:bg-bg-page border border-transparent hover:border-accent-indigo transition-all text-left"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-accent-indigo/15 flex items-center justify-center">
<CreditCard size={24} className="text-accent-indigo" />
</div>
<div>
<p className="font-medium text-text-primary"></p>
<p className="text-sm text-text-tertiary mt-0.5"></p>
</div>
</div>
<ChevronRight size={20} className="text-text-tertiary" />
</div>
</button>
{/* 法人手机验证 */}
<button
type="button"
onClick={() => handleStartVerify('legalPerson')}
className="w-full p-4 rounded-xl bg-bg-elevated hover:bg-bg-page border border-transparent hover:border-accent-indigo transition-all text-left"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-accent-green/15 flex items-center justify-center">
<Phone size={24} className="text-accent-green" />
</div>
<div>
<p className="font-medium text-text-primary"></p>
<p className="text-sm text-text-tertiary mt-0.5"></p>
</div>
</div>
<ChevronRight size={20} className="text-text-tertiary" />
</div>
</button>
<p className="text-xs text-text-tertiary text-center pt-2">
</p>
</>
) : (
<>
{verifyMethod === 'bank' ? (
<div className="space-y-4">
<div className="p-4 rounded-xl bg-accent-indigo/10 border border-accent-indigo/20">
<p className="text-sm text-accent-indigo font-medium mb-2"></p>
<p className="text-sm text-text-secondary">
<span className="text-text-primary font-medium">{formData.companyName}</span> 0.01-0.99
</p>
</div>
<div className="space-y-2">
<p className="text-sm text-text-secondary"></p>
<Input
value={verifyCode}
onChange={(e) => setVerifyCode(e.target.value)}
placeholder="例如: 0.23"
className="text-center text-lg font-mono"
/>
<p className="text-xs text-text-tertiary">1-3</p>
</div>
</div>
) : (
<div className="space-y-4">
<div className="p-4 rounded-xl bg-accent-green/10 border border-accent-green/20">
<p className="text-sm text-accent-green font-medium mb-2"></p>
<p className="text-sm text-text-secondary">
<span className="text-text-primary font-medium">{formData.legalPerson}</span>
</p>
</div>
<div className="space-y-2">
<p className="text-sm text-text-secondary">6</p>
<Input
value={verifyCode}
onChange={(e) => setVerifyCode(e.target.value)}
placeholder="请输入验证码"
maxLength={6}
className="text-center text-lg font-mono tracking-widest"
/>
<p className="text-xs text-text-tertiary">5</p>
</div>
</div>
)}
<div className="flex gap-3 pt-2">
<Button variant="secondary" className="flex-1" onClick={() => { setVerifyStep(1); setVerifyCode('') }}>
</Button>
<Button variant="primary" className="flex-1" onClick={handleSubmitVerify} disabled={isVerifying || !verifyCode.trim()}>
{isVerifying ? (
<>
<Loader2 size={16} className="animate-spin" />
</>
) : (
<>
<Check size={16} />
</>
)}
</Button>
</div>
</>
)}
</CardContent>
</Card>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 左侧:基本信息 */}
<div className="lg:col-span-2 space-y-6">
{/* 工商信息 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Building2 size={18} className="text-accent-indigo" />
</span>
{isEditing && !showSearch && (
<Button variant="secondary" size="sm" onClick={() => setShowSearch(true)}>
<Search size={14} />
</Button>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-sm text-text-secondary mb-1.5 block"></label>
{isEditing ? (
<Input
value={formData.companyName}
onChange={(e) => setFormData({ ...formData, companyName: e.target.value })}
/>
) : (
<p className="text-text-primary font-medium">{formData.companyName}</p>
)}
</div>
<div>
<label className="text-sm text-text-secondary mb-1.5 block"></label>
{isEditing ? (
<Input
value={formData.shortName}
onChange={(e) => setFormData({ ...formData, shortName: e.target.value })}
/>
) : (
<p className="text-text-primary font-medium">{formData.shortName}</p>
)}
</div>
<div>
<label className="text-sm text-text-secondary mb-1.5 block"></label>
<p className="text-text-primary font-mono">{formData.businessLicense}</p>
</div>
<div>
<label className="text-sm text-text-secondary mb-1.5 block"></label>
<p className="text-text-primary">{formData.legalPerson}</p>
</div>
<div>
<label className="text-sm text-text-secondary mb-1.5 block"></label>
<p className="text-text-primary">{formData.registeredCapital}</p>
</div>
<div>
<label className="text-sm text-text-secondary mb-1.5 block"></label>
<p className="text-text-primary">{formData.establishDate}</p>
</div>
</div>
<div>
<label className="text-sm text-text-secondary mb-1.5 block"></label>
<p className="text-text-primary">{formData.businessScope}</p>
</div>
<div>
<label className="text-sm text-text-secondary mb-1.5 block"></label>
<p className="text-text-primary">{formData.address}</p>
</div>
{isEditing && (
<div className="p-3 rounded-lg bg-accent-indigo/10 border border-accent-indigo/20">
<p className="text-sm text-accent-indigo">
💡 &ldquo;&rdquo;
</p>
</div>
)}
</CardContent>
</Card>
{/* 联系信息 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Phone size={18} className="text-accent-green" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="text-sm text-text-secondary mb-1.5 block flex items-center gap-1">
<Phone size={14} />
</label>
{isEditing ? (
<Input
value={formData.contactPhone}
onChange={(e) => setFormData({ ...formData, contactPhone: e.target.value })}
/>
) : (
<p className="text-text-primary">{formData.contactPhone}</p>
)}
</div>
<div>
<label className="text-sm text-text-secondary mb-1.5 block flex items-center gap-1">
<Mail size={14} />
</label>
{isEditing ? (
<Input
value={formData.contactEmail}
onChange={(e) => setFormData({ ...formData, contactEmail: e.target.value })}
/>
) : (
<p className="text-text-primary">{formData.contactEmail}</p>
)}
</div>
</div>
</CardContent>
</Card>
</div>
{/* 右侧:认证状态 */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
{renderVerifyStatus()}
</CardContent>
</Card>
{/* 已认证显示验证信息 */}
{formData.verifyStatus === 'verified' && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CreditCard size={18} className="text-accent-indigo" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div>
<label className="text-sm text-text-tertiary"></label>
<p className="text-text-primary">{formData.bankInfo?.bankName}</p>
</div>
<div>
<label className="text-sm text-text-tertiary"></label>
<p className="text-text-primary font-mono">{formData.bankInfo?.accountNumber}</p>
</div>
</div>
</CardContent>
</Card>
)}
</div>
</div>
</div>
)
}