财务门户设计

This commit is contained in:
2026-02-19 21:11:44 +08:00
parent df7cdb9716
commit 8e8fffc5aa
7 changed files with 956 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author gaoxq
*/
import { defHttp } from '@jeesite/core/utils/http/axios';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { BasicModel, Page } from '@jeesite/core/api/model/baseModel';
const { adminPath } = useGlobSetting();
export interface BizNotes extends BasicModel<BizNotes> {
createTime?: string; // 创建时间
content: string; // 便签内容
ustatus: string; // 便签状态
updateTime: string; // 更新时间
createUser?: string; // 创建用户
}
export const bizNotesList = (params?: BizNotes | any) =>
defHttp.get<BizNotes>({ url: adminPath + '/biz/notes/list', params });
export const bizNotesListAll = (params?: BizNotes | any) =>
defHttp.get<BizNotes[]>({ url: adminPath + '/biz/notes/listAll', params });
export const bizNotesListData = (params?: BizNotes | any) =>
defHttp.post<Page<BizNotes>>({ url: adminPath + '/biz/notes/listData', params });
export const bizNotesForm = (params?: BizNotes | any) =>
defHttp.get<BizNotes>({ url: adminPath + '/biz/notes/form', params });
export const bizNotesSave = (params?: any, data?: BizNotes | any) =>
defHttp.postJson<BizNotes>({ url: adminPath + '/biz/notes/save', params, data });
export const bizNotesDelete = (params?: BizNotes | any) =>
defHttp.get<BizNotes>({ url: adminPath + '/biz/notes/delete', params });

View File

@@ -0,0 +1,136 @@
<template>
<div class="work-green-container">
<div class="header-section">
<div class="card-item col-1-85 section-1-item">
</div>
<div class="card-item col-1-15 section-1-item">
头部第二列内容区域
</div>
</div>
<div class="main-container">
<div class="upper-section">
<div class="card-item col-3-equal">
</div>
<div class="card-item col-3-equal">
<HostInfo />
</div>
<div class="card-item col-3-equal">
上部第三列内容区域
</div>
</div>
<div class="middle-section">
<div class="card-item col-2-equal">
</div>
<div class="card-item col-2-equal">
中部第二列内容区域
</div>
</div>
<div class="lower-section">
<div class="card-item col-2-equal">
下部第一列内容区域
</div>
<div class="card-item col-2-equal">
下部第二列内容区域
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup name="WorkGreenPage">
import { Icon } from '@jeesite/core/components/Icon';
import { ref, onMounted, watch, onUnmounted, nextTick } from 'vue';
import { BasicForm, FormProps } from '@jeesite/core/components/Form';
</script>
<style scoped>
.work-green-container {
height: calc(100vh - 100px);
display: flex;
flex-direction: column;
gap: 8px;
padding: 2px;
box-sizing: border-box;
overflow: hidden;
}
.header-section {
height: 10%;
display: flex;
gap: 8px;
box-sizing: border-box;
}
.main-container {
height: 90%;
display: flex;
flex-direction: column;
gap: 8px;
box-sizing: border-box;
}
.upper-section, .middle-section, .lower-section {
height: calc(100% / 3);
display: flex;
gap: 8px;
box-sizing: border-box;
}
.card-item {
height: 100%;
overflow: auto;
box-sizing: border-box;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.section-1-item {
border-color: #b3d8ff;
border-width: 1px;
border-style: solid;
}
.col-1-85 {
width: 85%;
}
.col-1-15 {
width: 15%;
}
.col-3-equal {
flex: 1;
width: 33.333%;
}
.col-2-equal {
flex: 1;
width: 50%;
}
@media (max-width: 768px) {
.header-section, .upper-section, .middle-section, .lower-section {
flex-direction: column;
height: auto !important;
}
.col-1-75, .col-1-25, .col-3-equal, .col-2-equal {
width: 100%;
height: 100px;
}
.work-green-container {
height: auto;
min-height: calc(100vh - 100px);
}
.main-container {
height: auto;
}
}
</style>

View File

@@ -0,0 +1,531 @@
<template>
<div class="note-manager">
<div class="tabs-sidebar">
<div class="tab-item" @click="activeTab = 'todo'" :class="{ active: activeTab === 'todo' }">待开始 ({{ todoList.length }})</div>
<div class="tab-item" @click="activeTab = 'doing'" :class="{ active: activeTab === 'doing' }">进行中 ({{ doingList.length }})</div>
<div class="tab-item" @click="activeTab = 'done'" :class="{ active: activeTab === 'done' }">已完成 ({{ doneList.length }})</div>
</div>
<div class="content-container">
<div class="create-note" v-if="activeTab === 'todo'">
<textarea v-model="newNoteText" placeholder="请输入便签内容..." class="note-input"></textarea>
<button @click="addNote" class="add-btn">添加便签</button>
</div>
<div class="note-grid" v-if="activeTab === 'todo'">
<div class="note-card" v-for="note in todoList" :key="`todo-${note.id}`">
<ATooltip :title="note.content" placement="bottom" class="tooltip-custom">
<div class="note-card-body">
<div class="note-card-content-box">
<p class="ellipsis-3rows">{{ note.content }}</p>
</div>
<div class="note-card-time-row">
<span class="time-left">{{ note.createTime }}</span>
<span class="time-split"></span>
</div>
</div>
</ATooltip>
<div class="note-card-divider"></div>
<div class="note-card-actions">
<button class="start-btn" @click.stop="changeStatus(note, 'todo', 'doing')">开始</button>
<button class="delete-btn" @click.stop="deleteNote(note, 'todo')">删除</button>
</div>
</div>
<div class="empty-tip" v-if="todoList.length === 0">暂无待开始便签</div>
</div>
<div class="search-note" v-if="activeTab === 'doing'">
<a-input-search
v-model:value="doingSearchText"
placeholder="请输入关键词搜索便签"
enter-button
@search="onSearchDoing()"
/>
</div>
<div class="note-grid" v-if="activeTab === 'doing'">
<div class="note-card" v-for="note in doingList" :key="`doing-${note.id}`">
<ATooltip :title="note.content" placement="bottom" class="tooltip-custom">
<div class="note-card-body">
<div class="note-card-content-box">
<p class="ellipsis-3rows">{{ note.content }}</p>
</div>
<div class="note-card-time-row">
<span class="time-left">{{ note.createTime }}</span>
<span class="time-split"></span>
<span class="time-right">{{ note.updateTime }}</span>
</div>
</div>
</ATooltip>
<div class="note-card-divider"></div>
<div class="note-card-actions">
<button class="complete-btn" @click.stop="changeStatus(note, 'doing', 'done')">完成</button>
<button class="back-btn" @click.stop="changeStatus(note, 'doing', 'todo')">回退</button>
</div>
</div>
<div class="empty-tip" v-if="doingList.length === 0">暂无进行中便签</div>
</div>
<div class="search-note" v-if="activeTab === 'done'">
<a-input-search
v-model:value="doneSearchText"
placeholder="请输入关键词搜索便签"
enter-button
@search="onSearchDone()"
/>
</div>
<div class="note-grid" v-if="activeTab === 'done'">
<div class="note-card" v-for="note in doneList" :key="`done-${note.id}`">
<ATooltip :title="note.content" placement="bottom" class="tooltip-custom">
<div class="note-card-body">
<div class="note-card-content-box">
<p class="ellipsis-3rows">{{ note.content }}</p>
</div>
<div class="note-card-time-row">
<span class="time-left">{{ note.createTime }}</span>
<span class="time-split"></span>
<span class="time-right">{{ note.updateTime }}</span>
</div>
</div>
</ATooltip>
<div class="note-card-divider"></div>
<div class="note-card-actions">
<button class="back-btn" @click.stop="changeStatus(note, 'done', 'doing')">回退</button>
<button class="delete-btn" @click.stop="deleteNote(note, 'done')">删除</button>
</div>
</div>
<div class="empty-tip" v-if="doneList.length === 0">暂无已完成便签</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { Tooltip as ATooltip, message, Modal } from 'ant-design-vue'
import { useUserStore } from '@jeesite/core/store/modules/user';
import { bizNotesListAll, bizNotesSave, bizNotesDelete } from '@jeesite/biz/api/biz/notes';
const userStore = useUserStore();
const userinfo = computed(() => userStore.getUserInfo);
const activeTab = ref('todo');
const newNoteText = ref('');
const doingSearchText = ref('');
const doneSearchText = ref('');
const todoList = ref([]);
const doingList = ref([]);
const doneList = ref([]);
watch(activeTab, (newTab) => {
if (newTab === 'doing') {
doingSearchText.value = '';
fetchDataList({ ustatus: 'doing' });
} else if (newTab === 'done') {
doneSearchText.value = '';
fetchDataList({ ustatus: 'done' });
} else if (newTab === 'todo') {
fetchDataList({ ustatus: 'todo' });
}
});
const onSearchDoing = () => {
fetchDataList({
content: doingSearchText.value,
ustatus: 'doing',
});
};
const onSearchDone = () => {
fetchDataList({
content: doneSearchText.value,
ustatus: 'done',
});
};
const fetchDataList = async (customParams = {}) => {
try {
const reqParams = {
...customParams,
createUser: userinfo.value?.loginCode,
}
const result = await bizNotesListAll(reqParams);
if (reqParams.ustatus === 'todo') {
todoList.value = result ? result.filter(item => item.ustatus === 'todo') : [];
} else if (reqParams.ustatus === 'doing') {
doingList.value = result ? result.filter(item => item.ustatus === 'doing') : [];
} else if (reqParams.ustatus === 'done') {
doneList.value = result ? result.filter(item => item.ustatus === 'done') : [];
} else {
todoList.value = result ? result.filter(item => item.ustatus === 'todo') : [];
doingList.value = result ? result.filter(item => item.ustatus === 'doing') : [];
doneList.value = result ? result.filter(item => item.ustatus === 'done') : [];
}
} catch (error) {
console.error('获取便签列表失败:', error);
if (customParams.ustatus === 'doing') {
doingList.value = [];
} else if (customParams.ustatus === 'done') {
doneList.value = [];
} else if (customParams.ustatus === 'todo') {
todoList.value = [];
}
}
};
const addNote = async () => {
const text = newNoteText.value.trim()
if (!text) {
message.warning("请输入便签内容后继续...");
return
}
try {
const newNote = {
content: text,
ustatus: 'todo',
createUser: userinfo.value.loginCode,
};
const res = await bizNotesSave(newNote);
message.success(res?.message || '添加便签成功');
fetchDataList({ ustatus: 'todo' });
newNoteText.value = '';
} catch (error) {
console.error('添加便签失败:', error);
}
};
const changeStatus = async (note, fromStatus, toStatus) => {
try {
const updateData = {
id: note.id,
content: note.content,
ustatus: toStatus,
updateTime: new Date().toISOString()
};
const res = await bizNotesSave(updateData);
message.success(res?.message || '状态更新成功');
fetchDataList({ ustatus: fromStatus });
fetchDataList({ ustatus: toStatus });
} catch (error) {
console.error('更新便签状态失败:', error);
}
};
const deleteNote = async (note, fromStatus) => {
Modal.confirm({
title: '温馨提示',
content: '您确定要删除当前便签吗?',
okText: '确认',
cancelText: '取消',
okType: 'danger',
width: 420,
onOk: async () => {
try {
const res = await bizNotesDelete({ id: note.id });
message.success(res?.message || '删除便签成功');
fetchDataList({ ustatus: fromStatus });
} catch (error) {
console.error('删除便签失败:', error);
}
},
onCancel: () => {
message.info('已取消删除操作');
}
});
};
onMounted(() => {
fetchDataList({});
});
</script>
<style scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,body,#app {
width: 100%;
height: 100%;
}
.note-manager {
width: 100%;
height: calc(100vh - 445px);
padding: 2px;
background: #fafafa;
display: flex;
gap: 4px;
}
.tabs-sidebar {
width: 120px;
height: 100%;
border: 1px solid #cce5ff;
border-radius: 4px;
background: #fff;
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 0;
}
.tab-item {
padding: 8px 6px;
font-size: 12px;
cursor: pointer;
color: #666;
text-align: center;
position: relative;
transition: all 0.2s ease;
}
.tab-item:hover {
background: #f0f9ff;
}
.tab-item.active {
color: #409eff;
font-weight: 500;
background: #e6f7ff;
}
.tab-item.active::before {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 3px;
background: #409eff;
}
.content-container {
flex: 1;
height: 100%;
border: 1px solid #cce5ff;
border-radius: 4px;
background: #fff;
overflow: hidden;
display: flex;
flex-direction: column;
}
.create-note, .search-note {
padding: 10px;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
gap: 8px;
}
.note-input, .search-input {
height: 70px;
width: 100%;
padding: 4px 10px;
border: 1px solid #ddd;
border-radius: 8px;
resize: vertical;
font-size: 13px;
line-height: 1.5;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.search-input {
height: 40px;
resize: none;
}
.add-btn {
height: 36px;
width: 100%;
background: #409eff;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.add-btn:hover {
background: #66b1ff;
}
.note-grid {
flex: 1;
width: 100%;
padding: 10px;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-gap: 10px;
align-content: flex-start;
}
.note-card {
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
padding: 4px;
height: 100%;
display: flex;
flex-direction: column;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: all 0.3s ease;
cursor: default;
}
.note-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0,0,0,0.1);
border-color: #409eff;
}
.note-card-body {
flex: 1;
display: flex;
flex-direction: column;
margin-bottom: 8px;
}
.note-card-content-box {
flex: 1;
min-height: 3.6em;
max-height: 3.6em;
border: 1px solid #eee;
border-radius: 4px;
padding: 6px 8px;
margin-bottom: 6px;
display: flex;
align-items: center;
background: #fdfdfd;
}
.ellipsis-3rows {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
line-height: 1.4;
width: 100%;
}
.note-card-time-row {
display: flex;
align-items: center;
justify-content: space-between;
height: 16px;
font-size: 11px;
color: #999;
}
.time-left,
.time-right {
white-space: nowrap;
}
.time-split {
flex: 1;
height: 1px;
background: #eee;
margin: 0 6px;
}
.note-card-divider {
height: 1px;
width: 100%;
background: #f0f0f0;
margin: 0 0 8px 0;
}
.note-card-actions {
display: flex;
gap: 4px;
justify-content: flex-end;
height: 24px;
flex-shrink: 0;
padding-right: 2px;
}
.start-btn, .complete-btn, .back-btn, .delete-btn {
padding: 0 6px;
font-size: 11px;
border-radius: 3px;
width: auto;
min-width: 38px;
cursor: pointer;
border: none;
transition: background-color 0.2s;
height: 100%;
}
.start-btn { background: #409eff; color: #fff; }
.start-btn:hover { background: #66b1ff; }
.complete-btn { background: #67c23a; color: #fff; }
.complete-btn:hover { background: #85ce61; }
.back-btn { background: #e6a23c; color: #fff; }
.back-btn:hover { background: #ebb563; }
.delete-btn { background: #ff4d4f; color: #fff; }
.delete-btn:hover { background: #ff7875; }
.empty-tip {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 14px;
color: #999;
grid-column: 1 / -1;
}
.note-grid::-webkit-scrollbar {
width: 6px;
background: transparent;
}
.note-grid::-webkit-scrollbar-thumb {
background: #ddd;
border-radius: 3px;
}
.tooltip-custom :deep(.ant-tooltip-inner) {
width: 100% !important;
max-width: 100% !important;
white-space: pre-wrap;
word-wrap: break-word;
padding: 8px 12px;
font-size: 13px;
line-height: 1.5;
}
.tooltip-custom :deep(.ant-tooltip-placement-bottom) {
margin-top: 8px;
}
@media (max-width: 1400px) {
.note-grid { grid-template-columns: repeat(4, 1fr); }
}
@media (max-width: 1000px) {
.note-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 768px) {
.note-grid { grid-template-columns: repeat(2, 1fr); }
.tabs-sidebar { width: 100px; }
}
@media (max-width: 480px) {
.note-manager { flex-direction: column; }
.tabs-sidebar {
width: 100%;
height: auto;
flex-direction: row;
justify-content: space-around;
}
.tab-item.active::before {
width: 100%;
height: 3px;
top: auto;
bottom: 0;
}
.note-grid { grid-template-columns: 1fr; }
.note-input { height: 60px; }
.search-input { height: 36px; }
}
</style>