Merge remote-tracking branch 'origin/master'

This commit is contained in:
2025-08-12 10:18:12 +08:00
20 changed files with 1971 additions and 1755 deletions

View File

@@ -1,8 +1,8 @@
spring:
datasource:
driver-class-name: org.sqlite.JDBC
# url: jdbc:sqlite:C:\it\houtaigunli\biji\mydatabase.db
url: jdbc:sqlite:C:\KAIFA\2\mydatabase.db
url: jdbc:sqlite:C:\it\houtaigunli\biji\mydatabase.db
# url: jdbc:sqlite:C:\KAIFA\2\mydatabase.db
jpa:
hibernate:
ddl-auto: none

View File

@@ -6,18 +6,32 @@
.home-page.is-mobile .sidebar {
position: absolute;
z-index: 1001;
transition: transform 0.3s ease;
height: 100%;
transform: translateX(-100%);
background-color: #fff; /* 确保侧边栏有背景色 */
overflow: hidden; /* 避免内容在折叠过程中溢出 */
}
.home-page.is-mobile .sidebar:not(.is-collapsed) {
transform: translateX(0);
/* No transform needed, width is controlled by component */
}
.home-page.is-mobile .content {
padding: 8px;
height: 100vh;
display: flex;
flex-direction: column;
}
.home-page.is-mobile .list-view-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.home-page.is-mobile .note-list-wrapper {
flex: 1;
overflow-y: auto;
}
.sidebar-overlay {
@@ -121,4 +135,24 @@
/* 针对回收站的卡片式布局 (默认隐藏) */
.trash-cards {
display: none;
}
}
/* 优化移动端悬浮操作按钮 (FAB) */
.fab {
position: fixed !important;
right: 25px !important;
bottom: 90px !important; /* 调整位置以避开主题切换按钮 */
z-index: 1050 !important;
width: 56px !important;
height: 56px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
transition: transform 0.2s ease-in-out !important;
}
.fab:hover {
transform: scale(1.05); /* 添加轻微的悬停放大效果 */
}
.fab .el-icon {
font-size: 24px; /* 调整图标大小 */
}

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
<el-form-item>
<div class="button-group">
<el-button type="primary" @click="handleLogin" class="login-button">安全登录</el-button>
<el-button v-if="isRegistrationEnabled" @click="goToRegister" class="register-button">立即注册</el-button>
<el-button @click="goToRegister" class="register-button">立即注册</el-button>
</div>
</el-form-item>
</el-form>
@@ -38,14 +38,12 @@
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '../stores/user';
import { getRegistrationStatus } from '../api/CommonApi';
import { ElMessage } from 'element-plus';
import { User, Lock } from '@element-plus/icons-vue';
const router = useRouter();
const userStore = useUserStore();
const loginFormRef = ref(null);
const isRegistrationEnabled = ref(true);
const loginForm = ref({
username: '',
@@ -78,16 +76,6 @@ const goToHome = () => {
router.push('/home');
};
onMounted(async () => {
try {
const response = await getRegistrationStatus();
isRegistrationEnabled.value = response.data;
} catch (error) {
console.error("Failed to fetch registration status:", error);
// 保守起见,如果获取失败则禁用注册按钮
isRegistrationEnabled.value = false;
}
});
</script>
<style scoped>

View File

@@ -0,0 +1,172 @@
<template>
<div>
<!-- Desktop Header -->
<el-header class="header" v-if="!isMobile">
<h1 @click="$emit('reset-view')" style="cursor: pointer; flex-grow: 1;">我的笔记</h1>
<div class="actions">
<el-input
:model-value="searchKeyword"
@update:model-value="$emit('update:searchKeyword', $event)"
placeholder="搜索笔记标题"
class="search-input"
@keyup.enter="$emit('search')"
>
<template #append>
<el-button @click="$emit('search')">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
<div v-if="userStore.isLoggedIn" class="user-actions">
<span class="welcome-text">欢迎, {{ userStore.userInfo?.username }}</span>
<el-button type="danger" @click="$emit('logout')">退出</el-button>
<el-button type="primary" @click="$emit('show-update-password')">修改密码</el-button>
<el-button type="warning" @click="$emit('show-system-settings')">系统管理</el-button>
<el-button type="primary" @click="$emit('show-create-note')">新建笔记</el-button>
<el-upload
class="upload-btn"
action=""
:show-file-list="false"
:before-upload="handleUpload"
accept=".md"
>
<el-button type="success">上传Markdown</el-button>
</el-upload>
</div>
<div v-else class="guest-actions">
<el-button type="primary" @click="goToLogin">登录</el-button>
<el-button @click="goToRegister">注册</el-button>
</div>
</div>
</el-header>
<!-- Mobile Header -->
<el-header class="header mobile-header" v-if="isMobile">
<el-button @click="$emit('toggle-collapse')" text circle class="mobile-menu-toggle">
<el-icon size="24"><Menu /></el-icon>
</el-button>
<h1 class="mobile-title" @click="$emit('reset-view')">我的笔记</h1>
<el-button text circle class="mobile-search-toggle" @click="$emit('search')">
<el-icon size="22"><Search /></el-icon>
</el-button>
<el-button v-if="!userStore.isLoggedIn" text circle class="mobile-login-toggle" @click="goToLogin">
<el-icon size="24"><User /></el-icon>
</el-button>
</el-header>
<!-- Mobile Search Bar -->
<div v-if="isMobile" class="mobile-search-container">
<el-input
:model-value="searchKeyword"
@update:model-value="$emit('update:searchKeyword', $event)"
placeholder="搜索笔记标题"
class="mobile-search-input"
@keyup.enter="$emit('search')"
size="large"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
</div>
</template>
<script setup>
import { Search, Menu, User } from '@element-plus/icons-vue';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
const props = defineProps({
isMobile: Boolean,
searchKeyword: String,
});
const emit = defineEmits([
'update:searchKeyword',
'search',
'reset-view',
'logout',
'show-update-password',
'show-system-settings',
'show-create-note',
'upload-markdown',
'toggle-collapse'
]);
const userStore = useUserStore();
const router = useRouter();
const goToLogin = () => router.push('/login');
const goToRegister = () => router.push('/register');
const handleUpload = (file) => {
emit('upload-markdown', file);
return false; // Prevent el-upload's default behavior
};
</script>
<style scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding: 1rem;
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow-light);
gap: 1rem;
}
.dark-theme .header {
background-color: rgba(30, 30, 47, 0.8);
}
.actions {
display: flex;
gap: 10px;
align-items: center;
}
:deep(.search-input .el-input__wrapper) {
border-radius: var(--border-radius) !important;
}
.user-actions, .guest-actions {
display: flex;
align-items: center;
gap: 10px;
}
.welcome-text {
white-space: nowrap;
color: var(--text-color-secondary);
}
.upload-btn {
display: inline-block;
}
.mobile-header {
padding: 0 1rem;
}
.mobile-title {
cursor: pointer;
flex-grow: 1;
text-align: center;
margin: 0;
font-size: 1.25rem;
}
.mobile-search-container {
padding: 0 1.5rem;
margin-bottom: 1.5rem;
}
:deep(.mobile-search-input .el-input__wrapper) {
border-radius: 9999px !important;
}
</style>

View File

@@ -0,0 +1,172 @@
<template>
<div class="note-editor-wrapper">
<el-header class="editor-header">
<h2 class="editor-title">{{ editData.title }}</h2>
<div class="actions">
<el-button type="primary" @click="$emit('back', editData)">返回</el-button>
<el-button type="success" @click="save">保存</el-button>
<span class="save-status">{{ saveStatus }}</span>
</div>
</el-header>
<div id="vditor-editor" class="vditor" />
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import Vditor from 'vditor';
import 'vditor/dist/index.css';
import { ElMessage } from 'element-plus';
import { updateMarkdown, uploadImage } from '@/api/CommonApi.js';
const props = defineProps({
editData: {
type: Object,
required: true,
},
});
const emit = defineEmits(['back', 'save-success']);
const vditor = ref(null);
const saveStatus = ref('空闲');
let debounceTimer = null;
const initVditor = () => {
vditor.value = new Vditor('vditor-editor', {
height: 'calc(100vh - 120px)',
mode: 'ir',
after: () => {
if (props.editData && props.editData.content) {
vditor.value.setValue(props.editData.content);
}
},
input: (value) => {
saveStatus.value = '正在输入...';
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
handleSave(value);
}, 2000);
},
upload: {
accept: 'image/*',
handler(files) {
handleImageUpload(files);
},
},
});
};
const handleImageUpload = async (files) => {
const file = files;
if (!file) return;
try {
const promise = await uploadImage(file);
if (promise.url == null) {
ElMessage.error(promise.msg || '图片上传失败');
return;
}
const fullUrl = `${import.meta.env.VITE_API_BASE_URL}${promise.url}`;
vditor.value.insertValue(`![${file.name}](${fullUrl})`);
} catch (error) {
ElMessage.error('图片上传失败: ' + error.message);
}
};
const handleSave = async (content) => {
saveStatus.value = '正在保存...';
try {
const payload = {
id: props.editData.id,
title: props.editData.title,
groupingId: props.editData.groupingId,
content: content,
fileName: props.editData.fileName || `${props.editData.title}.md`,
isPrivate: props.editData.isPrivate,
};
const response = await updateMarkdown(payload);
emit('save-success', response);
saveStatus.value = '已保存';
ElMessage.success('保存成功');
} catch (error) {
saveStatus.value = '保存失败';
ElMessage.error('保存失败: ' + (error.response?.data?.message || error.message));
}
};
const save = () => {
if (vditor.value) {
handleSave(vditor.value.getValue());
}
};
onMounted(() => {
initVditor();
});
onBeforeUnmount(() => {
if (vditor.value) {
vditor.value.destroy();
}
clearTimeout(debounceTimer);
});
watch(() => props.editData, (newData) => {
if (vditor.value && newData) {
vditor.value.setValue(newData.content || '');
}
}, { deep: true });
// Expose the save method to the parent
defineExpose({ save });
</script>
<style scoped>
.note-editor-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding: 1rem;
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow-light);
}
.dark-theme .editor-header {
background-color: rgba(30, 30, 47, 0.8);
}
.editor-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.actions {
display: flex;
gap: 10px;
align-items: center;
}
.save-status {
font-size: 14px;
color: var(--text-color-secondary);
width: 80px; /* Give it a fixed width to prevent layout shifts */
text-align: center;
}
.vditor {
flex-grow: 1;
border: none;
}
</style>

View File

@@ -0,0 +1,117 @@
<template>
<div v-if="files.length > 0" class="file-list">
<el-card
v-for="file in files"
:key="file.id"
shadow="hover"
class="file-item"
:class="{ 'private-note': file.isPrivate === 1 }"
>
<div @click="$emit('preview', file)" class="file-content">
<div class="file-title-wrapper">
<el-icon class="file-icon"><Document /></el-icon>
<span class="file-title-text">{{ file.title }}</span>
</div>
<div class="file-meta">
<span class="file-group-name">{{ file.groupingName }}</span>
<el-icon v-if="file.isPrivate === 1 && !isUserLoggedIn" class="lock-icon"><Lock /></el-icon>
</div>
</div>
</el-card>
</div>
<el-empty v-else description="暂无笔记,请创建或上传" />
</template>
<script setup>
import { Lock, Document } from '@element-plus/icons-vue';
const props = defineProps({
files: {
type: Array,
required: true,
},
isUserLoggedIn: {
type: Boolean,
required: true,
}
});
defineEmits(['preview']);
</script>
<style scoped>
.file-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1.5rem;
}
.file-item {
cursor: pointer;
border-radius: 12px; /* Increased border radius */
border: 1px solid var(--border-color);
transition: all var(--transition-duration) ease;
background-color: #ffffff; /* Explicitly set to white for light theme */
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
overflow: hidden; /* Ensure content respects border radius */
}
.dark-theme .file-item {
background-color: #161b22; /* A darker card color for dark theme */
border-color: var(--border-color-dark);
}
.file-item:hover {
transform: translateY(-5px);
box-shadow: var(--box-shadow-lifted);
border-color: var(--primary-color);
}
.file-content {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 1rem 1.2rem;
min-height: 80px; /* Give some consistent height */
}
.file-title-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.file-icon {
font-size: 1.2rem;
color: var(--text-color-secondary);
}
.file-title-text {
font-weight: 600;
color: var(--text-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-meta {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 0.5rem;
}
.file-group-name {
font-size: 0.75rem;
font-weight: 500;
color: var(--primary-color);
background-color: var(--primary-color-light);
padding: 0.25rem 0.6rem;
border-radius: var(--border-radius-full);
}
.lock-icon {
color: var(--el-color-warning);
}
</style>

View File

@@ -0,0 +1,140 @@
<template>
<div class="file-preview">
<el-header class="preview-header">
<h2 class="preview-title">
<span>{{ file.title }}</span>
<el-icon v-if="file.isPrivate === 1" class="lock-icon"><Lock /></el-icon>
<el-icon class="edit-icon" @click="$emit('open-rename-dialog', file, 'file')"><Edit /></el-icon>
</h2>
<div class="actions">
<el-button :circle="isMobile" :size="isMobile ? 'small' : 'default'" type="primary" @click="$emit('back')">
<el-icon v-if="isMobile"><Back /></el-icon>
<span v-else>返回</span>
</el-button>
<el-button v-if="isUserLoggedIn && !isMobile" type="warning" @click="$emit('show-move-note-dialog')">移动</el-button>
<el-button v-if="isUserLoggedIn" :circle="isMobile" :size="isMobile ? 'small' : 'default'" type="primary" @click="$emit('edit')">
<el-icon v-if="isMobile"><Edit /></el-icon>
<span v-else>编辑</span>
</el-button>
<el-button v-if="isUserLoggedIn" :circle="isMobile" :size="isMobile ? 'small' : 'default'" type="danger" @click="$emit('delete')">
<el-icon v-if="isMobile"><Delete /></el-icon>
<span v-else>删除</span>
</el-button>
<el-button v-if="isUserLoggedIn" :circle="isMobile" :size="isMobile ? 'small' : 'default'" type="info" @click="$emit('show-privacy-dialog')">
<el-icon v-if="isMobile"><Lock /></el-icon>
<span v-else>{{ file.isPrivate === 1 ? '设为公开' : '设为私密' }}</span>
</el-button>
<el-dropdown v-if="isUserLoggedIn && !isMobile" @command="handleExport">
<el-button type="success">
导出<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="md">导出为 .md</el-dropdown-item>
<el-dropdown-item command="pdf">导出为 .pdf</el-dropdown-item>
<el-dropdown-item command="html">导出为 .html</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<div :key="file.id" class="markdown-preview">
<!-- Content is rendered by Vditor.preview in the parent -->
</div>
</div>
</template>
<script setup>
import { Lock, Edit, Delete, ArrowDown, Back } from '@element-plus/icons-vue';
const props = defineProps({
file: {
type: Object,
required: true,
},
isMobile: Boolean,
isUserLoggedIn: Boolean,
});
const emit = defineEmits([
'back',
'edit',
'delete',
'open-rename-dialog',
'show-move-note-dialog',
'show-privacy-dialog',
'export'
]);
const handleExport = (format) => {
emit('export', format);
};
</script>
<style scoped>
.file-preview {
display: flex;
flex-direction: column;
height: 100%;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding: 1rem;
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow-light);
gap: 1rem;
}
.dark-theme .preview-header {
background-color: rgba(30, 30, 47, 0.8);
}
.preview-title {
display: flex;
align-items: center;
gap: 10px;
flex-grow: 1;
overflow: hidden;
white-space: nowrap;
}
.preview-title span {
overflow: hidden;
text-overflow: ellipsis;
}
.preview-title .edit-icon {
visibility: visible;
opacity: 0.6;
cursor: pointer;
}
.preview-title .edit-icon:hover {
opacity: 1;
}
.actions {
display: flex;
gap: 10px;
align-items: center;
}
.markdown-preview {
flex-grow: 1;
overflow-y: auto;
padding: 2rem;
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow-light);
}
.dark-theme .markdown-preview {
background-color: rgba(30, 30, 47, 0.8);
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<el-aside class="sidebar" :width="isCollapsed ? (isMobile ? '0' : '64px') : '250px'">
<div class="sidebar-header">
<span v-if="!isCollapsed" style="margin-right: 15px; font-weight: bold;">笔记分类</span>
<el-button v-if="!isCollapsed" type="primary" size="small" @click="$emit('show-create-group')" circle>
<el-icon><Plus /></el-icon>
</el-button>
<el-button @click="$emit('toggle-collapse')" type="primary" size="small" circle v-if="!isMobile">
<el-icon>
<Fold v-if="!isCollapsed" />
<Expand v-else />
</el-icon>
</el-button>
</div>
<!-- Desktop Menu -->
<el-menu
v-if="!isMobile"
:default-active="activeMenu"
class="el-menu-vertical-demo"
:collapse="isCollapsed"
popper-effect="light"
:collapse-transition="false"
>
<template v-for="menu in categoryTree" :key="menu.id">
<component :is="renderMenu(menu)" />
</template>
<ElMenuItem index="trash" @click="goToTrash">
<ElIcon><Delete /></ElIcon>
<template #title>回收站</template>
</ElMenuItem>
</el-menu>
<!-- Mobile Menu -->
<el-menu
v-if="isMobile"
:default-active="activeMenu"
class="el-menu-vertical-demo"
:collapse="isCollapsed"
:collapse-transition="false"
>
<div class="mobile-menu-header">
<div v-if="userStore.isLoggedIn" class="user-info">
<span class="username">欢迎, {{ userStore.userInfo?.username }}</span>
</div>
<div v-else class="guest-info">
<el-button type="primary" @click="goToLogin">登录</el-button>
<el-button @click="goToRegister">注册</el-button>
</div>
</div>
<template v-for="menu in categoryTree" :key="menu.id">
<component :is="renderMenu(menu)" />
</template>
<ElMenuItem index="trash" @click="goToTrash">
<ElIcon><Delete /></ElIcon>
<template #title>回收站</template>
</ElMenuItem>
<ElMenuItem v-if="userStore.isLoggedIn" index="system-settings" @click="$emit('show-system-settings')">
<ElIcon><setting /></ElIcon>
<template #title>系统管理</template>
</ElMenuItem>
<ElMenuItem v-if="userStore.isLoggedIn" index="update-password" @click="$emit('show-update-password')">
<ElIcon><lock /></ElIcon>
<template #title>修改密码</template>
</ElMenuItem>
<ElMenuItem v-if="userStore.isLoggedIn" index="logout" @click="$emit('logout')">
<ElIcon><SwitchButton /></ElIcon>
<template #title>退出登录</template>
</ElMenuItem>
</el-menu>
</el-aside>
</template>
<script setup>
import { h } from 'vue';
import { ElSubMenu, ElMenuItem, ElIcon, ElMessageBox, ElTooltip } from 'element-plus';
import { Folder, Delete, Edit, Plus, Fold, Expand, Setting, Lock, SwitchButton } from '@element-plus/icons-vue';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
import { deleteGrouping } from '@/api/CommonApi.js';
const props = defineProps({
isCollapsed: Boolean,
isMobile: Boolean,
activeMenu: String,
categoryTree: Array,
});
const emit = defineEmits([
'select-file',
'show-rename-dialog',
'show-create-group',
'toggle-collapse',
'group-deleted',
'show-system-settings',
'show-update-password',
'logout'
]);
const userStore = useUserStore();
const router = useRouter();
const goToTrash = () => router.push({ name: 'Trash' });
const goToLogin = () => router.push('/login');
const goToRegister = () => router.push('/register');
const handleDeleteGroup = (group) => {
ElMessageBox.confirm(
`确定要删除分类 “${group.grouping}” 吗?这将同时删除该分类下的所有子分类和笔记。`,
'警告',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
).then(async () => {
try {
await deleteGrouping(group.id);
ElMessage.success('分类已删除');
emit('group-deleted');
} catch (error) {
ElMessage.error('删除分类失败: ' + error.message);
}
});
};
const renderMenu = (item) => {
const titleContent = () => h('div', { class: 'menu-item-title' }, [
h(ElIcon, () => h(Folder)),
h('span', { class: 'menu-item-text' }, item.grouping),
h('div', { class: 'menu-item-actions' }, [
h(ElIcon, { class: 'edit-icon', onClick: (e) => { e.stopPropagation(); emit('show-rename-dialog', item, 'group'); } }, () => h(Edit)),
h(ElIcon, { class: 'delete-icon', onClick: (e) => { e.stopPropagation(); handleDeleteGroup(item); } }, () => h(Delete))
])
]);
const wrappedContent = () => h(ElTooltip, {
content: item.grouping,
placement: 'right',
disabled: !props.isCollapsed,
effect: 'dark',
offset: 15,
}, { default: titleContent });
if (item.children && item.children.length > 0) {
return h(ElSubMenu, {
index: `group-${item.id}`,
popperClass: props.isCollapsed ? 'hide-popper' : ''
}, {
title: () => h('div', {
class: 'submenu-title-wrapper',
onClick: () => emit('select-file', item),
style: 'width: 100%; display: flex; align-items: center;'
}, [ wrappedContent() ]),
default: () => item.children.map(child => renderMenu(child))
});
}
return h(ElMenuItem, { index: `group-${item.id}`, onClick: () => emit('select-file', item) }, {
default: wrappedContent
});
};
</script>
<style scoped>
.sidebar {
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-right: 1px solid rgba(0, 0, 0, 0.05);
transition: width 0.3s ease; /* Apply transition to width */
display: flex;
flex-direction: column;
}
.dark-theme .sidebar {
background-color: rgba(23, 23, 39, 0.8);
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
flex-shrink: 0;
}
.el-menu {
border-right: none;
background: transparent;
flex-grow: 1;
overflow-y: auto;
overflow-x: hidden;
transition: none !important; /* Disable built-in transitions */
}
:deep(.el-menu-item), :deep(.el-sub-menu__title) {
height: 48px;
line-height: 48px;
border-radius: var(--border-radius);
margin: 0.25rem 0.5rem;
color: var(--text-color-secondary);
}
:deep(.el-menu-item.is-active) {
background-color: var(--primary-color);
color: #fff;
font-weight: 600;
}
:deep(.el-menu-item:hover), :deep(.el-sub-menu__title:hover) {
background-color: var(--primary-color-light);
color: var(--primary-color);
}
.menu-item-title {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 5px;
}
.menu-item-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
transition: opacity 0.2s ease; /* Add transition for smooth fade */
opacity: 1;
}
.menu-item-actions {
display: none;
align-items: center;
gap: 5px;
transition: opacity 0.2s ease;
opacity: 1;
}
.el-menu:not(.el-menu--collapse) .el-menu-item:hover .menu-item-actions,
.el-menu:not(.el-menu--collapse) .el-sub-menu__title:hover .menu-item-actions {
display: flex;
}
.el-menu--collapse .menu-item-text,
.el-menu--collapse .menu-item-actions {
opacity: 0; /* Fade out instead of just disappearing */
width: 0; /* Ensure it takes no space */
}
.edit-icon, .delete-icon {
cursor: pointer;
color: var(--text-color-secondary);
}
.edit-icon:hover, .delete-icon:hover {
color: var(--primary-color);
}
.mobile-menu-header {
padding: 20px;
text-align: center;
border-bottom: 1px solid #e0e0e0;
}
.mobile-menu-header .username {
font-weight: bold;
font-size: 16px;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<el-dialog
:model-value="visible"
title="新建分类"
width="400px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="父级分类">
<el-cascader
v-model="form.parentId"
:options="categoryOptions"
:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'grouping' }"
clearable
placeholder="不选则为一级分类"
style="width: 100%;"
></el-cascader>
</el-form-item>
<el-form-item label="分类名称" prop="name">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { addGroupings } from '@/api/CommonApi.js';
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
categoryOptions: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update:visible', 'group-created']);
const formRef = ref(null);
const form = ref({
name: '',
parentId: null,
});
const rules = ref({
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
});
// 当对话框关闭时,重置表单
watch(() => props.visible, (newVal) => {
if (!newVal && formRef.value) {
formRef.value.resetFields();
form.value = { name: '', parentId: null };
}
});
const handleClose = () => {
emit('update:visible', false);
};
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (valid) {
try {
const payload = {
grouping: form.value.name,
parentId: form.value.parentId || 0,
};
await addGroupings(payload);
ElMessage.success('分类创建成功');
emit('group-created'); // 通知父组件刷新
handleClose();
} catch (error) {
ElMessage.error('创建分类失败: ' + error.message);
}
}
});
};
</script>

View File

@@ -0,0 +1,109 @@
<template>
<el-dialog
:model-value="visible"
title="新建笔记"
width="400px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="笔记标题" prop="title">
<el-input v-model="form.title" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="选择分类" prop="groupingId">
<el-cascader
v-model="form.groupingId"
:options="categoryOptions"
:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'grouping' }"
clearable
placeholder="请选择笔记所属分类"
style="width: 100%;"
></el-cascader>
</el-form-item>
<el-form-item label="私密笔记">
<el-switch
v-model="form.isPrivate"
:active-value="1"
:inactive-value="0"
active-text="私密"
inactive-text="公开"
/>
<div class="form-item-help">私密笔记只有登录用户才能查看内容</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
categoryOptions: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update:visible', 'create-note']);
const formRef = ref(null);
const form = ref({
title: '',
groupingId: null,
isPrivate: 0,
});
const rules = ref({
title: [{ required: true, message: '请输入笔记标题', trigger: 'blur' }],
groupingId: [{ required: true, message: '请选择分类', trigger: 'change' }],
});
watch(() => props.visible, (newVal) => {
if (!newVal && formRef.value) {
formRef.value.resetFields();
form.value = { title: '', groupingId: null, isPrivate: 0 };
}
});
const handleClose = () => {
emit('update:visible', false);
};
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate((valid) => {
if (valid) {
const payload = {
id: null,
title: form.value.title,
groupingId: form.value.groupingId,
fileName: form.value.title + '.md',
content: '',
isPrivate: form.value.isPrivate,
};
emit('create-note', payload);
handleClose();
} else {
ElMessage.error('请填写必要的字段');
}
});
};
</script>
<style scoped>
.form-item-help {
font-size: 12px;
color: #909399;
margin-top: 4px;
line-height: 1.5;
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<el-dialog
:model-value="visible"
title="移动笔记到"
width="400px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-cascader
v-model="moveToGroupId"
:options="categoryOptions"
:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'grouping' }"
clearable
placeholder="请选择目标分类"
style="width: 100%;"
></el-cascader>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { updateMarkdown } from '@/api/CommonApi.js';
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
categoryOptions: {
type: Array,
default: () => [],
},
noteToMove: {
type: Object,
default: null,
}
});
const emit = defineEmits(['update:visible', 'move-success']);
const moveToGroupId = ref(null);
watch(() => props.visible, (newVal) => {
if (!newVal) {
moveToGroupId.value = null;
}
});
const handleClose = () => {
emit('update:visible', false);
};
const handleSubmit = async () => {
if (!moveToGroupId.value) {
ElMessage.error('请选择目标分类');
return;
}
if (!props.noteToMove) {
ElMessage.error('没有需要移动的笔记');
return;
}
try {
const payload = {
...props.noteToMove,
groupingId: moveToGroupId.value,
};
await updateMarkdown(payload);
ElMessage.success('笔记移动成功');
emit('move-success');
handleClose();
} catch (error) {
ElMessage.error('移动失败: ' + error.message);
}
};
</script>

View File

@@ -0,0 +1,91 @@
<template>
<el-dialog
:model-value="visible"
:title="title"
width="400px"
@close="handleClose"
:close-on-click-modal="false"
>
<div v-if="note">
<p>您确定要将笔记 <strong>"{{ note.title }}"</strong> {{ note.isPrivate === 1 ? '设为公开' : '设为私密' }}</p>
<div class="privacy-explanation">
<el-icon><InfoFilled /></el-icon>
<span>{{ explanation }}</span>
</div>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { computed } from 'vue';
import { ElMessage } from 'element-plus';
import { updateMarkdown } from '@/api/CommonApi.js';
import { InfoFilled } from '@element-plus/icons-vue';
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
note: {
type: Object,
default: null,
},
});
const emit = defineEmits(['update:visible', 'privacy-changed']);
const title = computed(() => {
if (!props.note) return '';
return props.note.isPrivate === 1 ? '设为公开笔记' : '设为私密笔记';
});
const explanation = computed(() => {
if (!props.note) return '';
return props.note.isPrivate === 1 ? '公开笔记:所有用户都可以查看内容' : '私密笔记:只有登录用户才能查看内容';
});
const handleClose = () => {
emit('update:visible', false);
};
const handleSubmit = async () => {
if (!props.note) return;
try {
const newPrivacyStatus = props.note.isPrivate === 1 ? 0 : 1;
const payload = {
...props.note,
isPrivate: newPrivacyStatus,
};
const updatedFile = await updateMarkdown(payload);
ElMessage.success(`笔记已${newPrivacyStatus === 1 ? '设为私密' : '设为公开'}`);
emit('privacy-changed', updatedFile);
handleClose();
} catch (error) {
ElMessage.error('修改笔记状态失败: ' + error.message);
}
};
</script>
<style scoped>
.privacy-explanation {
margin-top: 1rem;
padding: 0.75rem;
background-color: #f4f4f5;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
color: #909399;
}
.dark-theme .privacy-explanation {
background-color: #2c2c3e;
color: #a9a9a9;
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<el-dialog
:model-value="visible"
title="重命名"
width="400px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-input v-model="newName" placeholder="请输入新名称" @keyup.enter="handleSubmit"></el-input>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { updateMarkdownTitle, updateGroupingName } from '@/api/CommonApi.js';
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
item: {
type: Object,
default: () => null,
},
});
const emit = defineEmits(['update:visible', 'renamed']);
const newName = ref('');
watch(() => props.item, (newItem) => {
if (newItem) {
newName.value = newItem.type === 'file' ? newItem.title : newItem.grouping;
}
});
const handleClose = () => {
emit('update:visible', false);
};
const handleSubmit = async () => {
if (!newName.value.trim()) {
ElMessage.error('名称不能为空');
return;
}
try {
if (props.item.type === 'file') {
await updateMarkdownTitle({ id: props.item.id, title: newName.value });
} else {
await updateGroupingName({ id: props.item.id, grouping: newName.value });
}
ElMessage.success('重命名成功');
emit('renamed');
handleClose();
} catch (error) {
ElMessage.error('重命名失败: ' + error.message);
}
};
</script>

View File

@@ -0,0 +1,88 @@
<template>
<el-dialog
:model-value="visible"
title="选择导入的分类"
width="400px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-cascader
v-model="importGroupId"
:options="categoryOptions"
:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'grouping' }"
clearable
placeholder="请选择要导入的分类"
style="width: 100%;"
></el-cascader>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { updateMarkdown } from '@/api/CommonApi.js';
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
categoryOptions: {
type: Array,
default: () => [],
},
fileToImport: {
type: File,
default: null,
}
});
const emit = defineEmits(['update:visible', 'import-success']);
const importGroupId = ref(null);
watch(() => props.visible, (newVal) => {
if (!newVal) {
importGroupId.value = null;
}
});
const handleClose = () => {
emit('update:visible', false);
};
const handleSubmit = () => {
if (!importGroupId.value) {
ElMessage.error('请选择要导入的分类');
return;
}
if (!props.fileToImport) {
ElMessage.error('没有需要导入的文件');
return;
}
const reader = new FileReader();
reader.onload = async (e) => {
const content = e.target.result;
const payload = {
title: props.fileToImport.name.replace(/\.md$/, ''),
groupingId: importGroupId.value,
content: content,
fileName: props.fileToImport.name,
};
try {
await updateMarkdown(payload);
ElMessage.success('Markdown 文件导入成功');
emit('import-success');
handleClose();
} catch (error) {
ElMessage.error('导入失败: ' + error.message);
}
};
reader.readAsText(props.fileToImport);
};
</script>

View File

@@ -0,0 +1,98 @@
<template>
<el-dialog
:model-value="visible"
title="系统管理"
width="500px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-form label-width="120px">
<el-form-item label="开放注册">
<el-switch v-model="isRegistrationEnabled" @change="handleToggleRegistration"></el-switch>
</el-form-item>
<el-form-item label="生成注册码">
<el-button type="primary" @click="handleGenerateCode">生成</el-button>
</el-form-item>
<el-form-item v-if="generatedCode" label="新注册码">
<el-input v-model="generatedCode" readonly>
<template #append>
<el-button @click="copyToClipboard(generatedCode)">复制</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import {
getRegistrationStatus,
toggleRegistration,
generateRegistrationCode,
} from '@/api/CommonApi.js';
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:visible']);
const isRegistrationEnabled = ref(true);
const generatedCode = ref('');
watch(() => props.visible, (newVal) => {
if (newVal) {
fetchRegistrationStatus();
generatedCode.value = ''; // Reset code when dialog opens
}
});
const fetchRegistrationStatus = async () => {
try {
isRegistrationEnabled.value = await getRegistrationStatus();
} catch (error) {
console.error("Failed to fetch registration status:", error);
ElMessage.error('获取注册状态失败');
}
};
const handleToggleRegistration = async (value) => {
try {
await toggleRegistration(value);
ElMessage.success(`注册功能已${value ? '开启' : '关闭'}`);
} catch (error) {
ElMessage.error('操作失败');
isRegistrationEnabled.value = !value; // Revert on failure
}
};
const handleGenerateCode = async () => {
try {
const code = await generateRegistrationCode();
generatedCode.value = code;
ElMessage.success('注册码生成成功');
} catch (error) {
ElMessage.error('生成注册码失败: ' + error.message);
}
};
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text).then(() => {
ElMessage.success('已复制到剪贴板');
}, () => {
ElMessage.error('复制失败');
});
};
const handleClose = () => {
emit('update:visible', false);
};
</script>

View File

@@ -0,0 +1,97 @@
<template>
<el-dialog
:model-value="visible"
title="修改密码"
width="400px"
@close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="form.oldPassword" type="password" show-password autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="form.newPassword" type="password" show-password autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="确认新密码" prop="confirmPassword">
<el-input v-model="form.confirmPassword" type="password" show-password autocomplete="off"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { updatePassword } from '@/api/CommonApi.js';
import { useUserStore } from '@/stores/user';
import { useRouter } from 'vue-router';
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:visible', 'password-updated']);
const userStore = useUserStore();
const router = useRouter();
const formRef = ref(null);
const form = ref({
oldPassword: '',
newPassword: '',
confirmPassword: '',
});
const validateConfirmPassword = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入新密码'));
} else if (value !== form.value.newPassword) {
callback(new Error("两次输入的新密码不一致"));
} else {
callback();
}
};
const rules = ref({
oldPassword: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
newPassword: [{ required: true, message: '请输入新密码', trigger: 'blur' }, { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }],
confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }],
});
watch(() => props.visible, (newVal) => {
if (!newVal && formRef.value) {
formRef.value.resetFields();
}
});
const handleClose = () => {
emit('update:visible', false);
};
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (valid) {
try {
await updatePassword({
oldPassword: form.value.oldPassword,
newPassword: form.value.newPassword,
});
ElMessage.success('密码修改成功,请重新登录');
emit('password-updated');
handleClose();
// Logout logic will be handled by the parent component
} catch (error) {
ElMessage.error('密码修改失败: ' + error.message);
}
}
});
};
</script>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -1,371 +0,0 @@
### HTML
~~~html
<a href="https://front.codes/" class="logo" target="_blank">
<img src="https://assets.codepen.io/1462889/fcy.png" alt="">
</a>
<div class="section">
<div class="container">
<div class="row full-height justify-content-center">
<div class="col-12 text-center align-self-center py-5">
<div class="section pb-5 pt-5 pt-sm-2 text-center">
<h6 class="mb-0 pb-3"><span>Log In </span><span>Sign Up</span></h6>
<input class="checkbox" type="checkbox" id="reg-log" name="reg-log"/>
<label for="reg-log"></label>
<div class="card-3d-wrap mx-auto">
<div class="card-3d-wrapper">
<div class="card-front">
<div class="center-wrap">
<div class="section text-center">
<h4 class="mb-4 pb-3">Log In</h4>
<div class="form-group">
<input type="email" name="logemail" class="form-style" placeholder="Your Email" id="logemail" autocomplete="off">
<i class="input-icon uil uil-at"></i>
</div>
<div class="form-group mt-2">
<input type="password" name="logpass" class="form-style" placeholder="Your Password" id="logpass" autocomplete="off">
<i class="input-icon uil uil-lock-alt"></i>
</div>
<a href="#" class="btn mt-4">submit</a>
<p class="mb-0 mt-4 text-center"><a href="#0" class="link">Forgot your password?</a></p>
</div>
</div>
</div>
<div class="card-back">
<div class="center-wrap">
<div class="section text-center">
<h4 class="mb-4 pb-3">Sign Up</h4>
<div class="form-group">
<input type="text" name="logname" class="form-style" placeholder="Your Full Name" id="logname" autocomplete="off">
<i class="input-icon uil uil-user"></i>
</div>
<div class="form-group mt-2">
<input type="email" name="logemail" class="form-style" placeholder="Your Email" id="logemail" autocomplete="off">
<i class="input-icon uil uil-at"></i>
</div>
<div class="form-group mt-2">
<input type="password" name="logpass" class="form-style" placeholder="Your Password" id="logpass" autocomplete="off">
<i class="input-icon uil uil-lock-alt"></i>
</div>
<a href="#" class="btn mt-4">submit</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
~~~
### CSS
~~~css
/* Please ❤ this if you like it! */
@import url('https://fonts.googleapis.com/css?family=Poppins:400,500,600,700,800,900');
body{
font-family: 'Poppins', sans-serif;
font-weight: 300;
font-size: 15px;
line-height: 1.7;
color: #c4c3ca;
background-color: #1f2029;
overflow-x: hidden;
}
a {
cursor: pointer;
transition: all 200ms linear;
}
a:hover {
text-decoration: none;
}
.link {
color: #c4c3ca;
}
.link:hover {
color: #ffeba7;
}
p {
font-weight: 500;
font-size: 14px;
line-height: 1.7;
}
h4 {
font-weight: 600;
}
h6 span{
padding: 0 20px;
text-transform: uppercase;
font-weight: 700;
}
.section{
position: relative;
width: 100%;
display: block;
}
.full-height{
min-height: 100vh;
}
[type="checkbox"]:checked,
[type="checkbox"]:not(:checked){
position: absolute;
left: -9999px;
}
.checkbox:checked + label,
.checkbox:not(:checked) + label{
position: relative;
display: block;
text-align: center;
width: 60px;
height: 16px;
border-radius: 8px;
padding: 0;
margin: 10px auto;
cursor: pointer;
background-color: #ffeba7;
}
.checkbox:checked + label:before,
.checkbox:not(:checked) + label:before{
position: absolute;
display: block;
width: 36px;
height: 36px;
border-radius: 50%;
color: #ffeba7;
background-color: #102770;
font-family: 'unicons';
content: '\eb4f';
z-index: 20;
top: -10px;
left: -10px;
line-height: 36px;
text-align: center;
font-size: 24px;
transition: all 0.5s ease;
}
.checkbox:checked + label:before {
transform: translateX(44px) rotate(-270deg);
}
.card-3d-wrap {
position: relative;
width: 440px;
max-width: 100%;
height: 400px;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
perspective: 800px;
margin-top: 60px;
}
.card-3d-wrapper {
width: 100%;
height: 100%;
position:absolute;
top: 0;
left: 0;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
transition: all 600ms ease-out;
}
.card-front, .card-back {
width: 100%;
height: 100%;
background-color: #2a2b38;
background-image: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/1462889/pat.svg');
background-position: bottom center;
background-repeat: no-repeat;
background-size: 300%;
position: absolute;
border-radius: 6px;
left: 0;
top: 0;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
-o-backface-visibility: hidden;
backface-visibility: hidden;
}
.card-back {
transform: rotateY(180deg);
}
.checkbox:checked ~ .card-3d-wrap .card-3d-wrapper {
transform: rotateY(180deg);
}
.center-wrap{
position: absolute;
width: 100%;
padding: 0 35px;
top: 50%;
left: 0;
transform: translate3d(0, -50%, 35px) perspective(100px);
z-index: 20;
display: block;
}
.form-group{
position: relative;
display: block;
margin: 0;
padding: 0;
}
.form-style {
padding: 13px 20px;
padding-left: 55px;
height: 48px;
width: 100%;
font-weight: 500;
border-radius: 4px;
font-size: 14px;
line-height: 22px;
letter-spacing: 0.5px;
outline: none;
color: #c4c3ca;
background-color: #1f2029;
border: none;
-webkit-transition: all 200ms linear;
transition: all 200ms linear;
box-shadow: 0 4px 8px 0 rgba(21,21,21,.2);
}
.form-style:focus,
.form-style:active {
border: none;
outline: none;
box-shadow: 0 4px 8px 0 rgba(21,21,21,.2);
}
.input-icon {
position: absolute;
top: 0;
left: 18px;
height: 48px;
font-size: 24px;
line-height: 48px;
text-align: left;
color: #ffeba7;
-webkit-transition: all 200ms linear;
transition: all 200ms linear;
}
.form-group input:-ms-input-placeholder {
color: #c4c3ca;
opacity: 0.7;
-webkit-transition: all 200ms linear;
transition: all 200ms linear;
}
.form-group input::-moz-placeholder {
color: #c4c3ca;
opacity: 0.7;
-webkit-transition: all 200ms linear;
transition: all 200ms linear;
}
.form-group input:-moz-placeholder {
color: #c4c3ca;
opacity: 0.7;
-webkit-transition: all 200ms linear;
transition: all 200ms linear;
}
.form-group input::-webkit-input-placeholder {
color: #c4c3ca;
opacity: 0.7;
-webkit-transition: all 200ms linear;
transition: all 200ms linear;
}
.form-group input:focus:-ms-input-placeholder {
opacity: 0;
-webkit-transition: all 200ms linear;
transition: all 200ms linear;
}
.form-group input:focus::-moz-placeholder {
opacity: 0;
-webkit-transition: all 200ms linear;
transition: all 200ms linear;
}
.form-group input:focus:-moz-placeholder {
opacity: 0;
-webkit-transition: all 200ms linear;
transition: all 200ms linear;
}
.form-group input:focus::-webkit-input-placeholder {
opacity: 0;
-webkit-transition: all 200ms linear;
transition: all 200ms linear;
}
.btn{
border-radius: 4px;
height: 44px;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
-webkit-transition : all 200ms linear;
transition: all 200ms linear;
padding: 0 30px;
letter-spacing: 1px;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-align-items: center;
-moz-align-items: center;
-ms-align-items: center;
align-items: center;
-webkit-justify-content: center;
-moz-justify-content: center;
-ms-justify-content: center;
justify-content: center;
-ms-flex-pack: center;
text-align: center;
border: none;
background-color: #ffeba7;
color: #102770;
box-shadow: 0 8px 24px 0 rgba(255,235,167,.2);
}
.btn:active,
.btn:focus{
background-color: #102770;
color: #ffeba7;
box-shadow: 0 8px 24px 0 rgba(16,39,112,.2);
}
.btn:hover{
background-color: #102770;
color: #ffeba7;
box-shadow: 0 8px 24px 0 rgba(16,39,112,.2);
}
.logo {
position: absolute;
top: 30px;
right: 30px;
display: block;
z-index: 100;
transition: all 250ms linear;
}
.logo img {
height: 26px;
width: auto;
display: block;
}
~~~
### JS
### 页面
![img.png](img.png)