Files
toolbox/resources/js/components/tools/ProductionDiagnosis.vue
T
2026-08-12 18:00:09 +08:00

403 lines
17 KiB
Vue
Raw 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>
<p class="text-xs text-gray-500">
进产原因来自 CRM <code class="font-mono">ea_case_cstm.label_bit</code> agent-be
<code class="font-mono">stuck_payment_reason</code> 配置过滤后写入
<code class="font-mono">cases.is_need_pfp</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 === 'need_pfp'" class="mt-3 space-y-3">
<div>
<div class="text-xs text-gray-500 mb-1">
进产卡款原因
<span class="ml-2 text-gray-400">来源CRM label_bit &amp; stuck_payment_reason 配置</span>
</div>
<div v-if="check.reasons && check.reasons.length" class="space-y-2">
<div
v-for="reason in check.reasons"
:key="reason.bit"
class="border border-amber-200 bg-amber-50 rounded-lg p-2"
>
<div class="text-xs font-semibold text-amber-800">
{{ reason.label }}
<span class="ml-1 font-mono font-normal text-amber-600">bit {{ reason.bit }}</span>
<span v-if="reason.crm_label !== reason.label" class="ml-2 font-normal text-amber-600">
CRM: {{ reason.crm_label }}
</span>
</div>
<div class="text-xs text-amber-700 mt-1">{{ reason.description }}</div>
</div>
</div>
<div v-else class="text-xs text-gray-600 bg-gray-50 rounded-lg p-2">
该病例没有任何卡生产原因不需要代理放行
</div>
</div>
<div
v-if="check.crm_ignored_reasons && check.crm_ignored_reasons.length"
class="border border-blue-200 bg-blue-50 rounded-lg p-2"
>
<div class="text-xs font-semibold text-blue-800">CRM 有标记但未纳入配置</div>
<div
v-for="ignored in check.crm_ignored_reasons"
:key="ignored.bit"
class="text-xs text-blue-700 mt-1"
>
{{ ignored.label }} (bit {{ ignored.bit }}) {{ ignored.description }}
</div>
</div>
<div
v-if="check.sync_mismatch"
class="border border-orange-200 bg-orange-50 rounded-lg p-2 text-xs text-orange-800"
>
代理库与 CRM 不一致CRM label_bit = {{ check.crm_label_bit }}按配置应为
is_need_pfp = {{ check.expected_is_need_pfp }}实际为 {{ check.is_need_pfp }}
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs">
<div class="bg-gray-50 rounded p-2">
<div class="text-gray-500">CRM label_bit</div>
<div class="text-gray-900 break-all">
{{ check.crm_available ? check.crm_label_bit + ' · ' + check.crm_label_bit_text : check.crm_label_bit_text }}
</div>
</div>
<div class="bg-gray-50 rounded p-2">
<div class="text-gray-500">放行状态 (is_pfp)</div>
<div class="text-gray-900">{{ check.is_pfp_text }} ({{ check.is_pfp }})</div>
</div>
<div class="bg-gray-50 rounded p-2">
<div class="text-gray-500">配置来源</div>
<div class="text-gray-900 break-all">
{{ check.config_source_text }} · 掩码 {{ check.config_mask }}
</div>
</div>
</div>
<div v-if="check.config_options && check.config_options.length" class="text-xs text-gray-500">
当前 stuck_payment_reason
<span
v-for="option in check.config_options"
:key="option.bit"
class="inline-flex items-center px-1.5 py-0.5 rounded bg-gray-100 text-gray-700 mr-1 font-mono"
>
{{ option.bit }} · {{ option.label }}
</span>
</div>
</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.debt_reason_text || '无'} (${e.is_need_pfp})`
},
{ key: 'is_pfp', label: '放行状态 (is_pfp)', value: `${e.is_pfp_text || '-'} (${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>