Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
172
biji-qianduan/src/components/home/HomeHeader.vue
Normal file
172
biji-qianduan/src/components/home/HomeHeader.vue
Normal 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>
|
||||
172
biji-qianduan/src/components/home/NoteEditor.vue
Normal file
172
biji-qianduan/src/components/home/NoteEditor.vue
Normal 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(``);
|
||||
} 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>
|
||||
117
biji-qianduan/src/components/home/NoteList.vue
Normal file
117
biji-qianduan/src/components/home/NoteList.vue
Normal 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>
|
||||
140
biji-qianduan/src/components/home/NotePreview.vue
Normal file
140
biji-qianduan/src/components/home/NotePreview.vue
Normal 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>
|
||||
268
biji-qianduan/src/components/home/SidebarMenu.vue
Normal file
268
biji-qianduan/src/components/home/SidebarMenu.vue
Normal 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>
|
||||
@@ -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>
|
||||
109
biji-qianduan/src/components/home/dialogs/CreateNoteDialog.vue
Normal file
109
biji-qianduan/src/components/home/dialogs/CreateNoteDialog.vue
Normal 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>
|
||||
81
biji-qianduan/src/components/home/dialogs/MoveNoteDialog.vue
Normal file
81
biji-qianduan/src/components/home/dialogs/MoveNoteDialog.vue
Normal 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>
|
||||
91
biji-qianduan/src/components/home/dialogs/PrivacyDialog.vue
Normal file
91
biji-qianduan/src/components/home/dialogs/PrivacyDialog.vue
Normal 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>
|
||||
65
biji-qianduan/src/components/home/dialogs/RenameDialog.vue
Normal file
65
biji-qianduan/src/components/home/dialogs/RenameDialog.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
BIN
mydatabase.db
BIN
mydatabase.db
Binary file not shown.
BIN
plan/img.png
BIN
plan/img.png
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
371
plan/index.md
371
plan/index.md
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
### 页面
|
||||

|
||||
Reference in New Issue
Block a user