Import local app source
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon3.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>决裂者终端用户中心</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1880
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "my-vue-app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --config vite.config.js",
|
||||
"build": "vite build --config vite.config.js",
|
||||
"preview": "vite preview --config vite.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"naive-ui": "^2.43.2",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 60 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,21 @@
|
||||
<!-- src/App.vue -->
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 确保全屏撑开 */
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup>
|
||||
import LoginView from './views/LoginView.vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
async function handleLogin() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/method/login', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ usr: email.value, pwd: password.value })
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.message || 'Invalid credentials')
|
||||
}
|
||||
|
||||
// 登录成功!跳转到 Frappe 应用
|
||||
window.location.href = 'http://172.25.162.172:8000/app' // 或你的目标页面
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100vh; background: #0f0f1b; color: white;">
|
||||
<div style="background: #1a1a2e; padding: 2rem; border-radius: 10px; width: 300px; box-shadow: 0 4px 20px rgba(0,0,0,0.5);">
|
||||
<h2 style="text-align: center; margin-bottom: 1.5rem;">Sunderer Login</h2>
|
||||
<form @submit.prevent="handleLogin">
|
||||
<input
|
||||
v-model="email"
|
||||
type="text"
|
||||
placeholder="Email or Username"
|
||||
style="width: 100%; padding: 0.5rem; margin: 0.5rem 0; border: 1px solid #444; background: #0f0f1b; color: white; border-radius: 4px;"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
style="width: 100%; padding: 0.5rem; margin: 0.5rem 0; border: 1px solid #444; background: #0f0f1b; color: white; border-radius: 4px;"
|
||||
required
|
||||
/>
|
||||
<div v-if="error" style="color: #ff6b6b; margin-top: 0.5rem; font-size: 0.9rem;">
|
||||
{{ error }}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
style="width: 100%; padding: 0.5rem; background: #4e54c8; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem;"
|
||||
>
|
||||
{{ loading ? 'Signing in...' : 'Sign In' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
// src/increaseacc.js
|
||||
import { createApp } from 'vue'
|
||||
import IncreaseAccView from './views/IncreaseAccView.vue'
|
||||
|
||||
const app = createApp(IncreaseAccView)
|
||||
|
||||
// 挂载到 DOM
|
||||
app.mount('#app')
|
||||
|
||||
// 导出 app 实例,以便 UMD 格式可以访问
|
||||
// 如果指定了 name: 'IncreaseAccApp',这个 app 实例将可通过 window.IncreaseAccApp 访问
|
||||
export default app
|
||||
@@ -0,0 +1,141 @@
|
||||
<!-- src/layouts/AppLayout.vue -->
|
||||
<template>
|
||||
<n-layout position="absolute">
|
||||
<!-- 顶部栏 -->
|
||||
<n-layout-header class="header" bordered>
|
||||
<div class="header-content">
|
||||
<h1>My Admin Panel</h1>
|
||||
<div class="header-actions">
|
||||
<n-dropdown trigger="click" :options="profileOptions" @select="handleProfileSelect">
|
||||
<n-button text>
|
||||
<n-avatar round size="small" :src="userAvatar" />
|
||||
{{ userName }}
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</n-layout-header>
|
||||
|
||||
<n-layout has-sider position="absolute" style="top: 64px;">
|
||||
<!-- 侧边栏 -->
|
||||
<n-layout-sider
|
||||
bordered
|
||||
collapse-mode="width"
|
||||
:collapsed-width="64"
|
||||
:width="240"
|
||||
:native-scrollbar="false"
|
||||
show-trigger
|
||||
:collapsed="sCollapsed"
|
||||
@collapse="sCollapsed = !sCollapsed"
|
||||
@expand="sCollapsed = !sCollapsed"
|
||||
>
|
||||
<n-menu
|
||||
v-model:value="activeKey"
|
||||
:collapsed="sCollapsed"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
:indent="18"
|
||||
/>
|
||||
</n-layout-sider>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<n-layout content-style="padding: 24px;">
|
||||
<router-view /> <!-- 这里是后台各页面的渲染位置 -->
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NIcon, NText } from 'naive-ui'
|
||||
import { BookOutline as BookIcon, PersonOutline as PersonIcon, LogOutOutline as LogoutIcon } from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const sCollapsed = ref(false)
|
||||
const activeKey = ref('dashboard')
|
||||
|
||||
// 示例用户信息
|
||||
const userName = ref('Admin User')
|
||||
const userAvatar = ref('https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg')
|
||||
|
||||
function renderIcon(icon) {
|
||||
return () => h(NIcon, null, { default: () => h(icon) })
|
||||
}
|
||||
|
||||
function renderMenuLabel(option) {
|
||||
if (sCollapsed.value) {
|
||||
return h(NIcon, null, { default: () => h(option.icon) });
|
||||
} else {
|
||||
return h('span', null, option.label);
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏菜单
|
||||
const menuOptions = [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
key: 'dashboard',
|
||||
icon: renderIcon(BookIcon),
|
||||
props: { onClick: () => router.push('/app/dashboard') }
|
||||
},
|
||||
{
|
||||
label: 'Users',
|
||||
key: 'users',
|
||||
icon: renderIcon(PersonIcon),
|
||||
props: { onClick: () => router.push('/app/users') }
|
||||
},
|
||||
// 添加更多菜单项...
|
||||
]
|
||||
|
||||
// 顶栏用户菜单
|
||||
const profileOptions = [
|
||||
{ label: 'Profile', key: 'profile' },
|
||||
{ label: 'Settings', key: 'settings' },
|
||||
{ type: 'divider', key: 'd1' },
|
||||
{ label: 'Logout', key: 'logout', icon: renderIcon(LogoutIcon) }
|
||||
]
|
||||
|
||||
// 顶栏菜单选择处理
|
||||
const handleProfileSelect = (key) => {
|
||||
switch (key) {
|
||||
case 'profile':
|
||||
console.log('Go to Profile');
|
||||
break;
|
||||
case 'settings':
|
||||
console.log('Go to Settings');
|
||||
break;
|
||||
case 'logout':
|
||||
// 执行登出逻辑,例如清除 token
|
||||
localStorage.removeItem('authToken'); // 示例
|
||||
router.push('/login'); // 跳转回登录页
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-actions .n-button .n-avatar {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
// src/main.js
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router' // 确保这个导入是正确的
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router) // 使用路由器插件
|
||||
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,80 @@
|
||||
// C:\vueprj\my-vue-app\src\router.js
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import LoginView from './views/LoginView.vue'
|
||||
import RegisterView from './views/RegisterView.vue'
|
||||
import CustomPasswordSetup from './views/CustomPasswordSetup.vue'
|
||||
|
||||
// --- 新增:导入后台布局和页面 ---
|
||||
import AppLayout from './layouts/Applayout.vue' // 导入你创建的后台布局
|
||||
const DashboardView = () => import('./views/sundererapp/DashboardView.vue') // 动态导入后台页面
|
||||
const UsersView = () => import('./views/sundererapp/UsersView.vue') // 动态导入后台页面
|
||||
// --- 新增结束 ---
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: LoginView
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: LoginView
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: RegisterView
|
||||
},
|
||||
{
|
||||
path: '/update-password',
|
||||
name: 'CustomPasswordSetup',
|
||||
component: CustomPasswordSetup
|
||||
},
|
||||
{
|
||||
path: '/custom-update-password',
|
||||
name: 'CustomPasswordSetupAlt',
|
||||
component: CustomPasswordSetup
|
||||
},
|
||||
// --- 新增:后台路由组 ---
|
||||
{
|
||||
path: '/sundererapp', // 后台页面的基路径
|
||||
component: AppLayout, // 使用 AppLayout 作为父组件
|
||||
meta: { requiresAuth: true }, // 可选:标记此路由组需要认证
|
||||
children: [ // 定义后台页面的子路由
|
||||
{
|
||||
path: '', // /sundererapp 访问时,默认重定向到 dashboard
|
||||
redirect: '/sundererapp/dashboard'
|
||||
},
|
||||
{
|
||||
path: 'dashboard', // 最终路径为 /sundererapp/dashboard
|
||||
name: 'AppDashboard',
|
||||
component: DashboardView // 渲染 DashboardView 组件
|
||||
},
|
||||
{
|
||||
path: 'users', // 最终路径为 /sundererapp/users
|
||||
name: 'AppUsers',
|
||||
component: UsersView // 渲染 UsersView 组件
|
||||
},
|
||||
// 你可以在这里添加更多后台页面,例如:
|
||||
// {
|
||||
// path: 'settings',
|
||||
// name: 'AppSettings',
|
||||
// component: () => import('./views/sundererapp/SettingsView.vue')
|
||||
// }
|
||||
]
|
||||
},
|
||||
// --- 新增结束 ---
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: LoginView
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// src/main.js (或 src/sundererapp.js)
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue' // 你可能有一个顶层的 App.vue,或者可以直接创建一个简单的根组件
|
||||
import router from './router.js' // 导入你配置好的路由
|
||||
|
||||
// 如果你的 App.vue 是一个空壳,只是为了挂载 router-view
|
||||
// 你也可以不导入 App.vue,直接创建一个根组件
|
||||
// const RootComponent = {
|
||||
// template: '<router-view />'
|
||||
// }
|
||||
// const app = createApp(RootComponent);
|
||||
|
||||
const app = createApp(App); // 创建 Vue 应用实例
|
||||
|
||||
app.use(router) // 安装 Vue Router 插件
|
||||
|
||||
app.mount('#app') // 挂载到 DOM
|
||||
|
||||
// 导出 app 实例,以便 UMD 格式可以访问
|
||||
// 如果 vite.config.js 中指定了 name: 'SundererApp',这个 app 实例将可通过 window.SundererApp 访问
|
||||
export default app
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="password-setup-container">
|
||||
<div class="setup-card">
|
||||
<h2>设置密码</h2>
|
||||
|
||||
<form @submit.prevent="setupPassword">
|
||||
<div class="input-group">
|
||||
<label>新密码</label>
|
||||
<input
|
||||
v-model="newPassword"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
class="input-field"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>确认密码</label>
|
||||
<input
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
placeholder="请再次输入密码"
|
||||
class="input-field"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="btn-primary"
|
||||
>
|
||||
{{ loading ? '设置中...' : '设置密码' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const setupPassword = async () => {
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
error.value = '两次输入的密码不一致'
|
||||
return
|
||||
}
|
||||
|
||||
if (newPassword.value.length < 6) {
|
||||
error.value = '密码长度至少为6位'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const actualKey = route.query.key
|
||||
if (!actualKey) {
|
||||
error.value = '无效的密码重置链接'
|
||||
return
|
||||
}
|
||||
|
||||
const response = await fetch('http://172.25.162.172:8000/api/method/frappe.core.doctype.user.user.update_password', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
key: actualKey,
|
||||
new_password: newPassword.value
|
||||
})
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.exc) {
|
||||
error.value = result.exc || '设置密码失败'
|
||||
} else {
|
||||
alert('密码设置成功!')
|
||||
router.push('/login')
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = '网络错误,请稍后再试'
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.password-setup-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #00AFFF, #0077CC);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.setup-card {
|
||||
width: 400px;
|
||||
padding: 30px;
|
||||
background: rgba(0, 0, 0, 0.295);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 30px rgba(0, 175, 255, 0.4);
|
||||
border: 1px solid rgba(0, 175, 255, 0.3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 94%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: rgb(255, 255, 255);
|
||||
color: rgb(0, 0, 0);
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 0 0 1px rgba(0, 175, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff6b6b;
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<h2>设置您的密码</h2>
|
||||
<form @submit.prevent="setupPassword" class="register-view">
|
||||
<div class="form-group">
|
||||
<input type="password" id="password" v-model="password" required placeholder="请输入新密码" class="input-field"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" id="confirmPassword" v-model="confirmPassword" required placeholder="请确认新密码" class="input-field"/>
|
||||
</div>
|
||||
<button type="submit" :disabled="loading" class="btn-primary">{{ loading ? '正在设置...' : '设置密码' }}</button>
|
||||
<!-- 用于显示普通错误信息 -->
|
||||
<div v-if="message" :class="{ 'success-message': success, 'error-message': !success }">{{ message }}</div>
|
||||
<!-- 专门用于显示密码强度错误信息 -->
|
||||
<div v-if="passwordStrengthMessage" class="password-strength-error">
|
||||
{{ passwordStrengthMessage }}
|
||||
</div>
|
||||
</form>
|
||||
<div class="back-to-login">
|
||||
<button @click="goBackToLogin" class="btn-secondary">← 返回登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'IncreaseAcc',
|
||||
data() {
|
||||
return {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
message: '', // 用于存储一般性错误或成功信息
|
||||
passwordStrengthMessage: '', // 专门用于存储密码强度错误信息
|
||||
success: false,
|
||||
loading: false,
|
||||
key: null // 存储从 URL 获取的 key
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.key = urlParams.get('key');
|
||||
console.log("从 URL 获取的 key:", this.key);
|
||||
|
||||
if (!this.key) {
|
||||
this.message = '未找到密钥。';
|
||||
this.success = false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async setupPassword() {
|
||||
// 清空之前的消息
|
||||
this.message = '';
|
||||
this.passwordStrengthMessage = '';
|
||||
this.success = false;
|
||||
|
||||
if (this.password !== this.confirmPassword) {
|
||||
this.message = '两次输入的密码不匹配。';
|
||||
this.success = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.key) {
|
||||
this.message = '缺少密钥参数。';
|
||||
this.success = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await fetch('/api/method/sunderer_app.api.custom_user_signup.complete_account', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({key: this.key, password: this.password})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log("API 响应数据:", data);
|
||||
|
||||
if (response.ok) {
|
||||
// 成功情况
|
||||
this.message = "账户已激活,密码设置成功!";
|
||||
this.success = true;
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = 'http://172.25.162.172:8000/';
|
||||
}, 5000); // 5秒后跳转
|
||||
|
||||
} else {
|
||||
// --- 修改错误处理逻辑 ---
|
||||
let errorMessage = '操作失败,请稍后再试。';
|
||||
let isPasswordStrengthError = false; // 标记是否是密码强度错误
|
||||
|
||||
if (data && data.exc) {
|
||||
// 尝试解析 frappe.throw 返回的 exc 信息
|
||||
try {
|
||||
// Frappe 可能会将多个错误信息包装在一个字符串中,或者直接返回一个对象
|
||||
let parsedExc = data.exc;
|
||||
if (typeof data.exc === 'string') {
|
||||
// 尝试解析为 JSON 对象(如果后端返回了 JSON 结构的 exc)
|
||||
parsedExc = JSON.parse(data.exc);
|
||||
}
|
||||
|
||||
// 检查 parsedExc 是否是我们之前在后端构造的密码强度错误对象
|
||||
if (parsedExc && parsedExc.type === 'password_strength') {
|
||||
// 是密码强度错误
|
||||
isPasswordStrengthError = true;
|
||||
// 构建更友好的提示信息
|
||||
const details = parsedExc.details;
|
||||
const warning = details.warning || "";
|
||||
const suggestions = details.suggestions || [];
|
||||
|
||||
const parts = [];
|
||||
if (warning) parts.push(warning);
|
||||
if (suggestions.length > 0) parts.push(`建议: ${suggestions.join(', ')}`);
|
||||
|
||||
this.passwordStrengthMessage = parts.join(' ');
|
||||
} else {
|
||||
// 不是密码强度错误,处理其他 exc
|
||||
errorMessage = parsedExc._server_messages || parsedExc.message || data._server_messages || '未知错误';
|
||||
this.message = errorMessage;
|
||||
}
|
||||
} catch (parseError) {
|
||||
// 如果 exc 不是 JSON 格式,或者解析失败,则直接显示 exc 内容
|
||||
console.error("解析后端错误信息失败:", parseError);
|
||||
// 检查 exc 字符串是否包含密码强度相关的关键词
|
||||
if (typeof data.exc === 'string' &&
|
||||
(data.exc.includes("密码强度") || data.exc.includes("password") || data.exc.includes("strength") || data.exc.includes("重复"))) {
|
||||
isPasswordStrengthError = true;
|
||||
// 尝试从原始字符串中提取一些信息,或者给出通用提示
|
||||
// 这里可以根据实际返回的字符串进行更精细的解析
|
||||
this.passwordStrengthMessage = "密码强度不足,请设置一个更强的密码。";
|
||||
} else {
|
||||
this.message = data.exc || errorMessage;
|
||||
}
|
||||
}
|
||||
} else if (data && data.message) {
|
||||
// 检查 data.message 是否是密码强度错误对象
|
||||
if (typeof data.message === 'object' && data.message.type === 'password_strength') {
|
||||
isPasswordStrengthError = true;
|
||||
const details = data.message.details;
|
||||
const warning = details.warning || "";
|
||||
const suggestions = details.suggestions || [];
|
||||
|
||||
const parts = [];
|
||||
if (warning) parts.push(warning);
|
||||
if (suggestions.length > 0) parts.push(`建议: ${suggestions.join(', ')}`);
|
||||
|
||||
this.passwordStrengthMessage = parts.join(' ');
|
||||
} else {
|
||||
// 不是密码强度错误,处理其他 message
|
||||
errorMessage = typeof data.message === 'object' ? JSON.stringify(data.message) : data.message;
|
||||
this.message = errorMessage;
|
||||
}
|
||||
} else {
|
||||
// 如果 data 既没有 exc 也没有 message,可能是网络错误或其他问题
|
||||
this.message = '服务器返回了错误,但没有详细信息。';
|
||||
}
|
||||
|
||||
// 如果不是密码强度错误,则设置 general message
|
||||
if (!isPasswordStrengthError && !this.passwordStrengthMessage) {
|
||||
this.message = errorMessage; // 这行其实可能已经在上面的条件分支中设置了
|
||||
}
|
||||
// --- 修改结束 ---
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("网络错误或请求失败:", error);
|
||||
this.message = '网络错误或请求失败。';
|
||||
this.success = false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
goBackToLogin() {
|
||||
window.location.href = '/login'; // 跳转到登录页
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ... (保持原有的 CSS 不变) ... */
|
||||
|
||||
.register-view {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #00AFFF, #0077CC);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 400px;
|
||||
padding: 30px;
|
||||
background: rgba(0, 0, 0, 0.295);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 30px rgba(0, 175, 255, 0.4);
|
||||
border: 1px solid rgba(0, 175, 255, 0.3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: rgb(255, 255, 255);
|
||||
color: rgb(0, 0, 0);
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 0 0 1px rgba(0, 175, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
margin: 10px 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: green;
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: red;
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 新增:专门用于显示密码强度错误的样式 */
|
||||
.password-strength-error {
|
||||
color: orange; /* 使用橙色区分于红色的一般错误 */
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
font-size: 0.9em; /* 字体稍小一点 */
|
||||
font-style: italic; /* 斜体强调 */
|
||||
}
|
||||
|
||||
.back-to-login {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: linear-gradient(90deg, #6bffce, #c74dff);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<!-- 背景几何图案 -->
|
||||
<div class="bg-pattern"></div>
|
||||
|
||||
<!-- 登录卡 -->
|
||||
<div class="login-card">
|
||||
<div class="logo">
|
||||
<span class="brand">决裂者终端</span>
|
||||
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin">
|
||||
<input
|
||||
v-model="email"
|
||||
type="text"
|
||||
placeholder="请输入账号"
|
||||
required
|
||||
class="input-field"
|
||||
/>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
required
|
||||
class="input-field"
|
||||
/>
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
|
||||
<div class="auth-buttons">
|
||||
<button type="submit" :disabled="loginLoading" class="login-btn">
|
||||
{{ loginLoading ? '正在登录. . .' : '登录' }}
|
||||
</button>
|
||||
<button type="button" @click="goToRegister" :disabled="registerLoading" class="register-btn">
|
||||
{{ registerLoading ? '注册中...' : '注册' }}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<p class="footer">© 2026 Sunderer games. </p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router' // ✅ 确保导入
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const error = ref('')
|
||||
const loginLoading = ref(false)
|
||||
const registerLoading = ref(false)
|
||||
|
||||
|
||||
async function handleLogin() {
|
||||
loginLoading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/method/login', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ usr: email.value, pwd: password.value })
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.message || '账号或密码错误')
|
||||
}
|
||||
|
||||
// 跳转到 Frappe 后台
|
||||
window.location.href = 'http://172.25.162.172:8000/app'
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegister() {
|
||||
|
||||
registerLoading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/method/frappe.core.doctype.user.user.sign_up', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: email.value,
|
||||
full_name: email.value.split('@')[0], // 简单处理,可用其他字段
|
||||
redirect_to: '/app'
|
||||
})
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.message || '注册失败')
|
||||
}
|
||||
|
||||
// 注册成功后,Frappe 会发送验证邮件
|
||||
alert('注册成功!请查收邮箱验证链接。')
|
||||
// 或跳转到提示页
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
} finally {
|
||||
registerLoading.value = false
|
||||
}
|
||||
}
|
||||
function goToRegister() {
|
||||
router.push('/register')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #00AFFF, #0077CC);
|
||||
}
|
||||
|
||||
/* 登录卡片保持原样 */
|
||||
.login-card {
|
||||
background: rgba(0, 0, 0, 0.295);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 20px;
|
||||
padding: 2.5rem;
|
||||
width: 360px;
|
||||
box-shadow: 0 0 30px rgba(0, 175, 255, 0.4);
|
||||
border: 1px solid rgba(0, 175, 255, 0.3);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center; /* 水平居中内容 */
|
||||
align-items: center; /* 垂直居中内容 */
|
||||
min-height: 300px; /* 防止高度塌陷 */
|
||||
}
|
||||
|
||||
/* 其他样式(logo、输入框、按钮等)保持不变... */
|
||||
.logo {
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1.5rem;
|
||||
letter-spacing: 1px;
|
||||
color: white;
|
||||
text-shadow: 0 0 10px rgba(0, 175, 255, 0.8), 0 0 20px rgba(0, 175, 255, 0.6);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.brand {
|
||||
color: white;
|
||||
font-size: 2.2rem;
|
||||
text-shadow: 0 0 10px rgba(0, 175, 255, 0.8), 0 0 20px rgba(0, 175, 255, 0.6);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: #00FFFF;
|
||||
font-size: 1.8rem;
|
||||
margin: 0 0.5rem;
|
||||
text-shadow: 0 0 15px rgba(0, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 80%;
|
||||
padding: 0.8rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: rgb(255, 255, 255);
|
||||
color: rgb(0, 0, 0);
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 0 0 1px rgba(0, 175, 255, 0.3);
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
box-shadow: 0 0 0 2px rgba(0, 175, 255, 0.7), 0 0 10px rgba(0, 175, 255, 0.4);
|
||||
}
|
||||
.error {
|
||||
color: #ff6b6b;
|
||||
font-size: 0.85rem;
|
||||
margin: -0.5rem 0 1rem;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 70%;
|
||||
padding: 0.8rem;
|
||||
background: linear-gradient(90deg, #00AFFF, #0077CC);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 0 15px rgba(0, 175, 255, 0.5);
|
||||
}
|
||||
.register-btn {
|
||||
width: 70%;
|
||||
padding: 0.8rem;
|
||||
background: linear-gradient(90deg, #6bffce, #c74dff);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 0 15px rgba(255, 107, 107, 0.5);
|
||||
}
|
||||
.login-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0 20px rgba(0, 175, 255, 0.7);
|
||||
}
|
||||
|
||||
.login-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.auth-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
.register-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0 20px rgba(255, 107, 107, 0.7);
|
||||
}
|
||||
|
||||
.register-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="register-view">
|
||||
<div class="container">
|
||||
<h2>用户注册</h2>
|
||||
|
||||
<div v-if="step === 'email'" class="step-email">
|
||||
<input v-model="email" type="email" placeholder="请输入邮箱地址" class="input-field" />
|
||||
<button @click="sendCode" :disabled="sending" class="btn-primary">
|
||||
{{ sending ? '发送中...' : '发送验证码' }}
|
||||
</button>
|
||||
<p v-if="error" class="error-msg">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="step === 'verify'" class="step-verify">
|
||||
<input v-model="code" type="text" placeholder="请输入6位验证码" maxlength="6" class="input-field" />
|
||||
<button @click="verifyAndSignup" class="btn-primary">
|
||||
完成注册
|
||||
</button>
|
||||
<p v-if="error" class="error-msg">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 添加返回登录按钮 -->
|
||||
<div class="back-to-login">
|
||||
<button @click="goBackToLogin" class="btn-secondary">
|
||||
← 返回登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router' // 引入路由
|
||||
|
||||
const router = useRouter() // 获取路由实例
|
||||
const email = ref('')
|
||||
const code = ref('')
|
||||
const step = ref('email')
|
||||
const error = ref('')
|
||||
const sending = ref(false)
|
||||
|
||||
const validateEmail = (email) => {
|
||||
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
return regex.test(email)
|
||||
}
|
||||
|
||||
const sendCode = async () => {
|
||||
if (!validateEmail(email.value)) {
|
||||
error.value = '请输入有效的邮箱地址!'
|
||||
return
|
||||
}
|
||||
|
||||
sending.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const response = await fetch('/api/method/sunderer_app.api.verification.send_signup_code', { // 使用相对路径
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email.value
|
||||
})
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.exc) {
|
||||
error.value = result.exc
|
||||
} else {
|
||||
step.value = 'verify'
|
||||
console.log('验证码发送成功')
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = '网络错误,请稍后再试'
|
||||
console.error(err)
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const verifyAndSignup = async () => {
|
||||
console.log("DEBUG: verifyAndSignup function called");
|
||||
console.log('点击完成注册')
|
||||
console.log('email:', email.value)
|
||||
console.log('code:', code.value)
|
||||
|
||||
if (code.value.length !== 6) {
|
||||
error.value = '请输入6位验证码!'
|
||||
console.log("DEBUG: Code length check failed");
|
||||
return
|
||||
}
|
||||
console.log("DEBUG: Code length check passed");
|
||||
|
||||
try {
|
||||
// 1. 验证验证码
|
||||
const verifyResponse = await fetch('/api/method/sunderer_app.api.verification.verify_signup_code', { // 使用相对路径
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email.value,
|
||||
code: code.value
|
||||
})
|
||||
})
|
||||
|
||||
const verifyResult = await verifyResponse.json()
|
||||
console.log("DEBUG: Verify API Response:", verifyResult);
|
||||
|
||||
if (verifyResult.exc) {
|
||||
error.value = verifyResult.exc
|
||||
console.log("DEBUG: Verify API returned error:", verifyResult.exc);
|
||||
return
|
||||
}
|
||||
console.log("DEBUG: Verify API succeeded, proceeding to trigger email...");
|
||||
|
||||
// 2. 验证成功后,调用新API发送包含 IncreaseAcc 链接的邮件
|
||||
// 注意:你需要在后端创建一个新API来处理这个逻辑
|
||||
const triggerEmailResponse = await fetch('/api/method/sunderer_app.api.custom_user_signup.trigger_account_setup_email', { // 新API路径
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email.value
|
||||
})
|
||||
});
|
||||
|
||||
const triggerEmailResult = await triggerEmailResponse.json();
|
||||
console.log("DEBUG: Trigger Email API Response:", triggerEmailResult);
|
||||
|
||||
if (triggerEmailResult.exc) {
|
||||
error.value = triggerEmailResult.exc;
|
||||
console.log("DEBUG: Trigger Email API returned error:", triggerEmailResult.exc);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 提示用户并结束当前流程
|
||||
alert('验证成功!请查收邮箱中的注册链接,点击链接完成账户设置。');
|
||||
console.log('Verification successful, email sent with link.');
|
||||
// 可以选择清空输入框或跳转到等待页面,这里简单提示后清空
|
||||
email.value = '';
|
||||
code.value = '';
|
||||
step.value = 'email'; // 回到初始状态,或者可以设计一个等待页面
|
||||
error.value = ''; // 清除可能存在的旧错误
|
||||
|
||||
} catch (err) {
|
||||
error.value = '网络异常,请稍后再试';
|
||||
console.error('注册异常:', err);
|
||||
}
|
||||
}
|
||||
|
||||
const goBackToLogin = () => {
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ... CSS 样式保持不变 ... */
|
||||
.register-view {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #00AFFF, #0077CC);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 400px;
|
||||
padding: 30px;
|
||||
background: rgba(0, 0, 0, 0.295);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 30px rgba(0, 175, 255, 0.4);
|
||||
border: 1px solid rgba(0, 175, 255, 0.3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 90%;
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: rgb(255, 255, 255);
|
||||
color: rgb(0, 0, 0);
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 0 0 1px rgba(0, 175, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
margin: 10px 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: linear-gradient(90deg, #6bffce, #c74dff);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
color: #ff6b6b;
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.back-to-login {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
<!-- src/views/app/DashboardView.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<n-h2>Dashboard</n-h2>
|
||||
<p>Welcome to your admin dashboard!</p>
|
||||
<n-card title="Quick Stats" style="margin-top: 20px;">
|
||||
<n-statistic label="Total Users" value="12345" />
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// Dashboard 相关逻辑
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
<!-- src/views/app/UsersView.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<n-h2>Users Management</n-h2>
|
||||
<p>This is the users management page.</p>
|
||||
<n-data-table :columns="columns" :data="data" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { h } from 'vue'
|
||||
import { NTag, NButton } from 'naive-ui'
|
||||
|
||||
// 示例数据
|
||||
const columns = [
|
||||
{ title: 'Name', key: 'name' },
|
||||
{ title: 'Age', key: 'age' },
|
||||
{ title: 'Tags', key: 'tags', render: (row) => row.tags.map(tag => h(NTag, { type: 'info' }, { default: () => tag })) },
|
||||
{ title: 'Actions', key: 'actions', render: () => h(NButton, { size: 'small', type: 'primary' }, { default: () => 'Edit' }) }
|
||||
]
|
||||
|
||||
const data = [
|
||||
{ name: 'John Doe', age: 32, tags: ['admin', 'dev'] },
|
||||
{ name: 'Jane Smith', age: 28, tags: ['user'] }
|
||||
]
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
// vite.dev.config.js
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
base: '/assets/sunderer_app/dist/', // 开发环境使用根路径
|
||||
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: '../public/dist',
|
||||
emptyOutDir: true,
|
||||
assetsDir: '',
|
||||
manifest: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'js/[name].[hash].js',
|
||||
chunkFileNames: 'js/[name].[hash].js',
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (assetInfo.name.endsWith('.css')) {
|
||||
return 'css/[name].[hash].[ext]';
|
||||
}
|
||||
return 'assets/[name].[hash].[ext]';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
// 导入 Naive UI 的解析器,告诉插件哪些组件属于 Naive UI
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
{
|
||||
// 配置自动导入 Naive UI 的特定 API
|
||||
'naive-ui': [
|
||||
'useMessage', // 用于显示消息提示
|
||||
'useDialog', // 用于显示对话框
|
||||
'useNotification', // 用于显示通知
|
||||
'useLoadingBar' // 用于显示顶部加载条
|
||||
// 你可以根据需要添加更多,例如 'NButton', 'NCard' 等,
|
||||
// 但通常推荐让 Components 插件处理组件导入,这里只放 API。
|
||||
]
|
||||
}
|
||||
]
|
||||
}),
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver()]
|
||||
})
|
||||
],
|
||||
build: {
|
||||
outDir: '../public/SundererPage',
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
sundererapp: 'src/sundererapp.js' // 入口文件
|
||||
},
|
||||
output: {
|
||||
format: 'umd', // 设置输出格式为 UMD
|
||||
name: 'SundererApp', // 指定全局变量名,构建后应用会挂载到 window.IncreaseAccApp
|
||||
// 可以保留 hash 以利用缓存,但 UMD 通常用于简单场景,可选
|
||||
entryFileNames: 'js/sundererapp.[hash].js', // 例如: increaseacc.js (或 [name].[hash].js)
|
||||
chunkFileNames: 'js/sundererapp.[hash].js',
|
||||
assetFileNames: 'js/sundererapp.[hash].[ext]', // 例如: increaseacc.css (或 [name].[hash].[ext])
|
||||
// UMD 格式可能需要外部依赖,如果 Vue 是全局引入的,需要声明
|
||||
// globals: {
|
||||
// vue: 'Vue' // 假设 Vue 已通过 script 标签全局引入
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user