优化若干功能
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# CodeGraph data files — local to each machine, not for committing.
|
||||
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||
*
|
||||
!.gitignore
|
||||
@@ -88,6 +88,14 @@ export function getCurrentUser() {
|
||||
});
|
||||
}
|
||||
|
||||
// 获取当前登录用户的角色 / 部门 / 职位
|
||||
export function getCurrentUserProfile() {
|
||||
return request({
|
||||
url: '/backend/getCurrentUserProfile',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
// 注册
|
||||
export function register(data) {
|
||||
return request({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -195,6 +195,100 @@ func (c *BackendAuthController) GetCurrentUser() {
|
||||
})
|
||||
}
|
||||
|
||||
// GetCurrentUserProfile 当前登录用户的角色 / 部门 / 职位
|
||||
// GET /backend/getCurrentUserProfile
|
||||
// 返回 department / position(来自员工档案,按登录账号 + 租户匹配);
|
||||
// 若均无,则回退到角色名(role_name)。
|
||||
func (c *BackendAuthController) GetCurrentUserProfile() {
|
||||
authHeader := c.Ctx.Request.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "未登录"})
|
||||
return
|
||||
}
|
||||
authParts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(authParts) != 2 || authParts[0] != "Bearer" {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "认证信息格式错误"})
|
||||
return
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(authParts[1])
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "无效的token"})
|
||||
return
|
||||
}
|
||||
if claims.UserType != "backend" && claims.UserType != "app" {
|
||||
c.serveJSON(map[string]interface{}{"code": 403, "msg": "无权访问"})
|
||||
return
|
||||
}
|
||||
|
||||
var tenantUser models.SystemTenantUser
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", claims.UserID).
|
||||
Filter("tid", claims.TenantId).
|
||||
One(&tenantUser); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
account := ""
|
||||
if tenantUser.Account != nil {
|
||||
account = strings.TrimSpace(*tenantUser.Account)
|
||||
}
|
||||
name := ""
|
||||
if tenantUser.Name != nil {
|
||||
name = strings.TrimSpace(*tenantUser.Name)
|
||||
}
|
||||
|
||||
// 角色名:按 group_id 查询本租户下的租户角色(cid=2)
|
||||
roleName := ""
|
||||
if tenantUser.GroupID > 0 {
|
||||
var role models.AdminRole
|
||||
if err := models.Orm.QueryTable(new(models.AdminRole)).
|
||||
Filter("id", tenantUser.GroupID).
|
||||
Filter("cid", 2).
|
||||
Filter("tenant_id", claims.TenantId).
|
||||
One(&role); err == nil {
|
||||
roleName = role.Name
|
||||
}
|
||||
}
|
||||
|
||||
// 部门 / 职位:来自员工档案,按账号 + 租户匹配
|
||||
department := ""
|
||||
position := ""
|
||||
if account != "" {
|
||||
var emp models.BackendEmployee
|
||||
qs := models.Orm.QueryTable(new(models.BackendEmployee)).Filter("account", account)
|
||||
if claims.TenantId > 0 {
|
||||
qs = qs.Filter("tid", int(claims.TenantId))
|
||||
}
|
||||
if err := qs.One(&emp); err == nil {
|
||||
if emp.Department != nil {
|
||||
department = strings.TrimSpace(*emp.Department)
|
||||
}
|
||||
if emp.Position != nil {
|
||||
position = strings.TrimSpace(*emp.Position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 员工档案的 department 字段存的是组织ID,需转换为组织名称展示
|
||||
if department != "" {
|
||||
orgNames := (&BackendEmployeeFileController{}).efOrgNameMap(int(claims.TenantId))
|
||||
department = efOrgNameByID(orgNames, department)
|
||||
}
|
||||
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"account": account,
|
||||
"name": name,
|
||||
"role_name": roleName,
|
||||
"department": department,
|
||||
"position": position,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SendLoginCode 发送 backend 登录验证码
|
||||
func (c *BackendAuthController) SendLoginCode() {
|
||||
var req struct {
|
||||
|
||||
@@ -232,6 +232,49 @@ func (c *BackendMenuController) GetAllMenus() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// filterAssignableMenus 仅保留「已启用且已显示」的菜单。
|
||||
// 停用或隐藏的菜单不参与角色权限分配,直接在服务端屏蔽,不依赖前端过滤。
|
||||
func filterAssignableMenus(menus []models.SystemMenu) []models.SystemMenu {
|
||||
out := make([]models.SystemMenu, 0, len(menus))
|
||||
for _, m := range menus {
|
||||
if m.Status != 1 {
|
||||
continue
|
||||
}
|
||||
// is_visible 为空视为显示
|
||||
if m.IsVisible != nil && *m.IsVisible == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetAssignableMenus 获取可分配给角色的菜单树
|
||||
// 过滤规则(均在服务端完成):
|
||||
// 1. cid:按菜单所属端过滤(1=平台端 2=租户端,不传则不过滤)
|
||||
// 2. status=1:仅已启用的菜单
|
||||
// 3. is_visible != 0:仅已显示的菜单(为空视为显示)
|
||||
//
|
||||
// 父级菜单被过滤时,其子菜单一并不可分配(菜单树按 pid 构建,父级缺失则整条分支不会返回)。
|
||||
// GET /backend/assignableMenus?cid=2
|
||||
func (c *BackendMenuController) GetAssignableMenus() {
|
||||
var menus []models.SystemMenu
|
||||
cid, _ := c.GetInt("cid")
|
||||
|
||||
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).All(&menus); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取菜单失败: " + err.Error(), "data": nil}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if cid == 1 || cid == 2 {
|
||||
menus = filterMenusByView(menus, cid)
|
||||
}
|
||||
menus = filterAssignableMenus(menus)
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": buildMenuTree(menus, 0)}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *BackendMenuController) GetAllBackendMenus() {
|
||||
var menus []models.SystemMenu
|
||||
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).All(&menus); err != nil {
|
||||
|
||||
@@ -702,6 +702,54 @@ func (c *BackendOaScheduleController) SetImages() {
|
||||
c.oasOk(map[string]interface{}{"images": parseOaScheduleImages(imagesJSON)})
|
||||
}
|
||||
|
||||
// UpdateContent POST /backend/oa/schedule/content/:id 仅更新备注内容。
|
||||
// 与 SetImages 类似,只改写 Content 字段,不影响标题、日期、提醒等其他字段,
|
||||
// 因此即便日程已完成、已绑定提醒,补充备注也不会破坏既有状态。
|
||||
func (c *BackendOaScheduleController) UpdateContent() {
|
||||
claims, err := c.oaScheduleClaims()
|
||||
if err != nil {
|
||||
c.oasJsonErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.oasJsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if len(raw) > 0 && json.Unmarshal(raw, &payload) != nil {
|
||||
c.oasJsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var item models.OaSchedule
|
||||
err = models.Orm.QueryTable(new(models.OaSchedule)).
|
||||
Filter("id", id).
|
||||
Filter("is_deleted", 0).
|
||||
Filter("tid", claims.TenantId).
|
||||
Filter("user_id", claims.UserID).
|
||||
One(&item)
|
||||
if err != nil {
|
||||
c.oasJsonErr(404, 404, "日程不存在")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
item.Content = payload.Content
|
||||
item.UpdateTime = &now
|
||||
if _, err := models.Orm.Update(&item, "Content", "UpdateTime"); err != nil {
|
||||
c.oasJsonErr(500, 500, "保存失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.oasOk(map[string]interface{}{"content": item.Content})
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/oa/schedule/delete/:id 软删除日程。
|
||||
func (c *BackendOaScheduleController) Delete() {
|
||||
claims, err := c.oaScheduleClaims()
|
||||
|
||||
@@ -25,6 +25,92 @@ type backendRolePayload struct {
|
||||
Rights interface{} `json:"rights"`
|
||||
}
|
||||
|
||||
// parseSubmittedIDs 将前端提交的 rights 解析为菜单 ID 列表(兼容 JSON 数组 / 逗号分隔字符串)。
|
||||
func parseSubmittedIDs(v interface{}) []uint64 {
|
||||
ids := make([]uint64, 0)
|
||||
switch t := v.(type) {
|
||||
case []interface{}:
|
||||
for _, item := range t {
|
||||
switch n := item.(type) {
|
||||
case float64:
|
||||
ids = append(ids, uint64(n))
|
||||
case json.Number:
|
||||
if i, err := n.Int64(); err == nil {
|
||||
ids = append(ids, uint64(i))
|
||||
}
|
||||
case string:
|
||||
if i, err := strconv.ParseUint(strings.TrimSpace(n), 10, 64); err == nil {
|
||||
ids = append(ids, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
case []uint64:
|
||||
ids = append(ids, t...)
|
||||
case string:
|
||||
var arr []uint64
|
||||
if err := json.Unmarshal([]byte(t), &arr); err == nil {
|
||||
ids = append(ids, arr...)
|
||||
} else {
|
||||
for _, part := range strings.Split(t, ",") {
|
||||
if i, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64); err == nil {
|
||||
ids = append(ids, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// loadAssignableTenantMenuIDs 返回租户端可分配菜单的 ID 集合:cid=2 且已启用且已显示。
|
||||
func loadAssignableTenantMenuIDs() map[uint64]bool {
|
||||
ids := make(map[uint64]bool)
|
||||
var menus []models.SystemMenu
|
||||
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).All(&menus); err != nil {
|
||||
return ids
|
||||
}
|
||||
menus = filterMenusByView(menus, 2)
|
||||
menus = filterAssignableMenus(menus)
|
||||
for _, m := range menus {
|
||||
ids[m.ID] = true
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// sanitizeTenantRights 在服务端收敛租户角色权限:
|
||||
// 仅保留租户端(cid=2)已启用且已显示的菜单 ID,剔除平台菜单、停用菜单、隐藏菜单。
|
||||
// rights 为 nil 或空串表示全权限(与 filterMenusByRights 保持一致)。
|
||||
func sanitizeTenantRights(v interface{}) *string {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
if s, ok := v.(string); ok && strings.TrimSpace(s) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
submitted := parseSubmittedIDs(v)
|
||||
empty := "[]"
|
||||
if len(submitted) == 0 {
|
||||
return &empty
|
||||
}
|
||||
|
||||
allowed := loadAssignableTenantMenuIDs()
|
||||
kept := make([]uint64, 0, len(submitted))
|
||||
seen := make(map[uint64]bool, len(submitted))
|
||||
for _, id := range submitted {
|
||||
if id == 0 || seen[id] || !allowed[id] {
|
||||
continue
|
||||
}
|
||||
seen[id] = true
|
||||
kept = append(kept, id)
|
||||
}
|
||||
if len(kept) == 0 {
|
||||
return &empty
|
||||
}
|
||||
b, _ := json.Marshal(kept)
|
||||
s := string(b)
|
||||
return &s
|
||||
}
|
||||
|
||||
// currentTenantID 从 Bearer Token 解析当前登录租户 ID。
|
||||
// 返回 (tid, true) 表示成功;失败时已写入 401/403 响应并返回 (0, false)。
|
||||
func (c *BackendRoleController) currentTenantID() (uint64, bool) {
|
||||
@@ -137,7 +223,7 @@ func (c *BackendRoleController) CreateRole() {
|
||||
if p.Status != nil {
|
||||
status = *p.Status
|
||||
}
|
||||
rights := normalizeRights(p.Rights)
|
||||
rights := sanitizeTenantRights(p.Rights)
|
||||
// 租户端创建的角色一律标记为“自定义角色”,与系统/平台预置角色区分,并按租户隔离
|
||||
role := &models.AdminRole{
|
||||
TenantID: tid,
|
||||
@@ -188,7 +274,7 @@ func (c *BackendRoleController) UpdateRole() {
|
||||
update["status"] = *p.Status
|
||||
}
|
||||
if p.Rights != nil {
|
||||
update["rights"] = normalizeRights(p.Rights)
|
||||
update["rights"] = sanitizeTenantRights(p.Rights)
|
||||
}
|
||||
if len(update) == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无更新字段"}
|
||||
|
||||
@@ -42,8 +42,12 @@ func RegisterAuthRoutes() {
|
||||
|
||||
// 当前登录用户信息
|
||||
beego.Router("/backend/getCurrentUser", &controllers.BackendAuthController{}, "get:GetCurrentUser")
|
||||
// 当前登录用户的角色 / 部门 / 职位
|
||||
beego.Router("/backend/getCurrentUserProfile", &controllers.BackendAuthController{}, "get:GetCurrentUserProfile")
|
||||
|
||||
// 菜单接口
|
||||
// 可分配给角色的菜单(服务端已过滤停用/隐藏菜单)
|
||||
beego.Router("/backend/assignableMenus", &controllers.BackendMenuController{}, "get:GetAssignableMenus")
|
||||
beego.Router("/backend/allmenu", &controllers.BackendMenuController{}, "get:GetAllMenus")
|
||||
beego.Router("/backend/menu/:id", &controllers.BackendMenuController{}, "get:GetBackendMenu")
|
||||
// 前端菜单接口
|
||||
@@ -281,6 +285,7 @@ func RegisterAuthRoutes() {
|
||||
beego.Router("/backend/oa/schedule/carry/:id", &controllers.BackendOaScheduleController{}, "post:CarrySchedule")
|
||||
beego.Router("/backend/oa/schedule/carry-pending", &controllers.BackendOaScheduleController{}, "post:CarryPending")
|
||||
beego.Router("/backend/oa/schedule/images/:id", &controllers.BackendOaScheduleController{}, "post:SetImages")
|
||||
beego.Router("/backend/oa/schedule/content/:id", &controllers.BackendOaScheduleController{}, "post:UpdateContent")
|
||||
|
||||
// OA通知公告
|
||||
beego.Router("/backend/oa/notice/list", &controllers.BackendOaNoticeController{}, "get:List")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
exit status 0xffffffff
|
||||
Reference in New Issue
Block a user