inint
This commit is contained in:
@@ -1,23 +1,69 @@
|
||||
import {createRouter, createWebHistory} from 'vue-router'
|
||||
import Index from "../views/index.vue";
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
// 需要登录才能访问的路由名称
|
||||
const AUTH_ROUTES = ['Index', 'Withdraw']
|
||||
|
||||
// 不需要登录即可访问的路由名称(白名单)
|
||||
const PUBLIC_ROUTES = ['Login', 'LinuxdoBind', 'Agree']
|
||||
|
||||
// 示例路由配置
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Index,
|
||||
redirect: '/login'
|
||||
},
|
||||
{
|
||||
path: '/login', name: 'Login',
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/login.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/linuxdo-bind',
|
||||
name: 'LinuxdoBind',
|
||||
component: () => import('@/views/linuxdo-bind.vue')
|
||||
},
|
||||
{
|
||||
path: '/index',
|
||||
name: 'Index',
|
||||
component: () => import('@/views/index.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/play',
|
||||
name: 'Play',
|
||||
redirect: '/index',
|
||||
},
|
||||
{
|
||||
path: '/agree',
|
||||
name: 'Agree',
|
||||
component: () => import('@/views/agree.vue')
|
||||
},
|
||||
{
|
||||
path: '/withdraw',
|
||||
name: 'Withdraw',
|
||||
component: () => import('@/views/withdraw.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
routes,
|
||||
})
|
||||
|
||||
// ─── 全局路由守卫 ─────────────────────────────────────────────────────────────
|
||||
router.beforeEach((to, from, next) => {
|
||||
const token = sessionStorage.getItem('CQ-TOKEN')
|
||||
const isLoggedIn = !!token
|
||||
|
||||
if (to.meta?.requiresAuth && !isLoggedIn) {
|
||||
// 需要登录但未登录 → 跳转登录页
|
||||
next({ path: '/login', query: { redirect: to.fullPath } })
|
||||
} else if (to.name === 'Login' && isLoggedIn) {
|
||||
// 已登录访问登录页 → 跳转游戏主页
|
||||
next({ path: '/index' })
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,13 +1,35 @@
|
||||
import axios from "axios";
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const ins = axios.create({
|
||||
baseURL: '/',
|
||||
timeout: 10000,
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
// 请求拦截:自动附带 token
|
||||
ins.interceptors.request.use(config => {
|
||||
const token = sessionStorage.getItem('CQ-TOKEN')
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
ins.interceptors.response.use(res => {
|
||||
return res.data
|
||||
})
|
||||
|
||||
// 响应拦截:统一处理错误
|
||||
ins.interceptors.response.use(
|
||||
res => {
|
||||
return res.data
|
||||
},
|
||||
err => {
|
||||
if (err.response?.status === 401) {
|
||||
sessionStorage.removeItem('CQ-TOKEN')
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
window.location.href = '/login'
|
||||
} else {
|
||||
ElMessage.error(err.response?.data?.message || '网络错误,请稍后重试')
|
||||
}
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
export default ins
|
||||
|
||||
113
module/web/src/views/agree.vue
Normal file
113
module/web/src/views/agree.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const content = ref('')
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/misc/agree')
|
||||
if (!res.ok) throw new Error('请求失败')
|
||||
content.value = await res.text()
|
||||
} catch (e) {
|
||||
error.value = '用户协议加载失败,请稍后重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agree-page pagebg">
|
||||
<div class="agree-box">
|
||||
<div class="header">
|
||||
<button class="back-btn" @click="router.back()">← 返回</button>
|
||||
<h2>用户协议及隐私协议</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="state-tip">加载中…</div>
|
||||
<div v-else-if="error" class="state-tip error">{{ error }}</div>
|
||||
<div v-else class="content" v-html="content" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.agree-page {
|
||||
min-height: 100vh;
|
||||
background: #0a0a0a;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.agree-box {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
color: #ddd;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
h2 {
|
||||
color: #f5bd10;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
border-color: #f5bd10;
|
||||
color: #f5bd10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.state-tip {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 15px;
|
||||
|
||||
&.error {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
line-height: 1.8;
|
||||
font-size: 14px;
|
||||
|
||||
:deep(h1), :deep(h2), :deep(h3) {
|
||||
color: #f5bd10;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: #f5bd10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,77 @@
|
||||
<script setup>
|
||||
import Loading from "../components/loading.vue";
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import request from '@/utils/request'
|
||||
import Loading from '../components/loading.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const error = ref('')
|
||||
const ready = ref(false)
|
||||
|
||||
/**
|
||||
* 解析 JWT payload(无需依赖 jwt 库,仅用于读取 username/server_id)
|
||||
*/
|
||||
function parseJwt(token) {
|
||||
try {
|
||||
const base64Url = token.split('.')[1]
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
||||
return JSON.parse(decodeURIComponent(escape(atob(base64))))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const token = sessionStorage.getItem('CQ-TOKEN')
|
||||
if (!token) {
|
||||
router.replace('/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 JWT 获取账号信息
|
||||
const payload = parseJwt(token)
|
||||
if (!payload?.username) {
|
||||
sessionStorage.removeItem('CQ-TOKEN')
|
||||
router.replace('/login')
|
||||
return
|
||||
}
|
||||
|
||||
const account = payload.username
|
||||
const password = payload.password // md5 密码哈希,用于游戏客户端鉴权
|
||||
const serverId = payload.server_id || 1
|
||||
|
||||
// 调用 enter_game 接口,更新登录信息
|
||||
try {
|
||||
await request.post('/api/enter_game', {
|
||||
account,
|
||||
srvId: serverId,
|
||||
})
|
||||
} catch {
|
||||
// enter_game 失败不阻断进入游戏
|
||||
}
|
||||
|
||||
// 将账号/token/区服信息挂载到全局,供 Egret 游戏引擎读取
|
||||
window.__CQ_ACCOUNT__ = account
|
||||
window.__CQ_TOKEN__ = password
|
||||
window.__CQ_SERVER_ID__ = serverId
|
||||
|
||||
// 兼容旧版游戏引擎读取方式(通过 localStorage / cookie)
|
||||
try {
|
||||
localStorage.setItem('account', account)
|
||||
localStorage.setItem('token', password)
|
||||
localStorage.setItem('serverId', String(serverId))
|
||||
} catch {}
|
||||
|
||||
ready.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<loading/>
|
||||
<div v-if="error" style="color:#f00;text-align:center;padding-top:40vh;font-size:18px;">
|
||||
{{ error }}
|
||||
<br />
|
||||
<a style="color:#f5bd10;cursor:pointer" @click="$router.replace('/login')">重新登录</a>
|
||||
</div>
|
||||
<Loading v-else />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
170
module/web/src/views/linuxdo-bind.vue
Normal file
170
module/web/src/views/linuxdo-bind.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import request from '@/utils/request'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const connectId = ref('')
|
||||
const account = ref('')
|
||||
const password = ref('')
|
||||
const submitting = ref(false)
|
||||
const bindMode = ref('register') // 'register' | 'link'
|
||||
|
||||
onMounted(() => {
|
||||
connectId.value = route.query.connect_id || ''
|
||||
if (!connectId.value) {
|
||||
ElMessage.error('缺少授权参数,请重新登录')
|
||||
router.push('/login')
|
||||
}
|
||||
})
|
||||
|
||||
async function bindRegister() {
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await request.post('/api/linuxdo/bind', {
|
||||
connect_id: connectId.value,
|
||||
action: 'register',
|
||||
})
|
||||
if (res.code === 0) {
|
||||
sessionStorage.setItem('CQ-TOKEN', res.token || '')
|
||||
ElMessage.success('绑定成功,正在进入游戏...')
|
||||
setTimeout(() => {
|
||||
window.location.href = `/play?account=${res.account}&token=${res.token}`
|
||||
}, 1200)
|
||||
} else {
|
||||
ElMessage.error(res.message)
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function bindLink() {
|
||||
if (!account.value) return ElMessage.error('请输入账号')
|
||||
if (!password.value) return ElMessage.error('请输入密码')
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await request.post('/api/linuxdo/bind', {
|
||||
connect_id: connectId.value,
|
||||
account: account.value,
|
||||
password: password.value,
|
||||
action: 'link',
|
||||
})
|
||||
if (res.code === 0) {
|
||||
sessionStorage.setItem('CQ-TOKEN', res.token || '')
|
||||
ElMessage.success('绑定成功,正在进入游戏...')
|
||||
setTimeout(() => {
|
||||
window.location.href = `/play?account=${res.account}&token=${res.token}`
|
||||
}, 1200)
|
||||
} else {
|
||||
ElMessage.error(res.message)
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="linuxdo-bind pagebg">
|
||||
<div class="bind-box">
|
||||
<h2>LinuxDo 账号绑定</h2>
|
||||
<p class="sub">LinuxDo账号:<strong>{{ connectId }}</strong></p>
|
||||
|
||||
<div class="tabs">
|
||||
<span :class="{ active: bindMode === 'register' }" @click="bindMode = 'register'">
|
||||
直接绑定(自动创建账号)
|
||||
</span>
|
||||
<span :class="{ active: bindMode === 'link' }" @click="bindMode = 'link'">
|
||||
绑定已有账号
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="bindMode === 'register'" class="tab-content">
|
||||
<p>将以 LinuxDo 账号名自动创建游戏账号并绑定。</p>
|
||||
<button :disabled="submitting" @click="bindRegister">
|
||||
{{ submitting ? '处理中…' : '确认绑定' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="tab-content">
|
||||
<input v-model="account" type="text" placeholder="请输入游戏账号" autocomplete="off" />
|
||||
<input v-model="password" type="password" placeholder="请输入游戏密码" @keyup.enter="bindLink" />
|
||||
<button :disabled="submitting" @click="bindLink">
|
||||
{{ submitting ? '处理中…' : '绑定并登录' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.linuxdo-bind {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #0a0a0a;
|
||||
|
||||
.bind-box {
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
width: 360px;
|
||||
color: #fff;
|
||||
|
||||
h2 { text-align: center; color: #f5bd10; margin-bottom: 8px; }
|
||||
.sub { text-align: center; color: rgba(255,255,255,0.6); font-size: 13px; margin-bottom: 20px; }
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
margin-bottom: 20px;
|
||||
span {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
color: rgba(255,255,255,0.5);
|
||||
&.active { color: #f5bd10; border-bottom: 2px solid #f5bd10; }
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
p { font-size: 13px; color: rgba(255,255,255,0.6); }
|
||||
input {
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 6px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: #fff;
|
||||
outline: none;
|
||||
&::placeholder { color: rgba(255,255,255,0.35); }
|
||||
&:focus { border-color: #f5bd10; }
|
||||
}
|
||||
button {
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: #f5bd10;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
letter-spacing: 4px;
|
||||
&:hover { opacity: 0.85; }
|
||||
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,114 +1,573 @@
|
||||
<script setup>
|
||||
import {defineOptions, onMounted, ref} from "vue";
|
||||
import request from "@/utils/request";
|
||||
import {ElMessage} from "element-plus";
|
||||
import { defineOptions, onMounted, ref, computed, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import request from '@/utils/request'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({name: 'Login'});
|
||||
defineOptions({ name: 'Login' })
|
||||
|
||||
const servers = ref([])
|
||||
const router = useRouter()
|
||||
|
||||
// 表单模式:0=登录 1=注册 2=找回密码
|
||||
const mode = ref(0)
|
||||
const modeName = computed(() => ({ 0: '登 录', 1: '注 册', 2: '修改密文' }[mode.value]))
|
||||
|
||||
// 表单字段
|
||||
const account = ref('')
|
||||
const password = ref('')
|
||||
const password2 = ref('')
|
||||
const email = ref('')
|
||||
const code = ref('')
|
||||
const serverId = ref('')
|
||||
const agree = ref(false)
|
||||
const srvId = ref('')
|
||||
|
||||
// 区服列表
|
||||
const servers = ref([])
|
||||
// 验证码倒计时
|
||||
const codeCountdown = ref(0)
|
||||
let countdownTimer = null
|
||||
// 提交中
|
||||
const submitting = ref(false)
|
||||
|
||||
// 游戏配置(从后端获取)
|
||||
const gameConfig = ref({
|
||||
gameName: '清渊传奇',
|
||||
codeOpen: false,
|
||||
regCodeOpen: false,
|
||||
linuxdoAuthorizeUrl: '/api/linuxdo/authorize',
|
||||
})
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const res = await request.get('/api/config')
|
||||
if (res?.data) gameConfig.value = { ...gameConfig.value, ...res.data }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function loadServers() {
|
||||
try {
|
||||
const res = await request.get('/api/server/list')
|
||||
if (res?.code === 0) {
|
||||
const list = res.serverlist?.[0]?.serverlist || []
|
||||
servers.value = list
|
||||
if (list.length > 0) serverId.value = list[0].srvid
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ─── 登录 ─────────────────────────────────────────────────────────────────────
|
||||
async function handleLogin() {
|
||||
if (!agree.value) return ElMessage.error('请勾选同意用户协议')
|
||||
const {data} = await request.post('/api/login', {
|
||||
username: account,
|
||||
password
|
||||
})
|
||||
sessionStorage.setItem("CQ-TOKEN", data.token)
|
||||
if (!account.value) return ElMessage.error('请输入账号')
|
||||
if (!password.value) return ElMessage.error('请输入密码')
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await request.post('/api/login', { username: account.value, password: password.value })
|
||||
if (res.code === 0) {
|
||||
sessionStorage.setItem('CQ-TOKEN', res.token)
|
||||
ElMessage.success(res.message || '登录成功')
|
||||
setTimeout(() => router.push('/'), 1500)
|
||||
} else {
|
||||
ElMessage.error(res.message || '登录失败')
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function getServers() {
|
||||
const {data} = await request.get('/api/server/list')
|
||||
servers.value = data
|
||||
// ─── 注册 ─────────────────────────────────────────────────────────────────────
|
||||
async function handleRegister() {
|
||||
if (!agree.value) return ElMessage.error('请勾选同意用户协议')
|
||||
if (!account.value) return ElMessage.error('请输入账号')
|
||||
if (!password.value) return ElMessage.error('请输入密码')
|
||||
if (!password2.value) return ElMessage.error('请再次输入密码')
|
||||
if (password.value !== password2.value) return ElMessage.error('两次密码不一致!')
|
||||
if (!serverId.value) return ElMessage.error('请选择区服!')
|
||||
if (gameConfig.value.codeOpen && gameConfig.value.regCodeOpen) {
|
||||
if (!email.value) return ElMessage.error('请输入邮箱地址!')
|
||||
if (!code.value) return ElMessage.error('请输入验证码!')
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await request.post('/api/register', {
|
||||
username: account.value,
|
||||
password: password.value,
|
||||
password2: password2.value,
|
||||
serverId: serverId.value,
|
||||
email: email.value,
|
||||
code: code.value,
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessageBox.alert(res.message, '注册成功', { type: 'success', confirmButtonText: '开始游戏' })
|
||||
.then(() => {
|
||||
sessionStorage.setItem('CQ-TOKEN', res.token)
|
||||
router.push('/')
|
||||
})
|
||||
} else {
|
||||
ElMessage.error(res.message || '注册失败')
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function protectProtocol() {
|
||||
const agree = document.getElementById('agree')
|
||||
const agreeBtn = document.querySelector('#agree .agree_btn')
|
||||
agree.addEventListener('click', function () {
|
||||
agreeBtn.classList.toggle('agree_btn_checked')
|
||||
})
|
||||
// ─── 找回密码 ──────────────────────────────────────────────────────────────────
|
||||
async function handleResetPassword() {
|
||||
if (!account.value) return ElMessage.error('请输入账号')
|
||||
if (!email.value) return ElMessage.error('请输入邮箱地址')
|
||||
if (!password.value) return ElMessage.error('请输入新密码')
|
||||
if (!password2.value) return ElMessage.error('请再次输入新密码')
|
||||
if (password.value !== password2.value) return ElMessage.error('两次密码不一致!')
|
||||
if (!code.value) return ElMessage.error('请输入验证码!')
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await request.post('/api/reset_password', {
|
||||
username: account.value,
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
password2: password2.value,
|
||||
code: code.value,
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(res.message || '密码修改成功!')
|
||||
switchMode(0)
|
||||
} else {
|
||||
ElMessage.error(res.message || '操作失败')
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 发送验证码 ────────────────────────────────────────────────────────────────
|
||||
async function sendCode() {
|
||||
if (codeCountdown.value > 0) return
|
||||
if (!account.value) return ElMessage.error('请先输入账号')
|
||||
if (!email.value) return ElMessage.error('请先输入邮箱地址')
|
||||
try {
|
||||
const res = await request.post('/api/send_code', {
|
||||
username: account.value,
|
||||
email: email.value,
|
||||
type: mode.value === 1 ? 1 : 2,
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(res.message)
|
||||
codeCountdown.value = res.time || 60
|
||||
countdownTimer = setInterval(() => {
|
||||
codeCountdown.value--
|
||||
if (codeCountdown.value <= 0) clearInterval(countdownTimer)
|
||||
}, 1000)
|
||||
} else {
|
||||
ElMessage.error(res.message)
|
||||
if (res.time) {
|
||||
codeCountdown.value = res.time
|
||||
countdownTimer = setInterval(() => {
|
||||
codeCountdown.value--
|
||||
if (codeCountdown.value <= 0) clearInterval(countdownTimer)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ─── 提交总入口 ────────────────────────────────────────────────────────────────
|
||||
function handleSubmit() {
|
||||
if (mode.value === 0) handleLogin()
|
||||
else if (mode.value === 1) handleRegister()
|
||||
else handleResetPassword()
|
||||
}
|
||||
|
||||
// ─── 切换模式 ──────────────────────────────────────────────────────────────────
|
||||
function switchMode(m) {
|
||||
mode.value = m
|
||||
password.value = ''
|
||||
password2.value = ''
|
||||
code.value = ''
|
||||
if (m !== 1) email.value = ''
|
||||
}
|
||||
|
||||
// ─── 查看用户协议 ──────────────────────────────────────────────────────────────
|
||||
async function showProtocol() {
|
||||
try {
|
||||
const res = await fetch('/api/misc/agree')
|
||||
const html = await res.text()
|
||||
ElMessageBox({
|
||||
title: '用户协议及隐私协议',
|
||||
message: `<div style="max-height:60vh;overflow:auto;font-size:12px;">${html}</div>`,
|
||||
dangerouslyUseHTMLString: true,
|
||||
showCancelButton: false,
|
||||
confirmButtonText: '我已阅读',
|
||||
})
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ─── LinuxDo 登录 ──────────────────────────────────────────────────────────────
|
||||
function loginWithLinuxDo() {
|
||||
window.location.href = gameConfig.value.linuxdoAuthorizeUrl
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getServers()
|
||||
loadConfig()
|
||||
loadServers()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(countdownTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login wrapper pagebg">
|
||||
<div class="loginBox flex column p-8 gap-8" id="account-login">
|
||||
<b class="title mb-20">神临苍月</b>
|
||||
<input class="w100" type="text" id="account" v-model="account" placeholder="请输入账号" @keyup="v=>account=v.replace(/[\W]/g, '')" autocomplete="off"/>
|
||||
<input class="w100" type="password" id="password" v-model="password" placeholder="请输入密码"/>
|
||||
<el-select id="serverId" v-model="srvId" style="border: none; display: none; margin-bottom: 10px;">
|
||||
<option value="0">请选择区服</option>
|
||||
<option v-for="item in servers" :value="item.id">{{ item.name }}区</option>
|
||||
</el-select>
|
||||
<div id="agree" class="agree flex gap-4">
|
||||
<el-checkbox v-model="agree"/>
|
||||
我已阅读并同意 <a @click="protectProtocol">用户协议及隐私协议</a>
|
||||
<div class="login">
|
||||
<div class="loginBox" id="account-login">
|
||||
<b class="title">{{ gameConfig.gameName }}</b>
|
||||
|
||||
<!-- 账号 -->
|
||||
<input
|
||||
class="w100"
|
||||
type="text"
|
||||
v-model="account"
|
||||
placeholder="请输入账号"
|
||||
@input="account = account.replace(/[^\w]/g, '')"
|
||||
autocomplete="off"
|
||||
maxlength="16"
|
||||
/>
|
||||
|
||||
<!-- 密码 -->
|
||||
<input
|
||||
class="w100"
|
||||
type="password"
|
||||
v-model="password"
|
||||
:placeholder="mode === 2 ? '请输入新密码' : '请输入密码'"
|
||||
maxlength="16"
|
||||
@keyup.enter="handleSubmit"
|
||||
/>
|
||||
|
||||
<!-- 确认密码(注册/找回密码) -->
|
||||
<input
|
||||
v-if="mode !== 0"
|
||||
class="w100"
|
||||
type="password"
|
||||
v-model="password2"
|
||||
:placeholder="mode === 2 ? '请再次输入新密码' : '请再次输入密码'"
|
||||
maxlength="16"
|
||||
/>
|
||||
|
||||
<!-- 区服选择(注册) -->
|
||||
<select v-if="mode === 1" v-model="serverId" class="w100 server-select">
|
||||
<option value="">请选择区服</option>
|
||||
<option v-for="item in servers" :key="item.srvid" :value="item.srvid">
|
||||
{{ item.serverName }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- 邮箱(注册+验证码开启 / 找回密码) -->
|
||||
<input
|
||||
v-if="(mode === 1 && gameConfig.codeOpen) || mode === 2"
|
||||
class="w100"
|
||||
type="email"
|
||||
v-model="email"
|
||||
:placeholder="mode === 2 ? '请输入绑定的邮箱地址' : '邮箱地址(用于找回密码,选填)'"
|
||||
/>
|
||||
|
||||
<!-- 验证码(注册+强制验证 / 找回密码) -->
|
||||
<div
|
||||
v-if="(mode === 1 && gameConfig.codeOpen && gameConfig.regCodeOpen) || mode === 2"
|
||||
class="code-row"
|
||||
>
|
||||
<input class="flex-1" type="text" v-model="code" placeholder="请输入验证码" maxlength="6" />
|
||||
<button class="code-btn" :disabled="codeCountdown > 0" @click="sendCode">
|
||||
{{ codeCountdown > 0 ? `${codeCountdown}s 后重发` : '获取验证码' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="button pointer w100 py-8" @click="handleLogin">登 录</div>
|
||||
<div style="display:flex;justify-content:center;gap:8px;font-size:12px">
|
||||
<div style="display:flex;align-items:center;flex-direction:column;gap:4px;cursor:pointer" id="linuxdoConnect">
|
||||
<img src="/img/linuxdo_logo.png" style="width:60px;height:60px" alt="Linux.Do登录"/>
|
||||
<div>Linux.do</div>
|
||||
|
||||
<!-- 同意协议(登录/注册) -->
|
||||
<div v-if="mode !== 2" class="agree">
|
||||
<el-checkbox v-model="agree" />
|
||||
<span>我已阅读并同意 <a class="link" @click="showProtocol">用户协议及隐私协议</a></span>
|
||||
</div>
|
||||
|
||||
<!-- 主按钮 -->
|
||||
<div
|
||||
class="button"
|
||||
:class="{ disabled: submitting }"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ submitting ? '处理中…' : modeName }}
|
||||
</div>
|
||||
|
||||
<!-- 第三方登录 -->
|
||||
<div v-if="mode === 0" class="third-login">
|
||||
<div class="third-item" @click="loginWithLinuxDo">
|
||||
<img src="/img/linuxdo_logo.png" alt="Linux.Do 登录" />
|
||||
<div>Linux.Do</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="forget_password">
|
||||
<a href="javascript:void(0);" id="forgetPassword" data-type="2">忘记密码?</a>
|
||||
<a href="javascript:void(0);" class="pull-right" id="switchBtn" data-type="1">注册</a>
|
||||
|
||||
<!-- 底部切换 -->
|
||||
<div class="footer-links">
|
||||
<template v-if="mode === 0">
|
||||
<a class="link" @click="switchMode(2)">忘记密码?</a>
|
||||
<a class="link" @click="switchMode(1)">注册</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a class="link" @click="switchMode(0)">返回登录</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bg" class="gamebg"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* ── 页面容器 ─────────────────────────────────────────────────── */
|
||||
.login {
|
||||
position: relative;
|
||||
background-image: url("/img/login_bg.jpg");
|
||||
background-image: url('/img/login_bg.jpg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh; /* 兼容移动端动态视口 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loginBox {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
backdrop-filter: blur(4px);
|
||||
/* ── 登录卡片 ─────────────────────────────────────────────────── */
|
||||
.loginBox {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: rgba(0, 0, 0, 0.62);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border-radius: 14px;
|
||||
padding: 28px 24px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
& > .title {
|
||||
font-size: 28px
|
||||
/* 手机竖屏:紧贴两侧,去掉多余圆角 */
|
||||
@media (max-width: 480px) {
|
||||
max-width: 100%;
|
||||
padding: 20px 16px;
|
||||
border-radius: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 标题 ─────────────────────────────────────────────────────── */
|
||||
.title {
|
||||
font-size: 26px;
|
||||
color: #f5bd10;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 2px;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 表单元素 ─────────────────────────────────────────────────── */
|
||||
input, select {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
font-size: 15px; /* 防止 iOS Safari 自动缩放(< 16px 会触发) */
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.38);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #f5bd10;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
option {
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
height: 44px; /* 更大的点击区域 */
|
||||
}
|
||||
}
|
||||
|
||||
.server-select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── 验证码行 ──────────────────────────────────────────────────── */
|
||||
.code-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.code-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0 14px;
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f5bd10;
|
||||
background: transparent;
|
||||
color: #f5bd10;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: rgb(245, 189, 16);
|
||||
text-align: center;
|
||||
letter-spacing: 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: .8
|
||||
}
|
||||
&:not(:disabled):hover {
|
||||
background: rgba(245, 189, 16, 0.15);
|
||||
}
|
||||
|
||||
.agree_btn {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
& > input {
|
||||
height: 32px;
|
||||
@media (max-width: 480px) {
|
||||
height: 44px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 协议勾选 ──────────────────────────────────────────────────── */
|
||||
.agree {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
|
||||
.link {
|
||||
color: #f5bd10;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 主按钮 ────────────────────────────────────────────────────── */
|
||||
.button {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
background: #f5bd10;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
line-height: 46px;
|
||||
letter-spacing: 6px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
transition: opacity 0.2s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 第三方登录 ────────────────────────────────────────────────── */
|
||||
.third-login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
|
||||
.third-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover, &:active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 10px;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 底部链接 ──────────────────────────────────────────────────── */
|
||||
.footer-links {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
|
||||
.link {
|
||||
color: #f5bd10;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 通用工具类 ────────────────────────────────────────────────── */
|
||||
.flex { display: flex; }
|
||||
.flex-1 { flex: 1; }
|
||||
.column { flex-direction: column; }
|
||||
.align-center { align-items: center; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.gap-4 { gap: 4px; }
|
||||
.gap-8 { gap: 8px; }
|
||||
.w100 { width: 100%; }
|
||||
.mb-20 { margin-bottom: 4px; }
|
||||
.p-8 { padding: 8px; }
|
||||
.py-8 { padding-top: 10px; padding-bottom: 10px; }
|
||||
.pointer { cursor: pointer; }
|
||||
</style>
|
||||
|
||||
400
module/web/src/views/withdraw.vue
Normal file
400
module/web/src/views/withdraw.vue
Normal file
@@ -0,0 +1,400 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import request from '@/utils/request'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 账号信息(从 JWT 解析)
|
||||
const currentAccount = ref('')
|
||||
const currentServerId = ref(1)
|
||||
|
||||
// 表单
|
||||
const serverId = ref('')
|
||||
const roleId = ref('')
|
||||
const roleName = ref('')
|
||||
const payType = ref(0) // 0=支付宝 1=微信
|
||||
const payAccount = ref('')
|
||||
const amount = ref('')
|
||||
|
||||
// 区服列表
|
||||
const servers = ref([])
|
||||
|
||||
// 游戏配置
|
||||
const gameConfig = ref({})
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
// 计算兑换金额
|
||||
const money = computed(() => {
|
||||
const ratio = gameConfig.value?.withdrawRatio || 10000
|
||||
const a = parseInt(amount.value)
|
||||
if (!a || a < ratio) return 0
|
||||
return Math.floor(a / ratio)
|
||||
})
|
||||
|
||||
const currencyName = computed(() => gameConfig.value?.currencyName || '货币')
|
||||
|
||||
/**
|
||||
* 解析 JWT payload
|
||||
*/
|
||||
function parseJwt(token) {
|
||||
try {
|
||||
const base64Url = token.split('.')[1]
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
||||
return JSON.parse(decodeURIComponent(escape(atob(base64))))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const res = await request.get('/api/config')
|
||||
if (res?.data) {
|
||||
gameConfig.value = {
|
||||
...gameConfig.value,
|
||||
gameName: res.data.gameName,
|
||||
withdrawRatio: res.data.withdrawRatio || 10000,
|
||||
withdrawMinOnce: res.data.withdrawMinOnce || 20,
|
||||
currencyName: res.data.currencyName || '货币',
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function loadServers() {
|
||||
try {
|
||||
const res = await request.get('/api/server/list')
|
||||
if (res?.code === 0) {
|
||||
servers.value = res.serverlist?.[0]?.serverlist || []
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function handleWithdraw() {
|
||||
if (!serverId.value) return ElMessage.error('请选择区服')
|
||||
if (!roleId.value) return ElMessage.error('请输入角色 ID')
|
||||
if (!roleName.value) return ElMessage.error('请输入角色名')
|
||||
if (!payAccount.value) return ElMessage.error('请输入收款账户')
|
||||
const a = parseInt(amount.value)
|
||||
if (!a || a <= 0) return ElMessage.error('请输入提现数量')
|
||||
const ratio = gameConfig.value?.withdrawRatio || 10000
|
||||
const minOnce = gameConfig.value?.withdrawMinOnce || 20
|
||||
if (a < ratio * minOnce) return ElMessage.error(`单次提现数量不能低于 ${ratio * minOnce}`)
|
||||
|
||||
const payTypeName = payType.value === 0 ? '支付宝' : '微信'
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认提现 ${a} ${currencyName.value},折合人民币 ${money.value} 元?\n收款方式:${payTypeName}(${payAccount.value})`,
|
||||
'确认提现',
|
||||
{ confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await request.post('/api/game/withdraw', {
|
||||
server_id: 's' + serverId.value,
|
||||
account: currentAccount.value,
|
||||
role_id: roleId.value,
|
||||
role_name: roleName.value,
|
||||
pay_type: payType.value,
|
||||
pay_account: payAccount.value,
|
||||
amount: a,
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessageBox.alert(res.message, '提现成功', { type: 'success', confirmButtonText: '知道了' })
|
||||
amount.value = ''
|
||||
} else {
|
||||
ElMessage.error(res.message || '提现失败')
|
||||
}
|
||||
} catch {} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const token = sessionStorage.getItem('CQ-TOKEN')
|
||||
if (!token) {
|
||||
router.replace('/login')
|
||||
return
|
||||
}
|
||||
const payload = parseJwt(token)
|
||||
if (payload?.username) {
|
||||
currentAccount.value = payload.username
|
||||
currentServerId.value = payload.server_id || 1
|
||||
serverId.value = currentServerId.value
|
||||
}
|
||||
|
||||
await Promise.all([loadConfig(), loadServers()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="withdraw-page pagebg">
|
||||
<div class="withdraw-box">
|
||||
<div class="header">
|
||||
<button class="back-btn" @click="router.back()">← 返回</button>
|
||||
<h2>游戏提现</h2>
|
||||
</div>
|
||||
|
||||
<div class="account-info">
|
||||
当前账号:<strong>{{ currentAccount }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="form">
|
||||
<!-- 区服 -->
|
||||
<div class="form-item">
|
||||
<label>选择区服</label>
|
||||
<select v-model="serverId">
|
||||
<option value="">请选择区服</option>
|
||||
<option v-for="s in servers" :key="s.srvid" :value="s.srvid">
|
||||
{{ s.serverName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 角色 ID -->
|
||||
<div class="form-item">
|
||||
<label>角色 ID</label>
|
||||
<input
|
||||
v-model="roleId"
|
||||
type="number"
|
||||
placeholder="请输入游戏内角色 ID"
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 角色名 -->
|
||||
<div class="form-item">
|
||||
<label>角色名</label>
|
||||
<input
|
||||
v-model="roleName"
|
||||
type="text"
|
||||
placeholder="请输入游戏内角色名"
|
||||
maxlength="24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 提现数量 -->
|
||||
<div class="form-item">
|
||||
<label>提现数量({{ currencyName }})</label>
|
||||
<input
|
||||
v-model="amount"
|
||||
type="number"
|
||||
:placeholder="`最少 ${(gameConfig.withdrawRatio || 10000) * (gameConfig.withdrawMinOnce || 20)} ${currencyName}`"
|
||||
min="0"
|
||||
/>
|
||||
<div v-if="money > 0" class="hint">≈ 人民币 {{ money }} 元</div>
|
||||
</div>
|
||||
|
||||
<!-- 收款方式 -->
|
||||
<div class="form-item">
|
||||
<label>收款方式</label>
|
||||
<div class="pay-type-row">
|
||||
<label class="radio-label" :class="{ active: payType === 0 }" @click="payType = 0">
|
||||
支付宝
|
||||
</label>
|
||||
<label class="radio-label" :class="{ active: payType === 1 }" @click="payType = 1">
|
||||
微信
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 收款账户 -->
|
||||
<div class="form-item">
|
||||
<label>{{ payType === 0 ? '支付宝账号' : '微信号' }}</label>
|
||||
<input
|
||||
v-model="payAccount"
|
||||
type="text"
|
||||
:placeholder="payType === 0 ? '请输入支付宝账号(手机号/邮箱)' : '请输入微信号'"
|
||||
maxlength="30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tip">
|
||||
提示:提现到账时间为工作日,如有疑问请联系客服。
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="submit-btn"
|
||||
:disabled="submitting"
|
||||
@click="handleWithdraw"
|
||||
>
|
||||
{{ submitting ? '处理中…' : '申请提现' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.withdraw-page {
|
||||
min-height: 100vh;
|
||||
background: #0a0a0a;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.withdraw-box {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
color: #ddd;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
h2 {
|
||||
color: #f5bd10;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
border-color: #f5bd10;
|
||||
color: #f5bd10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-info {
|
||||
background: rgba(245, 189, 16, 0.08);
|
||||
border: 1px solid rgba(245, 189, 16, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
strong {
|
||||
color: #f5bd10;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
label {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
input, select {
|
||||
height: 38px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #f5bd10;
|
||||
}
|
||||
|
||||
option {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #f5bd10;
|
||||
}
|
||||
|
||||
.pay-type-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.radio-label {
|
||||
flex: 1;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
transition: all 0.2s;
|
||||
|
||||
&.active {
|
||||
border-color: #f5bd10;
|
||||
color: #f5bd10;
|
||||
background: rgba(245, 189, 16, 0.08);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid rgba(245, 189, 16, 0.4);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: #f5bd10;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
letter-spacing: 4px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user