|
|
|
@ -6,137 +6,179 @@
|
|
|
|
|
<div class="avatar-section">
|
|
|
|
|
<p class="section-title">头像</p>
|
|
|
|
|
<div class="avatar-preview">
|
|
|
|
|
<!-- 当前头像(从接口获取初始值) -->
|
|
|
|
|
<img
|
|
|
|
|
:src="currentAvatar"
|
|
|
|
|
alt="当前头像"
|
|
|
|
|
class="current-avatar"
|
|
|
|
|
>
|
|
|
|
|
<!-- 新头像预览(选择文件后显示) -->
|
|
|
|
|
<img
|
|
|
|
|
v-if="previewAvatar"
|
|
|
|
|
:src="previewAvatar"
|
|
|
|
|
alt="新头像预览"
|
|
|
|
|
class="new-avatar"
|
|
|
|
|
>
|
|
|
|
|
<img :src="currentAvatar" alt="当前头像" class="current-avatar">
|
|
|
|
|
<img v-if="previewAvatar" :src="previewAvatar" alt="新头像预览" class="new-avatar">
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 文件选择按钮 -->
|
|
|
|
|
<label class="upload-btn">
|
|
|
|
|
选择新头像
|
|
|
|
|
<input
|
|
|
|
|
type="file"
|
|
|
|
|
accept="image/*"
|
|
|
|
|
@change="handleAvatarChange"
|
|
|
|
|
class="file-input"
|
|
|
|
|
>
|
|
|
|
|
<input type="file" accept="image/*" @change="handleAvatarChange" class="file-input">
|
|
|
|
|
</label>
|
|
|
|
|
<!-- 头像错误提示 -->
|
|
|
|
|
<p v-if="errors.avatar" class="error-tip">{{ errors.avatar }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 表单 -->
|
|
|
|
|
<form @submit.prevent="handleSubmit" class="form">
|
|
|
|
|
<!-- 昵称 -->
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label class="label">昵称</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
v-model.trim="formData.nickname"
|
|
|
|
|
class="input"
|
|
|
|
|
:class="{ 'input-error': errors.nickname }"
|
|
|
|
|
>
|
|
|
|
|
<p v-if="errors.nickname" class="error-tip">{{ errors.nickname }}</p>
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<!-- 用户名 -->
|
|
|
|
|
<div class="form-item half">
|
|
|
|
|
<label class="label">用户名</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
v-model.trim="formData.username"
|
|
|
|
|
class="input"
|
|
|
|
|
:class="{ 'input-error': errors.username }"
|
|
|
|
|
maxlength="20"
|
|
|
|
|
placeholder="请输入用户名"
|
|
|
|
|
>
|
|
|
|
|
<p v-if="errors.username" class="error-tip">{{ errors.username }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 手机号 -->
|
|
|
|
|
<div class="form-item half">
|
|
|
|
|
<label class="label">手机号</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
v-model.trim="formData.phone"
|
|
|
|
|
class="input"
|
|
|
|
|
:class="{ 'input-error': errors.phone }"
|
|
|
|
|
maxlength="11"
|
|
|
|
|
placeholder="请输入11位手机号"
|
|
|
|
|
>
|
|
|
|
|
<p v-if="errors.phone" class="error-tip">{{ errors.phone }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<!-- 邮箱 -->
|
|
|
|
|
<div class="form-item half">
|
|
|
|
|
<label class="label">邮箱</label>
|
|
|
|
|
<input
|
|
|
|
|
type="email"
|
|
|
|
|
v-model.trim="formData.email"
|
|
|
|
|
class="input"
|
|
|
|
|
:class="{ 'input-error': errors.email }"
|
|
|
|
|
placeholder="请输入邮箱"
|
|
|
|
|
>
|
|
|
|
|
<p v-if="errors.email" class="error-tip">{{ errors.email }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 学号 -->
|
|
|
|
|
<div class="form-item half">
|
|
|
|
|
<label class="label">学号</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
v-model.trim="formData.studentId"
|
|
|
|
|
class="input"
|
|
|
|
|
:class="{ 'input-error': errors.studentId }"
|
|
|
|
|
maxlength="20"
|
|
|
|
|
placeholder="请输入学号"
|
|
|
|
|
>
|
|
|
|
|
<p v-if="errors.studentId" class="error-tip">{{ errors.studentId }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<!-- 性别 -->
|
|
|
|
|
<div class="form-item half">
|
|
|
|
|
<label class="label">性别</label>
|
|
|
|
|
<select v-model="formData.gender" class="input" :class="{ 'input-error': errors.gender }">
|
|
|
|
|
<option value="0">未知</option>
|
|
|
|
|
<option value="1">男</option>
|
|
|
|
|
<option value="2">女</option>
|
|
|
|
|
</select>
|
|
|
|
|
<p v-if="errors.gender" class="error-tip">{{ errors.gender }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 学院 -->
|
|
|
|
|
<div class="form-item half">
|
|
|
|
|
<label class="label">学院</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
v-model.trim="formData.college"
|
|
|
|
|
class="input"
|
|
|
|
|
:class="{ 'input-error': errors.college }"
|
|
|
|
|
maxlength="30"
|
|
|
|
|
placeholder="请输入学院"
|
|
|
|
|
>
|
|
|
|
|
<p v-if="errors.college" class="error-tip">{{ errors.college }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 个人简介 -->
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label class="label">个人简介</label>
|
|
|
|
|
<textarea
|
|
|
|
|
v-model.trim="formData.bio"
|
|
|
|
|
v-model.trim="formData.moto"
|
|
|
|
|
rows="4"
|
|
|
|
|
class="textarea"
|
|
|
|
|
:class="{ 'input-error': errors.bio }"
|
|
|
|
|
:class="{ 'input-error': errors.moto }"
|
|
|
|
|
maxlength="100"
|
|
|
|
|
placeholder="请输入个人简介(最多100字)"
|
|
|
|
|
></textarea>
|
|
|
|
|
<p v-if="errors.bio" class="error-tip">{{ errors.bio }}</p>
|
|
|
|
|
<p v-if="errors.moto" class="error-tip">{{ errors.moto }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 密码 -->
|
|
|
|
|
<div class="form-item">
|
|
|
|
|
<label class="label">新密码</label>
|
|
|
|
|
<input
|
|
|
|
|
type="password"
|
|
|
|
|
v-model.trim="formData.password"
|
|
|
|
|
class="input"
|
|
|
|
|
:class="{ 'input-error': errors.password }"
|
|
|
|
|
placeholder="如需修改请输入新密码"
|
|
|
|
|
>
|
|
|
|
|
<p v-if="errors.password" class="error-tip">{{ errors.password }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 提交按钮 -->
|
|
|
|
|
<button type="submit" class="submit-btn">保存修改</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, onMounted } from 'vue';
|
|
|
|
|
import { useRouter } from 'vue-router';
|
|
|
|
|
import { useUserStore } from '@/stores/user.js'; // 引入Pinia用户状态
|
|
|
|
|
import request from '@/utils/request'; // 引入封装的请求模块
|
|
|
|
|
import { ElMessage } from 'element-plus'; // 使用Element Plus提示组件
|
|
|
|
|
import { ref, computed } from 'vue';
|
|
|
|
|
import { useUserStore } from '@/stores/user';
|
|
|
|
|
import request from '@/utils/request';
|
|
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
|
|
|
|
|
// 从状态管理中获取用户ID和登录状态
|
|
|
|
|
const userId = ref(userStore.userInfo.userid);
|
|
|
|
|
const isLoggedIn = userStore.isLoggedIn;
|
|
|
|
|
const userInfo = computed(() => userStore.userInfo);
|
|
|
|
|
|
|
|
|
|
// 初始化表单数据
|
|
|
|
|
const formData = ref({
|
|
|
|
|
nickname: '',
|
|
|
|
|
bio: ''
|
|
|
|
|
username: userInfo.value.username || '',
|
|
|
|
|
phone: userInfo.value.phone || '',
|
|
|
|
|
email: userInfo.value.email || '',
|
|
|
|
|
studentId: userInfo.value.studentId || '',
|
|
|
|
|
gender: userInfo.value.gender !== undefined ? String(userInfo.value.gender) : '0',
|
|
|
|
|
college: userInfo.value.college || '',
|
|
|
|
|
moto: userInfo.value.moto || '',
|
|
|
|
|
password: userInfo.value.password || ''
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 头像相关变量
|
|
|
|
|
const avatarFile = ref(null); // 选中的头像文件
|
|
|
|
|
const previewAvatar = ref(''); // 新头像预览 URL
|
|
|
|
|
const currentAvatar = ref(''); // 初始头像
|
|
|
|
|
const errors = ref({}); // 错误提示
|
|
|
|
|
|
|
|
|
|
// 组件挂载时检查登录状态并获取用户信息
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
if (!isLoggedIn) {
|
|
|
|
|
ElMessage.error('请先登录');
|
|
|
|
|
router.push({ name: 'Login' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 使用封装的request模块请求用户信息
|
|
|
|
|
const res = await request.get(`/user/info/${userId.value}`);
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
formData.value.nickname = res.data.nickname;
|
|
|
|
|
formData.value.bio = res.data.bio;
|
|
|
|
|
currentAvatar.value = res.data.avatar;
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(res.msg || '接口返回异常');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error(`获取用户信息失败:${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const avatarFile = ref(null);
|
|
|
|
|
const previewAvatar = ref('');
|
|
|
|
|
const currentAvatar = ref(userInfo.value.avatar);
|
|
|
|
|
const errors = ref({});
|
|
|
|
|
const router=useRouter();
|
|
|
|
|
|
|
|
|
|
// 处理头像选择
|
|
|
|
|
const handleAvatarChange = (e) => {
|
|
|
|
|
const file = e.target.files[0];
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
|
|
|
|
// 验证文件类型和大小(接口文档要求)
|
|
|
|
|
if (!file.type.startsWith('image/')) {
|
|
|
|
|
errors.value.avatar = '请选择图片文件(jpg/png等)';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (file.size > 5 * 1024 * 1024) { // 接口限制5MB
|
|
|
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
|
|
|
errors.value.avatar = '图片大小不能超过5MB';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清除错误提示
|
|
|
|
|
errors.value.avatar = null;
|
|
|
|
|
// 生成预览 URL
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = (e) => previewAvatar.value = e.target.result;
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
// 保存文件对象(用于上传)
|
|
|
|
|
avatarFile.value = file;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@ -145,51 +187,111 @@ const validateForm = () => {
|
|
|
|
|
const newErrors = {};
|
|
|
|
|
|
|
|
|
|
// 昵称验证(接口要求:必填,≤20字符)
|
|
|
|
|
if (!formData.value.nickname) {
|
|
|
|
|
newErrors.nickname = '昵称不能为空';
|
|
|
|
|
} else if (formData.value.nickname.length > 20) {
|
|
|
|
|
newErrors.nickname = '昵称最多20字符';
|
|
|
|
|
if (!formData.value.username) {
|
|
|
|
|
newErrors.username = '用户名不能为空';
|
|
|
|
|
} else if (formData.value.username.length > 20) {
|
|
|
|
|
newErrors.username = '用户名最多20字符';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 手机号
|
|
|
|
|
if (!formData.value.phone) {
|
|
|
|
|
newErrors.phone = '手机号不能为空';
|
|
|
|
|
} else if (!/^\d{11}$/.test(formData.value.phone)) {
|
|
|
|
|
newErrors.phone = '手机号格式不正确';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 邮箱
|
|
|
|
|
if (!formData.value.email) {
|
|
|
|
|
newErrors.email = '邮箱不能为空';
|
|
|
|
|
} else if (!/^[\w.-]+@[\w.-]+\.\w+$/.test(formData.value.email)) {
|
|
|
|
|
newErrors.email = '邮箱格式不正确';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 简介验证(接口要求:≤100字符)
|
|
|
|
|
if (formData.value.bio.length > 100) {
|
|
|
|
|
newErrors.bio = '简介最多100字符';
|
|
|
|
|
// 学号
|
|
|
|
|
if (!formData.value.studentId) {
|
|
|
|
|
newErrors.studentId = '学号不能为空';
|
|
|
|
|
} else if (formData.value.studentId.length > 20) {
|
|
|
|
|
newErrors.studentId = '学号最多20字符';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 性别
|
|
|
|
|
if (!['0', '1', '2'].includes(formData.value.gender)) {
|
|
|
|
|
newErrors.gender = '请选择性别';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 学院
|
|
|
|
|
if (!formData.value.college) {
|
|
|
|
|
newErrors.college = '学院不能为空';
|
|
|
|
|
} else if (formData.value.college.length > 30) {
|
|
|
|
|
newErrors.college = '学院最多30字符';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 简介
|
|
|
|
|
if (formData.value.moto && formData.value.moto.length > 100) {
|
|
|
|
|
newErrors.moto = '简介最多100字符';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 密码
|
|
|
|
|
if (formData.value.password && formData.value.password.length < 6) {
|
|
|
|
|
newErrors.password = '密码长度不能少于6位';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
errors.value = newErrors;
|
|
|
|
|
return Object.keys(newErrors).length === 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 提交表单(调用接口文档中的 /user/info 接口)
|
|
|
|
|
// 提交表单
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
if (!validateForm()) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 构建接口要求的 FormData
|
|
|
|
|
const formDataToSubmit = new FormData();
|
|
|
|
|
// 添加头像(可选,有选择时才添加)
|
|
|
|
|
let avatarUrl = userInfo.value.avatar;
|
|
|
|
|
|
|
|
|
|
// 如果选择了新头像,先上传头像
|
|
|
|
|
if (avatarFile.value) {
|
|
|
|
|
formDataToSubmit.append('avatar', avatarFile.value);
|
|
|
|
|
const formDataObj = new FormData();
|
|
|
|
|
formDataObj.append('file', avatarFile.value);
|
|
|
|
|
const uploadRes = await request.post('/user/info/avatar', formDataObj, {
|
|
|
|
|
headers: { 'Content-Type': 'multipart/form-data' }
|
|
|
|
|
});
|
|
|
|
|
if (uploadRes.code !== 200) {
|
|
|
|
|
throw new Error(uploadRes.msg || '头像上传失败');
|
|
|
|
|
}
|
|
|
|
|
avatarUrl = uploadRes.data; // 假设后端返回的就是头像URL
|
|
|
|
|
currentAvatar.value = avatarUrl;
|
|
|
|
|
userInfo.value.avatar = avatarUrl; // 更新用户信息中的头像
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 修改用户信息
|
|
|
|
|
const updateInfoData = {
|
|
|
|
|
username: formData.value.username,
|
|
|
|
|
phone: formData.value.phone,
|
|
|
|
|
email: formData.value.email,
|
|
|
|
|
studentId: formData.value.studentId,
|
|
|
|
|
avatar: userInfo.value.avatar,
|
|
|
|
|
gender: Number(formData.value.gender),
|
|
|
|
|
college: formData.value.college,
|
|
|
|
|
};
|
|
|
|
|
const updateInfoRes = await request.post('/user/info/update', updateInfoData);
|
|
|
|
|
if (updateInfoRes.code !== 200) {
|
|
|
|
|
throw new Error(updateInfoRes.msg || '修改用户信息失败');
|
|
|
|
|
}
|
|
|
|
|
// 添加其他字段(接口要求的 UserUpdateDTO 字段)
|
|
|
|
|
formDataToSubmit.append('nickname', formData.value.nickname);
|
|
|
|
|
formDataToSubmit.append('bio', formData.value.bio);
|
|
|
|
|
|
|
|
|
|
// 调用接口文档中的 PUT /user/info 接口(使用封装的request)
|
|
|
|
|
const res = await request.put(`/user/info/${userId.value}`, formDataToSubmit, {
|
|
|
|
|
headers: { 'Content-Type': 'multipart/form-data' } // 必须设置
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 接口返回成功(根据文档:code=200 表示成功)
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
// 更新当前头像(从接口返回新的头像 URL)
|
|
|
|
|
if (avatarFile.value) {
|
|
|
|
|
currentAvatar.value = res.data.avatar; // 假设接口返回新头像 URL
|
|
|
|
|
Object.assign(userInfo.value, updateInfoData);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (formData.value.password) {
|
|
|
|
|
const passwordRes = await request.post('/user/info/password', null, {
|
|
|
|
|
params: { password: formData.value.password }
|
|
|
|
|
});
|
|
|
|
|
if (passwordRes.code !== 200) {
|
|
|
|
|
throw new Error(passwordRes.msg || '修改密码失败');
|
|
|
|
|
}
|
|
|
|
|
ElMessage.success('修改成功!');
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(res.msg || '接口返回异常');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ElMessage.success('修改成功!');
|
|
|
|
|
router.push('/');
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error(`修改失败:${error.message}`);
|
|
|
|
|
}
|
|
|
|
@ -197,21 +299,22 @@ const handleSubmit = async () => {
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
/* 样式与原代码基本一致,仅调整无关字段的布局 */
|
|
|
|
|
.change-info-container {
|
|
|
|
|
max-width: 600px;
|
|
|
|
|
max-width: 700px;
|
|
|
|
|
margin: 2rem auto;
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
padding: 2.5rem 2rem 2rem 2rem;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
box-shadow: 0 4px 16px rgba(52, 152, 219, 0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title {
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 2rem;
|
|
|
|
|
font-size: 1.7rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
margin-bottom: 2.2rem;
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
@ -240,56 +343,76 @@ const handleSubmit = async () => {
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border: 2px solid #bdc3c7;
|
|
|
|
|
background: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.new-avatar {
|
|
|
|
|
border-color: #3498db; /* 新头像边框用蓝色区分 */
|
|
|
|
|
border-color: #3498db;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upload-btn {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 0.5rem 1rem;
|
|
|
|
|
background: #3498db;
|
|
|
|
|
padding: 0.5rem 1.2rem;
|
|
|
|
|
background: linear-gradient(90deg, #3498db 60%, #6dd5fa 100%);
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
transition: background 0.3s ease;
|
|
|
|
|
margin-top: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.upload-btn:hover {
|
|
|
|
|
background: #2980b9;
|
|
|
|
|
background: linear-gradient(90deg, #2980b9 60%, #3498db 100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-input {
|
|
|
|
|
display: none; /* 隐藏原生文件输入框 */
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form {
|
|
|
|
|
margin-top: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 2rem;
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-item {
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-item.half {
|
|
|
|
|
width: 50%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.label {
|
|
|
|
|
display: block;
|
|
|
|
|
color: #34495e;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input, .textarea {
|
|
|
|
|
.input, .textarea, select.input {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
border: 1px solid #bdc3c7;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
transition: border-color 0.3s ease;
|
|
|
|
|
background: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input:focus, .textarea:focus {
|
|
|
|
|
.input:focus, .textarea:focus, select.input:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
border-color: #3498db;
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.08);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-error {
|
|
|
|
@ -309,18 +432,20 @@ const handleSubmit = async () => {
|
|
|
|
|
|
|
|
|
|
.submit-btn {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
background: #3498db;
|
|
|
|
|
padding: 0.85rem;
|
|
|
|
|
background: linear-gradient(90deg, #3498db 60%, #6dd5fa 100%);
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 0.3s ease;
|
|
|
|
|
margin-top: 0.5rem;
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.submit-btn:hover {
|
|
|
|
|
background: #2980b9;
|
|
|
|
|
background: linear-gradient(90deg, #2980b9 60%, #3498db 100%);
|
|
|
|
|
}
|
|
|
|
|
</style>
|