feat: improve authentication and user management
- Fix token missing issue after email code registration/login - Add last login time update for code-based login - Support email login in addition to username login - Fix password login for code-registered users (allow setting password) - Fix referral code parameter passing from URL hash - Add email column to user management table - Improve profile page layout (align card heights, reorganize layout) - Add i18n support for Register Bonus, Referral Bonus, Code Lock Minutes, Code Max Attempts - Fix billing service add_credits to support reference_id parameter - Update password handling documentation
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* Get security configuration (Turnstile, OAuth settings)
|
||||
*/
|
||||
export function getSecurityConfig () {
|
||||
return request({
|
||||
url: '/api/auth/security-config',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* User login
|
||||
* @param {Object} data - { username, password, turnstile_token }
|
||||
*/
|
||||
export function login (data) {
|
||||
return request({
|
||||
url: '/api/auth/login',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* User logout
|
||||
*/
|
||||
export function logout () {
|
||||
return request({
|
||||
url: '/api/auth/logout',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user info
|
||||
*/
|
||||
export function getUserInfo () {
|
||||
return request({
|
||||
url: '/api/auth/info',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send verification code
|
||||
* @param {Object} data - { email, type, turnstile_token }
|
||||
* type: 'register' | 'login' | 'reset_password' | 'change_password' | 'change_email'
|
||||
*/
|
||||
export function sendVerificationCode (data) {
|
||||
return request({
|
||||
url: '/api/auth/send-code',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with email verification code (quick login)
|
||||
* @param {Object} data - { email, code, turnstile_token }
|
||||
*/
|
||||
export function loginWithCode (data) {
|
||||
return request({
|
||||
url: '/api/auth/login-code',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* User registration
|
||||
* @param {Object} data - { email, code, username, password, turnstile_token }
|
||||
*/
|
||||
export function register (data) {
|
||||
return request({
|
||||
url: '/api/auth/register',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password
|
||||
* @param {Object} data - { email, code, new_password, turnstile_token }
|
||||
*/
|
||||
export function resetPassword (data) {
|
||||
return request({
|
||||
url: '/api/auth/reset-password',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Change password (for logged-in users)
|
||||
* @param {Object} data - { code, new_password }
|
||||
*/
|
||||
export function changePassword (data) {
|
||||
return request({
|
||||
url: '/api/auth/change-password',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Google OAuth URL
|
||||
*/
|
||||
export function getGoogleOAuthUrl () {
|
||||
return `${process.env.VUE_APP_API_BASE_URL || ''}/api/auth/oauth/google`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GitHub OAuth URL
|
||||
*/
|
||||
export function getGitHubOAuthUrl () {
|
||||
return `${process.env.VUE_APP_API_BASE_URL || ''}/api/auth/oauth/github`
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
const userApi = {
|
||||
Login: '/api/user/login',
|
||||
Logout: '/api/user/logout',
|
||||
UserInfo: '/api/user/info',
|
||||
Login: '/api/auth/login',
|
||||
Logout: '/api/auth/logout',
|
||||
UserInfo: '/api/auth/info',
|
||||
UserMenu: '/user/nav'
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,8 @@ export function runMonitor (id, params = {}) {
|
||||
return request({
|
||||
url: `/api/portfolio/monitors/${id}/run`,
|
||||
method: 'post',
|
||||
data: params
|
||||
data: params,
|
||||
timeout: 60000 // 60s timeout for monitor API (async mode should return immediately)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* Get user list (admin only)
|
||||
* @param {Object} params - { page, page_size }
|
||||
* @param {Object} params - { page, page_size, search }
|
||||
*/
|
||||
export function getUserList (params) {
|
||||
return request({
|
||||
@@ -124,3 +124,65 @@ export function changePassword (data) {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user's credits log
|
||||
* @param {Object} params - { page, page_size }
|
||||
*/
|
||||
export function getMyCreditsLog (params) {
|
||||
return request({
|
||||
url: '/api/users/my-credits-log',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user's referral list
|
||||
* @param {Object} params - { page, page_size }
|
||||
*/
|
||||
export function getMyReferrals (params) {
|
||||
return request({
|
||||
url: '/api/users/my-referrals',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== Billing Management (Admin) ====================
|
||||
|
||||
/**
|
||||
* Set user credits (admin only)
|
||||
* @param {Object} data - { user_id, credits, remark }
|
||||
*/
|
||||
export function setUserCredits (data) {
|
||||
return request({
|
||||
url: '/api/users/set-credits',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user VIP status (admin only)
|
||||
* @param {Object} data - { user_id, vip_days, vip_expires_at, remark }
|
||||
*/
|
||||
export function setUserVip (data) {
|
||||
return request({
|
||||
url: '/api/users/set-vip',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user credits log (admin only)
|
||||
* @param {Object} params - { user_id, page, page_size }
|
||||
*/
|
||||
export function getUserCreditsLog (params) {
|
||||
return request({
|
||||
url: '/api/users/credits-log',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="turnstile-container" v-if="enabled">
|
||||
<div ref="turnstileRef" :id="containerId"></div>
|
||||
<div v-if="error" class="turnstile-error">
|
||||
{{ error }}
|
||||
<a @click="reset">{{ $t('user.security.retry') || 'Retry' }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
let turnstileScriptLoaded = false
|
||||
let turnstileScriptLoading = false
|
||||
const turnstileCallbacks = []
|
||||
|
||||
function loadTurnstileScript () {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (turnstileScriptLoaded) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
turnstileCallbacks.push({ resolve, reject })
|
||||
|
||||
if (turnstileScriptLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
turnstileScriptLoading = true
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'
|
||||
script.async = true
|
||||
script.defer = true
|
||||
|
||||
script.onload = () => {
|
||||
turnstileScriptLoaded = true
|
||||
turnstileCallbacks.forEach(cb => cb.resolve())
|
||||
turnstileCallbacks.length = 0
|
||||
}
|
||||
|
||||
script.onerror = () => {
|
||||
turnstileScriptLoading = false
|
||||
turnstileCallbacks.forEach(cb => cb.reject(new Error('Failed to load Turnstile script')))
|
||||
turnstileCallbacks.length = 0
|
||||
}
|
||||
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Turnstile',
|
||||
|
||||
props: {
|
||||
siteKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'auto' // 'light', 'dark', 'auto'
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'normal' // 'normal', 'compact'
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
widgetId: null,
|
||||
token: null,
|
||||
error: null,
|
||||
containerId: `turnstile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.enabled && this.siteKey) {
|
||||
this.initTurnstile()
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
this.cleanup()
|
||||
},
|
||||
|
||||
watch: {
|
||||
siteKey (newVal) {
|
||||
if (newVal && this.enabled) {
|
||||
this.initTurnstile()
|
||||
}
|
||||
},
|
||||
enabled (newVal) {
|
||||
if (newVal && this.siteKey) {
|
||||
this.initTurnstile()
|
||||
} else {
|
||||
this.cleanup()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initTurnstile () {
|
||||
try {
|
||||
await loadTurnstileScript()
|
||||
this.renderWidget()
|
||||
} catch (e) {
|
||||
this.error = 'Failed to load verification'
|
||||
console.error('Turnstile init error:', e)
|
||||
}
|
||||
},
|
||||
|
||||
renderWidget () {
|
||||
if (!window.turnstile || !this.$refs.turnstileRef) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up existing widget
|
||||
this.cleanup()
|
||||
|
||||
this.widgetId = window.turnstile.render(this.$refs.turnstileRef, {
|
||||
sitekey: this.siteKey,
|
||||
theme: this.theme,
|
||||
size: this.size,
|
||||
callback: (token) => {
|
||||
this.token = token
|
||||
this.error = null
|
||||
this.$emit('success', token)
|
||||
},
|
||||
'error-callback': () => {
|
||||
this.token = null
|
||||
this.error = 'Verification failed'
|
||||
this.$emit('error')
|
||||
},
|
||||
'expired-callback': () => {
|
||||
this.token = null
|
||||
this.$emit('expired')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.token = null
|
||||
this.error = null
|
||||
if (window.turnstile && this.widgetId !== null) {
|
||||
window.turnstile.reset(this.widgetId)
|
||||
} else {
|
||||
this.renderWidget()
|
||||
}
|
||||
},
|
||||
|
||||
getToken () {
|
||||
return this.token
|
||||
},
|
||||
|
||||
cleanup () {
|
||||
if (window.turnstile && this.widgetId !== null) {
|
||||
try {
|
||||
window.turnstile.remove(this.widgetId)
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
this.widgetId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.turnstile-container {
|
||||
margin: 16px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.turnstile-error {
|
||||
margin-top: 8px;
|
||||
color: #ff4d4f;
|
||||
font-size: 13px;
|
||||
|
||||
a {
|
||||
margin-left: 8px;
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -159,11 +159,6 @@ export const constantRouterMap = [
|
||||
path: 'login',
|
||||
name: 'login',
|
||||
component: () => import(/* webpackChunkName: "user" */ '@/views/user/Login')
|
||||
},
|
||||
{
|
||||
path: 'recover',
|
||||
name: 'recover',
|
||||
component: undefined
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+7
-1
@@ -24,7 +24,13 @@ export default function Initializer () {
|
||||
store.commit(TOGGLE_WEAK, storage.get(TOGGLE_WEAK, defaultSettings.colorWeak))
|
||||
store.commit(TOGGLE_COLOR, storage.get(TOGGLE_COLOR, defaultSettings.primaryColor))
|
||||
store.commit(TOGGLE_MULTI_TAB, storage.get(TOGGLE_MULTI_TAB, defaultSettings.multiTab))
|
||||
store.commit('SET_TOKEN', storage.get(ACCESS_TOKEN))
|
||||
// 处理 token 可能是字符串或对象的情况
|
||||
let token = storage.get(ACCESS_TOKEN)
|
||||
if (token && typeof token !== 'string') {
|
||||
token = token.token || token.value || (typeof token === 'object' ? null : token)
|
||||
}
|
||||
token = typeof token === 'string' ? token : null
|
||||
store.commit('SET_TOKEN', token)
|
||||
|
||||
store.dispatch('setLang', storage.get(APP_LANGUAGE, 'en-US'))
|
||||
// last step
|
||||
|
||||
@@ -215,6 +215,100 @@ const locale = {
|
||||
'user.login.privacy.view': 'View Privacy Policy',
|
||||
'user.login.privacy.collapse': 'Hide Privacy Policy',
|
||||
'user.login.privacy.content': 'We value your privacy and data protection. (1) Scope of collection: We only collect information necessary to provide the service (e.g., email, mobile number, country code, Web3 wallet address) and limited logs/device data. (2) Purpose of use: Account login and security verification, feature provisioning, troubleshooting, and compliance requirements. (3) Storage & security: Data is encrypted and access-controlled to prevent unauthorized access, disclosure, or loss. (4) Sharing & third parties: We do not share personal data with third parties except as required by law or to deliver the service; where third-party services are involved (e.g., wallets, SMS providers), processing is limited to the minimum scope required. (5) Cookies/local storage: Used for session and login state (e.g., tokens, PHPSESSID). You may clear or restrict them in your browser. (6) Your rights: You may exercise rights of access, correction, deletion, and consent withdrawal as permitted by law. (7) Changes & notices: We will provide prominent notice for updates. Continued use indicates that you have read and agreed to the updated terms. If you do not agree, please stop using the service and contact us.',
|
||||
|
||||
// Login page additions
|
||||
'user.login.username': 'Username',
|
||||
'user.login.usernameRequired': 'Please enter username',
|
||||
'user.login.passwordRequired': 'Please enter password',
|
||||
'user.login.tab': 'Login',
|
||||
'user.login.submit': 'Login',
|
||||
'user.login.register': 'Create Account',
|
||||
'user.login.forgotPassword': 'Forgot Password?',
|
||||
'user.login.orLoginWith': 'Or login with',
|
||||
'user.login.methodPassword': 'Password',
|
||||
'user.login.methodCode': 'Email Code',
|
||||
'user.login.email': 'Email',
|
||||
'user.login.emailRequired': 'Please enter email',
|
||||
'user.login.emailInvalid': 'Invalid email format',
|
||||
'user.login.verificationCode': 'Verification Code',
|
||||
'user.login.codeRequired': 'Please enter verification code',
|
||||
'user.login.sendCode': 'Send',
|
||||
'user.login.codeSent': 'Verification code sent',
|
||||
'user.login.codeLoginHint': 'New users will be auto-registered',
|
||||
'user.login.welcomeNew': 'Welcome!',
|
||||
'user.login.accountCreated': 'Your account has been created',
|
||||
|
||||
// OAuth
|
||||
'user.oauth.processing': 'Processing login...',
|
||||
'user.oauth.error.missing_params': 'Missing required parameters',
|
||||
'user.oauth.error.invalid_state': 'Invalid state parameter',
|
||||
'user.oauth.error.user_creation_failed': 'Failed to create user',
|
||||
'user.oauth.error.server_error': 'Server error',
|
||||
|
||||
// Register page
|
||||
'user.register.tab': 'Register',
|
||||
'user.register.title': 'Create Account',
|
||||
'user.register.email': 'Email',
|
||||
'user.register.emailRequired': 'Please enter email',
|
||||
'user.register.emailInvalid': 'Invalid email format',
|
||||
'user.register.verificationCode': 'Verification Code',
|
||||
'user.register.codeRequired': 'Please enter verification code',
|
||||
'user.register.sendCode': 'Send Code',
|
||||
'user.register.codeSent': 'Verification code sent',
|
||||
'user.register.username': 'Username',
|
||||
'user.register.usernameRequired': 'Please enter username',
|
||||
'user.register.usernameLength': 'Username must be 3-30 characters',
|
||||
'user.register.usernamePattern': 'Start with letter, letters/numbers/underscore only',
|
||||
'user.register.password': 'Password',
|
||||
'user.register.passwordRequired': 'Please enter password',
|
||||
'user.register.confirmPassword': 'Confirm Password',
|
||||
'user.register.confirmPasswordRequired': 'Please confirm password',
|
||||
'user.register.passwordMismatch': 'Passwords do not match',
|
||||
'user.register.submit': 'Create Account',
|
||||
'user.register.haveAccount': 'Already have an account?',
|
||||
'user.register.login': 'Login',
|
||||
'user.register.success': 'Registration successful',
|
||||
'user.register.pleaseLogin': 'Please login with your new account',
|
||||
'user.register.pwdMinLength': 'At least 8 characters',
|
||||
'user.register.pwdUppercase': 'At least one uppercase letter',
|
||||
'user.register.pwdLowercase': 'At least one lowercase letter',
|
||||
'user.register.pwdNumber': 'At least one number',
|
||||
|
||||
// Reset password page
|
||||
'user.resetPassword.title': 'Reset Password',
|
||||
'user.resetPassword.email': 'Email',
|
||||
'user.resetPassword.emailRequired': 'Please enter email',
|
||||
'user.resetPassword.emailInvalid': 'Invalid email format',
|
||||
'user.resetPassword.verificationCode': 'Verification Code',
|
||||
'user.resetPassword.codeRequired': 'Please enter verification code',
|
||||
'user.resetPassword.sendCode': 'Send Code',
|
||||
'user.resetPassword.codeSent': 'Verification code sent',
|
||||
'user.resetPassword.next': 'Next',
|
||||
'user.resetPassword.backToLogin': 'Back to Login',
|
||||
'user.resetPassword.resettingFor': 'Resetting password for',
|
||||
'user.resetPassword.newPassword': 'New Password',
|
||||
'user.resetPassword.passwordRequired': 'Please enter new password',
|
||||
'user.resetPassword.confirmPassword': 'Confirm New Password',
|
||||
'user.resetPassword.confirmPasswordRequired': 'Please confirm password',
|
||||
'user.resetPassword.submit': 'Reset Password',
|
||||
'user.resetPassword.back': 'Back',
|
||||
'user.resetPassword.successTitle': 'Password Reset Successful',
|
||||
'user.resetPassword.successSubtitle': 'You can now login with your new password',
|
||||
'user.resetPassword.goToLogin': 'Go to Login',
|
||||
|
||||
// Security
|
||||
'user.security.retry': 'Retry',
|
||||
|
||||
// Profile - change password
|
||||
'profile.passwordHintNew': 'For security, email verification is required to change password. Password must be at least 8 characters with uppercase, lowercase, and number.',
|
||||
'profile.verificationCode': 'Verification Code',
|
||||
'profile.codeRequired': 'Please enter verification code',
|
||||
'profile.codePlaceholder': 'Enter verification code',
|
||||
'profile.sendCode': 'Send Code',
|
||||
'profile.codeSent': 'Verification code sent',
|
||||
'profile.codeWillSendTo': 'Code will be sent to',
|
||||
'profile.noEmailWarning': 'Please set your email first in Basic Info tab',
|
||||
|
||||
'account.basicInfo': 'Basic Information',
|
||||
'account.id': 'User ID',
|
||||
'account.username': 'Username',
|
||||
@@ -2019,6 +2113,9 @@ const locale = {
|
||||
'settings.link.supportedExchanges': 'Supported Exchanges',
|
||||
'settings.link.applyApi': 'Apply API',
|
||||
'settings.link.createSearchEngine': 'Create Search Engine',
|
||||
'settings.link.getTurnstileKey': 'Get Turnstile Key',
|
||||
'settings.link.getGoogleCredentials': 'Get Google Credentials',
|
||||
'settings.link.getGithubCredentials': 'Get GitHub Credentials',
|
||||
'settings.restartRequired': 'Settings saved. Some changes require Python service restart to take effect.',
|
||||
'settings.copyRestartCmd': 'Copy restart command',
|
||||
'settings.copySuccess': 'Copied',
|
||||
@@ -2037,11 +2134,35 @@ const locale = {
|
||||
'settings.group.agent': 'AI Agent',
|
||||
'settings.group.network': 'Network & Proxy',
|
||||
'settings.group.search': 'Web Search',
|
||||
'settings.group.security': 'Registration & Security',
|
||||
'settings.group.app': 'Application',
|
||||
// Settings fields - Auth
|
||||
'settings.field.SECRET_KEY': 'Secret Key',
|
||||
'settings.field.ADMIN_USER': 'Admin Username',
|
||||
'settings.field.ADMIN_PASSWORD': 'Admin Password',
|
||||
'settings.field.ADMIN_EMAIL': 'Admin Email',
|
||||
// Settings fields - Security
|
||||
'settings.field.ENABLE_REGISTRATION': 'Enable Registration',
|
||||
'settings.field.TURNSTILE_SITE_KEY': 'Turnstile Site Key',
|
||||
'settings.field.TURNSTILE_SECRET_KEY': 'Turnstile Secret Key',
|
||||
'settings.field.FRONTEND_URL': 'Frontend URL',
|
||||
'settings.field.GOOGLE_CLIENT_ID': 'Google Client ID',
|
||||
'settings.field.GOOGLE_CLIENT_SECRET': 'Google Client Secret',
|
||||
'settings.field.GOOGLE_REDIRECT_URI': 'Google Redirect URI',
|
||||
'settings.field.GITHUB_CLIENT_ID': 'GitHub Client ID',
|
||||
'settings.field.GITHUB_CLIENT_SECRET': 'GitHub Client Secret',
|
||||
'settings.field.GITHUB_REDIRECT_URI': 'GitHub Redirect URI',
|
||||
'settings.field.SECURITY_IP_MAX_ATTEMPTS': 'IP Max Failed Attempts',
|
||||
'settings.field.SECURITY_IP_WINDOW_MINUTES': 'IP Window (minutes)',
|
||||
'settings.field.SECURITY_IP_BLOCK_MINUTES': 'IP Block Duration (minutes)',
|
||||
'settings.field.SECURITY_ACCOUNT_MAX_ATTEMPTS': 'Account Max Failed Attempts',
|
||||
'settings.field.SECURITY_ACCOUNT_WINDOW_MINUTES': 'Account Window (minutes)',
|
||||
'settings.field.SECURITY_ACCOUNT_BLOCK_MINUTES': 'Account Block Duration (minutes)',
|
||||
'settings.field.VERIFICATION_CODE_EXPIRE_MINUTES': 'Verification Code Expiry (minutes)',
|
||||
'settings.field.VERIFICATION_CODE_RATE_LIMIT': 'Code Rate Limit (seconds)',
|
||||
'settings.field.VERIFICATION_CODE_IP_HOURLY_LIMIT': 'Code Hourly Limit per IP',
|
||||
'settings.field.VERIFICATION_CODE_MAX_ATTEMPTS': 'Code Max Attempts',
|
||||
'settings.field.VERIFICATION_CODE_LOCK_MINUTES': 'Code Lock Minutes',
|
||||
// Settings fields - Server
|
||||
'settings.field.PYTHON_API_HOST': 'Listen Address',
|
||||
'settings.field.PYTHON_API_PORT': 'Port',
|
||||
@@ -2179,6 +2300,10 @@ const locale = {
|
||||
'portfolio.monitors.channels': 'Channels',
|
||||
'portfolio.monitors.runNow': 'Run Now',
|
||||
'portfolio.monitors.analysisResult': 'AI Analysis Result',
|
||||
'portfolio.monitors.runningTitle': 'AI Analysis Started',
|
||||
'portfolio.monitors.runningDesc': 'Analysis is running in background. Results will be pushed via notification. Analyzing multiple positions may take a few minutes.',
|
||||
'portfolio.monitors.timeoutTitle': 'Request Timeout',
|
||||
'portfolio.monitors.timeoutDesc': 'Analysis may still be running in background. Please check notifications for results later. If no notification is received, please try again.',
|
||||
'portfolio.modal.addPosition': 'Add Position',
|
||||
'portfolio.modal.editPosition': 'Edit Position',
|
||||
'portfolio.modal.addMonitor': 'Add Monitor',
|
||||
@@ -2238,6 +2363,7 @@ const locale = {
|
||||
'portfolio.message.monitorDisabled': 'Monitor paused',
|
||||
'portfolio.message.monitorRunSuccess': 'Analysis completed',
|
||||
'portfolio.message.monitorRunFailed': 'Analysis failed',
|
||||
'portfolio.message.monitorRunning': 'AI analysis started, please wait for notification',
|
||||
// Portfolio - Groups
|
||||
'portfolio.groups.all': 'All Positions',
|
||||
'portfolio.groups.ungrouped': 'Ungrouped',
|
||||
@@ -2280,6 +2406,7 @@ const locale = {
|
||||
'common.refresh': 'Refresh',
|
||||
|
||||
'userManage.title': 'User Management',
|
||||
'userManage.searchPlaceholder': 'Search by username/email/nickname',
|
||||
'userManage.description': 'Manage system users, roles and permissions',
|
||||
'userManage.createUser': 'Create User',
|
||||
'userManage.editUser': 'Edit User',
|
||||
@@ -2325,6 +2452,7 @@ const locale = {
|
||||
'profile.nicknamePlaceholder': 'Enter your nickname',
|
||||
'profile.emailPlaceholder': 'Enter your email',
|
||||
'profile.emailInvalid': 'Invalid email format',
|
||||
'profile.emailCannotChange': 'Email cannot be changed after registration',
|
||||
'profile.passwordHint': 'Password must be at least 6 characters',
|
||||
'profile.oldPassword': 'Current Password',
|
||||
'profile.newPassword': 'New Password',
|
||||
@@ -2336,7 +2464,89 @@ const locale = {
|
||||
'profile.confirmPasswordRequired': 'Please confirm password',
|
||||
'profile.confirmPasswordPlaceholder': 'Confirm new password',
|
||||
'profile.passwordMin': 'Password must be at least 6 characters',
|
||||
'profile.passwordMismatch': 'Passwords do not match'
|
||||
'profile.passwordMismatch': 'Passwords do not match',
|
||||
|
||||
// Profile - Credits
|
||||
'profile.credits.title': 'My Credits',
|
||||
'profile.credits.unit': 'Credits',
|
||||
'profile.credits.recharge': 'Top Up',
|
||||
'profile.credits.vipExpires': 'VIP expires on',
|
||||
'profile.credits.vipExpired': 'VIP expired',
|
||||
'profile.credits.noVip': 'Not a VIP',
|
||||
'profile.credits.hint': 'AI analysis and other features consume credits. VIP users get free access.',
|
||||
|
||||
// Profile - Credits Log
|
||||
'profile.creditsLog': 'Credits History',
|
||||
'profile.creditsLog.time': 'Time',
|
||||
'profile.creditsLog.action': 'Type',
|
||||
'profile.creditsLog.amount': 'Change',
|
||||
'profile.creditsLog.balance': 'Balance',
|
||||
'profile.creditsLog.remark': 'Remark',
|
||||
'profile.creditsLog.actionConsume': 'Consume',
|
||||
'profile.creditsLog.actionRecharge': 'Recharge',
|
||||
'profile.creditsLog.actionAdjust': 'Adjust',
|
||||
'profile.creditsLog.actionRefund': 'Refund',
|
||||
'profile.creditsLog.actionVipGrant': 'VIP Grant',
|
||||
'profile.creditsLog.actionVipRevoke': 'VIP Revoke',
|
||||
'profile.creditsLog.actionRegisterBonus': 'Register Bonus',
|
||||
'profile.creditsLog.actionReferralBonus': 'Referral Bonus',
|
||||
|
||||
// Profile - Referral
|
||||
'profile.referral.title': 'Invite Friends',
|
||||
'profile.referral.listTab': 'Referrals',
|
||||
'profile.referral.totalInvited': 'Invited',
|
||||
'profile.referral.bonusPerInvite': 'Per Invite',
|
||||
'profile.referral.yourLink': 'Your Referral Link',
|
||||
'profile.referral.copyLink': 'Copy Link',
|
||||
'profile.referral.linkCopied': 'Referral link copied',
|
||||
'profile.referral.newUserBonus': 'New users get',
|
||||
'profile.referral.user': 'User',
|
||||
'profile.referral.registerTime': 'Registered',
|
||||
'profile.referral.noReferrals': 'No referrals yet',
|
||||
'profile.referral.shareNow': 'Share Now',
|
||||
|
||||
// User Manage - Credits & VIP
|
||||
'userManage.credits': 'Credits',
|
||||
'userManage.adjustCredits': 'Adjust Credits',
|
||||
'userManage.setVip': 'Set VIP',
|
||||
'userManage.currentCredits': 'Current Credits',
|
||||
'userManage.newCredits': 'New Credits',
|
||||
'userManage.enterCredits': 'Enter new credits amount',
|
||||
'userManage.creditsNonNegative': 'Credits cannot be negative',
|
||||
'userManage.currentVip': 'Current VIP Status',
|
||||
'userManage.vipActive': 'Active',
|
||||
'userManage.vipExpired': 'Expired',
|
||||
'userManage.vipDays': 'VIP Days',
|
||||
'userManage.vipExpiresAt': 'VIP Expires At',
|
||||
'userManage.cancelVip': 'Cancel VIP',
|
||||
'userManage.days': 'days',
|
||||
'userManage.customDate': 'Custom Date',
|
||||
'userManage.selectDate': 'Please select a date',
|
||||
'userManage.remark': 'Remark',
|
||||
'userManage.remarkPlaceholder': 'Optional remark',
|
||||
|
||||
// Settings - Billing
|
||||
'settings.group.billing': 'Billing & Credits',
|
||||
'settings.field.BILLING_ENABLED': 'Enable Billing',
|
||||
'settings.field.BILLING_VIP_BYPASS': 'VIP Free Access',
|
||||
'settings.field.BILLING_COST_AI_ANALYSIS': 'AI Analysis Cost',
|
||||
'settings.field.BILLING_COST_STRATEGY_RUN': 'Strategy Run Cost',
|
||||
'settings.field.BILLING_COST_BACKTEST': 'Backtest Cost',
|
||||
'settings.field.BILLING_COST_PORTFOLIO_MONITOR': 'Portfolio Monitor Cost',
|
||||
'settings.field.CREDITS_REGISTER_BONUS': 'Register Bonus',
|
||||
'settings.field.CREDITS_REFERRAL_BONUS': 'Referral Bonus',
|
||||
'settings.field.RECHARGE_TELEGRAM_URL': 'Recharge Telegram URL',
|
||||
'settings.desc.BILLING_ENABLED': 'Enable billing system. Users need credits to use certain features when enabled',
|
||||
'settings.desc.BILLING_VIP_BYPASS': 'VIP users can use all paid features for free during VIP period',
|
||||
'settings.desc.BILLING_COST_AI_ANALYSIS': 'Credits consumed per AI analysis request',
|
||||
'settings.desc.BILLING_COST_STRATEGY_RUN': 'Credits consumed when starting a strategy',
|
||||
'settings.desc.BILLING_COST_BACKTEST': 'Credits consumed per backtest run',
|
||||
'settings.desc.BILLING_COST_PORTFOLIO_MONITOR': 'Credits consumed per portfolio AI monitoring run',
|
||||
'settings.desc.CREDITS_REGISTER_BONUS': 'Credits awarded to new users on registration',
|
||||
'settings.desc.CREDITS_REFERRAL_BONUS': 'Credits awarded to referrer when someone signs up with their referral code',
|
||||
'settings.desc.VERIFICATION_CODE_MAX_ATTEMPTS': 'Maximum attempts to verify a code before lockout',
|
||||
'settings.desc.VERIFICATION_CODE_LOCK_MINUTES': 'Lockout duration after exceeding max attempts',
|
||||
'settings.desc.RECHARGE_TELEGRAM_URL': 'Telegram customer service URL for recharge inquiries'
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -215,6 +215,100 @@ const locale = {
|
||||
'user.login.privacy.view': '查看用户隐私条款',
|
||||
'user.login.privacy.collapse': '收起用户隐私条款',
|
||||
'user.login.privacy.content': '我们重视您的隐私与数据保护。1) 收集范围:仅收集实现功能所需的信息(如邮箱、手机号、区号、Web3 钱包地址)以及必要的日志与设备信息。2) 使用目的:用于账户登录与安全校验、服务功能提供、问题排查与合规要求。3) 存储与安全:数据加密存储,并采取必要的权限与访问控制措施,尽力防止未经授权的访问、披露或丢失。4) 共享与第三方:除法律法规要求或履行服务所必需外,不会与第三方共享您的个人信息;若涉及第三方服务(如钱包、短信服务商),仅在实现功能所需的最小范围内处理。5) Cookies/本地存储:用于登录态与必要的会话维持(如令牌、PHPSESSID),您可在浏览器中进行清理或限制。6) 个人权利:您可根据法律法规行使查询、更正、删除、撤回同意等权利。7) 变更与通知:本条款更新后将在页面显著位置提示。继续使用本服务即表示您已阅读并同意更新内容。若您不同意本条款或其中任何更新,请停止使用本服务并联系我们。',
|
||||
|
||||
// Login page additions
|
||||
'user.login.username': '用户名',
|
||||
'user.login.usernameRequired': '请输入用户名',
|
||||
'user.login.passwordRequired': '请输入密码',
|
||||
'user.login.tab': '登录',
|
||||
'user.login.submit': '登录',
|
||||
'user.login.register': '注册账户',
|
||||
'user.login.forgotPassword': '忘记密码?',
|
||||
'user.login.orLoginWith': '或使用以下方式登录',
|
||||
'user.login.methodPassword': '密码登录',
|
||||
'user.login.methodCode': '验证码登录',
|
||||
'user.login.email': '邮箱',
|
||||
'user.login.emailRequired': '请输入邮箱',
|
||||
'user.login.emailInvalid': '邮箱格式不正确',
|
||||
'user.login.verificationCode': '验证码',
|
||||
'user.login.codeRequired': '请输入验证码',
|
||||
'user.login.sendCode': '发送',
|
||||
'user.login.codeSent': '验证码已发送',
|
||||
'user.login.codeLoginHint': '新用户将自动注册',
|
||||
'user.login.welcomeNew': '欢迎!',
|
||||
'user.login.accountCreated': '您的账户已创建成功',
|
||||
|
||||
// OAuth
|
||||
'user.oauth.processing': '正在处理登录...',
|
||||
'user.oauth.error.missing_params': '缺少必要参数',
|
||||
'user.oauth.error.invalid_state': '无效的状态参数',
|
||||
'user.oauth.error.user_creation_failed': '创建用户失败',
|
||||
'user.oauth.error.server_error': '服务器错误',
|
||||
|
||||
// Register page
|
||||
'user.register.tab': '注册',
|
||||
'user.register.title': '创建账户',
|
||||
'user.register.email': '邮箱',
|
||||
'user.register.emailRequired': '请输入邮箱',
|
||||
'user.register.emailInvalid': '邮箱格式不正确',
|
||||
'user.register.verificationCode': '验证码',
|
||||
'user.register.codeRequired': '请输入验证码',
|
||||
'user.register.sendCode': '发送验证码',
|
||||
'user.register.codeSent': '验证码已发送',
|
||||
'user.register.username': '用户名',
|
||||
'user.register.usernameRequired': '请输入用户名',
|
||||
'user.register.usernameLength': '用户名需要3-30个字符',
|
||||
'user.register.usernamePattern': '以字母开头,只能包含字母、数字和下划线',
|
||||
'user.register.password': '密码',
|
||||
'user.register.passwordRequired': '请输入密码',
|
||||
'user.register.confirmPassword': '确认密码',
|
||||
'user.register.confirmPasswordRequired': '请确认密码',
|
||||
'user.register.passwordMismatch': '两次输入的密码不一致',
|
||||
'user.register.submit': '创建账户',
|
||||
'user.register.haveAccount': '已有账户?',
|
||||
'user.register.login': '登录',
|
||||
'user.register.success': '注册成功',
|
||||
'user.register.pleaseLogin': '请使用新账户登录',
|
||||
'user.register.pwdMinLength': '至少8个字符',
|
||||
'user.register.pwdUppercase': '至少包含一个大写字母',
|
||||
'user.register.pwdLowercase': '至少包含一个小写字母',
|
||||
'user.register.pwdNumber': '至少包含一个数字',
|
||||
|
||||
// Reset password page
|
||||
'user.resetPassword.title': '重置密码',
|
||||
'user.resetPassword.email': '邮箱',
|
||||
'user.resetPassword.emailRequired': '请输入邮箱',
|
||||
'user.resetPassword.emailInvalid': '邮箱格式不正确',
|
||||
'user.resetPassword.verificationCode': '验证码',
|
||||
'user.resetPassword.codeRequired': '请输入验证码',
|
||||
'user.resetPassword.sendCode': '发送验证码',
|
||||
'user.resetPassword.codeSent': '验证码已发送',
|
||||
'user.resetPassword.next': '下一步',
|
||||
'user.resetPassword.backToLogin': '返回登录',
|
||||
'user.resetPassword.resettingFor': '正在为以下邮箱重置密码',
|
||||
'user.resetPassword.newPassword': '新密码',
|
||||
'user.resetPassword.passwordRequired': '请输入新密码',
|
||||
'user.resetPassword.confirmPassword': '确认新密码',
|
||||
'user.resetPassword.confirmPasswordRequired': '请确认新密码',
|
||||
'user.resetPassword.submit': '重置密码',
|
||||
'user.resetPassword.back': '返回',
|
||||
'user.resetPassword.successTitle': '密码重置成功',
|
||||
'user.resetPassword.successSubtitle': '您现在可以使用新密码登录了',
|
||||
'user.resetPassword.goToLogin': '前往登录',
|
||||
|
||||
// Security
|
||||
'user.security.retry': '重试',
|
||||
|
||||
// Profile - change password
|
||||
'profile.passwordHintNew': '为了安全,修改密码需要邮箱验证。密码至少8个字符,包含大小写字母和数字。',
|
||||
'profile.verificationCode': '验证码',
|
||||
'profile.codeRequired': '请输入验证码',
|
||||
'profile.codePlaceholder': '输入验证码',
|
||||
'profile.sendCode': '发送验证码',
|
||||
'profile.codeSent': '验证码已发送',
|
||||
'profile.codeWillSendTo': '验证码将发送至',
|
||||
'profile.noEmailWarning': '请先在基本信息中设置邮箱',
|
||||
|
||||
'account.basicInfo': '基础信息',
|
||||
'account.id': '用户ID',
|
||||
'account.username': '用户名',
|
||||
@@ -1415,6 +1509,7 @@ const locale = {
|
||||
'trading-assistant.exchange.connectionFailed': '连接失败',
|
||||
'trading-assistant.exchange.testFailed': '连接测试失败',
|
||||
'trading-assistant.exchange.fillComplete': '请填写完整的交易所配置信息',
|
||||
'trading-assistant.exchange.ipWhitelistTip': '请在交易所API设置中将以下IP添加到白名单:',
|
||||
'trading-assistant.strategyTypeOptions.ai': 'AI驱动策略',
|
||||
'trading-assistant.strategyTypeOptions.indicator': '技术指标策略',
|
||||
'trading-assistant.strategyTypeOptions.aiDeveloping': 'AI驱动策略功能开发中,敬请期待',
|
||||
@@ -1758,6 +1853,9 @@ const locale = {
|
||||
'settings.link.supportedExchanges': '支持的交易所',
|
||||
'settings.link.applyApi': '申请API',
|
||||
'settings.link.createSearchEngine': '创建搜索引擎',
|
||||
'settings.link.getTurnstileKey': '获取Turnstile密钥',
|
||||
'settings.link.getGoogleCredentials': '获取Google凭据',
|
||||
'settings.link.getGithubCredentials': '获取GitHub凭据',
|
||||
'settings.restartRequired': '配置已保存,部分配置需要重启Python服务才能生效',
|
||||
'settings.copyRestartCmd': '复制重启命令',
|
||||
'settings.copySuccess': '复制成功',
|
||||
@@ -1775,11 +1873,35 @@ const locale = {
|
||||
'settings.group.agent': 'AI Agent',
|
||||
'settings.group.network': '网络代理',
|
||||
'settings.group.search': '搜索配置',
|
||||
'settings.group.security': '注册与安全',
|
||||
'settings.group.app': '应用配置',
|
||||
// Settings fields - Auth
|
||||
'settings.field.SECRET_KEY': 'Secret Key',
|
||||
'settings.field.ADMIN_USER': '管理员用户名',
|
||||
'settings.field.ADMIN_PASSWORD': '管理员密码',
|
||||
'settings.field.ADMIN_EMAIL': '管理员邮箱',
|
||||
// Settings fields - Security
|
||||
'settings.field.ENABLE_REGISTRATION': '允许注册',
|
||||
'settings.field.TURNSTILE_SITE_KEY': 'Turnstile Site Key',
|
||||
'settings.field.TURNSTILE_SECRET_KEY': 'Turnstile Secret Key',
|
||||
'settings.field.FRONTEND_URL': '前端URL',
|
||||
'settings.field.GOOGLE_CLIENT_ID': 'Google Client ID',
|
||||
'settings.field.GOOGLE_CLIENT_SECRET': 'Google Client Secret',
|
||||
'settings.field.GOOGLE_REDIRECT_URI': 'Google回调URL',
|
||||
'settings.field.GITHUB_CLIENT_ID': 'GitHub Client ID',
|
||||
'settings.field.GITHUB_CLIENT_SECRET': 'GitHub Client Secret',
|
||||
'settings.field.GITHUB_REDIRECT_URI': 'GitHub回调URL',
|
||||
'settings.field.SECURITY_IP_MAX_ATTEMPTS': 'IP最大失败次数',
|
||||
'settings.field.SECURITY_IP_WINDOW_MINUTES': 'IP统计窗口(分钟)',
|
||||
'settings.field.SECURITY_IP_BLOCK_MINUTES': 'IP封禁时长(分钟)',
|
||||
'settings.field.SECURITY_ACCOUNT_MAX_ATTEMPTS': '账户最大失败次数',
|
||||
'settings.field.SECURITY_ACCOUNT_WINDOW_MINUTES': '账户统计窗口(分钟)',
|
||||
'settings.field.SECURITY_ACCOUNT_BLOCK_MINUTES': '账户锁定时长(分钟)',
|
||||
'settings.field.VERIFICATION_CODE_EXPIRE_MINUTES': '验证码有效期(分钟)',
|
||||
'settings.field.VERIFICATION_CODE_RATE_LIMIT': '验证码发送间隔(秒)',
|
||||
'settings.field.VERIFICATION_CODE_IP_HOURLY_LIMIT': 'IP每小时验证码上限',
|
||||
'settings.field.VERIFICATION_CODE_MAX_ATTEMPTS': '验证码最大尝试次数',
|
||||
'settings.field.VERIFICATION_CODE_LOCK_MINUTES': '验证码锁定时长(分钟)',
|
||||
// Settings fields - Server
|
||||
'settings.field.PYTHON_API_HOST': '监听地址',
|
||||
'settings.field.PYTHON_API_PORT': '端口',
|
||||
@@ -1988,6 +2110,10 @@ const locale = {
|
||||
'portfolio.monitors.channels': '通知渠道',
|
||||
'portfolio.monitors.runNow': '立即执行',
|
||||
'portfolio.monitors.analysisResult': 'AI 分析结果',
|
||||
'portfolio.monitors.runningTitle': 'AI 分析已启动',
|
||||
'portfolio.monitors.runningDesc': '分析正在后台运行,完成后会通过通知推送结果。分析多个持仓可能需要几分钟时间。',
|
||||
'portfolio.monitors.timeoutTitle': '请求超时',
|
||||
'portfolio.monitors.timeoutDesc': '分析可能正在后台运行中,请稍后查看通知获取结果。如果长时间没有收到通知,请重试。',
|
||||
'portfolio.modal.addPosition': '添加持仓',
|
||||
'portfolio.modal.editPosition': '编辑持仓',
|
||||
'portfolio.modal.addMonitor': '添加监控',
|
||||
@@ -2047,6 +2173,7 @@ const locale = {
|
||||
'portfolio.message.monitorDisabled': '监控已暂停',
|
||||
'portfolio.message.monitorRunSuccess': '分析完成',
|
||||
'portfolio.message.monitorRunFailed': '分析失败',
|
||||
'portfolio.message.monitorRunning': 'AI 分析已启动,请等待通知',
|
||||
// Portfolio - 分组
|
||||
'portfolio.groups.all': '全部持仓',
|
||||
'portfolio.groups.ungrouped': '未分组',
|
||||
@@ -2089,6 +2216,7 @@ const locale = {
|
||||
'common.refresh': '刷新',
|
||||
|
||||
'userManage.title': '用户管理',
|
||||
'userManage.searchPlaceholder': '搜索用户名/邮箱/昵称',
|
||||
'userManage.description': '管理系统用户、角色和权限',
|
||||
'userManage.createUser': '创建用户',
|
||||
'userManage.editUser': '编辑用户',
|
||||
@@ -2134,6 +2262,7 @@ const locale = {
|
||||
'profile.nicknamePlaceholder': '输入您的昵称',
|
||||
'profile.emailPlaceholder': '输入您的邮箱',
|
||||
'profile.emailInvalid': '邮箱格式不正确',
|
||||
'profile.emailCannotChange': '注册后邮箱不可修改',
|
||||
'profile.passwordHint': '密码至少需要6个字符',
|
||||
'profile.oldPassword': '当前密码',
|
||||
'profile.newPassword': '新密码',
|
||||
@@ -2145,7 +2274,89 @@ const locale = {
|
||||
'profile.confirmPasswordRequired': '请确认新密码',
|
||||
'profile.confirmPasswordPlaceholder': '再次输入新密码',
|
||||
'profile.passwordMin': '密码至少6个字符',
|
||||
'profile.passwordMismatch': '两次输入的密码不一致'
|
||||
'profile.passwordMismatch': '两次输入的密码不一致',
|
||||
|
||||
// Profile - Credits
|
||||
'profile.credits.title': '我的积分',
|
||||
'profile.credits.unit': '积分',
|
||||
'profile.credits.recharge': '开通/充值',
|
||||
'profile.credits.vipExpires': 'VIP有效期至',
|
||||
'profile.credits.vipExpired': 'VIP已过期',
|
||||
'profile.credits.noVip': '非VIP用户',
|
||||
'profile.credits.hint': '使用AI分析等功能会消耗积分,VIP用户免费',
|
||||
|
||||
// Profile - Credits Log (消费记录)
|
||||
'profile.creditsLog': '消费记录',
|
||||
'profile.creditsLog.time': '时间',
|
||||
'profile.creditsLog.action': '类型',
|
||||
'profile.creditsLog.amount': '变动',
|
||||
'profile.creditsLog.balance': '余额',
|
||||
'profile.creditsLog.remark': '备注',
|
||||
'profile.creditsLog.actionConsume': '消费',
|
||||
'profile.creditsLog.actionRecharge': '充值',
|
||||
'profile.creditsLog.actionAdjust': '调整',
|
||||
'profile.creditsLog.actionRefund': '退款',
|
||||
'profile.creditsLog.actionVipGrant': 'VIP授予',
|
||||
'profile.creditsLog.actionVipRevoke': 'VIP取消',
|
||||
'profile.creditsLog.actionRegisterBonus': '注册奖励',
|
||||
'profile.creditsLog.actionReferralBonus': '邀请奖励',
|
||||
|
||||
// Profile - Referral (邀请)
|
||||
'profile.referral.title': '邀请好友',
|
||||
'profile.referral.listTab': '邀请列表',
|
||||
'profile.referral.totalInvited': '已邀请',
|
||||
'profile.referral.bonusPerInvite': '每邀请获得',
|
||||
'profile.referral.yourLink': '您的邀请链接',
|
||||
'profile.referral.copyLink': '复制链接',
|
||||
'profile.referral.linkCopied': '邀请链接已复制',
|
||||
'profile.referral.newUserBonus': '新用户注册获得',
|
||||
'profile.referral.user': '用户',
|
||||
'profile.referral.registerTime': '注册时间',
|
||||
'profile.referral.noReferrals': '暂无邀请记录',
|
||||
'profile.referral.shareNow': '立即分享邀请',
|
||||
|
||||
// User Manage - Credits & VIP
|
||||
'userManage.credits': '积分',
|
||||
'userManage.adjustCredits': '调整积分',
|
||||
'userManage.setVip': '设置VIP',
|
||||
'userManage.currentCredits': '当前积分',
|
||||
'userManage.newCredits': '新积分',
|
||||
'userManage.enterCredits': '输入新的积分数量',
|
||||
'userManage.creditsNonNegative': '积分不能为负数',
|
||||
'userManage.currentVip': '当前VIP状态',
|
||||
'userManage.vipActive': '有效',
|
||||
'userManage.vipExpired': '已过期',
|
||||
'userManage.vipDays': 'VIP天数',
|
||||
'userManage.vipExpiresAt': 'VIP过期时间',
|
||||
'userManage.cancelVip': '取消VIP',
|
||||
'userManage.days': '天',
|
||||
'userManage.customDate': '自定义日期',
|
||||
'userManage.selectDate': '请选择日期',
|
||||
'userManage.remark': '备注',
|
||||
'userManage.remarkPlaceholder': '可选备注',
|
||||
|
||||
// Settings - Billing
|
||||
'settings.group.billing': '计费配置',
|
||||
'settings.field.BILLING_ENABLED': '启用计费',
|
||||
'settings.field.BILLING_VIP_BYPASS': 'VIP免费',
|
||||
'settings.field.BILLING_COST_AI_ANALYSIS': 'AI分析消耗',
|
||||
'settings.field.BILLING_COST_STRATEGY_RUN': '策略运行消耗',
|
||||
'settings.field.BILLING_COST_BACKTEST': '回测消耗',
|
||||
'settings.field.BILLING_COST_PORTFOLIO_MONITOR': 'Portfolio监控消耗',
|
||||
'settings.field.CREDITS_REGISTER_BONUS': '注册奖励',
|
||||
'settings.field.CREDITS_REFERRAL_BONUS': '邀请奖励',
|
||||
'settings.field.RECHARGE_TELEGRAM_URL': '充值Telegram链接',
|
||||
'settings.desc.BILLING_ENABLED': '启用计费系统。启用后,用户使用某些功能需要消耗积分',
|
||||
'settings.desc.BILLING_VIP_BYPASS': 'VIP用户在有效期内可免费使用所有付费功能',
|
||||
'settings.desc.BILLING_COST_AI_ANALYSIS': '每次AI分析消耗的积分数',
|
||||
'settings.desc.BILLING_COST_STRATEGY_RUN': '启动策略时消耗的积分数',
|
||||
'settings.desc.BILLING_COST_BACKTEST': '每次回测消耗的积分数',
|
||||
'settings.desc.BILLING_COST_PORTFOLIO_MONITOR': '每次Portfolio AI监控消耗的积分数',
|
||||
'settings.desc.CREDITS_REGISTER_BONUS': '新用户注册时获得的积分奖励',
|
||||
'settings.desc.CREDITS_REFERRAL_BONUS': '用户通过邀请链接成功邀请新用户时,邀请人获得的积分奖励',
|
||||
'settings.desc.VERIFICATION_CODE_MAX_ATTEMPTS': '验证码验证失败的最大尝试次数,超过后将锁定',
|
||||
'settings.desc.VERIFICATION_CODE_LOCK_MINUTES': '验证码验证失败次数超过限制后的锁定时长(分钟)',
|
||||
'settings.desc.RECHARGE_TELEGRAM_URL': '用户点击充值时跳转的Telegram客服链接'
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -8,6 +8,17 @@ const components = {
|
||||
}
|
||||
|
||||
const locale = {
|
||||
// 通用
|
||||
'common.confirm': '確定',
|
||||
'common.cancel': '取消',
|
||||
'common.save': '保存',
|
||||
'common.delete': '刪除',
|
||||
'common.edit': '編輯',
|
||||
'common.add': '添加',
|
||||
'common.close': '關閉',
|
||||
'common.ok': '確定',
|
||||
'common.actions': '操作',
|
||||
'common.refresh': '刷新',
|
||||
'submit': '提交',
|
||||
'save': '保存',
|
||||
'submit.ok': '提交成功',
|
||||
@@ -19,7 +30,8 @@ const locale = {
|
||||
'menu.dashboard.community': '指標社區',
|
||||
'menu.dashboard.analysis': 'AI 分析',
|
||||
'menu.dashboard.tradingAssistant': '交易助手',
|
||||
'menu.settings': '系统设置',
|
||||
'menu.dashboard.portfolio': '資產監測',
|
||||
'menu.settings': '系統設置',
|
||||
'menu.dashboard.aiTradingAssistant': 'AI交易助手',
|
||||
'menu.dashboard.signalRobot': '信號機器人',
|
||||
'menu.dashboard.monitor': '監控頁',
|
||||
@@ -97,6 +109,36 @@ const locale = {
|
||||
'app.setting.themecolor.geekblue': '極客藍',
|
||||
'app.setting.themecolor.purple': '醬紫',
|
||||
'app.setting.tooltip': '頁面設置',
|
||||
|
||||
// 通知中心
|
||||
'notice.title': '通知中心',
|
||||
'notice.empty': '暫無通知',
|
||||
'notice.markAllRead': '全部已讀',
|
||||
'notice.clear': '清空通知',
|
||||
'notice.close': '關閉',
|
||||
'notice.justNow': '剛剛',
|
||||
'notice.minutesAgo': '分鐘前',
|
||||
'notice.hoursAgo': '小時前',
|
||||
'notice.daysAgo': '天前',
|
||||
'notice.detailInfo': '詳細信息',
|
||||
'notice.aiDecision': 'AI決策',
|
||||
'notice.confidence': '置信度',
|
||||
'notice.reasoning': '分析理由',
|
||||
'notice.symbol': '標的代碼',
|
||||
'notice.currentPrice': '當前價格',
|
||||
'notice.triggerPrice': '觸發價格',
|
||||
'notice.action': '操作',
|
||||
'notice.quantity': '數量',
|
||||
'notice.viewPortfolio': '查看持倉',
|
||||
'notice.type.aiMonitor': 'AI監控',
|
||||
'notice.type.priceAlert': '價格提醒',
|
||||
'notice.type.signal': '交易信號',
|
||||
'notice.type.buy': '買入信號',
|
||||
'notice.type.sell': '賣出信號',
|
||||
'notice.type.hold': '持有建議',
|
||||
'notice.type.trade': '交易執行',
|
||||
'notice.type.notification': '系統通知',
|
||||
|
||||
'user.login.userName': '用戶名',
|
||||
'user.login.password': '密碼',
|
||||
'user.login.username.placeholder': '賬戶: admin',
|
||||
@@ -1674,6 +1716,9 @@ const locale = {
|
||||
'settings.link.supportedExchanges': '支持的交易所',
|
||||
'settings.link.applyApi': '申請API',
|
||||
'settings.link.createSearchEngine': '創建搜索引擎',
|
||||
'settings.link.getTurnstileKey': '獲取Turnstile密鑰',
|
||||
'settings.link.getGoogleCredentials': '獲取Google憑據',
|
||||
'settings.link.getGithubCredentials': '獲取GitHub憑據',
|
||||
'settings.restartRequired': '配置已保存,部分配置需要重啟Python服務才能生效',
|
||||
'settings.copyRestartCmd': '複製重啟命令',
|
||||
'settings.copySuccess': '複製成功',
|
||||
@@ -1692,11 +1737,35 @@ const locale = {
|
||||
'settings.group.agent': 'AI Agent',
|
||||
'settings.group.network': '網絡代理',
|
||||
'settings.group.search': '搜索配置',
|
||||
'settings.group.security': '註冊與安全',
|
||||
'settings.group.app': '應用配置',
|
||||
// Settings fields - Auth
|
||||
'settings.field.SECRET_KEY': 'Secret Key',
|
||||
'settings.field.ADMIN_USER': '管理員用戶名',
|
||||
'settings.field.ADMIN_PASSWORD': '管理員密碼',
|
||||
'settings.field.ADMIN_EMAIL': '管理員郵箱',
|
||||
// Settings fields - Security
|
||||
'settings.field.ENABLE_REGISTRATION': '允許註冊',
|
||||
'settings.field.TURNSTILE_SITE_KEY': 'Turnstile Site Key',
|
||||
'settings.field.TURNSTILE_SECRET_KEY': 'Turnstile Secret Key',
|
||||
'settings.field.FRONTEND_URL': '前端URL',
|
||||
'settings.field.GOOGLE_CLIENT_ID': 'Google Client ID',
|
||||
'settings.field.GOOGLE_CLIENT_SECRET': 'Google Client Secret',
|
||||
'settings.field.GOOGLE_REDIRECT_URI': 'Google回調URL',
|
||||
'settings.field.GITHUB_CLIENT_ID': 'GitHub Client ID',
|
||||
'settings.field.GITHUB_CLIENT_SECRET': 'GitHub Client Secret',
|
||||
'settings.field.GITHUB_REDIRECT_URI': 'GitHub回調URL',
|
||||
'settings.field.SECURITY_IP_MAX_ATTEMPTS': 'IP最大失敗次數',
|
||||
'settings.field.SECURITY_IP_WINDOW_MINUTES': 'IP統計窗口(分鐘)',
|
||||
'settings.field.SECURITY_IP_BLOCK_MINUTES': 'IP封禁時長(分鐘)',
|
||||
'settings.field.SECURITY_ACCOUNT_MAX_ATTEMPTS': '帳戶最大失敗次數',
|
||||
'settings.field.SECURITY_ACCOUNT_WINDOW_MINUTES': '帳戶統計窗口(分鐘)',
|
||||
'settings.field.SECURITY_ACCOUNT_BLOCK_MINUTES': '帳戶鎖定時長(分鐘)',
|
||||
'settings.field.VERIFICATION_CODE_EXPIRE_MINUTES': '驗證碼有效期(分鐘)',
|
||||
'settings.field.VERIFICATION_CODE_RATE_LIMIT': '驗證碼發送間隔(秒)',
|
||||
'settings.field.VERIFICATION_CODE_IP_HOURLY_LIMIT': 'IP每小時驗證碼上限',
|
||||
'settings.field.VERIFICATION_CODE_MAX_ATTEMPTS': '驗證碼最大嘗試次數',
|
||||
'settings.field.VERIFICATION_CODE_LOCK_MINUTES': '驗證碼鎖定時長(分鐘)',
|
||||
// Settings fields - Server
|
||||
'settings.field.PYTHON_API_HOST': '監聽地址',
|
||||
'settings.field.PYTHON_API_PORT': '端口',
|
||||
@@ -1864,7 +1933,283 @@ const locale = {
|
||||
'settings.desc.RATE_LIMIT': '每IP每分鐘的API請求限制',
|
||||
'settings.desc.ENABLE_CACHE': '啟用響應緩存以提高性能',
|
||||
'settings.desc.ENABLE_REQUEST_LOG': '記錄所有API請求日誌,用於調試',
|
||||
'settings.desc.ENABLE_AI_ANALYSIS': '啟用AI驅動的市場分析功能'
|
||||
'settings.desc.ENABLE_AI_ANALYSIS': '啟用AI驅動的市場分析功能',
|
||||
|
||||
// Portfolio - 資產監測
|
||||
'portfolio.summary.totalValue': '總市值',
|
||||
'portfolio.summary.totalCost': '總成本',
|
||||
'portfolio.summary.totalPnl': '總盈虧',
|
||||
'portfolio.summary.positionCount': '持倉數量',
|
||||
'portfolio.summary.profitLossRatio': '盈利/虧損',
|
||||
'portfolio.summary.today': '今日',
|
||||
'portfolio.summary.todayPnl': '今日盈虧',
|
||||
'portfolio.summary.bestPerformer': '最佳表現',
|
||||
'portfolio.summary.worstPerformer': '最差表現',
|
||||
'portfolio.summary.priceSync': '價格同步',
|
||||
'portfolio.summary.syncInterval': '刷新間隔',
|
||||
'portfolio.summary.justNow': '剛剛',
|
||||
'portfolio.summary.ago': '前',
|
||||
'portfolio.positions.title': '我的持倉',
|
||||
'portfolio.positions.add': '添加持倉',
|
||||
'portfolio.positions.addFirst': '添加第一筆持倉',
|
||||
'portfolio.positions.empty': '暫無持倉記錄',
|
||||
'portfolio.positions.deleteConfirm': '確定刪除這筆持倉嗎?',
|
||||
'portfolio.positions.currentPrice': '現價',
|
||||
'portfolio.positions.entryPrice': '買入價',
|
||||
'portfolio.positions.quantity': '數量',
|
||||
'portfolio.positions.side': '方向',
|
||||
'portfolio.positions.long': '做多',
|
||||
'portfolio.positions.short': '做空',
|
||||
'portfolio.positions.marketValue': '市值',
|
||||
'portfolio.positions.pnl': '盈虧',
|
||||
'portfolio.positions.items': '個持倉',
|
||||
'portfolio.monitors.title': 'AI 監控',
|
||||
'portfolio.monitors.add': '添加監控',
|
||||
'portfolio.monitors.addFirst': '添加 AI 監控',
|
||||
'portfolio.monitors.empty': '暫無監控任務',
|
||||
'portfolio.monitors.deleteConfirm': '確定刪除這個監控任務嗎?',
|
||||
'portfolio.monitors.interval': '執行間隔',
|
||||
'portfolio.monitors.lastRun': '上次執行',
|
||||
'portfolio.monitors.nextRun': '下次執行',
|
||||
'portfolio.monitors.channels': '通知渠道',
|
||||
'portfolio.monitors.runNow': '立即執行',
|
||||
'portfolio.monitors.analysisResult': 'AI 分析結果',
|
||||
'portfolio.monitors.runningTitle': 'AI 分析已啟動',
|
||||
'portfolio.monitors.runningDesc': '分析正在後臺運行,完成後會通過通知推送結果。分析多個持倉可能需要幾分鐘時間。',
|
||||
'portfolio.monitors.timeoutTitle': '請求超時',
|
||||
'portfolio.monitors.timeoutDesc': '分析可能正在後臺運行中,請稍後查看通知獲取結果。如果長時間沒有收到通知,請重試。',
|
||||
'portfolio.modal.addPosition': '添加持倉',
|
||||
'portfolio.modal.editPosition': '編輯持倉',
|
||||
'portfolio.modal.addMonitor': '添加監控',
|
||||
'portfolio.modal.editMonitor': '編輯監控',
|
||||
'portfolio.form.market': '市場',
|
||||
'portfolio.form.marketRequired': '請選擇市場',
|
||||
'portfolio.form.selectMarket': '選擇市場',
|
||||
'portfolio.form.symbol': '標的代碼',
|
||||
'portfolio.form.symbolRequired': '請輸入標的代碼',
|
||||
'portfolio.form.searchSymbol': '搜索或輸入標的代碼',
|
||||
'portfolio.form.useAsSymbol': '使用',
|
||||
'portfolio.form.asSymbolCode': '作為標的代碼',
|
||||
'portfolio.form.symbolHint': '可搜索標的庫,或直接輸入任意代碼',
|
||||
'portfolio.form.side': '方向',
|
||||
'portfolio.form.quantity': '數量',
|
||||
'portfolio.form.quantityRequired': '請輸入數量',
|
||||
'portfolio.form.enterQuantity': '輸入持倉數量',
|
||||
'portfolio.form.entryPrice': '買入價',
|
||||
'portfolio.form.entryPriceRequired': '請輸入買入價',
|
||||
'portfolio.form.enterEntryPrice': '輸入買入價格',
|
||||
'portfolio.form.notes': '備注',
|
||||
'portfolio.form.enterNotes': '可選:添加備注',
|
||||
'portfolio.form.monitorName': '監控名稱',
|
||||
'portfolio.form.monitorNameRequired': '請輸入監控名稱',
|
||||
'portfolio.form.enterMonitorName': '例如:每日組合分析',
|
||||
'portfolio.form.interval': '執行間隔',
|
||||
'portfolio.form.minutes': '分鐘',
|
||||
'portfolio.form.hour': '小時',
|
||||
'portfolio.form.hours': '小時',
|
||||
'portfolio.form.notifyChannels': '通知渠道',
|
||||
'portfolio.form.browser': '瀏覽器通知',
|
||||
'portfolio.form.email': '郵件',
|
||||
'portfolio.form.telegramChatId': 'Telegram Chat ID',
|
||||
'portfolio.form.enterTelegramChatId': '輸入 Telegram Chat ID',
|
||||
'portfolio.form.telegramRequired': '請輸入 Telegram Chat ID',
|
||||
'portfolio.form.emailAddress': '郵箱地址',
|
||||
'portfolio.form.enterEmail': '輸入郵箱地址',
|
||||
'portfolio.form.emailRequired': '請輸入郵箱地址',
|
||||
'portfolio.form.emailInvalid': '請輸入有效的郵箱地址',
|
||||
'portfolio.form.customPrompt': '自定義提示',
|
||||
'portfolio.form.customPromptPlaceholder': '可選:添加特別關注點,例如"重點關注科技股風險"',
|
||||
'portfolio.form.monitorScope': '監控範圍',
|
||||
'portfolio.form.allPositions': '全部持倉',
|
||||
'portfolio.form.selectedPositions': '指定持倉',
|
||||
'portfolio.form.selectPositions': '選擇持倉',
|
||||
'portfolio.form.selectAll': '全選',
|
||||
'portfolio.form.deselectAll': '全不選',
|
||||
'portfolio.form.selectedCount': '已選 {count}/{total}',
|
||||
'portfolio.form.pleaseSelectPositions': '請至少選擇一個持倉進行監控',
|
||||
'portfolio.message.loadFailed': '加載數據失敗',
|
||||
'portfolio.message.saveSuccess': '保存成功',
|
||||
'portfolio.message.saveFailed': '保存失敗',
|
||||
'portfolio.message.deleteSuccess': '刪除成功',
|
||||
'portfolio.message.deleteFailed': '刪除失敗',
|
||||
'portfolio.message.updateFailed': '更新失敗',
|
||||
'portfolio.message.monitorEnabled': '監控已啟用',
|
||||
'portfolio.message.monitorDisabled': '監控已暫停',
|
||||
'portfolio.message.monitorRunSuccess': '分析完成',
|
||||
'portfolio.message.monitorRunFailed': '分析失敗',
|
||||
'portfolio.message.monitorRunning': 'AI 分析已啟動,請等待通知',
|
||||
// Portfolio - 分組
|
||||
'portfolio.groups.all': '全部持倉',
|
||||
'portfolio.groups.ungrouped': '未分組',
|
||||
'portfolio.form.group': '分組',
|
||||
'portfolio.form.enterGroup': '輸入或選擇分組',
|
||||
// Portfolio - 預警
|
||||
'portfolio.alerts.title': '價格/盈虧預警',
|
||||
'portfolio.alerts.addAlert': '添加預警',
|
||||
'portfolio.alerts.editAlert': '編輯預警',
|
||||
'portfolio.alerts.alertType': '預警類型',
|
||||
'portfolio.alerts.priceAbove': '價格高於',
|
||||
'portfolio.alerts.priceBelow': '價格低於',
|
||||
'portfolio.alerts.pnlAbove': '盈利高於 (%)',
|
||||
'portfolio.alerts.pnlBelow': '虧損低於 (%)',
|
||||
'portfolio.alerts.threshold': '閾值',
|
||||
'portfolio.alerts.thresholdRequired': '請輸入閾值',
|
||||
'portfolio.alerts.enterPrice': '輸入價格',
|
||||
'portfolio.alerts.enterPercent': '輸入百分比',
|
||||
'portfolio.alerts.currentPrice': '當前價格',
|
||||
'portfolio.alerts.currentPriceHint': '當前價格',
|
||||
'portfolio.alerts.repeatInterval': '重複提醒',
|
||||
'portfolio.alerts.noRepeat': '不重複 (觸發一次)',
|
||||
'portfolio.alerts.every5min': '每 5 分鐘',
|
||||
'portfolio.alerts.every15min': '每 15 分鐘',
|
||||
'portfolio.alerts.every30min': '每 30 分鐘',
|
||||
'portfolio.alerts.every1hour': '每 1 小時',
|
||||
'portfolio.alerts.every4hours': '每 4 小時',
|
||||
'portfolio.alerts.onceDaily': '每天一次',
|
||||
'portfolio.alerts.enabled': '啟用預警',
|
||||
'portfolio.alerts.enabledDesc': '開啟後將自動監測並觸發通知',
|
||||
'portfolio.alerts.delete': '刪除',
|
||||
'portfolio.alerts.deleteConfirm': '確定要刪除此預警嗎?',
|
||||
'portfolio.modal.addAlert': '添加預警',
|
||||
'portfolio.modal.editAlert': '編輯預警',
|
||||
|
||||
// User Management
|
||||
'menu.userManage': '用戶管理',
|
||||
'menu.myProfile': '個人中心',
|
||||
'userManage.title': '用戶管理',
|
||||
'userManage.searchPlaceholder': '搜索用戶名/郵箱/暱稱',
|
||||
'userManage.description': '管理系統用戶、角色和權限',
|
||||
'userManage.createUser': '創建用戶',
|
||||
'userManage.editUser': '編輯用戶',
|
||||
'userManage.username': '用戶名',
|
||||
'userManage.password': '密碼',
|
||||
'userManage.nickname': '暱稱',
|
||||
'userManage.email': '郵箱',
|
||||
'userManage.role': '角色',
|
||||
'userManage.status': '狀態',
|
||||
'userManage.lastLogin': '最後登錄',
|
||||
'userManage.active': '啟用',
|
||||
'userManage.disabled': '禁用',
|
||||
'userManage.neverLogin': '從未登錄',
|
||||
'userManage.usernameRequired': '請輸入用戶名',
|
||||
'userManage.usernamePlaceholder': '輸入用戶名',
|
||||
'userManage.passwordRequired': '請輸入密碼',
|
||||
'userManage.passwordPlaceholder': '輸入密碼(至少6位)',
|
||||
'userManage.passwordMin': '密碼至少6個字符',
|
||||
'userManage.nicknamePlaceholder': '輸入暱稱',
|
||||
'userManage.emailPlaceholder': '輸入郵箱',
|
||||
'userManage.emailInvalid': '郵箱格式不正確',
|
||||
'userManage.rolePlaceholder': '選擇角色',
|
||||
'userManage.statusPlaceholder': '選擇狀態',
|
||||
'userManage.resetPassword': '重置密碼',
|
||||
'userManage.resetPasswordWarning': '此操作將重置用戶密碼',
|
||||
'userManage.newPassword': '新密碼',
|
||||
'userManage.newPasswordPlaceholder': '輸入新密碼',
|
||||
'userManage.confirmDelete': '確定要刪除此用戶嗎?',
|
||||
'userManage.roleAdmin': '管理員',
|
||||
'userManage.roleManager': '經理',
|
||||
'userManage.roleUser': '普通用戶',
|
||||
'userManage.roleViewer': '訪客',
|
||||
'userManage.credits': '積分',
|
||||
'userManage.adjustCredits': '調整積分',
|
||||
'userManage.setVip': '設置VIP',
|
||||
'userManage.currentCredits': '當前積分',
|
||||
'userManage.newCredits': '新積分',
|
||||
'userManage.enterCredits': '輸入新的積分數量',
|
||||
'userManage.creditsNonNegative': '積分不能為負數',
|
||||
'userManage.currentVip': '當前VIP狀態',
|
||||
'userManage.vipActive': '有效',
|
||||
'userManage.vipExpired': '已過期',
|
||||
'userManage.vipDays': 'VIP天數',
|
||||
'userManage.vipExpiresAt': 'VIP過期時間',
|
||||
'userManage.cancelVip': '取消VIP',
|
||||
'userManage.days': '天',
|
||||
'userManage.customDate': '自定義日期',
|
||||
'userManage.selectDate': '請選擇日期',
|
||||
'userManage.remark': '備注',
|
||||
'userManage.remarkPlaceholder': '可選備注',
|
||||
|
||||
// Profile
|
||||
'profile.title': '個人中心',
|
||||
'profile.description': '管理您的賬戶設置和偏好',
|
||||
'profile.basicInfo': '基本信息',
|
||||
'profile.changePassword': '修改密碼',
|
||||
'profile.username': '用戶名',
|
||||
'profile.nickname': '暱稱',
|
||||
'profile.email': '郵箱',
|
||||
'profile.lastLogin': '最後登錄',
|
||||
'profile.nicknamePlaceholder': '輸入您的暱稱',
|
||||
'profile.emailPlaceholder': '輸入您的郵箱',
|
||||
'profile.emailInvalid': '郵箱格式不正確',
|
||||
'profile.emailCannotChange': '註冊後郵箱不可修改',
|
||||
'profile.passwordHint': '密碼至少需要6個字符',
|
||||
'profile.oldPassword': '當前密碼',
|
||||
'profile.newPassword': '新密碼',
|
||||
'profile.confirmPassword': '確認密碼',
|
||||
'profile.oldPasswordRequired': '請輸入當前密碼',
|
||||
'profile.oldPasswordPlaceholder': '輸入當前密碼',
|
||||
'profile.newPasswordRequired': '請輸入新密碼',
|
||||
'profile.newPasswordPlaceholder': '輸入新密碼',
|
||||
'profile.confirmPasswordRequired': '請確認新密碼',
|
||||
'profile.confirmPasswordPlaceholder': '再次輸入新密碼',
|
||||
'profile.passwordMin': '密碼至少6個字符',
|
||||
'profile.passwordMismatch': '兩次輸入的密碼不一致',
|
||||
'profile.credits.title': '我的積分',
|
||||
'profile.credits.unit': '積分',
|
||||
'profile.credits.recharge': '開通/充值',
|
||||
'profile.credits.vipExpires': 'VIP有效期至',
|
||||
'profile.credits.vipExpired': 'VIP已過期',
|
||||
'profile.credits.noVip': '非VIP用戶',
|
||||
'profile.credits.hint': '使用AI分析等功能會消耗積分,VIP用戶免費',
|
||||
'profile.creditsLog': '消費記錄',
|
||||
'profile.creditsLog.time': '時間',
|
||||
'profile.creditsLog.action': '類型',
|
||||
'profile.creditsLog.amount': '變動',
|
||||
'profile.creditsLog.balance': '餘額',
|
||||
'profile.creditsLog.remark': '備注',
|
||||
'profile.creditsLog.actionConsume': '消費',
|
||||
'profile.creditsLog.actionRecharge': '充值',
|
||||
'profile.creditsLog.actionAdjust': '調整',
|
||||
'profile.creditsLog.actionRefund': '退款',
|
||||
'profile.creditsLog.actionVipGrant': 'VIP授予',
|
||||
'profile.creditsLog.actionVipRevoke': 'VIP取消',
|
||||
'profile.creditsLog.actionRegisterBonus': '註冊獎勵',
|
||||
'profile.creditsLog.actionReferralBonus': '邀請獎勵',
|
||||
'profile.referral.title': '邀請好友',
|
||||
'profile.referral.listTab': '邀請列表',
|
||||
'profile.referral.totalInvited': '已邀請',
|
||||
'profile.referral.bonusPerInvite': '每邀請獲得',
|
||||
'profile.referral.yourLink': '您的邀請鏈接',
|
||||
'profile.referral.copyLink': '複製鏈接',
|
||||
'profile.referral.linkCopied': '邀請鏈接已複製',
|
||||
'profile.referral.newUserBonus': '新用戶註冊獲得',
|
||||
'profile.referral.user': '用戶',
|
||||
'profile.referral.registerTime': '註冊時間',
|
||||
'profile.referral.noReferrals': '暫無邀請記錄',
|
||||
'profile.referral.shareNow': '立即分享邀請',
|
||||
|
||||
// Settings - Billing
|
||||
'settings.group.billing': '計費配置',
|
||||
'settings.field.BILLING_ENABLED': '啟用計費',
|
||||
'settings.field.BILLING_VIP_BYPASS': 'VIP免費',
|
||||
'settings.field.BILLING_COST_AI_ANALYSIS': 'AI分析消耗',
|
||||
'settings.field.BILLING_COST_STRATEGY_RUN': '策略運行消耗',
|
||||
'settings.field.BILLING_COST_BACKTEST': '回測消耗',
|
||||
'settings.field.BILLING_COST_PORTFOLIO_MONITOR': 'Portfolio監控消耗',
|
||||
'settings.field.CREDITS_REGISTER_BONUS': '註冊獎勵',
|
||||
'settings.field.CREDITS_REFERRAL_BONUS': '邀請獎勵',
|
||||
'settings.field.RECHARGE_TELEGRAM_URL': '充值Telegram鏈接',
|
||||
'settings.desc.BILLING_ENABLED': '啟用計費系統。啟用後,用戶使用某些功能需要消耗積分',
|
||||
'settings.desc.BILLING_VIP_BYPASS': 'VIP用戶在有效期內可免費使用所有付費功能',
|
||||
'settings.desc.BILLING_COST_AI_ANALYSIS': '每次AI分析消耗的積分數',
|
||||
'settings.desc.BILLING_COST_STRATEGY_RUN': '啟動策略時消耗的積分數',
|
||||
'settings.desc.BILLING_COST_BACKTEST': '每次回測消耗的積分數',
|
||||
'settings.desc.BILLING_COST_PORTFOLIO_MONITOR': '每次Portfolio AI監控消耗的積分數',
|
||||
'settings.desc.CREDITS_REGISTER_BONUS': '新用戶註冊時獲得的積分獎勵',
|
||||
'settings.desc.CREDITS_REFERRAL_BONUS': '用戶通過邀請鏈接成功邀請新用戶時,邀請人獲得的積分獎勵',
|
||||
'settings.desc.VERIFICATION_CODE_MAX_ATTEMPTS': '驗證碼驗證失敗的最大嘗試次數,超過後將鎖定',
|
||||
'settings.desc.VERIFICATION_CODE_LOCK_MINUTES': '驗證碼驗證失敗次數超過限制後的鎖定時長(分鐘)',
|
||||
'settings.desc.RECHARGE_TELEGRAM_URL': '用戶點擊充值時跳轉的Telegram客服鏈接'
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -29,7 +29,12 @@ router.beforeEach((to, from, next) => {
|
||||
to.meta && typeof to.meta.title !== 'undefined' && setDocumentTitle(`${i18nRender(to.meta.title)} - ${domTitle}`)
|
||||
|
||||
// Check whether we have a token (local-only auth).
|
||||
const token = storage.get(ACCESS_TOKEN)
|
||||
// 处理 token 可能是字符串或对象的情况
|
||||
let token = storage.get(ACCESS_TOKEN)
|
||||
if (token && typeof token !== 'string') {
|
||||
token = token.token || token.value || (typeof token === 'object' ? null : token)
|
||||
}
|
||||
token = typeof token === 'string' ? token : null
|
||||
|
||||
if (token) {
|
||||
// 有 token,允许访问所有页面
|
||||
|
||||
@@ -10,6 +10,26 @@ const PHPSESSID_KEY = 'PHPSESSID'
|
||||
// Locale storage key used by vue-i18n (see src/locales/index.js)
|
||||
const LOCALE_KEY = 'lang'
|
||||
|
||||
/**
|
||||
* 获取 token,处理 token 可能是字符串或对象的情况
|
||||
*/
|
||||
function getToken () {
|
||||
let token = storage.get(ACCESS_TOKEN)
|
||||
if (!token) {
|
||||
return null
|
||||
}
|
||||
if (typeof token !== 'string') {
|
||||
// 如果是对象,尝试获取 token 属性
|
||||
if (token && typeof token === 'object') {
|
||||
token = token.token || token.value || null
|
||||
} else {
|
||||
token = null
|
||||
}
|
||||
}
|
||||
// 确保 token 是字符串且不为空
|
||||
return (typeof token === 'string' && token.length > 0) ? token : null
|
||||
}
|
||||
|
||||
// 创建 axios 实例
|
||||
const request = axios.create({
|
||||
// API 请求的默认前缀
|
||||
@@ -38,11 +58,11 @@ const errorHandler = (error) => {
|
||||
description: 'Authorization verification failed'
|
||||
})
|
||||
// 不清理本地 token,避免刷新后丢失登录态;仅跳转到登录页
|
||||
const loginPath = '/user/login'
|
||||
const cur = window.location.pathname + window.location.search
|
||||
if (!cur.includes('/user/login')) {
|
||||
const redirect = encodeURIComponent(cur)
|
||||
window.location.assign(`${loginPath}?redirect=${redirect}`)
|
||||
// 项目使用 hash 模式,需要跳转到 /#/user/login
|
||||
const curHash = window.location.hash || ''
|
||||
if (!curHash.includes('/user/login')) {
|
||||
const redirect = encodeURIComponent(curHash.replace('#', '') || '/')
|
||||
window.location.assign(`/#/user/login?redirect=${redirect}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +71,8 @@ const errorHandler = (error) => {
|
||||
|
||||
// request interceptor
|
||||
request.interceptors.request.use(config => {
|
||||
const token = storage.get(ACCESS_TOKEN)
|
||||
// 使用统一的 token 获取函数
|
||||
const token = getToken()
|
||||
const lang = storage.get(LOCALE_KEY) || 'en-US'
|
||||
|
||||
// Tell backend which UI language user is using, so AI reports can match it.
|
||||
@@ -67,6 +88,15 @@ request.interceptors.request.use(config => {
|
||||
config.headers[ACCESS_TOKEN] = token
|
||||
// 兼容后端要求的 token 头
|
||||
config.headers['token'] = token
|
||||
} else {
|
||||
// 调试:如果 token 不存在,记录日志
|
||||
if (config.url && config.url.includes('/api/auth/info')) {
|
||||
const rawToken = storage.get(ACCESS_TOKEN)
|
||||
console.warn('Token missing for /api/auth/info request')
|
||||
console.warn('Raw token from storage:', rawToken)
|
||||
console.warn('Token type:', typeof rawToken)
|
||||
console.warn('Token value:', rawToken)
|
||||
}
|
||||
}
|
||||
|
||||
// 防止缓存导致的 304:为请求添加禁止缓存的头
|
||||
|
||||
@@ -1585,13 +1585,21 @@ export default {
|
||||
async runMonitorNow (id) {
|
||||
this.runningMonitor = id
|
||||
try {
|
||||
// 传递当前语言给后端
|
||||
// 传递当前语言给后端,使用异步模式
|
||||
const currentLang = this.$store.getters.lang || 'en-US'
|
||||
const res = await runMonitor(id, { language: currentLang })
|
||||
const res = await runMonitor(id, { language: currentLang, async: true })
|
||||
if (res && res.code === 1) {
|
||||
if (res.data?.success) {
|
||||
// 异步模式:后端立即返回,在后台执行
|
||||
if (res.data?.status === 'running') {
|
||||
this.$message.success(this.$t('portfolio.message.monitorRunning'))
|
||||
this.$notification.info({
|
||||
message: this.$t('portfolio.monitors.runningTitle'),
|
||||
description: this.$t('portfolio.monitors.runningDesc'),
|
||||
duration: 5
|
||||
})
|
||||
} else if (res.data?.success) {
|
||||
// 同步模式返回结果(兼容旧逻辑)
|
||||
this.$message.success(this.$t('portfolio.message.monitorRunSuccess'))
|
||||
// Show analysis result in a modal or notification
|
||||
if (res.data.analysis) {
|
||||
this.$notification.open({
|
||||
message: this.$t('portfolio.monitors.analysisResult'),
|
||||
@@ -1599,13 +1607,22 @@ export default {
|
||||
duration: 0
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.$message.error(res.data?.error || this.$t('portfolio.message.monitorRunFailed'))
|
||||
} else if (res.data?.error) {
|
||||
this.$message.error(res.data.error || this.$t('portfolio.message.monitorRunFailed'))
|
||||
}
|
||||
this.loadMonitors()
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('portfolio.message.monitorRunFailed'))
|
||||
// Handle timeout gracefully - analysis may still be running in background
|
||||
if (e.code === 'ECONNABORTED' || e.message?.includes('timeout')) {
|
||||
this.$notification.warning({
|
||||
message: this.$t('portfolio.monitors.timeoutTitle'),
|
||||
description: this.$t('portfolio.monitors.timeoutDesc'),
|
||||
duration: 8
|
||||
})
|
||||
} else {
|
||||
this.$message.error(this.$t('portfolio.message.monitorRunFailed'))
|
||||
}
|
||||
} finally {
|
||||
this.runningMonitor = null
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,14 +10,26 @@
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="toolbar">
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
<a-icon type="user-add" />
|
||||
{{ $t('userManage.createUser') || 'Create User' }}
|
||||
</a-button>
|
||||
<a-button @click="loadUsers">
|
||||
<a-icon type="reload" />
|
||||
{{ $t('common.refresh') || 'Refresh' }}
|
||||
</a-button>
|
||||
<div class="toolbar-left">
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
<a-icon type="user-add" />
|
||||
{{ $t('userManage.createUser') || 'Create User' }}
|
||||
</a-button>
|
||||
<a-button @click="loadUsers">
|
||||
<a-icon type="reload" />
|
||||
{{ $t('common.refresh') || 'Refresh' }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<a-input-search
|
||||
v-model="searchKeyword"
|
||||
:placeholder="$t('userManage.searchPlaceholder') || 'Search by username/email'"
|
||||
style="width: 280px"
|
||||
allowClear
|
||||
@search="handleSearch"
|
||||
@pressEnter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Table -->
|
||||
@@ -50,6 +62,22 @@
|
||||
<span v-else class="text-muted">{{ $t('userManage.neverLogin') || 'Never' }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Credits Column -->
|
||||
<template slot="credits" slot-scope="text">
|
||||
<span class="credits-value">{{ formatCredits(text) }}</span>
|
||||
</template>
|
||||
|
||||
<!-- VIP Column -->
|
||||
<template slot="vip_expires_at" slot-scope="text">
|
||||
<template v-if="text && isVipActive(text)">
|
||||
<a-tag color="gold">
|
||||
<a-icon type="crown" />
|
||||
{{ formatDate(text) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<template slot="action" slot-scope="text, record">
|
||||
<a-space>
|
||||
@@ -58,6 +86,16 @@
|
||||
<a-icon type="edit" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="$t('userManage.adjustCredits') || 'Adjust Credits'">
|
||||
<a-button type="link" size="small" @click="showCreditsModal(record)">
|
||||
<a-icon type="wallet" style="color: #722ed1" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="$t('userManage.setVip') || 'Set VIP'">
|
||||
<a-button type="link" size="small" @click="showVipModal(record)">
|
||||
<a-icon type="crown" style="color: #faad14" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="$t('userManage.resetPassword') || 'Reset Password'">
|
||||
<a-button type="link" size="small" @click="showResetPasswordModal(record)">
|
||||
<a-icon type="key" />
|
||||
@@ -185,11 +223,85 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- Adjust Credits Modal -->
|
||||
<a-modal
|
||||
v-model="creditsModalVisible"
|
||||
:title="($t('userManage.adjustCredits') || 'Adjust Credits') + (creditsEditingUser ? ` - ${creditsEditingUser.username}` : '')"
|
||||
:confirmLoading="creditsLoading"
|
||||
@ok="handleSetCredits"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<div class="current-credits-info" v-if="creditsEditingUser">
|
||||
<span class="label">{{ $t('userManage.currentCredits') || 'Current Credits' }}:</span>
|
||||
<span class="value">{{ formatCredits(creditsEditingUser.credits) }}</span>
|
||||
</div>
|
||||
<a-form-item :label="$t('userManage.newCredits') || 'New Credits'">
|
||||
<a-input-number
|
||||
v-model="newCredits"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
:placeholder="$t('userManage.enterCredits') || 'Enter new credits amount'"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('userManage.remark') || 'Remark'">
|
||||
<a-input
|
||||
v-model="creditsRemark"
|
||||
:placeholder="$t('userManage.remarkPlaceholder') || 'Optional remark'"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- Set VIP Modal -->
|
||||
<a-modal
|
||||
v-model="vipModalVisible"
|
||||
:title="($t('userManage.setVip') || 'Set VIP') + (vipEditingUser ? ` - ${vipEditingUser.username}` : '')"
|
||||
:confirmLoading="vipLoading"
|
||||
@ok="handleSetVip"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<div class="current-vip-info" v-if="vipEditingUser && vipEditingUser.vip_expires_at">
|
||||
<span class="label">{{ $t('userManage.currentVip') || 'Current VIP' }}:</span>
|
||||
<span class="value" :class="isVipActive(vipEditingUser.vip_expires_at) ? 'active' : 'expired'">
|
||||
{{ isVipActive(vipEditingUser.vip_expires_at)
|
||||
? ($t('userManage.vipActive') || 'Active') + ` (${formatDate(vipEditingUser.vip_expires_at)})`
|
||||
: ($t('userManage.vipExpired') || 'Expired') }}
|
||||
</span>
|
||||
</div>
|
||||
<a-form-item :label="$t('userManage.vipDays') || 'VIP Days'">
|
||||
<a-select v-model="vipDays" style="width: 100%">
|
||||
<a-select-option :value="0">{{ $t('userManage.cancelVip') || 'Cancel VIP' }}</a-select-option>
|
||||
<a-select-option :value="7">7 {{ $t('userManage.days') || 'days' }}</a-select-option>
|
||||
<a-select-option :value="30">30 {{ $t('userManage.days') || 'days' }}</a-select-option>
|
||||
<a-select-option :value="90">90 {{ $t('userManage.days') || 'days' }}</a-select-option>
|
||||
<a-select-option :value="180">180 {{ $t('userManage.days') || 'days' }}</a-select-option>
|
||||
<a-select-option :value="365">365 {{ $t('userManage.days') || 'days' }}</a-select-option>
|
||||
<a-select-option :value="-1">{{ $t('userManage.customDate') || 'Custom Date' }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="vipDays === -1" :label="$t('userManage.vipExpiresAt') || 'VIP Expires At'">
|
||||
<a-date-picker
|
||||
v-model="vipCustomDate"
|
||||
showTime
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('userManage.remark') || 'Remark'">
|
||||
<a-input
|
||||
v-model="vipRemark"
|
||||
:placeholder="$t('userManage.remarkPlaceholder') || 'Optional remark'"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserList, createUser, updateUser, deleteUser, resetUserPassword, getRoles } from '@/api/user'
|
||||
import { getUserList, createUser, updateUser, deleteUser, resetUserPassword, getRoles, setUserCredits, setUserVip } from '@/api/user'
|
||||
import { baseMixin } from '@/store/app-mixin'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
@@ -201,6 +313,7 @@ export default {
|
||||
loading: false,
|
||||
users: [],
|
||||
roles: [],
|
||||
searchKeyword: '',
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
@@ -214,7 +327,20 @@ export default {
|
||||
// Reset Password Modal
|
||||
resetPasswordVisible: false,
|
||||
resetPasswordLoading: false,
|
||||
resetPasswordUserId: null
|
||||
resetPasswordUserId: null,
|
||||
// Credits Modal
|
||||
creditsModalVisible: false,
|
||||
creditsLoading: false,
|
||||
creditsEditingUser: null,
|
||||
newCredits: 0,
|
||||
creditsRemark: '',
|
||||
// VIP Modal
|
||||
vipModalVisible: false,
|
||||
vipLoading: false,
|
||||
vipEditingUser: null,
|
||||
vipDays: 30,
|
||||
vipCustomDate: null,
|
||||
vipRemark: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -240,7 +366,7 @@ export default {
|
||||
{
|
||||
title: this.$t('userManage.nickname') || 'Nickname',
|
||||
dataIndex: 'nickname',
|
||||
width: 120
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: this.$t('userManage.email') || 'Email',
|
||||
@@ -250,25 +376,37 @@ export default {
|
||||
{
|
||||
title: this.$t('userManage.role') || 'Role',
|
||||
dataIndex: 'role',
|
||||
width: 100,
|
||||
width: 90,
|
||||
scopedSlots: { customRender: 'role' }
|
||||
},
|
||||
{
|
||||
title: this.$t('userManage.credits') || 'Credits',
|
||||
dataIndex: 'credits',
|
||||
width: 100,
|
||||
scopedSlots: { customRender: 'credits' }
|
||||
},
|
||||
{
|
||||
title: 'VIP',
|
||||
dataIndex: 'vip_expires_at',
|
||||
width: 120,
|
||||
scopedSlots: { customRender: 'vip_expires_at' }
|
||||
},
|
||||
{
|
||||
title: this.$t('userManage.status') || 'Status',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
width: 90,
|
||||
scopedSlots: { customRender: 'status' }
|
||||
},
|
||||
{
|
||||
title: this.$t('userManage.lastLogin') || 'Last Login',
|
||||
dataIndex: 'last_login_at',
|
||||
width: 160,
|
||||
width: 150,
|
||||
scopedSlots: { customRender: 'last_login_at' }
|
||||
},
|
||||
{
|
||||
title: this.$t('common.actions') || 'Actions',
|
||||
dataIndex: 'action',
|
||||
width: 150,
|
||||
width: 180,
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
]
|
||||
@@ -288,7 +426,8 @@ export default {
|
||||
try {
|
||||
const res = await getUserList({
|
||||
page: this.pagination.current,
|
||||
page_size: this.pagination.pageSize
|
||||
page_size: this.pagination.pageSize,
|
||||
search: this.searchKeyword || ''
|
||||
})
|
||||
if (res.code === 1) {
|
||||
this.users = res.data.items || []
|
||||
@@ -303,6 +442,11 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
handleSearch () {
|
||||
this.pagination.current = 1
|
||||
this.loadUsers()
|
||||
},
|
||||
|
||||
async loadRoles () {
|
||||
try {
|
||||
const res = await getRoles()
|
||||
@@ -454,6 +598,99 @@ export default {
|
||||
if (!timestamp) return ''
|
||||
const date = new Date(typeof timestamp === 'number' ? timestamp * 1000 : timestamp)
|
||||
return date.toLocaleString()
|
||||
},
|
||||
|
||||
formatCredits (credits) {
|
||||
if (!credits && credits !== 0) return '0'
|
||||
return Number(credits).toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
|
||||
},
|
||||
|
||||
formatDate (dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString()
|
||||
},
|
||||
|
||||
isVipActive (expiresAt) {
|
||||
if (!expiresAt) return false
|
||||
return new Date(expiresAt) > new Date()
|
||||
},
|
||||
|
||||
// Credits Modal
|
||||
showCreditsModal (record) {
|
||||
this.creditsEditingUser = record
|
||||
this.newCredits = parseFloat(record.credits) || 0
|
||||
this.creditsRemark = ''
|
||||
this.creditsModalVisible = true
|
||||
},
|
||||
|
||||
async handleSetCredits () {
|
||||
if (this.newCredits < 0) {
|
||||
this.$message.error(this.$t('userManage.creditsNonNegative') || 'Credits cannot be negative')
|
||||
return
|
||||
}
|
||||
|
||||
this.creditsLoading = true
|
||||
try {
|
||||
const res = await setUserCredits({
|
||||
user_id: this.creditsEditingUser.id,
|
||||
credits: this.newCredits,
|
||||
remark: this.creditsRemark
|
||||
})
|
||||
if (res.code === 1) {
|
||||
this.$message.success(res.msg || 'Credits updated successfully')
|
||||
this.creditsModalVisible = false
|
||||
this.loadUsers()
|
||||
} else {
|
||||
this.$message.error(res.msg || 'Update failed')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('Update failed')
|
||||
} finally {
|
||||
this.creditsLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
// VIP Modal
|
||||
showVipModal (record) {
|
||||
this.vipEditingUser = record
|
||||
this.vipDays = 30
|
||||
this.vipCustomDate = null
|
||||
this.vipRemark = ''
|
||||
this.vipModalVisible = true
|
||||
},
|
||||
|
||||
async handleSetVip () {
|
||||
const data = {
|
||||
user_id: this.vipEditingUser.id,
|
||||
remark: this.vipRemark
|
||||
}
|
||||
|
||||
if (this.vipDays === -1) {
|
||||
if (!this.vipCustomDate) {
|
||||
this.$message.error(this.$t('userManage.selectDate') || 'Please select a date')
|
||||
return
|
||||
}
|
||||
data.vip_expires_at = this.vipCustomDate.toISOString()
|
||||
} else {
|
||||
data.vip_days = this.vipDays
|
||||
}
|
||||
|
||||
this.vipLoading = true
|
||||
try {
|
||||
const res = await setUserVip(data)
|
||||
if (res.code === 1) {
|
||||
this.$message.success(res.msg || 'VIP status updated successfully')
|
||||
this.vipModalVisible = false
|
||||
this.loadUsers()
|
||||
} else {
|
||||
this.$message.error(res.msg || 'Update failed')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('Update failed')
|
||||
} finally {
|
||||
this.vipLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -495,7 +732,18 @@ export default {
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-table-card {
|
||||
@@ -552,5 +800,41 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Credits value style
|
||||
.credits-value {
|
||||
font-weight: 600;
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
// Current info styles
|
||||
.current-credits-info,
|
||||
.current-vip-info {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
font-size: 18px;
|
||||
|
||||
&.active {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.expired {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user