Merge pull request #821 from aylvn/desktop
Desktop has implemented the large model configuration page and settings page functionality
This commit is contained in:
commit
7e18c97a2f
1
.gitignore
vendored
1
.gitignore
vendored
@ -181,4 +181,3 @@ workspace/
|
||||
|
||||
# Private Config
|
||||
config/config.toml
|
||||
|
||||
|
@ -69,7 +69,3 @@ To build the application:
|
||||
wails build
|
||||
|
||||
The built application will be located in the project’s dist directory.
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -69,7 +69,3 @@ nodejs官网安装: https://nodejs.org/en
|
||||
wails build
|
||||
|
||||
构建好的应用在项目dist目录下
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"OpenManus/src/utils"
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
@ -10,6 +11,12 @@ type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Result string `json:"result"`
|
||||
Error string `json:"error"`
|
||||
Callbackid string `json:"callbackid"`
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
@ -25,3 +32,11 @@ func (a *App) startup(ctx context.Context) {
|
||||
func (a *App) Greet(name string) string {
|
||||
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||
}
|
||||
|
||||
// ReadAll reads file content
|
||||
func (a *App) ReadAll(filePath string) string {
|
||||
// Read the file content, resulting in a JSON string containing file content and callback ID
|
||||
data := string(utils.ReadAll(filePath))
|
||||
utils.Log("ReadAll data: ", data)
|
||||
return data
|
||||
}
|
||||
|
@ -10,4 +10,3 @@
|
||||
<script src="./src/main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<!-- 全局配置 -->
|
||||
<!-- Global Configuration -->
|
||||
<el-config-provider :size="size" :z-index="zIndex" :locale="locale" :button="config" :message="config"
|
||||
:value-on-clear="null" :empty-values="[undefined, null]">
|
||||
<RouterView />
|
||||
@ -10,7 +10,7 @@
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import en from 'element-plus/es/locale/lang/en'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
/** 暗黑主题 */
|
||||
/** Dark Theme */
|
||||
import { useDark, useStorage } from '@vueuse/core'
|
||||
|
||||
const size = 'default'
|
||||
@ -19,14 +19,14 @@ const zIndex = 2000
|
||||
const localConfig = localStorage.getItem('config') ? JSON.parse(localStorage.getItem('config')) : {}
|
||||
|
||||
const localeStr = localConfig.selectedLang ? localConfig.selectedLang.code : 'en'
|
||||
const locale = localeStr == 'en'? en : zhCn
|
||||
const locale = localeStr == 'en' ? en : zhCn
|
||||
|
||||
const isDark = useDark()
|
||||
// 存储用户的喜好
|
||||
// Store user preferences
|
||||
const userPrefersDark = ref(null)
|
||||
onMounted(() => {
|
||||
|
||||
// 使用 useStorage 钩子来同步 isDark 和本地存储
|
||||
// Use useStorage hook to sync isDark and local storage
|
||||
useStorage(
|
||||
'user-prefers-dark',
|
||||
userPrefersDark,
|
||||
@ -35,17 +35,17 @@ onMounted(() => {
|
||||
)
|
||||
})
|
||||
|
||||
// 监听 isDark 变化,并更新本地存储
|
||||
// Watch isDark changes and update local storage
|
||||
watch(isDark, (newValue) => {
|
||||
userPrefersDark.value = newValue ? 'dark' : 'light'
|
||||
})
|
||||
|
||||
|
||||
/* 全局配置 */
|
||||
/* Global Configuration */
|
||||
const config = reactive({
|
||||
// 按钮-中文字符中间自动插入空格
|
||||
// Button - Automatically insert space between Chinese characters
|
||||
autoInsertSpace: true,
|
||||
// 消息-可同时显示的消息最大数量
|
||||
// Message - Maximum number of messages that can be displayed simultaneously
|
||||
max: 3,
|
||||
})
|
||||
</script>
|
||||
|
@ -1,6 +1,6 @@
|
||||
:root {
|
||||
--el-menu-base-level-padding: 10px !important;
|
||||
/** 子菜单缩进 */
|
||||
/** Indents for submenu items */
|
||||
--el-menu-level-padding: 20px !important;
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
/* 让broder不占用宽度 */
|
||||
/* Ensure borders don't take up space */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ body {
|
||||
padding: 0;
|
||||
line-height: 1.5;
|
||||
touch-action: none;
|
||||
/* 不使用原生滚动条 */
|
||||
/* Don't use native scroll bars */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* 防止双击选中 */
|
||||
/* Avoid double-click selection */
|
||||
a.no-select {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
@ -114,12 +114,12 @@ fieldset {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/** 文本缩进默认值,2个字符 */
|
||||
/** Default indent for text, 2 characters */
|
||||
.text-indent {
|
||||
text-indent: 2em;
|
||||
}
|
||||
|
||||
/** \n 换行 */
|
||||
/** Multiline text preset */
|
||||
.multiline-text {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
@ -130,7 +130,7 @@ input:-webkit-autofill,
|
||||
textarea:-webkit-autofill,
|
||||
select:-webkit-autofill {
|
||||
-webkit-text-fill-color: var(--el-text-color);
|
||||
/* transparent 背景透明 */
|
||||
/* transparent background */
|
||||
-webkit-box-shadow: 0 0 0px 1000px transparent inset;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
@ -151,32 +151,32 @@ input::selection,
|
||||
input:-webkit-autofill::selection,
|
||||
input:-webkit-autofill:hover::selection,
|
||||
input:-webkit-autofill:focus::selection {
|
||||
/* 设置自动填充时选中文本的样式 */
|
||||
/* 文字颜色 */
|
||||
/* Style for autocompleted text */
|
||||
/* Color for text */
|
||||
-webkit-text-fill-color: rgb(255, 255, 255);
|
||||
/* 背景颜色 */
|
||||
/* Color for background */
|
||||
background-color: rgb(0, 0, 255);
|
||||
-webkit-box-shadow: 0 0 0px 1000px rgb(255, 255, 255) inset;
|
||||
box-shadow: 0 0 0px 1000px rgb(255, 255, 255) inset;
|
||||
/* 防止背景颜色变化 */
|
||||
/* Avoid color changes for background */
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
img {
|
||||
/* 添加渐变效果 */
|
||||
/* Add transition */
|
||||
transition: opacity 0.4s ease;
|
||||
/* 默认透明度为1(不透明) */
|
||||
/* No transparency by default */
|
||||
opacity: 1;
|
||||
/* 添加圆角 */
|
||||
/* Add rounded corners */
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
img.edit:hover {
|
||||
/* 鼠标悬停时透明度降为0.5 */
|
||||
/* Degrade transparency by 0.5 on hover */
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 渐变边框 */
|
||||
/* Gradient border */
|
||||
.gradient-border {
|
||||
border: 12px solid transparent;
|
||||
border-radius: 6px;
|
||||
@ -187,13 +187,16 @@ img.edit:hover {
|
||||
|
||||
.main-content {
|
||||
width: 100%;
|
||||
height: calc(100vh - 44px);
|
||||
/** 44 + 16 + 16 */
|
||||
height: calc(100vh - 76px);
|
||||
padding: 0px 16px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/** Element Plus Start */
|
||||
|
||||
/* 移除全局 el-link 组件的下划线 */
|
||||
/* Remove underlines globally for el-link */
|
||||
.el-link::after {
|
||||
display: none;
|
||||
}
|
||||
@ -229,7 +232,7 @@ img.edit:hover {
|
||||
}
|
||||
|
||||
.el-textarea__inner::-webkit-scrollbar {
|
||||
/* 隐藏滚动条 */
|
||||
/* Hide scrollbar */
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
@ -250,14 +253,14 @@ button>span.el-button__text--expand {
|
||||
--el-font-size-base: 11px;
|
||||
}
|
||||
|
||||
/** 菜单样式,菜单组件部分样式必须定义在主类中,
|
||||
/** Menu styles - Some of the styles are intended to be defined in main classes. */
|
||||
|
||||
/* 覆盖el-menu的默认高度 */
|
||||
/* Override the default height for el-menu */
|
||||
.el-menu--horizontal {
|
||||
--el-menu-horizontal-height: 36px;
|
||||
}
|
||||
|
||||
/* 弹出菜单样式 */
|
||||
/* Styles for pop-up menus */
|
||||
.el-menu--collapse .el-menu .el-submenu,
|
||||
.el-menu--popup {
|
||||
min-width: 120px !important;
|
||||
@ -265,7 +268,7 @@ button>span.el-button__text--expand {
|
||||
margin: 0px 0px !important;
|
||||
}
|
||||
|
||||
/* 如果在AsideMenu定义, 关联的菜单组件可能读取不到某些覆盖的样式 */
|
||||
/* If defined in AsideMenu, menu components associated may not read some overridden styles */
|
||||
.el-sub-menu__title {
|
||||
padding: 0px 10px !important;
|
||||
border-radius: 6px;
|
||||
@ -277,7 +280,7 @@ button>span.el-button__text--expand {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/** 菜单折叠时hover菜单项高度这里必须再定义一次 */
|
||||
/* When menu is collapsed, hover menu item height must be redefined */
|
||||
.el-menu-item {
|
||||
border-radius: 6px;
|
||||
height: 32px !important;
|
||||
@ -298,7 +301,7 @@ button>span.el-button__text--expand {
|
||||
background-color: rgba(var(--el-color-primary-rgb), .1);
|
||||
}
|
||||
|
||||
/* 分割线样式 */
|
||||
/* Divider styles */
|
||||
div.el-divider {
|
||||
margin: 18px auto;
|
||||
}
|
||||
@ -412,7 +415,7 @@ div.el-divider {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 全屏居中 */
|
||||
/* Fullscreen center */
|
||||
.full-center {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -560,7 +563,7 @@ div.el-divider {
|
||||
}
|
||||
|
||||
/**
|
||||
* 可换行的弹性布局
|
||||
* Flex layout with multiline support
|
||||
*/
|
||||
.card-row-wrap {
|
||||
display: flex;
|
||||
@ -569,31 +572,31 @@ div.el-divider {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/** 卡片行内独占一行, 不能取代card-row-item wp-100 */
|
||||
/** Exclusive line in a card, doesn't replace card-row-item wp-100 */
|
||||
.card-row-aline {
|
||||
width: 100%;
|
||||
/*防止撑开父元素, 要用flex布局*/
|
||||
/* Prevent parent element to be affected, need a flex display */
|
||||
min-width: 0;
|
||||
margin: 8px 14px;
|
||||
/** 为子元素预留margin空间 */
|
||||
/* Reserve margin space for child elements in flex container */
|
||||
/* padding: 0px 10px; */
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/** 卡片行普通元素, 有固定宽度,自适应排版 */
|
||||
/* Card row item with fixed width and adaptive layout */
|
||||
.card-row-item {
|
||||
/*防止撑开父元素*/
|
||||
/* Prevent item from expanding parent container */
|
||||
min-width: 0;
|
||||
margin: 8px 14px;
|
||||
/** 不自动换行,可设置高度 */
|
||||
/* Flex layout without wrapping */
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* 元素水平居中 */
|
||||
/* Horizontal centering utility class */
|
||||
.item-h-center {
|
||||
margin: 0 auto;
|
||||
}
|
||||
@ -603,30 +606,30 @@ div.el-divider {
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
/* 第一个子元素 */
|
||||
/* First child element styling in card rows */
|
||||
.card-row-item>span:first-child,
|
||||
.card-row-item>label:first-child {
|
||||
align-self: start;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
/* 第二个子元素是span时 */
|
||||
/* Second child span element styling */
|
||||
.card-row-item>span:nth-child(2) {
|
||||
width: 240px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/* 第二个子元素是p时 */
|
||||
/* Second child paragraph element styling */
|
||||
.card-row-item>p:nth-child(2) {
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
/* 第二个子元素 */
|
||||
/* General second child element styling */
|
||||
.card-row-item>*:nth-child(2) {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
/* 第二个子元素 独占一行时 */
|
||||
/* Full-width modifier for second child elements */
|
||||
.card-row-item.wp-100>*:nth-child(2) {
|
||||
width: calc(100% - 100px);
|
||||
}
|
||||
@ -636,7 +639,7 @@ div.el-divider {
|
||||
}
|
||||
|
||||
/**
|
||||
* card行-无数据
|
||||
* No data placeholder styling
|
||||
*/
|
||||
.card-item-no-data {
|
||||
width: 100%;
|
||||
@ -663,17 +666,17 @@ div.el-divider {
|
||||
margin: -8px -14px;
|
||||
}
|
||||
|
||||
/** 全屏视频背景包装模块 */
|
||||
/** Full-screen video background container */
|
||||
.video-bg-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
/** 可设置背景在视频加载前或失败时做显示 设置为深色 */
|
||||
/* Fallback background for video */
|
||||
background: #000 url('') no-repeat fixed center center / cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/** 全屏视频背景包装模块-视频样式 */
|
||||
/** Video element styling */
|
||||
.video-bg-wrap video {
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
@ -684,26 +687,24 @@ div.el-divider {
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
/** 全屏视频背景包装模块-前台内容样式 */
|
||||
/** Front content overlay for video background */
|
||||
.video-bg-wrap .front {
|
||||
z-index: 1;
|
||||
/* 设置子元素为绝对定位 */
|
||||
/* Set children to be absolute positioned */
|
||||
position: absolute;
|
||||
/* 子元素顶部距离父元素顶部的距离为50% */
|
||||
/* Center content */
|
||||
top: 50%;
|
||||
/* 子元素左侧距离父元素左侧的距离为50% */
|
||||
left: 50%;
|
||||
/* 使用transform进行微调,将子元素居中 */
|
||||
transform: translate(-50%, -50%);
|
||||
/* background-color: rgb(255, 255, 255, 0.9); */
|
||||
/* 背景模糊 */
|
||||
/* Blur background */
|
||||
backdrop-filter: blur(10px);
|
||||
/* opacity: 0.8; */
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/**
|
||||
* 色彩标签
|
||||
* Color labels
|
||||
*/
|
||||
.color-label {
|
||||
padding: 2px 6px;
|
||||
@ -711,7 +712,7 @@ div.el-divider {
|
||||
}
|
||||
|
||||
/**
|
||||
* 布局管理器遮挡层
|
||||
* Modal layer for layout manager
|
||||
*/
|
||||
.layout-shade {
|
||||
position: fixed;
|
||||
@ -748,7 +749,7 @@ div.el-divider {
|
||||
}
|
||||
|
||||
.adv-search {
|
||||
/** 自动高度过渡动画 */
|
||||
/** Height transitions for height */
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 0.3s ease-in-out;
|
||||
@ -763,7 +764,7 @@ div.el-divider {
|
||||
}
|
||||
|
||||
/**
|
||||
* 背景颜色
|
||||
* Background colors
|
||||
*/
|
||||
.bg-primary {
|
||||
background-color: var(--el-color-primary);
|
||||
@ -786,43 +787,43 @@ div.el-divider {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
/* 定义文字渐变色动画 */
|
||||
/* Color cycling animation for text */
|
||||
@keyframes text-color-change {
|
||||
0% {
|
||||
color: #ff3334;
|
||||
/* 动画开始时的颜色 */
|
||||
/* Color for beginning of animation */
|
||||
}
|
||||
|
||||
20% {
|
||||
color: #ffcf00;
|
||||
/* 动画中间时的颜色 */
|
||||
/* Color for middle of animation */
|
||||
}
|
||||
|
||||
40% {
|
||||
color: #66ccff;
|
||||
/* 动画中间时的颜色 */
|
||||
/* Color for middle of animation */
|
||||
}
|
||||
|
||||
60% {
|
||||
color: #ff3399;
|
||||
/* 动画中间时的颜色 */
|
||||
/* Color for middle of animation */
|
||||
}
|
||||
|
||||
80% {
|
||||
color: #9933ff;
|
||||
/* 动画中间时的颜色 */
|
||||
/* Color for middle of animation */
|
||||
}
|
||||
|
||||
100% {
|
||||
color: #19e713;
|
||||
/* 动画结束时的颜色 */
|
||||
/* Color for ending of animation */
|
||||
}
|
||||
}
|
||||
|
||||
/* 应用动画到元素 */
|
||||
/* Animated text element */
|
||||
.animated-text {
|
||||
animation: text-color-change 2s infinite alternate;
|
||||
/* 动画名称,持续时间,循环次数,方向 */
|
||||
/* Animation name, duration, iteration count, direction */
|
||||
}
|
||||
|
||||
@keyframes hue {
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { ReadAll } from '@/../wailsjs/go/main/App.js'
|
||||
import utils from '@/assets/js/utils'
|
||||
|
||||
// 临时缓存文件信息
|
||||
// Temporary cache for file information
|
||||
function cache(fileObj, $event) {
|
||||
console.log('cache fileObj start:', fileObj, $event.target, $event.dataTransfer)
|
||||
console.log('Caching fileObj start:', fileObj, $event.target, $event.dataTransfer)
|
||||
console.log('typeof fileObj:', Array.isArray(fileObj))
|
||||
// 如果fileObj是数组,创建一个新的元素,追加到数组
|
||||
// event.target.files和event.dataTransfer.files是JavaScript中与文件上传和拖放相关的事件属性。
|
||||
// event.target.files:这个属性是在HTML的文件输入元素(<input type="file">)上使用时,
|
||||
// 当用户选择文件并触发change事件时,可以通过event.target.files获取到用户选择的文件列表。
|
||||
// event.dataTransfer.files:这个属性是在用户拖放文件到一个元素上时,
|
||||
// 可以通过event.dataTransfer.files获取到拖放的文件列表。
|
||||
// If fileObj is an array, create a new element and append to the array
|
||||
// event.target.files and event.dataTransfer.files are event properties in JavaScript related to file upload and drag-and-drop.
|
||||
// event.target.files: This property is used with HTML file input elements (<input type="file">),
|
||||
// When the user selects a file and triggers the change event, event.target.files can be used to get the list of files selected by the user.
|
||||
// event.dataTransfer.files: This property is used when the user drags and drops files onto an element,
|
||||
// event.dataTransfer.files can be used to get the list of dropped files.
|
||||
console.log('$event:', $event, $event.type)
|
||||
let files
|
||||
if ($event.type == 'change') {
|
||||
@ -17,36 +18,36 @@ function cache(fileObj, $event) {
|
||||
} else if ($event.type == 'drop') {
|
||||
files = $event.dataTransfer.files
|
||||
} else {
|
||||
console.error("无法识别的事件")
|
||||
console.error("Unrecognized event type")
|
||||
return
|
||||
}
|
||||
const file = files[0]
|
||||
console.log("file:", file)
|
||||
console.log("Selected file:", file)
|
||||
const fileInfo = Array.isArray(fileObj) ? new Object() : fileObj
|
||||
fileInfo.file = file
|
||||
let URL = window.URL || window.webkitURL
|
||||
fileInfo.fileUrl = URL.createObjectURL(file)
|
||||
const fileType = file.type
|
||||
console.log(fileType, typeof (fileType))
|
||||
console.log("File type:", fileType, typeof (fileType))
|
||||
if (utils.notNull(fileType) && fileType.startsWith("image")) {
|
||||
fileInfo.imgUrl = fileInfo.fileUrl
|
||||
}
|
||||
fileInfo.fileName = file.name
|
||||
console.log('cache fileObj end:', fileInfo)
|
||||
console.log('Caching fileObj completed:', fileInfo)
|
||||
if (Array.isArray(fileObj)) {
|
||||
// 操作成功后追加到数组末尾
|
||||
// Append to the end of the array after successful operation
|
||||
fileObj.push(fileInfo)
|
||||
}
|
||||
if ($event.type == 'change') {
|
||||
// 解决选择相同的文件 不触发change事件的问题,放在最后清理
|
||||
// Solve the problem of selecting the same file not triggering the change event, clean up at the end
|
||||
$event.target.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
// Upload file
|
||||
async function upload(fileObj) {
|
||||
console.log("准备开始上传文件!", fileObj, fileObj.file, fileObj.fileId)
|
||||
// 当前地址
|
||||
console.log("Preparing to upload file...", fileObj, fileObj.file, fileObj.fileId)
|
||||
// Current location handling
|
||||
if (utils.isNull(fileObj.file)) {
|
||||
if (utils.notNull(fileObj.fileId) && fileObj.remark != fileObj.remarkUpd) {
|
||||
let remark = null
|
||||
@ -57,7 +58,7 @@ async function upload(fileObj) {
|
||||
}
|
||||
return
|
||||
}
|
||||
console.log("开始上传文件!", fileObj, fileObj.file, fileObj.fileId)
|
||||
console.log("Starting file upload...", fileObj, fileObj.file, fileObj.fileId)
|
||||
const url = '/common/file/upload'
|
||||
const formData = new FormData()
|
||||
formData.append('file', fileObj.file)
|
||||
@ -72,38 +73,38 @@ async function upload(fileObj) {
|
||||
}
|
||||
})
|
||||
Object.assign(fileObj, data)
|
||||
console.log("文件同步上传处理完毕", fileObj)
|
||||
console.log("File upload processed successfully", fileObj)
|
||||
return fileObj
|
||||
}
|
||||
|
||||
// 更新文件备注
|
||||
// Update file remark
|
||||
async function updRemark(fileId, remarkUpd) {
|
||||
const param = {
|
||||
fileId: fileId,
|
||||
remark: remarkUpd
|
||||
}
|
||||
await utils.awaitPost('/common/file/updRemark', param)
|
||||
console.log("更新文件备注成功")
|
||||
console.log("File remark updated successfully")
|
||||
}
|
||||
|
||||
// 批量上传文件
|
||||
// Batch upload files
|
||||
async function uploads(fileObjs) {
|
||||
if (utils.isEmpty(fileObjs)) {
|
||||
return
|
||||
}
|
||||
for (let index in fileObjs) {
|
||||
console.log('fileObjs[index]:', fileObjs, index, fileObjs.length, fileObjs[index])
|
||||
console.log('Processing file object:', fileObjs, index, fileObjs.length, fileObjs[index])
|
||||
await upload(fileObjs[index])
|
||||
console.log("uploads index:", index, "上传文件完毕", fileObjs[index])
|
||||
console.log("uploads index:", index, "File upload completed", fileObjs[index])
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件(onChange时)
|
||||
// Handle file upload (onChange event)
|
||||
function upOnChg(fileObj, $event) {
|
||||
const file = $event.target.files[0] || $event.dataTransfer.files[0]
|
||||
// 当前地址
|
||||
// Current location
|
||||
let URL = window.URL || window.webkitURL
|
||||
// 转成 blob地址
|
||||
// Convert to blob URL
|
||||
fileObj.fileUrl = URL.createObjectURL(file)
|
||||
const url = '/common/file/upload'
|
||||
const formData = new FormData()
|
||||
@ -114,12 +115,13 @@ function upOnChg(fileObj, $event) {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}).then((data) => {
|
||||
console.log("文件上传结果:", data)
|
||||
console.log("File upload result:", data)
|
||||
Object.assign(fileObj, data)
|
||||
fileObj.remarkUpd = data.remark
|
||||
})
|
||||
}
|
||||
|
||||
// Add to component list
|
||||
function add(fileList) {
|
||||
const comp = {
|
||||
index: fileList.length,
|
||||
@ -133,8 +135,9 @@ function add(fileList) {
|
||||
fileList.push(comp)
|
||||
}
|
||||
|
||||
// Remove component from list
|
||||
function del(fileObj, index) {
|
||||
console.log("fileObj,index:", fileObj, index)
|
||||
console.log("Deleting file object:", fileObj, index)
|
||||
if (Array.isArray(fileObj)) {
|
||||
fileObj.splice(index, 1)
|
||||
} else {
|
||||
@ -142,11 +145,12 @@ function del(fileObj, index) {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert between Java and JS file objects
|
||||
function trans(javaFile, jsFile) {
|
||||
if (jsFile == undefined || jsFile == null) {
|
||||
return
|
||||
}
|
||||
// 如果是数组,先清空数组
|
||||
// Clear array if present
|
||||
if (jsFile instanceof Array) {
|
||||
jsFile.splice(0, jsFile.length)
|
||||
} else {
|
||||
@ -156,7 +160,7 @@ function trans(javaFile, jsFile) {
|
||||
if (javaFile == undefined || javaFile == null) {
|
||||
return
|
||||
}
|
||||
// 数组类型
|
||||
// Handle array type
|
||||
if (jsFile instanceof Array) {
|
||||
for (let java of javaFile) {
|
||||
const js = {}
|
||||
@ -165,37 +169,42 @@ function trans(javaFile, jsFile) {
|
||||
jsFile.push(js)
|
||||
}
|
||||
} else {
|
||||
// 对象类型
|
||||
console.log("对象类型", jsFile instanceof Array)
|
||||
// Handle object type
|
||||
console.log("Object type conversion", jsFile instanceof Array)
|
||||
javaFile.remarkUpd = javaFile.remark
|
||||
Object.assign(jsFile, javaFile)
|
||||
}
|
||||
}
|
||||
|
||||
// 从Comps中收集fileId
|
||||
// Collect file IDs from components
|
||||
function fileIds(fileList) {
|
||||
return fileList.map(comp => comp.fileId).join(',')
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
// onChange时缓存
|
||||
cache,
|
||||
// 上传文件
|
||||
upload,
|
||||
// 上传文件
|
||||
uploads,
|
||||
// 上传文件
|
||||
upOnChg,
|
||||
// onChange时上传
|
||||
upOnChg,
|
||||
// 添加到组件列表
|
||||
add,
|
||||
// 从组件列表中删除组件
|
||||
del,
|
||||
// 文件Java对象与js对象转换
|
||||
trans,
|
||||
// 从Comps中收集fileId
|
||||
fileIds
|
||||
|
||||
// Read file contents
|
||||
function readAll(filePath) {
|
||||
return ReadAll(filePath)
|
||||
}
|
||||
|
||||
export default {
|
||||
// Cache on onChange
|
||||
cache,
|
||||
// Upload file
|
||||
upload,
|
||||
// Upload files
|
||||
uploads,
|
||||
// Upload file
|
||||
upOnChg,
|
||||
// Upload on onChange
|
||||
upOnChg,
|
||||
// Add to component list
|
||||
add,
|
||||
// Delete component from component list
|
||||
del,
|
||||
// Convert between Java object and js object
|
||||
trans,
|
||||
// Collect fileId from Comps
|
||||
fileIds,
|
||||
// Read file
|
||||
readAll
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
/*
|
||||
* 显示页面遮罩
|
||||
* Show page shade
|
||||
*/
|
||||
export const showShade = function (closeCallBack) {
|
||||
const className = 'shade'
|
||||
@ -13,7 +13,7 @@ export const showShade = function (closeCallBack) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 隐藏页面遮罩
|
||||
* Hide page shade
|
||||
*/
|
||||
export const closeShade = function (closeCallBack = () => { }) {
|
||||
const shadeEl = document.querySelector('.layout-shade')
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { Greet } from '@/../wailsjs/go/main/App.js'
|
||||
import axios from "axios"
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Greet } from '@/../wailsjs/go/main/App.js'
|
||||
|
||||
/** axios start */
|
||||
// 创建 axios 实例
|
||||
// Create a new axios instance
|
||||
const $axios = axios.create({
|
||||
baseURL: "api",
|
||||
timeout: 12000
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
// Request interceptors
|
||||
$axios.interceptors.request.use(
|
||||
(config) => {
|
||||
config.headers["token"] = ''
|
||||
@ -27,24 +27,26 @@ $axios.interceptors.request.use(
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
// Response interceptors
|
||||
$axios.interceptors.response.use(
|
||||
(response) => {
|
||||
// console.log("response:", response)
|
||||
if (response.status == 200) {
|
||||
return response.data
|
||||
} else {
|
||||
pop("请求错误:" + response.status)
|
||||
pop("Exception occurred in response:" + response.status)
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.log("error:" + JSON.stringify(error))
|
||||
if (error.response == undefined || error.response == null) {
|
||||
pop("未知请求错误!")
|
||||
pop("Unknown request error!")
|
||||
pop("Unknown request error!")
|
||||
} else if (error.response.status == 500) {
|
||||
pop("请求后台服务异常,请稍后重试!")
|
||||
pop("Unable to communicate with backend, please retry later!")
|
||||
} else {
|
||||
pop("请求错误:" + error)
|
||||
pop("Request error:" + error)
|
||||
pop("Request error:" + error)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
@ -75,7 +77,7 @@ async function awaitDel(url, param) {
|
||||
}
|
||||
|
||||
/**
|
||||
* demo 调用 go 接口
|
||||
* demo call Go interfaces
|
||||
*/
|
||||
function greet(name) {
|
||||
return Greet(name).then(resp => {
|
||||
@ -85,78 +87,80 @@ function greet(name) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断对象为空
|
||||
* Check if object is null
|
||||
*/
|
||||
function isNull(obj) {
|
||||
return obj == undefined || obj == null
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断对象非空
|
||||
* Check if object is not null
|
||||
*/
|
||||
function notNull(obj) {
|
||||
return obj != undefined && obj != null
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断空字符串
|
||||
* Check if string is blank
|
||||
*/
|
||||
function isBlank(str) {
|
||||
return str == undefined || str == null || /^s*$/.test(str)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断不为空字符串
|
||||
* Identify a non-empty string
|
||||
*/
|
||||
function notBlank(str) {
|
||||
return !isBlank(str)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断数组为空
|
||||
*/
|
||||
* Check if array is empty
|
||||
*/
|
||||
function isEmpty(arr) {
|
||||
return arr == undefined || arr == null || (arr instanceof Array && arr.length == 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断数组非空
|
||||
* Check if array is not empty
|
||||
*/
|
||||
function notEmpty(arr) {
|
||||
return arr != undefined && arr != null && arr instanceof Array && arr.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断对象为true
|
||||
* Check if object is true
|
||||
*/
|
||||
function isTrue(obj) {
|
||||
return obj == true || obj == 'true'
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断对象为false
|
||||
* Check if object is false
|
||||
*/
|
||||
function isFalse(obj) {
|
||||
return !isTrue(obj)
|
||||
}
|
||||
|
||||
/** 获取字符串中某字符的个数
|
||||
* @param {string} str - 要搜索的字符串
|
||||
* @param {string} char - 要查找的字符
|
||||
* @returns {number} - 字符在字符串中出现的次数
|
||||
*/
|
||||
/**
|
||||
* Get count of a specific character in a string
|
||||
* @param {string} str - String to search
|
||||
* @param {string} char - Character to find
|
||||
* @returns {number} - Occurrence count
|
||||
*/
|
||||
function getCharCount(str, char) {
|
||||
// 使用g表示整个字符串都要匹配
|
||||
// g=match globally
|
||||
var regex = new RegExp(char, 'g')
|
||||
// match方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配
|
||||
// Search for all occurrences of the character in the string
|
||||
var result = str.match(regex)
|
||||
var count = !result ? 0 : result.length
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期格式化
|
||||
* 默认格式为yyyy-MM-dd HH:mm:ss
|
||||
* Format date with specified pattern
|
||||
* @param {Date|string} date - Date object or date string
|
||||
* @param {string} format - Target format pattern; by default, `yyyy-MM-dd HH:mm:ss`
|
||||
* @returns {string} - Formatted date string
|
||||
*/
|
||||
function dateFormat(date, format) {
|
||||
if (date == undefined || date == null || date == '') {
|
||||
@ -187,27 +191,28 @@ function dateFormat(date, format) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 遍历对象中的日期,并进行格式化
|
||||
* Recursively format Date properties in objects/arrays
|
||||
* @param {Object} obj - Target object to process
|
||||
*/
|
||||
function fomateDateProperty(obj) {
|
||||
for (let i in obj) {
|
||||
//遍历对象中的属性
|
||||
// Iterate through all properties of the object
|
||||
if (obj[i] == null) {
|
||||
continue
|
||||
} else if (obj[i] instanceof Date) {
|
||||
// 格式化为yyyy-MM-dd HH:mm:ss
|
||||
// Format as `yyyy-MM-dd HH:mm:ss`
|
||||
obj[i] = dateFormat(obj[i])
|
||||
} else if (obj[i].constructor === Object) {
|
||||
//如果发现该属性的值还是一个对象,再判空后进行迭代调用
|
||||
// Recursively format nested objects
|
||||
if (Object.keys(obj[i]).length > 0) {
|
||||
//判断对象上是否存在属性,如果为空对象则删除
|
||||
// Delete empty properties
|
||||
fomateDateProperty(obj[i])
|
||||
}
|
||||
} else if (obj[i].constructor === Array) {
|
||||
//对象值如果是数组,判断是否为空数组后进入数据遍历判空逻辑
|
||||
// Recursively clean nested arrays
|
||||
if (obj[i].length > 0) {
|
||||
for (let j = 0; j < obj[i].length; j++) {
|
||||
//遍历数组
|
||||
// Iterate through all array items
|
||||
fomateDateProperty(obj[i][j])
|
||||
}
|
||||
}
|
||||
@ -215,35 +220,37 @@ function fomateDateProperty(obj) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 遍历删除对象中的空值属性
|
||||
/**
|
||||
* Remove null/empty properties recursively
|
||||
* @param {Object} obj - Target object to clean
|
||||
*/
|
||||
function delNullProperty(obj) {
|
||||
for (let i in obj) {
|
||||
//遍历对象中的属性
|
||||
// Iterate through all properties of the object
|
||||
if (obj[i] === undefined || obj[i] === null || obj[i] === "") {
|
||||
//首先除去常规空数据,用delete关键字
|
||||
// Delete general null/empty properties
|
||||
delete obj[i]
|
||||
} else if (obj[i].constructor === Object) {
|
||||
//如果发现该属性的值还是一个对象,再判空后进行迭代调用
|
||||
// Recursively clean nested objects
|
||||
if (Object.keys(obj[i]).length === 0) delete obj[i]
|
||||
//判断对象上是否存在属性,如果为空对象则删除
|
||||
// Delete empty properties
|
||||
delNullProperty(obj[i])
|
||||
} else if (obj[i].constructor === Array) {
|
||||
//对象值如果是数组,判断是否为空数组后进入数据遍历判空逻辑
|
||||
// Recursively clean arrays
|
||||
if (obj[i].length === 0) {
|
||||
//如果数组为空则删除
|
||||
// Delete empty arrays
|
||||
delete obj[i]
|
||||
} else {
|
||||
for (let index = 0; index < obj[i].length; index++) {
|
||||
//遍历数组
|
||||
// Iterate through all array items
|
||||
if (obj[i][index] === undefined || obj[i][index] === null || obj[i][index] === "" || JSON.stringify(obj[i][index]) === "{}") {
|
||||
obj[i].splice(index, 1)
|
||||
//如果数组值为以上空值则修改数组长度,移除空值下标后续值依次提前
|
||||
// Delete null/empty array items
|
||||
index--
|
||||
//由于数组当前下标内容已经被替换成下一个值,所以计数器需要自减以抵消之后的自增
|
||||
// Do decrement to avoid skipping next item (index is now pointing to the next item)
|
||||
}
|
||||
if (obj[i].constructor === Object) {
|
||||
//如果发现数组值中有对象,则再次进入迭代
|
||||
// Recursively clean nested objects in array items
|
||||
delNullProperty(obj[i])
|
||||
}
|
||||
}
|
||||
@ -253,22 +260,27 @@ function delNullProperty(obj) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出消息框
|
||||
* @param msg 消息内容
|
||||
* @param type
|
||||
* Display message notification
|
||||
* @param {string} msg - Message content
|
||||
* @param {string} type - Message type (success/warning/error/etc)
|
||||
*/
|
||||
function pop(msg, type) {
|
||||
ElMessage({ message: msg, type: type })
|
||||
}
|
||||
|
||||
/**
|
||||
* Show default message when no data available
|
||||
* @param {*} data - Data to check
|
||||
*/
|
||||
function popNoData(data) {
|
||||
if (data == undefined || data == null || (data instanceof Array && data.length == 0)) {
|
||||
ElMessage("暂无数据!")
|
||||
ElMessage("No data available!")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前时间字符串
|
||||
* Get current datetime as formatted string
|
||||
* @returns {string} Current datetime in yyyy-MM-dd HH:mm format
|
||||
*/
|
||||
function nowDatetimeStr() {
|
||||
const date = new Date()
|
||||
@ -277,7 +289,9 @@ function nowDatetimeStr() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建分页
|
||||
* Pagination structure builder
|
||||
* @param {Object} source - Source pagination data
|
||||
* @param {Object} target - Target pagination object
|
||||
*/
|
||||
function buildPage(source, target) {
|
||||
target.pageNum = source.pageNum
|
||||
@ -287,7 +301,8 @@ function buildPage(source, target) {
|
||||
copyArray(source.list, target.list)
|
||||
}
|
||||
/**
|
||||
* 清空数组
|
||||
* Clear array contents
|
||||
* @param {Array} arr - Array to clear
|
||||
*/
|
||||
function clearArray(arr) {
|
||||
if (arr == undefined || arr == null || arr.length == 0) {
|
||||
@ -295,8 +310,10 @@ function clearArray(arr) {
|
||||
}
|
||||
arr.splice(0, arr.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空属性
|
||||
* Reset object properties to null
|
||||
* @param {Object} obj - Target object
|
||||
*/
|
||||
function clearProps(obj) {
|
||||
if (obj == undefined || obj == null) {
|
||||
@ -308,9 +325,11 @@ function clearProps(obj) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制对象属性
|
||||
* Copy properties between objects
|
||||
* @param {Object} source - Source object
|
||||
* @param {Object} target - Target object
|
||||
*/
|
||||
function copyProps(source, target) {
|
||||
function copyProps(source, target = {}) {
|
||||
if (target == undefined || target == null) {
|
||||
target = {}
|
||||
}
|
||||
@ -322,13 +341,15 @@ function copyProps(source, target) {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 复制数组
|
||||
* Clone array contents
|
||||
* @param {Array} source - Source array
|
||||
* @param {Array} target - Target array
|
||||
*/
|
||||
function copyArray(source, target) {
|
||||
if (target == undefined || target == null) {
|
||||
return
|
||||
}
|
||||
// 先清空数组
|
||||
// Clear the array first
|
||||
if (target.length > 0) {
|
||||
target.splice(0, target.length)
|
||||
/* while (target.length > 0) {
|
||||
@ -344,7 +365,10 @@ function copyArray(source, target) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发生变更的属性
|
||||
* Find changed properties between objects
|
||||
* @param {Object} origin - Original object
|
||||
* @param {Object} target - Modified object
|
||||
* @returns {Object} Changed properties
|
||||
*/
|
||||
function dfProps(origin, target) {
|
||||
if (origin == undefined || origin == null || target == undefined || target == null) {
|
||||
@ -359,9 +383,11 @@ function dfProps(origin, target) {
|
||||
return dfObj
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否存在不同属性
|
||||
* Check for property differences
|
||||
* @param {Object} origin - Original object
|
||||
* @param {Object} target - Modified object
|
||||
* @returns {boolean} True if differences exist
|
||||
*/
|
||||
function hasDfProps(origin, target) {
|
||||
const df = dfProps(origin, target)
|
||||
@ -374,7 +400,9 @@ function hasDfProps(origin, target) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有字段为空
|
||||
* Check if all object properties are null
|
||||
* @param {Object} target - Object to check
|
||||
* @returns {boolean} True if all properties are null
|
||||
*/
|
||||
function isAllPropsNull(target) {
|
||||
if (target == undefined || target == null) {
|
||||
@ -425,19 +453,21 @@ function colorByLabel(label) {
|
||||
|
||||
function descByLabel(label) {
|
||||
if ('ADD' == label) {
|
||||
return '新增'
|
||||
return 'Add'
|
||||
}
|
||||
if ('UPD' == label) {
|
||||
return '更新'
|
||||
return 'Update'
|
||||
}
|
||||
if ('DEL' == label) {
|
||||
return '删除'
|
||||
return 'Delete'
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试调用
|
||||
* Retry calls
|
||||
* @param {Function} method - Method to call
|
||||
* @param {any} params - Method parameters that are passed to the method
|
||||
*/
|
||||
function retry(method) {
|
||||
const params = []
|
||||
@ -450,7 +480,10 @@ function retry(method) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据opts编码匹配中文
|
||||
* Resolve label from options
|
||||
* @param {string|number} keyOrVal - Key or value to resolve
|
||||
* @param {Array} opts - Options array
|
||||
* @returns {string} Resolved label if found, or original keyOrVal if not found
|
||||
*/
|
||||
function resolveLabelFromOpts(keyOrVal, opts) {
|
||||
if (isEmpty(opts)) {
|
||||
@ -464,7 +497,11 @@ function resolveLabelFromOpts(keyOrVal, opts) {
|
||||
return keyOrVal
|
||||
}
|
||||
|
||||
/** 下划线转首字母小写驼峰 */
|
||||
/**
|
||||
* Underscored string to camel case string
|
||||
* @param {String} underscore Underscored string
|
||||
* @returns Camel case string
|
||||
*/
|
||||
function underScoreToCamelCase(underscore) {
|
||||
if (isNull(underscore) || !underscore.includes('_')) {
|
||||
return underscore
|
||||
@ -480,7 +517,12 @@ function underScoreToCamelCase(underscore) {
|
||||
return words.join("")
|
||||
}
|
||||
|
||||
/** 防抖函数 */
|
||||
/**
|
||||
* Debounce a function call
|
||||
* @param {Function} func Function to debounce
|
||||
* @param {Number} delay Delay in milliseconds
|
||||
* @returns Debounced function
|
||||
*/
|
||||
function debounce(func, delay) {
|
||||
let timer
|
||||
return function () {
|
||||
@ -494,44 +536,53 @@ function debounce(func, delay) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string to lines
|
||||
*/
|
||||
function stringToLines(str) {
|
||||
if (str == undefined || str == null) {
|
||||
return []
|
||||
}
|
||||
return str.split('\n')
|
||||
}
|
||||
export default {
|
||||
/**
|
||||
* http请求 GET请求
|
||||
* Synchronous GET HTTP request
|
||||
*/
|
||||
get,
|
||||
|
||||
/**
|
||||
* http请求, 异步等待 GET请求
|
||||
* Asynchronous GET HTTP request (async/await)
|
||||
*/
|
||||
awaitGet,
|
||||
|
||||
/**
|
||||
* http请求 POST请求
|
||||
* Synchronous POST HTTP request
|
||||
*/
|
||||
post,
|
||||
|
||||
/**
|
||||
* http请求, 异步等待 POST请求
|
||||
* Asynchronous POST HTTP request (async/await)
|
||||
*/
|
||||
awaitPost,
|
||||
|
||||
/**
|
||||
* http请求 DELETE请求
|
||||
* Synchronous DELETE HTTP request
|
||||
*/
|
||||
del,
|
||||
|
||||
/**
|
||||
* http请求, 异步等待 DELETE请求
|
||||
* Asynchronous DELETE HTTP request (async/await)
|
||||
*/
|
||||
awaitDel,
|
||||
|
||||
/**
|
||||
* 判断对象为空
|
||||
* Checks if a value is null/undefined
|
||||
*/
|
||||
isNull,
|
||||
|
||||
/**
|
||||
* 判断对象非空
|
||||
* Verifies a value is not null/undefined
|
||||
*/
|
||||
notNull,
|
||||
|
||||
@ -540,12 +591,12 @@ export default {
|
||||
notBlank,
|
||||
|
||||
/**
|
||||
* 判断数组为空
|
||||
* Checks if an array is empty
|
||||
*/
|
||||
isEmpty,
|
||||
|
||||
/**
|
||||
* 判断数组非空
|
||||
* Verifies an array contains elements
|
||||
*/
|
||||
notEmpty,
|
||||
|
||||
@ -556,64 +607,62 @@ export default {
|
||||
getCharCount,
|
||||
|
||||
/**
|
||||
* 弹出消息提示
|
||||
* Displays a toast notification
|
||||
*/
|
||||
pop,
|
||||
|
||||
/**
|
||||
* 判定数据是否为空, 如果为空则提示暂无数据
|
||||
* Shows "No data" notification for empty datasets
|
||||
*/
|
||||
popNoData,
|
||||
|
||||
/**
|
||||
* 遍历删除对象中的空值属性
|
||||
* Removes null/undefined properties from an object
|
||||
*/
|
||||
delNullProperty,
|
||||
|
||||
/**
|
||||
*
|
||||
* 当前时间字符串
|
||||
* Gets current datetime as formatted string (YYYY-MM-DD HH:mm:ss)
|
||||
*/
|
||||
nowDatetimeStr,
|
||||
|
||||
/**
|
||||
* 构建分页
|
||||
* Constructs pagination parameters
|
||||
*/
|
||||
buildPage,
|
||||
|
||||
/**
|
||||
* 清空数组
|
||||
* Clears all elements from an array
|
||||
*/
|
||||
clearArray,
|
||||
|
||||
/**
|
||||
* 清空属性
|
||||
* Resets object properties to null/undefined
|
||||
*/
|
||||
clearProps,
|
||||
|
||||
/**
|
||||
* 复制对象属性
|
||||
* Copies properties between objects
|
||||
*/
|
||||
copyProps,
|
||||
|
||||
/**
|
||||
* 复制数组
|
||||
* Creates a shallow array copy
|
||||
*/
|
||||
copyArray,
|
||||
|
||||
/**
|
||||
* 日期格式化
|
||||
* 默认格式为yyyy-MM-dd HH:mm:ss
|
||||
* Formats Date object to string (customizable format)
|
||||
*/
|
||||
dateFormat,
|
||||
|
||||
/**
|
||||
* 遍历对象中的日期,并进行格式化
|
||||
* Formats Date properties in objects to strings
|
||||
*/
|
||||
fomateDateProperty,
|
||||
|
||||
/**
|
||||
* 发生变更的属性
|
||||
* Tracks changed properties between object states
|
||||
*/
|
||||
dfProps,
|
||||
|
||||
@ -626,7 +675,7 @@ export default {
|
||||
descByLabel,
|
||||
|
||||
/**
|
||||
* 重试调用
|
||||
* Retries failed operations with attempts
|
||||
*/
|
||||
retry,
|
||||
|
||||
@ -636,4 +685,6 @@ export default {
|
||||
|
||||
debounce,
|
||||
|
||||
stringToLines,
|
||||
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
import utils from '@/assets/js/utils'
|
||||
|
||||
/** 英文编码正则 */
|
||||
/** Regex for English letters, numbers, and underscores */
|
||||
const codeReg = /^[A-Za-z0-9_\-\.]+$/
|
||||
|
||||
/** 手机号正则 */
|
||||
/** Regex for mobile phone number in China (Mainland) */
|
||||
const mobileReg = /^1[3456789]\d{9}$/
|
||||
|
||||
/** 大陆身份证正则 */
|
||||
/** Regex for ID card number in China (Mainland) */
|
||||
const idNoReg = /^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/
|
||||
|
||||
/** email正则 */
|
||||
/** Regex for email */
|
||||
const emailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
|
||||
|
||||
const commonValidator = (rule, value, callback) => {
|
||||
@ -22,7 +22,7 @@ const commonValidator = (rule, value, callback) => {
|
||||
|
||||
const notBlankValidator = (rule, value, callback) => {
|
||||
if (utils.isBlank(value)) {
|
||||
callback(new Error('输入不能为空'))
|
||||
callback(new Error('Input cannot be blank'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@ -32,7 +32,7 @@ const nameValidator = (rule, value, callback) => {
|
||||
if (utils.isBlank(value)) {
|
||||
callback()
|
||||
} else if (value.length > 50) {
|
||||
callback(new Error('字符数不能超过50'))
|
||||
callback(new Error('Name too long (max 50 characters)'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@ -42,7 +42,7 @@ const mobileValidator = (rule, value, callback) => {
|
||||
if (utils.isNull(value)) {
|
||||
callback()
|
||||
} else if (!mobileReg.test(value)) {
|
||||
callback(new Error('手机号格式错误'))
|
||||
callback(new Error('Invalid mobile number'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@ -52,7 +52,7 @@ const idNoValidator = (rule, value, callback) => {
|
||||
if (utils.isNull(value)) {
|
||||
callback()
|
||||
} else if (!idNoReg.test(value)) {
|
||||
callback(new Error('手机号格式错误'))
|
||||
callback(new Error('Invalid ID card number'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@ -62,7 +62,7 @@ const emailValidator = (rule, value, callback) => {
|
||||
if (utils.isNull(value)) {
|
||||
callback()
|
||||
} else if (!emailReg.test(value)) {
|
||||
callback(new Error('手机号格式错误'))
|
||||
callback(new Error('Invalid email address'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@ -72,7 +72,8 @@ const codeValidator = (rule, value, callback) => {
|
||||
if (utils.isBlank(value)) {
|
||||
callback()
|
||||
} else if (!codeReg.test(value)) {
|
||||
callback(new Error('编码格式错误'))
|
||||
callback(new Error('Invalid code format'))
|
||||
callback(new Error('Invalid code format'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@ -82,7 +83,7 @@ const intValidator = (rule, value, callback) => {
|
||||
if (utils.isBlank(value)) {
|
||||
callback()
|
||||
} else if (!Number.isInteger(value)) {
|
||||
callback(new Error('请输入整数'))
|
||||
callback(new Error('Input must be an integer'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@ -92,7 +93,7 @@ function validator() {
|
||||
console.log("arguments:", arguments)
|
||||
if (arguments.length <= 1) {
|
||||
const type = arguments[0]
|
||||
// 默认校验逻辑, 不含有特殊字符
|
||||
// Default validation logic, no special characters
|
||||
if (utils.isBlank(type)) {
|
||||
return commonValidator
|
||||
} else if (type == 'notBlank') {
|
||||
@ -113,22 +114,22 @@ function validator() {
|
||||
return commonValidator
|
||||
}
|
||||
}
|
||||
// 复合校验器
|
||||
// Complex validators
|
||||
const complexValidator = (rule, value, callback) => {
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
const typeStr = arguments[i]
|
||||
if (typeStr == 'notBlank' && utils.isBlank(value)) {
|
||||
callback(new Error('输入不能为空'))
|
||||
callback(new Error('Input cannot be blank'))
|
||||
break
|
||||
} else if (typeStr == 'code' && !codeReg.test(value)) {
|
||||
callback(new Error('编码格式错误'))
|
||||
callback(new Error('Invalid code format'))
|
||||
break
|
||||
} else if (typeStr == 'int' && Number.isInteger(value)) {
|
||||
callback(new Error('请输入整数'))
|
||||
callback(new Error('Please enter an integer'))
|
||||
break
|
||||
}
|
||||
}
|
||||
// 兜底callback()只会触发一次
|
||||
// Ensure callback is called at least once
|
||||
callback()
|
||||
}
|
||||
return complexValidator
|
||||
@ -138,62 +139,62 @@ export default {
|
||||
|
||||
username: (username) => {
|
||||
if (typeof (username) == "undefined" || username == null) {
|
||||
return "账号不能为空"
|
||||
return "Username cannot be blank"
|
||||
}
|
||||
username = username.trim()
|
||||
if (username.length < 4) {
|
||||
return "账号字符不能小于4位"
|
||||
return "Username must be at least 4 characters long"
|
||||
}
|
||||
if (username.length > 20) {
|
||||
return "账号字符不能大于20位"
|
||||
return "Username must be at most 20 characters long"
|
||||
}
|
||||
const reg = /^[A-Za-z0-9]+$/
|
||||
if (!reg.test(username)) {
|
||||
return "账号为必须为字母和数字"
|
||||
return "Username must be letters and numbers only"
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
password: (password) => {
|
||||
if (typeof (password) == "undefined" || password == null) {
|
||||
return "密码不能为空"
|
||||
return "Password cannot be blank"
|
||||
}
|
||||
password = password.trim()
|
||||
if (password.length < 4) {
|
||||
return "密码字符不能小于4位"
|
||||
return "Password must be at least 4 characters long"
|
||||
}
|
||||
if (password.length > 20) {
|
||||
return "密码字符不能大于20位"
|
||||
return "Password must be at most 20 characters long"
|
||||
}
|
||||
const reg = /^[A-Za-z0-9\.\-\_\+]+$/
|
||||
if (!reg.test(password)) {
|
||||
return "密码为必须为字母和数字或.-+_"
|
||||
return "Password must be letters, numbers, and special characters (.-_+) only"
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
email: (email) => {
|
||||
if (typeof (email) == "undefined" || email == null) {
|
||||
return "邮箱不能为空"
|
||||
return "Email cannot be blank"
|
||||
}
|
||||
const reg = /^[A-Za-z0-9._%-]+@([A-Za-z0-9-]+\.)+[A-Za-z]{2,4}$/
|
||||
if (!reg.test(email)) {
|
||||
return "邮箱格式不正确"
|
||||
return "Invalid email address"
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
validCode: (validCode) => {
|
||||
if (typeof (validCode) == "undefined" || validCode == null) {
|
||||
return "验证码不能为空"
|
||||
return "Verification code cannot be blank"
|
||||
}
|
||||
validCode = validCode.trim()
|
||||
if (validCode.length != 6) {
|
||||
return "验证码必须为6位"
|
||||
return "Verification code must be 6 characters long"
|
||||
}
|
||||
const reg = /^[A-Za-z0-9]{6}$/
|
||||
if (!reg.test(validCode)) {
|
||||
return "验证码格式不正确"
|
||||
return "Invalid verification code format"
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
@ -37,18 +37,18 @@ export default defineConfig({
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
// 分解块,将大块分解成更小的块
|
||||
// Split chunks, break large chunks into smaller ones
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
// 让每个插件都打包成独立的文件
|
||||
return id.toString().split('node_modules/')[1].split('/')[0].toString();
|
||||
// Let each plugin be packaged into an independent file
|
||||
return id.toString().split('node_modules/')[1].split('/')[0].toString()
|
||||
}
|
||||
// Unit b, merge smaller modules
|
||||
experimentalMinChunkSize: 10 * 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 单位b, 合并较小模块
|
||||
experimentalMinChunkSize: 10 * 1024,
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -52,7 +52,7 @@ const handleClose = (key, keyPath) => {
|
||||
// console.log(key, keyPath)
|
||||
}
|
||||
|
||||
// 菜单
|
||||
// Menu List
|
||||
const menuList = [
|
||||
{
|
||||
index: "M02",
|
||||
@ -89,7 +89,7 @@ const menuList = [
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
// 页面刷新后,检查菜单定位
|
||||
// Check menu position after refresh
|
||||
// activeMenu()
|
||||
})
|
||||
|
||||
@ -107,24 +107,35 @@ function listSubMenu(menuCode) {
|
||||
|
||||
watch(() => router.currentRoute.value.path, (newValue, oldValue) => {
|
||||
// console.log('LeftMenu侦听到router.currentRoute.value.path发生更新', newValue, oldValue)
|
||||
// 路由发生变更后,检查菜单定位
|
||||
// Check menu position after route change
|
||||
activeMenu()
|
||||
})
|
||||
|
||||
// 检查激活的菜单
|
||||
// Check activated menu position
|
||||
function activeMenu() {
|
||||
const currRoute = router.currentRoute
|
||||
const path = currRoute.value.path
|
||||
// console.log("currRoute path:", path)
|
||||
console.log("currRoute path:", path)
|
||||
let index = getIndexByPath(path)
|
||||
// console.log("index:", index)
|
||||
console.log("index:", index)
|
||||
if (utils.notNull(index)) {
|
||||
return index
|
||||
}
|
||||
// No match, try to find menu for parent path
|
||||
const lastIndex = path.lastIndexOf('/')
|
||||
if (lastIndex != -1) {
|
||||
const newPath = path.substring(0, lastIndex)
|
||||
console.log("newPath from parent path:", newPath)
|
||||
index = getIndexByPath(newPath)
|
||||
console.log("index from parent path:", index)
|
||||
if (utils.notNull(index)) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return "1"
|
||||
}
|
||||
|
||||
// 根据路径查询菜单index
|
||||
// Query menu index by path
|
||||
function getIndexByPath(path) {
|
||||
for (let fstMenu of menuList) {
|
||||
// console.log(fstMenu.index, fstMenu.href == path)
|
||||
@ -146,7 +157,7 @@ function getIndexByPath(path) {
|
||||
return thdMenu.index
|
||||
}
|
||||
}
|
||||
// 三级菜单path都没匹配上,取路由配置中的to进行匹配
|
||||
// If no third-level menu path matches, use the 'to' from the route configuration to find a match
|
||||
for (let thdMenu of thdMenuList) {
|
||||
const nodeList = routeMap.get(path)
|
||||
if (utils.isEmpty(nodeList)) {
|
||||
@ -154,14 +165,14 @@ function getIndexByPath(path) {
|
||||
}
|
||||
for (let node of nodeList) {
|
||||
if (node.to == thdMenu.href) {
|
||||
// console.log("匹配上node.to:", node.to)
|
||||
// console.log("A match was found for node.to:", node.to)
|
||||
return thdMenu.index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 二级菜单都没匹配上,取路由配置中的to进行匹配
|
||||
// Iterate through each secondary menu item in the secMenuList
|
||||
for (let secMenu of secMenuList) {
|
||||
// console.log(secMenu.index, secMenu.href == path)
|
||||
const nodeList = routeMap.get(path)
|
||||
@ -176,13 +187,13 @@ function getIndexByPath(path) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 一级菜单都没匹配上,取路由配置中的to进行匹配
|
||||
// None of the menu items match the path, try to find a match for the 'to' from the route configuration
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 获取routes配置
|
||||
const routes = router.options.routes;
|
||||
// get routes configuration
|
||||
const routes = router.options.routes
|
||||
// console.log("routes:", routes)
|
||||
const routeMap = new Map()
|
||||
routes.forEach(lv1 => {
|
||||
@ -250,7 +261,7 @@ function getMenuNameByCode(code) {
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
/* 防止双击选中 */
|
||||
/* Prevent text selection from double-clicking */
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
@ -258,16 +269,16 @@ span {
|
||||
}
|
||||
|
||||
span {
|
||||
/* 字体大小 */
|
||||
/* Font size */
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
li {
|
||||
/* 字体大小 */
|
||||
/* Font size */
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/** 菜单折叠时hover菜单项高度这里必须再定义一次 */
|
||||
/** When the menu is collapsed, redefine the hover menu height */
|
||||
.el-menu-item {
|
||||
min-width: 44px;
|
||||
height: 36px;
|
||||
|
@ -25,17 +25,18 @@
|
||||
</el-header>
|
||||
<el-main>
|
||||
<el-scrollbar style="width: 100%;">
|
||||
<!-- 路由展示区 -->
|
||||
<!-- { Component }指当前路由所对应的组件 -->
|
||||
<!-- Router View Container -->
|
||||
<!-- { Component } = currently matched route component -->
|
||||
<RouterView v-slot="{ Component }">
|
||||
<!-- 添加过渡动画 需要确保插入的component元素只有一个根节点, 否则报错. component中的根元素的transition会覆盖transitionName的样式
|
||||
而且需要保证component中根元素的宽度相同所以最好是统一给component添加一个根元素 -->
|
||||
<!-- Cached Route Transition: Only keeps alive components with keepAlive meta flag
|
||||
Transition animation requires single root element in component Key ensures proper re-rendering on route path changes -->
|
||||
<transition :name="transitionName">
|
||||
<KeepAlive>
|
||||
<Component :is="Component" v-if="keepAlive" :key="$route.path" />
|
||||
</KeepAlive>
|
||||
</transition>
|
||||
<!-- 添加过渡动画 需要确保插入的component元素只有一个根节点 -->
|
||||
<!-- Non-cached Route Transition: Fresh instance for other components
|
||||
Separate transition to prevent animation conflicts -->
|
||||
<transition :name="transitionName">
|
||||
<Component :is="Component" v-if="!keepAlive" :key="$route.path" />
|
||||
</transition>
|
||||
@ -66,46 +67,49 @@ const config = useConfig()
|
||||
const { shrink, menuCollapse } = storeToRefs(config)
|
||||
const currentRoute = reactive(router.currentRoute)
|
||||
|
||||
// 默认动画效果, 向左滑动
|
||||
// Default transition effect, slide to the left
|
||||
let transitionName = 'slide-left'
|
||||
|
||||
const keepAlive = computed(() => {
|
||||
return currentRoute.value.meta.keepAlive
|
||||
})
|
||||
|
||||
/** 固定菜单头展开折叠动画时间 刷新页面时菜单不会展开或折叠, 设置持续时间为0, 不产生动画 */
|
||||
/**
|
||||
* Set the menu animation duration to 0ms on page refresh to prevent the menu from expanding or collapsing
|
||||
* with an animation. This ensures that the menu state remains consistent after a page reload.
|
||||
*/
|
||||
const menuAnimationDuration = ref(0)
|
||||
|
||||
// 菜单折叠展开切换
|
||||
// Function to toggle the menu between expanded and collapsed states
|
||||
function menuToggle() {
|
||||
menuAnimationDuration.value = '300ms'
|
||||
|
||||
if (menuCollapse.value) {
|
||||
// console.log("折叠状态下, 进行展开菜单")
|
||||
// console.log("Extend menu")
|
||||
if (shrink.value) {
|
||||
// 收缩时, 展开遮罩
|
||||
// Expend the shade if menu is collapsing
|
||||
showShade(() => {
|
||||
// console.log("这里定义关闭遮罩回调函数, 关闭遮罩后, 折叠菜单")
|
||||
// Callback function to close the shade after the menu has collapsed
|
||||
config.setMenuCollapse(true)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// console.log("展开状态下, 进行折叠菜单, 关闭掉侧栏遮罩")
|
||||
// If the menu is in an expanded state, close the shade
|
||||
closeShade()
|
||||
}
|
||||
// 切换菜单折叠状态
|
||||
// Toggle the menu state
|
||||
config.setMenuCollapse(!menuCollapse.value)
|
||||
}
|
||||
|
||||
function onAdaptiveLayout() {
|
||||
// 获取当前窗口宽度
|
||||
// Get the current window width
|
||||
const clientWidth = document.body.clientWidth
|
||||
// console.log("menuCollapse:", menuCollapse.value, config.getMenuCollapse(), "clientWidth:", clientWidth)
|
||||
// 设定aside是否收缩
|
||||
// Determine if the aside menu should be shrunk based on the window width
|
||||
if (clientWidth < 800) {
|
||||
config.setShrink(true)
|
||||
if (!menuCollapse.value) {
|
||||
// 展开状态下, 收起菜单
|
||||
// Collapse the menu if it is not already collapsed
|
||||
menuToggle()
|
||||
}
|
||||
} else {
|
||||
@ -119,16 +123,15 @@ onBeforeMount(() => {
|
||||
})
|
||||
|
||||
watch(() => router.currentRoute.value.path, (newValue, oldValue) => {
|
||||
// console.log("路由变化,如果是收缩状态,则收起菜单:", newValue, oldValue)
|
||||
// If the layout is shrunk and the menu is expanded, collapse the menu
|
||||
if (shrink.value && !menuCollapse.value) {
|
||||
// console.log("收缩状态下, 且菜单展开时, 收起菜单")
|
||||
menuToggle()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
function refresh() {
|
||||
// console.log("刷新页面")
|
||||
// Reload the page
|
||||
location.reload()
|
||||
}
|
||||
|
||||
@ -143,7 +146,11 @@ header {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 0px;
|
||||
background-color: var(--el-bg-color);
|
||||
/* width: calc(100% -32px);
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
border-radius: 6px; */
|
||||
background-color: var(--el-fg-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -169,6 +176,7 @@ main {
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.menu-logo {
|
||||
@ -177,7 +185,7 @@ main {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/** 菜单折叠 */
|
||||
/* Keyframes for the menu collapse animation */
|
||||
@keyframes menuCollapse {
|
||||
0% {
|
||||
width: 200px;
|
||||
@ -188,7 +196,7 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
/** 菜单展开 */
|
||||
/* Keyframes for the menu expand animation */
|
||||
@keyframes menuExpand {
|
||||
0% {
|
||||
width: 44px;
|
||||
@ -204,9 +212,9 @@ main {
|
||||
z-index: 9999;
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
/* 引用上面定义的@keyframes名称 */
|
||||
/* Reference to the keyframes */
|
||||
animation-name: menuCollapse;
|
||||
/* 动画持续时间 */
|
||||
/* Duration of the animation */
|
||||
animation-duration: v-bind('menuAnimationDuration');
|
||||
animation-timing-function: ease-in-out;
|
||||
background-color: var(--el-fg-color);
|
||||
@ -217,13 +225,13 @@ main {
|
||||
z-index: 9999;
|
||||
height: 44px;
|
||||
width: 200px;
|
||||
/* 引用上面定义的@keyframes名称 */
|
||||
/* Reference to the keyframes */
|
||||
animation-name: menuExpand;
|
||||
/* 动画持续时间 */
|
||||
/* Duration of the animation */
|
||||
animation-duration: v-bind('menuAnimationDuration');
|
||||
animation-timing-function: ease-in-out;
|
||||
background-color: var(--el-fg-color);
|
||||
z-index: 9999999
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
.scrollbar-menu-wrapper {
|
||||
@ -235,6 +243,6 @@ main {
|
||||
.scrollbar-menu-wrapper.shrink {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
z-index: 9999999
|
||||
z-index: 9999999;
|
||||
}
|
||||
</style>
|
||||
|
@ -6,22 +6,16 @@
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="toAddPage" style="vertical-align: middle;">
|
||||
<el-icon :size="20" class="pr-4">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
新增
|
||||
</el-button>
|
||||
<el-button type="danger" class="ml-10" @click="delSelected" :disabled="selectedRows.length == 0">
|
||||
<el-icon :size="20" class="pr-4">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
删除
|
||||
{{ t('delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-show="advSearch">
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
<el-button @click="resetSearch"> {{ t('reset') }}</el-button>
|
||||
<el-button type="primary" @click="search"> {{ t('search') }}</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-input v-model="searchForm.kw" @input="baseSearch" clearable v-show="!advSearch" class="mr-8" />
|
||||
@ -54,7 +48,9 @@
|
||||
|
||||
<script setup>
|
||||
import { Refresh, Search, Grid, Plus, Delete } from '@element-plus/icons-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = defineProps({
|
||||
advSearch: {
|
||||
default: false
|
||||
@ -69,9 +65,6 @@ const props = defineProps({
|
||||
},
|
||||
selectedRows: {
|
||||
default: []
|
||||
},
|
||||
addable: {
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
@ -82,7 +75,6 @@ const emits = defineEmits([
|
||||
'checkTableColumn',
|
||||
'delSelected',
|
||||
'resetSearch',
|
||||
'toAddPage',
|
||||
])
|
||||
|
||||
const baseSearch = () => {
|
||||
@ -111,9 +103,6 @@ const resetSearch = () => {
|
||||
emits('resetSearch')
|
||||
}
|
||||
|
||||
const toAddPage = () => {
|
||||
emits('toAddPage')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -27,6 +27,13 @@
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<!-- 右侧 -->
|
||||
<div class="fxc">
|
||||
<div class="mlr-8">
|
||||
<el-switch v-model="isDark" :active-action-icon="Moon" :inactive-action-icon="Sunny" width="40"
|
||||
style="--el-switch-on-color: #4c4d4f; --el-switch-off-color: #f2f2f2;" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧固定下拉 -->
|
||||
<el-dropdown trigger="click" @command="handleSwitchLang" class="fxc plr-16">
|
||||
<span class="el-dropdown-link">
|
||||
@ -44,14 +51,18 @@
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ArrowDown, Refresh } from '@element-plus/icons-vue'
|
||||
import { ArrowDown, Refresh, Moon, Sunny } from '@element-plus/icons-vue'
|
||||
import { useConfig } from '@/store/config'
|
||||
/** 暗黑主题切换 */
|
||||
import { useDark } from '@vueuse/core'
|
||||
|
||||
const config = useConfig()
|
||||
const isDark = useDark()
|
||||
|
||||
const modelList = ref(config.modelList)
|
||||
|
||||
|
@ -1,4 +1,24 @@
|
||||
export default {
|
||||
add: "Add",
|
||||
edit: "Edit",
|
||||
delete: "Delete",
|
||||
search: "Search",
|
||||
reset: "Reset",
|
||||
confirm: "Confirm",
|
||||
cancel: "Cancel",
|
||||
save: "Save",
|
||||
submit: "Submit",
|
||||
export: "Export",
|
||||
import: "Import",
|
||||
copy: "Copy",
|
||||
paste: "Paste",
|
||||
cut: "Cut",
|
||||
baseInfo: "Base Info",
|
||||
|
||||
createdDt: "Created Date",
|
||||
updatedDt: "Updated Date",
|
||||
noData: "No Data",
|
||||
|
||||
menu: {
|
||||
task: "Task",
|
||||
history: "History",
|
||||
@ -13,6 +33,9 @@ export default {
|
||||
switchModel: "Switch Model",
|
||||
step: "Step",
|
||||
promptInputPlaceHolder: "Please Input Task Prompt",
|
||||
promptInput: "Prompt Input",
|
||||
promptInputKw: "Prompt Input",
|
||||
clearCache: "Clear Cache",
|
||||
clearCacheSuccess: "Clear cache success",
|
||||
openManusAgiTips: "The above content is generated by OpenManus for reference only",
|
||||
taskStatus: {
|
||||
@ -21,5 +44,10 @@ export default {
|
||||
failed: "Failed",
|
||||
running: "Running",
|
||||
terminated: "Terminated",
|
||||
}
|
||||
},
|
||||
newTask: "New Task",
|
||||
readConfigSuccess: "Read config success",
|
||||
readConfigFailed: "Read config failed",
|
||||
baseConfig: "Base Settings",
|
||||
serverConfig: "Server Config",
|
||||
}
|
||||
|
@ -1,4 +1,24 @@
|
||||
export default {
|
||||
add: "新增",
|
||||
edit: "编辑",
|
||||
delete: "删除",
|
||||
search: "搜索",
|
||||
reset: "重置",
|
||||
confirm: "确认",
|
||||
cancel: "取消",
|
||||
save: "保存",
|
||||
submit: "提交",
|
||||
export: "导出",
|
||||
import: "导入",
|
||||
copy: "复制",
|
||||
paste: "粘贴",
|
||||
cut: "剪切",
|
||||
baseInfo: "基本信息",
|
||||
|
||||
createdDt: "创建时间",
|
||||
updatedDt: "更新时间",
|
||||
noData: "暂无数据",
|
||||
|
||||
menu: {
|
||||
task: "任务",
|
||||
history: "历史记录",
|
||||
@ -12,6 +32,9 @@ export default {
|
||||
user: '用户',
|
||||
step: "步骤",
|
||||
promptInputPlaceHolder: "请输入任务提示词",
|
||||
promptInput: "提示词输入",
|
||||
promptInputKw: "提示词关键字",
|
||||
clearCache: "清理缓存",
|
||||
clearCacheSuccess: "清理缓存成功",
|
||||
openManusAgiTips: "以上内容由OpenManus生成, 仅供参考和借鉴",
|
||||
taskStatus: {
|
||||
@ -20,5 +43,10 @@ export default {
|
||||
failed: "失败",
|
||||
running: "运行中",
|
||||
terminated: "终止",
|
||||
}
|
||||
},
|
||||
newTask: "新任务",
|
||||
readConfigSuccess: "读取配置成功",
|
||||
readConfigFailed: "读取配置失败",
|
||||
baseConfig: "基础设置",
|
||||
serverConfig: "服务器配置",
|
||||
}
|
||||
|
@ -1,25 +1,25 @@
|
||||
import './assets/css/main.css'
|
||||
|
||||
import files from '@/assets/js/files'
|
||||
import utils from '@/assets/js/utils'
|
||||
import verify from '@/assets/js/verify'
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
import files from '@/assets/js/files'
|
||||
import verify from '@/assets/js/verify'
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import i18n from './locales/i18n'
|
||||
import router from './router'
|
||||
|
||||
// import ElementPlus from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
/* 暗黑主题模式 */
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import '@/assets/less/light.css'
|
||||
/* Dark theme configuration */
|
||||
import '@/assets/less/dark.css'
|
||||
// 定义特性标志
|
||||
window.__VUE_PROD_DEVTOOLS__ = false;
|
||||
window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false;
|
||||
import '@/assets/less/light.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
// Configure Vue production flags
|
||||
window.__VUE_PROD_DEVTOOLS__ = false
|
||||
window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false
|
||||
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
@ -28,21 +28,21 @@ const app = createApp(App)
|
||||
|
||||
app.use(pinia)
|
||||
|
||||
// 全局引用router
|
||||
// Globally reference router
|
||||
app.use(router)
|
||||
|
||||
app.use(i18n)
|
||||
|
||||
// ElMessage需要在utils中使用,这里单独引入
|
||||
// Register Element Plus Message component globally (required for use in utils)
|
||||
app.use(ElMessage)
|
||||
|
||||
// 全局使用
|
||||
// Global configuration of ElementPlus
|
||||
// ElSelect.props.placeholder.default = '请选择'
|
||||
// 在引入 ElementPlus 时,可以传入一个包含 size 和 zIndex 属性的全局配置对象。
|
||||
// size 用于设置表单组件的默认尺寸,zIndex 用于设置弹出组件的层级,zIndex 的默认值为 2000。
|
||||
// When ElementPlus is imported, a global configuration object can be passed in which contains size and zIndex properties
|
||||
// size is used to set the default size of form components, and zIndex is used to set the layer level of pop-up components.
|
||||
// app.use(ElementPlus, { locale, size: 'default', zIndex: 2000 })
|
||||
|
||||
// 使用vue3 provide注册
|
||||
// Configure global providers for shared utilities
|
||||
app.provide('utils', utils)
|
||||
|
||||
|
||||
@ -52,4 +52,3 @@ app.provide('verify', verify)
|
||||
/* app.provide('uuid', uuidv4) */
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
|
@ -14,18 +14,27 @@ const router = createRouter({
|
||||
children: [
|
||||
{
|
||||
path: 'task',
|
||||
component: () => import('@/views/main/Task.vue'),
|
||||
component: () => import('@/views/task/TaskIndex.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
title: "任务",
|
||||
keepAlive: true,
|
||||
title: "任务列表",
|
||||
index: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'task/:id',
|
||||
component: () => import('@/views/task/TaskInfo.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
title: "任务信息",
|
||||
index: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'history',
|
||||
component: () => import('@/views/main/Home.vue'),
|
||||
component: () => import('@/views/task/HistoryIndex.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
keepAlive: true,
|
||||
title: "历史记录",
|
||||
index: 0
|
||||
}
|
||||
|
@ -3,33 +3,211 @@
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="title fxsb">
|
||||
<div>基本信息</div>
|
||||
<div>{{ t('baseConfig') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Show Data -->
|
||||
<div class="card-row-wrap" v-show="baseShow">
|
||||
<div class="card-row-aline fxsb">
|
||||
<el-text>{{ t('clearCache') }}:</el-text>
|
||||
<el-button type="danger" class="mlr-10" @click="clearCache">{{ t('clearCache') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="title fxsb">
|
||||
<div> {{ t('serverConfig') }}</div>
|
||||
<div>
|
||||
<el-link type="primary" class="no-select plr-6" @click="clearCache()">清理缓存</el-link>
|
||||
<el-link type="primary" class="no-select plr-6" @click="toEdit('server')" v-show="serverShow">
|
||||
{{ t('edit') }}
|
||||
</el-link>
|
||||
<el-link type="primary" class="no-select plr-6" @click="toShow('server')" v-show="serverEdit">
|
||||
{{ t('cancel') }}
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- No Data -->
|
||||
<div class="no-data" v-show="serverNoData">{{ t('noData') }}</div>
|
||||
|
||||
<!-- Show Data -->
|
||||
<div class="card-row-wrap" v-show="serverShow">
|
||||
<div class="card-row-item">
|
||||
<el-text>host:</el-text>
|
||||
<el-text>{{ serverConfig.host }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>port:</el-text>
|
||||
<el-text tag="p">{{ serverConfig.port }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Module -->
|
||||
<el-form ref="ruleFormRef" :model="serverConfigUpd" status-icon :rules="rules" v-show="serverEdit">
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-item">
|
||||
<el-text>host:</el-text>
|
||||
<el-form-item prop="host">
|
||||
<el-input v-model="serverConfigUpd.host" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>port:</el-text>
|
||||
<el-form-item prop="port">
|
||||
<el-input v-model="serverConfigUpd.port" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-aline fxc" v-show="serverEdit">
|
||||
<el-button class="mlr-10" @click="toShow('server')">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" class="mlr-10" @click="submitForm">{{ t('submit') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, onMounted } from 'vue'
|
||||
import { ref, reactive, inject, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const utils = inject('utils')
|
||||
const files = inject('files')
|
||||
const verify = inject('verify')
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 视图模式
|
||||
const viewModel = reactive({
|
||||
base: 'show',
|
||||
server: 'show',
|
||||
})
|
||||
|
||||
function toShow(model) {
|
||||
console.log("toShow:" + model)
|
||||
viewModel[model] = 'show'
|
||||
}
|
||||
|
||||
function toEdit(model) {
|
||||
console.log("toEdit:" + model)
|
||||
viewModel[model] = 'edit'
|
||||
}
|
||||
|
||||
const baseShow = computed(() => {
|
||||
return viewModel.base == 'show'
|
||||
})
|
||||
|
||||
const baseEdit = computed(() => {
|
||||
return viewModel.base == 'edit'
|
||||
})
|
||||
|
||||
const baseNoData = computed(() => {
|
||||
return baseShow && serverConfig.model == null
|
||||
})
|
||||
|
||||
const serverShow = computed(() => {
|
||||
return viewModel.server == 'show'
|
||||
})
|
||||
|
||||
const serverEdit = computed(() => {
|
||||
return viewModel.server == 'edit'
|
||||
})
|
||||
|
||||
const readConfigSuccess = ref(false)
|
||||
|
||||
const serverNoData = computed(() => {
|
||||
return serverShow && !readConfigSuccess.value
|
||||
})
|
||||
|
||||
const serverConfig = reactive({
|
||||
host: null,
|
||||
port: null,
|
||||
})
|
||||
|
||||
const serverConfigUpd = reactive({
|
||||
host: null,
|
||||
port: null,
|
||||
})
|
||||
|
||||
function clearCache() {
|
||||
config.$reset()
|
||||
utils.pop(t('clearCacheSuccess'))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 读取配置文件config/config.toml
|
||||
files.readAll("@/../../config/config.toml").then((fileContent) => {
|
||||
console.log("config/config.toml: ", fileContent)
|
||||
if (utils.notBlank(fileContent)) {
|
||||
readConfigSuccess.value = true
|
||||
} else {
|
||||
utils.pop(t('readConfigFailed'))
|
||||
return
|
||||
}
|
||||
const lines = utils.stringToLines(fileContent)
|
||||
|
||||
// 读取[server]
|
||||
const serverStart = lines.findIndex((line) => {
|
||||
return line.includes("[server]")
|
||||
})
|
||||
for (let i = serverStart + 1; i < lines.length; i++) {
|
||||
console.log("line: ", lines[i])
|
||||
// 判定是否到了下个配置模块
|
||||
if (lines[i].startsWith("[")) {
|
||||
break
|
||||
}
|
||||
// 读取配置
|
||||
const line = lines[i]
|
||||
const lineArr = line.split("=")
|
||||
if (lineArr.length != 2) {
|
||||
continue
|
||||
}
|
||||
const key = lineArr[0].trim()
|
||||
const value = lineArr[1].trim()
|
||||
serverConfig[key] = value
|
||||
}
|
||||
console.log("serverConfig read from file: ", serverConfig)
|
||||
utils.copyProps(serverConfig, serverConfigUpd)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
await ruleFormRef.value.validate();
|
||||
if (!utils.hasDfProps(serverConfig, serverConfigUpd)) {
|
||||
ElMessage.success('未发生更改!');
|
||||
toShow('server')
|
||||
return
|
||||
}
|
||||
ElMessage.success('验证通过,提交表单');
|
||||
// update()
|
||||
} catch (error) {
|
||||
ElMessage.error('参数验证失败');
|
||||
}
|
||||
}
|
||||
|
||||
const rules = reactive({
|
||||
host: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
port: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
api_key: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
max_tokens: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
temperature: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -3,30 +3,222 @@
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="title fxsb">
|
||||
<div>基本信息</div>
|
||||
<div>LLM Config</div>
|
||||
<div>
|
||||
<el-link type="primary" class="no-select plr-6" @click="clearCache()">清理缓存</el-link>
|
||||
<el-link type="primary" class="no-select plr-6" @click="toEdit('base')" v-show="baseShow">
|
||||
{{ t('edit') }}
|
||||
</el-link>
|
||||
<el-link type="primary" class="no-select plr-6" @click="toShow('base')" v-show="baseEdit">
|
||||
{{ t('cancel') }}
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- No Data -->
|
||||
<div class="no-data" v-show="baseNoData">{{ t('noData') }}</div>
|
||||
|
||||
<!-- Show Data -->
|
||||
<div class="card-row-wrap" v-show="baseShow">
|
||||
<div class="card-row-item">
|
||||
<el-text>model:</el-text>
|
||||
<el-text>{{ llmConfig.model }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>base_url:</el-text>
|
||||
<el-text tag="p">{{ llmConfig.base_url }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>api_key:</el-text>
|
||||
<el-text>{{ llmConfig.api_key }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>max_tokens:</el-text>
|
||||
<el-text>{{ llmConfig.max_tokens }}</el-text>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>temperature:</el-text>
|
||||
<el-text>{{ llmConfig.temperature }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Module -->
|
||||
<el-form ref="ruleFormRef" :model="llmConfigUpd" status-icon :rules="rules" v-show="baseEdit">
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-item">
|
||||
<el-text>model:</el-text>
|
||||
<el-form-item prop="model">
|
||||
<el-input v-model="llmConfigUpd.model" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>base_url:</el-text>
|
||||
<el-form-item prop="base_url">
|
||||
<el-input v-model="llmConfigUpd.base_url" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>api_key:</el-text>
|
||||
<el-form-item prop="api_key">
|
||||
<el-input v-model="llmConfigUpd.api_key" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>max_tokens:</el-text>
|
||||
<el-form-item prop="max_tokens">
|
||||
<el-input v-model="llmConfigUpd.max_tokens" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>temperature:</el-text>
|
||||
<el-form-item prop="temperature">
|
||||
<el-input v-model="llmConfigUpd.temperature" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="card-row-aline fxc" v-show="baseEdit">
|
||||
<el-button class="mlr-10" @click="toShow('base')">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" class="mlr-10" @click="submitForm">{{ t('submit') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, onMounted } from 'vue'
|
||||
import { ref, reactive, inject, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {useConfig} from '@/store/config'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const utils = inject('utils')
|
||||
const files = inject('files')
|
||||
const verify = inject('verify')
|
||||
const router = useRouter()
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 视图模式
|
||||
const viewModel = reactive({
|
||||
base: 'show',
|
||||
})
|
||||
|
||||
function toShow(model) {
|
||||
console.log("toShow:" + model)
|
||||
viewModel[model] = 'show'
|
||||
}
|
||||
|
||||
function toEdit(model) {
|
||||
console.log("toEdit:" + model)
|
||||
viewModel[model] = 'edit'
|
||||
}
|
||||
|
||||
const baseShow = computed(() => {
|
||||
return viewModel.base == 'show'
|
||||
})
|
||||
|
||||
const baseEdit = computed(() => {
|
||||
return viewModel.base == 'edit'
|
||||
})
|
||||
|
||||
const readConfigSuccess = ref(false)
|
||||
|
||||
const baseNoData = computed(() => {
|
||||
return baseShow && !readConfigSuccess.value
|
||||
})
|
||||
|
||||
const llmConfig = reactive({
|
||||
model: null,
|
||||
base_url: null,
|
||||
api_key: null,
|
||||
max_tokens: null,
|
||||
temperature: null,
|
||||
})
|
||||
|
||||
const llmConfigUpd = reactive({
|
||||
model: null,
|
||||
base_url: null,
|
||||
api_key: null,
|
||||
max_tokens: null,
|
||||
temperature: null,
|
||||
})
|
||||
|
||||
function clearCache() {
|
||||
config.$reset()
|
||||
utils.pop(t('clearCacheSuccess'))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 读取配置文件config/config.toml
|
||||
files.readAll("@/../../config/config.toml").then((fileContent) => {
|
||||
console.log("config/config.toml: ", fileContent)
|
||||
if (utils.notBlank(fileContent)) {
|
||||
readConfigSuccess.value = true
|
||||
} else {
|
||||
utils.pop(t('readConfigFailed'))
|
||||
return
|
||||
}
|
||||
const lines = utils.stringToLines(fileContent)
|
||||
|
||||
// 读取[llm]
|
||||
const llmStart = lines.findIndex((line) => {
|
||||
return line.includes("[llm]")
|
||||
})
|
||||
for (let i = llmStart + 1; i < lines.length; i++) {
|
||||
console.log("line: ", lines[i])
|
||||
// 判定是否到了下个配置模块
|
||||
if (lines[i].startsWith("[")) {
|
||||
break
|
||||
}
|
||||
// 读取配置
|
||||
const line = lines[i]
|
||||
const lineArr = line.split("=")
|
||||
if (lineArr.length != 2) {
|
||||
continue
|
||||
}
|
||||
const key = lineArr[0].trim()
|
||||
const value = lineArr[1].trim()
|
||||
llmConfig[key] = value
|
||||
}
|
||||
console.log("llmConfig read from file: ", llmConfig)
|
||||
utils.copyProps(llmConfig, llmConfigUpd)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
await ruleFormRef.value.validate();
|
||||
if (!utils.hasDfProps(llmConfig, llmConfigUpd)) {
|
||||
ElMessage.success('未发生更改!');
|
||||
toShow('base')
|
||||
return
|
||||
}
|
||||
ElMessage.success('验证通过,提交表单');
|
||||
// update()
|
||||
} catch (error) {
|
||||
ElMessage.error('参数验证失败');
|
||||
}
|
||||
}
|
||||
|
||||
const rules = reactive({
|
||||
model: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
base_url: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
api_key: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
max_tokens: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
temperature: [{ validator: verify.validator('notBlank'), trigger: 'blur' }],
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -1,408 +0,0 @@
|
||||
<template :lang="i18n.locale">
|
||||
<div class="main-content fc">
|
||||
<el-scrollbar ref="scrollRef">
|
||||
<div class="output-area" v-show="taskInfo.taskId != null">
|
||||
|
||||
<div class="dialog-user">
|
||||
<div class="blank"></div>
|
||||
<div class="content">
|
||||
<el-text class="title">
|
||||
{{ t('user') }}
|
||||
</el-text>
|
||||
<el-text class="prompt">
|
||||
{{ taskInfo.prompt }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-ai">
|
||||
<el-text class="title"> OpenManus-AI </el-text>
|
||||
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-aline">
|
||||
<el-timeline class="wp-100">
|
||||
<el-timeline-item v-for="(step, index) in taskInfo.stepList" :key="index" :timestamp="step.createdDt"
|
||||
placement="top">
|
||||
<el-card>
|
||||
<div>
|
||||
<h4 class="color-label mr-10" :class="utils.colorByLabel('step')">
|
||||
{{ t('step') }}
|
||||
</h4>
|
||||
<el-text>{{ step.result }}</el-text>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div v-for="(subStep, subIndex) in step.subList">
|
||||
<div class="fxsb mtb-10">
|
||||
<el-text> {{ subStep.type }} </el-text>
|
||||
<el-text class="sub-step-time"> {{ subStep.createdDt }} </el-text>
|
||||
</div>
|
||||
<div>
|
||||
<el-text> {{ subStep.result }} </el-text>
|
||||
</div>
|
||||
<el-divider v-if="subIndex != step.subList.length - 1" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
|
||||
<el-text>{{ taskInfo.status }}</el-text>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<div class="input-area">
|
||||
<div class="input-box">
|
||||
<el-icon @click="uploadFile" class="add-file-area" :size="24">
|
||||
<FolderAdd />
|
||||
</el-icon>
|
||||
<el-input ref="promptEle" type="textarea" v-model="prompt" class="input-style" style="border: none;"
|
||||
:autosize="{ minRows: 1, maxRows: 4 }" autofocus :placeholder="t('promptInputPlaceHolder')"
|
||||
@keydown.enter="handleInputEnter" />
|
||||
|
||||
<el-link class="send-area">
|
||||
<el-icon @click="sendPrompt" :size="24" v-show="!loading">
|
||||
<Promotion />
|
||||
</el-icon>
|
||||
<el-icon @click="stop" :size="24" v-show="loading">
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-text class="tips">{{ t('openManusAgiTips') }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { FolderAdd, Promotion, Eleme, CircleClose } from '@element-plus/icons-vue'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import i18n from '@/locales/i18n'
|
||||
|
||||
const utils = inject('utils')
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
const prompt = ref('')
|
||||
const promptEle = ref(null)
|
||||
|
||||
const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message']
|
||||
const eventSource = ref(null)
|
||||
|
||||
const taskInfo = computed(() => {
|
||||
return config.getCurrTask()
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const scrollRef = ref(null)
|
||||
|
||||
// 建立EventSource连接
|
||||
const buildEventSource = (taskId) => {
|
||||
loading.value = true
|
||||
eventSource.value = new EventSource('http://localhost:5172/tasks/' + taskId + '/events')
|
||||
eventSource.value.onmessage = (event) => {
|
||||
console.log('Received data:', event.data)
|
||||
// 在这里处理接收到的数据 不起作用
|
||||
}
|
||||
|
||||
eventTypes.forEach(type => {
|
||||
eventSource.value.addEventListener(type, (event) => handleEvent(event, type))
|
||||
})
|
||||
|
||||
eventSource.value.onerror = (error) => {
|
||||
console.error('EventSource failed:', error)
|
||||
// 处理错误情况
|
||||
loading.value = false
|
||||
eventSource.value.close()
|
||||
taskInfo.value.status = "taskStatus.failed"
|
||||
utils.pop("任务执行失败", "error")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const handleEvent = (event, type) => {
|
||||
console.log('Received event, type:', type, event.data)
|
||||
// clearInterval(heartbeatTimer);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("type:", type, "data:", data)
|
||||
if (eventSource.value.readyState === EventSource.CLOSED) {
|
||||
console.log('Connection is closed');
|
||||
}
|
||||
if (type == "complete" || data.status == "completed") {
|
||||
console.log('task completed');
|
||||
loading.value = false
|
||||
eventSource.value.close()
|
||||
taskInfo.value.status = "taskStatus.success"
|
||||
utils.pop("任务已完成", "success")
|
||||
return
|
||||
}
|
||||
// autoScroll(stepContainer);
|
||||
buildOutput(taskInfo.value.taskId)
|
||||
} catch (e) {
|
||||
console.error(`Error handling ${type} event:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildOutput(taskId) {
|
||||
// 同步执行,确保数据顺序
|
||||
await utils.awaitGet('http://localhost:5172/tasks/' + taskId).then(data => {
|
||||
console.log("task info resp:", data)
|
||||
buildStepList(data.steps)
|
||||
console.log("stepList:", taskInfo.value.stepList)
|
||||
// 滚动到底部
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
// 封装stepList
|
||||
const buildStepList = (steps) => {
|
||||
// stepList
|
||||
steps.forEach((step, idx) => {
|
||||
// 步骤
|
||||
if (step.type == "log" && step.result.startsWith("Executing step")) {
|
||||
const stepStr = step.result.replace("Executing step ", "").replace("\n", "")
|
||||
const stepNo = stepStr.split("/")[0]
|
||||
if (taskInfo.value.stepList.length < stepNo) {
|
||||
// 添加此step到stepList
|
||||
const parentStep = {
|
||||
type: "log",
|
||||
idx: idx,
|
||||
stepNo: stepNo,
|
||||
result: stepStr,
|
||||
subList: [],
|
||||
createdDt: utils.dateFormat(new Date())
|
||||
}
|
||||
taskInfo.value.stepList.push(parentStep)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 子步骤
|
||||
const subStep = {
|
||||
type: step.type,
|
||||
idx: idx,
|
||||
result: step.result,
|
||||
createdDt: utils.dateFormat(new Date())
|
||||
}
|
||||
// 判定添加到stepList中的哪个元素元素的subList中
|
||||
console.log("stepList:", taskInfo.value.stepList, "idx:", idx)
|
||||
let parentStep = null
|
||||
const pStepIndex = taskInfo.value.stepList.findIndex(parentStep => parentStep.idx > idx)
|
||||
console.log("pStepIndex:", pStepIndex)
|
||||
if (pStepIndex != -1) {
|
||||
// 取pStep的上一个元素
|
||||
parentStep = taskInfo.value.stepList[pStepIndex - 1]
|
||||
} else {
|
||||
// 不存在时, 添加到stepList最后一个元素末尾
|
||||
parentStep = taskInfo.value.stepList[taskInfo.value.stepList.length - 1]
|
||||
}
|
||||
console.log("parentStep:", parentStep)
|
||||
const existSubStep = parentStep.subList.find(existSubStep => existSubStep.idx == idx)
|
||||
if (!existSubStep) {
|
||||
// 不存在时, 添加到末尾
|
||||
parentStep.subList.push(subStep)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时关闭EventSource连接
|
||||
if (eventSource.value) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
})
|
||||
|
||||
function handleInputEnter(event) {
|
||||
console.log("handleInputEnter:", event)
|
||||
event.preventDefault()
|
||||
sendPrompt()
|
||||
}
|
||||
|
||||
function uploadFile() {
|
||||
utils.pop("暂不支持,开发中", "warning")
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (scrollRef.value) {
|
||||
console.log("scrollRef:", scrollRef.value, scrollRef.value.wrapRef)
|
||||
const container = scrollRef.value.wrapRef
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送提示词
|
||||
function sendPrompt() {
|
||||
// 关闭之前的连接
|
||||
if (eventSource.value != null) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
|
||||
if (utils.isBlank(prompt.value)) {
|
||||
utils.pop("Please enter a valid prompt", "error")
|
||||
promptEle.value.focus()
|
||||
return
|
||||
}
|
||||
|
||||
utils.post('http://localhost:5172/tasks', { prompt: prompt.value }).then(data => {
|
||||
if (!data.task_id) {
|
||||
throw new Error('Invalid task ID')
|
||||
}
|
||||
const newTask = {
|
||||
taskId: data.task_id,
|
||||
prompt: prompt.value,
|
||||
status: "running",
|
||||
createdDt: utils.dateFormat(new Date()),
|
||||
stepList: []
|
||||
}
|
||||
// 保存历史记录
|
||||
config.addTaskHistory(newTask)
|
||||
// 发送完成后清空输入框
|
||||
prompt.value = ''
|
||||
// 建立新的EventSource连接
|
||||
buildEventSource(data.task_id)
|
||||
|
||||
console.log("new task created:", newTask)
|
||||
}).catch(error => {
|
||||
console.error('Failed to create task:', error)
|
||||
})
|
||||
}
|
||||
|
||||
function stop() {
|
||||
console.log("stop")
|
||||
loading.value = false
|
||||
eventSource.value.close()
|
||||
taskInfo.value.status = "taskStatus.terminated"
|
||||
utils.pop("用户终止任务", "error")
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.output-area {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dialog-user {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dialog-user .blank {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dialog-user .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: end;
|
||||
border-radius: 12px;
|
||||
background-color: var(--el-fg-color);
|
||||
}
|
||||
|
||||
.dialog-user .title {
|
||||
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
|
||||
align-self: flex-end;
|
||||
margin: 6px 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.dialog-user .prompt {
|
||||
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
|
||||
align-self: flex-end;
|
||||
margin: 0px 16px 6px 16px;
|
||||
}
|
||||
|
||||
|
||||
.dialog {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.dialog-ai {
|
||||
margin-bottom: 16px;
|
||||
background-color: var(--el-fg-color);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dialog-ai .title {
|
||||
margin: 6px 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
flex-grow: 0;
|
||||
width: 100%;
|
||||
max-height: 180px;
|
||||
padding-left: 80px;
|
||||
padding-right: 80px;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
background-color: var(--el-fg-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-style {
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-style :deep(.el-textarea__inner) {
|
||||
outline: none;
|
||||
border: none;
|
||||
resize: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.add-file-area {
|
||||
margin-left: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.send-area {
|
||||
margin-left: 8px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.tips {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.sub-step-time {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
237
desktop/frontend/src/views/task/HistoryIndex.vue
Normal file
237
desktop/frontend/src/views/task/HistoryIndex.vue
Normal file
@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="main-content">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="adv-search" :class="advSearch ? 'expand' : ''">
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-item">
|
||||
<el-text tag="label">taskId:</el-text>
|
||||
<el-input v-model="searchForm.taskId" placeholder="taskId" maxlength="50" show-word-limit />
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>{{ t('promptInputKw') }}:</el-text>
|
||||
<el-input v-model="searchForm.promptInput" :placeholder="t('promptInputKw')" />
|
||||
</div>
|
||||
|
||||
<div class="card-row-item">
|
||||
<el-text>{{ t('taskStatus.name') }}:</el-text>
|
||||
<el-select clearable v-model="searchForm.taskStatus">
|
||||
<el-option v-for="opt in taskStatusOpts" :key="opt.key" :value="opt.value" :label="t(opt.label)" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TableTools :advSearch="advSearch" :searchForm="searchForm" :tableColumns="tableColumns"
|
||||
:selectedRows="selectedRows" @baseSearch="baseSearch" @search="search" @advSearchSwitch="advSearchSwitch"
|
||||
@delSelected="delSelected" @resetSearch="resetSearch" @checkTableColumn="checkTableColumn" />
|
||||
</template>
|
||||
|
||||
<el-table ref="tableRef" @selection-change="handleSelectionChange" :data="pageInfo.list" stripe border
|
||||
style="width: 100%" highlight-current-row max-height="760" :cell-style="{ textAlign: 'center' }"
|
||||
:header-cell-style="{ textAlign: 'center' }">
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column prop="taskId" label="TaskId" width="300">
|
||||
<template #default="scope">
|
||||
<el-link @click="toTaskInfo(scope.row.taskId)" type="primary" class="h-20">
|
||||
{{ scope.row.taskId }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="col in showTableColumns" :prop=col.prop :label="col.label" :width="col.width"
|
||||
:minWidth="col.minWidth" :showOverflowTooltip="col.showOverflowTooltip" />
|
||||
</el-table>
|
||||
|
||||
<el-pagination v-model:current-page="pageInfo.pageNum" v-model:page-size="pageInfo.pageSize"
|
||||
:total="pageInfo.total" layout="total, prev, pager, next" />
|
||||
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, computed, onMounted, onBeforeUnmount, onUnmounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const router = useRouter()
|
||||
const utils = inject('utils')
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
const tableRef = ref()
|
||||
const selectedRows = ref([])
|
||||
|
||||
// 高级搜索
|
||||
const advSearch = ref(false)
|
||||
|
||||
function advSearchSwitch() {
|
||||
advSearch.value = !advSearch.value
|
||||
}
|
||||
|
||||
const pageInfo = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
pages: 1,
|
||||
list: []
|
||||
})
|
||||
|
||||
watch(() => pageInfo.pageNum, () => {
|
||||
// 触发列表查询
|
||||
search()
|
||||
})
|
||||
|
||||
watch(() => pageInfo.pageSize, () => {
|
||||
// 触发列表查询
|
||||
search()
|
||||
})
|
||||
|
||||
|
||||
const searchForm = reactive({
|
||||
kw: null,
|
||||
taskId: null,
|
||||
promptInput: null,
|
||||
taskStatus: null
|
||||
})
|
||||
|
||||
const taskStatusOpts = reactive([
|
||||
{ key: "success", value: "success", label: "taskStatus.success" },
|
||||
{ key: "failed", value: "failed", label: "taskStatus.failed" },
|
||||
{ key: "running", value: "running", label: "taskStatus.running" },
|
||||
{ key: "terminated", value: "terminated", label: "taskStatus.terminated" }
|
||||
])
|
||||
|
||||
const tableColumns = ref([
|
||||
{ prop: "prompt", label: t('promptInput'), isShow: true, showOverflowTooltip: true, minWidth: 300, sortable: true },
|
||||
{ prop: "statusDesc", label: t('taskStatus.name'), isShow: true, width: 160 },
|
||||
{ prop: "createdDt", label: t('createdDt'), isShow: true, width: 160 }
|
||||
])
|
||||
|
||||
const showTableColumns = computed(() => {
|
||||
return tableColumns.value.filter(item => item.isShow)
|
||||
})
|
||||
|
||||
// 任务历史
|
||||
const taskHistory = computed(() => {
|
||||
return config.taskHistory
|
||||
})
|
||||
|
||||
// 基本搜索
|
||||
const baseSearch = utils.debounce(() => {
|
||||
const kw = searchForm.kw
|
||||
utils.clearProps(searchForm)
|
||||
searchForm.kw = kw
|
||||
search()
|
||||
}, 500)
|
||||
|
||||
// 搜索
|
||||
// 修改search方法
|
||||
function search() {
|
||||
searchForm.pageNum = pageInfo.pageNum
|
||||
searchForm.pageSize = pageInfo.pageSize
|
||||
console.log("search searchForm:", searchForm, pageInfo)
|
||||
|
||||
const filteredTaskList = taskHistory.value.filter(taskInfo => {
|
||||
if (utils.notBlank(searchForm.kw)) {
|
||||
if (!taskInfo.prompt.includes(searchForm.kw) && !taskInfo.taskId.includes(searchForm.kw)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (utils.notBlank(searchForm.taskId) && taskInfo.taskId != searchForm.taskId) {
|
||||
return false
|
||||
}
|
||||
if (utils.notBlank(searchForm.promptInput) && !taskInfo.prompt.includes(searchForm.promptInput)) {
|
||||
return false
|
||||
}
|
||||
if (utils.notBlank(searchForm.taskStatus) && taskInfo.status != searchForm.taskStatus) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// 计算总条数
|
||||
pageInfo.total = filteredTaskList.length
|
||||
|
||||
// 分页处理
|
||||
const startIndex = (pageInfo.pageNum - 1) * pageInfo.pageSize
|
||||
const endIndex = startIndex + pageInfo.pageSize
|
||||
pageInfo.list = filteredTaskList.slice(startIndex, endIndex)
|
||||
|
||||
// 任务状态处理
|
||||
pageInfo.list.forEach(item => {
|
||||
item.statusDesc = t('taskStatus.' + item.status)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const handleSelectionChange = (val) => {
|
||||
selectedRows.value = val
|
||||
}
|
||||
|
||||
// 删除选中的数据
|
||||
function delSelected() {
|
||||
if (selectedRows.value.length == 0) {
|
||||
utils.pop("请选择要删除的数据!")
|
||||
return
|
||||
}
|
||||
selectedRows.value.forEach(item => {
|
||||
for (let i = 0; i < taskHistory.value.length; i++) {
|
||||
if (taskHistory.value[i].taskId == item.taskId) {
|
||||
taskHistory.value.splice(i, 1)
|
||||
i--
|
||||
}
|
||||
}
|
||||
})
|
||||
baseSearch()
|
||||
}
|
||||
|
||||
// 定义变量存储事件监听的引用(不能是常量)
|
||||
let listener = null
|
||||
|
||||
// 在组件挂载时添加事件监听
|
||||
onMounted(() => {
|
||||
listener = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
search()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keyup', listener)
|
||||
console.log("onMounted pageInfo:", pageInfo)
|
||||
search()
|
||||
})
|
||||
|
||||
// 在组件卸载前移除事件监听
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keyup', listener)
|
||||
})
|
||||
|
||||
function checkTableColumn(isCheck, prop) {
|
||||
console.log("checkTableColumn:", isCheck, prop)
|
||||
tableColumns.value.forEach(item => {
|
||||
if (item.prop == prop) {
|
||||
item.isShow = isCheck
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
utils.clearProps(searchForm)
|
||||
searchForm.openStatus = "OPEN"
|
||||
}
|
||||
|
||||
function toTaskInfo(taskId) {
|
||||
console.log("toTaskInfo:", taskId)
|
||||
router.push("/task/"+taskId)
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,4 +1,4 @@
|
||||
<template :lang="i18n.locale">
|
||||
<template>
|
||||
<div class="main-content fc">
|
||||
<el-scrollbar ref="scrollRef" style="width: 100%;">
|
||||
<div class="output-area" v-show="taskInfo.taskId != null">
|
@ -1,4 +1,4 @@
|
||||
<template :lang="i18n.locale">
|
||||
<template>
|
||||
<div class="main-content fc">
|
||||
<el-scrollbar ref="scrollRef">
|
||||
<div class="output-area" v-show="taskInfo.taskId != null">
|
||||
@ -6,9 +6,12 @@
|
||||
<div class="dialog-user">
|
||||
<div class="blank"></div>
|
||||
<div class="content">
|
||||
<el-text class="title">
|
||||
<div class="title fxc">
|
||||
<img src="@/assets/img/user.png" class="user-img" />
|
||||
<el-text>
|
||||
{{ t('user') }}
|
||||
</el-text>
|
||||
</div>
|
||||
<el-text class="prompt">
|
||||
{{ taskInfo.prompt }}
|
||||
</el-text>
|
||||
@ -47,15 +50,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
|
||||
<el-text>{{ t(taskInfo.status) }}</el-text>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<div class="input-area">
|
||||
|
||||
<div class="input-tools">
|
||||
<div class="new-task" v-show="!newTaskFlag">
|
||||
<el-button round @click="startNewTask">
|
||||
<el-icon :size="16">
|
||||
<CirclePlus />
|
||||
</el-icon>
|
||||
<span> {{ t('newTask') }} </span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="task-status" v-show="taskInfo.taskId != null">
|
||||
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
|
||||
<el-text>{{ taskInfo.status }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-box">
|
||||
<el-icon @click="uploadFile" class="add-file-area" :size="24">
|
||||
<FolderAdd />
|
||||
@ -65,10 +80,10 @@
|
||||
@keydown.enter="handleInputEnter" />
|
||||
|
||||
<el-link class="send-area">
|
||||
<el-icon @click="sendPrompt" :size="24" v-show="!loading">
|
||||
<el-icon @click="sendPrompt" :size="24" v-show="!loading && taskInfo.status != 'running'">
|
||||
<Promotion />
|
||||
</el-icon>
|
||||
<el-icon @click="stop" :size="24" v-show="loading">
|
||||
<el-icon @click="stop" :size="24" v-show="loading || taskInfo.status == 'running'">
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
</el-link>
|
||||
@ -84,10 +99,9 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { FolderAdd, Promotion, Eleme, CircleClose } from '@element-plus/icons-vue'
|
||||
import { FolderAdd, Promotion, CirclePlus, CircleClose } from '@element-plus/icons-vue'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import i18n from '@/locales/i18n'
|
||||
|
||||
const utils = inject('utils')
|
||||
const config = useConfig()
|
||||
@ -99,7 +113,12 @@ const promptEle = ref(null)
|
||||
const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message']
|
||||
const eventSource = ref(null)
|
||||
|
||||
const newTaskFlag = ref(false)
|
||||
|
||||
const taskInfo = computed(() => {
|
||||
if (newTaskFlag.value) {
|
||||
return {}
|
||||
}
|
||||
return config.getCurrTask()
|
||||
})
|
||||
|
||||
@ -249,17 +268,22 @@ const scrollToBottom = () => {
|
||||
|
||||
// 发送提示词
|
||||
function sendPrompt() {
|
||||
// 关闭之前的连接
|
||||
if (eventSource.value != null) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
|
||||
if (utils.isBlank(prompt.value)) {
|
||||
utils.pop("Please enter a valid prompt", "error")
|
||||
promptEle.value.focus()
|
||||
return
|
||||
}
|
||||
|
||||
if (taskInfo.value.status == "running") {
|
||||
utils.pop("请先终止当前任务", "error")
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭之前的连接
|
||||
if (eventSource.value != null) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
|
||||
utils.post('http://localhost:5172/tasks', { prompt: prompt.value }).then(data => {
|
||||
if (!data.task_id) {
|
||||
throw new Error('Invalid task ID')
|
||||
@ -273,6 +297,7 @@ function sendPrompt() {
|
||||
}
|
||||
// 保存历史记录
|
||||
config.addTaskHistory(newTask)
|
||||
newTaskFlag.value = false
|
||||
// 发送完成后清空输入框
|
||||
prompt.value = ''
|
||||
// 建立新的EventSource连接
|
||||
@ -287,11 +312,26 @@ function sendPrompt() {
|
||||
function stop() {
|
||||
console.log("stop")
|
||||
loading.value = false
|
||||
console.log("eventSource:", eventSource.value, "taskInfo:", taskInfo.value)
|
||||
if (eventSource.value != null) {
|
||||
eventSource.value.close()
|
||||
}
|
||||
|
||||
taskInfo.value.status = "terminated"
|
||||
utils.pop("用户终止任务", "error")
|
||||
}
|
||||
|
||||
function startNewTask() {
|
||||
console.log("startNewTask:", taskInfo.value)
|
||||
if (taskInfo.value.status == "running") {
|
||||
utils.pop("请先终止当前任务", "error")
|
||||
return
|
||||
}
|
||||
newTaskFlag.value = true
|
||||
prompt.value = ''
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -332,14 +372,18 @@ function stop() {
|
||||
margin: 0px 16px 6px 16px;
|
||||
}
|
||||
|
||||
.dialog-user .user-img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.dialog-ai {
|
||||
margin-bottom: 16px;
|
||||
background-color: var(--el-fg-color);
|
||||
border-radius: 12px;
|
||||
}
|
||||
@ -352,17 +396,34 @@ function stop() {
|
||||
.input-area {
|
||||
flex-grow: 0;
|
||||
width: 100%;
|
||||
max-height: 180px;
|
||||
max-height: 200px;
|
||||
padding-left: 80px;
|
||||
padding-right: 80px;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-area .input-tools {
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-area .input-tools .new-task {
|
||||
position: relative;
|
||||
left: -80px;
|
||||
}
|
||||
|
||||
.input-area .input-tools .task-status {
|
||||
position: relative;
|
||||
right: -80px;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
@ -398,7 +459,7 @@ function stop() {
|
||||
.tips {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
padding-top: 10px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.sub-step-time {
|
167
desktop/frontend/src/views/task/TaskInfo.vue
Normal file
167
desktop/frontend/src/views/task/TaskInfo.vue
Normal file
@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="main-content fc">
|
||||
<!-- 展示模块-暂无数据 -->
|
||||
<div class="no-data" v-show="baseNoData">{{ t('noData') }}</div>
|
||||
|
||||
<!-- 展示模块 -->
|
||||
<div class="output-area" v-show="baseShow">
|
||||
|
||||
<div class="dialog-user">
|
||||
<div class="blank"></div>
|
||||
<div class="content">
|
||||
<div class="title fxc">
|
||||
<img src="@/assets/img/user.png" class="user-img" />
|
||||
<el-text>
|
||||
{{ t('user') }}
|
||||
</el-text>
|
||||
</div>
|
||||
<el-text class="prompt">
|
||||
{{ taskInfo.prompt }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-ai">
|
||||
<el-text class="title"> OpenManus-AI </el-text>
|
||||
|
||||
<div class="card-row-wrap">
|
||||
<div class="card-row-aline">
|
||||
<el-timeline class="wp-100">
|
||||
<el-timeline-item v-for="(step, index) in taskInfo.stepList" :key="index" :timestamp="step.createdDt"
|
||||
placement="top">
|
||||
<el-card>
|
||||
<div>
|
||||
<h4 class="color-label mr-10" :class="utils.colorByLabel('step')">
|
||||
{{ t('step') }}
|
||||
</h4>
|
||||
<el-text>{{ step.result }}</el-text>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div v-for="(subStep, subIndex) in step.subList">
|
||||
<div class="fxsb mtb-10">
|
||||
<el-text> {{ subStep.type }} </el-text>
|
||||
<el-text class="sub-step-time"> {{ subStep.createdDt }} </el-text>
|
||||
</div>
|
||||
<div>
|
||||
<el-text> {{ subStep.result }} </el-text>
|
||||
</div>
|
||||
<el-divider v-if="subIndex != step.subList.length - 1" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-status" v-show="taskInfo != null">
|
||||
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
|
||||
<el-text>{{ taskInfo.status }}</el-text>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { User } from '@element-plus/icons-vue'
|
||||
import { useConfig } from '@/store/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const utils = inject('utils')
|
||||
const config = useConfig()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 视图模式
|
||||
const viewModel = reactive({
|
||||
base: 'show'
|
||||
})
|
||||
|
||||
const baseShow = computed(() => {
|
||||
return viewModel.base == 'show'
|
||||
})
|
||||
|
||||
const baseNoData = computed(() => {
|
||||
return baseShow && taskInfo.value == null
|
||||
})
|
||||
|
||||
const taskInfo = computed(() => {
|
||||
return config.getCurrTask()
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.output-area {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dialog-user {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dialog-user .blank {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dialog-user .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: end;
|
||||
border-radius: 12px;
|
||||
background-color: var(--el-fg-color);
|
||||
}
|
||||
|
||||
.dialog-user .title {
|
||||
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
|
||||
align-self: flex-end;
|
||||
margin: 6px 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.dialog-user .prompt {
|
||||
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
|
||||
align-self: flex-end;
|
||||
margin: 0px 16px 6px 16px;
|
||||
}
|
||||
|
||||
.dialog-user .user-img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-ai {
|
||||
background-color: var(--el-fg-color);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dialog-ai .title {
|
||||
margin: 6px 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.task-status {
|
||||
align-self: self-start;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.sub-step-time {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
@ -1,12 +1,12 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
@ -37,16 +37,16 @@ export default defineConfig({
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
// 分解块,将大块分解成更小的块
|
||||
// Fine-tune bundling strategy
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
// 让每个插件都打包成独立的文件
|
||||
return id.toString().split('node_modules/')[1].split('/')[0].toString();
|
||||
// Extract package name from module path to create separate chunks
|
||||
return id.toString().split('node_modules/')[1].split('/')[0].toString()
|
||||
}
|
||||
},
|
||||
// 单位b, 合并较小模块
|
||||
// Attempt to merge chunks smaller than 10KB (in bytes)
|
||||
experimentalMinChunkSize: 10 * 1024,
|
||||
}
|
||||
},
|
||||
|
2
desktop/frontend/wailsjs/go/main/App.d.ts
vendored
2
desktop/frontend/wailsjs/go/main/App.d.ts
vendored
@ -2,3 +2,5 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
|
||||
export function ReadAll(arg1:string):Promise<string>;
|
||||
|
@ -5,3 +5,7 @@
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function ReadAll(arg1) {
|
||||
return window['go']['main']['App']['ReadAll'](arg1);
|
||||
}
|
||||
|
29
desktop/src/utils/file.go
Normal file
29
desktop/src/utils/file.go
Normal file
@ -0,0 +1,29 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 打开文件
|
||||
func ReadAll(filePath string) []byte {
|
||||
if IsBlank(filePath) {
|
||||
fmt.Println("File path is nil")
|
||||
return nil
|
||||
}
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("Error opening file:", err)
|
||||
return nil
|
||||
}
|
||||
// 确保文件最后被关闭
|
||||
defer file.Close()
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
fmt.Println("Read file error:", err)
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
@ -1,10 +1,11 @@
|
||||
package main
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AnyToStr 任意类型数据转string
|
||||
@ -45,3 +46,19 @@ func AnyToStr(i interface{}) (string, error) {
|
||||
return "", fmt.Errorf("unable to cast %#v of type %T to string", i, i)
|
||||
}
|
||||
}
|
||||
|
||||
func IsEmpty(s string) bool {
|
||||
return len(s) == 0
|
||||
}
|
||||
|
||||
func IsNotEmpty(s string) bool {
|
||||
return len(s) > 0
|
||||
}
|
||||
|
||||
func IsBlank(s string) bool {
|
||||
return len(s) == 0 || strings.TrimSpace(s) == ""
|
||||
}
|
||||
|
||||
func IsNotBlank(s string) bool {
|
||||
return len(s) > 0 && strings.TrimSpace(s) != ""
|
||||
}
|
||||
|
@ -31,4 +31,3 @@ wails build
|
||||
npm install axios
|
||||
npm install qs
|
||||
npm i --save-dev @types/qs
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user