merge v4.1.5

This commit is contained in:
RockYang
2025-02-11 09:45:26 +08:00
163 changed files with 5667 additions and 14603 deletions

View File

@@ -132,18 +132,19 @@ const content =ref(processPrompt(props.data.content))
const files = ref([])
onMounted(() => {
// if (!finalTokens.value) {
// httpPost("/api/chat/tokens", {text: props.data.content, model: props.data.model}).then(res => {
// finalTokens.value = res.data;
// }).catch(() => {
// })
// }
processFiles()
})
const processFiles = () => {
if (!props.data.content) {
return
}
const linkRegex = /(https?:\/\/\S+)/g;
const links = props.data.content.match(linkRegex);
if (links) {
httpPost("/api/upload/list", {urls: links}).then(res => {
files.value = res.data
files.value = res.data.items
for (let link of links) {
if (isExternalImg(link, files.value)) {
@@ -159,8 +160,7 @@ onMounted(() => {
}
content.value = md.render(content.value.trim())
})
}
const isExternalImg = (link, files) => {
return isImage(link) && !files.find(file => file.url === link)
}

View File

@@ -15,7 +15,9 @@
<el-radio value="chat">对话样式</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="流式输出:">
<el-switch v-model="data.stream" @change="(val) => {store.setChatStream(val)}" />
</el-form-item>
</el-form>
</div>
</el-dialog>
@@ -28,6 +30,7 @@ const store = useSharedStore();
const data = ref({
style: store.chatListStyle,
stream: store.chatStream,
})
// eslint-disable-next-line no-undef
const props = defineProps({

View File

@@ -60,6 +60,7 @@ const removeFile = (file) => {
display flex
flex-flow row
margin-right 10px
max-width 600px
position relative
.el-image {

View File

@@ -1,57 +1,64 @@
<template>
<el-container class="file-select-box">
<a class="file-upload-img" @click="fetchFiles">
<a class="file-upload-img" @click="fetchFiles(1)">
<i class="iconfont icon-attachment-st"></i>
</a>
<el-dialog
class="file-list-dialog"
v-model="show"
:close-on-click-modal="true"
:show-close="true"
:width="800"
title="文件管理"
>
<div class="file-list">
<el-row :gutter="20">
<el-col :span="3">
<div class="grid-content">
<el-upload
class="avatar-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="afterRead"
accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf,.mp4,.mp3"
>
<el-icon class="avatar-uploader-icon">
<Plus/>
</el-icon>
</el-upload>
</div>
</el-col>
<el-col :span="3" v-for="file in fileList" :key="file.url">
<div class="grid-content">
<el-tooltip
class="box-item"
effect="dark"
:content="file.name"
placement="top">
<el-image :src="file.url" fit="cover" v-if="isImage(file.ext)" @click="insertURL(file)"/>
<el-image :src="GetFileIcon(file.ext)" fit="cover" v-else @click="insertURL(file)"/>
</el-tooltip>
<div class="opt">
<el-button type="danger" size="small" :icon="Delete" @click="removeFile(file)" circle/>
<el-scrollbar ref="scrollbarRef" max-height="80vh" style="height: 100%;" @scroll="onScroll">
<div class="file-list">
<el-row :gutter="20">
<el-col :span="3">
<div class="grid-content">
<el-upload
class="avatar-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="afterRead"
accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf,.mp4,.mp3"
>
<el-icon class="avatar-uploader-icon">
<Plus/>
</el-icon>
</el-upload>
</div>
</div>
</el-col>
</el-row>
</div>
</el-col>
<el-col :span="3" v-for="file in fileData.items" :key="file.url">
<div class="grid-content">
<el-tooltip
class="box-item"
effect="dark"
:content="file.name"
placement="top">
<el-image :src="file.url" fit="cover" v-if="isImage(file.ext)" @click="insertURL(file)"/>
<el-image :src="GetFileIcon(file.ext)" fit="cover" v-else @click="insertURL(file)"/>
</el-tooltip>
<div class="opt">
<el-button type="danger" size="small" :icon="Delete" @click="removeFile(file)" circle/>
</div>
</div>
</el-col>
</el-row>
<el-row justify="center" v-if="!fileData.isLastPage" @click="fetchFiles(fileData.page)">
<el-link>加载更多</el-link>
</el-row>
</div>
</el-scrollbar>
</el-dialog>
</el-container>
</template>
<script setup>
import {ref} from "vue";
import {reactive, ref} from "vue";
import {ElMessage} from "element-plus";
import {httpGet, httpPost} from "@/utils/http";
import {Delete, Plus} from "@element-plus/icons-vue";
@@ -64,15 +71,45 @@ const props = defineProps({
const emits = defineEmits(['selected']);
const show = ref(false)
const fileList = ref([])
const scrollbarRef = ref(null)
const fileData = reactive({
items:[],
page: 1,
isLastPage: true,
})
const fetchFiles = () => {
show.value = true
httpPost("/api/upload/list").then(res => {
fileList.value = res.data
const fetchFiles = (pageNo) => {
if(pageNo === 1) show.value = true
httpPost("/api/upload/list", { page: pageNo || 1, page_size: 30 }).then(res => {
const { items, page, total_page } = res.data
if(page === 1){
fileData.items = items
}else{
fileData.items = [...fileData.items, ...items]
}
fileData.isLastPage = (page === total_page)
if(!fileData.isLastPage){
fileData.page = page + 1
}
}).catch(() => {
})
}
// el-scrollbar 滚动回调
const onScroll = (options) => {
const wrapRef = scrollbarRef.value.wrapRef
scrollbarRef.value.moveY = wrapRef.scrollTop * 100 / wrapRef.clientHeight
scrollbarRef.value.moveX = wrapRef.scrollLeft * 100 / wrapRef.clientWidth
const poor = wrapRef.scrollHeight - wrapRef.clientHeight
// 判断滚动到底部 自动加载数据
if (options.scrollTop + 2 >= poor && !fileData.isLastPage) {
fetchFiles(fileData.page)
}
}
const afterRead = (file) => {
const formData = new FormData();
@@ -92,6 +129,7 @@ const removeFile = (file) => {
return v1.id === v2.id
})
ElMessage.success("文件删除成功!")
fetchFiles(1)
}).catch((e) => {
ElMessage.error('文件删除失败:' + e.message)
})
@@ -120,9 +158,10 @@ const insertURL = (file) => {
.el-dialog__body {
//padding 0
overflow hidden
.file-list {
margin-right 10px
.grid-content {
margin-bottom 10px
position relative

View File

@@ -1,5 +1,5 @@
<template>
<div class="list-box" ref="containerRef">
<div class="item__list-box" ref="containerRef">
<el-row :gutter="gap">
<el-col v-for="item in items" :key="item.id" :span="span" :style="{marginBottom:gap+'px'} ">
<slot :item="item"></slot>
@@ -54,9 +54,10 @@ const calcSpan = () => {
window.onresize = () => calcSpan()
</script>
<style scoped lang="stylus">
<style lang="stylus">
.list-box {
.item__list-box {
width 100%
}
</style>

View File

@@ -251,6 +251,7 @@ import Captcha from "@/components/Captcha.vue";
import ResetPass from "@/components/ResetPass.vue";
import {setRoute} from "@/store/system";
import {useRouter} from "vue-router";
import {useSharedStore} from "@/store/sharedata";
// eslint-disable-next-line no-undef
const props = defineProps({
@@ -263,8 +264,8 @@ watch(() => props.show, (newValue) => {
const login = ref(true)
const data = ref({
username: "",
password: "",
username: process.env.VUE_APP_USER,
password: process.env.VUE_APP_PASS,
mobile: "",
email: "",
repass: "",
@@ -285,6 +286,9 @@ const action = ref("login")
const enableVerify = ref(false)
const showResetPass = ref(false)
const router = useRouter()
const store = useSharedStore()
// 是否需要验证码,输入一次密码错之后就要验证码
const needVerify = ref(false)
onMounted(() => {
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`
@@ -297,17 +301,17 @@ onMounted(() => {
getSystemInfo().then(res => {
if (res.data) {
const registerWays = res.data['register_ways']
if (arrayContains(registerWays, "mobile")) {
enableMobile.value = true
activeName.value = activeName.value === "" ? "mobile" : activeName.value
if (arrayContains(registerWays, "username")) {
enableUser.value = true
activeName.value = 'username'
}
if (arrayContains(registerWays, "email")) {
enableEmail.value = true
activeName.value = activeName.value === "" ? "email" : activeName.value
activeName.value = 'email'
}
if (arrayContains(registerWays, "username")) {
enableUser.value = true
activeName.value = activeName.value === "" ? "username" : activeName.value
if (arrayContains(registerWays, "mobile")) {
enableMobile.value = true
activeName.value = 'mobile'
}
// 是否启用注册
enableRegister.value = res.data['enabled_register']
@@ -338,7 +342,7 @@ const submitLogin = () => {
if (data.value.password === '') {
return ElMessage.error('请输入密码');
}
if (enableVerify.value) {
if (enableVerify.value && needVerify.value) {
captchaRef.value.loadCaptcha()
action.value = "login"
} else {
@@ -352,11 +356,14 @@ const doLogin = (verifyData) => {
data.value.x = verifyData.x
httpPost('/api/user/login', data.value).then((res) => {
setUserToken(res.data.token)
store.setIsLogin(true)
ElMessage.success("登录成功!")
emits("hide")
emits('success')
needVerify.value = false
}).catch((e) => {
ElMessage.error('登录失败,' + e.message)
needVerify.value = true
})
}
@@ -384,7 +391,7 @@ const submitRegister = () => {
if ((activeName.value === 'mobile' || activeName.value === 'email') && data.value.code === '') {
return ElMessage.error('请输入验证码');
}
if (enableVerify.value) {
if (enableVerify.value && activeName.value === 'username') {
captchaRef.value.loadCaptcha()
action.value = "register"
} else {

View File

@@ -1,28 +1,14 @@
<template>
<div class="running-job-list">
<div class="running-job-box" v-if="list.length > 0">
<div class="job-item" v-for="item in list">
<div class="job-item" v-for="item in list" :key="item.id">
<div v-if="item.progress > 0" class="job-item-inner">
<el-image v-if="item.img_url" :src="item['img_url']" fit="cover" loading="lazy">
<template #placeholder>
<div class="image-slot">
正在加载图片
</div>
</template>
<template #error>
<div class="image-slot">
<el-icon>
<Picture/>
</el-icon>
</div>
</template>
</el-image>
<div class="progress" v-if="item.progress > 0">
<el-progress type="circle" :percentage="item.progress" :width="100"
color="#47fff1"/>
</div>
<div class="progress">
<el-progress type="circle" :percentage="item.progress" :width="100"
color="#47fff1"/>
</div>
</div>
<el-image fit="cover" v-else>
<template #error>
@@ -39,11 +25,7 @@
</template>
<script setup>
import {ref} from "vue";
import {CircleCloseFilled, Picture} from "@element-plus/icons-vue";
import {isImage, removeArrayItem, substr} from "@/utils/libs";
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system";
// eslint-disable-next-line no-undef
const props = defineProps({
list: {
type: Array,

View File

@@ -1,5 +1,5 @@
<template>
<div class="user-bill" v-loading="loading">
<div class="user-bill" v-loading="loading" element-loading-background="rgba(255,255,255,.3)">
<el-row v-if="items.length > 0">
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border
style="--el-table-border-color:#373C47;
@@ -22,7 +22,8 @@
<span>{{ scope.row.remark?.power }}</span>
</template>
</el-table-column>
<el-table-column prop="pay_way" label="支付方式"/>
<el-table-column prop="pay_method" label="支付渠道"/>
<el-table-column prop="pay_name" label="支付名称"/>
<el-table-column label="支付时间">
<template #default="scope">
<span v-if="scope.row['pay_time']">{{ dateFormat(scope.row['pay_time']) }}</span>

View File

@@ -52,13 +52,14 @@
</div>
</template>
<script setup>
import {computed, onMounted, ref} from 'vue';
import {onMounted, ref, watch} from 'vue';
import {getMenuItems, useSidebarStore} from '@/store/sidebar';
import {useRouter} from "vue-router";
import {ArrowDown, ArrowRight, Expand, Fold, Moon, Sunny} from "@element-plus/icons-vue";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {removeAdminToken} from "@/store/session";
import {useSharedStore} from "@/store/sharedata";
const version = ref(process.env.VUE_APP_VERSION)
const avatar = ref('/images/user-info.jpg')
@@ -66,24 +67,18 @@ const sidebar = useSidebarStore();
const router = useRouter();
const breadcrumb = ref([])
// eslint-disable-next-line no-undef
const props = defineProps({
theme: String,
});
const theme = computed(() => {
return props.theme
const store = useSharedStore()
const dark = ref(store.adminTheme === 'dark')
const theme = ref(store.adminTheme)
watch(() => store.adminTheme, (val) => {
theme.value = val
})
const dark = ref(props.theme === 'dark' ? true : false)
// eslint-disable-next-line no-undef
const emits = defineEmits(['changeTheme']);
const changeTheme = () => {
emits('changeTheme', dark.value)
store.setAdminTheme(dark.value ? 'dark' : 'light')
}
router.afterEach((to, from) => {
router.afterEach((to) => {
initBreadCrumb(to.path)
});

View File

@@ -52,11 +52,12 @@
</template>
<script setup>
import {computed, ref} from 'vue';
import {computed, ref, watch} from 'vue';
import {setMenuItems, useSidebarStore} from '@/store/sidebar';
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {useRoute} from "vue-router";
import {useSharedStore} from "@/store/sharedata";
const title = ref('')
const logo = ref('')
@@ -68,16 +69,11 @@ httpGet('/api/admin/config/get?key=system').then(res => {
}).catch(e => {
ElMessage.error("加载系统配置失败: " + e.message)
})
// eslint-disable-next-line no-undef
const props = defineProps({
theme: String,
});
const theme = computed(() => {
return props.theme
const store = useSharedStore()
const theme = ref(store.adminTheme)
watch(() => store.adminTheme, (val) => {
theme.value = val
})
const items = [
{
icon: 'home',
@@ -90,12 +86,22 @@ const items = [
index: '/admin/user',
title: '用户管理',
},
{
icon: 'menu',
index: '/admin/app',
index: '1',
title: '应用管理',
subs: [
{
index: '/admin/app',
title: '应用列表',
},
{
index: '/admin/app/type',
title: '应用分类',
},
],
},
{
icon: 'api-key',
index: '/admin/apikey',

View File

@@ -38,17 +38,14 @@ import {onBeforeRouteUpdate, useRoute, useRouter} from 'vue-router';
import {ArrowDown, Close} from "@element-plus/icons-vue";
import {checkAdminSession} from "@/store/cache";
import {ElMessageBox} from "element-plus";
import {computed} from "vue";
import {useSharedStore} from "@/store/sharedata";
import {ref, watch} from "vue";
// eslint-disable-next-line no-undef
const props = defineProps({
theme: String,
});
const theme = computed(() => {
return props.theme
const store = useSharedStore()
const theme = ref(store.adminTheme)
watch(() => store.adminTheme, (val) => {
theme.value = val
})
const router = useRouter();
checkAdminSession().catch(() => {
ElMessageBox({

View File

@@ -47,6 +47,7 @@ watch(() => props.value, (newValue) => {
model.value = newValue
})
const model = ref(props.value)
// eslint-disable-next-line no-undef
const emits = defineEmits(['update:value']);
const onInput = (value) => {
emits('update:value',value)

View File

@@ -0,0 +1,79 @@
<template>
<!-- 多项目输入组件 -->
<div class="items-input-box">
<el-tag
v-for="tag in tags"
:key="tag"
closable
:disable-transitions="false"
@close="handleClose(tag)"
>
{{ tag }}
</el-tag>
<el-input
v-if="inputVisible"
ref="InputRef"
v-model="inputValue"
class="w-20"
size="small"
@keyup.enter="handleInputConfirm"
@blur="handleInputConfirm"
/>
<el-button v-else class="button-new-tag" size="small" @click="showInput">
+ 新增
</el-button>
</div>
</template>
<script setup>
import {nextTick, ref, watch} from "vue";
// eslint-disable-next-line no-undef
const props = defineProps({
value : {
type: Array,
default: () => []
},
});
// eslint-disable-next-line no-undef
const emits = defineEmits(['update:value']);
const tags = ref(props.value)
const inputValue = ref('')
const inputVisible = ref(false)
const InputRef = ref(null)
watch(() => props.value, (newValue) => {
tags.value = newValue
})
const handleClose = (tag) => {
tags.value.splice(tags.value.indexOf(tag), 1)
}
const showInput = () => {
inputVisible.value = true
nextTick(() => {
InputRef.value?.input?.focus()
})
}
const handleInputConfirm = () => {
if (inputValue.value) {
tags.value.push(inputValue.value)
}
inputVisible.value = false
inputValue.value = ''
emits('update:value', tags.value)
}
</script>
<style scoped lang="stylus">
.items-input-box {
display flex
.el-tag {
display flex
margin-right 6px
}
}
</style>