Initial import of frappe sunderer app

This commit is contained in:
microYF
2026-04-23 14:45:47 +00:00
parent 283972860c
commit 0f323e8d41
17 changed files with 3239 additions and 469 deletions
-4
View File
@@ -1087,7 +1087,6 @@
"resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz",
"integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@emotion/hash": "~0.8.0",
"csstype": "~3.0.5"
@@ -1110,7 +1109,6 @@
"resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
@@ -1747,7 +1745,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -1819,7 +1816,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.26",
"@vue/compiler-sfc": "3.5.26",
@@ -80,13 +80,13 @@ const menuOptions = [
label: 'Dashboard',
key: 'dashboard',
icon: renderIcon(BookIcon),
props: { onClick: () => router.push('/app/dashboard') }
props: { onClick: () => router.push('/sundererapp/dashboard') }
},
{
label: 'Users',
key: 'users',
icon: renderIcon(PersonIcon),
props: { onClick: () => router.push('/app/users') }
props: { onClick: () => router.push('/sundererapp/users') }
},
// 添加更多菜单项...
]
@@ -1,166 +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;
}
<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('/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>
+257 -257
View File
@@ -1,258 +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;
}
<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 = '/sundererapp'
} 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>
+4 -4
View File
@@ -24,13 +24,13 @@ export default defineConfig({
manifest: true,
rollupOptions: {
output: {
entryFileNames: 'js/[name].[hash].js',
chunkFileNames: 'js/[name].[hash].js',
entryFileNames: 'js/index.js',
chunkFileNames: 'js/[name].js',
assetFileNames: (assetInfo) => {
if (assetInfo.name.endsWith('.css')) {
return 'css/[name].[hash].[ext]';
return 'css/index.css';
}
return 'assets/[name].[hash].[ext]';
return 'assets/[name][extname]';
}
}
}
+3 -3
View File
@@ -42,9 +42,9 @@ export default defineConfig({
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])
entryFileNames: 'js/sundererapp.js', // 例如: increaseacc.js (或 [name].[hash].js)
chunkFileNames: 'js/[name].js',
assetFileNames: 'assets/[name][extname]', // 例如: increaseacc.css (或 [name].[hash].[ext])
// UMD 格式可能需要外部依赖,如果 Vue 是全局引入的,需要声明
// globals: {
// vue: 'Vue' // 假设 Vue 已通过 script 标签全局引入