Files

313 lines
13 KiB
Vue
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.
<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>