Initial qiwei secondary development handoff
This commit is contained in:
16
frontend/clear_storage.html
Normal file
16
frontend/clear_storage.html
Normal 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
253
frontend/css/style.css
Normal 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
12
frontend/index.html
Normal 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
76
frontend/js/app.js
Normal 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
1116
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
frontend/package.json
Normal file
18
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
frontend/package.json.md5
Normal file
1
frontend/package.json.md5
Normal file
@@ -0,0 +1 @@
|
||||
db031e671111b343255373ca05cff100
|
||||
661
frontend/src/App.vue
Normal file
661
frontend/src/App.vue
Normal 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
765
frontend/src/app.css
Normal 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;
|
||||
}
|
||||
}
|
||||
93
frontend/src/assets/fonts/OFL.txt
Normal file
93
frontend/src/assets/fonts/OFL.txt
Normal 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.
|
||||
BIN
frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
BIN
frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/images/logo-universal.png
Normal file
BIN
frontend/src/assets/images/logo-universal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
1152
frontend/src/components/AfterSalesIssues.vue
Normal file
1152
frontend/src/components/AfterSalesIssues.vue
Normal file
File diff suppressed because it is too large
Load Diff
574
frontend/src/components/AfterSalesKnowledge.vue
Normal file
574
frontend/src/components/AfterSalesKnowledge.vue
Normal 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>
|
||||
3293
frontend/src/components/AutoReply.vue
Normal file
3293
frontend/src/components/AutoReply.vue
Normal file
File diff suppressed because it is too large
Load Diff
1066
frontend/src/components/EngineerDispatch.vue
Normal file
1066
frontend/src/components/EngineerDispatch.vue
Normal file
File diff suppressed because it is too large
Load Diff
623
frontend/src/components/KingdeeMonitor.vue
Normal file
623
frontend/src/components/KingdeeMonitor.vue
Normal 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>
|
||||
533
frontend/src/components/LoginModal.vue
Normal file
533
frontend/src/components/LoginModal.vue
Normal 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>
|
||||
199
frontend/src/components/OperationLogs.vue
Normal file
199
frontend/src/components/OperationLogs.vue
Normal 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>
|
||||
548
frontend/src/components/WxWorkAccount.vue
Normal file
548
frontend/src/components/WxWorkAccount.vue
Normal 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
427
frontend/src/main.js
Normal 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
87
frontend/src/style.css
Normal 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
12
frontend/vite.config.js
Normal 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
115
frontend/wailsjs/go/main/App.d.ts
vendored
Normal 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>;
|
||||
227
frontend/wailsjs/go/main/App.js
Normal file
227
frontend/wailsjs/go/main/App.js
Normal 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);
|
||||
}
|
||||
143
frontend/wailsjs/go/models.ts
Normal file
143
frontend/wailsjs/go/models.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
24
frontend/wailsjs/runtime/package.json
Normal file
24
frontend/wailsjs/runtime/package.json
Normal 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
249
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal 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
|
||||
238
frontend/wailsjs/runtime/runtime.js
Normal file
238
frontend/wailsjs/runtime/runtime.js
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user