Files
qiweimanager-master/frontend/src/components/KingdeeMonitor.vue

624 lines
15 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="kingdee-page">
<section class="kingdee-header">
<div>
<h2>ERP监听</h2>
<p>监听金蝶销售订单排产状态首次完成排产时自动通知对应会话</p>
</div>
<div class="header-actions">
<button class="primary-btn" @click="saveConfig" :disabled="busy">保存配置</button>
<button class="ghost-btn" @click="testConnection" :disabled="busy">测试连接</button>
<button class="ghost-btn" @click="runOnce" :disabled="busy">手动扫描</button>
<button class="ghost-btn" @click="loadAll" :disabled="busy">刷新</button>
</div>
</section>
<section class="status-grid">
<div class="metric">
<span>监听状态</span>
<strong :class="status.running ? 'ok' : 'muted'">{{ status.running ? '运行中' : '未运行' }}</strong>
</div>
<div class="metric">
<span>累计扫描</span>
<strong>{{ status.totalPolled || 0 }}</strong>
</div>
<div class="metric">
<span>已通知</span>
<strong class="ok">{{ status.totalNotified || 0 }}</strong>
</div>
<div class="metric">
<span>未映射</span>
<strong class="warn">{{ status.totalUnmapped || 0 }}</strong>
</div>
<div class="metric">
<span>上次扫描</span>
<strong>{{ formatTime(status.lastPollAt) }}</strong>
</div>
</section>
<div v-if="message" class="inline-message" :class="{ error: messageType === 'error' }">{{ message }}</div>
<section class="kingdee-layout">
<div class="config-column">
<section class="kingdee-panel">
<div class="panel-heading">
<h3>金蝶连接</h3>
<span>{{ config.enabled ? '已开启' : '已关闭' }}</span>
</div>
<div class="form-grid">
<label class="switch-row">
<span>开启监听</span>
<input type="checkbox" v-model="config.enabled">
</label>
<label>
<span>服务地址</span>
<input v-model.trim="config.baseUrl" placeholder="https://example.com/k3cloud">
</label>
<label>
<span>账套ID</span>
<input v-model.trim="config.acctId">
</label>
<label>
<span>用户名</span>
<input v-model.trim="config.username">
</label>
<label>
<span>密码</span>
<input v-model="config.password" type="password" autocomplete="new-password">
</label>
<label>
<span>语言/区域</span>
<input v-model.number="config.lcid" type="number" min="1">
</label>
</div>
</section>
<section class="kingdee-panel">
<div class="panel-heading">
<h3>监听规则</h3>
<span>{{ config.pollIntervalSeconds || 60 }} </span>
</div>
<div class="form-grid">
<label>
<span>轮询间隔</span>
<input v-model.number="config.pollIntervalSeconds" type="number" min="10">
</label>
<label>
<span>销售订单表单编码</span>
<input v-model.trim="config.formId">
</label>
<label>
<span>订单号字段</span>
<input v-model.trim="config.billNoFieldKey">
</label>
<label>
<span>订单ID字段</span>
<input v-model.trim="config.orderIdFieldKey">
</label>
<label>
<span>客户编码字段</span>
<input v-model.trim="config.customerFieldKey">
</label>
<label>
<span>排产状态字段</span>
<input v-model.trim="config.statusFieldKey">
</label>
<label>
<span>完成状态值</span>
<input v-model.trim="config.completedValue">
</label>
<label>
<span>修改时间字段</span>
<input v-model.trim="config.modifyTimeFieldKey">
</label>
</div>
<label class="full-field">
<span>通知模板</span>
<textarea v-model="config.notifyTemplate" rows="3"></textarea>
</label>
</section>
</div>
<div class="config-column">
<section class="kingdee-panel">
<div class="panel-heading">
<h3>通知映射</h3>
<button class="small-btn" type="button" @click="addMapping">新增映射</button>
</div>
<div class="mapping-list">
<div class="mapping-header">
<span>ERP客户编码</span>
<span>机器人账号</span>
<span>通知会话ID</span>
<span>备注</span>
<span></span>
</div>
<div v-for="(row, index) in mappingRows" :key="row.localId" class="mapping-row">
<input v-model.trim="row.customerNumber">
<input v-model.trim="row.robotId">
<input v-model.trim="row.conversationId">
<input v-model.trim="row.remark">
<button class="icon-btn" type="button" @click="removeMapping(index)"></button>
</div>
<p v-if="mappingRows.length === 0" class="empty-text">暂无通知映射</p>
</div>
</section>
<section class="kingdee-panel">
<div class="panel-heading">
<h3>运行记录</h3>
<span>{{ status.status || 'stopped' }}</span>
</div>
<div class="record-block">
<h4>最近通知</h4>
<div v-if="recentNotices.length === 0" class="empty-text">暂无通知记录</div>
<div v-for="item in recentNotices" :key="`${item.orderKey}-${item.notifiedAt}`" class="record-item">
<strong>{{ item.billNo || item.orderKey }}</strong>
<span>{{ item.customerNumber }} / {{ formatTime(item.notifiedAt) }}</span>
<p>{{ item.message }}</p>
</div>
</div>
<div class="record-block">
<h4>最近错误</h4>
<div v-if="recentErrors.length === 0" class="empty-text">暂无错误</div>
<div v-for="item in recentErrors" :key="`${item.message}-${item.createdAt}`" class="record-item error">
<strong>{{ item.billNo || item.customerNumber || '系统' }}</strong>
<span>{{ formatTime(item.createdAt) }}</span>
<p>{{ item.message }}</p>
</div>
</div>
</section>
</div>
</section>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import {
GetKingdeeMonitorConfig,
GetKingdeeMonitorStatus,
RunKingdeeMonitorOnce,
SaveKingdeeMonitorConfig,
TestKingdeeMonitorConnection
} from '../../wailsjs/go/main/App.js'
const defaultTemplate = '您好,您的订单 {{billNo}} 已完成排产,后续我们会继续跟进生产进度。'
const busy = ref(false)
const message = ref('')
const messageType = ref('success')
const status = ref({})
const mappingRows = ref([])
const config = ref(defaultConfig())
const recentNotices = computed(() => status.value.recentNotices || [])
const recentErrors = computed(() => status.value.recentErrors || [])
function defaultConfig() {
return {
enabled: false,
baseUrl: '',
acctId: '',
username: '',
password: '',
lcid: 2052,
pollIntervalSeconds: 60,
formId: 'SAL_SaleOrder',
billNoFieldKey: 'FBillNo',
orderIdFieldKey: 'FID',
customerFieldKey: 'FCustId.FNumber',
statusFieldKey: '',
completedValue: '排产已完成',
modifyTimeFieldKey: 'FModifyDate',
notifyTemplate: defaultTemplate,
customerMappings: {}
}
}
function setMessage(text, type = 'success') {
message.value = text || ''
messageType.value = type
}
async function loadAll() {
busy.value = true
try {
await Promise.all([loadConfig(), loadStatus()])
} finally {
busy.value = false
}
}
async function loadConfig() {
const result = await GetKingdeeMonitorConfig()
if (result?.success === false) {
setMessage(result.message || '加载金蝶配置失败', 'error')
return
}
const data = result?.data || result || {}
config.value = { ...defaultConfig(), ...data }
mappingRows.value = mappingsToRows(config.value.customerMappings || {})
}
async function loadStatus() {
const result = await GetKingdeeMonitorStatus()
if (result?.success === false) {
setMessage(result.message || '加载监听状态失败', 'error')
return
}
status.value = result?.data || {}
}
async function saveConfig() {
busy.value = true
try {
const payload = normalizedConfig()
const result = await SaveKingdeeMonitorConfig(JSON.stringify(payload))
const [ok, msg] = normalizeTupleResult(result)
if (!ok) {
setMessage(msg || '保存失败', 'error')
return
}
config.value = payload
setMessage('金蝶监听配置已保存')
await loadStatus()
} catch (err) {
setMessage(`保存失败: ${err.message || err}`, 'error')
} finally {
busy.value = false
}
}
async function testConnection() {
busy.value = true
try {
const result = await TestKingdeeMonitorConnection(JSON.stringify(normalizedConfig()))
if (result?.success === false) {
setMessage(result.message || '连接失败', 'error')
return
}
setMessage(result?.message || '金蝶连接正常')
} catch (err) {
setMessage(`连接失败: ${err.message || err}`, 'error')
} finally {
busy.value = false
}
}
async function runOnce() {
busy.value = true
try {
const result = await RunKingdeeMonitorOnce()
if (result?.success === false) {
setMessage(result.message || '扫描失败', 'error')
} else {
const data = result?.data || {}
setMessage(`扫描完成:查询 ${data.polled || 0} 条,通知 ${data.notified || 0}`)
}
await loadStatus()
} catch (err) {
setMessage(`扫描失败: ${err.message || err}`, 'error')
} finally {
busy.value = false
}
}
function normalizedConfig() {
return {
...config.value,
pollIntervalSeconds: Number(config.value.pollIntervalSeconds || 60),
lcid: Number(config.value.lcid || 2052),
customerMappings: rowsToMappings(mappingRows.value)
}
}
function mappingsToRows(mappings) {
return Object.entries(mappings || {}).map(([customerNumber, item], index) => ({
localId: `${customerNumber}-${index}-${Date.now()}`,
customerNumber,
robotId: item?.robotId || '',
conversationId: item?.conversationId || '',
remark: item?.remark || ''
}))
}
function rowsToMappings(rows) {
const mappings = {}
for (const row of rows) {
const customerNumber = String(row.customerNumber || '').trim()
if (!customerNumber) continue
mappings[customerNumber] = {
robotId: String(row.robotId || '').trim(),
conversationId: String(row.conversationId || '').trim(),
remark: String(row.remark || '').trim()
}
}
return mappings
}
function addMapping() {
mappingRows.value.push({
localId: `new-${Date.now()}-${mappingRows.value.length}`,
customerNumber: '',
robotId: '',
conversationId: '',
remark: ''
})
}
function removeMapping(index) {
mappingRows.value.splice(index, 1)
}
function normalizeTupleResult(result) {
if (Array.isArray(result)) return [Boolean(result[0]), result[1] || '']
if (result === true) return [true, 'success']
if (result && typeof result === 'object') return [result.success !== false, result.message || '']
return [Boolean(result), '']
}
function formatTime(value) {
const numeric = Number(value || 0)
if (!numeric) return '-'
return new Date(numeric * 1000).toLocaleString()
}
onMounted(loadAll)
</script>
<style scoped>
.kingdee-page {
color: #d6eef2;
min-width: 920px;
}
.kingdee-header,
.kingdee-panel,
.metric {
background: rgba(7, 24, 30, 0.74);
border: 1px solid rgba(72, 240, 220, 0.22);
border-radius: 8px;
}
.kingdee-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px 20px;
margin-bottom: 14px;
}
.kingdee-header h2,
.panel-heading h3,
.record-block h4 {
margin: 0;
}
.kingdee-header p {
margin: 6px 0 0;
color: #91b5bd;
}
.header-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
.primary-btn,
.ghost-btn,
.small-btn,
.icon-btn {
border: 1px solid rgba(72, 240, 220, 0.45);
border-radius: 6px;
color: #e9ffff;
background: rgba(20, 70, 78, 0.78);
cursor: pointer;
height: 34px;
padding: 0 12px;
}
.primary-btn {
background: linear-gradient(135deg, #34e7d5, #58a6ff);
color: #062027;
font-weight: 700;
}
.ghost-btn,
.small-btn,
.icon-btn {
background: rgba(10, 28, 34, 0.86);
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.status-grid {
display: grid;
grid-template-columns: repeat(5, minmax(120px, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.metric {
padding: 12px 14px;
}
.metric span {
display: block;
color: #8fb1b8;
font-size: 13px;
margin-bottom: 6px;
}
.metric strong {
font-size: 22px;
}
.ok {
color: #3df2d0;
}
.warn {
color: #ffd166;
}
.muted {
color: #9fb3b9;
}
.inline-message {
margin-bottom: 14px;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid rgba(72, 240, 220, 0.28);
background: rgba(18, 66, 64, 0.7);
}
.inline-message.error {
border-color: rgba(255, 113, 113, 0.42);
background: rgba(78, 26, 34, 0.78);
}
.kingdee-layout {
display: grid;
grid-template-columns: minmax(420px, 1fr) minmax(420px, 1fr);
gap: 14px;
}
.config-column {
display: flex;
flex-direction: column;
gap: 14px;
}
.kingdee-panel {
padding: 16px;
}
.panel-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.panel-heading span {
color: #72f0de;
font-size: 13px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
label,
.full-field {
display: flex;
flex-direction: column;
gap: 6px;
color: #a9c8ce;
font-size: 13px;
}
.switch-row {
justify-content: center;
}
input,
textarea {
width: 100%;
box-sizing: border-box;
border: 1px solid rgba(72, 240, 220, 0.22);
border-radius: 6px;
background: rgba(2, 15, 20, 0.86);
color: #eaffff;
min-height: 34px;
padding: 8px 10px;
outline: none;
}
textarea {
resize: vertical;
line-height: 1.5;
}
.full-field {
margin-top: 12px;
}
.mapping-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.mapping-header,
.mapping-row {
display: grid;
grid-template-columns: 0.8fr 1fr 1.25fr 0.8fr 42px;
gap: 8px;
align-items: center;
}
.mapping-header {
color: #8fb1b8;
font-size: 12px;
padding: 0 2px;
}
.icon-btn {
width: 38px;
padding: 0;
}
.record-block + .record-block {
margin-top: 16px;
}
.record-item {
border-top: 1px solid rgba(72, 240, 220, 0.12);
padding: 10px 0;
}
.record-item strong,
.record-item span {
display: block;
}
.record-item span,
.empty-text {
color: #8fb1b8;
font-size: 13px;
}
.record-item p {
margin: 6px 0 0;
color: #d6eef2;
line-height: 1.5;
}
.record-item.error p {
color: #ffb4b4;
}
@media (max-width: 1180px) {
.kingdee-layout,
.status-grid {
grid-template-columns: 1fr;
}
.kingdee-page {
min-width: 0;
}
}
</style>