624 lines
15 KiB
Vue
624 lines
15 KiB
Vue
<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>
|