Import local app source

This commit is contained in:
Codex
2026-04-23 10:43:49 +00:00
commit 283972860c
65 changed files with 7942 additions and 0 deletions
+24
View File
@@ -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?
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}
+5
View File
@@ -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).
+13
View File
@@ -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>
File diff suppressed because it is too large Load Diff
+23
View File
@@ -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

+1
View File
@@ -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

+21
View File
@@ -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>
+69
View File
@@ -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>
+1
View File
@@ -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>
+12
View File
@@ -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>
+10
View File
@@ -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')
+80
View File
@@ -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
+79
View File
@@ -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;
}
}
+21
View File
@@ -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>
+38
View File
@@ -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]';
}
}
}
}
})
+56
View File
@@ -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 标签全局引入
// }
}
}
}
})