365 lines
8.7 KiB
Vue
365 lines
8.7 KiB
Vue
<template>
|
|
<div class="user-form-container">
|
|
<h1 class="page-title">{{ isEditing ? '编辑用户' : '新建用户' }}</h1>
|
|
|
|
<div class="form-container">
|
|
<form @submit.prevent="handleSubmit" class="user-form">
|
|
<!-- Username Field -->
|
|
<div class="form-group">
|
|
<label for="username">用户名</label>
|
|
<input
|
|
type="text"
|
|
id="username"
|
|
v-model="form.username"
|
|
placeholder="请输入用户名"
|
|
required
|
|
/>
|
|
<div class="error-message" v-if="errors.username">
|
|
{{ errors.username }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Email Field -->
|
|
<div class="form-group">
|
|
<label for="email">邮箱</label>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
v-model="form.email"
|
|
placeholder="请输入邮箱"
|
|
required
|
|
/>
|
|
<div class="error-message" v-if="errors.email">
|
|
{{ errors.email }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Role Field -->
|
|
<div class="form-group">
|
|
<label for="role">角色</label>
|
|
<CustomSelect
|
|
v-model="form.role"
|
|
:options="roleOptions"
|
|
placeholder="请选择角色"
|
|
/>
|
|
<div class="error-message" v-if="errors.role">
|
|
{{ errors.role }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Field -->
|
|
<div class="form-group">
|
|
<label for="isActive">状态</label>
|
|
<CustomSelect
|
|
v-model="form.isActive"
|
|
:options="statusOptions"
|
|
placeholder="请选择状态"
|
|
/>
|
|
<div class="error-message" v-if="errors.isActive">
|
|
{{ errors.isActive }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Submit Buttons -->
|
|
<div class="form-actions">
|
|
<button type="button" class="cancel-btn" @click="handleCancel">
|
|
取消
|
|
</button>
|
|
<button type="submit" class="submit-btn" :disabled="isSubmitting">
|
|
{{ isSubmitting ? '提交中...' : (isEditing ? '更新用户' : '创建用户') }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, onMounted, computed } from 'vue'
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
import { useToast } from '../../composables/useToast'
|
|
import { createUser, updateUser, fetchUser, User, API_BASE, getAuthHeaders } from '../../services/api'
|
|
import CustomSelect from '../../components/CustomSelect.vue'
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const toast = useToast()
|
|
|
|
// Form state
|
|
const isSubmitting = ref(false)
|
|
const isEditing = computed(() => !!route.params.id)
|
|
const errors = reactive<Record<string, string>>({})
|
|
|
|
// Form data
|
|
const form = reactive({
|
|
username: '',
|
|
email: '',
|
|
role: 'viewer',
|
|
isActive: 1
|
|
})
|
|
|
|
// Select options
|
|
const roleOptions = [
|
|
{ value: 'admin', label: '管理员' },
|
|
{ value: 'editor', label: '编辑' },
|
|
{ value: 'viewer', label: '查看者' }
|
|
]
|
|
|
|
const statusOptions = [
|
|
{ value: 1, label: '激活' },
|
|
{ value: 0, label: '禁用' }
|
|
]
|
|
|
|
// Validation function
|
|
const validateForm = (): boolean => {
|
|
// Reset errors
|
|
Object.keys(errors).forEach(key => delete errors[key])
|
|
|
|
let isValid = true
|
|
|
|
// Validate username
|
|
if (!form.username.trim()) {
|
|
errors.username = '用户名不能为空'
|
|
isValid = false
|
|
}
|
|
|
|
// Validate email
|
|
if (!form.email.trim()) {
|
|
errors.email = '邮箱不能为空'
|
|
isValid = false
|
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
|
|
errors.email = '请输入有效的邮箱地址'
|
|
isValid = false
|
|
}
|
|
|
|
// Validate role
|
|
if (!form.role) {
|
|
errors.role = '请选择角色'
|
|
isValid = false
|
|
}
|
|
|
|
return isValid
|
|
}
|
|
|
|
// Submit handler
|
|
const handleSubmit = async () => {
|
|
if (!validateForm()) {
|
|
return
|
|
}
|
|
|
|
isSubmitting.value = true
|
|
|
|
try {
|
|
if (isEditing.value) {
|
|
// Update existing user
|
|
await updateUser(parseInt(route.params.id as string), form)
|
|
toast.success('用户更新成功')
|
|
} else {
|
|
// Create new user
|
|
await createUser(form)
|
|
toast.success('用户创建成功')
|
|
}
|
|
|
|
// Redirect to users list
|
|
router.push('/admin/users')
|
|
} catch (error: any) {
|
|
console.error('Error submitting form:', error)
|
|
toast.error(error.message || (isEditing.value ? '更新用户失败' : '创建用户失败'))
|
|
} finally {
|
|
isSubmitting.value = false
|
|
}
|
|
}
|
|
|
|
// Cancel handler
|
|
const handleCancel = () => {
|
|
router.push('/admin/users')
|
|
}
|
|
|
|
// Lifecycle
|
|
onMounted(async () => {
|
|
if (isEditing.value) {
|
|
try {
|
|
const userId = parseInt(route.params.id as string)
|
|
console.log(`Fetching user data for ID: ${userId}`)
|
|
|
|
// Direct fetch to debug
|
|
const response = await fetch(`${API_BASE}/admin/users/${userId}`, {
|
|
headers: getAuthHeaders()
|
|
})
|
|
|
|
console.log(`Response status: ${response.status}`)
|
|
|
|
// Check response headers
|
|
const contentType = response.headers.get('content-type')
|
|
console.log(`Response content-type: ${contentType}`)
|
|
|
|
// Read response as text first to debug
|
|
const responseText = await response.text()
|
|
console.log(`Response text: ${responseText}`)
|
|
|
|
// Then try to parse as JSON
|
|
if (!response.ok) {
|
|
// If response is not ok, still try to parse as JSON
|
|
let errorData
|
|
try {
|
|
errorData = JSON.parse(responseText)
|
|
throw new Error(errorData.error || '获取用户详情失败')
|
|
} catch (parseError) {
|
|
throw new Error(`获取用户详情失败,响应格式错误: ${parseError.message}`)
|
|
}
|
|
}
|
|
|
|
// Parse successful response
|
|
const user = JSON.parse(responseText)
|
|
console.log('Parsed user data:', user)
|
|
|
|
// Populate form with user data
|
|
form.username = user.username
|
|
form.email = user.email
|
|
form.role = user.role
|
|
form.isActive = user.isActive
|
|
} catch (error: any) {
|
|
console.error('Failed to fetch user data:', error)
|
|
console.error('Error stack:', error.stack)
|
|
toast.error('加载用户数据失败: ' + (error.message || '未知错误'))
|
|
}
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.user-form-container {
|
|
width: 100%;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 1.75rem;
|
|
font-weight: 600;
|
|
color: #d4b383;
|
|
margin-bottom: 1.5rem;
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
.form-container {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
backdrop-filter: blur(10px);
|
|
-webkit-backdrop-filter: blur(10px);
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
padding: 2rem;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.user-form {
|
|
max-width: 600px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
font-size: 0.95rem;
|
|
font-weight: 500;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
margin-bottom: 0.5rem;
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group select {
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
border-radius: 0.5rem;
|
|
font-size: 0.95rem;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
color: white;
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
.form-group input::placeholder,
|
|
.form-group select::placeholder {
|
|
color: rgba(255, 255, 255, 0.5);
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group select:focus {
|
|
outline: none;
|
|
border-color: #d4b383;
|
|
box-shadow: 0 0 0 3px rgba(212, 179, 131, 0.2);
|
|
}
|
|
|
|
.error-message {
|
|
color: #ef4444;
|
|
font-size: 0.875rem;
|
|
margin-top: 0.25rem;
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.cancel-btn {
|
|
padding: 0.75rem 1.5rem;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: rgba(255, 255, 255, 0.8);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
border-radius: 0.5rem;
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
.cancel-btn:hover {
|
|
background: rgba(212, 179, 131, 0.1);
|
|
border-color: #d4b383;
|
|
color: #d4b383;
|
|
}
|
|
|
|
.submit-btn {
|
|
padding: 0.75rem 1.5rem;
|
|
background-color: #d4b383;
|
|
color: #050505;
|
|
border: 1px solid #d4b383;
|
|
border-radius: 0.5rem;
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
.submit-btn:hover:not(:disabled) {
|
|
background-color: transparent;
|
|
color: #d4b383;
|
|
box-shadow: 0 0 15px rgba(212, 179, 131, 0.3);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.submit-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 768px) {
|
|
.form-container {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.form-actions {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.cancel-btn,
|
|
.submit-btn {
|
|
width: 100%;
|
|
}
|
|
}
|
|
</style>
|