Merge pull request #821 from aylvn/desktop

Desktop has implemented the large model configuration page and settings page functionality
This commit is contained in:
Isaac 2025-03-19 15:44:17 +08:00 committed by GitHub
commit 7e18c97a2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1588 additions and 960 deletions

1
.gitignore vendored
View File

@ -181,4 +181,3 @@ workspace/
# Private Config # Private Config
config/config.toml config/config.toml

View File

@ -69,7 +69,3 @@ To build the application:
wails build wails build
The built application will be located in the projects dist directory. The built application will be located in the projects dist directory.

View File

@ -69,7 +69,3 @@ nodejs官网安装: https://nodejs.org/en
wails build wails build
构建好的应用在项目dist目录下 构建好的应用在项目dist目录下

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"OpenManus/src/utils"
"context" "context"
"fmt" "fmt"
) )
@ -10,6 +11,12 @@ type App struct {
ctx context.Context 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 // NewApp creates a new App application struct
func NewApp() *App { func NewApp() *App {
return &App{} return &App{}
@ -25,3 +32,11 @@ func (a *App) startup(ctx context.Context) {
func (a *App) Greet(name string) string { func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, It's show time!", name) 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
}

View File

@ -10,4 +10,3 @@
<script src="./src/main.js" type="module"></script> <script src="./src/main.js" type="module"></script>
</body> </body>
</html> </html>

View File

@ -1,5 +1,5 @@
<template> <template>
<!-- 全局配置 --> <!-- Global Configuration -->
<el-config-provider :size="size" :z-index="zIndex" :locale="locale" :button="config" :message="config" <el-config-provider :size="size" :z-index="zIndex" :locale="locale" :button="config" :message="config"
:value-on-clear="null" :empty-values="[undefined, null]"> :value-on-clear="null" :empty-values="[undefined, null]">
<RouterView /> <RouterView />
@ -10,7 +10,7 @@
import { ref, reactive, onMounted, watch } from 'vue' import { ref, reactive, onMounted, watch } from 'vue'
import en from 'element-plus/es/locale/lang/en' import en from 'element-plus/es/locale/lang/en'
import zhCn from 'element-plus/es/locale/lang/zh-cn' import zhCn from 'element-plus/es/locale/lang/zh-cn'
/** 暗黑主题 */ /** Dark Theme */
import { useDark, useStorage } from '@vueuse/core' import { useDark, useStorage } from '@vueuse/core'
const size = 'default' const size = 'default'
@ -22,11 +22,11 @@ const localeStr = localConfig.selectedLang ? localConfig.selectedLang.code : 'en
const locale = localeStr == 'en' ? en : zhCn const locale = localeStr == 'en' ? en : zhCn
const isDark = useDark() const isDark = useDark()
// // Store user preferences
const userPrefersDark = ref(null) const userPrefersDark = ref(null)
onMounted(() => { onMounted(() => {
// 使 useStorage isDark // Use useStorage hook to sync isDark and local storage
useStorage( useStorage(
'user-prefers-dark', 'user-prefers-dark',
userPrefersDark, userPrefersDark,
@ -35,17 +35,17 @@ onMounted(() => {
) )
}) })
// isDark // Watch isDark changes and update local storage
watch(isDark, (newValue) => { watch(isDark, (newValue) => {
userPrefersDark.value = newValue ? 'dark' : 'light' userPrefersDark.value = newValue ? 'dark' : 'light'
}) })
/* 全局配置 */ /* Global Configuration */
const config = reactive({ const config = reactive({
// - // Button - Automatically insert space between Chinese characters
autoInsertSpace: true, autoInsertSpace: true,
// - // Message - Maximum number of messages that can be displayed simultaneously
max: 3, max: 3,
}) })
</script> </script>

View File

@ -1,6 +1,6 @@
:root { :root {
--el-menu-base-level-padding: 10px !important; --el-menu-base-level-padding: 10px !important;
/** 子菜单缩进 */ /** Indents for submenu items */
--el-menu-level-padding: 20px !important; --el-menu-level-padding: 20px !important;
} }
@ -13,7 +13,7 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: inherit; font-family: inherit;
/* 让broder不占用宽度 */ /* Ensure borders don't take up space */
box-sizing: border-box; box-sizing: border-box;
} }
@ -34,7 +34,7 @@ body {
padding: 0; padding: 0;
line-height: 1.5; line-height: 1.5;
touch-action: none; touch-action: none;
/* 不使用原生滚动条 */ /* Don't use native scroll bars */
overflow: hidden; overflow: hidden;
} }
@ -69,7 +69,7 @@ a {
text-decoration: none; text-decoration: none;
} }
/* 防止双击选中 */ /* Avoid double-click selection */
a.no-select { a.no-select {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
@ -114,12 +114,12 @@ fieldset {
text-align: right; text-align: right;
} }
/** 文本缩进默认值,2个字符 */ /** Default indent for text, 2 characters */
.text-indent { .text-indent {
text-indent: 2em; text-indent: 2em;
} }
/** \n 换行 */ /** Multiline text preset */
.multiline-text { .multiline-text {
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
@ -130,7 +130,7 @@ input:-webkit-autofill,
textarea:-webkit-autofill, textarea:-webkit-autofill,
select:-webkit-autofill { select:-webkit-autofill {
-webkit-text-fill-color: var(--el-text-color); -webkit-text-fill-color: var(--el-text-color);
/* transparent 背景透明 */ /* transparent background */
-webkit-box-shadow: 0 0 0px 1000px transparent inset; -webkit-box-shadow: 0 0 0px 1000px transparent inset;
transition: background-color 5000s ease-in-out 0s; transition: background-color 5000s ease-in-out 0s;
} }
@ -151,32 +151,32 @@ input::selection,
input:-webkit-autofill::selection, input:-webkit-autofill::selection,
input:-webkit-autofill:hover::selection, input:-webkit-autofill:hover::selection,
input:-webkit-autofill:focus::selection { input:-webkit-autofill:focus::selection {
/* 设置自动填充时选中文本的样式 */ /* Style for autocompleted text */
/* 文字颜色 */ /* Color for text */
-webkit-text-fill-color: rgb(255, 255, 255); -webkit-text-fill-color: rgb(255, 255, 255);
/* 背景颜色 */ /* Color for background */
background-color: rgb(0, 0, 255); background-color: rgb(0, 0, 255);
-webkit-box-shadow: 0 0 0px 1000px rgb(255, 255, 255) inset; -webkit-box-shadow: 0 0 0px 1000px rgb(255, 255, 255) inset;
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; transition: background-color 5000s ease-in-out 0s;
} }
img { img {
/* 添加渐变效果 */ /* Add transition */
transition: opacity 0.4s ease; transition: opacity 0.4s ease;
/* 默认透明度为1不透明 */ /* No transparency by default */
opacity: 1; opacity: 1;
/* 添加圆角 */ /* Add rounded corners */
border-radius: 6px; border-radius: 6px;
} }
img.edit:hover { img.edit:hover {
/* 鼠标悬停时透明度降为0.5 */ /* Degrade transparency by 0.5 on hover */
opacity: 0.5; opacity: 0.5;
} }
/* 渐变边框 */ /* Gradient border */
.gradient-border { .gradient-border {
border: 12px solid transparent; border: 12px solid transparent;
border-radius: 6px; border-radius: 6px;
@ -187,13 +187,16 @@ img.edit:hover {
.main-content { .main-content {
width: 100%; width: 100%;
height: calc(100vh - 44px); /** 44 + 16 + 16 */
height: calc(100vh - 76px);
padding: 0px 16px; padding: 0px 16px;
margin-top: 16px;
margin-bottom: 16px;
} }
/** Element Plus Start */ /** Element Plus Start */
/* 移除全局 el-link 组件的下划线 */ /* Remove underlines globally for el-link */
.el-link::after { .el-link::after {
display: none; display: none;
} }
@ -229,7 +232,7 @@ img.edit:hover {
} }
.el-textarea__inner::-webkit-scrollbar { .el-textarea__inner::-webkit-scrollbar {
/* 隐藏滚动条 */ /* Hide scrollbar */
width: 0 !important; width: 0 !important;
} }
@ -250,14 +253,14 @@ button>span.el-button__text--expand {
--el-font-size-base: 11px; --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 {
--el-menu-horizontal-height: 36px; --el-menu-horizontal-height: 36px;
} }
/* 弹出菜单样式 */ /* Styles for pop-up menus */
.el-menu--collapse .el-menu .el-submenu, .el-menu--collapse .el-menu .el-submenu,
.el-menu--popup { .el-menu--popup {
min-width: 120px !important; min-width: 120px !important;
@ -265,7 +268,7 @@ button>span.el-button__text--expand {
margin: 0px 0px !important; margin: 0px 0px !important;
} }
/* 如果在AsideMenu定义, 关联的菜单组件可能读取不到某些覆盖的样式 */ /* If defined in AsideMenu, menu components associated may not read some overridden styles */
.el-sub-menu__title { .el-sub-menu__title {
padding: 0px 10px !important; padding: 0px 10px !important;
border-radius: 6px; border-radius: 6px;
@ -277,7 +280,7 @@ button>span.el-button__text--expand {
color: var(--el-color-primary); color: var(--el-color-primary);
} }
/** 菜单折叠时hover菜单项高度这里必须再定义一次 */ /* When menu is collapsed, hover menu item height must be redefined */
.el-menu-item { .el-menu-item {
border-radius: 6px; border-radius: 6px;
height: 32px !important; height: 32px !important;
@ -298,7 +301,7 @@ button>span.el-button__text--expand {
background-color: rgba(var(--el-color-primary-rgb), .1); background-color: rgba(var(--el-color-primary-rgb), .1);
} }
/* 分割线样式 */ /* Divider styles */
div.el-divider { div.el-divider {
margin: 18px auto; margin: 18px auto;
} }
@ -412,7 +415,7 @@ div.el-divider {
align-items: center; align-items: center;
} }
/* 全屏居中 */ /* Fullscreen center */
.full-center { .full-center {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -560,7 +563,7 @@ div.el-divider {
} }
/** /**
* 可换行的弹性布局 * Flex layout with multiline support
*/ */
.card-row-wrap { .card-row-wrap {
display: flex; display: flex;
@ -569,31 +572,31 @@ div.el-divider {
flex-wrap: wrap; flex-wrap: wrap;
} }
/** 卡片行内独占一行, 不能取代card-row-item wp-100 */ /** Exclusive line in a card, doesn't replace card-row-item wp-100 */
.card-row-aline { .card-row-aline {
width: 100%; width: 100%;
/*防止撑开父元素, 要用flex布局*/ /* Prevent parent element to be affected, need a flex display */
min-width: 0; min-width: 0;
margin: 8px 14px; margin: 8px 14px;
/** 为子元素预留margin空间 */ /* Reserve margin space for child elements in flex container */
/* padding: 0px 10px; */ /* padding: 0px 10px; */
display: flex; display: flex;
justify-content: start; justify-content: start;
align-items: center; align-items: center;
} }
/** 卡片行普通元素, 有固定宽度,自适应排版 */ /* Card row item with fixed width and adaptive layout */
.card-row-item { .card-row-item {
/*防止撑开父元素*/ /* Prevent item from expanding parent container */
min-width: 0; min-width: 0;
margin: 8px 14px; margin: 8px 14px;
/** 不自动换行,可设置高度 */ /* Flex layout without wrapping */
display: flex; display: flex;
justify-content: start; justify-content: start;
align-items: start; align-items: start;
} }
/* 元素水平居中 */ /* Horizontal centering utility class */
.item-h-center { .item-h-center {
margin: 0 auto; margin: 0 auto;
} }
@ -603,30 +606,30 @@ div.el-divider {
line-height: 32px; line-height: 32px;
} }
/* 第一个子元素 */ /* First child element styling in card rows */
.card-row-item>span:first-child, .card-row-item>span:first-child,
.card-row-item>label:first-child { .card-row-item>label:first-child {
align-self: start; align-self: start;
width: 100px; width: 100px;
} }
/* 第二个子元素是span时 */ /* Second child span element styling */
.card-row-item>span:nth-child(2) { .card-row-item>span:nth-child(2) {
width: 240px; width: 240px;
padding-left: 12px; padding-left: 12px;
} }
/* 第二个子元素是p时 */ /* Second child paragraph element styling */
.card-row-item>p:nth-child(2) { .card-row-item>p:nth-child(2) {
padding: 5px 12px; padding: 5px 12px;
} }
/* 第二个子元素 */ /* General second child element styling */
.card-row-item>*:nth-child(2) { .card-row-item>*:nth-child(2) {
width: 240px; width: 240px;
} }
/* 第二个子元素 独占一行时 */ /* Full-width modifier for second child elements */
.card-row-item.wp-100>*:nth-child(2) { .card-row-item.wp-100>*:nth-child(2) {
width: calc(100% - 100px); width: calc(100% - 100px);
} }
@ -636,7 +639,7 @@ div.el-divider {
} }
/** /**
* card行-无数据 * No data placeholder styling
*/ */
.card-item-no-data { .card-item-no-data {
width: 100%; width: 100%;
@ -663,17 +666,17 @@ div.el-divider {
margin: -8px -14px; margin: -8px -14px;
} }
/** 全屏视频背景包装模块 */ /** Full-screen video background container */
.video-bg-wrap { .video-bg-wrap {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100vh; height: 100vh;
/** 可设置背景在视频加载前或失败时做显示 设置为深色 */ /* Fallback background for video */
background: #000 url('') no-repeat fixed center center / cover; background: #000 url('') no-repeat fixed center center / cover;
overflow: hidden; overflow: hidden;
} }
/** 全屏视频背景包装模块-视频样式 */ /** Video element styling */
.video-bg-wrap video { .video-bg-wrap video {
z-index: 0; z-index: 0;
position: absolute; position: absolute;
@ -684,26 +687,24 @@ div.el-divider {
object-fit: fill; object-fit: fill;
} }
/** 全屏视频背景包装模块-前台内容样式 */ /** Front content overlay for video background */
.video-bg-wrap .front { .video-bg-wrap .front {
z-index: 1; z-index: 1;
/* 设置子元素为绝对定位 */ /* Set children to be absolute positioned */
position: absolute; position: absolute;
/* 子元素顶部距离父元素顶部的距离为50% */ /* Center content */
top: 50%; top: 50%;
/* 子元素左侧距离父元素左侧的距离为50% */
left: 50%; left: 50%;
/* 使用transform进行微调将子元素居中 */
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
/* background-color: rgb(255, 255, 255, 0.9); */ /* background-color: rgb(255, 255, 255, 0.9); */
/* 背景模糊 */ /* Blur background */
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
/* opacity: 0.8; */ /* opacity: 0.8; */
border-radius: 6px; border-radius: 6px;
} }
/** /**
* 色彩标签 * Color labels
*/ */
.color-label { .color-label {
padding: 2px 6px; padding: 2px 6px;
@ -711,7 +712,7 @@ div.el-divider {
} }
/** /**
* 布局管理器遮挡层 * Modal layer for layout manager
*/ */
.layout-shade { .layout-shade {
position: fixed; position: fixed;
@ -748,7 +749,7 @@ div.el-divider {
} }
.adv-search { .adv-search {
/** 自动高度过渡动画 */ /** Height transitions for height */
display: grid; display: grid;
grid-template-rows: 0fr; grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease-in-out; transition: grid-template-rows 0.3s ease-in-out;
@ -763,7 +764,7 @@ div.el-divider {
} }
/** /**
* 背景颜色 * Background colors
*/ */
.bg-primary { .bg-primary {
background-color: var(--el-color-primary); background-color: var(--el-color-primary);
@ -786,43 +787,43 @@ div.el-divider {
color: transparent; color: transparent;
} }
/* 定义文字渐变色动画 */ /* Color cycling animation for text */
@keyframes text-color-change { @keyframes text-color-change {
0% { 0% {
color: #ff3334; color: #ff3334;
/* 动画开始时的颜色 */ /* Color for beginning of animation */
} }
20% { 20% {
color: #ffcf00; color: #ffcf00;
/* 动画中间时的颜色 */ /* Color for middle of animation */
} }
40% { 40% {
color: #66ccff; color: #66ccff;
/* 动画中间时的颜色 */ /* Color for middle of animation */
} }
60% { 60% {
color: #ff3399; color: #ff3399;
/* 动画中间时的颜色 */ /* Color for middle of animation */
} }
80% { 80% {
color: #9933ff; color: #9933ff;
/* 动画中间时的颜色 */ /* Color for middle of animation */
} }
100% { 100% {
color: #19e713; color: #19e713;
/* 动画结束时的颜色 */ /* Color for ending of animation */
} }
} }
/* 应用动画到元素 */ /* Animated text element */
.animated-text { .animated-text {
animation: text-color-change 2s infinite alternate; animation: text-color-change 2s infinite alternate;
/* 动画名称,持续时间,循环次数,方向 */ /* Animation name, duration, iteration count, direction */
} }
@keyframes hue { @keyframes hue {

View File

@ -1,15 +1,16 @@
import { ReadAll } from '@/../wailsjs/go/main/App.js'
import utils from '@/assets/js/utils' import utils from '@/assets/js/utils'
// 临时缓存文件信息 // Temporary cache for file information
function cache(fileObj, $event) { 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)) console.log('typeof fileObj:', Array.isArray(fileObj))
// 如果fileObj是数组,创建一个新的元素,追加到数组 // If fileObj is an array, create a new element and append to the array
// event.target.files和event.dataTransfer.files是JavaScript中与文件上传和拖放相关的事件属性。 // event.target.files and event.dataTransfer.files are event properties in JavaScript related to file upload and drag-and-drop.
// event.target.files这个属性是在HTML的文件输入元素<input type="file">)上使用时, // event.target.files: This property is used with HTML file input elements (<input type="file">),
// 当用户选择文件并触发change事件时可以通过event.target.files获取到用户选择的文件列表。 // 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这个属性是在用户拖放文件到一个元素上时 // event.dataTransfer.files: This property is used when the user drags and drops files onto an element,
// 可以通过event.dataTransfer.files获取到拖放的文件列表。 // event.dataTransfer.files can be used to get the list of dropped files.
console.log('$event:', $event, $event.type) console.log('$event:', $event, $event.type)
let files let files
if ($event.type == 'change') { if ($event.type == 'change') {
@ -17,36 +18,36 @@ function cache(fileObj, $event) {
} else if ($event.type == 'drop') { } else if ($event.type == 'drop') {
files = $event.dataTransfer.files files = $event.dataTransfer.files
} else { } else {
console.error("无法识别的事件") console.error("Unrecognized event type")
return return
} }
const file = files[0] const file = files[0]
console.log("file:", file) console.log("Selected file:", file)
const fileInfo = Array.isArray(fileObj) ? new Object() : fileObj const fileInfo = Array.isArray(fileObj) ? new Object() : fileObj
fileInfo.file = file fileInfo.file = file
let URL = window.URL || window.webkitURL let URL = window.URL || window.webkitURL
fileInfo.fileUrl = URL.createObjectURL(file) fileInfo.fileUrl = URL.createObjectURL(file)
const fileType = file.type const fileType = file.type
console.log(fileType, typeof (fileType)) console.log("File type:", fileType, typeof (fileType))
if (utils.notNull(fileType) && fileType.startsWith("image")) { if (utils.notNull(fileType) && fileType.startsWith("image")) {
fileInfo.imgUrl = fileInfo.fileUrl fileInfo.imgUrl = fileInfo.fileUrl
} }
fileInfo.fileName = file.name fileInfo.fileName = file.name
console.log('cache fileObj end:', fileInfo) console.log('Caching fileObj completed:', fileInfo)
if (Array.isArray(fileObj)) { if (Array.isArray(fileObj)) {
// 操作成功后追加到数组末尾 // Append to the end of the array after successful operation
fileObj.push(fileInfo) fileObj.push(fileInfo)
} }
if ($event.type == 'change') { 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 $event.target.value = null
} }
} }
// 上传文件 // Upload file
async function upload(fileObj) { 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.isNull(fileObj.file)) {
if (utils.notNull(fileObj.fileId) && fileObj.remark != fileObj.remarkUpd) { if (utils.notNull(fileObj.fileId) && fileObj.remark != fileObj.remarkUpd) {
let remark = null let remark = null
@ -57,7 +58,7 @@ async function upload(fileObj) {
} }
return return
} }
console.log("开始上传文件!", fileObj, fileObj.file, fileObj.fileId) console.log("Starting file upload...", fileObj, fileObj.file, fileObj.fileId)
const url = '/common/file/upload' const url = '/common/file/upload'
const formData = new FormData() const formData = new FormData()
formData.append('file', fileObj.file) formData.append('file', fileObj.file)
@ -72,38 +73,38 @@ async function upload(fileObj) {
} }
}) })
Object.assign(fileObj, data) Object.assign(fileObj, data)
console.log("文件同步上传处理完毕", fileObj) console.log("File upload processed successfully", fileObj)
return fileObj return fileObj
} }
// 更新文件备注 // Update file remark
async function updRemark(fileId, remarkUpd) { async function updRemark(fileId, remarkUpd) {
const param = { const param = {
fileId: fileId, fileId: fileId,
remark: remarkUpd remark: remarkUpd
} }
await utils.awaitPost('/common/file/updRemark', param) await utils.awaitPost('/common/file/updRemark', param)
console.log("更新文件备注成功") console.log("File remark updated successfully")
} }
// 批量上传文件 // Batch upload files
async function uploads(fileObjs) { async function uploads(fileObjs) {
if (utils.isEmpty(fileObjs)) { if (utils.isEmpty(fileObjs)) {
return return
} }
for (let index in fileObjs) { 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]) 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) { function upOnChg(fileObj, $event) {
const file = $event.target.files[0] || $event.dataTransfer.files[0] const file = $event.target.files[0] || $event.dataTransfer.files[0]
// 当前地址 // Current location
let URL = window.URL || window.webkitURL let URL = window.URL || window.webkitURL
// 转成 blob地址 // Convert to blob URL
fileObj.fileUrl = URL.createObjectURL(file) fileObj.fileUrl = URL.createObjectURL(file)
const url = '/common/file/upload' const url = '/common/file/upload'
const formData = new FormData() const formData = new FormData()
@ -114,12 +115,13 @@ function upOnChg(fileObj, $event) {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
} }
}).then((data) => { }).then((data) => {
console.log("文件上传结果:", data) console.log("File upload result:", data)
Object.assign(fileObj, data) Object.assign(fileObj, data)
fileObj.remarkUpd = data.remark fileObj.remarkUpd = data.remark
}) })
} }
// Add to component list
function add(fileList) { function add(fileList) {
const comp = { const comp = {
index: fileList.length, index: fileList.length,
@ -133,8 +135,9 @@ function add(fileList) {
fileList.push(comp) fileList.push(comp)
} }
// Remove component from list
function del(fileObj, index) { function del(fileObj, index) {
console.log("fileObj,index:", fileObj, index) console.log("Deleting file object:", fileObj, index)
if (Array.isArray(fileObj)) { if (Array.isArray(fileObj)) {
fileObj.splice(index, 1) fileObj.splice(index, 1)
} else { } else {
@ -142,11 +145,12 @@ function del(fileObj, index) {
} }
} }
// Convert between Java and JS file objects
function trans(javaFile, jsFile) { function trans(javaFile, jsFile) {
if (jsFile == undefined || jsFile == null) { if (jsFile == undefined || jsFile == null) {
return return
} }
// 如果是数组,先清空数组 // Clear array if present
if (jsFile instanceof Array) { if (jsFile instanceof Array) {
jsFile.splice(0, jsFile.length) jsFile.splice(0, jsFile.length)
} else { } else {
@ -156,7 +160,7 @@ function trans(javaFile, jsFile) {
if (javaFile == undefined || javaFile == null) { if (javaFile == undefined || javaFile == null) {
return return
} }
// 数组类型 // Handle array type
if (jsFile instanceof Array) { if (jsFile instanceof Array) {
for (let java of javaFile) { for (let java of javaFile) {
const js = {} const js = {}
@ -165,37 +169,42 @@ function trans(javaFile, jsFile) {
jsFile.push(js) jsFile.push(js)
} }
} else { } else {
// 对象类型 // Handle object type
console.log("对象类型", jsFile instanceof Array) console.log("Object type conversion", jsFile instanceof Array)
javaFile.remarkUpd = javaFile.remark javaFile.remarkUpd = javaFile.remark
Object.assign(jsFile, javaFile) Object.assign(jsFile, javaFile)
} }
} }
// 从Comps中收集fileId // Collect file IDs from components
function fileIds(fileList) { function fileIds(fileList) {
return fileList.map(comp => comp.fileId).join(',') return fileList.map(comp => comp.fileId).join(',')
} }
export default { // Read file contents
function readAll(filePath) {
// onChange时缓存 return ReadAll(filePath)
cache, }
// 上传文件
upload, export default {
// 上传文件 // Cache on onChange
uploads, cache,
// 上传文件 // Upload file
upOnChg, upload,
// onChange时上传 // Upload files
upOnChg, uploads,
// 添加到组件列表 // Upload file
add, upOnChg,
// 从组件列表中删除组件 // Upload on onChange
del, upOnChg,
// 文件Java对象与js对象转换 // Add to component list
trans, add,
// 从Comps中收集fileId // Delete component from component list
fileIds del,
// Convert between Java object and js object
trans,
// Collect fileId from Comps
fileIds,
// Read file
readAll
} }

View File

@ -1,7 +1,7 @@
import { useEventListener } from '@vueuse/core' import { useEventListener } from '@vueuse/core'
/* /*
* 显示页面遮罩 * Show page shade
*/ */
export const showShade = function (closeCallBack) { export const showShade = function (closeCallBack) {
const className = 'shade' const className = 'shade'
@ -13,7 +13,7 @@ export const showShade = function (closeCallBack) {
} }
/* /*
* 隐藏页面遮罩 * Hide page shade
*/ */
export const closeShade = function (closeCallBack = () => { }) { export const closeShade = function (closeCallBack = () => { }) {
const shadeEl = document.querySelector('.layout-shade') const shadeEl = document.querySelector('.layout-shade')

View File

@ -1,15 +1,15 @@
import { Greet } from '@/../wailsjs/go/main/App.js'
import axios from "axios" import axios from "axios"
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Greet } from '@/../wailsjs/go/main/App.js'
/** axios start */ /** axios start */
// 创建 axios 实例 // Create a new axios instance
const $axios = axios.create({ const $axios = axios.create({
baseURL: "api", baseURL: "api",
timeout: 12000 timeout: 12000
}) })
// 请求拦截器 // Request interceptors
$axios.interceptors.request.use( $axios.interceptors.request.use(
(config) => { (config) => {
config.headers["token"] = '' config.headers["token"] = ''
@ -27,24 +27,26 @@ $axios.interceptors.request.use(
} }
) )
// 响应拦截器 // Response interceptors
$axios.interceptors.response.use( $axios.interceptors.response.use(
(response) => { (response) => {
// console.log("response:", response) // console.log("response:", response)
if (response.status == 200) { if (response.status == 200) {
return response.data return response.data
} else { } else {
pop("请求错误:" + response.status) pop("Exception occurred in response:" + response.status)
} }
}, },
(error) => { (error) => {
console.log("error:" + JSON.stringify(error)) console.log("error:" + JSON.stringify(error))
if (error.response == undefined || error.response == null) { if (error.response == undefined || error.response == null) {
pop("未知请求错误!") pop("Unknown request error!")
pop("Unknown request error!")
} else if (error.response.status == 500) { } else if (error.response.status == 500) {
pop("请求后台服务异常,请稍后重试!") pop("Unable to communicate with backend, please retry later!")
} else { } else {
pop("请求错误:" + error) pop("Request error:" + error)
pop("Request error:" + error)
} }
return Promise.reject(error) return Promise.reject(error)
} }
@ -75,7 +77,7 @@ async function awaitDel(url, param) {
} }
/** /**
* demo 调用 go 接口 * demo call Go interfaces
*/ */
function greet(name) { function greet(name) {
return Greet(name).then(resp => { return Greet(name).then(resp => {
@ -85,78 +87,80 @@ function greet(name) {
} }
/** /**
* 判断对象为空 * Check if object is null
*/ */
function isNull(obj) { function isNull(obj) {
return obj == undefined || obj == null return obj == undefined || obj == null
} }
/** /**
* 判断对象非空 * Check if object is not null
*/ */
function notNull(obj) { function notNull(obj) {
return obj != undefined && obj != null return obj != undefined && obj != null
} }
/** /**
* 判断空字符串 * Check if string is blank
*/ */
function isBlank(str) { function isBlank(str) {
return str == undefined || str == null || /^s*$/.test(str) return str == undefined || str == null || /^s*$/.test(str)
} }
/** /**
* 判断不为空字符串 * Identify a non-empty string
*/ */
function notBlank(str) { function notBlank(str) {
return !isBlank(str) return !isBlank(str)
} }
/** /**
* 判断数组为空 * Check if array is empty
*/ */
function isEmpty(arr) { function isEmpty(arr) {
return arr == undefined || arr == null || (arr instanceof Array && arr.length == 0) return arr == undefined || arr == null || (arr instanceof Array && arr.length == 0)
} }
/** /**
* 判断数组非空 * Check if array is not empty
*/ */
function notEmpty(arr) { function notEmpty(arr) {
return arr != undefined && arr != null && arr instanceof Array && arr.length > 0 return arr != undefined && arr != null && arr instanceof Array && arr.length > 0
} }
/** /**
* 判断对象为true * Check if object is true
*/ */
function isTrue(obj) { function isTrue(obj) {
return obj == true || obj == 'true' return obj == true || obj == 'true'
} }
/** /**
* 判断对象为false * Check if object is false
*/ */
function isFalse(obj) { function isFalse(obj) {
return !isTrue(obj) return !isTrue(obj)
} }
/**
/** * Get count of a specific character in a string
* @param {string} str - 要搜索的字符串 * @param {string} str - String to search
* @param {string} char - 要查找的字符 * @param {string} char - Character to find
* @returns {number} - 字符在字符串中出现的次数 * @returns {number} - Occurrence count
*/ */
function getCharCount(str, char) { function getCharCount(str, char) {
// 使用g表示整个字符串都要匹配 // g=match globally
var regex = new RegExp(char, 'g') var regex = new RegExp(char, 'g')
// match方法可在字符串内检索指定的值或找到一个或多个正则表达式的匹配 // Search for all occurrences of the character in the string
var result = str.match(regex) var result = str.match(regex)
var count = !result ? 0 : result.length var count = !result ? 0 : result.length
return count return count
} }
/** /**
* 日期格式化 * Format date with specified pattern
* 默认格式为yyyy-MM-dd HH:mm:ss * @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) { function dateFormat(date, format) {
if (date == undefined || date == null || date == '') { 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) { function fomateDateProperty(obj) {
for (let i in obj) { for (let i in obj) {
//遍历对象中的属性 // Iterate through all properties of the object
if (obj[i] == null) { if (obj[i] == null) {
continue continue
} else if (obj[i] instanceof Date) { } 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]) obj[i] = dateFormat(obj[i])
} else if (obj[i].constructor === Object) { } else if (obj[i].constructor === Object) {
//如果发现该属性的值还是一个对象,再判空后进行迭代调用 // Recursively format nested objects
if (Object.keys(obj[i]).length > 0) { if (Object.keys(obj[i]).length > 0) {
//判断对象上是否存在属性,如果为空对象则删除 // Delete empty properties
fomateDateProperty(obj[i]) fomateDateProperty(obj[i])
} }
} else if (obj[i].constructor === Array) { } else if (obj[i].constructor === Array) {
//对象值如果是数组,判断是否为空数组后进入数据遍历判空逻辑 // Recursively clean nested arrays
if (obj[i].length > 0) { if (obj[i].length > 0) {
for (let j = 0; j < obj[i].length; j++) { for (let j = 0; j < obj[i].length; j++) {
//遍历数组 // Iterate through all array items
fomateDateProperty(obj[i][j]) 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) { function delNullProperty(obj) {
for (let i in obj) { for (let i in obj) {
//遍历对象中的属性 // Iterate through all properties of the object
if (obj[i] === undefined || obj[i] === null || obj[i] === "") { if (obj[i] === undefined || obj[i] === null || obj[i] === "") {
//首先除去常规空数据用delete关键字 // Delete general null/empty properties
delete obj[i] delete obj[i]
} else if (obj[i].constructor === Object) { } else if (obj[i].constructor === Object) {
//如果发现该属性的值还是一个对象,再判空后进行迭代调用 // Recursively clean nested objects
if (Object.keys(obj[i]).length === 0) delete obj[i] if (Object.keys(obj[i]).length === 0) delete obj[i]
//判断对象上是否存在属性,如果为空对象则删除 // Delete empty properties
delNullProperty(obj[i]) delNullProperty(obj[i])
} else if (obj[i].constructor === Array) { } else if (obj[i].constructor === Array) {
//对象值如果是数组,判断是否为空数组后进入数据遍历判空逻辑 // Recursively clean arrays
if (obj[i].length === 0) { if (obj[i].length === 0) {
//如果数组为空则删除 // Delete empty arrays
delete obj[i] delete obj[i]
} else { } else {
for (let index = 0; index < obj[i].length; index++) { 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]) === "{}") { if (obj[i][index] === undefined || obj[i][index] === null || obj[i][index] === "" || JSON.stringify(obj[i][index]) === "{}") {
obj[i].splice(index, 1) obj[i].splice(index, 1)
//如果数组值为以上空值则修改数组长度,移除空值下标后续值依次提前 // Delete null/empty array items
index-- index--
//由于数组当前下标内容已经被替换成下一个值,所以计数器需要自减以抵消之后的自增 // Do decrement to avoid skipping next item (index is now pointing to the next item)
} }
if (obj[i].constructor === Object) { if (obj[i].constructor === Object) {
//如果发现数组值中有对象,则再次进入迭代 // Recursively clean nested objects in array items
delNullProperty(obj[i]) delNullProperty(obj[i])
} }
} }
@ -253,22 +260,27 @@ function delNullProperty(obj) {
} }
/** /**
* 弹出消息框 * Display message notification
* @param msg 消息内容 * @param {string} msg - Message content
* @param type * @param {string} type - Message type (success/warning/error/etc)
*/ */
function pop(msg, type) { function pop(msg, type) {
ElMessage({ message: msg, type: type }) ElMessage({ message: msg, type: type })
} }
/**
* Show default message when no data available
* @param {*} data - Data to check
*/
function popNoData(data) { function popNoData(data) {
if (data == undefined || data == null || (data instanceof Array && data.length == 0)) { 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() { function nowDatetimeStr() {
const date = new Date() 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) { function buildPage(source, target) {
target.pageNum = source.pageNum target.pageNum = source.pageNum
@ -287,7 +301,8 @@ function buildPage(source, target) {
copyArray(source.list, target.list) copyArray(source.list, target.list)
} }
/** /**
* 清空数组 * Clear array contents
* @param {Array} arr - Array to clear
*/ */
function clearArray(arr) { function clearArray(arr) {
if (arr == undefined || arr == null || arr.length == 0) { if (arr == undefined || arr == null || arr.length == 0) {
@ -295,8 +310,10 @@ function clearArray(arr) {
} }
arr.splice(0, arr.length) arr.splice(0, arr.length)
} }
/** /**
* 清空属性 * Reset object properties to null
* @param {Object} obj - Target object
*/ */
function clearProps(obj) { function clearProps(obj) {
if (obj == undefined || obj == null) { 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) { if (target == undefined || target == null) {
target = {} 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) { function copyArray(source, target) {
if (target == undefined || target == null) { if (target == undefined || target == null) {
return return
} }
// 先清空数组 // Clear the array first
if (target.length > 0) { if (target.length > 0) {
target.splice(0, target.length) target.splice(0, target.length)
/* while (target.length > 0) { /* 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) { function dfProps(origin, target) {
if (origin == undefined || origin == null || target == undefined || target == null) { if (origin == undefined || origin == null || target == undefined || target == null) {
@ -359,9 +383,11 @@ function dfProps(origin, target) {
return dfObj 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) { function hasDfProps(origin, target) {
const df = dfProps(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) { function isAllPropsNull(target) {
if (target == undefined || target == null) { if (target == undefined || target == null) {
@ -425,19 +453,21 @@ function colorByLabel(label) {
function descByLabel(label) { function descByLabel(label) {
if ('ADD' == label) { if ('ADD' == label) {
return '新增' return 'Add'
} }
if ('UPD' == label) { if ('UPD' == label) {
return '更新' return 'Update'
} }
if ('DEL' == label) { if ('DEL' == label) {
return '删除' return 'Delete'
} }
return label return label
} }
/** /**
* 重试调用 * Retry calls
* @param {Function} method - Method to call
* @param {any} params - Method parameters that are passed to the method
*/ */
function retry(method) { function retry(method) {
const params = [] 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) { function resolveLabelFromOpts(keyOrVal, opts) {
if (isEmpty(opts)) { if (isEmpty(opts)) {
@ -464,7 +497,11 @@ function resolveLabelFromOpts(keyOrVal, opts) {
return keyOrVal return keyOrVal
} }
/** 下划线转首字母小写驼峰 */ /**
* Underscored string to camel case string
* @param {String} underscore Underscored string
* @returns Camel case string
*/
function underScoreToCamelCase(underscore) { function underScoreToCamelCase(underscore) {
if (isNull(underscore) || !underscore.includes('_')) { if (isNull(underscore) || !underscore.includes('_')) {
return underscore return underscore
@ -480,7 +517,12 @@ function underScoreToCamelCase(underscore) {
return words.join("") 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) { function debounce(func, delay) {
let timer let timer
return function () { 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 { export default {
/** /**
* http请求 GET请求 * Synchronous GET HTTP request
*/ */
get, get,
/** /**
* http请求, 异步等待 GET请求 * Asynchronous GET HTTP request (async/await)
*/ */
awaitGet, awaitGet,
/** /**
* http请求 POST请求 * Synchronous POST HTTP request
*/ */
post, post,
/** /**
* http请求, 异步等待 POST请求 * Asynchronous POST HTTP request (async/await)
*/ */
awaitPost, awaitPost,
/** /**
* http请求 DELETE请求 * Synchronous DELETE HTTP request
*/ */
del, del,
/** /**
* http请求, 异步等待 DELETE请求 * Asynchronous DELETE HTTP request (async/await)
*/ */
awaitDel, awaitDel,
/** /**
* 判断对象为空 * Checks if a value is null/undefined
*/ */
isNull, isNull,
/** /**
* 判断对象非空 * Verifies a value is not null/undefined
*/ */
notNull, notNull,
@ -540,12 +591,12 @@ export default {
notBlank, notBlank,
/** /**
* 判断数组为空 * Checks if an array is empty
*/ */
isEmpty, isEmpty,
/** /**
* 判断数组非空 * Verifies an array contains elements
*/ */
notEmpty, notEmpty,
@ -556,64 +607,62 @@ export default {
getCharCount, getCharCount,
/** /**
* 弹出消息提示 * Displays a toast notification
*/ */
pop, pop,
/** /**
* 判定数据是否为空, 如果为空则提示暂无数据 * Shows "No data" notification for empty datasets
*/ */
popNoData, popNoData,
/** /**
* 遍历删除对象中的空值属性 * Removes null/undefined properties from an object
*/ */
delNullProperty, delNullProperty,
/** /**
* * Gets current datetime as formatted string (YYYY-MM-DD HH:mm:ss)
* 当前时间字符串
*/ */
nowDatetimeStr, nowDatetimeStr,
/** /**
* 构建分页 * Constructs pagination parameters
*/ */
buildPage, buildPage,
/** /**
* 清空数组 * Clears all elements from an array
*/ */
clearArray, clearArray,
/** /**
* 清空属性 * Resets object properties to null/undefined
*/ */
clearProps, clearProps,
/** /**
* 复制对象属性 * Copies properties between objects
*/ */
copyProps, copyProps,
/** /**
* 复制数组 * Creates a shallow array copy
*/ */
copyArray, copyArray,
/** /**
* 日期格式化 * Formats Date object to string (customizable format)
* 默认格式为yyyy-MM-dd HH:mm:ss
*/ */
dateFormat, dateFormat,
/** /**
* 遍历对象中的日期,并进行格式化 * Formats Date properties in objects to strings
*/ */
fomateDateProperty, fomateDateProperty,
/** /**
* 发生变更的属性 * Tracks changed properties between object states
*/ */
dfProps, dfProps,
@ -626,7 +675,7 @@ export default {
descByLabel, descByLabel,
/** /**
* 重试调用 * Retries failed operations with attempts
*/ */
retry, retry,
@ -636,4 +685,6 @@ export default {
debounce, debounce,
stringToLines,
} }

View File

@ -1,15 +1,15 @@
import utils from '@/assets/js/utils' import utils from '@/assets/js/utils'
/** 英文编码正则 */ /** Regex for English letters, numbers, and underscores */
const codeReg = /^[A-Za-z0-9_\-\.]+$/ const codeReg = /^[A-Za-z0-9_\-\.]+$/
/** 手机号正则 */ /** Regex for mobile phone number in China (Mainland) */
const mobileReg = /^1[3456789]\d{9}$/ 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])$)$/ 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 emailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
const commonValidator = (rule, value, callback) => { const commonValidator = (rule, value, callback) => {
@ -22,7 +22,7 @@ const commonValidator = (rule, value, callback) => {
const notBlankValidator = (rule, value, callback) => { const notBlankValidator = (rule, value, callback) => {
if (utils.isBlank(value)) { if (utils.isBlank(value)) {
callback(new Error('输入不能为空')) callback(new Error('Input cannot be blank'))
} else { } else {
callback() callback()
} }
@ -32,7 +32,7 @@ const nameValidator = (rule, value, callback) => {
if (utils.isBlank(value)) { if (utils.isBlank(value)) {
callback() callback()
} else if (value.length > 50) { } else if (value.length > 50) {
callback(new Error('字符数不能超过50')) callback(new Error('Name too long (max 50 characters)'))
} else { } else {
callback() callback()
} }
@ -42,7 +42,7 @@ const mobileValidator = (rule, value, callback) => {
if (utils.isNull(value)) { if (utils.isNull(value)) {
callback() callback()
} else if (!mobileReg.test(value)) { } else if (!mobileReg.test(value)) {
callback(new Error('手机号格式错误')) callback(new Error('Invalid mobile number'))
} else { } else {
callback() callback()
} }
@ -52,7 +52,7 @@ const idNoValidator = (rule, value, callback) => {
if (utils.isNull(value)) { if (utils.isNull(value)) {
callback() callback()
} else if (!idNoReg.test(value)) { } else if (!idNoReg.test(value)) {
callback(new Error('手机号格式错误')) callback(new Error('Invalid ID card number'))
} else { } else {
callback() callback()
} }
@ -62,7 +62,7 @@ const emailValidator = (rule, value, callback) => {
if (utils.isNull(value)) { if (utils.isNull(value)) {
callback() callback()
} else if (!emailReg.test(value)) { } else if (!emailReg.test(value)) {
callback(new Error('手机号格式错误')) callback(new Error('Invalid email address'))
} else { } else {
callback() callback()
} }
@ -72,7 +72,8 @@ const codeValidator = (rule, value, callback) => {
if (utils.isBlank(value)) { if (utils.isBlank(value)) {
callback() callback()
} else if (!codeReg.test(value)) { } else if (!codeReg.test(value)) {
callback(new Error('编码格式错误')) callback(new Error('Invalid code format'))
callback(new Error('Invalid code format'))
} else { } else {
callback() callback()
} }
@ -82,7 +83,7 @@ const intValidator = (rule, value, callback) => {
if (utils.isBlank(value)) { if (utils.isBlank(value)) {
callback() callback()
} else if (!Number.isInteger(value)) { } else if (!Number.isInteger(value)) {
callback(new Error('请输入整数')) callback(new Error('Input must be an integer'))
} else { } else {
callback() callback()
} }
@ -92,7 +93,7 @@ function validator() {
console.log("arguments:", arguments) console.log("arguments:", arguments)
if (arguments.length <= 1) { if (arguments.length <= 1) {
const type = arguments[0] const type = arguments[0]
// 默认校验逻辑, 不含有特殊字符 // Default validation logic, no special characters
if (utils.isBlank(type)) { if (utils.isBlank(type)) {
return commonValidator return commonValidator
} else if (type == 'notBlank') { } else if (type == 'notBlank') {
@ -113,22 +114,22 @@ function validator() {
return commonValidator return commonValidator
} }
} }
// 复合校验器 // Complex validators
const complexValidator = (rule, value, callback) => { const complexValidator = (rule, value, callback) => {
for (let i = 0; i < arguments.length; i++) { for (let i = 0; i < arguments.length; i++) {
const typeStr = arguments[i] const typeStr = arguments[i]
if (typeStr == 'notBlank' && utils.isBlank(value)) { if (typeStr == 'notBlank' && utils.isBlank(value)) {
callback(new Error('输入不能为空')) callback(new Error('Input cannot be blank'))
break break
} else if (typeStr == 'code' && !codeReg.test(value)) { } else if (typeStr == 'code' && !codeReg.test(value)) {
callback(new Error('编码格式错误')) callback(new Error('Invalid code format'))
break break
} else if (typeStr == 'int' && Number.isInteger(value)) { } else if (typeStr == 'int' && Number.isInteger(value)) {
callback(new Error('请输入整数')) callback(new Error('Please enter an integer'))
break break
} }
} }
// 兜底callback()只会触发一次 // Ensure callback is called at least once
callback() callback()
} }
return complexValidator return complexValidator
@ -138,62 +139,62 @@ export default {
username: (username) => { username: (username) => {
if (typeof (username) == "undefined" || username == null) { if (typeof (username) == "undefined" || username == null) {
return "账号不能为空" return "Username cannot be blank"
} }
username = username.trim() username = username.trim()
if (username.length < 4) { if (username.length < 4) {
return "账号字符不能小于4位" return "Username must be at least 4 characters long"
} }
if (username.length > 20) { if (username.length > 20) {
return "账号字符不能大于20位" return "Username must be at most 20 characters long"
} }
const reg = /^[A-Za-z0-9]+$/ const reg = /^[A-Za-z0-9]+$/
if (!reg.test(username)) { if (!reg.test(username)) {
return "账号为必须为字母和数字" return "Username must be letters and numbers only"
} }
return null return null
}, },
password: (password) => { password: (password) => {
if (typeof (password) == "undefined" || password == null) { if (typeof (password) == "undefined" || password == null) {
return "密码不能为空" return "Password cannot be blank"
} }
password = password.trim() password = password.trim()
if (password.length < 4) { if (password.length < 4) {
return "密码字符不能小于4位" return "Password must be at least 4 characters long"
} }
if (password.length > 20) { if (password.length > 20) {
return "密码字符不能大于20位" return "Password must be at most 20 characters long"
} }
const reg = /^[A-Za-z0-9\.\-\_\+]+$/ const reg = /^[A-Za-z0-9\.\-\_\+]+$/
if (!reg.test(password)) { if (!reg.test(password)) {
return "密码为必须为字母和数字或.-+_" return "Password must be letters, numbers, and special characters (.-_+) only"
} }
return null return null
}, },
email: (email) => { email: (email) => {
if (typeof (email) == "undefined" || email == null) { 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}$/ const reg = /^[A-Za-z0-9._%-]+@([A-Za-z0-9-]+\.)+[A-Za-z]{2,4}$/
if (!reg.test(email)) { if (!reg.test(email)) {
return "邮箱格式不正确" return "Invalid email address"
} }
return null return null
}, },
validCode: (validCode) => { validCode: (validCode) => {
if (typeof (validCode) == "undefined" || validCode == null) { if (typeof (validCode) == "undefined" || validCode == null) {
return "验证码不能为空" return "Verification code cannot be blank"
} }
validCode = validCode.trim() validCode = validCode.trim()
if (validCode.length != 6) { if (validCode.length != 6) {
return "验证码必须为6位" return "Verification code must be 6 characters long"
} }
const reg = /^[A-Za-z0-9]{6}$/ const reg = /^[A-Za-z0-9]{6}$/
if (!reg.test(validCode)) { if (!reg.test(validCode)) {
return "验证码格式不正确" return "Invalid verification code format"
} }
return null return null
}, },

View File

@ -1,12 +1,12 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' 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 { 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/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
@ -37,18 +37,18 @@ export default defineConfig({
}, },
build: { build: {
chunkSizeWarningLimit: 1500, chunkSizeWarningLimit: 1500,
// 分解块,将大块分解成更小的块 // Split chunks, break large chunks into smaller ones
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks(id) { manualChunks(id) {
if (id.includes('node_modules')) { if (id.includes('node_modules')) {
// 让每个插件都打包成独立的文件 // Let each plugin be packaged into an independent file
return id.toString().split('node_modules/')[1].split('/')[0].toString(); return id.toString().split('node_modules/')[1].split('/')[0].toString()
}
// Unit b, merge smaller modules
experimentalMinChunkSize: 10 * 1024
}
}
} }
}, },
// 单位b, 合并较小模块
experimentalMinChunkSize: 10 * 1024,
}
},
}
}) })

View File

@ -52,7 +52,7 @@ const handleClose = (key, keyPath) => {
// console.log(key, keyPath) // console.log(key, keyPath)
} }
// // Menu List
const menuList = [ const menuList = [
{ {
index: "M02", index: "M02",
@ -89,7 +89,7 @@ const menuList = [
] ]
onMounted(() => { onMounted(() => {
// , // Check menu position after refresh
// activeMenu() // activeMenu()
}) })
@ -107,24 +107,35 @@ function listSubMenu(menuCode) {
watch(() => router.currentRoute.value.path, (newValue, oldValue) => { watch(() => router.currentRoute.value.path, (newValue, oldValue) => {
// console.log('LeftMenurouter.currentRoute.value.path', newValue, oldValue) // console.log('LeftMenurouter.currentRoute.value.path', newValue, oldValue)
// , // Check menu position after route change
activeMenu() activeMenu()
}) })
// // Check activated menu position
function activeMenu() { function activeMenu() {
const currRoute = router.currentRoute const currRoute = router.currentRoute
const path = currRoute.value.path const path = currRoute.value.path
// console.log("currRoute path:", path) console.log("currRoute path:", path)
let index = getIndexByPath(path) let index = getIndexByPath(path)
// console.log("index:", index) console.log("index:", index)
if (utils.notNull(index)) { if (utils.notNull(index)) {
return 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" return "1"
} }
// index // Query menu index by path
function getIndexByPath(path) { function getIndexByPath(path) {
for (let fstMenu of menuList) { for (let fstMenu of menuList) {
// console.log(fstMenu.index, fstMenu.href == path) // console.log(fstMenu.index, fstMenu.href == path)
@ -146,7 +157,7 @@ function getIndexByPath(path) {
return thdMenu.index 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) { for (let thdMenu of thdMenuList) {
const nodeList = routeMap.get(path) const nodeList = routeMap.get(path)
if (utils.isEmpty(nodeList)) { if (utils.isEmpty(nodeList)) {
@ -154,14 +165,14 @@ function getIndexByPath(path) {
} }
for (let node of nodeList) { for (let node of nodeList) {
if (node.to == thdMenu.href) { 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 return thdMenu.index
} }
} }
} }
} }
} }
// ,to // Iterate through each secondary menu item in the secMenuList
for (let secMenu of secMenuList) { for (let secMenu of secMenuList) {
// console.log(secMenu.index, secMenu.href == path) // console.log(secMenu.index, secMenu.href == path)
const nodeList = routeMap.get(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 // get routes configuration
const routes = router.options.routes; const routes = router.options.routes
// console.log("routes:", routes) // console.log("routes:", routes)
const routeMap = new Map() const routeMap = new Map()
routes.forEach(lv1 => { routes.forEach(lv1 => {
@ -250,7 +261,7 @@ function getMenuNameByCode(code) {
<style scoped> <style scoped>
span { span {
/* 防止双击选中 */ /* Prevent text selection from double-clicking */
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
@ -258,16 +269,16 @@ span {
} }
span { span {
/* 字体大小 */ /* Font size */
font-size: 16px; font-size: 16px;
} }
li { li {
/* 字体大小 */ /* Font size */
font-size: 15px; font-size: 15px;
} }
/** 菜单折叠时hover菜单项高度这里必须再定义一次 */ /** When the menu is collapsed, redefine the hover menu height */
.el-menu-item { .el-menu-item {
min-width: 44px; min-width: 44px;
height: 36px; height: 36px;

View File

@ -25,17 +25,18 @@
</el-header> </el-header>
<el-main> <el-main>
<el-scrollbar style="width: 100%;"> <el-scrollbar style="width: 100%;">
<!-- 路由展示区 --> <!-- Router View Container -->
<!-- { Component }指当前路由所对应的组件 --> <!-- { Component } = currently matched route component -->
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<!-- 添加过渡动画 需要确保插入的component元素只有一个根节点, 否则报错. component中的根元素的transition会覆盖transitionName的样式 <!-- Cached Route Transition: Only keeps alive components with keepAlive meta flag
而且需要保证component中根元素的宽度相同所以最好是统一给component添加一个根元素 --> Transition animation requires single root element in component Key ensures proper re-rendering on route path changes -->
<transition :name="transitionName"> <transition :name="transitionName">
<KeepAlive> <KeepAlive>
<Component :is="Component" v-if="keepAlive" :key="$route.path" /> <Component :is="Component" v-if="keepAlive" :key="$route.path" />
</KeepAlive> </KeepAlive>
</transition> </transition>
<!-- 添加过渡动画 需要确保插入的component元素只有一个根节点 --> <!-- Non-cached Route Transition: Fresh instance for other components
Separate transition to prevent animation conflicts -->
<transition :name="transitionName"> <transition :name="transitionName">
<Component :is="Component" v-if="!keepAlive" :key="$route.path" /> <Component :is="Component" v-if="!keepAlive" :key="$route.path" />
</transition> </transition>
@ -66,46 +67,49 @@ const config = useConfig()
const { shrink, menuCollapse } = storeToRefs(config) const { shrink, menuCollapse } = storeToRefs(config)
const currentRoute = reactive(router.currentRoute) const currentRoute = reactive(router.currentRoute)
// , // Default transition effect, slide to the left
let transitionName = 'slide-left' let transitionName = 'slide-left'
const keepAlive = computed(() => { const keepAlive = computed(() => {
return currentRoute.value.meta.keepAlive 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) const menuAnimationDuration = ref(0)
// // Function to toggle the menu between expanded and collapsed states
function menuToggle() { function menuToggle() {
menuAnimationDuration.value = '300ms' menuAnimationDuration.value = '300ms'
if (menuCollapse.value) { if (menuCollapse.value) {
// console.log(", ") // console.log("Extend menu")
if (shrink.value) { if (shrink.value) {
// , // Expend the shade if menu is collapsing
showShade(() => { showShade(() => {
// console.log(", , ") // Callback function to close the shade after the menu has collapsed
config.setMenuCollapse(true) config.setMenuCollapse(true)
}) })
} }
} else { } else {
// console.log(", , ") // If the menu is in an expanded state, close the shade
closeShade() closeShade()
} }
// // Toggle the menu state
config.setMenuCollapse(!menuCollapse.value) config.setMenuCollapse(!menuCollapse.value)
} }
function onAdaptiveLayout() { function onAdaptiveLayout() {
// // Get the current window width
const clientWidth = document.body.clientWidth const clientWidth = document.body.clientWidth
// console.log("menuCollapse:", menuCollapse.value, config.getMenuCollapse(), "clientWidth:", 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) { if (clientWidth < 800) {
config.setShrink(true) config.setShrink(true)
if (!menuCollapse.value) { if (!menuCollapse.value) {
// , // Collapse the menu if it is not already collapsed
menuToggle() menuToggle()
} }
} else { } else {
@ -119,16 +123,15 @@ onBeforeMount(() => {
}) })
watch(() => router.currentRoute.value.path, (newValue, oldValue) => { 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) { if (shrink.value && !menuCollapse.value) {
// console.log(", , ")
menuToggle() menuToggle()
} }
}) })
function refresh() { function refresh() {
// console.log("") // Reload the page
location.reload() location.reload()
} }
@ -143,7 +146,11 @@ header {
width: 100%; width: 100%;
height: 44px; height: 44px;
padding: 0px; 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; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -169,6 +176,7 @@ main {
width: 100%; width: 100%;
padding: 0px; padding: 0px;
overflow: hidden; overflow: hidden;
background-color: var(--el-bg-color);
} }
.menu-logo { .menu-logo {
@ -177,7 +185,7 @@ main {
white-space: nowrap; white-space: nowrap;
} }
/** 菜单折叠 */ /* Keyframes for the menu collapse animation */
@keyframes menuCollapse { @keyframes menuCollapse {
0% { 0% {
width: 200px; width: 200px;
@ -188,7 +196,7 @@ main {
} }
} }
/** 菜单展开 */ /* Keyframes for the menu expand animation */
@keyframes menuExpand { @keyframes menuExpand {
0% { 0% {
width: 44px; width: 44px;
@ -204,9 +212,9 @@ main {
z-index: 9999; z-index: 9999;
height: 44px; height: 44px;
width: 44px; width: 44px;
/* 引用上面定义的@keyframes名称 */ /* Reference to the keyframes */
animation-name: menuCollapse; animation-name: menuCollapse;
/* 动画持续时间 */ /* Duration of the animation */
animation-duration: v-bind('menuAnimationDuration'); animation-duration: v-bind('menuAnimationDuration');
animation-timing-function: ease-in-out; animation-timing-function: ease-in-out;
background-color: var(--el-fg-color); background-color: var(--el-fg-color);
@ -217,13 +225,13 @@ main {
z-index: 9999; z-index: 9999;
height: 44px; height: 44px;
width: 200px; width: 200px;
/* 引用上面定义的@keyframes名称 */ /* Reference to the keyframes */
animation-name: menuExpand; animation-name: menuExpand;
/* 动画持续时间 */ /* Duration of the animation */
animation-duration: v-bind('menuAnimationDuration'); animation-duration: v-bind('menuAnimationDuration');
animation-timing-function: ease-in-out; animation-timing-function: ease-in-out;
background-color: var(--el-fg-color); background-color: var(--el-fg-color);
z-index: 9999999 z-index: 9999999;
} }
.scrollbar-menu-wrapper { .scrollbar-menu-wrapper {
@ -235,6 +243,6 @@ main {
.scrollbar-menu-wrapper.shrink { .scrollbar-menu-wrapper.shrink {
position: fixed; position: fixed;
left: 0; left: 0;
z-index: 9999999 z-index: 9999999;
} }
</style> </style>

View File

@ -6,22 +6,16 @@
<Refresh /> <Refresh />
</el-icon> </el-icon>
</el-button> </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-button type="danger" class="ml-10" @click="delSelected" :disabled="selectedRows.length == 0">
<el-icon :size="20" class="pr-4"> <el-icon :size="20" class="pr-4">
<Delete /> <Delete />
</el-icon> </el-icon>
删除 {{ t('delete') }}
</el-button> </el-button>
</div> </div>
<div v-show="advSearch"> <div v-show="advSearch">
<el-button @click="resetSearch">重置</el-button> <el-button @click="resetSearch"> {{ t('reset') }}</el-button>
<el-button type="primary" @click="search">查询</el-button> <el-button type="primary" @click="search"> {{ t('search') }}</el-button>
</div> </div>
<div> <div>
<el-input v-model="searchForm.kw" @input="baseSearch" clearable v-show="!advSearch" class="mr-8" /> <el-input v-model="searchForm.kw" @input="baseSearch" clearable v-show="!advSearch" class="mr-8" />
@ -54,7 +48,9 @@
<script setup> <script setup>
import { Refresh, Search, Grid, Plus, Delete } from '@element-plus/icons-vue' import { Refresh, Search, Grid, Plus, Delete } from '@element-plus/icons-vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({ const props = defineProps({
advSearch: { advSearch: {
default: false default: false
@ -69,9 +65,6 @@ const props = defineProps({
}, },
selectedRows: { selectedRows: {
default: [] default: []
},
addable: {
default: false
} }
}) })
@ -82,7 +75,6 @@ const emits = defineEmits([
'checkTableColumn', 'checkTableColumn',
'delSelected', 'delSelected',
'resetSearch', 'resetSearch',
'toAddPage',
]) ])
const baseSearch = () => { const baseSearch = () => {
@ -111,9 +103,6 @@ const resetSearch = () => {
emits('resetSearch') emits('resetSearch')
} }
const toAddPage = () => {
emits('toAddPage')
}
</script> </script>
<style scoped> <style scoped>

View File

@ -27,6 +27,13 @@
</el-link> </el-link>
</div> </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"> <el-dropdown trigger="click" @command="handleSwitchLang" class="fxc plr-16">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
@ -44,14 +51,18 @@
</template> </template>
</el-dropdown> </el-dropdown>
</div> </div>
</div>
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from 'vue' 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 { useConfig } from '@/store/config'
/** 暗黑主题切换 */
import { useDark } from '@vueuse/core'
const config = useConfig() const config = useConfig()
const isDark = useDark()
const modelList = ref(config.modelList) const modelList = ref(config.modelList)

View File

@ -1,4 +1,24 @@
export default { 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: { menu: {
task: "Task", task: "Task",
history: "History", history: "History",
@ -13,6 +33,9 @@ export default {
switchModel: "Switch Model", switchModel: "Switch Model",
step: "Step", step: "Step",
promptInputPlaceHolder: "Please Input Task Prompt", promptInputPlaceHolder: "Please Input Task Prompt",
promptInput: "Prompt Input",
promptInputKw: "Prompt Input",
clearCache: "Clear Cache",
clearCacheSuccess: "Clear cache success", clearCacheSuccess: "Clear cache success",
openManusAgiTips: "The above content is generated by OpenManus for reference only", openManusAgiTips: "The above content is generated by OpenManus for reference only",
taskStatus: { taskStatus: {
@ -21,5 +44,10 @@ export default {
failed: "Failed", failed: "Failed",
running: "Running", running: "Running",
terminated: "Terminated", terminated: "Terminated",
} },
newTask: "New Task",
readConfigSuccess: "Read config success",
readConfigFailed: "Read config failed",
baseConfig: "Base Settings",
serverConfig: "Server Config",
} }

View File

@ -1,4 +1,24 @@
export default { export default {
add: "新增",
edit: "编辑",
delete: "删除",
search: "搜索",
reset: "重置",
confirm: "确认",
cancel: "取消",
save: "保存",
submit: "提交",
export: "导出",
import: "导入",
copy: "复制",
paste: "粘贴",
cut: "剪切",
baseInfo: "基本信息",
createdDt: "创建时间",
updatedDt: "更新时间",
noData: "暂无数据",
menu: { menu: {
task: "任务", task: "任务",
history: "历史记录", history: "历史记录",
@ -12,6 +32,9 @@ export default {
user: '用户', user: '用户',
step: "步骤", step: "步骤",
promptInputPlaceHolder: "请输入任务提示词", promptInputPlaceHolder: "请输入任务提示词",
promptInput: "提示词输入",
promptInputKw: "提示词关键字",
clearCache: "清理缓存",
clearCacheSuccess: "清理缓存成功", clearCacheSuccess: "清理缓存成功",
openManusAgiTips: "以上内容由OpenManus生成, 仅供参考和借鉴", openManusAgiTips: "以上内容由OpenManus生成, 仅供参考和借鉴",
taskStatus: { taskStatus: {
@ -20,5 +43,10 @@ export default {
failed: "失败", failed: "失败",
running: "运行中", running: "运行中",
terminated: "终止", terminated: "终止",
} },
newTask: "新任务",
readConfigSuccess: "读取配置成功",
readConfigFailed: "读取配置失败",
baseConfig: "基础设置",
serverConfig: "服务器配置",
} }

View File

@ -1,25 +1,25 @@
import './assets/css/main.css' import './assets/css/main.css'
import files from '@/assets/js/files'
import utils from '@/assets/js/utils' import utils from '@/assets/js/utils'
import verify from '@/assets/js/verify'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import files from '@/assets/js/files'
import verify from '@/assets/js/verify'
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router'
import i18n from './locales/i18n' import i18n from './locales/i18n'
import router from './router'
// import ElementPlus from 'element-plus' // import ElementPlus from 'element-plus'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
/* 暗黑主题模式 */ /* Dark theme configuration */
import 'element-plus/theme-chalk/dark/css-vars.css'
import '@/assets/less/light.css'
import '@/assets/less/dark.css' import '@/assets/less/dark.css'
// 定义特性标志 import '@/assets/less/light.css'
window.__VUE_PROD_DEVTOOLS__ = false; import 'element-plus/theme-chalk/dark/css-vars.css'
window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false; // Configure Vue production flags
window.__VUE_PROD_DEVTOOLS__ = false
window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false
const pinia = createPinia() const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) pinia.use(piniaPluginPersistedstate)
@ -28,21 +28,21 @@ const app = createApp(App)
app.use(pinia) app.use(pinia)
// 全局引用router // Globally reference router
app.use(router) app.use(router)
app.use(i18n) app.use(i18n)
// ElMessage需要在utils中使用,这里单独引入 // Register Element Plus Message component globally (required for use in utils)
app.use(ElMessage) app.use(ElMessage)
// 全局使用 // Global configuration of ElementPlus
// ElSelect.props.placeholder.default = '请选择' // ElSelect.props.placeholder.default = '请选择'
// 在引入 ElementPlus 时,可以传入一个包含 size 和 zIndex 属性的全局配置对象。 // When ElementPlus is imported, a global configuration object can be passed in which contains size and zIndex properties
// size 用于设置表单组件的默认尺寸zIndex 用于设置弹出组件的层级zIndex 的默认值为 2000。 // 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 }) // app.use(ElementPlus, { locale, size: 'default', zIndex: 2000 })
// 使用vue3 provide注册 // Configure global providers for shared utilities
app.provide('utils', utils) app.provide('utils', utils)
@ -52,4 +52,3 @@ app.provide('verify', verify)
/* app.provide('uuid', uuidv4) */ /* app.provide('uuid', uuidv4) */
app.mount('#app') app.mount('#app')

View File

@ -14,18 +14,27 @@ const router = createRouter({
children: [ children: [
{ {
path: 'task', path: 'task',
component: () => import('@/views/main/Task.vue'), component: () => import('@/views/task/TaskIndex.vue'),
meta: { meta: {
keepAlive: false, keepAlive: true,
title: "任务", title: "任务列表",
index: 0
}
},
{
path: 'task/:id',
component: () => import('@/views/task/TaskInfo.vue'),
meta: {
keepAlive: true,
title: "任务信息",
index: 0 index: 0
} }
}, },
{ {
path: 'history', path: 'history',
component: () => import('@/views/main/Home.vue'), component: () => import('@/views/task/HistoryIndex.vue'),
meta: { meta: {
keepAlive: false, keepAlive: true,
title: "历史记录", title: "历史记录",
index: 0 index: 0
} }

View File

@ -3,33 +3,211 @@
<el-card> <el-card>
<template #header> <template #header>
<div class="title fxsb"> <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> <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>
</div> </div>
</template> </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> </el-card>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, inject, onMounted } from 'vue' import { ref, reactive, inject, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useConfig } from '@/store/config' import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const utils = inject('utils') const utils = inject('utils')
const files = inject('files')
const verify = inject('verify')
const router = useRouter() const router = useRouter()
const config = useConfig() const config = useConfig()
const { t } = useI18n() 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() { function clearCache() {
config.$reset() config.$reset()
utils.pop(t('clearCacheSuccess')) 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> </script>
<style scoped></style> <style scoped></style>

View File

@ -3,30 +3,222 @@
<el-card> <el-card>
<template #header> <template #header>
<div class="title fxsb"> <div class="title fxsb">
<div>基本信息</div> <div>LLM Config</div>
<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>
</div> </div>
</template> </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> </el-card>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, inject, onMounted } from 'vue' import { ref, reactive, inject, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' 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 utils = inject('utils')
const files = inject('files')
const verify = inject('verify')
const router = useRouter() const router = useRouter()
const config = useConfig() 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() { function clearCache() {
config.$reset() 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> </script>
<style scoped></style> <style scoped></style>

View File

@ -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) {
// stepstepList
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())
}
// stepListsubList
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>

View 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>

View File

@ -1,4 +1,4 @@
<template :lang="i18n.locale"> <template>
<div class="main-content fc"> <div class="main-content fc">
<el-scrollbar ref="scrollRef" style="width: 100%;"> <el-scrollbar ref="scrollRef" style="width: 100%;">
<div class="output-area" v-show="taskInfo.taskId != null"> <div class="output-area" v-show="taskInfo.taskId != null">

View File

@ -1,4 +1,4 @@
<template :lang="i18n.locale"> <template>
<div class="main-content fc"> <div class="main-content fc">
<el-scrollbar ref="scrollRef"> <el-scrollbar ref="scrollRef">
<div class="output-area" v-show="taskInfo.taskId != null"> <div class="output-area" v-show="taskInfo.taskId != null">
@ -6,9 +6,12 @@
<div class="dialog-user"> <div class="dialog-user">
<div class="blank"></div> <div class="blank"></div>
<div class="content"> <div class="content">
<el-text class="title"> <div class="title fxc">
<img src="@/assets/img/user.png" class="user-img" />
<el-text>
{{ t('user') }} {{ t('user') }}
</el-text> </el-text>
</div>
<el-text class="prompt"> <el-text class="prompt">
{{ taskInfo.prompt }} {{ taskInfo.prompt }}
</el-text> </el-text>
@ -47,15 +50,27 @@
</div> </div>
</div> </div>
</div> </div>
<div>
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
<el-text>{{ t(taskInfo.status) }}</el-text>
</div>
</div> </div>
</el-scrollbar> </el-scrollbar>
<div class="input-area"> <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"> <div class="input-box">
<el-icon @click="uploadFile" class="add-file-area" :size="24"> <el-icon @click="uploadFile" class="add-file-area" :size="24">
<FolderAdd /> <FolderAdd />
@ -65,10 +80,10 @@
@keydown.enter="handleInputEnter" /> @keydown.enter="handleInputEnter" />
<el-link class="send-area"> <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 /> <Promotion />
</el-icon> </el-icon>
<el-icon @click="stop" :size="24" v-show="loading"> <el-icon @click="stop" :size="24" v-show="loading || taskInfo.status == 'running'">
<CircleClose /> <CircleClose />
</el-icon> </el-icon>
</el-link> </el-link>
@ -84,10 +99,9 @@
<script setup> <script setup>
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue' 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 { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import i18n from '@/locales/i18n'
const utils = inject('utils') const utils = inject('utils')
const config = useConfig() const config = useConfig()
@ -99,7 +113,12 @@ const promptEle = ref(null)
const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message'] const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message']
const eventSource = ref(null) const eventSource = ref(null)
const newTaskFlag = ref(false)
const taskInfo = computed(() => { const taskInfo = computed(() => {
if (newTaskFlag.value) {
return {}
}
return config.getCurrTask() return config.getCurrTask()
}) })
@ -249,17 +268,22 @@ const scrollToBottom = () => {
// //
function sendPrompt() { function sendPrompt() {
//
if (eventSource.value != null) {
eventSource.value.close()
}
if (utils.isBlank(prompt.value)) { if (utils.isBlank(prompt.value)) {
utils.pop("Please enter a valid prompt", "error") utils.pop("Please enter a valid prompt", "error")
promptEle.value.focus() promptEle.value.focus()
return 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 => { utils.post('http://localhost:5172/tasks', { prompt: prompt.value }).then(data => {
if (!data.task_id) { if (!data.task_id) {
throw new Error('Invalid task ID') throw new Error('Invalid task ID')
@ -273,6 +297,7 @@ function sendPrompt() {
} }
// //
config.addTaskHistory(newTask) config.addTaskHistory(newTask)
newTaskFlag.value = false
// //
prompt.value = '' prompt.value = ''
// EventSource // EventSource
@ -287,11 +312,26 @@ function sendPrompt() {
function stop() { function stop() {
console.log("stop") console.log("stop")
loading.value = false loading.value = false
console.log("eventSource:", eventSource.value, "taskInfo:", taskInfo.value)
if (eventSource.value != null) {
eventSource.value.close() eventSource.value.close()
}
taskInfo.value.status = "terminated" taskInfo.value.status = "terminated"
utils.pop("用户终止任务", "error") 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> </script>
<style scoped> <style scoped>
@ -332,14 +372,18 @@ function stop() {
margin: 0px 16px 6px 16px; margin: 0px 16px 6px 16px;
} }
.dialog-user .user-img {
width: 20px;
height: 20px;
margin-right: 2px;
margin-bottom: 4px;
}
.dialog { .dialog {
width: 100%; width: 100%;
} }
.dialog-ai { .dialog-ai {
margin-bottom: 16px;
background-color: var(--el-fg-color); background-color: var(--el-fg-color);
border-radius: 12px; border-radius: 12px;
} }
@ -352,17 +396,34 @@ function stop() {
.input-area { .input-area {
flex-grow: 0; flex-grow: 0;
width: 100%; width: 100%;
max-height: 180px; max-height: 200px;
padding-left: 80px; padding-left: 80px;
padding-right: 80px; padding-right: 80px;
padding-top: 12px; padding-top: 12px;
padding-bottom: 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: 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 { .input-box {
width: 100%; width: 100%;
border-radius: 16px; border-radius: 16px;
@ -398,7 +459,7 @@ function stop() {
.tips { .tips {
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
font-size: 12px; font-size: 12px;
padding-top: 10px; padding-top: 12px;
} }
.sub-step-time { .sub-step-time {

View 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>

View File

@ -1,12 +1,12 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' 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 { 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/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
@ -37,16 +37,16 @@ export default defineConfig({
}, },
build: { build: {
chunkSizeWarningLimit: 1500, chunkSizeWarningLimit: 1500,
// 分解块,将大块分解成更小的块 // Fine-tune bundling strategy
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks(id) { manualChunks(id) {
if (id.includes('node_modules')) { if (id.includes('node_modules')) {
// 让每个插件都打包成独立的文件 // Extract package name from module path to create separate chunks
return id.toString().split('node_modules/')[1].split('/')[0].toString(); return id.toString().split('node_modules/')[1].split('/')[0].toString()
} }
}, },
// 单位b, 合并较小模块 // Attempt to merge chunks smaller than 10KB (in bytes)
experimentalMinChunkSize: 10 * 1024, experimentalMinChunkSize: 10 * 1024,
} }
}, },

View File

@ -2,3 +2,5 @@
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
export function Greet(arg1:string):Promise<string>; export function Greet(arg1:string):Promise<string>;
export function ReadAll(arg1:string):Promise<string>;

View File

@ -5,3 +5,7 @@
export function Greet(arg1) { export function Greet(arg1) {
return window['go']['main']['App']['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
View 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
}

View File

@ -1,4 +1,4 @@
package main package utils
import ( import (
"encoding/json" "encoding/json"

View File

@ -1,4 +1,4 @@
package main package utils
import ( import (
"log" "log"

View File

@ -1,10 +1,11 @@
package main package utils
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"strconv" "strconv"
"strings"
) )
// AnyToStr 任意类型数据转string // 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) 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) != ""
}

View File

@ -31,4 +31,3 @@ wails build
npm install axios npm install axios
npm install qs npm install qs
npm i --save-dev @types/qs npm i --save-dev @types/qs