<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>订单录入与审核系统</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- React & ReactDOM -->
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Babel for JSX -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<script src="https://unpkg.com/lucide-react@latest/dist/umd/lucide-react.js"></script>
<!-- Framer Motion -->
<script src="https://unpkg.com/[email protected]/dist/framer-motion.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #bbb;
}
</style>
</head>
<body class="bg-[#F3F4F6] text-[#1F2937]">
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useMemo } = React;
// Robust library access for single-file HTML environment
const getMotion = () => {
const m = (window.Motion && window.Motion.motion) || (window.framerMotion && window.framerMotion.motion);
if (m) return m;
// Fallback Proxy to prevent "invalid type" errors if library fails to load
return new Proxy(({children}) => <div>{children}</div>, {
get: (target, prop) => {
if (typeof prop !== 'string') return target[prop];
return ({children, ...props}) => {
// Filter out motion-specific props for fallback
const { initial, animate, exit, transition, ...cleanProps } = props;
return React.createElement(prop, cleanProps, children);
};
}
});
};
const getAnimatePresence = () => {
return (window.Motion && window.Motion.AnimatePresence) || (window.framerMotion && window.framerMotion.AnimatePresence) || (({children}) => <>{children}</>);
};
const getIcons = () => {
const icons = window.LucideReact || {};
return new Proxy(icons, {
get: (target, prop) => {
return target[prop] || (() => <span className="w-4 h-4 inline-block" />);
}
});
};
const motion = getMotion();
const AnimatePresence = getAnimatePresence();
const icons = getIcons();
const {
Pill,
Activity,
CheckCircle2,
XCircle,
Clock,
ArrowLeft,
Plus,
Trash2,
ChevronDown,
ChevronUp
} = icons;
// --- Constants & Master Data ---
const MEDICINE_DATABASE = [
{
name: '赛增®重组人生长激素注射液',
type: '水剂',
specifications: ['GH15IU/瓶', 'GH15IU (非重组) /瓶', 'GH2IU/瓶', 'GH30IU/瓶', 'GH30IU (非重组) /瓶']
},
{
name: '赛增®注射用人生长激素',
type: '粉剂',
specifications: ['GH10IU/瓶', 'GH12IU/瓶', 'GH2.5IU/瓶', 'GH4.5IU/瓶', 'GH4IU/瓶']
},
{
name: '金赛增®金培生长激素注射液',
type: '长效',
specifications: [
'GH27IU 2支装 西林瓶/瓶',
'GH54IU/瓶',
'GH54IU 卡式瓶 基金会/瓶',
'GH54IU 卡式瓶新/瓶',
'GH54IU (卡式瓶) 基金会/瓶',
'GH54IU (卡式瓶) /瓶'
]
}
];
const createEmptyMedicineItem = () => ({
name: '',
type: '',
specification: '',
quantity: '',
price: '',
});
const createEmptyInvoice = () => ({
id: Math.random().toString(36).substr(2, 9),
medicineModule: {
items: [createEmptyMedicineItem()],
subtotal: '',
medicalInsuranceReimbursement: '',
thirdPartyReimbursement: '',
},
testingInfo: {
amount: '',
medicalInsuranceReimbursement: '',
thirdPartyReimbursement: '',
},
outOfPocketAmount: '',
invoiceNumber: '',
invoiceDate: '',
invoiceAmount: '',
});
const MOCK_ORDER = {
id: 'ORD-20240407-001',
status: 'pending',
invoices: [createEmptyInvoice()],
auditComment: ''
};
// --- Components ---
function InputField({ label, value, onChange, placeholder, readOnly }) {
return (
<div className="space-y-1.5">
{label && <label className="text-xs font-bold text-gray-500 uppercase tracking-wider ml-1">{label}</label>}
<input
type="text"
value={value}
readOnly={readOnly}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder || (label ? `请输入${label}` : '')}
className={`w-full px-4 py-2.5 rounded-lg border border-gray-200 transition-all outline-none text-sm font-medium ${readOnly ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white focus:ring-2 focus:ring-blue-500 focus:border-transparent'}`}
/>
</div>
);
}
function SelectField({ label, value, options, onChange }) {
return (
<div className="space-y-1.5">
{label && <label className="text-xs font-bold text-gray-500 uppercase tracking-wider ml-1">{label}</label>}
<div className="relative">
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full px-4 py-2.5 rounded-lg border border-gray-200 bg-white focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all outline-none text-sm font-medium appearance-none cursor-pointer"
>
<option value="">{label ? `请选择${label}` : '请选择'}</option>
{options.map((opt, i) => (
<option key={i} value={opt}>{opt}</option>
))}
</select>
<ChevronDown className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none" />
</div>
</div>
);
}
// --- Main App ---
function App() {
const [order, setOrder] = useState(MOCK_ORDER);
const [isSubmitting, setIsSubmitting] = useState(false);
const [auditComment, setAuditComment] = useState('');
const [expandedInvoices, setExpandedInvoices] = useState([MOCK_ORDER.invoices[0].id]);
const toggleInvoice = (id) => {
setExpandedInvoices(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
);
};
const handleAudit = async (status) => {
setIsSubmitting(true);
await new Promise(resolve => setTimeout(resolve, 1000));
setOrder(prev => ({ ...prev, status, auditComment }));
setIsSubmitting(false);
};
const addInvoice = () => {
const newInvoice = createEmptyInvoice();
setOrder(prev => ({
...prev,
invoices: [...prev.invoices, newInvoice]
}));
setExpandedInvoices(prev => [...prev, newInvoice.id]);
};
const removeInvoice = (id) => {
setOrder(prev => ({
...prev,
invoices: prev.invoices.filter(inv => inv.id !== id)
}));
};
const addMedicineItem = (invoiceId) => {
setOrder(prev => ({
...prev,
invoices: prev.invoices.map(inv =>
inv.id === invoiceId
? {
...inv,
medicineModule: {
...inv.medicineModule,
items: [...inv.medicineModule.items, createEmptyMedicineItem()]
}
}
: inv
)
}));
};
const removeMedicineItem = (invoiceId, itemIdx) => {
setOrder(prev => ({
...prev,
invoices: prev.invoices.map(inv =>
inv.id === invoiceId
? {
...inv,
medicineModule: {
...inv.medicineModule,
items: inv.medicineModule.items.filter((_, i) => i !== itemIdx)
}
}
: inv
)
}));
};
const updateMedicineItem = (invoiceId, itemIdx, field, value) => {
setOrder(prev => ({
...prev,
invoices: prev.invoices.map(inv =>
inv.id === invoiceId
? {
...inv,
medicineModule: {
...inv.medicineModule,
items: inv.medicineModule.items.map((item, i) => {
if (i !== itemIdx) return item;
const updatedItem = { ...item, [field]: value };
if (field === 'name') {
const masterData = MEDICINE_DATABASE.find(m => m.name === value);
updatedItem.type = masterData ? masterData.type : '';
updatedItem.specification = '';
}
return updatedItem;
})
}
}
: inv
)
}));
};
const updateMedicineSummary = (invoiceId, field, value) => {
setOrder(prev => ({
...prev,
invoices: prev.invoices.map(inv =>
inv.id === invoiceId
? {
...inv,
medicineModule: {
...inv.medicineModule,
[field]: value
}
}
: inv
)
}));
};
const updateTesting = (invoiceId, field, value) => {
setOrder(prev => ({
...prev,
invoices: prev.invoices.map(inv =>
inv.id === invoiceId
? { ...inv, testingInfo: { ...inv.testingInfo, [field]: value } }
: inv
)
}));
};
const updateInvoiceMeta = (invoiceId, field, value) => {
setOrder(prev => ({
...prev,
invoices: prev.invoices.map(inv =>
inv.id === invoiceId ? { ...inv, [field]: value } : inv
)
}));
};
const getStatusColor = (status) => {
switch (status) {
case 'approved': return 'text-emerald-600 bg-emerald-50 border-emerald-100';
case 'rejected': return 'text-rose-600 bg-rose-50 border-rose-100';
default: return 'text-amber-600 bg-amber-50 border-amber-100';
}
};
return (
<div className="min-h-screen bg-[#F3F4F6] text-[#1F2937] pb-32">
{/* Header */}
<header className="sticky top-0 z-50 bg-white border-b border-gray-200 px-6 py-4 shadow-sm">
<div className="max-w-6xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-4">
<button className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<ArrowLeft className="w-5 h-5 text-gray-500" />
</button>
<h1 className="text-xl font-bold tracking-tight">订单录入与审核</h1>
</div>
</div>
</header>
<main className="max-w-6xl mx-auto px-6 py-8 space-y-8">
<AnimatePresence mode="popLayout">
{order.invoices.map((invoice, invIdx) => (
<motion.div
key={invoice.id}
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
className="bg-white rounded-2xl border border-gray-200 shadow-sm overflow-hidden"
>
{/* Invoice Header */}
<div
className="px-6 py-4 bg-gray-50 border-b border-gray-200 flex items-center justify-between cursor-pointer hover:bg-gray-100 transition-colors"
onClick={() => toggleInvoice(invoice.id)}
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center text-white font-bold">
{invIdx + 1}
</div>
<h2 className="font-bold text-gray-700">发票信息 #{invIdx + 1}</h2>
{invoice.invoiceNumber && (
<span className="text-sm text-gray-500 font-mono ml-2">[{invoice.invoiceNumber}]</span>
)}
</div>
<div className="flex items-center gap-4">
{order.invoices.length > 1 && (
<button
onClick={(e) => { e.stopPropagation(); removeInvoice(invoice.id); }}
className="p-2 text-gray-400 hover:text-rose-600 transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
)}
{expandedInvoices.includes(invoice.id) ? <ChevronUp className="w-5 h-5" /> : <ChevronDown className="w-5 h-5" />}
</div>
</div>
{expandedInvoices.includes(invoice.id) && (
<div className="p-6 space-y-10">
{/* Medicine Module */}
<div className="space-y-6">
<div className="flex items-center justify-between border-b border-gray-100 pb-2">
<div className="flex items-center gap-2">
<Pill className="w-5 h-5 text-emerald-600" />
<h3 className="font-bold text-lg">购药信息</h3>
</div>
<button
onClick={() => addMedicineItem(invoice.id)}
className="flex items-center gap-1.5 text-sm font-bold text-emerald-600 hover:text-emerald-700 transition-colors bg-emerald-50 px-3 py-1.5 rounded-lg border border-emerald-100"
>
<Plus className="w-4 h-4" />
新增药品
</button>
</div>
{/* Medicine Items List */}
<div className="overflow-x-auto rounded-xl border border-gray-100">
<table className="w-full text-left border-collapse">
<thead className="bg-gray-50/80 text-xs font-bold text-gray-400 uppercase tracking-widest">
<tr>
<th className="px-4 py-3 border-b border-gray-100">药品名称</th>
<th className="px-4 py-3 border-b border-gray-100">药品类型</th>
<th className="px-4 py-3 border-b border-gray-100">药品规格</th>
<th className="px-4 py-3 border-b border-gray-100 w-24">数量</th>
<th className="px-4 py-3 border-b border-gray-100 w-32">金额</th>
<th className="px-4 py-3 border-b border-gray-100 w-12"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{invoice.medicineModule.items.map((item, itemIdx) => (
<tr key={itemIdx} className="hover:bg-gray-50/30 transition-colors">
<td className="px-3 py-3">
<SelectField
label=""
value={item.name}
options={MEDICINE_DATABASE.map(m => m.name)}
onChange={(v) => updateMedicineItem(invoice.id, itemIdx, 'name', v)}
/>
</td>
<td className="px-3 py-3">
<InputField
label=""
value={item.type}
readOnly
placeholder="自动显示"
onChange={() => {}}
/>
</td>
<td className="px-3 py-3">
<SelectField
label=""
value={item.specification}
options={MEDICINE_DATABASE.find(m => m.name === item.name)?.specifications || []}
onChange={(v) => updateMedicineItem(invoice.id, itemIdx, 'specification', v)}
/>
</td>
<td className="px-3 py-3">
<InputField label="" value={item.quantity} onChange={(v) => updateMedicineItem(invoice.id, itemIdx, 'quantity', v)} />
</td>
<td className="px-3 py-3">
<InputField label="" value={item.price} onChange={(v) => updateMedicineItem(invoice.id, itemIdx, 'price', v)} />
</td>
<td className="px-3 py-3 text-right">
{invoice.medicineModule.items.length > 1 && (
<button
onClick={() => removeMedicineItem(invoice.id, itemIdx)}
className="p-1.5 text-gray-300 hover:text-rose-500 transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Medicine Summary Fields */}
<div className="bg-emerald-50/30 p-6 rounded-xl border border-emerald-100">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<InputField label="药品金额小计" value={invoice.medicineModule.subtotal} onChange={(v) => updateMedicineSummary(invoice.id, 'subtotal', v)} />
<InputField label="药品医保报销金额" value={invoice.medicineModule.medicalInsuranceReimbursement} onChange={(v) => updateMedicineSummary(invoice.id, 'medicalInsuranceReimbursement', v)} />
<InputField label="药品第三方报销金额" value={invoice.medicineModule.thirdPartyReimbursement} onChange={(v) => updateMedicineSummary(invoice.id, 'thirdPartyReimbursement', v)} />
</div>
</div>
</div>
{/* Testing Info */}
<div className="space-y-6">
<div className="flex items-center gap-2 border-b border-gray-100 pb-2">
<Activity className="w-5 h-5 text-indigo-600" />
<h3 className="font-bold text-lg">检测信息</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<InputField label="检测金额" value={invoice.testingInfo.amount} onChange={(v) => updateTesting(invoice.id, 'amount', v)} />
<InputField label="检测医保报销金额" value={invoice.testingInfo.medicalInsuranceReimbursement} onChange={(v) => updateTesting(invoice.id, 'medicalInsuranceReimbursement', v)} />
<InputField label="检测第三方报销金额" value={invoice.testingInfo.thirdPartyReimbursement} onChange={(v) => updateTesting(invoice.id, 'thirdPartyReimbursement', v)} />
</div>
</div>
{/* Invoice Meta */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 pt-6 border-t border-gray-100">
<InputField label="自付自费金额" value={invoice.outOfPocketAmount} onChange={(v) => updateInvoiceMeta(invoice.id, 'outOfPocketAmount', v)} placeholder="请输入自付金额" />
<InputField label="发票号" value={invoice.invoiceNumber} onChange={(v) => updateInvoiceMeta(invoice.id, 'invoiceNumber', v)} placeholder="请输入发票号码" />
<InputField label="开票日期" value={invoice.invoiceDate} onChange={(v) => updateInvoiceMeta(invoice.id, 'invoiceDate', v)} placeholder="YYYY-MM-DD" />
<InputField label="发票金额" value={invoice.invoiceAmount} onChange={(v) => updateInvoiceMeta(invoice.id, 'invoiceAmount', v)} placeholder="请输入总金额" />
</div>
</div>
)}
</motion.div>
))}
</AnimatePresence>
{/* Add Invoice Button */}
<button
onClick={addInvoice}
className="w-full py-6 border-2 border-dashed border-gray-300 rounded-2xl text-gray-500 font-bold hover:border-blue-400 hover:text-blue-500 hover:bg-blue-50 transition-all flex items-center justify-center gap-2"
>
<Plus className="w-6 h-6" />
新增一张发票
</button>
{/* Audit Comment */}
<div className="bg-white rounded-2xl border border-gray-200 p-8 space-y-4 shadow-sm">
<h3 className="font-bold text-lg flex items-center gap-2">
<Clock className="w-5 h-5 text-blue-500" />
审核意见
</h3>
<textarea
className="w-full min-h-[120px] p-4 rounded-xl border border-gray-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all outline-none resize-none bg-gray-50/50"
placeholder="请输入审核备注..."
value={auditComment}
onChange={(e) => setAuditComment(e.target.value)}
/>
</div>
</main>
{/* Bottom Action Bar */}
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>
index.html
md
README.md
index.html