feat: add memo
This commit is contained in:
@@ -54,6 +54,7 @@ INSTALLED_APPS = [
|
||||
"email_notice",
|
||||
"personnel",
|
||||
"scheduler",
|
||||
"memo",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@@ -27,6 +27,7 @@ urlpatterns = [
|
||||
path("", include("finance.urls")),
|
||||
path("", include("email_notice.urls")),
|
||||
path("", include("personnel.urls")),
|
||||
path("", include("memo.urls")),
|
||||
path("api-auth/", include("rest_framework.urls")),
|
||||
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.12.2",
|
||||
"core-js": "^3.8.3",
|
||||
"dompurify": "^3.2.7",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.11.2",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"marked": "^16.3.0",
|
||||
"moment": "^2.30.1",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.5.1"
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
<el-icon><Tickets /></el-icon>
|
||||
财务记录
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/memo">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
备忘录
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
<div class="header-actions">
|
||||
<el-button type="text" class="settings-btn" @click="goToSettings" aria-label="设置">
|
||||
@@ -86,13 +90,17 @@
|
||||
<el-icon><Tickets /></el-icon>
|
||||
财务记录
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/memo">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
备忘录
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-drawer>
|
||||
</el-header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {Box, Document, House, Money, OfficeBuilding, Setting, Tickets, User, Menu} from '@element-plus/icons-vue'
|
||||
import {Box, Document, House, Money, OfficeBuilding, Setting, Tickets, User, Menu, EditPen} from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'AppHeader',
|
||||
@@ -105,7 +113,8 @@ export default {
|
||||
Setting,
|
||||
User,
|
||||
OfficeBuilding,
|
||||
Menu
|
||||
Menu,
|
||||
EditPen
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div class="memo-list">
|
||||
<div class="memo-list-header">
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="localSearchText"
|
||||
placeholder="搜索备忘录..."
|
||||
prefix-icon="Search"
|
||||
@input="handleSearchInput"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="$emit('create')"
|
||||
class="create-btn"
|
||||
:icon="Plus"
|
||||
>
|
||||
新建
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="memo-list-content">
|
||||
<el-loading v-if="loading" class="loading-container" text="加载中..." />
|
||||
|
||||
<div v-else-if="memos.length === 0" class="empty-state">
|
||||
<el-empty description="暂无备忘录" />
|
||||
</div>
|
||||
|
||||
<div v-else class="memo-items">
|
||||
<div
|
||||
v-for="memo in memos"
|
||||
:key="memo.id"
|
||||
class="memo-item"
|
||||
:class="{ active: selectedMemoId === memo.id }"
|
||||
@click="$emit('select', memo)"
|
||||
>
|
||||
<div class="memo-item-header">
|
||||
<h3 class="memo-title">{{ memo.title || '无标题' }}</h3>
|
||||
<el-dropdown trigger="click" @command="(command) => handleCommand(command, memo)">
|
||||
<el-button text size="small" class="more-btn">
|
||||
<el-icon><MoreFilled /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="delete" class="delete-item">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<p class="memo-preview">{{ memo.content_preview || '无内容' }}</p>
|
||||
|
||||
<div class="memo-meta">
|
||||
<span class="memo-author">{{ memo.created_by_name }}</span>
|
||||
<span class="memo-date">{{ formatDate(memo.updated_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { Plus, MoreFilled, Delete } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'MemoList',
|
||||
props: {
|
||||
memos: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
selectedMemoId: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['search', 'select', 'create', 'delete'],
|
||||
setup(props, { emit }) {
|
||||
const localSearchText = ref(props.searchText)
|
||||
let searchTimeout = null
|
||||
|
||||
const handleSearchInput = () => {
|
||||
clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(() => {
|
||||
emit('search', localSearchText.value)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const handleCommand = async (command, memo) => {
|
||||
if (command === 'delete') {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除备忘录 "${memo.title}" 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
emit('delete', memo.id)
|
||||
} catch {
|
||||
// 用户取消删除
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
const date = new Date(dateStr)
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
|
||||
// 如果是今天
|
||||
if (diff < 24 * 60 * 60 * 1000 && now.getDate() === date.getDate()) {
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 如果是今年
|
||||
if (now.getFullYear() === date.getFullYear()) {
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// 其他情况显示完整日期
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
watch(() => props.searchText, (newVal) => {
|
||||
localSearchText.value = newVal
|
||||
})
|
||||
|
||||
return {
|
||||
localSearchText,
|
||||
handleSearchInput,
|
||||
handleCommand,
|
||||
formatDate,
|
||||
Plus,
|
||||
MoreFilled,
|
||||
Delete
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.memo-list {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.memo-list-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.memo-list-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.memo-items {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.memo-item {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.memo-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.memo-item.active {
|
||||
background-color: #e8f4fd;
|
||||
border-left: 3px solid #409eff;
|
||||
}
|
||||
|
||||
.memo-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.memo-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
padding-right: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
color: #909399;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.memo-item:hover .more-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.memo-preview {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
line-height: 1.4;
|
||||
margin: 0 0 8px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.memo-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.memo-author {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.memo-date {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.delete-item {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.memo-list-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.memo-list-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.memo-list-content::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.memo-list-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
</style>
|
||||
@@ -11,6 +11,7 @@ import FinanceRecordDetail from '../views/FinanceRecordDetail.vue'
|
||||
import PersonnelList from '../views/PersonnelList.vue'
|
||||
import DepartmentProjectGroupManagement from '../views/DepartmentProjectGroupManagement.vue'
|
||||
import Settings from '../views/Settings.vue'
|
||||
import Memo from '../views/Memo.vue'
|
||||
import { authService } from '@/services/api'
|
||||
|
||||
const routes = [
|
||||
@@ -75,6 +76,11 @@ const routes = [
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: Settings
|
||||
},
|
||||
{
|
||||
path: '/memo',
|
||||
name: 'Memo',
|
||||
component: Memo
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -428,3 +428,52 @@ export const projectGroupService = {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 备忘录管理服务
|
||||
export const memoService = {
|
||||
// 获取所有备忘录
|
||||
getMemos(search = '') {
|
||||
const params = search ? { search } : {}
|
||||
return apiClient.get('/memos/', { params })
|
||||
},
|
||||
|
||||
// 获取备忘录详情
|
||||
getMemo(id) {
|
||||
return apiClient.get(`/memos/${id}/`)
|
||||
},
|
||||
|
||||
// 创建新备忘录
|
||||
createMemo(memo) {
|
||||
return apiClient.post('/memos/', memo)
|
||||
},
|
||||
|
||||
// 更新备忘录
|
||||
updateMemo(id, memo) {
|
||||
return apiClient.put(`/memos/${id}/`, memo)
|
||||
},
|
||||
|
||||
// 删除备忘录
|
||||
deleteMemo(id) {
|
||||
return apiClient.delete(`/memos/${id}/`)
|
||||
},
|
||||
|
||||
// 上传图片到备忘录
|
||||
uploadImage(memoId, file, altText = '') {
|
||||
const formData = new FormData()
|
||||
formData.append('image', file)
|
||||
formData.append('alt_text', altText)
|
||||
|
||||
return apiClient.post(`/memos/${memoId}/upload_image/`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 删除备忘录中的图片
|
||||
deleteImage(memoId, imageId) {
|
||||
return apiClient.delete(`/memos/${memoId}/delete_image/`, {
|
||||
data: { image_id: imageId }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<AppHeader />
|
||||
<div class="memo-container">
|
||||
<div class="memo-sidebar">
|
||||
<MemoList
|
||||
:memos="memos"
|
||||
:loading="loading"
|
||||
:search-text="searchText"
|
||||
@search="handleSearch"
|
||||
@select="handleSelectMemo"
|
||||
@create="handleCreateMemo"
|
||||
@delete="handleDeleteMemo"
|
||||
:selected-memo-id="selectedMemoId"
|
||||
/>
|
||||
</div>
|
||||
<div class="memo-editor">
|
||||
<MemoEditor
|
||||
:memo="selectedMemo"
|
||||
:loading="editorLoading"
|
||||
@save="handleSaveMemo"
|
||||
@upload-image="handleUploadImage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted, watch, nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import MemoList from '../components/MemoList.vue'
|
||||
import MemoEditor from '../components/MemoEditor.vue'
|
||||
import { memoService } from '@/services/api'
|
||||
import { API_BASE_URL_WITHOUT_API } from '@/services/api'
|
||||
import AppHeader from "@/components/AppHeader.vue";
|
||||
|
||||
export default {
|
||||
name: 'Memo',
|
||||
components: {
|
||||
AppHeader,
|
||||
MemoList,
|
||||
MemoEditor
|
||||
},
|
||||
setup() {
|
||||
const memos = ref([])
|
||||
const selectedMemo = ref(null)
|
||||
const selectedMemoId = ref(null)
|
||||
const loading = ref(false)
|
||||
const editorLoading = ref(false)
|
||||
const searchText = ref('')
|
||||
|
||||
const loadMemos = async (search = '') => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await memoService.getMemos(search)
|
||||
memos.value = response.data.results || response.data
|
||||
} catch (error) {
|
||||
console.error('加载备忘录失败:', error)
|
||||
ElMessage.error('加载备忘录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = (text) => {
|
||||
searchText.value = text
|
||||
loadMemos(text)
|
||||
}
|
||||
|
||||
const handleSelectMemo = async (memo) => {
|
||||
if (selectedMemoId.value === memo.id) return
|
||||
|
||||
try {
|
||||
editorLoading.value = true
|
||||
selectedMemoId.value = memo.id
|
||||
const response = await memoService.getMemo(memo.id)
|
||||
selectedMemo.value = response.data
|
||||
} catch (error) {
|
||||
console.error('加载备忘录详情失败:', error)
|
||||
ElMessage.error('加载备忘录详情失败')
|
||||
} finally {
|
||||
editorLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateMemo = async () => {
|
||||
try {
|
||||
// 先清空当前选择
|
||||
selectedMemo.value = null
|
||||
selectedMemoId.value = null
|
||||
|
||||
// 等待下一个 tick 确保 DOM 更新完成
|
||||
await nextTick()
|
||||
|
||||
const newMemo = {
|
||||
id: null,
|
||||
title: '新建备忘录',
|
||||
content: '',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
|
||||
selectedMemo.value = newMemo
|
||||
} catch (error) {
|
||||
console.error('创建新备忘录失败:', error)
|
||||
ElMessage.error('创建新备忘录失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveMemo = async (memo, isAutoSave = false) => {
|
||||
try {
|
||||
editorLoading.value = true
|
||||
let response
|
||||
const isNewMemo = !memo.id || memo.id === null || memo.id === undefined
|
||||
|
||||
if (isNewMemo) {
|
||||
response = await memoService.createMemo(memo)
|
||||
selectedMemoId.value = response.data.id
|
||||
} else {
|
||||
response = await memoService.updateMemo(memo.id, memo)
|
||||
}
|
||||
|
||||
selectedMemo.value = response.data
|
||||
|
||||
// 只在手动保存时显示成功消息
|
||||
if (!isAutoSave) {
|
||||
ElMessage.success('保存成功')
|
||||
}
|
||||
|
||||
// 只在创建新备忘录或手动保存时重新加载列表
|
||||
// 自动保存时避免重新加载以保持用户的滚动位置和焦点
|
||||
if (isNewMemo || !isAutoSave) {
|
||||
await loadMemos(searchText.value)
|
||||
} else {
|
||||
// 自动保存时只更新当前备忘录在列表中的显示(如果需要的话)
|
||||
const memoIndex = memos.value.findIndex(m => m.id === memo.id)
|
||||
if (memoIndex !== -1) {
|
||||
// 更新列表中的备忘录信息,但不重新加载整个列表
|
||||
memos.value[memoIndex] = {
|
||||
...memos.value[memoIndex],
|
||||
title: response.data.title,
|
||||
content_preview: response.data.content?.substring(0, 100) || '',
|
||||
updated_at: response.data.updated_at
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存备忘录失败:', error)
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
editorLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteMemo = async (memoId) => {
|
||||
try {
|
||||
await memoService.deleteMemo(memoId)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
// 如果删除的是当前选中的备忘录,清空编辑器
|
||||
if (selectedMemoId.value === memoId) {
|
||||
selectedMemo.value = null
|
||||
selectedMemoId.value = null
|
||||
}
|
||||
|
||||
// 重新加载列表
|
||||
await loadMemos(searchText.value)
|
||||
} catch (error) {
|
||||
console.error('删除备忘录失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadImage = async (file, memoId) => {
|
||||
try {
|
||||
const response = await memoService.uploadImage(memoId, file)
|
||||
|
||||
// 上传成功后,将图片插入到编辑器中
|
||||
if (response.data && response.data.image) {
|
||||
// 确保图片路径是完整的URL
|
||||
const imageUrl = response.data.image.startsWith('http')
|
||||
? response.data.image
|
||||
: `${API_BASE_URL_WITHOUT_API}${response.data.image}`
|
||||
const imageMarkdown = `\n`
|
||||
|
||||
// 找到文本域并插入图片标记
|
||||
const textarea = document.querySelector('.content-textarea textarea')
|
||||
if (textarea && textarea.parentNode) {
|
||||
const start = textarea.selectionStart || textarea.value.length
|
||||
const end = textarea.selectionEnd || textarea.value.length
|
||||
const currentContent = selectedMemo.value.content || ''
|
||||
|
||||
selectedMemo.value.content =
|
||||
currentContent.substring(0, start) +
|
||||
imageMarkdown +
|
||||
currentContent.substring(end)
|
||||
|
||||
// 更新光标位置
|
||||
setTimeout(() => {
|
||||
if (textarea && textarea.parentNode && textarea.focus && textarea.setSelectionRange) {
|
||||
textarea.focus()
|
||||
textarea.setSelectionRange(start + imageMarkdown.length, start + imageMarkdown.length)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
ElMessage.success('图片上传成功')
|
||||
}
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('上传图片失败:', error)
|
||||
ElMessage.error('上传图片失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMemos()
|
||||
})
|
||||
|
||||
return {
|
||||
memos,
|
||||
selectedMemo,
|
||||
selectedMemoId,
|
||||
loading,
|
||||
editorLoading,
|
||||
searchText,
|
||||
handleSearch,
|
||||
handleSelectMemo,
|
||||
handleCreateMemo,
|
||||
handleSaveMemo,
|
||||
handleDeleteMemo,
|
||||
handleUploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.memo-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 60px);
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.memo-sidebar {
|
||||
width: 350px;
|
||||
min-width: 300px;
|
||||
border-right: 1px solid #e4e7ed;
|
||||
background-color: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.memo-editor {
|
||||
flex: 1;
|
||||
background-color: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.memo-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.memo-sidebar {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.memo-editor {
|
||||
height: calc(100vh - 260px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user