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,16 @@
<!DOCTYPE html>
<html>
<head>
<title>清除本地存储</title>
</head>
<body>
<h1>正在清除本地存储...</h1>
<script>
localStorage.clear();
document.body.innerHTML = '<h1>本地存储已清除!请刷新主页面</h1>';
setTimeout(() => {
window.close();
}, 2000);
</script>
</body>
</html>

253
frontend/css/style.css Normal file
View File

@@ -0,0 +1,253 @@
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background-color: #f5f7fa;
color: #333;
display: flex;
height: 100vh;
overflow: hidden;
}
/* 侧边导航栏 */
nav {
width: 180px;
background-color: #2c3e50;
color: white;
display: flex;
flex-direction: column;
position: relative;
}
nav ul {
list-style: none;
padding: 20px 0;
}
nav ul li {
padding: 15px 20px;
cursor: pointer;
transition: all 0.3s ease;
}
nav ul li:hover {
background-color: #34495e;
transform: translateX(5px);
}
nav ul li a {
color: white;
text-decoration: none;
display: block;
font-size: 14px;
}
/* 主内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
width: calc(100% - 180px);
}
/* 头部 */
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 30px;
background-color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: relative;
z-index: 10;
}
header h1 {
font-size: 24px;
font-weight: bold;
}
header h1 .pro {
color: #3498db;
}
header .status {
color: #666;
}
header .top-btn {
padding: 8px 20px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
}
header .top-btn:hover {
background-color: #c0392b;
}
/* 主内容区域 */
main {
flex: 1;
padding: 30px;
overflow-y: auto;
background-color: #f5f7fa;
}
/* 卡片容器 */
.cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
/* 卡片样式 */
.card {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
min-height: 150px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
}
.card-icon {
font-size: 48px;
color: #3498db;
margin-bottom: 10px;
}
.card-title {
color: #666;
font-size: 14px;
margin-bottom: 5px;
}
.card-value {
font-size: 28px;
font-weight: bold;
color: #333;
}
.card-btn {
margin-top: 10px;
padding: 8px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
}
.card-btn:hover {
background-color: #2980b9;
}
/* 特殊卡片样式 - 新增账号卡片有不同的按钮 */
.card:nth-child(4) .card-btn {
background-color: #2ecc71;
}
.card:nth-child(4) .card-btn:hover {
background-color: #27ae60;
}
/* 底部 */
footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 30px;
background-color: white;
border-top: 1px solid #e0e0e0;
position: relative;
}
.status-indicator {
display: flex;
align-items: center;
}
.online {
color: #2ecc71;
font-size: 16px;
margin-right: 5px;
}
.version {
color: #666;
}
.company {
color: #999;
}
/* 适配Wails窗口 */
html, body {
width: 100%;
height: 100%;
}
/* 调整容器布局 */
header, main, footer {
width: 100%;
}
/* 滚动条样式 */
main::-webkit-scrollbar {
width: 8px;
}
main::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
main::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
main::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* 响应式设计 */
@media (max-width: 768px) {
.cards {
grid-template-columns: 1fr;
}
header {
padding: 15px 20px;
}
main {
padding: 20px;
}
footer {
padding: 15px 20px;
}
}

12
frontend/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>灵泽万川企微售后客服</title>
</head>
<body>
<div id="app"></div>
<script src="./src/main.js" type="module"></script>
</body>
</html>

76
frontend/js/app.js Normal file
View File

@@ -0,0 +1,76 @@
// 等待DOM加载完成
document.addEventListener('DOMContentLoaded', function() {
try {
// 导入Wails运行时API
import('../wailsjs/runtime/runtime.js').then(({ LogInfo, LogDebug, LogError }) => {
LogInfo('StarBot Pro frontend loaded successfully');
// 简化版本 - 只保留基本功能,移除可能导致问题的动画和复杂操作
// 侧边栏导航项点击事件 - 简化版
const navItems = document.querySelectorAll('nav ul li');
if (navItems.length > 0) {
navItems.forEach(item => {
item.addEventListener('click', function() {
LogDebug('导航项点击:', this.textContent.trim());
});
});
}
// 充值按钮点击事件 - 简化版
const rechargeBtn = document.querySelector('#rechargeBtn');
if (rechargeBtn) {
rechargeBtn.addEventListener('click', function() {
LogDebug('充值按钮点击');
});
}
// 添加账号按钮点击事件 - 简化版
const addAccountBtn = document.querySelector('#addAccountBtn');
if (addAccountBtn) {
addAccountBtn.addEventListener('click', function() {
LogDebug('添加账号按钮点击');
});
}
// 在页面上显示加载成功信息
const statusEl = document.createElement('div');
statusEl.style.position = 'fixed';
statusEl.style.bottom = '20px';
statusEl.style.right = '20px';
statusEl.style.padding = '10px';
statusEl.style.backgroundColor = '#2ecc71';
statusEl.style.color = 'white';
statusEl.style.borderRadius = '4px';
statusEl.textContent = '前端加载成功';
document.body.appendChild(statusEl);
}).catch(error => {
console.error('Failed to import Wails runtime:', error);
// 在页面上显示加载成功信息作为备选
const statusEl = document.createElement('div');
statusEl.style.position = 'fixed';
statusEl.style.bottom = '20px';
statusEl.style.right = '20px';
statusEl.style.padding = '10px';
statusEl.style.backgroundColor = '#2ecc71';
statusEl.style.color = 'white';
statusEl.style.borderRadius = '4px';
statusEl.textContent = '前端加载成功';
document.body.appendChild(statusEl);
});
} catch (error) {
console.error('Frontend error:', error);
// 在页面上显示错误信息
const errorEl = document.createElement('div');
errorEl.style.position = 'fixed';
errorEl.style.bottom = '20px';
errorEl.style.right = '20px';
errorEl.style.padding = '10px';
errorEl.style.backgroundColor = '#e74c3c';
errorEl.style.color = 'white';
errorEl.style.borderRadius = '4px';
errorEl.textContent = '前端加载错误: ' + error.message;
document.body.appendChild(errorEl);
}
});

1116
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
frontend/package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.2.0",
"vite": "^3.0.7",
"vue": "^3.2.0"
},
"dependencies": {
"element-plus": "^2.11.1"
}
}

View File

@@ -0,0 +1 @@
db031e671111b343255373ca05cff100

661
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,661 @@
<template>
<div id="app">
<div v-if="isLoggedIn" class="layout-container">
<!-- 左侧菜单 -->
<nav class="side-nav">
<!-- 左侧顶栏 -->
<div class="side-nav-header">
<h1>灵泽万川企微售后客服</h1>
</div>
<!-- 菜单项 -->
<ul>
<li :class="{ 'active': activeSection === '系统设置' }"><a href="#" @click="navigate('系统设置')">系统首页</a></li>
<li :class="{ 'active': activeSection === '自动客服' }"><a href="#" @click="navigate('自动客服')">自动客服</a></li>
<li :class="{ 'active': activeSection === '售后问题库' }"><a href="#" @click="navigate('售后问题库')">售后问题库</a></li>
<li :class="{ 'active': activeSection === '工程师派单' }"><a href="#" @click="navigate('工程师派单')">工程师派单</a></li>
<li :class="{ 'active': activeSection === '知识库' }"><a href="#" @click="navigate('知识库')">知识库</a></li>
<li :class="{ 'active': activeSection === 'ERP监听' }"><a href="#" @click="navigate('ERP监听')">ERP监听</a></li>
<li :class="{ 'active': activeSection === '操作记录' }"><a href="#" @click="navigate('操作记录')">操作记录</a></li>
<li :class="{ 'active': activeSection === '回调配置' }"><a href="#" @click="navigate('回调配置')">回调配置</a></li>
</ul>
<!-- 底部运行天数和版本信息 -->
<div class="side-nav-footer">
<div class="status" style="text-align: left; font-size: 14px;">运行天数: <span
style="font-size: 16px; font-weight: bold;">{{ runningDays }}</span> </div>
<div class="version" style="text-align: left; font-size: 12px; color: #666;">版本号: <span>{{ appVersion
}}</span></div>
</div>
</nav>
<!-- 主内容区域 -->
<div class="main-content">
<main>
<!-- 动态内容区域 -->
<div v-if="activeSection === '系统设置'" class="system-settings-content">
<section class="workspace-hero">
<div>
<p class="eyebrow">灵泽万川企微售后客服</p>
<h2>售后客服工作台</h2>
<p class="hero-subtitle">统一查看企微连接自动客服运行和今日处理概况</p>
</div>
<div class="hero-actions">
<button id="startWxworkBtn" class="primary-action" @click="handleStartWxwork">启动企微</button>
<button class="secondary-action" @click="navigate('自动客服')">配置自动客服</button>
</div>
</section>
<section class="overview-grid">
<article class="overview-card">
<span class="metric-label">自动客服</span>
<strong :class="autoReplyStatus.enabled ? 'state-ok' : 'state-muted'">
{{ autoReplyStatus.enabled ? '已开启' : '未开启' }}
</strong>
<p>{{ autoReplyStatus.running ? '正在监听客户消息' : '未进入监听状态' }}</p>
</article>
<article class="overview-card">
<span class="metric-label">活跃账号</span>
<strong>{{ activeClientCount }}</strong>
<p>当前已识别并可用的企微账号</p>
</article>
<article class="overview-card">
<span class="metric-label">今日回复</span>
<strong>{{ autoReplyStatus.todayReplied || 0 }}</strong>
<p>AI 或本地规则已回复消息</p>
</article>
<article class="overview-card">
<span class="metric-label">今日转人工</span>
<strong>{{ autoReplyStatus.todayHandoff || 0 }}</strong>
<p>已通知人工接管的问题</p>
</article>
</section>
<section class="workspace-columns">
<div class="work-panel">
<div class="panel-heading">
<h3>运行概览</h3>
<span>{{ formatDuration(autoReplyStatus.lastTotalDurationMs) }}</span>
</div>
<div class="detail-list">
<div>
<span>知识库</span>
<strong>{{ autoReplyStatus.knowledgeFileCount || 0 }} 文件 / {{ autoReplyStatus.knowledgeChunkCount || 0 }} 片段</strong>
</div>
<div>
<span>向量索引</span>
<strong>{{ autoReplyStatus.embeddingChunkCount || 0 }} 片段</strong>
</div>
<div>
<span>最近 AI 耗时</span>
<strong>{{ formatDuration(autoReplyStatus.lastAiDurationMs) }}</strong>
</div>
<div>
<span>身份缓存</span>
<strong>内部 {{ autoReplyStatus.internalContactCount || 0 }} / 外部 {{ autoReplyStatus.externalContactCount || 0 }}</strong>
</div>
</div>
</div>
<div class="work-panel">
<div class="panel-heading">
<h3>快捷操作</h3>
<span>常用入口</span>
</div>
<div class="quick-actions">
<button @click="handleStartWxwork">启动企微</button>
<button @click="navigate('自动客服')">自动客服</button>
<button @click="navigate('售后问题库')">售后问题库</button>
<button @click="navigate('工程师派单')">工程师派单</button>
<button @click="navigate('知识库')">知识库</button>
<button @click="navigate('ERP监听')">ERP监听</button>
<button @click="navigate('操作记录')">操作记录</button>
</div>
</div>
</section>
<section class="home-account-panel">
<WxWorkAccount :account-info="wxWorkAccountInfo" />
</section>
</div>
<!-- 自动客服内容区域 -->
<div v-else-if="activeSection === '自动客服'" class="auto-reply-content">
<AutoReply />
</div>
<!-- 售后问题库内容区域 -->
<div v-else-if="activeSection === '售后问题库'" class="after-sales-content">
<AfterSalesIssues />
</div>
<!-- 工程师派单内容区域 -->
<div v-else-if="activeSection === '工程师派单'" class="engineer-dispatch-content">
<EngineerDispatch />
</div>
<!-- 知识库内容区域 -->
<div v-else-if="activeSection === '知识库'" class="after-sales-knowledge-content">
<AfterSalesKnowledge />
</div>
<!-- ERP监听内容区域 -->
<div v-else-if="activeSection === 'ERP监听'" class="kingdee-monitor-content">
<KingdeeMonitor />
</div>
<!-- 操作记录内容区域 -->
<div v-else-if="activeSection === '操作记录'" class="operation-logs-content">
<OperationLogs />
</div>
<!-- 回调配置内容区域 -->
<div v-else-if="activeSection === '回调配置'" class="callback-config-content">
<h2>回调配置</h2>
<div class="config-form">
<div class="form-group">
<label>Http回调地址</label>
<textarea v-model="callbackUrl" placeholder="请输入回调地址" rows="3"></textarea>
</div>
<div class="form-group">
<label>文件上传地址</label>
<textarea v-model="fileUploadUrl" placeholder="请输入文件上传地址" rows="3"></textarea>
</div>
<!-- <div class="form-group">
<label>回调Token配置</label>
<input v-model="callbackToken" type="text" placeholder="请输入回调Token">
</div> -->
<div class="form-group">
<label>http端口</label>
<input v-model="httpPort" type="text" placeholder="请输入http端口">
</div>
<div class="form-group">
<label>设备编码</label>
<input v-model="deviceCode" type="text" placeholder="请输入设备编码">
</div>
<div class="form-group">
<label>开启回调</label>
<label class="switch">
<input type="checkbox" v-model="enableCallback">
<span class="slider round"></span>
</label>
<span class="status-text">{{ enableCallback ? '已开启' : '已关闭' }}</span>
</div>
<div class="form-group">
<label>授权云管理端</label>
<label class="switch">
<input type="checkbox" v-model="enableCloudAuth">
<span class="slider round"></span>
</label>
<span class="status-text">{{ enableCloudAuth ? '已开启' : '已关闭' }}</span>
</div>
<div class="form-actions">
<button class="save-btn" @click="handleSaveConfig">保存设置</button>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- 状态通知 -->
<div v-if="showNotification" :class="['notification', notificationType]">
{{ notificationMessage }}
</div>
</div>
</template>
<style scoped>
.notification {
position: fixed;
right: 24px;
bottom: 24px;
color: var(--cmd-text);
padding: 10px 14px;
background-color: rgba(9, 22, 28, 0.96);
border: 1px solid rgba(72, 240, 220, 0.32);
border-radius: 6px;
box-shadow: var(--cmd-shadow), 0 0 22px rgba(72, 240, 220, 0.14);
z-index: 1000;
}
</style>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { SendWxWorkData, GetCallbackConfig, SaveCallbackConfig, GetActiveClientCount, GetAutoReplyStatus, LogFrontend } from '../wailsjs/go/main/App.js';
// 导入组件
import WxWorkAccount from '@/components/WxWorkAccount.vue';
import OperationLogs from '@/components/OperationLogs.vue';
import AutoReply from '@/components/AutoReply.vue';
import AfterSalesIssues from '@/components/AfterSalesIssues.vue';
import AfterSalesKnowledge from '@/components/AfterSalesKnowledge.vue';
import EngineerDispatch from '@/components/EngineerDispatch.vue';
import KingdeeMonitor from '@/components/KingdeeMonitor.vue';
// 状态变量
const showNotification = ref(false);
const notificationMessage = ref('');
const notificationType = ref('success');
const runningDays = ref(1);
const activeSection = ref('系统设置');
const appVersion = ref('2.1.3'); // 应用版本号
const wxWorkAccountInfo = ref(null);
const activeClientCount = ref(0); // 活跃账号数量
const autoReplyStatus = ref({});
const isLoggedIn = ref(true); // 无登录界面版本:启动后直接进入主界面
// 回调配置相关状态
const callbackUrl = ref('');
const callbackToken = ref('');
const httpPort = ref('');
const enableCallback = ref(false);
const enableCloudAuth = ref(false);
const fileUploadUrl = ref('');
const deviceCode = ref('');
// 定时器ID
let activeClientCountInterval = null;
let autoReplyStatusInterval = null;
let runningDaysInterval = null;
// 导航处理
async function navigate(section) {
LogFrontend('debug', `导航到: ${section}`);
activeSection.value = section;
}
// 保存回调配置
async function handleSaveConfig() {
LogFrontend('info', '保存回调配置');
try {
// 构建配置数据
const configData = {
callbackUrl: callbackUrl.value,
callbackToken: callbackToken.value,
httpPort: httpPort.value,
enableCallback: enableCallback.value,
enableCloudAuth: enableCloudAuth.value,
fileUploadUrl: fileUploadUrl.value,
deviceCode: deviceCode.value
};
// 调用后端API保存配置
LogFrontend('debug', `配置数据: ${JSON.stringify(configData)}`);
const jsonData = JSON.stringify(configData);
// 在Wails中Go函数的多返回值会被转换为JavaScript数组
// SaveCallbackConfig在后端返回的是(bool, string)
let success = false;
let message = '';
try {
const result = await SaveCallbackConfig(jsonData);
// 处理可能的返回值格式
if (Array.isArray(result)) {
[success, message] = result;
} else if (result === true) {
success = true;
} else if (typeof result === 'string') {
message = result;
}
} catch (err) {
message = err.message || '未知错误';
}
if (success) {
// 显示保存成功通知
showNotificationMessage('配置保存成功,请重启应用', 'success');
} else {
// 显示保存失败通知
//showNotificationMessage('保存配置失败: ' + message, 'error');
}
} catch (error) {
LogFrontend('error', `保存配置失败: ${error}`);
//showNotificationMessage('保存配置失败: ' + error.message, 'error');
}
}
// 从exe路径加载配置
async function loadConfigFromExePath() {
console.log('[信息] [前端] 尝试从exe同级目录config文件夹加载config.json');
try {
// 调用Go函数获取配置
const configFromExe = await GetCallbackConfig();
console.log('[调试] [前端] GetCallbackConfig原始返回:', configFromExe);
// 现在直接接收配置对象,不再处理元组
if (configFromExe && typeof configFromExe === 'object') {
console.log('[信息] [前端] 从exe路径成功加载配置:', configFromExe);
return configFromExe;
} else {
console.warn('[警告] [前端] 从exe路径加载配置失败返回格式不正确:', configFromExe);
return null;
}
} catch (error) {
console.error('[错误] [前端] 从exe路径加载配置时出错:', error);
return null;
}
}
// 加载回调配置优先从exe路径加载
async function loadCallbackConfig() {
try {
LogFrontend('info', '加载回调配置');
// 尝试从exe路径加载配置
const configFromExe = await loadConfigFromExePath();
if (configFromExe) {
// 使用从exe路径加载的配置
// 正确处理嵌套结构
const callbackConfig = configFromExe.callbackConfig || configFromExe.CallbackConfig || configFromExe;
callbackUrl.value = callbackConfig.callbackUrl || callbackConfig.CallbackURL || '';
callbackToken.value = callbackConfig.callbackToken || callbackConfig.CallbackToken || '';
httpPort.value = callbackConfig.httpPort || callbackConfig.HTTPPort || '10001';
enableCallback.value = callbackConfig.enableCallback !== undefined ? callbackConfig.enableCallback :
(callbackConfig.EnableCallback !== undefined ? callbackConfig.EnableCallback : false);
enableCloudAuth.value = callbackConfig.enableCloudAuth !== undefined ? callbackConfig.enableCloudAuth :
(callbackConfig.EnableCloudAuth !== undefined ? callbackConfig.EnableCloudAuth : false);
fileUploadUrl.value = callbackConfig.fileUploadUrl || callbackConfig.FileUploadUrl || '';
deviceCode.value = callbackConfig.deviceCode || callbackConfig.DeviceCode || '';
LogFrontend('info', `成功从exe路径加载回调配置: ${JSON.stringify({
callbackUrl: callbackUrl.value,
callbackToken: callbackToken.value,
httpPort: httpPort.value,
enableCallback: enableCallback.value,
enableCloudAuth: enableCloudAuth.value,
fileUploadUrl: fileUploadUrl.value
})}`);
} else {
// 使用默认值
callbackUrl.value = '';
callbackToken.value = '';
httpPort.value = '10001';
enableCallback.value = false;
enableCloudAuth.value = false;
fileUploadUrl.value = '';
deviceCode.value = '';
LogFrontend('info', '使用默认回调配置');
}
} catch (error) {
LogFrontend('error', `加载回调配置时出现异常: ${error}`);
// 异常时使用默认值
callbackUrl.value = '';
callbackToken.value = '';
httpPort.value = '10001';
enableCallback.value = false;
enableCloudAuth.value = false;
fileUploadUrl.value = '';
deviceCode.value = '';
LogFrontend('info', '异常时使用默认回调配置');
}
}
// 启动企微按钮处理
async function handleStartWxwork() {
LogFrontend('info', '启动企微按钮点击,正在发送启动请求...');
try {
// 启动企微进程
const requestData = {
"type": 10000,
"data": {}
};
const jsonData = JSON.stringify(requestData);
// 调用后端SendWxWorkData函数客户端ID暂时使用0
const result = await SendWxWorkData("0", jsonData);
// 处理可能的返回值格式
let success = false;
let error = null;
if (Array.isArray(result)) {
// 数组格式: [success, error]
if (result.length >= 2) {
[success, error] = result;
} else if (result.length === 1) {
success = result[0];
}
} else if (result && typeof result === 'object') {
// 对象格式: {success, error}
success = result.success;
error = result.error;
} else {
// 未知格式
LogFrontend('warn', `SendWxWorkData返回未知格式: ${JSON.stringify(result)}`);
success = Boolean(result);
}
if (error) {
LogFrontend('error', `发送启动企微请求失败: ${error}`);
//showNotificationMessage('启动企微失败: ' + error, 'error');
return;
}
if (success) {
LogFrontend('info', '启动企微请求发送成功');
//showNotificationMessage('已发送启动企微请求', 'success');
} else {
LogFrontend('error', '启动企微请求返回失败');
//showNotificationMessage('启动企微失败,请重试', 'error');
}
} catch (err) {
LogFrontend('error', `启动企微过程中出现异常: ${err}`);
//showNotificationMessage('启动企微时出现异常: ' + err.message, 'error');
}
}
// 计算运行天数从1开始
function calculateRunningDays() {
// 假设应用启动时间存储在localStorage中
let startTime = localStorage.getItem('appStartTime');
// 如果没有启动时间,设置为当前时间
if (!startTime) {
startTime = new Date().toISOString();
localStorage.setItem('appStartTime', startTime);
}
// 计算从启动时间到现在的天数差从1开始
const startDate = new Date(startTime);
const currentDate = new Date();
const timeDiff = currentDate - startDate;
const daysDiff = Math.floor(timeDiff / (1000 * 3600 * 24)) + 1; // 从1开始计算
return daysDiff;
}
// 显示通知
function showNotificationMessage(message, type = 'success', duration = 3000) {
notificationMessage.value = message;
notificationType.value = type;
showNotification.value = true;
setTimeout(() => {
showNotification.value = false;
}, duration);
}
// 登录处理方法
const handleLoginSuccess = (loginData) => {
isLoggedIn.value = true;
localStorage.setItem('isLoggedIn', 'true');
LogFrontend('info', `用户登录成功: ${loginData.username}`);
showNotificationMessage('登录成功,欢迎使用灵泽万川企微售后客服!', 'success');
// 登录成功后初始化应用数据
runningDays.value = calculateRunningDays();
loadCallbackConfig();
updateActiveClientCount();
loadAutoReplyStatus();
// 设置定时器
runningDaysInterval = setInterval(() => {
runningDays.value = calculateRunningDays();
}, 900000);
activeClientCountInterval = setInterval(updateActiveClientCount, 900000);
autoReplyStatusInterval = setInterval(loadAutoReplyStatus, 900000);
};
const handleLoginFailed = (error) => {
LogFrontend('error', `用户登录失败: ${error}`);
//showNotificationMessage('登录失败: ' + error, 'error');
};
// 获取活跃客户端数量
async function updateActiveClientCount() {
try {
// 检测是否在Wails环境中运行
const isWailsEnvironment = typeof window !== 'undefined' && window['go'] && window['go']['main'];
if (isWailsEnvironment) {
try {
// 在Wails环境中调用Go后端获取活跃客户端数量
const count = await GetActiveClientCount();
activeClientCount.value = count;
LogFrontend('debug', `获取活跃客户端数量: ${count}`);
} catch (error) {
LogFrontend('error', `获取活跃客户端数量失败: ${error}`);
//showNotificationMessage('获取活跃客户端数量失败:', error);
// 出错时使用0作为默认值
activeClientCount.value = 0;
}
} else {
// 在开发环境中,使用模拟数据
LogFrontend('debug', '在开发环境中,使用模拟活跃客户端数量');
activeClientCount.value = 3; // 开发环境模拟3个活跃客户端
}
} catch (error) {
LogFrontend('error', `更新活跃客户端数量时出错: ${error}`);
activeClientCount.value = 0;
}
}
async function loadAutoReplyStatus() {
try {
const isWailsEnvironment = typeof window !== 'undefined' && window['go'] && window['go']['main'];
if (!isWailsEnvironment) {
autoReplyStatus.value = {
enabled: true,
running: true,
todayReplied: 8,
todayHandoff: 1,
knowledgeFileCount: 12,
knowledgeChunkCount: 86,
embeddingChunkCount: 86,
lastTotalDurationMs: 1260,
lastAiDurationMs: 940,
internalContactCount: 6,
externalContactCount: 18
};
return;
}
const result = await GetAutoReplyStatus();
if (result?.success !== false && result?.data) {
autoReplyStatus.value = result.data;
}
} catch (error) {
LogFrontend('warn', `加载自动客服状态失败: ${error}`);
}
}
function formatDuration(value) {
const numeric = Number(value || 0);
if (!numeric) return '-';
if (numeric < 1000) return `${Math.round(numeric)} ms`;
return `${(numeric / 1000).toFixed(1)} s`;
}
// 生命周期钩子
onMounted(async () => {
// 每次启动时清空appStartTime
localStorage.removeItem('appStartTime');
// 无登录界面版本:始终直接进入主界面
isLoggedIn.value = true;
localStorage.setItem('isLoggedIn', 'true');
LogFrontend('info', 'StarBot Pro frontend loaded successfully');
LogFrontend('info', '当前为无登录界面版本,已自动进入主界面');
// 初始化运行天数
runningDays.value = calculateRunningDays();
// 加载回调配置
loadCallbackConfig();
// 获取初始活跃客户端数量
await updateActiveClientCount();
await loadAutoReplyStatus();
// 每90秒更新一次运行天数
runningDaysInterval = setInterval(() => {
runningDays.value = calculateRunningDays();
}, 90000);
// 每8秒更新一次活跃客户端数量
activeClientCountInterval = setInterval(updateActiveClientCount, 8000);
autoReplyStatusInterval = setInterval(loadAutoReplyStatus, 8000);
});
onUnmounted(() => {
// 清理工作
if (activeClientCountInterval) {
clearInterval(activeClientCountInterval);
}
if (autoReplyStatusInterval) {
clearInterval(autoReplyStatusInterval);
}
if (runningDaysInterval) {
clearInterval(runningDaysInterval);
}
});
</script>
<style>
@import './style.css';
@import './app.css';
/* 通知样式 */
.notification {
position: fixed;
bottom: 20px;
right: 20px;
padding: 10px 14px;
border-radius: 6px;
color: var(--cmd-text);
border: 1px solid rgba(72, 240, 220, 0.28);
background-color: rgba(9, 22, 28, 0.96);
box-shadow: var(--cmd-shadow), 0 0 22px rgba(72, 240, 220, 0.14);
z-index: 1000;
}
.notification.success {
background-color: rgba(93, 242, 167, 0.12);
color: var(--cmd-green);
border-color: rgba(93, 242, 167, 0.34);
}
.notification.error {
background-color: rgba(255, 107, 125, 0.12);
color: var(--cmd-red);
border-color: rgba(255, 107, 125, 0.38);
}
</style>

765
frontend/src/app.css Normal file
View File

@@ -0,0 +1,765 @@
/* App shell */
.layout-container {
display: flex;
height: 100vh;
min-width: 960px;
color: var(--cmd-text);
background:
linear-gradient(rgba(72, 240, 220, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(72, 240, 220, 0.028) 1px, transparent 1px),
radial-gradient(circle at 18% 12%, rgba(72, 240, 220, 0.12), transparent 28%),
radial-gradient(circle at 90% 8%, rgba(99, 168, 255, 0.11), transparent 25%),
#071014;
background-size: 34px 34px, 34px 34px, auto, auto, auto;
}
.side-nav {
width: 252px;
flex-shrink: 0;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
color: var(--cmd-text-soft);
background:
linear-gradient(180deg, rgba(15, 35, 43, 0.98), rgba(4, 11, 14, 0.98)),
#071014;
border-right: 1px solid var(--cmd-line);
box-shadow: 18px 0 46px rgba(0, 0, 0, 0.34);
}
.side-nav::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(90deg, rgba(72, 240, 220, 0.09), transparent 42%),
repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0 1px, transparent 1px 7px);
}
.side-nav-header {
position: relative;
padding: 26px 22px 22px;
border-bottom: 1px solid rgba(72, 240, 220, 0.18);
}
.side-nav-header h1 {
margin: 0;
color: #f5feff;
font-size: 18px;
font-weight: 800;
line-height: 1.35;
letter-spacing: 0;
text-shadow: 0 0 18px rgba(72, 240, 220, 0.18);
}
.side-nav ul {
position: relative;
list-style: none;
padding: 16px 10px;
margin: 0;
flex: 1;
}
.side-nav ul li {
position: relative;
margin: 4px 0;
border-radius: var(--cmd-radius);
border: 1px solid transparent;
transition: border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
}
.side-nav ul li a {
display: block;
padding: 12px 14px;
color: var(--cmd-text-soft);
text-decoration: none;
font-size: 14px;
line-height: 1.4;
}
.side-nav ul li:hover {
border-color: rgba(72, 240, 220, 0.18);
background: rgba(72, 240, 220, 0.075);
}
.side-nav ul li.active {
border-color: rgba(72, 240, 220, 0.55);
background:
linear-gradient(90deg, rgba(72, 240, 220, 0.24), rgba(99, 168, 255, 0.12)),
rgba(13, 32, 39, 0.92);
box-shadow: inset 3px 0 0 var(--cmd-cyan), 0 0 24px rgba(72, 240, 220, 0.14);
}
.side-nav ul li.active a {
color: #f3ffff;
font-weight: 800;
}
.side-nav-footer {
position: relative;
padding: 16px 22px 20px;
color: var(--cmd-text-muted);
border-top: 1px solid rgba(72, 240, 220, 0.18);
background: rgba(0, 0, 0, 0.24);
}
.side-nav-footer .status,
.side-nav-footer .version {
font-size: 12px !important;
color: var(--cmd-text-muted) !important;
line-height: 1.8;
}
.side-nav-footer span {
color: var(--cmd-cyan);
text-shadow: 0 0 10px rgba(72, 240, 220, 0.28);
}
.main-content {
flex: 1;
min-width: 0;
overflow-y: auto;
padding: 24px;
background:
radial-gradient(circle at 50% -10%, rgba(72, 240, 220, 0.08), transparent 34%),
linear-gradient(180deg, rgba(255, 255, 255, 0.018), transparent 180px);
}
.main-content main {
max-width: 1480px;
margin: 0 auto;
}
/* Workbench */
.system-settings-content {
display: flex;
flex-direction: column;
gap: 18px;
}
.workspace-hero {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 26px;
border: 1px solid var(--cmd-line-strong);
border-radius: var(--cmd-radius);
background:
linear-gradient(135deg, rgba(20, 45, 55, 0.94), rgba(8, 20, 25, 0.96)),
var(--cmd-panel);
box-shadow: var(--cmd-shadow), var(--cmd-glow);
}
.workspace-hero::after {
content: "";
position: absolute;
right: -14%;
top: -80%;
width: 46%;
height: 220%;
pointer-events: none;
transform: rotate(18deg);
background: linear-gradient(90deg, transparent, rgba(72, 240, 220, 0.12), transparent);
}
.eyebrow {
margin: 0 0 8px;
color: var(--cmd-cyan);
font-size: 13px;
font-weight: 800;
text-transform: uppercase;
}
.workspace-hero h2 {
margin: 0;
color: var(--cmd-text);
font-size: 28px;
line-height: 1.25;
}
.hero-subtitle {
margin: 10px 0 0;
color: var(--cmd-text-soft);
font-size: 14px;
}
.hero-actions,
.panel-actions,
.form-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.primary-action,
.secondary-action,
.save-btn,
.card-btn,
.primary-btn,
.ghost-btn,
.mini-primary,
.mini-ghost,
.danger-btn,
.small-btn,
.mini-btn,
.advanced-toggle {
min-height: 36px;
border-radius: 6px;
padding: 0 16px;
font-size: 14px;
font-weight: 800;
cursor: pointer;
transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
}
.primary-action,
.save-btn,
.card-btn,
.primary-btn,
.mini-primary {
color: #031316;
border: 1px solid rgba(72, 240, 220, 0.82);
background: linear-gradient(135deg, var(--cmd-cyan), #63a8ff);
box-shadow: 0 0 20px rgba(72, 240, 220, 0.18);
}
.primary-action:hover,
.save-btn:hover,
.card-btn:hover,
.primary-btn:hover,
.mini-primary:hover {
transform: translateY(-1px);
box-shadow: 0 0 28px rgba(72, 240, 220, 0.28);
}
.secondary-action,
.ghost-btn,
.mini-ghost,
.mini-btn,
.advanced-toggle {
color: var(--cmd-cyan);
border: 1px solid rgba(72, 240, 220, 0.38);
background: rgba(9, 23, 29, 0.84);
}
.secondary-action:hover,
.ghost-btn:hover,
.mini-ghost:hover,
.mini-btn:hover,
.advanced-toggle:hover {
color: #eaffff;
border-color: rgba(72, 240, 220, 0.72);
background: rgba(72, 240, 220, 0.12);
box-shadow: 0 0 20px rgba(72, 240, 220, 0.14);
}
.danger-btn {
color: #ffd8dd;
border: 1px solid rgba(255, 107, 125, 0.48);
background: rgba(255, 107, 125, 0.1);
}
.danger-btn:hover {
border-color: rgba(255, 107, 125, 0.8);
background: rgba(255, 107, 125, 0.16);
}
button:disabled {
cursor: not-allowed !important;
opacity: 0.48;
transform: none !important;
box-shadow: none !important;
}
.overview-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
}
.overview-card,
.work-panel,
.config-form,
.wxwork-account-container,
.operation-logs-container,
.auto-reply-page {
border: 1px solid var(--cmd-line);
border-radius: var(--cmd-radius);
background: var(--cmd-panel);
box-shadow: var(--cmd-shadow);
}
.overview-card {
position: relative;
overflow: hidden;
padding: 18px;
min-height: 128px;
}
.overview-card::before,
.metric::before {
content: "";
position: absolute;
inset: 0 0 auto;
height: 2px;
background: linear-gradient(90deg, var(--cmd-cyan), transparent);
opacity: 0.75;
}
.metric-label {
display: block;
color: var(--cmd-text-soft);
font-size: 13px;
margin-bottom: 10px;
}
.overview-card strong {
display: block;
color: var(--cmd-text);
font-size: 28px;
line-height: 1.1;
}
.overview-card p {
margin: 12px 0 0;
color: var(--cmd-text-muted);
font-size: 13px;
line-height: 1.5;
}
.state-ok,
.ok {
color: var(--cmd-green) !important;
}
.state-muted,
.muted {
color: var(--cmd-text-muted) !important;
}
.danger {
color: var(--cmd-red) !important;
}
.workspace-columns {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(320px, 0.85fr);
gap: 14px;
}
.work-panel {
padding: 18px;
}
.panel-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-bottom: 14px;
}
.panel-heading h3 {
margin: 0;
color: var(--cmd-text);
font-size: 16px;
}
.panel-heading span {
color: var(--cmd-text-muted);
font-size: 13px;
}
.detail-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.detail-list div,
.quick-actions button {
border: 1px solid rgba(72, 240, 220, 0.16);
border-radius: 6px;
background: rgba(8, 21, 27, 0.78);
}
.detail-list div {
padding: 12px;
}
.detail-list span {
display: block;
color: var(--cmd-text-muted);
font-size: 12px;
margin-bottom: 6px;
}
.detail-list strong {
color: var(--cmd-text);
font-size: 14px;
}
.quick-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.quick-actions button {
min-height: 42px;
color: var(--cmd-text-soft);
font-weight: 800;
cursor: pointer;
}
.quick-actions button:hover {
border-color: rgba(72, 240, 220, 0.58);
color: var(--cmd-cyan);
background: rgba(72, 240, 220, 0.1);
}
.home-account-panel {
margin-top: 18px;
border: 1px solid rgba(72, 240, 220, 0.22);
border-radius: 8px;
background: rgba(7, 22, 29, 0.72);
box-shadow: var(--cmd-shadow);
}
.home-account-panel .wxwork-account-container {
padding: 18px;
}
.home-account-panel .wxwork-account-container > h2 {
margin: 0 0 14px;
color: var(--cmd-text);
font-size: 20px;
}
/* Shared page surfaces */
.callback-config-content,
.auto-reply-content,
.after-sales-content,
.engineer-dispatch-content,
.after-sales-knowledge-content,
.operation-logs-content {
width: 100%;
}
.callback-config-content > h2 {
margin: 0 0 16px;
color: var(--cmd-text);
font-size: 24px;
}
.config-form {
width: 100%;
max-width: 760px;
padding: 20px;
}
.form-group {
display: grid;
grid-template-columns: 132px minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.form-group label {
color: var(--cmd-text-soft);
font-size: 13px;
font-weight: 800;
}
.form-group input[type="text"],
.config-form .form-group textarea {
width: 100%;
max-width: none;
min-height: 36px;
border: 1px solid rgba(72, 240, 220, 0.22);
border-radius: 6px;
padding: 8px 10px;
color: var(--cmd-text);
background: rgba(7, 18, 23, 0.92);
font-size: 14px;
font-family: inherit;
}
.config-form .form-group textarea {
min-height: 92px;
resize: vertical;
}
.switch {
position: relative;
display: inline-block;
width: 42px;
height: 22px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
inset: 0;
background-color: rgba(127, 148, 156, 0.45);
transition: .2s;
border-radius: 999px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 2px;
bottom: 2px;
background-color: #ffffff;
transition: .2s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--cmd-cyan-strong);
}
input:checked + .slider:before {
transform: translateX(20px);
}
.status-text {
color: var(--cmd-cyan);
font-size: 13px;
font-weight: 800;
}
/* Element Plus dark command overrides */
.el-popper,
.el-select__popper,
.el-picker__popper {
color: var(--cmd-text) !important;
border: 1px solid rgba(72, 240, 220, 0.28) !important;
background: rgba(8, 20, 26, 0.98) !important;
box-shadow: var(--cmd-shadow) !important;
}
.el-select-dropdown,
.el-picker-panel,
.el-message-box,
.el-dialog {
color: var(--cmd-text) !important;
background: rgba(9, 22, 28, 0.98) !important;
border: 1px solid rgba(72, 240, 220, 0.24) !important;
box-shadow: var(--cmd-shadow), var(--cmd-glow) !important;
}
.el-select-dropdown__item {
color: var(--cmd-text-soft) !important;
}
.el-select-dropdown__item.hover,
.el-select-dropdown__item:hover,
.el-select-dropdown__item.selected {
color: var(--cmd-cyan) !important;
background: rgba(72, 240, 220, 0.12) !important;
}
.el-input__wrapper,
.el-textarea__inner,
.el-select .el-input__wrapper,
.el-select__wrapper {
color: var(--cmd-text) !important;
background: rgba(6, 17, 22, 0.9) !important;
border: 1px solid rgba(72, 240, 220, 0.22) !important;
box-shadow: none !important;
}
.el-input__wrapper:hover,
.el-input__wrapper.is-focus,
.el-select__wrapper:hover,
.el-select__wrapper.is-focused,
.el-textarea__inner:focus {
border-color: rgba(72, 240, 220, 0.62) !important;
box-shadow: 0 0 18px rgba(72, 240, 220, 0.12) !important;
}
.el-input__inner,
.el-textarea__inner,
.el-select__placeholder,
.el-select__selected-item,
.el-select__selected-item span {
color: var(--cmd-text) !important;
caret-color: var(--cmd-cyan);
}
.el-input__inner::placeholder,
.el-textarea__inner::placeholder,
.el-select__placeholder.is-transparent {
color: var(--cmd-text-muted) !important;
}
.el-select__caret,
.el-input__suffix,
.el-input__prefix {
color: var(--cmd-text-soft) !important;
}
.el-table {
--el-table-border-color: rgba(72, 240, 220, 0.16) !important;
--el-table-header-bg-color: rgba(13, 31, 39, 0.98) !important;
--el-table-tr-bg-color: rgba(8, 20, 26, 0.94) !important;
--el-table-row-hover-bg-color: rgba(72, 240, 220, 0.09) !important;
--el-table-current-row-bg-color: rgba(72, 240, 220, 0.12) !important;
color: var(--cmd-text-soft) !important;
background: transparent !important;
}
.el-table th.el-table__cell,
.el-table tr,
.el-table td.el-table__cell,
.el-table__body-wrapper {
background: transparent !important;
}
.el-table th.el-table__cell {
color: #d7feff !important;
background: rgba(13, 31, 39, 0.98) !important;
font-weight: 800;
}
.el-table td.el-table__cell {
color: var(--cmd-text-soft) !important;
border-bottom-color: rgba(72, 240, 220, 0.13) !important;
}
.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell {
background: rgba(255, 255, 255, 0.025) !important;
}
.el-table__body tr:hover > td.el-table__cell {
background: rgba(72, 240, 220, 0.09) !important;
}
.el-table .el-table-fixed-column--right,
.el-table .el-table-fixed-column--left,
.el-table th.el-table-fixed-column--right,
.el-table th.el-table-fixed-column--left,
.el-table td.el-table-fixed-column--right,
.el-table td.el-table-fixed-column--left {
background: rgb(8, 24, 30) !important;
}
.el-table th.el-table-fixed-column--right,
.el-table th.el-table-fixed-column--left {
background: rgb(13, 34, 42) !important;
}
.el-table td.el-table-fixed-column--right::before,
.el-table th.el-table-fixed-column--right::before {
content: "";
position: absolute;
inset: 0 auto 0 -18px;
width: 18px;
pointer-events: none;
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.42));
}
.el-table td.el-table-fixed-column--left::after,
.el-table th.el-table-fixed-column--left::after {
content: "";
position: absolute;
inset: 0 -18px 0 auto;
width: 18px;
pointer-events: none;
background: linear-gradient(90deg, rgba(0, 0, 0, 0.42), transparent);
}
.el-table__empty-text {
color: var(--cmd-text-muted) !important;
}
.el-tabs__item {
color: var(--cmd-text-soft) !important;
font-weight: 800;
}
.el-tabs__item.is-active,
.el-tabs__item:hover {
color: var(--cmd-cyan) !important;
}
.el-tabs__active-bar {
background-color: var(--cmd-cyan) !important;
box-shadow: 0 0 14px rgba(72, 240, 220, 0.42);
}
.el-tabs__nav-wrap::after {
background-color: rgba(72, 240, 220, 0.16) !important;
}
.el-dialog__title,
.el-message-box__title,
.el-dialog__body,
.el-message-box__content {
color: var(--cmd-text) !important;
}
.el-overlay {
background-color: rgba(1, 6, 8, 0.72) !important;
}
.el-switch.is-checked .el-switch__core {
border-color: var(--cmd-cyan-strong) !important;
background-color: var(--cmd-cyan-strong) !important;
}
.el-loading-mask {
background-color: rgba(5, 13, 17, 0.76) !important;
}
@media (max-width: 1200px) {
.overview-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.workspace-columns {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
.layout-container {
min-width: 0;
}
.side-nav {
width: 220px;
}
.main-content {
padding: 16px;
}
.workspace-hero {
align-items: flex-start;
flex-direction: column;
}
.overview-grid,
.detail-list,
.quick-actions {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,93 @@
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,574 @@
<template>
<div class="knowledge-page">
<section class="knowledge-toolbar">
<div>
<h2>售后知识库</h2>
<p>沉淀已处理售后案例并同步到自动客服知识检索</p>
</div>
<div class="toolbar-actions">
<button class="ghost-btn" @click="loadAll" :disabled="busy">刷新</button>
</div>
</section>
<section class="status-grid">
<div class="metric">
<span>已处理案例</span>
<strong>{{ cases.length }}</strong>
</div>
<div class="metric">
<span> Excel 归档</span>
<strong>{{ pendingCount }}</strong>
</div>
<div class="metric">
<span>Excel 表格</span>
<strong>{{ archives.length }}</strong>
</div>
<div class="metric wide">
<span>知识目录</span>
<strong class="path-text">{{ knowledgeDir }}</strong>
</div>
</section>
<el-tabs v-model="activeTab" class="knowledge-tabs">
<el-tab-pane label="已处理案例" name="cases">
<section class="filter-row">
<el-input v-model="caseFilters.keyword" clearable placeholder="搜索客户、群聊、问题、处理方案" />
<el-select v-model="caseFilters.engineer" class="filter-select" filterable>
<el-option label="全部工程师" value="all" />
<el-option label="未分配" value="unassigned" />
<el-option v-for="item in engineerOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-select v-model="caseFilters.room" class="filter-select" filterable>
<el-option label="全部群聊" value="all" />
<el-option v-for="item in roomOptions" :key="item" :label="item" :value="item" />
</el-select>
</section>
<section class="table-panel">
<el-table :data="filteredCases" border stripe height="calc(100vh - 405px)" empty-text="暂无已处理知识案例">
<el-table-column prop="resolvedAt" label="处理时间" width="150" fixed>
<template #default="{ row }">{{ formatDateTime(row.resolvedAt) }}</template>
</el-table-column>
<el-table-column prop="roomName" label="群聊" width="170" />
<el-table-column prop="customerName" label="客户" width="120" />
<el-table-column prop="issueContent" label="问题" min-width="240">
<template #default="{ row }"><div class="multiline">{{ row.issueContent || '-' }}</div></template>
</el-table-column>
<el-table-column prop="resolutionContent" label="最终处理方案" min-width="280">
<template #default="{ row }"><div class="multiline strong-text">{{ row.resolutionContent || '-' }}</div></template>
</el-table-column>
<el-table-column label="负责人" width="150">
<template #default="{ row }">{{ engineerName(row) }}</template>
</el-table-column>
<el-table-column label="知识文件" min-width="260">
<template #default="{ row }">
<span class="path-value">{{ row.markdownPath || '-' }}</span>
<em v-if="row.missingMarkdown" class="missing">文件缺失</em>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<button class="ghost-btn small-btn" @click="editCase(row)">编辑</button>
<button class="ghost-btn small-btn" @click="openCase(row)">打开</button>
</template>
</el-table-column>
</el-table>
</section>
</el-tab-pane>
<el-tab-pane label="Excel归档" name="archives">
<section class="archive-actions">
<button class="primary-btn" @click="archivePending" :disabled="busy || pendingCount === 0">保存本次新增</button>
<button class="ghost-btn" @click="openArchiveFolder" :disabled="busy">打开目录</button>
</section>
<section class="table-panel">
<el-table :data="archives" border stripe height="calc(100vh - 405px)" empty-text="暂无知识库表格">
<el-table-column prop="displayTime" label="保存时间" width="170" fixed>
<template #default="{ row }">{{ row.displayTime || formatDateTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column prop="fileName" label="Excel 文件" min-width="260">
<template #default="{ row }">
<div class="file-cell">
<span>{{ row.fileName }}</span>
<em v-if="row.missingFile">文件缺失</em>
</div>
</template>
</el-table-column>
<el-table-column prop="issueCount" label="问题数" width="110" />
<el-table-column prop="path" label="路径" min-width="360">
<template #default="{ row }"><span class="path-value">{{ row.path }}</span></template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<button class="ghost-btn small-btn" @click="openArchive(row.path)">打开</button>
</template>
</el-table-column>
</el-table>
</section>
</el-tab-pane>
</el-tabs>
<el-dialog v-model="editVisible" title="编辑最终处理方案" width="680px" destroy-on-close>
<div class="edit-dialog">
<p>{{ editingCase.issueContent || '-' }}</p>
<el-input v-model="editingResolution" type="textarea" :rows="8" placeholder="填写工程师确认后的最终处理方案" />
</div>
<template #footer>
<button class="ghost-btn" @click="editVisible = false" :disabled="busy">取消</button>
<button class="primary-btn" @click="saveCase" :disabled="busy || !editingResolution.trim()">保存</button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import {
ArchivePendingAfterSalesIssues,
GetPendingAfterSalesArchiveSummary,
ListAfterSalesKnowledgeArchives,
ListAfterSalesKnowledgeCases,
RevealAfterSalesKnowledgeArchive,
RevealAfterSalesKnowledgeCase,
UpdateAfterSalesKnowledgeCase
} from '../../wailsjs/go/main/App.js'
const activeTab = ref('cases')
const archives = ref([])
const cases = ref([])
const summary = ref({})
const busy = ref(false)
const editVisible = ref(false)
const editingCase = ref({})
const editingResolution = ref('')
const caseFilters = reactive({
keyword: '',
engineer: 'all',
room: 'all'
})
const pendingCount = computed(() => Number(summary.value?.pendingCount || 0))
const knowledgeDir = computed(() => {
const firstPath = cases.value.find(item => item.markdownPath)?.markdownPath || ''
const archivePath = archives.value.find(item => item.path)?.path || ''
const path = firstPath || archivePath
const index = Math.max(path.lastIndexOf('\\'), path.lastIndexOf('/'))
return index > 0 ? path.slice(0, index) : 'config\\knowledge\\after_sales_cases'
})
const engineerOptions = computed(() => uniqueSorted(cases.value.map(engineerName).filter(item => item && item !== '-')))
const roomOptions = computed(() => uniqueSorted(cases.value.map(item => item.roomName).filter(Boolean)))
const filteredCases = computed(() => {
const q = caseFilters.keyword.trim().toLowerCase()
return cases.value
.filter(item => {
if (caseFilters.engineer === 'all') return true
if (caseFilters.engineer === 'unassigned') return engineerName(item) === '-'
return engineerName(item) === caseFilters.engineer
})
.filter(item => caseFilters.room === 'all' || item.roomName === caseFilters.room)
.filter(item => {
if (!q) return true
return [item.roomName, item.customerName, item.issueContent, item.aiSuggestion, item.resolutionContent, engineerName(item)]
.some(text => String(text || '').toLowerCase().includes(q))
})
})
function unwrapResult(result) {
if (!result || typeof result !== 'object') {
return { success: Boolean(result), message: '', data: result }
}
return {
success: result.success !== false,
message: String(result.message || result.error || ''),
data: result.data
}
}
async function loadCases() {
const result = unwrapResult(await ListAfterSalesKnowledgeCases())
if (!result.success) {
ElMessage.error(result.message || '加载已处理案例失败')
cases.value = []
return
}
cases.value = Array.isArray(result.data) ? result.data : []
}
async function loadArchives() {
const result = unwrapResult(await ListAfterSalesKnowledgeArchives())
if (!result.success) {
ElMessage.error(result.message || '加载 Excel 归档失败')
archives.value = []
return
}
archives.value = Array.isArray(result.data) ? result.data : []
}
async function loadSummary() {
const result = unwrapResult(await GetPendingAfterSalesArchiveSummary())
summary.value = result.success ? (result.data || {}) : {}
}
async function loadAll() {
busy.value = true
try {
await Promise.all([loadCases(), loadArchives(), loadSummary()])
} finally {
busy.value = false
}
}
async function archivePending() {
busy.value = true
try {
const result = unwrapResult(await ArchivePendingAfterSalesIssues())
if (!result.success) {
ElMessage.error(result.message || '保存到 Excel 归档失败')
return
}
ElMessage.success(result.message || '已保存到 Excel 归档')
await Promise.all([loadArchives(), loadSummary()])
} catch (err) {
ElMessage.error(`保存到 Excel 归档失败: ${err.message || err}`)
} finally {
busy.value = false
}
}
function editCase(row) {
editingCase.value = row
editingResolution.value = row.resolutionContent || ''
editVisible.value = true
}
async function saveCase() {
busy.value = true
try {
const result = unwrapResult(await UpdateAfterSalesKnowledgeCase(editingCase.value.issueId, editingResolution.value.trim()))
if (!result.success) {
ElMessage.error(result.message || '保存案例失败')
return
}
ElMessage.success('知识案例已更新,知识索引正在后台更新')
editVisible.value = false
await loadCases()
} catch (err) {
ElMessage.error(`保存案例失败: ${err.message || err}`)
} finally {
busy.value = false
}
}
async function openCase(row) {
try {
const result = await RevealAfterSalesKnowledgeCase(row.issueId || '')
const ok = Array.isArray(result) ? result[0] : Boolean(result?.success ?? result)
const msg = Array.isArray(result) ? result[1] : String(result?.message || '')
if (!ok) ElMessage.error(msg || '打开失败')
} catch (err) {
ElMessage.error(`打开失败: ${err.message || err}`)
}
}
async function openArchive(path) {
try {
const result = await RevealAfterSalesKnowledgeArchive(path || '')
const ok = Array.isArray(result) ? result[0] : Boolean(result)
const msg = Array.isArray(result) ? result[1] : ''
if (!ok) ElMessage.error(msg || '打开失败')
} catch (err) {
ElMessage.error(`打开失败: ${err.message || err}`)
}
}
function openArchiveFolder() {
openArchive('')
}
function engineerName(row) {
return row.assignedEngineerName || row.assignedEngineerId || '-'
}
function uniqueSorted(items) {
return [...new Set(items.map(item => String(item || '').trim()).filter(Boolean))]
.sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'))
}
function formatDateTime(value) {
const text = String(value || '')
if (!text) return '-'
const date = new Date(text)
if (Number.isNaN(date.getTime())) return text
const pad = number => String(number).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
}
onMounted(loadAll)
</script>
<style scoped>
.knowledge-page {
color: #18282d;
padding: 18px;
background: #f5f7f8;
border: 1px solid #dfe5e8;
border-radius: 8px;
}
.knowledge-toolbar,
.toolbar-actions,
.filter-row,
.archive-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.knowledge-toolbar {
justify-content: space-between;
gap: 18px;
margin-bottom: 16px;
}
.knowledge-toolbar h2 {
margin: 0 0 6px;
font-size: 24px;
}
.knowledge-toolbar p {
margin: 0;
color: #667085;
font-size: 13px;
}
.status-grid {
display: grid;
grid-template-columns: 150px 150px 150px minmax(260px, 1fr);
gap: 12px;
margin-bottom: 14px;
}
.metric {
min-width: 0;
padding: 14px;
border: 1px solid #dfe5e8;
border-radius: 8px;
background: #fff;
}
.metric span {
display: block;
margin-bottom: 8px;
color: #667085;
font-size: 12px;
}
.metric strong {
display: block;
font-size: 22px;
}
.path-text,
.path-value {
display: block;
overflow: hidden;
color: #475467;
font-family: Consolas, monospace;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.knowledge-tabs {
padding: 12px;
border: 1px solid #dfe5e8;
border-radius: 8px;
background: #fff;
}
.filter-row,
.archive-actions {
margin-bottom: 12px;
}
.filter-row .el-input {
width: 320px;
}
.filter-select {
width: 180px;
}
.primary-btn,
.ghost-btn {
min-height: 34px;
border-radius: 6px;
padding: 0 12px;
font-weight: 700;
cursor: pointer;
}
.primary-btn {
border: 1px solid #16706d;
background: #16706d;
color: #fff;
}
.ghost-btn {
border: 1px solid #16706d;
background: #fff;
color: #16706d;
}
.small-btn {
min-height: 30px;
padding: 0 9px;
}
button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.table-panel {
min-height: 320px;
}
.multiline {
white-space: pre-wrap;
line-height: 1.5;
color: #24363b;
}
.strong-text {
font-weight: 600;
}
.missing,
.file-cell em {
display: block;
margin-top: 4px;
color: #b42318;
font-size: 12px;
font-style: normal;
}
.edit-dialog p {
margin: 0 0 12px;
color: #475467;
line-height: 1.5;
white-space: pre-wrap;
}
@media (max-width: 1080px) {
.status-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.filter-row .el-input,
.filter-select {
width: 100%;
}
}
/* Command center skin */
.knowledge-page {
color: var(--cmd-text);
border-color: var(--cmd-line);
background: transparent;
}
.knowledge-toolbar,
.metric,
.knowledge-tabs {
border-color: var(--cmd-line);
background: var(--cmd-panel);
box-shadow: var(--cmd-shadow);
}
.knowledge-toolbar {
position: relative;
overflow: hidden;
padding: 18px;
border: 1px solid var(--cmd-line);
border-radius: var(--cmd-radius);
background:
linear-gradient(135deg, rgba(22, 49, 60, 0.9), rgba(8, 20, 26, 0.96)),
var(--cmd-panel);
}
.knowledge-toolbar::before,
.metric::before {
content: "";
position: absolute;
inset: 0 0 auto;
height: 2px;
background: linear-gradient(90deg, var(--cmd-cyan), transparent);
}
.knowledge-toolbar h2 {
color: var(--cmd-text);
}
.knowledge-toolbar p,
.metric span,
.edit-dialog p {
color: var(--cmd-text-soft);
}
.metric {
position: relative;
overflow: hidden;
}
.metric strong {
color: var(--cmd-text);
text-shadow: 0 0 18px rgba(72, 240, 220, 0.12);
}
.path-text,
.path-value {
color: #9fd9df;
}
.knowledge-tabs {
background: rgba(8, 20, 26, 0.92);
}
.multiline {
color: var(--cmd-text-soft);
}
.strong-text {
color: var(--cmd-text);
}
.missing,
.file-cell em {
color: var(--cmd-red);
}
.edit-dialog :deep(.el-textarea__inner) {
color: var(--cmd-text);
background: rgba(5, 15, 20, 0.94);
border-color: rgba(72, 240, 220, 0.24);
}
.primary-btn {
color: #031316;
border-color: rgba(72, 240, 220, 0.82);
background: linear-gradient(135deg, var(--cmd-cyan), var(--cmd-blue));
box-shadow: 0 0 20px rgba(72, 240, 220, 0.16);
}
.ghost-btn {
color: var(--cmd-cyan);
border-color: rgba(72, 240, 220, 0.38);
background: rgba(9, 23, 29, 0.84);
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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>

View File

@@ -0,0 +1,533 @@
<template>
<div v-if="isVisible" class="login-modal-overlay">
<div class="login-modal">
<div class="login-header">
<h2>账号登录</h2>
</div>
<form @submit.prevent="handleLogin" class="login-form">
<div class="form-group">
<label for="username">账号</label>
<input id="username" v-model="username" type="text" placeholder="请输入账号" required />
</div>
<div class="form-group">
<label for="password">密码</label>
<input id="password" v-model="password" type="password" placeholder="请输入密码" required />
</div>
<div class="form-group">
<label for="captcha">验证码</label>
<div class="captcha-container">
<input id="captcha" v-model="captcha" type="text" placeholder="请输入验证码" maxlength="4" required />
<img :src="captchaImage" alt="验证码" @click="refreshCaptcha" class="captcha-image" />
</div>
</div>
<!--<div class="checkbox-group">
<label class="checkbox-label">
<input v-model="rememberPassword" type="checkbox" />
记住密码
</label>
</div>-->
<div class="checkbox-group">
<label class="checkbox-label">
<input v-model="agreeTerms" type="checkbox" required />
(勾选即代表同意)软件仅用于测试学习场景不得用于非法途径
</label>
</div>
<div class="form-actions">
<button type="submit" class="login-btn" :disabled="!agreeTerms">
登录
</button>
<a href="#" class="forgot-password" @click.prevent="handleForgotPassword">
忘记密码
</a>
</div>
</form>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { SaveCallbackConfig, GetCallbackConfig } from '../../wailsjs/go/main/App.js';
const props = defineProps({
isVisible: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['login-success', 'login-failed']);
const username = ref('');
const password = ref('');
const captcha = ref('');
const rememberPassword = ref(false);
const agreeTerms = ref(false);
const errorMessage = ref('');
const captchaImage = ref('');
// 生成简单的验证码
const generateCaptcha = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < 4; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
};
const captchaText = ref('');
const refreshCaptcha = () => {
captchaText.value = generateCaptcha();
// 创建简单的SVG验证码
const svgCaptcha = `
<svg width="100" height="40" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f0f0f0"/>
<text x="10" y="25" font-family="Arial" font-size="20" fill="#333"
letter-spacing="8" transform="rotate(-5 10 25)">
${captchaText.value}
</text>
<path d="M0,20 Q50,10 100,30" stroke="#ddd" stroke-width="1" fill="none"/>
<path d="M0,30 Q50,40 100,20" stroke="#ddd" stroke-width="1" fill="none"/>
</svg>
`;
captchaImage.value = 'data:image/svg+xml;base64,' + btoa(svgCaptcha);
};
const handleLogin = async () => {
// 验证码验证
if (captcha.value.toUpperCase() !== captchaText.value.toUpperCase()) {
errorMessage.value = '验证码错误,请重新输入';
refreshCaptcha();
return;
}
// 基础验证
if (!username.value || !password.value) {
errorMessage.value = '请输入账号和密码';
return;
}
try {
errorMessage.value = '';
//const response = await fetch('https://crm.cxier.com/admin-api/system/auth/work-pw-login', {
const response = await fetch('https://crm.yelangjiang.com/admin-api/system/auth/work-pw-login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username.value,
password: password.value
})
});
const result = await response.json();
if (result.code === 0 && result.data) {
// 登录成功
const { userId, accessToken, refreshToken, expiresTime } = result.data;
// 保存登录信息到本地存储
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
localStorage.setItem('userId', userId.toString());
localStorage.setItem('tokenExpires', expiresTime.toString());
// 将accessToken保存到config.json的callbackToken字段
try {
// 先获取当前配置
const currentConfig = await window.go.main.App.GetCallbackConfig();
// 构建正确的CallbackConfig结构
const callbackConfigData = {
// 获取现有的callbackConfig或创建一个空对象
...(currentConfig && typeof currentConfig === 'object' && currentConfig.callbackConfig ? currentConfig.callbackConfig : {}),
// 更新callbackToken字段
callbackToken: accessToken,
// 确保所有必需字段都存在
callbackUrl: currentConfig && currentConfig.callbackConfig && currentConfig.callbackConfig.callbackUrl ? currentConfig.callbackConfig.callbackUrl : '',
httpPort: currentConfig && currentConfig.callbackConfig && currentConfig.callbackConfig.httpPort ? currentConfig.callbackConfig.httpPort : '10001',
enableCallback: currentConfig && currentConfig.callbackConfig && currentConfig.callbackConfig.enableCallback !== undefined ? currentConfig.callbackConfig.enableCallback : false,
enableCloudAuth: currentConfig && currentConfig.callbackConfig && currentConfig.callbackConfig.enableCloudAuth !== undefined ? currentConfig.callbackConfig.enableCloudAuth : false,
fileUploadUrl: currentConfig && currentConfig.callbackConfig && currentConfig.callbackConfig.fileUploadUrl ? currentConfig.callbackConfig.fileUploadUrl : '',
deviceCode: currentConfig && currentConfig.callbackConfig && currentConfig.callbackConfig.deviceCode ? currentConfig.callbackConfig.deviceCode : ''
};
const jsonData = JSON.stringify(callbackConfigData);
await SaveCallbackConfig(jsonData);
} catch (err) {
console.error('保存callbackToken失败:', err);
}
// 记住密码功能
if (rememberPassword.value) {
localStorage.setItem('rememberedUsername', username.value);
localStorage.setItem('rememberedPassword', password.value);
} else {
localStorage.removeItem('rememberedUsername');
localStorage.removeItem('rememberedPassword');
}
// 触发登录成功事件
emit('login-success', {
username: username.value,
userId,
accessToken,
rememberPassword: rememberPassword.value
});
} else {
// 登录失败
errorMessage.value = result.msg || '登录失败,请检查账号密码';
refreshCaptcha();
}
} catch (error) {
console.error('登录请求失败:', error);
errorMessage.value = '网络连接失败,请检查网络后重试';
refreshCaptcha();
}
};
const handleForgotPassword = () => {
errorMessage.value = '请联系软件提供者重置密码';
};
// 加载记住的密码
const loadRememberedCredentials = () => {
const rememberedUsername = localStorage.getItem('rememberedUsername');
const rememberedPassword = localStorage.getItem('rememberedPassword');
if (rememberedUsername && rememberedPassword) {
username.value = rememberedUsername;
password.value = rememberedPassword;
rememberPassword.value = true;
}
};
onMounted(() => {
refreshCaptcha();
loadRememberedCredentials();
});
</script>
<style scoped>
.login-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.login-modal {
background: white;
border-radius: 20px;
padding: 50px 60px;
width: 420px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
animation: slideUp 0.4s ease-out;
position: relative;
overflow: hidden;
}
.login-modal::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2);
}
@keyframes slideUp {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.login-header h2 {
margin: 0 0 40px 0;
text-align: center;
color: #2c3e50;
font-size: 28px;
font-weight: 700;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.login-form {
width: 100%;
text-align: left;
margin: 0;
padding: 0;
}
.form-group {
margin-bottom: 20px;
text-align: left;
margin-left: 0;
padding-left: 0;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-size: 14px;
font-weight: 500;
text-align: left;
width: 40px;
}
.form-group input[type="text"] {
width: 100%;
padding: 15px 18px;
border: 2px solid #e8ecf0;
border-radius: 12px;
font-size: 16px;
box-sizing: border-box;
text-align: left;
display: block;
margin-left: -3px;
transition: all 0.3s ease;
background: #f8fafb;
}
.form-group input[type="password"] {
width: 100%;
padding: 15px 18px;
border: 2px solid #e8ecf0;
border-radius: 12px;
font-size: 16px;
box-sizing: border-box;
text-align: left;
display: block;
margin: 0;
transition: all 0.3s ease;
background: #f8fafb;
}
.form-group input[type="text"]:focus,
.form-group input[type="password"]:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
transform: translateY(-1px);
}
.form-group input[type="text"]:hover,
.form-group input[type="password"]:hover {
border-color: #667eea;
background: white;
}
.captcha-group {
text-align: left;
}
.captcha-container {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.captcha-container input {
flex: 1;
min-width: 0;
}
.captcha-image {
height: 48px;
cursor: pointer;
border: 2px solid #e8ecf0;
border-radius: 12px;
background: linear-gradient(135deg, #f8fafb, #e8ecf0);
flex-shrink: 0;
transition: all 0.3s ease;
}
.captcha-image:hover {
border-color: #667eea;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.checkbox-group {
display: flex;
align-items: flex-start;
justify-content: flex-start;
}
.checkbox-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-size: 14px;
font-weight: 500;
text-align: left;
width: auto;
}
.checkbox-label {
display: flex;
align-items: flex-start;
cursor: pointer;
font-size: 15px;
color: #5a6c7d;
text-align: left;
line-height: 1.5;
transition: color 0.3s ease;
width: 100%;
box-sizing: border-box;
}
.checkbox-label:hover {
color: #667eea;
}
.checkbox-label input[type="checkbox"] {
margin-right: 8px;
width: auto;
}
.form-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 30px;
}
.login-btn {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
padding: 16px 40px;
border-radius: 12px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
width: 150px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
position: relative;
overflow: hidden;
}
.login-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.login-btn:hover:not(:disabled)::before {
left: 100%;
}
.login-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}
.login-btn:hover:not(:disabled) {
background: #40a9ff;
}
.login-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.forgot-password {
color: #1890ff;
text-decoration: none;
font-size: 14px;
}
.forgot-password:hover {
text-decoration: underline;
}
.error-message {
color: #ff4d4f;
text-align: center;
margin-top: 10px;
font-size: 14px;
}
.checkmark {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid #e8ecf0;
border-radius: 4px;
margin-right: 10px;
position: relative;
transition: all 0.3s ease;
background: white;
}
.checkbox-label:hover .checkmark {
border-color: #667eea;
}
.checkbox-label input[type="checkbox"]:checked+.checkmark {
background: linear-gradient(135deg, #667eea, #764ba2);
border-color: #667eea;
}
.checkbox-label input[type="checkbox"]:checked+.checkmark::after {
content: '✓';
position: absolute;
top: 0;
left: 3px;
color: white;
font-size: 12px;
font-weight: bold;
width: 100%;
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<div class="operation-logs-container">
<div class="logs-header">
<h2>操作记录</h2>
</div>
<div v-if="errorMessage" class="logs-error">
{{ errorMessage }}
</div>
<div class="logs-table-container">
<el-table v-loading="isLoading" :data="sortedLogs" class="logs-table" border>
<el-table-column prop="time" label="时间" width="90" />
<el-table-column prop="source" label="来源" width="110" />
<el-table-column prop="type" label="类型" width="80">
<template #default="scope">
<span
:class="{
'text-info': scope.row.type === 'info',
'text-warning': scope.row.type === 'warning',
'text-danger': scope.row.type === 'error'
}"
>
{{ scope.row.type }}
</span>
</template>
</el-table-column>
<el-table-column prop="content" label="内容" min-width="260" />
<el-table-column prop="duration" label="耗时(ms)" width="110" align="right" />
</el-table>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue';
import { DebugLoadLogEntries, LogFrontend } from '../../wailsjs/go/main/App.js';
const logs = ref([]);
const isLoading = ref(false);
const errorMessage = ref('');
const sortedLogs = computed(() => {
return [...logs.value].sort((a, b) => Number(b.id || 0) - Number(a.id || 0)).slice(0, 10);
});
async function loadOperationLogs() {
isLoading.value = true;
try {
LogFrontend('info', 'loading operation logs...');
const result = await DebugLoadLogEntries();
LogFrontend('info', 'operation logs loaded', result);
if (Array.isArray(result?.entries)) {
logs.value = result.entries;
errorMessage.value = result.success === false
? (result.error || '操作日志文件损坏/读取失败,当前显示内存中的临时记录。')
: '';
return;
}
logs.value = [];
errorMessage.value = result?.error || '操作日志返回格式异常。';
} catch (error) {
logs.value = [];
errorMessage.value = error?.message || '操作日志加载失败。';
LogFrontend('error', 'operation log load threw: ' + errorMessage.value);
} finally {
isLoading.value = false;
}
}
onMounted(() => {
loadOperationLogs();
});
</script>
<style scoped>
.operation-logs-container {
width: 100%;
padding: 20px;
box-sizing: border-box;
color: #18282d;
}
.logs-header {
margin-bottom: 16px;
text-align: left;
}
.logs-header h2 {
margin: 0;
color: #18282d;
font-size: 24px;
font-weight: 600;
}
.logs-error {
margin-bottom: 12px;
padding: 10px 12px;
border: 1px solid #f3c7c7;
border-radius: 6px;
background: #fff2f2;
color: #9b1c1c;
font-size: 13px;
}
.logs-table-container {
overflow-x: auto;
border: 1px solid #dfe5e8;
border-radius: 8px;
background-color: #fff;
box-shadow: none;
}
.logs-table {
width: 100%;
border-collapse: collapse;
}
.logs-table :deep(.el-table__header-wrapper) th {
background-color: #f8fafb;
font-weight: 600;
color: #415056;
font-size: 14px;
}
.logs-table :deep(.el-table__body-wrapper) td {
font-size: 13px;
padding: 8px 1px;
}
.logs-table :deep(.el-table__body-wrapper) tr:hover {
background-color: #f8fafb;
}
.text-info {
color: #16706d;
font-weight: 500;
}
.text-warning {
color: #e6a23c;
font-weight: 500;
}
.text-danger {
color: #f56c6c;
font-weight: 500;
}
/* Command center skin */
.operation-logs-container {
color: var(--cmd-text);
border-color: var(--cmd-line);
background: var(--cmd-panel);
box-shadow: var(--cmd-shadow);
}
.logs-header h2 {
color: var(--cmd-text);
}
.logs-error {
color: var(--cmd-red);
border-color: rgba(255, 107, 125, 0.34);
background: rgba(255, 107, 125, 0.1);
}
.logs-table-container {
border-color: var(--cmd-line);
background: rgba(8, 20, 26, 0.9);
}
.logs-table :deep(.el-table__header-wrapper) th {
color: var(--cmd-text);
background-color: rgba(13, 31, 39, 0.98);
}
.logs-table :deep(.el-table__body-wrapper) td {
color: var(--cmd-text-soft);
}
.logs-table :deep(.el-table__body-wrapper) tr:hover {
background-color: rgba(72, 240, 220, 0.09);
}
.text-info {
color: var(--cmd-cyan);
}
.text-warning {
color: var(--cmd-amber);
}
.text-danger {
color: var(--cmd-red);
}
</style>

View File

@@ -0,0 +1,548 @@
<template>
<div class="wxwork-account-container">
<div class="account-toolbar-actions">
<button class="btn login-btn" @click="handleStartNewInstance" :disabled="startingInstance">
{{ startingInstance ? '启动中...' : '新增企微实例' }}
</button>
<button class="btn refresh-btn" @click="fetchAccountInfo" :disabled="loading">刷新</button>
</div>
<h2>企微账号信息</h2>
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="error" class="error-message">{{ error }}</div>
<div v-else-if="accounts.length > 0" class="accounts-list">
<div v-for="account in accounts" :key="account.user_id" class="account-card">
<!-- 头像区域 -->
<div class="avatar-container">
<img :src="account.avatar || '/default-avatar.png'" alt="用户头像" class="avatar">
<span class="status-indicator" :class="account.status === 1 ? 'online' : 'offline'"> </span>
</div>
<!-- 信息区域 -->
<div class="info-container">
<p class="user-id">clientId: {{ account.client_id || account.clientId || '-' }} / PID: {{ account.pid || '-' }}</p>
<p class="user-id">巡检: {{ account.health_message || account.healthMessage || account.runtime_status || '-' }}</p>
<p class="user-id">最后消息: {{ account.last_message_at || account.lastMessageAt || '-' }}</p>
<div class="name-status">
<h3>{{ account.username || account.name || '未知用户' }}</h3>
<span class="status-badge" :class="account.status === 1 ? 'online' : 'offline'">
{{ account.status === 1 ? '在线' : '离线' }}
</span>
</div>
<p class="user-id">用户ID: {{ account.user_id || account.userId || '未知ID' }}</p>
<!-- <p class="username">用户名: {{ account.username || account.name || '未知用户名' }}</p> -->
</div>
<!-- 操作按钮区域 -->
<div class="action-buttons">
<button v-if="account.status === 1" class="btn logout-btn" @click="handleLogout(account.user_id)">
下线
</button>
<button v-else class="btn delete-btn" @click="handleDelete(account.user_id)">
删除
</button>
</div>
</div>
</div>
<div v-else class="empty-state">
<i class="fas fa-user-slash"></i>
<h3>暂无企微账号</h3>
<p v-if="diagnostic" class="diagnostic">{{ diagnostic }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { LogFrontend, SendWxWorkData, StartNewWxWorkInstance } from '../../wailsjs/go/main/App.js'
// 移除旧的日志导入
// import { LogInfo, LogError } from '@/../wailsjs/runtime/runtime.js';
// 接收父组件传递的账号信息
const props = defineProps({
accountInfo: {
type: Object,
default: null
}
});
// 浏览器环境下的完整Wails运行时模拟 - 提供所有可能用到的API
if (typeof window.runtime === 'undefined') {
window.runtime = {
// 日志相关方法
LogPrint: console.log,
LogTrace: console.trace,
LogDebug: console.debug,
LogInfo: console.info,
LogWarning: console.warn,
LogError: console.error,
LogFatal: console.error,
// 事件相关方法
EventsOnMultiple: () => console.log('模拟: EventsOnMultiple'),
EventsOn: () => console.log('模拟: EventsOn'),
EventsOff: () => console.log('模拟: EventsOff'),
EventsOnce: () => console.log('模拟: EventsOnce'),
EventsEmit: () => console.log('模拟: EventsEmit'),
// 窗口相关方法
WindowReload: () => console.log('模拟: WindowReload'),
WindowReloadApp: () => console.log('模拟: WindowReloadApp'),
WindowSetAlwaysOnTop: () => console.log('模拟: WindowSetAlwaysOnTop'),
WindowSetSystemDefaultTheme: () => console.log('模拟: WindowSetSystemDefaultTheme'),
WindowSetLightTheme: () => console.log('模拟: WindowSetLightTheme'),
WindowSetDarkTheme: () => console.log('模拟: WindowSetDarkTheme'),
WindowCenter: () => console.log('模拟: WindowCenter'),
WindowSetTitle: () => console.log('模拟: WindowSetTitle'),
WindowFullscreen: () => console.log('模拟: WindowFullscreen'),
WindowUnfullscreen: () => console.log('模拟: WindowUnfullscreen'),
WindowIsFullscreen: () => false,
WindowGetSize: () => ({ width: 800, height: 600 }),
WindowSetSize: () => console.log('模拟: WindowSetSize'),
WindowSetMaxSize: () => console.log('模拟: WindowSetMaxSize'),
WindowSetMinSize: () => console.log('模拟: WindowSetMinSize'),
WindowSetPosition: () => console.log('模拟: WindowSetPosition'),
WindowGetPosition: () => ({ x: 100, y: 100 }),
WindowHide: () => console.log('模拟: WindowHide'),
WindowShow: () => console.log('模拟: WindowShow'),
WindowMaximise: () => console.log('模拟: WindowMaximise'),
WindowToggleMaximise: () => console.log('模拟: WindowToggleMaximise'),
WindowUnmaximise: () => console.log('模拟: WindowUnmaximise'),
WindowIsMaximised: () => false,
WindowMinimise: () => console.log('模拟: WindowMinimise'),
WindowUnminimise: () => console.log('模拟: WindowUnminimise'),
WindowSetBackgroundColour: () => console.log('模拟: WindowSetBackgroundColour'),
WindowIsMinimised: () => false,
WindowIsNormal: () => true,
// 其他功能方法
ScreenGetAll: () => [{ id: 1, width: 1920, height: 1080 }],
BrowserOpenURL: (url) => console.log(`模拟: 打开URL ${url}`),
Environment: () => ({ os: 'browser' }),
Quit: () => console.log('模拟: Quit'),
Hide: () => console.log('模拟: Hide'),
Show: () => console.log('模拟: Show'),
ClipboardGetText: () => '',
ClipboardSetText: (text) => console.log(`模拟: 设置剪贴板文本 ${text}`),
OnFileDrop: () => console.log('模拟: OnFileDrop'),
OnFileDropOff: () => console.log('模拟: OnFileDropOff'),
CanResolveFilePaths: () => false,
ResolveFilePaths: (files) => files
};
}
// 状态变量
const loading = ref(false);
const error = ref('');
const accounts = ref([]);
const diagnostic = ref('');
const startingInstance = ref(false);
// 计算属性,优先使用父组件传入的数据,如果没有则使用本地数据
const displayAccountInfo = computed(() => {
const result = props.accountInfo || localAccountInfo.value || null;
LogFrontend('info', `displayAccountInfo计算结果: ${result ? '有数据' : '无数据'}`);
if (result) {
console.log('displayAccountInfo详情:', result);
}
return result;
});
// 获取企微账号信息
async function fetchAccountInfo() {
LogFrontend('info', '开始获取企微账号信息');
loading.value = true;
error.value = '';
diagnostic.value = '';
try {
LogFrontend('info', '准备调用GetWxWorkAccountList方法');
// 通过主程序加载企微账号列表
if (window.go?.main?.App?.GetWxWorkAccountList) {
const result = await window.go.main.App.GetWxWorkAccountList();
// 处理Wails返回的单个值 - 优化处理逻辑
LogFrontend('info', 'Wails后端返回结果', {
result,
resultType: typeof result,
isArray: Array.isArray(result),
hasSuccess: result && typeof result === 'object' && result.success !== undefined,
hasData: result && typeof result === 'object' && result.data !== undefined
});
// 处理新的统一对象格式
if (result && typeof result === 'object') {
const { success, error: errorMsg, data, diagnostic: diagnosticMsg } = result;
if (success !== false && Array.isArray(data)) {
accounts.value = data;
diagnostic.value = diagnosticMsg || '';
LogFrontend('info', `成功加载 ${data.length} 个企微账号(对象格式)`);
} else {
LogFrontend('warn', errorMsg || '获取数据失败,使用空数组');
accounts.value = [];
diagnostic.value = diagnosticMsg || errorMsg || '';
}
} else if (Array.isArray(result)) {
// 兼容旧格式:直接数组
accounts.value = result;
LogFrontend('info', `成功加载 ${result.length} 个企微账号(数组格式)`);
} else {
LogFrontend('warn', '返回数据格式未知,使用空数组', result);
accounts.value = [];
diagnostic.value = '';
}
} else {
LogFrontend('error', 'Wails方法不可用');
error.value = 'Wails方法不可用';
accounts.value = [];
diagnostic.value = '';
}
} catch (err) {
const errorMessage = err?.message || '加载企微账号信息失败';
LogFrontend('error', `加载过程中出现异常: ${errorMessage}`);
error.value = errorMessage;
accounts.value = [];
diagnostic.value = '';
} finally {
loading.value = false;
}
}
// 处理退出
async function handleLogout(userId) {
LogFrontend('info', `尝试退出企微账号: ${userId}`);
try {
// 找到对应的账号并更新状态
const account = accounts.value.find(acc => acc.user_id === userId);
if (account) {
account.status = 0;
}
LogFrontend('info', `账号 ${userId} 已设置为离线状态`);
const requestData = {
"type": 11112,
"data": {}
};
const jsonData = JSON.stringify(requestData);
// 调用后端SendWxWorkData函数客户端ID暂时使用0
const result = await SendWxWorkData(userId, jsonData);
} catch (err) {
const errorMessage = err && err.message ? err.message : '退出过程中出现异常';
LogFrontend('error', errorMessage);
}
}
// 处理删除
async function handleDelete(userId) {
if (confirm('确定要删除此企微账号吗?')) {
LogFrontend('info', `删除企微账号: ${userId}`);
try {
// 从列表中删除账号
accounts.value = accounts.value.filter(acc => acc.user_id !== userId);
LogFrontend('info', `账号 ${userId} 已从列表中删除`);
// 调用后端方法删除client_status.json中的对应数据
if (window.go?.main?.App?.DeleteWxWorkAccount) {
LogFrontend('info', `调用后端删除client_status.json中的账号: ${userId}`);
const result = await window.go.main.App.DeleteWxWorkAccount(userId);
LogFrontend('info', `后端删除结果: ${result}`);
} else {
LogFrontend('warn', '后端删除方法不可用,仅删除了前端列表中的账号');
}
} catch (err) {
const errorMessage = err && err.message ? err.message : '删除过程中出现异常';
LogFrontend('error', errorMessage);
}
}
}
// 生命周期钩子
async function handleStartNewInstance() {
startingInstance.value = true;
error.value = '';
try {
const result = await StartNewWxWorkInstance();
if (!result?.success) {
error.value = result?.message || '新增企微实例失败';
return;
}
diagnostic.value = `${result.message || '已发送新增企微实例请求'},请在新窗口扫码登录。`;
await fetchAccountInfo();
} catch (err) {
error.value = err?.message || String(err || '新增企微实例失败');
} finally {
startingInstance.value = false;
}
}
onMounted(() => {
fetchAccountInfo();
});
</script>
<style scoped>
.wxwork-account-container {
padding: 20px;
color: #18282d;
}
.account-toolbar-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin: 0 0 16px;
}
.loading {
text-align: center;
padding: 20px;
color: #637379;
}
.diagnostic {
margin-top: 10px;
color: #667085;
font-size: 14px;
line-height: 1.6;
}
.error-message {
color: #9b1c1c;
padding: 10px 12px;
background-color: #fdecec;
border-radius: 6px;
}
.accounts-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.account-card {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 8px;
border: 1px solid #dfe5e8;
box-shadow: none;
padding: 16px 18px;
width: 100%;
box-sizing: border-box;
}
.avatar-container {
position: relative;
margin-right: 20px;
flex-shrink: 0;
}
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #eef2f4;
}
.status-indicator {
position: absolute;
bottom: 3px;
right: 3px;
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid white;
}
.status-indicator.online {
background-color: #16706d;
}
.status-indicator.offline {
background-color: #95a5a6;
}
.info-container {
flex: 1;
margin-right: 20px;
}
.name-status {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 5px;
}
h3 {
font-size: 18px;
margin: 0;
color: #18282d;
}
.status-badge {
padding: 3px 7px;
border-radius: 999px;
font-size: 11px;
font-weight: bold;
color: white;
}
.status-badge.online {
background-color: #16706d;
}
.status-badge.offline {
background-color: #95a5a6;
}
.user-id {
color: #637379;
margin: 0;
font-size: 14px;
}
.action-buttons {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.btn {
min-height: 34px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.2s, border-color 0.2s;
}
.login-btn {
background-color: #16706d;
color: white;
}
.login-btn:hover {
background-color: #115c5a;
}
.logout-btn {
background-color: #fff;
color: #9b1c1c;
border-color: #f0b8b8;
}
.logout-btn:hover {
background-color: #fdecec;
}
.delete-btn {
background-color: #fff;
color: #8a5a00;
border-color: #ead19a;
}
.delete-btn:hover {
background-color: #fff8e6;
}
.empty-state {
text-align: center;
padding: 40px 0;
color: #637379;
}
/* Command center skin */
.wxwork-account-container {
color: var(--cmd-text);
}
.loading,
.diagnostic,
.user-id,
.empty-state {
color: var(--cmd-text-soft);
}
.account-card {
border-color: var(--cmd-line);
background:
linear-gradient(135deg, rgba(18, 42, 52, 0.92), rgba(8, 20, 26, 0.96)),
var(--cmd-panel);
box-shadow: var(--cmd-shadow);
}
.account-card:hover {
border-color: rgba(72, 240, 220, 0.42);
box-shadow: var(--cmd-shadow), 0 0 24px rgba(72, 240, 220, 0.12);
}
.avatar {
border-color: rgba(72, 240, 220, 0.32);
box-shadow: 0 0 22px rgba(72, 240, 220, 0.12);
}
.status-indicator {
border-color: #071014;
}
.status-indicator.online,
.status-badge.online {
background: var(--cmd-green);
}
.status-indicator.offline,
.status-badge.offline {
background: #6f858e;
}
h3 {
color: var(--cmd-text);
}
.status-badge {
color: #031316;
}
.error-message {
color: var(--cmd-red);
border: 1px solid rgba(255, 107, 125, 0.34);
background: rgba(255, 107, 125, 0.1);
}
.login-btn {
color: #031316;
border-color: rgba(72, 240, 220, 0.82);
background: linear-gradient(135deg, var(--cmd-cyan), var(--cmd-blue));
box-shadow: 0 0 20px rgba(72, 240, 220, 0.16);
}
.login-btn:hover {
background: linear-gradient(135deg, #7ff7e9, #8fc2ff);
}
.logout-btn {
color: #ffd8dd;
border-color: rgba(255, 107, 125, 0.48);
background: rgba(255, 107, 125, 0.1);
}
.logout-btn:hover {
background: rgba(255, 107, 125, 0.16);
}
.delete-btn {
color: var(--cmd-amber);
border-color: rgba(255, 209, 102, 0.42);
background: rgba(255, 209, 102, 0.1);
}
.delete-btn:hover {
background: rgba(255, 209, 102, 0.16);
}
</style>

427
frontend/src/main.js Normal file
View File

@@ -0,0 +1,427 @@
// 浏览器环境下的完整Wails运行时模拟 - 在导入Wails库之前执行
if (typeof window.runtime === 'undefined') {
window.runtime = {
// 日志相关方法 - 生产环境使用新的LogFrontend接口
LogPrint: (message) => {
if (window.go && window.go.main && window.go.main.App && window.go.main.App.LogFrontend) {
window.go.main.App.LogFrontend('info', message);
} else {
console.log(message);
}
},
LogTrace: (message) => {
if (window.go && window.go.main && window.go.main.App && window.go.main.App.LogFrontend) {
window.go.main.App.LogFrontend('debug', message);
} else {
console.trace(message);
}
},
LogDebug: (message) => {
if (window.go && window.go.main && window.go.main.App && window.go.main.App.LogFrontend) {
window.go.main.App.LogFrontend('debug', message);
} else {
console.debug(message);
}
},
LogInfo: (message) => {
if (window.go && window.go.main && window.go.main.App && window.go.main.App.LogFrontend) {
window.go.main.App.LogFrontend('info', message);
} else {
console.info(message);
}
},
LogWarning: (message) => {
if (window.go && window.go.main && window.go.main.App && window.go.main.App.LogFrontend) {
window.go.main.App.LogFrontend('warning', message);
} else {
console.warn(message);
}
},
LogError: (message) => {
if (window.go && window.go.main && window.go.main.App && window.go.main.App.LogFrontend) {
window.go.main.App.LogFrontend('error', message);
} else {
console.error(message);
}
},
LogFatal: (message) => {
if (window.go && window.go.main && window.go.main.App && window.go.main.App.LogFrontend) {
window.go.main.App.LogFrontend('error', 'FATAL: ' + message);
} else {
console.error('FATAL: ' + message);
}
},
// 事件相关方法
EventsOnMultiple: () => console.log('模拟: EventsOnMultiple'),
EventsOn: () => console.log('模拟: EventsOn'),
EventsOff: () => console.log('模拟: EventsOff'),
EventsOnce: () => console.log('模拟: EventsOnce'),
EventsEmit: () => console.log('模拟: EventsEmit'),
// 窗口相关方法
WindowReload: () => console.log('模拟: WindowReload'),
WindowReloadApp: () => console.log('模拟: WindowReloadApp'),
WindowSetAlwaysOnTop: () => console.log('模拟: WindowSetAlwaysOnTop'),
WindowSetSystemDefaultTheme: () => console.log('模拟: WindowSetSystemDefaultTheme'),
WindowSetLightTheme: () => console.log('模拟: WindowSetLightTheme'),
WindowSetDarkTheme: () => console.log('模拟: WindowSetDarkTheme'),
WindowCenter: () => console.log('模拟: WindowCenter'),
WindowSetTitle: () => console.log('模拟: WindowSetTitle'),
WindowFullscreen: () => console.log('模拟: WindowFullscreen'),
WindowUnfullscreen: () => console.log('模拟: WindowUnfullscreen'),
WindowIsFullscreen: () => false,
WindowGetSize: () => ({ width: 800, height: 600 }),
WindowSetSize: () => console.log('模拟: WindowSetSize'),
WindowSetMaxSize: () => console.log('模拟: WindowSetMaxSize'),
WindowSetMinSize: () => console.log('模拟: WindowSetMinSize'),
WindowSetPosition: () => console.log('模拟: WindowSetPosition'),
WindowGetPosition: () => ({ x: 100, y: 100 }),
WindowHide: () => console.log('模拟: WindowHide'),
WindowShow: () => console.log('模拟: WindowShow'),
WindowMaximise: () => console.log('模拟: WindowMaximise'),
WindowToggleMaximise: () => console.log('模拟: WindowToggleMaximise'),
WindowUnmaximise: () => console.log('模拟: WindowUnmaximise'),
WindowIsMaximised: () => false,
WindowMinimise: () => console.log('模拟: WindowMinimise'),
WindowUnminimise: () => console.log('模拟: WindowUnminimise'),
WindowSetBackgroundColour: () => console.log('模拟: WindowSetBackgroundColour'),
WindowIsMinimised: () => false,
WindowIsNormal: () => true,
// 其他功能方法
ScreenGetAll: () => [{ id: 1, width: 1920, height: 1080 }],
BrowserOpenURL: (url) => console.log(`模拟: 打开URL ${url}`),
Environment: () => ({ os: 'browser' }),
Quit: () => console.log('模拟: Quit'),
Hide: () => console.log('模拟: Hide'),
Show: () => console.log('模拟: Show'),
ClipboardGetText: () => '',
ClipboardSetText: (text) => console.log(`模拟: 设置剪贴板文本 ${text}`),
OnFileDrop: () => console.log('模拟: OnFileDrop'),
OnFileDropOff: () => console.log('模拟: OnFileDropOff'),
CanResolveFilePaths: () => false,
ResolveFilePaths: (files) => files
};
}
// 添加全局LogFrontend函数
window.LogFrontend = (level, message) => {
if (typeof window.go !== 'undefined' && window.go.main && window.go.main.App && window.go.main.App.LogFrontend) {
return window.go.main.App.LogFrontend(level, message);
} else {
console.log(`[前端-${level.toUpperCase()}] ${message}`);
}
};
// 修改Go函数模拟添加LogFrontend方法
if (typeof window.go === 'undefined') {
window.go = {
main: {
App: {
// 模拟GetCallbackConfig函数
GetCallbackConfig: () => {
console.log('模拟: 获取回调配置');
// 返回模拟数据,格式与后端一致 (bool, interface{})
return [true, {
callbackUrl: '',
callbackToken: '',
httpPort: '10001',
enableCallback: false,
enableCloudAuth: false,
fileUploadUrl: ''
}];
},
// 模拟SaveCallbackConfig函数
SaveCallbackConfig: (jsonData) => {
console.log('模拟: 保存回调配置', jsonData);
// 返回模拟成功结果,格式与后端一致 (bool, string)
return [true, '配置保存成功'];
},
// 模拟SendWxWorkData函数
SendWxWorkData: (clientId, jsonData) => {
console.log('模拟: 发送企微数据', clientId, jsonData);
// 返回模拟成功结果
return [true, null, null];
},
// 模拟GetSystemMemoryUsage函数
GetSystemMemoryUsage: () => {
console.log('模拟: 获取系统内存使用情况');
// 返回模拟内存使用数据
return [true, Math.floor(Math.random() * 40) + 10]; // 10-50%之间的随机值
},
// 模拟LogFrontend函数
LogFrontend: (level, message) => {
console.log(`[前端-${level.toUpperCase()}] ${message}`);
},
GetAutoReplyConfig: () => ({
enabled: false,
listen: {
enablePrivateChat: true,
enableGroupChat: true,
groupTriggerMode: 'mention_only',
ignoreSelfMessage: true,
deduplicateSeconds: 300
},
knowledge: {
directory: 'config/knowledge',
indexPath: 'config/knowledge/index.json',
supportedExtensions: ['.md', '.txt', '.csv', '.xlsx', '.docx', '.pdf'],
topK: 8,
minScore: 0.40
},
retrieval: {
retrievalMode: 'hybrid_rerank',
embeddingIndexPath: 'config/knowledge/embedding_index.json',
embeddingModel: 'text-embedding-v4',
embeddingDimensions: 512,
rerankModel: 'qwen3-rerank',
recallTopK: 50,
rerankTopK: 30,
finalTopK: 8
},
ai: {
provider: 'openai_compatible',
baseUrl: '',
apiKey: '',
model: 'qwen-turbo',
visionModel: 'qwen3-vl-plus',
timeoutSeconds: 20,
enableThinking: false,
replyDetail: 'detailed',
temperature: 0,
maxTokens: 700
},
handoff: {
humanUserId: '',
humanConversationId: '',
messageTemplate: '',
includeKnowledgeHits: true,
sendHumanCardToCustomer: true,
sendCustomerCardToHuman: true,
cardTriggerMode: 'manual_keywords',
manualTriggerKeywords: ['人工', '客服', '转人工', '人工客服', '真人', '真人客服'],
cardKeywords: ['人工', '客服', '转人工', '人工客服', '真人', '真人客服']
},
identity: {
unknownPolicy: 'customer',
unknownHandoffPolicy: 'hold',
refreshOnStart: true,
refreshIntervalMinutes: 30,
pageSize: 200,
internalNoHandoffReply: '内部员工消息不触发转人工,如需协助请直接联系对应同事。',
unknownNoHandoffReply: '正在核验联系人身份,暂不触发转人工。如需协助请直接联系对应同事。',
internalUserIds: [],
externalUserIds: []
},
replyPolicy: {
unknownAnswerToken: 'NO_ANSWER',
maxQuestionLength: 1000,
cooldownSeconds: 3,
sensitiveKeywords: []
}
}),
SaveAutoReplyConfig: (jsonData) => {
console.log('模拟: 保存自动客服配置', jsonData);
return [true, 'success'];
},
SetAutoReplyEnabled: (enabled) => {
console.log('模拟: 设置自动客服开关', enabled);
return [true, 'success'];
},
GetAutoReplyStatus: () => ({
success: true,
data: {
enabled: false,
running: false,
knowledgeFileCount: 0,
knowledgeChunkCount: 0,
retrievalMode: 'hybrid_rerank',
embeddingChunkCount: 0,
embeddingModel: 'text-embedding-v4',
embeddingDimensions: 512,
embeddingLastIndexedAt: 0,
internalContactCount: 0,
externalContactCount: 0,
identityLastRefreshAt: 0,
identityRefreshError: '',
identityRefreshing: false,
identityLastResponseType: '',
identityLastResponseCount: 0,
identityLastResponseAt: 0,
identityLookupInFlight: 0,
todayReceived: 0,
todayReplied: 0,
todayHandoff: 0,
todayIgnored: 0,
todayAIFailed: 0,
lastKnowledgeDurationMs: 0,
lastKeywordDurationMs: 0,
lastVectorDurationMs: 0,
lastRerankDurationMs: 0,
lastAiDurationMs: 0,
lastTotalDurationMs: 0,
lastKeywordScore: 0,
lastVectorScore: 0,
lastRerankScore: 0,
lastMessages: []
}
}),
RebuildKnowledgeIndex: () => ({ success: true, message: 'rebuilt' }),
RefreshAutoReplyContacts: () => ({ success: true, message: 'contact refresh started' }),
GetAutoReplyIdentityOptions: () => ({
success: true,
data: {
internal: [
{ userId: 'engineer-a', name: '张工', source: 'mock', clientId: 1, lastSeenAt: 0 },
{ userId: 'engineer-b', name: '李工', source: 'mock', clientId: 1, lastSeenAt: 0 }
],
external: []
}
}),
GetAutoReplyGroupOptions: () => ({
success: true,
data: [
{ conversationId: 'R:demo-sales', name: '演示售后群', source: 'mock', clientId: 1, lastSeenAt: 0, memberCount: 12 }
]
}),
TestAIConnection: () => ({ success: true, data: { rawSummary: '连接正常', durationMs: 320 } }),
TestHumanHandoff: () => ({ success: true, message: 'sent' }),
GetIssues: () => [],
SaveIssue: () => [true, 'saved'],
DeleteIssue: () => [true, 'deleted'],
GetAfterSalesDispatchConfig: () => ({
success: true,
data: {
engineers: [{ userId: 'engineer-a', name: '张工', description: '负责热成像镜头和图像模糊问题', remark: '镜头组', enabled: true }],
rules: [{ id: 'demo-rule', name: '热成像问题', engineerUserId: 'engineer-a', engineerName: '张工', productKeywords: ['热成像'], issueKeywords: ['报错'], enabled: true }],
notifyTemplate: '',
notifyCooldownSeconds: 300,
autoNotifyEnabled: false,
autoNotifyMinConfidence: 0.75
}
}),
SaveAfterSalesDispatchConfig: () => [true, 'saved'],
GetAfterSalesDispatchQueue: () => ({
success: true,
data: {
issues: [],
summary: { pending: 0, unassigned: 0, assigned: 0, sent: 0, failed: 0, todayNew: 0, notSent: 0 },
config: { engineers: [{ userId: 'engineer-a', name: '张工', description: '负责热成像镜头和图像模糊问题', remark: '镜头组', enabled: true }], rules: [], notifyTemplate: '', notifyCooldownSeconds: 300, autoNotifyEnabled: false, autoNotifyMinConfidence: 0.75 }
}
}),
AssignAfterSalesIssue: () => [true, 'assigned'],
NotifyAfterSalesEngineer: () => ({ success: true, message: 'sent' }),
BatchNotifyAfterSalesEngineers: () => ({ success: true, message: '已成功推送 0/0 条' }),
ResolveAfterSalesIssue: () => ({ success: true, message: '已处理并保存到知识库', data: null }),
ListAfterSalesKnowledgeArchives: () => ({ success: true, message: 'ok', data: [] }),
ListAfterSalesKnowledgeCases: () => ({ success: true, message: 'ok', data: [] }),
UpdateAfterSalesKnowledgeCase: () => ({ success: true, message: '知识案例已更新', data: null }),
RevealAfterSalesKnowledgeCase: () => [true, 'opened'],
GetPendingAfterSalesArchiveSummary: () => ({
success: true,
message: 'ok',
data: { pendingCount: 0, totalCount: 0, archiveCount: 0 }
}),
ArchivePendingAfterSalesIssues: () => ({ success: true, message: '暂无需要保存的问题', data: null }),
RevealAfterSalesKnowledgeArchive: () => [true, 'opened'],
ImportAfterSalesHistory: () => [true, '已同步 2 条历史消息,分析 2 段,新增 1 条售后问题'],
PrepareWeComHistoryCopy: () => [true, '已发送复制命令'],
SyncCurrentWeComChatHistory: () => [true, '已同步 2 条历史消息,分析 2 段,新增 1 条售后问题'],
GetAfterSalesImageData: () => '',
ExportIssuesToExcel: () => [true, 'C:\\tmp\\售后问题库.xlsx'],
GetKingdeeMonitorConfig: () => ({
success: true,
message: 'ok',
data: {
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: '您好,您的订单 {{billNo}} 已完成排产,后续我们会继续跟进生产进度。',
customerMappings: {}
}
}),
SaveKingdeeMonitorConfig: () => [true, 'saved'],
GetKingdeeMonitorStatus: () => ({
success: true,
message: 'ok',
data: {
running: false,
status: 'stopped',
lastPollAt: 0,
lastCursorTime: '',
lastError: '',
totalPolled: 0,
totalNotified: 0,
totalUnmapped: 0,
notifiedOrders: {},
recentNotices: [],
recentErrors: []
}
}),
TestKingdeeMonitorConnection: () => ({ success: true, message: '金蝶连接正常' }),
RunKingdeeMonitorOnce: () => ({
success: true,
message: '扫描完成',
data: { polled: 0, matched: 0, notified: 0, skipped: 0, unmapped: 0, failed: 0 }
}),
TriggerManualCollect: () => [true, '售后问题收集已开始'],
SetAutoCollectTask: () => [true, 'saved'],
GetAfterSalesIssueStatus: () => ({
success: true,
data: {
autoCollectEnabled: false,
lastCollectAt: 0,
collecting: false,
lastCollectedAt: 0,
lastAddedCount: 0,
lastError: '',
messageBufferCount: 0
}
})
}
}
};
}
import { createApp } from 'vue'
import App from './App.vue'
import { LogInfo } from '../wailsjs/runtime/runtime.js'
// 导入Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 创建Vue应用实例
const app = createApp(App)
// 使用Element Plus
app.use(ElementPlus)
// 挂载应用到DOM
app.mount('#app')
if (window.go && window.go.main && window.go.main.App && window.go.main.App.LogFrontend) {
window.go.main.App.LogFrontend('info', 'Vue 3 application successfully mounted');
} else {
console.log('Vue 3 application successfully mounted');
}

87
frontend/src/style.css Normal file
View File

@@ -0,0 +1,87 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
color-scheme: dark;
--cmd-bg: #071014;
--cmd-bg-soft: #0b171d;
--cmd-panel: rgba(12, 26, 33, 0.92);
--cmd-panel-strong: rgba(16, 35, 44, 0.96);
--cmd-panel-hover: rgba(21, 48, 59, 0.98);
--cmd-line: rgba(107, 232, 219, 0.22);
--cmd-line-strong: rgba(107, 232, 219, 0.48);
--cmd-text: #e7f6f7;
--cmd-text-soft: #9fb5bd;
--cmd-text-muted: #6f858e;
--cmd-cyan: #48f0dc;
--cmd-cyan-strong: #12c8bd;
--cmd-blue: #63a8ff;
--cmd-violet: #8a7dff;
--cmd-green: #5df2a7;
--cmd-amber: #ffd166;
--cmd-red: #ff6b7d;
--cmd-radius: 8px;
--cmd-shadow: 0 18px 48px rgba(0, 0, 0, 0.34);
--cmd-glow: 0 0 0 1px rgba(72, 240, 220, 0.18), 0 0 28px rgba(72, 240, 220, 0.12);
}
html {
width: 100%;
height: 100%;
background: var(--cmd-bg);
color: var(--cmd-text);
}
body {
width: 100%;
height: 100%;
margin: 0;
color: var(--cmd-text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei",
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Helvetica Neue", Arial, sans-serif;
background:
radial-gradient(circle at 24% 12%, rgba(72, 240, 220, 0.16), transparent 34%),
radial-gradient(circle at 88% 2%, rgba(99, 168, 255, 0.14), transparent 30%),
linear-gradient(135deg, #071014 0%, #0a1419 52%, #091018 100%);
overflow: hidden;
}
button,
input,
textarea,
select {
font: inherit;
}
#app {
height: 100vh;
display: flex;
flex-direction: column;
}
::selection {
color: #031316;
background: rgba(72, 240, 220, 0.86);
}
::-webkit-scrollbar {
width: 9px;
height: 9px;
}
::-webkit-scrollbar-track {
background: rgba(7, 16, 20, 0.92);
}
::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(72, 240, 220, 0.48), rgba(99, 168, 255, 0.38));
border: 2px solid rgba(7, 16, 20, 0.92);
border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(72, 240, 220, 0.78), rgba(99, 168, 255, 0.62));
}

12
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

115
frontend/wailsjs/go/main/App.d.ts vendored Normal file
View File

@@ -0,0 +1,115 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {main} from '../models';
export function AddLogEntry(arg1:string,arg2:string,arg3:string,arg4:number):Promise<void>;
export function ArchivePendingAfterSalesIssues():Promise<any>;
export function AssignAfterSalesIssue(arg1:string,arg2:string):Promise<boolean|string>;
export function BatchNotifyAfterSalesEngineers(arg1:Array<string>):Promise<any>;
export function DebugLoadLogEntries():Promise<any>;
export function DeleteIssue(arg1:string):Promise<boolean|string>;
export function DeleteWxWorkAccount(arg1:string):Promise<string>;
export function ExportIssuesToExcel():Promise<boolean|string>;
export function GetActiveClientCount():Promise<number>;
export function GetAfterSalesDispatchConfig():Promise<any>;
export function GetAfterSalesDispatchQueue():Promise<any>;
export function GetAfterSalesImageData(arg1:string):Promise<string>;
export function GetAfterSalesIssueStatus():Promise<any>;
export function GetAutoReplyConfig():Promise<any>;
export function GetAutoReplyGroupOptions():Promise<any>;
export function GetAutoReplyIdentityOptions():Promise<any>;
export function GetAutoReplyStatus():Promise<any>;
export function GetCallbackConfig():Promise<any>;
export function GetIssues():Promise<Array<main.AfterSalesIssue>>;
export function GetKingdeeMonitorConfig():Promise<any>;
export function GetKingdeeMonitorStatus():Promise<any>;
export function GetPendingAfterSalesArchiveSummary():Promise<any>;
export function GetSystemMemoryUsage():Promise<number>;
export function GetWxWorkAccountList():Promise<any>;
export function Greet(arg1:string):Promise<string>;
export function ImportAfterSalesHistory(arg1:main.AfterSalesHistoryImportRequest):Promise<boolean|string>;
export function ListAfterSalesKnowledgeArchives():Promise<any>;
export function ListAfterSalesKnowledgeCases():Promise<any>;
export function LogFrontend(arg1:string,arg2:string):Promise<void>;
export function NotifyAfterSalesEngineer(arg1:string):Promise<any>;
export function PrepareWeComHistoryCopy():Promise<boolean|string>;
export function RebuildKnowledgeIndex():Promise<any>;
export function RefreshAutoReplyContacts():Promise<any>;
export function RefreshAutoReplyGroups():Promise<any>;
export function ResolveAfterSalesIssue(arg1:string,arg2:string):Promise<any>;
export function RevealAfterSalesAttachment(arg1:string):Promise<boolean|string>;
export function RevealAfterSalesKnowledgeArchive(arg1:string):Promise<boolean|string>;
export function RevealAfterSalesKnowledgeCase(arg1:string):Promise<boolean|string>;
export function RunKingdeeMonitorOnce():Promise<any>;
export function SaveAfterSalesDispatchConfig(arg1:string):Promise<boolean|string>;
export function SaveAutoReplyConfig(arg1:string):Promise<boolean|string>;
export function SaveCallbackConfig(arg1:string):Promise<boolean|string>;
export function SaveIssue(arg1:main.AfterSalesIssue):Promise<boolean|string>;
export function SaveKingdeeMonitorConfig(arg1:string):Promise<boolean|string>;
export function SendWxWorkData(arg1:string,arg2:string):Promise<boolean>;
export function SetAutoCollectTask(arg1:boolean):Promise<boolean|string>;
export function SetAutoReplyEnabled(arg1:boolean):Promise<boolean|string>;
export function StartNewWxWorkInstance():Promise<any>;
export function SyncAutoReplyInternalGroups():Promise<any>;
export function SyncAutoReplyMaterials():Promise<any>;
export function SyncCurrentWeComChatHistory(arg1:main.AfterSalesHistoryImportRequest):Promise<boolean|string>;
export function TestAIConnection():Promise<any>;
export function TestHumanHandoff():Promise<any>;
export function TestKingdeeMonitorConnection(arg1:string):Promise<any>;
export function TriggerManualCollect(arg1:string):Promise<boolean|string>;
export function UpdateAfterSalesKnowledgeCase(arg1:string,arg2:string):Promise<any>;

View File

@@ -0,0 +1,227 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function AddLogEntry(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['AddLogEntry'](arg1, arg2, arg3, arg4);
}
export function ArchivePendingAfterSalesIssues() {
return window['go']['main']['App']['ArchivePendingAfterSalesIssues']();
}
export function AssignAfterSalesIssue(arg1, arg2) {
return window['go']['main']['App']['AssignAfterSalesIssue'](arg1, arg2);
}
export function BatchNotifyAfterSalesEngineers(arg1) {
return window['go']['main']['App']['BatchNotifyAfterSalesEngineers'](arg1);
}
export function DebugLoadLogEntries() {
return window['go']['main']['App']['DebugLoadLogEntries']();
}
export function DeleteIssue(arg1) {
return window['go']['main']['App']['DeleteIssue'](arg1);
}
export function DeleteWxWorkAccount(arg1) {
return window['go']['main']['App']['DeleteWxWorkAccount'](arg1);
}
export function ExportIssuesToExcel() {
return window['go']['main']['App']['ExportIssuesToExcel']();
}
export function GetActiveClientCount() {
return window['go']['main']['App']['GetActiveClientCount']();
}
export function GetAfterSalesDispatchConfig() {
return window['go']['main']['App']['GetAfterSalesDispatchConfig']();
}
export function GetAfterSalesDispatchQueue() {
return window['go']['main']['App']['GetAfterSalesDispatchQueue']();
}
export function GetAfterSalesImageData(arg1) {
return window['go']['main']['App']['GetAfterSalesImageData'](arg1);
}
export function GetAfterSalesIssueStatus() {
return window['go']['main']['App']['GetAfterSalesIssueStatus']();
}
export function GetAutoReplyConfig() {
return window['go']['main']['App']['GetAutoReplyConfig']();
}
export function GetAutoReplyGroupOptions() {
return window['go']['main']['App']['GetAutoReplyGroupOptions']();
}
export function GetAutoReplyIdentityOptions() {
return window['go']['main']['App']['GetAutoReplyIdentityOptions']();
}
export function GetAutoReplyStatus() {
return window['go']['main']['App']['GetAutoReplyStatus']();
}
export function GetCallbackConfig() {
return window['go']['main']['App']['GetCallbackConfig']();
}
export function GetIssues() {
return window['go']['main']['App']['GetIssues']();
}
export function GetKingdeeMonitorConfig() {
return window['go']['main']['App']['GetKingdeeMonitorConfig']();
}
export function GetKingdeeMonitorStatus() {
return window['go']['main']['App']['GetKingdeeMonitorStatus']();
}
export function GetPendingAfterSalesArchiveSummary() {
return window['go']['main']['App']['GetPendingAfterSalesArchiveSummary']();
}
export function GetSystemMemoryUsage() {
return window['go']['main']['App']['GetSystemMemoryUsage']();
}
export function GetWxWorkAccountList() {
return window['go']['main']['App']['GetWxWorkAccountList']();
}
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
export function ImportAfterSalesHistory(arg1) {
return window['go']['main']['App']['ImportAfterSalesHistory'](arg1);
}
export function ListAfterSalesKnowledgeArchives() {
return window['go']['main']['App']['ListAfterSalesKnowledgeArchives']();
}
export function ListAfterSalesKnowledgeCases() {
return window['go']['main']['App']['ListAfterSalesKnowledgeCases']();
}
export function LogFrontend(arg1, arg2) {
return window['go']['main']['App']['LogFrontend'](arg1, arg2);
}
export function NotifyAfterSalesEngineer(arg1) {
return window['go']['main']['App']['NotifyAfterSalesEngineer'](arg1);
}
export function PrepareWeComHistoryCopy() {
return window['go']['main']['App']['PrepareWeComHistoryCopy']();
}
export function RebuildKnowledgeIndex() {
return window['go']['main']['App']['RebuildKnowledgeIndex']();
}
export function RefreshAutoReplyContacts() {
return window['go']['main']['App']['RefreshAutoReplyContacts']();
}
export function RefreshAutoReplyGroups() {
return window['go']['main']['App']['RefreshAutoReplyGroups']();
}
export function ResolveAfterSalesIssue(arg1, arg2) {
return window['go']['main']['App']['ResolveAfterSalesIssue'](arg1, arg2);
}
export function RevealAfterSalesAttachment(arg1) {
return window['go']['main']['App']['RevealAfterSalesAttachment'](arg1);
}
export function RevealAfterSalesKnowledgeArchive(arg1) {
return window['go']['main']['App']['RevealAfterSalesKnowledgeArchive'](arg1);
}
export function RevealAfterSalesKnowledgeCase(arg1) {
return window['go']['main']['App']['RevealAfterSalesKnowledgeCase'](arg1);
}
export function RunKingdeeMonitorOnce() {
return window['go']['main']['App']['RunKingdeeMonitorOnce']();
}
export function SaveAfterSalesDispatchConfig(arg1) {
return window['go']['main']['App']['SaveAfterSalesDispatchConfig'](arg1);
}
export function SaveAutoReplyConfig(arg1) {
return window['go']['main']['App']['SaveAutoReplyConfig'](arg1);
}
export function SaveCallbackConfig(arg1) {
return window['go']['main']['App']['SaveCallbackConfig'](arg1);
}
export function SaveIssue(arg1) {
return window['go']['main']['App']['SaveIssue'](arg1);
}
export function SaveKingdeeMonitorConfig(arg1) {
return window['go']['main']['App']['SaveKingdeeMonitorConfig'](arg1);
}
export function SendWxWorkData(arg1, arg2) {
return window['go']['main']['App']['SendWxWorkData'](arg1, arg2);
}
export function SetAutoCollectTask(arg1) {
return window['go']['main']['App']['SetAutoCollectTask'](arg1);
}
export function SetAutoReplyEnabled(arg1) {
return window['go']['main']['App']['SetAutoReplyEnabled'](arg1);
}
export function StartNewWxWorkInstance() {
return window['go']['main']['App']['StartNewWxWorkInstance']();
}
export function SyncAutoReplyInternalGroups() {
return window['go']['main']['App']['SyncAutoReplyInternalGroups']();
}
export function SyncAutoReplyMaterials() {
return window['go']['main']['App']['SyncAutoReplyMaterials']();
}
export function SyncCurrentWeComChatHistory(arg1) {
return window['go']['main']['App']['SyncCurrentWeComChatHistory'](arg1);
}
export function TestAIConnection() {
return window['go']['main']['App']['TestAIConnection']();
}
export function TestHumanHandoff() {
return window['go']['main']['App']['TestHumanHandoff']();
}
export function TestKingdeeMonitorConnection(arg1) {
return window['go']['main']['App']['TestKingdeeMonitorConnection'](arg1);
}
export function TriggerManualCollect(arg1) {
return window['go']['main']['App']['TriggerManualCollect'](arg1);
}
export function UpdateAfterSalesKnowledgeCase(arg1, arg2) {
return window['go']['main']['App']['UpdateAfterSalesKnowledgeCase'](arg1, arg2);
}

View File

@@ -0,0 +1,143 @@
export namespace main {
export class AfterSalesFileAttachment {
name: string;
path: string;
ref: string;
content: string;
extractStatus: string;
sourceMessageId: string;
static createFrom(source: any = {}) {
return new AfterSalesFileAttachment(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
this.path = source["path"];
this.ref = source["ref"];
this.content = source["content"];
this.extractStatus = source["extractStatus"];
this.sourceMessageId = source["sourceMessageId"];
}
}
export class AfterSalesHistoryImportRequest {
conversationId: string;
roomName: string;
rawText: string;
static createFrom(source: any = {}) {
return new AfterSalesHistoryImportRequest(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.conversationId = source["conversationId"];
this.roomName = source["roomName"];
this.rawText = source["rawText"];
}
}
export class AfterSalesIssue {
id: string;
createdAt: string;
updatedAt: string;
conversationId: string;
roomName: string;
sourceClientId: number;
sourceAccountUserId: string;
sourceAccountName: string;
customerUserId: string;
customerName: string;
issueContent: string;
imagePaths: string[];
imageRefs: string[];
fileAttachments: AfterSalesFileAttachment[];
aiSuggestion: string;
status: string;
sourceMessageIds: string[];
fingerprint: string;
collectBatchId: string;
aiConfidence: number;
aiSuggestionEdited: boolean;
assignedEngineerId: string;
assignedEngineerName: string;
dispatchStatus: string;
dispatchReason: string;
dispatchRuleId: string;
dispatchConfidence: number;
dispatchSource: string;
notifyStatus: string;
lastNotifiedAt: number;
notifyError: string;
notifyCount: number;
resolutionContent: string;
resolvedAt: string;
knowledgeArchivedAt: string;
knowledgeSourcePath: string;
static createFrom(source: any = {}) {
return new AfterSalesIssue(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.createdAt = source["createdAt"];
this.updatedAt = source["updatedAt"];
this.conversationId = source["conversationId"];
this.roomName = source["roomName"];
this.sourceClientId = source["sourceClientId"];
this.sourceAccountUserId = source["sourceAccountUserId"];
this.sourceAccountName = source["sourceAccountName"];
this.customerUserId = source["customerUserId"];
this.customerName = source["customerName"];
this.issueContent = source["issueContent"];
this.imagePaths = source["imagePaths"];
this.imageRefs = source["imageRefs"];
this.fileAttachments = this.convertValues(source["fileAttachments"], AfterSalesFileAttachment);
this.aiSuggestion = source["aiSuggestion"];
this.status = source["status"];
this.sourceMessageIds = source["sourceMessageIds"];
this.fingerprint = source["fingerprint"];
this.collectBatchId = source["collectBatchId"];
this.aiConfidence = source["aiConfidence"];
this.aiSuggestionEdited = source["aiSuggestionEdited"];
this.assignedEngineerId = source["assignedEngineerId"];
this.assignedEngineerName = source["assignedEngineerName"];
this.dispatchStatus = source["dispatchStatus"];
this.dispatchReason = source["dispatchReason"];
this.dispatchRuleId = source["dispatchRuleId"];
this.dispatchConfidence = source["dispatchConfidence"];
this.dispatchSource = source["dispatchSource"];
this.notifyStatus = source["notifyStatus"];
this.lastNotifiedAt = source["lastNotifiedAt"];
this.notifyError = source["notifyError"];
this.notifyCount = source["notifyCount"];
this.resolutionContent = source["resolutionContent"];
this.resolvedAt = source["resolvedAt"];
this.knowledgeArchivedAt = source["knowledgeArchivedAt"];
this.knowledgeSourcePath = source["knowledgeSourcePath"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}

View File

@@ -0,0 +1,24 @@
{
"name": "@wailsapp/runtime",
"version": "2.0.0",
"description": "Wails Javascript runtime library",
"main": "runtime.js",
"types": "runtime.d.ts",
"scripts": {
},
"repository": {
"type": "git",
"url": "git+https://github.com/wailsapp/wails.git"
},
"keywords": [
"Wails",
"Javascript",
"Go"
],
"author": "Lea Anthony <lea.anthony@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/wailsapp/wails/issues"
},
"homepage": "https://github.com/wailsapp/wails#readme"
}

249
frontend/wailsjs/runtime/runtime.d.ts vendored Normal file
View File

@@ -0,0 +1,249 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export interface Position {
x: number;
y: number;
}
export interface Size {
w: number;
h: number;
}
export interface Screen {
isCurrent: boolean;
isPrimary: boolean;
width : number
height : number
}
// Environment information such as platform, buildtype, ...
export interface EnvironmentInfo {
buildType: string;
platform: string;
arch: string;
}
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
// emits the given event. Optional data may be passed with the event.
// This will trigger any event listeners.
export function EventsEmit(eventName: string, ...data: any): void;
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
// sets up a listener for the given event name, but will only trigger a given number times.
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
// sets up a listener for the given event name, but will only trigger once.
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
// unregisters the listener for the given event name.
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
// unregisters all listeners.
export function EventsOffAll(): void;
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
// logs the given message as a raw message
export function LogPrint(message: string): void;
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
// logs the given message at the `trace` log level.
export function LogTrace(message: string): void;
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
// logs the given message at the `debug` log level.
export function LogDebug(message: string): void;
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
// logs the given message at the `error` log level.
export function LogError(message: string): void;
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
// logs the given message at the `fatal` log level.
// The application will quit after calling this method.
export function LogFatal(message: string): void;
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
// logs the given message at the `info` log level.
export function LogInfo(message: string): void;
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
// logs the given message at the `warning` log level.
export function LogWarning(message: string): void;
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
// Forces a reload by the main application as well as connected browsers.
export function WindowReload(): void;
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
// Reloads the application frontend.
export function WindowReloadApp(): void;
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
// Sets the window AlwaysOnTop or not on top.
export function WindowSetAlwaysOnTop(b: boolean): void;
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
// *Windows only*
// Sets window theme to system default (dark/light).
export function WindowSetSystemDefaultTheme(): void;
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
// *Windows only*
// Sets window to light theme.
export function WindowSetLightTheme(): void;
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
// *Windows only*
// Sets window to dark theme.
export function WindowSetDarkTheme(): void;
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
// Centers the window on the monitor the window is currently on.
export function WindowCenter(): void;
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
// Sets the text in the window title bar.
export function WindowSetTitle(title: string): void;
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
// Makes the window full screen.
export function WindowFullscreen(): void;
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
// Restores the previous window dimensions and position prior to full screen.
export function WindowUnfullscreen(): void;
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
export function WindowIsFullscreen(): Promise<boolean>;
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
// Sets the width and height of the window.
export function WindowSetSize(width: number, height: number): void;
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
// Gets the width and height of the window.
export function WindowGetSize(): Promise<Size>;
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMaxSize(width: number, height: number): void;
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
// Setting a size of 0,0 will disable this constraint.
export function WindowSetMinSize(width: number, height: number): void;
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
// Sets the window position relative to the monitor the window is currently on.
export function WindowSetPosition(x: number, y: number): void;
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
// Gets the window position relative to the monitor the window is currently on.
export function WindowGetPosition(): Promise<Position>;
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
// Hides the window.
export function WindowHide(): void;
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
// Shows the window, if it is currently hidden.
export function WindowShow(): void;
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
// Maximises the window to fill the screen.
export function WindowMaximise(): void;
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
// Toggles between Maximised and UnMaximised.
export function WindowToggleMaximise(): void;
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
// Restores the window to the dimensions and position prior to maximising.
export function WindowUnmaximise(): void;
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
// Returns the state of the window, i.e. whether the window is maximised or not.
export function WindowIsMaximised(): Promise<boolean>;
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
// Minimises the window.
export function WindowMinimise(): void;
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
// Restores the window to the dimensions and position prior to minimising.
export function WindowUnminimise(): void;
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
// Returns the state of the window, i.e. whether the window is minimised or not.
export function WindowIsMinimised(): Promise<boolean>;
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
// Returns the state of the window, i.e. whether the window is normal or not.
export function WindowIsNormal(): Promise<boolean>;
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
export function ScreenGetAll(): Promise<Screen[]>;
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
// Opens the given URL in the system browser.
export function BrowserOpenURL(url: string): void;
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
// Returns information about the environment
export function Environment(): Promise<EnvironmentInfo>;
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
// Quits the application.
export function Quit(): void;
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
// Hides the application.
export function Hide(): void;
// [Show](https://wails.io/docs/reference/runtime/intro#show)
// Shows the application.
export function Show(): void;
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
// Returns the current text stored on clipboard
export function ClipboardGetText(): Promise<string>;
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
// Sets a text on the clipboard
export function ClipboardSetText(text: string): Promise<boolean>;
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
// OnFileDropOff removes the drag and drop listeners and handlers.
export function OnFileDropOff() :void
// Check if the file path resolver is available
export function CanResolveFilePaths(): boolean;
// Resolves file paths for an array of files
export function ResolveFilePaths(files: File[]): void

View File

@@ -0,0 +1,238 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
export function LogPrint(message) {
window.runtime.LogPrint(message);
}
export function LogTrace(message) {
window.runtime.LogTrace(message);
}
export function LogDebug(message) {
window.runtime.LogDebug(message);
}
export function LogInfo(message) {
window.runtime.LogInfo(message);
}
export function LogWarning(message) {
window.runtime.LogWarning(message);
}
export function LogError(message) {
window.runtime.LogError(message);
}
export function LogFatal(message) {
window.runtime.LogFatal(message);
}
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
}
export function EventsOn(eventName, callback) {
return EventsOnMultiple(eventName, callback, -1);
}
export function EventsOff(eventName, ...additionalEventNames) {
return window.runtime.EventsOff(eventName, ...additionalEventNames);
}
export function EventsOnce(eventName, callback) {
return EventsOnMultiple(eventName, callback, 1);
}
export function EventsEmit(eventName) {
let args = [eventName].slice.call(arguments);
return window.runtime.EventsEmit.apply(null, args);
}
export function WindowReload() {
window.runtime.WindowReload();
}
export function WindowReloadApp() {
window.runtime.WindowReloadApp();
}
export function WindowSetAlwaysOnTop(b) {
window.runtime.WindowSetAlwaysOnTop(b);
}
export function WindowSetSystemDefaultTheme() {
window.runtime.WindowSetSystemDefaultTheme();
}
export function WindowSetLightTheme() {
window.runtime.WindowSetLightTheme();
}
export function WindowSetDarkTheme() {
window.runtime.WindowSetDarkTheme();
}
export function WindowCenter() {
window.runtime.WindowCenter();
}
export function WindowSetTitle(title) {
window.runtime.WindowSetTitle(title);
}
export function WindowFullscreen() {
window.runtime.WindowFullscreen();
}
export function WindowUnfullscreen() {
window.runtime.WindowUnfullscreen();
}
export function WindowIsFullscreen() {
return window.runtime.WindowIsFullscreen();
}
export function WindowGetSize() {
return window.runtime.WindowGetSize();
}
export function WindowSetSize(width, height) {
window.runtime.WindowSetSize(width, height);
}
export function WindowSetMaxSize(width, height) {
window.runtime.WindowSetMaxSize(width, height);
}
export function WindowSetMinSize(width, height) {
window.runtime.WindowSetMinSize(width, height);
}
export function WindowSetPosition(x, y) {
window.runtime.WindowSetPosition(x, y);
}
export function WindowGetPosition() {
return window.runtime.WindowGetPosition();
}
export function WindowHide() {
window.runtime.WindowHide();
}
export function WindowShow() {
window.runtime.WindowShow();
}
export function WindowMaximise() {
window.runtime.WindowMaximise();
}
export function WindowToggleMaximise() {
window.runtime.WindowToggleMaximise();
}
export function WindowUnmaximise() {
window.runtime.WindowUnmaximise();
}
export function WindowIsMaximised() {
return window.runtime.WindowIsMaximised();
}
export function WindowMinimise() {
window.runtime.WindowMinimise();
}
export function WindowUnminimise() {
window.runtime.WindowUnminimise();
}
export function WindowSetBackgroundColour(R, G, B, A) {
window.runtime.WindowSetBackgroundColour(R, G, B, A);
}
export function ScreenGetAll() {
return window.runtime.ScreenGetAll();
}
export function WindowIsMinimised() {
return window.runtime.WindowIsMinimised();
}
export function WindowIsNormal() {
return window.runtime.WindowIsNormal();
}
export function BrowserOpenURL(url) {
window.runtime.BrowserOpenURL(url);
}
export function Environment() {
return window.runtime.Environment();
}
export function Quit() {
window.runtime.Quit();
}
export function Hide() {
window.runtime.Hide();
}
export function Show() {
window.runtime.Show();
}
export function ClipboardGetText() {
return window.runtime.ClipboardGetText();
}
export function ClipboardSetText(text) {
return window.runtime.ClipboardSetText(text);
}
/**
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
*
* @export
* @callback OnFileDropCallback
* @param {number} x - x coordinate of the drop
* @param {number} y - y coordinate of the drop
* @param {string[]} paths - A list of file paths.
*/
/**
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
*
* @export
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
*/
export function OnFileDrop(callback, useDropTarget) {
return window.runtime.OnFileDrop(callback, useDropTarget);
}
/**
* OnFileDropOff removes the drag and drop listeners and handlers.
*/
export function OnFileDropOff() {
return window.runtime.OnFileDropOff();
}
export function CanResolveFilePaths() {
return window.runtime.CanResolveFilePaths();
}
export function ResolveFilePaths(files) {
return window.runtime.ResolveFilePaths(files);
}