Merge branch 'main' of github.com:maxkb-dev/maxkb

v3.2
shaohuzhang1 2023-11-16 13:17:13 +08:00
commit 24eba13fa7
77 changed files with 3499 additions and 425 deletions

4
ui/env.d.ts vendored
View File

@ -1,4 +1,4 @@
/// <reference types="vite/client" />
interface ImportMeta {
readonly env: ImportMetaEnv;
}
readonly env: ImportMetaEnv
}

2
ui/env/.env vendored
View File

@ -1,4 +1,4 @@
VITE_APP_NAME=ui
VITE_BASE_PATH=/ui/
VITE_APP_PORT=3000
VITE_APP_TITLE = '智能知识库'
VITE_APP_TITLE = 'MaxKB'

View File

@ -1,6 +1,6 @@
import { Result } from '@/request/Result'
import { get, post, del, put } from '@/request/index'
import type { datasetListRequest } from '@/api/type/dataset'
import type { datasetListRequest, datasetData } from '@/api/type/dataset'
const prefix = '/dataset'
@ -9,19 +9,22 @@ const prefix = '/dataset'
* @param {
"current_page": "string",
"page_size": "string",
"search_text": "string",
"name": "string",
}
*/
const getDateset: (param: datasetListRequest) => Promise<Result<any[]>> = (param) => {
return get(`${prefix}`, param)
const getDateset: (param: datasetListRequest) => Promise<Result<any>> = (param) => {
return get(
`${prefix}/${param.current_page}/${param.page_size}`,
param.name && { name: param.name }
)
}
/**
*
* @param search_text
* @param name
*/
const getAllDateset: (param?: String) => Promise<Result<any[]>> = (param) => {
return get(`${prefix}`, param && { search_text: param })
const getAllDateset: (param?: string) => Promise<Result<any[]>> = (param) => {
return get(`${prefix}`, param && { name: param })
}
/**
@ -32,9 +35,279 @@ const delDateset: (dataset_id: String) => Promise<Result<boolean>> = (dataset_id
return del(`${prefix}/${dataset_id}`)
}
/**
*
* @param
* {
"name": "string",
"desc": "string",
"documents": [
{
"name": "string",
"paragraphs": [
{
"content": "string",
"title": "string",
"problem_list": [
{
"id": "string",
"content": "string"
}
]
}
]
}
]
}
*/
const postDateset: (data: datasetData) => Promise<Result<any>> = (data) => {
return post(`${prefix}`, data)
}
/**
*
* @param dataset_id
*/
const getDatesetDetail: (dataset_id: string) => Promise<Result<any>> = (dataset_id) => {
return get(`${prefix}/${dataset_id}`)
}
/**
*
* @param
* dataset_id, document_id,
* {
"name": "string",
"desc": true
}
*/
const putDateset: (dataset_id: string, data: any) => Promise<Result<any>> = (
dataset_id,
data: any
) => {
return put(`${prefix}/${dataset_id}`, data)
}
/**
*
* @param file:file,limit:number,patterns:array,with_filter:boolean
*/
const postSplitDocument: (data: any) => Promise<Result<any>> = (data) => {
return post(`${prefix}/document/split`, data)
}
/**
*
* @param dataset_id, name
*/
const getDocument: (dataset_id: string, name?: string) => Promise<Result<any>> = (
dataset_id,
name
) => {
return get(`${prefix}/${dataset_id}/document`, name && { name })
}
/**
*
* @param
* {
"name": "string",
"paragraphs": [
{
"content": "string",
"title": "string",
"problem_list": [
{
"id": "string",
"content": "string"
}
]
}
]
}
*/
const postDocument: (dataset_id: string, data: any) => Promise<Result<any>> = (
dataset_id,
data
) => {
return post(`${prefix}/${dataset_id}/document`, data)
}
/**
*
* @param
* dataset_id, document_id,
* {
"name": "string",
"is_active": true
}
*/
const putDocument: (dataset_id: string, document_id: string, data: any) => Promise<Result<any>> = (
dataset_id,
document_id,
data: any
) => {
return put(`${prefix}/${dataset_id}/document/${document_id}`, data)
}
/**
*
* @param dataset_id, document_id,
*/
const delDocument: (dataset_id: string, document_id: string) => Promise<Result<boolean>> = (
dataset_id,
document_id
) => {
return del(`${prefix}/${dataset_id}/document/${document_id}`)
}
/**
*
* @param dataset_id
*/
const getDocumentDetail: (dataset_id: string, document_id: string) => Promise<Result<any>> = (
dataset_id,
document_id
) => {
return get(`${prefix}/${dataset_id}/document/${document_id}`)
}
/**
*
* @param dataset_id
*/
const getParagraph: (dataset_id: string, document_id: string) => Promise<Result<any>> = (
dataset_id,
document_id
) => {
return get(`${prefix}/${dataset_id}/document/${document_id}/paragraph`)
}
/**
*
* @param dataset_id, document_id, paragraph_id
*/
const delParagraph: (
dataset_id: string,
document_id: string,
paragraph_id: string
) => Promise<Result<boolean>> = (dataset_id, document_id, paragraph_id) => {
return del(`${prefix}/${dataset_id}/document/${document_id}/paragraph/${paragraph_id}`)
}
/**
*
* @param
* dataset_id, document_id
* {
"content": "string",
"title": "string",
"is_active": true,
"problem_list": [
{
"id": "string",
"content": "string"
}
]
}
*/
const postParagraph: (
dataset_id: string,
document_id: string,
data: any
) => Promise<Result<any>> = (dataset_id, document_id, data: any) => {
return post(`${prefix}/${dataset_id}/document/${document_id}/paragraph`, data)
}
/**
*
* @param
* dataset_id, document_id, paragraph_id
* {
"content": "string",
"title": "string",
"is_active": true,
"problem_list": [
{
"id": "string",
"content": "string"
}
]
}
*/
const putParagraph: (
dataset_id: string,
document_id: string,
paragraph_id: string,
data: any
) => Promise<Result<any>> = (dataset_id, document_id, paragraph_id, data: any) => {
return put(`${prefix}/${dataset_id}/document/${document_id}/paragraph/${paragraph_id}`, data)
}
/**
*
* @param dataset_iddocument_idparagraph_id
*/
const getProblem: (dataset_id: string, document_id: string, paragraph_id: string) => Promise<Result<any>> = (
dataset_id,
document_id,
paragraph_id: string,
) => {
return get(`${prefix}/${dataset_id}/document/${document_id}/paragraph/${paragraph_id}/problem`)
}
/**
*
* @param
* dataset_id, document_id, paragraph_id
* {
"id": "string",
content": "string"
}
*/
const postProblem: (
dataset_id: string,
document_id: string,
paragraph_id: string,
data: any
) => Promise<Result<any>> = (dataset_id, document_id, paragraph_id, data: any) => {
return post(
`${prefix}/${dataset_id}/document/${document_id}/paragraph/${paragraph_id}/problem`,
data
)
}
/**
*
* @param dataset_id, document_id, paragraph_id,problem_id
*/
const delProblem: (
dataset_id: string,
document_id: string,
paragraph_id: string,
problem_id: string,
) => Promise<Result<boolean>> = (dataset_id, document_id, paragraph_id,problem_id) => {
return del(`${prefix}/${dataset_id}/document/${document_id}/paragraph/${paragraph_id}/problem/${problem_id}`)
}
export default {
getDateset,
getAllDateset,
delDateset
delDateset,
postDateset,
getDatesetDetail,
putDateset,
postSplitDocument,
getDocument,
postDocument,
putDocument,
delDocument,
getDocumentDetail,
getParagraph,
delParagraph,
putParagraph,
postParagraph,
getProblem,
postProblem,
delProblem
}

View File

@ -56,9 +56,10 @@ const putMemberPermissions: (member_id: String, body: any) => Promise<Result<any
member_id,
body
) => {
return put(`${prefix}/${member_id}`, undefined, body)
return put(`${prefix}/${member_id}`, body)
}
export default {
getTeamMember,
postCreatTeamMember,

View File

@ -1,7 +1,13 @@
interface datasetListRequest {
current_page: number
page_size: number
search_text: string
name: string
}
export type { datasetListRequest }
interface datasetData {
name: String
desc: String
documents?: Array<any>
}
export type { datasetListRequest, datasetData }

View File

@ -0,0 +1,12 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_332_3845)">
<path opacity="0.5" d="M11.5903 10.7326C11.5903 8.64041 13.2864 6.9444 15.3785 6.9444C17.4706 6.9444 19.1667 8.64042 19.1667 10.7326V15.3784C19.1667 17.4705 17.4706 19.1666 15.3785 19.1666C13.2864 19.1666 11.5903 17.4705 11.5903 15.3784V10.7326Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6212 0.833374H14.6211V2.36113H13.1059V0.833374H2.83347C1.72892 0.833374 0.833495 1.72878 0.833474 2.83333L0.833313 11.1665C0.833228 15.5848 4.41497 19.1666 8.83331 19.1666H15.1666C15.5076 19.1666 15.8387 19.1239 16.1547 19.0436C15.2624 18.7368 14.6212 17.8902 14.6212 16.8937V0.833374ZM4.51881 6.35876C4.51881 5.89468 4.89501 5.51848 5.35909 5.51848H10.3457C10.8098 5.51848 11.186 5.89468 11.186 6.35876C11.186 6.82283 10.8098 7.19904 10.3457 7.19904H5.35909C4.89501 7.19904 4.51881 6.82283 4.51881 6.35876ZM5.35909 9.4398C4.89501 9.4398 4.51881 9.816 4.51881 10.2801C4.51881 10.7441 4.89501 11.1204 5.35909 11.1204H10.3457C10.8098 11.1204 11.186 10.7441 11.186 10.2801C11.186 9.816 10.8098 9.4398 10.3457 9.4398H5.35909Z" fill="white"/>
<ellipse cx="13.1058" cy="2.36108" rx="1.51527" ry="1.52776" fill="white"/>
</g>
<defs>
<clipPath id="clip0_332_3845">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,5 @@
<svg width="40" height="42" viewBox="0 0 40 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.66663 5.16667C6.66663 4.24619 7.41282 3.5 8.33329 3.5H24.6548C24.8758 3.5 25.0878 3.5878 25.244 3.74408L33.0892 11.5893C33.2455 11.7455 33.3333 11.9575 33.3333 12.1785V36.8333C33.3333 37.7538 32.5871 38.5 31.6666 38.5H8.33329C7.41282 38.5 6.66663 37.7538 6.66663 36.8333V5.16667Z" fill="#14C0FF"/>
<path d="M10 29.2051V21H12.0513L15.1282 24.0769L18.2051 21H20.2564V29.2051H18.2051V23.9026L15.1282 26.9795L12.0513 23.9026V29.2051H10ZM24.359 21H27.4359V25.1026H30L25.8974 29.7179L21.7949 25.1026H24.359V21Z" fill="white"/>
<path d="M25 3.57495C25.09 3.6159 25.1728 3.67292 25.2441 3.74418L33.0893 11.5894C33.1605 11.6606 33.2175 11.7434 33.2585 11.8334H26.6667C25.7462 11.8334 25 11.0872 25 10.1668V3.57495Z" fill="#11A3D9"/>
</svg>

After

Width:  |  Height:  |  Size: 839 B

View File

@ -0,0 +1,7 @@
<svg width="40" height="42" viewBox="0 0 40 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66699 5.16667C6.66699 4.24619 7.41318 3.5 8.33366 3.5H24.6551C24.8762 3.5 25.0881 3.5878 25.2444 3.74408L33.0896 11.5893C33.2459 11.7455 33.3337 11.9575 33.3337 12.1785V36.8333C33.3337 37.7538 32.5875 38.5 31.667 38.5H8.33366C7.41318 38.5 6.66699 37.7538 6.66699 36.8333V5.16667Z" fill="#3370FF"/>
<path d="M25.0532 21.8466H22.9132V20.5266H28.7632V21.8466H26.6232V27.6666H25.0532V21.8466Z" fill="white"/>
<path d="M18.5533 23.9266L16.2533 20.5266H18.0733L19.4733 22.8066L20.9233 20.5266H22.6433L20.3533 23.9366L22.8433 27.6666H20.9733L19.4133 25.1966L17.8233 27.6666H16.0633L18.5533 23.9266Z" fill="white"/>
<path d="M12.27 21.8466H10.13V20.5266H15.98V21.8466H13.84V27.6666H12.27V21.8466Z" fill="white"/>
<path d="M25 3.57495C25.09 3.6159 25.1728 3.67292 25.2441 3.74418L33.0893 11.5894C33.1605 11.6606 33.2175 11.7434 33.2585 11.8334H26.6667C25.7462 11.8334 25 11.0872 25 10.1668V3.57495Z" fill="#2B5FD9"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,6 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M33.0404 11.3738C33.2279 11.5614 33.3333 11.8157 33.3333 12.0809V34.8149C33.3333 35.8376 32.5374 36.6667 31.5555 36.6667H8.4444C7.46256 36.6667 6.66663 35.8376 6.66663 34.8149V5.18523C6.66663 4.16248 7.46256 3.33337 8.4444 3.33337H24.5857C24.851 3.33337 25.1053 3.43873 25.2929 3.62627L33.0404 11.3738Z" fill="#3370FF"/>
<path d="M20.6509 15.8135C20.3173 15.3965 19.683 15.3965 19.3494 15.8135L14.4166 21.9795C13.9801 22.5251 14.3686 23.3334 15.0673 23.3334H18.3335V26.6667H21.6668V23.3334H24.9329C25.6317 23.3334 26.0202 22.5251 25.5837 21.9795L20.6509 15.8135Z" fill="white"/>
<path d="M18.3335 30.0001V28.3334H21.6668V30.0001H18.3335Z" fill="white"/>
<path d="M25 3.33337L33.3333 11.6667H26.6667C25.7462 11.6667 25 10.9205 25 10V3.33337Z" fill="#2B5FD9"/>
</svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@ -0,0 +1,14 @@
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 7.33333H4C2.15905 7.33333 0 8.62098 0 10.9333V12.7333C0 13.0647 0.298477 13.3333 0.666667 13.3333H11.3333C11.7015 13.3333 12 13.0647 12 12.7333V10.9333C12 8.61904 9.84095 7.33333 8 7.33333Z" fill="url(#paint0_linear_264_32130)"/>
<path d="M2.66667 3.33333C2.66667 5.17428 4.15905 6.66667 6 6.66667C7.84095 6.66667 9.33333 5.17428 9.33333 3.33333C9.33333 1.49238 7.84095 0 6 0C4.15905 0 2.66667 1.49238 2.66667 3.33333Z" fill="url(#paint1_linear_264_32130)"/>
<defs>
<linearGradient id="paint0_linear_264_32130" x1="6" y1="-1.34111e-08" x2="6" y2="13.6667" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_264_32130" x1="6" y1="-1.34111e-08" x2="6" y2="13.6667" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,105 @@
<template>
<div class="app-table" :class="quickCreate ? 'table-quick-append' : ''">
<el-table v-bind="$attrs">
<template #append v-if="quickCreate">
<div v-if="showInput">
<el-input
ref="quickInputRef"
v-model="inputValue"
placeholder="请输入文档名称"
class="w-240 mr-12"
autofocus
/>
<el-button type="primary" @click="submitHandle" :disabled="loading">创建</el-button>
<el-button @click="showInput = false" :disabled="loading">取消</el-button>
</div>
<div v-else @click="quickCreateHandel" class="w-full">
<el-button type="primary" link>
<el-icon><Plus /></el-icon>
<span class="ml-4">快速创建空白文档</span>
</el-button>
</div>
</template>
<slot></slot>
</el-table>
<div class="app-table__pagination mt-16" v-if="$slots.pagination || paginationConfig">
<slot name="pagination">
<el-pagination
v-model:current-page="paginationConfig.currentPage"
v-model:page-size="paginationConfig.pageSize"
:page-sizes="pageSizes"
:total="paginationConfig.total"
layout="total, prev, pager, next, sizes"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</slot>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, watch, computed } from 'vue'
defineOptions({ name: 'AppTable' })
const props = defineProps({
paginationConfig: {
type: Object,
default: () => {}
},
quickCreate: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['changePage', 'sizeChange', 'creatQuick'])
const paginationConfig = computed(() => props.paginationConfig)
const pageSizes = [10, 20, 50, 100]
const quickInputRef = ref()
const loading = ref(false)
const showInput = ref(false)
const inputValue = ref('')
watch(showInput, (bool) => {
if (!bool) {
inputValue.value = ''
}
})
function submitHandle() {
loading.value = true
emit('creatQuick', inputValue.value)
setTimeout(() => {
showInput.value = false
loading.value = false
}, 200)
}
function quickCreateHandel() {
showInput.value = true
nextTick(() => {
quickInputRef.value?.focus()
})
}
function handleSizeChange() {
emit('sizeChange')
}
function handleCurrentChange() {
emit('changePage')
}
defineExpose({})
</script>
<style lang="scss" scoped>
.app-table {
&__pagination {
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@ -0,0 +1,29 @@
<template>
<el-icon class="back-button cursor mr-8" @click="jump">
<Back />
</el-icon>
</template>
<script setup lang="ts">
import { useRouter, type RouteLocationRaw } from 'vue-router'
defineOptions({ name: 'BackButton' })
const router = useRouter()
const props = defineProps({
to: String
})
const back: any = router.options.history.state.back //
function jump() {
if (props.to === '-1') {
back ? router.push(back) : router.go(-1)
} else {
router.push(props.to as RouteLocationRaw)
}
}
</script>
<style lang="scss">
.back-button {
font-size:20px;
}
</style>

View File

@ -1,8 +1,8 @@
<template>
<el-card shadow="hover">
<div class="card-add">
<AppIcon :iconName="icon" class="add-icon" />
<span class="ml-10">{{ title }}</span>
<el-card shadow="never" class="card-add">
<div class="flex-center">
<AppIcon iconName="Plus" class="add-icon p-8" />
<span>{{ title }}</span>
</div>
</el-card>
</template>
@ -12,27 +12,35 @@ defineProps({
title: {
type: String,
default: '标题'
},
icon: {
type: String,
default: 'CirclePlusFilled'
}
})
</script>
<style lang="scss" scoped>
.card-add {
width: 100%;
min-height: 110px;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 15px;
font-size: 16px;
cursor: pointer;
min-height: var(--card-min-height);
border: 1px dashed var(--el-color-primary);
background: #eff0f1;
.add-icon {
font-size: 14px;
border-radius: 4px;
border: 1px solid var(--app-border-color-dark);
background: var(--app-layout-bg-color);
margin-right: 12px;
}
&:hover {
color: var(--el-color-primary);
}
.add-icon {
font-size: 16px;
background: #ffffff;
.add-icon {
background: #ffffff;
border-color: var(--el-color-primary);
}
}
}
</style>

View File

@ -1,16 +1,16 @@
<template>
<el-card shadow="hover" class="card-box" @mouseenter="cardEnter()" @mouseleave="cardLeave()">
<el-card shadow="always" class="card-box" @mouseenter="cardEnter()" @mouseleave="cardLeave()">
<div class="card-header">
<slot name="header">
<div class="title flex align-center">
<AppAvatar class="mr-10">
<el-icon><Document /></el-icon>
<AppAvatar class="mr-12" shape="square" :size="32" v-if="showIcon">
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
<h4>{{ title }}</h4>
</div>
</slot>
</div>
<div class="description mt-10">
<div class="description mt-12">
<slot name="description">
{{ description }}
</slot>
@ -33,6 +33,10 @@ const props = defineProps({
description: {
type: String,
default: ''
},
showIcon: {
type: Boolean,
default: true
}
})
@ -48,7 +52,7 @@ function cardLeave() {
.card-box {
font-size: 14px;
position: relative;
min-height: 150px;
min-height: var(--card-min-height);
.description {
display: -webkit-box;
@ -56,11 +60,20 @@ function cardLeave() {
-webkit-line-clamp: 2;
overflow: hidden;
height: 40px;
color: var(--app-text-color-secondary);
line-height: 22px;
font-weight: 400;
}
.card-footer {
position: absolute;
bottom: 0;
bottom: 8px;
left: 0;
min-height: 30px;
color: var(--app-text-color-secondary);
font-weight: 400;
padding: 0 16px;
width: 100%;
box-sizing: border-box;
}
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<div class="common-list">
<el-scrollbar>
<ul v-if="data.length > 0">
<template v-for="(item, index) in data" :key="index">
<li
@click.prevent="clickHandle(item, index)"
:class="current === index ? 'active' : ''"
class="cursor"
>
<slot :row="item" :index="index"> </slot>
</li>
</template>
</ul>
<el-empty description="暂无数据" v-else />
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
defineOptions({ name: 'CommonList' })
const props = defineProps({
data: {
type: Array<any>,
default: () => []
}
})
const emit = defineEmits(['click'])
const current = ref(0)
function clickHandle(row: any, index: number) {
current.value = index
emit('click', row)
}
</script>
<style lang="scss" scoped>
// ui li
.common-list {
li {
padding: 11px 16px;
&.active {
background: var(--el-color-primary-light-9);
border-radius: 4px;
color: var(--el-color-primary);
}
}
}
</style>

View File

@ -1,42 +0,0 @@
<template>
<div class="content-container">
<div class="content-container__header mb-10" v-if="slots.header || header">
<slot name="header">
<span>{{ header }}</span>
</slot>
</div>
<el-scrollbar>
<div class="content-container__main main-calc-height">
<slot></slot>
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { useSlots } from 'vue'
defineOptions({ name: 'LayoutContent' })
const slots = useSlots()
defineProps({
header: String
})
</script>
<style lang="scss" scope>
.content-container {
transition: 0.3s;
padding: var(--app-view-padding);
.content-container__header {
font-weight: 600;
font-size: 18px;
box-sizing: border-box;
}
.content-container__main {
background-color: var(--app-view-bg-color);
border-radius: 6px;
box-sizing: border-box;
// overflow: auto;
// height: 100%;
}
}
</style>

View File

@ -34,6 +34,52 @@ export const iconMap: any = {
}
},
'app-user': {
iconReader: () => {
return h('i', [
h(
'svg',
{
viewBox: '0 0 24 24',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M14 12.3333H10C8.15905 12.3333 6 13.621 6 15.9333V17.7333C6 18.0647 6.29848 18.3333 6.66667 18.3333H17.3333C17.7015 18.3333 18 18.0647 18 17.7333V15.9333C18 13.619 15.841 12.3333 14 12.3333Z',
fill: 'currentColor'
}),
h('path', {
d: 'M8.66667 8.33333C8.66667 10.1743 10.1591 11.6667 12 11.6667C13.8409 11.6667 15.3333 10.1743 15.3333 8.33333C15.3333 6.49238 13.8409 5 12 5C10.1591 5 8.66667 6.49238 8.66667 8.33333Z',
fill: 'currentColor'
})
]
)
])
}
},
'app-add-users': {
iconReader: () => {
return h('i', [
h(
'svg',
{
viewBox: '0 0 20 20',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M6.24984 5.41667C6.24984 6.7975 7.37067 7.91667 8.74984 7.91667C10.129 7.91667 11.2498 6.7975 11.2498 5.41667C11.2498 4.03583 10.129 2.91667 8.74984 2.91667C7.37067 2.91667 6.24984 4.03583 6.24984 5.41667ZM8.74984 1.25C11.0498 1.25 12.9165 3.11542 12.9165 5.41667C12.9165 7.71792 11.0498 9.58333 8.74984 9.58333C6.44984 9.58333 4.58317 7.71792 4.58317 5.41667C4.58317 3.11542 6.44984 1.25 8.74984 1.25ZM3.43734 15C3.37067 15.2663 3.33317 15.5454 3.33317 15.8333V16.6667H10.854C11.0841 16.6667 11.2706 16.8532 11.2706 17.0833V17.9167C11.2706 18.1468 11.0841 18.3333 10.854 18.3333H2.49984C2.0415 18.3333 1.6665 17.9604 1.6665 17.5V15.8333C1.6665 13.0721 3.904 10.8333 6.6665 10.8333H10.854C11.0841 10.8333 11.2706 11.0199 11.2706 11.25V12.0833C11.2706 12.3135 11.0841 12.5 10.854 12.5H6.6665C5.11234 12.5 3.80817 13.5625 3.43734 15ZM15.4165 11.6667C15.6466 11.6667 15.8332 11.8532 15.8332 12.0833V14.1667H17.9165C18.1466 14.1667 18.3332 14.3532 18.3332 14.5833V15.4167C18.3332 15.6468 18.1466 15.8333 17.9165 15.8333H15.8332V17.9167C15.8332 18.1468 15.6466 18.3333 15.4165 18.3333H14.5832C14.3531 18.3333 14.1665 18.1468 14.1665 17.9167V15.8333H12.0832C11.8531 15.8333 11.6665 15.6468 11.6665 15.4167V14.5833C11.6665 14.3532 11.8531 14.1667 12.0832 14.1667H14.1665V12.0833C14.1665 11.8532 14.3531 11.6667 14.5832 11.6667H15.4165Z',
fill: 'currentColor'
})
]
)
])
}
},
'app-dataset': {
iconReader: () => {
return h('i', [
@ -95,39 +141,32 @@ export const iconMap: any = {
])
}
},
'app-team': {
iconReader: () => {
return h('i', [
h(
'svg',
{
viewBox: '0 0 1024 1024',
viewBox: '0 0 20 20',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M 824.2 699.9 c -25.4 -25.4 -54.7 -45.7 -86.4 -60.4 C 783.1 602.8 812 546.8 812 484 c 0 -110.8 -92.4 -201.7 -203.2 -200 c -109.1 1.7 -197 90.6 -197 200 c 0 62.8 29 118.8 74.2 155.5 c -31.7 14.7 -60.9 34.9 -86.4 60.4 C 345 754.6 314 826.8 312 903.8 c -0.1 4.5 3.5 8.2 8 8.2 h 56 c 4.3 0 7.9 -3.4 8 -7.7 c 1.9 -58 25.4 -112.3 66.7 -153.5 C 493.8 707.7 551.1 684 612 684 c 60.9 0 118.2 23.7 161.3 66.8 C 814.5 792 838 846.3 840 904.3 c 0.1 4.3 3.7 7.7 8 7.7 h 56 c 4.5 0 8.1 -3.7 8 -8.2 c -2 -77 -33 -149.2 -87.8 -203.9 Z M 612 612 c -34.2 0 -66.4 -13.3 -90.5 -37.5 c -24.5 -24.5 -37.9 -57.1 -37.5 -91.8 c 0.3 -32.8 13.4 -64.5 36.3 -88 c 24 -24.6 56.1 -38.3 90.4 -38.7 c 33.9 -0.3 66.8 12.9 91 36.6 c 24.8 24.3 38.4 56.8 38.4 91.4 c 0 34.2 -13.3 66.3 -37.5 90.5 c -24.2 24.2 -56.4 37.5 -90.6 37.5 Z M 361.5 510.4 c -0.9 -8.7 -1.4 -17.5 -1.4 -26.4 c 0 -15.9 1.5 -31.4 4.3 -46.5 c 0.7 -3.6 -1.2 -7.3 -4.5 -8.8 c -13.6 -6.1 -26.1 -14.5 -36.9 -25.1 c -25.8 -25.2 -39.7 -59.3 -38.7 -95.4 c 0.9 -32.1 13.8 -62.6 36.3 -85.6 c 24.7 -25.3 57.9 -39.1 93.2 -38.7 c 31.9 0.3 62.7 12.6 86 34.4 c 7.9 7.4 14.7 15.6 20.4 24.4 c 2 3.1 5.9 4.4 9.3 3.2 c 17.6 -6.1 36.2 -10.4 55.3 -12.4 c 5.6 -0.6 8.8 -6.6 6.3 -11.6 c -32.5 -64.3 -98.9 -108.7 -175.7 -109.9 c -110.9 -1.7 -203.3 89.2 -203.3 199.9 c 0 62.8 28.9 118.8 74.2 155.5 c -31.8 14.7 -61.1 35 -86.5 60.4 c -54.8 54.7 -85.8 126.9 -87.8 204 c -0.1 4.5 3.5 8.2 8 8.2 h 56.1 c 4.3 0 7.9 -3.4 8 -7.7 c 1.9 -58 25.4 -112.3 66.7 -153.5 c 29.4 -29.4 65.4 -49.8 104.7 -59.7 c 3.9 -1 6.5 -4.7 6 -8.7 Z',
d: 'M7.08317 10C9.15424 10 10.8332 8.32107 10.8332 6.25C10.8332 4.17893 9.15424 2.5 7.08317 2.5C5.0121 2.5 3.33317 4.17893 3.33317 6.25C3.33317 8.32107 5.0121 10 7.08317 10Z',
fill: 'currentColor'
})
]
)
])
}
},
'app-add-users': {
iconReader: () => {
return h('i', [
h(
'svg',
{
viewBox: '0 0 1024 1024',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
}),
h('path', {
d: 'M 892 772 h -80 v -80 c 0 -4.4 -3.6 -8 -8 -8 h -48 c -4.4 0 -8 3.6 -8 8 v 80 h -80 c -4.4 0 -8 3.6 -8 8 v 48 c 0 4.4 3.6 8 8 8 h 80 v 80 c 0 4.4 3.6 8 8 8 h 48 c 4.4 0 8 -3.6 8 -8 v -80 h 80 c 4.4 0 8 -3.6 8 -8 v -48 c 0 -4.4 -3.6 -8 -8 -8 Z M 373.5 498.4 c -0.9 -8.7 -1.4 -17.5 -1.4 -26.4 c 0 -15.9 1.5 -31.4 4.3 -46.5 c 0.7 -3.6 -1.2 -7.3 -4.5 -8.8 c -13.6 -6.1 -26.1 -14.5 -36.9 -25.1 c -25.8 -25.2 -39.7 -59.3 -38.7 -95.4 c 0.9 -32.1 13.8 -62.6 36.3 -85.6 c 24.7 -25.3 57.9 -39.1 93.2 -38.7 c 31.9 0.3 62.7 12.6 86 34.4 c 7.9 7.4 14.7 15.6 20.4 24.4 c 2 3.1 5.9 4.4 9.3 3.2 c 17.6 -6.1 36.2 -10.4 55.3 -12.4 c 5.6 -0.6 8.8 -6.6 6.3 -11.6 c -32.5 -64.3 -98.9 -108.7 -175.7 -109.9 c -110.8 -1.7 -203.2 89.2 -203.2 200 c 0 62.8 28.9 118.8 74.2 155.5 c -31.8 14.7 -61.1 35 -86.5 60.4 c -54.8 54.7 -85.8 126.9 -87.8 204 c -0.1 4.5 3.5 8.2 8 8.2 h 56.1 c 4.3 0 7.9 -3.4 8 -7.7 c 1.9 -58 25.4 -112.3 66.7 -153.5 c 29.4 -29.4 65.4 -49.8 104.7 -59.7 c 3.8 -1.1 6.4 -4.8 5.9 -8.8 Z M 824 472 c 0 -109.4 -87.9 -198.3 -196.9 -200 C 516.3 270.3 424 361.2 424 472 c 0 62.8 29 118.8 74.2 155.5 c -31.7 14.7 -60.9 34.9 -86.4 60.4 C 357 742.6 326 814.8 324 891.8 c -0.1 4.5 3.5 8.2 8 8.2 h 56 c 4.3 0 7.9 -3.4 8 -7.7 c 1.9 -58 25.4 -112.3 66.7 -153.5 C 505.8 695.7 563 672 624 672 c 110.4 0 200 -89.5 200 -200 Z m -109.5 90.5 C 690.3 586.7 658.2 600 624 600 s -66.3 -13.3 -90.5 -37.5 C 509 538 495.7 505.4 496 470.7 c 0.3 -32.8 13.4 -64.5 36.3 -88 c 24 -24.6 56.1 -38.3 90.4 -38.7 c 33.9 -0.3 66.8 12.9 91 36.6 c 24.8 24.3 38.4 56.8 38.4 91.4 c -0.1 34.2 -13.4 66.3 -37.6 90.5 Z',
d: 'M1.24984 18.3333C0.7896 18.3333 0.416504 17.9602 0.416504 17.5V15.8889C0.416504 13.0968 2.76035 10.8333 5.47333 10.8333H8.70065C11.4136 10.8333 13.7498 13.0968 13.7498 15.8889V17.5C13.7498 17.9602 13.3767 18.3333 12.9165 18.3333H1.24984Z',
fill: 'currentColor'
}),
h('path', {
d: 'M15.4165 17.5V17.2535C15.4165 15.3267 15.4165 13.3333 13.7498 12.0833C13.8196 12.0773 13.9366 12.0794 14.0491 12.0814C14.1036 12.0824 14.157 12.0833 14.2034 12.0833H15.8332C17.8679 12.0833 19.5832 13.3643 19.5832 15.4583V16.875C19.5832 17.2202 19.3033 17.5 18.9582 17.5H15.4165Z',
fill: 'currentColor'
}),
h('path', {
d: 'M14.5832 10.8333C15.9639 10.8333 17.0832 9.71405 17.0832 8.33333C17.0832 6.95262 15.9639 5.83333 14.5832 5.83333C13.2025 5.83333 12.0832 6.95262 12.0832 8.33333C12.0832 9.71405 13.2025 10.8333 14.5832 10.8333Z',
fill: 'currentColor'
})
]

View File

@ -3,12 +3,15 @@ import AppIcon from './icons/AppIcon.vue'
import AppAvatar from './app-avatar/index.vue'
import LoginLayout from './login-layout/index.vue'
import LoginContainer from './login-container/index.vue'
import LayoutContent from './content-container/LayoutContent.vue'
import LayoutContainer from './layout-container/index.vue'
import TagsInput from './tags-input/index.vue'
import CardBox from './card-box/index.vue'
import CardAdd from './card-add/index.vue'
import BackButton from './back-button/index.vue'
import AppTable from './app-table/index.vue'
import ReadWrite from './read-write/index.vue'
import TagEllipsis from './tag-ellipsis/index.vue'
import CommonList from './common-list/index.vue'
export default {
install(app: App) {
@ -16,9 +19,14 @@ export default {
app.component(AppAvatar.name, AppAvatar)
app.component(LoginLayout.name, LoginLayout)
app.component(LoginContainer.name, LoginContainer)
app.component(LayoutContent.name, LayoutContent)
app.component(LayoutContainer.name, LayoutContainer)
app.component(TagsInput.name, TagsInput)
app.component(CardBox.name, CardBox)
app.component(CardAdd.name, CardAdd)
app.component(BackButton.name, BackButton)
app.component(AppTable.name, AppTable)
app.component(ReadWrite.name, ReadWrite)
app.component(TagEllipsis.name, TagEllipsis)
app.component(CommonList.name, CommonList)
}
}

View File

@ -0,0 +1,44 @@
<template>
<div class="content-container">
<div class="content-container__header flex align-center" v-if="slots.header || header">
<back-button :to="backTo" v-if="showBack"></back-button>
<h3>{{ header }}</h3>
<slot name="header"> </slot>
</div>
<div class="content-container__main">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, useSlots } from 'vue'
defineOptions({ name: 'LayoutContainer' })
const slots = useSlots()
const props = defineProps({
header: String || null,
backTo: String
})
const showBack = computed(() => {
const { backTo } = props
return backTo
})
</script>
<style lang="scss" scope>
.content-container {
transition: 0.3s;
padding: 0 var(--app-view-padding) var(--app-view-padding);
.content-container__header {
box-sizing: border-box;
padding: 16px 0;
flex-wrap: wrap;
}
.content-container__main {
background-color: var(--app-view-bg-color);
border-radius: 4px;
box-sizing: border-box;
}
}
</style>

View File

@ -3,7 +3,7 @@
<div class="login-title">
<div class="title flex-center">
<div class="logo"></div>
<div>{{ title || defaultTitle }}</div>
<div class="app-logo-font">{{ title || defaultTitle }}</div>
</div>
<div class="sub-title" v-if="subTitle">{{ subTitle }}</div>
</div>
@ -27,8 +27,6 @@ defineProps({
.title {
font-size: 28px;
font-weight: 900;
color: #101010;
height: 60px;
.logo {
background-image: url('@/assets/logo.png');

View File

@ -0,0 +1,75 @@
<template>
<div class="cursor">
<slot name="read">
<div class="flex align-center" v-if="!isEdit">
<span>{{ data }}</span>
<el-button @click.stop="editNameHandle" text v-if="showEditIcon">
<el-icon><Edit /></el-icon>
</el-button>
</div>
</slot>
<slot>
<div class="flex align-center" v-if="isEdit">
<div @click.stop>
<el-input ref="inputRef" v-model="writeValue" placeholder="请输入" autofocus></el-input>
</div>
<span class="ml-4">
<el-button type="primary" text @click.stop="submit" :disabled="loading">
<el-icon><Select /></el-icon>
</el-button>
</span>
<span>
<el-button text @click.stop="isEdit = false" :disabled="loading">
<el-icon><CloseBold /></el-icon>
</el-button>
</span>
</div>
</slot>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, nextTick } from 'vue'
defineOptions({ name: 'ReadWrite' })
const props = defineProps({
data: {
type: String,
default: ''
},
showEditIcon: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['change'])
const inputRef = ref()
const isEdit = ref(false)
const writeValue = ref('')
const loading = ref(false)
watch(isEdit, (bool) => {
if (!bool) {
writeValue.value = ''
}
})
function submit() {
loading.value = true
emit('change', writeValue.value)
setTimeout(() => {
isEdit.value = false
loading.value = false
}, 200)
}
function editNameHandle(row: any) {
writeValue.value = props.data
isEdit.value = true
}
onMounted(() => {
nextTick(() => {
inputRef.value?.focus()
})
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,55 @@
<template>
<el-tag class="tag-ellipsis flex-between mb-8" effect="plain" v-bind="$attrs">
<el-tooltip
:disabled="!isShowTooltip"
effect="dark"
:content="tooltipContent"
placement="bottom"
>
<div ref="tagLabel">
<slot></slot>
</div>
</el-tooltip>
</el-tag>
</template>
<script setup lang="ts">
import { ref, computed, useSlots } from 'vue'
defineOptions({ name: 'TagEllipsis' })
const slots = useSlots()
const tooltipContent = slots.default()?.[0].children || ''
const tagLabel = ref()
const isShowTooltip = computed(() => {
const containerWeight = tagLabel.value?.scrollWidth
const contentWeight = tagLabel.value?.clientWidth
if (containerWeight > contentWeight) {
// >
return true
} else {
//
return false
}
})
</script>
<style lang="scss" scoped>
// tag
.tag-ellipsis {
border: 1px solid var(--el-border-color);
color: var(--app-text-color);
border-radius: 4px;
height: 30px;
line-height: 30px;
padding: 0 9px;
box-sizing: border-box;
:deep(.el-tag__content) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
:deep(.el-tooltip__trigger) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
</style>

View File

@ -8,7 +8,7 @@
:key="index"
@close="removeTag(item)"
closable
class="mr-10"
class="mr-8"
>{{ item }}
</el-tag>
</div>

View File

@ -20,8 +20,9 @@ import { TopBar, AppMain } from '../components'
.app-main {
height: calc(100vh - var(--app-header-height));
padding: 0 !important;
box-sizing: border-box;
}
.app-header {
background-color: var(--app-header-bg-color);
background: var(--app-header-bg-color);
}
</style>

View File

@ -13,11 +13,11 @@ import { ref, onBeforeUpdate } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const cachedViews: any = ref([])
const cachedViews: any = ref([])
onBeforeUpdate(() => {
let isCached = route.meta?.cache
let name = route.name
const { name, meta } = route
let isCached = meta?.cache
if (isCached && name && !cachedViews.value.includes(name)) {
cachedViews.value.push(name)
}

View File

@ -2,7 +2,11 @@
<div v-if="!menu.meta || !menu.meta.hidden" class="sidebar-item">
<el-menu-item ref="subMenu" :index="menu.path" popper-class="sidebar-popper">
<template #title>
<AppIcon v-if="menu.meta && menu.meta.icon" :iconName="menu.meta.icon" />
<AppIcon
v-if="menu.meta && menu.meta.icon"
:iconName="menu.meta.icon"
class="sidebar-icon"
/>
<span v-if="menu.meta && menu.meta.title">{{ menu.meta.title }}</span>
</template>
</el-menu-item>
@ -19,9 +23,18 @@ defineProps<{
<style scoped lang="scss">
.sidebar-item {
.sidebar-icon {
font-size: 20px;
margin-top: -2px;
}
.el-menu-item {
padding-left: 30px !important;
padding: 13px 12px 13px 16px !important;
font-weight: 500;
border-radius: 4px;
&:hover {
background: none;
color: var(--el-menu-active-color);
}
}
.el-menu-item.is-active {

View File

@ -1,12 +1,12 @@
<template>
<div class="sidebar">
<div class="sidebar p-8">
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu :default-active="activeMenu">
<el-menu :default-active="activeMenu" router>
<sidebar-item
:menu="menu"
v-hasPermission="menu.meta?.permission"
v-for="(menu, index) in subMenuList"
:key="index"
:menu="menu"
>
</sidebar-item>
</el-menu>
@ -21,16 +21,15 @@ import { getChildRouteListByPathAndName } from '@/router/index'
import SidebarItem from './SidebarItem.vue'
const route = useRoute()
const subMenuList = computed(() => {
return getChildRouteListByPathAndName(route.path, route.name)
const { meta } = route
return getChildRouteListByPathAndName(meta.parentPath, meta.parentName)
})
const activeMenu = computed(() => {
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
const { path, meta } = route
return meta.active || path
})
</script>

View File

@ -38,7 +38,6 @@
class="input-item"
:disabled="true"
v-bind:modelValue="user.userInfo?.email"
@change="() => {}"
placeholder="请输入邮箱"
>
<template #prepend>
@ -60,7 +59,7 @@
</el-input>
<el-button
size="large"
class="send-email-button ml-10"
class="send-email-button ml-16"
@click="sendEmail"
:loading="loading"
>获取验证码</el-button

View File

@ -1,6 +1,15 @@
<template>
<el-dropdown trigger="click" type="primary">
<AppAvatar :name="user.userInfo?.username" />
<div class="flex-center cursor">
<AppAvatar>
<img src="@/assets/user-icon.svg" style="width: 54%" alt="" />
</AppAvatar>
<span class="ml-8">{{ user.userInfo?.username }}</span>
<el-icon class="el-icon--right">
<CaretBottom />
</el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openResetPassword">

View File

@ -1,11 +1,10 @@
<template>
<div class="top-bar-container flex-between border-b">
<div class="top-bar-container border-b flex-between">
<div class="flex-center h-full">
<div class="app-title-container flex-center">
<div class="app-title-icon"></div>
<div class="app-title-text ml-10">{{ defaultTitle }}</div>
<div class="app-title-text app-logo-font">{{ defaultTitle }}</div>
</div>
<el-divider direction="vertical" class="line" />
<TopMenu></TopMenu>
</div>
<div class="avatar">
@ -25,18 +24,16 @@ const defaultTitle = import.meta.env.VITE_APP_TITLE
padding: var(--app-header-padding);
.app-title-container {
margin-right: 20px;
margin-right: 45px;
.app-title-icon {
background-image: url('@/assets/logo.png');
background-size: 100% 100%;
width: 40px;
height: 40px;
width: 34px;
height: 34px;
}
.app-title-text {
color: var(--el-color-primary);
font-size: 20px;
font-weight: 600;
font-size: 24px;
}
}
.line {

View File

@ -4,9 +4,9 @@
:class="isActive ? 'active' : ''"
@click="router.push({ name: menu.name })"
>
<div class="icon">
<!-- <div class="icon">
<AppIcon :iconName="menu.meta ? (menu.meta.icon as string) : '404'" />
</div>
</div> -->
<div class="title">{{ menu.meta?.title }}</div>
</div>
</template>
@ -15,21 +15,22 @@ import { useRouter, useRoute, type RouteRecordRaw } from 'vue-router'
import { computed } from 'vue'
const router = useRouter()
const route = useRoute()
const props = defineProps<{
menu: RouteRecordRaw
}>()
const isActive = computed(() => {
return (
(route.name == props.menu.name && route.path == props.menu.path) ||
route?.meta?.activeMenu == props.menu.path
)
const { name, path, meta } = route
return (name == props.menu.name && path == props.menu.path) || meta?.activeMenu == props.menu.path
})
</script>
<style lang="scss" scoped>
.menu-item-container {
padding: 0 20px;
margin-right: 28px;
cursor: pointer;
font-size: 16px;
position: relative;
.icon {
font-size: 15px;
margin-right: 5px;
@ -41,9 +42,15 @@ const isActive = computed(() => {
}
.active {
font-weight: 600;
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border-bottom: 3px solid var(--el-color-primary);
&::after {
position: absolute;
bottom: 0;
width: 100%;
height: 2px;
content: '';
background-color: var(--el-color-primary-light-9);
border-bottom: 3px solid var(--el-color-primary);
}
}
</style>

View File

@ -19,7 +19,5 @@ const topMenuList = computed(() => {
})
</script>
<style lang="scss" scope>
.top-menu-container {
margin-bottom: -1px;
}
</style>

View File

@ -1,6 +1,8 @@
<template>
<div class="main-layout h-full flex">
<div class="sidebar-container border-r"><Sidebar /></div>
<div class="sidebar-container">
<Sidebar />
</div>
<div class="view-container">
<AppMain />
</div>
@ -19,6 +21,6 @@ import { Sidebar, AppMain } from '../components'
background-color: var(--sidebar-bg-color);
}
.view-container {
width: 100%;
width: calc(100% - var(--sidebar-width));
}
</style>

View File

@ -149,8 +149,8 @@ export const put: (
params?: unknown,
data?: unknown,
loading?: NProgress | Ref<boolean>
) => Promise<Result<any>> = (url, params, data, loading) => {
return promise(request({ url: url, method: 'put', params, data }), loading)
) => Promise<Result<any>> = (url, data, params, loading) => {
return promise(request({ url: url, method: 'put', data, params }), loading)
}
/**

View File

@ -47,7 +47,7 @@ router.beforeEach(
}
)
export const getChildRouteListByPathAndName = (path: string, name?: RouteRecordName | null | undefined) => {
export const getChildRouteListByPathAndName = (path: any, name?: RouteRecordName | any) => {
return getChildRouteList(routes, path, name)
}

View File

@ -1,8 +1,68 @@
import Layout from '@/layout/main-layout/index.vue'
const applicationRouter = {
path: '/app',
name: 'app',
path: '/application',
name: 'application',
meta: { icon: 'app-applicaiton', title: '应用', permission: 'APPLICATION:READ' },
component: () => import('@/views/app/index.vue')
redirect: '/application',
children: [
{
path: '/application',
name: 'application',
component: () => import('@/views/application/index.vue')
},
{
path: '/application/create', // create
name: 'CreateApplication',
meta: { activeMenu: '/application' },
component: () => import('@/views/application/CreateApplication.vue'),
hidden: true
},
{
path: '/application/:appId',
name: 'ApplicationDetail',
meta: { title: '应用详情', activeMenu: '/application' },
component: Layout,
hidden: true,
children: [
{
path: 'overview',
name: 'AppOverview',
meta: {
icon: 'Document',
title: '概览',
active: 'overview',
parentPath: '/application/:appId',
parentName: 'ApplicationDetail'
},
component: () => import('@/views/application/AppOverview.vue')
},
{
path: 'setting',
name: 'AppSetting',
meta: {
icon: 'Setting',
title: '设置',
active: 'setting',
parentPath: '/application/:appId',
parentName: 'ApplicationDetail'
},
component: () => import('@/views/application/AppSetting.vue')
},
{
path: 'dialog',
name: 'DialogLog',
meta: {
icon: 'Setting',
title: '对话日志',
active: 'dialog',
parentPath: '/application/:appId',
parentName: 'ApplicationDetail'
},
component: () => import('@/views/application/DialogLog.vue')
}
]
},
]
}
export default applicationRouter

View File

@ -11,27 +11,51 @@ const datasetRouter = {
component: () => import('@/views/dataset/index.vue')
},
{
path: '/dataset/create',
path: '/dataset/:type', // create 或者 upload
name: 'CreateDataset',
meta: { activeMenu: '/dataset' },
component: () => import('@/views/dataset/CreateDataset.vue'),
hidden: true
},
{
path: '/dataset/doc',
name: 'DatasetDoc',
meta: { icon: 'House', title: '文档', activeMenu: '/dataset' },
path: '/dataset/:datasetId',
name: 'DatasetDetail',
meta: { title: '文档', activeMenu: '/dataset' },
component: Layout,
hidden: true,
redirect: '/dataset/doc',
children: [
{
path: '/dataset/doc',
name: 'DatasetDoc',
meta: { icon: 'House', title: '文档' },
component: () => import('@/views/dataset/DatasetDoc.vue')
path: 'document',
name: 'Document',
meta: {
icon: 'Document',
title: '文档',
active: 'document',
parentPath: '/dataset/:datasetId',
parentName: 'DatasetDetail'
},
component: () => import('@/views/document/index.vue')
},
{
path: 'setting',
name: 'DatasetSetting',
meta: {
icon: 'Setting',
title: '设置',
active: 'setting',
parentPath: '/dataset/:datasetId',
parentName: 'DatasetDetail'
},
component: () => import('@/views/document/DatasetSetting.vue')
}
]
},
{
path: '/dataset/:datasetId/:documentId', // 分段详情
name: 'Paragraph',
meta: { activeMenu: '/dataset' },
component: () => import('@/views/paragraph/index.vue'),
hidden: true
}
]
}

View File

@ -9,8 +9,25 @@ const settingRouter = {
{
path: '/setting',
name: 'setting',
meta: { icon: 'app-team', title: '团队管理' },
meta: {
icon: 'app-team',
title: '团队管理',
parentPath: '/setting',
parentName: 'setting'
},
component: () => import('@/views/setting/index.vue')
},
{
path: '/template',
name: 'template',
meta: {
icon: 'app-team',
title: '模版管理',
activeMenu: '/setting',
parentPath: '/setting',
parentName: 'setting'
},
component: () => import('@/views/template/index.vue')
}
]
}

View File

@ -9,7 +9,7 @@ export const routes: Array<RouteRecordRaw> = [
path: '/',
name: 'home',
component: () => import('@/layout/app-layout/index.vue'),
redirect: '/setting',
redirect: '/dataset',
children: [
// {
// path: '/first',

View File

@ -2,9 +2,13 @@ import { createPinia } from 'pinia'
const store = createPinia()
export { store }
import useUserStore from './modules/user'
import useDatasetStore from './modules/dataset'
import useParagraphStore from './modules/paragraph'
const useStore = () => ({
user: useUserStore()
user: useUserStore(),
dataset: useDatasetStore(),
paragraph: useParagraphStore(),
})
export default useStore

View File

@ -0,0 +1,26 @@
import { defineStore } from 'pinia'
import type { datasetData } from '@/api/type/dataset'
import type { UploadUserFile } from 'element-plus'
export interface datasetStateTypes {
baseInfo: datasetData | null
documentsFiles: UploadUserFile[]
}
const useDatasetStore = defineStore({
id: 'dataset',
state: (): datasetStateTypes => ({
baseInfo: null,
documentsFiles: []
}),
actions: {
saveBaseInfo(info: datasetData | null) {
this.baseInfo = info
},
saveDocumentsFile(file: UploadUserFile[]) {
this.documentsFiles = file
}
}
})
export default useDatasetStore

View File

@ -0,0 +1,23 @@
import { defineStore } from 'pinia'
import datasetApi from '@/api/dataset'
const useParagraphStore = defineStore({
id: 'paragraph',
state: () => ({}),
actions: {
async asyncPutParagraph(datasetId: string, documentId: string, paragraphId: string, data: any) {
return new Promise((resolve, reject) => {
datasetApi
.putParagraph(datasetId, documentId, paragraphId, data)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
}
}
})
export default useParagraphStore

View File

@ -2,14 +2,14 @@ import { defineStore } from 'pinia'
import type { User } from '@/api/type/user'
import UserApi from '@/api/user'
export interface appStateTypes {
export interface userStateTypes {
userInfo: User | null
token: any
}
const useUserStore = defineStore({
id: 'user',
state: (): appStateTypes => ({
state: (): userStateTypes => ({
userInfo: null,
token: ''
}),

View File

@ -9,14 +9,17 @@ html {
}
body {
font-size: 14px;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei',
'微软雅黑', Arial, sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 500;
height: 100%;
margin: 0;
padding: 0;
color: var(--app-text-color);
}
#app {
@ -58,7 +61,6 @@ ul {
//
::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: var(--ce-webkit-scrollbar-background-color, rgba(31, 35, 41, 0.3));
}
//
@ -73,6 +75,7 @@ h1 {
h2 {
font-size: 20px;
font-weight: 500;
}
h3 {
@ -83,38 +86,66 @@ h4 {
font-size: 16px;
}
.bold {
font-weight: 600;
}
.lighter {
font-weight: 400;
}
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
.w-240 {
width: 240px;
}
.mt-8 {
margin-top: 8px;
margin-top: var(--app-base-px);
}
.mt-10 {
margin-top: 10px;
.mt-12 {
margin-top: calc(var(--app-base-px) + 4px);
}
.mt-16 {
margin-top: calc(var(--app-base-px) * 2);
}
.mb-8 {
margin-bottom: var(--app-base-px);
}
.mb-16 {
margin-bottom: calc(var(--app-base-px) * 2);
}
.ml-10 {
margin-left: 10px;
.ml-4 {
margin-left: calc(var(--app-base-px) - 4px);
}
.ml-8 {
margin-left: var(--app-base-px);
}
.ml-16 {
margin-left: 16px;
margin-left: calc(var(--app-base-px) * 2);
}
.mr-10 {
margin-right: 10px;
.mr-8 {
margin-right: var(--app-base-px);
}
.mb-10 {
margin-bottom: 10px;
.mr-12 {
margin-right: calc(var(--app-base-px) + 4px);
}
.mb-20 {
margin-bottom: 20px;
.mr-16 {
margin-right: calc(var(--app-base-px) * 2);
}
.p-15 {
padding: 15px;
.p-8 {
padding: var(--app-base-px);
}
.p-16 {
padding: calc(var(--app-base-px) * 2);
}
.p-24 {
padding: calc(var(--app-base-px) * 3);
}
.flex {
@ -144,6 +175,14 @@ h4 {
text-align: right;
}
.vertical-middle {
vertical-align: middle;
}
.border-l {
border-left: 1px solid var(--el-border-color);
}
.border-b {
border-bottom: 1px solid var(--el-border-color);
}
@ -172,5 +211,84 @@ h4 {
//
.main-calc-height {
height: calc(100vh - 125px);
height: var(--app-main-height);
box-sizing: border-box;
}
// 线
.title-decoration-1 {
position: relative;
padding-left: 12px;
&:before {
position: absolute;
left: 2px;
top: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 80%;
content: '';
background: var(--el-color-primary);
}
}
.app-logo-font {
background: var(--app-logo-color);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-family: Arial Black;
font-style: normal;
font-weight: 900;
}
// tag
.default-tag {
background: var(--tag-deflaut-bg);
color: var(--tag-deflaut-color);
}
// card
.card-never {
background: var(--app-layout-bg-color);
border: none;
}
// 90
.rotate-90 {
transform: rotateZ(90deg);
}
//
.table-quick-append {
.el-table__append-wrapper {
position: absolute;
top: 0;
border-bottom: var(--el-table-border);
width: 100%;
height: 49px;
box-sizing: border-box;
align-items: center;
display: flex;
padding: 0 12px;
cursor: pointer;
&:hover {
background: var(--el-color-primary-light-9);
}
}
.el-table__body {
margin-top: 49px;
}
}
.success {
color: var(--el-color-success);
}
.danger {
color: var(--el-color-danger);
}
.warning {
color: var(--el-color-warning);
}
.primary {
color: var(--el-color-primary);
}

View File

@ -1,15 +1,28 @@
:root {
--el-color-primary: #3370ff;
--el-color-primary-light-9: rgba(51, 112, 255, 0.1);
--el-menu-item-height: 45px;
--el-text-color-primary: '#1F2329';
--el-box-shadow-light: 0px 2px 4px 0px rgba(31, 35, 41, 0.12);
--el-border-color: #dee0e3;
}
.el-button {
padding: 5px 12px;
&.is-text {
padding: 4px !important;
font-size: 16px;
max-height: 24px;
&:not(.is-disabled):hover {
background: var(--app-text-color-light-1);
}
}
}
.el-avatar {
--el-avatar-bg-color: var(--el-color-primary);
--el-avatar-size-small: 33px;
--el-avatar-border-radius: 8px;
cursor: pointer;
}
.el-popper {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.el-form {
--el-form-inline-content-width: 100%;
@ -26,28 +39,20 @@
}
.el-message-box {
padding-bottom: 24px;
.app-confirm {
.app-confirm-title {
color: var(--el-text-color-primary);
}
.icon {
font-size: 24px;
color: var(--el-color-warning);
}
.app-confirm-decription {
margin-left: 40px;
}
--el-messagebox-font-size: 16px;
padding: 24px;
.el-message-box__header {
padding: 0;
}
}
.el-message-box__content {
padding: 24px;
color: var(--el-text-color-primary);
padding: 24px 0;
color: var(--app-text-color);
font-weight: 400;
}
.el-message-box__btns {
padding: 5px 24px 0;
padding: 0;
button {
min-width: 80px;
&:nth-child(2) {
@ -57,46 +62,122 @@
button.danger {
background: var(--el-color-danger);
border: var(--el-color-danger);
color: #ffffff;
}
}
.el-message-box__headerbtn {
right: -5px;
top: -5px;
.el-message-box__close {
font-size: 20px;
}
}
@media only screen and (min-width: 1200px) {
.el-col-lg-5 {
display: block;
max-width: 25%;
flex: 0 0 25%;
}
}
@media only screen and (min-width: 1400px) {
.el-col-lg-5 {
display: block;
max-width: 20.8333333333%;
flex: 0 0 20.8333333333%;
.app-list-row {
.el-col-lg-6 {
display: block;
max-width: 20.8333333333%;
flex: 0 0 20.8333333333%;
}
}
}
@media only screen and (min-width: 1920px) {
.el-col-xl-4 {
display: block;
max-width: 16.6666666667%;
flex: 0 0 16.6666666667%;
.app-list-row {
.el-col-xl-4 {
display: block;
max-width: 16.6666666667%;
flex: 0 0 16.6666666667%;
}
}
}
//
.el-drawer {
.el-drawer__header {
padding: 0;
margin: 0 24px;
height: 56px;
border-bottom: 1px solid var(--el-border-color);
.el-drawer__title {
color: #1f2329;
.el-card {
--el-card-border-radius: 8px;
--el-card-padding: calc(var(--app-base-px) * 2);
}
.el-dropdown {
color: var(--app-text-color);
}
.el-tag {
--el-tag-border-radius: 2px;
}
.el-table {
--el-table-header-bg-color: var(--app-layout-bg-color);
--el-table-text-color: var(--app-text-color);
font-weight: 400;
thead {
color: var(--app-text-color-secondary);
th {
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
}
.el-drawer__body {
--el-drawer-padding-primary: 24px;
th.el-table__cell {
border-top: var(--el-table-border);
}
.el-table__cell {
padding: 12px 0;
}
.el-checkbox {
height: 23px;
}
}
.el-pagination .el-select .el-input {
width: 100px;
}
// el-steps
.el-step__icon {
background: none;
}
.el-step__head.is-process {
.el-step__icon {
&.is-text {
color: #ffffff;
border-color: var(--el-color-primary) !important;
background: var(--el-color-primary) !important;
}
}
}
.el-text {
font-weight: 400;
&.el-text--info {
color: var(--app-text-color-secondary);
}
}
.el-switch {
height: auto;
}
.el-slider {
--el-slider-button-size: 14px;
--el-slider-height: 4px;
}
.el-slider__button {
border: solid 1px var(--app-border-color-dark);
&.hover {
border: solid 2px var(--el-slider-main-bg-color);
}
}
.el-slider__runway.show-input {
margin-right: calc(var(--app-base-px) + 4px);
}
.el-slider__input {
width: 60px;
}
.input-with-select {
.el-input__wrapper {
// border: 1px solid var(--el-border-color);
// box-shadow: none!important;
}
.el-input-group__prepend {
background-color: var(--el-fill-color-blank);
}
}

View File

@ -1,17 +1,39 @@
:root {
--el-color-primary: rgba(51, 112, 255, 1);
--app-layout-bg-color: #f3f5f6;
--app-base-text-color: rgba(31, 35, 41, 1);
--app-view-padding: 15px;
--app-base-px: 8px;
--app-layout-bg-color: #f5f6f7;
--app-text-color: #1f2329;
--app-text-color-light-1: rgba(31, 35, 41, 0.1);
--app-text-color-secondary: #646a73;
--app-text-color-disable: #bbbfc4;
--app-view-padding: 24px;
--app-view-bg-color: #ffffff;
--hover-bg-color: #fafafa;
--app-border-color-dark: #bbbfc4;
/** header 组件 */
--app-header-height: 56px;
--app-header-padding: 0 20px;
--app-header-bg-color: #ffffff;
--app-header-bg-color: linear-gradient(90deg, #ebf1ff 24.34%, #e5fbf8 56.18%, #f2ebfe 90.18%);
--app-logo-color: linear-gradient(180deg, #3370ff 0%, #7f3bf5 100%);
//
--app-main-height: calc(100vh - var(--app-header-height) - var(--app-view-padding) * 2 - 40px);
/** sidebar 组件 */
--sidebar-bg-color: #ffffff;
--sidebar-width: 198px;
--sidebar-width: 240px;
/** tag */
--tag-deflaut-bg: rgba(51, 112, 255, 0.2);
--tag-deflaut-color: #2b5fd9;
/** card */
--card-width: 330px;
--card-min-height: 160px;
--card-min-width: 220px;
--team-manage-left-width : 280px;
/** setting */
--setting-left-width: 280px;
/** dataset */
--create-dataset-height: calc(
100vh - var(--app-header-height) - var(--app-view-padding) * 2 - 70px
);
}

View File

@ -43,20 +43,30 @@ export const MsgError = (message: string) => {
* @param message: {title, decription,type}
*/
export const MsgConfirm = ({ title, decription }: any, options?: any) => {
const message: any = h('div', { class: 'app-confirm' }, [
h('h4', { class: 'app-confirm-title flex align-center' }, [
h(ElIcon, { class: 'icon' }, [h(WarningFilled)]),
h('span', { class: 'ml-16' }, title)
]),
h('div', { class: 'app-confirm-decription mt-8' }, decription)
])
export const MsgConfirm = (title: string, decription: string, options?: any) => {
const defaultOptions: Object = {
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
...options
}
return ElMessageBox({ message, ...defaultOptions })
return ElMessageBox.confirm(decription, title, defaultOptions)
}
// export const MsgConfirm = ({ title, decription }: any, options?: any) => {
// const message: any = h('div', { class: 'app-confirm' }, [
// h('h4', { class: 'app-confirm-title flex align-center' }, [
// h(ElIcon, { class: 'icon' }, [h(WarningFilled)]),
// h('span', { class: 'ml-16' }, title)
// ]),
// h('div', { class: 'app-confirm-decription mt-8' }, decription)
// ])
// const defaultOptions: Object = {
// showCancelButton: true,
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// ...options
// }
// return ElMessageBox({ message, ...defaultOptions })
// }

View File

@ -0,0 +1,30 @@
const getCheckDate = (timestamp: any) => {
if (!timestamp) return false
const dt = new Date(timestamp)
if (isNaN(dt.getTime())) return false
return dt
}
export const datetimeFormat = (timestamp: any) => {
const dt = getCheckDate(timestamp)
if (!dt) return timestamp
const y = dt.getFullYear()
const m = (dt.getMonth() + 1 + '').padStart(2, '0')
const d = (dt.getDate() + '').padStart(2, '0')
const hh = (dt.getHours() + '').padStart(2, '0')
const mm = (dt.getMinutes() + '').padStart(2, '0')
const ss = (dt.getSeconds() + '').padStart(2, '0')
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
}
export const dateFormat = (timestamp: any) => {
const dt = getCheckDate(timestamp)
if (!dt) return timestamp
const y = dt.getFullYear()
const m = (dt.getMonth() + 1 + '').padStart(2, '0')
const d = (dt.getDate() + '').padStart(2, '0')
return `${y}-${m}-${d}`
}

View File

@ -0,0 +1,31 @@
export function toThousands(num: any) {
return num?.toString().replace(/\d+/, function (n: any) {
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
})
}
export function numberFormat(num: number) {
return num < 1000 ? toThousands(num) : toThousands((num / 1000).toFixed(1)) + 'k'
}
export function filesize(size: number) {
if (!size) return ''
const num = 1024.0 //byte
if (size < num) return size + 'B'
if (size < Math.pow(num, 2)) return (size / num).toFixed(2) + 'K' //kb
if (size < Math.pow(num, 3)) return (size / Math.pow(num, 2)).toFixed(2) + 'M' //M
if (size < Math.pow(num, 4)) return (size / Math.pow(num, 3)).toFixed(2) + 'G' //G
return (size / Math.pow(num, 4)).toFixed(2) + 'T' //T
}
// 获取文件后缀
export function fileType(name: string) {
const suffix = name.split('.')
return suffix[suffix.length - 1]
}
// 获得文件对应图片
export function getImgUrl(name: string) {
const type = fileType(name) || 'txt'
return `/src/assets/${type}-icon.svg`
}

View File

@ -31,7 +31,7 @@ const router = useRouter()
}
.message-container {
color: var(--app-base-text-color);
color: var(--app-text-color);
.title {
font-size: 50px;

View File

@ -1,8 +0,0 @@
<template >
<div>
app
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped></style>

View File

@ -1,7 +1,8 @@
<template>
<div>dataset 文档</div>
<div>
概览
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,8 @@
<template>
<div>
设置
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,8 @@
<template>
<div>
创建应用
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,8 @@
<template>
<div>
对话日志
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,148 @@
<template>
<div class="application-list-container p-24">
<div class="flex-between">
<h3>应用</h3>
<el-input
v-model="pageConfig.name"
@change="search"
placeholder="按 名称 搜索"
prefix-icon="Search"
class="w-240"
/>
</div>
<div v-loading.fullscreen.lock="loading">
<el-row
:gutter="15"
v-infinite-scroll="loadDataset"
:infinite-scroll-disabled="disabledScroll"
class="app-list-row"
>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mt-8">
<CardAdd title="创建应用" @click="router.push({ path: '/application/create' })" />
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mt-8">
<CardBox title="应用" @click="router.push({ path: '/application/1/overview' })" />
</el-col>
<!-- <el-col
:xs="24"
:sm="12"
:md="8"
:lg="6"
:xl="4"
v-for="(item, index) in applicationList"
:key="index"
class="mt-8"
>
<CardBox
:title="item.name"
:description="item.desc"
class="cursor"
@click="router.push({ path: `/dataset/${item.id}/document` })"
>
<template #mouseEnter>
<el-tooltip effect="dark" content="删除" placement="top">
<el-button text @click.stop="deleteDateset(item)" class="delete-button">
<el-icon><Delete /></el-icon>
</el-button>
</el-tooltip>
</template>
<template #footer>
<div class="footer-content">
<span class="bold">{{ item?.document_count || 0 }}</span>
文档<el-divider direction="vertical" />
<span class="bold">{{ numberFormat(item?.char_length) || 0 }}</span>
字符<el-divider direction="vertical" />
<span class="bold">{{ item?.char_length || 0 }}</span>
关联应用
</div>
</template>
</CardBox>
</el-col> -->
</el-row>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import datasetApi from '@/api/dataset'
import type { datasetListRequest } from '@/api/type/dataset'
// import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { useRouter } from 'vue-router'
// import { numberFormat } from '@/utils/utils'
const router = useRouter()
const loading = ref(false)
const disabledScroll = ref(false)
const pageConfig = reactive<datasetListRequest>({
current_page: 1,
page_size: 20,
name: ''
})
const applicationList = ref<any[]>([])
function loadDataset() {}
function search() {
pageConfig.current_page = 1
getList()
}
// function deleteDateset(row: any) {
// MsgConfirm(
// `${row.name} ?`,
// ` ${row.char_length} `,
// {
// confirmButtonText: '',
// confirmButtonClass: 'danger'
// }
// )
// .then(() => {
// loading.value = true
// datasetApi
// .delDateset(row.id)
// .then(() => {
// MsgSuccess('')
// getList()
// })
// .catch(() => {
// loading.value = false
// })
// })
// .catch(() => {})
// }
function getList() {
loading.value = true
datasetApi
.getDateset(pageConfig)
.then((res) => {
applicationList.value = res.data?.records
loading.value = false
})
.catch(() => {
loading.value = false
})
}
onMounted(() => {
// getList()
})
</script>
<style lang="scss" scoped>
// .dataset-list-container {
// .delete-button {
// position: absolute;
// right: 12px;
// top: 18px;
// height: auto;
// }
// .footer-content {
// .bold {
// color: var(--app-text-color);
// }
// }
// }
</style>

View File

@ -1,21 +1,181 @@
<template>
<LayoutContent header="创建数据集">
<el-steps :active="active" finish-status="success">
<el-step title="步骤 1"></el-step>
<el-step title="步骤 2"></el-step>
<el-step title="步骤 3"></el-step>
</el-steps>
<el-button style="margin-top: 12px" @click="next"></el-button>
</LayoutContent>
<LayoutContainer header="创建数据集" back-to="-1" class="create-dataset">
<template #header>
<el-steps :active="active" finish-status="success" align-center class="create-dataset__steps">
<el-step v-for="(item, index) in steps" :key="index">
<template #icon>
<div class="app-step flex align-center">
<div class="el-step__icon is-text">
<div class="el-step__icon-inner">
<el-icon v-if="active == index + 1" style="margin-top: 1px"><Select /></el-icon>
<span v-else> {{ index + 1 }}</span>
</div>
</div>
<span class="ml-4">{{ item.name }}</span>
</div>
</template>
</el-step>
</el-steps>
</template>
<div class="create-dataset__main flex" v-loading="loading">
<div class="create-dataset__component main-calc-height">
<template v-if="steps[active]?.component">
<component :is="steps[active].component" :ref="steps[active]?.ref" />
</template>
<template v-else-if="active === 2">
<el-result icon="success" title="🎉 数据集创建成功 🎉">
<template #sub-title>
<div class="mt-8">
<span class="bold">{{ successInfo?.document_count || 0 }}</span>
<el-text type="info" class="ml-4">文档</el-text>
<el-divider direction="vertical" />
<span class="bold">{{ successInfo?.document_list.length || 0 }}</span>
<el-text type="info" class="ml-4">分段</el-text>
<el-divider direction="vertical" />
<span class="bold">{{ toThousands(successInfo?.char_length) || 0 }}</span>
<el-text type="info" class="ml-4">字符</el-text>
</div>
</template>
<template #extra>
<el-button @click="router.push({ path: `/dataset` })">返回数据集列表</el-button>
<el-button
type="primary"
@click="router.push({ path: `/dataset/${successInfo?.id}/document` })"
>前往文档</el-button
>
</template>
</el-result>
</template>
</div>
</div>
<div class="create-dataset__footer text-right border-t" v-if="active !== 2">
<el-button @click="router.go(-1)" :disabled="loading"> </el-button>
<el-button @click="prev" v-if="active === 1" :disabled="loading"></el-button>
<el-button @click="next" type="primary" v-if="active === 0" :disabled="loading"
>下一步</el-button
>
<el-button @click="submit" type="primary" v-if="active === 1" :disabled="loading">
开始导入
</el-button>
</div>
</LayoutContainer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import StepFirst from './step/StepFirst.vue'
import StepSecond from './step/StepSecond.vue'
import datasetApi from '@/api/dataset'
import type { datasetData } from '@/api/type/dataset'
import { MsgSuccess } from '@/utils/message'
import { toThousands } from '@/utils/utils'
import useStore from '@/stores'
const { dataset } = useStore()
const baseInfo = computed(() => dataset.baseInfo)
const router = useRouter()
const route = useRoute()
const {
query: { id }
} = route as any
const steps = [
{
ref: 'StepFirstRef',
name: '上传文档',
component: StepFirst
},
{
ref: 'StepSecondRef',
name: '设置分段规则',
component: StepSecond
}
]
const StepFirstRef = ref()
const StepSecondRef = ref()
const loading = ref(false)
const active = ref(0)
const successInfo = ref<any>(null)
const next = () => {
if (active.value++ > 2) active.value = 0
async function next() {
if (await StepFirstRef.value.onSubmit()) {
if (active.value++ > 2) active.value = 0
}
}
const prev = () => {
active.value = 0
}
function clearStore() {
dataset.saveBaseInfo(null)
dataset.saveDocumentsFile([])
}
function submit() {
loading.value = true
const documents = [] as any[]
StepSecondRef.value.paragraphList.map((item: any) => {
documents.push({
name: item.name,
paragraphs: item.content
})
})
const obj = { ...baseInfo.value, documents } as datasetData
if (id) {
datasetApi
.postDocument(id, documents)
.then((res) => {
MsgSuccess('提交成功')
clearStore()
router.push({ path: `/dataset/${id}/document` })
})
.catch(() => {
loading.value = false
})
} else {
datasetApi
.postDateset(obj)
.then((res) => {
successInfo.value = res.data
active.value = 2
clearStore()
loading.value = false
})
.catch(() => {
loading.value = false
})
}
}
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.create-dataset {
&__steps {
min-width: 450px;
max-width: 800px;
width: 80%;
margin: 0 auto;
padding-right: 60px;
:deep(.el-step__line) {
left: 64% !important;
right: -33% !important;
}
}
&__component {
width: 100%;
margin: 0 auto;
overflow: hidden;
}
&__footer {
padding: 16px 24px;
position: fixed;
bottom: 0;
left: 0;
background: #ffffff;
width: 100%;
box-sizing: border-box;
}
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<h4 class="title-decoration-1 mb-16">基本信息</h4>
<el-form ref="FormRef" :model="form" :rules="rules" label-position="top">
<el-form-item label="数据集名称" prop="name">
<el-input
v-model.trim="form.name"
placeholder="请输入数据集名称"
maxlength="64"
show-word-limit
/>
</el-form-item>
<el-form-item label="数据集描述" prop="desc">
<el-input
v-model.trim="form.desc"
type="textarea"
placeholder="描述数据集的内容详尽的描述将帮助AI能深入理解该数据集的内容能更准确的检索到内容提高该数据集的命中率。"
maxlength="500"
show-word-limit
:autosize="{ minRows: 3 }"
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, computed, watch } from 'vue'
import useStore from '@/stores'
const props = defineProps({
data: {
type: Object,
default: () => {}
}
})
const { dataset } = useStore()
const baseInfo = computed(() => dataset.baseInfo)
const form = ref<any>({
name: '',
desc: ''
})
const rules = reactive({
name: [{ required: true, message: '请输入数据集名称', trigger: 'blur' }],
desc: [{ required: true, message: '请输入数据集描述', trigger: 'blur' }]
})
const FormRef = ref()
watch(
() => props.data,
(value) => {
if (value && JSON.stringify(value) !== '{}') {
form.value.name = value.name
form.value.desc = value.desc
}
},
{
//
immediate: true
}
)
//
function validate() {
if (!FormRef.value) return
return FormRef.value.validate((valid: any) => {
return valid
})
}
onMounted(() => {
if (baseInfo.value) {
form.value = baseInfo.value
}
})
onUnmounted(() => {
form.value = {
name: '',
desc: ''
}
})
defineExpose({
validate,
form
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,44 @@
<template>
<el-dialog title="编辑分段" v-model="dialogVisible" width="600">
<ParagraphForm ref="paragraphFormRef" :data="detail" />
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
<el-button type="primary" @click="submitHandle"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { cloneDeep } from 'lodash'
import ParagraphForm from '@/views/paragraph/component/ParagraphForm.vue'
const emit = defineEmits(['updateContent'])
const dialogVisible = ref<boolean>(false)
const detail = ref({})
const paragraphFormRef = ref()
watch(dialogVisible, (bool) => {
if (!bool) {
detail.value = {}
}
})
const open = (data: any) => {
detail.value = cloneDeep(data)
dialogVisible.value = true
}
const submitHandle = async () => {
if (await paragraphFormRef.value?.validate()) {
emit('updateContent', paragraphFormRef.value?.form)
dialogVisible.value = false
}
}
defineExpose({ open })
</script>
<style lang="scss" scope></style>

View File

@ -0,0 +1,140 @@
<template>
<el-tabs v-model="activeName" class="paragraph-tabs" @tab-click="handleClick">
<template v-for="(item, index) in newData" :key="index">
<el-tab-pane :label="item.name" :name="index">
<template #label>
<div class="flex-center">
<img :src="getImgUrl(item && item?.name)" alt="" height="16" />
<span class="ml-4">{{ item?.name }}</span>
</div>
</template>
<el-scrollbar>
<div class="paragraph-list">
<el-card
v-for="(child, cIndex) in item.content"
:key="cIndex"
shadow="never"
class="card-never mb-16"
>
<div class="flex-between">
<span>{{ child.title }}</span>
<div>
<!-- 编辑分段按钮 -->
<el-button link @click="editHandle(child, index, cIndex)">
<el-icon><Edit /></el-icon>
</el-button>
<!-- 删除分段按钮 -->
<el-button link @click="deleteHandle(child, index, cIndex)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
<div class="lighter mt-12">
{{ child.content }}
</div>
<div class="lighter mt-12">
<el-text type="info"> {{ child.content.length }} 个字符 </el-text>
</div>
</el-card>
</div>
</el-scrollbar>
</el-tab-pane>
</template>
</el-tabs>
<EditParagraphDialog ref="EditParagraphDialogRef" @updateContent="updateContent" />
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import type { TabsPaneContext } from 'element-plus'
import EditParagraphDialog from './EditParagraphDialog.vue'
import { filesize, getImgUrl } from '@/utils/utils'
import { MsgConfirm } from '@/utils/message'
const props = defineProps({
data: {
type: Array<any>,
default: () => []
}
})
const emit = defineEmits(['update:data'])
const EditParagraphDialogRef = ref()
const activeName = ref(0)
const currentPIndex = ref(null) as any
const currentCIndex = ref(null) as any
const newData = ref<any[]>([])
watch(
() => props.data,
(value) => {
newData.value = value
},
{
//
immediate: true
}
)
function editHandle(item: any, index: number, cIndex: number) {
currentPIndex.value = index
currentCIndex.value = cIndex
EditParagraphDialogRef.value.open(item)
}
function deleteHandle(item: any, index: number, cIndex: number) {
MsgConfirm(`是否删除分段:${item.title} ?`, `删除后将不会存入数据集,对本地文档无影响。`, {
confirmButtonText: '删除',
confirmButtonClass: 'danger'
})
.then(() => {
newData.value[index].content.splice(cIndex, 1)
emit('update:data', newData.value)
})
.catch(() => {})
}
function updateContent(data: any) {
newData.value[currentPIndex.value].content[currentCIndex.value] = data
emit('update:data', newData.value)
}
const handleClick = (tab: TabsPaneContext, event: Event) => {
// console.log(tab, event)
}
onMounted(() => {})
</script>
<style scoped lang="scss">
.paragraph-tabs {
:deep(.el-tabs__item) {
background: var(--app-text-color-light-1);
margin: 4px;
border-radius: 4px;
padding: 5px 10px 5px 8px !important;
height: auto;
&:nth-child(2) {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
&.is-active {
border: 1px solid var(--el-color-primary);
background: var(--el-color-primary-light-9);
color: var(--el-text-color-primary);
}
}
:deep(.el-tabs__nav-wrap::after) {
display: none;
}
:deep(.el-tabs__active-bar) {
display: none;
}
}
.paragraph-list {
height: calc(var(--create-dataset-height) - 125px);
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<h4 class="title-decoration-1 mb-8">上传文档</h4>
<el-form ref="FormRef" :model="form" :rules="rules" label-position="top">
<el-form-item prop="fileList">
<el-upload
class="w-full"
drag
multiple
v-model:file-list="form.fileList"
action="#"
:auto-upload="false"
:show-file-list="false"
accept=".txt, .md"
>
<img src="@/assets/upload-icon.svg" alt="" />
<div class="el-upload__text">
<p>
将文件拖拽至此区域或
<em> 选择文件上传 </em>
</p>
<div class="upload__decoration">
<p>支持格式TXTMarkdown每次最多上传50个文件每个文件不超过 10MB</p>
<p>若使用高级分段建议上传前规范文件的分段标识</p>
</div>
</div>
</el-upload>
</el-form-item>
</el-form>
<el-row :gutter="8" v-if="form.fileList?.length">
<template v-for="(item, index) in form.fileList" :key="index">
<el-col :span="12" class="mb-8">
<el-card shadow="never" class="file-List-card">
<div class="flex-between">
<div class="flex">
<img :src="getImgUrl(item && item?.name)" alt="" />
<div class="ml-8">
<p>{{ item && item?.name }}</p>
<el-text type="info">{{ filesize(item && item?.size) }}</el-text>
</div>
</div>
<el-button text @click="deleteFlie(index)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</el-card>
</el-col>
</template>
</el-row>
</template>
<script setup lang="ts">
import { ref, reactive, onUnmounted, onMounted, computed } from 'vue'
import type { UploadProps } from 'element-plus'
import { filesize, getImgUrl } from '@/utils/utils'
import { MsgError } from '@/utils/message'
import useStore from '@/stores'
const { dataset } = useStore()
const documentsFiles = computed(() => dataset.documentsFiles)
const form = ref({
fileList: [] as any
})
const rules = reactive({
fileList: [{ required: true, message: '请上传文件', trigger: 'change' }]
})
const FormRef = ref()
// const beforeUploadHandle: UploadProps['beforeUpload'] = (rawFile) => {
// const type = fileType(rawFile?.name)
// console.log(type)
// if (type !== 'txt' || type !== 'md') {
// MsgError('Avatar picture must be JPG format!')
// return false
// } else if (rawFile.size / 1024 / 1024 > 10) {
// MsgError(' 10MB!')
// return false
// }
// return true
// }
function deleteFlie(index: number) {
form.value.fileList.splice(index, 1)
}
//
function validate() {
if (!FormRef.value) return
return FormRef.value.validate((valid: any) => {
return valid
})
}
onMounted(() => {
if (documentsFiles.value) {
form.value.fileList = documentsFiles.value
}
})
onUnmounted(() => {
form.value = {
fileList: []
}
})
defineExpose({
validate,
form
})
</script>
<style scoped lang="scss">
.file-List-card {
border-radius: 4px;
:deep(.el-card__body) {
padding: 8px 16px 8px 12px;
}
}
.upload__decoration {
font-size: 12px;
line-height: 20px;
color: var(--el-text-color-secondary);
}
</style>

View File

@ -1,87 +1,103 @@
<template>
<LayoutContent header="数据集">
<div class="dataset-list-container p-15">
<div class="text-right">
<el-input
v-model="filterText"
placeholder="搜索内容"
suffix-icon="Search"
style="width: 300px"
/>
</div>
<div>
<el-row
:gutter="15"
v-infinite-scroll="loadDataset"
:infinite-scroll-disabled="disabledScroll"
>
<el-col :xs="24" :sm="12" :md="6" :lg="5" :xl="4" class="mt-10">
<CardAdd title="创建数据集" @click="router.push({ path: '/dataset/create' })" />
</el-col>
<el-col
:xs="24"
:sm="12"
:md="6"
:lg="5"
:xl="4"
v-for="(item, index) in datasetList"
:key="index"
class="mt-10"
>
<CardBox :title="item.name" :description="item.desc" class="cursor">
<template #mouseEnter>
<div class="delete-button">
<el-button type="primary" link @click.stop="deleteDateset(item)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</template>
<template #footer>
<div class="footer-content">
{{ item?.document_count || 0 }}文档数 {{ item?.char_length || 0 }}字符数
{{ item?.char_length || 0 }}关联应用
</div>
</template>
</CardBox>
</el-col>
</el-row>
</div>
<div class="dataset-list-container p-24">
<div class="flex-between">
<h3>数据集</h3>
<el-input
v-model="pageConfig.name"
@change="search"
placeholder="按 名称 搜索"
prefix-icon="Search"
class="w-240"
/>
</div>
</LayoutContent>
<div v-loading.fullscreen.lock="loading">
<el-row
:gutter="15"
v-infinite-scroll="loadDataset"
:infinite-scroll-disabled="disabledScroll"
class="app-list-row"
>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mt-8">
<CardAdd title="创建数据集" @click="router.push({ path: '/dataset/create' })" />
</el-col>
<el-col
:xs="24"
:sm="12"
:md="8"
:lg="6"
:xl="4"
v-for="(item, index) in datasetList"
:key="index"
class="mt-8"
>
<CardBox
:title="item.name"
:description="item.desc"
class="cursor"
@click="router.push({ path: `/dataset/${item.id}/document` })"
>
<template #mouseEnter>
<el-tooltip effect="dark" content="删除" placement="top">
<el-button text @click.stop="deleteDateset(item)" class="delete-button">
<el-icon><Delete /></el-icon>
</el-button>
</el-tooltip>
</template>
<template #footer>
<div class="footer-content">
<span class="bold">{{ item?.document_count || 0 }}</span>
文档<el-divider direction="vertical" />
<span class="bold">{{ numberFormat(item?.char_length) || 0 }}</span>
字符<el-divider direction="vertical" />
<span class="bold">{{ item?.char_length || 0 }}</span>
关联应用
</div>
</template>
</CardBox>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, reactive } from 'vue'
import datasetApi from '@/api/dataset'
import type { datasetListRequest } from '@/api/type/dataset'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { useRouter } from 'vue-router';
const router = useRouter();
import { useRouter } from 'vue-router'
import { numberFormat } from '@/utils/utils'
const router = useRouter()
const loading = ref(false)
const filterText = ref('')
const datasetList = ref<any[]>([])
const disabledScroll = ref(false)
const pageConfig = ref<datasetListRequest>({
const pageConfig = reactive<datasetListRequest>({
current_page: 1,
page_size: 20,
search_text: ''
name: ''
})
function loadDataset() { }
function loadDataset() {}
function search() {
pageConfig.current_page = 1
getList()
}
function deleteDateset(row: any) {
MsgConfirm({
title: `是否删除数据集:${row.name}`,
decription: '此数据集关联2个应用删除后无法恢复请谨慎操作。',
confirmButtonText: '删除',
}, {
confirmButtonClass: 'danger',
})
MsgConfirm(
`是否删除数据集:${row.name} ?`,
`此数据集关联 ${row.char_length} 个应用,删除后无法恢复,请谨慎操作。`,
{
confirmButtonText: '删除',
confirmButtonClass: 'danger'
}
)
.then(() => {
loading.value = true
datasetApi.delDateset(row.id)
datasetApi
.delDateset(row.id)
.then(() => {
MsgSuccess('删除成功')
getList()
@ -96,9 +112,9 @@ function deleteDateset(row: any) {
function getList() {
loading.value = true
datasetApi
.getDateset(pageConfig.value)
.getDateset(pageConfig)
.then((res) => {
datasetList.value = res.data
datasetList.value = res.data?.records
loading.value = false
})
.catch(() => {
@ -114,8 +130,14 @@ onMounted(() => {
.dataset-list-container {
.delete-button {
position: absolute;
right: 10px;
top: 10px;
right: 12px;
top: 18px;
height: auto;
}
.footer-content {
.bold {
color: var(--app-text-color);
}
}
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<el-scrollbar>
<div class="upload-document p-24">
<!-- 基本信息 -->
<BaseForm ref="BaseFormRef" v-if="isCreate" />
<!-- 上传文档 -->
<UploadComponent ref="UploadComponentRef" />
</div>
</el-scrollbar>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import BaseForm from '@/views/dataset/component/BaseForm.vue'
import UploadComponent from '@/views/dataset/component/UploadComponent.vue'
import useStore from '@/stores'
const { dataset } = useStore()
const route = useRoute()
const {
params: { type }
} = route
const isCreate = type === 'create'
const BaseFormRef = ref()
const UploadComponentRef = ref()
// submit
const onSubmit = async () => {
if (isCreate) {
if ((await BaseFormRef.value?.validate()) && (await UploadComponentRef.value.validate())) {
// stores
dataset.saveBaseInfo(BaseFormRef.value.form)
dataset.saveDocumentsFile(UploadComponentRef.value.form.fileList)
return true
} else {
return false
}
} else {
if (await UploadComponentRef.value.validate()) {
// stores
dataset.saveDocumentsFile(UploadComponentRef.value.form.fileList)
return true
} else {
return false
}
}
}
onMounted(() => {})
defineExpose({
onSubmit
})
</script>
<style scoped lang="scss">
.upload-document {
width: 70%;
margin: 0 auto;
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,179 @@
<template>
<div class="set-rules">
<el-row class="set-rules-height">
<el-col :span="12" class="p-24">
<h4 class="title-decoration-1 mb-8">设置分段规则</h4>
<div>
<el-scrollbar>
<div class="left-height">
<el-radio-group v-model="radio" class="set-rules__radio">
<el-radio label="1" size="large" border class="mb-16">
<p>智能分段推荐)</p>
<el-text type="info">不了解如何设置分段规则推荐使用智能分段</el-text>
</el-radio>
<el-radio label="2" size="large" border class="mb-16">
<p>高级分段</p>
<el-text type="info"
>用户可根据文档规范自行设置分段标识符分段长度以及清洗规则
</el-text>
<el-card shadow="never" class="card-never mt-16" v-if="radio === '2'">
<div class="set-rules__form">
<div class="form-item mb-16">
<div class="title flex align-center mb-8">
<span style="margin-right: 4px">分段标识</span>
<el-tooltip
effect="dark"
content="按照所选符号先后顺序做递归分割,分割结果超出分段长度将截取至分段长度。"
placement="right"
>
<el-icon style="font-size: 16px"><Warning /></el-icon>
</el-tooltip>
</div>
<el-select v-model="form.patterns" multiple placeholder="请选择">
<el-option
v-for="item in patternsList"
:key="item"
:label="item"
:value="item"
multiple
>
</el-option>
</el-select>
</div>
<div class="form-item mb-16">
<div class="title mb-8">分段长度</div>
<el-slider
v-model="form.limit"
show-input
:show-input-controls="false"
:min="10"
:max="1024"
/>
</div>
<div class="form-item mb-16">
<div class="title mb-8">自动清洗</div>
<el-switch v-model="form.with_filter" />
<div style="margin-top: 4px">
<el-text type="info">去掉重复多余符号空格空行制表符</el-text>
</div>
</div>
</div>
</el-card>
</el-radio>
</el-radio-group>
</div>
</el-scrollbar>
<div class="text-right">
<el-button @click="splitDocument"></el-button>
</div>
</div>
</el-col>
<el-col :span="12" class="p-24 border-l">
<div v-loading="loading">
<h4 class="title-decoration-1 mb-8">分段预览</h4>
<ParagraphPreview v-model:data="paragraphList" />
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, reactive } from 'vue'
import ParagraphPreview from '@/views/dataset/component/ParagraphPreview.vue'
import DatasetApi from '@/api/dataset'
import useStore from '@/stores'
const { dataset } = useStore()
const documentsFiles = computed(() => dataset.documentsFiles)
const patternType = ['空行', '#', '##', '###', '####', '-', '空格', '回车', '句号', '逗号', '分号']
const marks = reactive({
10: '10',
1024: '1024'
})
const radio = ref('1')
const loading = ref(false)
const paragraphList = ref<any[]>([])
const form = reactive<any>({
patterns: [] as any,
limit: 0,
with_filter: false
})
const patternsList = ref<string[]>(patternType)
function splitDocument() {
loading.value = true
let fd = new FormData()
documentsFiles.value.forEach((item) => {
if (item?.raw) {
fd.append('file', item?.raw)
}
})
if (radio.value === '2') {
Object.keys(form).forEach((key) => {
fd.append(key, form[key])
})
}
DatasetApi.postSplitDocument(fd)
.then((res: any) => {
paragraphList.value = res.data
loading.value = false
})
.catch(() => {
loading.value = false
})
}
onMounted(() => {
splitDocument()
})
defineExpose({
paragraphList
})
</script>
<style scoped lang="scss">
.set-rules {
width: 100%;
.set-rules-height {
height: var(--create-dataset-height);
}
.left-height {
max-height: calc(var(--create-dataset-height) - 105px);
overflow-x: hidden;
}
&__radio {
display: block;
.el-radio {
white-space: break-spaces;
width: 100%;
height: 100%;
padding: calc(var(--app-base-px) * 2);
line-height: 22px;
color: var(--app-text-color);
}
:deep(.el-radio__label) {
padding-left: 32px;
width: 100%;
}
:deep(.el-radio__input) {
position: absolute;
top: 30px;
}
}
&__form {
.el-select {
width: 100%;
}
.title {
font-size: 14px;
font-weight: 400;
}
}
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<LayoutContainer header="设置">
<div class="dataset-setting">
<div class="p-24" v-loading="loading">
<BaseForm ref="BaseFormRef" :data="detail" />
<div class="text-right">
<el-button @click="submit" type="primary"> 保存 </el-button>
</div>
</div>
</div>
</LayoutContainer>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import BaseForm from '@/views/dataset/component/BaseForm.vue'
import datasetApi from '@/api/dataset'
import { MsgSuccess } from '@/utils/message'
const route = useRoute()
const {
params: { datasetId }
} = route as any
const BaseFormRef = ref()
const loading = ref(false)
const detail = ref({})
async function submit() {
if (await BaseFormRef.value?.validate()) {
loading.value = true
datasetApi
.postDocument(datasetId, BaseFormRef.value.form)
.then((res) => {
MsgSuccess('保存成功')
loading.value = false
})
.catch(() => {
loading.value = false
})
}
}
function getDetail() {
loading.value = true
datasetApi
.getDatesetDetail(datasetId)
.then((res) => {
detail.value = res.data
loading.value = false
})
.catch(() => {
loading.value = false
})
}
onMounted(() => {
getDetail()
})
</script>
<style lang="scss" scoped>
.dataset-setting {
width: 70%;
margin: 0 auto;
}
</style>

View File

@ -0,0 +1,229 @@
<template>
<LayoutContainer header="文档">
<div class="main-calc-height">
<div class="p-24">
<div class="flex-between">
<el-button
type="primary"
@click="router.push({ path: '/dataset/upload', query: { id: datasetId } })"
>上传文档</el-button
>
<el-input
v-model="filterText"
placeholder="按 文档名称 搜索"
prefix-icon="Search"
class="w-240"
@change="getList"
/>
</div>
<app-table
class="mt-16"
:data="documentData"
:pagination-config="paginationConfig"
quick-create
@sizeChange="handleSizeChange"
@changePage="handleCurrentChange"
@cell-mouse-enter="cellMouseEnter"
@cell-mouse-leave="cellMouseLeave"
@creatQuick="creatQuickHandle"
@row-click="rowClickHandle"
v-loading="loading"
>
<el-table-column prop="name" label="文件名称" min-width="280">
<template #default="{ row }">
<ReadWrite
@change="editName"
:data="row.name"
:showEditIcon="row.id === currentMouseId"
/>
</template>
</el-table-column>
<el-table-column prop="char_length" label="字符数" align="right">
<template #default="{ row }">
{{ toThousands(row.char_length) }}
</template>
</el-table-column>
<el-table-column prop="paragraph_count" label="分段" align="right" />
<el-table-column prop="status" label="文件状态" min-width="90">
<template #default="{ row }">
<el-text v-if="row.status === '1'">
<el-icon class="success"><SuccessFilled /></el-icon>
</el-text>
<el-text v-else-if="row.status === '2'">
<el-icon class="danger"><CircleCloseFilled /></el-icon>
</el-text>
<el-text v-else-if="row.status === '0'">
<el-icon class="is-loading primary"><Loading /></el-icon>
</el-text>
</template>
</el-table-column>
<el-table-column prop="name" label="启动状态">
<template #default="{ row }">
<div @click.stop>
<el-switch v-model="row.is_active" @change="changeState($event, row)" />
</div>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="170">
<template #default="{ row }">
{{ datetimeFormat(row.create_time) }}
</template>
</el-table-column>
<el-table-column prop="update_time" label="更新时间" width="170">
<template #default="{ row }">
{{ datetimeFormat(row.update_time) }}
</template>
</el-table-column>
<el-table-column prop="name" label="操作" align="center">
<template #default="{ row }">
<span v-if="row.status === 2">
<el-tooltip effect="dark" content="刷新" placement="top">
<el-button type="primary" text>
<el-icon><RefreshRight /></el-icon>
</el-button>
</el-tooltip>
</span>
<span class="ml-4">
<el-tooltip effect="dark" content="删除" placement="top">
<el-button type="primary" text @click.stop="deleteDocument(row)">
<el-icon><Delete /></el-icon>
</el-button>
</el-tooltip>
</span>
</template>
</el-table-column>
</app-table>
</div>
</div>
</LayoutContainer>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import datasetApi from '@/api/dataset'
import { toThousands } from '@/utils/utils'
import { datetimeFormat } from '@/utils/time'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
const router = useRouter()
const route = useRoute()
const {
params: { datasetId }
} = route as any
const loading = ref(false)
const filterText = ref('')
const documentData = ref<any[]>([])
const currentMouseId = ref(null)
const paginationConfig = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
function rowClickHandle(row: any) {
router.push({ path: `/dataset/${datasetId}/${row.id}` })
}
//
function creatQuickHandle(val: string) {
loading.value = true
const obj = { name: val }
datasetApi
.postDocument(datasetId, obj)
.then((res) => {
getList()
MsgSuccess('创建成功')
})
.catch(() => {
loading.value = false
})
}
function deleteDocument(row: any) {
MsgConfirm(
`是否删除文档:${row.name} ?`,
`此文档下的 ${row.paragraph_count} 个分段都会被删除,请谨慎操作。`,
{
confirmButtonText: '删除',
confirmButtonClass: 'danger'
}
)
.then(() => {
loading.value = true
datasetApi
.delDocument(datasetId, row.id)
.then(() => {
MsgSuccess('删除成功')
getList()
})
.catch(() => {
loading.value = false
})
})
.catch(() => {})
}
//
function updateData(documentId: string, data: any) {
loading.value = true
datasetApi
.putDocument(datasetId, documentId, data)
.then((res) => {
const index = documentData.value.findIndex((v) => v.id === documentId)
documentData.value.splice(index, 1, res.data)
MsgSuccess('修改成功')
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function changeState(bool: Boolean, row: any) {
const obj = {
is_active: bool
}
currentMouseId.value && updateData(row.id, obj)
}
function editName(val: string) {
const obj = {
name: val
}
currentMouseId.value && updateData(currentMouseId.value, obj)
}
function cellMouseEnter(row: any) {
currentMouseId.value = row.id
}
function cellMouseLeave() {
currentMouseId.value = null
}
function handleSizeChange(val: number) {
console.log(`${val} items per page`)
}
function handleCurrentChange(val: number) {
console.log(`current page: ${val}`)
}
function getList() {
loading.value = true
datasetApi
.getDocument(datasetId as string, filterText.value)
.then((res) => {
documentData.value = res.data
paginationConfig.total = res.data.length
loading.value = false
})
.catch(() => {
loading.value = false
})
}
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped></style>

View File

@ -1,7 +1,7 @@
<template>
<login-layout>
<LoginContainer>
<h4 class="mb-20">忘记密码</h4>
<h4 class="mb-16">忘记密码</h4>
<el-form
class="register-form"
ref="resetPasswordFormRef"
@ -34,7 +34,7 @@
</el-input>
<el-button
size="large"
class="send-email-button ml-10"
class="send-email-button ml-16"
@click="sendEmail"
:loading="loading"
>获取验证码</el-button
@ -45,7 +45,7 @@
<el-button type="primary" class="login-submit-button w-full" @click="checkCode"
>立即验证</el-button
>
<div class="operate-container mt-10">
<div class="operate-container mt-8">
<el-button
class="register"
@click="router.push('/login')"

View File

@ -1,6 +1,6 @@
<template>
<login-layout v-loading="loading">
<LoginContainer subTitle="欢迎使用智能客服管理平台">
<LoginContainer subTitle="欢迎使用 MaxKB 管理平台">
<el-form class="login-form" :rules="rules" :model="loginForm" ref="loginFormRef">
<el-form-item>
<el-input

View File

@ -1,7 +1,7 @@
<template>
<login-layout>
<LoginContainer>
<h4 class="mb-20">注册</h4>
<h4 class="mb-16">注册</h4>
<el-form class="register-form" :model="registerForm" :rules="rules" ref="registerFormRef">
<el-form-item prop="username">
<el-input
@ -69,7 +69,7 @@
</el-input>
<el-button
size="large"
class="send-email-button ml-10"
class="send-email-button ml-16"
@click="sendEmail"
:loading="sendEmailLoading"
>获取验证码</el-button
@ -80,7 +80,7 @@
<el-button type="primary" class="login-submit-button w-full" @click="register"
>注册</el-button
>
<div class="operate-container mt-10">
<div class="operate-container mt-8">
<el-button
class="register"
@click="router.push('/login')"

View File

@ -1,7 +1,7 @@
<template>
<login-layout>
<LoginContainer>
<h4 class="mb-20">修改密码</h4>
<h4 class="mb-16">修改密码</h4>
<el-form
class="reset-password-form"
ref="resetPasswordFormRef"
@ -40,7 +40,7 @@
<el-button type="primary" class="login-submit-button w-full" @click="resetPassword"
>确认修改</el-button
>
<div class="operate-container mt-10">
<div class="operate-container mt-8">
<el-button
class="register"
@click="router.push('/login')"
@ -63,6 +63,9 @@ import type { FormInstance, FormRules } from 'element-plus'
import UserApi from '@/api/user'
const router = useRouter()
const route = useRoute()
const {
params: { code, email }
} = route
const resetPasswordForm = ref<ResetPasswordRequest>({
password: '',
re_password: '',
@ -71,8 +74,6 @@ const resetPasswordForm = ref<ResetPasswordRequest>({
})
onMounted(() => {
const code = route.params.code
const email = route.params.email
if (code && email) {
resetPasswordForm.value.code = code as string
resetPasswordForm.value.email = email as string
@ -133,4 +134,4 @@ const resetPassword = () => {
</script>
<style lang="scss" scope>
@import '../index.scss';
</style>
</style>

View File

@ -0,0 +1,147 @@
<template>
<el-dialog
:title="title"
v-model="dialogVisible"
width="800"
class="paragraph-dialog"
destroy-on-close
>
<el-row v-loading="loading">
<el-col :span="16" class="p-24">
<el-scrollbar>
<div style="height: 350px">
<div class="flex-between mb-16">
<div class="bold title align-center">分段内容</div>
<el-button text @click="isEdit = true" v-if="problemId && !isEdit">
<el-icon><EditPen /></el-icon>
</el-button>
</div>
<ParagraphForm ref="paragraphFormRef" :data="detail" :isEdit="isEdit" />
</div>
<div class="text-right" v-if="problemId && isEdit">
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
<el-button type="primary" :disabled="loading" @click="submitHandle"> </el-button>
</div>
</el-scrollbar>
</el-col>
<el-col :span="8" class="border-l p-24">
<!-- 关联问题 -->
<ProblemComponent :problemId="problemId" ref="ProblemRef" />
</el-col>
</el-row>
<template #footer v-if="!problemId">
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
<el-button type="primary" @click="submitHandle"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import ParagraphForm from '@/views/paragraph/component/ParagraphForm.vue'
import ProblemComponent from '@/views/paragraph/component/ProblemComponent.vue'
import datasetApi from '@/api/dataset'
import useStore from '@/stores'
const props = defineProps({
title: String
})
const { paragraph } = useStore()
const route = useRoute()
const {
params: { datasetId, documentId }
} = route as any
const emit = defineEmits(['refresh'])
const ProblemRef = ref()
const paragraphFormRef = ref<FormInstance>()
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
const problemId = ref('')
const detail = ref<any>({})
const isEdit = ref(false)
watch(dialogVisible, (bool) => {
if (!bool) {
problemId.value = ''
detail.value = {}
isEdit.value = false
}
})
const open = (data: any) => {
if (data) {
detail.value.title = data.title
detail.value.content = data.content
problemId.value = data.id
} else {
isEdit.value = true
}
dialogVisible.value = true
}
const submitHandle = async () => {
if (await paragraphFormRef.value?.validate()) {
loading.value = true
if (problemId.value) {
paragraph
.asyncPutParagraph(datasetId, documentId, problemId.value, paragraphFormRef.value?.form)
.then(() => {
emit('refresh')
loading.value = false
dialogVisible.value = false
})
.catch(() => {
loading.value = false
})
} else {
const obj =
ProblemRef.value.problemList.length > 0
? {
problem_list: ProblemRef.value.problemList,
...paragraphFormRef.value?.form
}
: paragraphFormRef.value?.form
datasetApi
.postParagraph(datasetId, documentId, obj)
.then((res) => {
emit('refresh')
loading.value = false
dialogVisible.value = false
})
.catch(() => {
loading.value = false
})
}
}
}
defineExpose({ open })
</script>
<style lang="scss" scope>
.paragraph-dialog {
.el-dialog__header {
padding-bottom: 16px;
}
.el-dialog__body {
border-top: 1px solid var(--el-border-color);
padding: 0 !important;
}
.el-dialog__footer {
padding-top: 16px;
border-top: 1px solid var(--el-border-color);
}
.title {
color: var(--app-text-color);
}
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<el-form ref="paragraphFormRef" :model="form" label-position="top" :rules="rules" @submit.prevent>
<el-form-item label="分段标题">
<el-input v-if="isEdit" v-model="form.title" placeholder="请输入分段标题"> </el-input>
<span v-else>{{ form.title }}</span>
</el-form-item>
<el-form-item label="分段内容" prop="content">
<el-input
v-if="isEdit"
v-model="form.content"
placeholder="请输入分段内容"
maxlength="1024"
show-word-limit
:rows="8"
type="textarea"
>
</el-input>
<span v-else>{{ form.content }}</span>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, onUnmounted, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
const props = defineProps({
data: {
type: Object,
default: () => {}
},
isEdit: Boolean
})
const form = ref<any>({
title: '',
content: ''
})
const rules = reactive<FormRules>({
content: [{ required: true, message: '请输入分段内容', trigger: 'blur' }]
})
const paragraphFormRef = ref<FormInstance>()
watch(
() => props.data,
(value) => {
if (value && JSON.stringify(value) !== '{}') {
form.value.title = value.title
form.value.content = value.content
}
},
{
//
immediate: true
}
)
//
function validate() {
if (!paragraphFormRef.value) return
return paragraphFormRef.value.validate((valid: any) => {
return valid
})
}
onUnmounted(() => {
form.value = {
title: '',
content: ''
}
})
defineExpose({
validate,
form
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,145 @@
<template>
<p class="bold title mb-16">
关联问题 <el-divider direction="vertical" />
<el-button text @click="addProblem">
<el-icon><Plus /></el-icon>
</el-button>
</p>
<div v-loading="loading" style="height: 350px">
<el-scrollbar>
<el-input
ref="inputRef"
v-if="isAddProblem"
v-model="problemValue"
@change="addProblemHandle"
placeholder="请输入问题,回车保存"
class="mb-8"
autofocus
/>
<template v-for="(item, index) in problemList" :key="index">
<TagEllipsis
@close="delProblemHandle(item, index)"
class="question-tag"
type="info"
effect="plain"
closable
>
{{ item.content }}
</TagEllipsis>
</template>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted, onUnmounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import datasetApi from '@/api/dataset'
const props = defineProps({
problemId: String
})
const route = useRoute()
const {
params: { datasetId, documentId }
} = route as any
const inputRef = ref()
const loading = ref(false)
const isAddProblem = ref(false)
const problemValue = ref('')
const problemList = ref<any[]>([])
watch(
() => props.problemId,
(value) => {
if (value) {
getProblemList()
}
},
{
//
immediate: true
}
)
function delProblemHandle(item: any, index: number) {
loading.value = true
if (item.id) {
datasetApi
.delProblem(datasetId, documentId, props.problemId || '', item.id)
.then((res) => {
getProblemList()
})
.catch(() => {
loading.value = false
})
} else {
problemList.value.splice(index, 1)
loading.value = false
}
}
function getProblemList() {
loading.value = true
datasetApi
.getProblem(datasetId, documentId, props.problemId || '')
.then((res) => {
problemList.value = res.data
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function addProblem() {
isAddProblem.value = true
nextTick(() => {
inputRef.value?.focus()
})
}
function addProblemHandle(val: string) {
if (val) {
const obj = {
content: val
}
loading.value = true
if (props.problemId) {
datasetApi
.postProblem(datasetId, documentId, props.problemId, obj)
.then((res) => {
getProblemList()
problemValue.value = ''
isAddProblem.value = false
})
.catch(() => {
loading.value = false
})
} else {
problemList.value.unshift(obj)
problemValue.value = ''
isAddProblem.value = false
loading.value = false
}
}
}
onMounted(() => {})
onUnmounted(() => {
problemList.value = []
problemValue.value = ''
isAddProblem.value = false
})
defineExpose({
problemList
})
</script>
<style scoped lang="scss">
.question-tag {
width: 217px;
}
</style>

View File

@ -0,0 +1,210 @@
<template>
<LayoutContainer :header="documentDetail?.name" back-to="-1" class="document-detail">
<template #header>
<div class="document-detail__header">
<el-button @click="addParagraph" type="primary" :disabled="loading"> 添加分段 </el-button>
</div>
</template>
<div class="document-detail__main p-16" v-loading="loading">
<div class="flex-between p-8">
<span>{{ paragraphDetail.length }} 段落</span>
<el-input
v-model="search"
placeholder="搜索"
class="input-with-select"
style="width: 260px"
>
<template #prepend>
<el-select v-model="searchType" placeholder="Select" style="width: 80px">
<el-option label="标题" value="title" />
<el-option label="内容" value="content" />
</el-select>
</template>
</el-input>
</div>
<el-scrollbar>
<div class="document-detail-height">
<el-row>
<el-col
:xs="24"
:sm="12"
:md="8"
:lg="6"
:xl="4"
v-for="(item, index) in paragraphDetail"
:key="index"
class="p-8"
>
<CardBox
shadow="hover"
:title="item.title"
:description="item.content"
class="document-card cursor"
:class="item.is_active ? '' : 'disabled'"
:showIcon="false"
@click="editParagraph(item)"
>
<div class="active-button">
<el-switch v-model="item.is_active" @change="changeState($event, item)" />
</div>
<template #footer>
<div class="footer-content flex-between">
<span> {{ numberFormat(item?.content.length) || 0 }} 字符 </span>
<el-tooltip effect="dark" content="删除" placement="top">
<el-button text @click.stop="deleteParagraph(item)" class="delete-button">
<el-icon><Delete /></el-icon>
</el-button>
</el-tooltip>
</div>
</template>
</CardBox>
</el-col>
</el-row>
</div>
</el-scrollbar>
</div>
<ParagraphDialog ref="ParagraphDialogRef" :title="title" @refresh="refresh" />
</LayoutContainer>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import datasetApi from '@/api/dataset'
import ParagraphDialog from './component/ParagraphDialog.vue'
import { numberFormat } from '@/utils/utils'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import useStore from '@/stores'
const { paragraph } = useStore()
const route = useRoute()
const {
params: { datasetId, documentId }
} = route as any
const ParagraphDialogRef = ref()
const loading = ref(false)
const documentDetail = ref<any>({})
const paragraphDetail = ref<any[]>([])
const title = ref('')
const search = ref('')
const searchType = ref('title')
function changeState(bool: Boolean, row: any) {
const obj = {
is_active: bool
}
loading.value = true
paragraph
.asyncPutParagraph(datasetId, documentId, row.id, obj)
.then((res) => {
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function deleteParagraph(row: any) {
MsgConfirm(`是否删除段落:${row.title} ?`, `删除后无法恢复,请谨慎操作。`, {
confirmButtonText: '删除',
confirmButtonClass: 'danger'
})
.then(() => {
loading.value = true
datasetApi
.delParagraph(datasetId, documentId, row.id)
.then(() => {
MsgSuccess('删除成功')
getParagraphDetail()
})
.catch(() => {
loading.value = false
})
})
.catch(() => {})
}
function addParagraph() {
title.value = '添加分段'
ParagraphDialogRef.value.open()
}
function editParagraph(row: any) {
title.value = '分段详情'
ParagraphDialogRef.value.open(row)
}
function getDetail() {
loading.value = true
datasetApi
.getDocumentDetail(datasetId, documentId)
.then((res) => {
documentDetail.value = res.data
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function getParagraphDetail() {
loading.value = true
datasetApi
.getParagraph(datasetId, documentId)
.then((res) => {
paragraphDetail.value = res.data
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function refresh() {
getParagraphDetail()
}
onMounted(() => {
getDetail()
getParagraphDetail()
})
</script>
<style lang="scss" scoped>
.document-detail {
&__header {
position: absolute;
right: calc(var(--app-base-px) * 3);
}
.document-detail-height {
height: calc(var(--app-main-height) - 75px);
}
.document-card {
height: 210px;
background: var(--app-layout-bg-color);
border: 1px solid var(--app-layout-bg-color);
&:hover {
background: #ffffff;
border: 1px solid var(--el-border-color);
}
&.disabled {
background: var(--app-layout-bg-color);
border: 1px solid var(--app-layout-bg-color);
:deep(.description) {
color: var(--app-border-color-dark);
}
:deep(.title) {
color: var(--app-border-color-dark);
}
}
:deep(.description) {
-webkit-line-clamp: 5 !important;
height: 110px;
}
.active-button {
position: absolute;
right: 16px;
top: 16px;
}
}
}
</style>

View File

@ -1,26 +1,28 @@
<template>
<el-input v-model="filterText" placeholder="搜索" prefix-icon="Search" class="mb-16" />
<el-table :data="data" :max-height="tableHeight">
<el-table-column prop="name" label="数据集名称" />
<el-table-column label="管理" align="center">
<template #header>
<!-- <template #header>
<el-checkbox
v-model="allChecked[MANAGE]"
label="管理"
@change="handleCheckAllChange($event, MANAGE)"
/>
</template>
</template> -->
<template #default="{ row }">
<el-checkbox v-model="row.operate[MANAGE]" @change="checkedOperateChange(MANAGE, row)" />
</template>
</el-table-column>
<el-table-column label="使用" align="center">
<template #header>
<!-- <template #header>
<el-checkbox
v-model="allChecked[USE]"
label="使用"
@change="handleCheckAllChange($event, USE)"
/>
</template>
</template> -->
<template #default="{ row }">
<el-checkbox v-model="row.operate[USE]" @change="checkedOperateChange(USE, row)" />
</template>
@ -49,6 +51,8 @@ const allChecked: any = ref({
const tableHeight = ref(0)
const filterText = ref('')
watch(
() => props.data,
(val) => {
@ -74,7 +78,7 @@ function handleCheckAllChange(val: string | number | boolean, Name: string | num
}
}
function checkedOperateChange(Name: string | number, row: any) {
if (Name === MANAGE) {
if (Name === MANAGE && row.operate[MANAGE]) {
props.data.map((item: any) => {
if (item.id === row.id) {
item.operate[USE] = true

View File

@ -1,55 +1,49 @@
<template>
<LayoutContent header="团队管理">
<LayoutContainer header="团队管理">
<div class="team-manage flex main-calc-height">
<div class="team-member p-15 border-r">
<h4>团队成员</h4>
<div class="text-right">
<div class="team-member p-8 border-r">
<div class="flex-between p-16">
<h4>成员</h4>
<el-button type="primary" link @click="addMember">
<AppIcon iconName="app-add-users" class="add-user-icon" />添加成员
<AppIcon iconName="app-add-users" class="add-user-icon" />
</el-button>
</div>
<div class="mt-10">
<el-input v-model="filterText" placeholder="请输入用户名搜索" suffix-icon="Search" />
<div class="team-member-input">
<el-input v-model="filterText" placeholder="请输入用户名搜索" prefix-icon="Search" />
</div>
<div class="member-list mt-10" v-loading="loading">
<el-scrollbar>
<ul v-if="filterMember.length > 0">
<template v-for="(item, index) in filterMember" :key="index">
<li
@click.prevent="clickMemberHandle(item.id)"
:class="currentUser === item.id ? 'active' : ''"
class="border-b-light flex-between p-15 cursor"
>
<div>
<span class="mr-10">{{ item.username }}</span>
<el-tag effect="dark" v-if="isManage(item.type)"></el-tag>
<el-tag effect="dark" type="warning" v-else></el-tag>
</div>
<span @click.stop>
<el-dropdown trigger="click" v-if="!isManage(item.type)">
<span class="cursor">
<el-icon><MoreFilled /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click.prevent="deleteMember(item)"
>移除</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
<common-list
:data="filterMember"
class="mt-8"
v-loading="loading"
@click="clickMemberHandle"
>
<template #default="{ row }">
<div class="flex-between">
<div>
<span class="mr-8">{{ row.username }}</span>
<el-tag v-if="isManage(row.type)" class="default-tag"></el-tag>
<el-tag type="warning" v-else></el-tag>
</div>
<div @click.stop style="margin-top: 5px">
<el-dropdown trigger="click" v-if="!isManage(row.type)">
<span class="cursor">
<el-icon class="rotate-90"><MoreFilled /></el-icon>
</span>
</li>
</template>
</ul>
<el-empty description="暂无数据" v-else />
</el-scrollbar>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click.prevent="deleteMember(row)">移除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
</common-list>
</div>
<div class="permission-setting flex" v-loading="rLoading">
<div class="team-manage__table p-15">
<div class="team-manage__table p-24">
<h4>权限设置</h4>
<el-tabs v-model="activeName" class="demo-tabs">
<el-tabs v-model="activeName" class="team-manage__tabs">
<el-tab-pane
v-for="item in settingTags"
:key="item.value"
@ -61,13 +55,13 @@
</el-tabs>
</div>
<div class="team-manage__footer border-t p-15 flex">
<div class="team-manage__footer border-t p-16 flex">
<el-button type="primary" @click="submitPermissions"></el-button>
</div>
</div>
</div>
<CreateMemberDialog ref="CreateMemberRef" @refresh="refresh" />
</LayoutContent>
</LayoutContainer>
</template>
<script lang="ts" setup>
@ -154,13 +148,15 @@ function MemberPermissions(id: String) {
}
function deleteMember(row: TeamMember) {
MsgConfirm({
title: `是否移除成员:${row.username}`,
decription: '移除后将会取消成员拥有的数据集和应用权限。',
confirmButtonText: '移除',
}, {
confirmButtonClass: 'danger',
})
MsgConfirm(
`是否移除成员:${row.username}?`,
'移除后将会取消成员拥有的数据集和应用权限。',
{
confirmButtonText: '移除',
confirmButtonClass: 'danger'
}
)
.then(() => {
loading.value = true
TeamApi.delTeamMember(row.id)
@ -179,9 +175,9 @@ function isManage(type: String) {
return type === 'manage'
}
function clickMemberHandle(id: String) {
currentUser.value = id
MemberPermissions(id)
function clickMemberHandle(item: any) {
currentUser.value = item.id
MemberPermissions(item.id)
}
function addMember() {
CreateMemberRef.value?.open()
@ -214,30 +210,30 @@ onMounted(() => {
<style lang="scss" scoped>
.team-manage {
.add-user-icon {
margin-right: 5px;
font-size: 20px;
font-size: 17px;
}
.team-member-input {
padding: 0 calc(var(--app-base-px) * 2);
}
.team-member {
box-sizing: border-box;
width: var(--team-manage-left-width);
min-width: var(--team-manage-left-width);
.member-list {
li {
&.active {
background: var(--el-color-primary-light-9);
}
}
}
width: var(--setting-left-width);
min-width: var(--setting-left-width);
}
.permission-setting {
box-sizing: border-box;
width: calc(100% - var(--team-manage-left-width) - 5px);
width: calc(100% - var(--setting-left-width));
flex-direction: column;
}
.team-manage__table {
&__tabs {
margin-top: 10px;
}
&__table {
flex: 1;
}
.team-manage__footer {
&__footer {
flex: 0 0 auto;
justify-content: right;
}

View File

@ -0,0 +1,44 @@
<template>
<LayoutContainer header="模版管理">
<div class="template-manage flex main-calc-height">
<div class="template-manage__left p-8 border-r">
<h4 class="p-16">供应商</h4>
<common-list :data="list" class="mt-8" v-loading="loading" @click="clickHandle">
<template #default="{ row }">
<div class="flex">
<img src="@/assets/icon_document.svg" alt="" class="mr-8" />
<span>{{ row.name }}</span>
</div>
</template>
</common-list>
</div>
<div class="template-manage__right p-24">
<h4>全部模型</h4>
</div>
</div>
</LayoutContainer>
</template>
<script lang="ts" setup>
import { onMounted, ref, reactive, watch } from 'vue'
const loading = ref(false)
const list = ref([
{
name: '1111'
}
])
function clickHandle(row: any) {}
onMounted(() => {})
</script>
<style lang="scss" scoped>
.template-manage {
&__left {
box-sizing: border-box;
width: var(--setting-left-width);
min-width: var(--setting-left-width);
}
}
</style>