Initial qiwei secondary development handoff

This commit is contained in:
2026-06-23 21:11:20 +08:00
commit 858cb68f4f
207 changed files with 52782 additions and 0 deletions

View File

@@ -0,0 +1,623 @@
<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>