使用il8n
This commit is contained in:
parent
9207ca69bb
commit
07a1214127
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import zhCN from './locales/zh-CN/index'
|
||||||
|
import enUS from './locales/en-US/index'
|
||||||
|
|
||||||
|
export type { LanguageType } from '@/stores/modules/language'
|
||||||
|
|
||||||
|
// 创建 i18n 实例
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false, // 使用 Composition API 模式
|
||||||
|
locale: localStorage.getItem('lang') || 'zh-CN', // 默认语言
|
||||||
|
fallbackLocale: 'zh-CN', // 回退语言
|
||||||
|
messages: {
|
||||||
|
'zh-CN': zhCN,
|
||||||
|
'en-US': enUS
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default i18n
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
// English translation file
|
||||||
|
export default {
|
||||||
|
// Common
|
||||||
|
common: {
|
||||||
|
loading: 'Loading...',
|
||||||
|
submit: 'Submit',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
confirm: 'Confirm',
|
||||||
|
edit: 'Edit',
|
||||||
|
delete: 'Delete',
|
||||||
|
save: 'Save',
|
||||||
|
close: 'Close',
|
||||||
|
search: 'Search',
|
||||||
|
reset: 'Reset',
|
||||||
|
pleaseInput: 'Please enter',
|
||||||
|
pleaseSelect: 'Please select',
|
||||||
|
operation: 'Operation',
|
||||||
|
status: 'Status',
|
||||||
|
createTime: 'Create Time',
|
||||||
|
updateTime: 'Update Time',
|
||||||
|
remark: 'Remark',
|
||||||
|
success: 'Operation successful',
|
||||||
|
error: 'Operation failed',
|
||||||
|
warning: 'Warning',
|
||||||
|
info: 'Info'
|
||||||
|
},
|
||||||
|
// Personal Center
|
||||||
|
personalCenter: {
|
||||||
|
pageTitle: 'Account Information',
|
||||||
|
username: 'Username',
|
||||||
|
email: 'Email',
|
||||||
|
mobile: 'Mobile',
|
||||||
|
password: 'Password',
|
||||||
|
language: 'Language',
|
||||||
|
languageDesc: 'Select your preferred communication language',
|
||||||
|
lastLogin: 'Last Login',
|
||||||
|
notSet: 'Not set',
|
||||||
|
notBound: 'Not bound',
|
||||||
|
bound: 'Bound',
|
||||||
|
goToBind: 'Go to bind',
|
||||||
|
bind: 'Bind',
|
||||||
|
modify: 'Modify',
|
||||||
|
bindEmail: 'Bind Email',
|
||||||
|
bindMobile: 'Bind Mobile',
|
||||||
|
rebindMobile: 'Rebind Mobile',
|
||||||
|
modifyUsername: 'Modify Username',
|
||||||
|
modifyPassword: 'Modify Password',
|
||||||
|
verifyMethod: 'Verification Method',
|
||||||
|
verifyAccount: 'Verification Account',
|
||||||
|
mobileVerify: 'Mobile Verification',
|
||||||
|
emailVerify: 'Email Verification',
|
||||||
|
newPassword: 'New Password',
|
||||||
|
confirmPassword: 'Confirm Password',
|
||||||
|
passwordPlaceholder: '6-12 characters, including uppercase, lowercase and special characters',
|
||||||
|
confirmPasswordPlaceholder: 'Please enter new password again',
|
||||||
|
// Validation messages
|
||||||
|
pleaseInputEmail: 'Please enter email',
|
||||||
|
emailFormatError: 'Email format is incorrect',
|
||||||
|
pleaseInputMobile: 'Please enter mobile number',
|
||||||
|
mobileFormatError: 'Mobile number format is incorrect',
|
||||||
|
pleaseInputUsername: 'Please enter username',
|
||||||
|
usernameLengthError: 'Username length must be 2-20 characters',
|
||||||
|
pleaseSelectVerifyMethod: 'Please select verification method',
|
||||||
|
pleaseInputNewPassword: 'Please enter new password',
|
||||||
|
passwordLengthError: 'Length must be 6-12 characters',
|
||||||
|
passwordComplexityError: 'Must include uppercase, lowercase and special characters',
|
||||||
|
pleaseConfirmNewPassword: 'Please confirm new password',
|
||||||
|
passwordMismatchError: 'Passwords do not match',
|
||||||
|
// Message prompts
|
||||||
|
pleaseBindMobileOrEmail: 'Please bind mobile or email first',
|
||||||
|
pleaseInputVerifyAccount: 'Please bind verification account first',
|
||||||
|
pleaseInputReceiverAccount: 'Please enter the account to receive verification code',
|
||||||
|
getUserInfoFailed: 'Failed to get user information',
|
||||||
|
languageChangeFailed: 'Failed to change language'
|
||||||
|
},
|
||||||
|
// Agent User Detail
|
||||||
|
agentUser: {
|
||||||
|
phone: 'Phone',
|
||||||
|
accountBalance: 'Account Balance',
|
||||||
|
depositAmount: 'Deposit Amount',
|
||||||
|
email: 'Email',
|
||||||
|
registerTime: 'Register Time',
|
||||||
|
historyTradeAmount: 'History Trade Amount',
|
||||||
|
tradeAccount: 'Trade Account',
|
||||||
|
accountName: 'Account Name',
|
||||||
|
balance: 'Balance',
|
||||||
|
tradeLeverage: 'Trade Leverage',
|
||||||
|
historyTradeVolume: 'History Trade Volume',
|
||||||
|
addTime: 'Add Time',
|
||||||
|
loadDetailFailed: 'Failed to load detail',
|
||||||
|
operateAccount: 'Operate Account',
|
||||||
|
role: {
|
||||||
|
1: 'Platform',
|
||||||
|
2: 'Contractor',
|
||||||
|
3: 'Special Market Maker',
|
||||||
|
4: 'Ordinary Market Maker',
|
||||||
|
5: 'Agent',
|
||||||
|
6: 'User'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
// 中文翻译文件
|
||||||
|
export default {
|
||||||
|
// 通用
|
||||||
|
common: {
|
||||||
|
loading: '加载中...',
|
||||||
|
submit: '提交',
|
||||||
|
cancel: '取消',
|
||||||
|
confirm: '确认',
|
||||||
|
edit: '修改',
|
||||||
|
delete: '删除',
|
||||||
|
save: '保存',
|
||||||
|
close: '关闭',
|
||||||
|
search: '搜索',
|
||||||
|
reset: '重置',
|
||||||
|
pleaseInput: '请输入',
|
||||||
|
pleaseSelect: '请选择',
|
||||||
|
operation: '操作',
|
||||||
|
status: '状态',
|
||||||
|
createTime: '创建时间',
|
||||||
|
updateTime: '更新时间',
|
||||||
|
remark: '备注',
|
||||||
|
success: '操作成功',
|
||||||
|
error: '操作失败',
|
||||||
|
warning: '警告',
|
||||||
|
info: '提示'
|
||||||
|
},
|
||||||
|
// 个人中心
|
||||||
|
personalCenter: {
|
||||||
|
pageTitle: '账户信息',
|
||||||
|
username: '用户名',
|
||||||
|
email: '邮箱',
|
||||||
|
mobile: '手机号',
|
||||||
|
password: '密码',
|
||||||
|
language: '语言',
|
||||||
|
languageDesc: '选择您偏好的通讯语言',
|
||||||
|
lastLogin: '最后登录',
|
||||||
|
notSet: '未设置',
|
||||||
|
notBound: '未绑定',
|
||||||
|
bound: '已绑定',
|
||||||
|
goToBind: '去绑定',
|
||||||
|
bind: '绑定',
|
||||||
|
modify: '修改',
|
||||||
|
bindEmail: '绑定邮箱',
|
||||||
|
bindMobile: '绑定手机号',
|
||||||
|
rebindMobile: '重新绑定手机号',
|
||||||
|
modifyUsername: '修改用户名',
|
||||||
|
modifyPassword: '修改密码',
|
||||||
|
verifyMethod: '验证方式',
|
||||||
|
verifyAccount: '验证账号',
|
||||||
|
mobileVerify: '手机验证',
|
||||||
|
emailVerify: '邮箱验证',
|
||||||
|
newPassword: '新密码',
|
||||||
|
confirmPassword: '确认密码',
|
||||||
|
passwordPlaceholder: '6-12位,含大小写+特殊字符',
|
||||||
|
confirmPasswordPlaceholder: '请再次输入新密码',
|
||||||
|
// 验证规则提示
|
||||||
|
pleaseInputEmail: '请输入邮箱',
|
||||||
|
emailFormatError: '邮箱格式不正确',
|
||||||
|
pleaseInputMobile: '请输入手机号',
|
||||||
|
mobileFormatError: '手机号格式不正确',
|
||||||
|
pleaseInputUsername: '请输入用户名',
|
||||||
|
usernameLengthError: '用户名长度 2-20 位',
|
||||||
|
pleaseSelectVerifyMethod: '请选择验证方式',
|
||||||
|
pleaseInputNewPassword: '请输入新密码',
|
||||||
|
passwordLengthError: '长度 6-12 位',
|
||||||
|
passwordComplexityError: '必须包含大小写+特殊字符',
|
||||||
|
pleaseConfirmNewPassword: '请确认新密码',
|
||||||
|
passwordMismatchError: '两次密码不一致',
|
||||||
|
// 消息提示
|
||||||
|
pleaseBindMobileOrEmail: '请先绑定手机或邮箱',
|
||||||
|
pleaseInputVerifyAccount: '请先绑定验证账号',
|
||||||
|
pleaseInputReceiverAccount: '请输入接收验证码的账号',
|
||||||
|
getUserInfoFailed: '获取用户信息失败',
|
||||||
|
languageChangeFailed: '语言切换失败'
|
||||||
|
},
|
||||||
|
// 代理用户详情
|
||||||
|
agentUser: {
|
||||||
|
phone: '手机号',
|
||||||
|
accountBalance: '账户余额',
|
||||||
|
depositAmount: '存款金额',
|
||||||
|
email: '邮箱',
|
||||||
|
registerTime: '注册时间',
|
||||||
|
historyTradeAmount: '历史交易金额',
|
||||||
|
tradeAccount: '交易账号',
|
||||||
|
accountName: '账户名称',
|
||||||
|
balance: '余额',
|
||||||
|
tradeLeverage: '交易杠杆',
|
||||||
|
historyTradeVolume: '历史交易量',
|
||||||
|
addTime: '添加时间',
|
||||||
|
loadDetailFailed: '加载详情失败',
|
||||||
|
operateAccount: '操作账户',
|
||||||
|
role: {
|
||||||
|
1: '平台',
|
||||||
|
2: '承包商',
|
||||||
|
3: '特殊做市商',
|
||||||
|
4: '普通做市商',
|
||||||
|
5: '代理',
|
||||||
|
6: '用户'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,12 +5,14 @@ import 'element-plus/dist/index.css'
|
||||||
//语言配置
|
//语言配置
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
import en from 'element-plus/dist/locale/en.mjs'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import i18n from './i18n'
|
||||||
import {setupAuthDirective} from './directives/auth'
|
import {setupAuthDirective} from './directives/auth'
|
||||||
import { setStorage } from '@/utils/storage'
|
import { setStorage } from '@/utils/storage'
|
||||||
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
|
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
|
||||||
|
|
@ -22,9 +24,13 @@ const app = createApp(App)
|
||||||
setupAuthDirective(app)
|
setupAuthDirective(app)
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
app.use(i18n)
|
||||||
app.use(ContextMenu)
|
app.use(ContextMenu)
|
||||||
app.use(ElementPlus, {
|
app.use(ElementPlus, {
|
||||||
locale: zhCn,
|
locale: zhCn,
|
||||||
})
|
})
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
||||||
|
// 导出 app 实例以便在其他地方使用
|
||||||
|
export default app
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,11 @@ export const useLanguageStore = defineStore('language', () => {
|
||||||
if (!getStorage('lang')) {
|
if (!getStorage('lang')) {
|
||||||
setStorage('lang', initLang)
|
setStorage('lang', initLang)
|
||||||
}
|
}
|
||||||
// const setHtmlLang = (currentLang: LanguageType) => {
|
|
||||||
// document.documentElement.lang = langMap[currentLang]
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setHtmlLang(lang.value)
|
|
||||||
|
|
||||||
const changeLang = (newLang: LanguageType) => {
|
const changeLang = (newLang: LanguageType) => {
|
||||||
lang.value = newLang
|
lang.value = newLang
|
||||||
setStorage('lang', newLang)
|
setStorage('lang', newLang)
|
||||||
// setHtmlLang(newLang)
|
// i18n 的 locale 会通过 watch 自动同步
|
||||||
}
|
}
|
||||||
|
|
||||||
return { lang, changeLang }
|
return { lang, changeLang }
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Phone />
|
<Phone />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>手机号</span>
|
<span>{{ t('agentUser.phone') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-value">{{ userInfo.phone || '-' }}</div>
|
<div class="item-value">{{ userInfo.phone || '-' }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<CreditCard />
|
<CreditCard />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>账户余额</span>
|
<span>{{ t('agentUser.accountBalance') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-value text-highlight">
|
<div class="item-value text-highlight">
|
||||||
<AnimatedNumber :value="Number(userInfo.balance) || 0" />
|
<AnimatedNumber :value="Number(userInfo.balance) || 0" />
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Money />
|
<Money />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>存款金额</span>
|
<span>{{ t('agentUser.depositAmount') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-value text-highlight">
|
<div class="item-value text-highlight">
|
||||||
<AnimatedNumber :value="Number(userInfo.deposit) || 0" />
|
<AnimatedNumber :value="Number(userInfo.deposit) || 0" />
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Message />
|
<Message />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>邮箱</span>
|
<span>{{ t('agentUser.email') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-value">{{ userInfo.email || '-' }}</div>
|
<div class="item-value">{{ userInfo.email || '-' }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Clock />
|
<Clock />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>注册时间</span>
|
<span>{{ t('agentUser.registerTime') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-value">{{ userInfo.registerTime || '-' }}</div>
|
<div class="item-value">{{ userInfo.registerTime || '-' }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<Wallet />
|
<Wallet />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>历史交易金额</span>
|
<span>{{ t('agentUser.historyTradeAmount') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-value">
|
<div class="item-value">
|
||||||
<AnimatedNumber :value="Number(userInfo.hisMeny) || 0" />
|
<AnimatedNumber :value="Number(userInfo.hisMeny) || 0" />
|
||||||
|
|
@ -98,9 +98,11 @@ defineOptions({
|
||||||
})
|
})
|
||||||
import { ref, watch, computed } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
import { ElAvatar, ElIcon, ElDrawer, ElButton, ElMessage } from 'element-plus'
|
import { ElAvatar, ElIcon, ElDrawer, ElButton, ElMessage } from 'element-plus'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { UserFilled, Phone, Message, Money, Wallet, Clock, CreditCard } from '@element-plus/icons-vue'
|
import { UserFilled, Phone, Message, Money, Wallet, Clock, CreditCard } from '@element-plus/icons-vue'
|
||||||
import CustomerListTable from '@/components/common/Table.vue'
|
import CustomerListTable from '@/components/common/Table.vue'
|
||||||
import { getCustomerDetailApi } from '@/api/modules/agent/customerDetail'
|
import { getCustomerDetailApi } from '@/api/modules/agent/customerDetail'
|
||||||
|
import { useLanguageStore } from '@/stores/modules/language'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import AnimatedNumber from './AnimatedNumber.vue'
|
import AnimatedNumber from './AnimatedNumber.vue'
|
||||||
|
|
||||||
|
|
@ -115,14 +117,18 @@ import type {
|
||||||
TableColumn
|
TableColumn
|
||||||
} from '@/api/types/agent'
|
} from '@/api/types/agent'
|
||||||
|
|
||||||
const roleMap: Record<RoleType, string> = {
|
const { t, locale } = useI18n()
|
||||||
1: '平台',
|
const languageStore = useLanguageStore()
|
||||||
2: '承包商',
|
|
||||||
3: '特殊做市商',
|
// 使用 computed 动态生成角色映射,支持语言切换
|
||||||
4: '普通做市商',
|
const roleMap = computed<Record<RoleType, string>>(() => ({
|
||||||
5: '代理',
|
1: t('agentUser.role.1'),
|
||||||
6: '用户',
|
2: t('agentUser.role.2'),
|
||||||
}
|
3: t('agentUser.role.3'),
|
||||||
|
4: t('agentUser.role.4'),
|
||||||
|
5: t('agentUser.role.5'),
|
||||||
|
6: t('agentUser.role.6'),
|
||||||
|
}))
|
||||||
|
|
||||||
// 接收父组件传值
|
// 接收父组件传值
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -165,13 +171,15 @@ const userInfo = ref<UserDisplayInfo>({
|
||||||
|
|
||||||
// 表格
|
// 表格
|
||||||
const tableData = ref<TableDataRow[]>([])
|
const tableData = ref<TableDataRow[]>([])
|
||||||
const tableColumnsOptions = ref<TableColumn[]>([
|
|
||||||
{ label: '交易账号', prop: 'tradeAccount' },
|
// 动态生成表格列配置,支持语言切换
|
||||||
{ label: '账户名称', prop: 'accountName' },
|
const tableColumnsOptions = computed<TableColumn[]>(() => [
|
||||||
{ label: '余额', prop: 'balance', sortable: true },
|
{ label: t('agentUser.tradeAccount'), prop: 'tradeAccount' },
|
||||||
{ label: '交易杠杆', prop: 'leverage' },
|
{ label: t('agentUser.accountName'), prop: 'accountName' },
|
||||||
{ label: '历史交易量', prop: 'tradeVolume' },
|
{ label: t('agentUser.balance'), prop: 'balance', sortable: true },
|
||||||
{ label: '添加时间', prop: 'addTime', width: 180 }
|
{ label: t('agentUser.tradeLeverage'), prop: 'leverage' },
|
||||||
|
{ label: t('agentUser.historyTradeVolume'), prop: 'tradeVolume' },
|
||||||
|
{ label: t('agentUser.addTime'), prop: 'addTime', width: 180 }
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -187,7 +195,7 @@ const loadDetailData = async (id: string | number) => {
|
||||||
userInfo.value = {
|
userInfo.value = {
|
||||||
name: user.email || user.mobile || '-',
|
name: user.email || user.mobile || '-',
|
||||||
id: user.id,
|
id: user.id,
|
||||||
role: roleMap[user.role_id] || '-',
|
role: roleMap.value[user.role_id] || '-',
|
||||||
phone: user.mobile || '-',
|
phone: user.mobile || '-',
|
||||||
email: user.email || '-',
|
email: user.email || '-',
|
||||||
balance: Number(user.capital_amount) || 0,
|
balance: Number(user.capital_amount) || 0,
|
||||||
|
|
@ -207,7 +215,7 @@ const loadDetailData = async (id: string | number) => {
|
||||||
}))
|
}))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载详情失败:', err);
|
console.error('加载详情失败:', err);
|
||||||
ElMessage.error('加载详情失败')
|
ElMessage.error(t('agentUser.loadDetailFailed'))
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
tableLoading.value = false
|
tableLoading.value = false
|
||||||
|
|
@ -224,9 +232,18 @@ watch(
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 监听语言切换,同步更新 i18n 的 locale
|
||||||
|
watch(
|
||||||
|
() => languageStore.lang,
|
||||||
|
(newLang) => {
|
||||||
|
locale.value = newLang
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
// 账户操作
|
// 账户操作
|
||||||
const handleAccount = (row: TableDataRow) => {
|
const handleAccount = (row: TableDataRow) => {
|
||||||
ElMessage.success('操作账户:' + row.tradeAccount)
|
ElMessage.success(t('agentUser.operateAccount') + ':' + row.tradeAccount)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="personal-center">
|
<div class="personal-center">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>账户信息</h2>
|
<h2>{{ t('personalCenter.pageTitle') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
|
|
@ -17,9 +17,9 @@
|
||||||
<div class="info-text">
|
<div class="info-text">
|
||||||
<div class="username-row">
|
<div class="username-row">
|
||||||
<span class="username-text">{{ userInfo.username }}</span>
|
<span class="username-text">{{ userInfo.username }}</span>
|
||||||
<el-button link type="primary" size="small" @click="showEditDialog('username')">修改</el-button>
|
<el-button link type="primary" size="small" @click="showEditDialog('username')">{{ t('personalCenter.modify') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-time">最后登录:{{ userInfo.last_login }}</div>
|
<div class="login-time">{{ t('personalCenter.lastLogin') }}:{{ userInfo.last_login }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
@ -27,35 +27,35 @@
|
||||||
<el-card class="card info-card">
|
<el-card class="card info-card">
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">邮箱</span>
|
<span class="label">{{ t('personalCenter.email') }}</span>
|
||||||
<span class="value">{{ userInfo.email || '未设置' }}</span>
|
<span class="value">{{ userInfo.email || t('personalCenter.notSet') }}</span>
|
||||||
<template v-if="!userInfo.email">
|
<template v-if="!userInfo.email">
|
||||||
<el-button link type="primary" @click="showEditDialog('email')">去绑定</el-button>
|
<el-button link type="primary" @click="showEditDialog('email')">{{ t('personalCenter.goToBind') }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-tag type="success" size="small">已绑定</el-tag>
|
<el-tag type="success" size="small">{{ t('personalCenter.bound') }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">手机号</span>
|
<span class="label">{{ t('personalCenter.mobile') }}</span>
|
||||||
<span class="value">{{ userInfo.mobile || '未绑定' }}</span>
|
<span class="value">{{ userInfo.mobile || t('personalCenter.notBound') }}</span>
|
||||||
<el-button v-if="!userInfo.mobile" link type="primary" @click="showEditDialog('mobile')">
|
<el-button v-if="!userInfo.mobile" link type="primary" @click="showEditDialog('mobile')">
|
||||||
绑定
|
{{ t('personalCenter.bind') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-tag type="success" size="small">已绑定</el-tag>
|
<el-tag type="success" size="small">{{ t('personalCenter.bound') }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">密码</span>
|
<span class="label">{{ t('personalCenter.password') }}</span>
|
||||||
<el-button link type="primary" @click="showPasswordDialog">修改</el-button>
|
<el-button link type="primary" @click="showPasswordDialog">{{ t('personalCenter.modify') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">语言</span>
|
<span class="label">{{ t('personalCenter.language') }}</span>
|
||||||
<span class="value">选择您偏好的通讯语言</span>
|
<span class="value">{{ t('personalCenter.languageDesc') }}</span>
|
||||||
<el-select v-model="userInfo.lang_type" @change="handleLanguageChange" style="width: 130px" size="small">
|
<el-select v-model="userInfo.lang_type" @change="handleLanguageChange" style="width: 130px" size="small">
|
||||||
<el-option label="简体中文" :value="1" />
|
<el-option label="简体中文" :value="1" />
|
||||||
<el-option label="English" :value="2" />
|
<el-option label="English" :value="2" />
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
@cancel="handleEditDialogReset" @close="handleEditDialogReset">
|
@cancel="handleEditDialogReset" @close="handleEditDialogReset">
|
||||||
<el-form ref="editFormRef" :model="editForm" :rules="getEditRules()" label-width="80px" label-position="top">
|
<el-form ref="editFormRef" :model="editForm" :rules="getEditRules()" label-width="80px" label-position="top">
|
||||||
<el-form-item :label="editForm.label" prop="value">
|
<el-form-item :label="editForm.label" prop="value">
|
||||||
<el-input v-if="editField !== 'language'" v-model="editForm.value" :placeholder="`请输入${editForm.label}`"
|
<el-input v-if="editField !== 'language'" v-model="editForm.value" :placeholder="`${t('common.pleaseInput')}${editForm.label}`"
|
||||||
size="large" />
|
size="large" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
|
@ -80,20 +80,20 @@
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- 密码修改弹窗 → 验证码模式 -->
|
<!-- 密码修改弹窗 → 验证码模式 -->
|
||||||
<Dialog :visible="passwordDialogVisible" title="修改密码" width="500px" @confirm="handlePasswordConfirm"
|
<Dialog :visible="passwordDialogVisible" :title="t('personalCenter.modifyPassword')" width="500px" @confirm="handlePasswordConfirm"
|
||||||
@cancel="handlePasswordDialogReset" @close="handlePasswordDialogReset">
|
@cancel="handlePasswordDialogReset" @close="handlePasswordDialogReset">
|
||||||
<el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordFormRules" label-width="100px"
|
<el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordFormRules" label-width="100px"
|
||||||
label-position="top">
|
label-position="top">
|
||||||
<!-- 选择验证方式 -->
|
<!-- 选择验证方式 -->
|
||||||
<el-form-item label="验证方式" prop="verifyType">
|
<el-form-item :label="t('personalCenter.verifyMethod')" prop="verifyType">
|
||||||
<el-radio-group v-model="passwordForm.verifyType" @change="onVerifyTypeChange">
|
<el-radio-group v-model="passwordForm.verifyType" @change="onVerifyTypeChange">
|
||||||
<el-radio label="mobile" :disabled="!userInfo.mobile">手机验证</el-radio>
|
<el-radio label="mobile" :disabled="!userInfo.mobile">{{ t('personalCenter.mobileVerify') }}</el-radio>
|
||||||
<el-radio label="email" :disabled="!userInfo.email">邮箱验证</el-radio>
|
<el-radio label="email" :disabled="!userInfo.email">{{ t('personalCenter.emailVerify') }}</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 验证账号 -->
|
<!-- 验证账号 -->
|
||||||
<el-form-item label="验证账号">
|
<el-form-item :label="t('personalCenter.verifyAccount')">
|
||||||
<span style="font-weight:500;color:#333">
|
<span style="font-weight:500;color:#333">
|
||||||
{{ passwordForm.verifyType === 'mobile' ? userInfo.mobile : userInfo.email }}
|
{{ passwordForm.verifyType === 'mobile' ? userInfo.mobile : userInfo.email }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -104,14 +104,14 @@
|
||||||
:show-send-button="showPwdSendButton" :send-state="pwdSendState" />
|
:show-send-button="showPwdSendButton" :send-state="pwdSendState" />
|
||||||
|
|
||||||
<!-- 新密码1 -->
|
<!-- 新密码1 -->
|
||||||
<el-form-item label="新密码" prop="login_password1">
|
<el-form-item :label="t('personalCenter.newPassword')" prop="login_password1">
|
||||||
<el-input v-model="passwordForm.login_password1" type="password" show-password placeholder="6-12位,含大小写+特殊字符"
|
<el-input v-model="passwordForm.login_password1" type="password" show-password :placeholder="t('personalCenter.passwordPlaceholder')"
|
||||||
size="large" />
|
size="large" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 新密码2 -->
|
<!-- 新密码2 -->
|
||||||
<el-form-item label="确认密码" prop="login_password2">
|
<el-form-item :label="t('personalCenter.confirmPassword')" prop="login_password2">
|
||||||
<el-input v-model="passwordForm.login_password2" type="password" show-password placeholder="请再次输入新密码"
|
<el-input v-model="passwordForm.login_password2" type="password" show-password :placeholder="t('personalCenter.confirmPasswordPlaceholder')"
|
||||||
size="large" />
|
size="large" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
@ -124,9 +124,10 @@
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'PersonalCenter'
|
name: 'PersonalCenter'
|
||||||
})
|
})
|
||||||
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
|
import { ref, reactive, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
||||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||||
import { UserFilled } from '@element-plus/icons-vue';
|
import { UserFilled } from '@element-plus/icons-vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import Dialog from '@/components/common/Dialog.vue';
|
import Dialog from '@/components/common/Dialog.vue';
|
||||||
import UserCaptcha from '@/views/user/components/Captcha.vue';
|
import UserCaptcha from '@/views/user/components/Captcha.vue';
|
||||||
import { useUserStore } from '@/stores/modules/user';
|
import { useUserStore } from '@/stores/modules/user';
|
||||||
|
|
@ -134,6 +135,7 @@ import { useLanguageStore } from '@/stores/modules/language';
|
||||||
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha';
|
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha';
|
||||||
import { codeRules } from '@/views/user/config/rules';
|
import { codeRules } from '@/views/user/config/rules';
|
||||||
|
|
||||||
|
const { t, locale } = useI18n()
|
||||||
// 类型定义
|
// 类型定义
|
||||||
import type {
|
import type {
|
||||||
UserInfo,
|
UserInfo,
|
||||||
|
|
@ -206,20 +208,20 @@ const getEditRules = (): FormRules => {
|
||||||
switch (editField.value) {
|
switch (editField.value) {
|
||||||
case 'email':
|
case 'email':
|
||||||
rules.value = [
|
rules.value = [
|
||||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
{ required: true, message: t('personalCenter.pleaseInputEmail'), trigger: 'blur' },
|
||||||
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
|
{ type: 'email', message: t('personalCenter.emailFormatError'), trigger: 'blur' }
|
||||||
];
|
];
|
||||||
return rules;
|
return rules;
|
||||||
case 'mobile':
|
case 'mobile':
|
||||||
rules.value = [
|
rules.value = [
|
||||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
{ required: true, message: t('personalCenter.pleaseInputMobile'), trigger: 'blur' },
|
||||||
{ pattern: /^\+?\d{5,20}$/, message: '手机号格式不正确', trigger: 'blur' }
|
{ pattern: /^\+?\d{5,20}$/, message: t('personalCenter.mobileFormatError'), trigger: 'blur' }
|
||||||
];
|
];
|
||||||
return rules;
|
return rules;
|
||||||
case 'username':
|
case 'username':
|
||||||
rules.value = [
|
rules.value = [
|
||||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
{ required: true, message: t('personalCenter.pleaseInputUsername'), trigger: 'blur' },
|
||||||
{ min: 2, max: 20, message: '用户名长度 2-20 位', trigger: 'blur' }
|
{ min: 2, max: 20, message: t('personalCenter.usernameLengthError'), trigger: 'blur' }
|
||||||
];
|
];
|
||||||
return rules;
|
return rules;
|
||||||
default:
|
default:
|
||||||
|
|
@ -229,22 +231,22 @@ const getEditRules = (): FormRules => {
|
||||||
|
|
||||||
// 密码强度校验
|
// 密码强度校验
|
||||||
const passwordFormRules = ref<FormRules>({
|
const passwordFormRules = ref<FormRules>({
|
||||||
verifyType: [{ required: true, message: '请选择验证方式', trigger: 'change' }],
|
verifyType: [{ required: true, message: t('personalCenter.pleaseSelectVerifyMethod'), trigger: 'change' }],
|
||||||
code: codeRules().code,
|
code: codeRules().code,
|
||||||
login_password1: [
|
login_password1: [
|
||||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
{ required: true, message: t('personalCenter.pleaseInputNewPassword'), trigger: 'blur' },
|
||||||
{ min: 6, max: 12, message: '长度 6-12 位', trigger: 'blur' },
|
{ min: 6, max: 12, message: t('personalCenter.passwordLengthError'), trigger: 'blur' },
|
||||||
{
|
{
|
||||||
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{6,12}$/,
|
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{6,12}$/,
|
||||||
message: '必须包含大小写+特殊字符', trigger: 'blur'
|
message: t('personalCenter.passwordComplexityError'), trigger: 'blur'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
login_password2: [
|
login_password2: [
|
||||||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
{ required: true, message: t('personalCenter.pleaseConfirmNewPassword'), trigger: 'blur' },
|
||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value !== passwordForm.login_password1) {
|
if (value !== passwordForm.login_password1) {
|
||||||
callback(new Error('两次密码不一致'));
|
callback(new Error(t('personalCenter.passwordMismatchError')));
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
@ -261,7 +263,7 @@ const fetchUserProfile = async () => {
|
||||||
Object.assign(userInfo, res);
|
Object.assign(userInfo, res);
|
||||||
languageStore.changeLang(res.lang_type === 1 ? 'zh-CN' : 'en-US');
|
languageStore.changeLang(res.lang_type === 1 ? 'zh-CN' : 'en-US');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取用户信息失败');
|
ElMessage.error(t('personalCenter.getUserInfoFailed'));
|
||||||
console.error('获取用户信息失败:', error);
|
console.error('获取用户信息失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -272,19 +274,19 @@ const showEditDialog = (field: 'email' | 'mobile' | 'username') => {
|
||||||
editForm.code = '';
|
editForm.code = '';
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'email':
|
case 'email':
|
||||||
editForm.label = '邮箱';
|
editForm.label = t('personalCenter.email');
|
||||||
editForm.value = userInfo.email;
|
editForm.value = userInfo.email;
|
||||||
dialogTitle.value = '绑定邮箱';
|
dialogTitle.value = t('personalCenter.bindEmail');
|
||||||
break;
|
break;
|
||||||
case 'mobile':
|
case 'mobile':
|
||||||
editForm.label = '手机号';
|
editForm.label = t('personalCenter.mobile');
|
||||||
editForm.value = userInfo.mobile;
|
editForm.value = userInfo.mobile;
|
||||||
dialogTitle.value = userInfo.mobile ? '重新绑定手机号' : '绑定手机号';
|
dialogTitle.value = userInfo.mobile ? t('personalCenter.rebindMobile') : t('personalCenter.bindMobile');
|
||||||
break;
|
break;
|
||||||
case 'username':
|
case 'username':
|
||||||
editForm.label = '用户名';
|
editForm.label = t('personalCenter.username');
|
||||||
editForm.value = userInfo.username;
|
editForm.value = userInfo.username;
|
||||||
dialogTitle.value = '修改用户名';
|
dialogTitle.value = t('personalCenter.modifyUsername');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
|
|
@ -296,13 +298,13 @@ const handleSendCaptcha = async () => {
|
||||||
const type = editField.value === 'email' ? 'email' : 'phone';
|
const type = editField.value === 'email' ? 'email' : 'phone';
|
||||||
const receiver = editForm.value?.trim();
|
const receiver = editForm.value?.trim();
|
||||||
if (!receiver) {
|
if (!receiver) {
|
||||||
ElMessage.warning('请输入接收验证码的账号');
|
ElMessage.warning(t('personalCenter.pleaseInputReceiverAccount'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentState = type === 'email' ? emailCaptchaState : phoneCaptchaState;
|
const currentState = type === 'email' ? emailCaptchaState : phoneCaptchaState;
|
||||||
await sendCaptcha(type, receiver, currentState, () => {
|
await sendCaptcha(type, receiver, currentState, () => {
|
||||||
return userStore.sendCode({
|
return userStore.sendCode({
|
||||||
reg_type: type === 'email' ? 1 : 2,
|
reg_type: userStore.userInfo!.reg_type,
|
||||||
[type === 'email' ? 'email' : 'mobile']: receiver,
|
[type === 'email' ? 'email' : 'mobile']: receiver,
|
||||||
type: type === 'email' ? 'transfer_code' : 'bindMobile_code'
|
type: type === 'email' ? 'transfer_code' : 'bindMobile_code'
|
||||||
});
|
});
|
||||||
|
|
@ -358,7 +360,7 @@ const handleEditConfirm = async () => {
|
||||||
// ==================== 密码修改逻辑 ====================
|
// ==================== 密码修改逻辑 ====================
|
||||||
const showPasswordDialog = () => {
|
const showPasswordDialog = () => {
|
||||||
if (!userInfo.mobile && !userInfo.email) {
|
if (!userInfo.mobile && !userInfo.email) {
|
||||||
ElMessage.warning('请先绑定手机或邮箱');
|
ElMessage.warning(t('personalCenter.pleaseBindMobileOrEmail'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
passwordForm.verifyType = userInfo.mobile ? 'mobile' : 'email';
|
passwordForm.verifyType = userInfo.mobile ? 'mobile' : 'email';
|
||||||
|
|
@ -389,14 +391,14 @@ const handleSendPwdCaptcha = async () => {
|
||||||
const type = passwordForm.verifyType === 'email' ? 'email' : 'phone';
|
const type = passwordForm.verifyType === 'email' ? 'email' : 'phone';
|
||||||
const receiver = passwordForm.verifyType === 'email' ? userInfo.email : userInfo.mobile;
|
const receiver = passwordForm.verifyType === 'email' ? userInfo.email : userInfo.mobile;
|
||||||
if (!receiver) {
|
if (!receiver) {
|
||||||
ElMessage.warning('请先绑定验证账号');
|
ElMessage.warning(t('personalCenter.pleaseInputVerifyAccount'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentState = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
|
const currentState = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
|
||||||
|
|
||||||
await sendCaptcha(type, receiver, currentState, () => {
|
await sendCaptcha(type, receiver, currentState, () => {
|
||||||
return userStore.sendCode({
|
return userStore.sendCode({
|
||||||
reg_type: passwordForm.verifyType === 'email' ? 1 : 2,
|
reg_type: userStore.userInfo!.reg_type,
|
||||||
[passwordForm.verifyType]: receiver,
|
[passwordForm.verifyType]: receiver,
|
||||||
type: 'resetPass_code'
|
type: 'resetPass_code'
|
||||||
});
|
});
|
||||||
|
|
@ -416,7 +418,7 @@ const handlePasswordConfirm = async () => {
|
||||||
await passwordFormRef.value.validate();
|
await passwordFormRef.value.validate();
|
||||||
|
|
||||||
const params: UpdatePwdByCaptchaParams = {
|
const params: UpdatePwdByCaptchaParams = {
|
||||||
reg_type: passwordForm.verifyType === 'email' ? 1 : 2,
|
reg_type: userStore.userInfo!.reg_type,
|
||||||
email: passwordForm.verifyType === 'email' ? userInfo.email : undefined,
|
email: passwordForm.verifyType === 'email' ? userInfo.email : undefined,
|
||||||
mobile: passwordForm.verifyType === 'mobile' ? userInfo.mobile : undefined,
|
mobile: passwordForm.verifyType === 'mobile' ? userInfo.mobile : undefined,
|
||||||
code: passwordForm.code,
|
code: passwordForm.code,
|
||||||
|
|
@ -443,6 +445,15 @@ const handlePasswordDialogReset = () => {
|
||||||
passwordFormRef.value?.resetFields();
|
passwordFormRef.value?.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 监听语言切换,同步更新 i18n 的 locale
|
||||||
|
watch(
|
||||||
|
() => languageStore.lang,
|
||||||
|
(newLang) => {
|
||||||
|
locale.value = newLang
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchUserProfile();
|
fetchUserProfile();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue