feat(user): user manage

This commit is contained in:
dataeaseShu
2025-07-14 18:26:33 +08:00
parent fbc63e4ead
commit d685280269
27 changed files with 2221 additions and 83 deletions
+1
View File
@@ -37,6 +37,7 @@
"vue-dompurify-html": "^5.3.0",
"vue-i18n": "^9.14.4",
"vue-router": "^4.5.0",
"vue-types": "^6.0.0",
"web-storage-cache": "^1.1.1"
},
"devDependencies": {
+16
View File
@@ -0,0 +1,16 @@
import { request } from '@/utils/request'
export const userImportApi = {
downExcelTemplateApi: () => request.post('/user/excelTemplate', {}, { responseType: 'blob' }),
importUserApi: (data: any) =>
request.post('/user/batchImport', data, {
headers: {
'Content-Type': 'multipart/form-data',
},
}),
downErrorRecordApi: (key: string) =>
request.get(`/user/errorRecord/${key}`, { responseType: 'blob' }),
clearErrorApi: (key: string) => {
request.get(`/user/clearErrorRecord/${key}`)
},
}
+9
View File
@@ -0,0 +1,9 @@
import { request } from '@/utils/request'
export const workspaceUserList = (params: any, pageNum: number, pageSize: number) =>
request.get(`/system/workspace/uws/pager/${pageNum}/${pageSize}`, { params })
export const workspaceCreate = (data: any) => request.post('/system/workspace/uws', data)
export const workspaceDelete = (data: any) => request.delete('/system/workspace/uws', data)
export const workspaceList = () => request.get('/system/workspace')
export const workspaceDetail = (id: any) => request.get(`/system/workspace/${id}`)
@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.3334 4.33333C10.3334 2.49233 8.84004 1 7.00004 1C5.16004 1 3.66671 2.49233 3.66671 4.33333C3.66671 6.17433 5.16004 7.66667 7.00004 7.66667C8.84004 7.66667 10.3334 6.17433 10.3334 4.33333ZM7.00004 6.33333C5.89671 6.33333 5.00004 5.438 5.00004 4.33333C5.00004 3.22867 5.89671 2.33333 7.00004 2.33333C8.10337 2.33333 9.00004 3.22867 9.00004 4.33333C9.00004 5.438 8.10337 6.33333 7.00004 6.33333Z" fill="#646A73"/>
<path d="M2.66671 12.6667C2.66671 12.4363 2.69671 12.213 2.75004 12C3.04671 10.85 4.09004 10 5.33337 10H8.01668C8.20077 10 8.35001 9.85076 8.35001 9.66667V9C8.35001 8.81591 8.20077 8.66667 8.01668 8.66667H5.33337C3.12337 8.66667 1.33337 10.4577 1.33337 12.6667V14C1.33337 14.3683 1.63337 14.6667 2.00004 14.6667H8.01668C8.20077 14.6667 8.35001 14.5174 8.35001 14.3333V13.6667C8.35001 13.4826 8.20077 13.3333 8.01668 13.3333H2.66671V12.6667Z" fill="#646A73"/>
<path d="M12.1312 10.3738L13.11 11.3526H9.66671C9.48261 11.3526 9.33337 11.5019 9.33337 11.686V12.3526C9.33337 12.5367 9.48261 12.686 9.66671 12.686H13.1188L12.1312 13.6736C12.001 13.8038 12.001 14.0148 12.1312 14.145L12.6026 14.6164C12.7328 14.7466 12.9438 14.7466 13.074 14.6164L15.431 12.2594C15.5612 12.1292 15.5612 11.9182 15.431 11.788L13.074 9.43096C12.9438 9.30079 12.7328 9.30079 12.6026 9.43096L12.1312 9.90237C12.001 10.0325 12.001 10.2436 12.1312 10.3738Z" fill="#646A73"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<path d="M5.99993 5.29289L9.85868 1.43413C9.95632 1.3365 10.1146 1.3365 10.2122 1.43413L10.5658 1.78769C10.6634 1.88532 10.6634 2.04361 10.5658 2.14124L6.70703 6L10.5658 9.85875C10.6634 9.95639 10.6634 10.1147 10.5658 10.2123L10.2122 10.5659C10.1146 10.6635 9.95632 10.6635 9.85868 10.5659L5.99993 6.7071L2.14117 10.5659C2.04354 10.6635 1.88525 10.6635 1.78762 10.5659L1.43406 10.2123C1.33643 10.1147 1.33643 9.95639 1.43406 9.85875L5.29282 6L1.43406 2.14124C1.33643 2.04361 1.33643 1.88532 1.43406 1.78769L1.78762 1.43413C1.88525 1.3365 2.04354 1.3365 2.14117 1.43413L5.99993 5.29289Z"/>
</svg>

After

Width:  |  Height:  |  Size: 680 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 8.66667H6C4.15905 8.66667 2 9.95432 2 12.2667V14.0667C2 14.398 2.29848 14.6667 2.66667 14.6667H13.3333C13.7015 14.6667 14 14.398 14 14.0667V12.2667C14 9.95237 11.841 8.66667 10 8.66667Z" fill="#8F959E"/>
<path d="M4.66667 4.66667C4.66667 6.50762 6.15905 8 8 8C9.84095 8 11.3333 6.50762 11.3333 4.66667C11.3333 2.82572 9.84095 1.33333 8 1.33333C6.15905 1.33333 4.66667 2.82572 4.66667 4.66667Z" fill="#8F959E"/>
</svg>

After

Width:  |  Height:  |  Size: 527 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.35399 11.2488H7.62467L14.3787 4.4946C14.6108 4.26253 14.6113 3.88643 14.3798 3.65375L12.3342 1.59735C12.1017 1.36485 11.7247 1.36485 11.4922 1.59735L11.0723 2.01722L11.074 2.01887L4.75864 8.3828V10.6535C4.75864 10.9823 5.02519 11.2488 5.35399 11.2488ZM11.8943 2.84351L13.1 4.05552L12.0797 5.07678L10.8803 3.87736L11.8943 2.84351ZM11.2382 5.91913L7.1225 10.0388H7.12068L5.9687 8.88498L10.0465 4.72746L11.2382 5.91913Z" fill="#646A73"/>
<path d="M8.76313 2.316H2.44339C2.07805 2.316 1.78189 2.61216 1.78189 2.9775V13.5615C1.78189 13.9268 2.07805 14.223 2.44339 14.223H13.0274C13.3927 14.223 13.6889 13.9268 13.6889 13.5615V7.1614L12.3576 8.50072V12.8982H3.11313V3.6408H7.46239L8.76313 2.316Z" fill="#646A73"/>
</svg>

After

Width:  |  Height:  |  Size: 823 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,3 @@
import DrawerFilter from './src/DrawerFilter.vue'
export { DrawerFilter }
@@ -0,0 +1,115 @@
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes'
import { reactive } from 'vue'
import { useEmitt } from '@/utils/useEmitt'
const props = defineProps({
optionList: propTypes.arrayOf(
propTypes.shape({
id: propTypes.string,
name: propTypes.string,
})
),
index: propTypes.number,
title: propTypes.string,
})
const state = reactive<{ activeStatus: any[]; optionList: any[] }>({
activeStatus: [],
optionList: [],
})
const nodeChange = (id: string | number) => {
const len = state.activeStatus.indexOf(id)
if (len >= 0) {
state.activeStatus.splice(len, 1)
} else {
state.activeStatus.push(id)
}
emits('filter-change', state.activeStatus)
}
const emits = defineEmits(['filter-change'])
const clear = (index: number) => {
if (index !== props.index) return
state.activeStatus = []
}
useEmitt({
name: 'clear-drawer_main',
callback: clear,
})
</script>
<template>
<div class="draw-filter_enum">
<span>{{ title }}</span>
<div class="filter-item">
<span
v-for="ele in props.optionList"
:key="ele.id"
class="item"
:class="[state.activeStatus.includes(ele.id) ? 'active' : '']"
@click="nodeChange(ele.id)"
>{{ ele.name }}</span
>
</div>
</div>
</template>
<style lang="less" scope>
.draw-filter_enum {
margin-bottom: 4px;
> :nth-child(1) {
color: var(--deTextSecondary, #1f2329);
font-family: var(--de-custom_font, 'PingFang');
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 22px;
white-space: nowrap;
}
.filter-item {
margin-top: 8px;
.item,
.more {
font-family: var(--de-custom_font, 'PingFang');
white-space: nowrap;
font-size: 14px;
font-weight: 400;
line-height: 24px;
margin-right: 12px;
text-align: center;
padding: 1px 6px;
background: var(--deTextPrimary5, #f5f6f7);
color: var(--deTextPrimary, #1f2329);
border-radius: 2px;
cursor: pointer;
display: inline-block;
margin-bottom: 12px;
}
.active,
.more:hover {
background: var(--primary10, var(--ed-color-primary-1a, #1cba9033));
color: var(--primaryselect, #0c296e);
}
.more {
white-space: nowrap;
display: inline-flex;
align-items: center;
i {
margin-right: 5px;
}
}
}
}
</style>
<style lang="less">
.filter-popper {
padding: 0 !important;
background: #fff !important;
}
</style>
@@ -0,0 +1,91 @@
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes'
import { ElSelect, ElOption } from 'element-plus-secondary'
import { computed, reactive } from 'vue'
import { useEmitt } from '@/utils/useEmitt'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
optionList: propTypes.arrayOf(
propTypes.shape({
id: propTypes.string,
name: propTypes.string,
})
),
index: propTypes.number,
title: propTypes.string,
property: {
type: Object,
default: () => ({}),
},
})
const state = reactive({
activeStatus: [],
})
const emits = defineEmits(['filter-change'])
const selectStatus = (ids: any[]) => {
emits(
'filter-change',
ids.map((item) => item.id || item.value)
)
}
const optionListNotSelect = computed(() => {
return [...(props.optionList as any[])]
})
const clear = (index: number) => {
if (index !== props.index) return
state.activeStatus = []
}
useEmitt({
name: 'clear-drawer_main',
callback: clear,
})
</script>
<template>
<div class="draw-filter_base">
<span>{{ title }}</span>
<div class="filter-item">
<el-select
v-model="state.activeStatus"
:teleported="false"
style="width: 100%"
value-key="id"
filterable
:placeholder="t('datasource.Please_select') + props.property.placeholder"
multiple
@change="selectStatus"
>
<el-option
v-for="item in optionListNotSelect"
:key="item.name"
:label="item.name"
:value="item"
/>
</el-select>
</div>
</div>
</template>
<style lang="less" scope>
.draw-filter_base {
margin-bottom: 16px;
> :nth-child(1) {
color: var(--deTextSecondary, #1f2329);
font-family: var(--de-custom_font, 'PingFang');
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 22px;
white-space: nowrap;
}
.filter-item {
margin-top: 8px;
}
}
</style>
@@ -0,0 +1,3 @@
import DrawerMain from './src/DrawerMain.vue'
export { DrawerMain }
@@ -0,0 +1,163 @@
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ElDrawer, ElButton } from 'element-plus-secondary'
import { propTypes } from '@/utils/propTypes'
import DrawerFilter from '@/components/drawer-filter/src/DrawerFilter.vue'
import DrawerEnumFilter from '@/components/drawer-filter/src/DrawerEnumFilter.vue'
import { useEmitt } from '@/utils/useEmitt'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
filterOptions: propTypes.arrayOf(
propTypes.shape({
type: propTypes.string,
field: propTypes.string,
option: propTypes.array,
title: propTypes.string,
property: propTypes.shape({}),
})
),
title: propTypes.string,
})
const componentList = computed(() => {
return props.filterOptions as any[]
})
const state = reactive<{ conditions: any[] }>({
conditions: [],
})
const userDrawer = ref(false)
const init = () => {
userDrawer.value = true
}
const clear = (index: any) => {
useEmitt().emitter.emit('clear-drawer_main', index)
}
const cleanrInnerValue = (index: number) => {
const field = componentList.value[index]?.field
if (!field) {
return
}
clear(index)
for (let i = 0; i < state.conditions.length; i++) {
if (state.conditions[i].field === field) {
state.conditions[i].value = []
}
}
}
const clearInnerTag = (index?: number) => {
if (isNaN(index as number)) {
for (let i = 0; i < componentList.value.length; i++) {
clear(i)
}
return
}
const condition = state.conditions[index as number]
const field = condition?.field
for (let i = 0; i < componentList.value.length; i++) {
if (componentList.value[i].field === field) {
clear(i)
}
}
}
const clearFilter = (id?: number) => {
clearInnerTag(id)
if (isNaN(id as number)) {
const len = state.conditions.length
state.conditions.splice(0, len)
} else {
state.conditions.splice(id as number, 1)
}
trigger()
}
const filterChange = (value: any, field: any, operator: any) => {
let exits = false
let len = state.conditions.length
while (len--) {
const condition = state.conditions[len]
if (condition.field === field) {
exits = true
condition['value'] = value
}
if (!condition?.value?.length) {
state.conditions.splice(len, 1)
}
}
if (!exits && value?.length) {
state.conditions.push({ field, value, operator })
}
treeFilterChange(value, field, operator)
}
const reset = () => {
clearFilter()
userDrawer.value = false
}
const close = () => {
userDrawer.value = false
}
const emits = defineEmits(['trigger-filter', 'tree-filter-change'])
const trigger = () => {
emits('trigger-filter', state.conditions)
}
const treeFilterChange = (value: any, field: any, operator: any) => {
emits('tree-filter-change', {
value,
field,
operator,
})
}
defineExpose({
init,
clearFilter,
close,
cleanrInnerValue,
})
</script>
<template>
<el-drawer
v-model="userDrawer"
:title="t('user.filter_conditions')"
size="600px"
modal-class="drawer-main-container"
direction="rtl"
>
<div v-for="(component, index) in componentList" :key="index">
<drawer-filter
v-if="component.type === 'select'"
:option-list="component.option"
:title="component.title"
:index="index"
:property="component.property"
@filter-change="(v) => filterChange(v, component.field, 'in')"
/>
<drawer-enum-filter
v-if="component.type === 'enum'"
:option-list="component.option"
:title="component.title"
:index="index"
@filter-change="(v) => filterChange(v, component.field, 'in')"
/>
</div>
<template #footer>
<el-button secondary @click="reset">{{ t('common.reset') }}</el-button>
<el-button type="primary" @click="trigger">{{ t('common.search') }}</el-button>
</template>
</el-drawer>
</template>
<style lang="less">
.drawer-main-container {
.ed-drawer__body {
padding: 16px 24px 80px !important;
}
.ed-drawer__footer {
padding: 16px 24px;
height: 64px;
}
}
</style>
@@ -0,0 +1,91 @@
import FilterText from './src/FilterText.vue'
export { FilterText }
const fieldText = (field: string, options: any[]) => {
for (let index = 0; index < options.length; index++) {
const element = options[index]
if (field === element.field) {
return element.title
}
}
}
const valueTextFormTree = (val: string, options: any[]) => {
let result = null
const stack = [...options]
while (stack.length) {
const item = stack.pop()
if (item.value === val) {
result = item.label
break
}
if (item.children?.length) {
item.children.forEach((kid: any) => stack.push(kid))
}
}
return result || val
}
const timestampFormatDate = (value: string) => {
if (!value) {
return '-'
}
return new Date(value).toLocaleString()
}
const valueText = (field: string, val: string, options: any[]) => {
for (let index = 0; index < options.length; index++) {
const element = options[index]
if (field === element.field) {
let isTree = false
const selectOption = element.option
for (let i = 0; i < selectOption.length; i++) {
const item = selectOption[i] as any
if (item.children !== undefined) {
isTree = true
break
}
if (item.id === val || item.value === val) {
return item.name || item.label
}
}
if (element.type === 'time') {
return timestampFormatDate(val)
}
if (isTree) {
return valueTextFormTree(val, selectOption)
}
}
}
return val
}
export const convertFilterText = (conditions: any[], options: any[]) => {
console.log('conditions', conditions, options)
const result: any[] = []
conditions.forEach((condition) => {
const field = condition.field
const ope = condition.operator
const value = condition.value
const fieldName = fieldText(field, options)
const textOpe = ':'
let separator = ','
if (ope === 'in') {
separator = '、'
}
if (ope === 'between') {
separator = '~'
}
let valText = value
if (value) {
if (Array.isArray(value)) {
const valTextArray = value.map((val) => valueText(field, val, options))
valText = valTextArray.join(separator)
}
if (valText?.length) {
const obj = fieldName + textOpe + valText
result.push(obj)
}
}
})
return result
}
@@ -0,0 +1,206 @@
<script lang="ts" setup>
import icon_left_outlined from '@/assets/svg/common-back.svg'
import icon_close_outlined from '@/assets/svg/icon_close_outlined.svg'
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete.svg'
import icon_right_outlined from '@/assets/svg/icon_right_outlined.svg'
import { nextTick, ref, watch } from 'vue'
import { Icon } from '@/components/icon-custom'
import { ElButton, ElDivider, ElIcon } from 'element-plus-secondary'
import { propTypes } from '@/utils/propTypes'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
filterTexts: {
type: Array<string>,
default: () => [],
},
total: propTypes.number.def(0),
})
const emits = defineEmits(['clearFilter'])
const container = ref<any>(null)
const showScroll = ref(false)
const scrollPre = () => {
container.value!.scrollLeft -= 10
if (container.value.scrollLeft <= 0) {
container.value.scrollLeft = 0
}
}
const scrollNext = () => {
container.value.scrollLeft += 10
const width = container.value.scrollWidth - container.value.offsetWidth
if (container.value.scrollLeft > width) {
container.value.scrollLeft = width
}
}
const clearFilter = (index?: number) => {
emits('clearFilter', index)
}
const clearFilterAll = () => {
emits('clearFilter', 'empty')
}
watch(
() => props.filterTexts,
() => {
nextTick(() => {
showScroll.value = container.value?.scrollWidth > container.value?.offsetWidth
})
},
{ deep: true }
)
</script>
<template>
<div v-if="filterTexts.length" class="filter-texts">
<span class="sum">{{ total }}</span>
<span class="title">{{ t('common.result_count') }}</span>
<el-divider direction="vertical" />
<el-icon v-if="showScroll" class="arrow-left arrow-filter" @click="scrollPre">
<Icon name="icon_left_outlined"><icon_left_outlined class="svg-icon" /></Icon>
</el-icon>
<div ref="container" class="filter-texts-container">
<p v-for="(ele, index) in filterTexts" :key="ele" class="text">
<el-tooltip effect="dark" :content="ele" placement="top-start">
{{ ele }}
</el-tooltip>
<el-icon @click="clearFilter(index)">
<Icon name="icon_close_outlined"><icon_close_outlined class="svg-icon" /></Icon>
</el-icon>
</p>
<el-button
v-if="!showScroll"
type="text"
class="clear-btn clear-btn-inner"
@click="clearFilterAll"
>
<template #icon>
<Icon name="icon_delete-trash_outlined"
><icon_deleteTrash_outlined class="svg-icon"
/></Icon>
</template>
{{ t('common.clear_filter') }}</el-button
>
</div>
<el-icon v-if="showScroll" class="arrow-right arrow-filter" @click="scrollNext">
<Icon name="icon_right_outlined"><icon_right_outlined class="svg-icon" /></Icon>
</el-icon>
<el-button
v-if="showScroll"
type="text"
class="clear-btn"
style="height: 24px; line-height: 24px"
@click="clearFilterAll"
>
<template #icon>
<Icon name="icon_delete-trash_outlined"
><icon_deleteTrash_outlined class="svg-icon"
/></Icon>
</template>
{{ t('common.clear_filter') }}</el-button
>
</div>
</template>
<style lang="less" scoped>
.filter-texts {
display: flex;
align-items: center;
margin: 16px 0;
font-weight: 400;
.sum {
color: #1f2329;
}
.title {
color: #999999;
margin-left: 8px;
}
.text {
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 1px 22px 1px 6px;
display: inline-block;
align-items: center;
color: #0c296e;
font-size: 14px;
line-height: 22px;
background: var(--ed-color-primary-1a, rgba(51, 112, 255, 0.1));
border-radius: 2px;
margin: 0;
margin-right: 8px;
position: relative;
i {
position: absolute;
right: 6px;
top: 50%;
font-size: 12px;
transform: translateY(-50%);
cursor: pointer;
}
}
.clear-btn {
color: #646a73;
padding: 0 4px;
border-radius: 6px;
height: 24px;
line-height: 24px;
}
.clear-btn:hover {
background: #1f23291a;
}
.clear-btn:active {
background: #1f232933;
}
.filter-texts-container::-webkit-scrollbar {
display: none;
}
.arrow-filter {
font-size: 16px;
width: 24px;
height: 24px;
cursor: pointer;
color: #646a73;
display: flex;
justify-content: center;
align-items: center;
}
.arrow-filter:hover {
background: rgba(31, 35, 41, 0.1);
border-radius: 4px;
}
.ed-icon-arrow-right.arrow-filter {
margin-left: 5px;
}
.ed-icon-arrow-left.arrow-filter {
margin-right: 5px;
}
.filter-texts-container {
flex: 1;
overflow-x: auto;
white-space: nowrap;
height: 24px;
.clear-btn-inner {
margin-top: -16px;
}
}
}
</style>
+14 -2
View File
@@ -1,15 +1,18 @@
<script lang="ts">
import { h } from 'vue'
import { ElMenuItem, ElSubMenu, ElIcon } from 'element-plus-secondary'
import { useRouter } from 'vue-router'
import model from '@/assets/svg/icon_dataset_filled.svg'
import ds from '@/assets/svg/ds.svg'
import user from '@/assets/svg/icon_member_filled.svg'
import workspace from '@/assets/svg/icon_moments-categories_outlined.svg'
const iconMap = {
icon_ai: model,
ds,
user,
workspace,
} as { [key: string]: any }
const titleWithIcon = (props: any) => {
@@ -21,6 +24,8 @@ const titleWithIcon = (props: any) => {
}
const MenuItem = (props: any) => {
const router = useRouter()
const { children, hidden, path } = props.menu
if (hidden) {
return null
@@ -28,7 +33,9 @@ const MenuItem = (props: any) => {
if (children?.length) {
return h(
ElSubMenu,
{ index: path },
{
index: path,
},
{
title: () => titleWithIcon(props),
default: () => children.map((ele: any) => h(MenuItem, { menu: ele })),
@@ -39,7 +46,12 @@ const MenuItem = (props: any) => {
const iconCom: any = iconMap[icon] ? ElIcon : null
return h(
ElMenuItem,
{ index: path },
{
index: path,
onClick: () => {
router.push(path)
},
},
{
title: h('span', null, { default: () => title }),
default: h(iconCom, null, {
+49 -2
View File
@@ -16,7 +16,10 @@
"save_success": "Save Successful",
"next": "Next",
"save": "Save",
"logout": "Logout"
"logout": "Logout",
"result_count": "result",
"clear_filter": "Clear conditions",
"reset": "Reset"
},
"dashboard": {
"length_1_64_characters": "Name field length must be 1-64 characters",
@@ -155,6 +158,7 @@
"field_name": "Field name",
"relevant_content_found": "No relevant content found",
"please_enter": "Please enter",
"Please_select": "Please select",
"table_notes": "Table notes",
"field_notes": "Field notes",
"select_all": "Select all",
@@ -221,7 +225,50 @@
"1240_results": "{msg} results",
"clear_conditions": "Clear conditions",
"disabled": "Disabled",
"enabled": "Enabled"
"enabled": "Enabled",
"edit_user": "Edit User",
"del_user": "Do you want to delete user: {msg}?",
"please_first": "Please first",
"download_the_template": "download the template",
"required_and_upload": ", fill in as required and upload",
"file": "file",
"upload_file": "Upload file",
"xls_format_files": "Only supports xlsx and xls format files",
"import": "Import",
"change_file": "Change file",
"data_import_completed": "Data import completed",
"imported_100_data": "Successfully imported {msg} data",
"return_to_view": "Return to view",
"continue_importing": "Continue importing",
"import_20_can": "Successfully imported {msg} data, failed to import {loss}, can",
"download_error_report": "Download error report",
"modify_and_re_import": ", modify and re-import",
"data_import_failed": "Data import failed",
"failed_100_can": "Import failed {msg}, can",
"change_password": "Change password",
"new_password": "New password",
"confirm_password": "Confirm password",
"old_password": "Old password"
},
"workspace": {
"add_workspace": "Add workspace",
"administrator": "Administrator",
"ordinary_member": "Ordinary member",
"people": "People",
"name_username_email": "Search name, username, email",
"add_member": "Add member",
"member_type": "Member type",
"workspace_name": "Workspace name",
"no_user": "No user",
"remove": "Remove",
"selected_2_members": "Do you want to remove the selected {msg} members?",
"removed_successfully": "Removed successfully",
"workspace:_de_workspace": "Do you want to delete the workspace: {msg}?",
"member:_feng_yibudao": "Do you want to remove the member: {msg}?",
"select_member": "Select member",
"selected:_2_people": "Selected: {msg} people",
"clear": "Clear",
"historical_dialogue": "No historical dialogue"
},
"chat": {
"chart_type": {
+49 -2
View File
@@ -16,7 +16,10 @@
"save_success": "保存成功",
"next": "下一步",
"save": "保存",
"logout": "退出登录"
"logout": "退出登录",
"result_count": "个结果",
"clear_filter": "清空条件",
"reset": "重置"
},
"dashboard": {
"length_1_64_characters": "名称字段长度1-64个字符",
@@ -155,6 +158,7 @@
"field_name": "字段名称",
"relevant_content_found": "没有找到相关内容",
"please_enter": "请输入",
"Please_select": "请选择",
"table_notes": "表备注",
"field_notes": "字段备注",
"select_all": "全选",
@@ -224,7 +228,50 @@
"1240_results": "{msg} 个结果 ",
"clear_conditions": "清空条件",
"disabled": "已禁用",
"enabled": "已启用"
"enabled": "已启用",
"edit_user": "编辑用户",
"del_user": "是否删除用户: {msg}?",
"please_first": "请先",
"download_the_template": "下载模板",
"required_and_upload": ",按要求填写后上传",
"file": "文件",
"upload_file": "上传文件",
"xls_format_files": "仅支持xlsx、xls格式的文件",
"import": "导入",
"change_file": "更换文件",
"data_import_completed": "数据导入完成",
"imported_100_data": "成功导入数据 {msg} 条",
"return_to_view": "返回查看",
"continue_importing": "继续导入",
"import_20_can": "成功导入数据 {msg} 条,导入失败 {loss} 条,可 ",
"download_error_report": "下载错误报告",
"modify_and_re_import": ",修改后重新导入",
"data_import_failed": "数据导入失败",
"failed_100_can": "导入失败 {msg} 条,可",
"change_password": "修改密码",
"new_password": "新密码",
"confirm_password": "确认密码",
"old_password": "旧密码"
},
"workspace": {
"add_workspace": "添加工作空间",
"administrator": "管理员",
"ordinary_member": "普通成员",
"people": "人",
"name_username_email": "搜索姓名、用户名、邮箱",
"add_member": "添加成员",
"member_type": "成员类型",
"workspace_name": "工作空间名称",
"no_user": "暂无用户",
"remove": "移除",
"selected_2_members": "是否移除选中的 {msg} 个成员?",
"removed_successfully": "移除成功",
"workspace:_de_workspace": "是否删除工作空间:{msg}?",
"member:_feng_yibudao": "是否移除成员:{msg}?",
"select_member": "选择成员",
"selected:_2_people": "已选:{msg} 人",
"clear": "清空",
"historical_dialogue": "暂无历史对话"
},
"chat": {
"chart_type": {
+13 -6
View File
@@ -11,6 +11,7 @@ import DashboardPreview from '@//views/dashboard/preview/SQPreviewSingle.vue'
import Dashboard from '@/views/dashboard/index.vue'
import Model from '@/views/system/model/Model.vue'
import User from '@/views/system/user/User.vue'
import Workspace from '@/views/system/workspace/index.vue'
import { watchRouter } from './watch'
const router = createRouter({
history: createWebHashHistory(),
@@ -120,18 +121,24 @@ const router = createRouter({
component: LayoutDsl,
redirect: '/system/model',
children: [
{
path: 'model',
name: 'model',
component: Model,
meta: { title: 'AI Model Configuration', icon: 'icon_ai' },
},
{
path: 'user',
name: 'user',
component: User,
meta: { title: '用户管理', icon: 'user' },
},
{
path: 'workspace',
name: 'workspace',
component: Workspace,
meta: { title: '工作空间', icon: 'workspace' },
},
{
path: 'model',
name: 'model',
component: Model,
meta: { title: 'AI Model Configuration', icon: 'icon_ai' },
},
],
},
],
+22
View File
@@ -154,3 +154,25 @@ body {
.ed-input__prefix-inner .ed-icon {
color: #646a73;
}
.confirm-no_icon {
border-radius: 12px;
padding: 24px;
.tip {
margin-top: 24px;
}
}
.form-content_error {
.ed-form-item--default {
margin-bottom: 16px;
&.is-error {
margin-bottom: 40px;
}
}
}
.ed-dialog {
border-radius: 12px !important;
}
+27
View File
@@ -0,0 +1,27 @@
import { toType, createTypes, type VueTypesInterface, type VueTypeValidableDef } from 'vue-types'
import type { CSSProperties } from 'vue'
// 自定义扩展vue-types
type PropTypes = VueTypesInterface & {
readonly style: VueTypeValidableDef<CSSProperties>
}
const ProjectTypes = createTypes({
func: undefined,
bool: undefined,
string: undefined,
number: undefined,
object: undefined,
integer: undefined,
}) as PropTypes
class propTypes extends ProjectTypes {
style() {
return toType('style', {
type: [String, Object],
default: undefined,
})
}
}
export { propTypes }
+3 -4
View File
@@ -12,8 +12,7 @@ const props = withDefaults(
defineProps<{
name: string
type: string
type_name: string
rate?: string
typeName: string
description?: string
id?: string
}>(),
@@ -22,7 +21,7 @@ const props = withDefaults(
type: '-',
description: '-',
id: '-',
type_name: '-',
typeName: '-',
}
)
@@ -58,7 +57,7 @@ const onClickOutside = () => {
<img :src="icon" width="32px" height="32px" />
<div class="info">
<div class="name">{{ name }}</div>
<div class="type">{{ type_name }}</div>
<div class="type">{{ typeName }}</div>
</div>
</div>
<div class="type-value">
+1 -1
View File
@@ -247,7 +247,7 @@ const dataTableDetail = (ele: any) => {
:key="ele.id"
:name="ele.name"
:type="ele.type"
:type_name="ele.type_name"
:type-name="ele.type_name"
:description="ele.description"
@question="handleQuestion"
@edit="handleEditDatasource(ele)"
+302 -66
View File
@@ -16,13 +16,13 @@
</template>
</el-input>
<el-button secondary>
<el-button secondary @click="drawerMainOpen">
<template #icon>
<iconFilter></iconFilter>
</template>
{{ $t('user.filter') }}
</el-button>
<el-button secondary>
<el-button secondary @click="handleUserImport">
<template #icon>
<ccmUpload></ccmUpload>
</template>
@@ -36,12 +36,17 @@
</el-button>
</div>
</div>
<div class="sqlbot-table">
<div class="sqlbot-table" :class="state.filterTexts.length && 'is-filter'">
<filter-text
:total="state.pageInfo.total"
:filter-texts="state.filterTexts"
@clear-filter="clearFilter"
/>
<el-table
ref="multipleTableRef"
@selection-change="handleSelectionChange"
:data="state.tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" :label="$t('user.name')" width="280" />
@@ -89,7 +94,7 @@
:content="$t('datasource.edit')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="editHandler(scope.row.id)">
<el-icon class="action-btn" size="16" @click="handleEditPassword(scope.row.id)">
<IconLock></IconLock>
</el-icon>
</el-tooltip>
@@ -100,7 +105,7 @@
:content="$t('dashboard.delete')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="deleteHandler(scope.row.id)">
<el-icon class="action-btn" size="16" @click="deleteHandler(scope.row)">
<IconOpeDelete></IconOpeDelete>
</el-icon>
</el-tooltip>
@@ -123,7 +128,7 @@
</div>
</div>
<div class="bottom-select">
<div v-if="multipleSelectionAll.length" class="bottom-select">
<el-checkbox
v-model="checkAll"
:indeterminate="isIndeterminate"
@@ -132,24 +137,24 @@
{{ $t('datasource.select_all') }}
</el-checkbox>
<button class="danger-button">{{ $t('dashboard.delete') }}</button>
<button class="danger-button" @click="deleteBatchUser">{{ $t('dashboard.delete') }}</button>
<span class="selected">{{
$t('user.selected_2_items', { msg: multipleSelectionAll.length })
}}</span>
<el-button text>
<el-button text @click="cancelDelete">
{{ $t('common.cancel') }}
</el-button>
</div>
</div>
<el-dialog
<el-drawer
v-model="dialogFormVisible"
title="Add Terminology"
:title="dialogTitle"
destroy-on-close
width="500"
class="sqlbot-dialog"
size="600px"
modle-class="sqlbot-drawer"
@close="onFormClose"
>
<el-form
@@ -157,40 +162,119 @@
:model="state.form"
label-width="180px"
label-position="top"
class="sqlbot-form"
size="large"
:rules="rules"
class="form-content_error"
>
<el-form-item label="Account">
<el-input v-model="state.form.term" autocomplete="off" />
<el-form-item prop="name" :label="t('user.name')">
<el-input
v-model="state.form.name"
:placeholder="$t('datasource.please_enter') + $t('common.empty') + $t('user.name')"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="Definition">
<el-input v-model="state.form.definition" type="textarea" />
<el-form-item prop="account" :label="t('user.account')">
<el-input
v-model="state.form.account"
:placeholder="$t('datasource.please_enter') + $t('common.empty') + $t('user.account')"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="Domain">
<el-select v-model="state.form.domain" placeholder="Please select a domain">
<el-form-item prop="email" :label="$t('user.email')">
<el-input
v-model="state.form.email"
:placeholder="$t('datasource.please_enter') + $t('common.empty') + $t('user.email')"
autocomplete="off"
/>
</el-form-item>
<el-form-item :label="$t('user.phone_number')">
<el-input
v-model="state.form.phoneNumber"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('user.phone_number')
"
autocomplete="off"
/>
</el-form-item>
<el-form-item :label="$t('user.workspace')">
<el-select
v-model="state.form.workspace"
:placeholder="$t('datasource.Please_select') + $t('common.empty') + $t('user.workspace')"
>
<el-option label="domain1" value="domain1" />
<el-option label="domain2" value="domain2" />
<el-option label="domain3" value="domain3" />
</el-select>
</el-form-item>
<el-form-item :label="$t('user.user_status')">
<el-switch v-model="state.form.status" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeForm">Cancel</el-button>
<el-button type="primary" @click="saveHandler"> save </el-button>
<el-button @click="closeForm">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="saveHandler"> {{ $t('model.add') }} </el-button>
</div>
</template>
</el-drawer>
<el-dialog
v-model="dialogVisiblePassword"
:title="$t('user.change_password')"
width="500"
:before-close="handleClosePassword"
>
<el-form
ref="passwordRef"
:model="password"
label-width="180px"
label-position="top"
:rules="passwordRules"
class="form-content_error"
>
<el-form-item prop="new" :label="t('user.new_password')">
<el-input
v-model="password.new"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('user.new_password')
"
autocomplete="off"
/>
</el-form-item>
<el-form-item prop="old" :label="t('user.confirm_password')">
<el-input
v-model="password.old"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('user.confirm_password')
"
autocomplete="off"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClosePassword">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="handleConfirmPassword">
{{ $t('common.save') }}
</el-button>
</div>
</template>
</el-dialog>
<UserImport ref="userImportRef"></UserImport>
<drawer-main
ref="drawerMainRef"
:filter-options="filterOption"
@trigger-filter="searchCondition"
/>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
/* import IconOpeUpload from '@/assets/svg/operate/ope-upload.svg';
import IconOpeDownload from '@/assets/svg/operate/ope-download.svg';
import IconOpeAdd from '@/assets/svg/operate/ope-add.svg'; */
import UserImport from './UserImport.vue'
import SuccessFilled from '@/assets/svg/gou_icon.svg'
import CircleCloseFilled from '@/assets/svg/icon_ban_filled.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import { useI18n } from 'vue-i18n'
import { convertFilterText, FilterText } from '@/components/filter-text'
import IconLock from '@/assets/svg/icon-key_outlined.svg'
import IconOpeEdit from '@/assets/svg/icon_edit_outlined.svg'
@@ -201,18 +285,28 @@ import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import { userApi } from '@/api/auth'
import { formatTimestamp } from '@/utils/date'
const { t } = useI18n()
const keyword = ref('')
const dialogFormVisible = ref(false)
const termFormRef = ref()
const checkAll = ref(false)
const dialogVisiblePassword = ref(false)
const isIndeterminate = ref(true)
const state = reactive({
const drawerMainRef = ref()
const userImportRef = ref()
const filterOption = ref<any[]>([])
const state = reactive<any>({
tableData: [],
filterTexts: [],
conditions: [],
form: {
id: '',
term: '',
definition: '',
domain: '',
name: '',
account: '',
workspace: '',
email: '',
status: '',
phoneNumber: '',
},
pageInfo: {
currentPage: 1,
@@ -220,10 +314,78 @@ const state = reactive({
total: 0,
},
})
const rules = {
name: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('user.name'),
trigger: 'blur',
},
],
account: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('user.account'),
trigger: 'blur',
},
],
email: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('user.email'),
trigger: 'blur',
},
],
}
const passwordRules = {
new: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('user.new_password'),
trigger: 'blur',
},
],
old: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('user.confirm_password'),
trigger: 'blur',
},
],
}
const multipleTableRef = ref()
const multipleSelectionAll = ref<any[]>([])
const dialogTitle = ref('')
const passwordRef = ref()
const password = ref({
new: '',
old: '',
id: '',
})
const handleClosePassword = () => {
dialogVisiblePassword.value = false
}
const handleEditPassword = (id: any) => {
password.value.id = id
dialogVisiblePassword.value = true
}
const handleUserImport = () => {
userImportRef.value.showDialog()
}
const handleConfirmPassword = () => {
passwordRef.value.validate((val: any) => {
if (val) {
console.log(val)
}
})
dialogVisiblePassword.value = false
}
const handleSelectionChange = (val: any[]) => {
const ids = state.tableData.map((ele: any) => ele.id)
multipleSelectionAll.value = [
@@ -236,8 +398,11 @@ const handleSelectionChange = (val: any[]) => {
const handleCheckAllChange = (val: any) => {
isIndeterminate.value = false
handleSelectionChange(val ? state.tableData : [])
val && handleToggleRowSelection()
!val && multipleTableRef.value.clearSelection()
if (val) {
handleToggleRowSelection()
} else {
multipleTableRef.value.clearSelection()
}
}
const handleToggleRowSelection = (check: boolean = true) => {
@@ -255,20 +420,110 @@ const handleToggleRowSelection = (check: boolean = true) => {
const handleSearch = (e: any) => {
console.log('search', e)
}
const fillFilterText = () => {
const textArray = state.conditions?.length
? convertFilterText(state.conditions, filterOption.value)
: []
state.filterTexts = [...textArray]
Object.assign(state.filterTexts, textArray)
}
const clearFilter = (params?: number) => {
let index = params ? params : 0
if (isNaN(index)) {
state.filterTexts = []
} else {
state.filterTexts.splice(index, 1)
}
drawerMainRef.value.clearFilter(index)
}
const searchCondition = (conditions: any) => {
state.conditions = conditions
fillFilterText()
console.log(conditions)
drawerMainClose()
}
const drawerMainOpen = async () => {
filterOption.value = [
{
type: 'enum',
option: [
{ id: true, name: t('user.enable') },
{ id: false, name: t('user.disable') },
],
field: 'statusList',
title: t('user.user_status'),
operate: 'in',
},
{
type: 'enum',
option: [
{ id: '0', name: t('user.local_creation') },
{ id: 1, name: 'LDAP' },
{ id: 2, name: 'OIDC' },
{ id: 3, name: 'CAS' },
{ id: 9, name: 'OAuth2' },
{ id: 4, name: t('user.feishu') },
{ id: 5, name: t('user.dingtalk') },
{ id: 6, name: t('user.wechat_for_business') },
],
field: 'originList',
title: t('user.user_source'),
operate: 'in',
},
{
type: 'select',
option: [{ id: '0', name: 'LOCAL' }],
field: 'workspaceList',
title: t('user.workspace'),
operate: 'in',
property: { placeholder: t('common.empty') + t('user.workspace') },
},
]
drawerMainRef.value.init()
}
const drawerMainClose = () => {
drawerMainRef.value.close()
}
const editHandler = (id: any) => {
console.log('editHandler', id)
/* userApi.query(id).then((res: any) => {
console.log('term detail', res)
state.form = res
dialogFormVisible.value = true
}) */
dialogFormVisible.value = true
dialogTitle.value = id ? t('user.edit_user') : t('user.add_users')
// userApi.query(id).then((res: any) => {
// console.log('term detail', res)
// state.form = res
// dialogFormVisible.value = true
// })
}
const deleteHandler = (id: any) => {
console.log('deleteHandler', id)
ElMessageBox.confirm('Are you sure to delete?', 'Warning', {
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
const cancelDelete = () => {
multipleSelectionAll.value = []
checkAll.value = false
isIndeterminate.value = false
}
const deleteBatchUser = () => {
ElMessageBox.confirm(t('user.selected_2_users', { msg: multipleSelectionAll.value.length }), {
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
callback: (val: string) => {
console.log(val)
},
})
}
const deleteHandler = (row: any) => {
ElMessageBox.confirm(t('user.del_user', { msg: row.name }), {
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
callback: (val: string) => {
console.log(val)
},
})
.then(() => {
/* userApi.delete(id).then(() => {
@@ -409,6 +664,10 @@ onMounted(() => {
border-radius: 6px;
width: 100%;
max-height: calc(100vh - 200px);
&.is-filter {
max-height: calc(100vh - 256px);
}
:deep(.ed-table) {
--el-table-header-bg-color: #f5f7fa;
--el-table-border-color: #ebeef5;
@@ -472,29 +731,6 @@ onMounted(() => {
}
}
}
.sqlbot-form {
width: 100%;
.ed-form-item {
margin-bottom: 16px;
text-align: left;
label {
font-size: 14px;
color: #606266;
}
}
}
.sqlbot-dialog {
width: 500px;
:deep(.ed-dialog__header) {
text-align: left !important;
font-size: 16px;
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
color: #606266;
font-size: 16px;
}
}
.user-status-container {
display: flex;
@@ -0,0 +1,475 @@
<script lang="ts" setup>
import icon_warning_filled from '@/assets/svg/icon_info_colorful.svg'
import icon_upload_outlined from '@/assets/svg/icon_upload_outlined.svg'
import icon_fileExcel_colorful from '@/assets/datasource/icon_excel.png'
import IconOpeDelete from '@/assets/svg/icon_delete.svg'
import { ref, reactive, h } from 'vue'
import {
ElMessage,
ElMessageBox,
ElLoading,
type UploadRequestOptions,
type UploadProps,
ElButton,
} from 'element-plus-secondary'
import { useI18n } from 'vue-i18n'
import { userImportApi } from '@/api/user'
const { t } = useI18n()
const defaultTip = t('user.xls_format_files')
const loadingInstance = ref<any>(null)
const dialogShow = ref(false)
const form = ref({})
const file = ref<any>(null)
const fileName = ref('')
const errorFileKey = ref(null)
const emits = defineEmits(['refresh-grid'])
const state = reactive({
errList: [],
filesTmp: [],
})
const showLoading = () => {
loadingInstance.value = ElLoading.service({ target: '.user-import-class' })
}
const closeLoading = () => {
loadingInstance.value?.close()
}
const showDialog = () => {
file.value = null
fileName.value = ''
errorFileKey.value = null
dialogShow.value = true
}
const closeDialog = () => {
dialogShow.value = false
}
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning(t('userimport.exceedMsg'))
}
const handleError = () => {
ElMessage.warning(t('user.contact_the_administrator'))
}
const uploadValidate = (file: any) => {
const suffix = file.name.substring(file.name.lastIndexOf('.') + 1)
if (suffix !== 'xlsx' && suffix !== 'xls') {
ElMessage.warning(t('userimport.suffixMsg'))
return false
}
if (file.size / 1024 / 1024 > 10) {
ElMessage.warning(t('userimport.limitMsg'))
return false
}
state.errList = []
return true
}
const fileSize = ref('-')
const setFile = (options: UploadRequestOptions) => {
file.value = options.file
fileName.value = options.file.name
fileSize.value = setSize(options.file.size)
}
const buildFormData = (file: any, files: any, param: any) => {
const formData = new FormData()
if (file) {
formData.append('file', file)
}
if (files) {
files.forEach((f: any) => {
formData.append('files', f)
})
}
if (param) {
formData.append('request', new Blob([JSON.stringify(param)], { type: 'application/json' }))
}
return formData
}
const downExcel = () => {
showLoading()
userImportApi
.downExcelTemplateApi()
.then((res) => {
const blobData = res.data
const blob = new Blob([blobData], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.download = 'user.xlsx' // 下载的文件名
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
closeLoading()
})
.catch(() => {
closeLoading()
})
}
const setSize = (size: any) => {
let data = ''
const _size = Number.parseFloat(size)
if (_size < 1 * 1024) {
//如果小于0.1KB转化成B
data = _size.toFixed(2) + 'B'
} else if (_size < 1 * 1024 * 1024) {
//如果小于0.1MB转化成KB
data = (_size / 1024).toFixed(2) + 'KB'
} else if (_size < 1 * 1024 * 1024 * 1024) {
//如果小于0.1GB转化成MB
data = (_size / (1024 * 1024)).toFixed(2) + 'MB'
} else {
//其他转化成GB
data = (_size / (1024 * 1024 * 1024)).toFixed(2) + 'GB'
}
const size_str = data + ''
const len = size_str.indexOf('.')
const dec = size_str.substr(len + 1, 2)
if (dec == '00') {
//当小数点后为00时 去掉小数部分
return size_str.substring(0, len) + size_str.substr(len + 3, 2)
}
return size_str
}
const toGrid = () => {
file.value = null
fileName.value = ''
dialogShow.value = false
emits('refresh-grid')
}
const clearFile = () => {
file.value = null
fileName.value = ''
}
const sure = () => {
const param = buildFormData(file.value, null, null)
showLoading()
userImportApi
.importUserApi(param)
.then((res) => {
closeLoading()
const data = res.data
errorFileKey.value = data.dataKey
closeDialog()
showTips(data.successCount, data.errorCount)
})
.catch(() => {
closeLoading()
})
}
const downErrorExcel = () => {
if (errorFileKey.value) {
showLoading()
userImportApi
.downErrorRecordApi(errorFileKey.value)
.then((res) => {
const blobData = res.data
const blob = new Blob([blobData], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.download = 'error.xlsx' // 下载的文件名
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
closeLoading()
// closeDialog()
})
.catch(() => {
closeLoading()
})
}
}
const showTips = (successCount: any, errorCount: any) => {
let title = !errorCount
? t('user.data_import_successful')
: successCount
? t('user.data_import_failed')
: t('user.data_import_failed_de')
const childrenDomList = [
h('strong', null, title),
h('br', {}, {}),
h('span', null, t('user.imported_1_data', { msg: successCount })),
]
if (errorCount) {
const errorCountDom = h('span', null, t('user.import_1_data', { msg: errorCount }))
const errorDom = h('div', { class: 'error-record-tip flex-align-center' }, [
h('span', null, t('user.can')),
h(
ElButton,
{
onClick: downErrorExcel,
type: 'primary',
text: true,
class: 'down-button',
},
t('user.download_error_report')
),
h('span', null, t('user.modify_and_re_import')),
])
childrenDomList.push(errorCountDom)
childrenDomList.push(errorDom)
}
ElMessageBox.confirm('', {
confirmButtonType: 'primary',
type: !errorCount ? 'success' : successCount ? 'warning' : 'error',
autofocus: false,
dangerouslyUseHTMLString: true,
message: h('div', { class: 'import-tip-box' }, childrenDomList),
showClose: false,
cancelButtonText: t('user.return_to_view'),
confirmButtonText: t('user.continue_importing'),
})
.then(() => {
clearErrorRecord()
showDialog()
emits('refresh-grid')
})
.catch(() => {
clearErrorRecord()
toGrid()
})
}
const clearErrorRecord = () => {
if (errorFileKey.value) {
userImportApi.clearErrorApi(errorFileKey.value)
}
}
const rules = {
file: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('user.file'),
trigger: 'blur',
},
],
}
defineExpose({
showDialog,
})
</script>
<template>
<el-dialog
v-model="dialogShow"
:title="t('user.batch_import')"
width="600px"
model-class="user-import-class"
@before-close="closeDialog"
>
<div class="down-template">
<span class="icon-span">
<el-icon>
<Icon name="icon_warning_filled"><icon_warning_filled class="svg-icon" /></Icon>
</el-icon>
</span>
<div class="down-template-content">
<span>{{ t('user.please_first') }}</span>
<el-button type="primary" text class="down-button" @click="downExcel">{{
t('user.download_the_template')
}}</el-button>
<span>{{ t('user.required_and_upload') }}</span>
</div>
</div>
<el-form
ref="form"
label-position="top"
:rules="rules"
class="form-content_error import-form"
:model="form"
label-width="0px"
>
<el-form-item prop="file" :label="t('user.file')" style="margin-bottom: 0px">
<div v-if="fileName" class="pdf-card">
<img :src="icon_fileExcel_colorful" width="40px" height="40px" />
<div class="file-name">
<div class="name">{{ fileName }}</div>
<div class="size">{{ fileName.split('.')[1] }} - {{ fileSize }}</div>
</div>
<el-icon class="action-btn" size="16" @click="clearFile">
<IconOpeDelete></IconOpeDelete>
</el-icon>
</div>
<el-upload
v-if="fileName"
class="upload-user"
action=""
accept=".xlsx,.xls"
:on-exceed="handleExceed"
:before-upload="uploadValidate"
:on-error="handleError"
:show-file-list="false"
:file-list="state.filesTmp"
:http-request="setFile"
>
<el-button text style="line-height: 22px; height: 22px">
{{ $t('user.change_file') }}
</el-button>
</el-upload>
<el-upload
v-else
class="upload-user"
action=""
accept=".xlsx,.xls"
:on-exceed="handleExceed"
:before-upload="uploadValidate"
:on-error="handleError"
:show-file-list="false"
:file-list="state.filesTmp"
:http-request="setFile"
>
<el-button secondary>
<el-icon size="16" style="margin-right: 4px">
<icon_upload_outlined></icon_upload_outlined>
</el-icon>
{{ t('user.upload_file') }}</el-button
>
</el-upload>
<el-link
v-if="!fileName"
style="width: 100%; line-height: 22px; height: 22px; justify-content: start"
class="font12"
type="info"
disabled
>
{{ defaultTip }}
</el-link>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeDialog">{{ t('common.cancel') }}</el-button>
<el-button
:type="file && fileName ? 'primary' : 'info'"
:disabled="!file || !fileName"
@click="sure"
>{{ t('user.import') }}</el-button
>
</span>
</template>
</el-dialog>
</template>
<style lang="less">
.upload-user {
height: 32px;
.ed-upload {
width: 100% !important;
}
}
.color-danger {
:deep(.el-link--inner) {
color: var(--deDanger, #f54a45) !important;
}
}
.font12 {
color: #8f959e !important;
font-family: var(--de-custom_font, 'PingFang');
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
.down-template {
display: flex;
flex-direction: row;
width: 100%;
height: 40px;
line-height: 40px;
background: #d2f1e9;
border-radius: 4px;
padding-left: 10px;
.icon-span {
color: var(--ed-color-primary);
font-size: 18px;
i {
top: 3px;
}
}
.down-template-content {
font-size: 14px;
display: flex;
flex-direction: row;
margin-left: 10px;
.down-button {
height: 40px;
}
}
}
.import-form {
margin-top: 16px;
.pdf-card {
width: 100%;
height: 58px;
display: flex;
align-items: center;
padding: 0 16px 0 12px;
border: 1px solid #dee0e3;
border-radius: 6px;
.file-name {
margin-left: 8px;
.name {
font-weight: 400;
font-size: 14px;
line-height: 22px;
}
.size {
font-weight: 400;
font-size: 12px;
line-height: 20px;
color: #8f959e;
}
}
.action-btn {
margin-left: auto;
}
.ed-icon {
position: relative;
cursor: pointer;
color: #646a73;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
}
}
.import-tip-box {
strong {
font-size: 16px;
}
span {
font-size: 13px;
}
.error-record-tip {
font-size: 13px;
flex-flow: wrap;
}
}
</style>
@@ -0,0 +1,108 @@
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const paramsRef = ref()
const paramsForm = reactive({
name: '',
key: '',
val: '',
})
const rules = {
name: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('model.display_name'),
trigger: 'blur',
},
],
key: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('model.parameters'),
trigger: 'blur',
},
],
val: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('model.parameter_value'),
trigger: 'blur',
},
],
}
const initForm = (item: any) => {
if (item) {
Object.assign(paramsForm, { ...item })
}
paramsRef.value.clearValidate()
}
const emits = defineEmits(['submit'])
const submit = () => {
paramsRef.value.validate((res: any) => {
if (res) {
emits('submit', paramsForm)
}
})
}
defineExpose({
initForm,
submit,
})
</script>
<template>
<div class="params-form">
<el-form
ref="paramsRef"
:rules="rules"
label-position="top"
:model="paramsForm"
style="width: 100%"
>
<el-form-item prop="key" :label="$t('model.parameters')">
<el-input
v-model="paramsForm.key"
:placeholder="$t('datasource.please_enter') + $t('common.empty') + $t('model.parameters')"
/>
</el-form-item>
<el-form-item prop="name" :label="$t('model.display_name')">
<el-input
v-model="paramsForm.name"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('model.display_name')
"
/>
</el-form-item>
<el-form-item prop="val" :label="$t('model.parameter_value')">
<el-input
v-model="paramsForm.val"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('model.parameter_value')
"
/>
</el-form-item>
</el-form>
</div>
</template>
<style lang="less" scoped>
.params-form {
.ed-form-item--default {
margin-bottom: 16px;
&.is-error {
margin-bottom: 40px;
}
}
.ed-input-number {
width: 100%;
}
}
</style>
@@ -0,0 +1,444 @@
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { workspaceList, workspaceUserList } from '@/api/workspace'
import icon_form_outlined from '@/assets/svg/icon_form_outlined.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
// import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
import assigned from '@/assets/svg/icon_assigned_outlined.svg'
// import rename from '@/assets/svg/icon_rename_outlined.svg'
// import icon_member from '@/assets/svg/icon_member.svg'
import SuccessFilled from '@/assets/svg/gou_icon.svg'
import CircleCloseFilled from '@/assets/svg/icon_ban_filled.svg'
import { useI18n } from 'vue-i18n'
import ParamsForm from './ParamsForm.vue'
interface Table {
name: string
host: string
port: string
username: string
password: string
database: string
extraJdbc: string
dbSchema: string
filename: string
sheets: string
mode: string
timeout: string
configuration: string
id: number
}
const props = withDefaults(
defineProps<{
info: Table
}>(),
{
info: () => ({
name: '-',
host: '-',
port: '-',
username: '-',
password: '-',
database: '-',
extraJdbc: '-',
dbSchema: '-',
filename: '-',
sheets: '-',
mode: '-',
timeout: '-',
configuration: '-',
id: 0,
}),
}
)
const { t } = useI18n()
const paramsFormRef = ref()
const tableList = ref([] as any[])
const keywords = ref('')
const tableListWithSearch = computed(() => {
if (!keywords.value) return tableList.value
return tableList.value.filter((ele) =>
ele.name.toLowerCase().includes(keywords.value.toLowerCase())
)
})
const currentTable = ref<any>({})
const init = () => {
workspaceList().then((res) => {
tableList.value = res
})
}
onMounted(() => {
init()
})
const tableComment = ref('')
const fieldDialog = ref<boolean>(false)
const tableDialog = ref<boolean>(false)
const fieldComment = ref('')
const currentField = ref<any>({})
const fieldList = ref<any>([])
const handleSelectTableList = () => {
paramsFormRef.value.open(props.info)
}
const clickTable = (table: any) => {
currentTable.value = table
workspaceUserList({ oid: table.id }, 1, 10).then((res) => {
fieldList.value = res
// datasourceApi.previewData(props.info.id, buildData()).then((res) => {
// previewData.value = res
// })
})
}
const closeTable = () => {
tableDialog.value = false
}
const editTable = () => {
tableComment.value = currentTable.value.custom_comment
tableDialog.value = true
}
const saveTable = () => {
currentTable.value.custom_comment = tableComment.value
// datasourceApi.saveTable(currentTable.value).then(() => {
// closeTable()
// ElMessage({
// message: t('common.save_success'),
// type: 'success',
// showClose: true,
// })
// })
}
const closeField = () => {
fieldDialog.value = false
}
const refresh = () => {
// datasourceApi.tableList(props.info.id).then((res) => {
// tableList.value = res
// if (!currentTable.value.name) return
// const nameArr = tableList.value.map((ele: any) => ele.name)
// if (!nameArr.includes(currentTable.value.name)) {
// currentTable.value = {}
// }
// })
}
const saveField = () => {
currentField.value.custom_comment = fieldComment.value
// datasourceApi.saveField(currentField.value).then(() => {
// closeField()
// ElMessage({
// message: t('common.save_success'),
// type: 'success',
// showClose: true,
// })
// })
}
const editField = (row: any) => {
currentField.value = row
fieldComment.value = currentField.value.custom_comment
fieldDialog.value = true
}
</script>
<template>
<div class="workspace no-padding">
<div class="side-list">
<div class="select-table_top">
{{ $t('user.workspace') }}
<el-tooltip effect="dark" :content="$t('workspace.add_workspace')" placement="top">
<el-icon size="18" @click="handleSelectTableList">
<icon_add_outlined></icon_add_outlined>
</el-icon>
</el-tooltip>
</div>
<el-input
v-model="keywords"
clearable
style="width: 232px"
:placeholder="$t('datasource.search')"
>
<template #prefix>
<el-icon>
<icon_searchOutline_outlined class="svg-icon" />
</el-icon>
</template>
</el-input>
<div class="list-content">
<div
v-for="ele in tableListWithSearch"
:key="ele.name"
class="model"
:class="currentTable.name === ele.name && 'isActive'"
@click="clickTable(ele)"
>
<el-icon size="16">
<icon_form_outlined></icon_form_outlined>
</el-icon>
<span class="name">{{ ele.name }}</span>
</div>
<div v-if="!!keywords && !tableListWithSearch.length" class="no-result">
{{ $t('workspace.historical_dialogue') }}
</div>
</div>
</div>
<div v-if="currentTable.name" class="info-table">
<div class="table-name">
<div class="name">{{ currentTable.name }}</div>
<div class="notes">
{{ $t('about.remark') }}:
{{ currentTable.custom_comment || '-' }}
<el-icon style="margin-left: 8px; cursor: pointer" size="16" @click="editTable">
<edit></edit>
</el-icon>
</div>
</div>
<div class="table-content">
<div class="preview-or-schema">
<el-table :data="fieldList" style="width: 100%">
<el-table-column type="selection" width="55" />
<el-table-column prop="name" :label="$t('user.name')" width="280" />
<el-table-column prop="account" :label="$t('user.account')" width="280" />
<el-table-column prop="status" :label="$t('user.user_status')" width="180">
<template #default="scope">
<div
class="user-status-container"
:class="[scope.row.status ? 'active' : 'disabled']"
>
<el-icon size="16">
<SuccessFilled v-if="scope.row.status" />
<CircleCloseFilled v-else />
</el-icon>
<span>{{ $t(`user.${scope.row.status ? 'enabled' : 'disabled'}`) }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="email" :label="$t('user.email')" width="280" />
<el-table-column prop="phone" :label="$t('user.phone_number')" width="280" />
<el-table-column prop="user_source" :label="$t('user.user_source')" width="280" />
<el-table-column :label="t('datasource.custom_notes')">
<template #default="scope">
<div class="field-comment">
<span>{{ scope.row.custom_comment }}</span>
<el-tooltip
:offset="14"
effect="dark"
:content="$t('datasource.edit')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="editField(scope.row)">
<assigned></assigned>
</el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</div>
<el-dialog
v-model="tableDialog"
:title="t('datasource.table_notes')"
width="600"
:destroy-on-close="true"
:close-on-click-modal="false"
@closed="closeTable"
>
<el-input
v-model="tableComment"
:placeholder="$t('datasource.please_enter')"
:rows="3"
type="textarea"
/>
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
<el-button @click="closeTable">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="saveTable">{{ t('common.confirm') }}</el-button>
</div>
</el-dialog>
<el-dialog
v-model="fieldDialog"
:title="t('datasource.field_notes')"
width="600"
:destroy-on-close="true"
:close-on-click-modal="false"
@closed="closeField"
>
<el-input
v-model="fieldComment"
:placeholder="$t('datasource.please_enter')"
:rows="3"
type="textarea"
/>
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
<el-button @click="closeField">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="saveField">{{ t('common.confirm') }}</el-button>
</div>
</el-dialog>
<ParamsForm ref="paramsFormRef" @refresh="refresh"></ParamsForm>
</template>
<style lang="less" scoped>
.workspace {
height: 100%;
position: relative;
.side-list {
width: 280px;
padding: 8px 16px;
height: 100%;
border-right: 1px solid #1f232926;
.select-table_top {
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
.ed-icon {
cursor: pointer;
color: var(--ed-color-primary);
position: relative;
&:hover {
&::after {
display: block;
}
}
&::after {
content: '';
display: none;
position: absolute;
width: 26px;
height: 26px;
background: #1cba901a;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 6px;
}
}
}
.ed-input {
margin: 8px;
}
.list-content {
height: calc(100% - 100px);
.no-result {
margin-top: 72px;
font-weight: 400;
font-size: 14px;
line-height: 22px;
text-align: center;
color: #646a73;
}
.model {
width: 100%;
height: 32px;
display: flex;
align-items: center;
padding-left: 8px;
border-radius: 4px;
cursor: pointer;
.name {
margin-left: 8px;
font-weight: 500;
font-size: 14px;
line-height: 22px;
}
&:hover {
background: #1f23291a;
}
&.isActive {
background: #1cba901a;
color: var(--ed-color-primary);
}
}
}
}
.info-table {
position: absolute;
right: 0;
top: 0;
width: calc(100% - 280px);
.table-name {
height: 80px;
padding: 16px 0 0 24px;
border-bottom: 1px solid #1f232926;
.name {
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
.notes {
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
display: flex;
align-items: center;
}
}
.table-content {
padding: 16px 24px;
.preview-or-schema {
margin-top: 16px;
.field-comment {
height: 24px;
}
.ed-icon {
position: relative;
cursor: pointer;
margin-top: 4px;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
.preview-num {
margin: 12px 0;
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
}
}
}
}
}
</style>