feat: webUI2.0 前端介面更新

1. 剩余登陆注册未完成
2. 剩余插件列表&市场未完成
This commit is contained in:
HYana
2025-03-26 17:01:22 +08:00
committed by Junyan Qin
parent 8511432dee
commit 453237aef8
97 changed files with 9757 additions and 9332 deletions

View File

@@ -1,90 +0,0 @@
<template>
<v-card class="about-dialog">
<div id="content-container">
<div id="logo-container">
<v-img id="logo-img" src="@/assets/langbot-logo.png" width="64" />
<div id="text-container">
<div id="title">
LangBot
</div>
<div id="subtitle">
版本 {{ proxy.$store.state.version }}
</div>
<div id="copyright">
版权所有 © 2024 RockChinQ
</div>
</div>
</div>
</div>
<template v-slot:actions>
<v-btn id="open-source-btn" text="代码仓库" prepend-icon="mdi-github"
@click="openSource"></v-btn>
<v-btn class="ml-auto" text="关闭" prepend-icon="mdi-close" @click="close"></v-btn>
</template>
</v-card>
</template>
<script setup>
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const close = () => {
emit('close')
}
const openSource = () => {
window.open('https://github.com/RockChinQ/LangBot', '_blank')
}
</script>
<style scoped>
.about-dialog {
padding-top: 2rem;
}
#content-container {
display: flex;
flex-direction: column;
align-items: center;
}
#logo-container {
display: flex;
flex-direction: column;
align-items: center;
}
#logo-img {}
#text-container {
display: flex;
flex-direction: column;
align-items: center;
}
#title {
font-size: 1.2rem;
margin-top: 0.5rem;
}
#subtitle {
font-size: 0.6rem;
color: #3c3c3c;
margin-top: 0.2rem;
}
#copyright {
font-size: 0.6rem;
color: #3c3c3c;
margin-top: 0.2rem;
margin-bottom: 0.5rem;
}
</style>

View File

@@ -1,88 +0,0 @@
<template>
<v-dialog v-model="dialog" width="500" persistent>
<v-card id="init-dialog">
<v-card-title class="d-flex align-center" style="gap: 0.5rem;">
<img src="@/assets/langbot-logo.png" height="32" width="32" />
<span>系统初始化</span>
</v-card-title>
<v-card-text>
<p>请输入初始管理员邮箱和密码</p>
</v-card-text>
<v-card-text class="d-flex flex-column" style="gap: 0.5rem;">
<v-text-field v-model="user" variant="outlined" label="管理员邮箱" :rules="[rules.required, rules.email]"
clearable />
<v-text-field v-model="password" variant="outlined" label="管理员密码" :rules="[rules.required]"
type="password" clearable />
</v-card-text>
<v-card-actions>
<v-btn color="primary" variant="flat" @click="initialize" prepend-icon="mdi-check">初始化</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, inject, getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['error', 'success', 'checkSystemInitialized'])
const dialog = ref(true)
const user = ref('')
const password = ref('')
const snackbar = inject('snackbar')
const rules = {
required: value => !!value || '必填项',
email: value => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(value) || '请输入有效的邮箱地址'
}
}
function checkEmailValid(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return regex.test(email)
}
const initialize = () => {
// 检查邮箱和密码是否为空
if (user.value == undefined || password.value == undefined) {
emit('error', '邮箱和密码不能为空')
return
}
if (user.value == '' || password.value == '') {
emit('error', '邮箱和密码不能为空')
return
}
if (!checkEmailValid(user.value)) {
emit('error', '请输入有效的邮箱地址')
return
}
proxy.$axios.post('/user/init', {
user: user.value,
password: password.value
}).then(res => {
emit('success', '系统初始化成功')
emit('checkSystemInitialized')
})
}
</script>
<style scoped>
#init-dialog {
padding-top: 0.8rem;
padding-inline: 0.5rem;
}
</style>

View File

@@ -1,76 +0,0 @@
<template>
<v-dialog v-model="dialog" width="350" persistent>
<v-card id="login-dialog">
<v-card-title class="d-flex align-center" style="gap: 0.5rem;">
<img src="@/assets/langbot-logo.png" height="32" width="32" />
<span>登录 LangBot</span>
</v-card-title>
<v-card-text class="d-flex flex-column" style="gap: 0.5rem;margin-bottom: -2rem;margin-top: 1rem;">
<v-text-field v-model="user" variant="outlined" label="邮箱" :rules="[rules.required, rules.email]"
clearable />
<v-text-field v-model="password" variant="outlined" label="密码" :rules="[rules.required]"
type="password" clearable />
</v-card-text>
<v-card-actions>
<v-btn color="primary" variant="flat" @click="login">登录</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['error', 'success', 'checkToken'])
const dialog = ref(true)
const user = ref('')
const password = ref('')
const rules = {
required: value => !!value || '必填项',
email: value => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(value) || '请输入有效的邮箱地址'
}
}
const login = () => {
proxy.$axios.post('/user/auth', {
user: user.value,
password: password.value
}).then(res => {
if (res.data.code == 0) {
emit('success', '登录成功')
localStorage.setItem('user-token', res.data.data.token)
setTimeout(() => {
location.reload()
}, 1000)
} else {
emit('error', res.data.msg)
}
}).catch(err => {
if (err.response.data.msg) {
emit('error', err.response.data.msg)
} else {
emit('error', '登录失败')
}
})
}
</script>
<style scoped>
#login-dialog {
padding-top: 0.8rem;
padding-bottom: 0.5rem;
padding-inline: 0.5rem;
}
</style>

View File

@@ -1,181 +0,0 @@
<template>
<div class="plugin-card">
<div class="plugin-card-header">
<div class="plugin-id">
<div class="plugin-card-author">{{ plugin.author }} /</div>
<div class="plugin-card-title">{{ plugin.name }}</div>
</div>
<div class="plugin-card-badges">
<v-icon class="plugin-github-source" icon="mdi-github" v-if="plugin.repository != ''"
@click="openGithubSource"></v-icon>
</div>
</div>
<div class="plugin-card-description" >{{ plugin.description }}</div>
<div class="plugin-card-brief-info">
<div class="plugin-card-brief-info-item">
<v-icon id="plugin-stars-icon" icon="mdi-star" />
<div id="plugin-stars-count">{{ plugin.stars }}</div>
</div>
<v-btn color="primary" @click="installPlugin" density="compact">安装</v-btn>
</div>
</div>
</template>
<script setup>
const props = defineProps({
plugin: {
type: Object,
required: true
},
});
const emit = defineEmits(['install']);
const openGithubSource = () => {
window.open("https://"+props.plugin.repository, '_blank');
}
const installPlugin = () => {
emit('install', props.plugin);
}
</script>
<style scoped>
.plugin-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 0.8rem;
padding-left: 1rem;
margin: 1rem 0;
background-color: white;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 10rem;
}
.plugin-card-header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.plugin-card-author {
font-size: 0.8rem;
color: #666;
font-weight: 500;
user-select: none;
}
.plugin-card-title {
font-size: 0.9rem;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
}
.plugin-card-description {
font-size: 0.7rem;
color: #666;
font-weight: 500;
margin-top: -1rem;
height: 2rem;
/* 超出部分自动换行,最多两行 */
text-overflow: ellipsis;
overflow-y: hidden;
white-space: wrap;
user-select: none;
}
.plugin-card-badges {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
.plugin-github-source {
cursor: pointer;
color: #222;
font-size: 1.3rem;
}
.plugin-disabled {
font-size: 0.7rem;
font-weight: 500;
height: 1.3rem;
padding-inline: 0.4rem;
user-select: none;
}
.plugin-card-brief-info {
display: flex;
flex-direction: row;
justify-content: space-between;
/* background-color: #f0f0f0; */
gap: 0.8rem;
margin-left: -0.2rem;
margin-bottom: 0rem;
}
.plugin-card-events {
display: flex;
flex-direction: row;
gap: 0.4rem;
}
.plugin-card-events-icon {
font-size: 1.8rem;
color: #666;
}
.plugin-card-events-count {
font-size: 1.2rem;
font-weight: 600;
color: #666;
}
.plugin-card-functions {
display: flex;
flex-direction: row;
gap: 0.4rem;
}
.plugin-card-functions-icon {
font-size: 1.6rem;
color: #666;
}
.plugin-card-functions-count {
font-size: 1.2rem;
font-weight: 600;
color: #666;
}
.plugin-card-brief-info-item {
display: flex;
flex-direction: row;
gap: 0.4rem;
}
#plugin-stars-icon {
color: #0073ff;
}
#plugin-stars-count {
margin-top: 0.1rem;
font-weight: 700;
color: #0073ff;
user-select: none;
}
.plugin-card-brief-info-item:hover .plugin-card-brief-info-item-icon {
color: #333;
}
</style>

View File

@@ -1,228 +0,0 @@
<template>
<div id="marketplace-container">
<div id="marketplace-search-bar">
<span style="width: 14rem;">
<v-text-field id="marketplace-search-bar-search-input" variant="solo" v-model="proxy.$store.state.marketplaceParams.query" label="搜索"
density="compact" @update:model-value="updateSearch" />
</span>
<!--下拉选择排序-->
<span style="width: 10rem;">
<v-select id="marketplace-search-bar-sort-select" v-model="sort" :items="sortItems" variant="solo"
label="排序" density="compact" @update:model-value="updateSort" />
</span>
<span style="margin-left: 1rem;">
<div id="marketplace-search-bar-total-plugins-count">
{{ proxy.$store.state.marketplaceTotalPluginsCount }} 个插件
</div>
</span>
<span style="margin-left: 1rem;">
<!-- 分页 -->
<v-pagination style="width: 14rem;" v-model="proxy.$store.state.marketplaceParams.page"
:length="proxy.$store.state.marketplaceTotalPages" variant="solo" density="compact"
total-visible="4" @update:model-value="updatePage" />
</span>
</div>
<div id="marketplace-plugins-container" ref="pluginsContainer">
<div id="marketplace-plugins-container-inner">
<MarketPluginCard v-for="plugin in proxy.$store.state.marketplacePlugins" :key="plugin.id" :plugin="plugin" @install="installPlugin" />
</div>
</div>
</div>
</template>
<script setup>
import MarketPluginCard from './MarketPluginCard.vue'
import { ref, getCurrentInstance, onMounted } from 'vue'
import { inject } from "vue";
const snackbar = inject('snackbar');
const { proxy } = getCurrentInstance()
const pluginsContainer = ref(null)
const sortItems = ref([
'最近新增',
'最多星标',
'最近更新',
])
const sortParams = ref({
'最近新增': {
sort_by: 'created_at',
sort_order: 'DESC',
},
'最多星标': {
sort_by: 'stars',
sort_order: 'DESC',
},
'最近更新': {
sort_by: 'pushed_at',
sort_order: 'DESC',
}
})
const sort = ref(sortItems.value[0])
proxy.$store.state.marketplaceParams.sort_by = sortParams.value[sort.value].sort_by
proxy.$store.state.marketplaceParams.sort_order = sortParams.value[sort.value].sort_order
const updateSort = (value) => {
console.log(value)
proxy.$store.state.marketplaceParams.sort_by = sortParams.value[value].sort_by
proxy.$store.state.marketplaceParams.sort_order = sortParams.value[value].sort_order
proxy.$store.state.marketplaceParams.page = 1
console.log(proxy.$store.state.marketplaceParams)
fetchMarketplacePlugins()
}
const updatePage = (value) => {
proxy.$store.state.marketplaceParams.page = value
fetchMarketplacePlugins()
}
const updateSearch = (value) => {
console.log(value)
proxy.$store.state.marketplaceParams.query = value
proxy.$store.state.marketplaceParams.page = 1
fetchMarketplacePlugins()
}
const calculatePluginsPerPage = () => {
if (!pluginsContainer.value) return 10
const containerWidth = pluginsContainer.value.clientWidth
const containerHeight = pluginsContainer.value.clientHeight
console.log(containerWidth, containerHeight)
// 每个卡片宽度18rem + gap 16px
const cardWidth = 18 * 16 + 16 // rem转px
// 每个卡片高度9rem + gap 16px
const cardHeight = 9 * 16 + 16
// 计算每行可以放几个卡片
const cardsPerRow = Math.floor(containerWidth / cardWidth)
// 计算每行可以放几行
const rows = Math.floor(containerHeight / cardHeight)
// 计算每页总数
const perPage = cardsPerRow * rows
proxy.$store.state.marketplaceParams.per_page = perPage > 0 ? perPage : 10
}
const fetchMarketplacePlugins = async () => {
calculatePluginsPerPage()
proxy.$axios.post('https://space.langbot.app/api/v1/market/plugins', {
query: proxy.$store.state.marketplaceParams.query,
sort_by: proxy.$store.state.marketplaceParams.sort_by,
sort_order: proxy.$store.state.marketplaceParams.sort_order,
page: proxy.$store.state.marketplaceParams.page,
page_size: proxy.$store.state.marketplaceParams.per_page,
}).then(response => {
console.log(response.data)
if (response.data.code != 0) {
snackbar.error(response.data.msg)
return
}
// 解析出 name 和 author
response.data.data.plugins.forEach(plugin => {
plugin.name = plugin.repository.split('/')[2]
plugin.author = plugin.repository.split('/')[1]
})
proxy.$store.state.marketplacePlugins = response.data.data.plugins
proxy.$store.state.marketplaceTotalPluginsCount = response.data.data.total
let totalPages = Math.floor(response.data.data.total / proxy.$store.state.marketplaceParams.per_page)
if (response.data.data.total % proxy.$store.state.marketplaceParams.per_page != 0) {
totalPages += 1
}
proxy.$store.state.marketplaceTotalPages = totalPages
}).catch(error => {
snackbar.error(error)
})
}
onMounted(() => {
calculatePluginsPerPage()
fetchMarketplacePlugins()
// 监听窗口大小变化
window.addEventListener('resize', () => {
calculatePluginsPerPage()
fetchMarketplacePlugins()
})
})
const emit = defineEmits(['installPlugin'])
const installPlugin = (plugin) => {
emit('installPlugin', plugin.repository)
}
</script>
<style scoped>
#marketplace-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
#marketplace-search-bar {
display: flex;
flex-direction: row;
margin-top: 1rem;
padding-right: 1rem;
gap: 1rem;
width: 100%;
}
#marketplace-search-bar-search-input {
position: relative;
left: 1rem;
width: 10rem;
}
#marketplace-search-bar-total-plugins-count {
font-size: 1.1rem;
font-weight: 500;
margin-top: 0.5rem;
color: #666;
user-select: none;
}
.plugin-card {
width: 18rem;
height: 9rem;
}
#marketplace-plugins-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-wrap: wrap;
gap: 16px;
margin-inline: 0rem;
width: 100%;
height: calc(100vh - 16rem);
overflow-y: auto;
}
#marketplace-plugins-container-inner {
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-wrap: wrap;
gap: 16px;
}
</style>

View File

@@ -1,75 +0,0 @@
<template>
<div id="number-field-data">
<div class="number-field-data-item" :style="{ color: color }">
{{ number }}
</div>
<div class="number-field-data-item-title" :style="{ marginTop: link ? '0rem' : '0.1rem', fontSize: link ? '0.9rem' : '1.1rem', marginBottom: link ? '0rem' : '0.5rem' }">
{{ title }}
</div>
<button class="number-field-data-item-link-text" v-if="link" @click="linkClick">{{ linkText }}</button>
</div>
</template>
<script setup>
const props = defineProps({
title: {
type: String,
required: true
},
number: {
type: Number,
required: true
},
color: {
type: String,
default: '#4271bf'
},
link: {
type: String,
default: ''
},
linkText: {
type: String,
default: '查看详情'
}
})
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
const linkClick = (e) => {
proxy.$router.push(props.link)
}
</script>
<style scoped>
#number-field-data {
display: flex;
flex-direction: column;
align-items: center;
/* justify-content: center; */
}
.number-field-data-item {
font-size: 2.2rem;
font-weight: 600;
user-select: none;
}
.number-field-data-item-title {
font-weight: 600;
user-select: none;
}
.number-field-data-item-link-text {
text-decoration: none;
appearance: none;
font-weight: 600;
color: #4271bf;
font-size: 0.6rem;
}
</style>

View File

@@ -1,51 +0,0 @@
<template>
<span class="title-container">
<h2 id="page-title">{{ title }}</h2>
<v-icon @click="refresh" id="refresh-icon" icon="mdi-refresh" />
</span>
</template>
<script setup>
defineProps({
title: {
type: String,
required: true
}
})
const emit = defineEmits(['refresh'])
const refresh = () => {
emit('refresh')
}
</script>
<style scoped>
#page-title {
font-weight: 500;
margin-left: 1.4rem;
user-select: none;
}
.title-container {
display: flex;
justify-content: flex-start;
align-items: center;
padding-top: 1rem;
}
#refresh-icon {
padding-top: 0.1rem;
margin-left: 0.5rem;
color: #aaa;
}
#refresh-icon:hover {
cursor: pointer;
}
#refresh-icon:active {
color: #6c6c6c;
}
</style>

View File

@@ -1,257 +0,0 @@
<template>
<div class="plugin-card">
<div class="plugin-card-header">
<div class="plugin-id">
<div class="plugin-card-author">{{ plugin.author }} /</div>
<div class="plugin-card-title">{{ plugin.name }}</div>
</div>
<div class="plugin-card-badges">
<v-icon class="plugin-github-source" icon="mdi-github" v-if="plugin.source != ''"
@click="openGithubSource"></v-icon>
<v-chip class="plugin-disabled" v-if="!plugin.enabled" color="error" variant="outlined"
density="compact">已禁用</v-chip>
<v-chip class="plugin-version" color="primary" density="compact" variant="flat">v{{ plugin.version
}}</v-chip>
</div>
</div>
<div class="plugin-card-description">{{ plugin.description }}</div>
<div class="plugin-card-brief-info">
<div class="plugin-card-brief-info-item">
<v-tooltip text="已注册的事件处理器" location="bottom">
<template v-slot:activator="{ props }">
<div class="plugin-card-events" v-bind="props">
<v-icon class="plugin-card-events-icon" icon="mdi-link-box-variant-outline" />
<div class="plugin-card-events-count">{{ Object.keys(plugin.event_handlers).length }}</div>
</div>
</template>
</v-tooltip>
<v-tooltip text="已注册的内容函数" location="bottom">
<template v-slot:activator="{ props }">
<div class="plugin-card-functions" v-bind="props">
<v-icon class="plugin-card-functions-icon" icon="mdi-tools" />
<div class="plugin-card-functions-count">{{ plugin.content_functions.length }}</div>
</div>
</template>
</v-tooltip>
</div>
<v-menu class="plugin-card-menu">
<template v-slot:activator="{ props }">
<v-icon class="plugin-card-menu-btn" icon="mdi-cog" v-bind="props" variant="text" size="small"/>
</template>
<v-list>
<template v-for="item in menuItems" :key="item.title">
<v-list-item v-if="item.condition(plugin)" @click="item.action">
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</template>
</v-list>
</v-menu>
</div>
</div>
</template>
<script setup>
const props = defineProps({
plugin: {
type: Object,
required: true
},
});
const emit = defineEmits(['toggle', 'update', 'remove']);
const openGithubSource = () => {
window.open(props.plugin.source, '_blank');
}
const togglePlugin = () => {
emit('toggle', props.plugin);
}
const updatePlugin = () => {
emit('update', props.plugin);
}
const removePlugin = () => {
emit('remove', props.plugin);
}
const menuItems = [
{
title: '禁用',
condition: (plugin) => plugin.enabled,
action: togglePlugin
},
{
title: '启用',
condition: (plugin) => !plugin.enabled,
action: togglePlugin
},
{
title: '更新',
condition: (plugin) => plugin.source != '',
action: updatePlugin
},
{
title: '删除',
condition: (plugin) => true,
action: removePlugin
}
]
</script>
<style scoped>
.plugin-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 0.8rem;
padding-left: 1rem;
margin: 1rem 0;
background-color: white;
display: flex;
flex-direction: column;
height: 10rem;
}
.plugin-card-header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.plugin-card-author {
font-size: 0.8rem;
color: #666;
font-weight: 500;
user-select: none;
}
.plugin-card-title {
font-size: 1.1rem;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
}
.plugin-card-description {
font-size: 0.7rem;
color: #666;
font-weight: 500;
margin-top: 0rem;
height: 2rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
}
.plugin-card-badges {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
.plugin-github-source {
cursor: pointer;
color: #222;
font-size: 1.3rem;
}
.plugin-disabled {
font-size: 0.7rem;
font-weight: 500;
height: 1.3rem;
padding-inline: 0.4rem;
user-select: none;
}
.plugin-version {
font-size: 0.7rem;
font-weight: 700;
height: 1.3rem;
padding-inline: 0.5rem;
user-select: none;
}
.plugin-card-brief-info {
display: flex;
flex-direction: row;
justify-content: space-between;
/* background-color: #f0f0f0; */
gap: 0.8rem;
margin-left: -0.2rem;
margin-top: 0.5rem;
}
.plugin-card-events {
display: flex;
flex-direction: row;
gap: 0.4rem;
}
.plugin-card-events-icon {
font-size: 1.8rem;
color: #666;
}
.plugin-card-events-count {
font-size: 1.2rem;
font-weight: 600;
color: #666;
}
.plugin-card-functions {
display: flex;
flex-direction: row;
gap: 0.4rem;
}
.plugin-card-functions-icon {
font-size: 1.6rem;
color: #666;
}
.plugin-card-functions-count {
font-size: 1.2rem;
font-weight: 600;
color: #666;
}
.plugin-card-brief-info-item {
display: flex;
flex-direction: row;
gap: 0.4rem;
}
.plugin-card-brief-info-item:hover {
cursor: pointer;
}
.plugin-card-brief-info-item:hover .plugin-card-brief-info-item-icon {
color: #333;
}
.plugin-card-menu {
margin-top: 0.5rem;
}
.plugin-card-menu-btn {
font-size: 1.4rem;
margin-top: 0.2rem;
font-weight: 400;
padding: 0rem;
color: #3265ba;
}
.plugin-card-menu-btn:hover {
color: #4271bf;
}
.plugin-card-menu-btn:active {
color: rgb(0, 47, 104);
}
</style>

View File

@@ -1,287 +0,0 @@
<template>
<v-card class="config-tab-toolbar">
<v-tooltip :text="currentManagerSchema == null ? '仅配置文件管理器提供了 JSON Schema 时支持可视化配置' : '切换编辑模式'" location="top">
<template v-slot:activator="{ props }">
<v-btn-toggle id="config-type-toggle" color="primary" v-model="configType" mandatory v-bind="props"
density="compact">
<v-btn class="config-type-toggle-btn" value="ui" :readonly="currentManagerSchema == null"
density="compact">
<v-icon>mdi-view-dashboard-edit-outline</v-icon>
</v-btn>
<v-btn class="config-type-toggle-btn" value="json" :readonly="currentManagerSchema == null"
density="compact">
<v-icon>mdi-code-json</v-icon>
</v-btn>
</v-btn-toggle>
</template>
</v-tooltip>
<div id="file-operation-toolbar">
<v-btn @click="reset" color="warning" prepend-icon="mdi-undo"
:disabled="!modified && configType == 'json'">重置</v-btn>
<v-btn @click="saveAndApply" color="primary" prepend-icon="mdi-content-save-outline"
:disabled="!modified && configType == 'json'">应用</v-btn>
</div>
</v-card>
<div id="config-tab-content">
<div id="config-tab-content-ui" v-if="configType == 'ui'">
<v-form id="config-tab-content-ui-form">
<vjsf id="config-tab-content-ui-form-vjsf" :schema="currentManagerSchema" v-model="currentManagerData"
:options="VJSFOptions" />
</v-form>
</div>
<v-card id="config-tab-content-json" v-if="configType == 'json'">
<JsonEditorVue id="config-tab-content-json-json-editor-vue" v-model="currentManagerData" mode="text"
@change="onInput" />
</v-card>
<div id="config-tab-json-doc-link"
v-if="configType == 'json' && currentManagerDocLink != undefined && currentManagerDocLink != ''">*配置文件格式请查看
<a :href="currentManagerDocLink" target="_blank">文档</a>
</div>
</div>
</template>
<script setup>
import { ref, getCurrentInstance, onMounted, inject } from 'vue'
import JsonEditorVue from 'json-editor-vue';
import Vjsf from '@koumoul/vjsf';
const VJSFOptions = {
"context": {},
"width": 1208,
"readOnly": false,
"summary": false,
"density": "comfortable",
"indent": true,
"titleDepth": 4,
"validateOn": "input",
"initialValidation": "withData",
"updateOn": "input",
"debounceInputMs": 300,
"defaultOn": "empty",
"removeAdditional": "error",
"autofocus": false,
"readOnlyPropertiesMode": "show",
"pluginsOptions": {},
"locale": "en",
"messages": {
"errorOneOf": "请选择一个",
"errorRequired": "必填信息",
"addItem": "添加",
"delete": "删除",
"edit": "编辑",
"close": "关闭",
"duplicate": "复制",
"sort": "排序",
"up": "向上移动",
"down": "向下移动",
"showHelp": "显示帮助信息",
"mdeLink1": "[链接标题",
"mdeLink2": "](链接地址)",
"mdeImg1": "![](",
"mdeImg2": "图片地址)",
"mdeTable1": "",
"mdeTable2": "\n\n| 列 1 | 列 2 | 列 3 |\n| -------- | -------- | -------- |\n| 文本 | 文本 | 文本 |\n\n",
"bold": "加粗",
"italic": "斜体",
"heading": "标题",
"quote": "引用",
"unorderedList": "无序列表",
"orderedList": "有序列表",
"createLink": "创建链接",
"insertImage": "插入图片",
"createTable": "创建表格",
"preview": "预览",
"mdeGuide": "文档",
"undo": "撤销",
"redo": "重做"
}
}
const snackbar = inject('snackbar');
const { proxy } = getCurrentInstance()
const props = defineProps({
name: String
})
const currentManagerData = ref({})
const currentManagerSchema = ref(null)
const currentManagerDocLink = ref(null)
const configType = ref('')
const modified = ref(false)
const fetchCurrentManagerData = (name) => {
console.log(name)
return new Promise((resolve, reject) => {
proxy.$axios.get(`/settings/${name}`).then(response => {
console.log(response.data.data)
currentManagerData.value = response.data.data.manager.data
currentManagerSchema.value = response.data.data.manager.schema
currentManagerDocLink.value = response.data.data.manager.doc_link
resolve()
}).catch(error => {
snackbar.error(error)
reject(error)
})
})
}
const autoSwitchConfigType = () => {
console.log(currentManagerSchema == null)
if (currentManagerSchema.value == null) {
configType.value = 'json'
} else {
configType.value = 'ui'
}
}
const isJsonValid = ref(true)
const errorMessage = ref('')
const checkJsonValid = () => {
try {
JSON.parse(currentManagerData.value)
isJsonValid.value = true
errorMessage.value = ''
} catch (error) {
isJsonValid.value = false
errorMessage.value = error.message
}
}
const onInput = () => {
modified.value = true
checkJsonValid()
}
const saveAndApply = () => {
if (configType.value == 'json') {
checkJsonValid()
}
if (!isJsonValid.value) {
snackbar.error('JSON 格式不正确: ' + errorMessage.value)
return
}
if (configType.value == 'json') {
currentManagerData.value = JSON.parse(currentManagerData.value)
}
proxy.$axios.put(`/settings/${props.name}/data`, {
data: currentManagerData.value
}).then(response => {
if (response.data.code != 0) {
snackbar.error(response.data.msg)
return
}
fetchCurrentManagerData(props.name).then(() => {
modified.value = false
snackbar.success('应用成功')
}).catch(error => {
snackbar.error(error)
})
})
}
const reset = () => {
fetchCurrentManagerData(props.name).then(() => {
snackbar.success('重置成功')
modified.value = false
})
}
onMounted(() => {
console.log(props.name)
fetchCurrentManagerData(props.name).then(() => {
autoSwitchConfigType()
})
})
</script>
<style scoped>
.config-tab-window {
overflow: hidden;
}
.config-tab-toolbar {
margin: 0.5rem;
height: 3.2rem;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
#file-operation-toolbar {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 0.5rem;
margin-right: 0.5rem;
}
#config-type-toggle {
margin: 0.5rem;
box-shadow: 0 0 0 2px #dddddd;
}
#config-tab-content {
margin: 0.2rem;
height: calc(100% - 1rem);
overflow: hidden;
}
#config-tab-content-ui {
margin: 0.5rem;
height: calc(100vh - 15rem);
margin-top: 1rem;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#config-tab-content-ui-form {
height: 100%;
width: calc(100% - 1.5rem);
margin-left: 0.5rem;
overflow-y: auto;
}
#config-tab-content-ui-form-vjsf {
height: 100%;
width: calc(100% - 1rem);
}
#config-tab-content-json {
margin: 0.5rem;
height: calc(100vh - 16rem);
margin-top: 1rem;
}
#config-tab-content-json-json-editor-vue {
height: 100%;
width: 100%;
}
#config-tab-json-doc-link {
margin: 0rem;
margin-left: 0.6rem;
height: 1.5rem;
margin-top: 0.2rem;
font-size: 0.8rem;
color: rgb(26, 98, 214);
}
</style>

View File

@@ -1,144 +0,0 @@
<template>
<div class="task-card">
<div class="task-card-icon">
<v-progress-circular :size="25" :width="2" indeterminate v-if="task.runtime.state == 'PENDING'" />
<v-icon v-else-if="task.runtime.state == 'FINISHED' && task.runtime.exception == null"
style="color: #4caf50;" :size="25" icon="mdi-check" />
<v-icon v-else-if="task.runtime.state == 'CANCELLED'" style="color: #f44336;" :size="25" icon="mdi-close" />
<v-icon v-else-if="task.runtime.state == 'FINISHED' && task.runtime.exception != null" style="color: #f44336;"
:size="25" icon="mdi-alert-circle-outline" v-bind="activatorProps" />
</div>
<div class="task-card-content">
<div class="task-card-kind">{{ task.kind }}</div>
<div class="task-card-label">{{ task.label }}</div>
<v-chip class="task-card-action" color="primary" variant="outlined" size="small" density="compact"
v-if="task.runtime.state == 'PENDING'">正在执行: {{ task.task_context.current_action }}</v-chip>
<v-chip class="task-card-action" color="success" variant="outlined" size="small" density="compact"
v-else-if="task.runtime.state == 'FINISHED' && task.runtime.exception == null">完成</v-chip>
<v-dialog max-width="500" persistent v-model="exceptionDialogShow">
<template v-slot:activator="{ props: activatorProps }">
<v-chip class="task-card-action" color="error" variant="outlined" size="small" density="compact"
v-if="task.runtime.state == 'FINISHED' && task.runtime.exception != null"
v-bind="activatorProps">{{ task.runtime.exception }}</v-chip>
</template>
<v-card prepend-icon="mdi-alert-circle-outline"
:text="task.runtime.exception_traceback"
title="任务执行失败">
<template v-slot:actions>
<v-spacer></v-spacer>
<v-btn @click="exceptionDialogShow = false" prepend-icon="mdi-close">
关闭
</v-btn>
</template>
</v-card>
</v-dialog>
</div>
<div class="task-card-actions">
<!-- <v-icon icon="mdi-details" /> -->
<v-dialog max-width="500" persistent v-model="detailsDialogShow">
<template v-slot:activator="{ props: activatorProps }">
<v-btn icon="mdi-file-outline" variant="text" v-bind="activatorProps" />
</template>
<v-card prepend-icon="mdi-file-outline" id="task-details-card"
:title="'任务执行日志 - '+task.label">
<div id="task-details-log-container">
<textarea id="task-details-log" v-model="task.task_context.log" readonly />
<v-spacer></v-spacer>
</div>
<template v-slot:actions>
<v-btn @click="detailsDialogShow = false" prepend-icon="mdi-close">
关闭
</v-btn>
</template>
</v-card>
</v-dialog>
</div>
</div>
</template>
<script setup>
defineProps({
task: {
type: Object,
required: true
}
})
import { ref } from 'vue'
const exceptionDialogShow = ref(false)
const detailsDialogShow = ref(false)
</script>
<style scoped>
.task-card {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
width: 100%;
padding-inline: 0.5rem;
}
.task-card-icon {
margin-right: 1rem;
margin-left: 0.8rem;
}
.task-card-content {
flex: 1;
margin-left: 0.4rem;
display: flex;
flex-direction: column;
gap: 0.02rem;
user-select: none;
}
.task-card-kind {
font-size: 0.6rem;
color: #666;
}
.task-card-label {
font-size: 0.8rem;
color: #333;
}
.task-card-action {
font-size: 0.6rem;
width: fit-content;
padding-inline: 0.3rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-card-actions {
justify-self: flex-end;
}
#task-details-log {
width: calc(100% - 2rem);
height: 10rem;
resize: none;
border: none;
padding: 0.6rem;
font-size: 0.8rem;
outline: none;
overflow: auto;
appearance: none;
background-color: #f6f6f6;
margin-inline: 1rem;
}
</style>

View File

@@ -1,91 +0,0 @@
<template>
<v-card class="task-dialog" prepend-icon="mdi-align-horizontal-left" title="任务列表">
<v-list id="task-list" v-if="taskList.length > 0">
<TaskCard class="task-card" v-for="task in taskList" :key="task.id" :task="task" />
</v-list>
<div v-else><v-alert color="warning" icon="$warning" title="暂无任务" text="暂无已添加的用户任务项" density="compact" style="margin-inline: 1rem;"></v-alert></div>
<template v-slot:actions>
<v-btn class="ml-auto" text="关闭" prepend-icon="mdi-close" @click="close"></v-btn>
</template>
</v-card>
</template>
<script setup>
defineProps({
})
const emit = defineEmits(['close'])
import TaskCard from '@/components/TaskCard.vue'
import { ref, onMounted, onUnmounted, getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
import { inject } from 'vue'
const snackbar = inject('snackbar')
const close = () => {
emit('close')
}
const taskList = ref([])
const refresh = () => {
proxy.$axios.get('/system/tasks', {
params: {
type: 'user'
}
}).then(response => {
if (response.data.code != 0) {
snackbar.error(response.data.message)
return
}
taskList.value = response.data.data.tasks
// 倒序
taskList.value.reverse()
}).catch(error => {
snackbar.error(error.message)
})
}
let refreshTask = null
onMounted(() => {
refresh()
refreshTask = setInterval(refresh, 1000)
})
onUnmounted(() => {
clearInterval(refreshTask)
})
</script>
<style scoped>
.task-dialog {
width: 100%;
}
#task-list {
max-height: 20rem;
overflow-y: auto;
margin-inline: 1rem;
width: calc(100% - 2.2rem);
padding-inline: 0.6rem;
}
.task-card {
/* margin-bottom: 0.1rem; */
display: flex;
flex-direction: row;
/* box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.1); */
height: 4rem;
/* border: 0.08rem solid #ccc; */
box-shadow: 0.1rem 0.1rem 0.2rem 0.05rem #ccc;
background-color: #ffffff;
}
</style>