feat: Terminology Management

This commit is contained in:
dataeaseShu
2025-08-25 16:04:24 +08:00
parent 2d181c4892
commit 747fab6a98
8 changed files with 842 additions and 10 deletions
+11
View File
@@ -0,0 +1,11 @@
import { request } from '@/utils/request'
export const professionalApi = {
getList: (pageNum: any, pageSize: any, params: any) =>
request.get(`/system/terminology/page/${pageNum}/${pageSize}`, {
params,
}),
updateEmbedded: (data: any) => request.put('/system/terminology', data),
deleteEmbedded: (params: any) => request.delete('/system/terminology', { data: params }),
getOne: (id: any) => request.get(`/system/terminology/${id}`),
}
+2
View File
@@ -49,6 +49,7 @@ const routerList = computed(() => {
!route.path.includes('embeddedPage') &&
!route.path.includes('canvas') &&
!route.path.includes('member') &&
!route.path.includes('professional') &&
!route.path.includes('permission') &&
!route.path.includes('preview') &&
route.path !== '/login' &&
@@ -92,6 +93,7 @@ const routerList = computed(() => {
.ed-sub-menu.is-active {
.ed-sub-menu__title {
background-color: #fff !important;
color: var(--ed-color-primary) !important;
}
}
+18
View File
@@ -5,6 +5,24 @@
"Dashboard": "Dashboard",
"AI Model Configuration": "AI Model Configuration"
},
"professional": {
"editing_terminology": "Editing Terminology",
"professional_terminology": "Professional Terminology",
"term_name": "Term Name",
"term_description": "Term Description",
"search_term": "Search Term",
"no_term": "No Term",
"create_new_term": "Create New Term",
"export_all": "Export All",
"professional_term_details": "Professional Term Details",
"business_term": "Business Term",
"synonyms": "Synonyms",
"all_236_terms": "Export all {msg} terms?",
"export": "Export",
"selected_2_terms": "Delete the selected {msg} terms?",
"selected_2_terms_de": "Export the selected {msg} terms?",
"the_term_gmv": "Delete the term: {msg}?"
},
"common": {
"zoom_in": "Zoom in",
"zoom_out": "Zoom out",
+18
View File
@@ -5,6 +5,24 @@
"Dashboard": "仪表盘",
"AI Model Configuration": "模型配置"
},
"professional": {
"editing_terminology": "编辑术语",
"professional_terminology": "专业术语",
"term_name": "术语名称",
"term_description": "术语描述",
"search_term": "搜索术语",
"no_term": "暂无术语",
"create_new_term": "新建术语",
"export_all": "全部导出",
"professional_term_details": "专业术语详情",
"business_term": "业务术语",
"synonyms": "同义词",
"all_236_terms": "是否导出全部 {msg} 条术语?",
"export": "导出",
"selected_2_terms": "是否删除选中的 {msg} 条术语?",
"selected_2_terms_de": "是否导出选中的 {msg} 条术语?",
"the_term_gmv": "是否删除术语:{msg}?"
},
"common": {
"zoom_in": "放大",
"zoom_out": "缩小",
+7
View File
@@ -15,6 +15,7 @@ import assistantTest from '@/views/system/embedded/Test.vue'
import assistant from '@/views/embedded/index.vue'
import EmbeddedPage from '@/views/embedded/page.vue'
import Member from '@/views/system/member/index.vue'
import Professional from '@/views/system/professional/index.vue'
import Appearance from '@/views/system/appearance/index.vue'
import Permission from '@/views/system/permission/index.vue'
import User from '@/views/system/user/User.vue'
@@ -105,6 +106,12 @@ export const routes = [
component: Permission,
meta: { title: t('workspace.permission_configuration') },
},
{
path: '/set/professional',
name: 'professional',
component: Professional,
meta: { title: t('professional.professional_terminology') },
},
],
},
{
+5
View File
@@ -357,3 +357,8 @@ strong {
.ed-checkbox {
--ed-checkbox-font-weight: 400 !important;
}
.ed-form-item__label.ed-form-item__label.ed-form-item__label {
height: 22px;
line-height: 22px;
}
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { computed } from 'vue'
import Default_avatar from '@/assets/workspace/default_avatar.png'
import Default_avatar_custom from '@/assets/img/Default-avatar.svg'
import icon_admin_outlined from '@/assets/svg/icon_admin_outlined.svg'
import icon_info_outlined_1 from '@/assets/svg/icon_info_outlined_1.svg'
import icon_maybe_outlined from '@/assets/svg/icon-maybe_outlined.svg'
@@ -9,7 +9,6 @@ import icon_translate_outlined from '@/assets/svg/icon_translate_outlined.svg'
import icon_logout_outlined from '@/assets/svg/icon_logout_outlined.svg'
import icon_right_outlined from '@/assets/svg/icon_right_outlined.svg'
import { useUserStore } from '@/stores/user'
import DefaultAvatarBlue from '@/assets/blue/Default-avatar_blue.png'
defineProps({
showDoc: { type: [Boolean], required: true },
@@ -24,19 +23,18 @@ const account = computed(() => userStore.getAccount)
<template>
<div style="position: relative">
<button class="person" :title="name">
<img
class="default-avatar"
:src="isBlue ? DefaultAvatarBlue : Default_avatar"
width="32px"
height="32px"
/>
<el-icon class="default-avatar" size="32">
<Default_avatar_custom></Default_avatar_custom>
</el-icon>
<span class="name ellipsis">{{ name }}</span>
</button>
<div class="ed-popper is-light ed-popover system-person_style">
<div class="popover">
<div class="popover-content">
<div class="info">
<img :src="isBlue ? DefaultAvatarBlue : Default_avatar" width="40px" height="40px" />
<el-icon class="img" size="40">
<Default_avatar_custom></Default_avatar_custom>
</el-icon>
<div :title="name" class="top ellipsis">{{ name }}</div>
<div :title="account" class="bottom ellipsis">{{ account }}</div>
</div>
@@ -156,7 +154,7 @@ const account = computed(() => userStore.getAccount)
height: 62px;
padding: 8px;
img {
.img {
float: left;
margin: 3px 8px 0 7px;
}
@@ -0,0 +1,773 @@
<script lang="ts" setup>
import { ref, onMounted, reactive, unref, nextTick } from 'vue'
import icon_export_outlined from '@/assets/svg/icon_export_outlined.svg'
import { professionalApi } from '@/api/professional'
import { formatTimestamp } from '@/utils/date'
import ccmUpload from '@/assets/svg/icon_ccm-upload_outlined.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import IconOpeEdit from '@/assets/svg/icon_edit_outlined.svg'
import IconOpeDelete from '@/assets/svg/icon_delete.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
import { useI18n } from 'vue-i18n'
import { cloneDeep } from 'lodash-es'
defineProps({
btnSelect: {
type: String,
default: '',
},
})
interface Form {
id?: string | null
word: string | null
other_words?: string[] | null
}
const { t } = useI18n()
const multipleSelectionAll = ref<any[]>([])
const keywords = ref('')
const searchLoading = ref(false)
const selectable = () => {
return true
}
onMounted(() => {
search()
})
const dialogFormVisible = ref<boolean>(false)
const multipleTableRef = ref()
const isIndeterminate = ref(true)
const checkAll = ref(false)
const fieldList = ref<any>([])
const pageInfo = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
})
const dialogTitle = ref('')
const defaultForm = {
id: null,
word: null,
other_words: [],
}
const pageForm = ref<Form>(cloneDeep(defaultForm))
const cancelDelete = () => {
handleToggleRowSelection(false)
multipleSelectionAll.value = []
checkAll.value = false
isIndeterminate.value = false
}
const exportBatchUser = () => {
ElMessageBox.confirm(
t('professional.selected_2_terms_de', { msg: multipleSelectionAll.value.length }),
{
confirmButtonType: 'primary',
confirmButtonText: t('professional.export'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
}
).then(() => {
professionalApi.deleteEmbedded(multipleSelectionAll.value.map((ele) => ele.id)).then(() => {
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
})
multipleSelectionAll.value = []
search()
})
})
}
const exportAllUser = () => {
ElMessageBox.confirm(t('professional.all_236_terms', { msg: pageInfo.total }), {
confirmButtonType: 'primary',
confirmButtonText: t('professional.export'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
}).then(() => {
professionalApi.deleteEmbedded(multipleSelectionAll.value.map((ele) => ele.id)).then(() => {
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
})
multipleSelectionAll.value = []
search()
})
})
}
const deleteBatchUser = () => {
ElMessageBox.confirm(
t('professional.selected_2_terms', { msg: multipleSelectionAll.value.length }),
{
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
}
).then(() => {
professionalApi.deleteEmbedded(multipleSelectionAll.value.map((ele) => ele.id)).then(() => {
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
})
multipleSelectionAll.value = []
search()
})
})
}
const deleteHandler = (row: any) => {
ElMessageBox.confirm(t('professional.the_term_gmv', { msg: row.name }), {
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
}).then(() => {
professionalApi.deleteEmbedded([row.id]).then(() => {
multipleSelectionAll.value = multipleSelectionAll.value.filter((ele) => row.id !== ele.id)
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
})
search()
})
})
}
const handleSelectionChange = (val: any[]) => {
if (toggleRowLoading.value) return
const arr = fieldList.value.filter(selectable)
const ids = arr.map((ele: any) => ele.id)
multipleSelectionAll.value = [
...multipleSelectionAll.value.filter((ele) => !ids.includes(ele.id)),
...val,
]
isIndeterminate.value = !(val.length === 0 || val.length === arr.length)
checkAll.value = val.length === arr.length
}
const handleCheckAllChange = (val: any) => {
isIndeterminate.value = false
handleSelectionChange(val ? fieldList.value.filter(selectable) : [])
if (val) {
handleToggleRowSelection()
} else {
multipleTableRef.value.clearSelection()
}
}
const toggleRowLoading = ref(false)
const handleToggleRowSelection = (check: boolean = true) => {
toggleRowLoading.value = true
const arr = fieldList.value.filter(selectable)
let i = 0
const ids = multipleSelectionAll.value.map((ele: any) => ele.id)
for (const key in arr) {
if (ids.includes((arr[key] as any).id)) {
i += 1
multipleTableRef.value.toggleRowSelection(arr[key], check)
}
}
toggleRowLoading.value = false
checkAll.value = i === arr.length
isIndeterminate.value = !(i === 0 || i === arr.length)
}
const search = () => {
searchLoading.value = true
professionalApi
.getList(pageInfo.currentPage, pageInfo.pageSize, { word: keywords.value })
.then((res) => {
toggleRowLoading.value = true
fieldList.value = res.items
pageInfo.total = res.total
nextTick(() => {
handleToggleRowSelection()
})
})
.finally(() => {
searchLoading.value = false
})
}
const termFormRef = ref()
const rules = {
name: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('professional.term_name'),
},
],
description: [
{
required: true,
message:
t('datasource.please_enter') + t('common.empty') + t('professional.term_description'),
},
],
}
const saveHandler = () => {
termFormRef.value.validate((res: any) => {
if (res) {
const obj = unref(pageForm)
if (obj.id === '') {
delete obj.id
}
professionalApi.updateEmbedded(obj).then(() => {
ElMessage({
type: 'success',
message: t('common.save_success'),
})
search()
onFormClose()
})
}
})
}
const editHandler = (row: any) => {
pageForm.value.id = null
if (row) {
pageForm.value = cloneDeep(row)
}
dialogTitle.value = row?.id
? t('professional.editing_terminology')
: t('professional.create_new_term')
dialogFormVisible.value = true
}
const onFormClose = () => {
pageForm.value = cloneDeep(defaultForm)
dialogFormVisible.value = false
}
const handleSizeChange = (val: number) => {
pageInfo.currentPage = 1
pageInfo.pageSize = val
search()
}
const handleCurrentChange = (val: number) => {
pageInfo.currentPage = val
search()
}
const rowInfoDialog = ref(false)
const handleRowClick = (row: any) => {
pageForm.value = cloneDeep(row)
rowInfoDialog.value = true
}
const onRowFormClose = () => {
pageForm.value = cloneDeep(defaultForm)
rowInfoDialog.value = false
}
const deleteHandlerItem = (idx: number) => {
pageForm.value.other_words = pageForm.value.other_words!.filter((_, index) => index !== idx)
}
</script>
<template>
<div v-loading="searchLoading" class="professional">
<div class="tool-left">
<span class="page-title">{{ $t('professional.professional_terminology') }}</span>
<div>
<el-input
v-model="keywords"
style="width: 240px; margin-right: 12px"
:placeholder="$t('professional.search_term')"
clearable
@keyup.enter="search"
@blur="search"
>
<template #prefix>
<el-icon>
<icon_searchOutline_outlined />
</el-icon>
</template>
</el-input>
<el-button secondary @click="exportAllUser">
<template #icon>
<icon_export_outlined />
</template>
{{ $t('professional.export_all') }}
</el-button>
<el-button secondary @click="editHandler(null)">
<template #icon>
<ccmUpload></ccmUpload>
</template>
{{ $t('user.batch_import') }}
</el-button>
<el-button type="primary" @click="editHandler(null)">
<template #icon>
<icon_add_outlined></icon_add_outlined>
</template>
{{ $t('professional.create_new_term') }}
</el-button>
</div>
</div>
<div
v-if="!searchLoading"
class="table-content"
:class="multipleSelectionAll.length && 'show-pagination_height'"
>
<template v-if="!keywords && !fieldList.length">
<EmptyBackground
class="datasource-yet"
:description="$t('professional.no_term')"
img-type="noneWhite"
/>
<div style="text-align: center; margin-top: -10px">
<el-button type="primary" @click="editHandler(null)">
<template #icon>
<icon_add_outlined></icon_add_outlined>
</template>
{{ $t('professional.create_new_term') }}
</el-button>
</div>
</template>
<div v-else class="preview-or-schema">
<el-table
ref="multipleTableRef"
:data="fieldList"
style="width: 100%"
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
>
<el-table-column :selectable="selectable" type="selection" width="55" />
<el-table-column prop="word" :label="$t('professional.term_name')" width="280" />
<el-table-column
prop="description"
:label="$t('professional.term_description')"
min-width="240"
/>
<el-table-column
prop="create_time"
sortable
:label="$t('dashboard.create_time')"
width="240"
>
<template #default="scope">
<span>{{ formatTimestamp(scope.row.create_time, 'YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" width="80" :label="t('ds.actions')">
<template #default="scope">
<div class="field-comment">
<el-tooltip
:offset="14"
effect="dark"
:content="$t('datasource.edit')"
placement="top"
>
<el-icon class="action-btn" size="16" @click.stop="editHandler(scope.row)">
<IconOpeEdit></IconOpeEdit>
</el-icon>
</el-tooltip>
<el-tooltip
:offset="14"
effect="dark"
:content="$t('dashboard.delete')"
placement="top"
>
<el-icon class="action-btn" size="16" @click.stop="deleteHandler(scope.row)">
<IconOpeDelete></IconOpeDelete>
</el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
<template #empty>
<EmptyBackground
v-if="!keywords && !fieldList.length"
:description="$t('professional.no_term')"
img-type="noneWhite"
/>
<EmptyBackground
v-if="!!keywords && !fieldList.length"
:description="$t('datasource.relevant_content_found')"
img-type="tree"
/>
</template>
</el-table>
</div>
</div>
<div v-if="fieldList.length" class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.currentPage"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 30]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="pageInfo.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<div v-if="multipleSelectionAll.length" class="bottom-select">
<el-checkbox
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>
{{ $t('datasource.select_all') }}
</el-checkbox>
<button class="primary-button" @click="exportBatchUser">
{{ $t('professional.export') }}
</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 @click="cancelDelete">
{{ $t('common.cancel') }}
</el-button>
</div>
</div>
<el-drawer
v-model="dialogFormVisible"
:title="dialogTitle"
destroy-on-close
size="600px"
:before-close="onFormClose"
modal-class="professional-add_drawer"
>
<el-form
ref="termFormRef"
:model="pageForm"
label-width="180px"
label-position="top"
:rules="rules"
class="form-content_error"
@submit.prevent
>
<el-form-item prop="word" :label="t('professional.term_name')">
<el-input
v-model="pageForm.word"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('professional.term_name')
"
autocomplete="off"
maxlength="50"
clearable
/>
</el-form-item>
<el-form-item prop="description" :label="t('professional.term_description')">
<el-input
v-model="pageForm.description"
:placeholder="$t('datasource.please_enter')"
:autosize="{ minRows: 3.636, maxRows: 11.09 }"
type="textarea"
/>
</el-form-item>
<el-form-item>
<template #label>
<div style="display: flex; align-items: center">
<span>{{ t('professional.synonyms') }}</span>
<span class="btn">
<el-icon style="margin-right: 4px" size="16" @click="pageForm.other_words.push('')">
<icon_add_outlined></icon_add_outlined>
</el-icon>
{{ $t('model.add') }}
</span>
</div>
</template>
<div class="synonyms-list">
<el-scrollbar max-height="218px">
<div v-for="(item, index) in pageForm.other_words" :key="item" class="scrollbar-item">
<el-input
v-model="pageForm.other_words[index]"
style="width: 528px"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('professional.synonyms')
"
clearable
/>
<el-tooltip
:offset="14"
effect="dark"
:content="$t('dashboard.delete')"
placement="top"
>
<el-icon
class="hover-icon_with_bg"
size="16"
@click.stop="deleteHandlerItem(index)"
>
<IconOpeDelete></IconOpeDelete>
</el-icon>
</el-tooltip>
</div>
</el-scrollbar>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button secondary @click="onFormClose">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="saveHandler">
{{ $t('common.save') }}
</el-button>
</div>
</template>
</el-drawer>
<el-drawer
v-model="rowInfoDialog"
:title="$t('professional.professional_term_details')"
destroy-on-close
size="600px"
:before-close="onRowFormClose"
modal-class="professional-term_drawer"
>
<el-form
:model="rowInfo"
label-width="180px"
label-position="top"
class="form-content_error"
@submit.prevent
>
<el-form-item :label="t('professional.business_term')">
<div class="content">
{{ pageForm.word }}
</div>
</el-form-item>
<el-form-item :label="t('professional.synonyms')">
<div class="content">
{{ pageForm.other_words }}
</div>
</el-form-item>
<el-form-item :label="t('professional.term_description')">
<div class="content">
{{ pageForm.description }}
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button secondary @click="onRowFormClose">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="saveHandler">
{{ $t('common.save') }}
</el-button>
</div>
</template>
</el-drawer>
</template>
<style lang="less" scoped>
.professional {
height: 100%;
position: relative;
.datasource-yet {
padding-bottom: 0;
height: auto;
padding-top: 200px;
}
.tool-left {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.page-title {
font-weight: 500;
font-size: 20px;
line-height: 28px;
}
}
.pagination-container {
display: flex;
justify-content: end;
align-items: center;
margin-top: 16px;
}
.table-content {
max-height: calc(100% - 104px);
overflow-y: auto;
&.show-pagination_height {
max-height: calc(100% - 165px);
}
.preview-or-schema {
.ed-icon {
color: #646a73;
}
.user-status-container {
display: flex;
align-items: center;
font-weight: 400;
font-size: 14px;
line-height: 22px;
height: 24px;
.ed-icon {
margin-left: 8px;
}
}
.field-comment {
height: 24px;
.ed-icon {
position: relative;
cursor: pointer;
margin-top: 4px;
margin-right: 10px;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:not(.not-allow):hover {
&::after {
display: block;
}
}
&.not-allow {
cursor: not-allowed;
}
}
}
.preview-num {
margin: 12px 0;
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
}
}
}
.bottom-select {
position: absolute;
height: 64px;
width: calc(100% + 48px);
left: -24px;
bottom: -16px;
border-top: 1px solid #1f232926;
display: flex;
background-color: #fff;
align-items: center;
padding-left: 24px;
background-color: #fff;
z-index: 10;
.danger-button {
border: 1px solid var(--ed-color-danger);
color: var(--ed-color-danger);
border-radius: var(--ed-border-radius-base);
min-width: 80px;
height: 32px;
line-height: 32px;
text-align: center;
cursor: pointer;
margin: 0 16px;
background-color: transparent;
}
.primary-button {
border: 1px solid var(--ed-color-primary);
color: var(--ed-color-primary);
border-radius: var(--ed-border-radius-base);
min-width: 80px;
height: 32px;
line-height: 32px;
text-align: center;
cursor: pointer;
margin: 0 16px;
background-color: transparent;
}
.selected {
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
margin-right: 12px;
}
}
}
</style>
<style lang="less">
.professional-term_drawer {
.ed-form-item--label-top .ed-form-item__label {
margin-bottom: 4px;
}
.ed-form-item__label {
color: #646a73;
}
.content {
width: 100%;
line-height: 22px;
word-break: break-all;
}
}
.professional-add_drawer {
.ed-textarea__inner {
line-height: 22px;
}
.ed-form-item__label:has(.btn) {
padding-right: 0;
width: 100%;
}
.scrollbar-item {
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 4px;
margin-bottom: 8px;
}
.synonyms-list {
position: absolute;
left: 0;
top: 0;
width: calc(100% + 4px);
}
.btn {
margin-left: auto;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
border-radius: 6px;
margin-right: -4px;
cursor: pointer;
&:hover {
background-color: #1f23291a;
}
}
}
</style>