111
This commit is contained in:
@@ -1,199 +1 @@
|
||||
/*
|
||||
* 全局样式定义
|
||||
* 此文件通过 nuxt.config.ts 全局引用
|
||||
*/
|
||||
|
||||
/* CSS变量定义 - 主题色 */
|
||||
:root {
|
||||
--primary-color: #409eff;
|
||||
--success-color: #67c23a;
|
||||
--warning-color: #e6a23c;
|
||||
--danger-color: #f56c6c;
|
||||
--info-color: #909399;
|
||||
|
||||
--text-color: #303133;
|
||||
--text-color-secondary: #606266;
|
||||
--text-color-placeholder: #c0c4cc;
|
||||
|
||||
--border-color: #dcdfe6;
|
||||
--border-color-light: #e4e7ed;
|
||||
--border-color-lighter: #ebeef5;
|
||||
|
||||
--bg-color: #ffffff;
|
||||
--bg-color-page: #f5f7fa;
|
||||
--bg-color-overlay: #ffffff;
|
||||
|
||||
--border-radius: 4px;
|
||||
--border-radius-small: 2px;
|
||||
|
||||
--box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
--box-shadow-light: 0 2px 8px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 深色主题 */
|
||||
[data-theme='dark'] {
|
||||
--text-color: #e5eaf3;
|
||||
--text-color-secondary: #a3a6ad;
|
||||
--text-color-placeholder: #8d9095;
|
||||
|
||||
--border-color: #4c4d4f;
|
||||
--border-color-light: #414243;
|
||||
--border-color-lighter: #363637;
|
||||
|
||||
--bg-color: #1d1e1f;
|
||||
--bg-color-page: #141414;
|
||||
--bg-color-overlay: #262727;
|
||||
|
||||
--box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.4);
|
||||
--box-shadow-light: 0 2px 8px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 重置样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||
sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color-page);
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #66b1ff;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
input,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-color-page);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c0c4cc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #909399;
|
||||
}
|
||||
|
||||
/* 工具类 - 文本对齐 */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 工具类 - 间距 */
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mb-10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mb-20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.p-20 {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 工具类 - 颜色 */
|
||||
.text-primary {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
/* 工具类 - 背景 */
|
||||
.bg-primary {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
background-color: var(--info-color);
|
||||
}
|
||||
/* 全局扩展样式留空,默认使用 Vant 样式体系。 */
|
||||
|
||||
@@ -1,469 +1,596 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="page-login">
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<h1 class="login-title">登录</h1>
|
||||
<p class="login-subtitle">欢迎使用 Kx UI Framework</p>
|
||||
<div class="login-orb login-orb-a"></div>
|
||||
<div class="login-orb login-orb-b"></div>
|
||||
|
||||
<div class="login-shell">
|
||||
<section class="login-hero">
|
||||
<p class="hero-kicker">SEA TIME</p>
|
||||
<h1>欢迎回到航海时代</h1>
|
||||
<p class="hero-copy">
|
||||
这是一套移动端登录页示例,保留 Vant 的按钮与标签组件,表单主体改为 SSR 稳定写法。
|
||||
</p>
|
||||
|
||||
<div class="hero-pills">
|
||||
<span>移动端优先</span>
|
||||
<span>SSR 安全</span>
|
||||
<span>模拟登录</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="login-card">
|
||||
<div class="card-top">
|
||||
<div>
|
||||
<p class="card-eyebrow">账号登录</p>
|
||||
<h2>继续你的航程</h2>
|
||||
</div>
|
||||
<van-tag round plain type="primary">Sample</van-tag>
|
||||
</div>
|
||||
|
||||
<form class="login-form" @submit.prevent="handleLogin">
|
||||
<!-- 用户名 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label" for="username">用户名</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="loginForm.username"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="请输入用户名"
|
||||
@blur="validateField('username')"
|
||||
/>
|
||||
<span class="form-error" v-if="errors.username">{{ errors.username }}</span>
|
||||
</div>
|
||||
<div class="card-banner">
|
||||
<span class="banner-dot"></span>
|
||||
默认可直接使用演示账号,也可以手动输入任意符合规则的内容。
|
||||
</div>
|
||||
|
||||
<!-- 密码 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label" for="password">密码</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
class="form-input"
|
||||
placeholder="请输入密码"
|
||||
@blur="validateField('password')"
|
||||
/>
|
||||
<span class="form-error" v-if="errors.password">{{ errors.password }}</span>
|
||||
<div class="demo-account">
|
||||
<div>
|
||||
<strong>演示账号</strong>
|
||||
<span>captain</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>演示密码</strong>
|
||||
<span>Voyage123</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label" for="captcha">验证码</label>
|
||||
<div class="captcha-wrapper">
|
||||
<form class="login-form" novalidate @submit.prevent="handleSubmit">
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="login-username">账号</label>
|
||||
<div class="field-box" :class="{ 'is-error': !!errors.username }">
|
||||
<span class="field-marker">A</span>
|
||||
<input
|
||||
id="captcha"
|
||||
v-model="loginForm.captcha"
|
||||
id="login-username"
|
||||
v-model.trim="form.username"
|
||||
class="field-input"
|
||||
type="text"
|
||||
class="form-input captcha-input"
|
||||
placeholder="请输入验证码"
|
||||
@blur="validateField('captcha')"
|
||||
inputmode="text"
|
||||
autocomplete="username"
|
||||
placeholder="请输入账号"
|
||||
@input="clearFieldError('username')"
|
||||
/>
|
||||
<div class="captcha-code" @click="refreshCaptcha">
|
||||
{{ captchaCode }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="form-error" v-if="errors.captcha">{{ errors.captcha }}</span>
|
||||
<p v-if="errors.username" class="field-error">{{ errors.username }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 记住密码 & 忘记密码 -->
|
||||
<div class="form-options">
|
||||
<label class="checkbox-label">
|
||||
<input v-model="loginForm.remember" type="checkbox" />
|
||||
<span>记住密码</span>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="login-password">密码</label>
|
||||
<div class="field-box" :class="{ 'is-error': !!errors.password }">
|
||||
<span class="field-marker">P</span>
|
||||
<input
|
||||
id="login-password"
|
||||
v-model.trim="form.password"
|
||||
class="field-input"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="请输入密码"
|
||||
@input="clearFieldError('password')"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="errors.password" class="field-error">{{ errors.password }}</p>
|
||||
</div>
|
||||
|
||||
<div class="login-options">
|
||||
<label class="check-row">
|
||||
<input v-model="form.remember" class="check-input" type="checkbox" />
|
||||
<span class="check-box"></span>
|
||||
<span class="check-text">记住演示账号</span>
|
||||
</label>
|
||||
<a href="javascript:;" class="forgot-link">忘记密码?</a>
|
||||
|
||||
<button class="ghost-link" type="button" @click="fillDemoAccount">一键填充</button>
|
||||
<van-button type="primary" to="/home" size="mini" :loading="false">路由跳转</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button type="submit" class="login-btn" :disabled="isLoading">
|
||||
{{ isLoading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
<div class="login-agreement">
|
||||
<label class="check-row">
|
||||
<input
|
||||
v-model="form.agreement"
|
||||
class="check-input"
|
||||
type="checkbox"
|
||||
@change="clearFieldError('agreement')"
|
||||
/>
|
||||
<span class="check-box"></span>
|
||||
<span class="check-text">我已阅读并同意演示使用说明</span>
|
||||
</label>
|
||||
<p v-if="errors.agreement" class="field-error agreement-error">{{ errors.agreement }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 注册链接 -->
|
||||
<div class="login-footer">
|
||||
<span class="register-tip">还没有账号?</span>
|
||||
<a href="javascript:;" class="register-link">立即注册</a>
|
||||
<div class="login-actions">
|
||||
<van-button
|
||||
block
|
||||
round
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
:loading="isSubmitting"
|
||||
:disabled="submitDisabled"
|
||||
>
|
||||
模拟登录
|
||||
</van-button>
|
||||
<van-button block round plain type="primary" native-type="button" @click="clearForm">
|
||||
清空输入
|
||||
</van-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="login-tips" v-if="tips.show">
|
||||
<p :class="['tip-text', tips.type]">{{ tips.message }}</p>
|
||||
<div class="login-tips">
|
||||
<div class="tip-item">
|
||||
<span>01</span>
|
||||
去掉 ClientOnly,刷新时不再有表单占位替换。
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<span>02</span>
|
||||
提交后延迟模拟请求,并跳转到主页。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// stores、extends、services 目录下的内容已自动导入
|
||||
// types目录需要手动import type
|
||||
|
||||
import type { ILoginParams } from '~/types/api'
|
||||
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: layout.empty
|
||||
})
|
||||
|
||||
// Pinia状态仓库(自动导入)
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
type ErrorField = 'username' | 'password' | 'agreement'
|
||||
|
||||
// 登录表单数据
|
||||
const loginForm = reactive({
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: '',
|
||||
remember: false
|
||||
remember: true,
|
||||
agreement: true
|
||||
})
|
||||
|
||||
// 表单校验错误
|
||||
const errors = reactive({
|
||||
const errors = reactive<Record<ErrorField, string>>({
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: ''
|
||||
agreement: ''
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 验证码
|
||||
const captchaCode = ref('')
|
||||
|
||||
// 提示信息
|
||||
const tips = reactive({
|
||||
show: false,
|
||||
message: '',
|
||||
type: 'info'
|
||||
const submitDisabled = computed(() => {
|
||||
return !form.username.trim() || !form.password.trim() || !form.agreement
|
||||
})
|
||||
|
||||
// 生成随机验证码
|
||||
const generateCaptcha = () => {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
let code = ''
|
||||
for (let i = 0; i < 4; i++) {
|
||||
code += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
captchaCode.value = code
|
||||
}
|
||||
|
||||
// 刷新验证码
|
||||
const refreshCaptcha = () => {
|
||||
generateCaptcha()
|
||||
}
|
||||
|
||||
// 校验单个字段
|
||||
const validateField = (field: keyof typeof errors) => {
|
||||
const clearFieldError = (field: ErrorField) => {
|
||||
errors[field] = ''
|
||||
}
|
||||
|
||||
switch (field) {
|
||||
case 'username':
|
||||
if (!loginForm.username) {
|
||||
errors.username = '请输入用户名'
|
||||
} else if (!ValidateEXTEND.minLength(loginForm.username, 3)) {
|
||||
errors.username = '用户名至少3个字符'
|
||||
}
|
||||
break
|
||||
case 'password':
|
||||
if (!loginForm.password) {
|
||||
errors.password = '请输入密码'
|
||||
} else if (!ValidateEXTEND.minLength(loginForm.password, 6)) {
|
||||
errors.password = '密码至少6个字符'
|
||||
}
|
||||
break
|
||||
case 'captcha':
|
||||
if (!loginForm.captcha) {
|
||||
errors.captcha = '请输入验证码'
|
||||
} else if (loginForm.captcha.toUpperCase() !== captchaCode.value.toUpperCase()) {
|
||||
errors.captcha = '验证码错误'
|
||||
}
|
||||
break
|
||||
const validateForm = () => {
|
||||
errors.username = ''
|
||||
errors.password = ''
|
||||
errors.agreement = ''
|
||||
|
||||
if (!form.username.trim()) {
|
||||
errors.username = '请输入账号'
|
||||
} else if (form.username.trim().length < 3) {
|
||||
errors.username = '账号至少 3 位'
|
||||
}
|
||||
|
||||
return !errors[field]
|
||||
if (!form.password.trim()) {
|
||||
errors.password = '请输入密码'
|
||||
} else if (form.password.trim().length < 6) {
|
||||
errors.password = '密码至少 6 位'
|
||||
}
|
||||
|
||||
if (!form.agreement) {
|
||||
errors.agreement = '请先勾选使用说明'
|
||||
}
|
||||
|
||||
return !errors.username && !errors.password && !errors.agreement
|
||||
}
|
||||
|
||||
// 校验整个表单
|
||||
const validateForm = () => {
|
||||
const usernameValid = validateField('username')
|
||||
const passwordValid = validateField('password')
|
||||
const captchaValid = validateField('captcha')
|
||||
return usernameValid && passwordValid && captchaValid
|
||||
const fillDemoAccount = () => {
|
||||
form.username = 'captain'
|
||||
form.password = 'Voyage123'
|
||||
errors.username = ''
|
||||
errors.password = ''
|
||||
showToast('已填充演示账号')
|
||||
}
|
||||
|
||||
// 显示提示
|
||||
const showTips = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||
tips.message = message
|
||||
tips.type = type
|
||||
tips.show = true
|
||||
|
||||
setTimeout(() => {
|
||||
tips.show = false
|
||||
}, 3000)
|
||||
const clearForm = () => {
|
||||
form.username = ''
|
||||
form.password = ''
|
||||
form.agreement = true
|
||||
errors.username = ''
|
||||
errors.password = ''
|
||||
errors.agreement = ''
|
||||
showToast('已清空输入')
|
||||
}
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
// 校验表单
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) {
|
||||
showFailToast(Object.values(errors).find(Boolean) || '请检查表单内容')
|
||||
return
|
||||
}
|
||||
|
||||
// 开始加载
|
||||
isLoading.value = true
|
||||
userStore.setLoginLoading(true)
|
||||
|
||||
try {
|
||||
// 模拟登录(实际项目中调用API)
|
||||
// const userService = new UserSERVICE()
|
||||
// const result = await userService.login(loginForm as ILoginParams)
|
||||
|
||||
// 模拟登录成功
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// 设置用户信息
|
||||
userStore.setUserInfo(
|
||||
{
|
||||
id: 1001,
|
||||
username: loginForm.username,
|
||||
nickname: loginForm.username,
|
||||
role: 'admin'
|
||||
},
|
||||
'demo-token-' + Date.now()
|
||||
)
|
||||
|
||||
showTips('登录成功!', 'success')
|
||||
|
||||
// 跳转到首页
|
||||
setTimeout(() => {
|
||||
router.push('/home')
|
||||
}, 1000)
|
||||
} catch (error: any) {
|
||||
console.error('登录失败:', error)
|
||||
showTips(error.message || '登录失败,请稍后重试', 'error')
|
||||
// 刷新验证码
|
||||
refreshCaptcha()
|
||||
loginForm.captcha = ''
|
||||
isSubmitting.value = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 900))
|
||||
showSuccessToast('登录成功')
|
||||
await navigateTo('/home')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
userStore.setLoginLoading(false)
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 如果已登录,跳转到首页
|
||||
if (userStore.isLogin) {
|
||||
router.push('/home')
|
||||
return
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
generateCaptcha()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-login {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
padding: 28px 16px 40px;
|
||||
background:
|
||||
linear-gradient(180deg, #0b3558 0%, #0f2943 46%, #f4f8fc 46%, #f4f8fc 100%);
|
||||
}
|
||||
|
||||
.login-orb {
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
filter: blur(4px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-orb-a {
|
||||
top: -36px;
|
||||
right: -22px;
|
||||
width: 148px;
|
||||
height: 148px;
|
||||
background: radial-gradient(circle, rgba(103, 232, 249, 0.56), rgba(103, 232, 249, 0));
|
||||
}
|
||||
|
||||
.login-orb-b {
|
||||
top: 168px;
|
||||
left: -52px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background: radial-gradient(circle, rgba(96, 165, 250, 0.24), rgba(96, 165, 250, 0));
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.login-hero {
|
||||
padding: 8px 6px 24px;
|
||||
color: #f8fbff;
|
||||
}
|
||||
|
||||
.hero-kicker {
|
||||
margin: 0 0 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.28em;
|
||||
color: rgba(186, 230, 253, 0.92);
|
||||
}
|
||||
|
||||
.login-hero h1 {
|
||||
margin: 0;
|
||||
font-size: 34px;
|
||||
line-height: 1.08;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
margin: 14px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: rgba(226, 232, 240, 0.92);
|
||||
}
|
||||
|
||||
.hero-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.hero-pills span {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(186, 230, 253, 0.18);
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
color: #dbeafe;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
margin-top: 8px;
|
||||
padding: 22px 16px 18px;
|
||||
border-radius: 28px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 28px 60px rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card-eyebrow {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.card-top h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.card-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #0f3d62;
|
||||
background: linear-gradient(135deg, #e0f2fe, #eff6ff);
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 20px;
|
||||
.banner-dot {
|
||||
flex: 0 0 auto;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #0ea5e9, #2563eb);
|
||||
box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.12);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
.demo-account {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
.demo-account div {
|
||||
padding: 12px 14px;
|
||||
border-radius: 18px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 10px;
|
||||
.demo-account strong,
|
||||
.demo-account span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
.demo-account strong {
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.demo-account span {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
.field-group + .field-group {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color-page);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: var(--primary-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--text-color-placeholder);
|
||||
}
|
||||
|
||||
.form-error {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--danger-color);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.captcha-wrapper {
|
||||
.field-box {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 52px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #dbe4f0;
|
||||
border-radius: 18px;
|
||||
background: #f8fafc;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.captcha-input {
|
||||
flex: 1;
|
||||
.field-box:focus-within {
|
||||
border-color: #3b82f6;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.captcha-code {
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
.field-box.is-error {
|
||||
border-color: #f87171;
|
||||
}
|
||||
|
||||
.field-marker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--bg-color-page);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4px;
|
||||
color: var(--primary-color);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #2563eb;
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.captcha-code:hover {
|
||||
background-color: var(--border-color-lighter);
|
||||
}
|
||||
|
||||
.form-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
font-size: 14px;
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgot-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
.field-input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background-color: var(--primary-color);
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.login-btn:hover:not(:disabled) {
|
||||
background-color: #66b1ff;
|
||||
.field-input::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.login-btn:disabled {
|
||||
background-color: var(--border-color);
|
||||
cursor: not-allowed;
|
||||
.field-input:-webkit-autofill,
|
||||
.field-input:-webkit-autofill:hover,
|
||||
.field-input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: #0f172a;
|
||||
box-shadow: 0 0 0 1000px #f8fafc inset;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
.field-error {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.register-tip {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
.login-options,
|
||||
.login-agreement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 2px 0;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
font-size: 14px;
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
margin-left: 5px;
|
||||
.login-agreement {
|
||||
display: block;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.register-link:hover {
|
||||
text-decoration: underline;
|
||||
.agreement-error {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
.check-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.check-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.check-box {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1.5px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.check-box::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 5px;
|
||||
width: 4px;
|
||||
height: 9px;
|
||||
border-right: 2px solid #ffffff;
|
||||
border-bottom: 2px solid #ffffff;
|
||||
opacity: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.check-input:checked + .check-box {
|
||||
border-color: #2563eb;
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.check-input:checked + .check-box::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.check-text {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ghost-link {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: #2563eb;
|
||||
background: transparent;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.login-tips {
|
||||
margin-top: 20px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #334155;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.tip-item span {
|
||||
flex: 0 0 auto;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
color: #2563eb;
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.page-login {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 36px 20px;
|
||||
}
|
||||
|
||||
.tip-text.success {
|
||||
color: var(--success-color);
|
||||
background-color: rgba(103, 194, 58, 0.1);
|
||||
.login-shell {
|
||||
max-width: 460px;
|
||||
}
|
||||
}
|
||||
|
||||
.tip-text.error {
|
||||
color: var(--danger-color);
|
||||
background-color: rgba(245, 108, 108, 0.1);
|
||||
}
|
||||
|
||||
.tip-text.info {
|
||||
color: var(--info-color);
|
||||
background-color: rgba(144, 147, 153, 0.1);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,438 +1,15 @@
|
||||
<template>
|
||||
<div class="page-home">
|
||||
<div class="container">
|
||||
<h1 class="page-title">Nuxt4 + Pinia 前端框架 Demo</h1>
|
||||
<p class="page-desc">本页面展示了框架各核心模块的调用方式</p>
|
||||
|
||||
<!-- 1. Pinia状态管理Demo -->
|
||||
<section class="demo-section">
|
||||
<h2 class="section-title">1. Pinia 状态管理</h2>
|
||||
<div class="demo-card">
|
||||
<div class="demo-item">
|
||||
<span class="label">登录状态:</span>
|
||||
<span class="value">{{ userStore.isLogin ? '已登录' : '未登录' }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">用户ID:</span>
|
||||
<span class="value">{{ userStore.userId || '-' }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">用户昵称:</span>
|
||||
<span class="value">{{ userStore.userNickname }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">用户角色:</span>
|
||||
<span class="value">{{ userStore.userRole }}</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="setUserInfo">设置用户信息</button>
|
||||
<button class="btn btn-danger" @click="clearUserInfo">清除用户信息</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. EXTEND工具库Demo -->
|
||||
<section class="demo-section">
|
||||
<h2 class="section-title">2. EXTEND 工具库</h2>
|
||||
<div class="demo-card">
|
||||
<h3 class="card-title">DateEXTEND - 日期工具</h3>
|
||||
<div class="demo-item">
|
||||
<span class="label">当前时间戳:</span>
|
||||
<span class="value">{{ currentTimestamp }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">格式化时间:</span>
|
||||
<span class="value">{{ formattedTime }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">智能时间显示:</span>
|
||||
<span class="value">{{ smartTime }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">是否今天:</span>
|
||||
<span class="value">{{ isToday ? '是' : '否' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h3 class="card-title">CryptoEXTEND - 加密工具</h3>
|
||||
<div class="demo-item">
|
||||
<span class="label">原始字符串:</span>
|
||||
<span class="value">{{ originalText }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">Base64加密:</span>
|
||||
<span class="value">{{ encryptedBase64 }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">自定义密钥加密:</span>
|
||||
<span class="value">{{ encryptedWithKey }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">自定义密钥解密:</span>
|
||||
<span class="value">{{ decryptedWithKey }}</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="testCrypto">测试加密解密</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h3 class="card-title">ValidateEXTEND - 校验工具</h3>
|
||||
<div class="demo-item">
|
||||
<span class="label">手机号校验:</span>
|
||||
<span class="value">{{ validateResult.phone }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">邮箱校验:</span>
|
||||
<span class="value">{{ validateResult.email }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">身份证校验:</span>
|
||||
<span class="value">{{ validateResult.idCard }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">密码强度校验:</span>
|
||||
<span class="value">{{ validateResult.password }}</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="testValidate">测试校验</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. API服务Demo -->
|
||||
<section class="demo-section">
|
||||
<h2 class="section-title">3. API 服务调用</h2>
|
||||
<div class="demo-card">
|
||||
<h3 class="card-title">UserSERVICE - 用户服务</h3>
|
||||
<div class="demo-item">
|
||||
<span class="label">服务调用方式:</span>
|
||||
<span class="value">const userService = new UserSERVICE()</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">可用方法:</span>
|
||||
<span class="value">login(), getInfo(), logout(), updateInfo(), changePassword()</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="testUserService">测试用户服务</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h3 class="card-title">CommonSERVICE - 通用服务</h3>
|
||||
<div class="demo-item">
|
||||
<span class="label">服务调用方式:</span>
|
||||
<span class="value">const commonService = new CommonSERVICE()</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">可用方法:</span>
|
||||
<span class="value">getDict(), upload(), getConfig(), sendSmsCode()</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="testCommonService">测试通用服务</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Composables Demo -->
|
||||
<section class="demo-section">
|
||||
<h2 class="section-title">4. Composables 组合式函数</h2>
|
||||
<div class="demo-card">
|
||||
<h3 class="card-title">useAuth - 权限校验</h3>
|
||||
<div class="demo-item">
|
||||
<span class="label">是否已登录:</span>
|
||||
<span class="value">{{ authState.isLogin ? '是' : '否' }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">是否为管理员:</span>
|
||||
<span class="value">{{ authState.isAdmin ? '是' : '否' }}</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="testAuth">测试权限校验</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. App状态Demo -->
|
||||
<section class="demo-section">
|
||||
<h2 class="section-title">5. 应用状态管理</h2>
|
||||
<div class="demo-card">
|
||||
<div class="demo-item">
|
||||
<span class="label">当前主题:</span>
|
||||
<span class="value">{{ appStore.theme }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">当前语言:</span>
|
||||
<span class="value">{{ appStore.locale }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">设备类型:</span>
|
||||
<span class="value">{{ appStore.device }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">屏幕尺寸:</span>
|
||||
<span class="value">{{ appStore.screenWidth }} x {{ appStore.screenHeight }}</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="toggleTheme">切换主题</button>
|
||||
<button class="btn" @click="setLoading">测试加载态</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. 路由Demo -->
|
||||
<section class="demo-section">
|
||||
<h2 class="section-title">6. 路由导航</h2>
|
||||
<div class="demo-card">
|
||||
<div class="demo-item">
|
||||
<span class="label">当前路径:</span>
|
||||
<span class="value">{{ route.path }}</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<NuxtLink to="/auth/login" class="btn btn-primary">跳转到登录页</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<template>
|
||||
<div class="page-home"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// stores、composables、extends、services 目录下的内容已自动导入
|
||||
// 无需手动import,直接使用即可
|
||||
|
||||
// 使用Pinia状态仓库(自动导入)
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 使用Composables
|
||||
const auth = useAuth()
|
||||
|
||||
// 路由
|
||||
const route = useRoute()
|
||||
|
||||
// DateEXTEND Demo
|
||||
// 使用客户端时间,避免SSR水合问题
|
||||
const currentTimestamp = ref(0)
|
||||
const formattedTime = ref('')
|
||||
const smartTime = ref('')
|
||||
const isToday = ref(false)
|
||||
|
||||
// 在客户端初始化时间
|
||||
if (import.meta.client) {
|
||||
currentTimestamp.value = DateEXTEND.now()
|
||||
formattedTime.value = DateEXTEND.format(Date.now())
|
||||
smartTime.value = DateEXTEND.smartFormat(Date.now() - 3600000)
|
||||
isToday.value = DateEXTEND.isToday(Date.now())
|
||||
}
|
||||
|
||||
// CryptoEXTEND Demo
|
||||
const originalText = ref('Hello Kx Framework')
|
||||
const encryptedBase64 = ref('')
|
||||
const encryptedWithKey = ref('')
|
||||
const decryptedWithKey = ref('')
|
||||
|
||||
// ValidateEXTEND Demo
|
||||
const validateResult = ref({
|
||||
phone: '',
|
||||
email: '',
|
||||
idCard: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
// Auth Demo
|
||||
const authState = computed(() => ({
|
||||
isLogin: auth.isLogin.value,
|
||||
isAdmin: auth.isAdmin()
|
||||
}))
|
||||
|
||||
// 测试设置用户信息
|
||||
const setUserInfo = () => {
|
||||
userStore.setUserInfo(
|
||||
{
|
||||
id: 1001,
|
||||
username: 'admin',
|
||||
nickname: '管理员',
|
||||
role: 'admin'
|
||||
},
|
||||
'demo-token-12345'
|
||||
)
|
||||
}
|
||||
|
||||
// 测试清除用户信息
|
||||
const clearUserInfo = () => {
|
||||
userStore.clearUserInfo()
|
||||
}
|
||||
|
||||
// 测试加密解密
|
||||
const testCrypto = () => {
|
||||
// Base64加密
|
||||
const crypto = new CryptoEXTEND()
|
||||
encryptedBase64.value = crypto.encryptBase64(originalText.value)
|
||||
|
||||
// 自定义密钥加密
|
||||
const customCrypto = new CryptoEXTEND('my-secret-key')
|
||||
encryptedWithKey.value = customCrypto.encrypt(originalText.value)
|
||||
decryptedWithKey.value = customCrypto.decrypt(encryptedWithKey.value)
|
||||
}
|
||||
|
||||
// 测试校验工具
|
||||
const testValidate = () => {
|
||||
validateResult.value = {
|
||||
phone: ValidateEXTEND.isPhone('13800138000') ? '有效' : '无效',
|
||||
email: ValidateEXTEND.isEmail('test@example.com') ? '有效' : '无效',
|
||||
idCard: ValidateEXTEND.isIdCard('110101199001011234') ? '有效' : '无效',
|
||||
password: ValidateEXTEND.isPassword('Aa123456') ? '有效' : '无效'
|
||||
}
|
||||
}
|
||||
|
||||
// 测试用户服务
|
||||
const testUserService = () => {
|
||||
const userService = new UserSERVICE()
|
||||
console.log('UserSERVICE 实例创建成功:', userService)
|
||||
console.log('可用方法: login(), getInfo(), logout(), updateInfo(), changePassword()')
|
||||
alert('UserSERVICE 实例已创建,请查看控制台输出')
|
||||
}
|
||||
|
||||
// 测试通用服务
|
||||
const testCommonService = () => {
|
||||
const commonService = new CommonSERVICE()
|
||||
console.log('CommonSERVICE 实例创建成功:', commonService)
|
||||
console.log('可用方法: getDict(), upload(), getConfig(), sendSmsCode()')
|
||||
alert('CommonSERVICE 实例已创建,请查看控制台输出')
|
||||
}
|
||||
|
||||
// 测试权限校验
|
||||
const testAuth = () => {
|
||||
console.log('isLogin:', auth.isLogin.value)
|
||||
console.log('userId:', auth.getUserId())
|
||||
console.log('nickname:', auth.getNickname())
|
||||
console.log('hasRole(admin):', auth.hasRole('admin'))
|
||||
console.log('isAdmin:', auth.isAdmin())
|
||||
alert('权限校验测试完成,请查看控制台输出')
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
const toggleTheme = () => {
|
||||
appStore.toggleTheme()
|
||||
}
|
||||
|
||||
// 测试加载态
|
||||
const setLoading = () => {
|
||||
appStore.startLoading('正在加载数据...')
|
||||
setTimeout(() => {
|
||||
appStore.endLoading()
|
||||
alert('加载完成')
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// 初始化测试
|
||||
onMounted(() => {
|
||||
testCrypto()
|
||||
testValidate()
|
||||
definePageMeta({
|
||||
layout: layout.default
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-home {
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 15px;
|
||||
padding-left: 10px;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background-color: var(--bg-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: var(--box-shadow-light);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color-lighter);
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.demo-item .label {
|
||||
width: 160px;
|
||||
color: var(--text-color-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.demo-item .value {
|
||||
color: var(--text-color);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
background-color: var(--bg-color-page);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--danger-color);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -5,9 +5,5 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
router.replace('/home')
|
||||
})
|
||||
</script>
|
||||
await navigateTo('/auth/login', { replace: true })
|
||||
</script>
|
||||
|
||||
@@ -1,148 +1,69 @@
|
||||
/**
|
||||
* 应用状态仓库
|
||||
* 管理全局应用状态,如主题、加载态、配置等
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
// 主题类型
|
||||
export type ThemeMode = 'light' | 'dark' | 'auto'
|
||||
|
||||
// 语言类型
|
||||
export type Locale = 'zh-CN' | 'en-US'
|
||||
|
||||
// 应用配置
|
||||
export interface IAppConfig {
|
||||
theme: ThemeMode
|
||||
locale: Locale
|
||||
sidebarCollapsed: boolean
|
||||
showDebug: boolean
|
||||
}
|
||||
|
||||
// localStorage key
|
||||
const STORAGE_KEY = 'app-config-store'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
// 1. 原始状态
|
||||
state: () => ({
|
||||
// 主题模式
|
||||
theme: 'light' as ThemeMode,
|
||||
// 当前语言
|
||||
locale: 'zh-CN' as Locale,
|
||||
// 侧边栏是否收起
|
||||
sidebarCollapsed: false,
|
||||
// 是否显示调试信息
|
||||
showDebug: false,
|
||||
// 全局加载态
|
||||
isLoading: false,
|
||||
// 加载提示文字
|
||||
loadingText: '',
|
||||
// 设备类型
|
||||
device: 'desktop' as 'desktop' | 'mobile' | 'tablet',
|
||||
// 浏览器是否在线
|
||||
isOnline: true,
|
||||
// 屏幕宽度
|
||||
screenWidth: 0,
|
||||
// 屏幕高度
|
||||
screenHeight: 0
|
||||
}),
|
||||
|
||||
// 2. 只读计算属性
|
||||
getters: {
|
||||
// 判断是否为浅色主题
|
||||
isLightTheme: (state) => state.theme === 'light',
|
||||
|
||||
// 判断是否为深色主题
|
||||
isDarkTheme: (state) => state.theme === 'dark',
|
||||
|
||||
// 判断是否为移动端
|
||||
isMobile: (state) => state.device === 'mobile',
|
||||
|
||||
// 判断是否为平板
|
||||
isTablet: (state) => state.device === 'tablet',
|
||||
|
||||
// 判断是否为桌面端
|
||||
isDesktop: (state) => state.device === 'desktop',
|
||||
|
||||
// 获取主题class
|
||||
themeClass: (state) => `theme-${state.theme}`,
|
||||
|
||||
// 判断是否有加载态
|
||||
hasLoading: (state) => state.isLoading
|
||||
},
|
||||
|
||||
// 3. 状态修改入口
|
||||
actions: {
|
||||
// 设置主题
|
||||
setTheme(theme: ThemeMode) {
|
||||
this.theme = theme
|
||||
this.syncToLocalStorage()
|
||||
// 应用主题到html元素
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
}
|
||||
},
|
||||
|
||||
// 切换主题
|
||||
toggleTheme() {
|
||||
this.theme = this.theme === 'light' ? 'dark' : 'light'
|
||||
this.syncToLocalStorage()
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-theme', this.theme)
|
||||
}
|
||||
},
|
||||
|
||||
// 设置语言
|
||||
setLocale(locale: Locale) {
|
||||
this.locale = locale
|
||||
this.syncToLocalStorage()
|
||||
},
|
||||
|
||||
// 切换侧边栏收起状态
|
||||
toggleSidebar() {
|
||||
this.sidebarCollapsed = !this.sidebarCollapsed
|
||||
this.syncToLocalStorage()
|
||||
},
|
||||
|
||||
// 设置侧边栏收起状态
|
||||
setSidebarCollapsed(collapsed: boolean) {
|
||||
this.sidebarCollapsed = collapsed
|
||||
this.syncToLocalStorage()
|
||||
},
|
||||
|
||||
// 设置调试模式
|
||||
setShowDebug(show: boolean) {
|
||||
this.showDebug = show
|
||||
this.syncToLocalStorage()
|
||||
},
|
||||
|
||||
// 开始加载
|
||||
startLoading(text: string = '加载中...') {
|
||||
this.isLoading = true
|
||||
this.loadingText = text
|
||||
},
|
||||
|
||||
// 结束加载
|
||||
endLoading() {
|
||||
this.isLoading = false
|
||||
this.loadingText = ''
|
||||
},
|
||||
|
||||
// 设置设备类型
|
||||
setDevice(device: 'desktop' | 'mobile' | 'tablet') {
|
||||
this.device = device
|
||||
},
|
||||
|
||||
// 设置网络状态
|
||||
setOnlineStatus(isOnline: boolean) {
|
||||
this.isOnline = isOnline
|
||||
},
|
||||
|
||||
// 更新屏幕尺寸
|
||||
updateScreenSize(width: number, height: number) {
|
||||
this.screenWidth = width
|
||||
this.screenHeight = height
|
||||
|
||||
// 根据宽度自动判断设备类型
|
||||
if (width < 768) {
|
||||
this.device = 'mobile'
|
||||
} else if (width < 1024) {
|
||||
@@ -152,18 +73,12 @@ export const useAppStore = defineStore('app', {
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化应用配置
|
||||
initConfig(config: Partial<IAppConfig>) {
|
||||
if (config.theme) this.setTheme(config.theme)
|
||||
if (config.locale) this.setLocale(config.locale)
|
||||
if (config.sidebarCollapsed !== undefined) this.setSidebarCollapsed(config.sidebarCollapsed)
|
||||
if (config.showDebug !== undefined) this.setShowDebug(config.showDebug)
|
||||
},
|
||||
|
||||
// 重置所有状态
|
||||
reset() {
|
||||
this.theme = 'light'
|
||||
this.locale = 'zh-CN'
|
||||
this.sidebarCollapsed = false
|
||||
this.showDebug = false
|
||||
this.isLoading = false
|
||||
@@ -171,12 +86,9 @@ export const useAppStore = defineStore('app', {
|
||||
this.clearLocalStorage()
|
||||
},
|
||||
|
||||
// 同步到localStorage
|
||||
syncToLocalStorage() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const data = {
|
||||
theme: this.theme,
|
||||
locale: this.locale,
|
||||
sidebarCollapsed: this.sidebarCollapsed,
|
||||
showDebug: this.showDebug
|
||||
}
|
||||
@@ -184,21 +96,14 @@ export const useAppStore = defineStore('app', {
|
||||
}
|
||||
},
|
||||
|
||||
// 从localStorage恢复
|
||||
restoreFromLocalStorage() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
try {
|
||||
const data = JSON.parse(stored)
|
||||
this.theme = data.theme || 'light'
|
||||
this.locale = data.locale || 'zh-CN'
|
||||
this.sidebarCollapsed = data.sidebarCollapsed || false
|
||||
this.showDebug = data.showDebug || false
|
||||
// 应用主题
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-theme', this.theme)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('恢复应用配置失败:', e)
|
||||
}
|
||||
@@ -206,11 +111,10 @@ export const useAppStore = defineStore('app', {
|
||||
}
|
||||
},
|
||||
|
||||
// 清除localStorage
|
||||
clearLocalStorage() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user