fix: harden real backend admin workflows and ui
This commit is contained in:
@@ -1,36 +1,153 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { adminApi } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Select } from '@/components/ui';
|
||||
import { ImagePlus } from 'lucide-react';
|
||||
import { adminApi, type TenantOption } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Select, Textarea } from '@/components/ui';
|
||||
|
||||
type EnterpriseForm = {
|
||||
name: string;
|
||||
code: string;
|
||||
status: string;
|
||||
creditCode: string;
|
||||
province: string;
|
||||
city: string;
|
||||
address: string;
|
||||
contactName: string;
|
||||
contactIdCard: string;
|
||||
contactPhone: string;
|
||||
contactEmail: string;
|
||||
photoFileObjectId: string;
|
||||
photoFileName: string;
|
||||
};
|
||||
|
||||
type EnterpriseFormErrors = Partial<Record<keyof EnterpriseForm, string>>;
|
||||
|
||||
const provinceOptions = [
|
||||
{ label: '请选择省/直辖市', value: '' },
|
||||
...'北京,上海,广东,山东,河南,江苏,浙江,四川,重庆,湖北,湖南,陕西'.split(',').map((item) => ({ label: item, value: item })),
|
||||
];
|
||||
|
||||
const cityOptionsByProvince: Record<string, Array<{ label: string; value: string }>> = {
|
||||
北京: [{ label: '北京市', value: '北京市' }],
|
||||
上海: [{ label: '上海市', value: '上海市' }],
|
||||
广东: ['广州市', '深圳市', '东莞市'].map((item) => ({ label: item, value: item })),
|
||||
山东: ['济南市', '青岛市', '烟台市'].map((item) => ({ label: item, value: item })),
|
||||
河南: ['郑州市', '洛阳市', '开封市'].map((item) => ({ label: item, value: item })),
|
||||
江苏: ['南京市', '苏州市', '无锡市'].map((item) => ({ label: item, value: item })),
|
||||
浙江: ['杭州市', '宁波市', '温州市'].map((item) => ({ label: item, value: item })),
|
||||
四川: ['成都市', '绵阳市', '德阳市'].map((item) => ({ label: item, value: item })),
|
||||
重庆: [{ label: '重庆市', value: '重庆市' }],
|
||||
湖北: ['武汉市', '宜昌市', '襄阳市'].map((item) => ({ label: item, value: item })),
|
||||
湖南: ['长沙市', '株洲市', '湘潭市'].map((item) => ({ label: item, value: item })),
|
||||
陕西: ['西安市', '咸阳市', '宝鸡市'].map((item) => ({ label: item, value: item })),
|
||||
};
|
||||
|
||||
const emptyForm: EnterpriseForm = {
|
||||
name: '',
|
||||
code: '',
|
||||
status: 'active',
|
||||
creditCode: '',
|
||||
province: '',
|
||||
city: '',
|
||||
address: '',
|
||||
contactName: '',
|
||||
contactIdCard: '',
|
||||
contactPhone: '',
|
||||
contactEmail: '',
|
||||
photoFileObjectId: '',
|
||||
photoFileName: '',
|
||||
};
|
||||
|
||||
function formFromTenant(tenant: TenantOption): EnterpriseForm {
|
||||
const profile = tenant.enterpriseProfile;
|
||||
return {
|
||||
name: tenant.name,
|
||||
code: tenant.code,
|
||||
status: tenant.status,
|
||||
creditCode: profile?.creditCode ?? '',
|
||||
province: profile?.province ?? '',
|
||||
city: profile?.city ?? '',
|
||||
address: profile?.address ?? '',
|
||||
contactName: profile?.contactName ?? '',
|
||||
contactIdCard: profile?.contactIdCard ?? '',
|
||||
contactPhone: profile?.contactPhone ?? '',
|
||||
contactEmail: profile?.contactEmail ?? '',
|
||||
photoFileObjectId: profile?.photoFileObjectId ?? '',
|
||||
photoFileName: profile?.photoFileObjectId ? '已上传企业照片' : '',
|
||||
};
|
||||
}
|
||||
|
||||
export function AdminCustomerFormPage() {
|
||||
const navigate = useNavigate();
|
||||
const { enterpriseId } = useParams();
|
||||
const isEdit = Boolean(enterpriseId);
|
||||
const [name, setName] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
const [status, setStatus] = useState('active');
|
||||
const [form, setForm] = useState<EnterpriseForm>(emptyForm);
|
||||
const [errors, setErrors] = useState<EnterpriseFormErrors>({});
|
||||
const [error, setError] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enterpriseId) return;
|
||||
if (!enterpriseId) {
|
||||
setForm(emptyForm);
|
||||
return;
|
||||
}
|
||||
adminApi.getTenant(enterpriseId)
|
||||
.then((tenant) => {
|
||||
setName(tenant.name);
|
||||
setCode(tenant.code);
|
||||
setStatus(tenant.status);
|
||||
setForm(formFromTenant(tenant));
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '企业信息加载失败'));
|
||||
}, [enterpriseId]);
|
||||
|
||||
const cityOptions = useMemo(() => [
|
||||
{ label: '请选择市/区', value: '' },
|
||||
...(cityOptionsByProvince[form.province] ?? []),
|
||||
], [form.province]);
|
||||
|
||||
function updateForm<K extends keyof EnterpriseForm>(key: K, value: EnterpriseForm[K]) {
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
[key]: value,
|
||||
...(key === 'province' ? { city: '' } : {}),
|
||||
}));
|
||||
setErrors((current) => ({ ...current, [key]: undefined }));
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
const nextErrors: EnterpriseFormErrors = {};
|
||||
if (!form.name.trim()) nextErrors.name = '请填写企业名称';
|
||||
if (!form.code.trim()) nextErrors.code = '请填写企业编码';
|
||||
if (!form.creditCode.trim()) nextErrors.creditCode = '请填写统一社会信用代码';
|
||||
if (!form.contactName.trim()) nextErrors.contactName = '请填写联系人姓名';
|
||||
if (!form.contactPhone.trim()) nextErrors.contactPhone = '请填写手机号';
|
||||
setErrors(nextErrors);
|
||||
return Object.keys(nextErrors).length === 0;
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
const action = isEdit && enterpriseId
|
||||
? adminApi.updateTenant(enterpriseId, { name, code, status })
|
||||
: adminApi.createTenant({ name, code, status });
|
||||
action
|
||||
if (!validateForm()) return;
|
||||
setSaving(true);
|
||||
const { photoFileName, ...payload } = form;
|
||||
const request = isEdit && enterpriseId
|
||||
? adminApi.updateTenant(enterpriseId, payload)
|
||||
: adminApi.createTenant(payload);
|
||||
request
|
||||
.then(() => navigate('/admin/customers'))
|
||||
.catch((failure: Error) => setError(failure.message || '企业保存失败'));
|
||||
.catch((failure: Error) => setError(failure.message || '企业保存失败'))
|
||||
.finally(() => setSaving(false));
|
||||
}
|
||||
|
||||
function uploadEnterprisePhoto(file: File | undefined) {
|
||||
if (!file) return;
|
||||
setUploadingPhoto(true);
|
||||
adminApi.uploadFileObject(file, { purpose: 'enterprise_photo', prefix: 'enterprise-photos' })
|
||||
.then((fileObject) => {
|
||||
setForm((current) => ({ ...current, photoFileObjectId: fileObject.id, photoFileName: fileObject.fileName }));
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '企业照片上传失败'))
|
||||
.finally(() => setUploadingPhoto(false));
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -38,7 +155,7 @@ export function AdminCustomerFormPage() {
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<Breadcrumb items={[isEdit ? '编辑企业' : '创建企业']} />
|
||||
<p>企业档案写入真实租户表,启用后可关联企业管理员和业务数据。</p>
|
||||
<p>填写企业基本信息、证照地址和联系人信息,保存到真实企业档案。</p>
|
||||
</div>
|
||||
</div>
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
@@ -48,23 +165,87 @@ export function AdminCustomerFormPage() {
|
||||
<div className="ui-detail-section__header">
|
||||
<div>
|
||||
<h3>基本信息</h3>
|
||||
<p>企业名称、企业编码和启用状态。</p>
|
||||
<p>企业主体、证照识别和通讯地址。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-grid form-grid--two">
|
||||
<Input label="企业名称" onChange={(event) => setName(event.target.value)} placeholder="请填写企业全称" required value={name} />
|
||||
<Input label="企业编码" onChange={(event) => setCode(event.target.value)} placeholder="请填写唯一企业编码" required value={code} />
|
||||
|
||||
<div className="enterprise-upload-panel">
|
||||
<span>企业照片</span>
|
||||
<label className="enterprise-upload-button">
|
||||
<ImagePlus size={28} />
|
||||
{uploadingPhoto ? '上传中...' : form.photoFileName || '上传企业照片'}
|
||||
<input
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
disabled={uploadingPhoto}
|
||||
onChange={(event) => uploadEnterprisePhoto(event.target.files?.[0])}
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
/>
|
||||
</label>
|
||||
<p>{form.photoFileObjectId ? `文件对象:${form.photoFileObjectId}` : '支持 JPG、PNG、WebP,上传后随企业档案保存。'}</p>
|
||||
</div>
|
||||
|
||||
<div className="form-grid form-grid--two">
|
||||
<Input error={errors.name} label="企业名称" onChange={(event) => updateForm('name', event.target.value)} placeholder="请填写企业全称" required value={form.name} />
|
||||
<Input error={errors.code} label="企业编码" onChange={(event) => updateForm('code', event.target.value)} placeholder="请填写唯一企业编码" required value={form.code} />
|
||||
</div>
|
||||
<Input
|
||||
error={errors.creditCode}
|
||||
hint="修改此项将同步更新该企业档案。"
|
||||
label="统一社会信用代码"
|
||||
onChange={(event) => updateForm('creditCode', event.target.value)}
|
||||
placeholder="请填写统一社会信用代码或纳税识别号"
|
||||
required
|
||||
value={form.creditCode}
|
||||
/>
|
||||
|
||||
<div className="form-grid form-grid--two">
|
||||
<Select label="省/直辖市" onChange={(event) => updateForm('province', event.target.value)} options={provinceOptions} value={form.province} />
|
||||
<Select label="市/区" onChange={(event) => updateForm('city', event.target.value)} options={cityOptions} value={form.city} />
|
||||
</div>
|
||||
|
||||
<Textarea
|
||||
hint="通讯地址可以与营业执照上的地址不一致。"
|
||||
label="通讯地址"
|
||||
onChange={(event) => updateForm('address', event.target.value)}
|
||||
placeholder="请填写详细通讯地址"
|
||||
rows={4}
|
||||
value={form.address}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="ui-detail-section">
|
||||
<div className="ui-detail-section__header">
|
||||
<div>
|
||||
<h3>联系人信息</h3>
|
||||
<p>建议填写联系人(法人或财务主管)的真实信息。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="enterprise-info-tip">
|
||||
为方便给企业提供更好的服务,建议填写联系人(法人或财务主管)的真实信息。
|
||||
</div>
|
||||
|
||||
<div className="form-grid form-grid--two">
|
||||
<Input error={errors.contactName} label="联系人姓名" onChange={(event) => updateForm('contactName', event.target.value)} placeholder="请填写企业联系人姓名" required value={form.contactName} />
|
||||
<Input label="身份证号" onChange={(event) => updateForm('contactIdCard', event.target.value)} placeholder="请填写企业联系人身份证号" value={form.contactIdCard} />
|
||||
</div>
|
||||
|
||||
<div className="form-grid form-grid--two">
|
||||
<Input error={errors.contactPhone} label="手机号" onChange={(event) => updateForm('contactPhone', event.target.value)} placeholder="请填写企业联系人手机号" required value={form.contactPhone} />
|
||||
<Input label="电子邮箱" onChange={(event) => updateForm('contactEmail', event.target.value)} placeholder="请填写企业联系人邮箱" type="email" value={form.contactEmail} />
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label="企业状态"
|
||||
onChange={(event) => setStatus(event.target.value)}
|
||||
onChange={(event) => updateForm('status', event.target.value)}
|
||||
options={[{ label: '正常', value: 'active' }, { label: '禁用', value: 'disabled' }]}
|
||||
value={status}
|
||||
value={form.status}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div className="enterprise-form-footer">
|
||||
<Button disabled={!name || !code} onClick={submitForm}>{isEdit ? '保存企业' : '创建企业'}</Button>
|
||||
<Button disabled={saving} onClick={submitForm}>{saving ? '保存中...' : isEdit ? '保存企业' : '创建企业'}</Button>
|
||||
<Button onClick={() => navigate('/admin/customers')} variant="ghost">取消</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user