Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ src/api/
`import WorkspaceApi from '@/api/admin/system/workspace'`;System 登录设置使用
`import AuthSettingApi from '@/api/admin/system/settings/auth-setting'`。

### 当前账号密码

`admin/auth/current-user.ts` 的 `postCurrentUserPassword` 向 `/user/current/reset_password`
提交 RSA 加密的 `{ encryptedData }`。头像菜单的 `layout/avatar-dropdown/ChangePasswordDialog.vue`
负责新密码与确认密码校验、加密和提交,成功后清除本地登录凭据并跳转登录页。

### 四类特殊资源 API

`application`、`knowledge`、`model`、`tool` 是需要同时考虑 Workspace、System 资源管理和
Expand Down
10 changes: 8 additions & 2 deletions ui/src/api/admin/auth/current-user.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/** 提供 Admin 登录后的当前用户接口。 */

import { get } from '../core/request'
import { get, post } from '../core/request'
import type { ListItem } from '@/api/types'
import type { PasswordRequest } from '../core/types'
import type { CurrentUserInfo } from './types'

/** 获取当前登录用户、权限、语言及可用工作空间。 */
Expand All @@ -19,4 +20,9 @@ const getCurrentUserRoleList = () => {
return get<ListItem[]>('/role_list/current_user')
}

export default { getCurrentUserInfo, getCurrentUserRoleList, getCurrentUserWorkspaceList }
/** 修改当前登录用户的密码,成功后当前登录凭据失效。 */
const postCurrentUserPassword = (password: PasswordRequest) => {
return post<PasswordRequest, boolean>('/user/current/reset_password', password)
}

export default { postCurrentUserPassword, getCurrentUserInfo, getCurrentUserRoleList, getCurrentUserWorkspaceList }
2 changes: 2 additions & 0 deletions ui/src/api/admin/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ const getCaptcha = (username?: string) => {
return get<CaptchaResponse>('/user/captcha', { username })
}



export default { getCaptcha, postLdapLogin, postLogin, postLogout }
4 changes: 2 additions & 2 deletions ui/src/api/admin/auth/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Admin 认证 API 及其 Store 消费方共同使用的类型。 */

import type { LoginMethod, QrCodeConfig, ListItem } from '@/api/types'
import type { LoginMethod, QrCodeConfig, WorkspaceItem } from '@/api/types'

export interface CurrentUserInfo {
email: string
Expand All @@ -14,7 +14,7 @@ export interface CurrentUserInfo {
role_name?: string[]
source?: string
username: string
workspace_list?: ListItem[]
workspace_list?: WorkspaceItem[]
}

export interface LoginRequest {
Expand Down
2 changes: 2 additions & 0 deletions ui/src/api/types/system-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export interface WorkspaceItem {
name: string
id?: string
user_count?: number
/** 当前用户在工作空间中的角色名称;普通工作空间列表可能不返回。 */
role_name?: string[]
}

export interface CreateWorkspaceMemberPayload {
Expand Down
2 changes: 1 addition & 1 deletion ui/src/assets/iconfont.js

Large diffs are not rendered by default.

1,213 changes: 200 additions & 1,013 deletions ui/src/components/COMPONENT_README.md

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions ui/src/components/business/workspace-dropdown/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import MkFilterableDropdown from '@/components/mk-filterable-dropdown/index.vue'

defineOptions({ name: 'WorkspaceDropdown' })

const props = defineProps<{ options: WorkspaceItem[] }>()
const props = withDefaults(
defineProps<{
options: WorkspaceItem[]
/** 是否显示工作空间角色标签 */
showRoleTags?: boolean
}>(),
{ showRoleTags: false },
)
const selectedWorkspaceId = defineModel<string>({ required: true })
const emit = defineEmits<{ select: [option: WorkspaceItem] }>()
</script>
Expand All @@ -18,6 +25,7 @@ const emit = defineEmits<{ select: [option: WorkspaceItem] }>()
@select="emit('select', $event)"
>
<template #default="{ text }">
<!-- 切换工作空间 -->
<el-button text class="flex max-w-50 items-center gap-1 rounded-md px-2! py-[7px]! text-N900!">
<MkIcon name="icon_moments-categories_outlined" class="mr-1" />
<span class="min-w-0 flex-1 truncate" :title="text">{{ text }}</span>
Expand All @@ -26,10 +34,13 @@ const emit = defineEmits<{ select: [option: WorkspaceItem] }>()
</template>

<template #option="{ option }">
<div class="flex items-center gap-2">
<MkIcon name="icon_moments-categories_outlined" />
<span class="min-w-0 flex-1 truncate" :title="option.name">{{ option.name }}</span>
<div class="flex min-w-0 flex-1 items-center gap-2">
<MkIcon name="icon_moments-categories_outlined" class="shrink-0" />
<span class="min-w-0 truncate" :title="option.name">{{ option.name }}</span>
<MkTagGroup v-if="props.showRoleTags && option.role_name?.length" :tags="option.role_name" size="small" class="min-w-0 shrink-0" />
</div>
</template>
</MkFilterableDropdown>
</template>

<style scoped lang="scss"></style>
15 changes: 11 additions & 4 deletions ui/src/components/global/mk-tag-group/index.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { TagProps } from 'element-plus'

defineOptions({ name: 'MkTagGroup' })

withDefaults(
Expand All @@ -7,14 +9,18 @@ withDefaults(
popoverDisabled?: boolean
/** 标签文字 */
tags?: string[]
/** 标签尺寸 */
size?: TagProps['size']
/** 首个标签的类型 */
type?: TagProps['type']
}>(),
{ popoverDisabled: false, tags: () => [] },
{ popoverDisabled: false, tags: () => [], type: 'info' },
)
</script>

<template>
<span class="inline-flex items-center gap-1">
<el-tag type="info" :title="tags?.[0]">
<el-tag :type="type" :size="size" :title="tags?.[0]" :disable-transitions="true">
{{ tags?.[0] }}
</el-tag>

Expand All @@ -24,13 +30,14 @@ withDefaults(
popper-class="mk-tag-group__popper"
trigger="hover"
:popper-style="{ maxWidth: '250px', width: 'auto' }"
:show-after="200"
v-if="tags?.length > 1"
>
<template #reference>
<el-tag type="info" class="cursor-pointer">+{{ tags.length - 1 }}</el-tag>
<el-tag type="info" :size="size" class="cursor-pointer" :disable-transitions="true">+{{ tags.length - 1 }}</el-tag>
</template>
<div class="flex-wrap gap-2 px-4 py-3">
<el-tag v-for="tag in tags.slice(1)" :key="tag" type="info" :title="tag">
<el-tag v-for="tag in tags.slice(1)" :key="tag" type="info" :size="size" :title="tag" :disable-transitions="true">
{{ tag }}
</el-tag>
</div>
Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/mk-filterable-dropdown/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ function handleItemClick(option: TOption) {
<slot :selected-option="selectedOption" :text="selectedText" />

<template #dropdown>
<div class="w-70 overflow-hidden rounded-md">
<div class="min-w-70 max-w-80 overflow-hidden rounded-md">
<div class="p-2 pb-1" @click.stop @keydown.stop>
<MkSearchInput v-model="searchKeyword" />
</div>

<el-scrollbar max-height="200px">
<el-scrollbar max-height="220px">
<div class="px-1">
<MkDropdownMenu>
<MkDropdownItem
Expand Down
122 changes: 122 additions & 0 deletions ui/src/layout/avatar-dropdown/ChangePasswordDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import JSEncrypt from 'jsencrypt'
import type { FormInstance, FormRules } from 'element-plus'
import { useRouter } from 'vue-router'
import CurrentUserApi from '@/api/admin/auth/current-user'
import type { UpdatePasswordForm } from '@/api/types'
import { useStore } from '@/stores'
import { MsgError, MsgSuccess } from '@/utils/message'

defineOptions({ name: 'ChangePasswordDialog' })

const router = useRouter()
const { auth } = useStore()

const dialogVisible = ref(false)
const passwordSubmitting = ref(false)

// 当前账号密码表单与校验。
const userPasswordFormRef = ref<FormInstance>()
const userPasswordForm = reactive<UpdatePasswordForm>({ password: '', re_password: '' })

function validateConfirmPassword(_rule: unknown, value: string, callback: (error?: Error) => void) {
if (!value) {
callback(new Error('请输入确认密码'))
} else if (value !== userPasswordForm.password) {
callback(new Error('输入的密码不一致'))
} else {
callback()
}
}

const userPasswordRules = reactive<FormRules<UpdatePasswordForm>>({
password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度应为 6-20 个字符', trigger: 'blur' },
],
re_password: [{ validator: validateConfirmPassword, trigger: 'blur' }],
})

function open() {
resetData()
dialogVisible.value = true
}

// 加密提交成功后,服务端会使当前登录凭据失效。
function submitPassword() {
if (passwordSubmitting.value) return
userPasswordFormRef.value?.validate((valid) => {
if (!valid) return
passwordSubmitting.value = true
const encryptor = new JSEncrypt()
encryptor.setPublicKey(auth.baseProfile?.rsa ?? '')
const encryptedData = encryptor.encrypt(JSON.stringify(userPasswordForm))
if (!encryptedData) {
passwordSubmitting.value = false
MsgError('密码加密失败,请刷新页面后重试')
return
}

return CurrentUserApi.postCurrentUserPassword({ encryptedData })
.then(() => {
MsgSuccess('密码修改成功,请重新登录')
auth.clearToken()
dialogVisible.value = false
return router.push({ name: 'login' }).then(() => undefined)
})
.finally(() => {
passwordSubmitting.value = false
})
})
}

function resetData() {
Object.assign(userPasswordForm, { password: '', re_password: '' })
passwordSubmitting.value = false
userPasswordFormRef.value?.clearValidate()
}
defineExpose({ open })
</script>

<template>
<MkDialog v-model="dialogVisible" title="修改密码" :show-close="!passwordSubmitting" @closed="resetData">
<el-form
ref="userPasswordFormRef"
:model="userPasswordForm"
:disabled="passwordSubmitting"
:rules="userPasswordRules"
label-position="top"
require-asterisk-position="right"
@submit.prevent
>
<el-form-item label="新密码" prop="password">
<el-input
v-model="userPasswordForm.password"
autocomplete="new-password"
maxlength="20"
placeholder="请输入新密码"
show-password
type="password"
/>
</el-form-item>
<el-form-item label="确认密码" prop="re_password">
<el-input
v-model="userPasswordForm.re_password"
autocomplete="new-password"
maxlength="20"
placeholder="请再次输入新密码"
show-password
type="password"
/>
</el-form-item>
</el-form>

<template #footer>
<!-- 取消修改密码 -->
<el-button :disabled="passwordSubmitting" plain @click="dialogVisible = false">取消</el-button>
<!-- 保存新密码 -->
<el-button :loading="passwordSubmitting" type="primary" @click="submitPassword"> 保存 </el-button>
</template>
</MkDialog>
</template>
Loading
Loading