feat: applicationChatUser

This commit is contained in:
teukkk 2025-06-16 20:14:36 +08:00
parent a892f3075f
commit d86ad90e31
18 changed files with 532 additions and 92 deletions

View File

@ -0,0 +1,48 @@
import type { Ref } from 'vue'
import { Result } from '@/request/Result'
import { get, put } from '@/request/index'
import type { ChatUserGroupItem, ChatUserGroupUserItem, ChatUserResourceParams, putUserGroupUserParams } from '@/api/type/workspaceChatUser'
import type { pageRequest, PageList } from '@/api/type/common'
const prefix = '/workspace/' + localStorage.getItem('workspace_id')
/**
*
*/
const getUserGroupList: (resource: ChatUserResourceParams, loading?: Ref<boolean>) => Promise<Result<ChatUserGroupItem[]>> = (resource, loading) => {
return get(`${prefix}/${resource.resource_type}/${resource.resource_id}/user_group`, undefined, loading)
}
/**
*
*/
const getUserGroupUserList: (
resource: ChatUserResourceParams,
user_group_id: string,
page: pageRequest,
param: any,
loading?: Ref<boolean>,
) => Promise<Result<PageList<ChatUserGroupUserItem[]>>> = (resource, user_group_id, page, param, loading) => {
return get(
`${prefix}/${resource.resource_type}/${resource.resource_id}/user_group_id/${user_group_id}/${page.current_page}/${page.page_size}`,
param,
loading,
)
}
/**
*
*/
const putUserGroupUser: (
resource: ChatUserResourceParams,
user_group_id: string,
data: putUserGroupUserParams[],
loading?: Ref<boolean>,
) => Promise<Result<boolean>> = (resource, user_group_id, data, loading) => {
return put(`${prefix}/${resource.resource_type}/${resource.resource_id}/user_group_id/${user_group_id}`, data, undefined, loading)
}
export default {
getUserGroupList,
getUserGroupUserList,
putUserGroupUser
}

View File

@ -1,22 +1,22 @@
import {Result} from '@/request/Result'
import {get, put, post, del} from '@/request/index'
import type {pageRequest} from '@/api/type/common'
import type {Ref} from 'vue'
import { Result } from '@/request/Result'
import { get, put, post, del } from '@/request/index'
import type { pageRequest } from '@/api/type/common'
import type { Ref } from 'vue'
const prefix = '/system/chat_user'
/**
*
* @query
email_or_username: string
username_or_nickname: string
*/
const getUserManage: (
page: pageRequest,
email_or_username: string,
username_or_nickname: string,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (page, email_or_username, loading) => {
) => Promise<Result<any>> = (page, username_or_nickname, loading) => {
return get(
`${prefix}/user_manage/${page.current_page}/${page.page_size}`,
email_or_username ? {email_or_username} : undefined,
username_or_nickname ? { username_or_nickname } : undefined,
loading,
)
}

View File

@ -0,0 +1,33 @@
import { ChatUserResourceEnum } from '@/enums/workspaceChatUser'
interface ChatUserResourceParams {
resource_id: string,
resource_type: ChatUserResourceEnum
}
interface ChatUserGroupItem {
id: string,
name: string,
is_auth: boolean
}
interface ChatUserGroupUserItem {
id: string,
is_auth: boolean,
email: string,
phone: string,
nick_name: string,
username: string,
password: string,
source: string,
is_active: boolean,
create_time: string,
update_time: string,
}
interface putUserGroupUserParams {
chat_user_id: string,
is_auth: boolean
}
export type { ChatUserGroupItem, putUserGroupUserParams, ChatUserResourceParams, ChatUserGroupUserItem }

View File

@ -0,0 +1,4 @@
export enum ChatUserResourceEnum {
KNOWLEDGE = 'KNOWLEDGE',
APPLICATION = 'APPLICATION',
}

View File

@ -84,4 +84,5 @@ export default {
},
info: 'Base Information',
otherSetting: 'Other Settings',
username: 'username'
}

View File

@ -0,0 +1,10 @@
export default {
title: 'Chat users',
syncUsers: 'Sync users',
setUserGroups: 'Set user groups',
autoAuthorization: 'Auto authorization',
authorization: 'Authorization',
group: {
title: 'User groups',
}
};

View File

@ -14,6 +14,7 @@ import document from './document'
import paragraph from './paragraph'
import problem from './problem'
import chatLog from './chat-log'
import chatUser from './chat-user'
import applicationWorkflow from './application-workflow'
import login from './login'
import operateLog from './operate-log'
@ -36,5 +37,6 @@ export default {
login,
operateLog,
role,
workspace
workspace,
chatUser
}

View File

@ -88,4 +88,5 @@ export default {
},
info: '基本信息',
otherSetting: '其他设置',
username: '用户名'
}

View File

@ -0,0 +1,10 @@
export default {
title: '对话用户',
syncUsers: '同步用户',
setUserGroups: '设置用户组',
autoAuthorization: '自动授权',
authorization: '授权',
group: {
title: '用户组',
}
}

View File

@ -14,6 +14,7 @@ import applicationOverview from './application-overview'
import applicationWorkflow from './application-workflow'
import paragraph from './paragraph'
import chatLog from './chat-log'
import chatUser from './chat-user'
// import notFound from './404'
// import operateLog from './operate-log'
@ -34,6 +35,7 @@ export default {
applicationWorkflow,
paragraph,
chatLog,
chatUser,
// notFound,
// operateLog

View File

@ -84,4 +84,5 @@ export default {
},
info: '使用者資訊',
otherSetting: '其他設定',
username: '用户名'
}

View File

@ -0,0 +1,10 @@
export default {
title: '對話用戶',
syncUsers: '同步用戶',
setUserGroups: '設定用戶組',
autoAuthorization: '自動授權',
authorization: '授權',
group: {
title: '用戶組',
}
};

View File

@ -14,6 +14,7 @@ import document from './document'
import paragraph from './paragraph'
import problem from './problem'
import chatLog from './chat-log'
import chatUser from './chat-user'
import applicationWorkflow from './application-workflow'
import login from './login'
import operateLog from './operate-log'
@ -36,5 +37,6 @@ export default {
login,
operateLog,
role,
workspace
workspace,
chatUser
}

View File

@ -1,3 +1,5 @@
import { ChatUserResourceEnum } from '@/enums/workspaceChatUser'
import { PermissionConst, RoleConst } from '@/utils/permission/data'
const ApplicationDetailRouter = {
@ -63,6 +65,20 @@ const ApplicationDetailRouter = {
},
component: () => import('@/views/hit-test/index.vue'),
},
{
path: 'chat-user',
name: 'applicationChatUser',
meta: {
icon: 'app-document',
iconActive: 'app-document-active',
title: 'views.chatUser.title',
active: 'chat-log',
parentPath: '/application/:id/:type',
parentName: 'ApplicationDetail',
resourceType: ChatUserResourceEnum.APPLICATION
},
component: () => import('@/views/chat-user/index.vue')
},
{
path: 'chat-log',
name: 'ChatLog',

View File

@ -1,3 +1,5 @@
import { ChatUserResourceEnum } from '@/enums/workspaceChatUser'
const DocumentRouter = {
path: '/knowledge/:id/:folderId',
name: 'KnowledgeDetail',
@ -43,6 +45,20 @@ const DocumentRouter = {
},
component: () => import('@/views/hit-test/index.vue'),
},
{
path: 'chat-user',
name: 'KnowledgeChatUser',
meta: {
icon: 'app-document',
iconActive: 'app-document-active',
title: 'views.chatUser.title',
active: 'chat-log',
parentPath: '/knowledge/:id/:folderId',
parentName: 'KnowledgeDetail',
resourceType: ChatUserResourceEnum.APPLICATION
},
component: () => import('@/views/chat-user/index.vue')
},
{
path: 'setting',
name: 'KnowledgeSetting',

View File

@ -111,7 +111,7 @@ const systemRouter = {
meta: {
icon: 'app-folder-share',
iconActive: 'app-folder-share-active',
title: '对话用户',
title: 'views.chatUser.title',
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',
@ -121,7 +121,7 @@ const systemRouter = {
path: '/system/chat/chat-user',
name: 'ChatUser',
meta: {
title: '对话用户',
title: 'views.chatUser.title',
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',
@ -132,7 +132,7 @@ const systemRouter = {
path: '/system/chat/group',
name: 'Group',
meta: {
title: '用户组',
title: 'views.chatUser.group.title',
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',
@ -143,7 +143,7 @@ const systemRouter = {
path: '/system/chat/authentication',
name: 'Authentication',
meta: {
title: '登录认证',
title: 'views.system.authentication.title',
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',

View File

@ -0,0 +1,260 @@
<template>
<ContentContainer>
<template #header>
<div>
<h2>{{ $t('views.chatUser.title') }}</h2>
<div class="color-secondary">{{ $t('views.user.title') }}</div>
</div>
</template>
<el-card style="--el-card-padding: 0" class="user-card">
<div class="flex h-full">
<div class="user-left border-r p-16">
<div class="user-left_title">
<h4 class="medium">{{ $t('views.chatUser.group.title') }}</h4>
</div>
<div class="p-8">
<el-input v-model="filterText" :placeholder="$t('common.search')" prefix-icon="Search" clearable />
</div>
<div class="list-height-left">
<el-scrollbar v-loading="loading">
<common-list :data="filterList" @click="clickUserGroup" :default-active="current?.id">
<template #default="{ row }">
<span>{{ row.name }}</span>
</template>
<template #empty>
<span></span>
</template>
</common-list>
</el-scrollbar>
</div>
</div>
<!-- 右边 -->
<div class="user-right" v-loading="rightLoading">
<div class="flex-between">
<div class="flex align-center">
<h4 class="medium">{{ current?.name }}</h4>
<el-divider direction="vertical" class="mr-8 ml-8" />
<AppIcon iconName="app-wordspace" style="font-size: 16px" class="color-input-placeholder"></AppIcon>
<span class="color-input-placeholder ml-4">
{{ current?.user_count }}
</span>
</div>
<el-button type="primary" @click="handleSave">
{{ t('common.save') }}
</el-button>
</div>
<div class="flex-between mb-16" style="margin-top: 18px;">
<div class="flex complex-search">
<el-select class="complex-search__left" v-model="searchType" style="width: 120px">
<el-option :label="$t('views.login.loginForm.username.label')" value="username_or_nickname" />
</el-select>
<el-input v-if="searchType === 'username_or_nickname'" v-model="searchForm.username_or_nickname"
@change="getList" :placeholder="$t('common.inputPlaceholder')" style="width: 220px" clearable />
</div>
<div class="flex align-center">
<div class="color-secondary mr-8">{{ $t('views.chatUser.autoAuthorization') }}</div>
<el-switch size="small" v-model="automaticAuthorization"></el-switch>
</div>
</div>
<app-table :data="tableData" :pagination-config="paginationConfig" @sizeChange="handleSizeChange"
@changePage="getList" v-loading="rightLoading">
<el-table-column prop="nick_name" :label="$t('views.userManage.userForm.nick_name.label')" />
<el-table-column prop="username" :label="$t('views.login.loginForm.username.label')" />
<el-table-column prop="source" :label="$t('views.userManage.source.label')">
<template #default="{ row }">
{{
row.source === 'LOCAL'
? $t('views.userManage.source.local')
: row.source === 'wecom'
? $t('views.userManage.source.wecom')
: row.source === 'lark'
? $t('views.userManage.source.lark')
: row.source === 'dingtalk'
? $t('views.userManage.source.dingtalk')
: row.source === 'OAUTH2' || row.source === 'OAuth2'
? 'OAuth2'
: row.source
}}
</template>
</el-table-column>
<el-table-column :width="140" align="center">
<template #header>
<el-checkbox :model-value="allChecked" :indeterminate="allIndeterminate" :disabled="disabled"
@change="handleCheckAll">{{ $t('views.chatUser.authorization') }}</el-checkbox>
</template>
<template #default="{ row }">
<el-checkbox v-model="row.enable" :indeterminate="row.indeterminate" :disabled="disabled"
@change="(value: boolean) => handleRowChange(value, row)" />
</template>
</el-table-column>
</app-table>
</div>
</div>
</el-card>
</ContentContainer>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch, reactive, computed } from 'vue'
import ChatUserApi from '@/api/chat-user/chat-user'
import { t } from '@/locales'
import type { ChatUserGroupItem, ChatUserResourceParams, ChatUserGroupUserItem } from '@/api/type/workspaceChatUser'
import { useRoute } from 'vue-router'
import { ChatUserResourceEnum } from '@/enums/workspaceChatUser'
const route = useRoute()
const resource: ChatUserResourceParams = { resource_id: route.params.id as string, resource_type: route.meta.resourceType as ChatUserResourceEnum }
const disabled = computed(() => false) // TODO
const filterText = ref('')
const loading = ref(false)
const list = ref<ChatUserGroupItem[]>([])
const filterList = ref<ChatUserGroupItem[]>([]) //
const current = ref<ChatUserGroupItem>()
async function getUserGroupList() {
try {
const res = await ChatUserApi.getUserGroupList(resource, loading)
list.value = res.data
filterList.value = filter(list.value, filterText.value)
} catch (error) {
console.error(error)
}
}
onMounted(async () => {
await getUserGroupList()
current.value = list.value[0]
})
function filter(list: ChatUserGroupItem[], filterText: string) {
if (!filterText.length) {
return list
}
return list.filter((v: ChatUserGroupItem) =>
v.name.toLowerCase().includes(filterText.toLowerCase()),
)
}
watch(filterText, (val: string) => {
filterList.value = filter(list.value, val)
})
function clickUserGroup(item: ChatUserGroupItem) {
current.value = item
}
const rightLoading = ref(false)
const searchType = ref('username_or_nickname')
const searchForm = ref<Record<string, any>>({
username_or_nickname: '',
})
const automaticAuthorization = ref(false)
const paginationConfig = reactive({
current_page: 1,
page_size: 20,
total: 0,
})
const tableData = ref<ChatUserGroupUserItem[]>([])
async function getList() {
if (!current.value?.id) return
try {
const params = {
[searchType.value]: searchForm.value[searchType.value],
}
const res = await ChatUserApi.getUserGroupUserList(resource, current.value?.id, paginationConfig, params, rightLoading)
tableData.value = res.data.records
paginationConfig.total = res.data.total
} catch (error) {
console.error(error)
}
}
function handleSizeChange() {
paginationConfig.current_page = 1
getList()
}
watch(() => current.value?.id, () => {
getList()
})
const allChecked = computed(() =>
tableData.value.length > 0 && tableData.value.every((item: ChatUserGroupUserItem) => item.is_auth)
);
const allIndeterminate = computed(() =>
!allChecked.value && tableData.value.some((item: ChatUserGroupUserItem) => item.is_auth)
);
const handleCheckAll = (checked: boolean) => {
tableData.value.forEach((item: ChatUserGroupUserItem) => {
item.is_auth = checked;
});
};
const handleRowChange = (value: boolean, row: ChatUserGroupUserItem) => {
row.is_auth = value;
};
async function handleSave() {
try {
const params = tableData.value.map(item => ({ chat_user_id: item.id, is_auth: item.is_auth }))
await ChatUserApi.putUserGroupUser(resource, current.value?.id as string, params, rightLoading)
} catch (error) {
console.error(error)
}
}
</script>
<style lang="scss" scoped>
.content-container {
height: 100%;
display: flex;
flex-direction: column;
:deep(.content-container__main) {
flex: 1;
overflow: hidden;
}
}
:deep(.user-card) {
height: 100%;
overflow: hidden;
}
.user-left {
box-sizing: border-box;
width: var(--setting-left-width);
min-width: var(--setting-left-width);
.user-left_title {
padding: 8px;
}
.list-height-left {
height: calc(100vh - 271px);
:deep(.common-list li) {
padding-right: 4px;
padding-left: 8px;
}
}
}
.user-right {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 24px;
}
</style>

View File

@ -1,48 +1,50 @@
<template>
<div class="p-16-24">
<h2 class="mb-16">{{ $t('views.userManage.title') }}</h2>
<el-card>
<ContentContainer>
<template #header>
<div class="shared-header">
<span class="title">{{ t('views.system.shared_resources') }}</span>
<el-icon size="12">
<rightOutlined></rightOutlined>
</el-icon>
<span class="sub-title">{{ t('views.knowledge.title') }}</span>
</div>
</template>
<el-card class="h-full">
<div class="flex-between mb-16">
<el-button type="primary" @click="createUser()">{{
$t('views.userManage.createUser')
}}
</el-button>
<div>
<el-button type="primary" @click="createUser()">
{{ t('views.userManage.createUser') }}
</el-button>
<el-button :disabled="multipleSelection.length === 0">
{{ $t('views.chatUser.syncUsers') }}
</el-button>
<el-button :disabled="multipleSelection.length === 0">
{{ $t('views.chatUser.setUserGroups') }}
</el-button>
<el-button :disabled="multipleSelection.length === 0">
{{ $t('common.delete') }}
</el-button>
</div>
<div class="flex-between complex-search">
<el-select
class="complex-search__left"
v-model="search_type"
style="width: 120px"
@change="search_type_change"
>
<el-option :label="$t('views.login.loginForm.username.label')" value="name"/>
<el-select class="complex-search__left" v-model="search_type" style="width: 120px"
@change="search_type_change">
<el-option :label="$t('views.login.loginForm.username.label')" value="name" />
</el-select>
<el-input
v-if="search_type === 'name'"
v-model="search_form.name"
@change="getList"
:placeholder="$t('common.searchBar.placeholder')"
style="width: 220px"
clearable
/>
<el-input v-if="search_type === 'name'" v-model="search_form.name" @change="getList"
:placeholder="$t('common.searchBar.placeholder')" style="width: 220px" clearable />
</div>
</div>
<app-table
class="mt-16"
:data="userTableData"
:pagination-config="paginationConfig"
@sizeChange="handleSizeChange"
@changePage="getList"
v-loading="loading"
>
<el-table-column prop="nick_name" :label="$t('views.userManage.userForm.nick_name.label')"/>
<el-table-column prop="username" :label="$t('views.userManage.userForm.username.label')"/>
<app-table class="mt-16" :data="userTableData" :pagination-config="paginationConfig"
@sizeChange="handleSizeChange" @changePage="getList" v-loading="loading"
@selection-change="handleSelectionChange" @sort-change="handleSortChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="nick_name" :label="$t('views.userManage.userForm.nick_name.label')" />
<el-table-column prop="username" :label="$t('common.username')" />
<el-table-column prop="is_active" :label="$t('common.status.label')">
<template #default="{ row }">
<div v-if="row.is_active" class="flex align-center">
<el-icon class="color-success mr-8" style="font-size: 16px"
>
<SuccessFilled
/>
<el-icon class="color-success mr-8" style="font-size: 16px">
<SuccessFilled />
</el-icon>
<span class="color-secondary">
{{ $t('common.status.enabled') }}
@ -71,6 +73,12 @@
{{ row.phone || '-' }}
</template>
</el-table-column>
<!-- TODO -->
<el-table-column prop="user_group_names" :label="$t('views.chatUser.group.title')">
<template #default="{ row }">
{{ row.user_group_names || '-' }}
</template>
</el-table-column>
<el-table-column prop="source" :label="$t('views.userManage.source.label')">
<template #default="{ row }">
{{
@ -98,57 +106,54 @@
<el-table-column :label="$t('common.operation')" width="160" align="left" fixed="right">
<template #default="{ row }">
<span @click.stop>
<el-switch
size="small"
v-model="row.is_active"
:before-change="() => changeState(row)"
/>
<el-switch size="small" v-model="row.is_active" :before-change="() => changeState(row)" />
</span>
<el-divider direction="vertical"/>
<el-divider direction="vertical" />
<span class="mr-8">
<el-button type="primary" text @click.stop="editUser(row)" :title="$t('common.edit')">
<el-icon><EditPen/></el-icon>
<el-icon>
<EditPen />
</el-icon>
</el-button>
</span>
<span class="mr-8">
<el-button
type="primary"
text
@click.stop="editPwdUser(row)"
:title="$t('views.userManage.setting.updatePwd')"
>
<el-icon><Lock/></el-icon>
<el-button type="primary" text @click.stop="editPwdUser(row)"
:title="$t('views.userManage.setting.updatePwd')">
<el-icon>
<Lock />
</el-icon>
</el-button>
</span>
<span>
<el-button
:disabled="row.role === 'ADMIN'"
type="primary"
text
@click.stop="deleteUserManage(row)"
:title="$t('common.delete')"
>
<el-icon><Delete/></el-icon>
<el-button :disabled="row.role === 'ADMIN'" type="primary" text @click.stop="deleteUserManage(row)"
:title="$t('common.delete')">
<el-icon>
<Delete />
</el-icon>
</el-button>
</span>
</template>
</el-table-column>
</app-table>
</el-card>
<UserDrawer :title="title" ref="UserDrawerRef" @refresh="refresh"/>
<UserPwdDialog ref="UserPwdDialogRef" @refresh="refresh"/>
</div>
</ContentContainer>
<UserDrawer :title="title" ref="UserDrawerRef" @refresh="refresh" />
<UserPwdDialog ref="UserPwdDialogRef" @refresh="refresh" />
</template>
<script lang="ts" setup>
import {onMounted, ref, reactive, watch} from 'vue'
import { onMounted, ref, reactive, watch } from 'vue'
import UserDrawer from './component/UserDrawer.vue'
import UserPwdDialog from './component/UserPwdDialog.vue'
import userManageApi from '@/api/system/chat-user'
import {datetimeFormat} from '@/utils/time'
import {MsgSuccess, MsgConfirm} from '@/utils/message'
import {t} from '@/locales'
import { datetimeFormat } from '@/utils/time'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { t } from '@/locales'
import iconMap from '@/components/app-icon/icons/common'
const rightOutlined = iconMap['right-outlined'].iconReader()
const search_type = ref('name')
const search_form = ref<{
@ -156,11 +161,17 @@ const search_form = ref<{
}>({
name: '',
})
const search_type_change = () => {
search_form.value = { name: '' }
}
const UserDrawerRef = ref()
const UserPwdDialogRef = ref()
const loading = ref(false)
const multipleSelection = ref<string[]>([])
function handleSelectionChange(val: string[]) {
multipleSelection.value = val
}
const paginationConfig = reactive({
current_page: 1,
page_size: 20,
@ -169,15 +180,6 @@ const paginationConfig = reactive({
const userTableData = ref<any[]>([])
const search_type_change = () => {
search_form.value = {name: ''}
}
function handleSizeChange() {
paginationConfig.current_page = 1
getList()
}
function getList() {
return userManageApi
.getUserManage(paginationConfig, search_form.value.name, loading)
@ -187,6 +189,17 @@ function getList() {
})
}
const orderBy = ref<string>('')
function handleSortChange({ prop, order }: { prop: string; order: string }) {
orderBy.value = order === 'ascending' ? prop : `-${prop}`
getList()
}
function handleSizeChange() {
paginationConfig.current_page = 1
getList()
}
function changeState(row: any) {
const obj = {
is_active: !row.is_active,
@ -205,7 +218,7 @@ function changeState(row: any) {
}
const title = ref('')
const UserDrawerRef = ref()
function editUser(row: any) {
title.value = t('views.userManage.editUser')
UserDrawerRef.value.open(row)
@ -236,6 +249,7 @@ function deleteUserManage(row: any) {
})
}
const UserPwdDialogRef = ref()
function editPwdUser(row: any) {
UserPwdDialogRef.value.open(row)
}
@ -249,4 +263,14 @@ onMounted(() => {
})
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.content-container {
height: 100%;
display: flex;
flex-direction: column;
:deep(.content-container__main) {
flex: 1;
}
}
</style>