feature(user): 优化用户体验

- 替换创建新对话按钮图标,提升视觉一致性
- 移动撤销源预览 URL 的逻辑,避免重复定义
- 移除知识库卡片组件中的多余间距,简化样式
This commit is contained in:
Wenjie Zhang 2026-04-21 00:18:59 +08:00
parent cd47227888
commit 4e0046beb1
4 changed files with 44 additions and 54 deletions

View File

@ -36,7 +36,7 @@
:disabled="chatUIStore.creatingNewChat" :disabled="chatUIStore.creatingNewChat"
> >
<LoaderCircle v-if="chatUIStore.creatingNewChat" size="18" class="loading-icon" /> <LoaderCircle v-if="chatUIStore.creatingNewChat" size="18" class="loading-icon" />
<MessageSquarePlus v-else size="18" /> <MessageCirclePlus v-else size="18" />
创建新对话 创建新对话
</button> </button>
</div> </div>
@ -112,7 +112,7 @@ import { computed, h } from 'vue'
import { message, Modal } from 'ant-design-vue' import { message, Modal } from 'ant-design-vue'
import { import {
PanelLeftClose, PanelLeftClose,
MessageSquarePlus, MessageCirclePlus,
LoaderCircle, LoaderCircle,
Pin, Pin,
PinOff, PinOff,
@ -318,15 +318,15 @@ const togglePin = (chatId) => {
height: 100%; height: 100%;
&.sidebar-open { &.sidebar-open {
box-shadow: 4px 0 16px rgba(0, 0, 0, 0.08); box-shadow: 4px 0 16px var(--dark-5);
border-right: none; border-right: 1px solid var(--gray-50);
} }
} }
&.sidebar-open { &.sidebar-open {
width: 280px; width: 280px;
max-width: 300px; max-width: 300px;
border-right: 1px solid var(--gray-200); border-right: 1px solid var(--gray-150);
} }
.sidebar-header { .sidebar-header {

View File

@ -156,6 +156,13 @@ const downloadingMarkdown = ref(false)
const sourcePreviewLoading = ref(false) const sourcePreviewLoading = ref(false)
const sourcePreviewUrl = ref('') const sourcePreviewUrl = ref('')
const revokeSourcePreviewUrl = () => {
if (sourcePreviewUrl.value) {
window.URL.revokeObjectURL(sourcePreviewUrl.value)
sourcePreviewUrl.value = ''
}
}
// //
const theme = computed(() => (themeStore.isDark ? 'dark' : 'light')) const theme = computed(() => (themeStore.isDark ? 'dark' : 'light'))
@ -239,13 +246,6 @@ const afterOpenChange = (open) => {
} }
} }
const revokeSourcePreviewUrl = () => {
if (sourcePreviewUrl.value) {
window.URL.revokeObjectURL(sourcePreviewUrl.value)
sourcePreviewUrl.value = ''
}
}
const loadSourcePreview = async () => { const loadSourcePreview = async () => {
if (!file.value?.file_id || !store.databaseId || !hasSourcePreview.value) return if (!file.value?.file_id || !store.databaseId || !hasSourcePreview.value) return
if (sourcePreviewUrl.value) return if (sourcePreviewUrl.value) return

View File

@ -501,7 +501,6 @@ const deleteDatabase = () => {
.tags-section { .tags-section {
display: flex; display: flex;
gap: 6px;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
} }

View File

@ -132,7 +132,6 @@
<a-input <a-input
v-model:value="userManagement.form.username" v-model:value="userManagement.form.username"
placeholder="请输入用户名2-20个字符" placeholder="请输入用户名2-20个字符"
size="large"
@blur="validateAndGenerateUserId" @blur="validateAndGenerateUserId"
:maxlength="20" :maxlength="20"
/> />
@ -141,23 +140,19 @@
</div> </div>
</a-form-item> </a-form-item>
<!-- 显示自动生成的用户ID --> <!-- 显示自动生成的用户ID仅添加用户时显示 -->
<a-form-item <a-form-item
v-if="userManagement.form.generatedUserId || userManagement.editMode" v-if="userManagement.form.generatedUserId && !userManagement.editMode"
label="用户ID" label="用户ID"
class="form-item" class="form-item"
> >
<a-input <a-input
:value="userManagement.form.generatedUserId" :value="userManagement.form.generatedUserId"
placeholder="自动生成" placeholder="自动生成"
size="large"
disabled disabled
:addon-before="userManagement.editMode ? '已存在ID' : '登录ID'" :addon-before="'登录ID'"
/> />
<div v-if="!userManagement.editMode" class="help-text"> <div class="help-text">此ID将用于登录根据用户名自动生成</div>
此ID将用于登录根据用户名自动生成
</div>
<div v-else class="help-text">编辑模式下不能修改用户ID</div>
</a-form-item> </a-form-item>
<!-- 手机号字段 --> <!-- 手机号字段 -->
@ -165,7 +160,6 @@
<a-input <a-input
v-model:value="userManagement.form.phoneNumber" v-model:value="userManagement.form.phoneNumber"
placeholder="请输入手机号(可选,可用于登录)" placeholder="请输入手机号(可选,可用于登录)"
size="large"
:maxlength="11" :maxlength="11"
/> />
<div v-if="userManagement.form.phoneError" class="error-text"> <div v-if="userManagement.form.phoneError" class="error-text">
@ -186,7 +180,6 @@
<a-input-password <a-input-password
v-model:value="userManagement.form.password" v-model:value="userManagement.form.password"
placeholder="请输入密码" placeholder="请输入密码"
size="large"
/> />
</a-form-item> </a-form-item>
@ -194,7 +187,6 @@
<a-input-password <a-input-password
v-model:value="userManagement.form.confirmPassword" v-model:value="userManagement.form.confirmPassword"
placeholder="请再次输入密码" placeholder="请再次输入密码"
size="large"
/> />
</a-form-item> </a-form-item>
</template> </template>
@ -204,11 +196,11 @@
label="角色" label="角色"
class="form-item" class="form-item"
> >
<a-input value="超级管理员" size="large" disabled /> <a-input value="超级管理员" disabled />
<div class="help-text">超级管理员账户无法修改角色</div> <div class="help-text">超级管理员账户无法修改角色</div>
</a-form-item> </a-form-item>
<a-form-item v-else label="角色" class="form-item"> <a-form-item v-else label="角色" class="form-item">
<a-select v-model:value="userManagement.form.role" size="large"> <a-select v-model:value="userManagement.form.role">
<a-select-option value="user">普通用户</a-select-option> <a-select-option value="user">普通用户</a-select-option>
<a-select-option value="admin" v-if="userStore.isSuperAdmin">管理员</a-select-option> <a-select-option value="admin" v-if="userStore.isSuperAdmin">管理员</a-select-option>
</a-select> </a-select>
@ -218,7 +210,6 @@
<a-form-item v-if="userStore.isSuperAdmin" label="部门" class="form-item"> <a-form-item v-if="userStore.isSuperAdmin" label="部门" class="form-item">
<a-select <a-select
v-model:value="userManagement.form.departmentId" v-model:value="userManagement.form.departmentId"
size="large"
placeholder="请选择部门" placeholder="请选择部门"
> >
<a-select-option <a-select-option
@ -237,7 +228,7 @@
<script setup> <script setup>
import { reactive, onMounted, watch } from 'vue' import { reactive, onMounted, watch } from 'vue'
import { notification, Modal } from 'ant-design-vue' import { message, Modal } from 'ant-design-vue'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { departmentApi } from '@/apis' import { departmentApi } from '@/apis'
import { Plus, Pencil, Trash2, User, UserLock, UserStar } from 'lucide-vue-next' import { Plus, Pencil, Trash2, User, UserLock, UserStar } from 'lucide-vue-next'
@ -407,7 +398,7 @@ const handleUserFormSubmit = async () => {
try { try {
// //
if (!userManagement.form.username.trim()) { if (!userManagement.form.username.trim()) {
notification.error({ message: '用户名不能为空' }) message.error('用户名不能为空')
return return
} }
@ -416,24 +407,24 @@ const handleUserFormSubmit = async () => {
userManagement.form.username.trim().length < 2 || userManagement.form.username.trim().length < 2 ||
userManagement.form.username.trim().length > 20 userManagement.form.username.trim().length > 20
) { ) {
notification.error({ message: '用户名长度必须在 2-20 个字符之间' }) message.error('用户名长度必须在 2-20 个字符之间')
return return
} }
// //
if (userManagement.form.phoneNumber && !validatePhoneNumber(userManagement.form.phoneNumber)) { if (userManagement.form.phoneNumber && !validatePhoneNumber(userManagement.form.phoneNumber)) {
notification.error({ message: '请输入正确的手机号格式' }) message.error('请输入正确的手机号格式')
return return
} }
if (userManagement.displayPasswordFields) { if (userManagement.displayPasswordFields) {
if (!userManagement.form.password) { if (!userManagement.form.password) {
notification.error({ message: '密码不能为空' }) message.error('密码不能为空')
return return
} }
if (userManagement.form.password !== userManagement.form.confirmPassword) { if (userManagement.form.password !== userManagement.form.confirmPassword) {
notification.error({ message: '两次输入的密码不一致' }) message.error('两次输入的密码不一致')
return return
} }
} }
@ -464,7 +455,7 @@ const handleUserFormSubmit = async () => {
} }
await userStore.updateUser(userManagement.editUserId, updateData) await userStore.updateUser(userManagement.editUserId, updateData)
notification.success({ message: '用户更新成功' }) message.success('用户更新成功')
} else { } else {
// //
const createData = { const createData = {
@ -484,7 +475,7 @@ const handleUserFormSubmit = async () => {
} }
await userStore.createUser(createData) await userStore.createUser(createData)
notification.success({ message: '用户创建成功' }) message.success('用户创建成功')
} }
// //
@ -492,10 +483,7 @@ const handleUserFormSubmit = async () => {
userManagement.modalVisible = false userManagement.modalVisible = false
} catch (error) { } catch (error) {
console.error('用户操作失败:', error) console.error('用户操作失败:', error)
notification.error({ message.error(error.message || '操作失败,请稍后重试')
message: '操作失败',
description: error.message || '请稍后重试'
})
} finally { } finally {
userManagement.loading = false userManagement.loading = false
} }
@ -505,7 +493,7 @@ const handleUserFormSubmit = async () => {
const confirmDeleteUser = (user) => { const confirmDeleteUser = (user) => {
// //
if (user.id === userStore.userId) { if (user.id === userStore.userId) {
notification.error({ message: '不能删除自己的账户' }) message.error('不能删除自己的账户')
return return
} }
@ -520,15 +508,12 @@ const confirmDeleteUser = (user) => {
try { try {
userManagement.loading = true userManagement.loading = true
await userStore.deleteUser(user.id) await userStore.deleteUser(user.id)
notification.success({ message: '用户删除成功' }) message.success('用户删除成功')
// //
await fetchUsers() await fetchUsers()
} catch (error) { } catch (error) {
console.error('删除用户失败:', error) console.error('删除用户失败:', error)
notification.error({ message.error(error.message || '删除失败,请稍后重试')
message: '删除失败',
description: error.message || '请稍后重试'
})
} finally { } finally {
userManagement.loading = false userManagement.loading = false
} }
@ -776,30 +761,31 @@ onMounted(async () => {
.user-modal { .user-modal {
:deep(.ant-modal-header) { :deep(.ant-modal-header) {
padding: 20px 24px; padding: 20px 24px 16px;
border-bottom: 1px solid var(--gray-150); border-bottom: 1px solid var(--gray-150);
.ant-modal-title { .ant-modal-title {
font-size: 16px; font-size: 17px;
font-weight: 600; font-weight: 600;
color: var(--gray-900); color: var(--gray-900);
} }
} }
:deep(.ant-modal-body) { :deep(.ant-modal-body) {
padding: 24px; padding: 20px 24px 24px;
} }
.user-form { .user-form {
.form-item { .form-item {
margin-bottom: 20px; margin-bottom: 16px;
:deep(.ant-form-item-label) { :deep(.ant-form-item-label) {
padding-bottom: 4px; padding-bottom: 6px;
label { label {
font-weight: 500; font-weight: 600;
color: var(--gray-900); font-size: 13px;
color: var(--gray-800);
} }
} }
} }
@ -820,10 +806,15 @@ onMounted(async () => {
.password-toggle { .password-toggle {
margin-bottom: 16px; margin-bottom: 16px;
padding: 12px 16px;
background: var(--gray-25);
border-radius: 8px;
border: 1px solid var(--gray-100);
:deep(.ant-checkbox-wrapper) { :deep(.ant-checkbox-wrapper) {
font-weight: 500; font-weight: 500;
color: var(--gray-600); color: var(--gray-700);
font-size: 13px;
} }
} }
} }