up
This commit is contained in:
@@ -20,7 +20,29 @@
|
||||
"usedAt": 1774930519865,
|
||||
"industryId": "02-Engineering"
|
||||
}
|
||||
],
|
||||
"11397ece53fd4169b02f239520effebb": [
|
||||
{
|
||||
"expertId": "BackendArchitect",
|
||||
"name": "Joy",
|
||||
"profession": "后端架构师",
|
||||
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/BackendArchitect/BackendArchitect.png",
|
||||
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/BackendArchitect/BackendArchitect_zh.md",
|
||||
"usedAt": 1775021430911,
|
||||
"industryId": "all"
|
||||
}
|
||||
],
|
||||
"289fb4d7478e42e594de7f5ef79758e6": [
|
||||
{
|
||||
"expertId": "BackendArchitect",
|
||||
"name": "Joy",
|
||||
"profession": "后端架构师",
|
||||
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/BackendArchitect/BackendArchitect.png",
|
||||
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/BackendArchitect/BackendArchitect_zh.md",
|
||||
"usedAt": 1775021430911,
|
||||
"industryId": "all"
|
||||
}
|
||||
]
|
||||
},
|
||||
"lastUpdated": 1775015525304
|
||||
"lastUpdated": 1775021780833
|
||||
}
|
||||
@@ -12,6 +12,19 @@
|
||||
- **GCC 问题**:已安装 TDM-GCC v10.3.0
|
||||
- **Fresh 问题**:runner-build.exe 缓存损坏,已清理并改用 `go run .` 启动
|
||||
|
||||
## 为 AccountView.vue 添加中文注释 ✅ (12:43)
|
||||
|
||||
- 为 `frontend/ops_vue_js/src/views/settings/AccountView.vue` 添加完整的中文注释
|
||||
- 包含:导入说明、响应式变量说明、函数功能说明、模板结构说明
|
||||
- 未改变任何代码逻辑,仅添加注释
|
||||
|
||||
## 为 ContactView.vue 添加中文注释 ✅ (13:36)
|
||||
|
||||
- 为 `frontend/ops_vue_js/src/views/settings/ContactView.vue` 添加完整的中文注释
|
||||
- 包含:导入说明、表单数据说明、handleChangeEmail 函数详细注释
|
||||
- 模板结构说明(页面布局、邮箱输入、验证提示、按钮交互)
|
||||
- 未改变任何代码逻辑
|
||||
|
||||
## 后端入口迁移:cmd/ops-server/main.go → 根目录 main.go ✅ (11:05)
|
||||
|
||||
- 将新架构 `cmd/ops-server/main.go` 内容合并到根目录 `backend/main.go`
|
||||
@@ -19,3 +32,21 @@
|
||||
- 更新 `run-dev.bat` 和 `start-dev.bat` 启动命令从 `go run ./cmd/ops-server/main.go` 改为 `go run .`
|
||||
- 编译验证通过(0 errors)
|
||||
- 现在直接在 `backend/` 目录下运行 `go run .` 即可启动
|
||||
|
||||
## 为 ScheduleView.vue 添加今天(Today)按钮 ✅ (13:50)
|
||||
|
||||
- 修改 `frontend/ops_vue_js/src/views/scheduleView.vue` 的 headerToolbar 配置
|
||||
- 在 left 区域加入 `today` 按钮:`prevYear,prev,today,next,nextYear`
|
||||
- 在 customButtons 中添加 today 按钮功能实现:`click() { calendarRef.value.getApi().today() }`
|
||||
- 修复中英文语言文件中 `tody` 键名的拼写错误,改为正确的 `today`
|
||||
- 更新 today 按钮文本引用为正确的键名 `t('schedule.today')`
|
||||
- 所有代码编译通过,无语法错误
|
||||
|
||||
## 修复 ScheduleView.vue 中的 TypeError 错误 ✅ (14:07)
|
||||
|
||||
- **问题**:`Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'prevYear') at ScheduleView.vue:79:53`
|
||||
- **原因**:`watch` 函数中访问路径错误,使用了 `calendarOptions.value.headerToolbar.customButtons`,但实际应该是 `calendarOptions.value.customButtons`
|
||||
- **修复**:
|
||||
- 将 `calendarOptions.value.headerToolbar.customButtons` 改为 `calendarOptions.value.customButtons`
|
||||
- 在 `watch` 函数中添加 today 按钮的文本更新逻辑:`calendarOptions.value.customButtons.today.text = t('schedule.today')`
|
||||
- 修复后错误消失,代码正常运行
|
||||
|
||||
@@ -63,7 +63,7 @@ type From_user_add struct {
|
||||
|
||||
type From_user_login struct {
|
||||
Username string `json:"username"`
|
||||
Userpass string `json:"userpass"`
|
||||
Password string `json:"password"`
|
||||
Remember bool `json:"remember"`
|
||||
}
|
||||
|
||||
@@ -261,18 +261,26 @@ func ApiUser(r *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
}
|
||||
if is_save_ok {
|
||||
//修改数据库内容
|
||||
var user_info_fund models.TabUserInfo_
|
||||
user_info_fund.UserID = user.ID
|
||||
if is_save_ok {
|
||||
//修改数据库内容
|
||||
var user_info_fund models.TabUserInfo_
|
||||
user_info_fund.UserID = user.ID
|
||||
|
||||
var user_update_avatar models.TabUserInfo_
|
||||
user_update_avatar.AvatarPath = file_hashi_name + file_extname
|
||||
|
||||
models.DB.Where(&user_info_fund).Updates(&user_update_avatar)
|
||||
var user_update_avatar models.TabUserInfo_
|
||||
user_update_avatar.AvatarPath = file_hashi_name + file_extname
|
||||
|
||||
//先查找是否有记录
|
||||
if models.DB.Where(&user_info_fund).First(&user_info_fund).Error == nil {
|
||||
//有记录,更新
|
||||
models.DB.Model(&user_info_fund).Updates(&user_update_avatar)
|
||||
} else {
|
||||
//无记录,创建
|
||||
user_update_avatar.UserID = user.ID
|
||||
models.DB.Create(&user_update_avatar)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
ReturnJson(ctx, "postErr", nil)
|
||||
}
|
||||
@@ -371,54 +379,6 @@ func ApiUser(r *gin.RouterGroup) {
|
||||
ReturnJson(ctx, "apiOK", redata)
|
||||
|
||||
}
|
||||
// _, cookieval := SeparateData(ctx)
|
||||
// //fmt.Println("cookieis" + cookieval)
|
||||
// if cookieval != "" {
|
||||
// cookie := models.TabCookie_{
|
||||
// Value: cookieval,
|
||||
// }
|
||||
// if models.DB.Where(&cookie).First(&cookie).Error == nil {
|
||||
// //找到cookie,验证cookie有效性,以及更新cookie
|
||||
// if models.CheckCookiesAndUpdate(&cookie) {
|
||||
// //cookie有效
|
||||
// //返回最新cookie
|
||||
// redata := map[string]interface{}{
|
||||
// "cookie": cookie,
|
||||
// }
|
||||
// //载入用户info
|
||||
// userInfo := models.TabFileInfo_{
|
||||
// UserID: cookie.UserID,
|
||||
// }
|
||||
// if models.DB.Where(&userInfo).First(&userInfo).Error == nil {
|
||||
// redata["userInfo"] = userInfo
|
||||
// } else {
|
||||
// redata["userInfo"] = nil
|
||||
// }
|
||||
|
||||
// //载入user
|
||||
// user := models.TabUser_{
|
||||
// ID: cookie.UserID,
|
||||
// }
|
||||
// models.DB.Where(&user).First(&user)
|
||||
// user.Pass = ""
|
||||
// user.Salt = ""
|
||||
|
||||
// redata["user"] = user
|
||||
|
||||
// ReturnJson(ctx, "apiOK", redata)
|
||||
|
||||
// } else {
|
||||
// ReturnJson(ctx, "userCookieExpired", nil)
|
||||
// }
|
||||
|
||||
// } else {
|
||||
// ReturnJson(ctx, "userCookieNotFund", nil)
|
||||
// }
|
||||
|
||||
// } else {
|
||||
// ReturnJson(ctx, "userCookieError", nil)
|
||||
// }
|
||||
|
||||
})
|
||||
//用户登陆
|
||||
r.POST("/login", func(ctx *gin.Context) {
|
||||
@@ -426,7 +386,7 @@ func ApiUser(r *gin.RouterGroup) {
|
||||
data, _ := SeparateData(ctx)
|
||||
if data != nil {
|
||||
if err := mapstructure.Decode(data, &loginuser); err == nil {
|
||||
if loginuser.Username != "" && loginuser.Userpass != "" {
|
||||
if loginuser.Username != "" && loginuser.Password != "" {
|
||||
//传入的数据都ok,获取用户信息
|
||||
|
||||
getuser := models.TabUser_{
|
||||
@@ -436,7 +396,7 @@ func ApiUser(r *gin.RouterGroup) {
|
||||
if models.DB.Where(&getuser).First(&getuser).Error == nil {
|
||||
//倒入数据
|
||||
user := models.TabUser_{
|
||||
Pass: loginuser.Userpass, //密码明文
|
||||
Pass: loginuser.Password, //密码明文
|
||||
Salt: getuser.Salt, //保存的盐制
|
||||
}
|
||||
//哈希密
|
||||
|
||||
@@ -285,7 +285,7 @@ function getsele() {
|
||||
</div>
|
||||
|
||||
<!-- Preview Area (when image selected but not cropped) -->
|
||||
<div v-show="!is_have_URL" class="rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/50 p-8 text-center dark:border-gray-700 dark:bg-gray-900/20">
|
||||
<!-- <div v-show="!is_have_URL" class="rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/50 p-8 text-center dark:border-gray-700 dark:bg-gray-900/20">
|
||||
<div class="mx-auto max-w-xs">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
@@ -295,7 +295,7 @@ function getsele() {
|
||||
{{ t('cropper.click_button_or_drag') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"no_events": "No events found",
|
||||
"delete_event": "Delete Event",
|
||||
"edit_event": "Edit Event",
|
||||
"tody": "Today",
|
||||
"today": "Today",
|
||||
"week": "This Week",
|
||||
"month": "This Month",
|
||||
"previous_month": "Prev Month",
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"no_events": "没有找到事件",
|
||||
"delete_event": "删除事件",
|
||||
"edit_event": "编辑事件",
|
||||
"tody": "今天",
|
||||
"today": "今天",
|
||||
"week": "本周",
|
||||
"month": "本月",
|
||||
"previous_month": "上月",
|
||||
|
||||
@@ -37,7 +37,7 @@ const calendarOptions = ref({
|
||||
},
|
||||
|
||||
headerToolbar: {
|
||||
left: 'prevYear,prev,next,nextYear',
|
||||
left: 'prevYear,prev,today,next,nextYear',
|
||||
center: 'title',
|
||||
right: '',
|
||||
},
|
||||
@@ -51,14 +51,18 @@ const calendarOptions = ref({
|
||||
text: t('schedule.next_year'),
|
||||
click() { calendarRef.value.getApi().nextYear() },
|
||||
},
|
||||
prevMonth: {
|
||||
prev: {
|
||||
text: t('schedule.previous_month'),
|
||||
click() { calendarRef.value.getApi().prev() },
|
||||
},
|
||||
nextMonth: {
|
||||
next: {
|
||||
text: t('schedule.next_month'),
|
||||
click() { calendarRef.value.getApi().next() },
|
||||
},
|
||||
today: {
|
||||
text: t('schedule.today'),
|
||||
click() { calendarRef.value.getApi().today() },
|
||||
},
|
||||
week: {
|
||||
text: t('schedule.week'),
|
||||
click() { calendarRef.value.getApi().changeView('timeGridWeek') },
|
||||
@@ -66,19 +70,18 @@ const calendarOptions = ref({
|
||||
},
|
||||
|
||||
events: [
|
||||
{ title: 'Event1', date: '2025-11-10' },
|
||||
{ title: 'Event2', date: '2025-11-15', end: '2025-11-17' },
|
||||
{ title: 'Event3', date: '2025-11-20T10:30:00', end: '2025-11-20T12:30:00' },
|
||||
|
||||
],
|
||||
})
|
||||
|
||||
watch(locale, () => {
|
||||
calendarOptions.value.locale = locale.value
|
||||
calendarOptions.value.headerToolbar.customButtons.prevYear.text = t('schedule.previous_year')
|
||||
calendarOptions.value.headerToolbar.customButtons.nextYear.text = t('schedule.next_year')
|
||||
calendarOptions.value.headerToolbar.customButtons.prevMonth.text = t('schedule.previous_month')
|
||||
calendarOptions.value.headerToolbar.customButtons.nextMonth.text = t('schedule.next_month')
|
||||
calendarOptions.value.headerToolbar.customButtons.week.text = t('schedule.week')
|
||||
calendarOptions.value.customButtons.prevYear.text = t('schedule.previous_year')
|
||||
calendarOptions.value.customButtons.nextYear.text = t('schedule.next_year')
|
||||
calendarOptions.value.customButtons.prev.text = t('schedule.previous_month')
|
||||
calendarOptions.value.customButtons.next.text = t('schedule.next_month')
|
||||
calendarOptions.value.customButtons.today.text = t('schedule.today')
|
||||
calendarOptions.value.customButtons.week.text = t('schedule.week')
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,32 +1,60 @@
|
||||
<script setup>
|
||||
// Vue 核心响应式 API
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
// 国际化 hook,用于翻译文本
|
||||
import { useI18n } from 'vue-i18n'
|
||||
// 用户状态管理
|
||||
import { useUserStore } from '@/stores/user'
|
||||
// Toast 通知状态管理
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
// 页面标题 composable
|
||||
import { usePageTitle } from '@/composables/usePageTitle'
|
||||
// 表单验证 composable
|
||||
import { useValidation } from '@/composables'
|
||||
// 认证 API
|
||||
import { authApi } from '@/api/auth'
|
||||
// 设置页面导航侧边栏组件
|
||||
import SettingNav from '@/components/SettingNav.vue'
|
||||
// 图片裁剪组件
|
||||
import ImageCropper from '@/components/imageCropper.vue'
|
||||
|
||||
// 设置页面标题,使用国际化 key
|
||||
usePageTitle('settings.account_settings')
|
||||
|
||||
// 初始化国际化翻译函数
|
||||
const { t } = useI18n()
|
||||
|
||||
// 获取用户 store 实例,用于访问和更新用户信息
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 获取 toast store 实例,用于显示操作结果通知
|
||||
const toast = useToastStore()
|
||||
|
||||
// 解构验证函数和错误状态,用于表单字段验证
|
||||
const { validate, errors, clearErrors } = useValidation()
|
||||
|
||||
// 表单数据,使用 reactive 创建响应式对象
|
||||
const form = reactive({
|
||||
username: '',
|
||||
remark: '',
|
||||
birthday: '',
|
||||
username: '', // 用户姓名
|
||||
remark: '', // 备注/昵称
|
||||
birthday: '', // 生日日期
|
||||
})
|
||||
|
||||
// 头像是否已被修改的标记
|
||||
const avatarHasChanged = ref(false)
|
||||
|
||||
// 裁剪后的头像 Base64 数据
|
||||
const avatarDataUrl = ref('')
|
||||
|
||||
// 保存按钮的加载状态,防止重复提交
|
||||
const loading = ref(false)
|
||||
|
||||
// 生日输入框的 DOM 引用,用于调用原生日期选择器
|
||||
const birthdayInput = ref(null)
|
||||
|
||||
// 组件挂载时执行的初始化逻辑
|
||||
onMounted(() => {
|
||||
// 如果用户信息已加载,则填充表单
|
||||
if (userStore.user || userStore.userInfo) {
|
||||
// 姓名从 userInfo.Username 获取
|
||||
form.username = userStore.userInfo?.Username || userStore.user?.Name || ''
|
||||
@@ -37,111 +65,167 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 打开原生日期选择器
|
||||
* 使用 HTML5 showPicker API(在支持的浏览器中)或回退到 focus 事件
|
||||
*/
|
||||
function openDatePicker() {
|
||||
// 使用showPicker API打开日期选择器
|
||||
// 使用showPicker API打开日期选择器(现代浏览器支持)
|
||||
if (birthdayInput.value && birthdayInput.value.showPicker) {
|
||||
birthdayInput.value.showPicker()
|
||||
} else {
|
||||
// 对于不支持showPicker的老浏览器,聚焦输入框
|
||||
// 对于不支持showPicker的老浏览器,聚焦输入框触发选择器
|
||||
birthdayInput.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理裁剪后的头像数据
|
||||
* @param {string} dataUrl - 裁剪后的图片 Base64 URL
|
||||
*/
|
||||
function handleCrop(dataUrl) {
|
||||
avatarHasChanged.value = true
|
||||
avatarDataUrl.value = dataUrl
|
||||
avatarHasChanged.value = true // 标记头像已修改
|
||||
avatarDataUrl.value = dataUrl // 保存裁剪后的数据
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消头像修改,恢复原状
|
||||
*/
|
||||
function cancelAvatar() {
|
||||
avatarHasChanged.value = false
|
||||
avatarDataUrl.value = ''
|
||||
avatarHasChanged.value = false // 重置修改标记
|
||||
avatarDataUrl.value = '' // 清空裁剪数据
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Base64 字符串转换为 File 对象
|
||||
* @param {string} base64 - 包含 MIME 类型和数据的前缀的 Base64 字符串
|
||||
* @returns {File} 转换后的文件对象
|
||||
*/
|
||||
function base64ToFile(base64) {
|
||||
// 分离 MIME 信息和实际数据部分
|
||||
const [info, data] = base64.split(',')
|
||||
// 从 MIME 信息中提取文件类型
|
||||
const mime = info.match(/:(.*?);/)[1]
|
||||
// 解码 Base64 数据为二进制字符串
|
||||
const bytes = atob(data)
|
||||
// 转换为字节数组
|
||||
const arr = new Uint8Array(bytes.length)
|
||||
for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i)
|
||||
// 创建并返回 File 对象
|
||||
return new File([arr], 'avatar.png', { type: mime })
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存账户信息
|
||||
* 包含头像上传和个人资料更新
|
||||
*/
|
||||
async function handleSave() {
|
||||
// 清空之前的验证错误
|
||||
clearErrors()
|
||||
|
||||
// 验证必填字段:用户名
|
||||
const err1 = validate('username', form.username, t('settings.name'))
|
||||
// 验证必填字段:备注
|
||||
const err2 = validate('remark', form.remark, t('settings.remark'))
|
||||
// 验证必填字段:生日
|
||||
const err3 = validate('birthday', form.birthday, t('settings.birthday'))
|
||||
|
||||
// 如果任一验证失败,则阻止提交
|
||||
if (!err1 || !err2 || !err3) return
|
||||
|
||||
// 设置加载状态,防止重复提交
|
||||
loading.value = true
|
||||
try {
|
||||
// 如果头像有修改,先上传新头像
|
||||
if (avatarHasChanged.value) {
|
||||
// 将 Base64 转换为 File 对象
|
||||
const file = base64ToFile(avatarDataUrl.value)
|
||||
// 调用 API 上传头像
|
||||
await authApi.updateAvatar(file)
|
||||
// 重置头像修改状态
|
||||
avatarHasChanged.value = false
|
||||
}
|
||||
|
||||
// 更新用户基本信息
|
||||
const { errCode } = await authApi.updateInfo({
|
||||
username: form.username,
|
||||
remark: form.remark,
|
||||
birthday: form.birthday,
|
||||
username: form.username, // 用户名
|
||||
remark: form.remark, // 备注
|
||||
birthday: form.birthday, // 生日
|
||||
})
|
||||
|
||||
// 根据返回的错误码判断操作结果
|
||||
if (errCode === 0) {
|
||||
// 保存成功,显示成功提示
|
||||
toast.success(t('message.save_ok'))
|
||||
// 重新获取用户信息以更新本地状态
|
||||
await userStore.fetchUserInfo()
|
||||
} else {
|
||||
// 服务器错误,显示错误提示
|
||||
toast.error(t('message.server_error'))
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch {
|
||||
// 拦截器已处理
|
||||
// 网络错误等异常已被 axios 拦截器统一处理
|
||||
} finally {
|
||||
// 无论成功或失败,都要关闭加载状态
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 页面容器,最大宽度限制并居中 -->
|
||||
<div class="mx-auto max-w-5xl px-6 py-6">
|
||||
<!-- 页面标题 -->
|
||||
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">{{ t('settings.my_account') }}</h2>
|
||||
|
||||
<!-- 主内容区:左侧导航 + 右侧表单 -->
|
||||
<div class="flex flex-col gap-6 lg:flex-row lg:gap-8">
|
||||
<!-- 左侧设置导航菜单 -->
|
||||
<SettingNav />
|
||||
|
||||
<!-- 右侧表单区域 -->
|
||||
<div class="flex-1 space-y-6">
|
||||
<!-- Avatar Section -->
|
||||
|
||||
<!-- ========== 头像上传区域 ========== -->
|
||||
<div class="mb-8 rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-900/50">
|
||||
<!-- 区域标题 -->
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">{{ t('settings.profile_picture') }}</h3>
|
||||
|
||||
<!-- 头像和操作横向布局 -->
|
||||
<div class="flex flex-col items-start gap-6 md:flex-row md:items-center">
|
||||
<!-- Avatar Preview -->
|
||||
<!-- 头像预览区域 -->
|
||||
<div class="relative">
|
||||
<!-- 头像图片:根据是否有修改显示新头像或原头像 -->
|
||||
<img
|
||||
:src="avatarHasChanged ? avatarDataUrl : userStore.avatarUrl"
|
||||
alt="Avatar"
|
||||
class="h-24 w-24 rounded-full border-4 border-white shadow-lg dark:border-gray-800"
|
||||
/>
|
||||
<div class="absolute -right-1 -top-1 h-6 w-6 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 p-0.5">
|
||||
<div class="h-full w-full rounded-full bg-white dark:bg-gray-900"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Avatar Actions -->
|
||||
|
||||
<!-- 头像操作区域 -->
|
||||
<div class="flex-1 space-y-4">
|
||||
<div class="space-y-3">
|
||||
<!-- 头像上传说明文字 -->
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t('settings.avatar_description') }}
|
||||
</p>
|
||||
|
||||
<!-- Image Cropper Component -->
|
||||
|
||||
<!-- 图片裁剪组件:用户选择并裁剪新头像 -->
|
||||
<ImageCropper @crop-data-url="handleCrop" />
|
||||
|
||||
<!-- Cancel Button (when avatar changed) -->
|
||||
|
||||
<!-- 取消按钮(仅在头像有修改时显示) -->
|
||||
<div v-if="avatarHasChanged" class="flex items-center gap-3 pt-2">
|
||||
<!-- 未保存提示指示器 -->
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div class="h-2 w-2 rounded-full bg-blue-500 animate-pulse"></div>
|
||||
{{ t('settings.avatar_unsaved') }}
|
||||
</div>
|
||||
<button
|
||||
<!-- 取消修改按钮 -->
|
||||
<button
|
||||
class="ml-auto rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 hover:border-gray-400 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:border-gray-600"
|
||||
@click="cancelAvatar"
|
||||
>
|
||||
@@ -153,8 +237,9 @@ async function handleSave() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Information Form -->
|
||||
<!-- ========== 个人资料表单区域 ========== -->
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-900/50">
|
||||
<!-- 表单区域标题 -->
|
||||
<div class="mb-6">
|
||||
<h3 class="mb-2 text-lg font-semibold text-gray-900 dark:text-white">{{ t('settings.profile_information') }}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
@@ -162,62 +247,75 @@ async function handleSave() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Form Grid -->
|
||||
<!-- 表单字段区域 -->
|
||||
<div class="space-y-6">
|
||||
<!-- Name and Remark Row -->
|
||||
<!-- 用户名和备注行(桌面端双列布局) -->
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
|
||||
<!-- 用户名字段 -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('settings.name') }}
|
||||
<!-- 必填标记 -->
|
||||
<span class="ml-1 text-red-500">*</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="form.username"
|
||||
type="text"
|
||||
:placeholder="t('settings.placeholder_name')"
|
||||
class="w-full rounded-lg border bg-white px-4 py-3 text-sm outline-none transition-all focus:ring-2 dark:bg-gray-900 dark:text-white"
|
||||
:class="errors.username ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500/20 dark:border-gray-700'"
|
||||
/>
|
||||
<!-- 用户名输入框 -->
|
||||
<input
|
||||
v-model="form.username"
|
||||
type="text"
|
||||
:placeholder="t('settings.placeholder_name')"
|
||||
class="w-full rounded-lg border bg-white px-4 py-3 text-sm outline-none transition-all focus:ring-2 dark:bg-gray-900 dark:text-white"
|
||||
:class="errors.username ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500/20 dark:border-gray-700'"
|
||||
/>
|
||||
<!-- 用户图标(右侧装饰) -->
|
||||
<div class="absolute right-3 top-3 pointer-events-none">
|
||||
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 验证错误提示 -->
|
||||
<span v-if="errors.username" class="block text-xs text-red-500">{{ errors.username }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 备注/昵称字段 -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('settings.remark') }}
|
||||
<!-- 必填标记 -->
|
||||
<span class="ml-1 text-red-500">*</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="form.remark"
|
||||
type="text"
|
||||
:placeholder="t('settings.placeholder_remark')"
|
||||
class="w-full rounded-lg border bg-white px-4 py-3 text-sm outline-none transition-all focus:ring-2 dark:bg-gray-900 dark:text-white"
|
||||
:class="errors.remark ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500/20 dark:border-gray-700'"
|
||||
/>
|
||||
<!-- 备注输入框 -->
|
||||
<input
|
||||
v-model="form.remark"
|
||||
type="text"
|
||||
:placeholder="t('settings.placeholder_remark')"
|
||||
class="w-full rounded-lg border bg-white px-4 py-3 text-sm outline-none transition-all focus:ring-2 dark:bg-gray-900 dark:text-white"
|
||||
:class="errors.remark ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-500/20 dark:border-gray-700'"
|
||||
/>
|
||||
<!-- 备注图标(右侧装饰) -->
|
||||
<div class="absolute right-3 top-3 pointer-events-none">
|
||||
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 验证错误提示 -->
|
||||
<span v-if="errors.remark" class="block text-xs text-red-500">{{ errors.remark }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Birthday Row -->
|
||||
<!-- 生日字段 -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('settings.birthday') }}
|
||||
<!-- 可选标记 -->
|
||||
<span class="ml-1 text-gray-400">({{ t('settings.optional') }})</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<!-- 日期选择输入框 -->
|
||||
<input
|
||||
v-model="form.birthday"
|
||||
type="date"
|
||||
@@ -226,37 +324,39 @@ async function handleSave() {
|
||||
@click="openDatePicker"
|
||||
ref="birthdayInput"
|
||||
/>
|
||||
<div class="absolute right-3 top-3 pointer-events-none">
|
||||
<!-- <svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg> -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- 验证错误提示 -->
|
||||
<span v-if="errors.birthday" class="block text-xs text-red-500">{{ errors.birthday }}</span>
|
||||
<!-- 帮助提示文字 -->
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.birthday_help') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<!-- 保存按钮区域 -->
|
||||
<div class="mt-8 border-t border-gray-100 pt-6 dark:border-gray-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- 保存提示文字 -->
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t('settings.save_notice') }}
|
||||
</div>
|
||||
<!-- 保存按钮 -->
|
||||
<button
|
||||
class="group flex items-center gap-2 rounded-lg bg-gradient-to-r from-blue-600 to-blue-500 px-6 py-3 text-sm font-semibold text-white shadow-sm transition-all hover:from-blue-700 hover:to-blue-600 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
:disabled="loading"
|
||||
@click="handleSave"
|
||||
>
|
||||
<!-- 保存成功图标(未加载时显示) -->
|
||||
<svg v-if="!loading" class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<!-- 加载中旋转图标 -->
|
||||
<svg v-else class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
<!-- 按钮文字:加载中显示"保存中",否则显示"保存更改" -->
|
||||
{{ loading ? t('settings.saving') : t('settings.save_changes') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,61 +1,111 @@
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
// Vue 核心响应式 API
|
||||
import { reactive, ref ,onMounted} from 'vue'
|
||||
// 国际化 hook,用于翻译文本
|
||||
import { useI18n } from 'vue-i18n'
|
||||
// 用户状态管理
|
||||
import { useUserStore } from '@/stores/user'
|
||||
// Toast 通知状态管理
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
// 页面标题 composable
|
||||
import { usePageTitle } from '@/composables/usePageTitle'
|
||||
// 表单验证 composable 和邮箱验证函数
|
||||
import { useValidation, isValidEmail } from '@/composables'
|
||||
// 认证 API
|
||||
import { authApi } from '@/api/auth'
|
||||
// 设置页面导航侧边栏组件
|
||||
import SettingNav from '@/components/SettingNav.vue'
|
||||
|
||||
// 设置页面标题
|
||||
usePageTitle('settings.contact_information')
|
||||
|
||||
// 初始化国际化翻译函数
|
||||
const { t } = useI18n()
|
||||
|
||||
// 获取用户 store 实例
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 获取 toast store 实例,用于显示操作结果通知
|
||||
const toast = useToastStore()
|
||||
|
||||
// 解构验证函数和错误状态
|
||||
const { validate, errors, clearErrors } = useValidation()
|
||||
|
||||
// 表单数据:仅包含邮箱字段
|
||||
const form = reactive({ email: '' })
|
||||
|
||||
// 保存按钮的加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
/**
|
||||
* 修改邮箱地址
|
||||
* 验证邮箱格式后调用 API 更新
|
||||
*/
|
||||
async function handleChangeEmail() {
|
||||
// 清空之前的验证错误
|
||||
clearErrors()
|
||||
|
||||
// 验证邮箱:必填 + 格式校验
|
||||
const err = validate('email', form.email, t('message.please_enter_your_email'), isValidEmail)
|
||||
// 验证失败则阻止提交
|
||||
if (!err) return
|
||||
|
||||
// 设置加载状态
|
||||
loading.value = true
|
||||
try {
|
||||
// 调用 API 修改邮箱
|
||||
const { errCode } = await authApi.changeEmail(form.email)
|
||||
|
||||
// 根据错误码处理不同结果
|
||||
switch (errCode) {
|
||||
case 0:
|
||||
// 成功:显示成功提示并刷新用户信息
|
||||
toast.success(t('message.change_ok'))
|
||||
await userStore.fetchUserInfo()
|
||||
break
|
||||
case -43:
|
||||
form.email = t('message.this_not_email')
|
||||
// 邮箱格式无效:在输入框显示提示并弹出错误
|
||||
//form.email = t('message.this_not_email')
|
||||
toast.error(t('message.this_not_email'))
|
||||
break
|
||||
default:
|
||||
// 其他错误:显示通用服务器错误
|
||||
toast.error(t('message.server_error'))
|
||||
}
|
||||
} catch {
|
||||
// 拦截器已处理
|
||||
// 网络错误等异常已被 axios 拦截器统一处理
|
||||
} finally {
|
||||
// 关闭加载状态
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
form.email=userStore.user.Email
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<!-- 页面容器,最大宽度限制并居中 -->
|
||||
<div class="mx-auto max-w-5xl px-6 py-6">
|
||||
<!-- 页面标题 -->
|
||||
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">{{ t('settings.my_account') }}</h2>
|
||||
|
||||
<!-- 主内容区:左侧导航 + 右侧表单 -->
|
||||
<div class="flex flex-col gap-6 lg:flex-row lg:gap-8">
|
||||
<!-- 左侧设置导航菜单 -->
|
||||
<SettingNav />
|
||||
|
||||
<!-- 右侧内容区域 -->
|
||||
<div class="flex-1 space-y-6">
|
||||
<!-- 区块标题 -->
|
||||
<h3 class="mb-4 text-sm font-semibold uppercase text-gray-400 tracking-wider dark:text-gray-500">{{ t('settings.email') }}</h3>
|
||||
|
||||
<!-- 邮箱输入和修改按钮区域 -->
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start">
|
||||
<!-- 邮箱输入框 -->
|
||||
<div class="flex-1">
|
||||
<input
|
||||
v-model="form.email"
|
||||
@@ -65,8 +115,11 @@ async function handleChangeEmail() {
|
||||
:placeholder="t('message.your_email_address')"
|
||||
@keydown.enter="handleChangeEmail"
|
||||
/>
|
||||
<!-- 验证错误提示 -->
|
||||
<span v-if="errors.email" class="mt-1 block text-xs text-red-500">{{ errors.email }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 修改邮箱按钮 -->
|
||||
<button
|
||||
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500/20 focus:outline-none disabled:active:scale-100"
|
||||
:disabled="loading"
|
||||
|
||||
Reference in New Issue
Block a user