313 lines
13 KiB
Vue
313 lines
13 KiB
Vue
<template>
|
||
<div class="production-diagnosis h-full flex flex-col p-4 box-border gap-4 overflow-y-auto">
|
||
<!-- 输入区 -->
|
||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 flex flex-col gap-3">
|
||
<div class="flex items-center justify-between">
|
||
<h2 class="text-sm lg:text-base font-semibold text-gray-900 flex items-center">
|
||
<svg class="w-4 h-4 text-blue-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||
</svg>
|
||
进产诊断
|
||
</h2>
|
||
<span class="text-xs text-gray-500">输入病例 / 业务单据 / 销售单据编号,查找无法进产的原因</span>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-[160px_1fr_auto] gap-3">
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">单据类型</label>
|
||
<select
|
||
v-model="type"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm bg-white"
|
||
>
|
||
<option v-for="opt in typeOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-600 mb-1">单据编号</label>
|
||
<input
|
||
v-model.trim="code"
|
||
type="text"
|
||
:placeholder="currentPlaceholder"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm font-mono"
|
||
@keyup.enter="diagnose"
|
||
/>
|
||
</div>
|
||
<div class="flex items-end gap-2">
|
||
<button
|
||
type="button"
|
||
@click="clearAll"
|
||
class="px-3 py-2 text-xs text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||
>
|
||
清空
|
||
</button>
|
||
<button
|
||
type="button"
|
||
@click="diagnose"
|
||
:disabled="loading || !code"
|
||
class="px-3 py-2 text-xs text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-60"
|
||
>
|
||
<span v-if="loading">诊断中...</span>
|
||
<span v-else>开始诊断</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="text-xs text-gray-500">
|
||
进产逻辑参考 agent-be:
|
||
<code class="font-mono">AgentCase\\ConfirmProduction::canProduction</code> /
|
||
<code class="font-mono">AgentBusinessDocument\\ConfirmProduction::canProduction</code> /
|
||
<code class="font-mono">AgentSaleDocument\\ConfirmPermit::canPermit</code>
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 错误 -->
|
||
<div v-if="errorMessage" class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700">
|
||
{{ errorMessage }}
|
||
</div>
|
||
|
||
<!-- 结果 -->
|
||
<div v-if="result" class="flex flex-col gap-4">
|
||
<!-- 概览 -->
|
||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
||
<div class="flex items-center justify-between flex-wrap gap-2">
|
||
<div class="flex items-center gap-3">
|
||
<span
|
||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border"
|
||
:class="overallBadgeClass"
|
||
>
|
||
{{ overallBadge }}
|
||
</span>
|
||
<span class="text-sm font-semibold text-gray-900">
|
||
{{ result.type_label }} · {{ result.code }}
|
||
</span>
|
||
</div>
|
||
<span v-if="result.found && result.entity" class="text-xs text-gray-500">
|
||
状态: {{ result.entity.status_label }} ({{ result.entity.status }})
|
||
<span class="mx-2 text-gray-300">|</span>
|
||
归属代理: {{ result.entity.agent_code || '-' }}
|
||
<span class="mx-2 text-gray-300">|</span>
|
||
产品: {{ result.entity.product_code || '-' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div v-if="!result.found" class="mt-3 text-sm text-gray-600">
|
||
{{ result.message }}
|
||
</div>
|
||
|
||
<div v-else-if="result.entity" class="mt-3 grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
|
||
<div v-for="field in entityFields" :key="field.key" class="bg-gray-50 rounded-lg p-2">
|
||
<div class="text-gray-500">{{ field.label }}</div>
|
||
<div class="font-mono text-gray-900 break-all">{{ field.value || '-' }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 检查项列表 -->
|
||
<div v-if="result.found && result.checks" class="space-y-3">
|
||
<div
|
||
v-for="check in result.checks"
|
||
:key="check.key"
|
||
class="bg-white rounded-xl shadow-sm border p-4"
|
||
:class="check.pass ? 'border-emerald-200' : 'border-red-200'"
|
||
>
|
||
<div class="flex items-start justify-between gap-3">
|
||
<div class="flex items-start gap-3 min-w-0">
|
||
<span
|
||
class="mt-0.5 inline-flex items-center justify-center w-6 h-6 rounded-full flex-shrink-0 text-xs font-bold"
|
||
:class="check.pass ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'"
|
||
>
|
||
{{ check.pass ? '✓' : '✗' }}
|
||
</span>
|
||
<div class="min-w-0">
|
||
<div class="text-sm font-semibold text-gray-900">{{ check.label }}</div>
|
||
<div class="text-xs text-gray-600 mt-1">{{ check.detail }}</div>
|
||
</div>
|
||
</div>
|
||
<span
|
||
class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium flex-shrink-0"
|
||
:class="check.pass ? 'bg-emerald-50 text-emerald-700' : 'bg-red-50 text-red-700'"
|
||
>
|
||
{{ check.pass ? '通过' : '未通过' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="mt-3 grid grid-cols-1 md:grid-cols-2 gap-2 text-xs">
|
||
<div class="bg-gray-50 rounded p-2">
|
||
<div class="text-gray-500">期望</div>
|
||
<div class="font-mono text-gray-900 break-all">{{ check.expected }}</div>
|
||
</div>
|
||
<div class="bg-gray-50 rounded p-2">
|
||
<div class="text-gray-500">实际</div>
|
||
<div class="font-mono text-gray-900 break-all">{{ check.actual }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="!check.pass && check.hint" class="mt-2 text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded p-2">
|
||
提示:{{ check.hint }}
|
||
</div>
|
||
|
||
<!-- 账期链路展示 -->
|
||
<div v-if="check.key === 'credit' && check.chain && check.chain.length" class="mt-3">
|
||
<div class="text-xs text-gray-500 mb-1">
|
||
代理账期链路 (产品 {{ check.product_code || '-' }})
|
||
<span v-if="!check.crm_configured" class="ml-2 text-amber-700">
|
||
· CRM 未配置,一级账期视为未知
|
||
</span>
|
||
<span v-else-if="!check.first_agent_credit_resolved" class="ml-2 text-amber-700">
|
||
· CRM 调用失败
|
||
</span>
|
||
</div>
|
||
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||
<table class="w-full text-xs">
|
||
<thead class="bg-gray-50 text-gray-600">
|
||
<tr>
|
||
<th class="px-2 py-1 text-left font-medium">层级</th>
|
||
<th class="px-2 py-1 text-left font-medium">代理编号</th>
|
||
<th class="px-2 py-1 text-left font-medium">代理名称</th>
|
||
<th class="px-2 py-1 text-left font-medium">是否有账期</th>
|
||
<th class="px-2 py-1 text-left font-medium">来源</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr
|
||
v-for="(node, idx) in check.chain"
|
||
:key="idx"
|
||
:class="[
|
||
node.broken ? 'bg-gray-50 text-gray-400' : '',
|
||
idx === check.chain.length - 1 ? 'border-t border-gray-100' : 'border-t border-gray-100'
|
||
]"
|
||
>
|
||
<td class="px-2 py-1 font-mono">
|
||
{{ node.is_root ? '一级' : (node.level != null ? node.level + '级' : '-') }}
|
||
</td>
|
||
<td class="px-2 py-1 font-mono">{{ node.agent_code }}</td>
|
||
<td class="px-2 py-1">{{ node.agent_name || '-' }}</td>
|
||
<td class="px-2 py-1">
|
||
<span
|
||
class="inline-flex items-center px-1.5 py-0.5 rounded text-[11px]"
|
||
:class="node.has_credit ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'"
|
||
>
|
||
{{ node.has_credit ? '有' : '无' }}
|
||
</span>
|
||
<span v-if="node.broken" class="ml-2 text-gray-400">(链路已中断)</span>
|
||
</td>
|
||
<td class="px-2 py-1 font-mono text-gray-500">{{ node.credit_source }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'ProductionDiagnosis',
|
||
data() {
|
||
return {
|
||
type: 'case',
|
||
code: '',
|
||
loading: false,
|
||
errorMessage: '',
|
||
result: null,
|
||
typeOptions: [
|
||
{ value: 'case', label: '病例', placeholder: '示例: C01008446046 / X0X60F' },
|
||
{ value: 'business_document', label: '业务单据', placeholder: '示例: B12345678 / LX5KP3' },
|
||
{ value: 'sale_document', label: '销售单据', placeholder: '示例: 销售单据 code' }
|
||
]
|
||
};
|
||
},
|
||
computed: {
|
||
currentPlaceholder() {
|
||
const opt = this.typeOptions.find((o) => o.value === this.type);
|
||
return opt ? opt.placeholder : '';
|
||
},
|
||
overallBadge() {
|
||
if (!this.result) return '';
|
||
if (!this.result.found) return '未找到';
|
||
return this.result.can_production ? '可以进产' : '不能进产';
|
||
},
|
||
overallBadgeClass() {
|
||
if (!this.result) return '';
|
||
if (!this.result.found) return 'bg-gray-50 text-gray-600 border-gray-200';
|
||
return this.result.can_production
|
||
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
||
: 'bg-red-50 text-red-700 border-red-200';
|
||
},
|
||
entityFields() {
|
||
if (!this.result || !this.result.entity) return [];
|
||
const e = this.result.entity;
|
||
const base = [
|
||
{ key: 'code', label: '编号', value: e.code },
|
||
{ key: 'agent_code', label: '归属代理 (agent_code)', value: e.agent_code },
|
||
{ key: 'settlement_agent_code', label: '结算代理', value: e.settlement_agent_code },
|
||
{ key: 'product_code', label: '产品编号', value: e.product_code }
|
||
];
|
||
if (this.result.type === 'case') {
|
||
base.push(
|
||
{ key: 'hospital_code', label: '机构编号', value: e.hospital_code },
|
||
{ key: 'doctor_code', label: '医生编号', value: e.doctor_code },
|
||
{ key: 'patient_name', label: '患者姓名', value: e.patient_name },
|
||
{ key: 'is_need_pfp', label: 'is_need_pfp', value: e.is_need_pfp },
|
||
{ key: 'is_pfp', label: 'is_pfp', value: e.is_pfp }
|
||
);
|
||
} else {
|
||
base.push({ key: 'hospital_code', label: '机构编号', value: e.hospital_code });
|
||
}
|
||
return base;
|
||
}
|
||
},
|
||
methods: {
|
||
clearAll() {
|
||
this.code = '';
|
||
this.result = null;
|
||
this.errorMessage = '';
|
||
},
|
||
async diagnose() {
|
||
if (!this.code) {
|
||
this.errorMessage = '请输入单据编号';
|
||
return;
|
||
}
|
||
this.loading = true;
|
||
this.errorMessage = '';
|
||
this.result = null;
|
||
|
||
try {
|
||
const csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
||
const response = await fetch('/api/production-diagnosis/diagnose', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
Accept: 'application/json',
|
||
'X-CSRF-TOKEN': csrfMeta ? csrfMeta.getAttribute('content') : ''
|
||
},
|
||
body: JSON.stringify({ type: this.type, code: this.code })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (!response.ok || !data.success) {
|
||
this.errorMessage = data.message || '诊断失败,请稍后重试';
|
||
return;
|
||
}
|
||
|
||
this.result = data.data;
|
||
} catch (err) {
|
||
this.errorMessage = '网络请求失败: ' + err.message;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.production-diagnosis {
|
||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
</style>
|