优化若干功能

This commit is contained in:
2026-09-09 17:02:29 +08:00
parent fef0233cc4
commit 2f238875e1
16 changed files with 609 additions and 68 deletions
+8
View File
@@ -88,6 +88,14 @@ export function getCurrentUser() {
});
}
// 获取当前登录用户的角色 / 部门 / 职位
export function getCurrentUserProfile() {
return request({
url: '/backend/getCurrentUserProfile',
method: 'get'
});
}
// 注册
export function register(data) {
return request({
+9
View File
@@ -10,6 +10,15 @@ export function getAllMenus(params = {}) {
});
}
// 获取可分配给角色的菜单(服务端已过滤停用/隐藏菜单,cid=2 时为租户端菜单)
export function getAssignableMenus(params = {}) {
return request({
url: `/backend/assignableMenus`,
method: "get",
params,
});
}
//获取用户菜单
export function getMenus(id){
return request({
+9
View File
@@ -66,6 +66,15 @@ export function updateScheduleImages(id, images) {
})
}
// 仅更新日程的备注内容(创建 / 修改 / 完成后均可),不会影响其他字段或已绑定提醒
export function updateScheduleContent(id, content) {
return request({
url: `/backend/oa/schedule/content/${id}`,
method: 'post',
data: { content }
})
}
// 批量顺延指定日期的全部未完成日程
export function carryPendingSchedules(data) {
return request({
+58 -4
View File
@@ -75,7 +75,10 @@
<el-dropdown trigger="click" @command="handleCommand">
<span class="el-dropdown-link" style="cursor: pointer;">
<img :src="getImageUrl('user')" class="user" />
<span class="user-name">{{ displayName }}</span>
<span class="user-meta">
<span class="user-name">{{ displayName }}</span>
<span v-if="loginRoleLabel" class="user-role">{{ loginRoleLabel }}</span>
</span>
</span>
<template #dropdown>
<el-dropdown-menu>
@@ -108,11 +111,11 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import { ref, computed, reactive, onMounted, onUnmounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useAllDataStore, useMenuStore, useTabsStore } from "@/stores";
import { useAuthStore } from "@/stores/auth";
import { logout } from "@/api/login";
import { logout, getCurrentUserProfile } from "@/api/login";
import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Loading, Message } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { getMySiteReminders, readAllSiteReminders } from "@/api/sitereminder";
@@ -297,6 +300,33 @@ const companyName = computed(() => {
return authStore.user?.tenant_name || '';
});
// 当前登录用户的角色 / 部门 / 职位(来自后端 /backend/getCurrentUserProfile)
const userProfile = reactive({ department: '', position: '', role_name: '' });
// 优先展示「部门·职位」,二者皆无时回退到角色名
const loginRoleLabel = computed(() => {
const dept = (userProfile.department || '').trim();
const pos = (userProfile.position || '').trim();
if (dept || pos) {
return [dept, pos].filter(Boolean).join(' · ');
}
return (userProfile.role_name || '').trim();
});
const fetchUserProfile = async () => {
if (!authStore.token) return;
try {
const res = await getCurrentUserProfile();
if (res && res.code === 200 && res.data) {
userProfile.department = res.data.department || '';
userProfile.position = res.data.position || '';
userProfile.role_name = res.data.role_name || '';
}
} catch (e) {
console.error('Failed to fetch current user profile:', e);
}
};
const handleCollapse = () => {
store.state.isCollapse = !store.state.isCollapse;
};
@@ -436,6 +466,8 @@ onMounted(async () => {
if (authStore.token) {
// 仅在后台页面刷新/首次挂载时更新未读数量,不再自动轮询。
fetchUnreadCount();
// 拉取当前登录用户的角色 / 部门 / 职位
fetchUserProfile();
}
});
@@ -560,7 +592,7 @@ onUnmounted(() => {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary-light-7);
border-radius: 14px;
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -572,6 +604,14 @@ onUnmounted(() => {
cursor: pointer;
gap: 12px;
.user-meta {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1.2;
min-width: 0;
}
.user-name {
font-size: 14px;
color: var(--el-text-color-primary);
@@ -586,6 +626,20 @@ onUnmounted(() => {
color: #ffffff;
}
}
.user-role {
font-size: 12px;
color: var(--el-text-color-secondary);
white-space: nowrap;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
// 亮色主题下使用半透明白色
html:not(.dark) & {
color: rgba(255, 255, 255, 0.85);
}
}
}
}
@@ -73,16 +73,46 @@
<span v-if="reminderLoading" class="reminder-loading">加载中…</span>
</span>
</div>
<div v-if="schedule.content" class="meta-row">
<span class="meta-label">备注</span>
<span class="meta-content">{{ schedule.content }}</span>
</div>
<div class="meta-row">
<span class="meta-label">创建时间</span>
<span>{{ formatDateTime(schedule.create_time) }}</span>
</div>
</div>
<!-- 备注:创建 / 修改 / 完成后都可编辑更新(完成后常需补充内容) -->
<div class="detail-remark">
<div class="remark-header">
<span class="remark-title">备注</span>
<span v-if="editable" class="remark-tip">完成后仍可补充更新</span>
</div>
<el-input
v-if="editable"
v-model="remarkText"
type="textarea"
:rows="3"
maxlength="2000"
show-word-limit
placeholder="补充说明(可选)"
/>
<pre v-else-if="remarkText" class="remark-view">{{ remarkText }}</pre>
<div v-if="editable" class="remark-actions">
<el-button
size="small"
type="primary"
:disabled="!remarkDirty"
:loading="remarkSaving"
@click="saveRemark"
>保存备注</el-button
>
<el-button
v-if="remarkDirty"
size="small"
@click="resetRemark"
>取消</el-button
>
</div>
</div>
<!-- 相关图片:可选择文件上传,也可在抽屉打开时直接 Ctrl+V 粘贴截图 -->
<div class="detail-images">
<div class="images-header">
@@ -173,7 +203,7 @@ import {
RefreshLeft
} from "@element-plus/icons-vue";
import { uploadFile } from "@/api/file";
import { updateScheduleImages } from "@/api/oaSchedule";
import { updateScheduleContent, updateScheduleImages } from "@/api/oaSchedule";
import { getReminderDetail, finishReminder } from "@/api/reminder";
const props = defineProps({
@@ -297,15 +327,52 @@ const previewList = computed(() =>
imageList.value.map(img => resolveUrl(img.url))
);
// ---------- 备注:创建 / 修改 / 完成后都可编辑 ----------
const remarkText = ref("");
const remarkSaving = ref(false);
const remarkDirty = computed(
() => remarkText.value !== (props.schedule?.content || "")
);
watch(
() => props.schedule,
schedule => {
// 未编辑备注时跟随日程内容回显,避免覆盖用户正在输入的未保存内容
if (!remarkDirty.value) {
remarkText.value = schedule?.content || "";
}
imageList.value = parseImages(schedule?.images);
loadReminderInfo(schedule?.reminder_id);
},
{ immediate: true }
);
function resetRemark() {
remarkText.value = props.schedule?.content || "";
}
async function saveRemark() {
const id = props.schedule?.id;
if (!id) {
return;
}
remarkSaving.value = true;
try {
const res = await updateScheduleContent(id, remarkText.value);
if (res?.code === 200) {
ElMessage.success("备注已保存");
// 通知父组件刷新,刷新后 watch 会以最新内容回显(此时已与输入一致,不再脏)
emit("changed", id);
} else {
ElMessage.error(res?.msg || "保存失败");
}
} catch {
ElMessage.error("保存失败");
} finally {
remarkSaving.value = false;
}
}
// ---------- 绑定提醒的状态回显与反馈 ----------
const reminderInfo = ref(null);
const reminderLoading = ref(false);
@@ -553,6 +620,45 @@ function formatDateTime(value) {
word-break: break-all;
}
/* 备注编辑区:独立于元信息,创建 / 修改 / 完成后都可更新 */
.detail-remark {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid #f2f4f8;
}
.remark-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.remark-title {
font-size: 14px;
color: #303133;
}
.remark-tip {
font-size: 12px;
color: #c0c4cc;
}
.remark-view {
margin: 0;
font-size: 14px;
color: #303133;
white-space: pre-wrap;
word-break: break-all;
font-family: inherit;
}
.remark-actions {
margin-top: 10px;
display: flex;
gap: 8px;
}
.reminder-status {
display: flex;
align-items: center;
@@ -645,6 +751,18 @@ html.dark & {
.meta-label {
color: var(--el-text-color-secondary);
}
.detail-remark {
border-top-color: var(--el-border-color-lighter);
}
.remark-title {
color: var(--el-text-color-primary);
}
.remark-tip {
color: var(--el-text-color-placeholder);
}
.remark-view {
color: var(--el-text-color-primary);
}
.detail-images {
border-top-color: var(--el-border-color-lighter);
}
@@ -11,9 +11,6 @@
<el-radio-button value="calendar">日历</el-radio-button>
<el-radio-button value="list">列表</el-radio-button>
</el-radio-group>
<el-button type="primary" :icon="Plus" @click="openCreate()"
>新建日程</el-button
>
</div>
</div>
@@ -55,7 +55,7 @@
import { ref, watch, computed } from "vue";
import { ElMessage } from "element-plus";
import { getRoleById } from "@/api/role";
import { getAllMenus } from "@/api/menu";
import { getAssignableMenus } from "@/api/menu";
interface Props {
modelValue: boolean;
@@ -151,10 +151,10 @@ const loadRoleDetail = async () => {
}
};
// 加载菜单树:租户端只展示租户端菜单(cid=2)
// 加载菜单树:服务端已按 cid=2(租户端)+ 已启用 + 已显示 过滤
const loadMenus = async () => {
try {
const res = await getAllMenus({ cid: 2 });
const res = await getAssignableMenus({ cid: 2 });
if (res.code === 200) {
allMenus.value = res.data || [];
}
@@ -21,9 +21,6 @@
<el-tag :type="form.is_custom ? 'warning' : 'primary'" size="small">
{{ form.is_custom ? "自定义角色" : "系统预置租户角色" }}
</el-tag>
<div class="form-tip">
当前为租户端,仅可维护本租户的自定义角色:权限范围限定在租户端菜单,数据按租户隔离,平台端不可见。
</div>
</el-form-item>
<el-form-item label="权限设置" prop="rights">
@@ -53,7 +50,7 @@
import { ref, watch, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { createRole, updateRole } from "@/api/role";
import { getAllMenus } from "@/api/menu";
import { getAssignableMenus } from "@/api/menu";
interface Props {
modelValue: boolean;
@@ -155,10 +152,10 @@ const toggleSelectAll = () => {
// --- 数据加载与监听 ---
// 加载菜单树数据:租户端只展示租户端菜单(cid=2),平台菜单不在此分配
// 加载菜单树数据:服务端已按 cid=2(租户端)+ 已启用 + 已显示 过滤
const loadMenus = async () => {
try {
const res = await getAllMenus({ cid: 2 });
const res = await getAssignableMenus({ cid: 2 });
if (res.code === 200) {
menuTree.value = res.data || [];
}
@@ -246,9 +243,9 @@ const handleSubmit = async () => {
await formRef.value.validate();
// 只获取完全选中的节点(不包含半选父节点)
const checkedKeys = treeRef.value.getCheckedKeys();
const checkedKeys: number[] = treeRef.value.getCheckedKeys();
// 租户端只提交基础字段,cid/tenant_id/is_custom 由服务端按当前租户写入
// 租户端只提交基础字段;cid/tenant_id/is_custom 与权限合法性由服务端校验收敛
const submitData: any = {
name: form.value.name,
status: form.value.status,
@@ -285,11 +282,4 @@ const handleSubmit = async () => {
:deep(.el-tree) {
background-color: var(--el-bg-color);
}
.form-tip {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.4;
}
</style>
+110 -36
View File
@@ -1,11 +1,9 @@
<template>
<div class="home-container">
<div class="home-container" :class="{ dark: isDark }">
<div class="home-top">
<div class="toolbar">
<div class="toolbar-left">
<span class="system-name">云泽管理平台</span>
<template v-if="tenantName">
<span class="tenant-divider"></span>
<span class="tenant-name" :title="tenantName">
<el-icon class="tenant-icon"><OfficeBuilding /></el-icon>
{{ tenantName }}
@@ -41,7 +39,10 @@
<el-avatar :size="32" :src="userAvatar">
{{ userName?.charAt(0)?.toUpperCase() }}
</el-avatar>
<span class="user-name">{{ userName }}</span>
<span class="user-meta">
<span class="user-name">{{ userName }}</span>
<span v-if="userSubtitle" class="user-subtitle">{{ userSubtitle }}</span>
</span>
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
@@ -135,11 +136,15 @@
<p class="empty-tip">请联系管理员配置系统菜单</p>
</div>
</div>
<div class="home-footer">
<span class="copyright">云泽管理平台</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { ref, reactive, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
@@ -155,6 +160,7 @@ import {
} from "@element-plus/icons-vue";
import { getTenantList } from "@/api/modules";
import { getCurrentUserProfile, logout } from "@/api/login";
import { useAuthStore } from "@/stores/auth";
import { useMenuStore } from "@/stores/menu";
@@ -229,6 +235,33 @@ const userName = computed(
);
const userAvatar = computed(() => authStore.user?.avatar || "");
// 当前登录用户的角色 / 部门 / 职位(来自后端 /backend/getCurrentUserProfile)
const userProfile = reactive({ department: "", position: "", role_name: "" });
// 优先展示「部门 · 职位」,二者皆无时回退到角色名
const userSubtitle = computed(() => {
const dept = (userProfile.department || "").trim();
const pos = (userProfile.position || "").trim();
if (dept || pos) {
return [dept, pos].filter(Boolean).join(" · ");
}
return (userProfile.role_name || "").trim();
});
const fetchUserProfile = async () => {
if (!authStore.token) return;
try {
const res = await getCurrentUserProfile();
if (res && res.code === 200 && res.data) {
userProfile.department = res.data.department || "";
userProfile.position = res.data.position || "";
userProfile.role_name = res.data.role_name || "";
}
} catch (e) {
console.error("Failed to fetch current user profile:", e);
}
};
// 当前租户(企业)名称,来自后端 /backend/getCurrentUser 或登录接口返回的 tenant_name
const tenantName = computed(() => authStore.user?.tenant_name || "");
@@ -271,7 +304,7 @@ async function handleCommand(command: string) {
async function handleLogout() {
try {
const user = authStore.user;
if (user?.id) await authStore.logout(user.id);
if (user?.id) await logout(user);
} catch (error) {
console.error("退出登录接口调用失败:", error);
}
@@ -312,6 +345,10 @@ async function loadModules() {
onMounted(() => {
loadModules();
initTheme();
// 拉取当前登录用户的角色 / 部门 / 职位,用于右上角展示
if (authStore.token) {
fetchUserProfile();
}
// 兜底:若登录时未带租户名称(老会话/缓存),主动拉取一次以显示企业名称
if (!tenantName.value) {
authStore.fetchCurrentUser();
@@ -349,20 +386,6 @@ onMounted(() => {
display: flex;
align-items: center;
.system-name {
font-size: 18px;
font-weight: 600;
color: @text-main;
}
.tenant-divider {
width: 1px;
height: 16px;
background: @text-regular;
opacity: 0.25;
margin: 0 4px;
}
.tenant-name {
display: inline-flex;
align-items: center;
@@ -418,10 +441,27 @@ onMounted(() => {
border-radius: 50%;
}
.user-meta {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1.2;
min-width: 0;
}
.user-name {
font-size: 14px;
color: @text-regular;
max-width: 80px;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-subtitle {
font-size: 12px;
color: #909399;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -631,6 +671,18 @@ onMounted(() => {
}
}
.home-footer {
border-top: 1px solid #ebeef5;
padding: 18px 24px 28px;
text-align: center;
color: #909399;
.copyright {
font-size: 13px;
letter-spacing: 0.5px;
}
}
@media (max-width: 1200px) {
.modules-section {
.module-grid {
@@ -676,13 +728,6 @@ onMounted(() => {
.toolbar-left {
min-width: 0;
flex-shrink: 1;
.system-name {
font-size: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.toolbar-right {
@@ -774,7 +819,7 @@ onMounted(() => {
.user-dropdown-link {
padding: 6px 4px;
.user-name {
.user-meta {
display: none;
}
}
@@ -834,12 +879,8 @@ onMounted(() => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
.toolbar {
.toolbar-left .system-name {
color: #e0e0e0;
}
.toolbar-left .tenant-name {
color: #b0b0b0;
color: #cfd3dc;
.tenant-icon {
color: #5a8dff;
@@ -865,7 +906,11 @@ onMounted(() => {
}
.user-name {
color: #b0b0b0;
color: #cfd3dc;
}
.user-subtitle {
color: #8c8c8c;
}
}
}
@@ -873,12 +918,18 @@ onMounted(() => {
}
.modules-section {
.category-title {
color: #f5f7fa;
border-left-color: #667eea;
}
.module-card {
background: #1a1a1a;
border-color: #3d3d3d;
.card-title {
color: #e0e0e0 !important;
color: #ffffff !important;
font-weight: 700;
}
.card-desc {
@@ -890,6 +941,29 @@ onMounted(() => {
}
}
}
.home-footer {
border-top-color: #2d2d2d;
color: #8c8c8c;
}
}
// 在容器自身挂上 .dark 时同样应用暗色样式,避免仅靠 <html class="dark"> 时未命中
&.dark {
.modules-section .category-title {
color: #ffffff !important;
border-left-color: #667eea;
font-weight: 700;
}
.modules-section .module-card .card-title {
color: #ffffff !important;
font-weight: 700;
}
.modules-section .module-card .card-desc {
color: #8c8c8c !important;
}
}
}
</style>